| File: RemoveUnusedVariable\AbstractRemoveUnusedVariableCodeFixProvider.cs | Web Access |
| Project: src\roslyn\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. #nullable disable using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.FindSymbols; using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.RemoveUnusedVariable; internal abstract class AbstractRemoveUnusedVariableCodeFixProvider<TLocalDeclarationStatement, TVariableDeclarator, TVariableDeclaration> : SyntaxEditorBasedCodeFixProvider where TLocalDeclarationStatement : SyntaxNode where TVariableDeclarator : SyntaxNode where TVariableDeclaration : SyntaxNode { protected abstract bool IsCatchDeclarationIdentifier(SyntaxToken token); protected abstract SyntaxNode GetNodeToRemoveOrReplace(SyntaxNode node); protected abstract void RemoveOrReplaceNode(SyntaxEditor editor, SyntaxNode node, IBlockFactsService blockFacts); protected abstract SeparatedSyntaxList<SyntaxNode> GetVariables(TLocalDeclarationStatement localDeclarationStatement); protected abstract bool ShouldOfferFixForLocalDeclaration(IBlockFactsService blockFacts, SyntaxNode node); public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) { var diagnostic = context.Diagnostics.First(); var document = context.Document; var root = await document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); var node = root.FindNode(diagnostic.Location.SourceSpan); var blockFacts = document.GetRequiredLanguageService<IBlockFactsService>(); if (ShouldOfferFixForLocalDeclaration(blockFacts, node)) { RegisterCodeFix(context, FeaturesResources.Remove_unused_variable, nameof(FeaturesResources.Remove_unused_variable)); } } protected override async Task FixAllAsync(Document document, ImmutableArray<Diagnostic> diagnostics, SyntaxEditor syntaxEditor, CancellationToken cancellationToken) { var nodesToRemove = new HashSet<SyntaxNode>(); // Create actions and keep their SpanStart. // Execute actions ordered descending by SpanStart to avoid conflicts. var actionsToPerform = new List<(int, Action)>(); var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); var documentEditor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); var documentsToBeSearched = ImmutableHashSet.Create(document); foreach (var diagnostic in diagnostics) { var token = diagnostic.Location.FindToken(cancellationToken); var node = root.FindNode(diagnostic.Location.SourceSpan); if (IsCatchDeclarationIdentifier(token)) { (int, Action) pair = (token.Parent.SpanStart, () => syntaxEditor.ReplaceNode( token.Parent, token.Parent.ReplaceToken(token, default(SyntaxToken)).WithAdditionalAnnotations(Formatter.Annotation))); actionsToPerform.Add(pair); } else { nodesToRemove.Add(node); } var symbol = documentEditor.SemanticModel.GetDeclaredSymbol(node, cancellationToken); var referencedSymbols = await SymbolFinder.FindReferencesAsync(symbol, document.Project.Solution, documentsToBeSearched, cancellationToken).ConfigureAwait(false); foreach (var referencedSymbol in referencedSymbols) { if (referencedSymbol?.Locations != null) { foreach (var location in referencedSymbol.Locations) { var referencedSymbolNode = root.FindNode(location.Location.SourceSpan); if (referencedSymbolNode != null) { var nodeToRemoveOrReplace = GetNodeToRemoveOrReplace(referencedSymbolNode); if (nodeToRemoveOrReplace != null) { nodesToRemove.Add(nodeToRemoveOrReplace); } } } } } } MergeNodesToRemove(nodesToRemove); var blockFacts = document.GetLanguageService<IBlockFactsService>(); foreach (var node in nodesToRemove) actionsToPerform.Add((node.SpanStart, () => RemoveOrReplaceNode(syntaxEditor, node, blockFacts))); // Process nodes in reverse order // to complete with nested declarations before processing the outer ones. foreach (var node in actionsToPerform.OrderByDescending(n => n.Item1)) node.Item2(); } protected static void RemoveNode(SyntaxEditor editor, SyntaxNode node, IBlockFactsService blockFacts) { var localDeclaration = node.GetAncestorOrThis<TLocalDeclarationStatement>(); var removeOptions = CreateSyntaxRemoveOptions(localDeclaration, blockFacts); editor.RemoveNode(node, removeOptions); } private static SyntaxRemoveOptions CreateSyntaxRemoveOptions( TLocalDeclarationStatement localDeclaration, IBlockFactsService blockFacts) { var removeOptions = SyntaxGenerator.DefaultRemoveOptions; if (localDeclaration != null) { if (localDeclaration.GetLeadingTrivia().Contains(t => t.IsDirective)) { removeOptions |= SyntaxRemoveOptions.KeepLeadingTrivia; } else { var statementParent = blockFacts.GetImmediateParentExecutableBlockForStatement(localDeclaration); if (statementParent != null) { var siblings = blockFacts.GetExecutableBlockStatements(statementParent); var localDeclarationIndex = siblings.IndexOf(localDeclaration); if (localDeclarationIndex != 0) { // if we're removing the first statement in a block, then we // want to have the elastic marker on it so that the next statement // properly formats with the space left behind. But if it's // not the first statement then just keep the trivia as is // so that the statement before and after it stay appropriately // spaced apart. removeOptions &= ~SyntaxRemoveOptions.AddElasticMarker; } } } } return removeOptions; } // Merges node like // var unused1 = 0, unused2 = 0; // to remove the whole line. private void MergeNodesToRemove(HashSet<SyntaxNode> nodesToRemove) { var candidateLocalDeclarationsToRemove = new HashSet<TLocalDeclarationStatement>(); foreach (var variableDeclarator in nodesToRemove.OfType<TVariableDeclarator>()) { // Parents of the variable declarator could be candaditaes for removal for example // if all declarators in a declaration will be removed. if (variableDeclarator.Parent?.Parent is TLocalDeclarationStatement candidate) { candidateLocalDeclarationsToRemove.Add(candidate); } } foreach (var candidate in candidateLocalDeclarationsToRemove) { var hasUsedLocal = false; foreach (var variable in GetVariables(candidate)) { if (!nodesToRemove.Contains(variable)) { hasUsedLocal = true; break; } } if (!hasUsedLocal) { nodesToRemove.Add(candidate); foreach (var variable in GetVariables(candidate)) { nodesToRemove.Remove(variable); } } } } }