Skip to content

Commit 6b46aa3

Browse files
committed
Add support for slnx solution files
- Add SlnxParser to parse the new XML-based solution format - Update PathUtility to recognize and handle .slnx files - Support automatic discovery of .slnx files in directories - Add test fixture and unit test for slnx parsing https://claude.ai/code/session_01MrKfiY4ERuPhaGTg9bTQSr
1 parent ab6734f commit 6b46aa3

5 files changed

Lines changed: 161 additions & 4 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<Solution>
2+
<Project Path="Foo\Foo.csproj" />
3+
<Project Path="Bar\Bar.csproj" />
4+
<Project Path="Baz\Baz.csproj" />
5+
<Project Path="Qux\Qux.csproj" />
6+
<Project Path="Zap\Zap.csproj" />
7+
<Project Path="Quux\Quux.csproj" />
8+
<Project Path="Quuux\Quuux.csproj" />
9+
<Project Path="Thud\Thud.csproj" />
10+
<Project Path="Thuuud\Thuuud.csproj" />
11+
<Project Path="FSharp\FSharp.fsproj" />
12+
</Solution>
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
Analyzing...
2+
Analyzing Snitch.Tests.Fixtures.slnx
3+
Analyzing Foo...
4+
Analyzing Bar...
5+
Analyzing Baz...
6+
Analyzing Qux...
7+
Analyzing Zap...
8+
Analyzing Quux...
9+
Analyzing Quuux...
10+
Analyzing Thud...
11+
Analyzing Thuuud...
12+
Analyzing FSharp...
13+
14+
╭─────────────────────────────────────────────────────────────────╮
15+
│ Packages that can be removed from Bar: │
16+
│ ┌──────────────────────┬──────────────────────────────────────┐ │
17+
│ │ Package │ Referenced by │ │
18+
│ ├──────────────────────┼──────────────────────────────────────┤ │
19+
│ │ Autofac │ Foo │ │
20+
│ └──────────────────────┴──────────────────────────────────────┘ │
21+
│ │
22+
│ Packages that can be removed from Baz: │
23+
│ ┌──────────────────────┬──────────────────────────────────────┐ │
24+
│ │ Package │ Referenced by │ │
25+
│ ├──────────────────────┼──────────────────────────────────────┤ │
26+
│ │ Autofac │ Foo │ │
27+
│ └──────────────────────┴──────────────────────────────────────┘ │
28+
│ │
29+
│ Packages that might be removed from Qux: │
30+
│ ┌───────────┬───────────┬─────────────────────────────────────┐ │
31+
│ │ Package │ Version │ Reason │ │
32+
│ ├───────────┼───────────┼─────────────────────────────────────┤ │
33+
│ │ Autofac │ 4.9.3 │ Downgraded from 4.9.4 in Foo │ │
34+
│ └───────────┴───────────┴─────────────────────────────────────┘ │
35+
│ │
36+
│ Packages that might be removed from Zap: │
37+
│ ┌──────────────────┬──────────┬───────────────────────────────┐ │
38+
│ │ Package │ Version │ Reason │ │
39+
│ ├──────────────────┼──────────┼───────────────────────────────┤ │
40+
│ │ Newtonsoft.Json │ 12.0.3 │ Updated from 12.0.1 in Foo │ │
41+
│ │ Autofac │ 4.9.3 │ Downgraded from 4.9.4 in Foo │ │
42+
│ └──────────────────┴──────────┴───────────────────────────────┘ │
43+
│ │
44+
│ Packages that might be removed from Thuuud: │
45+
│ ┌─────────────────┬──────────────┬────────────────────────────┐ │
46+
│ │ Package │ Version │ Reason │ │
47+
│ ├─────────────────┼──────────────┼────────────────────────────┤ │
48+
│ │ Newtonsoft.Json │ 13.0.2-beta2 │ Updated from 12.0.1 in Foo │ │
49+
│ └─────────────────┴──────────────┴────────────────────────────┘ │
50+
╰─────────────────────────────────────────────────────────────────╯

src/Snitch.Tests/ProgramTests.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,5 +222,21 @@ public async Task Should_Return_Expected_Result_For_FSharp_Not_Specifying_Framew
222222
exitCode.ShouldBe(0);
223223
await Verifier.Verify(output);
224224
}
225+
226+
[Fact]
227+
[Expectation("Slnx", "Default")]
228+
public async Task Should_Return_Expected_Result_For_Slnx_Solution()
229+
{
230+
// Given
231+
var fixture = new Fixture();
232+
var solution = Fixture.GetPath("Snitch.Tests.Fixtures.slnx");
233+
234+
// When
235+
var (exitCode, output) = await Fixture.Run(solution);
236+
237+
// Then
238+
exitCode.ShouldBe(0);
239+
await Verifier.Verify(output);
240+
}
225241
}
226242
}

