diff --git a/crates/ruff_db/src/diagnostic/mod.rs b/crates/ruff_db/src/diagnostic/mod.rs
index 774b341f8b..c44dce2116 100644
--- a/crates/ruff_db/src/diagnostic/mod.rs
+++ b/crates/ruff_db/src/diagnostic/mod.rs
@@ -1444,7 +1444,7 @@ pub enum DiagnosticFormat {
Junit,
/// Print diagnostics in the JSON format used by GitLab [Code Quality] reports.
///
- /// [Code Quality]: https://docs.gitlab.com/ee/ci/testing/code_quality.html#implement-a-custom-tool
+ /// [Code Quality]: https://docs.gitlab.com/ci/testing/code_quality/#code-quality-report-format
#[cfg(feature = "serde")]
Gitlab,
}
diff --git a/crates/ty/docs/cli.md b/crates/ty/docs/cli.md
index 9fff1e8876..c02bb6ea4b 100644
--- a/crates/ty/docs/cli.md
+++ b/crates/ty/docs/cli.md
@@ -60,8 +60,9 @@ over all configuration files.
--output-format output-formatThe format to use for printing diagnostic messages
Possible values:
-full: Print diagnostics verbosely, with context and helpful hints [default]
+full: Print diagnostics verbosely, with context and helpful hints (default)
concise: Print diagnostics concisely, one per line
+gitlab: Print diagnostics in the JSON format expected by GitLab Code Quality reports
--project projectRun the command within the given project directory.
All pyproject.toml files will be discovered by walking up the directory tree from the given project directory, as will the project's virtual environment (.venv) unless the venv-path option is set.
Other command-line arguments (such as relative paths) will be resolved relative to the current working directory.
diff --git a/crates/ty/src/args.rs b/crates/ty/src/args.rs
index f518e028db..1a2eabf5f3 100644
--- a/crates/ty/src/args.rs
+++ b/crates/ty/src/args.rs
@@ -306,7 +306,7 @@ impl clap::Args for RulesArg {
/// The diagnostic output format.
#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Default, clap::ValueEnum)]
pub enum OutputFormat {
- /// Print diagnostics verbosely, with context and helpful hints \[default\].
+ /// Print diagnostics verbosely, with context and helpful hints (default).
///
/// Diagnostic messages may include additional context and
/// annotations on the input to help understand the message.
@@ -321,6 +321,9 @@ pub enum OutputFormat {
/// dropped.
#[value(name = "concise")]
Concise,
+ /// Print diagnostics in the JSON format expected by GitLab Code Quality reports.
+ #[value(name = "gitlab")]
+ Gitlab,
}
impl From for ty_project::metadata::options::OutputFormat {
@@ -328,6 +331,7 @@ impl From for ty_project::metadata::options::OutputFormat {
match format {
OutputFormat::Full => Self::Full,
OutputFormat::Concise => Self::Concise,
+ OutputFormat::Gitlab => Self::Gitlab,
}
}
}
diff --git a/crates/ty/src/lib.rs b/crates/ty/src/lib.rs
index 3e11a3bedf..4e3c5993d0 100644
--- a/crates/ty/src/lib.rs
+++ b/crates/ty/src/lib.rs
@@ -21,7 +21,7 @@ use clap::{CommandFactory, Parser};
use colored::Colorize;
use crossbeam::channel as crossbeam_channel;
use rayon::ThreadPoolBuilder;
-use ruff_db::diagnostic::{Diagnostic, DisplayDiagnosticConfig, Severity};
+use ruff_db::diagnostic::{Diagnostic, DisplayDiagnosticConfig, DisplayDiagnostics, Severity};
use ruff_db::files::File;
use ruff_db::max_parallelism;
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
@@ -319,37 +319,48 @@ impl MainLoop {
return Ok(ExitStatus::Success);
}
+ let is_human_readable = terminal_settings.output_format.is_human_readable();
+
if result.is_empty() {
- writeln!(
- self.printer.stream_for_success_summary(),
- "{}",
- "All checks passed!".green().bold()
- )?;
+ if is_human_readable {
+ writeln!(
+ self.printer.stream_for_success_summary(),
+ "{}",
+ "All checks passed!".green().bold()
+ )?;
+ }
if self.watcher.is_none() {
return Ok(ExitStatus::Success);
}
} else {
- let mut max_severity = Severity::Info;
let diagnostics_count = result.len();
let mut stdout = self.printer.stream_for_details().lock();
- for diagnostic in result {
- // Only render diagnostics if they're going to be displayed, since doing
- // so is expensive.
- if stdout.is_enabled() {
- write!(stdout, "{}", diagnostic.display(db, &display_config))?;
- }
+ let max_severity = result
+ .iter()
+ .map(Diagnostic::severity)
+ .max()
+ .unwrap_or(Severity::Info);
- max_severity = max_severity.max(diagnostic.severity());
+ // Only render diagnostics if they're going to be displayed, since doing
+ // so is expensive.
+ if stdout.is_enabled() {
+ write!(
+ stdout,
+ "{}",
+ DisplayDiagnostics::new(db, &display_config, &result)
+ )?;
}
- writeln!(
- self.printer.stream_for_failure_summary(),
- "Found {} diagnostic{}",
- diagnostics_count,
- if diagnostics_count > 1 { "s" } else { "" }
- )?;
+ if is_human_readable {
+ writeln!(
+ self.printer.stream_for_failure_summary(),
+ "Found {} diagnostic{}",
+ diagnostics_count,
+ if diagnostics_count > 1 { "s" } else { "" }
+ )?;
+ }
if max_severity.is_fatal() {
tracing::warn!(
diff --git a/crates/ty/tests/cli/main.rs b/crates/ty/tests/cli/main.rs
index f85f294842..70d2859f8d 100644
--- a/crates/ty/tests/cli/main.rs
+++ b/crates/ty/tests/cli/main.rs
@@ -618,6 +618,71 @@ fn concise_diagnostics() -> anyhow::Result<()> {
Ok(())
}
+#[test]
+fn gitlab_diagnostics() -> anyhow::Result<()> {
+ let case = CliTest::with_file(
+ "test.py",
+ r#"
+ print(x) # [unresolved-reference]
+ print(4[1]) # [non-subscriptable]
+ "#,
+ )?;
+
+ let mut settings = insta::Settings::clone_current();
+ settings.add_filter(r#"("fingerprint": ")[a-z0-9]+(",)"#, "$1[FINGERPRINT]$2");
+ let _s = settings.bind_to_scope();
+
+ assert_cmd_snapshot!(case.command().arg("--output-format=gitlab").arg("--warn").arg("unresolved-reference"), @r#"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+ [
+ {
+ "check_name": "unresolved-reference",
+ "description": "unresolved-reference: Name `x` used when not defined",
+ "severity": "minor",
+ "fingerprint": "[FINGERPRINT]",
+ "location": {
+ "path": "test.py",
+ "positions": {
+ "begin": {
+ "line": 2,
+ "column": 7
+ },
+ "end": {
+ "line": 2,
+ "column": 8
+ }
+ }
+ }
+ },
+ {
+ "check_name": "non-subscriptable",
+ "description": "non-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method",
+ "severity": "major",
+ "fingerprint": "[FINGERPRINT]",
+ "location": {
+ "path": "test.py",
+ "positions": {
+ "begin": {
+ "line": 3,
+ "column": 7
+ },
+ "end": {
+ "line": 3,
+ "column": 8
+ }
+ }
+ }
+ }
+ ]
+ ----- stderr -----
+ WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
+ "#);
+
+ Ok(())
+}
+
/// This tests the diagnostic format for revealed type.
///
/// This test was introduced because changes were made to
diff --git a/crates/ty_project/src/metadata/options.rs b/crates/ty_project/src/metadata/options.rs
index 6e5d614de4..1ec0bb2edb 100644
--- a/crates/ty_project/src/metadata/options.rs
+++ b/crates/ty_project/src/metadata/options.rs
@@ -1055,6 +1055,21 @@ pub enum OutputFormat {
///
/// This may use color when printing to a `tty`.
Concise,
+ /// Print diagnostics in the JSON format expected by GitLab [Code Quality] reports.
+ ///
+ /// [Code Quality]: https://docs.gitlab.com/ci/testing/code_quality/#code-quality-report-format
+ Gitlab,
+}
+
+impl OutputFormat {
+ /// Returns `true` if this format is intended for users to read directly, in contrast to
+ /// machine-readable or structured formats.
+ ///
+ /// This can be used to check whether information beyond the diagnostics, such as a header or
+ /// `Found N diagnostics` footer, should be included.
+ pub const fn is_human_readable(&self) -> bool {
+ matches!(self, OutputFormat::Full | OutputFormat::Concise)
+ }
}
impl From for DiagnosticFormat {
@@ -1062,6 +1077,7 @@ impl From for DiagnosticFormat {
match value {
OutputFormat::Full => Self::Full,
OutputFormat::Concise => Self::Concise,
+ OutputFormat::Gitlab => Self::Gitlab,
}
}
}
diff --git a/ty.schema.json b/ty.schema.json
index a9261dfefb..45cc92b023 100644
--- a/ty.schema.json
+++ b/ty.schema.json
@@ -164,6 +164,13 @@
"enum": [
"concise"
]
+ },
+ {
+ "description": "Print diagnostics in the JSON format expected by GitLab [Code Quality] reports.\n\n[Code Quality]: https://docs.gitlab.com/ci/testing/code_quality/#code-quality-report-format",
+ "type": "string",
+ "enum": [
+ "gitlab"
+ ]
}
]
},