-
Notifications
You must be signed in to change notification settings - Fork 0
Add --required-files CLI argument with glob pattern matching and solution reference check
#43
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
1564a23
4d22767
6e1405e
a9600b4
ec1f478
e956c34
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -65,6 +65,59 @@ slnx-validator MySolution.slnx --sonarqube-report-file sonar-issues.json --conti | |
|
|
||
| Always exits with code `0`, even when validation errors are found. Useful in CI pipelines where SonarQube handles the failure decision. Default: `false`. | ||
|
|
||
| ### `--required-files` | ||
|
|
||
| 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. | ||
| - **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. | ||
|
|
||
| **Syntax** | ||
|
|
||
| ``` | ||
| --required-files "<pattern1>;<pattern2>;..." | ||
| ``` | ||
|
|
||
| Patterns are separated by `;`. Patterns starting with `!` are exclusions. Pattern order matters: a later pattern can override an earlier one. | ||
|
|
||
| **Supported glob syntax** | ||
|
|
||
| | Pattern | Meaning | Example | | ||
| |---|---|---| | ||
| | `*` | Any file in the current directory (no path separator) | `doc/*.md` | | ||
| | `**` | Any depth of subdirectories | `src/**/*.cs` | | ||
| | `!pattern` | Exclude matching paths | `!**/bin/**` | | ||
| | `dir/` | Match a directory and its contents | `docs/` | | ||
|
|
||
| > **Note:** `{a,b}` alternation and `[abc]` character classes are not supported by this library. Use multiple patterns separated by `;` instead. | ||
| > For example, instead of `*.{cs,fs}`, use `**/*.cs;**/*.fs`. | ||
|
|
||
| **Examples** | ||
|
|
||
| Require all `.md` files under `doc/`: | ||
| ``` | ||
| slnx-validator MySolution.slnx --required-files "doc/*.md" | ||
| ``` | ||
|
|
||
| Require all `.cs` files under `src/`, excluding the `bin` and `obj` folders: | ||
| ``` | ||
| slnx-validator MySolution.slnx --required-files "src/**/*.cs;!**/bin/**;!**/obj/**" | ||
| ``` | ||
|
|
||
| Require a specific config file and the entire `docs/` directory: | ||
| ``` | ||
| slnx-validator MySolution.slnx --required-files "appsettings.json;docs/" | ||
| ``` | ||
|
|
||
| **Exit codes** | ||
|
|
||
| | 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). | | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 1 should ne any validation error
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in ec1f478. Changed to: |
||
|
|
||
| ## SonarQube integration example | ||
|
|
||
| ```powershell | ||
|
|
@@ -185,6 +238,8 @@ 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. | | ||
| | `SLNX021` | `RequiredFileNotReferencedInSolution` | A `--required-files` matched file exists on disk but is not referenced as a `<File>` element in the solution. | | ||
|
|
||
| ## XSD Schema | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| using JulianVerdurmen.SlnxValidator.Core.ValidationResults; | ||
|
|
||
| namespace JulianVerdurmen.SlnxValidator.Core.Validation; | ||
|
|
||
| public interface IRequiredFilesChecker | ||
| { | ||
| /// <summary> | ||
| /// Resolves semicolon-separated glob patterns against <paramref name="rootDirectory"/> | ||
| /// and returns the matched absolute paths. Returns an empty list when no files match. | ||
| /// </summary> | ||
| IReadOnlyList<string> ResolveMatchedPaths(string patternsRaw, string rootDirectory); | ||
|
|
||
| /// <summary> | ||
| /// Checks which of the <paramref name="requiredAbsolutePaths"/> are NOT referenced as a | ||
| /// <c><File Path="..."></c> element in the given .slnx content. | ||
| /// Paths in the .slnx are resolved relative to <paramref name="slnxDirectory"/>. | ||
| /// Returns a <see cref="ValidationError"/> for each missing file. | ||
| /// </summary> | ||
| IReadOnlyList<ValidationError> CheckInSlnx( | ||
| IReadOnlyList<string> requiredAbsolutePaths, | ||
| string slnxContent, | ||
| string slnxDirectory); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| using System.Xml.Linq; | ||
| using JulianVerdurmen.SlnxValidator.Core.ValidationResults; | ||
| using Microsoft.Extensions.FileSystemGlobbing; | ||
| using Microsoft.Extensions.FileSystemGlobbing.Abstractions; | ||
|
|
||
| namespace JulianVerdurmen.SlnxValidator.Core.Validation; | ||
|
|
||
| internal sealed class RequiredFilesChecker : IRequiredFilesChecker | ||
| { | ||
| /// <inheritdoc /> | ||
| public IReadOnlyList<string> ResolveMatchedPaths(string patternsRaw, string rootDirectory) | ||
| { | ||
| var patterns = patternsRaw.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); | ||
|
|
||
| var matcher = new Matcher(StringComparison.OrdinalIgnoreCase, preserveFilterOrder: true); | ||
|
|
||
| foreach (var pattern in patterns) | ||
| { | ||
| if (pattern.StartsWith('!')) | ||
| matcher.AddExclude(pattern[1..]); | ||
| else | ||
| matcher.AddInclude(pattern); | ||
| } | ||
|
|
||
| var directoryInfo = new DirectoryInfoWrapper(new DirectoryInfo(rootDirectory)); | ||
| var result = matcher.Execute(directoryInfo); | ||
|
|
||
| return result.HasMatches | ||
| ? result.Files.Select(f => Path.GetFullPath(Path.Combine(rootDirectory, f.Path))).ToList() | ||
| : []; | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public IReadOnlyList<ValidationError> CheckInSlnx( | ||
| IReadOnlyList<string> requiredAbsolutePaths, | ||
| string slnxContent, | ||
| string slnxDirectory) | ||
| { | ||
| var slnxFileRefs = new HashSet<string>(StringComparer.OrdinalIgnoreCase); | ||
|
|
||
| try | ||
| { | ||
| var doc = XDocument.Parse(slnxContent); | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Deduplicate the parse of the slnx, create a dto
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in ec1f478. Created |
||
| foreach (var fileElement in doc.Descendants("File")) | ||
| { | ||
| var path = fileElement.Attribute("Path")?.Value; | ||
| if (path is null) | ||
| continue; | ||
|
|
||
| var fullPath = Path.IsPathRooted(path) | ||
| ? Path.GetFullPath(path) | ||
| : Path.GetFullPath(Path.Combine(slnxDirectory, path)); | ||
|
|
||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Deduplicate
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in ec1f478 — both the |
||
| slnxFileRefs.Add(fullPath); | ||
| } | ||
| } | ||
| catch (Exception) | ||
| { | ||
| // Malformed XML is already reported by the XML validator. | ||
| } | ||
|
|
||
| var errors = new List<ValidationError>(); | ||
| foreach (var requiredPath in requiredAbsolutePaths) | ||
| { | ||
| if (!slnxFileRefs.Contains(requiredPath)) | ||
| { | ||
| var relativePath = Path.GetRelativePath(slnxDirectory, requiredPath).Replace('\\', '/'); | ||
| errors.Add(new ValidationError( | ||
| ValidationErrorCode.RequiredFileNotReferencedInSolution, | ||
| $"Required file is not referenced in the solution: {requiredPath}" + | ||
| $" — add: <File Path=\"{relativePath}\" />")); | ||
| } | ||
| } | ||
|
|
||
| return errors; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,9 +4,13 @@ | |
|
|
||
| namespace JulianVerdurmen.SlnxValidator; | ||
|
|
||
| internal sealed class ValidationCollector(IFileSystem fileSystem, ISlnxValidator validator) | ||
| internal sealed class ValidationCollector(IFileSystem fileSystem, ISlnxValidator validator, IRequiredFilesChecker requiredFilesChecker) | ||
| { | ||
| public async Task<IReadOnlyList<FileValidationResult>> CollectAsync(IReadOnlyList<string> files, CancellationToken cancellationToken) | ||
| public async Task<IReadOnlyList<FileValidationResult>> CollectAsync( | ||
| IReadOnlyList<string> files, | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Create options type for the parameters
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in ec1f478. Created |
||
| IReadOnlyList<string>? matchedRequiredPaths, | ||
| string? requiredFilesPattern, | ||
| CancellationToken cancellationToken) | ||
| { | ||
| var results = new List<FileValidationResult>(files.Count); | ||
|
|
||
|
|
@@ -36,11 +40,27 @@ public async Task<IReadOnlyList<FileValidationResult>> CollectAsync(IReadOnlyLis | |
| var directory = Path.GetDirectoryName(file)!; | ||
| var result = await validator.ValidateAsync(content, directory, cancellationToken); | ||
|
|
||
| var allErrors = result.Errors.ToList(); | ||
|
|
||
| if (matchedRequiredPaths is not null) | ||
| { | ||
| if (matchedRequiredPaths.Count == 0) | ||
| { | ||
| allErrors.Add(new ValidationError( | ||
| ValidationErrorCode.RequiredFileDoesntExistOnSystem, | ||
| $"Required file does not exist on the system. No files matched: {requiredFilesPattern}")); | ||
| } | ||
| else | ||
| { | ||
| allErrors.AddRange(requiredFilesChecker.CheckInSlnx(matchedRequiredPaths, content, directory)); | ||
| } | ||
| } | ||
|
|
||
| results.Add(new FileValidationResult | ||
| { | ||
| File = file, | ||
| HasErrors = !result.IsValid, | ||
| Errors = result.Errors, | ||
| HasErrors = allErrors.Count > 0, | ||
| Errors = allErrors, | ||
| }); | ||
| } | ||
|
|
||
|
|
@@ -63,3 +83,4 @@ private static bool IsBinaryFile(string path) | |
| return buffer[..bytesRead].Contains((byte)0); | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,14 @@ | ||
| using JulianVerdurmen.SlnxValidator.Core; | ||
| using JulianVerdurmen.SlnxValidator.Core.FileSystem; | ||
| using JulianVerdurmen.SlnxValidator.Core.SonarQubeReporting; | ||
| using JulianVerdurmen.SlnxValidator.Core.Validation; | ||
|
|
||
| namespace JulianVerdurmen.SlnxValidator; | ||
|
|
||
| internal sealed class ValidatorRunner(ISlnxFileResolver resolver, ValidationCollector collector, ISonarReporter sonarReporter) | ||
| internal sealed class ValidatorRunner(ISlnxFileResolver resolver, ValidationCollector collector, ISonarReporter sonarReporter, IRequiredFilesChecker requiredFilesChecker) | ||
| { | ||
| 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) | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Create options type for the parameters (record)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in ec1f478. Created |
||
| { | ||
| var files = resolver.Resolve(input); | ||
|
|
||
|
|
@@ -16,7 +18,12 @@ public async Task<int> RunAsync(string input, string? sonarqubeReportPath, bool | |
| return continueOnError ? 0 : 1; | ||
| } | ||
|
|
||
| var results = await collector.CollectAsync(files, cancellationToken); | ||
| // Resolve required file glob patterns to absolute disk paths (once for all .slnx files). | ||
| IReadOnlyList<string>? matchedRequiredPaths = null; | ||
| if (requiredFilesPattern is not null) | ||
| matchedRequiredPaths = requiredFilesChecker.ResolveMatchedPaths(requiredFilesPattern, workingDirectory); | ||
|
|
||
| var results = await collector.CollectAsync(files, matchedRequiredPaths, requiredFilesPattern, cancellationToken); | ||
|
|
||
| await ValidationReporter.Report(results); | ||
|
|
||
|
|
||
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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").