src/Snitch/Analysis/Utilities/PathUtility.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ private static List<string> GetProjectsFromFile(string path)
5353
return GetProjectsFromSolution(path);
5454
}
5555

56+
if (path.EndsWith(".slnx", StringComparison.InvariantCulture))
57+
{
58+
return SlnxParser.GetProjectsFromSlnx(path);
59+
}
60+
5661
throw new InvalidOperationException("Project or solution file do not exist.");
5762
}
5863

@@ -61,8 +66,11 @@ private static List<string> FindProjects(string? root, out string entry)
6166
root ??= Environment.CurrentDirectory;
6267

6368
var slns = Directory.GetFiles(root, "*.sln");
69+
var slnxs = Directory.GetFiles(root, "*.slnx");
70+
var allSolutions = new List<string>(slns);
71+
allSolutions.AddRange(slnxs);
6472

65-
if (slns.Length == 0)
73+
if (allSolutions.Count == 0)
6674
{
6775
var subProjects = Directory.GetFiles(root, "*.csproj");
6876
if (subProjects.Length == 0)
@@ -77,14 +85,14 @@ private static List<string> FindProjects(string? root, out string entry)
7785
entry = subProjects[0];
7886
return new List<string>(new[] { subProjects[0] });
7987
}
80-
else if (slns.Length > 1)
88+
else if (allSolutions.Count > 1)
8189
{
8290
throw new InvalidOperationException("More than one solution file found.");
8391
}
8492
else
8593
{
86-
entry = slns[0];
87-
return GetProjectsFromSolution(slns[0]);
94+
entry = allSolutions[0];
95+
return GetProjectsFromFile(allSolutions[0]);
8896
}
8997
}
9098

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Xml.Linq;
5+
6+
namespace Snitch.Analysis.Utilities
7+
{
8+
internal static class SlnxParser
9+
{
10+
public static List<string> GetProjectsFromSlnx(string slnxPath)
11+
{
12+
if (!File.Exists(slnxPath))
13+
{
14+
throw new FileNotFoundException($"Solution file not found: {slnxPath}");
15+
}
16+
17+
var slnxDirectory = Path.GetDirectoryName(slnxPath)
18+
?? throw new InvalidOperationException("Could not determine solution directory.");
19+
20+
var doc = XDocument.Load(slnxPath);
21+
var solution = doc.Root;
22+
23+
if (solution == null || solution.Name.LocalName != "Solution")
24+
{
25+
throw new InvalidOperationException("Invalid slnx file: missing Solution root element.");
26+
}
27+
28+
var projects = new List<string>();
29+
CollectProjects(solution, slnxDirectory, projects);
30+
31+
return projects;
32+
}
33+
34+
private static void CollectProjects(XElement element, string slnxDirectory, List<string> projects)
35+
{
36+
foreach (var child in element.Elements())
37+
{
38+
if (child.Name.LocalName == "Project")
39+
{
40+
var pathAttr = child.Attribute("Path");
41+
if (pathAttr != null && !string.IsNullOrWhiteSpace(pathAttr.Value))
42+
{
43+
var projectPath = pathAttr.Value;
44+
45+
// Only include MSBuild project files (.csproj, .fsproj, .vbproj)
46+
if (IsMSBuildProject(projectPath))
47+
{
48+
var absolutePath = Path.GetFullPath(Path.Combine(slnxDirectory, projectPath));
49+
if (!projects.Contains(absolutePath))
50+
{
51+
projects.Add(absolutePath);
52+
}
53+
}
54+
}
55+
}
56+
else if (child.Name.LocalName == "Folder")
57+
{
58+
// Recursively collect projects from folders
59+
CollectProjects(child, slnxDirectory, projects);
60+
}
61+
}
62+
}
63+
64+
private static bool IsMSBuildProject(string path)
65+
{
66+
return path.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase)
67+
|| path.EndsWith(".fsproj", StringComparison.OrdinalIgnoreCase)
68+
|| path.EndsWith(".vbproj", StringComparison.OrdinalIgnoreCase);
69+
}
70+
}
71+
}

0 commit comments

Comments
 (0)