public static void CreateAndRegisterActions( CompilationStartAnalysisContext context, AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer analyzer) { var attributeSetForMethodsToIgnore = ImmutableHashSet.CreateRange(GetAttributesForMethodsToIgnore(context.Compilation).WhereNotNull()); var eventsArgType = context.Compilation.EventArgsType(); var deserializationConstructorCheck = new DeserializationConstructorCheck(context.Compilation); var iCustomMarshaler = context.Compilation.GetTypeByMetadataName(typeof(ICustomMarshaler).FullName !); context.RegisterSymbolStartAction(symbolStartContext => { if (HasSyntaxErrors((INamedTypeSymbol)symbolStartContext.Symbol, symbolStartContext.CancellationToken)) { // Bail out on syntax errors. return; } // Create a new SymbolStartAnalyzer instance for every named type symbol // to ensure there is no shared state (such as identified unused parameters within the type), // as that would lead to duplicate diagnostics being reported from symbol end action callbacks // for unrelated named types. var symbolAnalyzer = new SymbolStartAnalyzer(analyzer, eventsArgType, attributeSetForMethodsToIgnore, deserializationConstructorCheck, iCustomMarshaler); symbolAnalyzer.OnSymbolStart(symbolStartContext); }, SymbolKind.NamedType); return;
private 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)); }
public static void CreateAndRegisterActions( CompilationStartAnalysisContext context, AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer analyzer) { var attributeSetForMethodsToIgnore = ImmutableHashSet.CreateRange(GetAttributesForMethodsToIgnore(context.Compilation).WhereNotNull()); var eventsArgType = context.Compilation.EventArgsType(); var symbolAnalyzer = new SymbolStartAnalyzer(analyzer, eventsArgType, attributeSetForMethodsToIgnore); context.RegisterSymbolStartAction(symbolAnalyzer.OnSymbolStart, SymbolKind.NamedType); }
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)); }
public SymbolStartAnalyzer( AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer compilationAnalyzer, INamedTypeSymbol eventArgsTypeOpt, ImmutableHashSet <INamedTypeSymbol> attributeSetForMethodsToIgnore) { _compilationAnalyzer = compilationAnalyzer; _eventArgsTypeOpt = eventArgsTypeOpt; _attributeSetForMethodsToIgnore = attributeSetForMethodsToIgnore; _unusedParameters = new ConcurrentDictionary <IParameterSymbol, bool>(); _methodsUsedAsDelegates = new ConcurrentDictionary <IMethodSymbol, bool>(); }
public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) { var diagnostic = context.Diagnostics[0]; if (!AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.TryGetUnusedValuePreference(diagnostic, out var preference)) { return(Task.CompletedTask); } var isRemovableAssignment = AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.GetIsRemovableAssignmentDiagnostic(diagnostic); string title; if (isRemovableAssignment) { // Recommend removing the redundant constant value assignment. title = FeaturesResources.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(Task.CompletedTask); } title = FeaturesResources.Use_discard_underscore; break; case UnusedValuePreference.UnusedLocalVariable: title = FeaturesResources.Use_discarded_local; break; default: return(Task.CompletedTask); } } context.RegisterCodeFix( new MyCodeAction( title, c => FixAsync(context.Document, diagnostic, c), equivalenceKey: GetEquivalenceKey(preference, isRemovableAssignment)), diagnostic); return(Task.CompletedTask); }
public SymbolStartAnalyzer( AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer compilationAnalyzer, INamedTypeSymbol eventArgsTypeOpt, ImmutableHashSet <INamedTypeSymbol> attributeSetForMethodsToIgnore, DeserializationConstructorCheck deserializationConstructorCheck, INamedTypeSymbol iCustomMarshaler) { _compilationAnalyzer = compilationAnalyzer; _eventArgsTypeOpt = eventArgsTypeOpt; _attributeSetForMethodsToIgnore = attributeSetForMethodsToIgnore; _deserializationConstructorCheck = deserializationConstructorCheck; _unusedParameters = new ConcurrentDictionary <IParameterSymbol, bool>(); _methodsUsedAsDelegates = new ConcurrentDictionary <IMethodSymbol, bool>(); _iCustomMarshaler = iCustomMarshaler; }
public static void CreateAndRegisterActions( CompilationStartAnalysisContext context, AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer analyzer) { var attributeSetForMethodsToIgnore = ImmutableHashSet.CreateRange(GetAttributesForMethodsToIgnore(context.Compilation).WhereNotNull()); var eventsArgType = context.Compilation.EventArgsType(); context.RegisterSymbolStartAction(symbolStartContext => { // Create a new SymbolStartAnalyzer instance for every named type symbol // to ensure there is no shared state (such as identified unused parameters within the type), // as that would lead to duplicate diagnostics being reported from symbol end action callbacks // for unrelated named types. var symbolAnalyzer = new SymbolStartAnalyzer(analyzer, eventsArgType, attributeSetForMethodsToIgnore); symbolAnalyzer.OnSymbolStart(symbolStartContext); }, SymbolKind.NamedType); }
private async Task FixAllValueAssignedIsUnusedDiagnosticsAsync( IOrderedEnumerable <Diagnostic> diagnostics, Document document, SemanticModel semanticModel, SyntaxNode root, SyntaxNode containingMemberDeclaration, UnusedValuePreference preference, bool removeAssignments, UniqueVariableNameGenerator nameGenerator, SyntaxEditor editor, ISyntaxFactsService syntaxFacts, CancellationToken cancellationToken) { // 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. var nodeReplacementMap = PooledDictionary <SyntaxNode, SyntaxNode> .GetInstance(); var nodesToRemove = PooledHashSet <SyntaxNode> .GetInstance(); var nodesToAdd = PooledHashSet <(TLocalDeclarationStatementSyntax declarationStatement, SyntaxNode node)> .GetInstance(); // Indicates if the node's trivia was processed. var processedNodes = PooledHashSet <SyntaxNode> .GetInstance(); var candidateDeclarationStatementsForRemoval = PooledHashSet <TLocalDeclarationStatementSyntax> .GetInstance(); var hasAnyUnusedLocalAssignment = false; try { 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>(); Debug.Assert(variableDeclarator != null); nodesToRemove.Add(variableDeclarator); // Local declaration statement containing the declarator might be a candidate for removal if all its variables get marked for removal. candidateDeclarationStatementsForRemoval.Add(variableDeclarator.GetAncestor <TLocalDeclarationStatementSyntax>()); } else { // Redundant assignment or increment/decrement. if (syntaxFacts.IsOperandOfIncrementOrDecrementExpression(node)) { // For example, C# increment operation "a++;" Debug.Assert(node.Parent.Parent is TExpressionStatementSyntax); nodesToRemove.Add(node.Parent.Parent); } 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.Parent, syntaxFacts.GetRightHandSideOfAssignment(node.Parent)); } } } } 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 indentifier token with new named, based on user's preference. var newNameToken = preference == UnusedValuePreference.DiscardVariable ? editor.Generator.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.Parent, GetReplacementNodeForCompoundAssignment(node.Parent, newNameNode, editor, syntaxFacts)); } 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; Debug.Assert(type != null); Debug.Assert(newLocalNameOpt != null); 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 nodeToAdd in nodesToAdd) { InsertLocalDeclarationStatement(nodeToAdd.declarationStatement, nodeToAdd.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 abd 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 kvp in nodeReplacementMap) { editor.ReplaceNode(kvp.Key, kvp.Value.WithAdditionalAnnotations(Formatter.Annotation)); } } finally { nodeReplacementMap.Free(); nodesToRemove.Free(); nodesToAdd.Free(); processedNodes.Free(); } 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(editor.Generator.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 || syntaxFacts.IsExecutableBlock(n.Parent) && !(n is TCatchStatementSyntax) && !(n is 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 with the new declaration. if (nodesToRemove.Contains(insertionNode) && !processedNodes.Contains(insertionNode)) { declarationStatement = declarationStatement.WithLeadingTrivia(insertionNode.GetLeadingTrivia()); // 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); } }
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 = FeaturesResources.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 = FeaturesResources.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 = FeaturesResources.Remove_redundant_assignment; } break; case UnusedValuePreference.UnusedLocalVariable: title = FeaturesResources.Use_discarded_local; break; default: return; } } context.RegisterCodeFix( new MyCodeAction( title, c => FixAsync(context.Document, diagnostic, c), equivalenceKey: GetEquivalenceKey(preference, isRemovableAssignment)), diagnostic); return; }