diff --git a/src/Agoda.CodeCompass.MSBuild.Tests/Agoda.CodeCompass.MSBuild.Tests.csproj b/src/Agoda.CodeCompass.MSBuild.Tests/Agoda.CodeCompass.MSBuild.Tests.csproj new file mode 100644 index 0000000..7840aaf --- /dev/null +++ b/src/Agoda.CodeCompass.MSBuild.Tests/Agoda.CodeCompass.MSBuild.Tests.csproj @@ -0,0 +1,37 @@ + + + + net8.0 + enable + enable + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + + + + + + + + + + + + + + + diff --git a/src/Agoda.CodeCompass.MSBuild.Tests/SarifConversionTests.cs b/src/Agoda.CodeCompass.MSBuild.Tests/SarifConversionTests.cs new file mode 100644 index 0000000..5bf84f5 --- /dev/null +++ b/src/Agoda.CodeCompass.MSBuild.Tests/SarifConversionTests.cs @@ -0,0 +1,152 @@ +using System.Text.Json; +using NUnit.Framework; +using NUnit.Framework.Internal; +using Shouldly; + +namespace Agoda.CodeCompass.MSBuild.Tests; + +[TestFixture] +public class SarifConversionTests +{ + private readonly string _writeSarifPath = "TestData/write.sarif"; + private readonly string _sampleSarifPath = "TestData/sample.sarif"; + + [Test] + public async Task ConvertSarif_WithValidInput_ShouldAddTechDebtProperties() + { + var outfile = "TestData/" + Guid.NewGuid().ToString(); + // Arrange + var task = new TechDebtSarifTask + { + InputPath = _sampleSarifPath, + OutputPath = outfile + }; + + // Act + var result = task.Execute(); + + // Assert + result.ShouldBeTrue(); + + var outputJson = await File.ReadAllTextAsync(outfile); + var output = JsonSerializer.Deserialize(outputJson, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); + + output.ShouldNotBeNull(); + output.Runs.ShouldNotBeEmpty(); + output.Runs[0].Results.ShouldNotBeEmpty(); + + var firstResult = output.Runs[0].Results[0]; + firstResult.Properties.ShouldNotBeNull(); + firstResult.Properties.TechDebt.ShouldNotBeNull(); + firstResult.Properties.TechDebt.Minutes.ShouldBeGreaterThan(0); + firstResult.Properties.TechDebt.Category.ShouldNotBeNullOrWhiteSpace(); + firstResult.Properties.TechDebt.Priority.ShouldNotBeNullOrWhiteSpace(); + } + + [Test] + public void ConvertSarif_WithInvalidPath_ShouldReturnFalse() + { + var task = new TechDebtSarifTask + { + InputPath = "TestData/invalid.sarif", + OutputPath = Guid.NewGuid().ToString() + }; + + var result = task.Execute(); + + result.ShouldBeFalse(); + } + + [Test] + public async Task ConvertSarif_WithMultipleRules_ShouldPreserveRuleMetadata() + { + // Arrange + var sarif = new SarifReport + { + Runs = new[] + { + new Run + { + Results = new List + { + CreateSampleResult("CS8602"), + CreateSampleResult("CA1822") + } + } + } + }; + + await File.WriteAllTextAsync(_writeSarifPath, + JsonSerializer.Serialize(sarif, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })); + var outfile = "TestData/" + Guid.NewGuid().ToString(); + var task = new TechDebtSarifTask + { + InputPath = _writeSarifPath, + OutputPath = outfile + }; + + // Act + task.Execute(); + + // Assert + var outputJson = await File.ReadAllTextAsync(outfile); + var output = JsonSerializer.Deserialize(outputJson, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); + + output.ShouldNotBeNull(); + output.Runs[0].Results.Count.ShouldBe(2); + + var results = output.Runs[0].Results; + results[0].RuleId.ShouldBe("CS8602"); + results[1].RuleId.ShouldBe("CA1822"); + + results[0].Properties.TechDebt.Category.ShouldBe("NullableReference"); + results[1].Properties.TechDebt.Category.ShouldBe("Performance"); + } + + private static Result CreateSampleResult(string ruleId) => new() + { + RuleId = ruleId, + Message = new Message { Text = $"Sample message for {ruleId}" }, + Locations = new[] + { + new Location + { + PhysicalLocation = new PhysicalLocation + { + ArtifactLocation = new ArtifactLocation + { + Uri = "Test.cs" + }, + Region = new Region + { + StartLine = 1, + StartColumn = 1, + EndLine = 1, + EndColumn = 1 + } + } + } + } + }; + + [OneTimeSetUp] + public void Setup() + { + string[] lines = { "public class myClass {", "// comments", "}" }; + + using (StreamWriter outputFile = new StreamWriter("Test.cs")) + { + foreach (string line in lines) + outputFile.WriteLine(line); + } + } + + [OneTimeTearDown] + public void Cleanup() + { + if (Directory.Exists("TestData")) + { + Directory.Delete("TestData", true); + } + } +} \ No newline at end of file diff --git a/src/Agoda.CodeCompass.MSBuild.Tests/TestData/invalid.sarif b/src/Agoda.CodeCompass.MSBuild.Tests/TestData/invalid.sarif new file mode 100644 index 0000000..6c544a5 --- /dev/null +++ b/src/Agoda.CodeCompass.MSBuild.Tests/TestData/invalid.sarif @@ -0,0 +1,12 @@ +{ + "version": "2.1.0", + "runs": [ + { + "results": [ + { + "invalid": "data" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/Agoda.CodeCompass.MSBuild.Tests/TestData/sample.sarif b/src/Agoda.CodeCompass.MSBuild.Tests/TestData/sample.sarif new file mode 100644 index 0000000..c319fdc --- /dev/null +++ b/src/Agoda.CodeCompass.MSBuild.Tests/TestData/sample.sarif @@ -0,0 +1,72 @@ +{ + "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "Microsoft.CodeAnalysis.CSharp", + "version": "4.8.0", + "rules": [ + { + "id": "CS8602", + "shortDescription": { + "text": "Dereference of a possibly null reference" + } + }, + { + "id": "CA1822", + "shortDescription": { + "text": "Member could be marked as static" + } + } + ] + } + }, + "results": [ + { + "ruleId": "CS8602", + "message": { + "text": "Possible dereference of null reference 'user'" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "Test.cs" + }, + "region": { + "startLine": 10, + "startColumn": 13, + "endLine": 10, + "endColumn": 24 + } + } + } + ] + }, + { + "ruleId": "CA1822", + "message": { + "text": "Member DoSomething does not access instance data and can be marked as static" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "Test.cs" + }, + "region": { + "startLine": 15, + "startColumn": 17, + "endLine": 15, + "endColumn": 28 + } + } + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/src/Agoda.CodeCompass.MSBuild/Agoda.CodeCompass.MSBuild.csproj b/src/Agoda.CodeCompass.MSBuild/Agoda.CodeCompass.MSBuild.csproj index c80eae6..46900f9 100644 --- a/src/Agoda.CodeCompass.MSBuild/Agoda.CodeCompass.MSBuild.csproj +++ b/src/Agoda.CodeCompass.MSBuild/Agoda.CodeCompass.MSBuild.csproj @@ -7,7 +7,15 @@ true true - + + Joel Dickson + Apache-2.0 + https://github.com/agoda-com/code-compass-dotnet + AgodaAnalyzersAgoji.png + https://github.com/agoda-com/code-compass-dotnet + false + CodeCompass is a .NET analyzer that helps you navigate the treacherous waters of technical debt. It analyzes your code and produces standardized SARIF reports that quantify technical debt in terms of estimated remediation time, categorization, and priority. + @@ -15,11 +23,17 @@ - + + - - + + + + + + PreserveNewest + \ No newline at end of file diff --git a/src/Agoda.CodeCompass.MSBuild/AgodaAnalyzersAgoji.png b/src/Agoda.CodeCompass.MSBuild/AgodaAnalyzersAgoji.png new file mode 100644 index 0000000..b599ff6 Binary files /dev/null and b/src/Agoda.CodeCompass.MSBuild/AgodaAnalyzersAgoji.png differ diff --git a/src/Agoda.CodeCompass.MSBuild/ArtifactLocation.cs b/src/Agoda.CodeCompass.MSBuild/ArtifactLocation.cs new file mode 100644 index 0000000..2ca0d0c --- /dev/null +++ b/src/Agoda.CodeCompass.MSBuild/ArtifactLocation.cs @@ -0,0 +1,7 @@ +namespace Agoda.CodeCompass.MSBuild; + +public class ArtifactLocation +{ + public string Uri { get; set; } = string.Empty; + public string UriBaseId { get; set; } = "%SRCROOT%"; +} \ No newline at end of file diff --git a/src/Agoda.CodeCompass.MSBuild/Location.cs b/src/Agoda.CodeCompass.MSBuild/Location.cs new file mode 100644 index 0000000..609057a --- /dev/null +++ b/src/Agoda.CodeCompass.MSBuild/Location.cs @@ -0,0 +1,6 @@ +namespace Agoda.CodeCompass.MSBuild; + +public class Location +{ + public PhysicalLocation PhysicalLocation { get; set; } = new(); +} \ No newline at end of file diff --git a/src/Agoda.CodeCompass.MSBuild/Message.cs b/src/Agoda.CodeCompass.MSBuild/Message.cs new file mode 100644 index 0000000..6b8e8dd --- /dev/null +++ b/src/Agoda.CodeCompass.MSBuild/Message.cs @@ -0,0 +1,6 @@ +namespace Agoda.CodeCompass.MSBuild; + +public class Message +{ + public string Text { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/src/Agoda.CodeCompass.MSBuild/PhysicalLocation.cs b/src/Agoda.CodeCompass.MSBuild/PhysicalLocation.cs new file mode 100644 index 0000000..151ba38 --- /dev/null +++ b/src/Agoda.CodeCompass.MSBuild/PhysicalLocation.cs @@ -0,0 +1,7 @@ +namespace Agoda.CodeCompass.MSBuild; + +public class PhysicalLocation +{ + public ArtifactLocation ArtifactLocation { get; set; } = new(); + public Region Region { get; set; } = new(); +} \ No newline at end of file diff --git a/src/Agoda.CodeCompass.MSBuild/Region.cs b/src/Agoda.CodeCompass.MSBuild/Region.cs new file mode 100644 index 0000000..a18fabc --- /dev/null +++ b/src/Agoda.CodeCompass.MSBuild/Region.cs @@ -0,0 +1,9 @@ +namespace Agoda.CodeCompass.MSBuild; + +public class Region +{ + public int StartLine { get; set; } + public int StartColumn { get; set; } + public int EndLine { get; set; } + public int EndColumn { get; set; } +} \ No newline at end of file diff --git a/src/Agoda.CodeCompass.MSBuild/Result.cs b/src/Agoda.CodeCompass.MSBuild/Result.cs new file mode 100644 index 0000000..516fce0 --- /dev/null +++ b/src/Agoda.CodeCompass.MSBuild/Result.cs @@ -0,0 +1,9 @@ +namespace Agoda.CodeCompass.MSBuild; + +public class Result +{ + public string RuleId { get; set; } = string.Empty; + public Message Message { get; set; } = new(); + public Location[] Locations { get; set; } = Array.Empty(); + public TechDebtProperties Properties { get; set; } = new(); +} \ No newline at end of file diff --git a/src/Agoda.CodeCompass.MSBuild/Rule.cs b/src/Agoda.CodeCompass.MSBuild/Rule.cs new file mode 100644 index 0000000..a6f2c19 --- /dev/null +++ b/src/Agoda.CodeCompass.MSBuild/Rule.cs @@ -0,0 +1,11 @@ +using Agoda.CodeCompass.MSBuild; + +public class Rule +{ + public string Id { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public Message ShortDescription { get; set; } = new(); + public Message FullDescription { get; set; } = new(); + public Message Help { get; set; } = new(); + public TechDebtProperties Properties { get; set; } = new(); +} \ No newline at end of file diff --git a/src/Agoda.CodeCompass.MSBuild/Run.cs b/src/Agoda.CodeCompass.MSBuild/Run.cs new file mode 100644 index 0000000..246c8a6 --- /dev/null +++ b/src/Agoda.CodeCompass.MSBuild/Run.cs @@ -0,0 +1,7 @@ +using Agoda.CodeCompass.MSBuild; + +public class Run +{ + public Tool Tool { get; set; } = new(); + public List Results { get; set; } = new List(); +} \ No newline at end of file diff --git a/src/Agoda.CodeCompass.MSBuild/SarifReport.cs b/src/Agoda.CodeCompass.MSBuild/SarifReport.cs new file mode 100644 index 0000000..2448129 --- /dev/null +++ b/src/Agoda.CodeCompass.MSBuild/SarifReport.cs @@ -0,0 +1,8 @@ +namespace Agoda.CodeCompass.MSBuild; + +public class SarifReport +{ + public string Schema { get; set; } = "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json"; + public string Version { get; set; } = "2.1.0"; + public Run[] Runs { get; set; } = Array.Empty(); +} \ No newline at end of file diff --git a/src/Agoda.CodeCompass.MSBuild/SarifReporter.cs b/src/Agoda.CodeCompass.MSBuild/SarifReporter.cs new file mode 100644 index 0000000..46c2e73 --- /dev/null +++ b/src/Agoda.CodeCompass.MSBuild/SarifReporter.cs @@ -0,0 +1,81 @@ +using System.Text.Json; +using Agoda.CodeCompass.Data; +using Microsoft.CodeAnalysis; + +namespace Agoda.CodeCompass.MSBuild; + +public class SarifReporter +{ + public static string GenerateSarifReport(IEnumerable diagnostics) + { + var report = new SarifReport + { + Runs = new[] + { + new Run + { + Tool = new Tool + { + Driver = new ToolDriver + { + Rules = diagnostics + .Select(d => d.Descriptor) + .Distinct() + .Select(d => new Rule + { + Id = d.Id, + Name = d.Title.ToString(), + ShortDescription = new Message { Text = d.Title.ToString() }, + FullDescription = new Message { Text = d.Description.ToString() }, + Help = new Message { Text = d.Description.ToString() }, + Properties = GetTechDebtProperties(d.Id) + }) + .ToArray() + } + }, + Results = diagnostics.Select(d => new Result + { + RuleId = d.Id, + Message = new Message { Text = d.GetMessage() }, + Locations = new[] + { + new Location + { + PhysicalLocation = new PhysicalLocation + { + ArtifactLocation = new ArtifactLocation + { + Uri = d.Location.GetLineSpan().Path + }, + Region = new Region + { + StartLine = d.Location.GetLineSpan().StartLinePosition.Line + 1, + StartColumn = d.Location.GetLineSpan().StartLinePosition.Character + 1, + EndLine = d.Location.GetLineSpan().EndLinePosition.Line + 1, + EndColumn = d.Location.GetLineSpan().EndLinePosition.Character + 1 + } + } + } + }, + Properties = GetTechDebtProperties(d.Id) + }).ToList() + } + } + }; + + return JsonSerializer.Serialize(report, new JsonSerializerOptions + { + WriteIndented = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + } + + private static TechDebtProperties GetTechDebtProperties(string ruleId) + { + var techDebtInfo = TechDebtMetadata.GetTechDebtInfo(ruleId); + return new TechDebtProperties + { + TechDebt = techDebtInfo + }; + } +} \ No newline at end of file diff --git a/src/Agoda.CodeCompass.MSBuild/TechDebtInfo.cs b/src/Agoda.CodeCompass.MSBuild/TechDebtInfo.cs new file mode 100644 index 0000000..f38b27d --- /dev/null +++ b/src/Agoda.CodeCompass.MSBuild/TechDebtInfo.cs @@ -0,0 +1,10 @@ +namespace Agoda.CodeCompass.Models; + +public class TechDebtInfo +{ + public required int Minutes { get; init; } + public required string Category { get; init; } + public required string Priority { get; init; } + public string? Rationale { get; init; } + public string? Recommendation { get; init; } +} \ No newline at end of file diff --git a/src/Agoda.CodeCompass.MSBuild/TechDebtMetadata.cs b/src/Agoda.CodeCompass.MSBuild/TechDebtMetadata.cs new file mode 100644 index 0000000..00dc190 --- /dev/null +++ b/src/Agoda.CodeCompass.MSBuild/TechDebtMetadata.cs @@ -0,0 +1,315 @@ +using System.Collections.Immutable; +using Agoda.CodeCompass.Models; + +namespace Agoda.CodeCompass.Data; + +public static class TechDebtMetadata +{ + private static readonly Dictionary RuleMetadata = new() + { + // Agoda Rules + ["AG0002"] = new TechDebtInfo { Minutes = 15, Category = "AgodaSpecific", Priority = "Medium", Rationale = "Agoda-specific implementation issue", Recommendation = "Follow Agoda's implementation guidelines" }, + ["AG0003"] = new TechDebtInfo { Minutes = 20, Category = "AgodaSpecific", Priority = "Medium", Rationale = "Agoda-specific implementation issue", Recommendation = "Follow Agoda's implementation guidelines" }, + ["AG0005"] = new TechDebtInfo { Minutes = 25, Category = "AgodaSpecific", Priority = "High", Rationale = "Agoda-specific architecture violation", Recommendation = "Restructure according to architecture guidelines" }, + ["AG0009"] = new TechDebtInfo { Minutes = 15, Category = "AgodaSpecific", Priority = "Medium", Rationale = "Agoda-specific naming violation", Recommendation = "Follow naming conventions" }, + ["AG0010"] = new TechDebtInfo { Minutes = 20, Category = "AgodaSpecific", Priority = "Medium", Rationale = "Agoda-specific implementation issue", Recommendation = "Follow implementation guidelines" }, + ["AG0011"] = new TechDebtInfo { Minutes = 15, Category = "AgodaSpecific", Priority = "Medium", Rationale = "Agoda-specific implementation issue", Recommendation = "Follow implementation guidelines" }, + ["AG0012"] = new TechDebtInfo { Minutes = 20, Category = "AgodaSpecific", Priority = "Medium", Rationale = "Agoda-specific naming violation", Recommendation = "Follow naming conventions" }, + ["AG0013"] = new TechDebtInfo { Minutes = 25, Category = "AgodaSpecific", Priority = "High", Rationale = "Agoda-specific architecture violation", Recommendation = "Restructure according to architecture guidelines" }, + ["AG0018"] = new TechDebtInfo { Minutes = 20, Category = "AgodaSpecific", Priority = "High", Rationale = "Agoda-specific implementation issue", Recommendation = "Follow implementation guidelines" }, + ["AG0019"] = new TechDebtInfo { Minutes = 15, Category = "AgodaSpecific", Priority = "Medium", Rationale = "Agoda-specific implementation issue", Recommendation = "Follow implementation guidelines" }, + ["AG0020"] = new TechDebtInfo { Minutes = 20, Category = "AgodaSpecific", Priority = "Medium", Rationale = "Agoda-specific implementation issue", Recommendation = "Follow implementation guidelines" }, + ["AG0021"] = new TechDebtInfo { Minutes = 25, Category = "AgodaSpecific", Priority = "High", Rationale = "Agoda-specific architecture violation", Recommendation = "Restructure according to architecture guidelines" }, + ["AG0022"] = new TechDebtInfo { Minutes = 20, Category = "AgodaSpecific", Priority = "Medium", Rationale = "Agoda-specific implementation issue", Recommendation = "Follow implementation guidelines" }, + ["AG0023"] = new TechDebtInfo { Minutes = 15, Category = "AgodaSpecific", Priority = "Medium", Rationale = "Agoda-specific implementation issue", Recommendation = "Follow implementation guidelines" }, + ["AG0024"] = new TechDebtInfo { Minutes = 20, Category = "AgodaSpecific", Priority = "Medium", Rationale = "Agoda-specific implementation issue", Recommendation = "Follow implementation guidelines" }, + ["AG0026"] = new TechDebtInfo { Minutes = 25, Category = "AgodaSpecific", Priority = "High", Rationale = "Agoda-specific architecture violation", Recommendation = "Restructure according to architecture guidelines" }, + ["AG0032"] = new TechDebtInfo { Minutes = 20, Category = "AgodaSpecific", Priority = "Medium", Rationale = "Agoda-specific implementation issue", Recommendation = "Follow implementation guidelines" }, + ["AG0033"] = new TechDebtInfo { Minutes = 15, Category = "AgodaSpecific", Priority = "Medium", Rationale = "Agoda-specific implementation issue", Recommendation = "Follow implementation guidelines" }, + ["AG0035"] = new TechDebtInfo { Minutes = 20, Category = "AgodaSpecific", Priority = "Medium", Rationale = "Agoda-specific implementation issue", Recommendation = "Follow implementation guidelines" }, + ["AG0038"] = new TechDebtInfo { Minutes = 25, Category = "AgodaSpecific", Priority = "High", Rationale = "Agoda-specific architecture violation", Recommendation = "Restructure according to architecture guidelines" }, + ["AG0041"] = new TechDebtInfo { Minutes = 20, Category = "AgodaSpecific", Priority = "Medium", Rationale = "Agoda-specific implementation issue", Recommendation = "Follow implementation guidelines" }, + ["AG0042"] = new TechDebtInfo { Minutes = 15, Category = "AgodaSpecific", Priority = "Medium", Rationale = "Agoda-specific implementation issue", Recommendation = "Follow implementation guidelines" }, + + // Compiler Diagnostics + ["CS0105"] = new TechDebtInfo { Minutes = 5, Category = "Compiler", Priority = "Low", Rationale = "Duplicate using directive", Recommendation = "Remove duplicate using directive" }, + ["CS0108"] = new TechDebtInfo { Minutes = 15, Category = "Compiler", Priority = "Medium", Rationale = "Missing new keyword on hiding member", Recommendation = "Add new keyword or rename member" }, + ["CS0109"] = new TechDebtInfo { Minutes = 10, Category = "Compiler", Priority = "Low", Rationale = "Unnecessary new keyword", Recommendation = "Remove unnecessary new keyword" }, + ["CS0162"] = new TechDebtInfo { Minutes = 10, Category = "Compiler", Priority = "Low", Rationale = "Unreachable code detected", Recommendation = "Remove or fix unreachable code" }, + ["CS0164"] = new TechDebtInfo { Minutes = 5, Category = "Compiler", Priority = "Low", Rationale = "Label not referenced", Recommendation = "Remove unused label" }, + ["CS0168"] = new TechDebtInfo { Minutes = 5, Category = "Compiler", Priority = "Low", Rationale = "Variable declared but not used", Recommendation = "Remove unused variable" }, + ["CS0169"] = new TechDebtInfo { Minutes = 5, Category = "Compiler", Priority = "Low", Rationale = "Field never used", Recommendation = "Remove unused field" }, + ["CS0219"] = new TechDebtInfo { Minutes = 5, Category = "Compiler", Priority = "Low", Rationale = "Variable assigned but not used", Recommendation = "Remove unused variable assignment" }, + ["CS0414"] = new TechDebtInfo { Minutes = 5, Category = "Compiler", Priority = "Low", Rationale = "Field assigned but not used", Recommendation = "Remove unused field assignment" }, + ["CS0472"] = new TechDebtInfo { Minutes = 15, Category = "Compiler", Priority = "Medium", Rationale = "Result of expression always same", Recommendation = "Fix or simplify expression" }, + ["CS0612"] = new TechDebtInfo { Minutes = 20, Category = "Compiler", Priority = "High", Rationale = "Obsolete type or member usage", Recommendation = "Update to non-obsolete alternative" }, + ["CS0618"] = new TechDebtInfo { Minutes = 20, Category = "Compiler", Priority = "High", Rationale = "Obsolete type or member usage", Recommendation = "Update to non-obsolete alternative" }, + ["CS0628"] = new TechDebtInfo { Minutes = 15, Category = "Compiler", Priority = "Medium", Rationale = "New protected member in sealed class", Recommendation = "Change protection level or unseal class" }, + ["CS0649"] = new TechDebtInfo { Minutes = 10, Category = "Compiler", Priority = "Low", Rationale = "Field never assigned", Recommendation = "Initialize field or remove if unused" }, + ["CS0659"] = new TechDebtInfo { Minutes = 30, Category = "Compiler", Priority = "High", Rationale = "Overriding Equals requires GetHashCode override", Recommendation = "Implement proper GetHashCode override" }, + ["CS1030"] = new TechDebtInfo { Minutes = 5, Category = "Compiler", Priority = "Low", Rationale = "Directive not valid", Recommendation = "Fix or remove invalid directive" }, + ["CS1572"] = new TechDebtInfo { Minutes = 10, Category = "Compiler", Priority = "Low", Rationale = "XML comment has param tag for non-existent parameter", Recommendation = "Update XML documentation" }, + ["CS1573"] = new TechDebtInfo { Minutes = 10, Category = "Compiler", Priority = "Low", Rationale = "Missing XML comment for parameter", Recommendation = "Add parameter documentation" }, + ["CS1634"] = new TechDebtInfo { Minutes = 15, Category = "Compiler", Priority = "Medium", Rationale = "Assembly reference issue", Recommendation = "Fix assembly reference" }, + ["CS1696"] = new TechDebtInfo { Minutes = 20, Category = "Compiler", Priority = "Medium", Rationale = "Single-file compilation issue", Recommendation = "Fix single-file compilation setup" }, + ["CS4014"] = new TechDebtInfo { Minutes = 20, Category = "AsyncAwait", Priority = "High", Rationale = "Async method called without await", Recommendation = "Add await or handle Task explicitly" }, + ["CS8073"] = new TechDebtInfo { Minutes = 15, Category = "Compiler", Priority = "Medium", Rationale = "The result of the expression is always null", Recommendation = "Fix expression logic" }, + ["CS8600"] = new TechDebtInfo { Minutes = 15, Category = "NullableReference", Priority = "Medium", Rationale = "Converting null literal or possible null value", Recommendation = "Add null check or use appropriate null-handling" }, + ["CS8601"] = new TechDebtInfo { Minutes = 15, Category = "NullableReference", Priority = "Medium", Rationale = "Possible null reference assignment", Recommendation = "Add null check or ensure non-null value" }, + ["CS8602"] = new TechDebtInfo { Minutes = 20, Category = "NullableReference", Priority = "High", Rationale = "Dereference of possible null reference", Recommendation = "Add null check before dereferencing" }, + ["CS8603"] = new TechDebtInfo { Minutes = 15, Category = "NullableReference", Priority = "Medium", Rationale = "Possible null reference return", Recommendation = "Ensure non-null return or change return type" }, + ["CS8604"] = new TechDebtInfo { Minutes = 15, Category = "NullableReference", Priority = "Medium", Rationale = "Possible null reference argument", Recommendation = "Ensure non-null argument or add null check" }, + ["CS8605"] = new TechDebtInfo { Minutes = 15, Category = "NullableReference", Priority = "Medium", Rationale = "Unboxing possibly null value", Recommendation = "Add null check before unboxing" }, + ["CS8609"] = new TechDebtInfo { Minutes = 15, Category = "NullableReference", Priority = "Medium", Rationale = "Nullability mismatch in interface implementation", Recommendation = "Fix nullability annotations" }, + ["CS8613"] = new TechDebtInfo { Minutes = 15, Category = "NullableReference", Priority = "Medium", Rationale = "Nullability mismatch in method override", Recommendation = "Fix nullability annotations" }, + ["CS8619"] = new TechDebtInfo { Minutes = 15, Category = "NullableReference", Priority = "Medium", Rationale = "Nullability mismatch in generic type usage", Recommendation = "Fix generic type constraints or usage" }, + ["CS8620"] = new TechDebtInfo { Minutes = 15, Category = "NullableReference", Priority = "Medium", Rationale = "Generic type argument nullability mismatch", Recommendation = "Fix generic type argument nullability" }, + ["CS8622"] = new TechDebtInfo { Minutes = 15, Category = "NullableReference", Priority = "Medium", Rationale = "Nullability mismatch in delegate creation", Recommendation = "Fix delegate nullability annotations" }, + ["CS8625"] = new TechDebtInfo { Minutes = 15, Category = "NullableReference", Priority = "Medium", Rationale = "Cannot convert null literal to non-nullable type", Recommendation = "Use non-null value or change type to nullable" }, + ["CS8629"] = new TechDebtInfo { Minutes = 15, Category = "NullableReference", Priority = "Medium", Rationale = "Nullable value type may be null", Recommendation = "Add null check before value type usage" }, + ["CS8632"] = new TechDebtInfo { Minutes = 10, Category = "NullableReference", Priority = "Low", Rationale = "Nullable annotation context mismatch", Recommendation = "Fix nullable annotation context" }, + ["CS8669"] = new TechDebtInfo { Minutes = 15, Category = "NullableReference", Priority = "Medium", Rationale = "Nullability attribute mismatch", Recommendation = "Fix nullability attributes" }, + ["CS8714"] = new TechDebtInfo { Minutes = 15, Category = "NullableReference", Priority = "Medium", Rationale = "Generic type constraint nullability mismatch", Recommendation = "Fix generic type constraints" }, + ["CS8767"] = new TechDebtInfo { Minutes = 15, Category = "NullableReference", Priority = "Medium", Rationale = "Nullability mismatch in interface implementation", Recommendation = "Fix interface implementation nullability" }, + ["CS8981"] = new TechDebtInfo { Minutes = 10, Category = "Compiler", Priority = "Low", Rationale = "Type name only contains lower-cased ascii characters", Recommendation = "Use PascalCase for type names" }, + ["CS9107"] = new TechDebtInfo { Minutes = 15, Category = "Compiler", Priority = "Medium", Rationale = "Pattern matching issue", Recommendation = "Fix pattern matching logic" }, + ["CS9113"] = new TechDebtInfo { Minutes = 15, Category = "Compiler", Priority = "Medium", Rationale = "Parameter issue in primary constructor", Recommendation = "Fix primary constructor parameter" }, + + // Code Analysis Rules + ["CA1000"] = new TechDebtInfo { Minutes = 20, Category = "Design", Priority = "Medium", Rationale = "Do not declare static members on generic types", Recommendation = "Move static members to non-generic base class" }, + ["CA1001"] = new TechDebtInfo { Minutes = 25, Category = "Reliability", Priority = "High", Rationale = "Types that own disposable fields should be disposable", Recommendation = "Implement IDisposable pattern" }, + ["CA1002"] = new TechDebtInfo { Minutes = 20, Category = "Design", Priority = "Medium", Rationale = "Do not expose generic lists", Recommendation = "Use IReadOnlyList or IList interface instead" }, + ["CA1008"] = new TechDebtInfo { Minutes = 20, Category = "Design", Priority = "Medium", Rationale = "Enums should have zero value", Recommendation = "Add zero value to enum" }, + ["CA1010"] = new TechDebtInfo { Minutes = 20, Category = "Design", Priority = "Medium", Rationale = "Collections should implement generic interface", Recommendation = "Implement generic collection interface" }, + ["CA1018"] = new TechDebtInfo { Minutes = 10, Category = "Design", Priority = "Low", Rationale = "Mark attributes with AttributeUsageAttribute", Recommendation = "Add AttributeUsage attribute" }, + ["CA1019"] = new TechDebtInfo { Minutes = 15, Category = "Design", Priority = "Medium", Rationale = "Define accessors for attribute arguments", Recommendation = "Add property accessors for attribute arguments" }, + ["CA1024"] = new TechDebtInfo { Minutes = 15, Category = "Design", Priority = "Medium", Rationale = "Use properties where appropriate", Recommendation = "Convert method to property if it's a simple getter" }, + ["CA1027"] = new TechDebtInfo { Minutes = 15, Category = "Design", Priority = "Medium", Rationale = "Mark enums with FlagsAttribute", Recommendation = "Add Flags attribute to bitfield enums" }, + ["CA1032"] = new TechDebtInfo { Minutes = 20, Category = "Design", Priority = "Medium", Rationale = "Implement standard exception constructors", Recommendation = "Add standard exception constructors" }, + ["CA1033"] = new TechDebtInfo { Minutes = 25, Category = "Design", Priority = "Medium", Rationale = "Interface methods should be callable by child types", Recommendation = "Make interface methods accessible to child types" }, + ["CA1034"] = new TechDebtInfo { Minutes = 20, Category = "Design", Priority = "Medium", Rationale = "Nested types should not be visible", Recommendation = "Move nested type to top level" }, + ["CA1040"] = new TechDebtInfo { Minutes = 15, Category = "Design", Priority = "Low", Rationale = "Avoid empty interfaces", Recommendation = "Add members or remove interface" }, + ["CA1041"] = new TechDebtInfo { Minutes = 15, Category = "Design", Priority = "Medium", Rationale = "Provide ObsoleteAttribute message", Recommendation = "Add message to Obsolete attribute" }, + ["CA1044"] = new TechDebtInfo { Minutes = 20, Category = "Design", Priority = "Medium", Rationale = "Properties should not be write only", Recommendation = "Add getter or convert to method" }, + ["CA1050"] = new TechDebtInfo { Minutes = 10, Category = "Design", Priority = "Low", Rationale = "Declare types in namespaces", Recommendation = "Move type into a namespace" }, + ["CA1051"] = new TechDebtInfo { Minutes = 20, Category = "Design", Priority = "Medium", Rationale = "Do not declare visible instance fields", Recommendation = "Convert field to property" }, + ["CA1052"] = new TechDebtInfo { Minutes = 15, Category = "Design", Priority = "Medium", Rationale = "Static holder types should be Static or NotInheritable", Recommendation = "Make type static or sealed" }, + ["CA1054"] = new TechDebtInfo { Minutes = 20, Category = "Design", Priority = "Medium", Rationale = "URI parameters should not be strings", Recommendation = "Use Uri type for URI parameters" }, + ["CA1055"] = new TechDebtInfo { Minutes = 20, Category = "Design", Priority = "Medium", Rationale = "URI return values should not be strings", Recommendation = "Return Uri type instead of string" }, + ["CA1056"] = new TechDebtInfo { Minutes = 20, Category = "Design", Priority = "Medium", Rationale = "URI properties should not be strings", Recommendation = "Use Uri type for URI properties" }, + ["CA1063"] = new TechDebtInfo { Minutes = 30, Category = "Design", Priority = "High", Rationale = "Implement IDisposable correctly", Recommendation = "Follow dispose pattern guidelines" }, + ["CA1064"] = new TechDebtInfo { Minutes = 15, Category = "Design", Priority = "Medium", Rationale = "Exceptions should be public", Recommendation = "Make exception type public" }, + ["CA1066"] = new TechDebtInfo { Minutes = 25, Category = "Design", Priority = "Medium", Rationale = "Implement IEquatable when overriding Equals", Recommendation = "Implement IEquatable" }, + ["CA1068"] = new TechDebtInfo { Minutes = 15, Category = "Design", Priority = "Medium", Rationale = "CancellationToken parameters should come last", Recommendation = "Move CancellationToken to last parameter" }, + + // Globalization Rules + ["CA1303"] = new TechDebtInfo { Minutes = 25, Category = "Globalization", Priority = "Medium", Rationale = "Do not pass literals as localized parameters", Recommendation = "Use resource strings" }, + ["CA1304"] = new TechDebtInfo { Minutes = 20, Category = "Globalization", Priority = "Medium", Rationale = "Specify CultureInfo", Recommendation = "Add explicit culture info" }, + ["CA1305"] = new TechDebtInfo { Minutes = 20, Category = "Globalization", Priority = "Medium", Rationale = "Specify IFormatProvider", Recommendation = "Add explicit format provider" }, + ["CA1307"] = new TechDebtInfo { Minutes = 20, Category = "Globalization", Priority = "Medium", Rationale = "Specify StringComparison for clarity", Recommendation = "Add explicit string comparison" }, + ["CA1308"] = new TechDebtInfo { Minutes = 15, Category = "Globalization", Priority = "Medium", Rationale = "Normalize strings to uppercase", Recommendation = "Use ToUpperInvariant instead of ToLowerInvariant" }, + ["CA1309"] = new TechDebtInfo { Minutes = 15, Category = "Globalization", Priority = "Medium", Rationale = "Use ordinal string comparison", Recommendation = "Use StringComparison.Ordinal" }, + ["CA1310"] = new TechDebtInfo { Minutes = 15, Category = "Globalization", Priority = "Medium", Rationale = "Specify StringComparison for correctness", Recommendation = "Add explicit string comparison" }, + ["CA1311"] = new TechDebtInfo { Minutes = 15, Category = "Globalization", Priority = "Medium", Rationale = "Specify a culture or use invariant culture", Recommendation = "Add culture specification" }, + + // Performance Rules + ["CA1802"] = new TechDebtInfo { Minutes = 10, Category = "Performance", Priority = "Medium", Rationale = "Use literals where appropriate", Recommendation = "Use literal instead of computed value" }, + ["CA1805"] = new TechDebtInfo { Minutes = 10, Category = "Performance", Priority = "Low", Rationale = "Do not initialize unnecessarily", Recommendation = "Remove unnecessary initialization" }, + ["CA1806"] = new TechDebtInfo { Minutes = 15, Category = "Performance", Priority = "Medium", Rationale = "Do not ignore method results", Recommendation = "Use or check method results" }, + ["CA1810"] = new TechDebtInfo { Minutes = 20, Category = "Performance", Priority = "Medium", Rationale = "Initialize reference type static fields inline", Recommendation = "Move initialization inline" }, + ["CA1812"] = new TechDebtInfo { Minutes = 15, Category = "Performance", Priority = "Low", Rationale = "Avoid uninstantiated internal classes", Recommendation = "Remove unused class or add usage" }, + ["CA1813"] = new TechDebtInfo { Minutes = 15, Category = "Performance", Priority = "Medium", Rationale = "Avoid unsealed attributes", Recommendation = "Seal attribute classes" }, + ["CA1815"] = new TechDebtInfo { Minutes = 20, Category = "Performance", Priority = "Medium", Rationale = "Override equals and operator equals on value types", Recommendation = "Implement equality members" }, + ["CA1816"] = new TechDebtInfo { Minutes = 25, Category = "Performance", Priority = "High", Rationale = "Call GC.SuppressFinalize correctly", Recommendation = "Add or fix GC.SuppressFinalize call" }, + ["CA1819"] = new TechDebtInfo { Minutes = 25, Category = "Performance", Priority = "Medium", Rationale = "Properties should not return arrays", Recommendation = "Return array copy or use different type" }, + ["CA1820"] = new TechDebtInfo { Minutes = 15, Category = "Performance", Priority = "Medium", Rationale = "Test for empty strings using string length", Recommendation = "Use Length instead of comparison" }, + ["CA1821"] = new TechDebtInfo { Minutes = 20, Category = "Performance", Priority = "Medium", Rationale = "Remove empty finalizers", Recommendation = "Remove unnecessary finalizer" }, + ["CA1822"] = new TechDebtInfo { Minutes = 15, Category = "Performance", Priority = "Medium", Rationale = "Mark members as static", Recommendation = "Add static modifier where appropriate" }, + ["CA1823"] = new TechDebtInfo { Minutes = 15, Category = "Performance", Priority = "Medium", Rationale = "Avoid unused private fields", Recommendation = "Remove unused private fields" }, + ["CA1824"] = new TechDebtInfo { Minutes = 15, Category = "Performance", Priority = "Medium", Rationale = "Mark assemblies with NeutralResourcesLanguageAttribute", Recommendation = "Add NeutralResourcesLanguage attribute" }, + ["CA1825"] = new TechDebtInfo { Minutes = 15, Category = "Performance", Priority = "Medium", Rationale = "Avoid zero-length array allocations", Recommendation = "Use Array.Empty()" }, + + // Naming Rules + ["CA1707"] = new TechDebtInfo { Minutes = 10, Category = "Naming", Priority = "Low", Rationale = "Remove underscores from member names", Recommendation = "Use PascalCase naming" }, + ["CA1708"] = new TechDebtInfo { Minutes = 15, Category = "Naming", Priority = "Medium", Rationale = "Names should differ by more than case", Recommendation = "Rename to avoid confusion" }, + ["CA1710"] = new TechDebtInfo { Minutes = 15, Category = "Naming", Priority = "Low", Rationale = "Identifiers should have correct suffix", Recommendation = "Add appropriate suffix" }, + ["CA1711"] = new TechDebtInfo { Minutes = 15, Category = "Naming", Priority = "Low", Rationale = "Identifiers should not have incorrect suffix", Recommendation = "Remove or change incorrect suffix" }, + ["CA1715"] = new TechDebtInfo { Minutes = 15, Category = "Naming", Priority = "Low", Rationale = "Identifiers should have correct prefix", Recommendation = "Add appropriate prefix" }, + ["CA1716"] = new TechDebtInfo { Minutes = 20, Category = "Naming", Priority = "Medium", Rationale = "Identifiers should not match keywords", Recommendation = "Rename to avoid keyword conflict" }, + ["CA1720"] = new TechDebtInfo { Minutes = 15, Category = "Naming", Priority = "Low", Rationale = "Identifiers should not contain type names", Recommendation = "Remove type name from identifier" }, + ["CA1724"] = new TechDebtInfo { Minutes = 20, Category = "Naming", Priority = "Medium", Rationale = "Type names should not match namespaces", Recommendation = "Rename type or use different namespace" }, + ["CA1725"] = new TechDebtInfo { Minutes = 15, Category = "Naming", Priority = "Medium", Rationale = "Parameter names should match base declaration", Recommendation = "Match parameter names with base class" }, + + // NUnit Rules + ["NUnit1001"] = new TechDebtInfo { Minutes = 15, Category = "Testing", Priority = "Medium", Rationale = "Test class missing test attribute", Recommendation = "Add TestFixture attribute" }, + ["NUnit1002"] = new TechDebtInfo { Minutes = 10, Category = "Testing", Priority = "Low", Rationale = "Test method missing test attribute", Recommendation = "Add Test attribute" }, + ["NUnit1028"] = new TechDebtInfo { Minutes = 15, Category = "Testing", Priority = "Medium", Rationale = "Test method parameter issue", Recommendation = "Fix test method parameters" }, + ["NUnit1032"] = new TechDebtInfo { Minutes = 15, Category = "Testing", Priority = "Medium", Rationale = "Test attribute usage issue", Recommendation = "Fix test attribute usage" }, + ["NUnit2001"] = new TechDebtInfo { Minutes = 10, Category = "Testing", Priority = "Low", Rationale = "Consider using Assert.That", Recommendation = "Convert to Assert.That usage" }, + ["NUnit2002"] = new TechDebtInfo { Minutes = 10, Category = "Testing", Priority = "Low", Rationale = "Consider using Assert.That", Recommendation = "Convert to Assert.That usage" }, + ["NUnit2003"] = new TechDebtInfo { Minutes = 10, Category = "Testing", Priority = "Low", Rationale = "Consider using Assert.That", Recommendation = "Convert to Assert.That usage" }, + ["NUnit2004"] = new TechDebtInfo { Minutes = 10, Category = "Testing", Priority = "Low", Rationale = "Consider using Assert.That", Recommendation = "Convert to Assert.That usage" }, + ["NUnit2005"] = new TechDebtInfo { Minutes = 10, Category = "Testing", Priority = "Low", Rationale = "Consider using Assert.That", Recommendation = "Convert to Assert.That usage" }, + ["NUnit2007"] = new TechDebtInfo { Minutes = 10, Category = "Testing", Priority = "Low", Rationale = "Consider using Assert.That", Recommendation = "Convert to Assert.That usage" }, + ["NUnit2009"] = new TechDebtInfo { Minutes = 15, Category = "Testing", Priority = "Medium", Rationale = "Consider using Assert.That", Recommendation = "Convert to Assert.That usage" }, + ["NUnit2010"] = new TechDebtInfo { Minutes = 15, Category = "Testing", Priority = "Medium", Rationale = "Consider using Assert.That", Recommendation = "Convert to Assert.That usage" }, + ["NUnit2011"] = new TechDebtInfo { Minutes = 15, Category = "Testing", Priority = "Medium", Rationale = "Consider using Assert.That", Recommendation = "Convert to Assert.That usage" }, + ["NUnit2012"] = new TechDebtInfo { Minutes = 15, Category = "Testing", Priority = "Medium", Rationale = "Consider using Assert.That", Recommendation = "Convert to Assert.That usage" }, + ["NUnit2014"] = new TechDebtInfo { Minutes = 15, Category = "Testing", Priority = "Medium", Rationale = "Consider using Assert.That", Recommendation = "Convert to Assert.That usage" }, + ["NUnit2016"] = new TechDebtInfo { Minutes = 15, Category = "Testing", Priority = "Medium", Rationale = "Consider using Assert.That", Recommendation = "Convert to Assert.That usage" }, + ["NUnit2017"] = new TechDebtInfo { Minutes = 15, Category = "Testing", Priority = "Medium", Rationale = "Consider using Assert.That", Recommendation = "Convert to Assert.That usage" }, + ["NUnit2018"] = new TechDebtInfo { Minutes = 15, Category = "Testing", Priority = "Medium", Rationale = "Consider using Assert.That", Recommendation = "Convert to Assert.That usage" }, + ["NUnit2019"] = new TechDebtInfo { Minutes = 15, Category = "Testing", Priority = "Medium", Rationale = "Consider using Assert.That", Recommendation = "Convert to Assert.That usage" }, + ["NUnit2027"] = new TechDebtInfo { Minutes = 20, Category = "Testing", Priority = "Medium", Rationale = "Consider using Assert.That", Recommendation = "Convert to Assert.That usage" }, + ["NUnit2030"] = new TechDebtInfo { Minutes = 20, Category = "Testing", Priority = "Medium", Rationale = "Consider using Assert.That", Recommendation = "Convert to Assert.That usage" }, + ["NUnit2035"] = new TechDebtInfo { Minutes = 20, Category = "Testing", Priority = "Medium", Rationale = "Consider using Assert.That", Recommendation = "Convert to Assert.That usage" }, + ["NUnit2036"] = new TechDebtInfo { Minutes = 20, Category = "Testing", Priority = "Medium", Rationale = "Consider using Assert.That", Recommendation = "Convert to Assert.That usage" }, + ["NUnit2037"] = new TechDebtInfo { Minutes = 20, Category = "Testing", Priority = "Medium", Rationale = "Consider using Assert.That", Recommendation = "Convert to Assert.That usage" }, + ["NUnit2038"] = new TechDebtInfo { Minutes = 20, Category = "Testing", Priority = "Medium", Rationale = "Consider using Assert.That", Recommendation = "Convert to Assert.That usage" }, + ["NUnit2039"] = new TechDebtInfo { Minutes = 20, Category = "Testing", Priority = "Medium", Rationale = "Consider using Assert.That", Recommendation = "Convert to Assert.That usage" }, + ["NUnit2043"] = new TechDebtInfo { Minutes = 20, Category = "Testing", Priority = "Medium", Rationale = "Use ComparisonConstraint for better assertions", Recommendation = "Convert to constraint-based assertion" }, + ["NUnit2045"] = new TechDebtInfo { Minutes = 20, Category = "Testing", Priority = "Medium", Rationale = "Use Assert.That with constraints", Recommendation = "Convert to constraint-based assertion" }, + ["NUnit2046"] = new TechDebtInfo { Minutes = 20, Category = "Testing", Priority = "Medium", Rationale = "Use Assert.That with constraints", Recommendation = "Convert to constraint-based assertion" }, + ["NUnit2049"] = new TechDebtInfo { Minutes = 20, Category = "Testing", Priority = "Medium", Rationale = "Consider using Assert.That", Recommendation = "Convert to Assert.That usage" }, + + // SonarQube Rules + ["S101"] = new TechDebtInfo { Minutes = 10, Category = "Convention", Priority = "Low", Rationale = "Class naming convention violation", Recommendation = "Rename class to match conventions" }, + ["S108"] = new TechDebtInfo { Minutes = 15, Category = "BestPractices", Priority = "Medium", Rationale = "Empty block should be documented", Recommendation = "Add comment explaining empty block" }, + ["S112"] = new TechDebtInfo { Minutes = 20, Category = "ErrorHandling", Priority = "High", Rationale = "General exceptions should not be thrown", Recommendation = "Use more specific exception type" }, + ["S1066"] = new TechDebtInfo { Minutes = 15, Category = "CodeStyle", Priority = "Low", Rationale = "Merge collapsible if statements", Recommendation = "Combine if statements" }, + ["S1104"] = new TechDebtInfo { Minutes = 20, Category = "Design", Priority = "Medium", Rationale = "Public fields should be properties", Recommendation = "Convert field to property" }, + ["S1116"] = new TechDebtInfo { Minutes = 5, Category = "CodeStyle", Priority = "Low", Rationale = "Empty statements should be removed", Recommendation = "Remove empty statement" }, + ["S1117"] = new TechDebtInfo { Minutes = 15, Category = "BestPractices", Priority = "Medium", Rationale = "Variable shadowing should be avoided", Recommendation = "Rename shadowing variable" }, + ["S1118"] = new TechDebtInfo { Minutes = 15, Category = "Design", Priority = "Medium", Rationale = "Utility classes should not be instantiable", Recommendation = "Make constructor private" }, + ["S1121"] = new TechDebtInfo { Minutes = 20, Category = "BestPractices", Priority = "Medium", Rationale = "Assignments should not be made in conditions", Recommendation = "Move assignment outside condition" }, + ["S1123"] = new TechDebtInfo { Minutes = 15, Category = "Documentation", Priority = "Low", Rationale = "Obsolete attribute should have explanation", Recommendation = "Add explanation to Obsolete attribute" }, + ["S1125"] = new TechDebtInfo { Minutes = 10, Category = "CodeStyle", Priority = "Low", Rationale = "Boolean literals should not be redundant", Recommendation = "Remove redundant boolean literal" }, + ["S1133"] = new TechDebtInfo { Minutes = 15, Category = "Documentation", Priority = "Low", Rationale = "Deprecated code should be removed", Recommendation = "Remove or update deprecated code" }, + ["S1144"] = new TechDebtInfo { Minutes = 15, Category = "Dead Code", Priority = "Low", Rationale = "Remove unused private types or members", Recommendation = "Remove unused private code" }, + ["S1155"] = new TechDebtInfo { Minutes = 15, Category = "Performance", Priority = "Low", Rationale = "Use Count property instead of Any()", Recommendation = "Replace Any() with Count check" }, + ["S1168"] = new TechDebtInfo { Minutes = 20, Category = "BestPractices", Priority = "Medium", Rationale = "Return empty collection instead of null", Recommendation = "Return empty collection" }, + ["S1172"] = new TechDebtInfo { Minutes = 15, Category = "Dead Code", Priority = "Low", Rationale = "Remove unused method parameters", Recommendation = "Remove unused parameters" }, + ["S1186"] = new TechDebtInfo { Minutes = 15, Category = "Documentation", Priority = "Low", Rationale = "Empty methods should be documented", Recommendation = "Add documentation for empty method" }, + ["S1199"] = new TechDebtInfo { Minutes = 20, Category = "Design", Priority = "Medium", Rationale = "Nested code blocks should be extracted", Recommendation = "Extract nested code into methods" }, + ["S1206"] = new TechDebtInfo { Minutes = 25, Category = "BestPractices", Priority = "Medium", Rationale = "Property getter should be pure", Recommendation = "Remove side effects from getter" }, + ["S1450"] = new TechDebtInfo { Minutes = 15, Category = "Dead Code", Priority = "Low", Rationale = "Private fields only used in initialization", Recommendation = "Convert field to local variable" }, + ["S1481"] = new TechDebtInfo { Minutes = 10, Category = "Dead Code", Priority = "Low", Rationale = "Remove unused local variables", Recommendation = "Remove unused variables" }, + ["S1607"] = new TechDebtInfo { Minutes = 20, Category = "Testing", Priority = "Medium", Rationale = "Test method should contain assertion", Recommendation = "Add test assertions" }, + ["S1751"] = new TechDebtInfo { Minutes = 15, Category = "BestPractices", Priority = "Medium", Rationale = "Jump statements should not be redundant", Recommendation = "Remove redundant jump statements" }, + ["S1854"] = new TechDebtInfo { Minutes = 15, Category = "Dead Code", Priority = "Low", Rationale = "Remove dead stores", Recommendation = "Remove unused assignments" }, + ["S1905"] = new TechDebtInfo { Minutes = 15, Category = "CodeStyle", Priority = "Low", Rationale = "Redundant cast should be removed", Recommendation = "Remove unnecessary cast" }, + ["S1939"] = new TechDebtInfo { Minutes = 20, Category = "BestPractices", Priority = "Medium", Rationale = "Inheritance list should not be redundant", Recommendation = "Remove redundant interface implementations" }, + ["S1940"] = new TechDebtInfo { Minutes = 20, Category = "BestPractices", Priority = "Medium", Rationale = "Use ISerializable appropriately", Recommendation = "Fix serialization implementation" }, + ["S2094"] = new TechDebtInfo { Minutes = 10, Category = "Dead Code", Priority = "Low", Rationale = "Remove empty class", Recommendation = "Remove or implement empty class" }, + ["S2178"] = new TechDebtInfo { Minutes = 20, Category = "BestPractices", Priority = "Medium", Rationale = "Short-circuit logic should be used", Recommendation = "Use && or || instead of & or |" }, + ["S2187"] = new TechDebtInfo { Minutes = 20, Category = "Testing", Priority = "Medium", Rationale = "Test classes should contain tests", Recommendation = "Add tests or remove test class" }, + ["S2201"] = new TechDebtInfo { Minutes = 15, Category = "BestPractices", Priority = "Medium", Rationale = "Return value should not be ignored", Recommendation = "Use or remove return value" }, + ["S2223"] = new TechDebtInfo { Minutes = 20, Category = "BestPractices", Priority = "Medium", Rationale = "Change non-readonly to readonly", Recommendation = "Add readonly modifier" }, + ["S2251"] = new TechDebtInfo { Minutes = 25, Category = "BestPractices", Priority = "Medium", Rationale = "For loop increment clause should modify loop counter", Recommendation = "Fix loop increment" }, + ["S2259"] = new TechDebtInfo { Minutes = 25, Category = "BugRisk", Priority = "High", Rationale = "Null pointer dereference", Recommendation = "Add null check" }, + ["S2292"] = new TechDebtInfo { Minutes = 20, Category = "BestPractices", Priority = "Medium", Rationale = "Trivial properties should be auto-implemented", Recommendation = "Convert to auto-property" }, + ["S2344"] = new TechDebtInfo { Minutes = 15, Category = "Convention", Priority = "Low", Rationale = "Enumeration type naming convention", Recommendation = "Rename enumeration" }, + ["S2365"] = new TechDebtInfo { Minutes = 20, Category = "BestPractices", Priority = "Medium", Rationale = "Properties should not be recursive", Recommendation = "Remove property recursion" }, + ["S2386"] = new TechDebtInfo { Minutes = 25, Category = "Security", Priority = "High", Rationale = "Public mutable fields should not be public", Recommendation = "Encapsulate public fields" }, + ["S2479"] = new TechDebtInfo { Minutes = 15, Category = "Convention", Priority = "Low", Rationale = "Whitespace and control characters in string", Recommendation = "Remove control characters" }, + ["S2486"] = new TechDebtInfo { Minutes = 20, Category = "BestPractices", Priority = "Medium", Rationale = "Generic exceptions should not be caught", Recommendation = "Catch more specific exceptions" }, + ["S2583"] = new TechDebtInfo { Minutes = 20, Category = "BugRisk", Priority = "High", Rationale = "Conditionally executed code should be possible", Recommendation = "Fix impossible condition" }, + ["S2589"] = new TechDebtInfo { Minutes = 15, Category = "CodeStyle", Priority = "Low", Rationale = "Boolean expressions should not be gratuitous", Recommendation = "Simplify boolean expression" }, + ["S2699"] = new TechDebtInfo { Minutes = 20, Category = "Testing", Priority = "Medium", Rationale = "Tests should include assertions", Recommendation = "Add assertions to test" }, + ["S2925"] = new TechDebtInfo { Minutes = 20, Category = "BestPractices", Priority = "Medium", Rationale = "Thread.Sleep should not be used in tests", Recommendation = "Use proper test synchronization" }, + ["S2933"] = new TechDebtInfo { Minutes = 15, Category = "BestPractices", Priority = "Medium", Rationale = "Fields should be initialized inline", Recommendation = "Move initialization inline" }, + ["S2971"] = new TechDebtInfo { Minutes = 20, Category = "Performance", Priority = "Medium", Rationale = "IEnumerable LINQs should be simplified", Recommendation = "Simplify LINQ query" }, + ["S3010"] = new TechDebtInfo { Minutes = 20, Category = "BestPractices", Priority = "Medium", Rationale = "Static fields should be initialized statically", Recommendation = "Move initialization to static constructor" }, + ["S3063"] = new TechDebtInfo { Minutes = 20, Category = "Design", Priority = "Medium", Rationale = "Single responsibility principle violation", Recommendation = "Split class responsibilities" }, + ["S3218"] = new TechDebtInfo { Minutes = 15, Category = "BestPractices", Priority = "Medium", Rationale = "Inner class members should be simplified", Recommendation = "Simplify inner class members" }, + ["S3260"] = new TechDebtInfo { Minutes = 20, Category = "BestPractices", Priority = "Medium", Rationale = "Convert to auto-implemented property", Recommendation = "Use auto-property" }, + ["S3267"] = new TechDebtInfo { Minutes = 15, Category = "BestPractices", Priority = "Medium", Rationale = "Loop should be simplified", Recommendation = "Simplify loop structure" }, + ["S3358"] = new TechDebtInfo { Minutes = 20, Category = "BestPractices", Priority = "Medium", Rationale = "Ternary operator should not be nested", Recommendation = "Extract nested ternary" }, + ["S3376"] = new TechDebtInfo { Minutes = 20, Category = "BestPractices", Priority = "Medium", Rationale = "Attribute instances should be replaceable", Recommendation = "Make attributes replaceable" }, + ["S3400"] = new TechDebtInfo { Minutes = 15, Category = "BestPractices", Priority = "Medium", Rationale = "Constants should be defined", Recommendation = "Extract magic numbers to constants" }, + ["S3415"] = new TechDebtInfo { Minutes = 20, Category = "BestPractices", Priority = "Medium", Rationale = "Assertion arguments should be passed in correct order", Recommendation = "Fix assertion argument order" }, + ["S3427"] = new TechDebtInfo { Minutes = 20, Category = "BestPractices", Priority = "Medium", Rationale = "Method overloads should be grouped", Recommendation = "Group method overloads" }, + ["S3459"] = new TechDebtInfo { Minutes = 15, Category = "BestPractices", Priority = "Medium", Rationale = "Unassigned members should be removed", Recommendation = "Remove unassigned members" }, + ["S3604"] = new TechDebtInfo { Minutes = 20, Category = "BestPractices", Priority = "Medium", Rationale = "Member initializer values should not be redundant", Recommendation = "Remove redundant initialization" }, + ["S3626"] = new TechDebtInfo { Minutes = 15, Category = "CodeStyle", Priority = "Low", Rationale = "Jump statements should not be redundant", Recommendation = "Remove redundant jumps" }, + ["S3655"] = new TechDebtInfo { Minutes = 25, Category = "BugRisk", Priority = "High", Rationale = "Empty alternative branches should be removed", Recommendation = "Remove empty branches" }, + ["S3871"] = new TechDebtInfo { Minutes = 20, Category = "BestPractices", Priority = "Medium", Rationale = "Exception types should be public", Recommendation = "Make exception public" }, + ["S3878"] = new TechDebtInfo { Minutes = 20, Category = "BestPractices", Priority = "Medium", Rationale = "Arrays should not be created for params parameters", Recommendation = "Use params directly" }, + ["S3881"] = new TechDebtInfo { Minutes = 20, Category = "BestPractices", Priority = "Medium", Rationale = "Fix sonar issue", Recommendation = "Follow sonar recommendation" }, + ["S3887"] = new TechDebtInfo { Minutes = 20, Category = "BestPractices", Priority = "Medium", Rationale = "Fix sonar issue", Recommendation = "Follow sonar recommendation" }, + ["S3903"] = new TechDebtInfo { Minutes = 15, Category = "Convention", Priority = "Low", Rationale = "Types should be in named namespaces", Recommendation = "Move type to named namespace" }, + ["S3925"] = new TechDebtInfo { Minutes = 15, Category = "BestPractices", Priority = "Medium", Rationale = "Implement ISerializable correctly", Recommendation = "Fix serialization implementation" }, + ["S3928"] = new TechDebtInfo { Minutes = 15, Category = "BestPractices", Priority = "Medium", Rationale = "Parameter names should match base declaration", Recommendation = "Fix parameter names" }, + ["S3963"] = new TechDebtInfo { Minutes = 20, Category = "BestPractices", Priority = "Medium", Rationale = "String interpolation should be used", Recommendation = "Use string interpolation" }, + ["S4035"] = new TechDebtInfo { Minutes = 20, Category = "BestPractices", Priority = "Medium", Rationale = "Classes implementing IEquatable should be sealed", Recommendation = "Seal class or remove IEquatable" }, + ["S4136"] = new TechDebtInfo { Minutes = 15, Category = "CodeStyle", Priority = "Low", Rationale = "Method overloads should be grouped together", Recommendation = "Reorder method declarations" }, + ["S4144"] = new TechDebtInfo { Minutes = 25, Category = "CodeStyle", Priority = "Medium", Rationale = "Methods should not have identical implementations", Recommendation = "Remove duplicate method or extract common code" }, + ["S4158"] = new TechDebtInfo { Minutes = 15, Category = "BestPractices", Priority = "Medium", Rationale = "Empty collections should be empty, not null", Recommendation = "Return empty collection instead of null" }, + ["S4201"] = new TechDebtInfo { Minutes = 20, Category = "BestPractices", Priority = "Medium", Rationale = "Null checks should not be used with is", Recommendation = "Remove redundant null check" }, + ["S4487"] = new TechDebtInfo { Minutes = 15, Category = "BestPractices", Priority = "Medium", Rationale = "Unread 'private' fields should be removed", Recommendation = "Remove unused private fields" }, + ["S4581"] = new TechDebtInfo { Minutes = 20, Category = "BestPractices", Priority = "Medium", Rationale = "new Guid() should not be used", Recommendation = "Use Guid.Empty or Guid.NewGuid()" }, + ["S4586"] = new TechDebtInfo { Minutes = 15, Category = "BestPractices", Priority = "Medium", Rationale = "Non-async method names should not end with 'Async'", Recommendation = "Rename method or make async" }, + ["S4663"] = new TechDebtInfo { Minutes = 20, Category = "BestPractices", Priority = "Medium", Rationale = "Disposable types should implement IDisposable", Recommendation = "Implement IDisposable" }, + ["S6561"] = new TechDebtInfo { Minutes = 25, Category = "Performance", Priority = "High", Rationale = "DateTimeKind should be specified", Recommendation = "Specify DateTimeKind" }, + ["S6562"] = new TechDebtInfo { Minutes = 20, Category = "BestPractices", Priority = "Medium", Rationale = "Always set DateTimeKind", Recommendation = "Specify DateTimeKind" }, + ["S6580"] = new TechDebtInfo { Minutes = 15, Category = "Performance", Priority = "Medium", Rationale = "Use spans for string operations", Recommendation = "Convert to span operations" }, + ["S6602"] = new TechDebtInfo { Minutes = 20, Category = "BestPractices", Priority = "Medium", Rationale = "Fix sonar issue", Recommendation = "Follow sonar recommendation" }, + ["S6603"] = new TechDebtInfo { Minutes = 20, Category = "BestPractices", Priority = "Medium", Rationale = "Fix sonar issue", Recommendation = "Follow sonar recommendation" }, + ["S6605"] = new TechDebtInfo { Minutes = 20, Category = "BestPractices", Priority = "Medium", Rationale = "Fix sonar issue", Recommendation = "Follow sonar recommendation" }, + ["S6608"] = new TechDebtInfo { Minutes = 20, Category = "BestPractices", Priority = "Medium", Rationale = "Fix sonar issue", Recommendation = "Follow sonar recommendation" }, + ["S6610"] = new TechDebtInfo { Minutes = 20, Category = "BestPractices", Priority = "Medium", Rationale = "Fix sonar issue", Recommendation = "Follow sonar recommendation" }, + ["S6617"] = new TechDebtInfo { Minutes = 20, Category = "BestPractices", Priority = "Medium", Rationale = "Fix sonar issue", Recommendation = "Follow sonar recommendation" }, + + // IDE Rules + ["IDE0028"] = new TechDebtInfo { Minutes = 5, Category = "CodeStyle", Priority = "Low", Rationale = "Collection initialization can be simplified", Recommendation = "Use collection initializer" }, + ["IDE0037"] = new TechDebtInfo { Minutes = 5, Category = "CodeStyle", Priority = "Low", Rationale = "Use inferred member name", Recommendation = "Simplify member name" }, + ["IDE0051"] = new TechDebtInfo { Minutes = 10, Category = "CodeStyle", Priority = "Low", Rationale = "Remove unused private member", Recommendation = "Remove unused member" }, + ["IDE0052"] = new TechDebtInfo { Minutes = 10, Category = "CodeStyle", Priority = "Low", Rationale = "Remove unread private member", Recommendation = "Remove unread member" }, + + // ASP.NET Rules + ["ASP0001"] = new TechDebtInfo { Minutes = 20, Category = "Security", Priority = "High", Rationale = "Parameter may be vulnerable to XSS", Recommendation = "Encode output" }, + ["ASP0015"] = new TechDebtInfo { Minutes = 15, Category = "BestPractices", Priority = "Medium", Rationale = "Suggest async operations", Recommendation = "Convert to async" }, + ["ASP0018"] = new TechDebtInfo { Minutes = 20, Category = "Performance", Priority = "Medium", Rationale = "Avoid sync operations", Recommendation = "Use async alternatives" }, + ["ASP0019"] = new TechDebtInfo { Minutes = 20, Category = "BestPractices", Priority = "Medium", Rationale = "Fix ASP.NET issue", Recommendation = "Follow ASP.NET guidelines" }, + ["ASP0025"] = new TechDebtInfo { Minutes = 20, Category = "BestPractices", Priority = "Medium", Rationale = "Fix ASP.NET issue", Recommendation = "Follow ASP.NET guidelines" }, + + // StyleCop Rules + ["SA1106"] = new TechDebtInfo { Minutes = 5, Category = "CodeStyle", Priority = "Low", Rationale = "Code should declare access modifier", Recommendation = "Add access modifier" }, + ["SA1107"] = new TechDebtInfo { Minutes = 5, Category = "CodeStyle", Priority = "Low", Rationale = "Code should not have multiple statements on one line", Recommendation = "Split into multiple lines" }, + ["SA1123"] = new TechDebtInfo { Minutes = 5, Category = "CodeStyle", Priority = "Low", Rationale = "Region should not be located within a code element", Recommendation = "Move region" }, + + // SysLib Rules + ["SYSLIB0012"] = new TechDebtInfo { Minutes = 25, Category = "Obsolescence", Priority = "High", Rationale = "Type or member is obsolete", Recommendation = "Update to supported alternative" }, + ["SYSLIB0014"] = new TechDebtInfo { Minutes = 25, Category = "Obsolescence", Priority = "High", Rationale = "Type or member is obsolete", Recommendation = "Update to supported alternative" }, + ["SYSLIB0021"] = new TechDebtInfo { Minutes = 25, Category = "Obsolescence", Priority = "High", Rationale = "Type or member is obsolete", Recommendation = "Update to supported alternative" }, + ["SYSLIB0022"] = new TechDebtInfo { Minutes = 25, Category = "Obsolescence", Priority = "High", Rationale = "Type or member is obsolete", Recommendation = "Update to supported alternative" }, + ["SYSLIB0023"] = new TechDebtInfo { Minutes = 25, Category = "Obsolescence", Priority = "High", Rationale = "Type or member is obsolete", Recommendation = "Update to supported alternative" }, + ["SYSLIB0041"] = new TechDebtInfo { Minutes = 25, Category = "Obsolescence", Priority = "High", Rationale = "Type or member is obsolete", Recommendation = "Update to supported alternative" }, + ["SYSLIB0045"] = new TechDebtInfo { Minutes = 25, Category = "Obsolescence", Priority = "High", Rationale = "Type or member is obsolete", Recommendation = "Update to supported alternative" }, + ["SYSLIB0051"] = new TechDebtInfo { Minutes = 25, Category = "Obsolescence", Priority = "High", Rationale = "Type or member is obsolete", Recommendation = "Update to supported alternative" }, + + // Suppyl Specific Rules + ["SUP001"] = new TechDebtInfo { Minutes = 15, Category = "BestPractices", Priority = "Medium", Rationale = "Follow Supply guideline", Recommendation = "Use Switch Case Instead Of multiple ifelse" }, + ["SUP002"] = new TechDebtInfo { Minutes = 15, Category = "BestPractices", Priority = "Medium", Rationale = "Follow Supply guideline", Recommendation = "Always use a Security Attribute on public controllers" }, + ["SUP003"] = new TechDebtInfo { Minutes = 15, Category = "BestPractices", Priority = "Medium", Rationale = "Follow Supply guideline", Recommendation = "No Direct HttpContext Access, go via our Library" } + }; + + public static TechDebtInfo? GetTechDebtInfo(string ruleId) => + RuleMetadata.TryGetValue(ruleId, out var info) ? info : null; + + public static IEnumerable GetAllRuleIds() => RuleMetadata.Keys; + + public static IEnumerable GetRuleIdsByCategory(string category) => + RuleMetadata.Where(kvp => kvp.Value.Category == category) + .Select(kvp => kvp.Key); + + public static int GetEstimatedTotalMinutes(IEnumerable ruleIds) => + ruleIds.Sum(ruleId => GetTechDebtInfo(ruleId)?.Minutes ?? 0); + + public static IEnumerable GetAllCategories() => + RuleMetadata.Select(kvp => kvp.Value.Category).Distinct(); + + public static IDictionary GetTotalMinutesByCategory() => + RuleMetadata.GroupBy(kvp => kvp.Value.Category) + .ToDictionary(g => g.Key, g => g.Sum(kvp => kvp.Value.Minutes)); +} \ No newline at end of file diff --git a/src/Agoda.CodeCompass.MSBuild/TechDebtProperties.cs b/src/Agoda.CodeCompass.MSBuild/TechDebtProperties.cs new file mode 100644 index 0000000..fa3e919 --- /dev/null +++ b/src/Agoda.CodeCompass.MSBuild/TechDebtProperties.cs @@ -0,0 +1,8 @@ +using Agoda.CodeCompass.Models; + +namespace Agoda.CodeCompass.MSBuild; + +public class TechDebtProperties +{ + public TechDebtInfo? TechDebt { get; set; } +} \ No newline at end of file diff --git a/src/Agoda.CodeCompass.MSBuild/TechDebtReportTask.cs b/src/Agoda.CodeCompass.MSBuild/TechDebtReportTask.cs index c375f56..34b4194 100644 --- a/src/Agoda.CodeCompass.MSBuild/TechDebtReportTask.cs +++ b/src/Agoda.CodeCompass.MSBuild/TechDebtReportTask.cs @@ -1,22 +1,19 @@ using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Text; using System.Collections.Immutable; +using System.Text.Json; using Task = Microsoft.Build.Utilities.Task; namespace Agoda.CodeCompass.MSBuild; - -public class TechDebtReportTask : Task +// TechDebtSarifTask.cs +public class TechDebtSarifTask : Task { [Required] - public string[] AnalyzerAssemblyPaths { get; set; } = Array.Empty(); - - [Required] - public string[] CompilationAssemblyPaths { get; set; } = Array.Empty(); - - [Required] - public string[] SourceFiles { get; set; } = Array.Empty(); + public string InputPath { get; set; } = string.Empty; [Required] public string OutputPath { get; set; } = string.Empty; @@ -25,72 +22,75 @@ public override bool Execute() { try { - // Load all analyzer assemblies - var analyzers = LoadAnalyzers(); - - // Create compilation - var compilation = CreateCompilation(); - - // Run analysis - var compilationWithAnalyzers = compilation.WithAnalyzers( - ImmutableArray.Create(analyzers.ToArray())); - - var diagnostics = compilationWithAnalyzers - .GetAnalyzerDiagnosticsAsync() - .GetAwaiter() - .GetResult(); - - // Generate and save SARIF report - var sarifOutput = SarifReporter.GenerateSarifReport(diagnostics); - File.WriteAllText(OutputPath, sarifOutput); - + var inputSarif = File.ReadAllText(InputPath); + var inputDiagnostics = ParseSarifDiagnostics(inputSarif); + var techDebtSarif = SarifReporter.GenerateSarifReport(inputDiagnostics); + File.WriteAllText(OutputPath, techDebtSarif); return true; } + catch (InvalidDataException iex) + { + return false; + } catch (Exception ex) { - Log.LogError($"Failed to generate tech debt report: {ex.Message}"); + Log.LogError($"Failed to process SARIF report: {ex.Message}"); return false; } } - private IEnumerable LoadAnalyzers() + private IEnumerable ParseSarifDiagnostics(string sarifContent) { - foreach (var path in AnalyzerAssemblyPaths) - { - var assembly = System.Runtime.Loader.AssemblyLoadContext - .Default - .LoadFromAssemblyPath(path); - - var analyzerTypes = assembly.GetTypes() - .Where(t => !t.IsAbstract && - typeof(DiagnosticAnalyzer).IsAssignableFrom(t)); - - foreach (var analyzerType in analyzerTypes) - { - if (Activator.CreateInstance(analyzerType) is DiagnosticAnalyzer analyzer) - { - yield return analyzer; - } - } - } + // Parse SARIF JSON into list of Diagnostics + var sarif = JsonSerializer.Deserialize(sarifContent, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); + return sarif.Runs.SelectMany(r => r.Results) + .Select(r => CreateDiagnosticFromSarif(r)); } - private Compilation CreateCompilation() + private Diagnostic CreateDiagnosticFromSarif(Result result) { - var syntaxTrees = SourceFiles - .Select(file => Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree - .ParseText(File.ReadAllText(file), path: file)) - .ToArray(); + if (result.Locations.Length == 0) throw new InvalidDataException("Sarif input was wrong"); + + var lineSpan = result.Locations.FirstOrDefault()?.PhysicalLocation?.Region; + var linePosition = new LinePositionSpan( + new LinePosition( + (lineSpan?.StartLine ?? 1) - 1, + (lineSpan?.StartColumn ?? 1) - 1), + new LinePosition( + (lineSpan?.EndLine ?? 1) - 1, + (lineSpan?.EndColumn ?? 1) - 1) + ); - var references = CompilationAssemblyPaths - .Select(path => MetadataReference.CreateFromFile(path)) - .ToArray(); + var filePath = result.Locations.FirstOrDefault()?.PhysicalLocation?.ArtifactLocation?.Uri ?? ""; + var sourceText = SourceText.From(File.ReadAllText(filePath)); + var syntaxTree = CSharpSyntaxTree.ParseText(sourceText); + + var location = Microsoft.CodeAnalysis.Location.Create( + syntaxTree, + new TextSpan(0, 0)); + + var descriptor = new DiagnosticDescriptor( + id: result.RuleId, + title: result.Message.Text, + messageFormat: result.Message.Text, + category: result.Properties?.TechDebt?.Category ?? "Default", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + var properties = ImmutableDictionary.CreateBuilder(); + if (result.Properties?.TechDebt != null) + { + properties.Add("techDebtMinutes", result.Properties.TechDebt.Minutes.ToString()); + properties.Add("techDebtCategory", result.Properties.TechDebt.Category); + properties.Add("techDebtPriority", result.Properties.TechDebt.Priority); + properties.Add("techDebtRationale", result.Properties.TechDebt.Rationale); + properties.Add("techDebtRecommendation", result.Properties.TechDebt.Recommendation); + } - return Microsoft.CodeAnalysis.CSharp.CSharpCompilation.Create( - "TechDebtAnalysis", - syntaxTrees, - references, - new Microsoft.CodeAnalysis.CSharp.CSharpCompilationOptions( - (OutputKind)Microsoft.CodeAnalysis.CSharp.LanguageVersion.Latest)); + return Diagnostic.Create( + descriptor, + location, + properties.ToImmutable(), + result.Message.Text); } } \ No newline at end of file diff --git a/src/Agoda.CodeCompass.MSBuild/Tool.cs b/src/Agoda.CodeCompass.MSBuild/Tool.cs new file mode 100644 index 0000000..5d8203f --- /dev/null +++ b/src/Agoda.CodeCompass.MSBuild/Tool.cs @@ -0,0 +1,6 @@ +namespace Agoda.CodeCompass.MSBuild; + +public class Tool +{ + public ToolDriver Driver { get; set; } = new(); +} \ No newline at end of file diff --git a/src/Agoda.CodeCompass.MSBuild/ToolDriver.cs b/src/Agoda.CodeCompass.MSBuild/ToolDriver.cs new file mode 100644 index 0000000..f1d2807 --- /dev/null +++ b/src/Agoda.CodeCompass.MSBuild/ToolDriver.cs @@ -0,0 +1,9 @@ +namespace Agoda.CodeCompass.MSBuild; + +public class ToolDriver +{ + public string Name { get; set; } = "Agoda.CodeCompass"; + public string SemanticVersion { get; set; } = "1.0.0"; + public string InformationUri { get; set; } = "https://agoda.github.io/code-compass"; + public Rule[] Rules { get; set; } = Array.Empty(); +} \ No newline at end of file diff --git a/src/Agoda.CodeCompass.MSBuild/build/Agoda.CodeCompass.MSBuild.targets b/src/Agoda.CodeCompass.MSBuild/build/Agoda.CodeCompass.MSBuild.targets index a5e1747..6ea5013 100644 --- a/src/Agoda.CodeCompass.MSBuild/build/Agoda.CodeCompass.MSBuild.targets +++ b/src/Agoda.CodeCompass.MSBuild/build/Agoda.CodeCompass.MSBuild.targets @@ -1,19 +1,15 @@ - + - - - - - - + + + $(MSBuildProjectDirectory)/buildlog.sarif + - - + + \ No newline at end of file diff --git a/src/CodeCompass.sln b/src/CodeCompass.sln index e4c9e07..1145e3c 100644 --- a/src/CodeCompass.sln +++ b/src/CodeCompass.sln @@ -3,8 +3,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.11.35327.3 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Agoda.CodeCompass", "Agoda.CodeCompass\Agoda.CodeCompass.csproj", "{7CB8A73E-2302-4BD8-A7AC-F6D6FF60204E}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Agoda.CodeCompass.MSBuild", "Agoda.CodeCompass.MSBuild\Agoda.CodeCompass.MSBuild.csproj", "{0BB5F054-97D3-4B0C-9398-EAB69D258E7B}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{314049A9-07D2-46B3-BBC7-EC97DEBDE59F}" @@ -12,20 +10,22 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ..\README.md = ..\README.md EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Agoda.CodeCompass.MSBuild.Tests", "Agoda.CodeCompass.MSBuild.Tests\Agoda.CodeCompass.MSBuild.Tests.csproj", "{DB2A783C-2700-4778-96B6-EB83E8121C97}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {7CB8A73E-2302-4BD8-A7AC-F6D6FF60204E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7CB8A73E-2302-4BD8-A7AC-F6D6FF60204E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7CB8A73E-2302-4BD8-A7AC-F6D6FF60204E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7CB8A73E-2302-4BD8-A7AC-F6D6FF60204E}.Release|Any CPU.Build.0 = Release|Any CPU {0BB5F054-97D3-4B0C-9398-EAB69D258E7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0BB5F054-97D3-4B0C-9398-EAB69D258E7B}.Debug|Any CPU.Build.0 = Debug|Any CPU {0BB5F054-97D3-4B0C-9398-EAB69D258E7B}.Release|Any CPU.ActiveCfg = Release|Any CPU {0BB5F054-97D3-4B0C-9398-EAB69D258E7B}.Release|Any CPU.Build.0 = Release|Any CPU + {DB2A783C-2700-4778-96B6-EB83E8121C97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DB2A783C-2700-4778-96B6-EB83E8121C97}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DB2A783C-2700-4778-96B6-EB83E8121C97}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DB2A783C-2700-4778-96B6-EB83E8121C97}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE