Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- [CLI] Add support for GitLab analyzer reports ([PR](https://github.com/dotnet/roslynator/pull/1633))

## [4.13.1] - 2025-02-23

### Added
Expand Down
12 changes: 10 additions & 2 deletions src/CommandLine/Commands/AnalyzeCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Roslynator.CommandLine.Json;
using Roslynator.CommandLine.Xml;
using Roslynator.Diagnostics;
using static Roslynator.Logger;
Expand Down Expand Up @@ -96,8 +97,15 @@ protected override void ProcessResults(IList<AnalyzeCommandResult> results)
&& analysisResults.Any(f => f.Diagnostics.Any() || f.CompilerDiagnostics.Any()))
{
CultureInfo culture = (Options.Culture is not null) ? CultureInfo.GetCultureInfo(Options.Culture) : null;

DiagnosticXmlSerializer.Serialize(analysisResults, Options.Output, culture);
if (!string.IsNullOrWhiteSpace(Options.OutputFormat) && Options.OutputFormat.Equals("gitlab", StringComparison.CurrentCultureIgnoreCase))
{
DiagnosticGitLabJsonSerializer.Serialize(analysisResults, Options.Output, culture);
}
else
{
// Default output format is xml
DiagnosticXmlSerializer.Serialize(analysisResults, Options.Output, culture);
}
}
}

Expand Down
15 changes: 15 additions & 0 deletions src/CommandLine/GitLab/GitLabIssue.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Newtonsoft.Json;

namespace Roslynator.CommandLine.GitLab;

internal sealed class GitLabIssue
{
public string Type { get; set; }
public string Fingerprint { get; set; }
[JsonProperty("check_name")]
public string CheckName { get; set; }
public string Description { get; set; }
public string Severity { get; set; }
public GitLabIssueLocation Location { get; set; }
public string[] Categories { get; set; }
}
7 changes: 7 additions & 0 deletions src/CommandLine/GitLab/GitLabIssueLocation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Roslynator.CommandLine.GitLab;

internal sealed class GitLabIssueLocation
{
public string Path { get; set; }
public GitLabLocationLines Lines { get; set; }
}
6 changes: 6 additions & 0 deletions src/CommandLine/GitLab/GitLabLocationLines.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Roslynator.CommandLine.GitLab;

internal sealed class GitLabLocationLines
{
public int Begin { get; set; }
}
87 changes: 87 additions & 0 deletions src/CommandLine/Json/DiagnosticGitLabJsonSerializer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using Microsoft.CodeAnalysis;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Roslynator.CommandLine.GitLab;
using Roslynator.Diagnostics;

namespace Roslynator.CommandLine.Json;

internal static class DiagnosticGitLabJsonSerializer
{
private static readonly JsonSerializerSettings _jsonSerializerSettings = new()
{
Formatting = Newtonsoft.Json.Formatting.Indented,
NullValueHandling = NullValueHandling.Ignore,
ContractResolver = new DefaultContractResolver()
{
NamingStrategy = new CamelCaseNamingStrategy()
},
};

public static void Serialize(
IEnumerable<ProjectAnalysisResult> results,
string filePath,
IFormatProvider formatProvider = null)
{
IEnumerable<DiagnosticInfo> diagnostics = results.SelectMany(f => f.CompilerDiagnostics.Concat(f.Diagnostics));

var reportItems = new List<GitLabIssue>();
foreach (DiagnosticInfo diagnostic in diagnostics)
{
GitLabIssueLocation location = null;
if (diagnostic.LineSpan.IsValid)
{
location = new GitLabIssueLocation()
{
Path = diagnostic.LineSpan.Path,
Lines = new GitLabLocationLines()
{
Begin = diagnostic.LineSpan.StartLinePosition.Line
},
};
}

var severity = "minor";
severity = diagnostic.Severity switch
{
DiagnosticSeverity.Warning => "major",
DiagnosticSeverity.Error => "critical",
_ => "minor",
};

string issueFingerPrint = $"{diagnostic.Descriptor.Id}-{diagnostic.Severity}-{location?.Path}-{location?.Lines.Begin}";
byte[] source = Encoding.UTF8.GetBytes(issueFingerPrint);
byte[] hashBytes;
#if NETFRAMEWORK
using (var sha256 = SHA256.Create())
hashBytes = sha256.ComputeHash(source);
#else
hashBytes = SHA256.HashData(source);
#endif
issueFingerPrint = BitConverter.ToString(hashBytes)
.Replace("-", "")
.ToLowerInvariant();

reportItems.Add(new GitLabIssue()
{
Type = "issue",
Fingerprint = issueFingerPrint,
CheckName = diagnostic.Descriptor.Id,
Description = diagnostic.Descriptor.Title.ToString(formatProvider),
Severity = severity,
Location = location,
Categories = new string[] { diagnostic.Descriptor.Category },
});
}

string report = JsonConvert.SerializeObject(reportItems, _jsonSerializerSettings);

File.WriteAllText(filePath, report, Encoding.UTF8);
}
}
12 changes: 11 additions & 1 deletion src/CommandLine/Options/AnalyzeCommandLineOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,15 @@ public class AnalyzeCommandLineOptions : AbstractAnalyzeCommandLineOptions
[Option(
shortName: OptionShortNames.Output,
longName: "output",
HelpText = "Defines path to file that will store reported diagnostics in XML format.",
HelpText = "Defines path to file that will store reported diagnostics. The format of the file is determined by the --output-format option, with the default being xml.",
MetaValue = "<FILE_PATH>")]
public string Output { get; set; }

[Option(
longName: "output-format",
HelpText = "Defines the file format of the report written to file. Supported options are: gitlab and xml, with xml the default if no option is provided.")]
public string OutputFormat { get; set; }

[Option(
longName: "report-not-configurable",
HelpText = "Indicates whether diagnostics with 'NotConfigurable' tag should be reported.")]
Expand All @@ -33,4 +38,9 @@ public class AnalyzeCommandLineOptions : AbstractAnalyzeCommandLineOptions
longName: "report-suppressed-diagnostics",
HelpText = "Indicates whether suppressed diagnostics should be reported.")]
public bool ReportSuppressedDiagnostics { get; set; }

internal bool ValidateOutputFormat()
{
return ParseHelpers.TryParseOutputFormat(OutputFormat);
}
}
23 changes: 23 additions & 0 deletions src/CommandLine/ParseHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -349,4 +349,27 @@ public static bool TryReadAllText(
return false;
}
}

public static bool TryParseOutputFormat(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
// Default to XML if no value is provided
return true;
}

bool valid = value.Trim().ToLowerInvariant() switch
{
"gitlab" => true,
"xml" => true,
_ => false
};

if (!valid)
{
WriteLine($"Unknown output format '{value}'.", Verbosity.Quiet);
}

return valid;
}
}
3 changes: 3 additions & 0 deletions src/CommandLine/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,9 @@ private static async Task<int> AnalyzeAsync(AnalyzeCommandLineOptions options)
if (!TryParsePaths(options.Paths, out ImmutableArray<PathInfo> paths))
return ExitCodes.Error;

if (!options.ValidateOutputFormat())
return ExitCodes.Error;

var command = new AnalyzeCommand(options, severityLevel, projectFilter, CreateFileSystemFilter(options));

CommandStatus status = await command.ExecuteAsync(paths, options.MSBuildPath, options.Properties);
Expand Down
Loading