Skip to content

Commit

Permalink
Add command find-symbol (#1255)
Browse files Browse the repository at this point in the history
  • Loading branch information
josefpihrt authored Nov 23, 2023
1 parent c5d3da3 commit 60a73e5
Show file tree
Hide file tree
Showing 20 changed files with 679 additions and 54 deletions.
6 changes: 6 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)).
Expand Down
3 changes: 3 additions & 0 deletions src/CSharp.Workspaces/CSharp.Workspaces.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ Roslynator.NameGenerator</Description>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>Roslynator.Formatting.Analyzers.Tests, PublicKey=$(RoslynatorPublicKey)</_Parameter1>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>Roslynator, PublicKey=$(RoslynatorPublicKey)</_Parameter1>
</AssemblyAttribute>
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@ internal static Task<Document> 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);
Expand Down
51 changes: 50 additions & 1 deletion src/CSharp/CSharp/SyntaxRefactorings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ public static SyntaxRemoveOptions GetRemoveOptions(CSharpSyntaxNode node)
return removeOptions;
}

internal static MemberDeclarationSyntax RemoveSingleLineDocumentationComment(MemberDeclarationSyntax declaration)
internal static TMemberDeclaration RemoveSingleLineDocumentationComment<TMemberDeclaration>(TMemberDeclaration declaration) where TMemberDeclaration : MemberDeclarationSyntax
{
if (declaration is null)
throw new ArgumentNullException(nameof(declaration));
Expand Down Expand Up @@ -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>(
T declaration,
Func<T, SyntaxList<MemberDeclarationSyntax>> getMembers,
Expand Down Expand Up @@ -524,6 +541,38 @@ private static T RemoveNode<T>(
return newDeclaration;
}

private static T RemoveNode<T>(
T declaration,
Func<T, SeparatedSyntaxList<EnumMemberDeclarationSyntax>> getMembers,
int index,
SyntaxRemoveOptions removeOptions) where T : SyntaxNode
{
SeparatedSyntaxList<EnumMemberDeclarationSyntax> 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;
Expand Down
252 changes: 252 additions & 0 deletions src/CommandLine/Commands/FindSymbolCommand.cs
Original file line number Diff line number Diff line change
@@ -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<CommandResult>
{
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<CommandResult> ExecuteAsync(ProjectOrSolution projectOrSolution, CancellationToken cancellationToken = default)
{
ImmutableArray<ISymbol> 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<ISymbol>.Builder symbols = ImmutableArray.CreateBuilder<ISymbol>();

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<ISymbol> 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<ISymbol>.Empty;

LogHelpers.WriteElapsedTime($"Analyzed solution '{solution.FilePath}'", stopwatch.Elapsed, Verbosity.Minimal);
}

if (allSymbols.Any())
{
Dictionary<SymbolGroup, int> 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<SymbolGroup, int> 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<ImmutableArray<ISymbol>> AnalyzeProject(
Project project,
SymbolFinderOptions options,
CancellationToken cancellationToken)
{
if (!project.SupportsCompilation)
{
WriteLine(" Project does not support compilation", Verbosity.Normal);
return Task.FromResult(ImmutableArray<ISymbol>.Empty);
}

if (!MefWorkspaceServices.Default.SupportedLanguages.Contains(project.Language))
{
WriteLine($" Language '{project.Language}' is not supported", Verbosity.Normal);
return Task.FromResult(ImmutableArray<ISymbol>.Empty);
}

return SymbolFinder.FindSymbolsAsync(project, options, cancellationToken);
}

private static async Task<Project> RemoveSymbolsAsync(
ImmutableArray<ISymbol> symbols,
Project project,
CancellationToken cancellationToken)
{
foreach (IGrouping<DocumentId, SyntaxReference> 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);
}
}
Loading

0 comments on commit 60a73e5

Please sign in to comment.