Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ Always exits with code `0`, even when validation errors are found. Useful in CI

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.

- **Disk check** — if no files match the glob patterns, a `SLNX020` (`RequiredFileDoesntExistOnSystem`) error is added to the solution result.
- **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.
- **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.

Relative paths in the `.slnx` are resolved relative to the solution file's location.
Expand Down Expand Up @@ -150,8 +150,8 @@ slnx-validator MySolution.slnx --required-files "appsettings.json;docs/"

| Code | Description |
|------|-------------|
| `0` | All patterns matched and all matched files are referenced in the solution. |
| `1` | Any validation error — including required files not existing or not referenced. |
| `0` | All matched files are referenced in the solution (or a wildcard pattern matched no files). |
| `1` | Any validation error — including a literal required file not existing or matched files not referenced. |

### Severity override flags

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

## XSD Schema
Expand Down
12 changes: 10 additions & 2 deletions src/SLNX-validator/RequiredFilesOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,18 @@ namespace JulianVerdurmen.SlnxValidator;
/// </summary>
/// <param name="MatchedPaths">
/// Absolute disk paths that were matched by <see cref="Pattern"/>.
/// An empty list means the pattern matched no files.
/// An empty list with a wildcard pattern means no files to check — not an error.
/// An empty list with a literal (non-wildcard) pattern means the file does not exist — emits <c>SLNX020</c>.
/// <see langword="null"/> means the <c>--required-files</c> option was not used.
/// </param>
/// <param name="Pattern">The raw semicolon-separated pattern string supplied by the user.</param>
internal sealed record RequiredFilesOptions(
IReadOnlyList<string>? MatchedPaths,
string? Pattern);
string? Pattern)
{
/// <summary>
/// Returns <see langword="true"/> when <see cref="Pattern"/> contains wildcard characters
/// (<c>*</c> or <c>?</c>), meaning zero matches is not an error.
/// </summary>
internal bool HasWildcard => Pattern is not null && (Pattern.Contains('*') || Pattern.Contains('?'));
}
9 changes: 6 additions & 3 deletions src/SLNX-validator/SlnxCollector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

internal sealed class SlnxCollector(IFileSystem fileSystem, ISlnxFileResolver fileResolver, ISlnxValidator validator, IRequiredFilesChecker requiredFilesChecker)
{
public async Task<IReadOnlyList<FileValidationResult>> CollectAsync(

Check warning on line 11 in src/SLNX-validator/SlnxCollector.cs

View workflow job for this annotation

GitHub Actions / sonarcloud

Refactor this method to reduce its Cognitive Complexity from 24 to the 15 allowed.

Check warning on line 11 in src/SLNX-validator/SlnxCollector.cs

View workflow job for this annotation

GitHub Actions / sonarcloud

Refactor this method to reduce its Cognitive Complexity from 24 to the 15 allowed.

Check warning on line 11 in src/SLNX-validator/SlnxCollector.cs

View workflow job for this annotation

GitHub Actions / sonarcloud

Refactor this method to reduce its Cognitive Complexity from 24 to the 15 allowed.

Check warning on line 11 in src/SLNX-validator/SlnxCollector.cs

View workflow job for this annotation

GitHub Actions / sonarcloud

Refactor this method to reduce its Cognitive Complexity from 24 to the 15 allowed.

Check warning on line 11 in src/SLNX-validator/SlnxCollector.cs

View workflow job for this annotation

GitHub Actions / sonarcloud

Refactor this method to reduce its Cognitive Complexity from 24 to the 15 allowed.

Check warning on line 11 in src/SLNX-validator/SlnxCollector.cs

View workflow job for this annotation

GitHub Actions / sonarcloud

Refactor this method to reduce its Cognitive Complexity from 24 to the 15 allowed.
string input,
RequiredFilesOptions? requiredFilesOptions,
CancellationToken cancellationToken)
Expand Down Expand Up @@ -63,9 +63,12 @@
var matched = requiredFilesOptions.MatchedPaths;
if (matched is null || matched.Count == 0)
{
allErrors.Add(new ValidationError(
ValidationErrorCode.RequiredFileDoesntExistOnSystem,
$"Required file does not exist on the system. No files matched: {requiredFilesOptions.Pattern}"));
if (!requiredFilesOptions.HasWildcard)
{
allErrors.Add(new ValidationError(
ValidationErrorCode.RequiredFileDoesntExistOnSystem,
$"Required file does not exist on the system. No files matched: {requiredFilesOptions.Pattern}"));
}
}
else
{
Expand Down
17 changes: 16 additions & 1 deletion tests/SLNX-validator.Tests/SlnxCollectorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ private static (SlnxCollector collector, IRequiredFilesChecker checker) CreateCo
#region CollectAsync

[Test]
public async Task CollectAsync_RequiredFilesPatternNoMatch_AddsRequiredFileDoesntExistOnSystemError()
public async Task CollectAsync_WildcardPatternNoMatch_NoError()
{
// Arrange
var (collector, _) = CreateCollector();
Expand All @@ -40,6 +40,21 @@ public async Task CollectAsync_RequiredFilesPatternNoMatch_AddsRequiredFileDoesn
// Act
var results = await collector.CollectAsync(SlnxPath, options, CancellationToken.None);

// Assert
results.Should().HaveCount(1);
results[0].HasErrors.Should().BeFalse();
}

[Test]
public async Task CollectAsync_LiteralPatternNoMatch_AddsRequiredFileDoesntExistOnSystemError()
{
// Arrange
var (collector, _) = CreateCollector();
var options = new RequiredFilesOptions(MatchedPaths: [], Pattern: "unknownfile.md");

// Act
var results = await collector.CollectAsync(SlnxPath, options, CancellationToken.None);

// Assert
results.Should().HaveCount(1);
results[0].HasErrors.Should().BeTrue();
Expand Down
24 changes: 22 additions & 2 deletions tests/SLNX-validator.Tests/ValidatorRunnerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,21 +125,41 @@ public async Task RunAsync_RequiredFiles_AllMatchedAndReferenced_ReturnsZero()
}

[Test]
public async Task RunAsync_RequiredFiles_NoMatchOnDisk_ReturnsOne()
public async Task RunAsync_RequiredFiles_WildcardNoMatchOnDisk_ReturnsZero()
{
// Arrange
var slnxPath = Path.GetFullPath("test.slnx");

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

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

// Act
var exitCode = await runner.RunAsync(
Options(slnxPath, requiredFilesPattern: "nonexistent/**/*.md"), CancellationToken.None);

// Assert
exitCode.Should().Be(0);
}

[Test]
public async Task RunAsync_RequiredFiles_LiteralNoMatchOnDisk_ReturnsOne()
{
// Arrange
var slnxPath = Path.GetFullPath("test.slnx");

var checker = Substitute.For<IRequiredFilesChecker>();
checker.ResolveMatchedPaths(Arg.Any<string>(), Arg.Any<string>())
.Returns([]); // nothing matched on disk — literal path, so SLNX020 expected

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

// Act
var exitCode = await runner.RunAsync(
Options(slnxPath, requiredFilesPattern: "unknownfile.md"), CancellationToken.None);

// Assert
exitCode.Should().Be(1);
}
Expand Down
Loading