Skip to content

Commit 6e42d47

Browse files
Refactor: reduce reporter duplication via SeverityOverrides, ReportResults, ResolvedRule, and ReporterBase (#64)
* Initial plan * refactor: reduce code duplication in reporters via SeverityOverrides, ReportResults, ResolvedRule, and ReporterBase Agent-Logs-Url: https://github.com/304NotModified/SLNX-validator/sessions/0c79c5be-cc10-434b-83a4-cfc15df24893 Co-authored-by: 304NotModified <5808377+304NotModified@users.noreply.github.com> * fix: rename SeverityOverrides constructor param to camelCase to avoid shadowing type name Agent-Logs-Url: https://github.com/304NotModified/SLNX-validator/sessions/0c79c5be-cc10-434b-83a4-cfc15df24893 Co-authored-by: 304NotModified <5808377+304NotModified@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: 304NotModified <5808377+304NotModified@users.noreply.github.com>
1 parent a99e5aa commit 6e42d47

File tree

16 files changed

+219
-192
lines changed

16 files changed

+219
-192
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using JulianVerdurmen.SlnxValidator.Core.ValidationResults;
2+
3+
namespace JulianVerdurmen.SlnxValidator.Core.Reporting;
4+
5+
public sealed class ReportResults
6+
{
7+
public IReadOnlyList<FileValidationResult> Results { get; }
8+
public SeverityOverrides Overrides { get; }
9+
10+
public ReportResults(IReadOnlyList<FileValidationResult> results, SeverityOverrides? overrides = null)
11+
{
12+
Results = results;
13+
Overrides = overrides ?? SeverityOverrides.Empty;
14+
}
15+
16+
public IReadOnlyList<ValidationErrorCode> UsedCodes => Overrides.GetUsedCodes(Results);
17+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using System.Text.Encodings.Web;
2+
using System.Text.Json;
3+
using System.Text.Json.Serialization;
4+
using JulianVerdurmen.SlnxValidator.Core.FileSystem;
5+
6+
namespace JulianVerdurmen.SlnxValidator.Core.Reporting;
7+
8+
public abstract class ReporterBase(IFileSystem fileSystem)
9+
{
10+
protected static readonly JsonSerializerOptions JsonOptions = new()
11+
{
12+
WriteIndented = true,
13+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
14+
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
15+
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
16+
Converters = { new JsonStringEnumConverter() }
17+
};
18+
19+
public async Task WriteReportAsync(ReportResults results, string outputPath)
20+
{
21+
var directory = Path.GetDirectoryName(outputPath);
22+
if (!string.IsNullOrEmpty(directory))
23+
fileSystem.CreateDirectory(directory);
24+
25+
await using var stream = fileSystem.CreateFile(outputPath);
26+
await WriteReportAsync(results, stream);
27+
}
28+
29+
public abstract Task WriteReportAsync(ReportResults results, Stream outputStream);
30+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace JulianVerdurmen.SlnxValidator.Core.Reporting;
2+
3+
public sealed record ResolvedRule(
4+
string Id,
5+
string Name,
6+
string Description,
7+
RuleSeverity EffectiveSeverity);

src/SLNX-validator.Core/Reporting/RuleProvider.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,10 @@ public static Rule Get(ValidationErrorCode code)
5757
return rule;
5858
throw new ArgumentOutOfRangeException(nameof(code), code, null);
5959
}
60+
61+
public static ResolvedRule Resolve(ValidationErrorCode code, SeverityOverrides overrides)
62+
{
63+
var rule = Get(code);
64+
return new ResolvedRule(rule.Id, rule.Name, rule.Description, overrides.GetEffectiveSeverity(code));
65+
}
6066
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using System.Collections;
2+
using JulianVerdurmen.SlnxValidator.Core.ValidationResults;
3+
4+
namespace JulianVerdurmen.SlnxValidator.Core.Reporting;
5+
6+
public sealed class SeverityOverrides : IEnumerable<KeyValuePair<ValidationErrorCode, RuleSeverity?>>
7+
{
8+
private readonly IReadOnlyDictionary<ValidationErrorCode, RuleSeverity?> _overrides;
9+
10+
public static readonly SeverityOverrides Empty = new(new Dictionary<ValidationErrorCode, RuleSeverity?>());
11+
12+
public int Count => _overrides.Count;
13+
14+
public RuleSeverity? this[ValidationErrorCode code] => _overrides[code];
15+
16+
public SeverityOverrides(IReadOnlyDictionary<ValidationErrorCode, RuleSeverity?> overrides)
17+
{
18+
_overrides = overrides;
19+
}
20+
21+
public bool IsIgnored(ValidationErrorCode code) =>
22+
_overrides.TryGetValue(code, out var severity) && severity is null;
23+
24+
public bool IsVisible(ValidationErrorCode code) =>
25+
!_overrides.TryGetValue(code, out var severity) || severity is not null;
26+
27+
public bool IsFailingError(ValidationErrorCode code)
28+
{
29+
if (_overrides.TryGetValue(code, out var severity))
30+
return severity is RuleSeverity.BLOCKER or RuleSeverity.CRITICAL or RuleSeverity.MAJOR;
31+
return true; // default: all errors are failing
32+
}
33+
34+
public RuleSeverity GetEffectiveSeverity(ValidationErrorCode code)
35+
{
36+
if (_overrides.TryGetValue(code, out var severity) && severity.HasValue)
37+
return severity.Value;
38+
return RuleProvider.Get(code).DefaultSeverity;
39+
}
40+
41+
public IReadOnlyList<ValidationErrorCode> GetUsedCodes(IReadOnlyList<FileValidationResult> results) =>
42+
results
43+
.SelectMany(r => r.Errors)
44+
.Select(e => e.Code)
45+
.Where(c => !IsIgnored(c))
46+
.Distinct()
47+
.OrderBy(c => (int)c)
48+
.ToList();
49+
50+
public IEnumerator<KeyValuePair<ValidationErrorCode, RuleSeverity?>> GetEnumerator() =>
51+
_overrides.GetEnumerator();
52+
53+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
54+
}
Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
using JulianVerdurmen.SlnxValidator.Core.Reporting;
2-
using JulianVerdurmen.SlnxValidator.Core.ValidationResults;
32

43
namespace JulianVerdurmen.SlnxValidator.Core.SarifReporting;
54

65
public interface ISarifReporter
76
{
8-
Task WriteReportAsync(IReadOnlyList<FileValidationResult> results, string outputPath,
9-
IReadOnlyDictionary<ValidationErrorCode, RuleSeverity?>? severityOverrides = null);
7+
Task WriteReportAsync(ReportResults results, string outputPath);
108

11-
Task WriteReportAsync(IReadOnlyList<FileValidationResult> results, Stream outputStream,
12-
IReadOnlyDictionary<ValidationErrorCode, RuleSeverity?>? severityOverrides = null);
9+
Task WriteReportAsync(ReportResults results, Stream outputStream);
1310
}

src/SLNX-validator.Core/SarifReporting/SarifReporter.cs

Lines changed: 15 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,29 @@
1-
using System.Text.Encodings.Web;
21
using System.Text.Json;
3-
using System.Text.Json.Serialization;
42
using JulianVerdurmen.SlnxValidator.Core.FileSystem;
53
using JulianVerdurmen.SlnxValidator.Core.Reporting;
64
using JulianVerdurmen.SlnxValidator.Core.ValidationResults;
75

86
namespace JulianVerdurmen.SlnxValidator.Core.SarifReporting;
97

10-
public sealed class SarifReporter(IFileSystem fileSystem) : ISarifReporter
8+
public sealed class SarifReporter(IFileSystem fileSystem) : ReporterBase(fileSystem), ISarifReporter
119
{
1210
private const string SarifSchema = "https://json.schemastore.org/sarif-2.1.0.json";
1311
private const string SarifVersion = "2.1.0";
1412
private const string ToolName = "slnx-validator";
1513
private const string ToolInformationUri = "https://github.com/304NotModified/SLNX-validator";
1614

17-
private static readonly JsonSerializerOptions JsonOptions = new()
15+
public override async Task WriteReportAsync(ReportResults reportResults, Stream outputStream)
1816
{
19-
WriteIndented = true,
20-
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
21-
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
22-
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
23-
Converters = { new JsonStringEnumConverter() }
24-
};
25-
26-
public async Task WriteReportAsync(IReadOnlyList<FileValidationResult> results, string outputPath,
27-
IReadOnlyDictionary<ValidationErrorCode, RuleSeverity?>? severityOverrides = null)
28-
{
29-
var directory = Path.GetDirectoryName(outputPath);
30-
if (!string.IsNullOrEmpty(directory))
31-
fileSystem.CreateDirectory(directory);
32-
33-
await using var stream = fileSystem.CreateFile(outputPath);
34-
await WriteReportAsync(results, stream, severityOverrides);
35-
}
36-
37-
public async Task WriteReportAsync(IReadOnlyList<FileValidationResult> results, Stream outputStream,
38-
IReadOnlyDictionary<ValidationErrorCode, RuleSeverity?>? severityOverrides = null)
39-
{
40-
var usedCodes = results
41-
.SelectMany(r => r.Errors)
42-
.Select(e => e.Code)
43-
.Where(c => !IsIgnored(c, severityOverrides))
44-
.Distinct()
45-
.OrderBy(c => (int)c)
46-
.ToList();
17+
var usedCodes = reportResults.UsedCodes;
4718

4819
var rules = usedCodes
49-
.Select(c => BuildRule(c, severityOverrides))
20+
.Select(c => BuildRule(c, reportResults.Overrides))
5021
.ToList();
5122

52-
var sarifResults = results
23+
var sarifResults = reportResults.Results
5324
.SelectMany(r => r.Errors
54-
.Where(e => !IsIgnored(e.Code, severityOverrides))
55-
.Select(e => BuildResult(r.File, e, severityOverrides)))
25+
.Where(e => !reportResults.Overrides.IsIgnored(e.Code))
26+
.Select(e => BuildResult(r.File, e, reportResults.Overrides)))
5627
.ToList();
5728

5829
var log = new SarifLog
@@ -80,38 +51,24 @@ public async Task WriteReportAsync(IReadOnlyList<FileValidationResult> results,
8051
await JsonSerializer.SerializeAsync(outputStream, log, JsonOptions);
8152
}
8253

83-
private static bool IsIgnored(ValidationErrorCode code, IReadOnlyDictionary<ValidationErrorCode, RuleSeverity?>? overrides) =>
84-
overrides is not null && overrides.TryGetValue(code, out var severity) && severity is null;
85-
86-
private static RuleSeverity GetEffectiveSeverity(ValidationErrorCode code,
87-
IReadOnlyDictionary<ValidationErrorCode, RuleSeverity?>? overrides)
88-
{
89-
if (overrides is not null && overrides.TryGetValue(code, out var severity) && severity.HasValue)
90-
return severity.Value;
91-
return RuleProvider.Get(code).DefaultSeverity;
92-
}
93-
94-
private static SarifReportingDescriptor BuildRule(ValidationErrorCode code,
95-
IReadOnlyDictionary<ValidationErrorCode, RuleSeverity?>? overrides)
54+
private static SarifReportingDescriptor BuildRule(ValidationErrorCode code, SeverityOverrides overrides)
9655
{
97-
var meta = RuleProvider.Get(code);
98-
var effectiveSeverity = GetEffectiveSeverity(code, overrides);
56+
var resolved = RuleProvider.Resolve(code, overrides);
9957
return new SarifReportingDescriptor
10058
{
101-
Id = meta.Id,
102-
ShortDescription = new SarifMessage { Text = meta.Name },
103-
FullDescription = new SarifMessage { Text = meta.Description },
59+
Id = resolved.Id,
60+
ShortDescription = new SarifMessage { Text = resolved.Name },
61+
FullDescription = new SarifMessage { Text = resolved.Description },
10462
DefaultConfiguration = new SarifDefaultConfiguration
10563
{
106-
Level = MapToSarifLevel(effectiveSeverity)
64+
Level = MapToSarifLevel(resolved.EffectiveSeverity)
10765
}
10866
};
10967
}
11068

111-
private static SarifResult BuildResult(string filePath, ValidationError error,
112-
IReadOnlyDictionary<ValidationErrorCode, RuleSeverity?>? overrides)
69+
private static SarifResult BuildResult(string filePath, ValidationError error, SeverityOverrides overrides)
11370
{
114-
var effectiveSeverity = GetEffectiveSeverity(error.Code, overrides);
71+
var effectiveSeverity = overrides.GetEffectiveSeverity(error.Code);
11572
return new SarifResult
11673
{
11774
RuleId = error.Code.ToCode(),
Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
using JulianVerdurmen.SlnxValidator.Core.Reporting;
2-
using JulianVerdurmen.SlnxValidator.Core.ValidationResults;
32

43
namespace JulianVerdurmen.SlnxValidator.Core.SonarQubeReporting;
54

65
public interface ISonarReporter
76
{
8-
Task WriteReportAsync(IReadOnlyList<FileValidationResult> results, string outputPath,
9-
IReadOnlyDictionary<ValidationErrorCode, RuleSeverity?>? severityOverrides = null);
7+
Task WriteReportAsync(ReportResults results, string outputPath);
108

11-
Task WriteReportAsync(IReadOnlyList<FileValidationResult> results, Stream outputStream,
12-
IReadOnlyDictionary<ValidationErrorCode, RuleSeverity?>? severityOverrides = null);
13-
}
9+
Task WriteReportAsync(ReportResults results, Stream outputStream);
10+
}
Lines changed: 11 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,21 @@
1-
using System.Text.Encodings.Web;
21
using System.Text.Json;
3-
using System.Text.Json.Serialization;
42
using JulianVerdurmen.SlnxValidator.Core.FileSystem;
53
using JulianVerdurmen.SlnxValidator.Core.Reporting;
64
using JulianVerdurmen.SlnxValidator.Core.ValidationResults;
75

86
namespace JulianVerdurmen.SlnxValidator.Core.SonarQubeReporting;
97

10-
public sealed class SonarReporter(IFileSystem fileSystem) : ISonarReporter
8+
public sealed class SonarReporter(IFileSystem fileSystem) : ReporterBase(fileSystem), ISonarReporter
119
{
12-
private static readonly JsonSerializerOptions JsonOptions = new()
10+
public override async Task WriteReportAsync(ReportResults reportResults, Stream outputStream)
1311
{
14-
WriteIndented = true,
15-
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
16-
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
17-
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
18-
Converters = { new JsonStringEnumConverter() }
19-
};
20-
21-
public async Task WriteReportAsync(IReadOnlyList<FileValidationResult> results, string outputPath,
22-
IReadOnlyDictionary<ValidationErrorCode, RuleSeverity?>? severityOverrides = null)
23-
{
24-
var directory = Path.GetDirectoryName(outputPath);
25-
if (!string.IsNullOrEmpty(directory))
26-
fileSystem.CreateDirectory(directory);
27-
28-
await using var stream = fileSystem.CreateFile(outputPath);
29-
await WriteReportAsync(results, stream, severityOverrides);
30-
}
31-
32-
public async Task WriteReportAsync(IReadOnlyList<FileValidationResult> results, Stream outputStream,
33-
IReadOnlyDictionary<ValidationErrorCode, RuleSeverity?>? severityOverrides = null)
34-
{
35-
var usedCodes = results
36-
.SelectMany(r => r.Errors)
37-
.Select(e => e.Code)
38-
.Where(c => !IsIgnored(c, severityOverrides))
39-
.Distinct()
40-
.OrderBy(c => (int)c)
41-
.ToList();
12+
var usedCodes = reportResults.UsedCodes;
4213

43-
var rules = usedCodes.Select(c => ApplyOverride(GetRuleDefinition(c), c, severityOverrides)).ToList();
14+
var rules = usedCodes.Select(c => BuildRule(c, reportResults.Overrides)).ToList();
4415

45-
var issues = results
16+
var issues = reportResults.Results
4617
.SelectMany(r => r.Errors
47-
.Where(e => !IsIgnored(e.Code, severityOverrides))
18+
.Where(e => !reportResults.Overrides.IsIgnored(e.Code))
4819
.Select(e => BuildIssue(r.File, e)))
4920
.ToList();
5021

@@ -64,22 +35,11 @@ public async Task WriteReportAsync(IReadOnlyList<FileValidationResult> results,
6435
}
6536
};
6637

67-
private static bool IsIgnored(ValidationErrorCode code, IReadOnlyDictionary<ValidationErrorCode, RuleSeverity?>? overrides) =>
68-
overrides is not null && overrides.TryGetValue(code, out var severity) && severity is null;
69-
70-
private static SonarRule ApplyOverride(SonarRule rule, ValidationErrorCode code,
71-
IReadOnlyDictionary<ValidationErrorCode, RuleSeverity?>? overrides)
72-
{
73-
if (overrides is not null && overrides.TryGetValue(code, out var severity) && severity.HasValue)
74-
return rule with { Severity = severity.Value };
75-
return rule;
76-
}
77-
78-
private static SonarRule GetRuleDefinition(ValidationErrorCode code)
38+
private static SonarRule BuildRule(ValidationErrorCode code, SeverityOverrides overrides)
7939
{
80-
var meta = RuleProvider.Get(code);
81-
return CreateRule(code, meta.Name, meta.Description, GetSonarRuleType(code),
82-
meta.DefaultSeverity, GetCleanCodeAttribute(code), GetImpactSeverity(code));
40+
var resolved = RuleProvider.Resolve(code, overrides);
41+
return CreateRule(code, resolved.Name, resolved.Description, GetSonarRuleType(code),
42+
resolved.EffectiveSeverity, GetCleanCodeAttribute(code), GetImpactSeverity(code));
8343
}
8444

8545
private static SonarRuleType GetSonarRuleType(ValidationErrorCode code) => SonarRuleType.BUG;
@@ -104,4 +64,4 @@ private static SonarRule CreateRule(ValidationErrorCode code, string name, strin
10464
Severity = severity,
10565
Impacts = [new SonarImpact { SoftwareQuality = SonarSoftwareQuality.MAINTAINABILITY, Severity = impactSeverity }]
10666
};
107-
}
67+
}

src/SLNX-validator/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ public static async Task<int> Main(string[] args)
9292
ContinueOnError: parseResult.GetValue(continueOnErrorOption),
9393
RequiredFilesPattern: parseResult.GetValue(requiredFilesOption),
9494
WorkingDirectory: Environment.CurrentDirectory,
95-
SeverityOverrides: SeverityOverridesParser.Parse(
95+
severityOverrides: SeverityOverridesParser.Parse(
9696
parseResult.GetValue(blockerOption),
9797
parseResult.GetValue(criticalOption),
9898
parseResult.GetValue(majorOption),

0 commit comments

Comments
 (0)