Repository: oxc-project/oxc
Pull request #10927: feat(linter): add --format gitlab for gitlab code quality reports
Author: login: cjpearson
Labels: name: C-enhancement
description: Category - New feature or request
name: A-cli
description: Area - CLI
Created: 2025-05-09T17:32:43Z | Merged: 2025-05-09T22:48:03Z

Issue #10352

Adds support for outputting a gitlab code quality report. Modeled mostly on the JSON and Github reporters. 

Can be used like `oxlint --format gitlab > codequality.json`


Diff:
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<dyn DiagnosticReporter> {
+        Box::new(GitlabReporter::default())
+    }
+}
+
+/// Renders reports as a Gitlab Code Quality Report
+///
+/// <https://docs.gitlab.com/ci/testing/code_quality/#code-quality-report-format>
+///
+/// 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<Error>,
+}
+
+impl DiagnosticReporter for GitlabReporter {
+    fn finish(&mut self, _: &DiagnosticResult) -> Option<String> {
+        Some(format_gitlab(&mut self.diagnostics))
+    }
+
+    fn render_error(&mut self, error: Error) -> Option<String> {
+        self.diagnostics.push(error);
+        None
+    }
+}
+
+fn format_gitlab(diagnostics: &mut Vec<Error>) -> 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::<Vec<_>>()).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
     /// <https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-a-notice-message>
     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::<JsonOutputFormatter>::default(),
             OutputFormat::Checkstyle => Box::<CheckStyleOutputFormatter>::default(),
             OutputFormat::Github => Box::new(GithubOutputFormatter),
+            OutputFormat::Gitlab => Box::<GitlabOutputFormatter>::default(),
             OutputFormat::Unix => Box::<UnixOutputFormatter>::default(),
             OutputFormat::Default => Box::new(DefaultOutputFormatter),
             OutputFormat::Stylish => Box::<StylishOutputFormatter>::default(),


Reviews:
user: login: camc314
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T22:46:01Z
user: login: camc314
body: thank you!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T22:47:28Z
user: login: cjpearson
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-10T12:28:23Z

Review comments:
diff_hunk: @@ -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<dyn DiagnosticReporter> {
+        Box::new(GitlabReporter::default())
+    }
+}
+
+/// Renders reports as a Gitlab Code Quality Report
+///
+/// <https://docs.gitlab.com/ci/testing/code_quality/#code-quality-report-format>
+///
+/// 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.
path: apps/oxlint/src/output_formatter/gitlab.rs
user: login: camc314
body: note there're are optimizations we can do here, but i think this is fine for now

e.g, at start write `[`, then for each diagnostic as it arrives (skip comma for the first one) `format!(",{}")`, then on finish, write the final `]`
created_at: 2025-05-09T22:46:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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<dyn DiagnosticReporter> {
+        Box::new(GitlabReporter::default())
+    }
+}
+
+/// Renders reports as a Gitlab Code Quality Report
+///
+/// <https://docs.gitlab.com/ci/testing/code_quality/#code-quality-report-format>
+///
+/// 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.
path: apps/oxlint/src/output_formatter/gitlab.rs
user: login: cjpearson
body: Thanks for the tip! This is all still new to me, so I'm sure there's plenty of room for improvement :)
created_at: 2025-05-10T12:28:23Z
author_association: CONTRIBUTOR

PR comments:
user: login: graphite-app[bot]
created_at: 2025-05-09T17:32:50Z
body: ## How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:
- <kbd>0-merge</kbd> - adds this PR to the back of the merge queue
- <kbd>hotfix</kbd> - for urgent hot fixes, skip the queue and merge this PR next

You must have a Graphite account in order to use the merge queue. Sign up using [this link](https://app.graphite.dev/invite/github/oxc-project?ref=merge-queue-instructions-comment&prId=6200292205).

<h2></h2>
<sub>

An organization admin has enabled the [Graphite Merge Queue](https://graphite.dev/docs/graphite-merge-queue) in this repository.

</sub>

<sub>

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

</sub>
author_association: CONTRIBUTOR
user: login: camc314
created_at: 2025-05-09T17:42:34Z
body: this is great! do you mind shareing a screenshot of what it looks like in gitlab? If not, no worries, i can test and try it next week
author_association: CONTRIBUTOR
user: login: cjpearson
created_at: 2025-05-09T19:15:08Z
body: Sure, here's a quick sample of a warning: 

![Screenshot 2025-05-09 at 21 12 42](https://github.com/user-attachments/assets/db3faad0-f37a-44e7-b90b-7c36eb601c1a)
﻿
author_association: CONTRIBUTOR