Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add command find-symbol #1255

Merged
merged 36 commits into from
Nov 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
e52431d
Add command find-symbols
josefpihrt Nov 20, 2023
240708f
merge main
josefpihrt Nov 20, 2023
caa956e
Revert "merge main"
josefpihrt Nov 20, 2023
43234e1
update
josefpihrt Nov 20, 2023
0d4669e
Merge branch 'main' into feature/find-symbols
josefpihrt Nov 20, 2023
c3e8d99
update
josefpihrt Nov 20, 2023
b3e0e40
Merge branch 'main' into feature/find-symbols
josefpihrt Nov 20, 2023
3a5be64
Merge branch 'main' into feature/find-symbols
josefpihrt Nov 20, 2023
83a494a
Merge branch 'main' into feature/find-symbols
josefpihrt Nov 20, 2023
aedb8cc
update
josefpihrt Nov 20, 2023
31ec54c
update
josefpihrt Nov 20, 2023
3f64d8a
update
josefpihrt Nov 21, 2023
1d04026
update
josefpihrt Nov 21, 2023
3527f96
update
josefpihrt Nov 21, 2023
a307bb5
update
josefpihrt Nov 21, 2023
404c486
Merge branch 'main' into feature/find-symbols
josefpihrt Nov 21, 2023
2247004
update
josefpihrt Nov 21, 2023
50add28
Merge branch 'main' into feature/find-symbols
josefpihrt Nov 21, 2023
29e89bb
update
josefpihrt Nov 22, 2023
abf1a30
update
josefpihrt Nov 22, 2023
f46772c
update
josefpihrt Nov 22, 2023
63d91a0
update
josefpihrt Nov 22, 2023
74cec45
update
josefpihrt Nov 22, 2023
0ad0177
update
josefpihrt Nov 22, 2023
56340e2
Merge branch 'main' into feature/find-symbols
josefpihrt Nov 22, 2023
7af07ad
Merge branch 'main' into feature/find-symbols
josefpihrt Nov 22, 2023
ba523a5
Merge branch 'main' into feature/find-symbols
josefpihrt Nov 22, 2023
ba7700c
update
josefpihrt Nov 22, 2023
83c6375
update
josefpihrt Nov 22, 2023
80fd7e2
update
josefpihrt Nov 22, 2023
9006b36
update
josefpihrt Nov 22, 2023
8ed8883
update
josefpihrt Nov 22, 2023
10cd85f
update
josefpihrt Nov 23, 2023
5ca0fd1
update
josefpihrt Nov 23, 2023
2070a66
update
josefpihrt Nov 23, 2023
972e2b1
update
josefpihrt Nov 23, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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