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

namespace Microsoft.NetCore.Analyzers.Runtime
{
    using static MicrosoftNetCoreAnalyzersResources;

    /// <summary>
    /// CA1830: <inheritdoc cref="PreferTypedStringBuilderAppendOverloadsTitle"/>
    /// </summary>
    [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
    public sealed class PreferTypedStringBuilderAppendOverloads : DiagnosticAnalyzer
    {
        internal const string RuleId = "CA1830";

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

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

        public sealed override void Initialize(AnalysisContext context)
        {
            context.EnableConcurrentExecution();
            context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
            context.RegisterCompilationStartAction(compilationContext =>
            {
                var typeProvider = WellKnownTypeProvider.GetOrCreate(compilationContext.Compilation);

                // Get the String and StringBuilder types.
                if (!typeProvider.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemTextStringBuilder, out var stringBuilderType))
                {
                    return;
                }

                // Get all of the one-arg StringBuilder.Append methods and two-arg Insert methods.  We skip over the object-based
                // overloads, as there's little benefit to removing such ToString calls.  And we skip over arrays, to avoid
                // differences of behavior between Append(char[]) and Append(char[].ToString()).
                var appendMethods = stringBuilderType
                    .GetMembers("Append")
                    .OfType<IMethodSymbol>()
                    .WhereAsArray(s =>
                        s.Parameters.Length == 1 &&
                        s.Parameters[0].Type.SpecialType != SpecialType.System_Object &&
                        s.Parameters[0].Type.TypeKind != TypeKind.Array);
                var insertMethods = stringBuilderType
                    .GetMembers("Insert")
                    .OfType<IMethodSymbol>()
                    .WhereAsArray(s =>
                        s.Parameters.Length == 2 &&
                        s.Parameters[0].Type.SpecialType == SpecialType.System_Int32 &&
                        s.Parameters[1].Type.SpecialType != SpecialType.System_Object &&
                        s.Parameters[1].Type.TypeKind != TypeKind.Array);

                // Get the Append(char, int) overload for the string constructor pattern.
                // Note: There is no Insert(int, char, int) overload, so we only handle Append.
                var appendCharIntMethod = stringBuilderType
                    .GetMembers("Append")
                    .OfType<IMethodSymbol>()
                    .FirstOrDefault(s =>
                        s.Parameters.Length == 2 &&
                        s.Parameters[0].Type.SpecialType == SpecialType.System_Char &&
                        s.Parameters[1].Type.SpecialType == SpecialType.System_Int32);

                // Get the StringBuilder.Append(string)/Insert(int, string) method, for comparison purposes.
                var appendStringMethod = appendMethods.FirstOrDefault(s =>
                    s.Parameters[0].Type.SpecialType == SpecialType.System_String);
                var insertStringMethod = insertMethods.FirstOrDefault(s =>
                    s.Parameters[0].Type.SpecialType == SpecialType.System_Int32 &&
                    s.Parameters[1].Type.SpecialType == SpecialType.System_String);
                if (appendStringMethod is null || insertStringMethod is null)
                {
                    return;
                }

                // Check every invocation to see if it's StringBuilder.Append(string)/Insert(int, string).
                compilationContext.RegisterOperationAction(operationContext =>
                {
                    var invocation = (IInvocationOperation)operationContext.Operation;

                    // We're interested in the string argument to Append(string) and Insert(int, string).
                    // Get the argument position, or bail if this isn't either method.
                    int stringParamIndex;
                    if (appendStringMethod.Equals(invocation.TargetMethod))
                    {
                        stringParamIndex = 0;
                    }
                    else if (insertStringMethod.Equals(invocation.TargetMethod))
                    {
                        stringParamIndex = 1;
                    }
                    else
                    {
                        return;
                    }

                    if (invocation.Arguments.Length != stringParamIndex + 1 ||
                        invocation.Arguments[stringParamIndex] is not IArgumentOperation argument)
                    {
                        return;
                    }

                    // Check if the string argument is a "string ToString()" call.
                    if (argument.Value is IInvocationOperation toStringInvoke &&
                        toStringInvoke.TargetMethod.Name == "ToString" &&
                        toStringInvoke.Type?.SpecialType == SpecialType.System_String &&
                        toStringInvoke.TargetMethod.Parameters.IsEmpty)
                    {
                        // We're only interested if the receiver type of that ToString call has a corresponding strongly-typed overload.
                        IMethodSymbol? stronglyTypedAppend =
                            (stringParamIndex == 0 ? appendMethods : insertMethods)
                            .FirstOrDefault(s => s.Parameters[stringParamIndex].Type.Equals(toStringInvoke.TargetMethod.ReceiverType));
                        if (stronglyTypedAppend is null)
                        {
                            return;
                        }

                        // Warn.
                        operationContext.ReportDiagnostic(toStringInvoke.CreateDiagnostic(Rule));
                    }
                    // Check if the string argument is a "new string(char, int)" constructor call.
                    // Note: This optimization only applies to Append, not Insert, as there's no Insert(int, char, int) overload.
                    else if (stringParamIndex == 0 &&
                        argument.Value is IObjectCreationOperation objectCreation &&
                        objectCreation.Type?.SpecialType == SpecialType.System_String &&
                        objectCreation.Arguments.Length == 2 &&
                        objectCreation.Arguments[0].Value?.Type?.SpecialType == SpecialType.System_Char &&
                        objectCreation.Arguments[1].Value?.Type?.SpecialType == SpecialType.System_Int32 &&
                        appendCharIntMethod is not null)
                    {
                        // Warn.
                        operationContext.ReportDiagnostic(objectCreation.CreateDiagnostic(Rule));
                    }
                }, OperationKind.Invocation);
            });
        }
    }
}