diff --git a/src/Diagnostics/Roslyn/CSharp/CSharpRoslynDiagnosticAnalyzers.csproj b/src/Diagnostics/Roslyn/CSharp/CSharpRoslynDiagnosticAnalyzers.csproj index c7ad82f7b1a18830ee8841ea0bc62857252ed51e..46199e99af42aa06312b9c97fe5504b8f7aedb00 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 0000000000000000000000000000000000000000..c8e8b157dac541272e921d23cdb20bb2893fd5d7 --- /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 0000000000000000000000000000000000000000..6e65efbb1f54eebe2881d53dfc6bed24babecfe4 --- /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 0000000000000000000000000000000000000000..924bc6cdcd1d5526202c019b7fadb3a937ca5c90 --- /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 3c800b26602f59c7c3b3ce1b98eb31735b6719ca..6de240dc56d62c31ed1130861fd4ec695557142d 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 7d458f9de2b5d3733b18c1c4e641c34583394f76..c3e36659d2e2c7d1b52d1334b44dce0dc957802e 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 c7e9d32f4ab9693152ae911f471a7afa951cf0cf..843433b1935dc109c154ef71f8ec0b57a3b7e0f3 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 5bad417e70d28d3cac5142a26f3c22b70481f358..817a55268cfefee10e1c53bf02d375e851ae4287 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 442848af1f8f00b81e4f1aa7d6ee7564c086146c..65f1e57807b9c0365324291fb8a07b93fbdb1504 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 0000000000000000000000000000000000000000..32afe13d6ca8a55df37b298e09f1abfbcc9d7ba7 --- /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