diff --git a/src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Performance/CSharpUseSearchValues.Fixer.cs b/src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Performance/CSharpUseSearchValues.Fixer.cs index 4e59a07c85..9d0511e82c 100644 --- a/src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Performance/CSharpUseSearchValues.Fixer.cs +++ b/src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Performance/CSharpUseSearchValues.Fixer.cs @@ -5,7 +5,6 @@ using System.Diagnostics; using System.Threading; using System.Threading.Tasks; -using Analyzer.Utilities.Extensions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; @@ -62,6 +61,8 @@ protected override SyntaxNode GetDeclaratorInitializer(SyntaxNode syntax) // new[] { 'a', 'b', 'c' } => "abc" // new[] { (byte)'a', (byte)'b', (byte)'c' } => "abc"u8 // "abc".ToCharArray() => "abc" + // ['a', 'b', 'c'] => "abc" + // [(byte)'a', (byte)'b', (byte)'c'] => "abc"u8 protected override SyntaxNode? TryReplaceArrayCreationWithInlineLiteralExpression(IOperation operation) { if (operation is IConversionOperation conversion) @@ -69,8 +70,34 @@ protected override SyntaxNode GetDeclaratorInitializer(SyntaxNode syntax) operation = conversion.Operand; } - if (operation is IArrayCreationOperation arrayCreation && - arrayCreation.GetElementType() is { } elementType) + if (operation is IInvocationOperation invocation) + { + if (UseSearchValuesAnalyzer.IsConstantStringToCharArrayInvocation(invocation, out _)) + { + Debug.Assert(invocation.Instance is not null); + return invocation.Instance!.Syntax; + } + + return null; + } + + ITypeSymbol? elementType = null; + + if (operation.Type is IArrayTypeSymbol arrayType) + { + elementType = arrayType.ElementType; + } + else if (operation.Type is INamedTypeSymbol namedType) + { + if (namedType.TypeArguments is [var typeArgument]) + { + Debug.Assert(namedType.Name.Contains("Span", StringComparison.Ordinal), namedType.Name); + + elementType = typeArgument; + } + } + + if (elementType is not null) { bool isByte = elementType.SpecialType == SpecialType.System_Byte; @@ -84,7 +111,7 @@ protected override SyntaxNode GetDeclaratorInitializer(SyntaxNode syntax) List values = new(); - if (arrayCreation.Syntax is ExpressionSyntax creationSyntax && + if (operation.Syntax is ExpressionSyntax creationSyntax && CSharpUseSearchValuesAnalyzer.IsConstantByteOrCharArrayCreationExpression(operation.SemanticModel!, creationSyntax, values, out _) && values.Count <= 128 && // Arbitrary limit to avoid emitting huge literals !ContainsAnyComments(creationSyntax)) // Avoid removing potentially valuable comments @@ -118,14 +145,6 @@ protected override SyntaxNode GetDeclaratorInitializer(SyntaxNode syntax) trailing: default)); } } - else if (operation is IInvocationOperation invocation) - { - if (UseSearchValuesAnalyzer.IsConstantStringToCharArrayInvocation(invocation, out _)) - { - Debug.Assert(invocation.Instance is not null); - return invocation.Instance!.Syntax; - } - } return null; } diff --git a/src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Performance/CSharpUseSearchValues.cs b/src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Performance/CSharpUseSearchValues.cs index 0667941d8d..5a39ce4b80 100644 --- a/src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Performance/CSharpUseSearchValues.cs +++ b/src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Performance/CSharpUseSearchValues.cs @@ -19,6 +19,8 @@ public sealed class CSharpUseSearchValuesAnalyzer : UseSearchValuesAnalyzer // char[] myField = "abc".ToCharArray(); // char[] myField = ConstString.ToCharArray(); // byte[] myField = new[] { (byte)'a', (byte)'b', (byte)'c' }; + // char[] myField = ['a', 'b', 'c']; + // byte[] myField = [(byte)'a', (byte)'b', (byte)'c']; protected override bool IsConstantByteOrCharArrayVariableDeclaratorSyntax(SemanticModel semanticModel, SyntaxNode syntax, out int length) { length = 0; @@ -37,6 +39,8 @@ syntax is VariableDeclaratorSyntax variableDeclarator && // ReadOnlySpan myProperty => "abc"u8; // ReadOnlySpan myProperty { get => "abc"u8; } // ReadOnlySpan myProperty { get { return "abc"u8; } } + // ReadOnlySpan myProperty => ['a', 'b', 'c']; + // ReadOnlySpan myProperty => [(byte)'a', (byte)'b', (byte)'c']; protected override bool IsConstantByteOrCharReadOnlySpanPropertyDeclarationSyntax(SemanticModel semanticModel, SyntaxNode syntax, out int length) { length = 0; @@ -44,7 +48,9 @@ protected override bool IsConstantByteOrCharReadOnlySpanPropertyDeclarationSynta return syntax is PropertyDeclarationSyntax propertyDeclaration && TryGetPropertyGetterExpression(propertyDeclaration) is { } expression && - (IsConstantByteOrCharArrayCreationExpression(semanticModel, expression, values: null, out length) || IsUtf8StringLiteralExpression(expression, out length)); + (IsConstantByteOrCharArrayCreationExpression(semanticModel, expression, values: null, out length) || + IsUtf8StringLiteralExpression(expression, out length) || + (semanticModel.GetOperation(expression) is { } operation && IsConstantByteOrCharCollectionExpression(operation, values: null, out length))); } protected override bool IsConstantByteOrCharArrayCreationSyntax(SemanticModel semanticModel, SyntaxNode syntax, out int length) @@ -106,6 +112,12 @@ internal static bool IsConstantByteOrCharArrayCreationExpression(SemanticModel s return true; } } + else + { + return + semanticModel.GetOperation(expression) is { } operation && + IsConstantByteOrCharCollectionExpression(operation, values, out length); + } if (arrayInitializer?.Expressions is { } valueExpressions) { diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseSearchValues.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseSearchValues.cs index 3df26e55fe..52538b4c87 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseSearchValues.cs +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseSearchValues.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. +using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -183,6 +184,12 @@ private bool AreConstantValuesWorthReplacing(IOperation argument, INamedTypeSymb length >= MinLengthWorthReplacing || conversion.Operand is IInvocationOperation; } + else if (IsConstantByteOrCharCollectionExpression(conversion.Operand, values: null, out length)) + { + // text.IndexOfAny(['a', 'b', 'c']) + // text.IndexOfAny([(byte)'a', (byte)'b', (byte)'c']) + return length >= MinLengthWorthReplacing; + } } else if (argument.Kind == OperationKindEx.Utf8String) { @@ -310,5 +317,49 @@ operation is ILiteralOperation or IFieldReferenceOperation or ILocalReferenceOpe value = null; return false; } + + internal static bool IsConstantByteOrCharCollectionExpression(IOperation operation, List? values, out int length) + { + if (operation.Kind == OperationKindEx.CollectionExpression && + ICollectionExpressionOperationWrapper.IsInstance(operation) && + ICollectionExpressionOperationWrapper.FromOperation(operation) is { } collection && + AllElementsAreConstantByteOrCharLiterals(collection.Elements, values)) + { + length = collection.Elements.Length; + return true; + } + + length = 0; + return false; + + static bool AllElementsAreConstantByteOrCharLiterals(ImmutableArray elements, List? values) + { + foreach (IOperation element in elements) + { + IOperation operation = element; + + if (operation is IConversionOperation conversion) + { + if (operation.Type is not { SpecialType: SpecialType.System_Byte }) + { + return false; + } + + operation = conversion.Operand; + } + + if (operation.Type is not { SpecialType: SpecialType.System_Char } || + operation is not ILiteralOperation literal || + literal.ConstantValue.Value is not char charValue) + { + return false; + } + + values?.Add(charValue); + } + + return true; + } + } } } \ No newline at end of file diff --git a/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseSearchValuesTests.cs b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseSearchValuesTests.cs index 71fa7d297b..6aa93fe79d 100644 --- a/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseSearchValuesTests.cs +++ b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseSearchValuesTests.cs @@ -236,6 +236,48 @@ private void TestMethod(ReadOnlySpan bytes) """); } + [Fact] + public async Task TestCollectionExpressionAnalyzer() + { + await VerifyAnalyzerAsync(LanguageVersion.CSharp12, + """ + using System; + + internal sealed class Test + { + private static readonly char[] ShortStaticReadonlyCharArrayField = ['a', 'e', 'i', 'o', 'u']; + private static readonly char[] LongStaticReadonlyCharArrayField = ['a', 'e', 'i', 'o', 'u', 'A']; + private ReadOnlySpan ShortReadOnlySpanOfCharRVAProperty => ['a', 'e', 'i', 'o', 'u']; + private ReadOnlySpan LongReadOnlySpanOfCharRVAProperty => ['a', 'e', 'i', 'o', 'u', 'A']; + private static readonly byte[] ShortStaticReadonlyByteArrayField = [(byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u']; + private static readonly byte[] LongStaticReadonlyByteArrayField = [(byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u', (byte)'A']; + private ReadOnlySpan ShortReadOnlySpanOfByteRVAProperty => [(byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u']; + private ReadOnlySpan LongReadOnlySpanOfByteRVAProperty => [(byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u', (byte)'A']; + + private void TestMethod(ReadOnlySpan chars, ReadOnlySpan bytes) + { + _ = chars.IndexOfAny([]); + _ = chars.IndexOfAny(['a', 'e', 'i', 'o', 'u']); + _ = chars.IndexOfAny([|['a', 'e', 'i', 'o', 'u', 'A']|]); + + _ = chars.IndexOfAny(ShortStaticReadonlyCharArrayField); + _ = chars.IndexOfAny([|LongStaticReadonlyCharArrayField|]); + _ = chars.IndexOfAny(ShortReadOnlySpanOfCharRVAProperty); + _ = chars.IndexOfAny([|LongReadOnlySpanOfCharRVAProperty|]); + + _ = bytes.IndexOfAny([]); + _ = bytes.IndexOfAny([(byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u']); + _ = bytes.IndexOfAny([|[(byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u', (byte)'A']|]); + + _ = bytes.IndexOfAny(ShortStaticReadonlyByteArrayField); + _ = bytes.IndexOfAny([|LongStaticReadonlyByteArrayField|]); + _ = bytes.IndexOfAny(ShortReadOnlySpanOfByteRVAProperty); + _ = bytes.IndexOfAny([|LongReadOnlySpanOfByteRVAProperty|]); + } + } + """); + } + public static IEnumerable TestAllIndexOfAnyAndContainsAnySpanOverloads_MemberData() { return @@ -330,11 +372,15 @@ private void TestMethod(string input) [InlineData("static readonly char[]", "= new[] { 'a', 'e', 'i', 'o', 'u', 'A' };", false)] [InlineData("static readonly byte[]", "= new[] { (byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u', (byte)'A' };", false)] [InlineData("readonly char[]", "= new[] { 'a', 'e', 'i', 'o', 'u', 'A' };", false)] + [InlineData("readonly char[]", "= ['a', 'e', 'i', 'o', 'u', 'A'];", false)] [InlineData("readonly byte[]", "= new[] { (byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u', (byte)'A' };", false)] + [InlineData("readonly byte[]", "= [(byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u', (byte)'A'];", false)] [InlineData("readonly char[]", "= new char[] { 'a', 'e', 'i', 'o', 'u', 'A' };", false)] [InlineData("readonly char[]", "= new char[] { 'a', 'e', 'i', 'o', 'u', 'A' };", false)] [InlineData("ReadOnlySpan", "=> new[] { 'a', 'e', 'i', 'o', 'u', 'A' };", false)] + [InlineData("ReadOnlySpan", "=> ['a', 'e', 'i', 'o', 'u', 'A'];", false)] [InlineData("ReadOnlySpan", "=> new[] { (byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u', (byte)'A' };", false)] + [InlineData("ReadOnlySpan", "=> [(byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u', (byte)'A'];", false)] [InlineData("ReadOnlySpan", "=> \"aeiouA\"u8;", false)] [InlineData("static ReadOnlySpan", "=> new[] { 'a', 'e', 'i', 'o', 'u', 'A' };", true)] [InlineData("static ReadOnlySpan", "=> new[] { (byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u', (byte)'A' };", true)] @@ -342,7 +388,9 @@ private void TestMethod(string input) [InlineData("ReadOnlySpan", "{ get => \"aeiouA\"u8; }", false)] [InlineData("ReadOnlySpan", "{ get { return \"aeiouA\"u8; } }", false)] [InlineData("ReadOnlySpan", "{ get => new[] { 'a', 'e', 'i', 'o', 'u', 'A' }; }", false)] + [InlineData("ReadOnlySpan", "{ get => ['a', 'e', 'i', 'o', 'u', 'A']; }", false)] [InlineData("ReadOnlySpan", "{ get { return new[] { (byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u', (byte)'A' }; } }", false)] + [InlineData("ReadOnlySpan", "{ get { return [(byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u', (byte)'A']; } }", false)] [InlineData("static ReadOnlySpan", "{ get => \"aeiouA\"u8; }", true)] [InlineData("static ReadOnlySpan", "{ get { return \"aeiouA\"u8; } }", true)] [InlineData("static ReadOnlySpan", "{ get => new[] { 'a', 'e', 'i', 'o', 'u', 'A' }; }", true)] @@ -395,6 +443,7 @@ public async Task TestCodeFixerNamedArguments(string modifiersAndType, string in await TestAsync(LanguageVersion.CSharp7_3, createExpression); await TestAsync(LanguageVersion.CSharp11, cSharp11CreateExpression ?? createExpression); + await TestAsync(LanguageVersion.CSharp12, cSharp11CreateExpression ?? createExpression); async Task TestAsync(LanguageVersion languageVersion, string expectedCreateExpression) { @@ -405,6 +454,13 @@ async Task TestAsync(LanguageVersion languageVersion, string expectedCreateExpre return; } + if (languageVersion < LanguageVersion.CSharp12 && + memberDefinition.LastIndexOf(']') - memberDefinition.IndexOf('[', StringComparison.Ordinal) > 1) + { + // Need CSharp 12 or newer to use collection expressions + return; + } + string source = $$""" using System; @@ -616,6 +672,8 @@ private void TestMethod({{argumentType}} text) [InlineData(LanguageVersion.CSharp7_3, "\"aeiouA\"", "\"aeiouA\"")] [InlineData(LanguageVersion.CSharp7_3, "@\"aeiouA\"", "@\"aeiouA\"")] [InlineData(LanguageVersion.CSharp11, "\"aeiouA\"u8", "\"aeiouA\"u8")] + [InlineData(LanguageVersion.CSharp12, "['a', 'e', 'i', 'o', 'u', 'A']", "\"aeiouA\"")] + [InlineData(LanguageVersion.CSharp12, "[(byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u', (byte)'A']", "\"aeiouA\"u8")] [InlineData(LanguageVersion.CSharp7_3, "new[] { 'a', 'e', 'i', 'o', 'u', 'A' }", "\"aeiouA\"")] [InlineData(LanguageVersion.CSharp7_3, "new char[] { 'a', 'e', 'i', 'o', 'u', 'A' }", "\"aeiouA\"")] [InlineData(LanguageVersion.CSharp7_3, "new char[] { 'a', 'e', 'i', 'o', 'u', 'A' }", "\"aeiouA\"")] diff --git a/src/Utilities/Compiler/Analyzer.Utilities.projitems b/src/Utilities/Compiler/Analyzer.Utilities.projitems index 441ad25bb5..13c50f50f9 100644 --- a/src/Utilities/Compiler/Analyzer.Utilities.projitems +++ b/src/Utilities/Compiler/Analyzer.Utilities.projitems @@ -66,6 +66,7 @@ + diff --git a/src/Utilities/Compiler/Lightup/ICollectionExpressionOperationWrapper.cs b/src/Utilities/Compiler/Lightup/ICollectionExpressionOperationWrapper.cs new file mode 100644 index 0000000000..5dbb25193d --- /dev/null +++ b/src/Utilities/Compiler/Lightup/ICollectionExpressionOperationWrapper.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. + +#if HAS_IOPERATION + +namespace Analyzer.Utilities.Lightup +{ + using System; + using System.Collections.Immutable; + using System.Diagnostics.CodeAnalysis; + using Microsoft.CodeAnalysis; + + [SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", Justification = "Not a comparable instance.")] + internal readonly struct ICollectionExpressionOperationWrapper : IOperationWrapper + { + internal const string WrappedTypeName = "Microsoft.CodeAnalysis.Operations.ICollectionExpressionOperation"; + private static readonly Type? WrappedType = OperationWrapperHelper.GetWrappedType(typeof(ICollectionExpressionOperationWrapper)); + + private static readonly Func> ElementsAccessor = LightupHelpers.CreateOperationPropertyAccessor>(WrappedType, nameof(Elements), fallbackResult: default); + + private ICollectionExpressionOperationWrapper(IOperation operation) + { + WrappedOperation = operation; + } + + public IOperation WrappedOperation { get; } + public ITypeSymbol? Type => WrappedOperation.Type; + public ImmutableArray Elements => ElementsAccessor(WrappedOperation); + + public static ICollectionExpressionOperationWrapper FromOperation(IOperation operation) + { + if (operation == null) + { + return default; + } + + if (!IsInstance(operation)) + { + throw new InvalidCastException($"Cannot cast '{operation.GetType().FullName}' to '{WrappedTypeName}'"); + } + + return new ICollectionExpressionOperationWrapper(operation); + } + + public static bool IsInstance(IOperation operation) + { + return operation != null && LightupHelpers.CanWrapOperation(operation, WrappedType); + } + } +} + +#endif diff --git a/src/Utilities/Compiler/Lightup/OperationWrapperHelper.cs b/src/Utilities/Compiler/Lightup/OperationWrapperHelper.cs index d4b6360a94..90b7ae007e 100644 --- a/src/Utilities/Compiler/Lightup/OperationWrapperHelper.cs +++ b/src/Utilities/Compiler/Lightup/OperationWrapperHelper.cs @@ -15,7 +15,8 @@ internal static class OperationWrapperHelper private static readonly ImmutableDictionary WrappedTypes = ImmutableDictionary.Create() .Add(typeof(IFunctionPointerInvocationOperationWrapper), s_codeAnalysisAssembly.GetType(IFunctionPointerInvocationOperationWrapper.WrappedTypeName)) - .Add(typeof(IUtf8StringOperationWrapper), s_codeAnalysisAssembly.GetType(IUtf8StringOperationWrapper.WrappedTypeName)); + .Add(typeof(IUtf8StringOperationWrapper), s_codeAnalysisAssembly.GetType(IUtf8StringOperationWrapper.WrappedTypeName)) + .Add(typeof(ICollectionExpressionOperationWrapper), s_codeAnalysisAssembly.GetType(ICollectionExpressionOperationWrapper.WrappedTypeName)); /// /// Gets the type that is wrapped by the given wrapper.