Skip to content

Commit 22f975c

Browse files
fix: wildcard patterns with 0 matches are OK; literal patterns with 0 matches emit SLNX020
Agent-Logs-Url: https://github.com/304NotModified/SLNX-validator/sessions/deabf102-3e90-4a06-a252-62362daaa419 Co-authored-by: 304NotModified <5808377+304NotModified@users.noreply.github.com>
1 parent 15252df commit 22f975c

5 files changed

Lines changed: 64 additions & 9 deletions

File tree

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@ Always exits with code `0`, even when validation errors are found. Useful in CI
104104

105105
Verify that a set of files or directories matching glob patterns exist on disk **and** are referenced as `<File>` entries in the solution file(s) being validated. Any failure is reported as a normal validation error (exit code `1`) that also appears in SonarQube reports.
106106

107-
- **Reference check** — for each matched file that is not referenced as `<File Path="...">` in the `.slnx`, a `SLNX021` (`RequiredFileNotReferencedInSolution`) error is added. The error message shows the exact `<File>` element that should be added. If the pattern matches no files, the check is skipped silently (not an error).
107+
- **Disk check** — if a **literal** (non-wildcard) pattern matches no files on disk, a `SLNX020` (`RequiredFileDoesntExistOnSystem`) error is added. If the pattern contains wildcards (`*` or `?`) and matches no files, the check is skipped silently — zero matches is not an error.
108+
- **Reference check** — for each matched file that is not referenced as `<File Path="...">` in the `.slnx`, a `SLNX021` (`RequiredFileNotReferencedInSolution`) error is added. The error message shows the exact `<File>` element that should be added.
108109

109110
Relative paths in the `.slnx` are resolved relative to the solution file's location.
110111

@@ -149,8 +150,8 @@ slnx-validator MySolution.slnx --required-files "appsettings.json;docs/"
149150

150151
| Code | Description |
151152
|------|-------------|
152-
| `0` | All matched files are referenced in the solution (or no files matched the pattern). |
153-
| `1` | Any validation error — including required files not referenced. |
153+
| `0` | All matched files are referenced in the solution (or a wildcard pattern matched no files). |
154+
| `1` | Any validation error — including a literal required file not existing or matched files not referenced. |
154155

155156
### Severity override flags
156157

@@ -402,6 +403,7 @@ The following are **intentionally out of scope** because the toolchain already h
402403
| `SLNX011` | `ReferencedFileNotFound` | A file referenced in `<File Path="...">` does not exist on disk. |
403404
| `SLNX012` | `InvalidWildcardUsage` | A `<File Path="...">` contains a wildcard pattern (see [`examples/invalid-wildcard.slnx`](examples/invalid-wildcard.slnx)). |
404405
| `SLNX013` | `XsdViolation` | The XML structure violates the schema, e.g. `<Folder>` inside `<Folder>` (see [`examples/invalid-xsd.slnx`](examples/invalid-xsd.slnx)). |
406+
| `SLNX020` | `RequiredFileDoesntExistOnSystem` | A literal (non-wildcard) `--required-files` pattern matched no files on the file system. |
405407
| `SLNX021` | `RequiredFileNotReferencedInSolution` | A `--required-files` matched file exists on disk but is not referenced as a `<File>` element in the solution. |
406408

407409
## XSD Schema

src/SLNX-validator/RequiredFilesOptions.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,18 @@ namespace JulianVerdurmen.SlnxValidator;
66
/// </summary>
77
/// <param name="MatchedPaths">
88
/// Absolute disk paths that were matched by <see cref="Pattern"/>.
9-
/// An empty list means the pattern matched no files — this is not an error (no files to check).
9+
/// An empty list with a wildcard pattern means no files to check — not an error.
10+
/// An empty list with a literal (non-wildcard) pattern means the file does not exist — emits <c>SLNX020</c>.
1011
/// <see langword="null"/> means the <c>--required-files</c> option was not used.
1112
/// </param>
1213
/// <param name="Pattern">The raw semicolon-separated pattern string supplied by the user.</param>
1314
internal sealed record RequiredFilesOptions(
1415
IReadOnlyList<string>? MatchedPaths,
15-
string? Pattern);
16+
string? Pattern)
17+
{
18+
/// <summary>
19+
/// Returns <see langword="true"/> when <see cref="Pattern"/> contains wildcard characters
20+
/// (<c>*</c> or <c>?</c>), meaning zero matches is not an error.
21+
/// </summary>
22+
internal bool HasWildcard => Pattern is not null && (Pattern.Contains('*') || Pattern.Contains('?'));
23+
}

