From 6c7e4ea371910ae99ba98fdc09293a8d4aa4160d Mon Sep 17 00:00:00 2001 From: heejaechang Date: Tue, 3 Feb 2015 13:26:32 -0800 Subject: [PATCH] Add an analyzer that can detect unused declarations. this is compilation end analyzer. this is supposed to squiggle name and fade out whole code block when it detects unused declarations. but for now, it only reports the issue in error list and not in editor due to engine limitation. we will use this analyzer to drive making compilation end analyzer experience better. so that we can get things like squiggle, fading out or code fix work with compilation end analyzer. (changeset 1409499) --- .../CSharpRoslynDiagnosticAnalyzers.csproj | 3 +- .../CSharpUnusedDeclarationsAnalyzer.cs | 45 ++++ .../UnusedDeclarationsAnalyzer.cs | 206 ++++++++++++++++++ .../UnusedDeclarationsCodeFixer.cs | 50 +++++ .../Core/RoslynDiagnosticAnalyzers.csproj | 4 +- .../Roslyn/Core/RoslynDiagnosticIds.cs | 2 + .../RoslynDiagnosticsResources.Designer.cs | 27 +++ .../Core/RoslynDiagnosticsResources.resx | 12 + .../BasicRoslynDiagnosticAnalyzers.vbproj | 3 +- .../BasicUnusedDeclarationsAnalyzer.vb | 37 ++++ 10 files changed, 386 insertions(+), 3 deletions(-) create mode 100644 src/Diagnostics/Roslyn/CSharp/Maintainability/CSharpUnusedDeclarationsAnalyzer.cs create mode 100644 src/Diagnostics/Roslyn/Core/Maintainability/UnusedDeclarationsAnalyzer.cs create mode 100644 src/Diagnostics/Roslyn/Core/Maintainability/UnusedDeclarationsCodeFixer.cs create mode 100644 src/Diagnostics/Roslyn/VisualBasic/Maintainability/BasicUnusedDeclarationsAnalyzer.vb diff --git a/src/Diagnostics/Roslyn/CSharp/CSharpRoslynDiagnosticAnalyzers.csproj b/src/Diagnostics/Roslyn/CSharp/CSharpRoslynDiagnosticAnalyzers.csproj index c7ad82f7b1a..46199e99af4 100644 --- a/src/Diagnostics/Roslyn/CSharp/CSharpRoslynDiagnosticAnalyzers.csproj +++ b/src/Diagnostics/Roslyn/CSharp/CSharpRoslynDiagnosticAnalyzers.csproj @@ -75,6 +75,7 @@ + @@ -92,4 +93,4 @@ - + \ No newline at end of file diff --git a/src/Diagnostics/Roslyn/CSharp/Maintainability/CSharpUnusedDeclarationsAnalyzer.cs b/src/Diagnostics/Roslyn/CSharp/Maintainability/CSharpUnusedDeclarationsAnalyzer.cs new file mode 100644 index 00000000000..c8e8b157dac --- /dev/null +++ b/src/Diagnostics/Roslyn/CSharp/Maintainability/CSharpUnusedDeclarationsAnalyzer.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Roslyn.Diagnostics.Analyzers.CSharp +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + internal class CSharpUnusedDeclarationsAnalyzer : UnusedDeclarationsAnalyzer + { + protected override SyntaxKind IdentifierSyntaxKind + { + get { return SyntaxKind.IdentifierName; } + } + + protected override SyntaxKind LocalDeclarationStatementSyntaxKind + { + get { return SyntaxKind.LocalDeclarationStatement; } + } + + protected override IEnumerable GetLocalDeclarationNodes(SyntaxNode node, CancellationToken cancellationToken) + { + var locals = node as LocalDeclarationStatementSyntax; + if (locals == null) + { + yield break; + } + + var variables = (locals.Declaration == null) ? (SeparatedSyntaxList?)null : locals.Declaration.Variables; + if (variables == null) + { + yield break; + } + + foreach (var variable in variables) + { + yield return variable; + } + } + } +} diff --git a/src/Diagnostics/Roslyn/Core/Maintainability/UnusedDeclarationsAnalyzer.cs b/src/Diagnostics/Roslyn/Core/Maintainability/UnusedDeclarationsAnalyzer.cs new file mode 100644 index 00000000000..6e65efbb1f5 --- /dev/null +++ b/src/Diagnostics/Roslyn/Core/Maintainability/UnusedDeclarationsAnalyzer.cs @@ -0,0 +1,206 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Roslyn.Diagnostics.Analyzers +{ + internal abstract partial class UnusedDeclarationsAnalyzer : DiagnosticAnalyzer where TLanguageKindEnum : struct + { + internal const string Category = "Maintainability"; + + private static readonly LocalizableString s_title = new LocalizableResourceString(nameof(RoslynDiagnosticsResources.UnusedDeclarationsTitle), RoslynDiagnosticsResources.ResourceManager, typeof(RoslynDiagnosticsResources)); + + private static readonly LocalizableString s_messageFormat = new LocalizableResourceString(nameof(RoslynDiagnosticsResources.UnusedDeclarationsMessage), RoslynDiagnosticsResources.ResourceManager, typeof(RoslynDiagnosticsResources)); + + internal static readonly DiagnosticDescriptor s_rule = new DiagnosticDescriptor(RoslynDiagnosticIds.DeadCodeRuleId, s_title, s_messageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true); + + internal static readonly DiagnosticDescriptor s_triggerRule = new TriggerDiagnosticDescriptor(RoslynDiagnosticIds.DeadCodeTriggerRuleId, WellKnownDiagnosticTags.Unnecessary, WellKnownDiagnosticTags.Telemetry); + + public override ImmutableArray SupportedDiagnostics + { + get { return ImmutableArray.Create(s_rule, s_triggerRule); } + } + + protected abstract TLanguageKindEnum IdentifierSyntaxKind { get; } + protected abstract TLanguageKindEnum LocalDeclarationStatementSyntaxKind { get; } + + protected abstract IEnumerable GetLocalDeclarationNodes(SyntaxNode node, CancellationToken cancellationToken); + + public override void Initialize(AnalysisContext context) + { + context.RegisterCompilationStartAction(c => + { + var tracker = new UnusedDeclarationsTracker(this); + + c.RegisterCompilationEndAction(tracker.OnCompilationEnd); + c.RegisterSyntaxNodeAction(tracker.OnIdentifier, IdentifierSyntaxKind); + c.RegisterSyntaxNodeAction(tracker.OnLocalDeclaration, LocalDeclarationStatementSyntaxKind); + c.RegisterSymbolAction( + tracker.OnSymbol, + SymbolKind.NamedType, + SymbolKind.Method, + SymbolKind.Property, + SymbolKind.Event, + SymbolKind.Field); + }); + } + + private class UnusedDeclarationsTracker + { + private readonly ConcurrentDictionary _used = new ConcurrentDictionary(concurrencyLevel: 2, capacity: 100); + private readonly UnusedDeclarationsAnalyzer _owner; + + public UnusedDeclarationsTracker(UnusedDeclarationsAnalyzer owner) + { + _owner = owner; + } + + public void OnIdentifier(SyntaxNodeAnalysisContext context) + { + context.CancellationToken.ThrowIfCancellationRequested(); + + var info = context.SemanticModel.GetSymbolInfo(context.Node, context.CancellationToken); + + var hasLocations = info.Symbol?.OriginalDefinition?.Locations.Length > 0; + var inSource = info.Symbol?.OriginalDefinition?.Locations[0].IsInSource == true; + if (!hasLocations || !inSource || AccessibleFromOutside(info.Symbol.OriginalDefinition)) + { + return; + } + + _used.AddOrUpdate(info.Symbol.OriginalDefinition, true, (k, v) => true); + } + + public void OnLocalDeclaration(SyntaxNodeAnalysisContext context) + { + foreach (var node in _owner.GetLocalDeclarationNodes(context.Node, context.CancellationToken)) + { + var local = context.SemanticModel.GetDeclaredSymbol(node, context.CancellationToken); + if (local == null) + { + continue; + } + + _used.TryAdd(local, false); + } + } + + public void OnSymbol(SymbolAnalysisContext context) + { + context.CancellationToken.ThrowIfCancellationRequested(); + var symbol = context.Symbol; + + if (!AccessibleFromOutside(symbol)) + { + _used.TryAdd(symbol, false); + } + + var type = symbol as INamedTypeSymbol; + if (type != null) + { + AddSymbolDeclarations(type.TypeParameters); + } + + var method = symbol as IMethodSymbol; + if (method != null) + { + AddParameters(method.DeclaredAccessibility, method.TypeParameters); + AddParameters(method.DeclaredAccessibility, method.Parameters); + } + + var property = symbol as IPropertySymbol; + if (property != null) + { + AddParameters(property.DeclaredAccessibility, property.Parameters); + + if (!AccessibleFromOutside(property.GetMethod)) + { + _used.TryAdd(property.GetMethod, false); + } + + if (!AccessibleFromOutside(property.SetMethod)) + { + _used.TryAdd(property.SetMethod, false); + + AddParameters(property.SetMethod.DeclaredAccessibility, property.SetMethod.Parameters); + } + } + } + + private void AddParameters(Accessibility accessibility, IEnumerable parameters) + { + // only add parameters if accessibility is explicitly set to private. + if (accessibility != Accessibility.Private) + { + return; + } + + AddSymbolDeclarations(parameters); + } + + private void AddSymbolDeclarations(IEnumerable symbols) + { + if (symbols == null) + { + return; + } + + foreach (var symbol in symbols) + { + _used.TryAdd(symbol, false); + } + } + + public void OnCompilationEnd(CompilationEndAnalysisContext context) + { + foreach (var kv in _used.Where(kv => !kv.Value && (kv.Key.Locations.FirstOrDefault()?.IsInSource == true))) + { + context.CancellationToken.ThrowIfCancellationRequested(); + var symbol = kv.Key; + + // report visible error only if symbol is not local symbol + if (!(symbol is ILocalSymbol)) + { + context.ReportDiagnostic(Diagnostic.Create(s_rule, symbol.Locations[0], symbol.Locations.Skip(1), symbol.Name)); + } + + // where code fix works + foreach (var reference in symbol.DeclaringSyntaxReferences) + { + context.CancellationToken.ThrowIfCancellationRequested(); + + context.ReportDiagnostic(Diagnostic.Create(s_triggerRule, Location.Create(reference.SyntaxTree, reference.Span))); + } + } + } + + private static bool AccessibleFromOutside(ISymbol symbol) + { + if (symbol == null || + symbol.Kind == SymbolKind.Namespace) + { + return true; + } + + if (symbol.DeclaredAccessibility == Accessibility.Private || + symbol.DeclaredAccessibility == Accessibility.NotApplicable) + { + return false; + } + + if (symbol.ContainingSymbol == null) + { + return true; + } + + return AccessibleFromOutside(symbol.ContainingSymbol.OriginalDefinition); + } + } + } +} diff --git a/src/Diagnostics/Roslyn/Core/Maintainability/UnusedDeclarationsCodeFixer.cs b/src/Diagnostics/Roslyn/Core/Maintainability/UnusedDeclarationsCodeFixer.cs new file mode 100644 index 00000000000..924bc6cdcd1 --- /dev/null +++ b/src/Diagnostics/Roslyn/Core/Maintainability/UnusedDeclarationsCodeFixer.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Immutable; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; + +namespace Roslyn.Diagnostics.Analyzers +{ + [ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic, Name = nameof(UnusedDeclarationsCodeFixProvider)), Shared] + internal class UnusedDeclarationsCodeFixProvider : CodeFixProvider + { + private static readonly Task _done = Task.Run(() => { }); + + public sealed override ImmutableArray FixableDiagnosticIds + { + get { return ImmutableArray.Create(RoslynDiagnosticIds.DeadCodeTriggerRuleId); } + } + + public sealed override FixAllProvider GetFixAllProvider() + { + return WellKnownFixAllProviders.BatchFixer; + } + + public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) + { + context.CancellationToken.ThrowIfCancellationRequested(); + + var document = context.Document; + context.RegisterCodeFix(new MyCodeAction(RoslynDiagnosticsResources.UnusedDeclarationsCodeFixTitle, async c => + { + var text = await document.GetTextAsync(c).ConfigureAwait(false); + return document.WithText(text.Replace(context.Span, string.Empty)); + }, RoslynDiagnosticIds.DeadCodeTriggerRuleId), context.Diagnostics); + + return _done; + } + + private class MyCodeAction : DocumentChangeAction + { + public MyCodeAction(string title, Func> changedDocument, string key) : + base(title, changedDocument, key) + { + } + } + } +} diff --git a/src/Diagnostics/Roslyn/Core/RoslynDiagnosticAnalyzers.csproj b/src/Diagnostics/Roslyn/Core/RoslynDiagnosticAnalyzers.csproj index 3c800b26602..6de240dc56d 100644 --- a/src/Diagnostics/Roslyn/Core/RoslynDiagnosticAnalyzers.csproj +++ b/src/Diagnostics/Roslyn/Core/RoslynDiagnosticAnalyzers.csproj @@ -74,6 +74,8 @@ + + @@ -108,4 +110,4 @@ - + \ No newline at end of file diff --git a/src/Diagnostics/Roslyn/Core/RoslynDiagnosticIds.cs b/src/Diagnostics/Roslyn/Core/RoslynDiagnosticIds.cs index 7d458f9de2b..c3e36659d2e 100644 --- a/src/Diagnostics/Roslyn/Core/RoslynDiagnosticIds.cs +++ b/src/Diagnostics/Roslyn/Core/RoslynDiagnosticIds.cs @@ -24,5 +24,7 @@ internal static class RoslynDiagnosticIds public const string RemoveDeletedApiRuleId = "RS0017"; public const string DoNotCreateTasksWithoutTaskSchedulerRuleId = "RS0018"; public const string SymbolDeclaredEventRuleId = "RS0019"; + public const string DeadCodeRuleId = "RS0020"; + public const string DeadCodeTriggerRuleId = "RS0021"; } } diff --git a/src/Diagnostics/Roslyn/Core/RoslynDiagnosticsResources.Designer.cs b/src/Diagnostics/Roslyn/Core/RoslynDiagnosticsResources.Designer.cs index c7e9d32f4ab..843433b1935 100644 --- a/src/Diagnostics/Roslyn/Core/RoslynDiagnosticsResources.Designer.cs +++ b/src/Diagnostics/Roslyn/Core/RoslynDiagnosticsResources.Designer.cs @@ -375,6 +375,33 @@ internal class RoslynDiagnosticsResources { } } + /// + /// Looks up a localized string similar to Remove unused code. + /// + internal static string UnusedDeclarationsCodeFixTitle { + get { + return ResourceManager.GetString("UnusedDeclarationsCodeFixTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to '{0}' is not used in solution.. + /// + internal static string UnusedDeclarationsMessage { + get { + return ResourceManager.GetString("UnusedDeclarationsMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to unused code. + /// + internal static string UnusedDeclarationsTitle { + get { + return ResourceManager.GetString("UnusedDeclarationsTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to Avoid zero-length array allocations.. /// diff --git a/src/Diagnostics/Roslyn/Core/RoslynDiagnosticsResources.resx b/src/Diagnostics/Roslyn/Core/RoslynDiagnosticsResources.resx index 5bad417e70d..817a55268cf 100644 --- a/src/Diagnostics/Roslyn/Core/RoslynDiagnosticsResources.resx +++ b/src/Diagnostics/Roslyn/Core/RoslynDiagnosticsResources.resx @@ -237,6 +237,18 @@ When removing a public type or member the corresponding entry in PublicAPI.txt should also be removed. This draws attention to API changes in the code reviews and source control history, and helps prevent breaking changes. + + Remove unused code + The title of the fix + + + '{0}' is not used in solution. + The format-able message the diagnostic displays. + + + unused code + The title of the diagnostic. + Do not create tasks unless you are using one of the overloads that takes a TaskScheduler. The default is to schedule on TaskScheduler.Current, which would lead to deadlocks. Either use TaskScheduler.Default to schedule on the thread pool, or explicitly pass TaskScheduler.Current to make your intentions clear. diff --git a/src/Diagnostics/Roslyn/VisualBasic/BasicRoslynDiagnosticAnalyzers.vbproj b/src/Diagnostics/Roslyn/VisualBasic/BasicRoslynDiagnosticAnalyzers.vbproj index 442848af1f8..65f1e57807b 100644 --- a/src/Diagnostics/Roslyn/VisualBasic/BasicRoslynDiagnosticAnalyzers.vbproj +++ b/src/Diagnostics/Roslyn/VisualBasic/BasicRoslynDiagnosticAnalyzers.vbproj @@ -95,6 +95,7 @@ + @@ -115,4 +116,4 @@ - + \ No newline at end of file diff --git a/src/Diagnostics/Roslyn/VisualBasic/Maintainability/BasicUnusedDeclarationsAnalyzer.vb b/src/Diagnostics/Roslyn/VisualBasic/Maintainability/BasicUnusedDeclarationsAnalyzer.vb new file mode 100644 index 00000000000..32afe13d6ca --- /dev/null +++ b/src/Diagnostics/Roslyn/VisualBasic/Maintainability/BasicUnusedDeclarationsAnalyzer.vb @@ -0,0 +1,37 @@ +' Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +Imports System.Threading +Imports Microsoft.CodeAnalysis +Imports Microsoft.CodeAnalysis.Diagnostics +Imports Microsoft.CodeAnalysis.VisualBasic +Imports Microsoft.CodeAnalysis.VisualBasic.Syntax + +Namespace Roslyn.Diagnostics.Analyzers.VisualBasic + + Friend Class VisualBasicUnusedDeclarationsAnalyzer + Inherits UnusedDeclarationsAnalyzer(Of SyntaxKind) + + Protected Overrides ReadOnly Property IdentifierSyntaxKind As SyntaxKind + Get + Return SyntaxKind.IdentifierName + End Get + End Property + + Protected Overrides ReadOnly Property LocalDeclarationStatementSyntaxKind As SyntaxKind + Get + Return SyntaxKind.LocalDeclarationStatement + End Get + End Property + + Protected Overrides Iterator Function GetLocalDeclarationNodes(node As SyntaxNode, cancellationToken As CancellationToken) As IEnumerable(Of SyntaxNode) + Dim locals = TryCast(node, LocalDeclarationStatementSyntax) + If locals Is Nothing Then + Return + End If + + For Each variable In locals.Declarators + Yield variable + Next + End Function + End Class +End Namespace \ No newline at end of file -- GitLab