├── Test ├── Usings.cs ├── gl-quality2.json ├── gl-quality3.json ├── gl-quality1.json ├── roslynator.xml ├── codeanalysis.sarif.json ├── codeanalysis2.sarif.json ├── codeanalysis.sarif21.json ├── TestRoslynator.cs ├── TestTransform.cs ├── crash.sarif.json ├── Nested │ └── codeanalysis2.sarif.json ├── TestMergecs.cs ├── Test.csproj ├── TestSarif.cs ├── codeanalysis.sarif3.json ├── codeanalysis.sarif2.json └── codeanalysis.sarif4.json ├── CodeQualityToGitlab.sln.DotSettings ├── .github └── workflows │ └── action.yml ├── CodeQualityToGitlab ├── SarifConverters │ ├── SarifConverter.cs │ ├── Converter2.cs │ └── Converter1.cs ├── CodeQuality.cs ├── Common.cs ├── Merger.cs ├── CodeQualityToGitlab.csproj ├── Transform.cs ├── Roslynator.cs ├── RoslynatorConverter.cs └── Program.cs ├── LICENSE ├── CodeQualityToGitlab.sln ├── README.md └── .gitignore /Test/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; 2 | -------------------------------------------------------------------------------- /Test/gl-quality2.json: -------------------------------------------------------------------------------- 1 | [{"description":"CS8618: Non-nullable property \u0027Name\u0027 must contain a non-null value when exiting constructor. Consider declaring the property as nullable.","fingerprint":"10E2DA0085F71F06D782272E0BE393B0","severity":"major","location":{"path":"dev/example\\Reader.cs","lines":{"begin":12}}}] -------------------------------------------------------------------------------- /Test/gl-quality3.json: -------------------------------------------------------------------------------- 1 | [{"description":"CS8618: Non-nullable property \u0027Name\u0027 must contain a non-null value when exiting constructor. Consider declaring the property as nullable.","fingerprint":"10E2DA0085F71F06D782272E0BE393A0","severity":"major","location":{"path":"dev/example\\Reader.cs","lines":{"begin":12}}}] -------------------------------------------------------------------------------- /CodeQualityToGitlab.sln.DotSettings: -------------------------------------------------------------------------------- 1 | 2 | True -------------------------------------------------------------------------------- /Test/gl-quality1.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "CS8618: Non-nullable property \u0027Name\u0027 must contain a non-null value when exiting constructor. Consider declaring the property as nullable.", 4 | "fingerprint": "10E2DA0085F71F06D782272E0BE393A0", 5 | "severity": "major", 6 | "location": { 7 | "path": "dev/example\\Reader.cs", 8 | "lines": { 9 | "begin": 12 10 | } 11 | } 12 | }, 13 | { 14 | "description": "CUSTOM", 15 | "fingerprint": "10E2DA0085F71F06D782272E0BE393A2", 16 | "severity": "minor", 17 | "location": { 18 | "path": "dev/example\\Reader.cs", 19 | "lines": { 20 | "begin": 12 21 | } 22 | } 23 | } 24 | ] -------------------------------------------------------------------------------- /.github/workflows/action.yml: -------------------------------------------------------------------------------- 1 | name: dotnet package 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - name: Setup .NET Core SDK 11 | uses: actions/setup-dotnet@v3 12 | with: 13 | dotnet-version: | 14 | 8 15 | 9 16 | - name: Install dependencies 17 | run: dotnet restore 18 | - name: Build 19 | run: dotnet build --configuration Release --no-restore 20 | - name: Test 21 | run: dotnet test --no-restore --verbosity normal 22 | - name: Nuget 23 | if: startsWith(github.ref, 'refs/heads/main') 24 | run: dotnet nuget push ./CodeQualityToGitlab/nupkg/CodeQualityToGitlab.*.nupkg --source 'https://api.nuget.org/v3/index.json' --api-key ${{secrets.NUGET_API_KEY}} -------------------------------------------------------------------------------- /CodeQualityToGitlab/SarifConverters/SarifConverter.cs: -------------------------------------------------------------------------------- 1 | namespace CodeQualityToGitlab.SarifConverters; 2 | 3 | public static class SarifConverter 4 | { 5 | public static List ConvertToCodeQualityRaw(FileInfo source, string? pathRoot) 6 | { 7 | var logContents = File.ReadAllText(source.FullName); 8 | 9 | return logContents.Contains(" \"$schema\": \"http://json.schemastore.org/sarif-2") 10 | ? new Converter2(source, pathRoot).Convert() 11 | : new Converter1(source, pathRoot).Convert(); 12 | } 13 | 14 | public static void ConvertToCodeQuality( 15 | FileInfo source, 16 | FileInfo target, 17 | string? pathRoot = null 18 | ) 19 | { 20 | var cqrs = ConvertToCodeQualityRaw(source, pathRoot); 21 | Common.WriteToDisk(target, cqrs); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /CodeQualityToGitlab/CodeQuality.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | namespace CodeQualityToGitlab; 5 | 6 | [DebuggerDisplay("{Description} {Severity} {Location.Path} {Location.Lines.Begin}")] 7 | public class CodeQuality 8 | { 9 | public required string Description { get; set; } 10 | public required string Fingerprint { get; set; } 11 | public required Severity Severity { get; set; } 12 | public required LocationCq Location { get; set; } 13 | } 14 | 15 | [DebuggerDisplay("{Path} {Lines.Begin}")] 16 | public class LocationCq 17 | { 18 | public required string Path { get; set; } 19 | public required Lines Lines { get; set; } 20 | } 21 | 22 | [DebuggerDisplay("{Begin}")] 23 | public class Lines 24 | { 25 | public required int Begin { get; set; } 26 | } 27 | 28 | [SuppressMessage("ReSharper", "InconsistentNaming")] 29 | public enum Severity 30 | { 31 | info, 32 | minor, 33 | major, 34 | critical, 35 | blocker 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 codecentric AG 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /CodeQualityToGitlab/Common.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | using System.Text; 3 | using System.Text.Json; 4 | using System.Text.Json.Serialization; 5 | using Serilog; 6 | 7 | namespace CodeQualityToGitlab; 8 | 9 | internal static class Common 10 | { 11 | public static string GetHash(string input) 12 | { 13 | var inputBytes = Encoding.ASCII.GetBytes(input); 14 | var hashBytes = MD5.HashData(inputBytes); 15 | 16 | return Convert.ToHexString(hashBytes); 17 | } 18 | 19 | public static void WriteToDisk(FileInfo target, IEnumerable result) 20 | { 21 | var options = new JsonSerializerOptions 22 | { 23 | WriteIndented = true, 24 | PropertyNamingPolicy = new LowerCaseNamingPolicy(), 25 | Converters = { new JsonStringEnumConverter() } 26 | }; 27 | using var fileStream = File.Create(target.FullName); 28 | using var utf8JsonWriter = new Utf8JsonWriter(fileStream); 29 | JsonSerializer.Serialize(utf8JsonWriter, result, options); 30 | Log.Information("Result written to: {TargetFullName}", target.FullName); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /CodeQualityToGitlab.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeQualityToGitlab", "CodeQualityToGitlab\CodeQualityToGitlab.csproj", "{AB18A911-AEFC-476C-AE9B-35EFDDAC9167}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Test", "Test\Test.csproj", "{3DBC2C5A-BDE5-48DF-85FA-CAA118226B96}" 6 | EndProject 7 | Global 8 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 9 | Debug|Any CPU = Debug|Any CPU 10 | Release|Any CPU = Release|Any CPU 11 | EndGlobalSection 12 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 13 | {AB18A911-AEFC-476C-AE9B-35EFDDAC9167}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 14 | {AB18A911-AEFC-476C-AE9B-35EFDDAC9167}.Debug|Any CPU.Build.0 = Debug|Any CPU 15 | {AB18A911-AEFC-476C-AE9B-35EFDDAC9167}.Release|Any CPU.ActiveCfg = Release|Any CPU 16 | {AB18A911-AEFC-476C-AE9B-35EFDDAC9167}.Release|Any CPU.Build.0 = Release|Any CPU 17 | {3DBC2C5A-BDE5-48DF-85FA-CAA118226B96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 18 | {3DBC2C5A-BDE5-48DF-85FA-CAA118226B96}.Debug|Any CPU.Build.0 = Debug|Any CPU 19 | {3DBC2C5A-BDE5-48DF-85FA-CAA118226B96}.Release|Any CPU.ActiveCfg = Release|Any CPU 20 | {3DBC2C5A-BDE5-48DF-85FA-CAA118226B96}.Release|Any CPU.Build.0 = Release|Any CPU 21 | EndGlobalSection 22 | EndGlobal 23 | -------------------------------------------------------------------------------- /Test/roslynator.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Info 9 | Use Length/Count property instead of Count() when available 10 | C:\dev\example\TestFirebirdImport.cs 11 | 12 | 13 | 14 | 15 | Info 16 | Use Length/Count property instead of Count() when available 17 | C:\dev\example\TestFirebirdImport.cs 18 | 19 | 20 | 21 | Info 22 | Use Length/Count property instead of Count() when available 23 | C:\dev\example\TestFirebirdImport.cs 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Test/codeanalysis.sarif.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/sarif-1.0.0", 3 | "version": "1.0.0", 4 | "runs": [ 5 | { 6 | "tool": { 7 | "name": "Microsoft (R) Visual C# Compiler", 8 | "version": "4.4.0.0", 9 | "fileVersion": "4.4.0-4.22520.11 (9e075f03)", 10 | "semanticVersion": "4.4.0", 11 | "language": "en-US" 12 | }, 13 | "results": [ 14 | { 15 | "ruleId": "CS8618", 16 | "level": "warning", 17 | "message": "Non-nullable property 'Name' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.", 18 | "locations": [ 19 | { 20 | "resultFile": { 21 | "uri": "file:///C:/dev/example//Reader.cs", 22 | "region": { 23 | "startLine": 12, 24 | "startColumn": 16, 25 | "endLine": 12, 26 | "endColumn": 25 27 | } 28 | } 29 | } 30 | ], 31 | "relatedLocations": [ 32 | { 33 | "physicalLocation": { 34 | "uri": "file:///C:/dev/example//Reader.cs", 35 | "region": { 36 | "startLine": 7, 37 | "startColumn": 23, 38 | "endLine": 7, 39 | "endColumn": 27 40 | } 41 | } 42 | } 43 | ], 44 | "properties": { 45 | "warningLevel": 1 46 | } 47 | } 48 | 49 | ] 50 | } 51 | ] 52 | } -------------------------------------------------------------------------------- /Test/codeanalysis2.sarif.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/sarif-1.0.0", 3 | "version": "1.0.0", 4 | "runs": [ 5 | { 6 | "tool": { 7 | "name": "Microsoft (R) Visual C# Compiler", 8 | "version": "4.4.0.0", 9 | "fileVersion": "4.4.0-4.22520.11 (9e075f03)", 10 | "semanticVersion": "4.4.0", 11 | "language": "en-US" 12 | }, 13 | "results": [ 14 | { 15 | "ruleId": "CS8618", 16 | "level": "warning", 17 | "message": "Non-nullable property 'Name' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.", 18 | "locations": [ 19 | { 20 | "resultFile": { 21 | "uri": "file:///C:/dev/example//Reader2.cs", 22 | "region": { 23 | "startLine": 12, 24 | "startColumn": 16, 25 | "endLine": 12, 26 | "endColumn": 25 27 | } 28 | } 29 | } 30 | ], 31 | "relatedLocations": [ 32 | { 33 | "physicalLocation": { 34 | "uri": "file:///C:/dev/example//Reader2.cs", 35 | "region": { 36 | "startLine": 7, 37 | "startColumn": 23, 38 | "endLine": 7, 39 | "endColumn": 27 40 | } 41 | } 42 | } 43 | ], 44 | "properties": { 45 | "warningLevel": 1 46 | } 47 | } 48 | 49 | ] 50 | } 51 | ] 52 | } -------------------------------------------------------------------------------- /Test/codeanalysis.sarif21.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.1.0", 3 | "$schema": "http://json.schemastore.org/sarif-2.1.0-rtm.4", 4 | "runs": [ 5 | { 6 | "tool": { 7 | "driver": { 8 | "name": "ESLint", 9 | "informationUri": "https://eslint.org", 10 | "rules": [ 11 | { 12 | "id": "no-unused-vars", 13 | "shortDescription": { 14 | "text": "disallow unused variables" 15 | }, 16 | "helpUri": "https://eslint.org/docs/rules/no-unused-vars", 17 | "properties": { 18 | "category": "Variables" 19 | } 20 | } 21 | ] 22 | } 23 | }, 24 | "artifacts": [ 25 | { 26 | "location": { 27 | "uri": "file:///C:/dev/sarif/sarif-tutorials/samples/Introduction/simple-example.js" 28 | } 29 | } 30 | ], 31 | "results": [ 32 | { 33 | "level": "error", 34 | "message": { 35 | "text": "'x' is assigned a value but never used." 36 | }, 37 | "locations": [ 38 | { 39 | "physicalLocation": { 40 | "artifactLocation": { 41 | "uri": "file:///C:/dev/sarif/sarif-tutorials/samples/Introduction/simple-example.js", 42 | "index": 0 43 | }, 44 | "region": { 45 | "startLine": 1, 46 | "startColumn": 5 47 | } 48 | } 49 | } 50 | ], 51 | "ruleId": "no-unused-vars", 52 | "ruleIndex": 0 53 | } 54 | ] 55 | } 56 | ] 57 | } -------------------------------------------------------------------------------- /Test/TestRoslynator.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | using CodeQualityToGitlab; 5 | using FluentAssertions; 6 | 7 | namespace Test; 8 | 9 | public class TestRoslynator 10 | { 11 | [Fact] 12 | public void TestRoslynatorWorks() 13 | { 14 | var source = new FileInfo("roslynator.xml"); 15 | var target = new FileInfo(Path.GetTempFileName()); 16 | 17 | RoslynatorConverter.ConvertToCodeQuality( 18 | source, 19 | target, 20 | "C:\\dev" + Path.DirectorySeparatorChar 21 | ); 22 | 23 | var options = new JsonSerializerOptions 24 | { 25 | WriteIndented = true, 26 | PropertyNamingPolicy = new LowerCaseNamingPolicy(), 27 | Converters = { new JsonStringEnumConverter() } 28 | }; 29 | 30 | using var r = new StreamReader(target.FullName); 31 | var json = r.ReadToEnd(); 32 | var result = JsonSerializer.Deserialize>(json, options); 33 | 34 | result.Should().HaveCount(3); 35 | var codeQuality = result!.First(); 36 | codeQuality 37 | .Description 38 | .Should() 39 | .Be("CA1829: Use Length/Count property instead of Count() when available"); 40 | codeQuality.Severity.Should().Be(Severity.info); 41 | 42 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 43 | { 44 | // seems not to work on non windows atm 45 | codeQuality.Location.Path.Should().Be("example\\TestFirebirdImport.cs"); 46 | } 47 | 48 | codeQuality.Location.Lines.Begin.Should().Be(80); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Test/TestTransform.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | using CodeQualityToGitlab; 4 | using FluentAssertions; 5 | 6 | namespace Test; 7 | 8 | public class TestTransform 9 | { 10 | private static readonly JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions 11 | { 12 | WriteIndented = true, 13 | PropertyNamingPolicy = new LowerCaseNamingPolicy(), 14 | Converters = { new JsonStringEnumConverter() } 15 | }; 16 | 17 | [Fact] 18 | public void TestTransformAllWorks() 19 | { 20 | var target = new FileInfo(Path.GetTempFileName()); 21 | 22 | Transform.TransformAll("**/**.sarif.json", "**/*roslynator.xml", target, null, true); 23 | 24 | var options = JsonSerializerOptions; 25 | 26 | using var r = new StreamReader(target.FullName); 27 | var json = r.ReadToEnd(); 28 | var result = JsonSerializer.Deserialize>(json, options); 29 | 30 | result.Should().HaveCount(8); 31 | } 32 | 33 | [Fact] 34 | public void TestTHandlesDotsInPathsForSarif1() 35 | { 36 | var target = new FileInfo(Path.GetTempFileName()); 37 | 38 | Transform.TransformAll("codeanalysis.sarif4.json", "", target, "/builds/folder/backend/", true); 39 | 40 | var options = JsonSerializerOptions; 41 | 42 | using var r = new StreamReader(target.FullName); 43 | var json = r.ReadToEnd(); 44 | var result = JsonSerializer.Deserialize>(json, options); 45 | 46 | result.Should().NotBeNull(); 47 | result!.First().Location.Path.Should().Contain("SR.CLI"); 48 | 49 | result.Should().HaveCount(4); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Test/crash.sarif.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-1.0.0.json", 3 | "version": "1.0.0", 4 | "runs": [ 5 | { 6 | "tool": { 7 | "name": "PREfast", 8 | "fullName": "PREfast Code Analysis", 9 | "version": "14.29.30148.0", 10 | "language": "en-US" 11 | }, 12 | "invocation": { 13 | "commandLine": "cmd" 14 | }, 15 | "files": { 16 | "file:///c:/dev/app/stdafx.cpp": { 17 | "hashes": [ 18 | { 19 | "value": "72febd207eaaf204298b40f071737e0c", 20 | "algorithm": "unknown" 21 | } 22 | ] 23 | } 24 | }, 25 | "results": [] 26 | }, 27 | { 28 | "tool": { 29 | "name": "PREfast", 30 | "fullName": "PREfast Code Analysis", 31 | "version": "14.29.30148.0", 32 | "language": "en-US" 33 | }, 34 | "invocation": { 35 | "commandLine": "cmd" 36 | }, 37 | "files": { 38 | "file:///c:/dev/app/mainApp.cpp": { 39 | "hashes": [ 40 | { 41 | "value": "6f448b3da75733e7b8e86ef60c97894e", 42 | "algorithm": "unknown" 43 | } 44 | ] 45 | } 46 | }, 47 | "results": [ 48 | { 49 | "ruleId": "C26451", 50 | "message": "Arithmetic overflow: Using operator '+' on a 4 byte value and then casting the result to a 8 byte value. Cast the value to the wider type before calling operator '+' to avoid overflow (io.2).", 51 | "locations": [ 52 | { 53 | "resultFile": { 54 | "region": { 55 | "startLine": 86, 56 | "startColumn": 65, 57 | "endLine": 86, 58 | "endColumn": 77 59 | } 60 | } 61 | } 62 | ] 63 | } 64 | ] 65 | } 66 | ] 67 | } -------------------------------------------------------------------------------- /CodeQualityToGitlab/Merger.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | using Serilog; 4 | 5 | namespace CodeQualityToGitlab; 6 | 7 | public static class Merger 8 | { 9 | public static void Merge(FileInfo[] sources, FileInfo target, bool bumpToMajor) 10 | { 11 | Log.Information("bump to major is: {BumpToMajor}", bumpToMajor); 12 | var result = new List(); 13 | var options = new JsonSerializerOptions 14 | { 15 | WriteIndented = true, 16 | PropertyNamingPolicy = new LowerCaseNamingPolicy(), 17 | Converters = { new JsonStringEnumConverter() } 18 | }; 19 | 20 | foreach (var source in sources) 21 | { 22 | if (!source.Exists) 23 | { 24 | throw new FileNotFoundException( 25 | $"The file '{source.FullName}' does not exist", 26 | source.FullName 27 | ); 28 | } 29 | 30 | using var f = source.OpenRead(); 31 | try 32 | { 33 | var data = JsonSerializer.Deserialize>(f, options); 34 | 35 | if (data == null) 36 | { 37 | throw new ArgumentNullException( 38 | $"could not deserialize content of {source.FullName}" 39 | ); 40 | } 41 | result.AddRange(data); 42 | } 43 | catch (Exception e) 44 | { 45 | Log.Error(e, "Error while deserializing file {SourceFullName}", source.FullName); 46 | throw; 47 | } 48 | } 49 | 50 | result = result.DistinctBy(x => x.Fingerprint).ToList(); 51 | 52 | if (bumpToMajor) 53 | { 54 | foreach ( 55 | var cqr in result.Where(cqr => cqr.Severity is Severity.minor or Severity.info) 56 | ) 57 | { 58 | cqr.Severity = Severity.major; 59 | } 60 | } 61 | 62 | Common.WriteToDisk(target, result); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /CodeQualityToGitlab/CodeQualityToGitlab.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0;net9.0 6 | enable 7 | enable 8 | true 9 | cq 10 | ./nupkg 11 | 2.0.0 12 | codecentric 13 | (c) codecentric 14 | https://github.com/codecentric/dotnet_gitlab_code_quality 15 | Convert Dotnet warnings into Gitlab code quality format 16 | https://github.com/codecentric/dotnet_gitlab_code_quality.git 17 | git 18 | Gitlab; code quality; roslynator 19 | MIT 20 | README.md 21 | true 22 | true 23 | latest 24 | 25 | 26 | 27 | 28 | 29 | 30 | all 31 | runtime; build; native; contentfiles; analyzers; buildtransitive 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /CodeQualityToGitlab/Transform.cs: -------------------------------------------------------------------------------- 1 | using CodeQualityToGitlab.SarifConverters; 2 | using Microsoft.Extensions.FileSystemGlobbing; 3 | using Microsoft.Extensions.FileSystemGlobbing.Abstractions; 4 | using Serilog; 5 | 6 | namespace CodeQualityToGitlab; 7 | 8 | public static class Transform 9 | { 10 | private static IEnumerable TransformAllRaw( 11 | string sarifGlob, 12 | string roslynatorGlob, 13 | string? pathRoot 14 | ) 15 | { 16 | var allIssues = new List(); 17 | Process(sarifGlob, pathRoot, allIssues, SarifConverter.ConvertToCodeQualityRaw); 18 | Process(roslynatorGlob, pathRoot, allIssues, RoslynatorConverter.ConvertToCodeQualityRaw); 19 | 20 | return allIssues; 21 | } 22 | 23 | private static void Process( 24 | string roslynatorGlob, 25 | string? pathRoot, 26 | List allIssues, 27 | Func> processFunc 28 | ) 29 | { 30 | Matcher matcher = new(); 31 | matcher.AddIncludePatterns([roslynatorGlob]); 32 | 33 | const string searchDirectory = "."; 34 | 35 | var currentDir = new DirectoryInfo(searchDirectory); 36 | var result = matcher.Execute(new DirectoryInfoWrapper(currentDir)); 37 | 38 | if (!result.HasMatches) 39 | { 40 | Log.Warning( 41 | "No matching files found for pattern: {Pattern} in {CurrentDir}", 42 | roslynatorGlob, 43 | currentDir.FullName 44 | ); 45 | } 46 | 47 | foreach (var match in result.Files) 48 | { 49 | var toTransform = match.Path; 50 | Log.Information("Processing: {File}", toTransform); 51 | var cqrs = processFunc(new(toTransform), pathRoot); 52 | allIssues.AddRange(cqrs); 53 | } 54 | } 55 | 56 | public static void TransformAll( 57 | string sarifGlob, 58 | string roslynatorGlob, 59 | FileInfo target, 60 | string? pathRoot, 61 | bool bumpToMajor 62 | ) 63 | { 64 | var cqrs = TransformAllRaw(sarifGlob, roslynatorGlob, pathRoot); 65 | 66 | cqrs = cqrs.DistinctBy(x => x.Fingerprint).ToList(); 67 | 68 | if (bumpToMajor) 69 | { 70 | foreach (var cqr in cqrs.Where(cqr => cqr.Severity is Severity.minor or Severity.info)) 71 | { 72 | cqr.Severity = Severity.major; 73 | } 74 | } 75 | 76 | Common.WriteToDisk(target, cqrs); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /CodeQualityToGitlab/Roslynator.cs: -------------------------------------------------------------------------------- 1 | namespace CodeQualityToGitlab; 2 | 3 | using System.Xml.Serialization; 4 | 5 | [XmlRoot(ElementName = "Diagnostic")] 6 | public class Diagnostic 7 | { 8 | [XmlAttribute(AttributeName = "Id")] 9 | public required string Id { get; set; } 10 | 11 | [XmlAttribute(AttributeName = "Title")] 12 | public required string Title { get; set; } 13 | 14 | [XmlAttribute(AttributeName = "Count")] 15 | public required string Count { get; set; } 16 | 17 | [XmlElement(ElementName = "Severity")] 18 | public required string Severity { get; set; } 19 | 20 | [XmlElement(ElementName = "Message")] 21 | public required string Message { get; set; } 22 | 23 | [XmlElement(ElementName = "FilePath")] 24 | public string? FilePath { get; set; } 25 | 26 | [XmlElement(ElementName = "Location")] 27 | public Location? Location { get; set; } 28 | } 29 | 30 | [XmlRoot(ElementName = "Summary")] 31 | public class Summary 32 | { 33 | [XmlElement(ElementName = "Diagnostic")] 34 | public required List Diagnostic { get; set; } = [ ]; 35 | } 36 | 37 | [XmlRoot(ElementName = "Location")] 38 | public class Location 39 | { 40 | [XmlAttribute(AttributeName = "Line")] 41 | public required string Line { get; set; } 42 | 43 | [XmlAttribute(AttributeName = "Character")] 44 | public required string Character { get; set; } 45 | } 46 | 47 | [XmlRoot(ElementName = "Diagnostics")] 48 | public class Diagnostics 49 | { 50 | [XmlElement(ElementName = "Diagnostic")] 51 | public List Diagnostic { get; set; } = [ ]; 52 | } 53 | 54 | [XmlRoot(ElementName = "Project")] 55 | public class Project 56 | { 57 | [XmlElement(ElementName = "Diagnostics")] 58 | public required Diagnostics Diagnostics { get; set; } 59 | 60 | [XmlAttribute(AttributeName = "Name")] 61 | public required string Name { get; set; } 62 | 63 | [XmlAttribute(AttributeName = "FilePath")] 64 | public required string FilePath { get; set; } 65 | } 66 | 67 | [XmlRoot(ElementName = "Projects")] 68 | public class Projects 69 | { 70 | [XmlElement(ElementName = "Project")] 71 | public List Project { get; set; } = [ ]; 72 | } 73 | 74 | [XmlRoot(ElementName = "CodeAnalysis")] 75 | public class CodeAnalysis 76 | { 77 | [XmlElement(ElementName = "Summary")] 78 | public required Summary Summary { get; set; } 79 | 80 | [XmlElement(ElementName = "Projects")] 81 | public required Projects Projects { get; set; } 82 | } 83 | 84 | [XmlRoot(ElementName = "Roslynator")] 85 | public class Roslynator 86 | { 87 | [XmlElement(ElementName = "CodeAnalysis")] 88 | public required CodeAnalysis CodeAnalysis { get; set; } 89 | } 90 | -------------------------------------------------------------------------------- /Test/Nested/codeanalysis2.sarif.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/sarif-1.0.0", 3 | "version": "1.0.0", 4 | "runs": [ 5 | { 6 | "tool": { 7 | "name": "Microsoft (R) Visual C# Compiler", 8 | "version": "4.4.0.0", 9 | "fileVersion": "4.4.0-4.22520.11 (9e075f03)", 10 | "semanticVersion": "4.4.0", 11 | "language": "en-US" 12 | }, 13 | "results": [ 14 | { 15 | "ruleId": "CS8618", 16 | "level": "warning", 17 | "message": "Non-nullable property 'Name' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.", 18 | "locations": [ 19 | { 20 | "resultFile": { 21 | "uri": "file:///C:/dev/example//Reader3.cs", 22 | "region": { 23 | "startLine": 12, 24 | "startColumn": 16, 25 | "endLine": 12, 26 | "endColumn": 25 27 | } 28 | } 29 | } 30 | ], 31 | "relatedLocations": [ 32 | { 33 | "physicalLocation": { 34 | "uri": "file:///C:/dev/example//Reader3.cs", 35 | "region": { 36 | "startLine": 7, 37 | "startColumn": 23, 38 | "endLine": 7, 39 | "endColumn": 27 40 | } 41 | } 42 | } 43 | ], 44 | "properties": { 45 | "warningLevel": 1 46 | } 47 | }, 48 | { 49 | "ruleId": "CS86182", 50 | "level": "warning", 51 | "message": "Non-nullable property 'Name' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.", 52 | "locations": [ 53 | { 54 | "resultFile": { 55 | "uri": "file:///C:/dev/example//Reader4.cs", 56 | "region": { 57 | "startLine": 12, 58 | "startColumn": 16, 59 | "endLine": 12, 60 | "endColumn": 25 61 | } 62 | } 63 | } 64 | ], 65 | "relatedLocations": [ 66 | { 67 | "physicalLocation": { 68 | "uri": "file:///C:/dev/example//Reader4.cs", 69 | "region": { 70 | "startLine": 7, 71 | "startColumn": 23, 72 | "endLine": 7, 73 | "endColumn": 27 74 | } 75 | } 76 | } 77 | ], 78 | "properties": { 79 | "warningLevel": 1 80 | } 81 | } 82 | 83 | ] 84 | } 85 | ] 86 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dotnet_gitlab_code_quality 2 | 3 | ## What does it do? 4 | 5 | Gitlabs Code Quality Issue format is different from the format used by .Net to report code quality issues (Sarif 1.0 as of the time of writing). Reporting code quality issues in Gitlab is therefore not really possible. 6 | This tool aims to rectify this problem by offering three functions: 7 | 8 | - Convert Microsoft Build time code quality issues into Gitlabs format 9 | - Convert Roslynator issues into Gitlabs format 10 | - Merge multiple Gitlab files into one file 11 | 12 | ### Example: 13 | 14 | I assume that you have your Project at c:\dev\myproject and you have build it, so that a codequality file exists at `c:\dev\myproject\codeanalysis.sarif.json` 15 | 16 | Now we want to generate a Gitlab compatible file: 17 | ```shell 18 | dotnet tool run cq sarif codeanalysis.sarif.json targetfile.json c:/dev 19 | ``` 20 | 21 | For Roslynator: 22 | ```shell 23 | dotnet tool run cq roslynator roslynator.xml targetfile.json c:/dev 24 | ``` 25 | 26 | For merging: 27 | 28 | ```shell 29 | dotnet tool run cq merge target.json source1.json source2.json 30 | ``` 31 | 32 | Note the third argument, it is used to report only the path relative to the repository, not the full local path. 33 | Now you can upload your file in Gitlab und you SHOULD be able to see it in the merge view 34 | Gotcha: Gitlab compares issues to the target of the merge. When there are no issues in the target branch, it will not display anything. So please run this tool on your main branch first then open a merge request to see it in the Gitlab UI. 35 | 36 | All in one: 37 | 38 | ```shell 39 | dotnet tool run cq transform '**/*.sarif.json' '**/roslynator.xml' gl-code-quality-report.json 40 | ``` 41 | 42 | This basically globs for the relevant files and merges them. 43 | 44 | 45 | Gitlab Pipeline should look like this: 46 | 47 | ```yaml 48 | code_quality_job: 49 | image: mcr.microsoft.com/dotnet/sdk:7.0 50 | stage: test 51 | script: 52 | - 'dotnet build ./MySln.sln' 53 | - 'dotnet tool run roslynator analyze ./MySln.sln -o roslynator.xml || true' 54 | - 'dotnet tool run cq roslynator.xml gl-code-quality-report.json c:\dev' 55 | artifacts: 56 | paths: 57 | - roslynator.xml 58 | - gl-code-quality-report.json 59 | expose_as: 'code_quality_reports' 60 | reports: 61 | codequality: gl-code-quality-report.json 62 | 63 | rules: 64 | - if: $CI_MERGE_REQUEST_ID 65 | - if: $CI_COMMIT_REF_NAME == "release" 66 | - if: $CI_COMMIT_REF_NAME == "develop" 67 | when: 68 | always 69 | allow_failure: false 70 | ``` 71 | 72 | ## How to install? 73 | 74 | For interactive usage 75 | 76 | ```shell 77 | dotnet tool install --global CodeQualityToGitlab --version 0.1.1 78 | ``` 79 | 80 | for pipeline use a manifest: 81 | 82 | 83 | ## How to contribute? 84 | 85 | Make a PR in this repo. 86 | 87 | ## Additional 88 | 89 | While dotnet only outputs Sarif 1, other projects use Sarif 2. For convenience, this library supports both Sarif versions -------------------------------------------------------------------------------- /CodeQualityToGitlab/RoslynatorConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Xml.Serialization; 3 | using Serilog; 4 | 5 | namespace CodeQualityToGitlab; 6 | 7 | public static class RoslynatorConverter 8 | { 9 | public static List ConvertToCodeQualityRaw(FileInfo source, string? pathRoot) 10 | { 11 | var serializer = new XmlSerializer(typeof(Roslynator)); 12 | 13 | var result = new List(); 14 | using Stream reader = new FileStream(source.FullName, FileMode.Open); 15 | var roslynator = (Roslynator)( 16 | serializer.Deserialize(reader) ?? throw new ArgumentException("no data") 17 | ); 18 | foreach (var project in roslynator.CodeAnalysis.Projects.Project) 19 | { 20 | Log.Information("Working on {ProjectName}", project.Name); 21 | 22 | foreach (var diagnostic in project.Diagnostics.Diagnostic) 23 | { 24 | var lineNumber = GetLineNumber(diagnostic); 25 | var cqr = new CodeQuality 26 | { 27 | Description = $"{diagnostic.Id}: {diagnostic.Message}", 28 | Severity = GetSeverity(diagnostic.Severity), 29 | Location = new() 30 | { 31 | Path = GetPath(diagnostic, project, pathRoot), 32 | Lines = new() { Begin = lineNumber } 33 | }, 34 | Fingerprint = Common.GetHash($"{project.Name}{diagnostic.Id}{lineNumber}") 35 | }; 36 | 37 | result.Add(cqr); 38 | } 39 | } 40 | 41 | return result; 42 | } 43 | 44 | public static void ConvertToCodeQuality(FileInfo source, FileInfo target, string? pathRoot) 45 | { 46 | var cqrs = ConvertToCodeQualityRaw(source, pathRoot); 47 | Common.WriteToDisk(target, cqrs); 48 | } 49 | 50 | private static string GetPath(Diagnostic diagnostic, Project project, string? pathRoot) 51 | { 52 | var path = diagnostic.FilePath ?? project.FilePath; 53 | 54 | if (string.IsNullOrWhiteSpace(pathRoot)) 55 | { 56 | return path; 57 | } 58 | 59 | var rv = path.Replace(pathRoot, ""); 60 | return rv; 61 | } 62 | 63 | private static int GetLineNumber(Diagnostic diagnostic) 64 | { 65 | return diagnostic.Location != null ? Convert.ToInt32(diagnostic.Location.Line) : 1; 66 | } 67 | 68 | private static Severity GetSeverity(string diagnosticSeverity) 69 | { 70 | return diagnosticSeverity switch 71 | { 72 | "Info" => Severity.info, 73 | "Warning" => Severity.major, 74 | "Error" => Severity.critical, 75 | "Hidden" => Severity.minor, 76 | _ 77 | => throw new ArgumentOutOfRangeException( 78 | diagnosticSeverity, 79 | $"unknown: {diagnosticSeverity}" 80 | ) 81 | }; 82 | } 83 | } 84 | 85 | public class LowerCaseNamingPolicy : JsonNamingPolicy 86 | { 87 | public override string ConvertName(string name) => name.ToLower(); 88 | } 89 | -------------------------------------------------------------------------------- /Test/TestMergecs.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | using CodeQualityToGitlab; 4 | using FluentAssertions; 5 | 6 | namespace Test; 7 | 8 | public class TestMerger 9 | { 10 | [Fact] 11 | public void TestMergerWorks() 12 | { 13 | var source1 = new FileInfo("gl-quality1.json"); 14 | var source2 = new FileInfo("gl-quality2.json"); 15 | var target = new FileInfo(Path.GetTempFileName()); 16 | 17 | Merger.Merge(new[] { source1, source2 }, target, false); 18 | 19 | var options = new JsonSerializerOptions 20 | { 21 | WriteIndented = true, 22 | PropertyNamingPolicy = new LowerCaseNamingPolicy(), 23 | Converters = { new JsonStringEnumConverter() } 24 | }; 25 | 26 | using var r = new StreamReader(target.FullName); 27 | var json = r.ReadToEnd(); 28 | var result = JsonSerializer.Deserialize>(json, options); 29 | 30 | result.Should().HaveCount(3); 31 | var codeQuality = result!.First(); 32 | codeQuality 33 | .Description 34 | .Should() 35 | .Be( 36 | "CS8618: Non-nullable property 'Name' must contain a non-null value when exiting constructor. Consider declaring the property as nullable." 37 | ); 38 | codeQuality.Severity.Should().Be(Severity.major); 39 | codeQuality.Location.Lines.Begin.Should().Be(12); 40 | } 41 | 42 | [Fact] 43 | public void TestMergerBumpsMinorToMajor() 44 | { 45 | var source1 = new FileInfo("gl-quality1.json"); 46 | var source2 = new FileInfo("gl-quality2.json"); 47 | var target = new FileInfo(Path.GetTempFileName()); 48 | 49 | Merger.Merge(new[] { source1, source2 }, target, true); 50 | 51 | var options = new JsonSerializerOptions 52 | { 53 | WriteIndented = true, 54 | PropertyNamingPolicy = new LowerCaseNamingPolicy(), 55 | Converters = { new JsonStringEnumConverter() } 56 | }; 57 | 58 | using var r = new StreamReader(target.FullName); 59 | var json = r.ReadToEnd(); 60 | var result = JsonSerializer.Deserialize>(json, options); 61 | 62 | result.Should().HaveCount(3); 63 | result.Should().AllSatisfy(x => x.Severity.Should().Be(Severity.major)); 64 | } 65 | 66 | [Fact] 67 | public void TestMergerRemovesDuplicates() 68 | { 69 | var source1 = new FileInfo("gl-quality1.json"); 70 | var source2 = new FileInfo("gl-quality2.json"); 71 | var source3 = new FileInfo("gl-quality3.json"); 72 | var target = new FileInfo(Path.GetTempFileName()); 73 | 74 | Merger.Merge(new[] { source1, source2, source3 }, target, true); 75 | 76 | var options = new JsonSerializerOptions 77 | { 78 | WriteIndented = true, 79 | PropertyNamingPolicy = new LowerCaseNamingPolicy(), 80 | Converters = { new JsonStringEnumConverter() } 81 | }; 82 | 83 | using var r = new StreamReader(target.FullName); 84 | var json = r.ReadToEnd(); 85 | var result = JsonSerializer.Deserialize>(json, options); 86 | 87 | result.Should().HaveCount(3); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /CodeQualityToGitlab/SarifConverters/Converter2.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis.Sarif; 2 | using Serilog; 3 | 4 | namespace CodeQualityToGitlab.SarifConverters; 5 | 6 | public class Converter2(FileInfo source, string? pathRoot) 7 | { 8 | public List Convert() 9 | { 10 | Log.Information("Sarif Version 2 detected"); 11 | 12 | var log = SarifLog.Load(source.FullName); 13 | 14 | var results = log.Runs 15 | .SelectMany(x => x.Results) 16 | .Where(r => r.Suppressions == null || r.Suppressions.Any()); 17 | 18 | var cqrs = new List(); 19 | foreach (var result in results) 20 | { 21 | var begin = result.Locations?.FirstOrDefault(); 22 | 23 | if (begin == null) 24 | { 25 | Log.Warning("An issue has no location, skipping: {@Result}", result.Message); 26 | continue; 27 | } 28 | 29 | try 30 | { 31 | var startLine = begin.PhysicalLocation.Region.StartLine; 32 | var cqr = new CodeQuality 33 | { 34 | Description = $"{result.RuleId}: {result.Message}", 35 | Severity = GetSeverity(result.Level), 36 | Location = new() 37 | { 38 | Path = GetPath(pathRoot, begin), 39 | Lines = new() { Begin = startLine } 40 | }, 41 | Fingerprint = Common.GetHash( 42 | $"{result.RuleId}|{begin.PhysicalLocation.ArtifactLocation.Uri}|{startLine}" 43 | ) 44 | }; 45 | cqrs.Add(cqr); 46 | } 47 | catch (Exception e) 48 | { 49 | Log.Error(e, "Could not convert {@Result}, skipping", result); 50 | } 51 | } 52 | 53 | return cqrs; 54 | } 55 | 56 | private static string GetPath(string? pathRoot, Microsoft.CodeAnalysis.Sarif.Location begin) 57 | { 58 | // nullability says Uri is always set, but there are tools which omit this. 59 | var artifactLocationUri = begin.PhysicalLocation.ArtifactLocation.Uri; 60 | if (artifactLocationUri == null) 61 | { 62 | Log.Error( 63 | "There is no valid Path for the issue {@Region}, cannot create a path. Check the source sarif for missing physicalLocation.ArtifactLocation.uri", 64 | begin.PhysicalLocation.ArtifactLocation 65 | ); 66 | return "noPathInSourceSarif"; 67 | } 68 | 69 | if (!artifactLocationUri.IsAbsoluteUri) 70 | { 71 | return artifactLocationUri.ToString(); 72 | } 73 | 74 | if (string.IsNullOrWhiteSpace(pathRoot)) 75 | { 76 | return artifactLocationUri.LocalPath.Replace("//", "\\"); 77 | } 78 | var uri = new Uri(pathRoot); 79 | return uri.MakeRelativeUri(artifactLocationUri).ToString().Replace("//", "\\"); 80 | } 81 | 82 | private static Severity GetSeverity(FailureLevel resultLevel) 83 | { 84 | return resultLevel switch 85 | { 86 | FailureLevel.None => Severity.minor, 87 | FailureLevel.Note => Severity.minor, 88 | FailureLevel.Warning => Severity.major, 89 | FailureLevel.Error => Severity.blocker, 90 | _ => throw new ArgumentOutOfRangeException(nameof(resultLevel), resultLevel, null) 91 | }; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Test/Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0;net9.0 5 | enable 6 | enable 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | all 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | PreserveNewest 33 | 34 | 35 | 36 | PreserveNewest 37 | 38 | 39 | 40 | PreserveNewest 41 | 42 | 43 | 44 | PreserveNewest 45 | 46 | 47 | 48 | PreserveNewest 49 | 50 | 51 | 52 | PreserveNewest 53 | 54 | 55 | PreserveNewest 56 | 57 | 58 | 59 | PreserveNewest 60 | 61 | 62 | 63 | PreserveNewest 64 | 65 | 66 | PreserveNewest 67 | 68 | 69 | PreserveNewest 70 | 71 | 72 | 73 | PreserveNewest 74 | 75 | 76 | PreserveNewest 77 | 78 | 79 | PreserveNewest 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /CodeQualityToGitlab/SarifConverters/Converter1.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis.Sarif.Readers; 2 | using Microsoft.CodeAnalysis.Sarif.VersionOne; 3 | using Newtonsoft.Json; 4 | using Serilog; 5 | 6 | namespace CodeQualityToGitlab.SarifConverters; 7 | 8 | public class Converter1(FileInfo source, string? pathRoot) 9 | { 10 | public List Convert() 11 | { 12 | Log.Information("Sarif Version 1 detected"); 13 | 14 | var logContents = File.ReadAllText(source.FullName); 15 | 16 | var settings = new JsonSerializerSettings 17 | { 18 | ContractResolver = SarifContractResolverVersionOne.Instance 19 | }; 20 | 21 | var log = JsonConvert.DeserializeObject(logContents, settings); 22 | 23 | var results = 24 | log?.Runs 25 | .SelectMany(x => x.Results) 26 | .Where(r => r.SuppressionStates == SuppressionStatesVersionOne.None) ?? [ ]; 27 | 28 | var cqrs = new List(); 29 | foreach (var result in results) 30 | { 31 | var begin = result.Locations?.FirstOrDefault(); 32 | 33 | if (begin == null) 34 | { 35 | Log.Warning("An issue has no location, skipping: {Result}", result.Message); 36 | continue; 37 | } 38 | 39 | try 40 | { 41 | var cqr = new CodeQuality 42 | { 43 | Description = $"{result.RuleId}: {result.Message}", 44 | Severity = GetSeverity(result.Level), 45 | Location = new() 46 | { 47 | Path = GetPathOld(pathRoot, begin), 48 | Lines = new() { Begin = begin.ResultFile.Region.StartLine } 49 | }, 50 | Fingerprint = Common.GetHash( 51 | $"{result.RuleId}|{begin.ResultFile.Uri}|{begin.ResultFile.Region.StartLine}" 52 | ) 53 | }; 54 | cqrs.Add(cqr); 55 | } 56 | catch (Exception e) 57 | { 58 | Log.Error(e, "Could not convert {@Result}, skipping", result); 59 | } 60 | } 61 | 62 | return cqrs; 63 | } 64 | 65 | private static string GetPathOld(string? pathRoot, LocationVersionOne begin) 66 | { 67 | // nullability says Uri is always set, but there are tools which omit this. 68 | if (begin.ResultFile.Uri == null) 69 | { 70 | Log.Error( 71 | "There is no valid Path for the issue {@Region}, cannot create a path. Check the source sarif for missing physicalLocation.uri", 72 | begin.ResultFile.Region 73 | ); 74 | return "noPathInSourceSarif"; 75 | } 76 | 77 | if (!begin.ResultFile.Uri!.IsAbsoluteUri) 78 | { 79 | return begin.ResultFile.Uri.ToString(); 80 | } 81 | 82 | if (string.IsNullOrWhiteSpace(pathRoot)) 83 | { 84 | return begin.ResultFile.Uri.LocalPath.Replace("//", "\\"); 85 | } 86 | var rootUri = GetUri(pathRoot); 87 | return rootUri.MakeRelativeUri(begin.ResultFile.Uri).ToString().Replace("//", "\\"); 88 | } 89 | 90 | 91 | private static Uri GetUri(string pathRoot) 92 | { 93 | if (Path.IsPathRooted(pathRoot)) 94 | { 95 | return new(new Uri("file://"),pathRoot); 96 | } 97 | 98 | return new(pathRoot); 99 | } 100 | 101 | private static Severity GetSeverity(ResultLevelVersionOne resultLevel) 102 | { 103 | return resultLevel switch 104 | { 105 | ResultLevelVersionOne.NotApplicable => Severity.minor, 106 | ResultLevelVersionOne.Pass => Severity.minor, 107 | ResultLevelVersionOne.Note => Severity.minor, 108 | ResultLevelVersionOne.Warning => Severity.major, 109 | ResultLevelVersionOne.Default => Severity.major, 110 | ResultLevelVersionOne.Error => Severity.blocker, 111 | _ => throw new ArgumentOutOfRangeException(nameof(resultLevel), resultLevel, null) 112 | }; 113 | } 114 | 115 | private static string NormalizeSeparators(string source) 116 | { 117 | return source.Replace(@"\\", @"\").Replace("//", @"\"); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Test/TestSarif.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | using CodeQualityToGitlab; 4 | using CodeQualityToGitlab.SarifConverters; 5 | using FluentAssertions; 6 | 7 | namespace Test; 8 | 9 | public class TestSarif 10 | { 11 | private static readonly JsonSerializerOptions JsonSerializerOptions = new() 12 | { 13 | WriteIndented = true, 14 | PropertyNamingPolicy = new LowerCaseNamingPolicy(), 15 | Converters = { new JsonStringEnumConverter() } 16 | }; 17 | 18 | [Fact] 19 | public void TestSarifWorks() 20 | { 21 | var source = new FileInfo("codeanalysis.sarif.json"); 22 | var target = new FileInfo(Path.GetTempFileName()); 23 | 24 | SarifConverter.ConvertToCodeQuality( 25 | source, 26 | target, 27 | $"C:{Path.DirectorySeparatorChar}dev" + Path.DirectorySeparatorChar 28 | ); 29 | 30 | var options = JsonSerializerOptions; 31 | 32 | using var r = new StreamReader(target.FullName); 33 | var json = r.ReadToEnd(); 34 | var result = JsonSerializer.Deserialize>(json, options); 35 | 36 | result.Should().HaveCount(1); 37 | var codeQuality = result!.First(); 38 | codeQuality 39 | .Description 40 | .Should() 41 | .Be( 42 | "CS8618: Non-nullable property 'Name' must contain a non-null value when exiting constructor. Consider declaring the property as nullable." 43 | ); 44 | codeQuality.Severity.Should().Be(Severity.major); 45 | codeQuality.Location.Path.Should().Be("example\\Reader.cs"); 46 | codeQuality.Location.Lines.Begin.Should().Be(12); 47 | } 48 | 49 | [Fact] 50 | public void TestSarifWorks2() 51 | { 52 | var source = new FileInfo("codeanalysis.sarif2.json"); 53 | var target = new FileInfo(Path.GetTempFileName()); 54 | 55 | SarifConverter.ConvertToCodeQuality(source, target); 56 | 57 | var options = JsonSerializerOptions; 58 | 59 | using var r = new StreamReader(target.FullName); 60 | var json = r.ReadToEnd(); 61 | var result = JsonSerializer.Deserialize>(json, options); 62 | 63 | result.Should().HaveCount(7); 64 | var codeQuality = result!.First(); 65 | codeQuality 66 | .Location 67 | .Path 68 | .Should() 69 | .Be("/builds/christian.sauer/net_examle/ClassLibrary1/Class1.cs"); 70 | codeQuality.Location.Lines.Begin.Should().Be(26); 71 | } 72 | 73 | [Fact] 74 | public void TestCrashWorks() 75 | { 76 | var source = new FileInfo("crash.sarif.json"); 77 | var target = new FileInfo(Path.GetTempFileName()); 78 | 79 | SarifConverter.ConvertToCodeQuality(source, target); 80 | 81 | var options = JsonSerializerOptions; 82 | 83 | using var r = new StreamReader(target.FullName); 84 | var json = r.ReadToEnd(); 85 | var result = JsonSerializer.Deserialize>(json, options); 86 | 87 | result.Should().HaveCount(1); 88 | } 89 | 90 | [Fact] 91 | public void TestSarif21Works() 92 | { 93 | var source = new FileInfo("codeanalysis.sarif21.json"); 94 | var target = new FileInfo(Path.GetTempFileName()); 95 | 96 | SarifConverter.ConvertToCodeQuality(source, target); 97 | 98 | var options = JsonSerializerOptions; 99 | 100 | using var r = new StreamReader(target.FullName); 101 | var json = r.ReadToEnd(); 102 | var result = JsonSerializer.Deserialize>(json, options); 103 | 104 | result.Should().HaveCount(1); 105 | result! 106 | .First() 107 | .Should() 108 | .BeEquivalentTo( 109 | new CodeQuality 110 | { 111 | Description = "no-unused-vars: Microsoft.CodeAnalysis.Sarif.Message", 112 | Fingerprint = "75510FEE03EDAC9F5241783C86ACBFEB", 113 | Severity = Severity.blocker, 114 | Location = new() 115 | { 116 | Path = 117 | @"C:\dev\sarif\sarif-tutorials\samples\Introduction\simple-example.js", 118 | Lines = new() { Begin = 1 } 119 | } 120 | } 121 | ); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /CodeQualityToGitlab/Program.cs: -------------------------------------------------------------------------------- 1 | using System.CommandLine; 2 | using CodeQualityToGitlab.SarifConverters; 3 | using Serilog; 4 | 5 | namespace CodeQualityToGitlab; 6 | 7 | internal static class Program 8 | { 9 | private static async Task Main(string[] args) 10 | { 11 | Log.Logger = new LoggerConfiguration().WriteTo.Console().CreateLogger(); 12 | 13 | var sourceArgument = new Argument( 14 | name: "source", 15 | description: "The file to convert" 16 | ); 17 | 18 | var targetArgument = new Argument(name: "target", description: "The target file"); 19 | 20 | var bumpToMajorOption = new Option( 21 | name: "--all_major", 22 | "if true all info and minor issues are promoted to major for Gitlab" 23 | ); 24 | 25 | var rootPathArgument = new Argument( 26 | name: "root", 27 | description: "The name root of the repository. Gitlab requires Code Quality issues to contain paths relative to the repository, " 28 | + "but the tools report them as absolute file paths. " 29 | + "Everything given in with this option will be removed. E.g. root is 'c:/dev' and the file name is something like 'c:/dev/myrepo/file.cs' it will transformed to 'myrepo/file.cs'. Can often be omitted. ", 30 | getDefaultValue: Directory.GetCurrentDirectory 31 | ); 32 | 33 | var rootCommand = new RootCommand("Tool to convert Dotnet-Formats to Gitlab code quality"); 34 | var roslynatorToCodeQuality = new Command( 35 | "roslynator", 36 | "Convert Roslynator file to Code Quality issue" 37 | ) 38 | { 39 | sourceArgument, 40 | targetArgument, 41 | rootPathArgument 42 | }; 43 | 44 | var sarifToCodeQuality = new Command("sarif", "Convert Sarif files to Code Quality issue") 45 | { 46 | sourceArgument, 47 | targetArgument, 48 | rootPathArgument 49 | }; 50 | 51 | var sourcesArgument = new Argument( 52 | name: "sources", 53 | description: "The files to merge" 54 | ); 55 | 56 | var mergeCodeQuality = new Command("merge", "Merge multiple code quality files into one") 57 | { 58 | targetArgument, 59 | sourcesArgument, 60 | bumpToMajorOption 61 | }; 62 | 63 | var sourceGlobArgument = new Argument( 64 | name: "sarifGlob", 65 | description: "Glob pattern for the sarif files", 66 | getDefaultValue: () => "**/*.sarif.json" 67 | ); 68 | 69 | var sourceRoslynatorArgument = new Argument( 70 | name: "roslynatorGlob", 71 | description: "Glob pattern for the roslynator files", 72 | getDefaultValue: () => "**/roslynator.xml" 73 | ); 74 | 75 | var transformCodeQuality = new Command( 76 | "transform", 77 | "Transforms files from a glob mapping and merges them to one file" 78 | ) 79 | { 80 | sourceGlobArgument, 81 | sourceRoslynatorArgument, 82 | targetArgument, 83 | rootPathArgument, 84 | bumpToMajorOption 85 | }; 86 | 87 | roslynatorToCodeQuality.SetHandler( 88 | RoslynatorConverter.ConvertToCodeQuality, 89 | sourceArgument, 90 | targetArgument, 91 | rootPathArgument 92 | ); 93 | sarifToCodeQuality.SetHandler( 94 | SarifConverter.ConvertToCodeQuality, 95 | sourceArgument, 96 | targetArgument, 97 | rootPathArgument 98 | ); 99 | mergeCodeQuality.SetHandler( 100 | Merger.Merge, 101 | sourcesArgument, 102 | targetArgument, 103 | bumpToMajorOption 104 | ); 105 | transformCodeQuality.SetHandler( 106 | Transform.TransformAll, 107 | sourceGlobArgument, 108 | sourceRoslynatorArgument, 109 | targetArgument, 110 | rootPathArgument, 111 | bumpToMajorOption 112 | ); 113 | rootCommand.Add(roslynatorToCodeQuality); 114 | rootCommand.Add(sarifToCodeQuality); 115 | rootCommand.Add(mergeCodeQuality); 116 | rootCommand.Add(transformCodeQuality); 117 | 118 | return await rootCommand.InvokeAsync(args); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Test/codeanalysis.sarif3.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/sarif-1.0.0", 3 | "version": "1.0.0", 4 | "runs": [ 5 | { 6 | "tool": { 7 | "name": "Microsoft (R) Visual C# Compiler", 8 | "version": "4.4.0.0", 9 | "fileVersion": "4.4.0-6.22565.8 (53091686)", 10 | "semanticVersion": "4.4.0", 11 | "language": "" 12 | }, 13 | "results": [ 14 | { 15 | "ruleId": "CS1998", 16 | "level": "warning", 17 | "message": "This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.", 18 | "suppressionStates": [ 19 | "suppressedInSource" 20 | ], 21 | "locations": [ 22 | { 23 | "resultFile": { 24 | "uri": "Microsoft.NET.Sdk.Razor.SourceGenerators/Microsoft.NET.Sdk.Razor.SourceGenerators.RazorSourceGenerator/Views_Apaleo_TgcBooking_cshtml.g.cs", 25 | "region": { 26 | "startLine": 217, 27 | "startColumn": 234, 28 | "endLine": 217, 29 | "endColumn": 236 30 | } 31 | } 32 | } 33 | ], 34 | "properties": { 35 | "warningLevel": 1 36 | } 37 | }], 38 | "rules": { 39 | "CS0162": { 40 | "id": "CS0162", 41 | "shortDescription": "Unreachable code detected", 42 | "defaultLevel": "warning", 43 | "helpUri": "https://msdn.microsoft.com/query/roslyn.query?appId=roslyn&k=k(CS0162)", 44 | "properties": { 45 | "category": "Compiler", 46 | "isEnabledByDefault": true, 47 | "tags": [ 48 | "Compiler", 49 | "Telemetry" 50 | ] 51 | } 52 | }, 53 | "CS0168": { 54 | "id": "CS0168", 55 | "shortDescription": "Variable is declared but never used", 56 | "defaultLevel": "warning", 57 | "helpUri": "https://msdn.microsoft.com/query/roslyn.query?appId=roslyn&k=k(CS0168)", 58 | "properties": { 59 | "category": "Compiler", 60 | "isEnabledByDefault": true, 61 | "tags": [ 62 | "Compiler", 63 | "Telemetry" 64 | ] 65 | } 66 | }, 67 | "IDE0036": { 68 | "id": "IDE0036", 69 | "shortDescription": "Order modifiers", 70 | "defaultLevel": "warning", 71 | "helpUri": "https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0036", 72 | "properties": { 73 | "category": "Style", 74 | "isEnabledByDefault": true, 75 | "tags": [ 76 | "Telemetry", 77 | "EnforceOnBuild_HighlyRecommended" 78 | ] 79 | } 80 | }, 81 | "IDE0055": { 82 | "id": "IDE0055", 83 | "shortDescription": "Fix formatting", 84 | "defaultLevel": "warning", 85 | "helpUri": "https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055", 86 | "properties": { 87 | "category": "Style", 88 | "isEnabledByDefault": true, 89 | "tags": [ 90 | "Telemetry", 91 | "EnforceOnBuild_HighlyRecommended" 92 | ] 93 | } 94 | }, 95 | "IDE0059": { 96 | "id": "IDE0059", 97 | "shortDescription": "Unnecessary assignment of a value", 98 | "fullDescription": "Avoid unnecessary value assignments in your code, as these likely indicate redundant value computations. If the value computation is not redundant and you intend to retain the assignment, then change the assignment target to a local variable whose name starts with an underscore and is optionally followed by an integer, such as '_', '_1', '_2', etc. These are treated as special discard symbol names.", 99 | "defaultLevel": "warning", 100 | "helpUri": "https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0059", 101 | "properties": { 102 | "category": "Style", 103 | "isEnabledByDefault": true, 104 | "tags": [ 105 | "Telemetry", 106 | "EnforceOnBuild_HighlyRecommended", 107 | "Unnecessary" 108 | ] 109 | } 110 | }, 111 | "IDE0100": { 112 | "id": "IDE0100", 113 | "shortDescription": "Remove redundant equality", 114 | "defaultLevel": "warning", 115 | "helpUri": "https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0100", 116 | "properties": { 117 | "category": "Style", 118 | "isEnabledByDefault": true, 119 | "tags": [ 120 | "Telemetry", 121 | "EnforceOnBuild_Recommended" 122 | ] 123 | } 124 | }, 125 | "IDE0160": { 126 | "id": "IDE0160", 127 | "shortDescription": "Convert to block scoped namespace", 128 | "defaultLevel": "warning", 129 | "helpUri": "https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0160", 130 | "properties": { 131 | "category": "Style", 132 | "isEnabledByDefault": true, 133 | "tags": [ 134 | "Telemetry", 135 | "EnforceOnBuild_HighlyRecommended" 136 | ] 137 | } 138 | } 139 | } 140 | } 141 | ] 142 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### VisualStudio template 2 | ## Ignore Visual Studio temporary files, build results, and 3 | ## files generated by popular Visual Studio add-ons. 4 | ## 5 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 6 | 7 | # User-specific files 8 | *.rsuser 9 | *.suo 10 | *.user 11 | *.userosscache 12 | *.sln.docstates 13 | 14 | # User-specific files (MonoDevelop/Xamarin Studio) 15 | *.userprefs 16 | 17 | # Mono auto generated files 18 | mono_crash.* 19 | 20 | # Build results 21 | [Dd]ebug/ 22 | [Dd]ebugPublic/ 23 | [Rr]elease/ 24 | [Rr]eleases/ 25 | x64/ 26 | x86/ 27 | [Ww][Ii][Nn]32/ 28 | [Aa][Rr][Mm]/ 29 | [Aa][Rr][Mm]64/ 30 | bld/ 31 | [Bb]in/ 32 | [Oo]bj/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.tlog 95 | *.vspscc 96 | *.vssscc 97 | .builds 98 | *.pidb 99 | *.svclog 100 | *.scc 101 | 102 | # Chutzpah Test files 103 | _Chutzpah* 104 | 105 | # Visual C++ cache files 106 | ipch/ 107 | *.aps 108 | *.ncb 109 | *.opendb 110 | *.opensdf 111 | *.sdf 112 | *.cachefile 113 | *.VC.db 114 | *.VC.VC.opendb 115 | 116 | # Visual Studio profiler 117 | *.psess 118 | *.vsp 119 | *.vspx 120 | *.sap 121 | 122 | # Visual Studio Trace Files 123 | *.e2e 124 | 125 | # TFS 2012 Local Workspace 126 | $tf/ 127 | 128 | # Guidance Automation Toolkit 129 | *.gpState 130 | 131 | # ReSharper is a .NET coding add-in 132 | _ReSharper*/ 133 | *.[Rr]e[Ss]harper 134 | *.DotSettings.user 135 | 136 | # TeamCity is a build add-in 137 | _TeamCity* 138 | 139 | # DotCover is a Code Coverage Tool 140 | *.dotCover 141 | 142 | # AxoCover is a Code Coverage Tool 143 | .axoCover/* 144 | !.axoCover/settings.json 145 | 146 | # Coverlet is a free, cross platform Code Coverage Tool 147 | coverage*.json 148 | coverage*.xml 149 | coverage*.info 150 | 151 | # Visual Studio code coverage results 152 | *.coverage 153 | *.coveragexml 154 | 155 | # NCrunch 156 | _NCrunch_* 157 | .*crunch*.local.xml 158 | nCrunchTemp_* 159 | 160 | # MightyMoose 161 | *.mm.* 162 | AutoTest.Net/ 163 | 164 | # Web workbench (sass) 165 | .sass-cache/ 166 | 167 | # Installshield output folder 168 | [Ee]xpress/ 169 | 170 | # DocProject is a documentation generator add-in 171 | DocProject/buildhelp/ 172 | DocProject/Help/*.HxT 173 | DocProject/Help/*.HxC 174 | DocProject/Help/*.hhc 175 | DocProject/Help/*.hhk 176 | DocProject/Help/*.hhp 177 | DocProject/Help/Html2 178 | DocProject/Help/html 179 | 180 | # Click-Once directory 181 | publish/ 182 | 183 | # Publish Web Output 184 | *.[Pp]ublish.xml 185 | *.azurePubxml 186 | # Note: Comment the next line if you want to checkin your web deploy settings, 187 | # but database connection strings (with potential passwords) will be unencrypted 188 | *.pubxml 189 | *.publishproj 190 | 191 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 192 | # checkin your Azure Web App publish settings, but sensitive information contained 193 | # in these scripts will be unencrypted 194 | PublishScripts/ 195 | 196 | # NuGet Packages 197 | *.nupkg 198 | # NuGet Symbol Packages 199 | *.snupkg 200 | # The packages folder can be ignored because of Package Restore 201 | **/[Pp]ackages/* 202 | # except build/, which is used as an MSBuild target. 203 | !**/[Pp]ackages/build/ 204 | # Uncomment if necessary however generally it will be regenerated when needed 205 | #!**/[Pp]ackages/repositories.config 206 | # NuGet v3's project.json files produces more ignorable files 207 | *.nuget.props 208 | *.nuget.targets 209 | 210 | # Microsoft Azure Build Output 211 | csx/ 212 | *.build.csdef 213 | 214 | # Microsoft Azure Emulator 215 | ecf/ 216 | rcf/ 217 | 218 | # Windows Store app package directories and files 219 | AppPackages/ 220 | BundleArtifacts/ 221 | Package.StoreAssociation.xml 222 | _pkginfo.txt 223 | *.appx 224 | *.appxbundle 225 | *.appxupload 226 | 227 | # Visual Studio cache files 228 | # files ending in .cache can be ignored 229 | *.[Cc]ache 230 | # but keep track of directories ending in .cache 231 | !?*.[Cc]ache/ 232 | 233 | # Others 234 | ClientBin/ 235 | ~$* 236 | *~ 237 | *.dbmdl 238 | *.dbproj.schemaview 239 | *.jfm 240 | *.pfx 241 | *.publishsettings 242 | orleans.codegen.cs 243 | 244 | # Including strong name files can present a security risk 245 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 246 | #*.snk 247 | 248 | # Since there are multiple workflows, uncomment next line to ignore bower_components 249 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 250 | #bower_components/ 251 | 252 | # RIA/Silverlight projects 253 | Generated_Code/ 254 | 255 | # Backup & report files from converting an old project file 256 | # to a newer Visual Studio version. Backup files are not needed, 257 | # because we have git ;-) 258 | _UpgradeReport_Files/ 259 | Backup*/ 260 | UpgradeLog*.XML 261 | UpgradeLog*.htm 262 | ServiceFabricBackup/ 263 | *.rptproj.bak 264 | 265 | # SQL Server files 266 | *.mdf 267 | *.ldf 268 | *.ndf 269 | 270 | # Business Intelligence projects 271 | *.rdl.data 272 | *.bim.layout 273 | *.bim_*.settings 274 | *.rptproj.rsuser 275 | *- [Bb]ackup.rdl 276 | *- [Bb]ackup ([0-9]).rdl 277 | *- [Bb]ackup ([0-9][0-9]).rdl 278 | 279 | # Microsoft Fakes 280 | FakesAssemblies/ 281 | 282 | # GhostDoc plugin setting file 283 | *.GhostDoc.xml 284 | 285 | # Node.js Tools for Visual Studio 286 | .ntvs_analysis.dat 287 | node_modules/ 288 | 289 | # Visual Studio 6 build log 290 | *.plg 291 | 292 | # Visual Studio 6 workspace options file 293 | *.opt 294 | 295 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 296 | *.vbw 297 | 298 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 299 | *.vbp 300 | 301 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 302 | *.dsw 303 | *.dsp 304 | 305 | # Visual Studio 6 technical files 306 | *.ncb 307 | *.aps 308 | 309 | # Visual Studio LightSwitch build output 310 | **/*.HTMLClient/GeneratedArtifacts 311 | **/*.DesktopClient/GeneratedArtifacts 312 | **/*.DesktopClient/ModelManifest.xml 313 | **/*.Server/GeneratedArtifacts 314 | **/*.Server/ModelManifest.xml 315 | _Pvt_Extensions 316 | 317 | # Paket dependency manager 318 | .paket/paket.exe 319 | paket-files/ 320 | 321 | # FAKE - F# Make 322 | .fake/ 323 | 324 | # CodeRush personal settings 325 | .cr/personal 326 | 327 | # Python Tools for Visual Studio (PTVS) 328 | __pycache__/ 329 | *.pyc 330 | 331 | # Cake - Uncomment if you are using it 332 | # tools/** 333 | # !tools/packages.config 334 | 335 | # Tabs Studio 336 | *.tss 337 | 338 | # Telerik's JustMock configuration file 339 | *.jmconfig 340 | 341 | # BizTalk build output 342 | *.btp.cs 343 | *.btm.cs 344 | *.odx.cs 345 | *.xsd.cs 346 | 347 | # OpenCover UI analysis results 348 | OpenCover/ 349 | 350 | # Azure Stream Analytics local run output 351 | ASALocalRun/ 352 | 353 | # MSBuild Binary and Structured Log 354 | *.binlog 355 | 356 | # NVidia Nsight GPU debugger configuration file 357 | *.nvuser 358 | 359 | # MFractors (Xamarin productivity tool) working folder 360 | .mfractor/ 361 | 362 | # Local History for Visual Studio 363 | .localhistory/ 364 | 365 | # Visual Studio History (VSHistory) files 366 | .vshistory/ 367 | 368 | # BeatPulse healthcheck temp database 369 | healthchecksdb 370 | 371 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 372 | MigrationBackup/ 373 | 374 | # Ionide (cross platform F# VS Code tools) working folder 375 | .ionide/ 376 | 377 | # Fody - auto-generated XML schema 378 | FodyWeavers.xsd 379 | 380 | # VS Code files for those working on multiple tools 381 | .vscode/* 382 | !.vscode/settings.json 383 | !.vscode/tasks.json 384 | !.vscode/launch.json 385 | !.vscode/extensions.json 386 | *.code-workspace 387 | 388 | # Local History for Visual Studio Code 389 | .history/ 390 | 391 | # Windows Installer files from build outputs 392 | *.cab 393 | *.msi 394 | *.msix 395 | *.msm 396 | *.msp 397 | 398 | # JetBrains Rider 399 | *.sln.iml 400 | 401 | .idea -------------------------------------------------------------------------------- /Test/codeanalysis.sarif2.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/sarif-1.0.0", 3 | "version": "1.0.0", 4 | "runs": [ 5 | { 6 | "tool": { 7 | "name": "Microsoft (R) Visual C# Compiler", 8 | "version": "4.4.0.0", 9 | "fileVersion": "4.4.0-6.22565.8 (53091686)", 10 | "semanticVersion": "4.4.0", 11 | "language": "" 12 | }, 13 | "results": [ 14 | { 15 | "ruleId": "CS0162", 16 | "level": "warning", 17 | "message": "Unreachable code detected", 18 | "locations": [ 19 | { 20 | "resultFile": { 21 | "uri": "file:///builds/christian.sauer/net_examle/ClassLibrary1/Class1.cs", 22 | "region": { 23 | "startLine": 26, 24 | "startColumn": 9, 25 | "endLine": 26, 26 | "endColumn": 11 27 | } 28 | } 29 | } 30 | ], 31 | "properties": { 32 | "warningLevel": 2 33 | } 34 | }, 35 | { 36 | "ruleId": "CS0168", 37 | "level": "warning", 38 | "message": "The variable 'ex' is declared but never used", 39 | "locations": [ 40 | { 41 | "resultFile": { 42 | "uri": "file:///builds/christian.sauer/net_examle/ClassLibrary1/Class1.cs", 43 | "region": { 44 | "startLine": 19, 45 | "startColumn": 26, 46 | "endLine": 19, 47 | "endColumn": 28 48 | } 49 | } 50 | } 51 | ], 52 | "properties": { 53 | "warningLevel": 3 54 | } 55 | }, 56 | { 57 | "ruleId": "IDE0160", 58 | "level": "warning", 59 | "message": "Convert to block scoped namespace", 60 | "locations": [ 61 | { 62 | "resultFile": { 63 | "uri": "file:///builds/christian.sauer/net_examle/ClassLibrary1/Class1.cs", 64 | "region": { 65 | "startLine": 3, 66 | "startColumn": 1, 67 | "endLine": 3, 68 | "endColumn": 25 69 | } 70 | } 71 | } 72 | ], 73 | "relatedLocations": [ 74 | { 75 | "physicalLocation": { 76 | "uri": "file:///builds/christian.sauer/net_examle/ClassLibrary1/Class1.cs", 77 | "region": { 78 | "startLine": 3, 79 | "startColumn": 1, 80 | "endLine": 36, 81 | "endColumn": 2 82 | } 83 | } 84 | } 85 | ], 86 | "properties": { 87 | "warningLevel": 1 88 | } 89 | }, 90 | { 91 | "ruleId": "IDE0055", 92 | "level": "warning", 93 | "message": "Fix formatting", 94 | "locations": [ 95 | { 96 | "resultFile": { 97 | "uri": "file:///builds/christian.sauer/net_examle/ClassLibrary1/Class1.cs", 98 | "region": { 99 | "startLine": 21, 100 | "startColumn": 1, 101 | "endLine": 21, 102 | "endColumn": 13 103 | } 104 | } 105 | } 106 | ], 107 | "properties": { 108 | "warningLevel": 1 109 | } 110 | }, 111 | { 112 | "ruleId": "IDE0036", 113 | "level": "warning", 114 | "message": "Modifiers are not ordered", 115 | "locations": [ 116 | { 117 | "resultFile": { 118 | "uri": "file:///builds/christian.sauer/net_examle/ClassLibrary1/Class1.cs", 119 | "region": { 120 | "startLine": 7, 121 | "startColumn": 5, 122 | "endLine": 7, 123 | "endColumn": 11 124 | } 125 | } 126 | } 127 | ], 128 | "properties": { 129 | "warningLevel": 1 130 | } 131 | }, 132 | { 133 | "ruleId": "IDE0100", 134 | "level": "warning", 135 | "message": "Remove redundant equality", 136 | "locations": [ 137 | { 138 | "resultFile": { 139 | "uri": "file:///builds/christian.sauer/net_examle/ClassLibrary1/Class1.cs", 140 | "region": { 141 | "startLine": 26, 142 | "startColumn": 19, 143 | "endLine": 26, 144 | "endColumn": 21 145 | } 146 | } 147 | } 148 | ], 149 | "relatedLocations": [ 150 | { 151 | "physicalLocation": { 152 | "uri": "file:///builds/christian.sauer/net_examle/ClassLibrary1/Class1.cs", 153 | "region": { 154 | "startLine": 26, 155 | "startColumn": 13, 156 | "endLine": 26, 157 | "endColumn": 26 158 | } 159 | } 160 | } 161 | ], 162 | "properties": { 163 | "warningLevel": 1, 164 | "customProperties": { 165 | "RedundantSide": "Right" 166 | } 167 | } 168 | }, 169 | { 170 | "ruleId": "IDE0059", 171 | "level": "warning", 172 | "message": "Unnecessary assignment of a value to 'ex'", 173 | "locations": [ 174 | { 175 | "resultFile": { 176 | "uri": "file:///builds/christian.sauer/net_examle/ClassLibrary1/Class1.cs", 177 | "region": { 178 | "startLine": 19, 179 | "startColumn": 15, 180 | "endLine": 19, 181 | "endColumn": 29 182 | } 183 | } 184 | } 185 | ], 186 | "properties": { 187 | "warningLevel": 1, 188 | "customProperties": { 189 | "IsUnusedLocalAssignmentKey": "", 190 | "UnusedValuePreferenceKey": "DiscardVariable" 191 | } 192 | } 193 | } 194 | ], 195 | "rules": { 196 | "CS0162": { 197 | "id": "CS0162", 198 | "shortDescription": "Unreachable code detected", 199 | "defaultLevel": "warning", 200 | "helpUri": "https://msdn.microsoft.com/query/roslyn.query?appId=roslyn&k=k(CS0162)", 201 | "properties": { 202 | "category": "Compiler", 203 | "isEnabledByDefault": true, 204 | "tags": [ 205 | "Compiler", 206 | "Telemetry" 207 | ] 208 | } 209 | }, 210 | "CS0168": { 211 | "id": "CS0168", 212 | "shortDescription": "Variable is declared but never used", 213 | "defaultLevel": "warning", 214 | "helpUri": "https://msdn.microsoft.com/query/roslyn.query?appId=roslyn&k=k(CS0168)", 215 | "properties": { 216 | "category": "Compiler", 217 | "isEnabledByDefault": true, 218 | "tags": [ 219 | "Compiler", 220 | "Telemetry" 221 | ] 222 | } 223 | }, 224 | "IDE0036": { 225 | "id": "IDE0036", 226 | "shortDescription": "Order modifiers", 227 | "defaultLevel": "warning", 228 | "helpUri": "https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0036", 229 | "properties": { 230 | "category": "Style", 231 | "isEnabledByDefault": true, 232 | "tags": [ 233 | "Telemetry", 234 | "EnforceOnBuild_HighlyRecommended" 235 | ] 236 | } 237 | }, 238 | "IDE0055": { 239 | "id": "IDE0055", 240 | "shortDescription": "Fix formatting", 241 | "defaultLevel": "warning", 242 | "helpUri": "https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055", 243 | "properties": { 244 | "category": "Style", 245 | "isEnabledByDefault": true, 246 | "tags": [ 247 | "Telemetry", 248 | "EnforceOnBuild_HighlyRecommended" 249 | ] 250 | } 251 | }, 252 | "IDE0059": { 253 | "id": "IDE0059", 254 | "shortDescription": "Unnecessary assignment of a value", 255 | "fullDescription": "Avoid unnecessary value assignments in your code, as these likely indicate redundant value computations. If the value computation is not redundant and you intend to retain the assignment, then change the assignment target to a local variable whose name starts with an underscore and is optionally followed by an integer, such as '_', '_1', '_2', etc. These are treated as special discard symbol names.", 256 | "defaultLevel": "warning", 257 | "helpUri": "https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0059", 258 | "properties": { 259 | "category": "Style", 260 | "isEnabledByDefault": true, 261 | "tags": [ 262 | "Telemetry", 263 | "EnforceOnBuild_HighlyRecommended", 264 | "Unnecessary" 265 | ] 266 | } 267 | }, 268 | "IDE0100": { 269 | "id": "IDE0100", 270 | "shortDescription": "Remove redundant equality", 271 | "defaultLevel": "warning", 272 | "helpUri": "https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0100", 273 | "properties": { 274 | "category": "Style", 275 | "isEnabledByDefault": true, 276 | "tags": [ 277 | "Telemetry", 278 | "EnforceOnBuild_Recommended" 279 | ] 280 | } 281 | }, 282 | "IDE0160": { 283 | "id": "IDE0160", 284 | "shortDescription": "Convert to block scoped namespace", 285 | "defaultLevel": "warning", 286 | "helpUri": "https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0160", 287 | "properties": { 288 | "category": "Style", 289 | "isEnabledByDefault": true, 290 | "tags": [ 291 | "Telemetry", 292 | "EnforceOnBuild_HighlyRecommended" 293 | ] 294 | } 295 | } 296 | } 297 | } 298 | ] 299 | } -------------------------------------------------------------------------------- /Test/codeanalysis.sarif4.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/sarif-1.0.0", 3 | "version": "1.0.0", 4 | "runs": [ 5 | { 6 | "tool": { 7 | "name": "Microsoft (R) Visual C# Compiler", 8 | "version": "4.8.0.0", 9 | "fileVersion": "4.8.0-7.24574.2 (4ff64493)", 10 | "semanticVersion": "4.8.0" 11 | }, 12 | "results": [ 13 | { 14 | "ruleId": "MA0048", 15 | "level": "warning", 16 | "message": "File name must match type name", 17 | "suppressionStates": [ 18 | "suppressedInSource" 19 | ], 20 | "locations": [ 21 | { 22 | "resultFile": { 23 | "uri": "file:///builds/folder/backend/SR.CLI/Program_Generate.cs", 24 | "region": { 25 | "startLine": 12, 26 | "startColumn": 24, 27 | "endLine": 12, 28 | "endColumn": 31 29 | } 30 | } 31 | } 32 | ], 33 | "properties": { 34 | "warningLevel": 1 35 | } 36 | }, 37 | { 38 | "ruleId": "CA1822", 39 | "level": "warning", 40 | "message": "Member 'Note' does not access instance data and can be marked as static", 41 | "suppressionStates": [ 42 | "suppressedInSource" 43 | ], 44 | "locations": [ 45 | { 46 | "resultFile": { 47 | "uri": "file:///builds/folder/backend/SR.CLI/Program_Generate.cs", 48 | "region": { 49 | "startLine": 21, 50 | "startColumn": 27, 51 | "endLine": 21, 52 | "endColumn": 31 53 | } 54 | } 55 | } 56 | ], 57 | "properties": { 58 | "warningLevel": 1 59 | } 60 | }, 61 | { 62 | "ruleId": "CA1822", 63 | "level": "warning", 64 | "message": "Member 'StockCount' does not access instance data and can be marked as static", 65 | "suppressionStates": [ 66 | "suppressedInSource" 67 | ], 68 | "locations": [ 69 | { 70 | "resultFile": { 71 | "uri": "file:///builds/folder/backend/SR.CLI/Program_Generate.cs", 72 | "region": { 73 | "startLine": 69, 74 | "startColumn": 27, 75 | "endLine": 69, 76 | "endColumn": 37 77 | } 78 | } 79 | } 80 | ], 81 | "properties": { 82 | "warningLevel": 1 83 | } 84 | }, 85 | { 86 | "ruleId": "CA1822", 87 | "level": "warning", 88 | "message": "Member 'QualityNotification' does not access instance data and can be marked as static", 89 | "suppressionStates": [ 90 | "suppressedInSource" 91 | ], 92 | "locations": [ 93 | { 94 | "resultFile": { 95 | "uri": "file:///builds/folder/backend/SR.CLI/Program_Generate.cs", 96 | "region": { 97 | "startLine": 129, 98 | "startColumn": 27, 99 | "endLine": 129, 100 | "endColumn": 46 101 | } 102 | } 103 | } 104 | ], 105 | "properties": { 106 | "warningLevel": 1 107 | } 108 | }, 109 | { 110 | "ruleId": "CA1822", 111 | "level": "warning", 112 | "message": "Member 'Document' does not access instance data and can be marked as static", 113 | "suppressionStates": [ 114 | "suppressedInSource" 115 | ], 116 | "locations": [ 117 | { 118 | "resultFile": { 119 | "uri": "file:///builds/folder/backend/SR.CLI/Program_Generate.cs", 120 | "region": { 121 | "startLine": 194, 122 | "startColumn": 27, 123 | "endLine": 194, 124 | "endColumn": 35 125 | } 126 | } 127 | } 128 | ], 129 | "properties": { 130 | "warningLevel": 1 131 | } 132 | }, 133 | { 134 | "ruleId": "CA1822", 135 | "level": "warning", 136 | "message": "Member 'Asn' does not access instance data and can be marked as static", 137 | "suppressionStates": [ 138 | "suppressedInSource" 139 | ], 140 | "locations": [ 141 | { 142 | "resultFile": { 143 | "uri": "file:///builds/folder/backend/SR.CLI/Program_Generate.cs", 144 | "region": { 145 | "startLine": 236, 146 | "startColumn": 27, 147 | "endLine": 236, 148 | "endColumn": 30 149 | } 150 | } 151 | } 152 | ], 153 | "properties": { 154 | "warningLevel": 1 155 | } 156 | }, 157 | { 158 | "ruleId": "CA1822", 159 | "level": "warning", 160 | "message": "Member 'PriceChange' does not access instance data and can be marked as static", 161 | "suppressionStates": [ 162 | "suppressedInSource" 163 | ], 164 | "locations": [ 165 | { 166 | "resultFile": { 167 | "uri": "file:///builds/folder/backend/SR.CLI/Program_Generate.cs", 168 | "region": { 169 | "startLine": 260, 170 | "startColumn": 27, 171 | "endLine": 260, 172 | "endColumn": 38 173 | } 174 | } 175 | } 176 | ], 177 | "properties": { 178 | "warningLevel": 1 179 | } 180 | }, 181 | { 182 | "ruleId": "CA1822", 183 | "level": "warning", 184 | "message": "Member 'OrderPlan' does not access instance data and can be marked as static", 185 | "suppressionStates": [ 186 | "suppressedInSource" 187 | ], 188 | "locations": [ 189 | { 190 | "resultFile": { 191 | "uri": "file:///builds/folder/backend/SR.CLI/Program_Generate.cs", 192 | "region": { 193 | "startLine": 327, 194 | "startColumn": 27, 195 | "endLine": 327, 196 | "endColumn": 36 197 | } 198 | } 199 | } 200 | ], 201 | "properties": { 202 | "warningLevel": 1 203 | } 204 | }, 205 | { 206 | "ruleId": "CA1822", 207 | "level": "warning", 208 | "message": "Member 'Forecast' does not access instance data and can be marked as static", 209 | "suppressionStates": [ 210 | "suppressedInSource" 211 | ], 212 | "locations": [ 213 | { 214 | "resultFile": { 215 | "uri": "file:///builds/folder/backend/SR.CLI/Program_Generate.cs", 216 | "region": { 217 | "startLine": 339, 218 | "startColumn": 27, 219 | "endLine": 339, 220 | "endColumn": 35 221 | } 222 | } 223 | } 224 | ], 225 | "properties": { 226 | "warningLevel": 1 227 | } 228 | }, 229 | { 230 | "ruleId": "CA1822", 231 | "level": "warning", 232 | "message": "Member 'GiftCard' does not access instance data and can be marked as static", 233 | "suppressionStates": [ 234 | "suppressedInSource" 235 | ], 236 | "locations": [ 237 | { 238 | "resultFile": { 239 | "uri": "file:///builds/folder/backend/SR.CLI/Program_Generate.cs", 240 | "region": { 241 | "startLine": 361, 242 | "startColumn": 27, 243 | "endLine": 361, 244 | "endColumn": 35 245 | } 246 | } 247 | } 248 | ], 249 | "properties": { 250 | "warningLevel": 1 251 | } 252 | }, 253 | { 254 | "ruleId": "CA1852", 255 | "level": "warning", 256 | "message": "Type 'ForecastArgs' can be sealed because it has no subtypes in its containing assembly and is not externally visible", 257 | "locations": [ 258 | { 259 | "resultFile": { 260 | "uri": "file:///builds/folder/backend/SR.CLI/Program_Generate.cs", 261 | "region": { 262 | "startLine": 348, 263 | "startColumn": 22, 264 | "endLine": 348, 265 | "endColumn": 34 266 | } 267 | } 268 | } 269 | ], 270 | "properties": { 271 | "warningLevel": 1 272 | } 273 | }, 274 | { 275 | "ruleId": "CA1852", 276 | "level": "warning", 277 | "message": "Type 'OrderPlanArgs' can be sealed because it has no subtypes in its containing assembly and is not externally visible", 278 | "locations": [ 279 | { 280 | "resultFile": { 281 | "uri": "file:///builds/folder/backend/SR.CLI/Program_Generate.cs", 282 | "region": { 283 | "startLine": 336, 284 | "startColumn": 22, 285 | "endLine": 336, 286 | "endColumn": 35 287 | } 288 | } 289 | } 290 | ], 291 | "properties": { 292 | "warningLevel": 1 293 | } 294 | }, 295 | { 296 | "ruleId": "MA0053", 297 | "level": "note", 298 | "message": "Make class sealed", 299 | "locations": [ 300 | { 301 | "resultFile": { 302 | "uri": "file:///builds/folder/backend/SR.CLI/Program_Generate.cs", 303 | "region": { 304 | "startLine": 348, 305 | "startColumn": 22, 306 | "endLine": 348, 307 | "endColumn": 34 308 | } 309 | } 310 | } 311 | ], 312 | "properties": { 313 | "warningLevel": 1 314 | } 315 | }, 316 | { 317 | "ruleId": "MA0053", 318 | "level": "note", 319 | "message": "Make class sealed", 320 | "locations": [ 321 | { 322 | "resultFile": { 323 | "uri": "file:///builds/folder/backend/SR.CLI/Program_Generate.cs", 324 | "region": { 325 | "startLine": 336, 326 | "startColumn": 22, 327 | "endLine": 336, 328 | "endColumn": 35 329 | } 330 | } 331 | } 332 | ], 333 | "properties": { 334 | "warningLevel": 1 335 | } 336 | } 337 | ], 338 | "rules": { 339 | "CA1822": { 340 | "id": "CA1822", 341 | "shortDescription": "Mark members as static", 342 | "fullDescription": "Members that do not access instance data or call instance methods can be marked as static. After you mark the methods as static, the compiler will emit nonvirtual call sites to these members. This can give you a measurable performance gain for performance-sensitive code.", 343 | "defaultLevel": "note", 344 | "helpUri": "https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822", 345 | "properties": { 346 | "category": "Performance", 347 | "isEnabledByDefault": true, 348 | "tags": [ 349 | "PortedFromFxCop", 350 | "Telemetry", 351 | "EnabledRuleInAggressiveMode" 352 | ] 353 | } 354 | }, 355 | "CA1852": { 356 | "id": "CA1852", 357 | "shortDescription": "Seal internal types", 358 | "fullDescription": "When a type is not accessible outside its assembly and has no subtypes within its containing assembly, it can be safely sealed. Sealing types can improve performance.", 359 | "defaultLevel": "note", 360 | "helpUri": "https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1852", 361 | "properties": { 362 | "category": "Performance", 363 | "isEnabledByDefault": true, 364 | "tags": [ 365 | "Telemetry", 366 | "EnabledRuleInAggressiveMode", 367 | "CompilationEnd" 368 | ] 369 | } 370 | }, 371 | "MA0048": { 372 | "id": "MA0048", 373 | "shortDescription": "File name must match type name", 374 | "defaultLevel": "warning", 375 | "helpUri": "https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0048.md", 376 | "properties": { 377 | "category": "Design", 378 | "isEnabledByDefault": true 379 | } 380 | }, 381 | "MA0053": { 382 | "id": "MA0053", 383 | "shortDescription": "Make class sealed", 384 | "defaultLevel": "note", 385 | "helpUri": "https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0053.md", 386 | "properties": { 387 | "category": "Design", 388 | "isEnabledByDefault": true 389 | } 390 | } 391 | } 392 | } 393 | ] 394 | } --------------------------------------------------------------------------------