File: src\Analyzers\CSharp\CodeFixes\RemoveUnusedParametersAndValues\CSharpRemoveUnusedValuesCodeFixProvider.cs
Web Access
Project: src\src\Features\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.Features.csproj (Microsoft.CodeAnalysis.CSharp.Features)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System.Collections.Generic;
using System.Composition;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Formatting;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.RemoveUnusedParametersAndValues;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.RemoveUnusedParametersAndValues;
 
using static SyntaxFactory;
 
[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.RemoveUnusedValues), Shared]
[ExtensionOrder(After = PredefinedCodeFixProviderNames.AddImport)]
[method: ImportingConstructor]
[method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")]
internal sealed class CSharpRemoveUnusedValuesCodeFixProvider()
    : AbstractRemoveUnusedValuesCodeFixProvider<ExpressionSyntax, StatementSyntax, BlockSyntax,
        ExpressionStatementSyntax, LocalDeclarationStatementSyntax, VariableDeclaratorSyntax,
        ForEachStatementSyntax, SwitchSectionSyntax, SwitchLabelSyntax, CatchClauseSyntax, CatchClauseSyntax>
{
    protected override ISyntaxFormatting SyntaxFormatting
        => CSharpSyntaxFormatting.Instance;
 
    protected override BlockSyntax WrapWithBlockIfNecessary(IEnumerable<StatementSyntax> statements)
        => Block(statements);
 
    protected override SyntaxToken GetForEachStatementIdentifier(ForEachStatementSyntax node)
        => node.Identifier;
 
    protected override LocalDeclarationStatementSyntax? GetCandidateLocalDeclarationForRemoval(VariableDeclaratorSyntax declarator)
        => declarator.Parent?.Parent as LocalDeclarationStatementSyntax;
 
    protected override SyntaxNode? TryUpdateNameForFlaggedNode(SyntaxNode node, SyntaxToken newName)
    {
        switch (node.Kind())
        {
            case SyntaxKind.IdentifierName:
                var identifierName = (IdentifierNameSyntax)node;
                return identifierName.WithIdentifier(newName.WithTriviaFrom(identifierName.Identifier));
 
            case SyntaxKind.VariableDeclarator:
                var variableDeclarator = (VariableDeclaratorSyntax)node;
                if (newName.ValueText == AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.DiscardVariableName &&
                    variableDeclarator.Initializer?.Value is ImplicitObjectCreationExpressionSyntax implicitObjectCreation &&
                    variableDeclarator.Parent is VariableDeclarationSyntax parent)
                {
                    // If we are generating a discard on the left of an initialization with an implicit object creation on the right,
                    // then we need to replace the implicit object creation with an explicit one.
                    // For example: 'TypeName v = new();' must be changed to '_ = new TypeName();'
                    var objectCreationNode = ObjectCreationExpression(
                        newKeyword: implicitObjectCreation.NewKeyword,
                        type: parent.Type,
                        argumentList: implicitObjectCreation.ArgumentList,
                        initializer: implicitObjectCreation.Initializer);
                    variableDeclarator = variableDeclarator.WithInitializer(variableDeclarator.Initializer.WithValue(objectCreationNode));
                }
 
                return variableDeclarator.WithIdentifier(newName.WithTriviaFrom(variableDeclarator.Identifier));
 
            case SyntaxKind.SingleVariableDesignation:
                return newName.ValueText == AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.DiscardVariableName
                    ? DiscardDesignation().WithTriviaFrom(node)
                    : SingleVariableDesignation(newName).WithTriviaFrom(node);
 
            case SyntaxKind.CatchDeclaration:
                var catchDeclaration = (CatchDeclarationSyntax)node;
                return catchDeclaration.WithIdentifier(newName.WithTriviaFrom(catchDeclaration.Identifier));
 
            case SyntaxKind.VarPattern:
                return node.IsParentKind(SyntaxKind.Subpattern)
                    ? DiscardPattern().WithTriviaFrom(node)
                    : DiscardDesignation();
 
            default:
                Debug.Fail($"Unexpected node kind for local/parameter declaration or reference: '{node.Kind()}'");
                return null;
        }
    }
 
    protected override SyntaxNode? TryUpdateParentOfUpdatedNode(SyntaxNode parent, SyntaxNode newNameNode, SyntaxEditor editor, ISyntaxFacts syntaxFacts, SemanticModel semanticModel)
    {
        if (newNameNode.IsKind(SyntaxKind.DiscardDesignation))
        {
            var triviaToAppend = newNameNode.GetLeadingTrivia().AddRange(newNameNode.GetTrailingTrivia());
 
            // 1) `... is MyType variable` -> `... is MyType`
            // 2) `... is MyType /*1*/ variable /*2*/` -> `... is MyType /*1*/  /*2*/`
            if (parent is DeclarationPatternSyntax declarationPattern &&
                parent.SyntaxTree.Options.LanguageVersion() >= LanguageVersion.CSharp9)
            {
                var trailingTrivia = declarationPattern.Type.GetTrailingTrivia().AddRange(triviaToAppend);
                return TypePattern(declarationPattern.Type).WithTrailingTrivia(trailingTrivia);
            }
 
            // 1) `... is { } variable` -> `... is { }`
            // 2) `... is { } /*1*/ variable /*2*/` -> `... is { } /*1*/  /*2*/`
            if (parent is RecursivePatternSyntax recursivePattern)
            {
                var withoutDesignation = recursivePattern.WithDesignation(null);
                return withoutDesignation.WithAppendedTrailingTrivia(triviaToAppend);
            }
 
            // 1) `... is [] variable` -> `... is []`
            // 2) `... is [] /*1*/ variable /*2*/` -> `... is [] /*1*/  /*2*/`
            if (parent is ListPatternSyntax listPattern)
            {
                var withoutDesignation = listPattern.WithDesignation(null);
                return withoutDesignation.WithAppendedTrailingTrivia(triviaToAppend);
            }
        }
        else if (parent is AssignmentExpressionSyntax assignment &&
            assignment.Right is ImplicitObjectCreationExpressionSyntax implicitObjectCreation &&
            newNameNode is IdentifierNameSyntax { Identifier.ValueText: AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.DiscardVariableName } &&
            semanticModel.GetTypeInfo(implicitObjectCreation).Type is { } type)
        {
            // If we are generating a discard on the left of an assignment with an implicit object creation on the right,
            // then we need to replace the implicit object creation with an explicit one.
            // For example: 'v = new();' must be changed to '_ = new TypeOfV();'
            var objectCreationNode = ObjectCreationExpression(
                newKeyword: implicitObjectCreation.NewKeyword,
                type: type.GenerateTypeSyntax(allowVar: false),
                argumentList: implicitObjectCreation.ArgumentList,
                initializer: implicitObjectCreation.Initializer);
            return assignment.Update((ExpressionSyntax)newNameNode, assignment.OperatorToken, objectCreationNode);
        }
 
        return null;
    }
 
    protected override SyntaxNode ComputeReplacementNode(SyntaxNode originalOldNode, SyntaxNode changedOldNode, SyntaxNode proposedReplacementNode)
    {
        // Check for the following change: `{ ... } variable` -> `{ ... }`
        // Since the internals of this patterns might be changed during previous iterations
        // we apply the same change (remove `variable` declaration) to the `changedOldNode`
        if (originalOldNode is RecursivePatternSyntax originalOldRecursivePattern &&
            proposedReplacementNode is RecursivePatternSyntax proposedReplacementRecursivePattern &&
            proposedReplacementRecursivePattern.IsEquivalentTo(originalOldRecursivePattern.WithDesignation(null)))
        {
            proposedReplacementNode = ((RecursivePatternSyntax)changedOldNode).WithDesignation(null)
                                                                              .WithTrailingTrivia(proposedReplacementNode.GetTrailingTrivia());
        }
 
        // Check for the following change: `[...] variable` -> `[...]`
        // Since the internals of this patterns might be changed during previous iterations
        // we apply the same change (remove `variable` declaration) to the `changedOldNode`
        if (originalOldNode is ListPatternSyntax originalOldListPattern &&
            proposedReplacementNode is ListPatternSyntax proposedReplacementListPattern &&
            proposedReplacementListPattern.IsEquivalentTo(originalOldListPattern.WithDesignation(null)))
        {
            proposedReplacementNode = ((ListPatternSyntax)changedOldNode).WithDesignation(null)
                                                                         .WithTrailingTrivia(proposedReplacementNode.GetTrailingTrivia());
        }
 
        return proposedReplacementNode.WithAdditionalAnnotations(Formatter.Annotation);
    }
 
    protected override void InsertAtStartOfSwitchCaseBlockForDeclarationInCaseLabelOrClause(SwitchSectionSyntax switchCaseBlock, SyntaxEditor editor, LocalDeclarationStatementSyntax declarationStatement)
    {
        var firstStatement = switchCaseBlock.Statements.FirstOrDefault();
        if (firstStatement != null)
        {
            editor.InsertBefore(firstStatement, declarationStatement);
        }
        else
        {
            // Switch section without any statements is an error case.
            // Insert before containing switch statement.
            editor.InsertBefore(switchCaseBlock.GetRequiredParent(), declarationStatement);
        }
    }
 
    protected override SyntaxNode GetReplacementNodeForCompoundAssignment(
        SyntaxNode originalCompoundAssignment,
        SyntaxNode newAssignmentTarget,
        SyntaxEditor editor,
        ISyntaxFactsService syntaxFacts)
    {
        // 1. Compound assignment is changed to simple assignment.
        // For example, "x += MethodCall();", where assignment to 'x' is redundant
        // is replaced with "_ = MethodCall();" or "var unused = MethodCall();
        //
        // 2. Null coalesce assignment is changed to assignment with null coalesce
        // expression on the right.
        // For example, "x ??= MethodCall();", where assignment to 'x' is redundant
        // is replaced with "_ = x ?? MethodCall();" or "var unused = x ?? MethodCall();
        //
        // 3. However, if the node is not parented by an expression statement then we
        // don't generate an assignment, but just the expression.
        // For example, "return x += MethodCall();" is replaced with "return x + MethodCall();"
        // and "return x ??= MethodCall();" is replaced with "return x ?? MethodCall();"
 
        if (originalCompoundAssignment is not AssignmentExpressionSyntax assignmentExpression)
        {
            Debug.Fail($"Unexpected kind for originalCompoundAssignment: {originalCompoundAssignment.Kind()}");
            return originalCompoundAssignment;
        }
 
        var leftOfAssignment = assignmentExpression.Left;
        var rightOfAssignment = assignmentExpression.Right;
 
        if (originalCompoundAssignment.Parent.IsKind(SyntaxKind.ExpressionStatement))
        {
            if (!originalCompoundAssignment.IsKind(SyntaxKind.CoalesceAssignmentExpression))
            {
                // Case 1. Simple compound assignment parented by an expression statement.
                return editor.Generator.AssignmentStatement(newAssignmentTarget, rightOfAssignment);
            }
            else
            {
                // Case 2. Null coalescing compound assignment parented by an expression statement.
                // Remove leading trivia from 'leftOfAssignment' as it should have been moved to 'newAssignmentTarget'.
                leftOfAssignment = leftOfAssignment.WithoutLeadingTrivia();
                return editor.Generator.AssignmentStatement(newAssignmentTarget,
                    BinaryExpression(SyntaxKind.CoalesceExpression, leftOfAssignment, rightOfAssignment));
            }
        }
        else
        {
            // Case 3. Compound assignment not parented by an expression statement.
            var mappedBinaryExpressionKind = originalCompoundAssignment.Kind().MapCompoundAssignmentKindToBinaryExpressionKind();
            if (mappedBinaryExpressionKind == SyntaxKind.None)
            {
                return originalCompoundAssignment;
            }
 
            return BinaryExpression(mappedBinaryExpressionKind, leftOfAssignment, rightOfAssignment);
        }
    }
 
    protected override SyntaxNode GetReplacementNodeForVarPattern(SyntaxNode originalVarPattern, SyntaxNode newNameNode)
    {
        if (originalVarPattern is not VarPatternSyntax pattern)
            throw ExceptionUtilities.Unreachable();
 
        // If the replacement node is DiscardDesignationSyntax
        // then we need to just change the incoming var's pattern designation
        if (newNameNode is DiscardDesignationSyntax discardDesignation)
        {
            return pattern.WithDesignation(discardDesignation.WithTriviaFrom(pattern.Designation));
        }
 
        // Otherwise just return new node as a replacement.
        // This would be the default behavior if there was no special case described above
        return newNameNode;
    }
}