diff --git a/src/libraries/FlashOWare.Tool.Cli/CliApplication.UsingDirectives.cs b/src/libraries/FlashOWare.Tool.Cli/CliApplication.UsingDirectives.cs index 1d3e756..0661b6a 100644 --- a/src/libraries/FlashOWare.Tool.Cli/CliApplication.UsingDirectives.cs +++ b/src/libraries/FlashOWare.Tool.Cli/CliApplication.UsingDirectives.cs @@ -1,4 +1,5 @@ using FlashOWare.Tool.Cli.CodeAnalysis; +using FlashOWare.Tool.Cli.IO; using FlashOWare.Tool.Core.UsingDirectives; using Microsoft.CodeAnalysis; using System.CommandLine.IO; @@ -8,13 +9,13 @@ namespace FlashOWare.Tool.Cli; public static partial class CliApplication { - private static void AddUsingCommand(RootCommand rootCommand, MSBuildWorkspace workspace) + 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 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 project file to operate on.") + 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(); countCommand.Add(projectOption); @@ -23,7 +24,15 @@ private static void AddUsingCommand(RootCommand rootCommand, MSBuildWorkspace wo FileInfo? project = context.ParseResult.GetValueForOption(projectOption); if (project is null) { - throw new NotImplementedException("Please pass a specific project path via --project."); + var currentDirectory = fileSystem.GetCurrentDirectory(); + var files = currentDirectory.GetFiles("*.*proj"); + + project = files switch + { + [] => throw new InvalidOperationException("Specify a project file. The current working directory does not contain a project file."), + [var file] => file, + [..] => throw new InvalidOperationException("Specify which project file to use because this folder contains more than one project file."), + }; } await CountUsingsAsync(workspace, project.FullName, context.Console, context.GetCancellationToken()); @@ -38,7 +47,15 @@ private static void AddUsingCommand(RootCommand rootCommand, MSBuildWorkspace wo FileInfo? project = context.ParseResult.GetValueForOption(projectOption); if (project is null) { - throw new NotImplementedException("Please pass a specific project path via --project."); + var currentDirectory = fileSystem.GetCurrentDirectory(); + var files = currentDirectory.GetFiles("*.*proj"); + + project = files switch + { + [] => throw new InvalidOperationException("Specify a project file. The current working directory does not contain a project file."), + [var file] => file, + [..] => throw new InvalidOperationException("Specify which project file to use because this folder contains more than one project file."), + }; } await GlobalizeUsingsAsync(workspace, project.FullName, localUsing, context.Console, context.GetCancellationToken()); diff --git a/src/libraries/FlashOWare.Tool.Cli/CliApplication.cs b/src/libraries/FlashOWare.Tool.Cli/CliApplication.cs index 2dcda93..40c60e7 100644 --- a/src/libraries/FlashOWare.Tool.Cli/CliApplication.cs +++ b/src/libraries/FlashOWare.Tool.Cli/CliApplication.cs @@ -1,4 +1,5 @@ using FlashOWare.Tool.Cli.CommandLine; +using FlashOWare.Tool.Cli.IO; using Microsoft.Build.Locator; using Microsoft.CodeAnalysis; using System.Collections.Immutable; @@ -9,9 +10,10 @@ public static partial class CliApplication { private static readonly SemaphoreSlim s_msBuildMutex = new(1, 1); - public static async Task RunAsync(string[] args, IConsole? console = null, VisualStudioInstance? msBuild = null) + public static async Task RunAsync(string[] args, IConsole? console = null, VisualStudioInstance? msBuild = null, IFileSystemAccessor? fileSystem = null) { msBuild ??= MSBuildLocator.RegisterDefaults(); + fileSystem ??= FileSystemAccessor.System; var properties = ImmutableDictionary.Empty.Add("Configuration", "Release"); using var workspace = MSBuildWorkspace.Create(properties); @@ -40,7 +42,7 @@ public static async Task RunAsync(string[] args, IConsole? console = null, } }); - AddUsingCommand(rootCommand, workspace); + AddUsingCommand(rootCommand, workspace, fileSystem); int exitCode = await rootCommand.InvokeAsync(args, console); workspace.WorkspaceFailed -= OnWorkspaceFailed; diff --git a/src/libraries/FlashOWare.Tool.Cli/IO/FileSystemAccessor.cs b/src/libraries/FlashOWare.Tool.Cli/IO/FileSystemAccessor.cs new file mode 100644 index 0000000..5c6e388 --- /dev/null +++ b/src/libraries/FlashOWare.Tool.Cli/IO/FileSystemAccessor.cs @@ -0,0 +1,16 @@ +namespace FlashOWare.Tool.Cli.IO; + +internal sealed class FileSystemAccessor : IFileSystemAccessor +{ + public static FileSystemAccessor System { get; } = new FileSystemAccessor(); + + private FileSystemAccessor() + { + } + + public DirectoryInfo GetCurrentDirectory() + { + string currentDirectory = Directory.GetCurrentDirectory(); + return new DirectoryInfo(currentDirectory); + } +} diff --git a/src/libraries/FlashOWare.Tool.Cli/IO/IFileSystemAccessor.cs b/src/libraries/FlashOWare.Tool.Cli/IO/IFileSystemAccessor.cs new file mode 100644 index 0000000..9c39657 --- /dev/null +++ b/src/libraries/FlashOWare.Tool.Cli/IO/IFileSystemAccessor.cs @@ -0,0 +1,6 @@ +namespace FlashOWare.Tool.Cli.IO; + +public interface IFileSystemAccessor +{ + DirectoryInfo GetCurrentDirectory(); +} diff --git a/src/tests/FlashOWare.Tool.Cli.Tests/IO/FileSystemAccessor.cs b/src/tests/FlashOWare.Tool.Cli.Tests/IO/FileSystemAccessor.cs new file mode 100644 index 0000000..6bf141e --- /dev/null +++ b/src/tests/FlashOWare.Tool.Cli.Tests/IO/FileSystemAccessor.cs @@ -0,0 +1,18 @@ +using FlashOWare.Tool.Cli.IO; + +namespace FlashOWare.Tool.Cli.Tests.IO; + +internal sealed class FileSystemAccessor : IFileSystemAccessor +{ + private readonly DirectoryInfo _directory; + + public FileSystemAccessor(DirectoryInfo directory) + { + _directory = directory; + } + + public DirectoryInfo GetCurrentDirectory() + { + return _directory; + } +} diff --git a/src/tests/FlashOWare.Tool.Cli.Tests/Testing/IntegrationTests.cs b/src/tests/FlashOWare.Tool.Cli.Tests/Testing/IntegrationTests.cs index 821bda3..d6f8612 100644 --- a/src/tests/FlashOWare.Tool.Cli.Tests/Testing/IntegrationTests.cs +++ b/src/tests/FlashOWare.Tool.Cli.Tests/Testing/IntegrationTests.cs @@ -1,7 +1,6 @@ using FlashOWare.Tool.Cli.Tests.IO; using FlashOWare.Tool.Cli.Tests.Workspaces; using Microsoft.Build.Locator; -using Microsoft.CodeAnalysis; using System.CommandLine.IO; using Xunit.Abstractions; @@ -13,6 +12,7 @@ public abstract class IntegrationTests : IDisposable private static int s_number = 0; private readonly DirectoryInfo _scratch; + private readonly FileSystemAccessor _fileSystem; private readonly RedirectedConsole _system; protected IntegrationTests() @@ -31,6 +31,7 @@ protected IntegrationTests(ITestOutputHelper? output) string name = type.FullName ?? type.Name; _scratch = FileSystemUtilities.CreateScratchDirectory(Build.Configuration, Build.TFM, name, incremented); + _fileSystem = new FileSystemAccessor(_scratch); Workspace = new PhysicalWorkspaceProvider(_scratch); Result = new RunResult(); @@ -50,7 +51,7 @@ protected IntegrationTests(ITestOutputHelper? output) protected async Task RunAsync(params string[] args) { - int exitCode = await CliApplication.RunAsync(args, Console, MSBuild); + int exitCode = await CliApplication.RunAsync(args, Console, MSBuild, _fileSystem); Result.Set(exitCode); } diff --git a/src/tests/FlashOWare.Tool.Cli.Tests/UsingDirectives/UsingCounterTests.cs b/src/tests/FlashOWare.Tool.Cli.Tests/UsingDirectives/UsingCounterTests.cs index f6bcd7f..3bd0111 100644 --- a/src/tests/FlashOWare.Tool.Cli.Tests/UsingDirectives/UsingCounterTests.cs +++ b/src/tests/FlashOWare.Tool.Cli.Tests/UsingDirectives/UsingCounterTests.cs @@ -70,7 +70,7 @@ internal class MyClass2 } [Fact] - public async Task Count_ProjectFileDoesNotExist_FailsValidation() + public async Task Count_ExplicitProjectFileDoesNotExist_FailsValidation() { //Arrange string project = "ProjectFileDoesNotExist.csproj"; @@ -102,4 +102,58 @@ End Class Console.VerifyContains(null, $"Cannot open project '{project.File}' because the language '{LanguageNames.VisualBasic}' is not supported."); Result.Verify(ExitCodes.Error); } + + [Fact] + public async Task Count_ImplicitSingleProject_UseCurrentDirectory() + { + //Arrange + _ = Workspace.CreateProject() + .AddDocument(""" + using System; + + namespace ProjectUnderTest.NetCore; + + internal class MyClass1 + { + } + """, "MyClass1") + .Initialize(ProjectKind.SdkStyle, TargetFramework.Net60, LanguageVersion.CSharp10); + string[] args = new[] { "using", "count" }; + //Act + await RunAsync(args); + //Assert + Console.Verify($""" + Project: {Names.Project} + System: 1 + """); + Result.Verify(ExitCodes.Success); + } + + [Fact] + public async Task Count_ImplicitProjectMissing_Error() + { + //Arrange + string[] args = new[] { "using", "count" }; + //Act + await RunAsync(args); + //Assert + Console.VerifyContains(null, "Specify a project file. The current working directory does not contain a project file."); + Result.Verify(ExitCodes.Error); + } + + [Fact] + public async Task Count_ImplicitMultipleProjects_Ambiguous() + { + //Arrange + _ = Workspace.CreateProject() + .Initialize(ProjectKind.SdkStyle, TargetFramework.Net60, LanguageVersion.CSharp10); + _ = Workspace.CreateProject().WithProjectName("Ambiguous") + .Initialize(ProjectKind.SdkStyle, TargetFramework.Net60, LanguageVersion.CSharp10); + string[] args = new[] { "using", "count" }; + //Act + await RunAsync(args); + //Assert + Console.VerifyContains(null, "Specify which project file to use because this folder contains more than one project file."); + Result.Verify(ExitCodes.Error); + } } diff --git a/src/tests/FlashOWare.Tool.Cli.Tests/UsingDirectives/UsingGlobalizerTests.cs b/src/tests/FlashOWare.Tool.Cli.Tests/UsingDirectives/UsingGlobalizerTests.cs index c306acb..9c6f446 100644 --- a/src/tests/FlashOWare.Tool.Cli.Tests/UsingDirectives/UsingGlobalizerTests.cs +++ b/src/tests/FlashOWare.Tool.Cli.Tests/UsingDirectives/UsingGlobalizerTests.cs @@ -238,7 +238,7 @@ internal class MyClass2 } [Fact] - public async Task Globalize_ProjectFileDoesNotExist_FailsValidation() + public async Task Globalize_ExplicitProjectFileDoesNotExist_FailsValidation() { //Arrange string project = "ProjectFileDoesNotExist.csproj"; @@ -270,4 +270,75 @@ End Class Console.VerifyContains(null, $"Cannot open project '{project.File}' because the language '{LanguageNames.VisualBasic}' is not supported."); Result.Verify(ExitCodes.Error); } + + [Fact] + public async Task Globalize_ImplicitSingleProject_UseCurrentDirectory() + { + //Arrange + _ = Workspace.CreateProject() + .AddDocument(""" + using System; + using System.Collections.Generic; + + namespace ProjectUnderTest.NetCore; + + internal class MyClass1 + { + } + """, "MyClass1") + .Initialize(ProjectKind.SdkStyle, TargetFramework.Net60, LanguageVersion.CSharp10); + string[] args = new[] { "using", "globalize", Usings.System }; + //Act + await RunAsync(args); + //Assert + Console.Verify($""" + Project: {Names.Project} + 1 occurrence of Using Directive "System" was globalized to "GlobalUsings.cs". + """); + Workspace.CreateExpectation() + .AppendFile(""" + global using System; + + """, Names.GlobalUsings) + .AppendFile(""" + using System.Collections.Generic; + + namespace ProjectUnderTest.NetCore; + + internal class MyClass1 + { + } + """, "MyClass1.cs") + .AppendFile(ProjectText.Create(TargetFramework.Net60, LanguageVersion.CSharp10), Names.CSharpProject) + .Verify(); + Result.Verify(ExitCodes.Success); + } + + [Fact] + public async Task Globalize_ImplicitProjectMissing_Error() + { + //Arrange + string[] args = new[] { "using", "globalize", Usings.System }; + //Act + await RunAsync(args); + //Assert + Console.VerifyContains(null, "Specify a project file. The current working directory does not contain a project file."); + Result.Verify(ExitCodes.Error); + } + + [Fact] + public async Task Globalize_ImplicitMultipleProjects_Ambiguous() + { + //Arrange + _ = Workspace.CreateProject() + .Initialize(ProjectKind.SdkStyle, TargetFramework.Net60, LanguageVersion.CSharp10); + _ = Workspace.CreateProject().WithProjectName("Ambiguous") + .Initialize(ProjectKind.SdkStyle, TargetFramework.Net60, LanguageVersion.CSharp10); + string[] args = new[] { "using", "globalize", Usings.System }; + //Act + await RunAsync(args); + //Assert + Console.VerifyContains(null, "Specify which project file to use because this folder contains more than one project file."); + Result.Verify(ExitCodes.Error); + } } diff --git a/src/tests/FlashOWare.Tool.Cli.Tests/Workspaces/PhysicalDocument.cs b/src/tests/FlashOWare.Tool.Cli.Tests/Workspaces/PhysicalDocument.cs index 9aac9cc..071eec1 100644 --- a/src/tests/FlashOWare.Tool.Cli.Tests/Workspaces/PhysicalDocument.cs +++ b/src/tests/FlashOWare.Tool.Cli.Tests/Workspaces/PhysicalDocument.cs @@ -5,15 +5,20 @@ namespace FlashOWare.Tool.Cli.Tests.Workspaces; internal sealed class PhysicalDocument { - public PhysicalDocument(string text, string directory, string file) + private PhysicalDocument(string text, string directory, string fileName) + : this(text, new DirectoryInfo(directory), fileName) + { + } + + private PhysicalDocument(string text, DirectoryInfo directory, string fileName) { Text = text; Directory = directory; - FullName = Path.Combine(directory, file); + FullName = Path.Combine(directory.FullName, fileName); } public string Text { get; } - public string Directory { get; } + public DirectoryInfo Directory { get; } public string FullName { get; } public static PhysicalDocument Create(string text, DirectoryInfo directory, string fileName, Language language) @@ -21,7 +26,7 @@ public static PhysicalDocument Create(string text, DirectoryInfo directory, stri string extension = language.GetDocumentExtension(true); fileName = PathUtilities.WithExtension(extension, fileName); - return new PhysicalDocument(text, directory.FullName, fileName); + return new PhysicalDocument(text, directory, fileName); } public static PhysicalDocument Create(string text, DirectoryInfo directory, string fileName, string[] folders, Language language) @@ -38,4 +43,19 @@ public static PhysicalDocument Create(string text, DirectoryInfo directory, stri return new PhysicalDocument(text, folder, fileName); } + + public void Write() + { + if (!Directory.Exists) + { + Directory.Create(); + } + + if (File.Exists(FullName)) + { + throw new InvalidOperationException($"Document '{FullName}' already exists."); + } + + File.WriteAllText(FullName, Text); + } } diff --git a/src/tests/FlashOWare.Tool.Cli.Tests/Workspaces/PhysicalProject.cs b/src/tests/FlashOWare.Tool.Cli.Tests/Workspaces/PhysicalProject.cs index 6e08be5..6a088a1 100644 --- a/src/tests/FlashOWare.Tool.Cli.Tests/Workspaces/PhysicalProject.cs +++ b/src/tests/FlashOWare.Tool.Cli.Tests/Workspaces/PhysicalProject.cs @@ -1,5 +1,6 @@ using FlashOWare.Tool.Cli.Tests.IO; using FlashOWare.Tool.Cli.Tests.Testing; +using System.Diagnostics; namespace FlashOWare.Tool.Cli.Tests.Workspaces; @@ -20,4 +21,21 @@ public static PhysicalProject Create(DirectoryInfo directory, string name, Langu string path = Path.Combine(directory.FullName, fileName); return new PhysicalProject(path); } + + public string GetDirectoryName() + { + string? directory = File.DirectoryName; + Debug.Assert(directory is not null, $"'{File}' is in a root directory."); + return directory; + } + + public void Write(string text) + { + if (File.Exists) + { + throw new InvalidOperationException($"Project '{File}' already exists."); + } + + System.IO.File.WriteAllText(File.FullName, text); + } } diff --git a/src/tests/FlashOWare.Tool.Cli.Tests/Workspaces/PhysicalProjectBuilder.cs b/src/tests/FlashOWare.Tool.Cli.Tests/Workspaces/PhysicalProjectBuilder.cs index 7c37ba5..dd655f6 100644 --- a/src/tests/FlashOWare.Tool.Cli.Tests/Workspaces/PhysicalProjectBuilder.cs +++ b/src/tests/FlashOWare.Tool.Cli.Tests/Workspaces/PhysicalProjectBuilder.cs @@ -71,8 +71,7 @@ public PhysicalProject Initialize(ProjectKind kind, TargetFramework tfm, Languag foreach (PhysicalDocument document in _documents) { - Directory.CreateDirectory(document.Directory); - File.WriteAllText(document.FullName, document.Text); + document.Write(); } PhysicalProject project = PhysicalProject.Create(_directory, _projectName, _language); @@ -87,7 +86,7 @@ public PhysicalProject Initialize(ProjectKind kind, TargetFramework tfm, Languag ? ProjectText.CreateNonSdk(tfm, langVersion.DefaultIfNull(tfm), files) : ProjectText.Create(tfm, langVersion); - File.WriteAllText(project.File.FullName, projectText); + project.Write(projectText); } else { @@ -97,7 +96,7 @@ public PhysicalProject Initialize(ProjectKind kind, TargetFramework tfm, Languag ? throw new NotImplementedException($"{Language.VisualBasic} non-SDK .NET Framework project is not implemented.") : ProjectText.CreateVisualBasic(tfm, langVersion.HasValue ? throw new NotImplementedException($"{Language.VisualBasic} {nameof(Microsoft.CodeAnalysis.VisualBasic.LanguageVersion)} is not implemented.") : null); - File.WriteAllText(project.File.FullName, projectText); + project.Write(projectText); } return project; diff --git a/src/tests/FlashOWare.Tool.Cli.Tests/Workspaces/PhysicalWorkspaceProvider.cs b/src/tests/FlashOWare.Tool.Cli.Tests/Workspaces/PhysicalWorkspaceProvider.cs index 3ea97aa..a7a5a7c 100644 --- a/src/tests/FlashOWare.Tool.Cli.Tests/Workspaces/PhysicalWorkspaceProvider.cs +++ b/src/tests/FlashOWare.Tool.Cli.Tests/Workspaces/PhysicalWorkspaceProvider.cs @@ -26,4 +26,9 @@ internal FileSystemExpectation CreateExpectation(Language language = Language.CS { return new FileSystemExpectation(_directory, language); } + + internal string GetDirectoryName() + { + return _directory.FullName; + } }