Skip to content

Commit d3847de

Browse files
peopleworksclaude
andcommitted
Make the build-output filter behave the same on every platform
CI caught this on ubuntu: the new filter used Path.GetRelativePath, which honours only the running platform's separator. On Linux a Windows-style path is one long segment containing no directories at all, so relativizing failed, the whole path was examined, and a project under a directory named "bin" was skipped again -- the exact bug the filter exists to prevent, reappearing whenever the analysis ran anywhere but Windows. Analyzing a Windows XAF project from a container or a CI runner is an ordinary thing to do, so the comparison now normalizes separators and matches prefixes itself. It requires a separator at the boundary too, so a root of "App.Module" cannot swallow a sibling named "App.Module2". The test now covers both separator styles rather than assuming the host's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 460b9bd commit d3847de

2 files changed

Lines changed: 39 additions & 16 deletions

File tree

src/XafLogicExplainer.Core/Analyzers/BuildOutputFilter.cs

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -58,23 +58,33 @@ public static bool IsAnalyzable(string? path, string? rootDirectory = null) =>
5858
/// Reduces a path to the part below the analyzed root, or leaves it alone when that cannot be
5959
/// established.
6060
/// </summary>
61+
/// <remarks>
62+
/// Deliberately not <see cref="Path.GetRelativePath"/>. That method honours only the running
63+
/// platform's separator, so on Linux a Windows-style path is one long segment containing no
64+
/// directories at all — and analyzing a Windows project from a container or a CI runner is an
65+
/// ordinary thing to do. Comparing normalized prefixes gives the same answer everywhere.
66+
/// </remarks>
6167
private static string Relativize(string path, string? rootDirectory)
6268
{
6369
if (string.IsNullOrWhiteSpace(rootDirectory))
6470
return path;
6571

66-
try
67-
{
68-
var relative = Path.GetRelativePath(rootDirectory, path);
72+
var normalizedPath = Normalize(path);
73+
var normalizedRoot = Normalize(rootDirectory);
6974

70-
// A path outside the root comes back with a leading "..", which tells us nothing about
71-
// build output; fall back to examining the whole path in that case.
72-
return relative.StartsWith("..", StringComparison.Ordinal) ? path : relative;
73-
}
74-
catch (ArgumentException)
75-
{
76-
// Different drive roots, or an unusable path. Examine what we were given.
75+
if (normalizedRoot.Length == 0 || normalizedPath.Length <= normalizedRoot.Length)
7776
return path;
78-
}
77+
78+
if (!normalizedPath.StartsWith(normalizedRoot, StringComparison.OrdinalIgnoreCase))
79+
return path;
80+
81+
// Require a separator at the boundary so that a root of "App.Module" does not swallow a
82+
// sibling directory named "App.Module2".
83+
return normalizedPath[normalizedRoot.Length] == '/'
84+
? normalizedPath[(normalizedRoot.Length + 1)..]
85+
: path;
7986
}
87+
88+
private static string Normalize(string path) =>
89+
path.Replace('\\', '/').TrimEnd('/');
8090
}

tests/XafLogicExplainer.Tests/BuildOutputFilterTests.cs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,28 @@ public void SkipsBuildOutput(string path) =>
3030
public void KeepsSourceWhoseNameMerelyContainsThoseLetters(string path) =>
3131
Assert.False(BuildOutputFilter.IsBuildOutput(path));
3232

33-
[Fact]
34-
public void KeepsProjectsThatLiveUnderADirectoryNamedBin()
33+
[Theory]
34+
// Both separator styles, on whichever platform the tests happen to run: analyzing a Windows
35+
// project from a Linux container or CI runner is ordinary, and the answer must not change.
36+
[InlineData(@"C:\bin\Sales\App.Module", @"C:\bin\Sales\App.Module\BusinessObjects\Invoice.cs", @"C:\bin\Sales\App.Module\bin\Debug\App.dll")]
37+
[InlineData("/srv/bin/sales/App.Module", "/srv/bin/sales/App.Module/BusinessObjects/Invoice.cs", "/srv/bin/sales/App.Module/obj/Debug/App.g.cs")]
38+
public void KeepsProjectsThatLiveUnderADirectoryNamedBin(string root, string source, string output)
3539
{
3640
// Build output is always below the project root. A "bin" above it is somebody's folder
3741
// name, and skipping those files would report an application with nothing in it.
38-
const string root = @"C:\bin\Sales\App.Module";
42+
Assert.False(BuildOutputFilter.IsBuildOutput(source, root));
43+
Assert.True(BuildOutputFilter.IsBuildOutput(output, root));
44+
}
45+
46+
[Fact]
47+
public void DoesNotMistakeASiblingDirectoryForTheRoot()
48+
{
49+
const string root = @"C:\Solution\App.Module";
3950

40-
Assert.False(BuildOutputFilter.IsBuildOutput($@"{root}\BusinessObjects\Invoice.cs", root));
41-
Assert.True(BuildOutputFilter.IsBuildOutput($@"{root}\bin\Debug\net10.0\App.dll", root));
51+
// "App.Module2" starts with the root's text but is not inside it, so the whole path is
52+
// examined rather than a bogus relative remainder.
53+
Assert.False(BuildOutputFilter.IsBuildOutput(@"C:\Solution\App.Module2\Order.cs", root));
54+
Assert.True(BuildOutputFilter.IsBuildOutput(@"C:\Solution\App.Module2\bin\Order.cs", root));
4255
}
4356

4457
[Fact]

0 commit comments

Comments
 (0)