File: Microsoft.NetCore.Analyzers\Runtime\PreferConstCharOverConstUnitString.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.Generic;
using System.Collections.Immutable;
using System.Composition;
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.NetAnalyzers;
using Microsoft.CodeAnalysis.Operations;

namespace Microsoft.NetCore.Analyzers.Runtime
{
    [ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic), Shared]
    public class PreferConstCharOverConstUnitStringFixer : CodeFixProvider
    {
        public sealed override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(PreferConstCharOverConstUnitStringAnalyzer.RuleId);

        // Several `Append` calls can reference one const local, and the fix rewrites that local's declaration,
        // so a fix-all pass has to rewrite it once rather than once per call.
        public sealed override FixAllProvider GetFixAllProvider()
            => SyntaxEditorFixAllProvider.Create<HashSet<SyntaxNode>>(
                static _ => new HashSet<SyntaxNode>(),
                ApplyFixAsync);

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

            if (await TryGetFixAsync(root.FindNode(context.Span), semanticModel, cancellationToken).ConfigureAwait(false) is null)
            {
                return;
            }

            ImmutableArray<Diagnostic> diagnostics = context.Diagnostics;
            HashSet<SyntaxNode> rewritten = new();

            context.RegisterCodeFix(
                CodeAction.Create(
                    title: MicrosoftNetCoreAnalyzersResources.PreferConstCharOverConstUnitStringInStringBuilderTitle,
                    createChangedDocument: cancellationToken => SyntaxEditorFixAllProvider.ApplyFixesAsync(
                        document,
                        diagnostics,
                        (document, diagnostic, editor, cancellationToken) => ApplyFixAsync(document, diagnostic, editor, rewritten, cancellationToken),
                        cancellationToken),
                    equivalenceKey: MicrosoftNetCoreAnalyzersResources.PreferConstCharOverConstUnitStringInStringBuilderMessage),
                diagnostics);
        }

        private static async Task ApplyFixAsync(Document document, Diagnostic diagnostic, SyntaxEditor editor, HashSet<SyntaxNode> rewritten, CancellationToken cancellationToken)
        {
            SemanticModel semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
            SyntaxNode node = editor.OriginalRoot.FindNode(diagnostic.Location.SourceSpan);

            if (await TryGetFixAsync(node, semanticModel, cancellationToken).ConfigureAwait(false) is not { } fix ||
                !rewritten.Add(fix.Target))
            {
                return;
            }

            (SyntaxNode target, char charValue, string? localName) = fix;

            SyntaxGenerator generator = editor.Generator;
            SyntaxNode charLiteralExpressionNode = generator.LiteralExpression(charValue);

            if (localName is null)
            {
                // Both replacements are generated from the constant value alone, so neither carries any of the
                // syntax it replaces over into the new tree and neither needs to re-read the current node.
                editor.ReplaceNode(target, charLiteralExpressionNode);
                return;
            }

            SyntaxNode charTypeNode = generator.TypeExpression(SpecialType.System_Char);
            SyntaxNode charSyntaxNode = generator.LocalDeclarationStatement(charTypeNode, localName, charLiteralExpressionNode, isConst: true);
            editor.ReplaceNode(target, charSyntaxNode.WithTriviaFrom(target));
        }

        /// <summary>
        /// Returns the node the fix replaces — the reported string literal, or the whole declaration of the
        /// const local the argument references — the character it becomes, and the local's name when it is a
        /// declaration. <see langword="null"/> when the shape is not one the fix handles.
        /// </summary>
        private static async Task<(SyntaxNode Target, char CharValue, string? LocalName)?> TryGetFixAsync(
            SyntaxNode node, SemanticModel semanticModel, CancellationToken cancellationToken)
        {
            if (semanticModel.GetOperation(node, cancellationToken) is not IArgumentOperation argumentOperation)
            {
                return null;
            }

            if (argumentOperation.Value is ILiteralOperation literalOperation)
            {
                return (literalOperation.Syntax, ((string)literalOperation.ConstantValue.Value!)[0], LocalName: null);
            }

            if (argumentOperation.Value is not ILocalReferenceOperation localReferenceOperation ||
                localReferenceOperation.Local.DeclaringSyntaxReferences.FirstOrDefault() is not SyntaxReference declaringSyntaxReference)
            {
                return null;
            }

            SyntaxNode declaringSyntax = await declaringSyntaxReference.GetSyntaxAsync(cancellationToken).ConfigureAwait(false);

            if (semanticModel.GetOperationWalkingUpParentChain(declaringSyntax, cancellationToken) is not IVariableDeclaratorOperation variableDeclaratorOperation ||
                variableDeclaratorOperation.GetVariableInitializer() is not IVariableInitializerOperation variableInitializerOperation ||
                variableDeclaratorOperation.Parent is not IVariableDeclarationOperation { Declarators.Length: 1 } variableDeclarationOperation ||
                variableDeclarationOperation.Parent is not IVariableDeclarationGroupOperation { Declarations.Length: 1 } variableGroupDeclarationOperation)
            {
                return null;
            }

            return (variableGroupDeclarationOperation.Syntax, ((string)variableInitializerOperation.Value.ConstantValue.Value!)[0], variableDeclaratorOperation.Symbol.Name);
        }
    }
}