File: Microsoft.NetCore.Analyzers\Performance\UseSearchValues.cs
Web Access
Project: src\sdk\src\Microsoft.CodeAnalysis.NetAnalyzers\src\Microsoft.CodeAnalysis.NetAnalyzers\Microsoft.CodeAnalysis.NetAnalyzers.csproj (Microsoft.CodeAnalysis.NetAnalyzers)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
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;

    /// <summary>
    /// CA1870: <inheritdoc cref="UseSearchValuesTitle"/>
    /// </summary>
    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<DiagnosticDescriptor> 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<IMethodSymbol> 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<IMethodSymbol> GetIndexOfAnyMethods(Compilation compilation)
        {
            var methods = ImmutableHashSet.CreateBuilder<IMethodSymbol>();

            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<T>.{Last}IndexOfAny{Except}(ReadOnlySpan<T>) and ContainsAny{Except}(ReadOnlySpan<T>)
            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 (IsConstantByteOrCharCollectionExpression(conversion.Operand, values: null, out length))
                {
                    // text.IndexOfAny(['a', 'b', 'c'])
                    // text.IndexOfAny([(byte)'a', (byte)'b', (byte)'c'])
                    return length >= MinLengthWorthReplacing;
                }
            }
            else if (argument.Kind == OperationKindEx.Utf8String)
            {
                // text.IndexOfAny("abc"u8)
                return
                    IUtf8StringOperationWrapper.IsInstance(argument) &&
                    IUtf8StringOperationWrapper.FromOperation(argument).Value.Length >= MinLengthWorthReplacing;
            }
            else if (argument is IPropertyReferenceOperation propertyReference)
            {
                // ReadOnlySpan<byte> Values => "abc"u8;
                // ReadOnlySpan<byte> Values => new byte[] { (byte)'a', (byte)'b', (byte)'c' };
                // ReadOnlySpan<char> Values => new char[] { 'a', 'b', 'c' };
                // ReadOnlySpan<char> Values => "abc".ToCharArray();
                // ReadOnlySpan<char> 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;
        }

        internal static bool IsConstantByteOrCharCollectionExpression(IOperation operation, List<char>? values, out int length)
        {
            if (operation.Kind == OperationKindEx.CollectionExpression &&
                ICollectionExpressionOperationWrapper.IsInstance(operation) &&
                ICollectionExpressionOperationWrapper.FromOperation(operation) is { } collection &&
                AllElementsAreConstantByteOrCharLiterals(collection.Elements, values))
            {
                length = collection.Elements.Length;
                return true;
            }

            length = 0;
            return false;

            static bool AllElementsAreConstantByteOrCharLiterals(ImmutableArray<IOperation> elements, List<char>? values)
            {
                foreach (IOperation element in elements)
                {
                    IOperation operation = element;

                    if (operation is IConversionOperation conversion)
                    {
                        if (operation.Type is not { SpecialType: SpecialType.System_Byte })
                        {
                            return false;
                        }

                        operation = conversion.Operand;
                    }

                    if (operation.Type is not { SpecialType: SpecialType.System_Char } ||
                        operation is not ILiteralOperation literal ||
                        literal.ConstantValue.Value is not char charValue)
                    {
                        return false;
                    }

                    values?.Add(charValue);
                }

                return true;
            }
        }
    }
}