From 2913595a1333d68085308a980687e62c5fb7967d Mon Sep 17 00:00:00 2001 From: Florian Schick Date: Tue, 21 Jul 2026 07:26:03 +0200 Subject: [PATCH] feat: auto detect solution from current directory. --- README.md | 4 +- .../ResultAssertions.cs | 6 ++ .../SolutionLocatorTests.cs | 78 +++++++++++++++++++ ReferenceSwitcher.Tool/ArgumentParser.cs | 18 +++-- ReferenceSwitcher.Tool/SolutionLocator.cs | 31 ++++++++ 5 files changed, 129 insertions(+), 8 deletions(-) create mode 100644 ReferenceSwitcher.Tool.Tests/SolutionLocatorTests.cs create mode 100644 ReferenceSwitcher.Tool/SolutionLocator.cs diff --git a/README.md b/README.md index ffc9e2b..f7af741 100644 --- a/README.md +++ b/README.md @@ -22,13 +22,13 @@ Both commands share the same basic arguments: ```bash reference-switcher \ - --solution \ --scan-directory \ + [--solution ] \ [--add-projects-to-solution] ``` -- `--solution` (`-s`): path to the `.sln` or `.slnx` file that defines the starting projects. - `--scan-directory` (`-d`): directory that will be scanned recursively for `.csproj` files used to build the project index. +- `--solution` (`-s`): path to the `.sln` or `.slnx` file that defines the starting projects. Optional when the current directory contains exactly one `.sln` or `.slnx` file. - `--add-projects-to-solution`: when specified, the tool also updates the solution file to reflect the changes. ### Behavior of `--add-projects-to-solution` diff --git a/ReferenceSwitcher.Tool.Tests/ResultAssertions.cs b/ReferenceSwitcher.Tool.Tests/ResultAssertions.cs index 5b3ee4a..a03097f 100644 --- a/ReferenceSwitcher.Tool.Tests/ResultAssertions.cs +++ b/ReferenceSwitcher.Tool.Tests/ResultAssertions.cs @@ -15,4 +15,10 @@ public static void Succeeded(Result result) if (result.IsFailure) Assert.Fail(result.Error); } + + public static void Failed(Result result) + { + if (result.IsSuccess) + Assert.Fail("Expected failure, but result succeeded."); + } } diff --git a/ReferenceSwitcher.Tool.Tests/SolutionLocatorTests.cs b/ReferenceSwitcher.Tool.Tests/SolutionLocatorTests.cs new file mode 100644 index 0000000..76b0c0c --- /dev/null +++ b/ReferenceSwitcher.Tool.Tests/SolutionLocatorTests.cs @@ -0,0 +1,78 @@ +namespace ReferenceSwitcher.Tool.Tests; + +public sealed class SolutionLocatorTests +{ + [Fact] + public void UsesProvidedSolutionPath() + { + using var workspace = TestWorkspace.Create(); + var solution = workspace.WriteFile("App.sln", "Microsoft Visual Studio Solution File"); + + var result = SolutionLocator.Resolve(solution, workspace.Root); + + ResultAssertions.Succeeded(result); + Assert.Equal(Path.GetFullPath(solution), result.Value); + } + + [Fact] + public void FindsSingleSlnInDirectory() + { + using var workspace = TestWorkspace.Create(); + var solution = workspace.WriteFile("App.sln", "Microsoft Visual Studio Solution File"); + + var result = SolutionLocator.Resolve(null, workspace.Root); + + ResultAssertions.Succeeded(result); + Assert.Equal(Path.GetFullPath(solution), result.Value); + } + + [Fact] + public void FindsSingleSlnxInDirectory() + { + using var workspace = TestWorkspace.Create(); + var solution = workspace.WriteFile("App.slnx", ""); + + var result = SolutionLocator.Resolve(null, workspace.Root); + + ResultAssertions.Succeeded(result); + Assert.Equal(Path.GetFullPath(solution), result.Value); + } + + [Fact] + public void FailsWhenNoSolutionExists() + { + using var workspace = TestWorkspace.Create(); + + var result = SolutionLocator.Resolve(null, workspace.Root); + + ResultAssertions.Failed(result); + Assert.Contains("--solution", result.Error); + } + + [Fact] + public void FailsWhenMultipleSolutionsExist() + { + using var workspace = TestWorkspace.Create(); + workspace.WriteFile("App.sln", "Microsoft Visual Studio Solution File"); + workspace.WriteFile("Other.slnx", ""); + + var result = SolutionLocator.Resolve(null, workspace.Root); + + ResultAssertions.Failed(result); + Assert.Contains("--solution", result.Error); + Assert.Contains("more than one", result.Error); + } + + [Fact] + public void PrefersProvidedPathEvenWhenDirectoryHasSolutions() + { + using var workspace = TestWorkspace.Create(); + workspace.WriteFile("App.sln", "Microsoft Visual Studio Solution File"); + var other = workspace.WriteFile("nested/Other.slnx", ""); + + var result = SolutionLocator.Resolve(other, workspace.Root); + + ResultAssertions.Succeeded(result); + Assert.Equal(Path.GetFullPath(other), result.Value); + } +} diff --git a/ReferenceSwitcher.Tool/ArgumentParser.cs b/ReferenceSwitcher.Tool/ArgumentParser.cs index 6f7fc5c..20f5a57 100644 --- a/ReferenceSwitcher.Tool/ArgumentParser.cs +++ b/ReferenceSwitcher.Tool/ArgumentParser.cs @@ -6,10 +6,9 @@ internal static class ArgumentParser { public static RootCommand BuildRootCommand() { - var solutionOption = new Option("--solution", ["-s"]) + var solutionOption = new Option("--solution", ["-s"]) { - Description = "Path to the base .sln or .slnx file.", - Required = true + Description = "Path to the base .sln or .slnx file. Optional when the current directory contains exactly one .sln or .slnx file." }; var scanDirectoryOption = new Option("--scan-directory", ["-d"]) @@ -42,14 +41,21 @@ Command BuildCommand(string name, string description, SwitchMode mode) command.SetAction(parseResult => { - var solutionFile = parseResult.GetValue(solutionOption); + var providedSolution = parseResult.GetValue(solutionOption); var scanDirectory = parseResult.GetValue(scanDirectoryOption); var updateSolution = parseResult.GetValue(updateSolutionOption); - ArgumentNullException.ThrowIfNull(solutionFile); ArgumentNullException.ThrowIfNull(scanDirectory); - RunSwitch(mode, solutionFile, scanDirectory, updateSolution); + var solutionResult = SolutionLocator.Resolve(providedSolution?.FullName, Directory.GetCurrentDirectory()); + if (solutionResult.IsFailure) + { + Console.Error.WriteLine(solutionResult.Error); + Environment.ExitCode = 1; + return; + } + + RunSwitch(mode, new FileInfo(solutionResult.Value), scanDirectory, updateSolution); }); return command; diff --git a/ReferenceSwitcher.Tool/SolutionLocator.cs b/ReferenceSwitcher.Tool/SolutionLocator.cs new file mode 100644 index 0000000..31ca778 --- /dev/null +++ b/ReferenceSwitcher.Tool/SolutionLocator.cs @@ -0,0 +1,31 @@ +using CSharpFunctionalExtensions; + +namespace ReferenceSwitcher.Tool; + +internal static class SolutionLocator +{ + public static Result Resolve(string? providedSolutionPath, string directory) + { + if (!string.IsNullOrWhiteSpace(providedSolutionPath)) + return Result.Success(Path.GetFullPath(providedSolutionPath)); + + var solutions = Directory.EnumerateFiles(directory) + .Where(IsSolutionFile) + .OrderBy(path => path, StringComparer.OrdinalIgnoreCase) + .ToArray(); + + return solutions.Length switch + { + 1 => Result.Success(Path.GetFullPath(solutions[0])), + 0 => Result.Failure("Option '--solution' is required when the current directory does not contain a .sln or .slnx file."), + _ => Result.Failure("Option '--solution' is required when the current directory contains more than one .sln or .slnx file.") + }; + } + + private static bool IsSolutionFile(string path) + { + var extension = Path.GetExtension(path); + return extension.Equals(".sln", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".slnx", StringComparison.OrdinalIgnoreCase); + } +}