// 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; } } } }