<commit_before>﻿// 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.CodeActions;
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.LanguageServices;
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 SyntaxAnnotation();
        private static readonly SyntaxAnnotation s_newLocalDeclarationStatementAnnotation = new SyntaxAnnotation();
        private static readonly SyntaxAnnotation s_unusedLocalDeclarationAnnotation = new SyntaxAnnotation();
        private static readonly SyntaxAnnotation s_existingLocalDeclarationWithoutInitializerAnnotation = new SyntaxAnnotation();

        public sealed override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(IDEDiagnosticIds.ExpressionValueIsUnusedDiagnosticId,
                                                                                                    IDEDiagnosticIds.ValueAssignedIsUnusedDiagnosticId);

        internal sealed override CodeFixCategory CodeFixCategory => CodeFixCategory.CodeQuality;

        /// <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">Flaggged 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>
        /// Rewrite the parent of a node which was rewritted 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>
        /// <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)
        {
            return null;
        }

        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;

                        // 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 _".

                        var syntaxFacts = context.Document.GetLanguageService<ISyntaxFactsService>();
                        var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
                        var node = root.FindNode(context.Span, getInnermostNodeForTie: true);
                        if (syntaxFacts.IsLeftSideOfCompoundAssignment(node) &&
                            !syntaxFacts.IsExpressionStatement(node.Parent))
                        {
                            title = CodeFixesResources.Remove_redundant_assignment;
                        }

                        break;

                    case UnusedValuePreference.UnusedLocalVariable:
                        title = CodeFixesResources.Use_discarded_local;
                        break;

                    default:
                        return;
                }
            }

            context.RegisterCodeFix(
                new MyCodeAction(
                    title,
                    c => FixAsync(context.Document, diagnostic, c),
                    equivalenceKey: GetEquivalenceKey(preference, isRemovableAssignment)),
                diagnostic);

            return;
        }

        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.GetLanguageService<ISyntaxFactsService>();
            return syntaxFacts.IsForEachStatement(diagnostic.Location.FindNode(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)
            => diagnostics.GroupBy(d => syntaxFacts.GetContainingMemberDeclaration(root, d.Location.SourceSpan.Start));

        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.GetLanguageService<ISyntaxFactsService>();
            var root = await document.GetSyntaxRootAsync(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)
        {
            document = await PreprocessDocumentAsync(document, diagnostics, cancellationToken).ConfigureAwait(false);
            var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
            var syntaxFacts = document.GetLanguageService<ISyntaxFactsService>();
            var semanticFacts = document.GetLanguageService<ISemanticFactsService>();

            var originalEditor = editor;
            editor = new SyntaxEditor(root, document.Project.Solution.Workspace);

            try
            {
                // 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 from 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.
                var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
                foreach (var diagnosticsToFix in diagnosticsGroupedByMember)
                {
                    var orderedDiagnostics = diagnosticsToFix.OrderByDescending(d => GetDepthOfLocation(d.Location, root));
                    var containingMemberDeclaration = diagnosticsToFix.Key;
                    using var nameGenerator = new UniqueVariableNameGenerator(containingMemberDeclaration, semanticModel, semanticFacts, cancellationToken);

                    await FixAllAsync(diagnosticId, orderedDiagnostics, document, semanticModel, root, containingMemberDeclaration, preference,
                        removeAssignments, nameGenerator, editor, syntaxFacts, cancellationToken).ConfigureAwait(false);
                }

                // Second pass to post process the document.
                var currentRoot = editor.GetChangedRoot();
                var newRoot = await PostProcessDocumentAsync(document, currentRoot,
                    diagnosticId, preference, cancellationToken).ConfigureAwait(false);
                if (currentRoot != newRoot)
                {
                    editor.ReplaceNode(root, newRoot);
                }
            }
            finally
            {
                originalEditor.ReplaceNode(originalEditor.OriginalRoot, editor.GetChangedRoot());
            }
        }

        private static int GetDepthOfLocation(Location location, SyntaxNode root)
        {
            var node = root.FindNode(location.SourceSpan);
            int depth = 0;
            while (node != null && node != root)
            {
                depth += 1;
                node = node.Parent;
            }

            return depth;
        }

        private async Task FixAllAsync(
            string diagnosticId,
            IOrderedEnumerable<Diagnostic> diagnostics,
            Document document,
            SemanticModel semanticModel,
            SyntaxNode root,
            SyntaxNode containingMemberDeclaration,
            UnusedValuePreference preference,
            bool removeAssignments,
            UniqueVariableNameGenerator nameGenerator,
            SyntaxEditor editor,
            ISyntaxFactsService syntaxFacts,
            CancellationToken cancellationToken)
        {
            switch (diagnosticId)
            {
                case IDEDiagnosticIds.ExpressionValueIsUnusedDiagnosticId:
                    FixAllExpressionValueIsUnusedDiagnostics(diagnostics, semanticModel, root,
                        preference, nameGenerator, editor, syntaxFacts);
                    break;
<commit_msg>Use the simple way to fix<commit_after>﻿// 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.CodeActions;
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.LanguageServices;
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 SyntaxAnnotation();
        private static readonly SyntaxAnnotation s_newLocalDeclarationStatementAnnotation = new SyntaxAnnotation();
        private static readonly SyntaxAnnotation s_unusedLocalDeclarationAnnotation = new SyntaxAnnotation();
        private static readonly SyntaxAnnotation s_existingLocalDeclarationWithoutInitializerAnnotation = new SyntaxAnnotation();

        public sealed override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(IDEDiagnosticIds.ExpressionValueIsUnusedDiagnosticId,
                                                                                                    IDEDiagnosticIds.ValueAssignedIsUnusedDiagnosticId);

        internal sealed override CodeFixCategory CodeFixCategory => CodeFixCategory.CodeQuality;

        /// <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">Flaggged 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>
        /// Rewrite the parent of a node which was rewritted 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>
        /// <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)
        {
            return null;
        }

        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;

                        // 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 _".

                        var syntaxFacts = context.Document.GetLanguageService<ISyntaxFactsService>();
                        var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
                        var node = root.FindNode(context.Span, getInnermostNodeForTie: true);
                        if (syntaxFacts.IsLeftSideOfCompoundAssignment(node) &&
                            !syntaxFacts.IsExpressionStatement(node.Parent))
                        {
                            title = CodeFixesResources.Remove_redundant_assignment;
                        }

                        break;

                    case UnusedValuePreference.UnusedLocalVariable:
                        title = CodeFixesResources.Use_discarded_local;
                        break;

                    default:
                        return;
                }
            }

            context.RegisterCodeFix(
                new MyCodeAction(
                    title,
                    c => FixAsync(context.Document, diagnostic, c),
                    equivalenceKey: GetEquivalenceKey(preference, isRemovableAssignment)),
                diagnostic);

            return;
        }

        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.GetLanguageService<ISyntaxFactsService>();
            return syntaxFacts.IsForEachStatement(diagnostic.Location.FindNode(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)
            => diagnostics.GroupBy(d => syntaxFacts.GetContainingMemberDeclaration(root, d.Location.SourceSpan.Start));

        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.GetLanguageService<ISyntaxFactsService>();
            var root = await document.GetSyntaxRootAsync(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)
        {
            document = await PreprocessDocumentAsync(document, diagnostics, cancellationToken).ConfigureAwait(false);
            var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
            var syntaxFacts = document.GetLanguageService<ISyntaxFactsService>();
            var semanticFacts = document.GetLanguageService<ISemanticFactsService>();

            var originalEditor = editor;
            editor = new SyntaxEditor(root, document.Project.Solution.Workspace);

            try
            {
                // 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 from 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.
                var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
                foreach (var diagnosticsToFix in diagnosticsGroupedByMember)
                {
                    var orderedDiagnostics = diagnosticsToFix.OrderByDescending(d => d.Location.SourceSpan.Start);
                    var containingMemberDeclaration = diagnosticsToFix.Key;
                    using var nameGenerator = new UniqueVariableNameGenerator(containingMemberDeclaration, semanticModel, semanticFacts, cancellationToken);

                    await FixAllAsync(diagnosticId, orderedDiagnostics, document, semanticModel, root, containingMemberDeclaration, preference,
                        removeAssignments, nameGenerator, editor, syntaxFacts, cancellationToken).ConfigureAwait(false);
                }

                // Second pass to post process the document.
                var currentRoot = editor.GetChangedRoot();
                var newRoot = await PostProcessDocumentAsync(document, currentRoot,
                    diagnosticId, preference, cancellationToken).ConfigureAwait(false);
                if (currentRoot != newRoot)
                {
                    editor.ReplaceNode(root, newRoot);
                }
            }
            finally
            {
                originalEditor.ReplaceNode(originalEditor.OriginalRoot, editor.GetChangedRoot());
            }
        }

        private async Task FixAllAsync(
            string diagnosticId,
            IOrderedEnumerable<Diagnostic> diagnostics,
            Document document,
            SemanticModel semanticModel,
            SyntaxNode root,
            SyntaxNode containingMemberDeclaration,
            UnusedValuePreference preference,
            bool removeAssignments,
            UniqueVariableNameGenerator nameGenerator,
            SyntaxEditor editor,
            ISyntaxFactsService syntaxFacts,
            CancellationToken cancellationToken)
        {
            switch (diagnosticId)
            {
                case IDEDiagnosticIds.ExpressionValueIsUnusedDiagnosticId:
                    FixAllExpressionValueIsUnusedDiagnostics(diagnostics, semanticModel, root,
                        preference, nameGenerator, editor, syntaxFacts);
                    break;

{'fraction_non_alphanumeric': 0.062461554234160346, 'fraction_numerical': 0.0006663932745540291, 'mean_word_length': 2.474663816902663, 'pattern_counts': {'":': 0, '<': 111, '<?xml version=': 0, '>': 135, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 10, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6351735', 'n_tokens_mistral': 8721, 'n_tokens_neox': 8410, 'n_words': 2586}