Skip to content

Add --required-files CLI argument with glob pattern matching and solution reference check#43

Merged
304NotModified merged 6 commits intomainfrom
copilot/add-required-files-cli-argument
Mar 21, 2026
Merged

Add --required-files CLI argument with glob pattern matching and solution reference check#43
304NotModified merged 6 commits intomainfrom
copilot/add-required-files-cli-argument

Conversation

Copy link
Contributor

Copilot AI commented Mar 20, 2026

Adds a --required-files option that verifies glob-matched files both exist on disk and are referenced as <File> entries in the validated .slnx solution file(s). Uses semicolon-separated glob patterns with ordered include/exclude semantics. Failures are reported as standard validation errors (exit code 1) and appear in SonarQube reports.

Changes

Core logic

  • IRequiredFilesChecker.cs — public interface in SLNX-validator.Core with two methods:
    • ResolveMatchedPaths — resolves glob patterns to absolute disk paths using Microsoft.Extensions.FileSystemGlobbing (Matcher with preserveFilterOrder: true, StringComparison.OrdinalIgnoreCase)
    • CheckInSlnx — accepts a SlnxFile DTO and checks that each required path is referenced; error message includes the exact <File Path="..."> element to add
  • RequiredFilesChecker.cs — internal sealed implementation in Core, registered via DI in ServiceCollectionExtensions
  • SlnxFile.cs — new DTO in Core that owns the single XDocument.Parse call; SlnxFile.Parse(content, dir) returns SlnxFile? (null on malformed XML) and normalises all <File> entries to absolute paths; stores SlnxDirectory for relative-path error messages
  • IFileSystem — extended with OpenRead and ReadAllTextAsync so all file I/O is routed through the abstraction

New error codes

  • SLNX020 RequiredFileDoesntExistOnSystem — no files matched the required-files glob patterns on disk
  • SLNX021 RequiredFileNotReferencedInSolution — a matched file exists on disk but is not referenced as <File> in the solution

CLI

  • ValidatorRunnerOptions — new record grouping all ValidatorRunner.RunAsync parameters (Input, SonarqubeReportPath, ContinueOnError, RequiredFilesPattern, WorkingDirectory)
  • RequiredFilesOptions — new record grouping the required-files parameters passed to ValidationCollector.CollectAsync (MatchedPaths, Pattern)
  • ValidatorRunner.cs — uses ValidatorRunnerOptions; resolves glob patterns once and passes RequiredFilesOptions to the collector
  • ValidationCollector.cs — uses RequiredFilesOptions; calls SlnxFile.Parse once per file and passes the DTO to CheckInSlnx; uses IFileSystem for all file I/O; skips required-files check when XML is invalid
  • Program.cs — builds and passes ValidatorRunnerOptions to the runner

