From 721308d46836ba44fcb08cd000c2648e90f001a0 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Tue, 16 Jul 2024 08:27:18 -0700 Subject: [PATCH 1/5] Add overloads for string AsSpan(), AsMemory() and *OrDefault() variants - Add overloads for string.AsSpan() and string.AsMemory() that take Razor's Index and Range polyfill types. - Add overloads for string.AsSpanOrDefault() and string.AsMemoryOrDefault() that take start, start+length, and Razor Index and Range polyfill types. Note: If compiling for .NET, these forward to the relevant runtime methods. --- .../StringExtensions.cs | 152 ++++++++++++++++++ .../Utilities/ThrowHelper.cs | 28 ++++ 2 files changed, 180 insertions(+) diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/StringExtensions.cs b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/StringExtensions.cs index f649b756075..fb0b34d9b3b 100644 --- a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/StringExtensions.cs +++ b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/StringExtensions.cs @@ -3,6 +3,10 @@ using System.Diagnostics.CodeAnalysis; +#if !NET +using ThrowHelper = Microsoft.AspNetCore.Razor.Utilities.ThrowHelper; +#endif + namespace System; internal static class StringExtensions @@ -13,12 +17,160 @@ public static bool IsNullOrEmpty([NotNullWhen(false)] this string? s) public static bool IsNullOrWhiteSpace([NotNullWhen(false)] this string? s) => string.IsNullOrWhiteSpace(s); + public static ReadOnlySpan AsSpan(this string? s, Index startIndex) + { +#if NET + return MemoryExtensions.AsSpan(s, startIndex); +#else + if (s is null) + { + if (!startIndex.Equals(Index.Start)) + { + ThrowHelper.ThrowArgumentOutOfRange(nameof(startIndex)); + } + + return default; + } + + return s.AsSpan(startIndex.GetOffset(s.Length)); +#endif + } + + public static ReadOnlySpan AsSpan(this string? s, Range range) + { +#if NET + return MemoryExtensions.AsSpan(s, range); +#else + if (s is null) + { + if (!range.Start.Equals(Index.Start) || !range.End.Equals(Index.Start)) + { + ThrowHelper.ThrowArgumentNull(nameof(s)); + } + + return default; + } + + var (start, length) = range.GetOffsetAndLength(s.Length); + return s.AsSpan(start, length); +#endif + } + public static ReadOnlySpan AsSpanOrDefault(this string? s) => s is not null ? s.AsSpan() : default; + public static ReadOnlySpan AsSpanOrDefault(this string? s, int start) + => s is not null ? s.AsSpan(start) : default; + + public static ReadOnlySpan AsSpanOrDefault(this string? s, int start, int length) + => s is not null ? s.AsSpan(start, length) : default; + + public static ReadOnlySpan AsSpanOrDefault(this string? s, Index startIndex) + { + if (s is null) + { + return default; + } + +#if NET + return MemoryExtensions.AsSpan(s, startIndex); +#else + return s.AsSpan(startIndex.GetOffset(s.Length)); +#endif + } + + public static ReadOnlySpan AsSpanOrDefault(this string? s, Range range) + { + if (s is null) + { + return default; + } + +#if NET + return MemoryExtensions.AsSpan(s, range); +#else + var (start, length) = range.GetOffsetAndLength(s.Length); + return s.AsSpan(start, length); +#endif + } + + public static ReadOnlyMemory AsMemory(this string? s, Index startIndex) + { +#if NET + return MemoryExtensions.AsMemory(s, startIndex); +#else + if (s is null) + { + if (!startIndex.Equals(Index.Start)) + { + ThrowHelper.ThrowArgumentOutOfRange(nameof(startIndex)); + } + + return default; + } + + return s.AsMemory(startIndex.GetOffset(s.Length)); +#endif + } + + public static ReadOnlyMemory AsMemory(this string? s, Range range) + { +#if NET + return MemoryExtensions.AsMemory(s, range); +#else + if (s is null) + { + if (!range.Start.Equals(Index.Start) || !range.End.Equals(Index.Start)) + { + ThrowHelper.ThrowArgumentNull(nameof(s)); + } + + return default; + } + + var (start, length) = range.GetOffsetAndLength(s.Length); + return s.AsMemory(start, length); +#endif + } + public static ReadOnlyMemory AsMemoryOrDefault(this string? s) => s is not null ? s.AsMemory() : default; + public static ReadOnlyMemory AsMemoryOrDefault(this string? s, int start) + => s is not null ? s.AsMemory(start) : default; + + public static ReadOnlyMemory AsMemoryOrDefault(this string? s, int start, int length) + => s is not null ? s.AsMemory(start, length) : default; + + public static ReadOnlyMemory AsMemoryOrDefault(this string? s, Index startIndex) + { + if (s is null) + { + return default; + } + +#if NET + return MemoryExtensions.AsMemory(s, startIndex); +#else + return s.AsMemory(startIndex.GetOffset(s.Length)); +#endif + } + + public static ReadOnlyMemory AsMemoryOrDefault(this string? s, Range range) + { + if (s is null) + { + return default; + } + +#if NET + return MemoryExtensions.AsMemory(s, range); +#else + var (start, length) = range.GetOffsetAndLength(s.Length); + return s.AsMemory(start, length); +#endif + } + // This method doesn't exist on .NET Framework, but it does on .NET Core. public static bool Contains(this string s, char ch) => s.IndexOf(ch) >= 0; diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/Utilities/ThrowHelper.cs b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/Utilities/ThrowHelper.cs index 1a78ee3a3d2..168871c0218 100644 --- a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/Utilities/ThrowHelper.cs +++ b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/Utilities/ThrowHelper.cs @@ -8,6 +8,34 @@ namespace Microsoft.AspNetCore.Razor.Utilities; internal static class ThrowHelper { + /// + /// This is present to help the JIT inline methods that need to throw an . + /// + [DoesNotReturn] + public static void ThrowArgumentNull(string paramName) + => throw new ArgumentNullException(paramName); + + /// + /// This is present to help the JIT inline methods that need to throw an . + /// + [DoesNotReturn] + public static T ThrowArgumentNull(string paramName) + => throw new ArgumentNullException(paramName); + + /// + /// This is present to help the JIT inline methods that need to throw an . + /// + [DoesNotReturn] + public static void ThrowArgumentOutOfRange(string paramName) + => throw new ArgumentOutOfRangeException(paramName); + + /// + /// This is present to help the JIT inline methods that need to throw an . + /// + [DoesNotReturn] + public static T ThrowArgumentOutOfRange(string paramName) + => throw new ArgumentOutOfRangeException(paramName); + /// /// This is present to help the JIT inline methods that need to throw an . /// From 2e994ba3adf7ad65410739ebfa848a0dbfba5957 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Tue, 16 Jul 2024 09:01:04 -0700 Subject: [PATCH 2/5] Add overloads for string.Contains(), IndexOf, StartsWith, and EndsWith Add overloads for string.Contains(), IndexOf(), StartsWith, and EndsWith() extensions methods that exist on .NET Core but not on .NET Framework or .NET Standard 2.0 --- .../StringExtensions.cs | 155 +++++++++++++++++- 1 file changed, 149 insertions(+), 6 deletions(-) diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/StringExtensions.cs b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/StringExtensions.cs index fb0b34d9b3b..59e7f106e6b 100644 --- a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/StringExtensions.cs +++ b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/StringExtensions.cs @@ -171,11 +171,154 @@ public static ReadOnlyMemory AsMemoryOrDefault(this string? s, Range range #endif } - // This method doesn't exist on .NET Framework, but it does on .NET Core. - public static bool Contains(this string s, char ch) - => s.IndexOf(ch) >= 0; + /// + /// Returns a value indicating whether a specified character occurs within a string instance. + /// + /// + /// The string instance. + /// + /// + /// The character to seek. + /// + /// + /// if the value parameter occurs within the string; otherwise, . + /// + /// + /// This method exists on .NET Core, but doesn't on .NET Framework or .NET Standard 2.0. + /// + public static bool Contains(this string text, char value) + { +#if NET + return text.Contains(value); +#else + return text.IndexOf(value) >= 0; +#endif + } + + /// + /// Returns a value indicating whether a specified character occurs within a string instance, + /// using the specified comparison rules. + /// + /// + /// The string instance. + /// + /// + /// The character to seek. + /// + /// + /// One of the enumeration values that specifies the rules to use in the comparison. + /// + /// + /// if the value parameter occurs within the string; otherwise, . + /// + /// + /// This method exists on .NET Core, but doesn't on .NET Framework or .NET Standard 2.0. + /// + public static bool Contains(this string text, char value, StringComparison comparisonType) + { +#if NET + return text.Contains(value, comparisonType); +#else + return text.IndexOf(value, comparisonType) != 0; +#endif + } + + /// + /// Reports the zero-based index of the first occurrence of the specified Unicode character in a string instance. + /// A parameter specifies the type of search to use for the specified character. + /// + /// + /// The string instance. + /// + /// + /// The character to compare to the character at the start of this string. + /// + /// + /// An enumeration value that specifies the rules for the search. + /// + /// + /// The zero-based index of if that character is found, or -1 if it is not. + /// + /// + /// + /// Index numbering starts from zero. + /// + /// + /// The parameter is a enumeration member + /// that specifies whether the search for the argument uses the current or invariant culture, + /// is case-sensitive or case-insensitive, or uses word or ordinal comparison rules. + /// + /// + /// This method exists on .NET Core, but doesn't on .NET Framework or .NET Standard 2.0. + /// + /// + public static int IndexOf(this string text, char value, StringComparison comparisonType) + { +#if NET + return text.IndexOf(value, comparisonType); +#else + // [ch] produces a ReadOnlySpan using a ref to ch. + return text.AsSpan().IndexOf([value], comparisonType); +#endif + } + + /// + /// Determines whether a string instance starts with the specified character. + /// + /// + /// The string instance. + /// + /// + /// The character to compare to the character at the start of this string. + /// + /// + /// if matches the start of the string; + /// otherwise, . + /// + /// + /// + /// This method performs an ordinal (case-sensitive and culture-insensitive) comparison. + /// + /// + /// This method exists on .NET Core, but doesn't on .NET Framework or .NET Standard 2.0. + /// + /// + public static bool StartsWith(this string text, char value) + { +#if NET + return text.StartsWith(value); +#else + return text.Length > 0 && text[0] == value; +#endif + } - // This method doesn't exist on .NET Framework, but it does on .NET Core. - public static bool EndsWith(this string s, char ch) - => s.Length > 0 && s[^1] == ch; + /// + /// Determines whether the end of a string instance matches the specified character. + /// + /// + /// The string instance. + /// + /// + /// The character to compare to the character at the end of this string. + /// + /// + /// if matches the end of this string; + /// otherwise, . + /// + /// + /// + /// This method performs an ordinal (case-sensitive and culture-insensitive) comparison. + /// + /// + /// This method exists on .NET Core, but doesn't on .NET Framework or .NET Standard 2.0. + /// + /// + public static bool EndsWith(this string text, char value) + { +#if NET + return text.EndsWith(value); +#else + return text.Length > 0 && text[^1] == value; +#endif + } } From 55aeabb88bcbcd5b76ddc58e8634a97781847ed1 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Tue, 16 Jul 2024 10:00:14 -0700 Subject: [PATCH 3/5] Add more XML doc comments --- .../StringExtensions.cs | 341 +++++++++++++++--- 1 file changed, 287 insertions(+), 54 deletions(-) diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/StringExtensions.cs b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/StringExtensions.cs index 59e7f106e6b..9dde4e4b539 100644 --- a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/StringExtensions.cs +++ b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/StringExtensions.cs @@ -11,18 +11,64 @@ namespace System; internal static class StringExtensions { - public static bool IsNullOrEmpty([NotNullWhen(false)] this string? s) - => string.IsNullOrEmpty(s); + /// + /// Indicates whether the specified string is or an empty string (""). + /// + /// + /// The string to test. + /// + /// + /// if the parameter is + /// or an empty string (""); otherwise, . + /// + /// + /// This extension method is useful on .NET Framework and .NET Standard 2.0 where + /// is not annotated for nullability. + /// + public static bool IsNullOrEmpty([NotNullWhen(false)] this string? value) + => string.IsNullOrEmpty(value); - public static bool IsNullOrWhiteSpace([NotNullWhen(false)] this string? s) - => string.IsNullOrWhiteSpace(s); + /// + /// Indicates whether a specified string is , empty, or consists only + /// of white-space characters. + /// + /// + /// The string to test. + /// + /// + /// if the parameter is + /// or , or if consists exclusively of + /// white-space characters. + /// + /// + /// This extension method is useful on .NET Framework and .NET Standard 2.0 where + /// is not annotated for nullability. + /// + public static bool IsNullOrWhiteSpace([NotNullWhen(false)] this string? value) + => string.IsNullOrWhiteSpace(value); - public static ReadOnlySpan AsSpan(this string? s, Index startIndex) + /// + /// Creates a new over a portion of the target string from + /// a specified position to the end of the string. + /// + /// + /// The target string. + /// + /// + /// The index at which to begin this slice. + /// + /// + /// This uses Razor's type, which is type-forwarded on .NET. + /// + /// + /// is less than 0 or greater than .Length. + /// + public static ReadOnlySpan AsSpan(this string? text, Index startIndex) { #if NET - return MemoryExtensions.AsSpan(s, startIndex); + return MemoryExtensions.AsSpan(text, startIndex); #else - if (s is null) + if (text is null) { if (!startIndex.Equals(Index.Start)) { @@ -32,74 +78,175 @@ public static ReadOnlySpan AsSpan(this string? s, Index startIndex) return default; } - return s.AsSpan(startIndex.GetOffset(s.Length)); + return text.AsSpan(startIndex.GetOffset(text.Length)); #endif } - public static ReadOnlySpan AsSpan(this string? s, Range range) + /// + /// Creates a new over a portion of a target string using + /// the range start and end indexes. + /// + /// + /// The target string. + /// + /// + /// The range that has start and end indexes to use for slicing the string. + /// + /// + /// This uses Razor's type, which is type-forwarded on .NET. + /// + /// + /// 's start or end index is not within the bounds of the string. + /// + /// + /// 's start index is greater than its end index. + /// + public static ReadOnlySpan AsSpan(this string? text, Range range) { #if NET - return MemoryExtensions.AsSpan(s, range); + return MemoryExtensions.AsSpan(text, range); #else - if (s is null) + if (text is null) { if (!range.Start.Equals(Index.Start) || !range.End.Equals(Index.Start)) { - ThrowHelper.ThrowArgumentNull(nameof(s)); + ThrowHelper.ThrowArgumentNull(nameof(text)); } return default; } - var (start, length) = range.GetOffsetAndLength(s.Length); - return s.AsSpan(start, length); + var (start, length) = range.GetOffsetAndLength(text.Length); + return text.AsSpan(start, length); #endif } - public static ReadOnlySpan AsSpanOrDefault(this string? s) - => s is not null ? s.AsSpan() : default; + /// + /// Creates a new over a string. If the target string + /// is a () is returned. + /// + /// + /// The target string. + /// + public static ReadOnlySpan AsSpanOrDefault(this string? text) + => text is not null ? text.AsSpan() : default; - public static ReadOnlySpan AsSpanOrDefault(this string? s, int start) - => s is not null ? s.AsSpan(start) : default; + /// + /// Creates a new over a portion of the target string from + /// a specified position to the end of the string. If the target string is + /// a () is returned. + /// + /// + /// The target string. + /// + /// + /// The index at which to begin this slice. + /// + /// + /// is less than 0 or greater than .Length. + /// + public static ReadOnlySpan AsSpanOrDefault(this string? text, int start) + => text is not null ? text.AsSpan(start) : default; - public static ReadOnlySpan AsSpanOrDefault(this string? s, int start, int length) - => s is not null ? s.AsSpan(start, length) : default; + /// + /// Creates a new over a portion of the target string from + /// a specified position for a specified number of characters. If the target string is + /// a () is returned. + /// + /// + /// The target string. + /// + /// + /// The index at which to begin this slice. + /// + /// + /// The desired length for the slice. + /// + /// + /// , , or + + /// is not in the range of . + /// + public static ReadOnlySpan AsSpanOrDefault(this string? text, int start, int length) + => text is not null ? text.AsSpan(start, length) : default; - public static ReadOnlySpan AsSpanOrDefault(this string? s, Index startIndex) + /// + /// Creates a new over a portion of the target string from + /// a specified position to the end of the string. If the target string is + /// a () is returned. + /// + /// + /// The target string. + /// + /// + /// The index at which to begin this slice. + /// + public static ReadOnlySpan AsSpanOrDefault(this string? text, Index startIndex) { - if (s is null) + if (text is null) { return default; } #if NET - return MemoryExtensions.AsSpan(s, startIndex); + return MemoryExtensions.AsSpan(text, startIndex); #else - return s.AsSpan(startIndex.GetOffset(s.Length)); + return text.AsSpan(startIndex.GetOffset(text.Length)); #endif } - public static ReadOnlySpan AsSpanOrDefault(this string? s, Range range) + /// + /// Creates a new over a portion of the target string using the range + /// start and end indexes. If the target string is a + /// () is returned. + /// + /// + /// The target string. + /// + /// + /// The range that has start and end indexes to use for slicing the string. + /// + /// + /// 's start or end index is not within the bounds of the string. + /// + /// + /// 's start index is greater than its end index. + /// + public static ReadOnlySpan AsSpanOrDefault(this string? text, Range range) { - if (s is null) + if (text is null) { return default; } #if NET - return MemoryExtensions.AsSpan(s, range); + return MemoryExtensions.AsSpan(text, range); #else - var (start, length) = range.GetOffsetAndLength(s.Length); - return s.AsSpan(start, length); + var (start, length) = range.GetOffsetAndLength(text.Length); + return text.AsSpan(start, length); #endif } - public static ReadOnlyMemory AsMemory(this string? s, Index startIndex) + /// + /// Creates a new over a portion of a target string starting at a specified index. + /// + /// + /// The target string. + /// + /// + /// The index at which to begin this slice. + /// + /// + /// This uses Razor's type, which is type-forwarded on .NET. + /// + /// + /// is less than 0 or greater than .Length. + /// + public static ReadOnlyMemory AsMemory(this string? text, Index startIndex) { #if NET - return MemoryExtensions.AsMemory(s, startIndex); + return MemoryExtensions.AsMemory(text, startIndex); #else - if (s is null) + if (text is null) { if (!startIndex.Equals(Index.Start)) { @@ -109,65 +256,151 @@ public static ReadOnlyMemory AsMemory(this string? s, Index startIndex) return default; } - return s.AsMemory(startIndex.GetOffset(s.Length)); + return text.AsMemory(startIndex.GetOffset(text.Length)); #endif } - public static ReadOnlyMemory AsMemory(this string? s, Range range) + /// + /// Creates a new over a portion of a target string using + /// the range start and end indexes. + /// + /// + /// The target string. + /// + /// + /// The range that has start and end indexes to use for slicing the string. + /// + /// + /// This uses Razor's type, which is type-forwarded on .NET. + /// + /// + /// 's start or end index is not within the bounds of the string. + /// + /// + /// 's start index is greater than its end index. + /// + public static ReadOnlyMemory AsMemory(this string? text, Range range) { #if NET - return MemoryExtensions.AsMemory(s, range); + return MemoryExtensions.AsMemory(text, range); #else - if (s is null) + if (text is null) { if (!range.Start.Equals(Index.Start) || !range.End.Equals(Index.Start)) { - ThrowHelper.ThrowArgumentNull(nameof(s)); + ThrowHelper.ThrowArgumentNull(nameof(text)); } return default; } - var (start, length) = range.GetOffsetAndLength(s.Length); - return s.AsMemory(start, length); + var (start, length) = range.GetOffsetAndLength(text.Length); + return text.AsMemory(start, length); #endif } - public static ReadOnlyMemory AsMemoryOrDefault(this string? s) - => s is not null ? s.AsMemory() : default; + /// + /// Creates a new over a string. If the target string + /// is a () is returned. + /// + /// + /// The target string. + /// + public static ReadOnlyMemory AsMemoryOrDefault(this string? text) + => text is not null ? text.AsMemory() : default; - public static ReadOnlyMemory AsMemoryOrDefault(this string? s, int start) - => s is not null ? s.AsMemory(start) : default; + /// + /// Creates a new over a portion of the target string from + /// a specified position to the end of the string. If the target string is + /// a () is returned. + /// + /// + /// The target string. + /// + /// + /// The index at which to begin this slice. + /// + /// + /// is less than 0 or greater than .Length. + /// + public static ReadOnlyMemory AsMemoryOrDefault(this string? text, int start) + => text is not null ? text.AsMemory(start) : default; - public static ReadOnlyMemory AsMemoryOrDefault(this string? s, int start, int length) - => s is not null ? s.AsMemory(start, length) : default; + /// + /// Creates a new over a portion of the target string from + /// a specified position for a specified number of characters. If the target string is + /// a () is returned. + /// + /// + /// The target string. + /// + /// + /// The index at which to begin this slice. + /// + /// + /// The desired length for the slice. + /// + /// + /// , , or + + /// is not in the range of . + /// + public static ReadOnlyMemory AsMemoryOrDefault(this string? text, int start, int length) + => text is not null ? text.AsMemory(start, length) : default; - public static ReadOnlyMemory AsMemoryOrDefault(this string? s, Index startIndex) + /// + /// Creates a new over a portion of the target string from + /// a specified position to the end of the string. If the target string is + /// a () is returned. + /// + /// + /// The target string. + /// + /// + /// The index at which to begin this slice. + /// + public static ReadOnlyMemory AsMemoryOrDefault(this string? text, Index startIndex) { - if (s is null) + if (text is null) { return default; } #if NET - return MemoryExtensions.AsMemory(s, startIndex); + return MemoryExtensions.AsMemory(text, startIndex); #else - return s.AsMemory(startIndex.GetOffset(s.Length)); + return text.AsMemory(startIndex.GetOffset(text.Length)); #endif } - public static ReadOnlyMemory AsMemoryOrDefault(this string? s, Range range) + /// + /// Creates a new over a portion of the target string using the range + /// start and end indexes. If the target string is a + /// () is returned. + /// + /// + /// The target string. + /// + /// + /// The range that has start and end indexes to use for slicing the string. + /// + /// + /// 's start or end index is not within the bounds of the string. + /// + /// + /// 's start index is greater than its end index. + /// + public static ReadOnlyMemory AsMemoryOrDefault(this string? text, Range range) { - if (s is null) + if (text is null) { return default; } #if NET - return MemoryExtensions.AsMemory(s, range); + return MemoryExtensions.AsMemory(text, range); #else - var (start, length) = range.GetOffsetAndLength(s.Length); - return s.AsMemory(start, length); + var (start, length) = range.GetOffsetAndLength(text.Length); + return text.AsMemory(start, length); #endif } From 19f2f7fe0d222367a7193833406c34eacd58d263 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Tue, 16 Jul 2024 10:23:08 -0700 Subject: [PATCH 4/5] Add array AsSpan and AsMemory extensions that use Razor's polyfill types --- .../ArrayExtensions.cs | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/ArrayExtensions.cs b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/ArrayExtensions.cs index 2663e636bd7..f09b4c8176e 100644 --- a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/ArrayExtensions.cs +++ b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/ArrayExtensions.cs @@ -1,13 +1,188 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. +using System; using System.Collections.Generic; using System.Collections.Immutable; +#if !NET +using ThrowHelper = Microsoft.AspNetCore.Razor.Utilities.ThrowHelper; +#endif + namespace Microsoft.AspNetCore.Razor; internal static class ArrayExtensions { + /// + /// Creates a new span over the portion of the target array defined by an value. + /// + /// + /// The array to convert. + /// + /// + /// The starting index. + /// + /// + /// This uses Razor's type, which is type-forwarded on .NET. + /// + /// + /// is . + /// + /// + /// is less than 0 or greater than .Length. + /// + /// + /// is covariant, and the array's type is not exactly []. + /// + public static ReadOnlySpan AsSpan(this T[]? array, Index startIndex) + { +#if NET + return MemoryExtensions.AsSpan(array, startIndex); +#else + if (array is null) + { + if (!startIndex.Equals(Index.Start)) + { + ThrowHelper.ThrowArgumentOutOfRange(nameof(startIndex)); + } + + return default; + } + + return MemoryExtensions.AsSpan(array, startIndex.GetOffset(array.Length)); +#endif + } + + /// + /// Creates a new span over the portion of the target array defined by a value. + /// + /// + /// The array to convert. + /// + /// + /// The range of the array to convert. + /// + /// + /// This uses Razor's type, which is type-forwarded on .NET. + /// + /// + /// is . + /// + /// + /// 's start or end index is not within the bounds of the string. + /// + /// + /// 's start index is greater than its end index. + /// + /// + /// is covariant, and the array's type is not exactly []. + /// + public static ReadOnlySpan AsSpan(this T[]? array, Range range) + { +#if NET + return MemoryExtensions.AsSpan(array, range); +#else + if (array is null) + { + if (!range.Start.Equals(Index.Start) || !range.End.Equals(Index.Start)) + { + ThrowHelper.ThrowArgumentNull(nameof(array)); + } + + return default; + } + + var (start, length) = range.GetOffsetAndLength(array.Length); + return MemoryExtensions.AsSpan(array, start, length); +#endif + } + + /// + /// Creates a new memory region over the portion of the target starting at the specified index + /// to the end of the array. + /// + /// + /// The array to convert. + /// + /// + /// The first position of the array. + /// + /// + /// This uses Razor's type, which is type-forwarded on .NET. + /// + /// + /// is . + /// + /// + /// is less than 0 or greater than .Length. + /// + /// + /// is covariant, and the array's type is not exactly []. + /// + public static ReadOnlyMemory AsMemory(this T[]? array, Index startIndex) + { +#if NET + return MemoryExtensions.AsMemory(array, startIndex); +#else + if (array is null) + { + if (!startIndex.Equals(Index.Start)) + { + ThrowHelper.ThrowArgumentOutOfRange(nameof(startIndex)); + } + + return default; + } + + return MemoryExtensions.AsMemory(array, startIndex.GetOffset(array.Length)); +#endif + } + + /// + /// Creates a new memory region over the portion of the target array beginning at + /// inclusive start index of the range and ending at the exclusive end index of the range. + /// + /// + /// The array to convert. + /// + /// + /// The range of the array to convert. + /// + /// + /// This uses Razor's type, which is type-forwarded on .NET. + /// + /// + /// is . + /// + /// + /// 's start or end index is not within the bounds of the string. + /// + /// + /// 's start index is greater than its end index. + /// + /// + /// is covariant, and the array's type is not exactly []. + /// + public static ReadOnlyMemory AsMemory(this T[]? array, Range range) + { +#if NET + return MemoryExtensions.AsMemory(array, range); +#else + if (array is null) + { + if (!range.Start.Equals(Index.Start) || !range.End.Equals(Index.Start)) + { + ThrowHelper.ThrowArgumentNull(nameof(array)); + } + + return default; + } + + var (start, length) = range.GetOffsetAndLength(array.Length); + return MemoryExtensions.AsMemory(array, start, length); +#endif + } + public static ImmutableDictionary ToImmutableDictionary( this (TKey key, TValue value)[] array, IEqualityComparer keyComparer) where TKey : notnull From 90cbe54474deca5d2844e6714d69fb484858c8c5 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Tue, 16 Jul 2024 10:28:25 -0700 Subject: [PATCH 5/5] Remote DrainToImmutable() extension method Since moving to System.Collections.Immutable 8.0, this extension method is no longer needed. --- .../ImmutableArrayExtensions.cs | 33 ------------------- 1 file changed, 33 deletions(-) diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/ImmutableArrayExtensions.cs b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/ImmutableArrayExtensions.cs index e0d7478cca7..3107a833101 100644 --- a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/ImmutableArrayExtensions.cs +++ b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/ImmutableArrayExtensions.cs @@ -29,39 +29,6 @@ public static void SetCapacityIfLarger(this ImmutableArray.Builder builder } } - /// - /// Returns the current contents as an and sets - /// the collection to a zero length array. - /// - /// - /// If equals - /// , the internal array will be extracted - /// as an without copying the contents. Otherwise, the - /// contents will be copied into a new array. The collection will then be set to a - /// zero-length array. - /// - /// An immutable array. - public static ImmutableArray DrainToImmutable(this ImmutableArray.Builder builder) - { -#if NET8_0_OR_GREATER - return builder.DrainToImmutable(); -#else - if (builder.Count == 0) - { - return []; - } - - if (builder.Count == builder.Capacity) - { - return builder.MoveToImmutable(); - } - - var result = builder.ToImmutable(); - builder.Clear(); - return result; -#endif - } - public static ImmutableArray SelectAsArray(this ImmutableArray source, Func selector) { return source switch