File: Microsoft.NetCore.Analyzers\Performance\PreferDictionaryTryMethodsOverContainsKeyGuardFixer.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.Generic;
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.NetAnalyzers;

namespace Microsoft.NetCore.Analyzers.Performance
{
    public abstract class PreferDictionaryTryMethodsOverContainsKeyGuardFixer : CodeFixProvider
    {
        protected const string Value = "value";
        protected const string TryGetValue = nameof(TryGetValue);
        protected const string TryAdd = nameof(TryAdd);

        protected const string TryGetValueEquivalenceKey = nameof(MicrosoftNetCoreAnalyzersResources.PreferDictionaryTryGetValueCodeFixTitle);
        protected const string TryAddEquivalenceKey = nameof(MicrosoftNetCoreAnalyzersResources.PreferDictionaryTryAddValueCodeFixTitle);

        public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(
            PreferDictionaryTryMethodsOverContainsKeyGuardAnalyzer.PreferTryGetValueRuleId,
            PreferDictionaryTryMethodsOverContainsKeyGuardAnalyzer.PreferTryAddRuleId
        );

        protected static string PreferDictionaryTryGetValueCodeFixTitle => MicrosoftNetCoreAnalyzersResources.PreferDictionaryTryGetValueCodeFixTitle;

        protected static string PreferDictionaryTryAddValueCodeFixTitle => MicrosoftNetCoreAnalyzersResources.PreferDictionaryTryAddValueCodeFixTitle;

        public sealed override FixAllProvider GetFixAllProvider()
            => SyntaxEditorFixAllProvider.Create<FixAllState>(context => new FixAllState(context.CodeActionEquivalenceKey), ApplyFixAsync);

        /// <summary>
        /// Registers an action applying <see cref="ApplyFixAsync"/> to every diagnostic in
        /// <paramref name="context"/>, through the same editor and state a fix-all pass would use.
        /// </summary>
        protected void RegisterCodeFix(CodeFixContext context, string title, string equivalenceKey)
        {
            Document document = context.Document;
            ImmutableArray<Diagnostic> diagnostics = context.Diagnostics;
            var state = new FixAllState(equivalenceKey);

            CodeAction codeAction = CodeAction.Create(
                title,
                (cancellationToken) => SyntaxEditorFixAllProvider.ApplyFixesAsync(
                    document,
                    diagnostics,
                    (fixDocument, fixDiagnostic, editor, token) => ApplyFixAsync(fixDocument, fixDiagnostic, editor, state, token),
                    cancellationToken),
                equivalenceKey
            );
            context.RegisterCodeFix(codeAction, diagnostics);
        }

        protected abstract Task ApplyFixAsync(Document document, Diagnostic diagnostic, SyntaxEditor editor, FixAllState state, CancellationToken cancellationToken);

        /// <summary>
        /// The state shared by every fix applied to one document.
        /// </summary>
        protected sealed class FixAllState
        {
            private readonly List<(ISymbol? Scope, string Name)> _introducedNames = new();

            public FixAllState(string? equivalenceKey)
            {
                EquivalenceKey = equivalenceKey;
            }

            /// <summary>
            /// The key of the action being applied. <see cref="SyntaxEditorFixAllProvider"/> does not filter
            /// diagnostics by it, so a fixer offering more than one action has to do so itself.
            /// </summary>
            public string? EquivalenceKey { get; }

            /// <summary>
            /// The locals a previously applied fix introduced into the member containing
            /// <paramref name="position"/>. They are invisible to <paramref name="semanticModel"/>, which is
            /// bound to the document as it was before any fix ran, so without this two guards in one member
            /// would both introduce a local named <see cref="Value"/>.
            /// </summary>
            public ISet<string> GetReservedNames(SemanticModel semanticModel, int position, CancellationToken cancellationToken)
            {
                ISymbol? scope = GetScope(semanticModel, position, cancellationToken);
                var reserved = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

                foreach ((ISymbol? introducedScope, string name) in _introducedNames)
                {
                    if (SymbolEqualityComparer.Default.Equals(introducedScope, scope))
                    {
                        reserved.Add(name);
                    }
                }

                return reserved;
            }

            public void RecordIntroducedName(SemanticModel semanticModel, int position, string name, CancellationToken cancellationToken)
            {
                _introducedNames.Add((GetScope(semanticModel, position, cancellationToken), name));
            }

            /// <summary>
            /// The member a local declared at <paramref name="position"/> shares its name space with. Neither
            /// language lets a local shadow one declared further out in the same member, so a lambda or a
            /// local function resolves to the member containing it.
            /// </summary>
            private static ISymbol? GetScope(SemanticModel semanticModel, int position, CancellationToken cancellationToken)
            {
                ISymbol? symbol = semanticModel.GetEnclosingSymbol(position, cancellationToken);

                while (symbol is IMethodSymbol { MethodKind: MethodKind.AnonymousFunction or MethodKind.LocalFunction } method)
                {
                    symbol = method.ContainingSymbol;
                }

                return symbol;
            }
        }
    }
}