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 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 diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/StringExtensions.cs b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/StringExtensions.cs index f649b756075..9dde4e4b539 100644 --- a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/StringExtensions.cs +++ b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/StringExtensions.cs @@ -3,27 +3,555 @@ using System.Diagnostics.CodeAnalysis; +#if !NET +using ThrowHelper = Microsoft.AspNetCore.Razor.Utilities.ThrowHelper; +#endif + 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); + + /// + /// 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); + + /// + /// 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(text, startIndex); +#else + if (text is null) + { + if (!startIndex.Equals(Index.Start)) + { + ThrowHelper.ThrowArgumentOutOfRange(nameof(startIndex)); + } + + return default; + } + + return text.AsSpan(startIndex.GetOffset(text.Length)); +#endif + } + + /// + /// 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(text, range); +#else + if (text is null) + { + if (!range.Start.Equals(Index.Start) || !range.End.Equals(Index.Start)) + { + ThrowHelper.ThrowArgumentNull(nameof(text)); + } + + return default; + } + + var (start, length) = range.GetOffsetAndLength(text.Length); + return text.AsSpan(start, length); +#endif + } + + /// + /// 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; + + /// + /// 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; + + /// + /// 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; + + /// + /// 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 (text is null) + { + return default; + } + +#if NET + return MemoryExtensions.AsSpan(text, startIndex); +#else + return text.AsSpan(startIndex.GetOffset(text.Length)); +#endif + } + + /// + /// 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 (text is null) + { + return default; + } + +#if NET + return MemoryExtensions.AsSpan(text, range); +#else + var (start, length) = range.GetOffsetAndLength(text.Length); + return text.AsSpan(start, length); +#endif + } + + /// + /// 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(text, startIndex); +#else + if (text is null) + { + if (!startIndex.Equals(Index.Start)) + { + ThrowHelper.ThrowArgumentOutOfRange(nameof(startIndex)); + } + + return default; + } + + return text.AsMemory(startIndex.GetOffset(text.Length)); +#endif + } + + /// + /// 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(text, range); +#else + if (text is null) + { + if (!range.Start.Equals(Index.Start) || !range.End.Equals(Index.Start)) + { + ThrowHelper.ThrowArgumentNull(nameof(text)); + } + + return default; + } + + var (start, length) = range.GetOffsetAndLength(text.Length); + return text.AsMemory(start, length); +#endif + } + + /// + /// 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; + + /// + /// 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; + + /// + /// 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; + + /// + /// 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 (text is null) + { + return default; + } + +#if NET + return MemoryExtensions.AsMemory(text, startIndex); +#else + return text.AsMemory(startIndex.GetOffset(text.Length)); +#endif + } + + /// + /// 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 (text is null) + { + return default; + } + +#if NET + return MemoryExtensions.AsMemory(text, range); +#else + var (start, length) = range.GetOffsetAndLength(text.Length); + return text.AsMemory(start, length); +#endif + } - public static bool IsNullOrWhiteSpace([NotNullWhen(false)] this string? s) - => string.IsNullOrWhiteSpace(s); + /// + /// 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 + } - public static ReadOnlySpan AsSpanOrDefault(this string? s) - => s is not null ? s.AsSpan() : default; + /// + /// 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 + } - public static ReadOnlyMemory AsMemoryOrDefault(this string? s) - => s is not null ? s.AsMemory() : default; + /// + /// 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 + } - // 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; + /// + /// 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 + } } 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 . ///