File: src\Analyzers\Core\CodeFixes\RemoveUnusedParametersAndValues\AbstractRemoveUnusedValuesCodeFixProvider.cs
Web Access
Project: src\src\CodeStyle\Core\CodeFixes\Microsoft.CodeAnalysis.CodeStyle.Fixes.csproj (Microsoft.CodeAnalysis.CodeStyle.Fixes)
// 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;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.MoveDeclarationNearReference;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.ReplaceDiscardDeclarationsWithAssignments;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Simplification;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.RemoveUnusedParametersAndValues;
 
/// <summary>
/// Code fixer for unused expression value diagnostics reported by <see cref="AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer"/>.
/// We provide following code fixes:
///     1. If the unused value assigned to a local/parameter has no side-effects,
///        we recommend removing the assignment. We consider an expression value to have no side effects
///        if one of the following is true:
///         1. Value is a compile time constant.
///         2. Value is a local or parameter reference.
///         3. Value is a field reference with no or implicit this instance.
///     2. Otherwise, if user preference is set to DiscardVariable, and project's
///        language version supports discard variable, we recommend assigning the value to discard.
///     3. Otherwise, we recommend assigning the value to a new unused local variable which has no reads.
/// </summary>
internal abstract class AbstractRemoveUnusedValuesCodeFixProvider<TExpressionSyntax, TStatementSyntax, TBlockSyntax,
    TExpressionStatementSyntax, TLocalDeclarationStatementSyntax, TVariableDeclaratorSyntax, TForEachStatementSyntax,
    TSwitchCaseBlockSyntax, TSwitchCaseLabelOrClauseSyntax, TCatchStatementSyntax, TCatchBlockSyntax>
    : SyntaxEditorBasedCodeFixProvider
    where TExpressionSyntax : SyntaxNode
    where TStatementSyntax : SyntaxNode
    where TBlockSyntax : TStatementSyntax
    where TExpressionStatementSyntax : TStatementSyntax
    where TLocalDeclarationStatementSyntax : TStatementSyntax
    where TForEachStatementSyntax : TStatementSyntax
    where TVariableDeclaratorSyntax : SyntaxNode
    where TSwitchCaseBlockSyntax : SyntaxNode
    where TSwitchCaseLabelOrClauseSyntax : SyntaxNode
{
    private static readonly SyntaxAnnotation s_memberAnnotation = new();
    private static readonly SyntaxAnnotation s_newLocalDeclarationStatementAnnotation = new();
    private static readonly SyntaxAnnotation s_unusedLocalDeclarationAnnotation = new();
    private static readonly SyntaxAnnotation s_existingLocalDeclarationWithoutInitializerAnnotation = new();
 
    public sealed override ImmutableArray<string> FixableDiagnosticIds
        => [IDEDiagnosticIds.ExpressionValueIsUnusedDiagnosticId, IDEDiagnosticIds.ValueAssignedIsUnusedDiagnosticId];
 
    protected abstract ISyntaxFormatting SyntaxFormatting { get; }
 
    /// <summary>
    /// Method to update the identifier token for the local/parameter declaration or reference
    /// that was flagged as an unused value write by the analyzer.
    /// Returns null if the provided node is not one of the handled node kinds.
    /// Otherwise, returns the new node with updated identifier.
    /// </summary>
    /// <param name="node">Flagged node containing the identifier token to be replaced.</param>
    /// <param name="newName">New identifier token</param>
    protected abstract SyntaxNode? TryUpdateNameForFlaggedNode(SyntaxNode node, SyntaxToken newName);
 
    /// <summary>
    /// Gets the identifier token for the iteration variable of the given foreach statement node.
    /// </summary>
    protected abstract SyntaxToken GetForEachStatementIdentifier(TForEachStatementSyntax node);
 
    /// <summary>
    /// Wraps the given statements within a block statement.
    /// Note this method is invoked when replacing a statement that is parented by a non-block statement syntax.
    /// </summary>
    protected abstract TBlockSyntax WrapWithBlockIfNecessary(IEnumerable<TStatementSyntax> statements);
 
    /// <summary>
    /// Inserts the given declaration statement at the start of the given switch case block.
    /// </summary>
    protected abstract void InsertAtStartOfSwitchCaseBlockForDeclarationInCaseLabelOrClause(TSwitchCaseBlockSyntax switchCaseBlock, SyntaxEditor editor, TLocalDeclarationStatementSyntax declarationStatement);
 
    /// <summary>
    /// Gets the replacement node for a compound assignment expression whose
    /// assigned value is redundant.
    /// For example, "x += MethodCall()", where assignment to 'x' is redundant
    /// is replaced with "_ = MethodCall()" or "var unused = MethodCall()"
    /// </summary>
    protected abstract SyntaxNode GetReplacementNodeForCompoundAssignment(
        SyntaxNode originalCompoundAssignment,
        SyntaxNode newAssignmentTarget,
        SyntaxEditor editor,
        ISyntaxFactsService syntaxFacts);
 
    /// <summary>
    /// Gets the replacement node for a var pattern.
    /// We need just to change the identifier of the pattern, not the whole node
    /// </summary>
    protected abstract SyntaxNode GetReplacementNodeForVarPattern(SyntaxNode originalVarPattern, SyntaxNode newNameNode);
 
    /// <summary>
    /// Rewrite the parent of a node which was rewritten by <see cref="TryUpdateNameForFlaggedNode"/>.
    /// </summary>
    /// <param name="parent">The original parent of the node rewritten by <see cref="TryUpdateNameForFlaggedNode"/>.</param>
    /// <param name="newNameNode">The rewritten node produced by <see cref="TryUpdateNameForFlaggedNode"/>.</param>
    /// <param name="editor">The syntax editor for the code fix.</param>
    /// <param name="syntaxFacts">The syntax facts for the current language.</param>
    /// <param name="semanticModel">Semantic model for the tree.</param>
    /// <returns>The replacement node to use in the rewritten syntax tree; otherwise, <see langword="null"/> to only
    /// rewrite the node originally rewritten by <see cref="TryUpdateNameForFlaggedNode"/>.</returns>
    protected virtual SyntaxNode? TryUpdateParentOfUpdatedNode(SyntaxNode parent, SyntaxNode newNameNode, SyntaxEditor editor, ISyntaxFacts syntaxFacts, SemanticModel semanticModel) => null;
 
    /// <summary>
    /// Computes correct replacement node, including cases with recursive changes (e.g. recursive pattern node rewrite in fix-all scenario)
    /// </summary>
    /// <param name="originalOldNode">The original node for replacement</param>
    /// <param name="changedOldNode">Node for replacement transformed by previous replacements</param>
    /// <param name="proposedReplacementNode">Proposed replacement node with changes relative to <paramref name="originalOldNode"/></param>
    /// <returns>The final replacement for the node</returns>
    protected abstract SyntaxNode ComputeReplacementNode(SyntaxNode originalOldNode, SyntaxNode changedOldNode, SyntaxNode proposedReplacementNode);
 
    public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
    {
        var diagnostic = context.Diagnostics[0];
        if (!AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.TryGetUnusedValuePreference(diagnostic, out var preference))
        {
            return;
        }
 
        var isRemovableAssignment = AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.GetIsRemovableAssignmentDiagnostic(diagnostic);
 
        string title;
        if (isRemovableAssignment)
        {
            // Recommend removing the redundant constant value assignment.
            title = CodeFixesResources.Remove_redundant_assignment;
        }
        else
        {
            // Recommend using discard/unused local for redundant non-constant assignment.
            switch (preference)
            {
                case UnusedValuePreference.DiscardVariable:
                    if (IsForEachIterationVariableDiagnostic(diagnostic, context.Document, context.CancellationToken))
                    {
                        // Do not offer a fix to replace unused foreach iteration variable with discard.
                        // User should probably replace it with a for loop based on the collection length.
                        return;
                    }
 
                    title = CodeFixesResources.Use_discard_underscore;
 
                    var syntaxFacts = context.Document.GetRequiredLanguageService<ISyntaxFactsService>();
                    var root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
                    var node = root.FindNode(context.Span, getInnermostNodeForTie: true);
 
                    // Check if this is compound assignment which is not parented by an expression statement,
                    // for example "return x += M();" OR "=> x ??= new C();"
                    // If so, we will be replacing this compound assignment with the underlying binary operation.
                    // For the above examples, it will be "return x + M();" AND "=> x ?? new C();" respectively.
                    // For these cases, we want to show the title as "Remove redundant assignment" instead of "Use discard _".
                    if (syntaxFacts.IsLeftSideOfCompoundAssignment(node) &&
                        !syntaxFacts.IsExpressionStatement(node.Parent))
                    {
                        title = CodeFixesResources.Remove_redundant_assignment;
                    }
                    // Also we want to show "Remove redundant assignment" title for variable designation in pattern matching,
                    // since this assignment will be fully removed. Cases:
                    // 1) `if (obj is SomeType someType)`
                    // 2) `if (obj is { } someType)`
                    // 3) `if (obj is [] someType)`
                    else if (syntaxFacts.IsDeclarationPattern(node.Parent) ||
                             syntaxFacts.IsRecursivePattern(node.Parent) ||
                             syntaxFacts.IsListPattern(node.Parent))
                    {
                        title = CodeFixesResources.Remove_redundant_assignment;
                    }
 
                    break;
 
                case UnusedValuePreference.UnusedLocalVariable:
                    title = CodeFixesResources.Use_discarded_local;
                    break;
 
                default:
                    return;
            }
        }
 
        RegisterCodeFix(context, title, GetEquivalenceKey(preference, isRemovableAssignment));
    }
 
    private static bool IsForEachIterationVariableDiagnostic(Diagnostic diagnostic, Document document, CancellationToken cancellationToken)
    {
        // Do not offer a fix to replace unused foreach iteration variable with discard.
        // User should probably replace it with a for loop based on the collection length.
        var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
        return syntaxFacts.IsForEachStatement(diagnostic.Location.FindNode(getInnermostNodeForTie: true, cancellationToken));
    }
 
    private static string GetEquivalenceKey(UnusedValuePreference preference, bool isRemovableAssignment)
        => preference.ToString() + isRemovableAssignment;
 
    private static string GetEquivalenceKey(Diagnostic diagnostic)
    {
        if (!AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.TryGetUnusedValuePreference(diagnostic, out var preference))
        {
            return string.Empty;
        }
 
        var isRemovableAssignment = AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.GetIsRemovableAssignmentDiagnostic(diagnostic);
        return GetEquivalenceKey(preference, isRemovableAssignment);
    }
 
    /// <summary>
    /// Flag to indicate if the code fix can introduce local declaration statements
    /// that need to be moved closer to the first reference of the declared variable.
    /// This is currently only possible for the unused value assignment fix.
    /// </summary>
    private static bool NeedsToMoveNewLocalDeclarationsNearReference(string diagnosticId)
        => diagnosticId == IDEDiagnosticIds.ValueAssignedIsUnusedDiagnosticId;
 
    protected override bool IncludeDiagnosticDuringFixAll(Diagnostic diagnostic, Document document, string? equivalenceKey, CancellationToken cancellationToken)
    {
        return equivalenceKey == GetEquivalenceKey(diagnostic) &&
            !IsForEachIterationVariableDiagnostic(diagnostic, document, cancellationToken);
    }
 
    private static IEnumerable<IGrouping<SyntaxNode, Diagnostic>> GetDiagnosticsGroupedByMember(
        ImmutableArray<Diagnostic> diagnostics,
        ISyntaxFactsService syntaxFacts,
        SyntaxNode root,
        out string diagnosticId,
        out UnusedValuePreference preference,
        out bool removeAssignments)
    {
        diagnosticId = diagnostics[0].Id;
        var success = AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.TryGetUnusedValuePreference(diagnostics[0], out preference);
        Debug.Assert(success);
        removeAssignments = AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.GetIsRemovableAssignmentDiagnostic(diagnostics[0]);
#if DEBUG
        foreach (var diagnostic in diagnostics)
        {
            Debug.Assert(diagnosticId == diagnostic.Id);
            Debug.Assert(AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.TryGetUnusedValuePreference(diagnostic, out var diagnosticPreference) &&
                         diagnosticPreference == preference);
            Debug.Assert(removeAssignments == AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.GetIsRemovableAssignmentDiagnostic(diagnostic));
        }
#endif
 
        return GetDiagnosticsGroupedByMember(diagnostics, syntaxFacts, root);
    }
 
    private static IEnumerable<IGrouping<SyntaxNode, Diagnostic>> GetDiagnosticsGroupedByMember(
        ImmutableArray<Diagnostic> diagnostics,
        ISyntaxFactsService syntaxFacts,
        SyntaxNode root)
    {
        return diagnostics.GroupBy(d => syntaxFacts.GetContainingMemberDeclaration(root, d.Location.SourceSpan.Start) ?? root);
    }
 
    private static async Task<Document> PreprocessDocumentAsync(Document document, ImmutableArray<Diagnostic> diagnostics, CancellationToken cancellationToken)
    {
        // Track all the member declaration nodes that have diagnostics.
        // We will post process all these tracked nodes after applying the fix (see "PostProcessDocumentAsync" below in this source file).
 
        var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
        var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
        var memberDeclarations = GetDiagnosticsGroupedByMember(diagnostics, syntaxFacts, root).Select(g => g.Key);
        root = root.ReplaceNodes(memberDeclarations, computeReplacementNode: (_, n) => n.WithAdditionalAnnotations(s_memberAnnotation));
        return document.WithSyntaxRoot(root);
    }
 
    protected sealed override async Task FixAllAsync(Document document, ImmutableArray<Diagnostic> diagnostics, SyntaxEditor editor, CancellationToken cancellationToken)
    {
        var formattingOptions = await document.GetSyntaxFormattingOptionsAsync(SyntaxFormatting, cancellationToken).ConfigureAwait(false);
        var preprocessedDocument = await PreprocessDocumentAsync(document, diagnostics, cancellationToken).ConfigureAwait(false);
        var newRoot = await GetNewRootAsync(preprocessedDocument, formattingOptions, diagnostics, cancellationToken).ConfigureAwait(false);
        editor.ReplaceNode(editor.OriginalRoot, newRoot);
    }
 
    private async Task<SyntaxNode> GetNewRootAsync(
        Document document,
        SyntaxFormattingOptions options,
        ImmutableArray<Diagnostic> diagnostics,
        CancellationToken cancellationToken)
    {
        var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
        var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
        var semanticFacts = document.GetRequiredLanguageService<ISemanticFactsService>();
        var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
 
        var editor = new SyntaxEditor(root, document.Project.Solution.Services);
 
        // We compute the code fix in two passes:
        //   1. The first pass groups the diagnostics to fix by containing member declaration and
        //      computes and applies the core code fixes. Grouping is done to ensure we choose
        //      the most appropriate name for new unused local declarations, which can clash
        //      with existing local declarations in the method body.
        //   2. Second pass (PostProcessDocumentAsync) performs additional syntax manipulations
        //      for the fixes produced from the first pass:
        //      a. Replace discard declarations, such as "var _ = M();" that conflict with newly added
        //         discard assignments, with discard assignments of the form "_ = M();"
        //      b. Move newly introduced local declaration statements closer to the local variable's
        //         first reference.
 
        // Get diagnostics grouped by member.
        var diagnosticsGroupedByMember = GetDiagnosticsGroupedByMember(diagnostics, syntaxFacts, root,
            out var diagnosticId, out var preference, out var removeAssignments);
 
        // First pass to compute and apply the core code fixes.
        foreach (var diagnosticsToFix in diagnosticsGroupedByMember)
        {
            var containingMemberDeclaration = diagnosticsToFix.Key;
            using var nameGenerator = new UniqueVariableNameGenerator(containingMemberDeclaration, semanticModel, semanticFacts, cancellationToken);
 
            await FixAllAsync(
                diagnosticId, diagnosticsToFix.Select(d => d),
                document, semanticModel, root, containingMemberDeclaration, preference,
                removeAssignments, nameGenerator, editor, cancellationToken).ConfigureAwait(false);
        }
 
        // Second pass to post process the document.
        var currentRoot = editor.GetChangedRoot();
        var newRoot = await PostProcessDocumentAsync(document, options, currentRoot,
            diagnosticId, preference, cancellationToken).ConfigureAwait(false);
 
        if (currentRoot != newRoot)
            editor.ReplaceNode(root, newRoot);
 
        return editor.GetChangedRoot();
    }
 
    private async Task FixAllAsync(
        string diagnosticId,
        IEnumerable<Diagnostic> diagnostics,
        Document document,
        SemanticModel semanticModel,
        SyntaxNode root,
        SyntaxNode containingMemberDeclaration,
        UnusedValuePreference preference,
        bool removeAssignments,
        UniqueVariableNameGenerator nameGenerator,
        SyntaxEditor editor,
        CancellationToken cancellationToken)
    {
        switch (diagnosticId)
        {
            case IDEDiagnosticIds.ExpressionValueIsUnusedDiagnosticId:
                // Make sure the inner diagnostics are placed first
                FixAllExpressionValueIsUnusedDiagnostics(
                    diagnostics.OrderByDescending(d => d.Location.SourceSpan.Start),
                    document, semanticModel, root, preference, nameGenerator, editor);
                break;
 
            case IDEDiagnosticIds.ValueAssignedIsUnusedDiagnosticId:
                // Make sure the diagnostics are placed in order.
                // Example: 
                // int a = 0; int b = 1;
                // After fix it would be int a; int b;
                await FixAllValueAssignedIsUnusedDiagnosticsAsync(
                    diagnostics.OrderBy(d => d.Location.SourceSpan.Start),
                    document, semanticModel, root, containingMemberDeclaration,
                    preference, removeAssignments, nameGenerator, editor, cancellationToken).ConfigureAwait(false);
                break;
 
            default:
                throw ExceptionUtilities.Unreachable();
        }
    }
 
    private static void FixAllExpressionValueIsUnusedDiagnostics(
        IOrderedEnumerable<Diagnostic> diagnostics,
        Document document,
        SemanticModel semanticModel,
        SyntaxNode root,
        UnusedValuePreference preference,
        UniqueVariableNameGenerator nameGenerator,
        SyntaxEditor editor)
    {
        var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
 
        // This method applies the code fix for diagnostics reported for expression statement dropping values.
        // We replace each flagged expression statement with an assignment to a discard variable or a new unused local,
        // based on the user's preference.
        // Note: The diagnostic order here should be inner first and outer second.
        // Example: Foo1(() => { Foo2(); })
        // Foo2() should be the first in this case.
        foreach (var diagnostic in diagnostics)
        {
            var expressionStatement = root.FindNode(diagnostic.Location.SourceSpan).FirstAncestorOrSelf<TExpressionStatementSyntax>();
            if (expressionStatement == null)
            {
                continue;
            }
 
            switch (preference)
            {
                case UnusedValuePreference.DiscardVariable:
                    Debug.Assert(semanticModel.Language != LanguageNames.VisualBasic);
                    var expression = syntaxFacts.GetExpressionOfExpressionStatement(expressionStatement);
                    editor.ReplaceNode(expression, (node, generator) =>
                    {
                        var discardAssignmentExpression = (TExpressionSyntax)generator.AssignmentStatement(
                                                                left: generator.IdentifierName(AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.DiscardVariableName),
                                                                right: node.WithoutTrivia())
                                                            .WithTriviaFrom(node)
                                                            .WithAdditionalAnnotations(Simplifier.Annotation, Formatter.Annotation);
                        return discardAssignmentExpression;
                    });
                    break;
 
                case UnusedValuePreference.UnusedLocalVariable:
                    var name = nameGenerator.GenerateUniqueNameAtSpanStart(expressionStatement).ValueText;
                    editor.ReplaceNode(expressionStatement, (node, generator) =>
                    {
                        var expression = syntaxFacts.GetExpressionOfExpressionStatement(node);
                        // Add Simplifier annotation so that 'var'/explicit type is correctly added based on user options.
                        var localDecl = editor.Generator.LocalDeclarationStatement(
                                            name: name,
                                            initializer: expression.WithoutLeadingTrivia())
                                        .WithTriviaFrom(node)
                                        .WithAdditionalAnnotations(Simplifier.Annotation, Formatter.Annotation);
                        return localDecl;
                    });
                    break;
            }
        }
    }
 
    private async Task FixAllValueAssignedIsUnusedDiagnosticsAsync(
        IOrderedEnumerable<Diagnostic> diagnostics,
        Document document,
        SemanticModel semanticModel,
        SyntaxNode root,
        SyntaxNode containingMemberDeclaration,
        UnusedValuePreference preference,
        bool removeAssignments,
        UniqueVariableNameGenerator nameGenerator,
        SyntaxEditor editor,
        CancellationToken cancellationToken)
    {
        var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
        var blockFacts = document.GetRequiredLanguageService<IBlockFactsService>();
 
        // This method applies the code fix for diagnostics reported for unused value assignments to local/parameter.
        // The actual code fix depends on whether or not the right hand side of the assignment has side effects.
        // For example, if the right hand side is a constant or a reference to a local/parameter, then it has no side effects.
        // The lack of side effects is indicated by the "removeAssignments" parameter for this function.
 
        // If the right hand side has no side effects, then we can replace the assignments with variable declarations that have no initializer
        // or completely remove the statement.
        // If the right hand side does have side effects, we replace the identifier token for unused value assignment with
        // a new identifier token (either discard '_' or new unused local variable name).
 
        // For both the above cases, if the original diagnostic was reported on a local declaration, i.e. redundant initialization
        // at declaration, then we also add a new variable declaration statement without initializer for this local.
 
        using var _1 = PooledDictionary<SyntaxNode, SyntaxNode>.GetInstance(out var nodeReplacementMap);
        using var _2 = PooledHashSet<SyntaxNode>.GetInstance(out var nodesToRemove);
        using var _3 = PooledHashSet<(TLocalDeclarationStatementSyntax declarationStatement, SyntaxNode node)>.GetInstance(out var nodesToAdd);
        // Indicates if the node's trivia was processed.
        using var _4 = PooledHashSet<SyntaxNode>.GetInstance(out var processedNodes);
        using var _5 = PooledHashSet<TLocalDeclarationStatementSyntax>.GetInstance(out var candidateDeclarationStatementsForRemoval);
        var hasAnyUnusedLocalAssignment = false;
 
        foreach (var (node, isUnusedLocalAssignment) in GetNodesToFix())
        {
            hasAnyUnusedLocalAssignment |= isUnusedLocalAssignment;
 
            var declaredLocal = semanticModel.GetDeclaredSymbol(node, cancellationToken) as ILocalSymbol;
            if (declaredLocal == null && node.Parent is TCatchStatementSyntax)
            {
                declaredLocal = semanticModel.GetDeclaredSymbol(node.Parent, cancellationToken) as ILocalSymbol;
            }
 
            string? newLocalNameOpt = null;
            if (removeAssignments)
            {
                // Removable assignment or initialization, such that right hand side has no side effects.
                if (declaredLocal != null)
                {
                    // Redundant initialization.
                    // For example, "int a = 0;"
                    var variableDeclarator = node.FirstAncestorOrSelf<TVariableDeclaratorSyntax>();
                    Contract.ThrowIfNull(variableDeclarator);
                    nodesToRemove.Add(variableDeclarator);
 
                    // Local declaration statement containing the declarator might be a candidate for removal if all its variables get marked for removal.
                    var candidate = GetCandidateLocalDeclarationForRemoval(variableDeclarator);
                    if (candidate != null)
                    {
                        candidateDeclarationStatementsForRemoval.Add(candidate);
                    }
                }
                else
                {
                    // Redundant assignment or increment/decrement.
                    if (syntaxFacts.IsOperandOfIncrementOrDecrementExpression(node))
                    {
                        // For example, C# increment operation "a++;"
                        Contract.ThrowIfFalse(node.GetRequiredParent().Parent is TExpressionStatementSyntax);
                        nodesToRemove.Add(node.GetRequiredParent().GetRequiredParent());
                    }
                    else
                    {
                        Debug.Assert(syntaxFacts.IsLeftSideOfAnyAssignment(node));
 
                        if (node.Parent is TStatementSyntax)
                        {
                            // For example, VB simple assignment statement "a = 0"
                            nodesToRemove.Add(node.Parent);
                        }
                        else if (node.Parent is TExpressionSyntax && node.Parent.Parent is TExpressionStatementSyntax)
                        {
                            // For example, C# simple assignment statement "a = 0;"
                            nodesToRemove.Add(node.Parent.Parent);
                        }
                        else
                        {
                            // For example, C# nested assignment statement "a = b = 0;", where assignment to 'b' is redundant.
                            // We replace the node with "a = 0;"
                            nodeReplacementMap.Add(node.GetRequiredParent(), syntaxFacts.GetRightHandSideOfAssignment(node.GetRequiredParent()));
                        }
                    }
                }
            }
            else
            {
                // Value initialization/assignment where the right hand side may have side effects,
                // and hence needs to be preserved in fixed code.
                // For example, "x = MethodCall();" is replaced with "_ = MethodCall();" or "var unused = MethodCall();"
 
                // Replace the flagged variable's identifier token with new named, based on user's preference.
                var newNameToken = preference == UnusedValuePreference.DiscardVariable
                    ? document.GetRequiredLanguageService<SyntaxGeneratorInternal>().Identifier(AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.DiscardVariableName)
                    : nameGenerator.GenerateUniqueNameAtSpanStart(node);
                newLocalNameOpt = newNameToken.ValueText;
                var newNameNode = TryUpdateNameForFlaggedNode(node, newNameToken);
                if (newNameNode == null)
                {
                    continue;
                }
 
                // Is this is compound assignment?
                if (syntaxFacts.IsLeftSideOfAnyAssignment(node) && !syntaxFacts.IsLeftSideOfAssignment(node))
                {
                    // 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();"
                    nodeReplacementMap.Add(node.GetRequiredParent(), GetReplacementNodeForCompoundAssignment(node.GetRequiredParent(), newNameNode, editor, syntaxFacts));
                }
                else if (syntaxFacts.IsVarPattern(node))
                {
                    nodeReplacementMap.Add(node, GetReplacementNodeForVarPattern(node, newNameNode));
                }
                else
                {
                    var newParentNode = TryUpdateParentOfUpdatedNode(node.GetRequiredParent(), newNameNode, editor, syntaxFacts, semanticModel);
                    if (newParentNode is not null)
                    {
                        nodeReplacementMap.Add(node.GetRequiredParent(), newParentNode);
                    }
                    else
                    {
                        nodeReplacementMap.Add(node, newNameNode);
                    }
                }
            }
 
            if (declaredLocal != null)
            {
                // We have a dead initialization for a local declaration.
                // Introduce a new local declaration statement without an initializer for this local.
                var declarationStatement = CreateLocalDeclarationStatement(declaredLocal.Type, declaredLocal.Name);
                if (isUnusedLocalAssignment)
                {
                    declarationStatement = declarationStatement.WithAdditionalAnnotations(s_unusedLocalDeclarationAnnotation);
                }
 
                nodesToAdd.Add((declarationStatement, node));
            }
            else
            {
                // We have a dead assignment to a local/parameter, which is not at the declaration site.
                // Create a new local declaration for the unused local if both following conditions are met:
                //  1. User prefers unused local variables for unused value assignment AND
                //  2. Assignment value has side effects and hence cannot be removed.
                if (preference == UnusedValuePreference.UnusedLocalVariable && !removeAssignments)
                {
                    var type = semanticModel.GetTypeInfo(node, cancellationToken).Type;
                    Contract.ThrowIfNull(type);
                    Contract.ThrowIfNull(newLocalNameOpt);
                    var declarationStatement = CreateLocalDeclarationStatement(type, newLocalNameOpt);
                    nodesToAdd.Add((declarationStatement, node));
                }
            }
        }
 
        // Process candidate declaration statements for removal.
        foreach (var localDeclarationStatement in candidateDeclarationStatementsForRemoval)
        {
            // If all the variable declarators for the local declaration statement are being removed,
            // we can remove the entire local declaration statement.
            if (ShouldRemoveStatement(localDeclarationStatement, out var variables))
            {
                nodesToRemove.Add(localDeclarationStatement);
                nodesToRemove.RemoveRange(variables);
            }
        }
 
        foreach (var (declarationStatement, node) in nodesToAdd)
        {
            InsertLocalDeclarationStatement(declarationStatement, node);
        }
 
        if (hasAnyUnusedLocalAssignment)
        {
            // Local declaration statements with no initializer, but non-zero references are candidates for removal
            // if the code fix removes all these references.
            // We annotate such declaration statements with no initializer and non-zero references here
            // and remove them in post process document pass later, if the code fix did remove all these references.
            foreach (var localDeclarationStatement in containingMemberDeclaration.DescendantNodes().OfType<TLocalDeclarationStatementSyntax>())
            {
                var variables = syntaxFacts.GetVariablesOfLocalDeclarationStatement(localDeclarationStatement);
                if (variables.Count == 1 &&
                    syntaxFacts.GetInitializerOfVariableDeclarator(variables[0]) == null &&
                    !(await IsLocalDeclarationWithNoReferencesAsync(localDeclarationStatement, document, cancellationToken).ConfigureAwait(false)))
                {
                    nodeReplacementMap.Add(localDeclarationStatement, localDeclarationStatement.WithAdditionalAnnotations(s_existingLocalDeclarationWithoutInitializerAnnotation));
                }
            }
        }
 
        foreach (var node in nodesToRemove)
        {
            var removeOptions = SyntaxGenerator.DefaultRemoveOptions;
            // If the leading trivia was not added to a new node, process it now.
            if (!processedNodes.Contains(node))
            {
                // Don't keep trivia if the node is part of a multiple declaration statement.
                // e.g. int x = 0, y = 0, z = 0; any white space left behind can cause problems if the declaration gets split apart.
                var containingDeclaration = node.GetAncestor<TLocalDeclarationStatementSyntax>();
                if (containingDeclaration != null && candidateDeclarationStatementsForRemoval.Contains(containingDeclaration))
                {
                    removeOptions = SyntaxRemoveOptions.KeepNoTrivia;
                }
                else
                {
                    removeOptions |= SyntaxRemoveOptions.KeepLeadingTrivia;
                }
            }
 
            editor.RemoveNode(node, removeOptions);
        }
 
        foreach (var (node, replacement) in nodeReplacementMap)
            editor.ReplaceNode(node, (oldNode, _) => ComputeReplacementNode(node, oldNode, replacement));
 
        return;
 
        // Local functions.
        IEnumerable<(SyntaxNode node, bool isUnusedLocalAssignment)> GetNodesToFix()
        {
            foreach (var diagnostic in diagnostics)
            {
                var node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true);
                var isUnusedLocalAssignment = AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.GetIsUnusedLocalDiagnostic(diagnostic);
                yield return (node, isUnusedLocalAssignment);
            }
        }
 
        // Mark generated local declaration statement with:
        //  1. "s_newLocalDeclarationAnnotation" for post processing in "MoveNewLocalDeclarationsNearReference" below.
        //  2. Simplifier annotation so that 'var'/explicit type is correctly added based on user options.
        TLocalDeclarationStatementSyntax CreateLocalDeclarationStatement(ITypeSymbol type, string name)
            => (TLocalDeclarationStatementSyntax)editor.Generator.LocalDeclarationStatement(type, name)
               .WithLeadingTrivia(syntaxFacts.ElasticCarriageReturnLineFeed)
               .WithAdditionalAnnotations(s_newLocalDeclarationStatementAnnotation, Simplifier.Annotation);
 
        void InsertLocalDeclarationStatement(TLocalDeclarationStatementSyntax declarationStatement, SyntaxNode node)
        {
            // Find the correct place to insert the given declaration statement based on the node's ancestors.
            var insertionNode = node.FirstAncestorOrSelf<SyntaxNode>(
                n => n.Parent is TSwitchCaseBlockSyntax ||
                     blockFacts.IsExecutableBlock(n.Parent) &&
                     n is not TCatchStatementSyntax and not TCatchBlockSyntax);
            if (insertionNode is TSwitchCaseLabelOrClauseSyntax)
            {
                InsertAtStartOfSwitchCaseBlockForDeclarationInCaseLabelOrClause(
                    insertionNode.GetAncestor<TSwitchCaseBlockSyntax>()!, editor, declarationStatement);
            }
            else if (insertionNode is TStatementSyntax)
            {
                // If the insertion node is being removed, keep the leading trivia (following any directives) with
                // the new declaration.
                if (nodesToRemove.Contains(insertionNode) && !processedNodes.Contains(insertionNode))
                {
                    // Fix 48070 - The Leading Trivia of the insertion node needs to be filtered
                    // to only include trivia after Directives (if there are any)
                    var leadingTrivia = insertionNode.GetLeadingTrivia();
                    var lastDirective = leadingTrivia.LastOrDefault(t => t.IsDirective);
                    var lastDirectiveIndex = leadingTrivia.IndexOf(lastDirective);
                    declarationStatement = declarationStatement.WithLeadingTrivia(leadingTrivia.Skip(lastDirectiveIndex + 1));
 
                    // Mark the node as processed so that the trivia only gets added once.
                    processedNodes.Add(insertionNode);
                }
 
                editor.InsertBefore(insertionNode, declarationStatement);
            }
        }
 
        bool ShouldRemoveStatement(TLocalDeclarationStatementSyntax localDeclarationStatement, out SeparatedSyntaxList<SyntaxNode> variables)
        {
            Debug.Assert(removeAssignments);
 
            // We should remove the entire local declaration statement if all its variables are marked for removal.
            variables = syntaxFacts.GetVariablesOfLocalDeclarationStatement(localDeclarationStatement);
            foreach (var variable in variables)
            {
                if (!nodesToRemove.Contains(variable))
                {
                    return false;
                }
            }
 
            return true;
        }
    }
 
    protected abstract TLocalDeclarationStatementSyntax? GetCandidateLocalDeclarationForRemoval(TVariableDeclaratorSyntax declarator);
 
    private async Task<SyntaxNode> PostProcessDocumentAsync(
        Document document,
        SyntaxFormattingOptions options,
        SyntaxNode currentRoot,
        string diagnosticId,
        UnusedValuePreference preference,
        CancellationToken cancellationToken)
    {
        // If we added discard assignments, replace all discard variable declarations in
        // this method with discard assignments, i.e. "var _ = M();" is replaced with "_ = M();"
        // This is done to prevent compiler errors where the existing method has a discard
        // variable declaration at a line following the one we added a discard assignment in our fix.
        if (preference == UnusedValuePreference.DiscardVariable)
        {
            currentRoot = await PostProcessDocumentCoreAsync(
                ReplaceDiscardDeclarationsWithAssignmentsAsync, currentRoot, document, options, cancellationToken).ConfigureAwait(false);
        }
 
        // If we added new variable declaration statements, move these as close as possible to their
        // first reference site.
        if (NeedsToMoveNewLocalDeclarationsNearReference(diagnosticId))
        {
            currentRoot = await PostProcessDocumentCoreAsync(
                AdjustLocalDeclarationsAsync, currentRoot, document, options, cancellationToken).ConfigureAwait(false);
        }
 
        return currentRoot;
    }
 
    private static async Task<SyntaxNode> PostProcessDocumentCoreAsync(
        Func<SyntaxNode, Document, SyntaxFormattingOptions, CancellationToken, Task<SyntaxNode>> processMemberDeclarationAsync,
        SyntaxNode currentRoot,
        Document document,
        SyntaxFormattingOptions options,
        CancellationToken cancellationToken)
    {
        // Process each member declaration which had at least one diagnostic reported in the original tree and hence
        // was annotated with "s_memberAnnotation" for post processing.
 
        var newDocument = document.WithSyntaxRoot(currentRoot);
        var newRoot = await newDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
        using var _1 = PooledDictionary<SyntaxNode, SyntaxNode>.GetInstance(out var memberDeclReplacementsMap);
 
        foreach (var memberDecl in newRoot.DescendantNodes().Where(n => n.HasAnnotation(s_memberAnnotation)))
        {
            var newMemberDecl = await processMemberDeclarationAsync(memberDecl, newDocument, options, cancellationToken).ConfigureAwait(false);
            memberDeclReplacementsMap.Add(memberDecl, newMemberDecl);
        }
 
        return newRoot.ReplaceNodes(memberDeclReplacementsMap.Keys,
            computeReplacementNode: (node, _) => memberDeclReplacementsMap[node]);
    }
 
    /// <summary>
    /// Returns an updated <paramref name="memberDeclaration"/> with all the
    /// local declarations named '_' converted to simple assignments to discard.
    /// For example, <code>int _ = Computation();</code> is converted to
    /// <code>_ = Computation();</code>.
    /// This is needed to prevent the code fix/FixAll from generating code with
    /// multiple local variables named '_', which is a compiler error.
    /// </summary>
    private async Task<SyntaxNode> ReplaceDiscardDeclarationsWithAssignmentsAsync(SyntaxNode memberDeclaration, Document document, SyntaxFormattingOptions options, CancellationToken cancellationToken)
    {
        var service = document.GetLanguageService<IReplaceDiscardDeclarationsWithAssignmentsService>();
        if (service == null)
            return memberDeclaration;
 
        return await service.ReplaceAsync(document, memberDeclaration, cancellationToken).ConfigureAwait(false);
    }
 
    /// <summary>
    /// Returns an updated <paramref name="memberDeclaration"/> with all the new
    /// local declaration statements annotated with <see cref="s_newLocalDeclarationStatementAnnotation"/>
    /// moved closer to first reference and all the existing
    /// local declaration statements annotated with <see cref="s_existingLocalDeclarationWithoutInitializerAnnotation"/>
    /// whose declared local is no longer used removed.
    /// </summary>
    private async Task<SyntaxNode> AdjustLocalDeclarationsAsync(
        SyntaxNode memberDeclaration,
        Document document,
        SyntaxFormattingOptions options,
        CancellationToken cancellationToken)
    {
        var moveDeclarationService = document.GetRequiredLanguageService<IMoveDeclarationNearReferenceService>();
        var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
        var originalDocument = document;
        var originalDeclStatementsToMoveOrRemove =
            memberDeclaration.DescendantNodes()
                             .Where(n => n.HasAnnotation(s_newLocalDeclarationStatementAnnotation) ||
                                         n.HasAnnotation(s_existingLocalDeclarationWithoutInitializerAnnotation))
                             .ToImmutableArray();
        if (originalDeclStatementsToMoveOrRemove.IsEmpty)
        {
            return memberDeclaration;
        }
 
        // Moving declarations closer to a reference can lead to conflicting edits.
        // So, we track all the declaration statements to be moved upfront, and update
        // the root, document, editor and memberDeclaration for every edit.
        // Finally, we apply replace the memberDeclaration in the originalEditor as a single edit.
        var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
        var rootWithTrackedNodes = root.TrackNodes(originalDeclStatementsToMoveOrRemove);
        var spansToFormat = originalDeclStatementsToMoveOrRemove.Select(s => s.Span);
 
        // Run formatter prior to invoking IMoveDeclarationNearReferenceService.
        rootWithTrackedNodes = SyntaxFormatting.GetFormattingResult(rootWithTrackedNodes, spansToFormat, options, rules: default, cancellationToken).GetFormattedRoot(cancellationToken);
 
        document = document.WithSyntaxRoot(rootWithTrackedNodes);
        await OnDocumentUpdatedAsync().ConfigureAwait(false);
 
        foreach (TLocalDeclarationStatementSyntax originalDeclStatement in originalDeclStatementsToMoveOrRemove)
        {
            // Get the current declaration statement.
            var declStatement = memberDeclaration.GetCurrentNode(originalDeclStatement);
            Contract.ThrowIfNull(declStatement);
 
            // Check if the variable declaration is unused after all the fixes, and hence can be removed.
            if (await TryRemoveUnusedLocalAsync(declStatement, originalDeclStatement).ConfigureAwait(false))
            {
                await OnDocumentUpdatedAsync().ConfigureAwait(false);
            }
            else if (declStatement.HasAnnotation(s_newLocalDeclarationStatementAnnotation))
            {
                // Otherwise, move the declaration closer to the first reference if possible.  Note: because the initial
                // declaration value was unused (which is why we're removing/moving the local decl in the first place),
                // there's no concern about changing the scope where the declaration now lives, which is why it's fine
                // to ignore 'mayChangeSemantics' here.
 
                var (canUse, mayChangeSemantics) = await moveDeclarationService.CanMoveDeclarationNearReferenceAsync(document, declStatement, cancellationToken).ConfigureAwait(false);
                if (canUse)
                {
                    document = await moveDeclarationService.MoveDeclarationNearReferenceAsync(document, declStatement, cancellationToken).ConfigureAwait(false);
                    await OnDocumentUpdatedAsync().ConfigureAwait(false);
                }
            }
        }
 
        return memberDeclaration;
 
        // Local functions.
        async Task OnDocumentUpdatedAsync()
        {
            root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
            memberDeclaration = syntaxFacts.GetContainingMemberDeclaration(root, memberDeclaration.SpanStart) ?? root;
        }
 
        async Task<bool> TryRemoveUnusedLocalAsync(TLocalDeclarationStatementSyntax newDecl, TLocalDeclarationStatementSyntax originalDecl)
        {
            // If we introduced this new local declaration statement while computing the code fix, but all it's
            // existing references were removed as part of FixAll, then we can remove the unnecessary local
            // declaration statement. Additionally, if this is an existing local declaration without an initializer,
            // such that the local has no references anymore, we can remove it.
 
            if (newDecl.HasAnnotation(s_unusedLocalDeclarationAnnotation) ||
                newDecl.HasAnnotation(s_existingLocalDeclarationWithoutInitializerAnnotation))
            {
                // Check if we have no references to local in fixed code.
                if (await IsLocalDeclarationWithNoReferencesAsync(newDecl, document, cancellationToken).ConfigureAwait(false))
                {
                    var rootWithRemovedDeclaration = root.RemoveNode(newDecl, SyntaxGenerator.DefaultRemoveOptions | SyntaxRemoveOptions.KeepLeadingTrivia);
                    Contract.ThrowIfNull(rootWithRemovedDeclaration);
                    document = document.WithSyntaxRoot(rootWithRemovedDeclaration);
                    return true;
                }
            }
 
            return false;
        }
    }
 
    private static async Task<bool> IsLocalDeclarationWithNoReferencesAsync(
        TLocalDeclarationStatementSyntax declStatement,
        Document document,
        CancellationToken cancellationToken)
    {
        var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
        var localDeclarationOperation = (IVariableDeclarationGroupOperation)semanticModel.GetRequiredOperation(declStatement, cancellationToken);
        var local = localDeclarationOperation.GetDeclaredVariables().Single();
 
        // Check if the declared variable has no references in fixed code.
        var referencedSymbols = await SymbolFinder.FindReferencesAsync(local, document.Project.Solution, cancellationToken).ConfigureAwait(false);
        return referencedSymbols.Count() == 1 &&
            referencedSymbols.Single().Locations.IsEmpty();
    }
 
    protected sealed class UniqueVariableNameGenerator(
        SyntaxNode memberDeclaration,
        SemanticModel semanticModel,
        ISemanticFactsService semanticFacts,
        CancellationToken cancellationToken) : IDisposable
    {
        private readonly SyntaxNode _memberDeclaration = memberDeclaration;
        private readonly SemanticModel _semanticModel = semanticModel;
        private readonly ISemanticFactsService _semanticFacts = semanticFacts;
        private readonly CancellationToken _cancellationToken = cancellationToken;
        private readonly PooledHashSet<string> _usedNames = PooledHashSet<string>.GetInstance();
 
        public SyntaxToken GenerateUniqueNameAtSpanStart(SyntaxNode node)
        {
            var nameToken = _semanticFacts.GenerateUniqueName(_semanticModel, node, _memberDeclaration, "unused", _usedNames, _cancellationToken);
            _usedNames.Add(nameToken.ValueText);
            return nameToken;
        }
 
        public void Dispose() => _usedNames.Free();
    }
}