/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Analyzers/Core/CodeFixes/RemoveUnusedParametersAndValues/AbstractRemoveUnusedValuesCodeFixProvider.cs
949 строк
51 KB
Cyrus Najmabadi
Use WhereAsArray
05 авг 2025, 13:55
05 авг 2025, 13:55
ed8dfdd
Код
Авторство
О чём код?
// 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(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() .WhereAsArray(n => n.HasAnnotation(s_newLocalDeclarationStatementAnnotation) || n.HasAnnotation(s_existingLocalDeclarationWithoutInitializerAnnotation)); 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(); } }