diff --git a/Directory.Build.props b/Directory.Build.props index 2b1adc8..0614752 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,8 @@ true - 13.0 + + preview enable true diff --git a/PolySharp.slnx b/PolySharp.slnx index ffda3b4..e30a0c9 100644 --- a/PolySharp.slnx +++ b/PolySharp.slnx @@ -7,6 +7,7 @@ + diff --git a/README.md b/README.md index 3e65b86..551fd10 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,18 @@ Here's an example of some of the new features that **PolySharp** can enable down - `[OverloadResolutionPriority]` (needed for [overload resolution priority](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-13#overload-resolution-priority)) - `[ParamsCollection]` (needed for [params collection](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-13#params-collections)) - `[ConstantExpected]` (see [proposal](https://github.com/dotnet/runtime/issues/33771)) +- Throw helper methods (only for C# 14 and above, because these polyfills require [Extension members](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-14#extension-members)) + - `ArgumentException.ThrowIfNullOrEmpty(string? argument, string? paramName = default)` (see [docs](https://learn.microsoft.com/dotnet/api/system.argumentexception.throwifnullorempty)) + - `ArgumentException.ThrowIfNullOrWhiteSpace(string? argument, string? paramName = default)` (see [docs](https://learn.microsoft.com/dotnet/api/system.argumentexception.throwifnullorwhitespace)) + - `ArgumentNullException.ThrowIfNull(object? argument, string? paramName = default)` (see [docs](https://learn.microsoft.com/dotnet/api/system.argumentnullexception.throwifnull)) + - `ArgumentOutOfRangeException.ThrowIfEqual(T value, T other, string? paramName = default)` (see [docs](https://learn.microsoft.com/dotnet/api/system.argumentoutofrangeexception.throwifequal)) + - `ArgumentOutOfRangeException.ThrowIfNotEqual(T value, T other, string? paramName = default)` (see [docs](https://learn.microsoft.com/dotnet/api/system.argumentoutofrangeexception.throwifnotequal)) + - `ArgumentOutOfRangeException.ThrowIfGreaterThan(T value, T other, string? paramName = default)` (see [docs](https://learn.microsoft.com/dotnet/api/system.argumentoutofrangeexception.throwifgreaterthan)) + - `ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(T value, T other, string? paramName = default)` (see [docs](https://learn.microsoft.com/dotnet/api/system.argumentoutofrangeexception.throwifgreaterthanorequal)) + - `ArgumentOutOfRangeException.ThrowIfLessThan(T value, T other, string? paramName = default)` (see [docs](https://learn.microsoft.com/dotnet/api/system.argumentoutofrangeexception.throwiflessthan)) + - `ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(T value, T other, string? paramName = default)` (see [docs](https://learn.microsoft.com/dotnet/api/system.argumentoutofrangeexception.throwiflessthanorequal)) + - `ObjectDisposedException.ThrowIf(bool condition, object instance)` (see [docs](https://learn.microsoft.com/dotnet/api/system.objectdisposedexception.throwif#system-objectdisposedexception-throwif(system-boolean-system-object))) + - `ObjectDisposedException.ThrowIf(bool condition, Type type)` (see [docs](https://learn.microsoft.com/dotnet/api/system.objectdisposedexception.throwif#system-objectdisposedexception-throwif(system-boolean-system-type))) To leverage them, make sure to bump your C# language version. You can do this by setting the `` MSBuild property in your project. For instance, by adding `13.0` (or your desired C# version) to the first `` of your .csproj file. For more info on this, [see here](https://sergiopedri.medium.com/enabling-and-using-c-9-features-on-older-and-unsupported-runtimes-ce384d8debb), but remember that you don't need to manually copy polyfills anymore: simply adding a reference to **PolySharp** will do this for you automatically. diff --git a/src/PolySharp.SourceGenerators/EmbeddedResources/System.ExceptionPolyfills.cs b/src/PolySharp.SourceGenerators/EmbeddedResources/System.ExceptionPolyfills.cs new file mode 100644 index 0000000..14a67df --- /dev/null +++ b/src/PolySharp.SourceGenerators/EmbeddedResources/System.ExceptionPolyfills.cs @@ -0,0 +1,169 @@ +// +#pragma warning disable +#nullable enable annotations + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System +{ + /// Provides downlevel polyfills for static methods on Exception-derived types. + internal static class ExceptionPolyfills + { + extension(global::System.ArgumentException) + { + /// Throws an exception if is null or empty. + /// The string argument to validate as non-null and non-empty. + /// The name of the parameter with which corresponds. + /// is null. + /// is empty. + public static void ThrowIfNullOrEmpty( + [global::System.Diagnostics.CodeAnalysis.NotNull] string? argument, + [global::System.Runtime.CompilerServices.CallerArgumentExpression(nameof(argument))] string? paramName = null + ) + { + if (string.IsNullOrEmpty(argument)) + ThrowArgumentException(argument, paramName, "The value cannot be an empty string."); + } + + /// Throws an exception if is null, empty, or consists only of white-space characters. + /// The string argument to validate. + /// The name of the parameter with which corresponds. + /// is null. + /// is empty or consists only of white-space characters. + public static void ThrowIfNullOrWhiteSpace( + [global::System.Diagnostics.CodeAnalysis.NotNull] string? argument, + [global::System.Runtime.CompilerServices.CallerArgumentExpression(nameof(argument))] string? paramName = null + ) + { + if (string.IsNullOrWhiteSpace(argument)) + ThrowArgumentException(argument, paramName, "The value cannot be an empty string or composed entirely of whitespace."); + } + } + + [global::System.Diagnostics.CodeAnalysis.DoesNotReturn] + private static void ThrowArgumentException(string? argument, string? paramName, string message) + { + ExceptionPolyfills.ThrowIfNull(argument, paramName); + throw new global::System.ArgumentException(message, paramName); + } + + extension(global::System.ArgumentNullException) + { + /// Throws an if is null. + /// The reference type argument to validate as non-null. + /// The name of the parameter with which corresponds. + /// is null. + public static void ThrowIfNull( + [global::System.Diagnostics.CodeAnalysis.NotNull] object? argument, + [global::System.Runtime.CompilerServices.CallerArgumentExpression(nameof(argument))] string? paramName = null + ) + { + if (argument is null) + ThrowArgumentNullException(paramName); + } + } + + [global::System.Diagnostics.CodeAnalysis.DoesNotReturn] + private static void ThrowArgumentNullException(string? paramName) => + throw new global::System.ArgumentNullException(paramName); + + extension(global::System.ArgumentOutOfRangeException) + { + /// Throws an if is equal to . + /// The argument to validate as not equal to . + /// The value to compare with . + /// The name of the parameter with which corresponds. + public static void ThrowIfEqual(T value, T other, [global::System.Runtime.CompilerServices.CallerArgumentExpression(nameof(value))] string? paramName = null) + { + if (global::System.Collections.Generic.EqualityComparer.Default.Equals(value, other)) + ThrowArgumentOutOfRangeException(paramName, $"{paramName} ('{(object?)value ?? "null"}') must not be equal to '{(object?)other ?? "null"}'."); + } + + /// Throws an if is not equal to . + /// The argument to validate as equal to . + /// The value to compare with . + /// The name of the parameter with which corresponds. + public static void ThrowIfNotEqual(T value, T other, [global::System.Runtime.CompilerServices.CallerArgumentExpression(nameof(value))] string? paramName = null) + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(value, other)) + ThrowArgumentOutOfRangeException(paramName, $"{paramName} ('{(object?)value ?? "null"}') must be equal to '{(object?)other ?? "null"}'."); + } + + /// Throws an if is greater than . + /// The argument to validate as less or equal than . + /// The value to compare with . + /// The name of the parameter with which corresponds. + public static void ThrowIfGreaterThan(T value, T other, [global::System.Runtime.CompilerServices.CallerArgumentExpression(nameof(value))] string? paramName = null) + where T : global::System.IComparable + { + if (value.CompareTo(other) > 0) + ThrowArgumentOutOfRangeException(paramName, $"{paramName} ('{value}') must be less than or equal to '{other}'."); + } + + /// Throws an if is greater than or equal . + /// The argument to validate as less than . + /// The value to compare with . + /// The name of the parameter with which corresponds. + public static void ThrowIfGreaterThanOrEqual(T value, T other, [global::System.Runtime.CompilerServices.CallerArgumentExpression(nameof(value))] string? paramName = null) + where T : global::System.IComparable + { + if (value.CompareTo(other) >= 0) + ThrowArgumentOutOfRangeException(paramName, $"{paramName} ('{value}') must be less than '{other}'."); + } + + /// Throws an if is less than . + /// The argument to validate as greater than or equal than . + /// The value to compare with . + /// The name of the parameter with which corresponds. + public static void ThrowIfLessThan(T value, T other, [global::System.Runtime.CompilerServices.CallerArgumentExpression(nameof(value))] string? paramName = null) + where T : global::System.IComparable + { + if (value.CompareTo(other) < 0) + ThrowArgumentOutOfRangeException(paramName, $"{paramName} ('{value}') must be greater than or equal to '{other}'."); + } + + /// Throws an if is less than or equal . + /// The argument to validate as greater than than . + /// The value to compare with . + /// The name of the parameter with which corresponds. + public static void ThrowIfLessThanOrEqual(T value, T other, [global::System.Runtime.CompilerServices.CallerArgumentExpression(nameof(value))] string? paramName = null) + where T : global::System.IComparable + { + if (value.CompareTo(other) <= 0) + ThrowArgumentOutOfRangeException(paramName, $"{paramName} ('{value}') must be greater than '{other}'."); + } + } + + [global::System.Diagnostics.CodeAnalysis.DoesNotReturn] + private static void ThrowArgumentOutOfRangeException(string? paramName, string message) => + throw new global::System.ArgumentOutOfRangeException(paramName, message); + + extension(global::System.ObjectDisposedException) + { + /// Throws an if the specified is . + /// The condition to evaluate. + /// The object whose type's full name should be included in any resulting . + /// The is . + public static void ThrowIf([global::System.Diagnostics.CodeAnalysis.DoesNotReturnIf(true)] bool condition, object instance) + { + if (condition) + ThrowObjectDisposedException(instance?.GetType()); + } + + /// Throws an if the specified is . + /// The condition to evaluate. + /// The type whose full name should be included in any resulting . + /// The is . + public static void ThrowIf([global::System.Diagnostics.CodeAnalysis.DoesNotReturnIf(true)] bool condition, global::System.Type type) + { + if (condition) + ThrowObjectDisposedException(type); + } + } + + [global::System.Diagnostics.CodeAnalysis.DoesNotReturn] + private static void ThrowObjectDisposedException(global::System.Type? type) => + throw new global::System.ObjectDisposedException(type?.FullName); + } +} \ No newline at end of file diff --git a/src/PolySharp.SourceGenerators/PolyfillsGenerator.Polyfills.cs b/src/PolySharp.SourceGenerators/PolyfillsGenerator.Polyfills.cs index eec140e..580e253 100644 --- a/src/PolySharp.SourceGenerators/PolyfillsGenerator.Polyfills.cs +++ b/src/PolySharp.SourceGenerators/PolyfillsGenerator.Polyfills.cs @@ -132,6 +132,12 @@ static bool IsTypeAvailable(Compilation compilation, string name, CancellationTo return compilation.GetTypeByMetadataName("System.ValueTuple`2") is not null; } + // Extension members only available starting with C# 14.0 + if (name is "System.ExceptionPolyfills") + { + return compilation.HasLanguageVersionAtLeastEqualTo((LanguageVersion)1400); + } + return true; } diff --git a/tests/PolySharp.OldCSharpVersion.Tests/PolySharp.OldCSharpVersion.Tests.csproj b/tests/PolySharp.OldCSharpVersion.Tests/PolySharp.OldCSharpVersion.Tests.csproj new file mode 100644 index 0000000..f951cd8 --- /dev/null +++ b/tests/PolySharp.OldCSharpVersion.Tests/PolySharp.OldCSharpVersion.Tests.csproj @@ -0,0 +1,13 @@ + + + + netstandard2.0 + true + 13.0 + + + + + + + diff --git a/tests/PolySharp.Tests/LanguageFeatures.cs b/tests/PolySharp.Tests/LanguageFeatures.cs index d5df709..0225b9e 100644 --- a/tests/PolySharp.Tests/LanguageFeatures.cs +++ b/tests/PolySharp.Tests/LanguageFeatures.cs @@ -297,3 +297,48 @@ public static void AnotherCpuIntrinsic([ConstantExpected(Min = 0, Max = 8)] int { } } + +internal class ExceptionPolyfillsTests : IDisposable +{ + private bool disposedValue = false; + private readonly IDisposable disposable; + + public ExceptionPolyfillsTests(IDisposable disposable) + { + ArgumentNullException.ThrowIfNull(disposable); + this.disposable = disposable; + } + + protected virtual void Dispose(bool disposing) + { + if (!this.disposedValue) + { + if (disposing) + { + this.disposable.Dispose(); + } + + this.disposedValue = true; + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public void Connect() + { + ObjectDisposedException.ThrowIf(this.disposedValue, this); + + ArgumentException.ThrowIfNullOrEmpty("foo"); + ArgumentException.ThrowIfNullOrWhiteSpace("foo"); + ArgumentOutOfRangeException.ThrowIfEqual(1, 0); + ArgumentOutOfRangeException.ThrowIfNotEqual(1, 1); + ArgumentOutOfRangeException.ThrowIfGreaterThan(0, 1); + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(0, 1); + ArgumentOutOfRangeException.ThrowIfLessThan(1, 0); + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(1, 0); + } +}