Skip to content
Merged
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
11 changes: 10 additions & 1 deletion .github/agents/software-quality-enforcer.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,16 @@ adherence to coding conventions.

### Code Style Standards

Based on `.editorconfig`:
Based on `.editorconfig` and project preferences:

- **Indentation**: 4 spaces for C#, 2 spaces for YAML/JSON/XML
- **Namespaces**: Use file-scoped namespaces (C# 10+)
- **Braces**: Required for all control statements (enforced as warning)
- **Using Directives**: Sort system directives first
- **Encoding**: UTF-8 with BOM
- **Line Endings**: LF with final newline
- **Literate Coding Style**: Each "paragraph" of code should start with a comment explaining what it does, with
paragraphs separated by blank lines. This makes code more readable and self-documenting.

### Naming Conventions

Expand All @@ -77,6 +79,13 @@ Based on `.editorconfig`:
- **Expression-Bodied Members**: Use for properties, indexers, accessors, and lambdas; avoid for methods,
constructors, and operators
- **Unused Parameters**: Trigger warnings
- **Code Analyzers**:
- Microsoft.CodeAnalysis.NetAnalyzers enabled
- SonarAnalyzer.CSharp enabled
- EnforceCodeStyleInBuild enabled
- AnalysisLevel set to latest
- **Analyzer Suppressions**: Use `.globalconfig` file to disable rules globally when needed (e.g., for serializer
DTOs), rather than SuppressMessage attributes

### Test Requirements

Expand Down
8 changes: 8 additions & 0 deletions .globalconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Global analyzer configuration for ReqStream project
is_global = true

# Disable S3459 (unassigned auto-property) - False positive for YAML deserializer DTOs
dotnet_diagnostic.S3459.severity = none

# Disable S1144 (unused private setter) - False positive for YAML deserializer DTOs
dotnet_diagnostic.S1144.severity = none
64 changes: 48 additions & 16 deletions src/DemaConsulting.ReqStream/Context.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,24 +106,31 @@
/// <param name="args">Command-line arguments.</param>
/// <returns>A new Context instance.</returns>
/// <exception cref="ArgumentException">Thrown when arguments are invalid.</exception>
public static Context Create(string[] args)

Check warning on line 109 in src/DemaConsulting.ReqStream/Context.cs

View workflow job for this annotation

GitHub Actions / Build Linux / build

Refactor this method to reduce its Cognitive Complexity from 35 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)

Check warning on line 109 in src/DemaConsulting.ReqStream/Context.cs

View workflow job for this annotation

GitHub Actions / Build Windows / build

