From 3a474f774847f320a253b3387974e6e2f81d2cb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Thu, 2 Nov 2023 00:58:14 +0100 Subject: [PATCH] feat: count only specified using directives (#70) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit normalizes arguments with globalize command Co-Authored-By: Eva Ditzelmüller <87754804+EvaDitzelmueller@users.noreply.github.com> --- .../CliApplication.UsingDirectives.cs | 12 ++- .../UsingDirectives/UsingCountResult.cs | 20 ++++ .../UsingDirectives/UsingCounter.cs | 36 +++++-- .../Testing/Usings.cs | 8 ++ .../UsingDirectives/UsingCounterTests.cs | 31 ++++++ .../UsingDirectives/UsingCounterTests.cs | 101 ++++++++++++++++++ 6 files changed, 193 insertions(+), 15 deletions(-) diff --git a/src/libraries/FlashOWare.Tool.Cli/CliApplication.UsingDirectives.cs b/src/libraries/FlashOWare.Tool.Cli/CliApplication.UsingDirectives.cs index 0661b6a..bebcb46 100644 --- a/src/libraries/FlashOWare.Tool.Cli/CliApplication.UsingDirectives.cs +++ b/src/libraries/FlashOWare.Tool.Cli/CliApplication.UsingDirectives.cs @@ -2,6 +2,7 @@ using FlashOWare.Tool.Cli.IO; using FlashOWare.Tool.Core.UsingDirectives; using Microsoft.CodeAnalysis; +using System.Collections.Immutable; using System.CommandLine.IO; using System.Diagnostics; @@ -12,15 +13,18 @@ public static partial class CliApplication private static void AddUsingCommand(RootCommand rootCommand, MSBuildWorkspace workspace, IFileSystemAccessor fileSystem) { var usingCommand = new Command("using", " Analyze or refactor C# using directives."); - var countCommand = new Command("count", "Count and list all top-level using directives of a C# project."); + var countCommand = new Command("count", "Count and list the top-level using directives of a C# project."); var globalizeCommand = new Command("globalize", "Move a top-level using directive to a global using directive in a C# project."); var projectOption = new Option(new[] { "--project", "--proj" }, "The path to the project file to operate on (defaults to the current directory if there is only one project).") .ExistingOnly(); + var countArgument = new Argument("USINGS", "The names of the top-level using directives to count. If usings are not specified, the command will list all top-level directives."); + countCommand.Add(countArgument); countCommand.Add(projectOption); countCommand.SetHandler(async (InvocationContext context) => { + string[] usings = context.ParseResult.GetValueForArgument(countArgument); FileInfo? project = context.ParseResult.GetValueForOption(projectOption); if (project is null) { @@ -35,7 +39,7 @@ private static void AddUsingCommand(RootCommand rootCommand, MSBuildWorkspace wo }; } - await CountUsingsAsync(workspace, project.FullName, context.Console, context.GetCancellationToken()); + await CountUsingsAsync(workspace, project.FullName, usings.ToImmutableArray(), context.Console, context.GetCancellationToken()); }); var usingArgument = new Argument("USING", "The name of the top-level using directive to convert to a global using directive."); @@ -66,14 +70,14 @@ private static void AddUsingCommand(RootCommand rootCommand, MSBuildWorkspace wo rootCommand.Add(usingCommand); } - private static async Task CountUsingsAsync(MSBuildWorkspace workspace, string projectFilePath, IConsole console, CancellationToken cancellationToken) + private static async Task CountUsingsAsync(MSBuildWorkspace workspace, string projectFilePath, ImmutableArray usings, IConsole console, CancellationToken cancellationToken) { try { await s_msBuildMutex.WaitAsync(cancellationToken); Project project = await workspace.OpenProjectAsync(projectFilePath, null, cancellationToken); - var result = await UsingCounter.CountAsync(project, cancellationToken); + var result = await UsingCounter.CountAsync(project, usings, cancellationToken); console.WriteLine($"{nameof(Project)}: {result.ProjectName}"); foreach (var usingDirective in result.Usings) { diff --git a/src/libraries/FlashOWare.Tool.Core/UsingDirectives/UsingCountResult.cs b/src/libraries/FlashOWare.Tool.Core/UsingDirectives/UsingCountResult.cs index a232a0e..3b7628b 100644 --- a/src/libraries/FlashOWare.Tool.Core/UsingDirectives/UsingCountResult.cs +++ b/src/libraries/FlashOWare.Tool.Core/UsingDirectives/UsingCountResult.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; namespace FlashOWare.Tool.Core.UsingDirectives; @@ -19,6 +20,25 @@ internal UsingCountResult(string projectName) public required string ProjectName { get; init; } public IReadOnlyCollection Usings => _usings.Values; + internal void Add(string identifier) + { + _ = _usings.TryAdd(identifier, new UsingDirective(identifier)); + } + + internal void AddRange(ImmutableArray identifiers) + { + foreach (string identifier in identifiers) + { + Add(identifier); + } + } + + internal void Increment(string identifier) + { + UsingDirective usingDirective = _usings[identifier]; + usingDirective.Occurrences++; + } + internal void IncrementOrAdd(string identifier) { if (_usings.TryGetValue(identifier, out UsingDirective? usingDirective)) diff --git a/src/libraries/FlashOWare.Tool.Core/UsingDirectives/UsingCounter.cs b/src/libraries/FlashOWare.Tool.Core/UsingDirectives/UsingCounter.cs index c59d0bd..a761a75 100644 --- a/src/libraries/FlashOWare.Tool.Core/UsingDirectives/UsingCounter.cs +++ b/src/libraries/FlashOWare.Tool.Core/UsingDirectives/UsingCounter.cs @@ -2,12 +2,23 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Collections.Immutable; namespace FlashOWare.Tool.Core.UsingDirectives; public static class UsingCounter { - public static async Task CountAsync(Project project, CancellationToken cancellationToken = default) + public static Task CountAsync(Project project, CancellationToken cancellationToken = default) + { + return CountAsync(project, ImmutableArray.Empty, cancellationToken); + } + + public static Task CountAsync(Project project, string localUsing, CancellationToken cancellationToken = default) + { + return CountAsync(project, ImmutableArray.Create(localUsing), cancellationToken); + } + + public static async Task CountAsync(Project project, ImmutableArray usings, CancellationToken cancellationToken = default) { RoslynUtilities.ThrowIfNotCSharp(project); @@ -18,6 +29,7 @@ public static async Task CountAsync(Project project, Cancellat } var result = new UsingCountResult(project.Name); + result.AddRange(usings); if (RoslynUtilities.IsGeneratedCode(compilation)) { @@ -48,31 +60,33 @@ public static async Task CountAsync(Project project, Cancellat cancellationToken.ThrowIfCancellationRequested(); - AggregateUsings(result, compilationUnit); + AggregateUsings(result, compilationUnit, usings); } return result; } - private static void AggregateUsings(UsingCountResult result, CompilationUnitSyntax compilationUnit) + private static void AggregateUsings(UsingCountResult result, CompilationUnitSyntax compilationUnit, ImmutableArray usings) { foreach (UsingDirectiveSyntax usingNode in compilationUnit.Usings) { - if (usingNode.Alias is not null) + if (usingNode.Alias is not null || + !usingNode.StaticKeyword.IsKind(SyntaxKind.None) || + !usingNode.GlobalKeyword.IsKind(SyntaxKind.None)) { continue; } - if (!usingNode.StaticKeyword.IsKind(SyntaxKind.None)) + + string identifier = usingNode.Name.ToString(); + + if (usings.Length == 0) { - continue; + result.IncrementOrAdd(identifier); } - if (!usingNode.GlobalKeyword.IsKind(SyntaxKind.None)) + else if (usings.Contains(identifier)) { - continue; + result.Increment(identifier); } - - string identifier = usingNode.Name.ToString(); - result.IncrementOrAdd(identifier); } } } diff --git a/src/tests/FlashOWare.Tool.Cli.Tests/Testing/Usings.cs b/src/tests/FlashOWare.Tool.Cli.Tests/Testing/Usings.cs index d5b608f..97eca3d 100644 --- a/src/tests/FlashOWare.Tool.Cli.Tests/Testing/Usings.cs +++ b/src/tests/FlashOWare.Tool.Cli.Tests/Testing/Usings.cs @@ -3,4 +3,12 @@ namespace FlashOWare.Tool.Cli.Tests.Testing; internal static class Usings { public const string System = "System"; + public const string System_Collections_Generic = "System.Collections.Generic"; + public const string System_IO = "System.IO"; + public const string System_Linq = "System.Linq"; + public const string System_Net_Http = "System.Net.Http"; + public const string System_Threading = "System.Threading"; + public const string System_Threading_Tasks = "System.Threading.Tasks"; + + public const string System_Text = "System.Text"; } diff --git a/src/tests/FlashOWare.Tool.Cli.Tests/UsingDirectives/UsingCounterTests.cs b/src/tests/FlashOWare.Tool.Cli.Tests/UsingDirectives/UsingCounterTests.cs index 3bd0111..752d80f 100644 --- a/src/tests/FlashOWare.Tool.Cli.Tests/UsingDirectives/UsingCounterTests.cs +++ b/src/tests/FlashOWare.Tool.Cli.Tests/UsingDirectives/UsingCounterTests.cs @@ -69,6 +69,37 @@ internal class MyClass2 Result.Verify(ExitCodes.Success); } + [Fact] + public async Task Count_SpecifyUsings_FindSpecifiedOccurrences() + { + //Arrange + var project = Workspace.CreateProject() + .AddDocument(""" + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + + namespace ProjectUnderTest.NetCore; + + internal class MyClass1 + { + } + """, "MyClass1") + .Initialize(ProjectKind.SdkStyle, TargetFramework.Net60, LanguageVersion.CSharp10); + string[] args = new[] { "using", "count", Usings.System, Usings.System_Linq, "--project", project.File.FullName }; + //Act + await RunAsync(args); + //Assert + Console.Verify($""" + Project: {Names.Project} + System: 1 + System.Linq: 1 + """); + Result.Verify(ExitCodes.Success); + } + [Fact] public async Task Count_ExplicitProjectFileDoesNotExist_FailsValidation() { diff --git a/src/tests/FlashOWare.Tool.Core.Tests/UsingDirectives/UsingCounterTests.cs b/src/tests/FlashOWare.Tool.Core.Tests/UsingDirectives/UsingCounterTests.cs index fef57de..9f52b03 100644 --- a/src/tests/FlashOWare.Tool.Core.Tests/UsingDirectives/UsingCounterTests.cs +++ b/src/tests/FlashOWare.Tool.Core.Tests/UsingDirectives/UsingCounterTests.cs @@ -1,6 +1,7 @@ using FlashOWare.Tool.Core.Tests.Assertions; using FlashOWare.Tool.Core.Tests.Testing; using FlashOWare.Tool.Core.UsingDirectives; +using System.Collections.Immutable; namespace FlashOWare.Tool.Core.Tests.UsingDirectives; @@ -305,6 +306,106 @@ public async Task CountAsync_WithGlobalUsings_DoNotInclude() ToolAssert.Equal(expectedResult, actualResult); } + [Fact] + public async Task CountAsync_SpecificUsing_FindsSpecifiedOccurrences() + { + //Arrange + var project = await CreateProjectCheckedAsync(""" + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net.Http; + using System.Threading; + using System.Threading.Tasks; + """); + var expectedResult = new UsingDirective[] + { + new("System", 1 ), + }; + //Act + var actualResult = await UsingCounter.CountAsync(project, "System"); + //Assert + ToolAssert.Equal(expectedResult, actualResult); + } + + [Fact] + public async Task CountAsync_SpecificUsingNotFound_ContainsWithoutOccurrences() + { + //Arrange + var project = await CreateProjectCheckedAsync(""" + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net.Http; + using System.Threading; + using System.Threading.Tasks; + """); + var expectedResult = new UsingDirective[] + { + new("Not.Found", 0), + }; + //Act + var actualResult = await UsingCounter.CountAsync(project, "Not.Found"); + //Assert + ToolAssert.Equal(expectedResult, actualResult); + } + + [Fact] + public async Task CountAsync_SpecificUsings_FindsSpecifiedOccurrences() + { + //Arrange + var project = await CreateProjectCheckedAsync(""" + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net.Http; + using System.Threading; + using System.Threading.Tasks; + """); + var expectedResult = new UsingDirective[] + { + new("System", 1 ), + new("System.IO", 1 ), + new("System.Net.Http", 1 ), + new("System.Threading.Tasks", 1 ), + }; + //Act + var actualResult = await UsingCounter.CountAsync(project, ImmutableArray.Create("System", "System.IO", "System.Net.Http", "System.Threading.Tasks")); + //Assert + ToolAssert.Equal(expectedResult, actualResult); + } + + [Fact] + public async Task CountAsync_SpecificUsingsNotFound_ContainsWithoutOccurrences() + { + //Arrange + var project = await CreateProjectCheckedAsync(""" + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net.Http; + using System.Threading; + using System.Threading.Tasks; + """); + var expectedResult = new UsingDirective[] + { + new("System.Collections.Generic", 1), + new("Not.Found", 0), + new("System.Linq", 1), + new("Duplicate", 0), + new("System.Text", 0), + new("System.Threading", 1), + }; + //Act + var actualResult = await UsingCounter.CountAsync(project, ImmutableArray.Create("System.Collections.Generic", "Not.Found", "System.Linq", "Duplicate", "System.Text", "Duplicate", "System.Threading")); + //Assert + ToolAssert.Equal(expectedResult, actualResult); + } + [Fact] public async Task CountAsync_GeneratedCodeAttribute_Ignore() {