Skip to content

Commit

Permalink
feat: globalize many using directives or force all (#71)
Browse files Browse the repository at this point in the history
normalizes arguments with count command
fixes duplicate globalizations

Co-Authored-By: Eva Ditzelmüller <87754804+EvaDitzelmueller@users.noreply.github.com>
  • Loading branch information
Flash0ver and EvaDitzelmueller authored Nov 3, 2023
1 parent 3a474f7 commit 2769f56
Show file tree
Hide file tree
Showing 9 changed files with 574 additions and 80 deletions.
41 changes: 29 additions & 12 deletions src/libraries/FlashOWare.Tool.Cli/CliApplication.UsingDirectives.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ private static void AddUsingCommand(RootCommand rootCommand, MSBuildWorkspace wo
{
var usingCommand = new Command("using", " Analyze or refactor C# using directives.");
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 globalizeCommand = new Command("globalize", "Move top-level using directives to global using directives 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();
Expand Down Expand Up @@ -42,12 +42,14 @@ private static void AddUsingCommand(RootCommand rootCommand, MSBuildWorkspace wo
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.");
globalizeCommand.Add(usingArgument);
var globalizeArgument = new Argument<string[]>("USINGS", "The names of the top-level using directives to convert to global using directives. If usings are not specified, the command will globalize all top-level directives.");
var forceOption = new Option<bool>("--force", "Forces all top-level using directives to be globalized when no usings are specified.");
globalizeCommand.Add(globalizeArgument);
globalizeCommand.Add(projectOption);
globalizeCommand.Add(forceOption);
globalizeCommand.SetHandler(async (InvocationContext context) =>
{
string localUsing = context.ParseResult.GetValueForArgument(usingArgument);
string[] usings = context.ParseResult.GetValueForArgument(globalizeArgument);
FileInfo? project = context.ParseResult.GetValueForOption(projectOption);
if (project is null)
{
Expand All @@ -62,7 +64,13 @@ private static void AddUsingCommand(RootCommand rootCommand, MSBuildWorkspace wo
};
}

await GlobalizeUsingsAsync(workspace, project.FullName, localUsing, context.Console, context.GetCancellationToken());
bool isForced = context.ParseResult.GetValueForOption(forceOption);
if (usings.Length == 0 && !isForced)
{
throw new InvalidOperationException("No usings specified. To globalize all top-level using directives, run the command with '--force' option.");
}

await GlobalizeUsingsAsync(workspace, project.FullName, usings.ToImmutableArray(), context.Console, context.GetCancellationToken());
});

usingCommand.Add(countCommand);
Expand Down Expand Up @@ -94,15 +102,15 @@ private static async Task CountUsingsAsync(MSBuildWorkspace workspace, string pr
}
}

private static async Task GlobalizeUsingsAsync(MSBuildWorkspace workspace, string projectFilePath, string localUsing, IConsole console, CancellationToken cancellationToken)
private static async Task GlobalizeUsingsAsync(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);

workspace.ThrowIfCannotApplyChanges(ApplyChangesKind.AddDocument, ApplyChangesKind.ChangeDocument);
var result = await UsingGlobalizer.GlobalizeAsync(project, localUsing, cancellationToken);
var result = await UsingGlobalizer.GlobalizeAsync(project, usings, cancellationToken);

string? oldProject = null;
if (project.DocumentIds.Count < result.Project.DocumentIds.Count)
Expand All @@ -121,17 +129,26 @@ private static async Task GlobalizeUsingsAsync(MSBuildWorkspace workspace, strin

console.WriteLine($"{nameof(Project)}: {result.Project.Name}");

if (result.Using.Occurrences == 0)
if (result.Occurrences == 0)
{
console.WriteLine($"""No occurrences of Using Directive "{localUsing}" were globalized.""");
string message = result.Usings.Count == 1
? $"""No occurrences of Using Directive "{result.Usings.Single().Name}" were globalized."""
: $"""No occurrences of {result.Usings.Count} Using Directives were globalized.""";
console.WriteLine(message);
}
else if (result.Using.Occurrences == 1)
else if (result.Occurrences == 1)
{
console.WriteLine($"""1 occurrence of Using Directive "{localUsing}" was globalized to "{result.TargetDocument}".""");
string message = result.Usings.Count == 1
? $"""1 occurrence of Using Directive "{result.Usings.Single().Name}" was globalized to "{result.TargetDocument}"."""
: $"""1 occurrence of {result.Usings.Count} Using Directives was globalized to "{result.TargetDocument}".""";
console.WriteLine(message);
}
else
{
console.WriteLine($"""{result.Using.Occurrences} occurrences of Using Directive "{localUsing}" were globalized to "{result.TargetDocument}".""");
string message = result.Usings.Count == 1
? $"""{result.Usings.Single().Occurrences} occurrences of Using Directive "{result.Usings.Single().Name}" were globalized to "{result.TargetDocument}"."""
: $"""{result.Occurrences} occurrences of {result.Usings.Count} Using Directives were globalized to "{result.TargetDocument}".""";
console.WriteLine(message);
}
}
catch (OperationCanceledException)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

namespace FlashOWare.Tool.Core.CodeAnalysis;

internal static class CSharpSyntaxFactory
internal static partial class CSharpSyntaxFactory
{
public static CompilationUnitSyntax GlobalUsingDirectiveRoot(string name, DocumentOptionSet options)
{
Expand All @@ -16,11 +16,23 @@ public static CompilationUnitSyntax GlobalUsingDirectiveRoot(string name, Docume
.WithEndOfFileToken(Token(SyntaxKind.EndOfFileToken));
}

public static CompilationUnitSyntax GlobalUsingDirectivesRoot(IEnumerable<string> names, DocumentOptionSet options)
{
return CompilationUnit()
.WithUsings(GlobalUsingDirectiveList(names, options))
.WithEndOfFileToken(Token(SyntaxKind.EndOfFileToken));
}

private static SyntaxList<UsingDirectiveSyntax> GlobalUsingDirectiveList(string name, DocumentOptionSet options)
{
return SingletonList(GlobalUsingDirective(name, options));
}

private static SyntaxList<UsingDirectiveSyntax> GlobalUsingDirectiveList(IEnumerable<string> names, DocumentOptionSet options)
{
return List(GlobalUsingDirectives(names, options));
}

public static UsingDirectiveSyntax GlobalUsingDirective(string name, DocumentOptionSet options)
{
return UsingDirective(IdentifierName(name))
Expand All @@ -29,6 +41,17 @@ public static UsingDirectiveSyntax GlobalUsingDirective(string name, DocumentOpt
.WithSemicolonToken(Token(TriviaList(), SyntaxKind.SemicolonToken, EndOfLineList(options)));
}

public static IEnumerable<UsingDirectiveSyntax> GlobalUsingDirectives(IEnumerable<string> names, DocumentOptionSet options)
{
foreach (var name in names)
{
yield return GlobalUsingDirective(name, options);
}
}
}

internal static partial class CSharpSyntaxFactory
{
private static SyntaxTriviaList EndOfLineList(DocumentOptionSet options)
{
return TriviaList(EndOfLine(options));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@ internal void AddRange(ImmutableArray<string> identifiers)
internal void Increment(string identifier)
{
UsingDirective usingDirective = _usings[identifier];
usingDirective.Occurrences++;
usingDirective.IncrementOccurrences();
}

internal void IncrementOrAdd(string identifier)
{
if (_usings.TryGetValue(identifier, out UsingDirective? usingDirective))
{
usingDirective.Occurrences++;
usingDirective.IncrementOccurrences();
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ public UsingDirective(string name, int occurrences)
}

public required string Name { get; init; }
public int Occurrences { get; internal set; }
public int Occurrences { get; private set; }

internal void IncrementOccurrences()
{
Occurrences++;
}

public override string ToString()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,79 @@
using Microsoft.CodeAnalysis;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;

namespace FlashOWare.Tool.Core.UsingDirectives;

public sealed class UsingGlobalizationResult
{
internal UsingGlobalizationResult()
private readonly Dictionary<string, UsingDirective> _usings = new();

internal UsingGlobalizationResult(Project project)
{
Project = project;
}

[SetsRequiredMembers]
internal UsingGlobalizationResult(Project project, UsingDirective @using, string targetDocument)
internal UsingGlobalizationResult(Project project, string targetDocument)
{
Project = project;
Using = @using;
TargetDocument = targetDocument;
}

public required Project Project { get; init; }
public required UsingDirective Using { get; init; }
public Project Project { get; private set; }
public IReadOnlyCollection<UsingDirective> Usings => _usings.Values;
public required string TargetDocument { get; init; }
public int Occurrences { get; private set; }

internal void Initialize(ImmutableArray<string> identifiers)
{
Debug.Assert(Occurrences == 0, $"Result has already been updated.");
Debug.Assert(_usings.Count == 0, $"Result has already been initialized.");

foreach (string identifier in identifiers)
{
_ = _usings.TryAdd(identifier, new UsingDirective(identifier));
}
}

internal void Update(Project project)
{
Project = project;
}

internal void Update(string identifier)
{
if (_usings.TryGetValue(identifier, out UsingDirective? usingDirective))
{
usingDirective.IncrementOccurrences();
}
else
{
usingDirective = new UsingDirective(identifier, 1);
_usings.Add(identifier, usingDirective);
}

Occurrences++;
}

internal void Update(string[] identifiers)
{
foreach (var identifier in identifiers)
{
Update(identifier);
}
}

[Conditional("DEBUG")]
internal void Verify()
{
int occurrences = _usings.Values.Aggregate(0, static (sum, directive) => sum + directive.Occurrences);
Debug.Assert(occurrences == Occurrences, $"Verification of {nameof(UsingGlobalizationResult)} failed: {nameof(Occurrences)}={Occurrences} Aggregate={occurrences}");
}

public override string ToString()
{
return Project.Name;
}
}
Loading

0 comments on commit 2769f56

Please sign in to comment.