[ty] Add GitLab output format (#20155)

## Summary

This wires up the GitLab output format moved into `ruff_db` in
https://github.com/astral-sh/ruff/pull/20117 to the ty CLI.

While I was here, I made one unrelated change to the CLI docs. Clap was
rendering the escapes around the `\[default\]` brackets for the `full`
output, so I just switched those to parentheses:

```
--output-format <OUTPUT_FORMAT>
    The format to use for printing diagnostic messages

    Possible values:
    - 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
```

## Test Plan

New CLI test, and a manual test with `--config 'terminal.output-format =
"gitlab"'` to make sure this works as a configuration option too. I also
tried piping the output through jq to make sure it's at least valid JSON
This commit is contained in:
Brent Westbrook 2025-09-03 09:08:12 -04:00 committed by GitHub
parent 4e97b97a76
commit aee9350df1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 127 additions and 23 deletions

View file

@ -1444,7 +1444,7 @@ pub enum DiagnosticFormat {
Junit, Junit,
/// Print diagnostics in the JSON format used by GitLab [Code Quality] reports. /// 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")] #[cfg(feature = "serde")]
Gitlab, Gitlab,
} }

3
crates/ty/docs/cli.md generated
View file

@ -60,8 +60,9 @@ over all configuration files.</p>
</dd><dt id="ty-check--output-format"><a href="#ty-check--output-format"><code>--output-format</code></a> <i>output-format</i></dt><dd><p>The format to use for printing diagnostic messages</p> </dd><dt id="ty-check--output-format"><a href="#ty-check--output-format"><code>--output-format</code></a> <i>output-format</i></dt><dd><p>The format to use for printing diagnostic messages</p>
<p>Possible values:</p> <p>Possible values:</p>
<ul> <ul>
<li><code>full</code>: Print diagnostics verbosely, with context and helpful hints [default]</li> <li><code>full</code>: Print diagnostics verbosely, with context and helpful hints (default)</li>
<li><code>concise</code>: Print diagnostics concisely, one per line</li> <li><code>concise</code>: Print diagnostics concisely, one per line</li>
<li><code>gitlab</code>: Print diagnostics in the JSON format expected by GitLab Code Quality reports</li>
</ul></dd><dt id="ty-check--project"><a href="#ty-check--project"><code>--project</code></a> <i>project</i></dt><dd><p>Run the command within the given project directory.</p> </ul></dd><dt id="ty-check--project"><a href="#ty-check--project"><code>--project</code></a> <i>project</i></dt><dd><p>Run the command within the given project directory.</p>
<p>All <code>pyproject.toml</code> files will be discovered by walking up the directory tree from the given project directory, as will the project's virtual environment (<code>.venv</code>) unless the <code>venv-path</code> option is set.</p> <p>All <code>pyproject.toml</code> files will be discovered by walking up the directory tree from the given project directory, as will the project's virtual environment (<code>.venv</code>) unless the <code>venv-path</code> option is set.</p>
<p>Other command-line arguments (such as relative paths) will be resolved relative to the current working directory.</p> <p>Other command-line arguments (such as relative paths) will be resolved relative to the current working directory.</p>

View file

@ -306,7 +306,7 @@ impl clap::Args for RulesArg {
/// The diagnostic output format. /// The diagnostic output format.
#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Default, clap::ValueEnum)] #[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Default, clap::ValueEnum)]
pub enum OutputFormat { 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 /// Diagnostic messages may include additional context and
/// annotations on the input to help understand the message. /// annotations on the input to help understand the message.
@ -321,6 +321,9 @@ pub enum OutputFormat {
/// dropped. /// dropped.
#[value(name = "concise")] #[value(name = "concise")]
Concise, Concise,
/// Print diagnostics in the JSON format expected by GitLab Code Quality reports.
#[value(name = "gitlab")]
Gitlab,
} }
impl From<OutputFormat> for ty_project::metadata::options::OutputFormat { impl From<OutputFormat> for ty_project::metadata::options::OutputFormat {
@ -328,6 +331,7 @@ impl From<OutputFormat> for ty_project::metadata::options::OutputFormat {
match format { match format {
OutputFormat::Full => Self::Full, OutputFormat::Full => Self::Full,
OutputFormat::Concise => Self::Concise, OutputFormat::Concise => Self::Concise,
OutputFormat::Gitlab => Self::Gitlab,
} }
} }
} }

View file

@ -21,7 +21,7 @@ use clap::{CommandFactory, Parser};
use colored::Colorize; use colored::Colorize;
use crossbeam::channel as crossbeam_channel; use crossbeam::channel as crossbeam_channel;
use rayon::ThreadPoolBuilder; 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::files::File;
use ruff_db::max_parallelism; use ruff_db::max_parallelism;
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf}; use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
@ -319,37 +319,48 @@ impl MainLoop {
return Ok(ExitStatus::Success); return Ok(ExitStatus::Success);
} }
let is_human_readable = terminal_settings.output_format.is_human_readable();
if result.is_empty() { if result.is_empty() {
writeln!( if is_human_readable {
self.printer.stream_for_success_summary(), writeln!(
"{}", self.printer.stream_for_success_summary(),
"All checks passed!".green().bold() "{}",
)?; "All checks passed!".green().bold()
)?;
}
if self.watcher.is_none() { if self.watcher.is_none() {
return Ok(ExitStatus::Success); return Ok(ExitStatus::Success);
} }
} else { } else {
let mut max_severity = Severity::Info;
let diagnostics_count = result.len(); let diagnostics_count = result.len();
let mut stdout = self.printer.stream_for_details().lock(); let mut stdout = self.printer.stream_for_details().lock();
for diagnostic in result { let max_severity = result
// Only render diagnostics if they're going to be displayed, since doing .iter()
// so is expensive. .map(Diagnostic::severity)
if stdout.is_enabled() { .max()
write!(stdout, "{}", diagnostic.display(db, &display_config))?; .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!( if is_human_readable {
self.printer.stream_for_failure_summary(), writeln!(
"Found {} diagnostic{}", self.printer.stream_for_failure_summary(),
diagnostics_count, "Found {} diagnostic{}",
if diagnostics_count > 1 { "s" } else { "" } diagnostics_count,
)?; if diagnostics_count > 1 { "s" } else { "" }
)?;
}
if max_severity.is_fatal() { if max_severity.is_fatal() {
tracing::warn!( tracing::warn!(

View file

@ -618,6 +618,71 @@ fn concise_diagnostics() -> anyhow::Result<()> {
Ok(()) 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 tests the diagnostic format for revealed type.
/// ///
/// This test was introduced because changes were made to /// This test was introduced because changes were made to

View file

@ -1055,6 +1055,21 @@ pub enum OutputFormat {
/// ///
/// This may use color when printing to a `tty`. /// This may use color when printing to a `tty`.
Concise, 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<OutputFormat> for DiagnosticFormat { impl From<OutputFormat> for DiagnosticFormat {
@ -1062,6 +1077,7 @@ impl From<OutputFormat> for DiagnosticFormat {
match value { match value {
OutputFormat::Full => Self::Full, OutputFormat::Full => Self::Full,
OutputFormat::Concise => Self::Concise, OutputFormat::Concise => Self::Concise,
OutputFormat::Gitlab => Self::Gitlab,
} }
} }
} }

7
ty.schema.json generated
View file

@ -164,6 +164,13 @@
"enum": [ "enum": [
"concise" "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"
]
} }
] ]
}, },