From 24b08636c0aadc9b725659d660da7c9d729821dc Mon Sep 17 00:00:00 2001 From: skydevlab Date: Tue, 6 Oct 2026 23:20:27 +0530 Subject: [PATCH 1/2] Implement ISpanParsable on System.Version Fixes #125026 --- .../src/System/Version.cs | 31 +++- .../System.Runtime/ref/System.Runtime.cs | 6 +- .../System/VersionTests.cs | 138 ++++++++++++++++++ 3 files changed, 170 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Version.cs b/src/libraries/System.Private.CoreLib/src/System/Version.cs index 9b8cf0ba33b560..d28b5dddf59e7a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Version.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Version.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers.Text; @@ -20,7 +20,7 @@ namespace System [Serializable] [TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")] - public sealed class Version : ICloneable, IComparable, IComparable, IEquatable, ISpanFormattable, IUtf8SpanFormattable, IUtf8SpanParsable + public sealed class Version : ICloneable, IComparable, IComparable, IEquatable, ISpanFormattable, IUtf8SpanFormattable, IUtf8SpanParsable, ISpanParsable { // AssemblyName depends on the order staying the same private readonly int _Major; // Do not rename (binary serialization) @@ -288,13 +288,28 @@ public static Version Parse(string input) return ParseVersion(input.AsSpan(), throwOnFailure: true)!; } + /// + static Version IParsable.Parse(string s, IFormatProvider? provider) + { + ArgumentNullException.ThrowIfNull(s); + return ParseHelper(s.AsSpan()); + } + public static Version Parse(ReadOnlySpan input) => ParseVersion(input, throwOnFailure: true)!; + /// + static Version ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) => + ParseHelper(s); + /// - static Version IUtf8SpanParsable.Parse(ReadOnlySpan utf8Text, IFormatProvider? provider) + static Version IUtf8SpanParsable.Parse(ReadOnlySpan utf8Text, IFormatProvider? provider) => + ParseHelper(utf8Text); + + private static Version ParseHelper(ReadOnlySpan input) + where TChar : unmanaged, IUtfChar { - Version? result = ParseVersion(utf8Text, throwOnFailure: false); + Version? result = ParseVersion(input, throwOnFailure: false); // Required to throw FormatException for invalid input according to contract. if (result == null) { @@ -327,12 +342,20 @@ public static bool TryParse([NotNullWhen(true)] string? input, [NotNullWhen(true return result is not null; } + /// + static bool IParsable.TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [NotNullWhen(true)] out Version? result) => + TryParse(s, out result); + public static bool TryParse(ReadOnlySpan input, [NotNullWhen(true)] out Version? result) { result = ParseVersion(input, throwOnFailure: false); return result is not null; } + /// + static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, [NotNullWhen(true)] out Version? result) => + TryParse(s, out result); + /// /// Tries to convert the UTF-8 representation of a version number to an equivalent Version object, and returns a value that indicates whether the conversion succeeded. /// diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 7b2fd5e2ea9641..7e0d35116f8e67 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -7895,7 +7895,7 @@ protected ValueType() { } public override int GetHashCode() { throw null; } public override string? ToString() { throw null; } } - public sealed partial class Version : System.ICloneable, System.IComparable, System.IComparable, System.IEquatable, System.IFormattable, System.ISpanFormattable, System.IUtf8SpanFormattable, System.IUtf8SpanParsable + public sealed partial class Version : System.ICloneable, System.IComparable, System.IComparable, System.IEquatable, System.IFormattable, System.IParsable, System.ISpanFormattable, System.ISpanParsable, System.IUtf8SpanFormattable, System.IUtf8SpanParsable { public Version() { } public Version(int major, int minor) { } @@ -7924,7 +7924,11 @@ public Version(string version) { } public static System.Version Parse(System.ReadOnlySpan input) { throw null; } public static System.Version Parse(string input) { throw null; } string System.IFormattable.ToString(string? format, System.IFormatProvider? formatProvider) { throw null; } + static System.Version System.IParsable.Parse(string s, System.IFormatProvider? provider) { throw null; } + static bool System.IParsable.TryParse([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? s, System.IFormatProvider? provider, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Version result) { throw null; } bool System.ISpanFormattable.TryFormat(System.Span destination, out int charsWritten, System.ReadOnlySpan format, System.IFormatProvider? provider) { throw null; } + static System.Version System.ISpanParsable.Parse(System.ReadOnlySpan s, System.IFormatProvider? provider) { throw null; } + static bool System.ISpanParsable.TryParse(System.ReadOnlySpan s, System.IFormatProvider? provider, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Version result) { throw null; } bool System.IUtf8SpanFormattable.TryFormat(System.Span utf8Destination, out int bytesWritten, System.ReadOnlySpan format, System.IFormatProvider? provider) { throw null; } static System.Version System.IUtf8SpanParsable.Parse(System.ReadOnlySpan utf8Text, System.IFormatProvider? provider) { throw null; } static bool System.IUtf8SpanParsable.TryParse(System.ReadOnlySpan utf8Text, System.IFormatProvider? provider, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Version result) { throw null; } diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/VersionTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/VersionTests.cs index 6a9a088341c9fa..6e090d17e04b46 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/VersionTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/VersionTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Globalization; using System.Text; using Xunit; @@ -502,5 +503,142 @@ public static void TryFormat_Invoke_WritesExpected(Version version, string[] exp AssertExtensions.Throws("fieldCount", () => version.TryFormat(dest, maxFieldCount + 1, out bytesWritten)); // Index > version.fieldCount } } + + private static T GenericParse(string s, IFormatProvider? provider = null) where T : IParsable => + T.Parse(s, provider); + + private static bool GenericTryParse(string? s, IFormatProvider? provider, out T? result) where T : IParsable => + T.TryParse(s, provider, out result); + + private static T GenericSpanParse(ReadOnlySpan s, IFormatProvider? provider = null) where T : ISpanParsable => + T.Parse(s, provider); + + private static bool GenericSpanTryParse(ReadOnlySpan s, IFormatProvider? provider, out T? result) where T : ISpanParsable => + T.TryParse(s, provider, out result); + + private static T GenericUtf8SpanParse(ReadOnlySpan s, IFormatProvider? provider = null) where T : IUtf8SpanParsable => + T.Parse(s, provider); + + private static bool GenericUtf8SpanTryParse(ReadOnlySpan s, IFormatProvider? provider, out T? result) where T : IUtf8SpanParsable => + T.TryParse(s, provider, out result); + + public static IEnumerable Parse_Invalid_NonNull_TestData() + { + foreach (object[] data in Parse_Invalid_TestData()) + { + if (data[0] != null) + { + yield return new object[] { data[0] }; + } + } + } + + [Theory] + [MemberData(nameof(Parse_Valid_TestData))] + public static void IParsable_Parse_ValidInput_ReturnsExpected(string input, Version expected) + { + Assert.Equal(expected, GenericParse(input, null)); + Assert.Equal(expected, GenericParse(input, CultureInfo.InvariantCulture)); + + Assert.True(GenericTryParse(input, null, out Version? result)); + Assert.Equal(expected, result); + Assert.True(GenericTryParse(input, CultureInfo.InvariantCulture, out result)); + Assert.Equal(expected, result); + } + + [Theory] + [MemberData(nameof(Parse_Valid_TestData))] + public static void ISpanParsable_Parse_ValidInput_ReturnsExpected(string input, Version expected) + { + Assert.Equal(expected, GenericSpanParse(input.AsSpan(), null)); + Assert.Equal(expected, GenericSpanParse(input.AsSpan(), CultureInfo.InvariantCulture)); + + Assert.True(GenericSpanTryParse(input.AsSpan(), null, out Version? result)); + Assert.Equal(expected, result); + Assert.True(GenericSpanTryParse(input.AsSpan(), CultureInfo.InvariantCulture, out result)); + Assert.Equal(expected, result); + } + + [Theory] + [MemberData(nameof(Parse_Valid_TestData))] + public static void IUtf8SpanParsable_Parse_ValidInput_ReturnsExpected(string input, Version expected) + { + byte[] utf8 = Encoding.UTF8.GetBytes(input); + Assert.Equal(expected, GenericUtf8SpanParse(utf8, null)); + Assert.Equal(expected, GenericUtf8SpanParse(utf8, CultureInfo.InvariantCulture)); + + Assert.True(GenericUtf8SpanTryParse(utf8, null, out Version? result)); + Assert.Equal(expected, result); + Assert.True(GenericUtf8SpanTryParse(utf8, CultureInfo.InvariantCulture, out result)); + Assert.Equal(expected, result); + } + + [Fact] + public static void IParsable_Parse_NullInput_ThrowsArgumentNullException() + { + AssertExtensions.Throws("s", () => GenericParse(null!, null)); + AssertExtensions.Throws("s", () => GenericParse(null!, CultureInfo.InvariantCulture)); + + Assert.False(GenericTryParse(null, null, out Version? result)); + Assert.Null(result); + Assert.False(GenericTryParse(null, CultureInfo.InvariantCulture, out result)); + Assert.Null(result); + } + + [Theory] + [MemberData(nameof(Parse_Invalid_NonNull_TestData))] + public static void IParsable_Parse_InvalidInput_ThrowsFormatException(string input) + { + Assert.Throws(() => GenericParse(input, null)); + Assert.Throws(() => GenericParse(input, CultureInfo.InvariantCulture)); + + Assert.False(GenericTryParse(input, null, out Version? result)); + Assert.Null(result); + Assert.False(GenericTryParse(input, CultureInfo.InvariantCulture, out result)); + Assert.Null(result); + } + + [Theory] + [MemberData(nameof(Parse_Invalid_NonNull_TestData))] + public static void ISpanParsable_Parse_InvalidInput_ThrowsFormatException(string input) + { + Assert.Throws(() => GenericSpanParse(input.AsSpan(), null)); + Assert.Throws(() => GenericSpanParse(input.AsSpan(), CultureInfo.InvariantCulture)); + + Assert.False(GenericSpanTryParse(input.AsSpan(), null, out Version? result)); + Assert.Null(result); + Assert.False(GenericSpanTryParse(input.AsSpan(), CultureInfo.InvariantCulture, out result)); + Assert.Null(result); + } + + [Theory] + [MemberData(nameof(Parse_Invalid_NonNull_TestData))] + public static void IUtf8SpanParsable_Parse_InvalidInput_ThrowsFormatException(string input) + { + byte[] utf8 = Encoding.UTF8.GetBytes(input); + Assert.Throws(() => GenericUtf8SpanParse(utf8, null)); + Assert.Throws(() => GenericUtf8SpanParse(utf8, CultureInfo.InvariantCulture)); + + Assert.False(GenericUtf8SpanTryParse(utf8, null, out Version? result)); + Assert.Null(result); + Assert.False(GenericUtf8SpanTryParse(utf8, CultureInfo.InvariantCulture, out result)); + Assert.Null(result); + } + + [Theory] + [InlineData(".")] + [InlineData("1.")] + [InlineData("1.0.")] + [InlineData("1.0.0.")] + public static void IParsable_ISpanParsable_Parse_TrailingDot_ThrowsFormatException(string input) + { + Assert.Throws(() => GenericParse(input, null)); + Assert.Throws(() => GenericSpanParse(input.AsSpan(), null)); + + Assert.False(GenericTryParse(input, null, out Version? result)); + Assert.Null(result); + Assert.False(GenericSpanTryParse(input.AsSpan(), null, out result)); + Assert.Null(result); + } } } From 4c30ddbfedf92d2c8e37d1535a3d87b6eb90df6f Mon Sep 17 00:00:00 2001 From: skydevlab Date: Wed, 7 Oct 2026 22:43:36 +0530 Subject: [PATCH 2/2] Address review feedback: restore BOM in Version.cs and use shared ParsableHelper test classes --- .../src/System/Version.cs | 2 +- .../System/VersionTests.cs | 82 ++++++++----------- 2 files changed, 33 insertions(+), 51 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Version.cs b/src/libraries/System.Private.CoreLib/src/System/Version.cs index d28b5dddf59e7a..6d0641f8ce4807 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Version.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Version.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers.Text; diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/VersionTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/VersionTests.cs index 6e090d17e04b46..7c949ee622a72a 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/VersionTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/VersionTests.cs @@ -504,24 +504,6 @@ public static void TryFormat_Invoke_WritesExpected(Version version, string[] exp } } - private static T GenericParse(string s, IFormatProvider? provider = null) where T : IParsable => - T.Parse(s, provider); - - private static bool GenericTryParse(string? s, IFormatProvider? provider, out T? result) where T : IParsable => - T.TryParse(s, provider, out result); - - private static T GenericSpanParse(ReadOnlySpan s, IFormatProvider? provider = null) where T : ISpanParsable => - T.Parse(s, provider); - - private static bool GenericSpanTryParse(ReadOnlySpan s, IFormatProvider? provider, out T? result) where T : ISpanParsable => - T.TryParse(s, provider, out result); - - private static T GenericUtf8SpanParse(ReadOnlySpan s, IFormatProvider? provider = null) where T : IUtf8SpanParsable => - T.Parse(s, provider); - - private static bool GenericUtf8SpanTryParse(ReadOnlySpan s, IFormatProvider? provider, out T? result) where T : IUtf8SpanParsable => - T.TryParse(s, provider, out result); - public static IEnumerable Parse_Invalid_NonNull_TestData() { foreach (object[] data in Parse_Invalid_TestData()) @@ -537,12 +519,12 @@ public static IEnumerable Parse_Invalid_NonNull_TestData() [MemberData(nameof(Parse_Valid_TestData))] public static void IParsable_Parse_ValidInput_ReturnsExpected(string input, Version expected) { - Assert.Equal(expected, GenericParse(input, null)); - Assert.Equal(expected, GenericParse(input, CultureInfo.InvariantCulture)); + Assert.Equal(expected, ParsableHelper.Parse(input, null)); + Assert.Equal(expected, ParsableHelper.Parse(input, CultureInfo.InvariantCulture)); - Assert.True(GenericTryParse(input, null, out Version? result)); + Assert.True(ParsableHelper.TryParse(input, null, out Version? result)); Assert.Equal(expected, result); - Assert.True(GenericTryParse(input, CultureInfo.InvariantCulture, out result)); + Assert.True(ParsableHelper.TryParse(input, CultureInfo.InvariantCulture, out result)); Assert.Equal(expected, result); } @@ -550,12 +532,12 @@ public static void IParsable_Parse_ValidInput_ReturnsExpected(string input, Vers [MemberData(nameof(Parse_Valid_TestData))] public static void ISpanParsable_Parse_ValidInput_ReturnsExpected(string input, Version expected) { - Assert.Equal(expected, GenericSpanParse(input.AsSpan(), null)); - Assert.Equal(expected, GenericSpanParse(input.AsSpan(), CultureInfo.InvariantCulture)); + Assert.Equal(expected, SpanParsableHelper.Parse(input.AsSpan(), null)); + Assert.Equal(expected, SpanParsableHelper.Parse(input.AsSpan(), CultureInfo.InvariantCulture)); - Assert.True(GenericSpanTryParse(input.AsSpan(), null, out Version? result)); + Assert.True(SpanParsableHelper.TryParse(input.AsSpan(), null, out Version? result)); Assert.Equal(expected, result); - Assert.True(GenericSpanTryParse(input.AsSpan(), CultureInfo.InvariantCulture, out result)); + Assert.True(SpanParsableHelper.TryParse(input.AsSpan(), CultureInfo.InvariantCulture, out result)); Assert.Equal(expected, result); } @@ -564,24 +546,24 @@ public static void ISpanParsable_Parse_ValidInput_ReturnsExpected(string input, public static void IUtf8SpanParsable_Parse_ValidInput_ReturnsExpected(string input, Version expected) { byte[] utf8 = Encoding.UTF8.GetBytes(input); - Assert.Equal(expected, GenericUtf8SpanParse(utf8, null)); - Assert.Equal(expected, GenericUtf8SpanParse(utf8, CultureInfo.InvariantCulture)); + Assert.Equal(expected, Utf8SpanParsableHelper.Parse(utf8, null)); + Assert.Equal(expected, Utf8SpanParsableHelper.Parse(utf8, CultureInfo.InvariantCulture)); - Assert.True(GenericUtf8SpanTryParse(utf8, null, out Version? result)); + Assert.True(Utf8SpanParsableHelper.TryParse(utf8, null, out Version? result)); Assert.Equal(expected, result); - Assert.True(GenericUtf8SpanTryParse(utf8, CultureInfo.InvariantCulture, out result)); + Assert.True(Utf8SpanParsableHelper.TryParse(utf8, CultureInfo.InvariantCulture, out result)); Assert.Equal(expected, result); } [Fact] public static void IParsable_Parse_NullInput_ThrowsArgumentNullException() { - AssertExtensions.Throws("s", () => GenericParse(null!, null)); - AssertExtensions.Throws("s", () => GenericParse(null!, CultureInfo.InvariantCulture)); + AssertExtensions.Throws("s", () => ParsableHelper.Parse(null!, null)); + AssertExtensions.Throws("s", () => ParsableHelper.Parse(null!, CultureInfo.InvariantCulture)); - Assert.False(GenericTryParse(null, null, out Version? result)); + Assert.False(ParsableHelper.TryParse(null, null, out Version? result)); Assert.Null(result); - Assert.False(GenericTryParse(null, CultureInfo.InvariantCulture, out result)); + Assert.False(ParsableHelper.TryParse(null, CultureInfo.InvariantCulture, out result)); Assert.Null(result); } @@ -589,12 +571,12 @@ public static void IParsable_Parse_NullInput_ThrowsArgumentNullException() [MemberData(nameof(Parse_Invalid_NonNull_TestData))] public static void IParsable_Parse_InvalidInput_ThrowsFormatException(string input) { - Assert.Throws(() => GenericParse(input, null)); - Assert.Throws(() => GenericParse(input, CultureInfo.InvariantCulture)); + Assert.Throws(() => ParsableHelper.Parse(input, null)); + Assert.Throws(() => ParsableHelper.Parse(input, CultureInfo.InvariantCulture)); - Assert.False(GenericTryParse(input, null, out Version? result)); + Assert.False(ParsableHelper.TryParse(input, null, out Version? result)); Assert.Null(result); - Assert.False(GenericTryParse(input, CultureInfo.InvariantCulture, out result)); + Assert.False(ParsableHelper.TryParse(input, CultureInfo.InvariantCulture, out result)); Assert.Null(result); } @@ -602,12 +584,12 @@ public static void IParsable_Parse_InvalidInput_ThrowsFormatException(string inp [MemberData(nameof(Parse_Invalid_NonNull_TestData))] public static void ISpanParsable_Parse_InvalidInput_ThrowsFormatException(string input) { - Assert.Throws(() => GenericSpanParse(input.AsSpan(), null)); - Assert.Throws(() => GenericSpanParse(input.AsSpan(), CultureInfo.InvariantCulture)); + Assert.Throws(() => SpanParsableHelper.Parse(input.AsSpan(), null)); + Assert.Throws(() => SpanParsableHelper.Parse(input.AsSpan(), CultureInfo.InvariantCulture)); - Assert.False(GenericSpanTryParse(input.AsSpan(), null, out Version? result)); + Assert.False(SpanParsableHelper.TryParse(input.AsSpan(), null, out Version? result)); Assert.Null(result); - Assert.False(GenericSpanTryParse(input.AsSpan(), CultureInfo.InvariantCulture, out result)); + Assert.False(SpanParsableHelper.TryParse(input.AsSpan(), CultureInfo.InvariantCulture, out result)); Assert.Null(result); } @@ -616,12 +598,12 @@ public static void ISpanParsable_Parse_InvalidInput_ThrowsFormatException(string public static void IUtf8SpanParsable_Parse_InvalidInput_ThrowsFormatException(string input) { byte[] utf8 = Encoding.UTF8.GetBytes(input); - Assert.Throws(() => GenericUtf8SpanParse(utf8, null)); - Assert.Throws(() => GenericUtf8SpanParse(utf8, CultureInfo.InvariantCulture)); + Assert.Throws(() => Utf8SpanParsableHelper.Parse(utf8, null)); + Assert.Throws(() => Utf8SpanParsableHelper.Parse(utf8, CultureInfo.InvariantCulture)); - Assert.False(GenericUtf8SpanTryParse(utf8, null, out Version? result)); + Assert.False(Utf8SpanParsableHelper.TryParse(utf8, null, out Version? result)); Assert.Null(result); - Assert.False(GenericUtf8SpanTryParse(utf8, CultureInfo.InvariantCulture, out result)); + Assert.False(Utf8SpanParsableHelper.TryParse(utf8, CultureInfo.InvariantCulture, out result)); Assert.Null(result); } @@ -632,12 +614,12 @@ public static void IUtf8SpanParsable_Parse_InvalidInput_ThrowsFormatException(st [InlineData("1.0.0.")] public static void IParsable_ISpanParsable_Parse_TrailingDot_ThrowsFormatException(string input) { - Assert.Throws(() => GenericParse(input, null)); - Assert.Throws(() => GenericSpanParse(input.AsSpan(), null)); + Assert.Throws(() => ParsableHelper.Parse(input, null)); + Assert.Throws(() => SpanParsableHelper.Parse(input.AsSpan(), null)); - Assert.False(GenericTryParse(input, null, out Version? result)); + Assert.False(ParsableHelper.TryParse(input, null, out Version? result)); Assert.Null(result); - Assert.False(GenericSpanTryParse(input.AsSpan(), null, out result)); + Assert.False(SpanParsableHelper.TryParse(input.AsSpan(), null, out result)); Assert.Null(result); } }