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