From 8011355fb332fee2ad0ba2a68ecd9c08fc4e50e2 Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Fri, 8 Sep 2023 19:39:27 +0200 Subject: [PATCH] Add analyzer/fixer to suggest using cached SearchValues instances (#6898) * Initial Analyzer implementation * Code fixer * Add support for string.IndexOfAny(char[]) * Catch simple cases of array element modification * Use built-in helper instead of Linq * Also detect field assignments in ctor * Move system namespace import to helper method * Replace array creations wtih string literals * Add support for more property getter patterns * Simplify test helper * Revert Utf8String support :( * Update tests * msbuild /t:pack /v:m * Fix editor renaming * Exclude string uses on a conditional access * Add test for array field with const char reference * Add back Utf8String support * Update messages/descriptions * Add support for field initialized from literal.ToCharArray * More tests for ToCharArray * Better handle member names that start with _ * Avoid some duplication between Syntax and Operation analysis * Fix top-level statements and add logic to remove unused members * ImmutableHashSet, no OfType * Remove some duplication * Turn one analyzer test into code fixer tests * Shorten analyzer title --- .../CSharpUseSearchValues.Fixer.cs | 133 ++ .../Performance/CSharpUseSearchValues.cs | 221 ++++ .../Core/AnalyzerReleases.Unshipped.md | 1 + .../MicrosoftNetCoreAnalyzersResources.resx | 12 + .../Performance/UseSearchValues.Fixer.cs | 374 ++++++ .../Performance/UseSearchValues.cs | 314 +++++ .../MicrosoftNetCoreAnalyzersResources.cs.xlf | 20 + .../MicrosoftNetCoreAnalyzersResources.de.xlf | 20 + .../MicrosoftNetCoreAnalyzersResources.es.xlf | 20 + .../MicrosoftNetCoreAnalyzersResources.fr.xlf | 20 + .../MicrosoftNetCoreAnalyzersResources.it.xlf | 20 + .../MicrosoftNetCoreAnalyzersResources.ja.xlf | 20 + .../MicrosoftNetCoreAnalyzersResources.ko.xlf | 20 + .../MicrosoftNetCoreAnalyzersResources.pl.xlf | 20 + ...crosoftNetCoreAnalyzersResources.pt-BR.xlf | 20 + .../MicrosoftNetCoreAnalyzersResources.ru.xlf | 20 + .../MicrosoftNetCoreAnalyzersResources.tr.xlf | 20 + ...osoftNetCoreAnalyzersResources.zh-Hans.xlf | 20 + ...osoftNetCoreAnalyzersResources.zh-Hant.xlf | 20 + .../Microsoft.CodeAnalysis.NetAnalyzers.md | 12 + .../Microsoft.CodeAnalysis.NetAnalyzers.sarif | 19 + src/NetAnalyzers/RulesMissingDocumentation.md | 1 + .../Performance/UseSearchValuesTests.cs | 1076 +++++++++++++++++ .../Compiler/Analyzer.Utilities.projitems | 1 + .../DiagnosticCategoryAndIdRanges.txt | 2 +- .../Lightup/IUtf8StringOperationWrapper.cs | 50 + .../Compiler/Lightup/OperationKindEx.cs | 1 + .../Lightup/OperationWrapperHelper.cs | 3 +- src/Utilities/Compiler/WellKnownTypeNames.cs | 2 + 29 files changed, 2480 insertions(+), 2 deletions(-) create mode 100644 src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Performance/CSharpUseSearchValues.Fixer.cs create mode 100644 src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Performance/CSharpUseSearchValues.cs create mode 100644 src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseSearchValues.Fixer.cs create mode 100644 src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseSearchValues.cs create mode 100644 src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseSearchValuesTests.cs create mode 100644 src/Utilities/Compiler/Lightup/IUtf8StringOperationWrapper.cs diff --git a/src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Performance/CSharpUseSearchValues.Fixer.cs b/src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Performance/CSharpUseSearchValues.Fixer.cs new file mode 100644 index 0000000000..7cc58e7efb --- /dev/null +++ b/src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Performance/CSharpUseSearchValues.Fixer.cs @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft. 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.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Analyzer.Utilities.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Operations; +using Microsoft.NetCore.Analyzers.Performance; + +namespace Microsoft.NetCore.CSharp.Analyzers.Performance +{ + /// + [ExportCodeFixProvider(LanguageNames.CSharp)] + public sealed class CSharpUseSearchValuesFixer : UseSearchValuesFixer + { + protected override async ValueTask<(SyntaxNode TypeDeclaration, INamedTypeSymbol? TypeSymbol, bool IsRealType)> GetTypeSymbolAsync(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken) + { + SyntaxNode? typeDeclarationOrCompilationUnit = node.FirstAncestorOrSelf(); + + typeDeclarationOrCompilationUnit ??= await node.SyntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false); + + return typeDeclarationOrCompilationUnit is TypeDeclarationSyntax typeDeclaration + ? (typeDeclaration, semanticModel.GetDeclaredSymbol(typeDeclaration, cancellationToken), IsRealType: true) + : (typeDeclarationOrCompilationUnit, semanticModel.GetDeclaredSymbol((CompilationUnitSyntax)typeDeclarationOrCompilationUnit, cancellationToken)?.ContainingType, IsRealType: false); + } + + protected override SyntaxNode ReplaceSearchValuesFieldName(SyntaxNode node) + { + if (node is FieldDeclarationSyntax fieldDeclaration && + fieldDeclaration.Declaration is { } declaration && + declaration.Variables is [var declarator]) + { + var newDeclarator = declarator.ReplaceToken(declarator.Identifier, declarator.Identifier.WithAdditionalAnnotations(RenameAnnotation.Create())); + return fieldDeclaration.WithDeclaration(declaration.WithVariables(new SeparatedSyntaxList().Add(newDeclarator))); + } + + return node; + } + + protected override SyntaxNode GetDeclaratorInitializer(SyntaxNode syntax) + { + if (syntax is VariableDeclaratorSyntax variableDeclarator) + { + return variableDeclarator.Initializer!.Value; + } + + if (syntax is PropertyDeclarationSyntax propertyDeclaration) + { + return CSharpUseSearchValuesAnalyzer.TryGetPropertyGetterExpression(propertyDeclaration)!; + } + + throw new InvalidOperationException($"Expected 'VariableDeclaratorSyntax' or 'PropertyDeclarationSyntax', got {syntax.GetType().Name}"); + } + + // new[] { 'a', 'b', 'c' } => "abc" + // new[] { (byte)'a', (byte)'b', (byte)'c' } => "abc"u8 + // "abc".ToCharArray() => "abc" + protected override SyntaxNode? TryReplaceArrayCreationWithInlineLiteralExpression(IOperation operation) + { + if (operation is IConversionOperation conversion) + { + operation = conversion.Operand; + } + + if (operation is IArrayCreationOperation arrayCreation && + arrayCreation.GetElementType() is { } elementType) + { + bool isByte = elementType.SpecialType == SpecialType.System_Byte; + + if (isByte && + (operation.SemanticModel?.Compilation is not CSharpCompilation compilation || + compilation.LanguageVersion < (LanguageVersion)1100)) // LanguageVersion.CSharp11 + { + // Can't use Utf8StringLiterals + return null; + } + + List values = new(); + + if (arrayCreation.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 + { + string valuesString = string.Concat(values); + string stringLiteral = SymbolDisplay.FormatLiteral(valuesString, quote: true); + + const SyntaxKind Utf8StringLiteralExpression = (SyntaxKind)8756; + const SyntaxKind Utf8StringLiteralToken = (SyntaxKind)8520; + + return SyntaxFactory.LiteralExpression( + isByte ? Utf8StringLiteralExpression : SyntaxKind.StringLiteralExpression, + SyntaxFactory.Token( + leading: default, + kind: isByte ? Utf8StringLiteralToken : SyntaxKind.StringLiteralToken, + text: isByte ? $"{stringLiteral}u8" : stringLiteral, + valueText: valuesString, + 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; + } + + private static bool ContainsAnyComments(SyntaxNode node) + { + foreach (SyntaxTrivia trivia in node.DescendantTrivia(node.Span)) + { + if (trivia.Kind() is SyntaxKind.SingleLineCommentTrivia or SyntaxKind.MultiLineCommentTrivia) + { + return true; + } + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Performance/CSharpUseSearchValues.cs b/src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Performance/CSharpUseSearchValues.cs new file mode 100644 index 0000000000..0667941d8d --- /dev/null +++ b/src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Performance/CSharpUseSearchValues.cs @@ -0,0 +1,221 @@ +// 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 Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +using Microsoft.NetCore.Analyzers.Performance; + +namespace Microsoft.NetCore.CSharp.Analyzers.Performance +{ + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class CSharpUseSearchValuesAnalyzer : UseSearchValuesAnalyzer + { + // char[] myField = new char[] { 'a', 'b', 'c' }; + // char[] myField = new[] { 'a', 'b', 'c' }; + // char[] myField = "abc".ToCharArray(); + // char[] myField = ConstString.ToCharArray(); + // byte[] myField = new[] { (byte)'a', (byte)'b', (byte)'c' }; + protected override bool IsConstantByteOrCharArrayVariableDeclaratorSyntax(SemanticModel semanticModel, SyntaxNode syntax, out int length) + { + length = 0; + + return + syntax is VariableDeclaratorSyntax variableDeclarator && + variableDeclarator.Initializer?.Value is { } initializer && + IsConstantByteOrCharArrayCreationExpression(semanticModel, initializer, values: null, out length); + } + + // ReadOnlySpan myProperty => new char[] { 'a', 'b', 'c' }; + // ReadOnlySpan myProperty => new[] { 'a', 'b', 'c' }; + // ReadOnlySpan myProperty => "abc".ToCharArray(); + // ReadOnlySpan myProperty => ConstString.ToCharArray(); + // ReadOnlySpan myProperty => new[] { (byte)'a', (byte)'b', (byte)'c' }; + // ReadOnlySpan myProperty => "abc"u8; + // ReadOnlySpan myProperty { get => "abc"u8; } + // ReadOnlySpan myProperty { get { return "abc"u8; } } + protected override bool IsConstantByteOrCharReadOnlySpanPropertyDeclarationSyntax(SemanticModel semanticModel, SyntaxNode syntax, out int length) + { + length = 0; + + return + syntax is PropertyDeclarationSyntax propertyDeclaration && + TryGetPropertyGetterExpression(propertyDeclaration) is { } expression && + (IsConstantByteOrCharArrayCreationExpression(semanticModel, expression, values: null, out length) || IsUtf8StringLiteralExpression(expression, out length)); + } + + protected override bool IsConstantByteOrCharArrayCreationSyntax(SemanticModel semanticModel, SyntaxNode syntax, out int length) + { + length = 0; + + return + syntax is ExpressionSyntax expression && + IsConstantByteOrCharArrayCreationExpression(semanticModel, expression, values: null, out length); + } + + internal static ExpressionSyntax? TryGetPropertyGetterExpression(PropertyDeclarationSyntax propertyDeclaration) + { + var expression = propertyDeclaration.ExpressionBody?.Expression; + + if (expression is null && + propertyDeclaration.AccessorList?.Accessors is [var accessor] && + accessor.IsKind(SyntaxKind.GetAccessorDeclaration)) + { + expression = accessor.ExpressionBody?.Expression; + + if (expression is null && + accessor.Body?.Statements is [var statement] && + statement is ReturnStatementSyntax returnStatement) + { + expression = returnStatement.Expression; + } + } + + return expression; + } + + // new char[] { 'a', 'b', 'c' }; + // new[] { 'a', 'b', 'c' }; + // new[] { (byte)'a', (byte)'b', (byte)'c' }; + // "abc".ToCharArray() + // ConstString.ToCharArray() + internal static bool IsConstantByteOrCharArrayCreationExpression(SemanticModel semanticModel, ExpressionSyntax expression, List? values, out int length) + { + length = 0; + + InitializerExpressionSyntax? arrayInitializer = null; + + if (expression is ArrayCreationExpressionSyntax arrayCreation) + { + arrayInitializer = arrayCreation.Initializer; + } + else if (expression is ImplicitArrayCreationExpressionSyntax implicitArrayCreation) + { + arrayInitializer = implicitArrayCreation.Initializer; + } + else if (expression is InvocationExpressionSyntax invocation) + { + if (semanticModel.GetOperation(invocation) is IInvocationOperation invocationOperation && + IsConstantStringToCharArrayInvocation(invocationOperation, out string? value)) + { + values?.AddRange(value); + length = value.Length; + return true; + } + } + + if (arrayInitializer?.Expressions is { } valueExpressions) + { + foreach (var valueExpression in valueExpressions) + { + if (!TryGetByteOrCharLiteral(valueExpression, out char value)) + { + return false; + } + + values?.Add(value); + } + + length = valueExpressions.Count; + return true; + } + + return false; + + // 'a' or (byte)'a' + static bool TryGetByteOrCharLiteral(ExpressionSyntax? expression, out char value) + { + if (expression is not null) + { + if (expression is CastExpressionSyntax cast && + cast.Type is PredefinedTypeSyntax predefinedType && + predefinedType.Keyword.IsKind(SyntaxKind.ByteKeyword)) + { + expression = cast.Expression; + } + + if (expression.IsKind(SyntaxKind.CharacterLiteralExpression) && + expression is LiteralExpressionSyntax characterLiteral && + characterLiteral.Token.Value is char charValue) + { + value = charValue; + return true; + } + } + + value = default; + return false; + } + } + + private static bool IsUtf8StringLiteralExpression(ExpressionSyntax expression, out int length) + { + const SyntaxKind Utf8StringLiteralExpression = (SyntaxKind)8756; + const SyntaxKind Utf8StringLiteralToken = (SyntaxKind)8520; + + if (expression.IsKind(Utf8StringLiteralExpression) && + expression is LiteralExpressionSyntax literal && + literal.Token.IsKind(Utf8StringLiteralToken) && + literal.Token.Value is string value) + { + length = value.Length; + return true; + } + + length = 0; + return false; + } + + protected override bool ArrayFieldUsesAreLikelyReadOnly(SyntaxNode syntax) + { + if (syntax is not VariableDeclaratorSyntax variableDeclarator || + variableDeclarator.Identifier.Value is not string fieldName || + syntax.FirstAncestorOrSelf() is not { } typeDeclaration) + { + return false; + } + + // An optimistic implementation that only looks for simple assignments to the field or its array elements. + foreach (var member in typeDeclaration.Members) + { + bool isCtor = member.IsKind(SyntaxKind.ConstructorDeclaration); + + foreach (var node in member.DescendantNodes()) + { + if (node.IsKind(SyntaxKind.SimpleAssignmentExpression) && + node is AssignmentExpressionSyntax assignment) + { + if (assignment.Left.IsKind(SyntaxKind.ElementAccessExpression)) + { + if (assignment.Left is ElementAccessExpressionSyntax elementAccess && + IsFieldReference(elementAccess.Expression, fieldName)) + { + // s_array[42] = foo; + return false; + } + } + else if (isCtor) + { + if (IsFieldReference(assignment.Left, fieldName)) + { + // s_array = foo; + return false; + } + } + } + } + } + + return true; + + static bool IsFieldReference(ExpressionSyntax expression, string fieldName) => + expression.IsKind(SyntaxKind.IdentifierName) && + expression is IdentifierNameSyntax identifierName && + identifierName.Identifier.Value is string value && + value == fieldName; + } + } +} \ No newline at end of file diff --git a/src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md b/src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md index cb52bd1f99..5a971f9c36 100644 --- a/src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md +++ b/src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md @@ -23,6 +23,7 @@ CA1861 | Performance | Info | AvoidConstArrays, [Documentation](https://learn.mi CA1862 | Performance | Info | RecommendCaseInsensitiveStringComparison, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1862) CA1863 | Performance | Hidden | UseCompositeFormatAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1862) CA1864 | Performance | Info | PreferDictionaryTryAddValueOverGuardedAddAnalyzer, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1864) +CA1870 | Performance | Info | UseSearchValuesAnalyzer, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1870) CA2021 | Reliability | Warning | DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2021) ### Removed Rules diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx index 72017396c9..0964337cc2 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx @@ -1916,6 +1916,18 @@ Widening and user defined conversions are not supported with generic types. Prefer 'Clear' over 'Fill' + + Use a cached 'SearchValues' instance + + + Use a cached 'SearchValues' instance for improved searching performance + + + Using a cached 'SearchValues' instance is more efficient than passing values to 'IndexOfAny'/'ContainsAny' directly. + + + Use 'SearchValues' + Some built-in operators added in .NET 7 behave differently when overflowing than did the corresponding user-defined operators in .NET 6 and earlier versions. Some operators that previously threw in an unchecked context now don't throw unless wrapped within a checked context. Also, some operators that did not previously throw in a checked context now throw unless wrapped in an unchecked context. diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseSearchValues.Fixer.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseSearchValues.Fixer.cs new file mode 100644 index 0000000000..a726a6e403 --- /dev/null +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseSearchValues.Fixer.cs @@ -0,0 +1,374 @@ +// Copyright (c) Microsoft. 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; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Analyzer.Utilities; +using Analyzer.Utilities.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.FindSymbols; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.NetCore.Analyzers.Performance +{ + using static MicrosoftNetCoreAnalyzersResources; + + /// + /// CA1870: + /// + public abstract class UseSearchValuesFixer : CodeFixProvider + { + public sealed override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create(UseSearchValuesAnalyzer.DiagnosticId); + + public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + if (root is null) + { + return; + } + + var node = root.FindNode(context.Span, getInnermostNodeForTie: true); + if (node is null) + { + return; + } + + context.RegisterCodeFix( + CodeAction.Create( + UseSearchValuesCodeFixTitle, + cancellationToken => ConvertToSearchValuesAsync(context.Document, node, cancellationToken), + equivalenceKey: nameof(UseSearchValuesCodeFixTitle)), + context.Diagnostics); + } + + protected abstract ValueTask<(SyntaxNode TypeDeclaration, INamedTypeSymbol? TypeSymbol, bool IsRealType)> GetTypeSymbolAsync(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken); + + protected abstract SyntaxNode ReplaceSearchValuesFieldName(SyntaxNode node); + + protected abstract SyntaxNode GetDeclaratorInitializer(SyntaxNode syntax); + + protected abstract SyntaxNode? TryReplaceArrayCreationWithInlineLiteralExpression(IOperation operation); + + private async Task ConvertToSearchValuesAsync(Document document, SyntaxNode argumentNode, CancellationToken cancellationToken) + { + SemanticModel? semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + DocumentEditor editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + SyntaxGenerator generator = editor.Generator; + + if (semanticModel?.Compilation is not { } compilation || + !compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemBuffersSearchValues, out INamedTypeSymbol? searchValues) || + !compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemBuffersSearchValues1, out INamedTypeSymbol? searchValuesOfT) || + !compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemMemoryExtensions, out INamedTypeSymbol? memoryExtensions) || + semanticModel.GetOperation(argumentNode, cancellationToken) is not { } argument || + GetArgumentOperationAncestorOrSelf(argument) is not { } argumentOperation) + { + return document; + } + + bool isByte = + argumentOperation.Parameter?.Type is INamedTypeSymbol parameterType && + parameterType.TypeArguments is [var typeArgument] && + typeArgument.SpecialType == SpecialType.System_Byte; + + SyntaxNode createArgument = CreateSearchValuesCreateArgument(argumentOperation.Syntax, argumentOperation.Value, out SyntaxNode? memberToRemove, cancellationToken); + + string? removedMemberName = null; + + // If the member we're relacing is not public, and the argument to IndexOfAny was its only use, remove it. + if (memberToRemove is not null && + semanticModel.GetDeclaredSymbol(memberToRemove, cancellationToken) is { } symbolToRemove && + symbolToRemove.DeclaredAccessibility is Accessibility.NotApplicable or Accessibility.Private && + !symbolToRemove.IsImplementationOfAnyInterfaceMember() && + symbolToRemove.Locations.Length == 1) + { + var refs = await SymbolFinder.FindReferencesAsync(symbolToRemove, document.Project.Solution, cancellationToken).ConfigureAwait(false); + var locations = refs.SelectMany(r => r.Locations); + var documentLocations = locations.Select(loc => (loc.Document.FilePath, loc.Location.SourceSpan)); + + if (documentLocations.Distinct().Count() == 1) + { + // A single location in a single document. + editor.RemoveNode(memberToRemove); + removedMemberName = symbolToRemove.Name; + } + } + + string defaultSearchValuesFieldName = GetSearchValuesFieldName(argumentOperation.Value, isByte, removedOriginalMember: removedMemberName is not null); + + string fieldName = defaultSearchValuesFieldName; + + (var typeDeclaration, var typeSymbol, bool isRealType) = await GetTypeSymbolAsync(semanticModel, argumentNode, cancellationToken).ConfigureAwait(false); + + // Find a unique name for the field that does not conflict with other members in scope. + if (typeSymbol is not null && fieldName != removedMemberName) + { + var members = GetAllMemberNamesInScope(typeSymbol).ToArray(); + int memberCount = 1; + while (members.Contains(fieldName, StringComparer.Ordinal)) + { + fieldName = $"{defaultSearchValuesFieldName}{memberCount++}"; + } + } + + // private static readonly SearchValues s_myValues = SearchValues.Create(argument); + var newField = generator.FieldDeclaration( + fieldName, + generator.TypeExpression(searchValuesOfT.Construct(compilation.GetSpecialType(isByte ? SpecialType.System_Byte : SpecialType.System_Char))), + Accessibility.Private, + DeclarationModifiers.Static.WithIsReadOnly(true), + generator.InvocationExpression( + generator.MemberAccessExpression(generator.TypeExpressionForStaticMemberAccess(searchValues), "Create"), + createArgument)); + + // Allow the user to pick a different name for the method. + newField = ReplaceSearchValuesFieldName(newField); + + // foo.IndexOfAny(argument) => foo.IndexOfAny(s_myValues) + editor.ReplaceNode(argumentNode, generator.IdentifierName(fieldName)); + + if (isRealType) + { + // Insert the new field at the top of the parent type. + editor.InsertMembers(typeDeclaration, 0, new[] { newField }); + } + else + { + // We are in the 'Program' class of a top-level statements file. + // Create a new partial Program class with the new field. + editor.AddMember(typeDeclaration, generator.ClassDeclaration("Program", modifiers: DeclarationModifiers.Partial, members: new[] { newField })); + } + + // If this was a string IndexOfAny call, we must also insert an AsSpan call. + if (!isByte && + argumentOperation.Parent is IInvocationOperation indexOfAnyOperation && + indexOfAnyOperation.Instance?.Syntax is { } stringInstance) + { + // foo.IndexOfAny => foo.AsSpan().IndexOfAny + editor.ReplaceNode(stringInstance, generator.InvocationExpression(generator.MemberAccessExpression(stringInstance, "AsSpan"))); + + // We are now using the MemoryExtensions.AsSpan() extension method. Make sure it's in scope. + ImportSystemNamespaceIfNeeded(editor, memoryExtensions, stringInstance); + } + + return editor.GetChangedDocument(); + } + + private static void ImportSystemNamespaceIfNeeded(DocumentEditor editor, INamedTypeSymbol memoryExtensions, SyntaxNode node) + { + var symbols = editor.SemanticModel.LookupNamespacesAndTypes(node.SpanStart, name: nameof(MemoryExtensions)); + + if (!symbols.Contains(memoryExtensions, SymbolEqualityComparer.Default)) + { + SyntaxNode withoutSystemImport = editor.GetChangedRoot(); + SyntaxNode systemNamespaceImportStatement = editor.Generator.NamespaceImportDeclaration(nameof(System)); + SyntaxNode withSystemImport = editor.Generator.AddNamespaceImports(withoutSystemImport, systemNamespaceImportStatement); + editor.ReplaceNode(editor.OriginalRoot, withSystemImport); + } + } + + private static IArgumentOperation? GetArgumentOperationAncestorOrSelf(IOperation operation) => + (operation as IArgumentOperation) ?? + operation.GetAncestor(OperationKind.Argument); + + private static IEnumerable GetAllMemberNamesInScope(ITypeSymbol? symbol) + { + while (symbol != null) + { + foreach (ISymbol member in symbol.GetMembers()) + { + yield return member.Name; + } + + symbol = symbol.BaseType; + } + } + + private static string GetSearchValuesFieldName(IOperation argument, bool isByte, bool removedOriginalMember) + { + if (argument is IConversionOperation conversion) + { + if (TryGetNameFromLocalOrFieldReference(conversion.Operand, out string? name)) + { + return name; + } + else if (conversion.Operand is IInvocationOperation invocation) + { + if (TryGetNameFromLocalOrFieldReference(invocation.Instance, out name)) + { + return name; + } + } + } + else if (TryGetNameFromLocalOrFieldReference(argument, out string? name)) + { + return name; + } + else if (argument is IPropertyReferenceOperation propertyReference) + { + return CreateFromExistingName(propertyReference.Property.Name); + } + else if (argument is IInvocationOperation invocation) + { + if (TryGetNameFromLocalOrFieldReference(invocation.Instance, out name)) + { + return name; + } + } + + return isByte ? "s_myBytes" : "s_myChars"; + + bool TryGetNameFromLocalOrFieldReference(IOperation? argument, [NotNullWhen(true)] out string? name) + { + if (argument is ILocalReferenceOperation localReference) + { + name = CreateFromExistingName(localReference.Local.Name); + return true; + } + else if (argument is IFieldReferenceOperation fieldReference) + { + name = CreateFromExistingName(fieldReference.Field.Name); + return true; + } + + name = null; + return false; + } + + string CreateFromExistingName(string name) + { + if (!name.StartsWith("s_", StringComparison.OrdinalIgnoreCase)) + { + if (name.Length >= 2 && IsAsciiLetterUpper(name[0]) && !IsAsciiLetterUpper(name[1])) + { + name = $"{char.ToLowerInvariant(name[0])}{name[1..]}"; + } + + return $"s_{name.TrimStart('_')}"; + } + + return removedOriginalMember + ? name + : $"{name}SearchValues"; + + static bool IsAsciiLetterUpper(char c) => c is >= 'A' and <= 'Z'; + } + } + + private SyntaxNode CreateSearchValuesCreateArgument(SyntaxNode originalSyntax, IOperation argument, out SyntaxNode? memberToRemove, CancellationToken cancellationToken) + { + SyntaxNode createArgument = CreateSearchValuesCreateArgumentCore(originalSyntax, argument, out memberToRemove, cancellationToken); + + // If the argument is an inline array creation, we can transform it into a string literal expression instead. + if (argument.SemanticModel?.GetOperation(createArgument, cancellationToken) is { } newOperation) + { + if (newOperation is IArgumentOperation argumentOperation) + { + newOperation = argumentOperation.Value; + } + + if (TryReplaceArrayCreationWithInlineLiteralExpression(newOperation) is { } literalExpression) + { + return literalExpression; + } + } + + return createArgument; + } + + private SyntaxNode CreateSearchValuesCreateArgumentCore(SyntaxNode originalSyntax, IOperation argument, out SyntaxNode? memberToRemove, CancellationToken cancellationToken) + { + if (argument is IConversionOperation conversion) + { + argument = conversion.Operand; + } + + if (argument is IPropertyReferenceOperation propertyReference) + { + if (!propertyReference.Property.IsStatic) + { + // Can't access an instance property from a field initializer. + memberToRemove = GetDeclarator(propertyReference.Property); + return GetDeclaratorInitializer(memberToRemove); + } + } + else if (TryGetArgumentFromLocalOrFieldReference(argument, out SyntaxNode? createArgument, out memberToRemove)) + { + return createArgument; + } + else if (TryGetArgumentFromStringToCharArray(argument, out createArgument, out memberToRemove)) + { + return createArgument; + } + + // Use the original syntax (e.g. string literal, inline array creation, static property reference ...) + memberToRemove = null; + return originalSyntax; + + bool TryGetArgumentFromStringToCharArray(IOperation operation, [NotNullWhen(true)] out SyntaxNode? createArgument, out SyntaxNode? memberToRemove) + { + if (operation is IInvocationOperation invocation && + invocation.Instance is { } stringInstance) + { + Debug.Assert(invocation.TargetMethod.Name == nameof(string.ToCharArray)); + + if (!TryGetArgumentFromLocalOrFieldReference(stringInstance, out createArgument, out memberToRemove)) + { + // This is a string.ToCharArray call, but the string instance is not something we can refer to by name. + // e.g. for '"foo".ToCharArray()', we want to emit '"foo"'. + createArgument = stringInstance.Syntax; + } + + return true; + } + + createArgument = null; + memberToRemove = null; + return false; + } + + bool TryGetArgumentFromLocalOrFieldReference(IOperation operation, [NotNullWhen(true)] out SyntaxNode? createArgument, out SyntaxNode? memberToRemove) + { + if (operation is ILocalReferenceOperation localReference) + { + // Local string literal would be out of scope in the field declaration. + memberToRemove = GetDeclarator(localReference.Local); + createArgument = GetDeclaratorInitializer(memberToRemove); + return true; + } + else if (operation is IFieldReferenceOperation fieldReference) + { + if (!fieldReference.ConstantValue.HasValue) + { + // If we were to use the field reference directly, we risk initializing the SearchValues field to an empty + // instance depending on field declaration order. + memberToRemove = GetDeclarator(fieldReference.Field); + createArgument = GetDeclaratorInitializer(memberToRemove); + return true; + } + } + + createArgument = null; + memberToRemove = null; + return false; + } + + SyntaxNode GetDeclarator(ISymbol symbol) + { + Debug.Assert(symbol.DeclaringSyntaxReferences.Length == 1); + + return symbol.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken); + } + } + } +} \ No newline at end of file diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseSearchValues.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseSearchValues.cs new file mode 100644 index 0000000000..3df26e55fe --- /dev/null +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseSearchValues.cs @@ -0,0 +1,314 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Immutable; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Analyzer.Utilities; +using Analyzer.Utilities.Extensions; +using Analyzer.Utilities.Lightup; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.NetCore.Analyzers.Performance +{ + using static MicrosoftNetCoreAnalyzersResources; + + /// + /// CA1870: + /// + public abstract class UseSearchValuesAnalyzer : DiagnosticAnalyzer + { + internal const string DiagnosticId = "CA1870"; + + internal static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create( + DiagnosticId, + CreateLocalizableResourceString(nameof(UseSearchValuesTitle)), + CreateLocalizableResourceString(nameof(UseSearchValuesMessage)), + DiagnosticCategory.Performance, + RuleLevel.IdeSuggestion, + description: CreateLocalizableResourceString(nameof(UseSearchValuesDescription)), + isPortedFxCopRule: false, + isDataflowRule: false); + + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(Rule); + + protected abstract bool IsConstantByteOrCharArrayVariableDeclaratorSyntax(SemanticModel semanticModel, SyntaxNode syntax, out int length); + + protected abstract bool IsConstantByteOrCharReadOnlySpanPropertyDeclarationSyntax(SemanticModel semanticModel, SyntaxNode syntax, out int length); + + protected abstract bool IsConstantByteOrCharArrayCreationSyntax(SemanticModel semanticModel, SyntaxNode syntax, out int length); + + protected abstract bool ArrayFieldUsesAreLikelyReadOnly(SyntaxNode syntax); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(context => + { + if (!context.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemBuffersSearchValues, out _) || + !context.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemReadOnlySpan1, out var readOnlySpanType)) + { + return; + } + + var indexOfAnyMethods = GetIndexOfAnyMethods(context.Compilation); + + context.RegisterOperationAction(context => AnalyzeInvocation(context, indexOfAnyMethods, readOnlySpanType), OperationKind.Invocation); + }); + } + + private void AnalyzeInvocation(OperationAnalysisContext context, ImmutableHashSet indexOfAnyMethodsToDetect, INamedTypeSymbol readOnlySpanType) + { + var invocation = (IInvocationOperation)context.Operation; + + if (!indexOfAnyMethodsToDetect.Contains(invocation.TargetMethod)) + { + return; + } + + Debug.Assert(invocation.Arguments.Length is 1 or 2); + IArgumentOperation valuesArgument = invocation.Arguments[^1]; + + bool isStringIndexOfAny = invocation.TargetMethod.ContainingType.SpecialType == SpecialType.System_String; + + if (isStringIndexOfAny && invocation.Parent is IConditionalAccessOperation) + { + // We don't flag uses like "string?.IndexOfAny(char[])" + // as it's not trivial to rewrite to the span-based variant. + return; + } + + if (isStringIndexOfAny + ? AreConstantValuesWorthReplacingForStringIndexOfAny(valuesArgument.Value) + : AreConstantValuesWorthReplacing(valuesArgument.Value, readOnlySpanType)) + { + context.ReportDiagnostic(valuesArgument.CreateDiagnostic(Rule)); + } + } + + private static ImmutableHashSet GetIndexOfAnyMethods(Compilation compilation) + { + var methods = ImmutableHashSet.CreateBuilder(); + + var stringType = compilation.GetSpecialType(SpecialType.System_String); + + // string.{Last}IndexOfAny(char[]) + // Overloads that accept 'startOffset' or 'count' are excluded as they can't be trivially converted to AsSpan. + foreach (var member in stringType.GetMembers()) + { + if (member is IMethodSymbol method && + method.Name is "IndexOfAny" or "LastIndexOfAny" && + method.Parameters.Length == 1) + { + methods.Add(method); + } + } + + // {ReadOnly}Span.{Last}IndexOfAny{Except}(ReadOnlySpan) and ContainsAny{Except}(ReadOnlySpan) + if (compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemMemoryExtensions, out var memoryExtensionsType) && + compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemSpan1, out var spanType) && + compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemReadOnlySpan1, out var readOnlySpanType)) + { + foreach (var member in memoryExtensionsType.GetMembers()) + { + if (member is not IMethodSymbol method || + method.Parameters.Length != 2 || + method.TypeParameters.Length != 1 || + method.Name is not ("IndexOfAny" or "IndexOfAnyExcept" or "LastIndexOfAny" or "LastIndexOfAnyExcept" or "ContainsAny" or "ContainsAnyExcept")) + { + continue; + } + + var firstParameterType = method.Parameters[0].Type.OriginalDefinition; + var secondParameterType = method.Parameters[1].Type.OriginalDefinition; + + if (!SymbolEqualityComparer.Default.Equals(firstParameterType, spanType) && + !SymbolEqualityComparer.Default.Equals(firstParameterType, readOnlySpanType)) + { + continue; + } + + if (!SymbolEqualityComparer.Default.Equals(secondParameterType, readOnlySpanType)) + { + continue; + } + + // All of these methods are generic for any T, but SearchValues only supports byte/char variants. + methods.Add(method.Construct(compilation.GetSpecialType(SpecialType.System_Byte))); + methods.Add(method.Construct(compilation.GetSpecialType(SpecialType.System_Char))); + } + } + + return methods.ToImmutable(); + } + + // It's not always worth going through SearchValues if there are very few values used + // such that they will already be using dedicated vectorized paths. + private const int MinLengthWorthReplacing = 6; + + private bool AreConstantValuesWorthReplacingForStringIndexOfAny(IOperation argument) + { + if (IsArrayCreationOrFieldReferenceOrToCharArrayInvocation(argument, out int length)) + { + // If the existing expression will allocate on every call ("abc".ToCharArray() or new[] { ... }), + // we may want to replace it even if there are very few values. + return + length >= MinLengthWorthReplacing || + argument is not IFieldReferenceOperation; + } + + return false; + } + + private bool AreConstantValuesWorthReplacing(IOperation argument, INamedTypeSymbol readOnlySpanType) + { + if (argument is IConversionOperation conversion) + { + if (IsConstantStringLiteralOrReference(conversion.Operand, out string? value)) + { + // text.IndexOfAny("abc") + // or + // const string ValuesLocalOrField = "abc"; + // text.IndexOfAny(ValuesLocalOrField) + return value.Length >= MinLengthWorthReplacing; + } + else if (IsArrayCreationOrFieldReferenceOrToCharArrayInvocation(conversion.Operand, out int length)) + { + // If the existing expression will allocate on every call ("abc".ToCharArray()), + // we may want to replace it even if there are very few values. + return + length >= MinLengthWorthReplacing || + conversion.Operand is IInvocationOperation; + } + } + else if (argument.Kind == OperationKindEx.Utf8String) + { + // text.IndexOfAny("abc"u8) + return + IUtf8StringOperationWrapper.IsInstance(argument) && + IUtf8StringOperationWrapper.FromOperation(argument).Value.Length >= MinLengthWorthReplacing; + } + else if (argument is IPropertyReferenceOperation propertyReference) + { + // ReadOnlySpan Values => "abc"u8; + // ReadOnlySpan Values => new byte[] { (byte)'a', (byte)'b', (byte)'c' }; + // ReadOnlySpan Values => new char[] { 'a', 'b', 'c' }; + // ReadOnlySpan Values => "abc".ToCharArray(); + // ReadOnlySpan Values => ConstString.ToCharArray(); + // text.IndexOfAny(Values) + return + propertyReference.Member is IPropertySymbol property && + property.IsReadOnly && + IsByteOrCharReadOnlySpan(property.Type, readOnlySpanType) && + property.DeclaringSyntaxReferences is [var declaringSyntaxReference] && + declaringSyntaxReference.GetSyntax() is { } syntax && + IsConstantByteOrCharReadOnlySpanPropertyDeclarationSyntax(propertyReference.SemanticModel!, syntax, out int length) && + length >= MinLengthWorthReplacing; + } + + return false; + } + + private bool IsArrayCreationOrFieldReferenceOrToCharArrayInvocation(IOperation argument, out int length) + { + if (argument is IArrayCreationOperation arrayCreation) + { + // text.IndexOfAny(new[] { 'a', 'b', 'c' }) + return IsConstantByteOrCharSZArrayCreation(arrayCreation, out length); + } + else if (argument is IFieldReferenceOperation fieldReference) + { + // readonly char[] Values = new char[] { 'a', 'b', 'c' }; + // readonly char[] Values = "abc".ToCharArray(); + // readonly char[] Values = ConstString.ToCharArray(); + // text.IndexOfAny(Values) + return IsConstantByteOrCharSZArrayFieldReference(fieldReference, out length); + } + else if (argument is IInvocationOperation invocation) + { + // text.IndexOfAny("abc".ToCharArray()) + // text.IndexOfAny(StringConst.ToCharArray()) + if (IsConstantStringToCharArrayInvocation(invocation, out string? value)) + { + length = value.Length; + return true; + } + } + + length = 0; + return false; + } + + private static bool IsByteOrCharSZArray(ITypeSymbol? type) => + type is IArrayTypeSymbol array && + array.IsSZArray && + array.ElementType.SpecialType is SpecialType.System_Byte or SpecialType.System_Char; + + private static bool IsByteOrCharReadOnlySpan(ISymbol symbol, INamedTypeSymbol readOnlySpanType) => + symbol is INamedTypeSymbol namedType && + SymbolEqualityComparer.Default.Equals(namedType.OriginalDefinition, readOnlySpanType) && + namedType.TypeArguments is [var typeArgument] && + typeArgument.SpecialType is SpecialType.System_Byte or SpecialType.System_Char; + + // text.IndexOfAny("abc".ToCharArray()) + // text.IndexOfAny(StringConst.ToCharArray()) + internal static bool IsConstantStringToCharArrayInvocation(IInvocationOperation invocation, [NotNullWhen(true)] out string? value) + { + if (invocation.TargetMethod.ContainingType.SpecialType == SpecialType.System_String && + invocation.TargetMethod.Name == nameof(string.ToCharArray) && + invocation.Instance is { } stringInstance && + IsConstantStringLiteralOrReference(stringInstance, out value)) + { + return true; + } + + value = null; + return false; + } + + private bool IsConstantByteOrCharSZArrayFieldReference(IFieldReferenceOperation fieldReference, out int length) + { + if (fieldReference.Field is { } field && + field.IsReadOnly && + field.DeclaredAccessibility is Accessibility.NotApplicable or Accessibility.Private && + IsByteOrCharSZArray(field.Type) && + field.DeclaringSyntaxReferences is [var declaringSyntaxReference] && + declaringSyntaxReference.GetSyntax() is { } syntax && + IsConstantByteOrCharArrayVariableDeclaratorSyntax(fieldReference.SemanticModel!, syntax, out length) && + ArrayFieldUsesAreLikelyReadOnly(syntax)) + { + return true; + } + + length = 0; + return false; + } + + private bool IsConstantByteOrCharSZArrayCreation(IArrayCreationOperation arrayCreation, out int length) + { + length = 0; + + return + IsByteOrCharSZArray(arrayCreation.Type) && + IsConstantByteOrCharArrayCreationSyntax(arrayCreation.SemanticModel!, arrayCreation.Syntax, out length); + } + + private static bool IsConstantStringLiteralOrReference(IOperation operation, [NotNullWhen(true)] out string? value) + { + if (operation.Type is { SpecialType: SpecialType.System_String } && + operation.ConstantValue.HasValue && + operation is ILiteralOperation or IFieldReferenceOperation or ILocalReferenceOperation && + operation.ConstantValue.Value is string stringValue) + { + value = stringValue; + return true; + } + + value = null; + return false; + } + } +} \ No newline at end of file diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf index 43d310dde9..81d742a391 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf @@ -2988,6 +2988,26 @@ Obecné přetypování (IL unbox.any) používané sekvencí vrácenou metodou E Velikost klíče algoritmu asymetrického šifrování {0} je menší než 2048. Použijte radši algoritmus RSA s velikostí klíče alespoň 2048, ECDH nebo ECDSA. + + Use 'SearchValues' + Use 'SearchValues' + + + + Using a cached 'SearchValues' instance is more efficient than passing values to 'IndexOfAny'/'ContainsAny' directly. + Using a cached 'SearchValues' instance is more efficient than passing values to 'IndexOfAny'/'ContainsAny' directly. + + + + Use a cached 'SearchValues' instance for improved searching performance + Use a cached 'SearchValues' instance for improved searching performance + + + + Use a cached 'SearchValues' instance + Use a cached 'SearchValues' instance + + Applications available over HTTPS must use secure cookies. Aplikace, které jsou k dispozici přes HTTPS, musí používat zabezpečené soubory cookie. diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf index ff3b8f8b1c..2dc4e7e86a 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf @@ -2988,6 +2988,26 @@ Erweiterungen und benutzerdefinierte Konvertierungen werden bei generischen Type Die Schlüsselgröße des asymmetrischen Verschlüsselungsalgorithmus "{0}" beträgt weniger als 2048. Wechseln Sie stattdessen zu einer RSA-Verschlüsselung mit ECDH- oder ECDSA-Algorithmus mit einer Schlüsselgröße von mindestens 2048. + + Use 'SearchValues' + Use 'SearchValues' + + + + Using a cached 'SearchValues' instance is more efficient than passing values to 'IndexOfAny'/'ContainsAny' directly. + Using a cached 'SearchValues' instance is more efficient than passing values to 'IndexOfAny'/'ContainsAny' directly. + + + + Use a cached 'SearchValues' instance for improved searching performance + Use a cached 'SearchValues' instance for improved searching performance + + + + Use a cached 'SearchValues' instance + Use a cached 'SearchValues' instance + + Applications available over HTTPS must use secure cookies. Anwendungen, die über HTTPS verfügbar sind, müssen sichere Cookies verwenden. diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf index 913fb8f2e8..894c5e9d08 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf @@ -2988,6 +2988,26 @@ La ampliación y las conversiones definidas por el usuario no se admiten con tip El tamaño de clave del algoritmo de cifrado asimétrico {0} es inferior a 2048. Cambie a un algoritmo de ECDSA o ECDH con RSA que tenga un tamaño de clave mínimo de 2048. + + Use 'SearchValues' + Use 'SearchValues' + + + + Using a cached 'SearchValues' instance is more efficient than passing values to 'IndexOfAny'/'ContainsAny' directly. + Using a cached 'SearchValues' instance is more efficient than passing values to 'IndexOfAny'/'ContainsAny' directly. + + + + Use a cached 'SearchValues' instance for improved searching performance + Use a cached 'SearchValues' instance for improved searching performance + + + + Use a cached 'SearchValues' instance + Use a cached 'SearchValues' instance + + Applications available over HTTPS must use secure cookies. Las aplicaciones disponibles a través de HTTPS deben usar cookies seguras. diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.fr.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.fr.xlf index 51cbcedeef..f122eb2790 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.fr.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.fr.xlf @@ -2988,6 +2988,26 @@ Les conversions étendues et définies par l’utilisateur ne sont pas prises en La taille de clé de l'algorithme de chiffrement asymétrique {0} est inférieure à 2 048 bits. Passez plutôt à un algorithme RSA avec une taille de clé d'au moins 2 048 bits, à un algorithme ECDH ou à un algorithme ECDSA. + + Use 'SearchValues' + Use 'SearchValues' + + + + Using a cached 'SearchValues' instance is more efficient than passing values to 'IndexOfAny'/'ContainsAny' directly. + Using a cached 'SearchValues' instance is more efficient than passing values to 'IndexOfAny'/'ContainsAny' directly. + + + + Use a cached 'SearchValues' instance for improved searching performance + Use a cached 'SearchValues' instance for improved searching performance + + + + Use a cached 'SearchValues' instance + Use a cached 'SearchValues' instance + + Applications available over HTTPS must use secure cookies. Les applications disponibles via HTTPS doivent utiliser des cookies sécurisés. diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.it.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.it.xlf index e18e3a4716..72b1dc2209 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.it.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.it.xlf @@ -2988,6 +2988,26 @@ L'ampliamento e le conversioni definite dall'utente non sono supportate con tipi La dimensione di chiave dell'algoritmo di crittografia asimmetrica {0} è minore di 2048. Passare a un algoritmo RSA con dimensione di chiave minima pari a 2048 oppure a un algoritmo ECDH o ECDSA. + + Use 'SearchValues' + Use 'SearchValues' + + + + Using a cached 'SearchValues' instance is more efficient than passing values to 'IndexOfAny'/'ContainsAny' directly. + Using a cached 'SearchValues' instance is more efficient than passing values to 'IndexOfAny'/'ContainsAny' directly. + + + + Use a cached 'SearchValues' instance for improved searching performance + Use a cached 'SearchValues' instance for improved searching performance + + + + Use a cached 'SearchValues' instance + Use a cached 'SearchValues' instance + + Applications available over HTTPS must use secure cookies. Le applicazioni disponibili tramite HTTPS devono usare cookie protetti. diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ja.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ja.xlf index 6e5a108de4..ac41f025b8 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ja.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ja.xlf @@ -2988,6 +2988,26 @@ Enumerable.OfType<T> で使用されるジェネリック型チェック ( 非対称暗号化アルゴリズム {0} のキー サイズが 2048 未満です。キー サイズが少なくとも 2048 の RSA、ECDH、または ECDSA アルゴリズムに切り替えてください。 + + Use 'SearchValues' + Use 'SearchValues' + + + + Using a cached 'SearchValues' instance is more efficient than passing values to 'IndexOfAny'/'ContainsAny' directly. + Using a cached 'SearchValues' instance is more efficient than passing values to 'IndexOfAny'/'ContainsAny' directly. + + + + Use a cached 'SearchValues' instance for improved searching performance + Use a cached 'SearchValues' instance for improved searching performance + + + + Use a cached 'SearchValues' instance + Use a cached 'SearchValues' instance + + Applications available over HTTPS must use secure cookies. HTTPS 経由で使用できるアプリケーションは、セキュリティで保護された Cookie を使用する必要があります。 diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ko.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ko.xlf index 2cb8927d8b..18e04a2d1f 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ko.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ko.xlf @@ -2988,6 +2988,26 @@ Enumerable.OfType<T>에서 사용하는 제네릭 형식 검사(C# 'is' 비대칭 암호화 알고리즘 {0}의 키 크기가 2048보다 작습니다. 최소 2048 키 크기, ECDH 또는 ECDSA 알고리즘이 포함된 RSA로 전환하세요. + + Use 'SearchValues' + Use 'SearchValues' + + + + Using a cached 'SearchValues' instance is more efficient than passing values to 'IndexOfAny'/'ContainsAny' directly. + Using a cached 'SearchValues' instance is more efficient than passing values to 'IndexOfAny'/'ContainsAny' directly. + + + + Use a cached 'SearchValues' instance for improved searching performance + Use a cached 'SearchValues' instance for improved searching performance + + + + Use a cached 'SearchValues' instance + Use a cached 'SearchValues' instance + + Applications available over HTTPS must use secure cookies. HTTPS를 통해 사용할 수 있는 애플리케이션은 보안 쿠키를 사용해야 합니다. diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pl.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pl.xlf index b5a80ebbf3..d7de43ea2b 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pl.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pl.xlf @@ -2988,6 +2988,26 @@ Konwersje poszerzane i zdefiniowane przez użytkownika nie są obsługiwane w pr Rozmiar klucza algorytmu szyfrowania asymetrycznego {0} jest mniejszy niż 2048. Przełącz się na algorytm RSA z kluczem o rozmiarze co najmniej 2048, algorytm ECDH lub algorytm ECDSA. + + Use 'SearchValues' + Use 'SearchValues' + + + + Using a cached 'SearchValues' instance is more efficient than passing values to 'IndexOfAny'/'ContainsAny' directly. + Using a cached 'SearchValues' instance is more efficient than passing values to 'IndexOfAny'/'ContainsAny' directly. + + + + Use a cached 'SearchValues' instance for improved searching performance + Use a cached 'SearchValues' instance for improved searching performance + + + + Use a cached 'SearchValues' instance + Use a cached 'SearchValues' instance + + Applications available over HTTPS must use secure cookies. Aplikacje dostępne za pośrednictwem protokołu HTTPS muszą korzystać z bezpiecznych plików cookie. diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pt-BR.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pt-BR.xlf index 18278dd817..f10d120406 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pt-BR.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pt-BR.xlf @@ -2988,6 +2988,26 @@ Ampliação e conversões definidas pelo usuário não são compatíveis com tip O tamanho de chave do algoritmo de criptografia assimétrica {0} é menor que 2048. Alterne para um RSA com um tamanho de chave de pelo menos 2048 ou para um algoritmo ECDH ou ECDSA. + + Use 'SearchValues' + Use 'SearchValues' + + + + Using a cached 'SearchValues' instance is more efficient than passing values to 'IndexOfAny'/'ContainsAny' directly. + Using a cached 'SearchValues' instance is more efficient than passing values to 'IndexOfAny'/'ContainsAny' directly. + + + + Use a cached 'SearchValues' instance for improved searching performance + Use a cached 'SearchValues' instance for improved searching performance + + + + Use a cached 'SearchValues' instance + Use a cached 'SearchValues' instance + + Applications available over HTTPS must use secure cookies. Os aplicativos disponíveis via HTTPS devem usar cookies seguros. diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ru.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ru.xlf index 333c09d2c6..ab490d3276 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ru.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ru.xlf @@ -2988,6 +2988,26 @@ Widening and user defined conversions are not supported with generic types.Размер ключа для алгоритма асимметричного шифрования {0} меньше 2048 бит. Перейдите на алгоритм RSA с размером ключа не менее 2048 бит или на алгоритмы ECDH или ECDSA. + + Use 'SearchValues' + Use 'SearchValues' + + + + Using a cached 'SearchValues' instance is more efficient than passing values to 'IndexOfAny'/'ContainsAny' directly. + Using a cached 'SearchValues' instance is more efficient than passing values to 'IndexOfAny'/'ContainsAny' directly. + + + + Use a cached 'SearchValues' instance for improved searching performance + Use a cached 'SearchValues' instance for improved searching performance + + + + Use a cached 'SearchValues' instance + Use a cached 'SearchValues' instance + + Applications available over HTTPS must use secure cookies. Приложения, доступные через HTTPS, должны использовать защищенные файлы cookie. diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.tr.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.tr.xlf index f4700b6a05..6cde4d1bf0 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.tr.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.tr.xlf @@ -2988,6 +2988,26 @@ Genel türlerde genişletme ve kullanıcı tanımlı dönüştürmeler desteklen Asimetrik şifreleme algoritması {0} anahtar boyutu 2048'den az. Bunun yerine en az 2048 anahtar boyutuna, ECDH veya ECDSA algoritmasına sahip bir RSA'ya geçiş yapın. + + Use 'SearchValues' + Use 'SearchValues' + + + + Using a cached 'SearchValues' instance is more efficient than passing values to 'IndexOfAny'/'ContainsAny' directly. + Using a cached 'SearchValues' instance is more efficient than passing values to 'IndexOfAny'/'ContainsAny' directly. + + + + Use a cached 'SearchValues' instance for improved searching performance + Use a cached 'SearchValues' instance for improved searching performance + + + + Use a cached 'SearchValues' instance + Use a cached 'SearchValues' instance + + Applications available over HTTPS must use secure cookies. HTTPS üzerinden kullanılabilen uygulamalar güvenli tanımlama bilgileri kullanmalıdır. diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hans.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hans.xlf index b9fc322c91..1eaaacb9e3 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hans.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hans.xlf @@ -2988,6 +2988,26 @@ Enumerable.OfType<T> 使用的泛型类型检查 (C# 'is' operator/IL 'isi 非对称加密算法 {0} 的密钥大小小于 2048 位。请转而切换到至少具有 2048 位密钥大小的 RSA、ECDH 或者 ECDSA 算法。 + + Use 'SearchValues' + Use 'SearchValues' + + + + Using a cached 'SearchValues' instance is more efficient than passing values to 'IndexOfAny'/'ContainsAny' directly. + Using a cached 'SearchValues' instance is more efficient than passing values to 'IndexOfAny'/'ContainsAny' directly. + + + + Use a cached 'SearchValues' instance for improved searching performance + Use a cached 'SearchValues' instance for improved searching performance + + + + Use a cached 'SearchValues' instance + Use a cached 'SearchValues' instance + + Applications available over HTTPS must use secure cookies. 通过 HTTPS 提供的应用程序必须使用安全 Cookie。 diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hant.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hant.xlf index 09dc41c4fa..0a7295d7ec 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hant.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hant.xlf @@ -2988,6 +2988,26 @@ Enumerable.OfType<T> 使用的一般型別檢查 (C# 'is' operator/IL 'isi 非對稱式加密演算法 {0} 的金鑰大小小於 2048。請改為切換成至少有 2048 金鑰大小的 RSA、ECDH 或 ECDSA 演算法。 + + Use 'SearchValues' + Use 'SearchValues' + + + + Using a cached 'SearchValues' instance is more efficient than passing values to 'IndexOfAny'/'ContainsAny' directly. + Using a cached 'SearchValues' instance is more efficient than passing values to 'IndexOfAny'/'ContainsAny' directly. + + + + Use a cached 'SearchValues' instance for improved searching performance + Use a cached 'SearchValues' instance for improved searching performance + + + + Use a cached 'SearchValues' instance + Use a cached 'SearchValues' instance + + Applications available over HTTPS must use secure cookies. 透過 HTTPS 使用的應用程式必須使用安全 Cookie。 diff --git a/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md b/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md index 3679a4482a..b4303098e1 100644 --- a/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md +++ b/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md @@ -1800,6 +1800,18 @@ Avoid creating a new 'JsonSerializerOptions' instance for every serialization op |CodeFix|False| --- +## [CA1870](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1870): Use a cached 'SearchValues' instance + +Using a cached 'SearchValues' instance is more efficient than passing values to 'IndexOfAny'/'ContainsAny' directly. + +|Item|Value| +|-|-| +|Category|Performance| +|Enabled|True| +|Severity|Info| +|CodeFix|True| +--- + ## [CA2000](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2000): Dispose objects before losing scope If a disposable object is not explicitly disposed before all references to it are out of scope, the object will be disposed at some indeterminate time when the garbage collector runs the finalizer of the object. Because an exceptional event might occur that will prevent the finalizer of the object from running, the object should be explicitly disposed instead. diff --git a/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif b/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif index a8749e9830..f914a3acea 100644 --- a/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif +++ b/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif @@ -399,6 +399,25 @@ ] } }, + "CA1870": { + "id": "CA1870", + "shortDescription": "Use a cached 'SearchValues' instance", + "fullDescription": "Using a cached 'SearchValues' instance is more efficient than passing values to 'IndexOfAny'/'ContainsAny' directly.", + "defaultLevel": "note", + "helpUri": "https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1870", + "properties": { + "category": "Performance", + "isEnabledByDefault": true, + "typeName": "CSharpUseSearchValuesAnalyzer", + "languages": [ + "C#" + ], + "tags": [ + "Telemetry", + "EnabledRuleInAggressiveMode" + ] + } + }, "CA2014": { "id": "CA2014", "shortDescription": "Do not use stackalloc in loops", diff --git a/src/NetAnalyzers/RulesMissingDocumentation.md b/src/NetAnalyzers/RulesMissingDocumentation.md index 42df726b08..b4cb6c526d 100644 --- a/src/NetAnalyzers/RulesMissingDocumentation.md +++ b/src/NetAnalyzers/RulesMissingDocumentation.md @@ -13,5 +13,6 @@ CA1863 | | Use char overload | CA1866 | | Use char overload | CA1867 | | Use char overload | +CA1870 | | Use a cached 'SearchValues' instance | CA2021 | | Do not call Enumerable.Cast\ or Enumerable.OfType\ with incompatible types | CA2261 | | Do not use ConfigureAwaitOptions.SuppressThrowing with Task\ | diff --git a/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseSearchValuesTests.cs b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseSearchValuesTests.cs new file mode 100644 index 0000000000..7dc9d17b1a --- /dev/null +++ b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseSearchValuesTests.cs @@ -0,0 +1,1076 @@ +// Copyright (c) Microsoft. 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.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Testing; +using Xunit; +using VerifyCS = Test.Utilities.CSharpCodeFixVerifier< + Microsoft.NetCore.CSharp.Analyzers.Performance.CSharpUseSearchValuesAnalyzer, + Microsoft.NetCore.CSharp.Analyzers.Performance.CSharpUseSearchValuesFixer>; + +namespace Microsoft.NetCore.Analyzers.Performance.UnitTests +{ + public class UseSearchValuesTests + { + [Fact] + public async Task TestIndexOfAnyAnalyzer() + { + string source = + """ + using System; + + internal sealed class Test + { + private const string ShortConstStringTypeMember = "foo"; + private const string LongConstStringTypeMember = "aeiouA"; + private static string NonConstStringProperty => "aeiouA"; + private string NonConstStringInstanceField = "aeiouA"; + private static string NonConstStringStaticField = "aeiouA"; + private readonly string NonConstStringReadonlyInstanceField = "aeiouA"; + private static readonly string NonConstStringReadonlyStaticField = "aeiouA"; + private const char ConstChar = 'A'; + private static char NonConstChar => 'A'; + private const byte ConstByte = (byte)'A'; + private static byte NonConstByte => (byte)'A'; + private static readonly char[] ShortStaticReadonlyCharArrayField = new[] { 'a', 'e', 'i', 'o', 'u' }; + private static readonly char[] LongStaticReadonlyCharArrayField = new[] { 'a', 'e', 'i', 'o', 'u', 'A' }; + private static readonly char[] LongStaticReadonlyCharArrayFieldWithNonInlineLiteral = new[] { 'a', 'e', 'i', 'o', 'u', ConstChar }; + private readonly char[] LongReadonlyCharArrayFieldWithSimpleFieldModification = new[] { 'a', 'e', 'i', 'o', 'u', 'A' }; + private readonly char[] LongReadonlyCharArrayFieldWithSimpleElementModification = new[] { 'a', 'e', 'i', 'o', 'u', 'A' }; + static readonly char[] LongStaticReadonlyCharArrayFieldWithoutAccessibility = new[] { 'a', 'e', 'i', 'o', 'u', 'A' }; + public static readonly char[] LongStaticReadonlyCharArrayFieldWithPublicAccessibility = new[] { 'a', 'e', 'i', 'o', 'u', 'A' }; + private static readonly char[] LongStaticReadonlyExplicitCharArrayField = new char[] { 'a', 'e', 'i', 'o', 'u', 'A' }; + private readonly char[] InstanceReadonlyCharArrayField = new[] { 'a', 'e', 'i', 'o', 'u', 'A' }; + private char[] InstanceSettableCharArrayField = new[] { 'a', 'e', 'i', 'o', 'u', 'A' }; + private readonly char[] ShortReadonlyCharArrayFieldFromToCharArray = "aeiou".ToCharArray(); + private readonly char[] LongReadonlyCharArrayFieldFromToCharArray = "aeiouA".ToCharArray(); + private readonly char[] ShortReadonlyCharArrayFieldFromConstToCharArray = ShortConstStringTypeMember.ToCharArray(); + private readonly char[] LongReadonlyCharArrayFieldFromConstToCharArray = LongConstStringTypeMember.ToCharArray(); + private ReadOnlySpan ShortReadOnlySpanOfCharRVAProperty => new[] { 'a', 'e', 'i', 'o', 'u' }; + private ReadOnlySpan LongReadOnlySpanOfCharRVAProperty => new[] { 'a', 'e', 'i', 'o', 'u', 'A' }; + private ReadOnlySpan ShortReadOnlySpanOfByteRVAProperty => new[] { (byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u' }; + private ReadOnlySpan LongReadOnlySpanOfByteRVAProperty => new[] { (byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u', (byte)'A' }; + private ReadOnlySpan ShortReadOnlySpanOfCharFromToCharArrayProperty => "aeiou".ToCharArray(); + private ReadOnlySpan LongReadOnlySpanOfCharFromToCharArrayProperty => "aeiouA".ToCharArray(); + private ReadOnlySpan ShortReadOnlySpanOfCharFromConstToCharArrayProperty => ShortConstStringTypeMember.ToCharArray(); + private ReadOnlySpan LongReadOnlySpanOfCharFromConstToCharArrayProperty => LongConstStringTypeMember.ToCharArray(); + private ReadOnlySpan LongReadOnlySpanOfCharRVAPropertyWithGetAccessor1 + { + get => new[] { 'a', 'e', 'i', 'o', 'u', 'A' }; + } + private ReadOnlySpan LongReadOnlySpanOfCharRVAPropertyWithGetAccessor2 + { + get + { + return new[] { 'a', 'e', 'i', 'o', 'u', 'A' }; + } + } + private ReadOnlySpan LongReadOnlySpanOfCharRVAPropertyWithGetAccessor3 + { + get + { + Console.WriteLine("foo"); + return new[] { 'a', 'e', 'i', 'o', 'u', 'A' }; + } + } + + public Test() + { + LongReadonlyCharArrayFieldWithSimpleFieldModification = new[] { 'a' }; + Console.WriteLine(InstanceReadonlyCharArrayField); + } + + public void Dummy() + { + LongReadonlyCharArrayFieldWithSimpleElementModification[0] = 'a'; + Console.WriteLine(InstanceReadonlyCharArrayField[0]); + } + + private void TestMethod(string str, ReadOnlySpan chars, ReadOnlySpan bytes) + { + const string ShortConstStringLocal = "foo"; + const string LongConstStringLocal = "aeiouA"; + string NonConstStringLocal = "aeiouA"; + + _ = chars.IndexOfAny("aeiou"); + _ = chars.IndexOfAny([|"aeiouA"|]); + _ = chars.IndexOfAny("aeiouA" + NonConstStringProperty); + _ = chars.IndexOfAny("aeiouA" + NonConstStringLocal); + + _ = chars.IndexOfAny(new[] { 'a', 'e', 'i', 'o', 'u' }); + _ = chars.IndexOfAny([|new[] { 'a', 'e', 'i', 'o', 'u', 'A' }|]); + _ = chars.IndexOfAny([|new char[] { 'a', 'e', 'i', 'o', 'u', 'A' }|]); + _ = chars.IndexOfAny(new[] { 'a', 'e', 'i', 'o', 'u', NonConstChar }); + + _ = chars.IndexOfAny([|new[] { 'a', 'e', /* Comment */ 'i', 'o', 'u', 'A' }|]); + _ = chars.IndexOfAny([|new[] + { + // Comment + 'a', 'e', 'i', 'o', 'u', 'A' + }|]); + + _ = str.IndexOfAny([|new char[] { }|]); + _ = str.IndexOfAny([|new[] { 'a', 'e', 'i', 'o', 'u' }|]); + _ = str.IndexOfAny([|new[] { 'a', 'e', 'i', 'o', 'u', 'A' }|]); + _ = str.IndexOfAny([|new char[] { 'a', 'e', 'i', 'o', 'u', 'A' }|]); + + _ = str?.IndexOfAny(new[] { 'a', 'e', 'i', 'o', 'u', 'A' }); + + _ = bytes.IndexOfAny(new[] { (byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u' }); + _ = bytes.IndexOfAny([|new[] { (byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u', (byte)'A' }|]); + _ = bytes.IndexOfAny([|new byte[] { (byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u', (byte)'A' }|]); + _ = bytes.IndexOfAny(new[] { (byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u', NonConstByte }); + + _ = chars.IndexOfAny(ShortConstStringTypeMember); + _ = chars.IndexOfAny([|LongConstStringTypeMember|]); + _ = chars.IndexOfAny(NonConstStringProperty); + + _ = chars.IndexOfAny(ShortConstStringLocal); + _ = chars.IndexOfAny([|LongConstStringLocal|]); + _ = chars.IndexOfAny(NonConstStringLocal); + + _ = chars.IndexOfAny(ShortStaticReadonlyCharArrayField); + _ = chars.IndexOfAny([|LongStaticReadonlyCharArrayField|]); + _ = chars.IndexOfAny([|LongStaticReadonlyExplicitCharArrayField|]); + _ = chars.IndexOfAny([|InstanceReadonlyCharArrayField|]); + _ = chars.IndexOfAny(InstanceSettableCharArrayField); + _ = chars.IndexOfAny(ShortReadonlyCharArrayFieldFromToCharArray); + _ = chars.IndexOfAny([|LongReadonlyCharArrayFieldFromToCharArray|]); + _ = chars.IndexOfAny(ShortReadonlyCharArrayFieldFromConstToCharArray); + _ = chars.IndexOfAny([|LongReadonlyCharArrayFieldFromConstToCharArray|]); + + _ = str.IndexOfAny(ShortStaticReadonlyCharArrayField); + _ = str.IndexOfAny([|LongStaticReadonlyCharArrayField|]); + _ = str.IndexOfAny([|LongStaticReadonlyExplicitCharArrayField|]); + _ = str.IndexOfAny([|InstanceReadonlyCharArrayField|]); + _ = str.IndexOfAny(InstanceSettableCharArrayField); + _ = str.IndexOfAny(ShortReadonlyCharArrayFieldFromToCharArray); + _ = str.IndexOfAny([|LongReadonlyCharArrayFieldFromToCharArray|]); + _ = str.IndexOfAny(ShortReadonlyCharArrayFieldFromConstToCharArray); + _ = str.IndexOfAny([|LongReadonlyCharArrayFieldFromConstToCharArray|]); + + _ = chars.IndexOfAny([|LongStaticReadonlyCharArrayFieldWithoutAccessibility|]); + _ = chars.IndexOfAny(LongStaticReadonlyCharArrayFieldWithPublicAccessibility); + + _ = chars.IndexOfAny(ShortReadOnlySpanOfCharRVAProperty); + _ = chars.IndexOfAny([|LongReadOnlySpanOfCharRVAProperty|]); + _ = chars.IndexOfAny(ShortReadOnlySpanOfCharFromToCharArrayProperty); + _ = chars.IndexOfAny([|LongReadOnlySpanOfCharFromToCharArrayProperty|]); + _ = chars.IndexOfAny(ShortReadOnlySpanOfCharFromConstToCharArrayProperty); + _ = chars.IndexOfAny([|LongReadOnlySpanOfCharFromConstToCharArrayProperty|]); + + _ = bytes.IndexOfAny(ShortReadOnlySpanOfByteRVAProperty); + _ = bytes.IndexOfAny([|LongReadOnlySpanOfByteRVAProperty|]); + + _ = chars.IndexOfAny([|LongReadOnlySpanOfCharRVAPropertyWithGetAccessor1|]); + _ = chars.IndexOfAny([|LongReadOnlySpanOfCharRVAPropertyWithGetAccessor2|]); + _ = chars.IndexOfAny(LongReadOnlySpanOfCharRVAPropertyWithGetAccessor3); + + + // We detect simple cases where the array may be modified + _ = str.IndexOfAny(LongReadonlyCharArrayFieldWithSimpleFieldModification); + _ = str.IndexOfAny(LongReadonlyCharArrayFieldWithSimpleElementModification); + + + // Uses of ToCharArray are flagged even for shorter values. + _ = chars.IndexOfAny([|ShortConstStringTypeMember.ToCharArray()|]); + _ = chars.IndexOfAny([|LongConstStringTypeMember.ToCharArray()|]); + _ = chars.IndexOfAny([|"aeiou".ToCharArray()|]); + _ = chars.IndexOfAny([|"aeiouA".ToCharArray()|]); + + _ = str.IndexOfAny([|ShortConstStringTypeMember.ToCharArray()|]); + _ = str.IndexOfAny([|LongConstStringTypeMember.ToCharArray()|]); + _ = str.IndexOfAny([|"aeiou".ToCharArray()|]); + _ = str.IndexOfAny([|"aeiouA".ToCharArray()|]); + + + // For cases like this that we'd want to flag, a different analyzer should suggest making the field 'const' first. + _ = chars.IndexOfAny(NonConstStringInstanceField); + _ = chars.IndexOfAny(NonConstStringStaticField); + _ = chars.IndexOfAny(NonConstStringReadonlyInstanceField); + _ = chars.IndexOfAny(NonConstStringReadonlyStaticField); + + + // A few cases that could be flagged, but currently aren't: + _ = chars.IndexOfAny("aeiou" + 'A'); + _ = chars.IndexOfAny("aeiou" + "A"); + _ = chars.IndexOfAny(new[] { 'a', 'e', 'i', 'o', 'u', ConstChar }); + _ = chars.IndexOfAny("aeiouA" + ShortConstStringTypeMember); + _ = chars.IndexOfAny(LongConstStringTypeMember + ShortConstStringTypeMember); + _ = bytes.IndexOfAny(new[] { (byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u', ConstByte }); + _ = bytes.IndexOfAny(new[] { (byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u', (byte)ConstChar }); + _ = chars.IndexOfAny(LongStaticReadonlyCharArrayFieldWithNonInlineLiteral); + } + } + """; + + await VerifyAnalyzerAsync(LanguageVersion.CSharp7_3, source); + await VerifyAnalyzerAsync(LanguageVersion.CSharp11, source); + } + + [Fact] + public async Task TestUtf8StringLiteralsAnalyzer() + { + await VerifyAnalyzerAsync(LanguageVersion.CSharp11, + """ + using System; + + internal sealed class Test + { + private ReadOnlySpan ShortReadOnlySpanOfByteRVAPropertyU8 => "aeiou"u8; + private ReadOnlySpan LongReadOnlySpanOfByteRVAPropertyU8 => "aeiouA"u8; + + private void TestMethod(ReadOnlySpan bytes) + { + _ = bytes.IndexOfAny("aeiou"u8); + _ = bytes.IndexOfAny([|"aeiouA"u8|]); + + _ = bytes.IndexOfAny(ShortReadOnlySpanOfByteRVAPropertyU8); + _ = bytes.IndexOfAny([|LongReadOnlySpanOfByteRVAPropertyU8|]); + } + } + """); + } + + public static IEnumerable TestAllIndexOfAnyAndContainsAnySpanOverloads_MemberData() + { + return + from method in new[] { "IndexOfAny", "LastIndexOfAny", "IndexOfAnyExcept", "LastIndexOfAnyExcept", "ContainsAny", "ContainsAnyExcept" } + from bytes in new[] { true, false } + from readOnlySpan in new[] { true, false } + select new object[] { method, bytes, readOnlySpan }; + } + + [Theory] + [MemberData(nameof(TestAllIndexOfAnyAndContainsAnySpanOverloads_MemberData))] + public async Task TestAllIndexOfAnyAndContainsAnySpanOverloads(string method, bool bytes, bool readOnlySpan) + { + string type = bytes ? "byte" : "char"; + string argumentType = $"{(readOnlySpan ? "ReadOnly" : "")}Span<{type}>"; + string valuesExpression = bytes ? "\"aeiouA\"u8" : "\"aeiouA\""; + string searchValuesFieldName = bytes ? "s_myBytes" : "s_myChars"; + + string source = + $$""" + using System; + using System.Buffers; + + internal sealed class Test + { + private void TestMethod({{argumentType}} input) + { + _ = input.{{method}}([|{{valuesExpression}}|]); + } + } + """; + + string expected = + $$""" + using System; + using System.Buffers; + + internal sealed class Test + { + private static readonly SearchValues<{{type}}> {{searchValuesFieldName}} = SearchValues.Create({{valuesExpression}}); + + private void TestMethod({{argumentType}} input) + { + _ = input.{{method}}({{searchValuesFieldName}}); + } + } + """; + + await VerifyCodeFixAsync(LanguageVersion.CSharp11, source, expected); + } + + [Theory] + [InlineData("IndexOfAny")] + [InlineData("LastIndexOfAny")] + public async Task TestAllIndexOfAnyStringOverloads(string method) + { + string source = + $$""" + using System; + using System.Buffers; + + internal sealed class Test + { + private void TestMethod(string input) + { + _ = input.{{method}}([|"aeiouA".ToCharArray()|]); + } + } + """; + + string expected = + $$""" + using System; + using System.Buffers; + + internal sealed class Test + { + private static readonly SearchValues s_myChars = SearchValues.Create("aeiouA"); + + private void TestMethod(string input) + { + _ = input.AsSpan().{{method}}(s_myChars); + } + } + """; + + await VerifyCodeFixAsync(LanguageVersion.CSharp7_3, source, expected); + } + + [Theory] + [InlineData("const string", "= \"aeiouA\";", true)] + [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 byte[]", "= new[] { (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", "=> new[] { (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)] + [InlineData("static ReadOnlySpan", "=> \"aeiouA\"u8;", true)] + [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 { return new[] { (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)] + [InlineData("static ReadOnlySpan", "{ get { return new[] { (byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u', (byte)'A' }; } }", true)] + [InlineData("readonly char[]", "= \"aeiouA\".ToCharArray();", false)] + [InlineData("static readonly char[]", "= \"aeiouA\".ToCharArray();", false)] + [InlineData("ReadOnlySpan", "=> \"aeiouA\".ToCharArray();", false)] + [InlineData("static ReadOnlySpan", "=> \"aeiouA\".ToCharArray();", true)] + [InlineData("readonly char[]", "= ConstStringTypeMember.ToCharArray();", false, "ConstStringTypeMember")] + [InlineData("static readonly char[]", "= ConstStringTypeMember.ToCharArray();", false, "ConstStringTypeMember")] + [InlineData("ReadOnlySpan", "=> ConstStringTypeMember.ToCharArray();", false, "ConstStringTypeMember")] + [InlineData("static ReadOnlySpan", "=> ConstStringTypeMember.ToCharArray();", true)] + public async Task TestCodeFixerNamedArguments(string modifiersAndType, string initializer, bool createWillUseMemberReference, string createExpression = null) + { + const string OriginalValuesName = "MyValuesTypeMember"; + const string SearchValuesFieldName = "s_myValuesTypeMember"; + + string byteOrChar = modifiersAndType.Contains("byte", StringComparison.Ordinal) ? "byte" : "char"; + string memberDefinition = $"{modifiersAndType} {OriginalValuesName} {initializer}"; + bool isProperty = initializer.Contains("=>", StringComparison.Ordinal) || initializer.Contains("get", StringComparison.Ordinal); + + string cSharp11CreateExpression = null; + + if (createWillUseMemberReference) + { + Assert.Null(createExpression); + createExpression = OriginalValuesName; + } + else + { + if (byteOrChar == "char") + { + createExpression ??= "\"aeiouA\""; + } + else + { + if (createExpression is null) + { + createExpression = initializer.TrimStart('{', ' ', '=', '>', 'g', 'e', 't').TrimEnd(' ', '}').TrimEnd(';'); + + if (createExpression.StartsWith("return ", StringComparison.Ordinal)) + { + createExpression = createExpression.Substring("return ".Length); + } + } + + cSharp11CreateExpression = "\"aeiouA\"u8"; + } + } + + await TestAsync(LanguageVersion.CSharp7_3, createExpression); + await TestAsync(LanguageVersion.CSharp11, cSharp11CreateExpression ?? createExpression); + + async Task TestAsync(LanguageVersion languageVersion, string expectedCreateExpression) + { + if (languageVersion < LanguageVersion.CSharp11 && + (memberDefinition.Contains("u8", StringComparison.Ordinal) || expectedCreateExpression.Contains("u8", StringComparison.Ordinal))) + { + // Need CSharp 11 or newer to use Utf8 string literals + return; + } + + string source = + $$""" + using System; + using System.Buffers; + + internal sealed class Test + { + {{memberDefinition}} + private const string ConstStringTypeMember = "aeiouA"; + + private void TestMethod(ReadOnlySpan<{{byteOrChar}}> span) + { + _ = span.IndexOfAny([|{{OriginalValuesName}}|]); + } + } + """; + + string newLineAfterSearchValues = isProperty && createWillUseMemberReference ? Environment.NewLine : ""; + string expectedMemberDefinition = createWillUseMemberReference ? $"{memberDefinition}{Environment.NewLine} " : ""; + + string expected = + $$""" + using System; + using System.Buffers; + + internal sealed class Test + { + private static readonly SearchValues<{{byteOrChar}}> {{SearchValuesFieldName}} = SearchValues.Create({{expectedCreateExpression}});{{newLineAfterSearchValues}} + {{expectedMemberDefinition}}private const string ConstStringTypeMember = "aeiouA"; + + private void TestMethod(ReadOnlySpan<{{byteOrChar}}> span) + { + _ = span.IndexOfAny({{SearchValuesFieldName}}); + } + } + """; + + await VerifyCodeFixAsync(languageVersion, source, expected); + } + } + + [Theory] + [InlineData("static readonly char[]", "new[] { 'a', 'e', 'i', 'o', 'u', 'A' }")] + [InlineData("readonly char[]", "new[] { 'a', 'e', 'i', 'o', 'u', 'A' }")] + [InlineData("readonly char[]", "new char[] { 'a', 'e', 'i', 'o', 'u', 'A' }")] + [InlineData("readonly char[]", "new char[] { 'a', 'e', 'i', 'o', 'u', 'A' }")] + public async Task TestCodeFixerNamedArgumentsStringIndexOfAny(string modifiersAndType, string initializer) + { + const string OriginalValuesName = "MyValuesTypeMember"; + const string SearchValuesFieldName = "s_myValuesTypeMember"; + + string memberDefinition = $"{modifiersAndType} {OriginalValuesName} = {initializer};"; + + string source = + $$""" + using System; + using System.Buffers; + + internal sealed class Test + { + {{memberDefinition}} + + private void TestMethod(string text) + { + _ = text.IndexOfAny([|{{OriginalValuesName}}|]); + } + } + """; + + string expected = + $$""" + using System; + using System.Buffers; + + internal sealed class Test + { + private static readonly SearchValues {{SearchValuesFieldName}} = SearchValues.Create("aeiouA"); + + private void TestMethod(string text) + { + _ = text.AsSpan().IndexOfAny({{SearchValuesFieldName}}); + } + } + """; + + await VerifyCodeFixAsync(LanguageVersion.CSharp7_3, source, expected); + } + + [Fact] + public async Task TestCodeFixerConstStringMemberToCharArray() + { + string source = + """ + using System; + using System.Buffers; + + internal sealed class Test + { + private const string MyValuesTypeMember = "aeiouA"; + + private void TestMethod(string text) + { + _ = text.IndexOfAny([|MyValuesTypeMember.ToCharArray()|]); + } + } + """; + + string expected = + """ + using System; + using System.Buffers; + + internal sealed class Test + { + private static readonly SearchValues s_myValuesTypeMember = SearchValues.Create(MyValuesTypeMember); + private const string MyValuesTypeMember = "aeiouA"; + + private void TestMethod(string text) + { + _ = text.AsSpan().IndexOfAny(s_myValuesTypeMember); + } + } + """; + + await VerifyCodeFixAsync(LanguageVersion.CSharp7_3, source, expected); + } + + [Fact] + public async Task TestCodeFixerLocalStringConst() + { + string source = + """ + using System; + using System.Buffers; + + internal sealed class Test + { + private void TestMethod(ReadOnlySpan chars) + { + const string Values = "aeiouA"; + + _ = chars.IndexOfAny([|Values|]); + } + } + """; + + string expected = + """ + using System; + using System.Buffers; + + internal sealed class Test + { + private static readonly SearchValues s_values = SearchValues.Create("aeiouA"); + + private void TestMethod(ReadOnlySpan chars) + { + _ = chars.IndexOfAny(s_values); + } + } + """; + + await VerifyCodeFixAsync(LanguageVersion.CSharp7_3, source, expected); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task TestCodeFixerLocalStringConstToCharArray(bool spanInput) + { + string argumentType = spanInput ? "ReadOnlySpan" : "string"; + + string source = + $$""" + using System; + using System.Buffers; + + internal sealed class Test + { + private void TestMethod({{argumentType}} text) + { + const string Values = "aeiouA"; + + _ = text.IndexOfAny([|Values.ToCharArray()|]); + } + } + """; + + string expected = + $$""" + using System; + using System.Buffers; + + internal sealed class Test + { + private static readonly SearchValues s_values = SearchValues.Create("aeiouA"); + + private void TestMethod({{argumentType}} text) + { + _ = text{{(spanInput ? "" : ".AsSpan()")}}.IndexOfAny(s_values); + } + } + """; + + await VerifyCodeFixAsync(LanguageVersion.CSharp7_3, source, expected); + } + + [Theory] + [InlineData(LanguageVersion.CSharp7_3, "\"aeiouA\"", "\"aeiouA\"")] + [InlineData(LanguageVersion.CSharp7_3, "@\"aeiouA\"", "@\"aeiouA\"")] + [InlineData(LanguageVersion.CSharp11, "\"aeiouA\"u8", "\"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\"")] + [InlineData(LanguageVersion.CSharp7_3, "new[] { (byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u', (byte)'A' }", "new[] { (byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u', (byte)'A' }")] + [InlineData(LanguageVersion.CSharp11, "new[] { (byte)'a', (byte)'e', (byte)'i', (byte)'o', (byte)'u', (byte)'A' }", "\"aeiouA\"u8")] + public async Task TestCodeFixerInlineArguments(LanguageVersion languageVersion, string values, string expectedCreateArgument) + { + string byteOrChar = values.Contains("byte", StringComparison.Ordinal) || values.Contains("u8", StringComparison.Ordinal) ? "byte" : "char"; + string searchValuesFieldName = byteOrChar == "byte" ? "s_myBytes" : "s_myChars"; + + string source = + $$""" + using System; + using System.Buffers; + + internal sealed class Test + { + private void TestMethod(ReadOnlySpan<{{byteOrChar}}> span) + { + _ = span.IndexOfAny([|{{values}}|]); + } + } + """; + + string expected = + $$""" + using System; + using System.Buffers; + + internal sealed class Test + { + private static readonly SearchValues<{{byteOrChar}}> {{searchValuesFieldName}} = SearchValues.Create({{expectedCreateArgument}}); + + private void TestMethod(ReadOnlySpan<{{byteOrChar}}> span) + { + _ = span.IndexOfAny({{searchValuesFieldName}}); + } + } + """; + + await VerifyCodeFixAsync(languageVersion, source, expected); + } + + [Fact] + public async Task TestCodeFixerInlineStringLiteralToCharArray() + { + string source = + """ + using System; + using System.Buffers; + + internal sealed class Test + { + private void TestMethod(string text) + { + _ = text.IndexOfAny([|"aeiouA".ToCharArray()|]); + } + } + """; + + string expected = + """ + using System; + using System.Buffers; + + internal sealed class Test + { + private static readonly SearchValues s_myChars = SearchValues.Create("aeiouA"); + + private void TestMethod(string text) + { + _ = text.AsSpan().IndexOfAny(s_myChars); + } + } + """; + + await VerifyCodeFixAsync(LanguageVersion.CSharp7_3, source, expected); + } + + [Fact] + public async Task TestCodeFixerDoesNotRemoveCommentsInArrayInitializer() + { + string source = + """ + using System; + using System.Buffers; + + internal sealed class Test + { + private void TestMethod(string text) + { + _ = text.IndexOfAny([|new[] { 'a', /* Useful comment */ 'b' }|]); + } + } + """; + + string expected = + """ + using System; + using System.Buffers; + + internal sealed class Test + { + private static readonly SearchValues s_myChars = SearchValues.Create(new[] { 'a', /* Useful comment */ 'b' }); + + private void TestMethod(string text) + { + _ = text.AsSpan().IndexOfAny(s_myChars); + } + } + """; + + await VerifyCodeFixAsync(LanguageVersion.CSharp7_3, source, expected); + + source = + """ + using System; + using System.Buffers; + + internal sealed class Test + { + private void TestMethod(string text) + { + _ = text.IndexOfAny([|new[] + { + 'a', + 'b' // Useful comment + }|]); + } + } + """; + + expected = + """ + using System; + using System.Buffers; + + internal sealed class Test + { + private static readonly SearchValues s_myChars = SearchValues.Create(new[] + { + 'a', + 'b' // Useful comment + }); + + private void TestMethod(string text) + { + _ = text.AsSpan().IndexOfAny(s_myChars); + } + } + """; + + await VerifyCodeFixAsync(LanguageVersion.CSharp7_3, source, expected); + } + + [Fact] + public static async Task TestCodeFixerDoesNotRemoveTheOriginalMemberIfPublic() + { + string source = + """ + using System; + using System.Buffers; + + internal sealed class Test + { + public ReadOnlySpan InvalidChars => new[] { 'a', 'b', 'c', 'd', 'e', 'f' }; + + public int IndexOfInvalidChar(ReadOnlySpan input) => + input.IndexOfAny([|InvalidChars|]); + } + """; + + string expected = + """ + using System; + using System.Buffers; + + internal sealed class Test + { + private static readonly SearchValues s_invalidChars = SearchValues.Create("abcdef"); + + public ReadOnlySpan InvalidChars => new[] { 'a', 'b', 'c', 'd', 'e', 'f' }; + + public int IndexOfInvalidChar(ReadOnlySpan input) => + input.IndexOfAny(s_invalidChars); + } + """; + + await VerifyCodeFixAsync(LanguageVersion.CSharp7_3, source, expected); + } + + [Fact] + public async Task TestCodeFixerAddsSystemUsingIfNeeded() + { + string source = + """ + using System.Buffers; + + internal sealed class Test + { + private void TestMethod(string text) + { + _ = text.IndexOfAny([|"aeiouA".ToCharArray()|]); + } + } + """; + + string expected = + """ + using System.Buffers; + using System; + + internal sealed class Test + { + private static readonly SearchValues s_myChars = SearchValues.Create("aeiouA"); + + private void TestMethod(string text) + { + _ = text.AsSpan().IndexOfAny(s_myChars); + } + } + """; + + await VerifyCodeFixAsync(LanguageVersion.CSharp7_3, source, expected); + } + + [Theory] + [InlineData("MyValues", "s_myValues")] + [InlineData("myValues", "s_myValues")] + [InlineData("_myValues", "s_myValues")] + [InlineData("_MyValues", "s_MyValues")] + [InlineData("s_myValues", "s_myValues", false)] + [InlineData("s_MyValues", "s_MyValues", false)] + [InlineData("s_myValues", "s_myValuesSearchValues", true)] + [InlineData("s_MyValues", "s_MyValuesSearchValues", true)] + public async Task TestCodeFixerPicksFriendlyFieldNames(string memberName, string expectedFieldName, bool memberHasOtherUses = false) + { + string memberDefinition = $"private readonly char[] {memberName} = \"aeiouA\".ToCharArray();"; + string otherMemberUses = memberHasOtherUses ? $" _ = {memberName};" : ""; + + string source = + $$""" + using System; + using System.Buffers; + + internal sealed class Test + { + {{memberDefinition}} + + private void TestMethod(ReadOnlySpan text) + { + _ = text.IndexOfAny([|{{memberName}}|]);{{otherMemberUses}} + } + } + """; + + string expectedMemberDefinition = memberHasOtherUses ? $"{Environment.NewLine} {memberDefinition}" : ""; + + string expected = + $$""" + using System; + using System.Buffers; + + internal sealed class Test + { + private static readonly SearchValues {{expectedFieldName}} = SearchValues.Create("aeiouA");{{expectedMemberDefinition}} + + private void TestMethod(ReadOnlySpan text) + { + _ = text.IndexOfAny({{expectedFieldName}});{{otherMemberUses}} + } + } + """; + + await VerifyCodeFixAsync(LanguageVersion.CSharp7_3, source, expected); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task TestCodeFixerAccountsForUsingStatements(bool hasSystemBuffersUsing) + { + string usingLine = hasSystemBuffersUsing ? "using System.Buffers;" : ""; + string systemBuffersPrefix = hasSystemBuffersUsing ? "" : "System.Buffers."; + + string source = + $$""" + using System; + {{usingLine}} + + internal sealed class Test + { + private void TestMethod(string text) + { + _ = text.IndexOfAny([|"aeiouA".ToCharArray()|]); + } + } + """; + + string expected = + $$""" + using System; + {{usingLine}} + + internal sealed class Test + { + private static readonly {{systemBuffersPrefix}}SearchValues s_myChars = {{systemBuffersPrefix}}SearchValues.Create("aeiouA"); + + private void TestMethod(string text) + { + _ = text.AsSpan().IndexOfAny(s_myChars); + } + } + """; + + await VerifyCodeFixAsync(LanguageVersion.CSharp7_3, source, expected); + } + + [Fact] + public async Task TestCodeFixerAvoidsMemberNameConflicts() + { + string source = + """ + using System; + using System.Buffers; + + internal sealed class Test + { + private static string s_myChars => ""; + private static readonly string s_myChars1 = ""; + private static string s_myChars2() => ""; + private sealed class s_myChars3 { } + + private void TestMethod(ReadOnlySpan chars) + { + _ = chars.IndexOfAny([|"aeiouA"|]); + } + } + """; + + string expected = + """ + using System; + using System.Buffers; + + internal sealed class Test + { + private static readonly SearchValues s_myChars4 = SearchValues.Create("aeiouA"); + + private static string s_myChars => ""; + private static readonly string s_myChars1 = ""; + private static string s_myChars2() => ""; + private sealed class s_myChars3 { } + + private void TestMethod(ReadOnlySpan chars) + { + _ = chars.IndexOfAny(s_myChars4); + } + } + """; + + await VerifyCodeFixAsync(LanguageVersion.CSharp7_3, source, expected); + } + + [Fact] + public async Task TestCodeFixerWorksInTopLevelStatementsDocument() + { + string source = + """ + using System.Buffers; + + _ = "".IndexOfAny([|"aeiouA".ToCharArray()|]); + """; + + string expected = + """ + using System.Buffers; + using System; + + _ = "".AsSpan().IndexOfAny(s_myChars); + + partial class Program + { + private static readonly SearchValues s_myChars = SearchValues.Create("aeiouA"); + } + """; + + await VerifyCodeFixAsync(LanguageVersion.CSharp9, source, expected, topLevelStatements: true); + + source = + """ + using System.Buffers; + + _ = "".IndexOfAny([|s_myValues|]); + + partial class Program + { + private static readonly char[] s_myValues = new[] { 'a', 'e', 'i', 'o', 'u', 'A' }; + } + """; + + expected = + """ + using System.Buffers; + using System; + + _ = "".AsSpan().IndexOfAny(s_myValues); + + partial class Program + { + } + + partial class Program + { + private static readonly SearchValues s_myValues = SearchValues.Create("aeiouA"); + } + """; + + await VerifyCodeFixAsync(LanguageVersion.CSharp9, source, expected, topLevelStatements: true); + } + + private static async Task VerifyAnalyzerAsync(LanguageVersion languageVersion, string source) => + await VerifyCodeFixAsync(languageVersion, source, expected: null); + + private static async Task VerifyCodeFixAsync(LanguageVersion languageVersion, string source, string expected, bool topLevelStatements = false) + { + await new VerifyCS.Test + { + ReferenceAssemblies = Net80, + LanguageVersion = languageVersion, + TestCode = source, + FixedCode = expected, + TestState = { OutputKind = topLevelStatements ? OutputKind.ConsoleApplication : null }, + }.RunAsync(); + } + + // TEMP - need newer version of Microsoft.CodeAnalysis.Analyzer.Testing + // Replace with 'ReferenceAssemblies.Net.Net80' + private static readonly Lazy _lazyNet80 = + new(() => + { + if (!NuGet.Frameworks.NuGetFramework.Parse("net8.0").IsPackageBased) + { + // The NuGet version provided at runtime does not recognize the 'net8.0' target framework + throw new NotSupportedException("The 'net8.0' target framework is not supported by this version of NuGet."); + } + + return new ReferenceAssemblies( + "net8.0", + new PackageIdentity( + "Microsoft.NETCore.App.Ref", + "8.0.0-preview.7.23375.6"), + System.IO.Path.Combine("ref", "net8.0")); + }); + + public static ReferenceAssemblies Net80 => _lazyNet80.Value; + } +} diff --git a/src/Utilities/Compiler/Analyzer.Utilities.projitems b/src/Utilities/Compiler/Analyzer.Utilities.projitems index a1ed7e3713..441ad25bb5 100644 --- a/src/Utilities/Compiler/Analyzer.Utilities.projitems +++ b/src/Utilities/Compiler/Analyzer.Utilities.projitems @@ -68,6 +68,7 @@ + diff --git a/src/Utilities/Compiler/DiagnosticCategoryAndIdRanges.txt b/src/Utilities/Compiler/DiagnosticCategoryAndIdRanges.txt index 06f507268f..34ae27b448 100644 --- a/src/Utilities/Compiler/DiagnosticCategoryAndIdRanges.txt +++ b/src/Utilities/Compiler/DiagnosticCategoryAndIdRanges.txt @@ -12,7 +12,7 @@ Design: CA2210, CA1000-CA1070 Globalization: CA2101, CA1300-CA1311 Mobility: CA1600-CA1601 -Performance: HA, CA1800-CA1869 +Performance: HA, CA1800-CA1870 Security: CA2100-CA2153, CA2300-CA2330, CA3000-CA3147, CA5300-CA5405 Usage: CA1801, CA1806, CA1816, CA2200-CA2209, CA2211-CA2261 Naming: CA1700-CA1727 diff --git a/src/Utilities/Compiler/Lightup/IUtf8StringOperationWrapper.cs b/src/Utilities/Compiler/Lightup/IUtf8StringOperationWrapper.cs new file mode 100644 index 0000000000..d235999d1a --- /dev/null +++ b/src/Utilities/Compiler/Lightup/IUtf8StringOperationWrapper.cs @@ -0,0 +1,50 @@ +// 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.Diagnostics.CodeAnalysis; + using Microsoft.CodeAnalysis; + + [SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", Justification = "Not a comparable instance.")] + internal readonly struct IUtf8StringOperationWrapper : IOperationWrapper + { + internal const string WrappedTypeName = "Microsoft.CodeAnalysis.Operations.IUtf8StringOperation"; + private static readonly Type? WrappedType = OperationWrapperHelper.GetWrappedType(typeof(IUtf8StringOperationWrapper)); + + private static readonly Func ValueAccessor = LightupHelpers.CreateOperationPropertyAccessor(WrappedType, nameof(Value), fallbackResult: null!); + + private IUtf8StringOperationWrapper(IOperation operation) + { + WrappedOperation = operation; + } + + public IOperation WrappedOperation { get; } + public ITypeSymbol? Type => WrappedOperation.Type; + public string Value => ValueAccessor(WrappedOperation); + + public static IUtf8StringOperationWrapper FromOperation(IOperation operation) + { + if (operation == null) + { + return default; + } + + if (!IsInstance(operation)) + { + throw new InvalidCastException($"Cannot cast '{operation.GetType().FullName}' to '{WrappedTypeName}'"); + } + + return new IUtf8StringOperationWrapper(operation); + } + + public static bool IsInstance(IOperation operation) + { + return operation != null && LightupHelpers.CanWrapOperation(operation, WrappedType); + } + } +} + +#endif diff --git a/src/Utilities/Compiler/Lightup/OperationKindEx.cs b/src/Utilities/Compiler/Lightup/OperationKindEx.cs index b4a8661bc5..e5df73142f 100644 --- a/src/Utilities/Compiler/Lightup/OperationKindEx.cs +++ b/src/Utilities/Compiler/Lightup/OperationKindEx.cs @@ -10,6 +10,7 @@ internal static class OperationKindEx { public const OperationKind FunctionPointerInvocation = (OperationKind)0x78; public const OperationKind ImplicitIndexerReference = (OperationKind)0x7b; + public const OperationKind Utf8String = (OperationKind)0x7c; public const OperationKind Attribute = (OperationKind)0x7d; } } diff --git a/src/Utilities/Compiler/Lightup/OperationWrapperHelper.cs b/src/Utilities/Compiler/Lightup/OperationWrapperHelper.cs index 71e47fc6da..d4b6360a94 100644 --- a/src/Utilities/Compiler/Lightup/OperationWrapperHelper.cs +++ b/src/Utilities/Compiler/Lightup/OperationWrapperHelper.cs @@ -14,7 +14,8 @@ internal static class OperationWrapperHelper private static readonly Assembly s_codeAnalysisAssembly = typeof(SyntaxNode).GetTypeInfo().Assembly; private static readonly ImmutableDictionary WrappedTypes = ImmutableDictionary.Create() - .Add(typeof(IFunctionPointerInvocationOperationWrapper), s_codeAnalysisAssembly.GetType(IFunctionPointerInvocationOperationWrapper.WrappedTypeName)); + .Add(typeof(IFunctionPointerInvocationOperationWrapper), s_codeAnalysisAssembly.GetType(IFunctionPointerInvocationOperationWrapper.WrappedTypeName)) + .Add(typeof(IUtf8StringOperationWrapper), s_codeAnalysisAssembly.GetType(IUtf8StringOperationWrapper.WrappedTypeName)); /// /// Gets the type that is wrapped by the given wrapper. diff --git a/src/Utilities/Compiler/WellKnownTypeNames.cs b/src/Utilities/Compiler/WellKnownTypeNames.cs index 91f78ab145..7fa5290af8 100644 --- a/src/Utilities/Compiler/WellKnownTypeNames.cs +++ b/src/Utilities/Compiler/WellKnownTypeNames.cs @@ -115,6 +115,8 @@ internal static class WellKnownTypeNames public const string SystemBoolean = "System.Boolean"; public const string SystemBuffer = "System.Buffer"; public const string SystemBuffersMemoryManager1 = "System.Buffers.MemoryManager`1"; + public const string SystemBuffersSearchValues = "System.Buffers.SearchValues"; + public const string SystemBuffersSearchValues1 = "System.Buffers.SearchValues`1"; public const string SystemByte = "System.Byte"; public const string SystemChar = "System.Char"; public const string SystemCLSCompliantAttribute = "System.CLSCompliantAttribute";