diff --git a/apps/oxlint/src/output_formatter/gitlab.rs b/apps/oxlint/src/output_formatter/gitlab.rs new file mode 100644 index 0000000000000..aba91dc3783a2 --- /dev/null +++ b/apps/oxlint/src/output_formatter/gitlab.rs @@ -0,0 +1,129 @@ +use oxc_diagnostics::{ + Error, Severity, + reporter::{DiagnosticReporter, DiagnosticResult, Info}, +}; + +use std::hash::{DefaultHasher, Hash, Hasher}; + +use crate::output_formatter::InternalFormatter; + +#[derive(Debug, Default)] +pub struct GitlabOutputFormatter; + +#[derive(Debug, serde::Serialize)] +struct GitlabErrorLocationLinesJson { + begin: usize, + end: usize, +} + +#[derive(Debug, serde::Serialize)] +struct GitlabErrorLocationJson { + path: String, + lines: GitlabErrorLocationLinesJson, +} + +#[derive(Debug, serde::Serialize)] +struct GitlabErrorJson { + description: String, + check_name: String, + fingerprint: String, + severity: String, + location: GitlabErrorLocationJson, +} + +impl InternalFormatter for GitlabOutputFormatter { + fn get_diagnostic_reporter(&self) -> Box { + Box::new(GitlabReporter::default()) + } +} + +/// 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, +} + +impl DiagnosticReporter for GitlabReporter { + fn finish(&mut self, _: &DiagnosticResult) -> Option { + Some(format_gitlab(&mut self.diagnostics)) + } + + fn render_error(&mut self, error: Error) -> Option { + self.diagnostics.push(error); + None + } +} + +fn format_gitlab(diagnostics: &mut Vec) -> String { + let errors = diagnostics.drain(..).map(|error| { + let Info { start, end, filename, message, severity, rule_id } = Info::new(&error); + let severity = match severity { + Severity::Error => "critical".to_string(), + Severity::Warning => "major".to_string(), + Severity::Advice => "minor".to_string(), + }; + + let fingerprint = { + let mut hasher = DefaultHasher::new(); + start.line.hash(&mut hasher); + end.line.hash(&mut hasher); + filename.hash(&mut hasher); + message.hash(&mut hasher); + severity.hash(&mut hasher); + + format!("{:x}", hasher.finish()) + }; + + GitlabErrorJson { + description: message, + check_name: rule_id.unwrap_or_default(), + location: GitlabErrorLocationJson { + path: filename, + lines: GitlabErrorLocationLinesJson { begin: start.line, end: end.line }, + }, + fingerprint, + severity, + } + }); + + serde_json::to_string_pretty(&errors.collect::>()).expect("Failed to serialize") +} + +#[cfg(test)] +mod test { + use oxc_diagnostics::{ + NamedSource, OxcDiagnostic, + reporter::{DiagnosticReporter, DiagnosticResult}, + }; + use oxc_span::Span; + + use super::GitlabReporter; + + #[test] + fn reporter() { + let mut reporter = GitlabReporter::default(); + + let error = OxcDiagnostic::warn("error message") + .with_label(Span::new(0, 8)) + .with_source_code(NamedSource::new("file://test.ts", "debugger;")); + + let first_result = reporter.render_error(error); + + // reporter keeps it in memory + assert!(first_result.is_none()); + + // reporter gives results when finishing + let second_result = reporter.finish(&DiagnosticResult::default()); + + assert!(second_result.is_some()); + assert_eq!( + second_result.unwrap(), + "[\n {\n \"description\": \"error message\",\n \"check_name\": \"\",\n \"fingerprint\": \"8b23bd85b148d3\",\n \"severity\": \"major\",\n \"location\": {\n \"path\": \"file://test.ts\",\n \"lines\": {\n \"begin\": 1,\n \"end\": 1\n }\n }\n }\n]" + ); + } +} diff --git a/apps/oxlint/src/output_formatter/mod.rs b/apps/oxlint/src/output_formatter/mod.rs index a502f45efac6a..39a922fae62ea 100644 --- a/apps/oxlint/src/output_formatter/mod.rs +++ b/apps/oxlint/src/output_formatter/mod.rs @@ -1,6 +1,7 @@ mod checkstyle; mod default; mod github; +mod gitlab; mod json; mod junit; mod stylish; @@ -12,6 +13,7 @@ use std::time::Duration; use checkstyle::CheckStyleOutputFormatter; use github::GithubOutputFormatter; +use gitlab::GitlabOutputFormatter; use junit::JUnitOutputFormatter; use stylish::StylishOutputFormatter; use unix::UnixOutputFormatter; @@ -26,6 +28,7 @@ pub enum OutputFormat { /// GitHub Check Annotation /// Github, + Gitlab, Json, Unix, Checkstyle, @@ -43,6 +46,7 @@ impl FromStr for OutputFormat { "unix" => Ok(Self::Unix), "checkstyle" => Ok(Self::Checkstyle), "github" => Ok(Self::Github), + "gitlab" => Ok(Self::Gitlab), "stylish" => Ok(Self::Stylish), "junit" => Ok(Self::JUnit), _ => Err(format!("'{s}' is not a known format")), @@ -96,6 +100,7 @@ impl OutputFormatter { OutputFormat::Json => Box::::default(), OutputFormat::Checkstyle => Box::::default(), OutputFormat::Github => Box::new(GithubOutputFormatter), + OutputFormat::Gitlab => Box::::default(), OutputFormat::Unix => Box::::default(), OutputFormat::Default => Box::new(DefaultOutputFormatter), OutputFormat::Stylish => Box::::default(),