Refactor this method to reduce its Cognitive Complexity from 35 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)
{
// Initialize flag variables
var version = false;
var help = false;
var silent = false;
var validate = false;

// Initialize collection variables
var requirementsFiles = new List<string>();
var testFiles = new List<string>();

// Initialize optional parameters
string? requirementsReport = null;
var reportDepth = 1;
string? matrix = null;
var matrixDepth = 1;
string? logFile = null;

// Parse command-line arguments
for (int i = 0; i < args.Length; i++)
int i = 0;
while (i < args.Length)
{
var arg = args[i];
// Get current argument and advance index
var arg = args[i++];

switch (arg)
{
Expand All @@ -147,65 +154,83 @@
break;

case "--log":
if (i + 1 >= args.Length)
// Ensure argument has a value
if (i >= args.Length)
{
throw new ArgumentException($"{arg} requires a filename argument", nameof(args));
}
logFile = args[++i];

logFile = args[i++];
break;

case "--requirements":
if (i + 1 >= args.Length)
// Ensure argument has a value
if (i >= args.Length)
{
throw new ArgumentException($"{arg} requires a pattern argument", nameof(args));
}
requirementsFiles.AddRange(ExpandGlobPattern(args[++i]));

requirementsFiles.AddRange(ExpandGlobPattern(args[i++]));
break;

case "--tests":
if (i + 1 >= args.Length)
// Ensure argument has a value
if (i >= args.Length)
{
throw new ArgumentException($"{arg} requires a pattern argument", nameof(args));
}
testFiles.AddRange(ExpandGlobPattern(args[++i]));

testFiles.AddRange(ExpandGlobPattern(args[i++]));
break;

case "--report":
if (i + 1 >= args.Length)
// Ensure argument has a value
if (i >= args.Length)
{
throw new ArgumentException($"{arg} requires a filename argument", nameof(args));
}
requirementsReport = args[++i];

requirementsReport = args[i++];
break;

case "--report-depth":
if (i + 1 >= args.Length)
// Ensure argument has a value
if (i >= args.Length)
{
throw new ArgumentException($"{arg} requires a depth argument", nameof(args));
}
if (!int.TryParse(args[++i], out reportDepth) || reportDepth < 1)

// Parse and validate depth value
if (!int.TryParse(args[i++], out reportDepth) || reportDepth < 1)
{
throw new ArgumentException($"{arg} requires a positive integer", nameof(args));
}

break;

case "--matrix":
if (i + 1 >= args.Length)
// Ensure argument has a value
if (i >= args.Length)
{
throw new ArgumentException($"{arg} requires a filename argument", nameof(args));
}
matrix = args[++i];

matrix = args[i++];
break;

case "--matrix-depth":
if (i + 1 >= args.Length)
// Ensure argument has a value
if (i >= args.Length)
{
throw new ArgumentException($"{arg} requires a depth argument", nameof(args));
}
if (!int.TryParse(args[++i], out matrixDepth) || matrixDepth < 1)

// Parse and validate depth value
if (!int.TryParse(args[i++], out matrixDepth) || matrixDepth < 1)
{
throw new ArgumentException($"{arg} requires a positive integer", nameof(args));
}

break;

default:
Expand Down Expand Up @@ -251,12 +276,17 @@
/// <returns>A list of matching file paths.</returns>
private static List<string> ExpandGlobPattern(string pattern)
{
// Create a matcher and add the glob pattern
var matcher = new Matcher();
matcher.AddInclude(pattern);

// Get the current directory for matching
var currentDirectory = Directory.GetCurrentDirectory();

// Execute the matcher against the current directory
var result = matcher.Execute(new DirectoryInfoWrapper(new DirectoryInfo(currentDirectory)));

// Return the full paths of matched files
return result.Files.Select(f => Path.Combine(currentDirectory, f.Path)).ToList();
}

Expand All @@ -282,6 +312,7 @@
/// <param name="message">The error message to write.</param>
public void WriteError(string message)
{
// Mark that we have encountered errors
_hasErrors = true;

// Write to error console unless silent mode is enabled
Expand All @@ -302,6 +333,7 @@
/// </summary>
public void Dispose()
{
// Close and dispose the log file writer if it exists
_logWriter?.Dispose();
_logWriter = null;
}
Expand Down
13 changes: 13 additions & 0 deletions src/DemaConsulting.ReqStream/DemaConsulting.ReqStream.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,26 @@
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<ContinuousIntegrationBuild Condition="'$(GITHUB_ACTIONS)' == 'true'">true</ContinuousIntegrationBuild>

<!-- Code Quality Configuration -->
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisLevel>latest</AnalysisLevel>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="DemaConsulting.TestResults" Version="1.2.0" />
<PackageReference Include="Microsoft.Extensions.FileSystemGlobbing" Version="9.0.0" />
<PackageReference Include="YamlDotNet" Version="16.3.0" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.4.0.108396">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
Expand Down
42 changes: 16 additions & 26 deletions src/DemaConsulting.ReqStream/Requirements.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@
/// Reads and processes a YAML file, including any referenced include files.
/// </summary>
/// <param name="path">The path to the YAML file to read.</param>
private void ReadFile(string path)

Check warning on line 137 in src/DemaConsulting.ReqStream/Requirements.cs

View workflow job for this annotation

GitHub Actions / Build Linux / build

Refactor this method to reduce its Cognitive Complexity from 24 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)

Check warning on line 137 in src/DemaConsulting.ReqStream/Requirements.cs

View workflow job for this annotation

GitHub Actions / Build Windows / build

Refactor this method to reduce its Cognitive Complexity from 24 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)
{
// Convert to full path and check if already included to prevent loops
var fullPath = Path.GetFullPath(path);
Expand Down Expand Up @@ -196,24 +196,17 @@
throw new InvalidOperationException($"Mapping requirement ID cannot be blank in file: {fullPath}");
}

// Find the requirement by ID
if (_allRequirements.TryGetValue(mapping.Id, out var requirement))
// Find the requirement by ID and add tests if they exist
if (_allRequirements.TryGetValue(mapping.Id, out var requirement) && mapping.Tests != null)
{
// Add the tests if they exist
if (mapping.Tests != null)
// Validate no test names are blank
if (mapping.Tests.Any(string.IsNullOrWhiteSpace))
{
// Validate each test name is not blank
foreach (var test in mapping.Tests)
{
if (string.IsNullOrWhiteSpace(test))
{
throw new InvalidOperationException(
$"Test name cannot be blank in mapping for requirement '{mapping.Id}' in file: {fullPath}");
}
}

requirement.Tests.AddRange(mapping.Tests);
throw new InvalidOperationException(
$"Test name cannot be blank in mapping for requirement '{mapping.Id}' in file: {fullPath}");
}

requirement.Tests.AddRange(mapping.Tests);
}
}
}
Expand Down Expand Up @@ -287,14 +280,11 @@
// Add any inline tests
if (req.Tests != null)
{
// Validate each test name is not blank
foreach (var test in req.Tests)
// Validate no test names are blank
if (req.Tests.Any(string.IsNullOrWhiteSpace))
{
if (string.IsNullOrWhiteSpace(test))
{
throw new InvalidOperationException(
$"Test name cannot be blank for requirement '{req.Id}' in file: {filePath}");
}
throw new InvalidOperationException(
$"Test name cannot be blank for requirement '{req.Id}' in file: {filePath}");
}

requirement.Tests.AddRange(req.Tests);
Expand Down Expand Up @@ -332,28 +322,28 @@
/// <summary>
/// Internal class for deserializing the YAML document structure.
/// </summary>
private class YamlDocument
private sealed class YamlDocument
{
/// <summary>
/// Gets or sets the sections in the document.
/// </summary>
public List<YamlSection>? Sections { get; set; }

Check warning on line 330 in src/DemaConsulting.ReqStream/Requirements.cs

View workflow job for this annotation

GitHub Actions / Build Linux / build

Remove the unused private set accessor in property 'Sections'. (https://rules.sonarsource.com/csharp/RSPEC-1144)

Check warning on line 330 in src/DemaConsulting.ReqStream/Requirements.cs

View workflow job for this annotation

GitHub Actions / Build Windows / build

Remove the unused private set accessor in property 'Sections'. (https://rules.sonarsource.com/csharp/RSPEC-1144)

/// <summary>
/// Gets or sets the test mappings in the document.
/// </summary>
public List<YamlMapping>? Mappings { get; set; }

Check warning on line 335 in src/DemaConsulting.ReqStream/Requirements.cs

View workflow job for this annotation

GitHub Actions / Build Linux / build

Remove the unused private set accessor in property 'Mappings'. (https://rules.sonarsource.com/csharp/RSPEC-1144)

Check warning on line 335 in src/DemaConsulting.ReqStream/Requirements.cs

View workflow job for this annotation

GitHub Actions / Build Windows / build

Remove the unused private set accessor in property 'Mappings'. (https://rules.sonarsource.com/csharp/RSPEC-1144)

/// <summary>
/// Gets or sets the list of include files.
/// </summary>
public List<string>? Includes { get; set; }

Check warning on line 340 in src/DemaConsulting.ReqStream/Requirements.cs

View workflow job for this annotation

GitHub Actions / Build Linux / build

Remove the unused private set accessor in property 'Includes'. (https://rules.sonarsource.com/csharp/RSPEC-1144)

Check warning on line 340 in src/DemaConsulting.ReqStream/Requirements.cs

View workflow job for this annotation

GitHub Actions / Build Windows / build

Remove the unused private set accessor in property 'Includes'. (https://rules.sonarsource.com/csharp/RSPEC-1144)
}

/// <summary>
/// Internal class for deserializing a YAML section.
/// </summary>
private class YamlSection
private sealed class YamlSection
{
/// <summary>
/// Gets or sets the title of the section.
Expand All @@ -363,18 +353,18 @@
/// <summary>
/// Gets or sets the requirements in this section.
/// </summary>
public List<YamlRequirement>? Requirements { get; set; }

Check warning on line 356 in src/DemaConsulting.ReqStream/Requirements.cs

View workflow job for this annotation

GitHub Actions / Build Linux / build

Remove the unused private set accessor in property 'Requirements'. (https://rules.sonarsource.com/csharp/RSPEC-1144)

Check warning on line 356 in src/DemaConsulting.ReqStream/Requirements.cs

View workflow job for this annotation

GitHub Actions / Build Windows / build

Remove the unused private set accessor in property 'Requirements'. (https://rules.sonarsource.com/csharp/RSPEC-1144)

/// <summary>
/// Gets or sets the child sections.
/// </summary>
public List<YamlSection>? Sections { get; set; }

Check warning on line 361 in src/DemaConsulting.ReqStream/Requirements.cs

View workflow job for this annotation

GitHub Actions / Build Linux / build

Remove the unused private set accessor in property 'Sections'. (https://rules.sonarsource.com/csharp/RSPEC-1144)

Check warning on line 361 in src/DemaConsulting.ReqStream/Requirements.cs

View workflow job for this annotation

GitHub Actions / Build Windows / build

Remove the unused private set accessor in property 'Sections'. (https://rules.sonarsource.com/csharp/RSPEC-1144)
}

/// <summary>
/// Internal class for deserializing a YAML requirement.
/// </summary>
private class YamlRequirement
private sealed class YamlRequirement
{
/// <summary>
/// Gets or sets the requirement ID.
Expand All @@ -389,18 +379,18 @@
/// <summary>
/// Gets or sets the list of tests.
/// </summary>
public List<string>? Tests { get; set; }

Check warning on line 382 in src/DemaConsulting.ReqStream/Requirements.cs

View workflow job for this annotation

GitHub Actions / Build Linux / build

Remove the unused private set accessor in property 'Tests'. (https://rules.sonarsource.com/csharp/RSPEC-1144)

Check warning on line 382 in src/DemaConsulting.ReqStream/Requirements.cs

View workflow job for this annotation

GitHub Actions / Build Windows / build

Remove the unused private set accessor in property 'Tests'. (https://rules.sonarsource.com/csharp/RSPEC-1144)

/// <summary>
/// Gets or sets the list of child requirement IDs.
/// </summary>
public List<string>? Children { get; set; }

Check warning on line 387 in src/DemaConsulting.ReqStream/Requirements.cs

View workflow job for this annotation

GitHub Actions / Build Linux / build

Remove the unused private set accessor in property 'Children'. (https://rules.sonarsource.com/csharp/RSPEC-1144)

Check warning on line 387 in src/DemaConsulting.ReqStream/Requirements.cs

View workflow job for this annotation

GitHub Actions / Build Windows / build

Remove the unused private set accessor in property 'Children'. (https://rules.sonarsource.com/csharp/RSPEC-1144)
}

/// <summary>
/// Internal class for deserializing a YAML test mapping.
/// </summary>
private class YamlMapping
private sealed class YamlMapping
{
/// <summary>
/// Gets or sets the requirement ID for this mapping.
Expand All @@ -410,6 +400,6 @@
/// <summary>
/// Gets or sets the list of tests.
/// </summary>
public List<string>? Tests { get; set; }

Check warning on line 403 in src/DemaConsulting.ReqStream/Requirements.cs

View workflow job for this annotation

GitHub Actions / Build Linux / build

Remove the unused private set accessor in property 'Tests'. (https://rules.sonarsource.com/csharp/RSPEC-1144)

Check warning on line 403 in src/DemaConsulting.ReqStream/Requirements.cs

View workflow job for this annotation

GitHub Actions / Build Windows / build

Remove the unused private set accessor in property 'Tests'. (https://rules.sonarsource.com/csharp/RSPEC-1144)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,26 @@

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>

<!-- Code Quality Configuration -->
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisLevel>latest</AnalysisLevel>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="MSTest.TestAdapter" Version="4.0.2" />
<PackageReference Include="MSTest.TestFramework" Version="4.0.2" />
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.4.0.108396">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
Expand Down