diff --git a/apps/oxlint/src/output_formatter/gitlab.rs b/apps/oxlint/src/output_formatter/gitlab.rs index 98788abaf8e78..b5d3d7ce12c6b 100644 --- a/apps/oxlint/src/output_formatter/gitlab.rs +++ b/apps/oxlint/src/output_formatter/gitlab.rs @@ -1,4 +1,8 @@ use std::hash::{DefaultHasher, Hash, Hasher}; +use std::path::{Path, PathBuf}; + +#[cfg(windows)] +use cow_utils::CowUtils; use serde::Serialize; @@ -39,20 +43,66 @@ impl InternalFormatter for GitlabOutputFormatter { } } +/// Find the git repository root by walking up from the current directory. +/// Returns `None` if no `.git` directory is found. +fn find_git_root() -> Option { + let cwd = std::env::current_dir().ok()?; + find_git_root_from(&cwd) +} + +/// Find the git repository root by walking up from the given path. +fn find_git_root_from(start: &Path) -> Option { + let mut current = start.to_path_buf(); + loop { + if current.join(".git").exists() { + return Some(current); + } + if !current.pop() { + return None; + } + } +} + +/// Get the path prefix from CWD to the git repository root. +/// This prefix should be prepended to CWD-relative paths to make them repo-relative. +/// +/// For example, if git root is `/repo` and CWD is `/repo/packages/foo`, +/// this returns `Some("packages/foo")`. +fn get_repo_path_prefix() -> Option { + let cwd = std::env::current_dir().ok()?; + let git_root = find_git_root()?; + + // Get the relative path from git root to CWD + let relative = cwd.strip_prefix(&git_root).ok()?; + if relative.as_os_str().is_empty() { + return None; + } + + Some(relative.to_path_buf()) +} + /// Renders reports as a Gitlab Code Quality Report /// /// /// /// Note that, due to syntactic restrictions of JSON arrays, this reporter waits until all /// diagnostics have been reported before writing them to the output stream. -#[derive(Default)] struct GitlabReporter { diagnostics: Vec, + /// Path prefix to prepend to CWD-relative paths to make them repo-relative. + /// `None` if CWD is the git root or if we're not in a git repository. + repo_path_prefix: Option, +} + +impl GitlabReporter { + fn default() -> Self { + Self { diagnostics: Vec::new(), repo_path_prefix: get_repo_path_prefix() } + } } impl DiagnosticReporter for GitlabReporter { fn finish(&mut self, _: &DiagnosticResult) -> Option { - Some(format_gitlab(&mut self.diagnostics)) + Some(format_gitlab(&mut self.diagnostics, self.repo_path_prefix.as_deref())) } fn render_error(&mut self, error: Error) -> Option { @@ -61,7 +111,7 @@ impl DiagnosticReporter for GitlabReporter { } } -fn format_gitlab(diagnostics: &mut Vec) -> String { +fn format_gitlab(diagnostics: &mut Vec, repo_path_prefix: Option<&Path>) -> String { let errors = diagnostics.drain(..).map(|error| { let Info { start, end, filename, message, severity, rule_id } = Info::new(&error); let severity = match severity { @@ -85,7 +135,23 @@ fn format_gitlab(diagnostics: &mut Vec) -> String { description: message, check_name: rule_id.unwrap_or_default(), location: GitlabErrorLocationJson { - path: filename, + // GitLab expects file paths to be relative to the repository + // root, so adjust accordingly. + path: match repo_path_prefix { + Some(prefix) => { + // only do the path swap on Windows + #[cfg(windows)] + { + let combined = prefix.join(&filename); + combined.to_string_lossy().cow_replace('\\', "/").into_owned() + } + #[cfg(not(windows))] + { + prefix.join(&filename).to_string_lossy().to_string() + } + } + None => filename, + }, lines: GitlabErrorLocationLinesJson { begin: start.line, end: end.line }, }, fingerprint, @@ -98,13 +164,15 @@ fn format_gitlab(diagnostics: &mut Vec) -> String { #[cfg(test)] mod test { + use std::path::{Path, PathBuf}; + use oxc_diagnostics::{ - NamedSource, OxcDiagnostic, + Error, NamedSource, OxcDiagnostic, reporter::{DiagnosticReporter, DiagnosticResult}, }; use oxc_span::Span; - use super::GitlabReporter; + use super::{GitlabReporter, find_git_root_from, format_gitlab}; #[test] fn reporter() { @@ -112,7 +180,7 @@ mod test { let error = OxcDiagnostic::warn("error message") .with_label(Span::new(0, 8)) - .with_source_code(NamedSource::new("file://test.ts", "debugger;")); + .with_source_code(NamedSource::new("test.ts", "debugger;")); let first_result = reporter.render_error(error); @@ -133,9 +201,73 @@ mod test { assert!(value["fingerprint"].is_string()); // value is different on different architectures assert_eq!(value["severity"], "major"); let location = value["location"].as_object().unwrap(); - assert_eq!(location["path"], "file://test.ts"); + assert_eq!(location["path"], "apps/oxlint/test.ts"); let lines = location["lines"].as_object().unwrap(); assert_eq!(lines["begin"], 1); assert_eq!(lines["end"], 1); } + + #[test] + fn find_git_root_from_current_dir() { + // This test runs from within the oxc repo, so we should find a git root + let cwd = std::env::current_dir().unwrap(); + let git_root = find_git_root_from(&cwd); + assert!(git_root.is_some()); + assert!(git_root.unwrap().join(".git").exists()); + } + + #[test] + fn find_git_root_from_nonexistent() { + // A path that doesn't exist or has no git repo + let path = PathBuf::from("/"); + let git_root = find_git_root_from(&path); + // Root directory typically doesn't have a .git folder + assert!(git_root.is_none() || *git_root.unwrap() == *"/"); + } + + #[test] + fn format_gitlab_with_prefix() { + let error = OxcDiagnostic::warn("test error") + .with_label(Span::new(0, 5)) + .with_source_code(NamedSource::new("example.js", "const x = 1;")); + + let mut diagnostics: Vec = vec![error]; + + // Test with a prefix + let result = format_gitlab(&mut diagnostics, Some(Path::new("packages/foo"))); + let json: serde_json::Value = serde_json::from_str(&result).unwrap(); + let path = json[0]["location"]["path"].as_str().unwrap(); + assert_eq!(path, "packages/foo/example.js"); + } + + #[test] + fn format_gitlab_without_prefix() { + let error = OxcDiagnostic::warn("test error") + .with_label(Span::new(0, 5)) + .with_source_code(NamedSource::new("example.js", "const x = 1;")); + + let mut diagnostics: Vec = vec![error]; + + // Test without a prefix (CWD is at git root) + let result = format_gitlab(&mut diagnostics, None); + let json: serde_json::Value = serde_json::from_str(&result).unwrap(); + let path = json[0]["location"]["path"].as_str().unwrap(); + assert_eq!(path, "example.js"); + } + + #[cfg(windows)] + #[test] + fn format_gitlab_windows_normalization() { + let error = OxcDiagnostic::warn("test error") + .with_label(Span::new(0, 5)) + .with_source_code(NamedSource::new("example.js", "const x = 1;")); + + let mut diagnostics: Vec = vec![error]; + + // Windows-style prefix with backslashes should be normalized to forward slashes + let result = format_gitlab(&mut diagnostics, Some(Path::new(r"packages\foo"))); + let json: serde_json::Value = serde_json::from_str(&result).unwrap(); + let path = json[0]["location"]["path"].as_str().unwrap(); + assert_eq!(path, "packages/foo/example.js"); + } } diff --git a/apps/oxlint/src/snapshots/fixtures__output_formatter_diagnostic_--format=gitlab --report-unused-disable-directives disable-directive.js@oxlint.snap b/apps/oxlint/src/snapshots/fixtures__output_formatter_diagnostic_--format=gitlab --report-unused-disable-directives disable-directive.js@oxlint.snap index 50acf35fb3ed4..8544c875601bf 100644 --- a/apps/oxlint/src/snapshots/fixtures__output_formatter_diagnostic_--format=gitlab --report-unused-disable-directives disable-directive.js@oxlint.snap +++ b/apps/oxlint/src/snapshots/fixtures__output_formatter_diagnostic_--format=gitlab --report-unused-disable-directives disable-directive.js@oxlint.snap @@ -12,7 +12,7 @@ working directory: fixtures/output_formatter_diagnostic "fingerprint": "6460b46f54ac436d", "severity": "major", "location": { - "path": "disable-directive.js", + "path": "apps/oxlint/disable-directive.js", "lines": { "begin": 9, "end": 9 @@ -25,7 +25,7 @@ working directory: fixtures/output_formatter_diagnostic "fingerprint": "74c434632ebc59be", "severity": "major", "location": { - "path": "disable-directive.js", + "path": "apps/oxlint/disable-directive.js", "lines": { "begin": 12, "end": 12 @@ -38,7 +38,7 @@ working directory: fixtures/output_formatter_diagnostic "fingerprint": "9854c19736e51e0c", "severity": "major", "location": { - "path": "disable-directive.js", + "path": "apps/oxlint/disable-directive.js", "lines": { "begin": 15, "end": 15 diff --git a/apps/oxlint/src/snapshots/fixtures__output_formatter_diagnostic_--format=gitlab test.js@oxlint.snap b/apps/oxlint/src/snapshots/fixtures__output_formatter_diagnostic_--format=gitlab test.js@oxlint.snap index 6a73c2a2ed634..047dc99b23175 100644 --- a/apps/oxlint/src/snapshots/fixtures__output_formatter_diagnostic_--format=gitlab test.js@oxlint.snap +++ b/apps/oxlint/src/snapshots/fixtures__output_formatter_diagnostic_--format=gitlab test.js@oxlint.snap @@ -12,7 +12,7 @@ working directory: fixtures/output_formatter_diagnostic "fingerprint": "9333a3278325994", "severity": "critical", "location": { - "path": "test.js", + "path": "apps/oxlint/test.js", "lines": { "begin": 5, "end": 5 @@ -25,7 +25,7 @@ working directory: fixtures/output_formatter_diagnostic "fingerprint": "b1e9b343a9c1e457", "severity": "major", "location": { - "path": "test.js", + "path": "apps/oxlint/test.js", "lines": { "begin": 1, "end": 1 @@ -38,7 +38,7 @@ working directory: fixtures/output_formatter_diagnostic "fingerprint": "53ecf7c08335f91", "severity": "major", "location": { - "path": "test.js", + "path": "apps/oxlint/test.js", "lines": { "begin": 1, "end": 1