File: Microsoft.NetCore.Analyzers\Runtime\UseSpanBasedStringConcat.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;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
using static Microsoft.NetCore.Analyzers.MicrosoftNetCoreAnalyzersResources;

namespace Microsoft.NetCore.Analyzers.Runtime
{
    /// <summary>
    /// CA1845: <inheritdoc cref="UseSpanBasedStringConcatTitle"/>
    /// </summary>
    public abstract class UseSpanBasedStringConcat : DiagnosticAnalyzer
    {
        internal const string RuleId = "CA1845";

        internal static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create(
            RuleId,
            CreateLocalizableResourceString(nameof(UseSpanBasedStringConcatTitle)),
            CreateLocalizableResourceString(nameof(UseSpanBasedStringConcatMessage)),
            DiagnosticCategory.Performance,
            RuleLevel.IdeSuggestion,
            CreateLocalizableResourceString(nameof(UseSpanBasedStringConcatDescription)),
            isPortedFxCopRule: false,
            isDataflowRule: false);

        /// <summary>
        /// Returns true if the specified binary operation is a top-most string concatenation operation
        /// </summary>
        private protected abstract bool IsTopMostConcatOperation(IBinaryOperation binaryOperation);

        /// <summary>
        /// Remove the built in implicit conversion on operands to concat.
        /// In VB, the conversion can be to either string or object.
        /// In C#, the conversion is always to object.
        /// </summary>
        private protected abstract IOperation WalkDownBuiltInImplicitConversionOnConcatOperand(IOperation operand);

        public sealed override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(Rule);

        public sealed override void Initialize(AnalysisContext context)
        {
            context.EnableConcurrentExecution();
            context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
            context.RegisterCompilationStartAction(OnCompilationStart);
        }

        private void OnCompilationStart(CompilationStartAnalysisContext context)
        {
            if (!RequiredSymbols.TryGetSymbols(context.Compilation, out RequiredSymbols symbols))
                return;

            context.RegisterOperationAction(context =>
            {
                //  Report diagnostic only if the operation is the top-most concat operation so we don't report sub-expressions of an
                //  already-reported violation.
                //  We also don't report any diagnostic if the concat operation has too many operands for the span-based
                //  Concat overloads to handle.
                var binary = (IBinaryOperation)context.Operation;
                if (IsTopMostConcatOperation(binary) && ShouldBeReported(binary))
                    context.ReportDiagnostic(binary.CreateDiagnostic(Rule));
            }, OperationKind.Binary);
            return;

            // Local functions

            bool ShouldBeReported(IBinaryOperation topMostConcatOperation)
            {
                var concatOperands = FlattenBinaryOperation(topMostConcatOperation);

                //  Bail if no suitable overload of 'string.Concat' exists.
                if (!symbols.TryGetRoscharConcatMethodWithArity(concatOperands.Length, out _))
                    return false;

                bool anySubstringInvocations = false;
                foreach (var operand in concatOperands)
                {
                    var value = WalkDownBuiltInImplicitConversionOnConcatOperand(operand);
                    switch (value.Type?.SpecialType)
                    {
                        //  Report diagnostics only when operands are exclusively strings and character literals.
                        case SpecialType.System_String:
                        case SpecialType.System_Char when value is ILiteralOperation:
                            if (IsAnyDirectOrConditionalSubstringInvocation(value))
                                anySubstringInvocations = true;
                            break;
                        default:
                            return false;
                    }
                }

                return anySubstringInvocations;
            }

            bool IsAnyDirectOrConditionalSubstringInvocation(IOperation operation)
            {
                if (operation is IConditionalAccessOperation conditionalAccessOperation)
                    operation = conditionalAccessOperation.WhenNotNull;

                return operation is IInvocationOperation invocation && symbols.IsAnySubstringMethod(invocation.TargetMethod);
            }
        }

        internal static ImmutableArray<IOperation> FlattenBinaryOperation(IBinaryOperation root)
        {
            var walker = new BinaryOperandWalker();
            walker.Visit(root);

            return walker.Operands.ToImmutable();
        }

        private sealed class BinaryOperandWalker : OperationWalker
        {
            private BinaryOperatorKind _operatorKind;

            public BinaryOperandWalker() : base() { }

            public ImmutableArray<IOperation>.Builder Operands { get; } = ImmutableArray.CreateBuilder<IOperation>();

            public override void DefaultVisit(IOperation operation)
            {
                Operands.Add(operation);
            }

            public override void VisitBinaryOperator(IBinaryOperation operation)
            {
                if (_operatorKind is BinaryOperatorKind.None)
                {
                    _operatorKind = operation.OperatorKind;
                }
                else if (_operatorKind != operation.OperatorKind)
                {
                    DefaultVisit(operation);
                    return;
                }

                Visit(operation.LeftOperand);
                Visit(operation.RightOperand);
            }
        }

        internal static IOperation CSharpWalkDownBuiltInImplicitConversionOnConcatOperand(IOperation operand)
        {
            if (operand is not IConversionOperation conversion)
                return operand;
            if (!conversion.IsImplicit || conversion.Conversion.IsUserDefined)
                return conversion;
            if (conversion.Type?.SpecialType is SpecialType.System_Object)
                return conversion.Operand;

            return conversion;
        }

