diff --git a/ChangeLog.md b/ChangeLog.md index e6cd921b1e..0291904a0d 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix analyzer [RCS1046](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1046) to report `async void` methods without `Async` suffix ([PR](https://github.com/dotnet/roslynator/pull/1790)) - Fix analyzer [RCS1265](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1265) to not report catch clauses with a `when` filter ([PR](https://github.com/dotnet/roslynator/pull/1789)) - Fix analyzer [RCS0034](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS0034) for types with a primary constructor and multiple constraint clauses ([PR](https://github.com/dotnet/roslynator/pull/1791)) +- [CLI] Fix GitLab output format to use relative paths, forward slashes, and 1-based line numbers ([PR](https://github.com/dotnet/roslynator/pull/1792)) ## [4.16.0] - 2026-08-08 diff --git a/src/CommandLine/CommandResults/AnalyzeCommandResult.cs b/src/CommandLine/CommandResults/AnalyzeCommandResult.cs index 7d6aab3ec5..2a5e4589d6 100644 --- a/src/CommandLine/CommandResults/AnalyzeCommandResult.cs +++ b/src/CommandLine/CommandResults/AnalyzeCommandResult.cs @@ -7,11 +7,14 @@ namespace Roslynator.CommandLine; internal class AnalyzeCommandResult : CommandResult { - public AnalyzeCommandResult(CommandStatus status, ImmutableArray analysisResults) + public AnalyzeCommandResult(CommandStatus status, ImmutableArray analysisResults, string rootDirectoryPath = null) : base(status) { AnalysisResults = analysisResults; + RootDirectoryPath = rootDirectoryPath; } public ImmutableArray AnalysisResults { get; } + + public string RootDirectoryPath { get; } } diff --git a/src/CommandLine/Commands/AnalyzeCommand.cs b/src/CommandLine/Commands/AnalyzeCommand.cs index 4e7000f087..b935843b72 100644 --- a/src/CommandLine/Commands/AnalyzeCommand.cs +++ b/src/CommandLine/Commands/AnalyzeCommand.cs @@ -81,7 +81,7 @@ public override async Task ExecuteAsync(ProjectOrSolution results = await codeAnalyzer.AnalyzeSolutionAsync(solution, f => IsMatch(f), cancellationToken); } - return new AnalyzeCommandResult(GetCommandStatus(Options, results), results); + return new AnalyzeCommandResult(GetCommandStatus(Options, results), results, FileSystemFilter?.RootDirectoryPath); } private static CommandStatus GetCommandStatus(AnalyzeCommandLineOptions options, ImmutableArray results) @@ -100,7 +100,7 @@ protected override void ProcessResults(IList results) CultureInfo culture = (Options.Culture is not null) ? CultureInfo.GetCultureInfo(Options.Culture) : null; if (!string.IsNullOrWhiteSpace(Options.OutputFormat) && Options.OutputFormat.Equals("gitlab", StringComparison.CurrentCultureIgnoreCase)) { - DiagnosticGitLabJsonSerializer.Serialize(analysisResults, Options.Output, culture); + DiagnosticGitLabJsonSerializer.Serialize(results, Options.Output, culture); } else { diff --git a/src/CommandLine/Json/DiagnosticGitLabJsonSerializer.cs b/src/CommandLine/Json/DiagnosticGitLabJsonSerializer.cs index 7e238b74e0..7b3805f3da 100644 --- a/src/CommandLine/Json/DiagnosticGitLabJsonSerializer.cs +++ b/src/CommandLine/Json/DiagnosticGitLabJsonSerializer.cs @@ -25,65 +25,84 @@ internal static class DiagnosticGitLabJsonSerializer }; public static void Serialize( - IEnumerable results, + IList results, string filePath, IFormatProvider formatProvider = null) { - IEnumerable diagnostics = results.SelectMany(f => f.CompilerDiagnostics.Concat(f.Diagnostics)); - var reportItems = new List(); - foreach (DiagnosticInfo diagnostic in diagnostics) + + foreach (AnalyzeCommandResult commandResult in results) { - GitLabIssueLocation location = null; - if (diagnostic.LineSpan.IsValid) + string baseDirectoryPath = commandResult.RootDirectoryPath; + + foreach (ProjectAnalysisResult result in commandResult.AnalysisResults) { - location = new GitLabIssueLocation() + foreach (DiagnosticInfo diagnostic in result.CompilerDiagnostics.Concat(result.Diagnostics)) { - Path = diagnostic.LineSpan.Path, - Lines = new GitLabLocationLines() + GitLabIssueLocation location = null; + if (diagnostic.LineSpan.IsValid) { - Begin = diagnostic.LineSpan.StartLinePosition.Line - }, - }; - } + location = new GitLabIssueLocation() + { + Path = FormatPath(diagnostic.LineSpan.Path, baseDirectoryPath), + Lines = new GitLabLocationLines() + { + Begin = diagnostic.LineSpan.StartLinePosition.Line + 1 + }, + }; + } - var severity = "minor"; - severity = diagnostic.Severity switch - { - DiagnosticSeverity.Warning => "major", - DiagnosticSeverity.Error => "critical", - _ => "minor", - }; + 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; + 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); + using (var sha256 = SHA256.Create()) + hashBytes = sha256.ComputeHash(source); #else - hashBytes = SHA256.HashData(source); + hashBytes = SHA256.HashData(source); #endif #pragma warning disable CA1872 // Use Convert.ToHexString instead of BitConverter.ToString - issueFingerPrint = BitConverter.ToString(hashBytes) - .Replace("-", "") - .ToLowerInvariant(); + issueFingerPrint = BitConverter.ToString(hashBytes) + .Replace("-", "") + .ToLowerInvariant(); #pragma warning restore CA1872 - 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 }, - }); + 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); } + + private static string FormatPath(string path, string baseDirectoryPath) + { + if (!string.IsNullOrEmpty(baseDirectoryPath) + && FileSystemHelpers.TryGetNormalizedFullPath(path, out string normalizedPath) + && FileSystemHelpers.TryGetNormalizedFullPath(baseDirectoryPath, out string normalizedBase)) + { + path = PathUtilities.TrimStart(normalizedPath, normalizedBase); + } + + return path.Replace('\\', '/'); + } } diff --git a/src/Workspaces.Core/PathUtilities.cs b/src/Workspaces.Core/PathUtilities.cs index 6f89f92a93..66b14d5d75 100644 --- a/src/Workspaces.Core/PathUtilities.cs +++ b/src/Workspaces.Core/PathUtilities.cs @@ -11,17 +11,17 @@ internal static string TrimStart(string path, string? basePath, bool trimLeading { if (basePath is not null) { - if (string.Equals(path, basePath, StringComparison.Ordinal)) + if (string.Equals(path, basePath, FileSystemHelpers.Comparison)) return Path.GetFileName(path); - if (path.StartsWith(basePath)) + if (path.StartsWith(basePath, FileSystemHelpers.Comparison)) { int length = basePath.Length; if (trimLeadingDirectorySeparator) { while (length < path.Length - && path[length] == Path.DirectorySeparatorChar) + && FileSystemHelpers.IsDirectorySeparator(path[length])) { length++; }