diff --git a/ChangeLog.md b/ChangeLog.md
index 63e74c6e7b..eedd9b96c8 100644
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Added
+
+- [CLI] Add command `find-symbol` ([PR](https://github.com/dotnet/roslynator/pull/1255))
+ - This command can be used not only to find symbols but also to find unused symbols and optionally remove them.
+ - Example: `roslynator find-symbol --symbol-kind type --visibility internal private --unused --remove`
+
### Changed
- Bump Roslyn to 4.6.0 ([PR](https://github.com/dotnet/roslynator/pull/1248)).
diff --git a/src/CSharp.Workspaces/CSharp.Workspaces.csproj b/src/CSharp.Workspaces/CSharp.Workspaces.csproj
index f3578ed24e..c54fb4fcca 100644
--- a/src/CSharp.Workspaces/CSharp.Workspaces.csproj
+++ b/src/CSharp.Workspaces/CSharp.Workspaces.csproj
@@ -106,6 +106,9 @@ Roslynator.NameGenerator
<_Parameter1>Roslynator.Formatting.Analyzers.Tests, PublicKey=$(RoslynatorPublicKey)
+
+ <_Parameter1>Roslynator, PublicKey=$(RoslynatorPublicKey)
+
diff --git a/src/CSharp.Workspaces/CSharp/Extensions/WorkspaceExtensions.cs b/src/CSharp.Workspaces/CSharp/Extensions/WorkspaceExtensions.cs
index a4da0d04a3..5a259c19a7 100644
--- a/src/CSharp.Workspaces/CSharp/Extensions/WorkspaceExtensions.cs
+++ b/src/CSharp.Workspaces/CSharp/Extensions/WorkspaceExtensions.cs
@@ -126,6 +126,12 @@ internal static Task RemoveMemberAsync(
return document.ReplaceNodeAsync(recordDeclaration, SyntaxRefactorings.RemoveMember(recordDeclaration, member), cancellationToken);
}
+ case SyntaxKind.EnumDeclaration:
+ {
+ var enumDeclaration = (EnumDeclarationSyntax)parent;
+
+ return document.ReplaceNodeAsync(enumDeclaration, SyntaxRefactorings.RemoveMember(enumDeclaration, (EnumMemberDeclarationSyntax)member), cancellationToken);
+ }
default:
{
SyntaxDebug.Assert(parent is null, parent);
diff --git a/src/CSharp/CSharp/SyntaxRefactorings.cs b/src/CSharp/CSharp/SyntaxRefactorings.cs
index 1d15b6fba9..3f22662191 100644
--- a/src/CSharp/CSharp/SyntaxRefactorings.cs
+++ b/src/CSharp/CSharp/SyntaxRefactorings.cs
@@ -153,7 +153,7 @@ public static SyntaxRemoveOptions GetRemoveOptions(CSharpSyntaxNode node)
return removeOptions;
}
- internal static MemberDeclarationSyntax RemoveSingleLineDocumentationComment(MemberDeclarationSyntax declaration)
+ internal static TMemberDeclaration RemoveSingleLineDocumentationComment(TMemberDeclaration declaration) where TMemberDeclaration : MemberDeclarationSyntax
{
if (declaration is null)
throw new ArgumentNullException(nameof(declaration));
@@ -492,6 +492,23 @@ public static TypeDeclarationSyntax RemoveMember(TypeDeclarationSyntax typeDecla
return RemoveNode(typeDeclaration, f => f.Members, index, GetRemoveOptions(newMember));
}
+ public static EnumDeclarationSyntax RemoveMember(EnumDeclarationSyntax typeDeclaration, EnumMemberDeclarationSyntax member)
+ {
+ if (typeDeclaration is null)
+ throw new ArgumentNullException(nameof(typeDeclaration));
+
+ if (member is null)
+ throw new ArgumentNullException(nameof(member));
+
+ int index = typeDeclaration.Members.IndexOf(member);
+
+ EnumMemberDeclarationSyntax newMember = RemoveSingleLineDocumentationComment(member);
+
+ typeDeclaration = typeDeclaration.WithMembers(typeDeclaration.Members.ReplaceAt(index, newMember));
+
+ return RemoveNode(typeDeclaration, f => f.Members, index, GetRemoveOptions(newMember));
+ }
+
private static T RemoveNode(
T declaration,
Func> getMembers,
@@ -524,6 +541,38 @@ private static T RemoveNode(
return newDeclaration;
}
+ private static T RemoveNode(
+ T declaration,
+ Func> getMembers,
+ int index,
+ SyntaxRemoveOptions removeOptions) where T : SyntaxNode
+ {
+ SeparatedSyntaxList members = getMembers(declaration);
+
+ T newDeclaration = declaration.RemoveNode(members[index], removeOptions)!;
+
+ if (index == 0
+ && index < members.Count - 1)
+ {
+ members = getMembers(newDeclaration);
+
+ EnumMemberDeclarationSyntax nextMember = members[index];
+
+ SyntaxTriviaList leadingTrivia = nextMember.GetLeadingTrivia();
+
+ SyntaxTrivia trivia = leadingTrivia.FirstOrDefault();
+
+ if (trivia.IsEndOfLineTrivia())
+ {
+ EnumMemberDeclarationSyntax newNextMember = nextMember.WithLeadingTrivia(leadingTrivia.RemoveAt(0));
+
+ newDeclaration = newDeclaration.ReplaceNode(nextMember, newNextMember);
+ }
+ }
+
+ return newDeclaration;
+ }
+
public static BlockSyntax RemoveUnsafeContext(UnsafeStatementSyntax unsafeStatement)
{
SyntaxToken keyword = unsafeStatement.UnsafeKeyword;
diff --git a/src/CommandLine/Commands/FindSymbolCommand.cs b/src/CommandLine/Commands/FindSymbolCommand.cs
new file mode 100644
index 0000000000..4178c6dd2b
--- /dev/null
+++ b/src/CommandLine/Commands/FindSymbolCommand.cs
@@ -0,0 +1,252 @@
+// Copyright (c) .NET Foundation and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license 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.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Roslynator.CSharp;
+using Roslynator.FindSymbols;
+using Roslynator.Host.Mef;
+using static Roslynator.Logger;
+
+namespace Roslynator.CommandLine;
+
+internal class FindSymbolCommand : MSBuildWorkspaceCommand
+{
+ private static readonly SymbolDisplayFormat _nameAndContainingTypesSymbolDisplayFormat = SymbolDisplayFormat.CSharpErrorMessageFormat.Update(
+ typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypes,
+ parameterOptions: SymbolDisplayParameterOptions.IncludeParamsRefOut
+ | SymbolDisplayParameterOptions.IncludeType
+ | SymbolDisplayParameterOptions.IncludeName
+ | SymbolDisplayParameterOptions.IncludeDefaultValue,
+ miscellaneousOptions: SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers
+ | SymbolDisplayMiscellaneousOptions.UseSpecialTypes
+ | SymbolDisplayMiscellaneousOptions.UseErrorTypeSymbolName);
+
+ public FindSymbolCommand(
+ FindSymbolCommandLineOptions options,
+ SymbolFinderOptions symbolFinderOptions,
+ in ProjectFilter projectFilter,
+ FileSystemFilter fileSystemFilter) : base(projectFilter, fileSystemFilter)
+ {
+ Options = options;
+ SymbolFinderOptions = symbolFinderOptions;
+ }
+
+ public FindSymbolCommandLineOptions Options { get; }
+
+ public SymbolFinderOptions SymbolFinderOptions { get; }
+
+ public override async Task ExecuteAsync(ProjectOrSolution projectOrSolution, CancellationToken cancellationToken = default)
+ {
+ ImmutableArray allSymbols;
+
+ if (projectOrSolution.IsProject)
+ {
+ Project project = projectOrSolution.AsProject();
+
+ WriteLine($"Analyze '{project.Name}'", Verbosity.Minimal);
+
+ allSymbols = await AnalyzeProject(project, SymbolFinderOptions, cancellationToken);
+ }
+ else
+ {
+ Solution solution = projectOrSolution.AsSolution();
+
+ WriteLine($"Analyze solution '{solution.FilePath}'", Verbosity.Minimal);
+
+ ImmutableArray.Builder symbols = ImmutableArray.CreateBuilder();
+
+ Stopwatch stopwatch = Stopwatch.StartNew();
+
+ foreach (ProjectId projectId in FilterProjects(
+ solution,
+ s => s
+ .GetProjectDependencyGraph()
+ .GetTopologicallySortedProjects(cancellationToken)
+ .ToImmutableArray())
+ .Select(f => f.Id))
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ Project project = solution.GetProject(projectId);
+
+ WriteLine($" Analyze '{project.Name}'", Verbosity.Minimal);
+
+ ImmutableArray projectSymbols = await AnalyzeProject(project, SymbolFinderOptions, cancellationToken);
+
+ if (!projectSymbols.Any())
+ continue;
+
+ int maxKindLength = projectSymbols
+ .Select(f => f.GetSymbolGroup())
+ .Distinct()
+ .Max(f => f.ToString().Length);
+
+ foreach (ISymbol symbol in projectSymbols.OrderBy(f => f, SymbolDefinitionComparer.SystemFirst))
+ {
+ WriteSymbol(symbol, Verbosity.Normal, indentation: " ", padding: maxKindLength);
+ }
+
+ if (Options.Remove)
+ {
+ project = await RemoveSymbolsAsync(projectSymbols, project, cancellationToken);
+
+ if (!solution.Workspace.TryApplyChanges(project.Solution))
+ WriteLine("Cannot remove symbols from a solution", ConsoleColors.Yellow, Verbosity.Detailed);
+
+ solution = solution.Workspace.CurrentSolution;
+ }
+
+ symbols.AddRange(projectSymbols);
+ }
+
+ stopwatch.Stop();
+
+ allSymbols = symbols?.ToImmutableArray() ?? ImmutableArray.Empty;
+
+ LogHelpers.WriteElapsedTime($"Analyzed solution '{solution.FilePath}'", stopwatch.Elapsed, Verbosity.Minimal);
+ }
+
+ if (allSymbols.Any())
+ {
+ Dictionary countByGroup = allSymbols
+ .GroupBy(f => f.GetSymbolGroup())
+ .OrderByDescending(f => f.Count())
+ .ThenBy(f => f.Key)
+ .ToDictionary(f => f.Key, f => f.Count());
+
+ int maxKindLength = countByGroup.Max(f => f.Key.ToString().Length);
+ int maxCountLength = countByGroup.Max(f => f.Value.ToString().Length);
+
+ WriteLine(Verbosity.Normal);
+
+ foreach (ISymbol symbol in allSymbols.OrderBy(f => f, SymbolDefinitionComparer.SystemFirst))
+ {
+ WriteSymbol(symbol, Verbosity.Normal, colorNamespace: true, padding: maxKindLength);
+ }
+
+ WriteLine(Verbosity.Normal);
+
+ foreach (KeyValuePair kvp in countByGroup)
+ {
+ WriteLine($"{kvp.Value.ToString().PadLeft(maxCountLength)} {kvp.Key.ToString().ToLowerInvariant()} symbols", Verbosity.Normal);
+ }
+ }
+
+ WriteLine(Verbosity.Minimal);
+ WriteLine($"{allSymbols.Length} {((allSymbols.Length == 1) ? "symbol" : "symbols")} found", ConsoleColors.Green, Verbosity.Minimal);
+
+ return CommandResults.Success;
+ }
+
+ private static Task> AnalyzeProject(
+ Project project,
+ SymbolFinderOptions options,
+ CancellationToken cancellationToken)
+ {
+ if (!project.SupportsCompilation)
+ {
+ WriteLine(" Project does not support compilation", Verbosity.Normal);
+ return Task.FromResult(ImmutableArray.Empty);
+ }
+
+ if (!MefWorkspaceServices.Default.SupportedLanguages.Contains(project.Language))
+ {
+ WriteLine($" Language '{project.Language}' is not supported", Verbosity.Normal);
+ return Task.FromResult(ImmutableArray.Empty);
+ }
+
+ return SymbolFinder.FindSymbolsAsync(project, options, cancellationToken);
+ }
+
+ private static async Task RemoveSymbolsAsync(
+ ImmutableArray symbols,
+ Project project,
+ CancellationToken cancellationToken)
+ {
+ foreach (IGrouping grouping in symbols
+ .SelectMany(f => f.DeclaringSyntaxReferences)
+ .GroupBy(f => project.GetDocument(f.SyntaxTree).Id))
+ {
+ foreach (SyntaxReference reference in grouping.OrderByDescending(f => f.Span.Start))
+ {
+ Document document = project.GetDocument(grouping.Key);
+ SyntaxNode root = await document.GetSyntaxRootAsync(cancellationToken);
+ SyntaxNode node = root.FindNode(reference.Span);
+
+ if (node is MemberDeclarationSyntax memberDeclaration)
+ {
+ Document newDocument = await document.RemoveMemberAsync(memberDeclaration, cancellationToken);
+ project = newDocument.Project;
+ }
+ else if (node is VariableDeclaratorSyntax
+ && node.Parent is VariableDeclarationSyntax variableDeclaration
+ && node.Parent.Parent is FieldDeclarationSyntax fieldDeclaration)
+ {
+ if (variableDeclaration.Variables.Count == 1)
+ {
+ Document newDocument = await document.RemoveMemberAsync(fieldDeclaration, cancellationToken);
+ project = newDocument.Project;
+ }
+ }
+ else
+ {
+ Debug.Fail(node.Kind().ToString());
+ }
+ }
+ }
+
+ return project;
+ }
+
+ private static void WriteSymbol(
+ ISymbol symbol,
+ Verbosity verbosity,
+ string indentation = "",
+ bool colorNamespace = true,
+ int padding = 0)
+ {
+ if (!ShouldWrite(verbosity))
+ return;
+
+ Write(indentation, verbosity);
+
+ string kindText = symbol.GetSymbolGroup().ToString().ToLowerInvariant();
+
+ if (symbol.IsKind(SymbolKind.NamedType))
+ {
+ Write(kindText, ConsoleColors.Cyan, verbosity);
+ }
+ else
+ {
+ Write(kindText, verbosity);
+ }
+
+ Write(' ', padding - kindText.Length + 1, verbosity);
+
+ string namespaceText = symbol.ContainingNamespace.ToDisplayString();
+
+ if (namespaceText.Length > 0)
+ {
+ if (colorNamespace)
+ {
+ Write(namespaceText, ConsoleColors.DarkGray, verbosity);
+ Write(".", ConsoleColors.DarkGray, verbosity);
+ }
+ else
+ {
+ Write(namespaceText, verbosity);
+ Write(".", verbosity);
+ }
+ }
+
+ WriteLine(symbol.ToDisplayString(_nameAndContainingTypesSymbolDisplayFormat), verbosity);
+ }
+}
diff --git a/src/CommandLine/FindSymbols/SymbolFinder.cs b/src/CommandLine/FindSymbols/SymbolFinder.cs
new file mode 100644
index 0000000000..2b5912dc37
--- /dev/null
+++ b/src/CommandLine/FindSymbols/SymbolFinder.cs
@@ -0,0 +1,130 @@
+// Copyright (c) .NET Foundation and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Diagnostics;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using Roslynator.Host.Mef;
+
+namespace Roslynator.FindSymbols;
+
+internal static class SymbolFinder
+{
+ internal static async Task> FindSymbolsAsync(
+ Project project,
+ SymbolFinderOptions options = null,
+ CancellationToken cancellationToken = default)
+ {
+ options ??= SymbolFinderOptions.Default;
+
+ Compilation compilation = (await project.GetCompilationAsync(cancellationToken))!;
+
+ INamedTypeSymbol generatedCodeAttribute = compilation.GetTypeByMetadataName("System.CodeDom.Compiler.GeneratedCodeAttribute");
+ ISyntaxFactsService syntaxFactsService = MefWorkspaceServices.Default.GetService(compilation.Language);
+
+ ImmutableArray.Builder symbols = null;
+
+ var namespaceOrTypeSymbols = new Stack();
+
+ namespaceOrTypeSymbols.Push(compilation.Assembly.GlobalNamespace);
+
+ while (namespaceOrTypeSymbols.Count > 0)
+ {
+ bool? canContainUnityScriptMethods = null;
+
+ INamespaceOrTypeSymbol namespaceOrTypeSymbol = namespaceOrTypeSymbols.Pop();
+
+ foreach (ISymbol symbol in namespaceOrTypeSymbol.GetMembers())
+ {
+ SymbolKind kind = symbol.Kind;
+
+ if (kind == SymbolKind.Namespace)
+ {
+ var namespaceSymbol = (INamespaceSymbol)symbol;
+
+ SymbolFilterReason reason = options.GetReason(namespaceSymbol);
+
+ if (reason == SymbolFilterReason.None)
+ namespaceOrTypeSymbols.Push(namespaceSymbol);
+ }
+ else if (!symbol.IsImplicitlyDeclared)
+ {
+ if (!options.Unused
+ || UnusedSymbolUtility.CanBeUnusedSymbol(symbol))
+ {
+ SymbolFilterReason reason = options.GetReason(symbol);
+
+ switch (reason)
+ {
+ case SymbolFilterReason.None:
+ {
+ if (options.IgnoreGeneratedCode
+ && GeneratedCodeUtility.IsGeneratedCode(symbol, generatedCodeAttribute, f => syntaxFactsService.IsComment(f), cancellationToken))
+ {
+ continue;
+ }
+
+ if (symbol.IsKind(SymbolKind.Method))
+ {
+ if (canContainUnityScriptMethods is null
+ && namespaceOrTypeSymbol is INamedTypeSymbol typeSymbol)
+ {
+ canContainUnityScriptMethods = typeSymbol.InheritsFrom(UnityScriptMethods.MonoBehaviourClassName);
+ }
+
+ if (canContainUnityScriptMethods == true
+ && UnityScriptMethods.MethodNames.Contains(symbol.Name))
+ {
+ continue;
+ }
+ }
+
+ if (options.Unused)
+ {
+ bool isUnused = await UnusedSymbolUtility.IsUnusedSymbolAsync(symbol, project.Solution, cancellationToken);
+
+ if (isUnused
+ && !UnusedSymbolUtility.CanBeUnreferenced(symbol))
+ {
+ (symbols ??= ImmutableArray.CreateBuilder()).Add(symbol);
+ continue;
+ }
+ }
+ else
+ {
+ (symbols ??= ImmutableArray.CreateBuilder()).Add(symbol);
+ }
+
+ break;
+ }
+ case SymbolFilterReason.WithoutAttribute:
+ {
+ continue;
+ }
+ case SymbolFilterReason.Visibility:
+ case SymbolFilterReason.SymbolGroup:
+ case SymbolFilterReason.Ignored:
+ case SymbolFilterReason.WithAttribute:
+ case SymbolFilterReason.Other:
+ {
+ break;
+ }
+ default:
+ {
+ Debug.Fail(reason.ToString());
+ break;
+ }
+ }
+ }
+
+ if (kind == SymbolKind.NamedType)
+ namespaceOrTypeSymbols.Push((INamedTypeSymbol)symbol);
+ }
+ }
+ }
+
+ return symbols?.ToImmutableArray() ?? ImmutableArray.Empty;
+ }
+}
diff --git a/src/CommandLine/FindSymbols/SymbolFinderOptions.cs b/src/CommandLine/FindSymbols/SymbolFinderOptions.cs
new file mode 100644
index 0000000000..c37347216e
--- /dev/null
+++ b/src/CommandLine/FindSymbols/SymbolFinderOptions.cs
@@ -0,0 +1,27 @@
+// Copyright (c) .NET Foundation and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Collections.Generic;
+
+namespace Roslynator.FindSymbols;
+
+internal class SymbolFinderOptions : SymbolFilterOptions
+{
+ internal SymbolFinderOptions(
+ FileSystemFilter fileSystemFilter = null,
+ VisibilityFilter visibility = VisibilityFilter.All,
+ SymbolGroupFilter symbolGroups = SymbolGroupFilter.TypeOrMember,
+ IEnumerable rules = null,
+ IEnumerable attributeRules = null,
+ bool ignoreGeneratedCode = false,
+ bool unused = false) : base(fileSystemFilter, visibility, symbolGroups, rules, attributeRules)
+ {
+ IgnoreGeneratedCode = ignoreGeneratedCode;
+ Unused = unused;
+ }
+
+ new public static SymbolFinderOptions Default { get; } = new();
+
+ public bool IgnoreGeneratedCode { get; }
+
+ public bool Unused { get; }
+}
diff --git a/src/Workspaces.Core/FindSymbols/WithAttributeFilterRule.cs b/src/CommandLine/FindSymbols/SymbolWithAttributeFilterRule.cs
similarity index 88%
rename from src/Workspaces.Core/FindSymbols/WithAttributeFilterRule.cs
rename to src/CommandLine/FindSymbols/SymbolWithAttributeFilterRule.cs
index 7209c879a7..dd18685fa1 100644
--- a/src/Workspaces.Core/FindSymbols/WithAttributeFilterRule.cs
+++ b/src/CommandLine/FindSymbols/SymbolWithAttributeFilterRule.cs
@@ -7,9 +7,9 @@
namespace Roslynator.FindSymbols;
[DebuggerDisplay("{DebuggerDisplay,nq}")]
-internal class WithAttributeFilterRule : SymbolFilterRule
+internal class SymbolWithAttributeFilterRule : SymbolFilterRule
{
- public WithAttributeFilterRule(IEnumerable attributeNames)
+ public SymbolWithAttributeFilterRule(IEnumerable attributeNames)
{
AttributeNames = new MetadataNameSet(attributeNames);
}
diff --git a/src/Workspaces.Core/FindSymbols/WithoutAttributeFilterRule.cs b/src/CommandLine/FindSymbols/SymbolWithoutAttributeFilterRule.cs
similarity index 73%
rename from src/Workspaces.Core/FindSymbols/WithoutAttributeFilterRule.cs
rename to src/CommandLine/FindSymbols/SymbolWithoutAttributeFilterRule.cs
index 121afedb91..b70c2c210f 100644
--- a/src/Workspaces.Core/FindSymbols/WithoutAttributeFilterRule.cs
+++ b/src/CommandLine/FindSymbols/SymbolWithoutAttributeFilterRule.cs
@@ -7,9 +7,9 @@
namespace Roslynator.FindSymbols;
[DebuggerDisplay("{DebuggerDisplay,nq}")]
-internal class WithoutAttributeFilterRule : WithAttributeFilterRule
+internal class SymbolWithoutAttributeFilterRule : SymbolWithAttributeFilterRule
{
- public WithoutAttributeFilterRule(IEnumerable attributeNames) : base(attributeNames)
+ public SymbolWithoutAttributeFilterRule(IEnumerable attributeNames) : base(attributeNames)
{
}
diff --git a/src/Workspaces.Core/UnusedSymbolUtility.cs b/src/CommandLine/FindSymbols/UnusedSymbolUtility.cs
similarity index 80%
rename from src/Workspaces.Core/UnusedSymbolUtility.cs
rename to src/CommandLine/FindSymbols/UnusedSymbolUtility.cs
index ee5019a8c4..abd3892aa1 100644
--- a/src/Workspaces.Core/UnusedSymbolUtility.cs
+++ b/src/CommandLine/FindSymbols/UnusedSymbolUtility.cs
@@ -13,6 +13,20 @@ namespace Roslynator;
internal static class UnusedSymbolUtility
{
+ private static readonly MetadataNameSet _classAttributeSymbols = new(new[]
+ {
+ MetadataName.Parse("Microsoft.CodeAnalysis.CodeFixes.ExportCodeFixProviderAttribute"),
+ MetadataName.Parse("Microsoft.CodeAnalysis.CodeRefactorings.ExportCodeRefactoringProviderAttribute"),
+ MetadataName.Parse("Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzerAttribute"),
+ MetadataName.Parse("System.Composition.ExportAttribute"),
+ });
+
+ private static readonly MetadataNameSet _methodAttributeSymbols = new(new[]
+ {
+ MetadataName.Parse("Xunit.FactAttribute"),
+ MetadataName.Parse("Xunit.TheoryAttribute"),
+ });
+
public static bool CanBeUnusedSymbol(ISymbol symbol)
{
switch (symbol.Kind)
@@ -23,30 +37,6 @@ public static bool CanBeUnusedSymbol(ISymbol symbol)
}
case SymbolKind.NamedType:
{
- var namedType = (INamedTypeSymbol)symbol;
-
- if ((namedType.TypeKind == TypeKind.Class || namedType.TypeKind == TypeKind.Module)
- && namedType.IsStatic)
- {
- foreach (ISymbol member in namedType.GetMembers())
- {
- switch (member.Kind)
- {
- case SymbolKind.NamedType:
- {
- return false;
- }
- case SymbolKind.Method:
- {
- if (((IMethodSymbol)member).IsExtensionMethod)
- return false;
-
- break;
- }
- }
- }
- }
-
return true;
}
case SymbolKind.Event:
@@ -122,17 +112,19 @@ public static async Task IsUnusedSymbolAsync(
if (IsReferencedInDebuggerDisplayAttribute(symbol))
return false;
- IEnumerable referencedSymbols = await SymbolFinder.FindReferencesAsync(symbol, solution, cancellationToken).ConfigureAwait(false);
+ IEnumerable overrides = await SymbolFinder.FindOverridesAsync(symbol, solution, null, cancellationToken);
+
+ if (overrides.Any())
+ return false;
+
+ IEnumerable referencedSymbols = await SymbolFinder.FindReferencesAsync(symbol, solution, cancellationToken);
foreach (ReferencedSymbol referencedSymbol in referencedSymbols)
{
foreach (ReferenceLocation referenceLocation in referencedSymbol.Locations)
{
- if (referenceLocation.IsImplicit)
- continue;
-
if (referenceLocation.IsCandidateLocation)
- return false;
+ continue;
Location location = referenceLocation.Location;
@@ -196,7 +188,7 @@ private static bool IsReferencedInDebuggerDisplayAttribute(ISymbol symbol)
if (symbol.DeclaredAccessibility == Accessibility.Private
&& CanBeReferencedInDebuggerDisplayAttribute())
{
- string? value = symbol.ContainingType
+ string value = symbol.ContainingType
.GetAttribute(MetadataNames.System_Diagnostics_DebuggerDisplayAttribute)?
.ConstructorArguments
.SingleOrDefault(shouldThrow: false)
@@ -294,4 +286,46 @@ bool IsReferencedInDebuggerDisplayAttribute(string value)
return false;
}
}
+
+ public static bool CanBeUnreferenced(ISymbol symbol)
+ {
+ if (symbol is INamedTypeSymbol typeSymbol)
+ {
+ if (typeSymbol.TypeKind == TypeKind.Class
+ && HasAttribute(typeSymbol, _classAttributeSymbols))
+ {
+ return true;
+ }
+
+ foreach (ISymbol member in typeSymbol.GetMembers())
+ {
+ if (member is IMethodSymbol methodSymbol)
+ {
+ if (methodSymbol.IsExtensionMethod)
+ return true;
+
+ if (HasAttribute(methodSymbol, _methodAttributeSymbols))
+ return true;
+ }
+ }
+ }
+ else if (symbol is IMethodSymbol methodSymbol)
+ {
+ if (HasAttribute(methodSymbol, _methodAttributeSymbols))
+ return true;
+ }
+
+ return false;
+
+ static bool HasAttribute(ISymbol symbol, MetadataNameSet attributeNames)
+ {
+ foreach (AttributeData attributeData in symbol.GetAttributes())
+ {
+ if (attributeNames.Contains(attributeData.AttributeClass))
+ return true;
+ }
+
+ return false;
+ }
+ }
}
diff --git a/src/CommandLine/OptionNames.cs b/src/CommandLine/OptionNames.cs
index 65da4597f9..29fec32a3f 100644
--- a/src/CommandLine/OptionNames.cs
+++ b/src/CommandLine/OptionNames.cs
@@ -39,6 +39,7 @@ internal static class OptionNames
public const string RootDirectoryUrl = "root-directory-url";
public const string Scope = "scope";
public const string SeverityLevel = "severity-level";
+ public const string SymbolKind = "symbol-kind";
public const string TargetVersion = "target-version";
public const string Type = "type";
public const string Visibility = "visibility";
diff --git a/src/CommandLine/Options/FindSymbolCommandLineOptions.cs b/src/CommandLine/Options/FindSymbolCommandLineOptions.cs
new file mode 100644
index 0000000000..e08d0b70a6
--- /dev/null
+++ b/src/CommandLine/Options/FindSymbolCommandLineOptions.cs
@@ -0,0 +1,53 @@
+// Copyright (c) .NET Foundation and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Collections.Generic;
+using CommandLine;
+
+namespace Roslynator.CommandLine;
+
+[Verb("find-symbol", HelpText = "Finds symbols in the specified project or solution.")]
+public class FindSymbolCommandLineOptions : MSBuildCommandLineOptions
+{
+ [Value(
+ index: 0,
+ HelpText = "Path to one or more project/solution files.",
+ MetaName = "")]
+ public IEnumerable Paths { get; set; }
+
+ [Option(longName: "ignore-generated-code")]
+ public bool IgnoreGeneratedCode { get; set; }
+
+ [Option(
+ longName: "remove",
+ HelpText = "Remove found symbols' declarations.")]
+ public bool Remove { get; set; }
+
+ [Option(
+ longName: OptionNames.SymbolKind,
+ HelpText = "Space separated list of symbol kinds to be included. "
+ + "Allowed values are class, delegate, enum, interface, struct, event, field, enum-field, const, method, property, indexer, member and type.")]
+ public IEnumerable SymbolKind { get; set; }
+
+ [Option(
+ longName: "unused",
+ HelpText = "Search only for symbols that have zero references.")]
+ public bool Unused { get; set; }
+
+ [Option(
+ longName: OptionNames.Visibility,
+ HelpText = "Space separated list of visibilities of a type or a member. Allowed values are public, internal and private.",
+ MetaValue = "")]
+ public IEnumerable Visibility { get; set; }
+
+ [Option(
+ longName: "with-attribute",
+ HelpText = "Space separated list of attributes that should be included.",
+ MetaValue = "")]
+ public IEnumerable WithAttribute { get; set; }
+
+ [Option(
+ longName: "without-attribute",
+ HelpText = "Space separated list of attributes that should be excluded.",
+ MetaValue = "")]
+ public IEnumerable WithoutAttribute { get; set; }
+}
diff --git a/src/CommandLine/Options/ListSymbolsCommandLineOptions.cs b/src/CommandLine/Options/ListSymbolsCommandLineOptions.cs
index 468051bd6e..d9ae2704d6 100644
--- a/src/CommandLine/Options/ListSymbolsCommandLineOptions.cs
+++ b/src/CommandLine/Options/ListSymbolsCommandLineOptions.cs
@@ -93,8 +93,8 @@ public class ListSymbolsCommandLineOptions : MSBuildCommandLineOptions
[Option(
longName: OptionNames.Visibility,
- Default = new string[] { nameof(Roslynator.Visibility.Public) },
- HelpText = "Defines one or more visibility of a type or a member. Allowed values are public, internal or private.",
+ Default = new[] { "public" },
+ HelpText = "Space separated list of visibilities of a type or a member. Allowed values are public, internal and private.",
MetaValue = "")]
public IEnumerable Visibility { get; set; }
}
diff --git a/src/CommandLine/Program.cs b/src/CommandLine/Program.cs
index dc8eca9582..bc5143af2d 100644
--- a/src/CommandLine/Program.cs
+++ b/src/CommandLine/Program.cs
@@ -106,6 +106,7 @@ private static int Main(string[] args)
typeof(PhysicalLinesOfCodeCommandLineOptions),
typeof(RenameSymbolCommandLineOptions),
typeof(SpellcheckCommandLineOptions),
+ typeof(FindSymbolCommandLineOptions),
});
parserResult.WithNotParsed(e =>
@@ -190,6 +191,8 @@ private static int Main(string[] args)
return RenameSymbolAsync(renameSymbolCommandLineOptions).Result;
case SpellcheckCommandLineOptions spellcheckCommandLineOptions:
return SpellcheckAsync(spellcheckCommandLineOptions).Result;
+ case FindSymbolCommandLineOptions findSymbolCommandLineOptions:
+ return FindSymbolAsync(findSymbolCommandLineOptions).Result;
default:
throw new InvalidOperationException();
}
@@ -342,6 +345,55 @@ private static async Task AnalyzeAsync(AnalyzeCommandLineOptions options)
return GetExitCode(status);
}
+ private static async Task FindSymbolAsync(FindSymbolCommandLineOptions options)
+ {
+ if (!options.TryGetProjectFilter(out ProjectFilter projectFilter))
+ return ExitCodes.Error;
+
+ if (!TryParseOptionValueAsEnumFlags(options.SymbolKind, OptionNames.SymbolKind, out SymbolGroupFilter symbolGroups, SymbolFinderOptions.Default.SymbolGroups))
+ return ExitCodes.Error;
+
+ if (!TryParseOptionValueAsEnumFlags(options.Visibility, OptionNames.Visibility, out VisibilityFilter visibility, SymbolFinderOptions.Default.Visibility))
+ return ExitCodes.Error;
+
+ if (!TryParseMetadataNames(options.WithAttribute, out ImmutableArray withAttributes))
+ return ExitCodes.Error;
+
+ if (!TryParseMetadataNames(options.WithoutAttribute, out ImmutableArray withoutAttributes))
+ return ExitCodes.Error;
+
+ if (!TryParsePaths(options.Paths, out ImmutableArray paths))
+ return ExitCodes.Error;
+
+ ImmutableArray.Builder rules = ImmutableArray.CreateBuilder();
+
+ if (withAttributes.Any())
+ rules.Add(new SymbolWithAttributeFilterRule(withAttributes));
+
+ if (withoutAttributes.Any())
+ rules.Add(new SymbolWithoutAttributeFilterRule(withoutAttributes));
+
+ FileSystemFilter fileSystemFilter = CreateFileSystemFilter(options);
+
+ var symbolFinderOptions = new SymbolFinderOptions(
+ fileSystemFilter,
+ visibility: visibility,
+ symbolGroups: symbolGroups,
+ rules: rules,
+ ignoreGeneratedCode: options.IgnoreGeneratedCode,
+ unused: options.Unused);
+
+ var command = new FindSymbolCommand(
+ options: options,
+ symbolFinderOptions: symbolFinderOptions,
+ projectFilter: projectFilter,
+ fileSystemFilter: fileSystemFilter);
+
+ CommandStatus status = await command.ExecuteAsync(paths, options.MSBuildPath, options.Properties);
+
+ return GetExitCode(status);
+ }
+
private static async Task RenameSymbolAsync(RenameSymbolCommandLineOptions options)
{
if (!options.TryGetProjectFilter(out ProjectFilter projectFilter))
diff --git a/src/Core/GeneratedCodeUtility.cs b/src/Core/GeneratedCodeUtility.cs
index 197b2d7043..a3679bb8bf 100644
--- a/src/Core/GeneratedCodeUtility.cs
+++ b/src/Core/GeneratedCodeUtility.cs
@@ -19,14 +19,18 @@ public static bool IsGeneratedCode(SyntaxTree tree, Func isC
public static bool IsGeneratedCode(
ISymbol symbol,
- INamedTypeSymbol generatedCodeAttribute,
- Func isComment,
+ INamedTypeSymbol? generatedCodeAttribute,
+ Func? isComment,
CancellationToken cancellationToken = default)
{
- if (IsMarkedWithGeneratedCodeAttribute(symbol, generatedCodeAttribute))
+ if (generatedCodeAttribute is not null
+ && IsMarkedWithGeneratedCodeAttribute(symbol, generatedCodeAttribute))
+ {
return true;
+ }
- if (symbol.Kind != SymbolKind.Namespace)
+ if (isComment is not null
+ && symbol.Kind != SymbolKind.Namespace)
{
foreach (SyntaxReference syntaxReference in symbol.DeclaringSyntaxReferences)
{
diff --git a/src/Analyzers/CSharp/Analysis/UnusedMember/UnityScriptMethods.cs b/src/Core/UnityScriptMethods.cs
similarity index 96%
rename from src/Analyzers/CSharp/Analysis/UnusedMember/UnityScriptMethods.cs
rename to src/Core/UnityScriptMethods.cs
index af96958c8e..a15c6f72be 100644
--- a/src/Analyzers/CSharp/Analysis/UnusedMember/UnityScriptMethods.cs
+++ b/src/Core/UnityScriptMethods.cs
@@ -3,11 +3,11 @@
using System.Collections.Immutable;
using System.Threading;
-namespace Roslynator.CSharp.Analysis.UnusedMember;
+namespace Roslynator;
internal static class UnityScriptMethods
{
- private static ImmutableHashSet _methodNames;
+ private static ImmutableHashSet? _methodNames;
public static MetadataName MonoBehaviourClassName { get; } = MetadataName.Parse("UnityEngine.MonoBehaviour");
diff --git a/src/Workspaces.Core/Extensions/Extensions.cs b/src/Workspaces.Core/Extensions/Extensions.cs
index eb6a04d32b..e64c7fb71a 100644
--- a/src/Workspaces.Core/Extensions/Extensions.cs
+++ b/src/Workspaces.Core/Extensions/Extensions.cs
@@ -20,6 +20,11 @@ internal static class Extensions
{
public static bool IsMatch(this Matcher matcher, ISymbol symbol, string? rootDirectoryPath)
{
+ Debug.Assert(!symbol.IsKind(SymbolKind.Namespace), symbol.Kind.ToString());
+
+ if (symbol.IsKind(SymbolKind.Namespace))
+ return true;
+
foreach (Location location in symbol.Locations)
{
SyntaxTree? tree = location.SourceTree;
diff --git a/src/Workspaces.Core/FindSymbols/IFindSymbolsProgress.cs b/src/Workspaces.Core/FindSymbols/IFindSymbolsProgress.cs
deleted file mode 100644
index f96853edd9..0000000000
--- a/src/Workspaces.Core/FindSymbols/IFindSymbolsProgress.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-// Copyright (c) .NET Foundation and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using Microsoft.CodeAnalysis;
-
-namespace Roslynator.FindSymbols;
-
-internal interface IFindSymbolsProgress
-{
- void OnSymbolFound(ISymbol symbol);
-}
diff --git a/src/Workspaces.Core/FindSymbols/SymbolFilterOptions.cs b/src/Workspaces.Core/FindSymbols/SymbolFilterOptions.cs
index 55793d2d31..bcde940153 100644
--- a/src/Workspaces.Core/FindSymbols/SymbolFilterOptions.cs
+++ b/src/Workspaces.Core/FindSymbols/SymbolFilterOptions.cs
@@ -106,7 +106,16 @@ public SymbolFilterReason GetReason(ISymbol symbol)
public virtual SymbolFilterReason GetReason(INamespaceSymbol namespaceSymbol)
{
- return GetRulesReason(namespaceSymbol);
+ foreach (SymbolFilterRule rule in Rules)
+ {
+ if (rule.IsApplicable(namespaceSymbol)
+ && !rule.IsMatch(namespaceSymbol))
+ {
+ return rule.Reason;
+ }
+ }
+
+ return SymbolFilterReason.None;
}
public virtual SymbolFilterReason GetReason(INamedTypeSymbol typeSymbol)
diff --git a/tools/remove_unused_symbols.ps1 b/tools/remove_unused_symbols.ps1
new file mode 100644
index 0000000000..cd63451bf9
--- /dev/null
+++ b/tools/remove_unused_symbols.ps1
@@ -0,0 +1,4 @@
+#dotnet tool install -g roslynator.dotnet.cli
+
+roslynator find-symbol "$PSScriptRoot/../src/Roslynator.sln" `
+ --symbol-kind type --unused --remove --exclude CommandLine/Orang/** --without-attribute System.ObsoleteAttribute
\ No newline at end of file