File: Microsoft.NetCore.Analyzers\Performance\CSharpPreferDictionaryTryMethodsOverContainsKeyGuardFixer.cs
Web Access
Project: src\sdk\src\Microsoft.CodeAnalysis.NetAnalyzers\src\Microsoft.CodeAnalysis.CSharp.NetAnalyzers\Microsoft.CodeAnalysis.CSharp.NetAnalyzers.csproj (Microsoft.CodeAnalysis.CSharp.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 Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Simplification;
using Microsoft.NetCore.Analyzers.Performance;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;

namespace Microsoft.NetCore.CSharp.Analyzers.Performance
{
    [ExportCodeFixProvider(LanguageNames.CSharp), Shared]
    public sealed class CSharpPreferDictionaryTryMethodsOverContainsKeyGuardFixer : PreferDictionaryTryMethodsOverContainsKeyGuardFixer
    {
        private const string Var = "var";

        public override async Task RegisterCodeFixesAsync(CodeFixContext context)
        {
            var diagnostic = context.Diagnostics.FirstOrDefault();
            if (diagnostic is not { AdditionalLocations.Count: > 0 })
            {
                return;
            }

            Document document = context.Document;
            SyntaxNode root = await document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);

            if (diagnostic.Id == PreferDictionaryTryMethodsOverContainsKeyGuardAnalyzer.PreferTryGetValueRuleId)
            {
                var model = await document.GetRequiredSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
                if (TryGetTryGetValueFix(diagnostic, root, model, context.CancellationToken, out _))
                {
                    RegisterCodeFix(context, PreferDictionaryTryGetValueCodeFixTitle, TryGetValueEquivalenceKey);
                }
            }
            else if (TryGetTryAddFix(diagnostic, root, out _))
            {
                RegisterCodeFix(context, PreferDictionaryTryAddValueCodeFixTitle, TryAddEquivalenceKey);
            }
        }

        protected override async Task ApplyFixAsync(Document document, Diagnostic diagnostic, SyntaxEditor editor, FixAllState state, CancellationToken cancellationToken)
        {
            if (diagnostic.Id == PreferDictionaryTryMethodsOverContainsKeyGuardAnalyzer.PreferTryGetValueRuleId)
            {
                if (state.EquivalenceKey is not null && state.EquivalenceKey != TryGetValueEquivalenceKey)
                {
                    return;
                }

                var model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
                if (TryGetTryGetValueFix(diagnostic, editor.OriginalRoot, model, cancellationToken, out TryGetValueFix tryGetValueFix))
                {
                    ApplyTryGetValueFix(editor, model, state, tryGetValueFix, cancellationToken);
                }
            }
            else
            {
                if (state.EquivalenceKey is not null && state.EquivalenceKey != TryAddEquivalenceKey)
                {
                    return;
                }

                if (TryGetTryAddFix(diagnostic, editor.OriginalRoot, out TryAddFix tryAddFix))
                {
                    ApplyTryAddFix(editor, tryAddFix);
                }
            }
        }

        private static bool TryGetContainsKeyInvocation(Diagnostic diagnostic, SyntaxNode root, out InvocationExpressionSyntax containsKeyInvocation, out MemberAccessExpressionSyntax containsKeyAccess)
        {
            if (root.FindNode(diagnostic.Location.SourceSpan) is InvocationExpressionSyntax
                {
                    Expression: MemberAccessExpressionSyntax access
                } invocation)
            {
                containsKeyInvocation = invocation;
                containsKeyAccess = access;

                return true;
            }

            containsKeyInvocation = null!;
            containsKeyAccess = null!;

            return false;
        }

        private static bool TryGetTryGetValueFix(Diagnostic diagnostic, SyntaxNode root, SemanticModel model, CancellationToken cancellationToken, out TryGetValueFix fix)
        {
            fix = default;

            if (!TryGetContainsKeyInvocation(diagnostic, root, out var containsKeyInvocation, out var containsKeyAccess))
            {
                return false;
            }

            var dictionaryAccessors = ImmutableArray.CreateBuilder<SyntaxNode>();
            ExpressionStatementSyntax? addStatementNode = null;
            SyntaxNode? changedValueNode = null;
            string? variableName = null;
            LocalDeclarationStatementSyntax? localDeclarationStatement = null;
            VariableDeclaratorSyntax? variableDeclarator = null;
            var additionalNodes = 0;
            SyntaxNode? typeNode = null;
            foreach (var location in diagnostic.AdditionalLocations)
            {
                var node = root.FindNode(location.SourceSpan, getInnermostNodeForTie: true);
                switch (node)
                {
                    case ElementAccessExpressionSyntax:
                        dictionaryAccessors.Add(node);
                        typeNode ??= node;
                        break;
                    case ExpressionStatementSyntax exp:
                        if (addStatementNode != null)
                            return false;

                        addStatementNode = exp;
                        additionalNodes++;
                        switch (addStatementNode.Expression)
                        {
                            case AssignmentExpressionSyntax assign:
                                changedValueNode = assign.Right;
                                break;
                            case InvocationExpressionSyntax invocation:
                                changedValueNode = invocation.ArgumentList.Arguments[1].Expression;
                                break;
                            default:
                                return false;
                        }

                        break;
                    case LocalDeclarationStatementSyntax local:
                        localDeclarationStatement = local;
                        variableName = local.Declaration.Variables[0].Identifier.ValueText;
                        additionalNodes++;
                        typeNode ??= local.Declaration.Type;
                        break;
                    case VariableDeclaratorSyntax
                    {
                        Parent: VariableDeclarationSyntax
                        {
                            Parent: LocalDeclarationStatementSyntax local
                        }
                    } declarator:
                        variableDeclarator = declarator;
                        localDeclarationStatement = local;
                        variableName = declarator.Identifier.ValueText;
                        additionalNodes++;
                        typeNode ??= local.Declaration.Type;
                        break;
                }
            }

            if (diagnostic.AdditionalLocations.Count != dictionaryAccessors.Count + additionalNodes)
                return false;

            fix = new TryGetValueFix(
                containsKeyInvocation,
                containsKeyAccess,
                dictionaryAccessors.ToImmutable(),
                addStatementNode,
                changedValueNode,
                variableName,
                localDeclarationStatement,
                variableDeclarator,
                model.GetTypeInfo(typeNode!, cancellationToken).Type);

            return true;
        }

        private static void ApplyTryGetValueFix(SyntaxEditor editor, SemanticModel model, FixAllState state, TryGetValueFix fix, CancellationToken cancellationToken)
        {
            var generator = editor.Generator;

            // Roslyn has reducers that are run after a code action is applied, one of which will
            // simplify a TypeSyntax to `var` if the user prefers that. So we generate TypeSyntax, add
            // simplifier annotation, and then let Roslyn decide whether to keep TypeSyntax or convert it to var.
            // If the type is unknown (null) (likely in error scenario), then fallback to using var.
            TypeSyntax typeSyntax;
            if (fix.Type is not null)
            {
                typeSyntax = (TypeSyntax)generator.TypeExpression(fix.Type);
                if (fix.Type.IsReferenceType)
                    typeSyntax = (TypeSyntax)generator.NullableTypeExpression(typeSyntax);

                typeSyntax = typeSyntax.WithAdditionalAnnotations(Simplifier.Annotation);
            }
            else
            {
                typeSyntax = IdentifierName(Var);
            }

            var identifierName = (IdentifierNameSyntax)(fix.VariableName is not null
                ? generator.IdentifierName(fix.VariableName)
                : generator.FirstUnusedIdentifierName(model, fix.ContainsKeyInvocation.SpanStart, Value,
                    reservedNames: state.GetReservedNames(model, fix.ContainsKeyInvocation.SpanStart, cancellationToken)));
            state.RecordIntroducedName(model, fix.ContainsKeyInvocation.SpanStart, identifierName.Identifier.ValueText, cancellationToken);

            var outArgument = (ArgumentSyntax)generator.Argument(RefKind.Out,
                DeclarationExpression(
                    typeSyntax,
                    SingleVariableDesignation(identifierName.Identifier)
                )
            );

            var tryGetValueInvocation = fix.ContainsKeyInvocation
                .ReplaceNode(fix.ContainsKeyAccess.Name, IdentifierName(TryGetValue).WithTriviaFrom(fix.ContainsKeyAccess.Name))
                .AddArgumentListArguments(outArgument);
            editor.ReplaceNode(fix.ContainsKeyInvocation, tryGetValueInvocation);

            if (fix.AddStatementNode is not null && fix.ChangedValueNode is not null)
            {
                editor.InsertBefore(fix.AddStatementNode,
                    generator.ExpressionStatement(generator.AssignmentStatement(identifierName, fix.ChangedValueNode)));
                editor.ReplaceNode(fix.ChangedValueNode, identifierName);
            }

            foreach (var dictionaryAccess in fix.DictionaryAccessors)
            {
                switch (dictionaryAccess.Parent)
                {
                    case PostfixUnaryExpressionSyntax { RawKind: (int)SyntaxKind.PostDecrementExpression } post:
                        editor.ReplaceNode(post, generator.AssignmentStatement(dictionaryAccess,
                            PrefixUnaryExpression(SyntaxKind.PreDecrementExpression, identifierName)).
                            WithTriviaFrom(post));
                        break;
                    case PostfixUnaryExpressionSyntax { RawKind: (int)SyntaxKind.PostIncrementExpression } post:
                        editor.ReplaceNode(post, generator.AssignmentStatement(dictionaryAccess,
                            PrefixUnaryExpression(SyntaxKind.PreIncrementExpression, identifierName)).
                            WithTriviaFrom(post));
                        break;
                    case PrefixUnaryExpressionSyntax pre:
                        editor.ReplaceNode(pre, generator.AssignmentStatement(dictionaryAccess,
                            pre.WithOperand(identifierName)).WithTriviaFrom(pre));
                        break;
                    default:
                        editor.ReplaceNode(dictionaryAccess, identifierName);
                        break;
                }
            }

            if (fix.LocalDeclarationStatement is not null)
            {
                if (fix.VariableDeclarator is null)
                {
                    editor.RemoveNode(fix.LocalDeclarationStatement);
                }
                else
                {
                    editor.RemoveNode(fix.VariableDeclarator);
                }
            }
        }

        private static bool TryGetTryAddFix(Diagnostic diagnostic, SyntaxNode root, out TryAddFix fix)
        {
            fix = default;

            if (!TryGetContainsKeyInvocation(diagnostic, root, out var containsKeyInvocation, out var containsKeyAccess))
            {
                return false;
            }

            var dictionaryAdd = root.FindNode(diagnostic.AdditionalLocations[0].SourceSpan, getInnermostNodeForTie: true);
            if (dictionaryAdd is not InvocationExpressionSyntax dictionaryAddInvocation)
            {
                return false;
            }

            var ifStatement = containsKeyInvocation.FirstAncestorOrSelf<IfStatementSyntax>();
            if (ifStatement is null)
            {
                return false;
            }

            fix = new TryAddFix(containsKeyInvocation, containsKeyAccess, dictionaryAddInvocation, ifStatement);

            return true;
        }

        private static void ApplyTryAddFix(SyntaxEditor editor, TryAddFix fix)
        {
            var generator = editor.Generator;

            var tryAddValueAccess = generator.MemberAccessExpression(fix.ContainsKeyAccess.Expression, TryAdd);
            var dictionaryAddArguments = fix.DictionaryAddInvocation.ArgumentList.Arguments;
            var tryAddInvocation = generator.InvocationExpression(tryAddValueAccess, dictionaryAddArguments[0], dictionaryAddArguments[1]);
            var ifStatement = fix.IfStatement;

            if (ifStatement.Condition is PrefixUnaryExpressionSyntax unary && unary.IsKind(SyntaxKind.LogicalNotExpression))
            {
                if (ifStatement.Statement is BlockSyntax { Statements.Count: 1 } or ExpressionStatementSyntax)
                {
                    if (ifStatement.Else is null)
                    {
                        // d.Add() is the only statement in the if and is guarded with a !d.ContainsKey().
                        // Since there is no else-branch, we can replace the entire if-statement with a d.TryAdd() call.
                        var invocationWithTrivia = tryAddInvocation.WithTriviaFrom(ifStatement);
                        editor.ReplaceNode(ifStatement, generator.ExpressionStatement(invocationWithTrivia));
                    }
                    else
                    {
                        // d.Add() is the only statement in the if and is guarded with a !d.ContainsKey().
                        // In this case, we switch out the !d.ContainsKey() call with a !d.TryAdd() call and move the else-branch into the if.
                        editor.ReplaceNode(fix.ContainsKeyInvocation, tryAddInvocation);
                        editor.ReplaceNode(ifStatement.Statement, ifStatement.Else.Statement);
                        editor.RemoveNode(ifStatement.Else, SyntaxRemoveOptions.KeepNoTrivia);
                    }
                }
                else
                {
                    // d.Add() is one of many statements in the if and is guarded with a !d.ContainsKey().
                    // In this case, we switch out the !d.ContainsKey() call for a d.TryAdd() call.
                    editor.RemoveNode(fix.DictionaryAddInvocation.Parent!, SyntaxRemoveOptions.KeepNoTrivia);
                    editor.ReplaceNode(unary, tryAddInvocation);
                }
            }
            else if (ifStatement.Condition.IsKind(SyntaxKind.InvocationExpression) && ifStatement.Else is not null)
            {
                var negatedTryAddInvocation = generator.LogicalNotExpression(tryAddInvocation);
                editor.ReplaceNode(fix.ContainsKeyInvocation, negatedTryAddInvocation);
                if (ifStatement.Else.Statement is BlockSyntax { Statements.Count: 1 } or ExpressionStatementSyntax)
                {
                    // d.Add() is the only statement the else-branch and guarded by a d.ContainsKey() call in the if.
                    // In this case we replace the d.ContainsKey() call with a !d.TryAdd() call and remove the entire else-branch.
                    editor.RemoveNode(ifStatement.Else);
                }
                else
                {
                    // d.Add() is one of many statements in the else-branch and guarded by a d.ContainsKey() call in the if.
                    // In this case we replace the d.ContainsKey() call with a !d.TryAdd() call and remove the d.Add() call in the else-branch.
                    editor.RemoveNode(fix.DictionaryAddInvocation.Parent!, SyntaxRemoveOptions.KeepNoTrivia);
                }
            }
        }

        private readonly struct TryGetValueFix
        {
            public TryGetValueFix(
                InvocationExpressionSyntax containsKeyInvocation,
                MemberAccessExpressionSyntax containsKeyAccess,
                ImmutableArray<SyntaxNode> dictionaryAccessors,
                ExpressionStatementSyntax? addStatementNode,
                SyntaxNode? changedValueNode,
                string? variableName,
                LocalDeclarationStatementSyntax? localDeclarationStatement,
                VariableDeclaratorSyntax? variableDeclarator,
                ITypeSymbol? type)
            {
                ContainsKeyInvocation = containsKeyInvocation;
                ContainsKeyAccess = containsKeyAccess;
                DictionaryAccessors = dictionaryAccessors;
                AddStatementNode = addStatementNode;
                ChangedValueNode = changedValueNode;
                VariableName = variableName;
                LocalDeclarationStatement = localDeclarationStatement;
                VariableDeclarator = variableDeclarator;
                Type = type;
            }

            public InvocationExpressionSyntax ContainsKeyInvocation { get; }

            public MemberAccessExpressionSyntax ContainsKeyAccess { get; }

            public ImmutableArray<SyntaxNode> DictionaryAccessors { get; }

            public ExpressionStatementSyntax? AddStatementNode { get; }

            public SyntaxNode? ChangedValueNode { get; }

            /// <summary>
            /// The name of the local the value is already read into, or <see langword="null"/> when the fix
            /// has to introduce one.
            /// </summary>
            public string? VariableName { get; }

            public LocalDeclarationStatementSyntax? LocalDeclarationStatement { get; }

            public VariableDeclaratorSyntax? VariableDeclarator { get; }

            public ITypeSymbol? Type { get; }
        }

        private readonly struct TryAddFix
        {
            public TryAddFix(
                InvocationExpressionSyntax containsKeyInvocation,
                MemberAccessExpressionSyntax containsKeyAccess,
                InvocationExpressionSyntax dictionaryAddInvocation,
                IfStatementSyntax ifStatement)
            {
                ContainsKeyInvocation = containsKeyInvocation;
                ContainsKeyAccess = containsKeyAccess;
                DictionaryAddInvocation = dictionaryAddInvocation;
                IfStatement = ifStatement;
            }

            public InvocationExpressionSyntax ContainsKeyInvocation { get; }

            public MemberAccessExpressionSyntax ContainsKeyAccess { get; }

            public InvocationExpressionSyntax DictionaryAddInvocation { get; }

            public IfStatementSyntax IfStatement { get; }
        }
    }
}