Documentation

  • README.md — updated --required-files section with SLNX020/SLNX021 error codes, updated glob example to "all yaml files except in src folder" (**/*.yaml;!src/**), clarified exit code 1 as "any validation error"

Tests

  • RequiredFilesCheckerTests.cs — pure unit tests for CheckInSlnx and SlnxFile.Parse (valid XML, invalid XML returns null, path normalisation); uses #region sections and // Arrange / Act / Assert comments
  • RequiredFilesCheckerIntegrationTests.cs — 2 integration tests for ResolveMatchedPaths (happy + exclude) in a dedicated *IntegrationTests class
  • ValidationCollectorTests.cs — true unit tests using MockFileSystem with in-memory file content (no temp dirs); mocked IRequiredFilesChecker and ISlnxValidator
  • ValidatorRunnerTests.cs — updated for ValidatorRunnerOptions; includes 2 required-files unit tests with mocked IRequiredFilesChecker
  • ProgramIntegrationTests.cs--required-files integration tests removed (covered by unit tests above)
  • MockFileSystem — extended with a Dictionary<string,string> constructor for in-memory file content, and OpenRead/ReadAllTextAsync implementations

Usage

slnx-validator MySolution.slnx --required-files "**/*.yaml;!src/**"

Pattern order is respected: a later !exclude can suppress an earlier include, and a later include can re-include something previously excluded. All required-files failures use exit code 1 and appear in SonarQube reports alongside other validation errors.


⌨️ Start Copilot coding agent tasks without leaving your editor — available in VS Code, Visual Studio, JetBrains IDEs and Eclipse.

…d README docs

Co-authored-by: 304NotModified <5808377+304NotModified@users.noreply.github.com>
Agent-Logs-Url: https://github.com/304NotModified/SLNX-validator/sessions/a91fd106-0ab8-4a61-966c-d18d183152f2
Copilot AI changed the title [WIP] Add --required-files CLI argument to application Add --required-files CLI argument with glob pattern matching Mar 20, 2026
Copilot AI requested a review from 304NotModified March 20, 2026 22:51
@304NotModified 304NotModified marked this pull request as ready for review March 20, 2026 23:08
@304NotModified
Copy link
Owner

@copilot it should check that these required file are in the slnx (as <file>), as last check

Correctly adapt relative paths

…lnx solution

Co-authored-by: 304NotModified <5808377+304NotModified@users.noreply.github.com>
Agent-Logs-Url: https://github.com/304NotModified/SLNX-validator/sessions/2cd8b089-f7a8-46ee-a2c4-ad527da73512
Copy link
Contributor Author

Copilot AI commented Mar 21, 2026

@copilot it should check that these required file are in the slnx (as <file>), as last check

Correctly adapt relative paths

Done in 6e1405e. The check now runs in two stages:

  1. Pre-check (before validation) — glob patterns must match at least one file on disk.
  2. Last check (after validation) — each matched file must also appear as a <File Path="..."> element in the validated .slnx file(s).

Relative paths inside the .slnx are resolved relative to that solution file's directory via Path.GetFullPath(Path.Combine(slnxDir, path)), so a ../doc/readme.md reference from a subdirectory .slnx correctly matches the absolute glob-matched path.

Copilot AI changed the title Add --required-files CLI argument with glob pattern matching Add --required-files CLI argument with glob pattern matching and solution reference check Mar 21, 2026
var sonarqubeReport = parseResult.GetValue(sonarqubeReportOption);
var continueOnError = parseResult.GetValue(continueOnErrorOption);
return await services.GetRequiredService<ValidatorRunner>().RunAsync(input!, sonarqubeReport, continueOnError, cancellationToken);
var runResult = await services.GetRequiredService<ValidatorRunner>().RunAsync(input!, sonarqubeReport, continueOnError, cancellationToken);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The checks should run in the runner. So it's also create errors that will be in the sonarqube report (of enabled)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in a9600b4. ValidatorRunner resolves the glob patterns via IRequiredFilesChecker.ResolveMatchedPaths, then passes the matched paths to ValidationCollector.CollectAsync. The collector adds SLNX020/SLNX021 errors directly into FileValidationResult, so they appear in both the console output and the SonarQube report.

}

[Test]
[NotInParallel("CurrentDirectory")]
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only one happy and alternative integration test. Others should tests to unit tests (with mocked file system)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in a9600b4. ProgramIntegrationTests now has only 1 happy + 1 alt integration test for --required-files. The rest became unit tests: CheckInSlnx pure-string unit tests are in SLNX-validator.Core.Tests/RequiredFilesCheckerTests.cs, and ValidationCollectorTests.cs (new file) covers the collector logic with NSubstitute mocks for IRequiredFilesChecker and ISlnxValidator.

README.md Outdated
| `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` | `RequiredFilesNotFound` | A `--required-files` pattern produced no disk matches, or a matched file is not referenced as `<File>` in the solution (exits with code `2`). |
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The special exit code is not needed for this. Also add the error requiredFileDoesntExistOnSystem (spelled correctly)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in a9600b4. Removed the special exit code 2 — required-files failures use exit code 1 like all other validation errors. Added RequiredFileDoesntExistOnSystem = 20 (SLNX020) and RequiredFileNotReferencedInSolution = 21 (SLNX021) to ValidationErrorCode. README updated accordingly.


var missing = requiredAbsolutePaths.Where(p => !slnxFileRefs.Contains(p)).ToList();
foreach (var m in missing)
await Console.Error.WriteLineAsync($"[SLNX020] Required file not referenced in solution: {m}");
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also print how it should look in the slnx (including html element)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in a9600b4. The RequiredFileNotReferencedInSolution error message now includes the exact element to add, e.g.: Required file is not referenced in the solution: /path/to/readme.md — add: <File Path="doc/readme.md" />

…ator pipeline, fix error codes

Co-authored-by: 304NotModified <5808377+304NotModified@users.noreply.github.com>
Agent-Logs-Url: https://github.com/304NotModified/SLNX-validator/sessions/b69c15c2-a945-4420-bc1c-b4542a60f632

try
{
var doc = XDocument.Parse(slnxContent);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deduplicate the parse of the slnx, create a dto

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in ec1f478. Created SlnxFileRefs DTO with a Parse(slnxContent, slnxDirectory) factory that owns the single XDocument.Parse call and resolves all <File> paths to absolute normalised strings. ValidationCollector calls Parse once and passes the result to CheckInSlnx.

var fullPath = Path.IsPathRooted(path)
? Path.GetFullPath(path)
: Path.GetFullPath(Path.Combine(slnxDirectory, path));

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deduplicate

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in ec1f478 — both the XDocument.Parse and the Path.IsPathRooted / Path.GetFullPath path-resolution logic now live exclusively in SlnxFileRefs.Parse; CheckInSlnx just compares against slnxFileRefs.AbsoluteFilePaths.

{
public async Task<IReadOnlyList<FileValidationResult>> CollectAsync(IReadOnlyList<string> files, CancellationToken cancellationToken)
public async Task<IReadOnlyList<FileValidationResult>> CollectAsync(
IReadOnlyList<string> files,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create options type for the parameters

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in ec1f478. Created RequiredFilesOptions(IReadOnlyList<string>? MatchedPaths, string? Pattern) record. CollectAsync now takes RequiredFilesOptions? requiredFilesOptions instead of two separate nullable parameters.

{
public async Task<int> RunAsync(string input, string? sonarqubeReportPath, bool continueOnError, CancellationToken cancellationToken)
public async Task<int> RunAsync(string input, string? sonarqubeReportPath, bool continueOnError,
string? requiredFilesPattern, string workingDirectory, CancellationToken cancellationToken)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create options type for the parameters (record)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in ec1f478. Created ValidatorRunnerOptions(string Input, string? SonarqubeReportPath, bool ContinueOnError, string? RequiredFilesPattern, string WorkingDirectory) record. RunAsync now takes ValidatorRunnerOptions options, CancellationToken cancellationToken.

[Test]
public void ResolveMatchedPaths_SingleInclude_MatchesFiles_ReturnsNonEmpty()
{
var tempDir = CreateTempDir();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are too many integrations tests. I need two, one happy one alternative. Rest to unit tests with file system mock.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in ec1f478. ResolveMatchedPaths integration tests reduced to 2 (happy path + exclude). All CheckInSlnx tests are pure unit tests using SlnxFileRefs.Parse on inline XML strings with no real filesystem access.

[Test]
public async Task CollectAsync_RequiredFilesPatternNoMatch_AddsRequiredFileDoesntExistOnSystemError()
{
var tempDir = CreateTempDir();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too many integration tests. Move to unit tests

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in ec1f478. IFileSystem gained OpenRead and ReadAllTextAsync; ValidationCollector uses them instead of File.* directly. MockFileSystem now accepts a Dictionary<string,string> constructor for in-memory file content. All 4 ValidationCollectorTests are now true unit tests with zero real filesystem I/O.

await File.WriteAllTextAsync(Path.Combine(docDir, "readme.md"), "# Readme");
// The .slnx references doc/readme.md as a <File> so the last check passes.
await File.WriteAllTextAsync(slnxPath, """
<Solution>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too many integration tests. Move to unit tests

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in ec1f478. Removed both --required-files integration tests from ProgramIntegrationTests and replaced them with two unit tests in ValidatorRunnerTests (RunAsync_RequiredFiles_AllMatchedAndReferenced_ReturnsZero and RunAsync_RequiredFiles_NoMatchOnDisk_ReturnsOne) using mocked IRequiredFilesChecker and MockFileSystem.

README.md Outdated

Require all `.cs` files under `src/`, excluding the `bin` and `obj` folders:
```
slnx-validator MySolution.slnx --required-files "src/**/*.cs;!**/bin/**;!**/obj/**"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change this example to: all yaml files except in src folder.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in ec1f478. Changed the example to **/*.yaml;!src/** ("all yaml files except in src folder").

README.md Outdated
| Code | Description |
|------|-------------|
| `0` | All patterns matched and all matched files are referenced in the solution. |
| `1` | One or more required files don't exist on disk or are not referenced in the solution (same as any other validation error). |
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 should ne any validation error

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in ec1f478. Changed to: 1 — Any validation error — including required files not existing or not referenced.

…sts to unit tests; update README

Co-authored-by: 304NotModified <5808377+304NotModified@users.noreply.github.com>
Agent-Logs-Url: https://github.com/304NotModified/SLNX-validator/sessions/b2fb8573-e2fd-4582-aa07-b67537345be2
Copy link
Contributor Author

Copilot AI commented Mar 21, 2026

@copilot it should check that these required file are in the slnx (as <file>), as last check

Correctly adapt relative paths

Already implemented — SlnxFileRefs.Parse resolves all <File Path="..."> elements to absolute paths using Path.GetFullPath(Path.Combine(slnxDirectory, path)), so ../doc/readme.md from a sub-directory .slnx is correctly normalised before comparison (ec1f478).

Copy link
Owner

@304NotModified 304NotModified left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also check for duplication in code and tests.

/// Represents the set of absolute file paths that are referenced as
/// <c>&lt;File Path="..."&gt;</c> elements inside a .slnx solution file.
/// </summary>
public sealed class SlnxFileRefs
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename to SlnxFile

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in e956c34. SlnxFileRefs.cs renamed to SlnxFile.cs and the class renamed to SlnxFile.


/// <summary>Absolute, normalised paths for every <c>&lt;File&gt;</c> entry in the solution.</summary>
public IReadOnlyList<string> AbsoluteFilePaths { get; }

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename to Files (the xml docs are file)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in e956c34. AbsoluteFilePaths renamed to Files.

}
catch (Exception)
{
// Malformed XML is already reported by the XML validator.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't catch, Don't call this method if the xml is invalid OR return null

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in e956c34. SlnxFile.Parse now returns SlnxFile? — it tries to parse, returns null on XmlException (no swallowing). ValidationCollector guards with if (slnxFile is not null) before calling CheckInSlnx.


private static RequiredFilesChecker CreateChecker() => new();

// ── ResolveMatchedPaths (integration: 2 tests) ───────────────────────────
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use #regions for these kind of comments

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add this to copilot instructions

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in e956c34. All test classes now use #region blocks to group related tests. Convention stored to memory.

File.WriteAllText(Path.Combine(docDir, "readme.md"), "# Readme");
File.WriteAllText(Path.Combine(docDir, "contributing.md"), "# Contributing");

var matched = CreateChecker().ResolveMatchedPaths("doc/*.md", tempDir);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add

// Arrange
// Act
// Assert to all comments

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add this to copilot instructions

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in e956c34. All test methods now have // Arrange, // Act, // Assert comments. Convention stored to memory.


public class RequiredFilesCheckerTests
{
private static string CreateTempDir()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move the integration tests to a separate test class, RequiredFilesCheckerIntegrationTests

The same for other test files.

Add this to copilot instructions

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in e956c34. ResolveMatchedPaths integration tests moved to new RequiredFilesCheckerIntegrationTests class. RequiredFilesCheckerTests now contains only unit tests. Convention stored to memory.

"primaryLocation": {
"message": "Sample message for RequiredFileDoesntExistOnSystem",
"filePath": "Solution.slnx",
"textRange": {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not possible like this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The snapshot is valid. The AllErrorCodes test uses allCodes.Select((code, i) => new ValidationError(code, ..., Line: i + 1)) — it assigns an artificial Line: i+1 to every error to test that textRange is included in the output. SLNX020 is the 8th enum value (index 7), so Line: 8, giving "startLine": 8 in the snapshot. The snapshot was accepted after adding the two new error codes.

…it integration tests

Co-authored-by: 304NotModified <5808377+304NotModified@users.noreply.github.com>
Agent-Logs-Url: https://github.com/304NotModified/SLNX-validator/sessions/ceaf8ba9-6628-42a8-b534-1dfec61aafc3
Copy link
Contributor Author

Copilot AI commented Mar 21, 2026

Also check for duplication in code and tests.

Reviewed in e956c34. The main duplication was the double XDocument.Parse call that was already deduplicated into SlnxFile in the previous round. No other meaningful duplication remains — CreateChecker()/CreateRunner()/CreateRunnerWithSlnx() factory helpers are used consistently across test classes.

@304NotModified 304NotModified merged commit c1bf743 into main Mar 21, 2026
5 checks passed
@304NotModified 304NotModified deleted the copilot/add-required-files-cli-argument branch March 21, 2026 13:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants