├── 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 | }
--------------------------------------------------------------------------------