src/SLNX-validator/SlnxCollector.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,16 @@ public async Task<IReadOnlyList<FileValidationResult>> CollectAsync(
6161
if (requiredFilesOptions is not null)
6262
{
6363
var matched = requiredFilesOptions.MatchedPaths;
64-
if (matched is not null && matched.Count > 0)
64+
if (matched is null || matched.Count == 0)
65+
{
66+
if (!requiredFilesOptions.HasWildcard)
67+
{
68+
allErrors.Add(new ValidationError(
69+
ValidationErrorCode.RequiredFileDoesntExistOnSystem,
70+
$"Required file does not exist on the system. No files matched: {requiredFilesOptions.Pattern}"));
71+
}
72+
}
73+
else
6574
{
6675
var hasXsdErrors = allErrors.Any(e => e.Code == ValidationErrorCode.XsdViolation);
6776
if (!hasXsdErrors)

tests/SLNX-validator.Tests/SlnxCollectorTests.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ private static (SlnxCollector collector, IRequiredFilesChecker checker) CreateCo
3131
#region CollectAsync
3232

3333
[Test]
34-
public async Task CollectAsync_RequiredFilesPatternNoMatch_NoError()
34+
public async Task CollectAsync_WildcardPatternNoMatch_NoError()
3535
{
3636
// Arrange
3737
var (collector, _) = CreateCollector();
@@ -45,6 +45,22 @@ public async Task CollectAsync_RequiredFilesPatternNoMatch_NoError()
4545
results[0].HasErrors.Should().BeFalse();
4646
}
4747

48+
[Test]
49+
public async Task CollectAsync_LiteralPatternNoMatch_AddsRequiredFileDoesntExistOnSystemError()
50+
{
51+
// Arrange
52+
var (collector, _) = CreateCollector();
53+
var options = new RequiredFilesOptions(MatchedPaths: [], Pattern: "unknownfile.md");
54+
55+
// Act
56+
var results = await collector.CollectAsync(SlnxPath, options, CancellationToken.None);
57+
58+
// Assert
59+
results.Should().HaveCount(1);
60+
results[0].HasErrors.Should().BeTrue();
61+
results[0].Errors.Should().ContainSingle(e => e.Code == ValidationErrorCode.RequiredFileDoesntExistOnSystem);
62+
}
63+
4864
[Test]
4965
public async Task CollectAsync_RequiredFileMissingFromSlnx_AddsRequiredFileNotReferencedInSolutionError()
5066
{

tests/SLNX-validator.Tests/ValidatorRunnerTests.cs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,14 +125,14 @@ public async Task RunAsync_RequiredFiles_AllMatchedAndReferenced_ReturnsZero()
125125
}
126126

127127
[Test]
128-
public async Task RunAsync_RequiredFiles_NoMatchOnDisk_ReturnsZero()
128+
public async Task RunAsync_RequiredFiles_WildcardNoMatchOnDisk_ReturnsZero()
129129
{
130130
// Arrange
131131
var slnxPath = Path.GetFullPath("test.slnx");
132132

133133
var checker = Substitute.For<IRequiredFilesChecker>();
134134
checker.ResolveMatchedPaths(Arg.Any<string>(), Arg.Any<string>())
135-
.Returns([]); // nothing matched on disk — no error expected
135+
.Returns([]); // nothing matched on disk — wildcard, so no error expected
136136

137137
var runner = CreateRunnerWithSlnx(slnxPath, "<Solution />", checker);
138138

@@ -144,6 +144,26 @@ public async Task RunAsync_RequiredFiles_NoMatchOnDisk_ReturnsZero()
144144
exitCode.Should().Be(0);
145145
}
146146

147+
[Test]
148+
public async Task RunAsync_RequiredFiles_LiteralNoMatchOnDisk_ReturnsOne()
149+
{
150+
// Arrange
151+
var slnxPath = Path.GetFullPath("test.slnx");
152+
153+
var checker = Substitute.For<IRequiredFilesChecker>();
154+
checker.ResolveMatchedPaths(Arg.Any<string>(), Arg.Any<string>())
155+
.Returns([]); // nothing matched on disk — literal path, so SLNX020 expected
156+
157+
var runner = CreateRunnerWithSlnx(slnxPath, "<Solution />", checker);
158+
159+
// Act
160+
var exitCode = await runner.RunAsync(
161+
Options(slnxPath, requiredFilesPattern: "unknownfile.md"), CancellationToken.None);
162+
163+
// Assert
164+
exitCode.Should().Be(1);
165+
}
166+
147167
#endregion
148168

149169
#region RunAsync – severity overrides

0 commit comments

Comments
 (0)