        internal static IOperation BasicWalkDownBuiltInImplicitConversionOnConcatOperand(IOperation operand)
        {
            if (operand is not IConversionOperation conversion)
                return operand;
            if (!conversion.IsImplicit || conversion.Conversion.IsUserDefined)
                return conversion;
            if (conversion.Type?.SpecialType is SpecialType.System_Object or SpecialType.System_String)
                return conversion.Operand;

            return conversion;
        }

        // Use readonly struct instead of record type to save on allocations, since it's not passed by-value.
        // We aren't comparing these.
#pragma warning disable CA1815 // Override equals and operator equals on value types
        internal readonly struct RequiredSymbols
#pragma warning restore CA1815 // Override equals and operator equals on value types
        {
            private RequiredSymbols(
                INamedTypeSymbol stringType, INamedTypeSymbol roscharType,
                IMethodSymbol substringStart, IMethodSymbol substringStartLength,
                IMethodSymbol asSpanStart, IMethodSymbol asSpanStartLength)
            {
                StringType = stringType;
                ReadOnlySpanOfCharType = roscharType;
                SubstringStart = substringStart;
                SubstringStartLength = substringStartLength;
                AsSpanStart = asSpanStart;
                AsSpanStartLength = asSpanStartLength;
            }

            public static bool TryGetSymbols(Compilation compilation, out RequiredSymbols symbols)
            {
                var stringType = compilation.GetSpecialType(SpecialType.System_String);
                var charType = compilation.GetSpecialType(SpecialType.System_Char);

                if (stringType is null || charType is null)
                {
                    symbols = default;
                    return false;
                }

                var readOnlySpanOfCharType = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemReadOnlySpan1)?.Construct(charType);
                var memoryExtensionsType = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemMemoryExtensions);

                if (readOnlySpanOfCharType is null || memoryExtensionsType is null)
                {
                    symbols = default;
                    return false;
                }

                var intParamInfo = ParameterInfo.GetParameterInfo(compilation.GetSpecialType(SpecialType.System_Int32));
                var stringParamInfo = ParameterInfo.GetParameterInfo(stringType);

                var substringMembers = stringType.GetMembers(nameof(string.Substring)).OfType<IMethodSymbol>();
                var substringStart = substringMembers.GetFirstOrDefaultMemberWithParameterInfos(intParamInfo);
                var substringStartLength = substringMembers.GetFirstOrDefaultMemberWithParameterInfos(intParamInfo, intParamInfo);

                var asSpanMembers = memoryExtensionsType.GetMembers(nameof(MemoryExtensions.AsSpan)).OfType<IMethodSymbol>();
                var asSpanStart = asSpanMembers.GetFirstOrDefaultMemberWithParameterInfos(stringParamInfo, intParamInfo)?.ReduceExtensionMethod(stringType);
                var asSpanStartLength = asSpanMembers.GetFirstOrDefaultMemberWithParameterInfos(stringParamInfo, intParamInfo, intParamInfo)?.ReduceExtensionMethod(stringType);

                if (substringStart is null || substringStartLength is null || asSpanStart is null || asSpanStartLength is null)
                {
                    symbols = default;
                    return false;
                }

                symbols = new RequiredSymbols(
                    stringType, readOnlySpanOfCharType,
                    substringStart, substringStartLength,
                    asSpanStart, asSpanStartLength);
                return true;
            }

            public INamedTypeSymbol StringType { get; }
            public INamedTypeSymbol ReadOnlySpanOfCharType { get; }
            public IMethodSymbol SubstringStart { get; }
            public IMethodSymbol SubstringStartLength { get; }
            public IMethodSymbol AsSpanStart { get; }
            public IMethodSymbol AsSpanStartLength { get; }

            public IMethodSymbol? GetAsSpanEquivalent(IMethodSymbol? substringMethod)
            {
                if (SymbolEqualityComparer.Default.Equals(substringMethod, SubstringStart))
                    return AsSpanStart;
                if (SymbolEqualityComparer.Default.Equals(substringMethod, SubstringStartLength))
                    return AsSpanStartLength;
                return null;
            }

            public bool IsAnySubstringMethod(IMethodSymbol? method)
            {
                return SymbolEqualityComparer.Default.Equals(method, SubstringStart) ||
                    SymbolEqualityComparer.Default.Equals(method, SubstringStartLength);
            }

            public bool IsAnySubstringStartIndexParameter(IParameterSymbol? parameter)
            {
                return SymbolEqualityComparer.Default.Equals(parameter, SubstringStart.Parameters.First()) ||
                    SymbolEqualityComparer.Default.Equals(parameter, SubstringStartLength.Parameters.First());
            }

            public bool TryGetRoscharConcatMethodWithArity(int arity, [NotNullWhen(true)] out IMethodSymbol? concatMethod)
            {
                var roscharParamInfo = ParameterInfo.GetParameterInfo(ReadOnlySpanOfCharType);
                var argumentList = new ParameterInfo[arity];
                for (int index = 0; index < arity; index++)
                    argumentList[index] = roscharParamInfo;

                concatMethod = StringType.GetMembers(nameof(string.Concat))
                    .OfType<IMethodSymbol>()
                    .GetFirstOrDefaultMemberWithParameterInfos(argumentList);
                return concatMethod is not null;
            }
        }
    }
}