File: Microsoft.NetCore.Analyzers\Runtime\PreferTypedStringBuilderAppendOverloads.Fixer.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.Composition;
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.NetAnalyzers;
using Microsoft.CodeAnalysis.Operations;

namespace Microsoft.NetCore.Analyzers.Runtime
{
    /// <summary>CA1830: Prefer strongly-typed StringBuilder.Append overloads.</summary>
    [ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic), Shared]
    public sealed class PreferTypedStringBuilderAppendOverloadsFixer : CodeFixProvider
    {
        private static readonly string s_removeToStringTitle = MicrosoftNetCoreAnalyzersResources.PreferTypedStringBuilderAppendOverloadsRemoveToString;
        private static readonly string s_replaceStringConstructorTitle = MicrosoftNetCoreAnalyzersResources.PreferTypedStringBuilderAppendOverloadsReplaceStringConstructor;

        public sealed override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(PreferTypedStringBuilderAppendOverloads.RuleId);

        // The two shapes carry different fix titles, and so different equivalence keys, which
        // SyntaxEditorFixAllProvider does not filter on - so the state is the key to apply.
        public sealed override FixAllProvider GetFixAllProvider()
            => SyntaxEditorFixAllProvider.Create<string?>(context => context.CodeActionEquivalenceKey, ApplyFixAsync);

        public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
        {
            Document doc = context.Document;
            CancellationToken cancellationToken = context.CancellationToken;
            SyntaxNode root = await doc.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
            SemanticModel model = await doc.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);

            if (GetTitle(model, root.FindNode(context.Span), cancellationToken) is not string title)
            {
                return;
            }

            ImmutableArray<Diagnostic> diagnostics = context.Diagnostics;

            context.RegisterCodeFix(
                CodeAction.Create(
                    title,
                    cancellationToken => SyntaxEditorFixAllProvider.ApplyFixesAsync(
                        doc,
                        diagnostics,
                        (document, diagnostic, editor, token) => ApplyFixAsync(document, diagnostic, editor, title, token),
                        cancellationToken),
                    equivalenceKey: title),
                diagnostics);
        }

        private static async Task ApplyFixAsync(Document document, Diagnostic diagnostic, SyntaxEditor editor, string? equivalenceKey, CancellationToken cancellationToken)
        {
            SemanticModel model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
            SyntaxNode expression = editor.OriginalRoot.FindNode(diagnostic.Location.SourceSpan);
            IOperation? operation = model.GetOperationWalkingUpParentChain(expression, cancellationToken);

            if (GetTitle(operation) is not string title ||
                (equivalenceKey is not null && title != equivalenceKey))
            {
                return;
            }

            // Handle ToString() case
            if (title == s_removeToStringTitle)
            {
                SyntaxNode replacement = ((IInvocationOperation)((IArgumentOperation)operation!).Value).Instance!.Syntax;

                editor.TrackNode(replacement);
                editor.ReplaceNode(expression, (currentNode, generator) => generator.Argument(currentNode.GetCurrentNode(replacement) ?? replacement));
            }
            // Handle new string(char, int) case (only for Append, not Insert)
            else
            {
                var argOp = (IArgumentOperation)operation!;
                var objectCreation = (IObjectCreationOperation)argOp.Value;
                var invocationOp = (IInvocationOperation)argOp.Parent!;

                // Get the char and int arguments from the string constructor
                SyntaxNode instance = invocationOp.Instance!.Syntax;
                SyntaxNode charArgSyntax = objectCreation.Arguments[0].Value.Syntax;
                SyntaxNode intArgSyntax = objectCreation.Arguments[1].Value.Syntax;

                editor.TrackNode(instance);
                editor.TrackNode(charArgSyntax);
                editor.TrackNode(intArgSyntax);

                // Append(new string(c, count)) -> Append(c, count)
                editor.ReplaceNode(invocationOp.Syntax, (currentNode, generator) =>
                {
                    SyntaxNode Current(SyntaxNode original) => currentNode.GetCurrentNode(original) ?? original;

                    return generator.InvocationExpression(
                        generator.MemberAccessExpression(Current(instance), "Append"),
                        generator.Argument(Current(charArgSyntax)),
                        generator.Argument(Current(intArgSyntax)));
                });
            }
        }

        private static string? GetTitle(SemanticModel model, SyntaxNode? expression, CancellationToken cancellationToken)
            => expression is null ? null : GetTitle(model.GetOperationWalkingUpParentChain(expression, cancellationToken));

        private static string? GetTitle(IOperation? operation)
        {
            if (operation is not IArgumentOperation argument)
            {
                return null;
            }

            if (argument.Value is IInvocationOperation invoke && invoke.Instance is not null)
            {
                return s_removeToStringTitle;
            }

            return argument.Value is IObjectCreationOperation objectCreation &&
                objectCreation.Arguments.Length == 2 &&
                argument.Parent is IInvocationOperation invocationOp &&
                invocationOp.TargetMethod.Name == "Append" &&
                invocationOp.Instance is not null
                ? s_replaceStringConstructorTitle
                : null;
        }
    }
}