From a8c3046e742f4f45c5b522a9ad43e7b4a7d43246 Mon Sep 17 00:00:00 2001 From: nogic1008 <24802730+nogic1008@users.noreply.github.com> Date: Sat, 11 Oct 2025 11:21:05 +0900 Subject: [PATCH 1/5] build: use C# 14 --- Directory.Build.props | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 From b43980d6dd529f96382619c563e006ac78908f14 Mon Sep 17 00:00:00 2001 From: nogic1008 <24802730+nogic1008@users.noreply.github.com> Date: Sat, 11 Oct 2025 11:42:03 +0900 Subject: [PATCH 2/5] feat: generate ExceptionPolyfills when used C# 14 --- .../System.ExceptionPolyfills.cs | 62 +++++++++++++++++++ .../PolyfillsGenerator.Polyfills.cs | 6 ++ 2 files changed, 68 insertions(+) create mode 100644 src/PolySharp.SourceGenerators/EmbeddedResources/System.ExceptionPolyfills.cs diff --git a/src/PolySharp.SourceGenerators/EmbeddedResources/System.ExceptionPolyfills.cs b/src/PolySharp.SourceGenerators/EmbeddedResources/System.ExceptionPolyfills.cs new file mode 100644 index 0000000..13ca47b --- /dev/null +++ b/src/PolySharp.SourceGenerators/EmbeddedResources/System.ExceptionPolyfills.cs @@ -0,0 +1,62 @@ +// +#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.ArgumentNullException) + { + 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.ObjectDisposedException) + { + public static void ThrowIf([global::System.Diagnostics.CodeAnalysis.DoesNotReturnIf(true)] bool condition, object instance) + { + if (condition) + { + ThrowObjectDisposedException(instance); + } + } + + 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(object? instance) + { + throw new global::System.ObjectDisposedException(instance?.GetType().FullName); + } + + [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; } From a87b200f0f620bb94fde6ad83742bc88c92c44cf Mon Sep 17 00:00:00 2001 From: nogic1008 <24802730+nogic1008@users.noreply.github.com> Date: Sat, 11 Oct 2025 11:44:27 +0900 Subject: [PATCH 3/5] test: add source generation test for `ExceptionPolyfills` - `PolySharp.OldCSharpVersion.Tests` (C# 13) does not generate polyfill - `PolySharp.Tests` generates polyfill --- PolySharp.slnx | 1 + .../PolySharp.OldCSharpVersion.Tests.csproj | 13 +++++++ tests/PolySharp.Tests/LanguageFeatures.cs | 36 +++++++++++++++++++ 3 files changed, 50 insertions(+) create mode 100644 tests/PolySharp.OldCSharpVersion.Tests/PolySharp.OldCSharpVersion.Tests.csproj 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/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..f071a63 100644 --- a/tests/PolySharp.Tests/LanguageFeatures.cs +++ b/tests/PolySharp.Tests/LanguageFeatures.cs @@ -297,3 +297,39 @@ 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); + } +} From c8e3f9982a7860574b77cdf06cc413509c231f5b Mon Sep 17 00:00:00 2001 From: nogic1008 <24802730+nogic1008@users.noreply.github.com> Date: Sat, 11 Oct 2025 11:55:33 +0900 Subject: [PATCH 4/5] docs: update README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 3e65b86..1fb73f1 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,10 @@ 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)) + - `ArgumentNullException.ThrowIfNull(object? argument, string? paramName = default)` (see [docs](https://learn.microsoft.com/dotnet/api/system.argumentnullexception.throwifnull)) + - `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. From e671eb47c41dbae2b85fa406fd6577b0a5028426 Mon Sep 17 00:00:00 2001 From: nogic1008 <24802730+nogic1008@users.noreply.github.com> Date: Sat, 11 Oct 2025 15:49:04 +0900 Subject: [PATCH 5/5] feat: add more throw helpers --- README.md | 8 + .../System.ExceptionPolyfills.cs | 139 ++++++++++++++++-- tests/PolySharp.Tests/LanguageFeatures.cs | 9 ++ 3 files changed, 140 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 1fb73f1..551fd10 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,15 @@ Here's an example of some of the new features that **PolySharp** can enable down - `[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))) diff --git a/src/PolySharp.SourceGenerators/EmbeddedResources/System.ExceptionPolyfills.cs b/src/PolySharp.SourceGenerators/EmbeddedResources/System.ExceptionPolyfills.cs index 13ca47b..14a67df 100644 --- a/src/PolySharp.SourceGenerators/EmbeddedResources/System.ExceptionPolyfills.cs +++ b/src/PolySharp.SourceGenerators/EmbeddedResources/System.ExceptionPolyfills.cs @@ -10,17 +10,57 @@ 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); - } } } @@ -28,35 +68,102 @@ public static void ThrowIfNull( 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); - } + 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(object? instance) - { - throw new global::System.ObjectDisposedException(instance?.GetType().FullName); - } - - [global::System.Diagnostics.CodeAnalysis.DoesNotReturn] - private static void ThrowObjectDisposedException(global::System.Type? type) - { + private static void ThrowObjectDisposedException(global::System.Type? type) => throw new global::System.ObjectDisposedException(type?.FullName); - } } } \ No newline at end of file diff --git a/tests/PolySharp.Tests/LanguageFeatures.cs b/tests/PolySharp.Tests/LanguageFeatures.cs index f071a63..0225b9e 100644 --- a/tests/PolySharp.Tests/LanguageFeatures.cs +++ b/tests/PolySharp.Tests/LanguageFeatures.cs @@ -331,5 +331,14 @@ public void Dispose() 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); } }