File: Microsoft.NetCore.Analyzers\Runtime\PreferDictionaryContainsMethods.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.Threading;
using System.Threading.Tasks;
using Analyzer.Utilities;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.NetAnalyzers;

namespace Microsoft.NetCore.Analyzers.Runtime
{
    public abstract class PreferDictionaryContainsMethodsFixer : CodeFixProvider
    {
        public sealed override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(PreferDictionaryContainsMethods.RuleId);

        // One title per replaced property, so a fix-all pass has to apply only the one the user invoked it from.
        public sealed override FixAllProvider GetFixAllProvider()
            => SyntaxEditorFixAllProvider.Create<string?>(
                static fixAllContext => fixAllContext.CodeActionEquivalenceKey,
                (document, diagnostic, editor, equivalenceKey, cancellationToken) =>
                {
                    ApplyFix(diagnostic, editor, equivalenceKey);
                    return Task.CompletedTask;
                });

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

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

            Document document = context.Document;
            ImmutableArray<Diagnostic> diagnostics = context.Diagnostics;

            context.RegisterCodeFix(
                CodeAction.Create(
                    title,
                    cancellationToken => SyntaxEditorFixAllProvider.ApplyFixesAsync(
                        document,
                        diagnostics,
                        (_, diagnostic, editor, _) =>
                        {
                            ApplyFix(diagnostic, editor, title);
                            return Task.CompletedTask;
                        },
                        cancellationToken),
                    equivalenceKey: title),
                diagnostics);
        }

        private void ApplyFix(Diagnostic diagnostic, SyntaxEditor editor, string? equivalenceKey)
        {
            SyntaxNode node = editor.OriginalRoot.FindNode(diagnostic.Location.SourceSpan);

            if (GetPropertyName(node) is not string propertyName ||
                (equivalenceKey is not null && equivalenceKey != GetTitle(propertyName)))
            {
                return;
            }

            string methodName = propertyName == PreferDictionaryContainsMethods.KeysPropertyName
                ? PreferDictionaryContainsMethods.ContainsKeyMethodName
                : PreferDictionaryContainsMethods.ContainsValueMethodName;

            // The replacement re-emits the dictionary and the arguments, and a `Keys.Contains` call can sit
            // inside the argument of another, so those are read off the node as an inner fix has already
            // rewritten it.
            editor.ReplaceNode(node, (currentNode, generator) => Rewrite(currentNode, methodName, generator) ?? currentNode);
        }

        private string? GetTitle(SyntaxNode node)
            => GetPropertyName(node) is string propertyName ? GetTitle(propertyName) : null;

        private static string GetTitle(string propertyName)
            => propertyName == PreferDictionaryContainsMethods.KeysPropertyName
                ? MicrosoftNetCoreAnalyzersResources.PreferDictionaryContainsKeyCodeFixTitle
                : MicrosoftNetCoreAnalyzersResources.PreferDictionaryContainsValueCodeFixTitle;

        /// <summary>
        /// Returns <see cref="PreferDictionaryContainsMethods.KeysPropertyName"/> or
        /// <see cref="PreferDictionaryContainsMethods.ValuesPropertyName"/> for the property
        /// <paramref name="invocation"/> calls <c>Contains</c> on, or <see langword="null"/> when it is not a
        /// shape the fix handles.
        /// </summary>
        protected abstract string? GetPropertyName(SyntaxNode invocation);

        /// <summary>
        /// Rewrites <paramref name="invocation"/> as a call to <paramref name="methodName"/> on the dictionary
        /// itself, or returns <see langword="null"/> when it is not a shape the fix handles.
        /// </summary>
        protected abstract SyntaxNode? Rewrite(SyntaxNode invocation, string methodName, SyntaxGenerator generator);
    }
}