Skip to content

Commit

Permalink
feat: count only specified using directives (#70)
Browse files Browse the repository at this point in the history
normalizes arguments with globalize command

Co-Authored-By: Eva Ditzelmüller <87754804+EvaDitzelmueller@users.noreply.github.com>
  • Loading branch information
Flash0ver and EvaDitzelmueller authored Nov 1, 2023
1 parent e5a9937 commit 3a474f7
Show file tree
Hide file tree
Showing 6 changed files with 193 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<FileInfo>(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<string[]>("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)
{
Expand All @@ -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<string>("USING", "The name of the top-level using directive to convert to a global using directive.");
Expand Down Expand Up @@ -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<string> 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)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;

namespace FlashOWare.Tool.Core.UsingDirectives;
Expand All @@ -19,6 +20,25 @@ internal UsingCountResult(string projectName)
public required string ProjectName { get; init; }
public IReadOnlyCollection<UsingDirective> Usings => _usings.Values;

internal void Add(string identifier)
{
_ = _usings.TryAdd(identifier, new UsingDirective(identifier));
}

internal void AddRange(ImmutableArray<string> 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))
Expand Down
36 changes: 25 additions & 11 deletions src/libraries/FlashOWare.Tool.Core/UsingDirectives/UsingCounter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<UsingCountResult> CountAsync(Project project, CancellationToken cancellationToken = default)
public static Task<UsingCountResult> CountAsync(Project project, CancellationToken cancellationToken = default)
{
return CountAsync(project, ImmutableArray<string>.Empty, cancellationToken);
}

public static Task<UsingCountResult> CountAsync(Project project, string localUsing, CancellationToken cancellationToken = default)
{
return CountAsync(project, ImmutableArray.Create(localUsing), cancellationToken);
}

public static async Task<UsingCountResult> CountAsync(Project project, ImmutableArray<string> usings, CancellationToken cancellationToken = default)
{
RoslynUtilities.ThrowIfNotCSharp(project);

Expand All @@ -18,6 +29,7 @@ public static async Task<UsingCountResult> CountAsync(Project project, Cancellat
}

var result = new UsingCountResult(project.Name);
result.AddRange(usings);

if (RoslynUtilities.IsGeneratedCode(compilation))
{
Expand Down Expand Up @@ -48,31 +60,33 @@ public static async Task<UsingCountResult> 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<string> 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);
}
}
}
8 changes: 8 additions & 0 deletions src/tests/FlashOWare.Tool.Cli.Tests/Testing/Usings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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()
{
Expand Down

0 comments on commit 3a474f7

Please sign in to comment.