mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-02 14:51:25 +00:00
[ty] Add GitHub output format (#20358)
## Summary This PR wires up the GitHub output format moved to `ruff_db` in #20320 to the ty CLI. It's a bit smaller than the GitLab version (#20155) because some of the helpers were already in place, but I did factor out a few `DisplayDiagnosticConfig` constructor calls in Ruff. I also exposed the `GithubRenderer` and a wrapper `DisplayGithubDiagnostics` type because we needed a way to configure the program name displayed in the GitHub diagnostics. This was previously hard-coded to `Ruff`: <img width="675" height="247" alt="image" src="https://github.com/user-attachments/assets/592da860-d2f5-4abd-bc5a-66071d742509" /> Another option would be to drop the program name in the output format, but I think it can be helpful in workflows with multiple programs emitting annotations (such as Ruff and ty!) ## Test Plan New CLI test, and a manual test with `--config 'terminal.output-format = "github"'`
This commit is contained in:
parent
7e464b8150
commit
ac5488086f
11 changed files with 95 additions and 39 deletions
|
@ -10,7 +10,8 @@ use ruff_linter::linter::FixTable;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use ruff_db::diagnostic::{
|
use ruff_db::diagnostic::{
|
||||||
Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig, DisplayDiagnostics, SecondaryCode,
|
Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig, DisplayDiagnostics,
|
||||||
|
DisplayGithubDiagnostics, GithubRenderer, SecondaryCode,
|
||||||
};
|
};
|
||||||
use ruff_linter::fs::relativize_path;
|
use ruff_linter::fs::relativize_path;
|
||||||
use ruff_linter::logging::LogLevel;
|
use ruff_linter::logging::LogLevel;
|
||||||
|
@ -224,32 +225,26 @@ impl Printer {
|
||||||
let context = EmitterContext::new(&diagnostics.notebook_indexes);
|
let context = EmitterContext::new(&diagnostics.notebook_indexes);
|
||||||
let fixables = FixableStatistics::try_from(diagnostics, self.unsafe_fixes);
|
let fixables = FixableStatistics::try_from(diagnostics, self.unsafe_fixes);
|
||||||
|
|
||||||
|
let config = DisplayDiagnosticConfig::default().preview(preview);
|
||||||
|
|
||||||
match self.format {
|
match self.format {
|
||||||
OutputFormat::Json => {
|
OutputFormat::Json => {
|
||||||
let config = DisplayDiagnosticConfig::default()
|
let config = config.format(DiagnosticFormat::Json);
|
||||||
.format(DiagnosticFormat::Json)
|
|
||||||
.preview(preview);
|
|
||||||
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
|
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
|
||||||
write!(writer, "{value}")?;
|
write!(writer, "{value}")?;
|
||||||
}
|
}
|
||||||
OutputFormat::Rdjson => {
|
OutputFormat::Rdjson => {
|
||||||
let config = DisplayDiagnosticConfig::default()
|
let config = config.format(DiagnosticFormat::Rdjson);
|
||||||
.format(DiagnosticFormat::Rdjson)
|
|
||||||
.preview(preview);
|
|
||||||
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
|
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
|
||||||
write!(writer, "{value}")?;
|
write!(writer, "{value}")?;
|
||||||
}
|
}
|
||||||
OutputFormat::JsonLines => {
|
OutputFormat::JsonLines => {
|
||||||
let config = DisplayDiagnosticConfig::default()
|
let config = config.format(DiagnosticFormat::JsonLines);
|
||||||
.format(DiagnosticFormat::JsonLines)
|
|
||||||
.preview(preview);
|
|
||||||
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
|
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
|
||||||
write!(writer, "{value}")?;
|
write!(writer, "{value}")?;
|
||||||
}
|
}
|
||||||
OutputFormat::Junit => {
|
OutputFormat::Junit => {
|
||||||
let config = DisplayDiagnosticConfig::default()
|
let config = config.format(DiagnosticFormat::Junit);
|
||||||
.format(DiagnosticFormat::Junit)
|
|
||||||
.preview(preview);
|
|
||||||
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
|
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
|
||||||
write!(writer, "{value}")?;
|
write!(writer, "{value}")?;
|
||||||
}
|
}
|
||||||
|
@ -288,30 +283,22 @@ impl Printer {
|
||||||
self.write_summary_text(writer, diagnostics)?;
|
self.write_summary_text(writer, diagnostics)?;
|
||||||
}
|
}
|
||||||
OutputFormat::Github => {
|
OutputFormat::Github => {
|
||||||
let config = DisplayDiagnosticConfig::default()
|
let renderer = GithubRenderer::new(&context, "Ruff");
|
||||||
.format(DiagnosticFormat::Github)
|
let value = DisplayGithubDiagnostics::new(&renderer, &diagnostics.inner);
|
||||||
.preview(preview);
|
|
||||||
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
|
|
||||||
write!(writer, "{value}")?;
|
write!(writer, "{value}")?;
|
||||||
}
|
}
|
||||||
OutputFormat::Gitlab => {
|
OutputFormat::Gitlab => {
|
||||||
let config = DisplayDiagnosticConfig::default()
|
let config = config.format(DiagnosticFormat::Gitlab);
|
||||||
.format(DiagnosticFormat::Gitlab)
|
|
||||||
.preview(preview);
|
|
||||||
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
|
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
|
||||||
write!(writer, "{value}")?;
|
write!(writer, "{value}")?;
|
||||||
}
|
}
|
||||||
OutputFormat::Pylint => {
|
OutputFormat::Pylint => {
|
||||||
let config = DisplayDiagnosticConfig::default()
|
let config = config.format(DiagnosticFormat::Pylint);
|
||||||
.format(DiagnosticFormat::Pylint)
|
|
||||||
.preview(preview);
|
|
||||||
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
|
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
|
||||||
write!(writer, "{value}")?;
|
write!(writer, "{value}")?;
|
||||||
}
|
}
|
||||||
OutputFormat::Azure => {
|
OutputFormat::Azure => {
|
||||||
let config = DisplayDiagnosticConfig::default()
|
let config = config.format(DiagnosticFormat::Azure);
|
||||||
.format(DiagnosticFormat::Azure)
|
|
||||||
.preview(preview);
|
|
||||||
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
|
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
|
||||||
write!(writer, "{value}")?;
|
write!(writer, "{value}")?;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||||
|
|
||||||
pub use self::render::{
|
pub use self::render::{
|
||||||
DisplayDiagnostic, DisplayDiagnostics, FileResolver, Input, ceil_char_boundary,
|
DisplayDiagnostic, DisplayDiagnostics, FileResolver, Input, ceil_char_boundary,
|
||||||
|
github::{DisplayGithubDiagnostics, GithubRenderer},
|
||||||
};
|
};
|
||||||
use crate::{Db, files::File};
|
use crate::{Db, files::File};
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ use pylint::PylintRenderer;
|
||||||
mod azure;
|
mod azure;
|
||||||
mod concise;
|
mod concise;
|
||||||
mod full;
|
mod full;
|
||||||
mod github;
|
pub mod github;
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
mod gitlab;
|
mod gitlab;
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
|
@ -145,7 +145,7 @@ impl std::fmt::Display for DisplayDiagnostics<'_> {
|
||||||
gitlab::GitlabRenderer::new(self.resolver).render(f, self.diagnostics)?;
|
gitlab::GitlabRenderer::new(self.resolver).render(f, self.diagnostics)?;
|
||||||
}
|
}
|
||||||
DiagnosticFormat::Github => {
|
DiagnosticFormat::Github => {
|
||||||
GithubRenderer::new(self.resolver).render(f, self.diagnostics)?;
|
GithubRenderer::new(self.resolver, "ty").render(f, self.diagnostics)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
use crate::diagnostic::{Diagnostic, FileResolver};
|
use crate::diagnostic::{Diagnostic, FileResolver, Severity};
|
||||||
|
|
||||||
pub(super) struct GithubRenderer<'a> {
|
pub struct GithubRenderer<'a> {
|
||||||
resolver: &'a dyn FileResolver,
|
resolver: &'a dyn FileResolver,
|
||||||
|
program: &'a str,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> GithubRenderer<'a> {
|
impl<'a> GithubRenderer<'a> {
|
||||||
pub(super) fn new(resolver: &'a dyn FileResolver) -> Self {
|
pub fn new(resolver: &'a dyn FileResolver, program: &'a str) -> Self {
|
||||||
Self { resolver }
|
Self { resolver, program }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn render(
|
pub(super) fn render(
|
||||||
|
@ -15,9 +16,15 @@ impl<'a> GithubRenderer<'a> {
|
||||||
diagnostics: &[Diagnostic],
|
diagnostics: &[Diagnostic],
|
||||||
) -> std::fmt::Result {
|
) -> std::fmt::Result {
|
||||||
for diagnostic in diagnostics {
|
for diagnostic in diagnostics {
|
||||||
|
let severity = match diagnostic.severity() {
|
||||||
|
Severity::Info => "notice",
|
||||||
|
Severity::Warning => "warning",
|
||||||
|
Severity::Error | Severity::Fatal => "error",
|
||||||
|
};
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"::error title=Ruff ({code})",
|
"::{severity} title={program} ({code})",
|
||||||
|
program = self.program,
|
||||||
code = diagnostic.secondary_code_or_id()
|
code = diagnostic.secondary_code_or_id()
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
@ -75,6 +82,26 @@ impl<'a> GithubRenderer<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct DisplayGithubDiagnostics<'a> {
|
||||||
|
renderer: &'a GithubRenderer<'a>,
|
||||||
|
diagnostics: &'a [Diagnostic],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> DisplayGithubDiagnostics<'a> {
|
||||||
|
pub fn new(renderer: &'a GithubRenderer<'a>, diagnostics: &'a [Diagnostic]) -> Self {
|
||||||
|
Self {
|
||||||
|
renderer,
|
||||||
|
diagnostics,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for DisplayGithubDiagnostics<'_> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
self.renderer.render(f, self.diagnostics)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::diagnostic::{
|
use crate::diagnostic::{
|
||||||
|
@ -103,7 +130,7 @@ mod tests {
|
||||||
|
|
||||||
insta::assert_snapshot!(
|
insta::assert_snapshot!(
|
||||||
env.render(&diag),
|
env.render(&diag),
|
||||||
@"::error title=Ruff (test-diagnostic)::test-diagnostic: main diagnostic message",
|
@"::error title=ty (test-diagnostic)::test-diagnostic: main diagnostic message",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,6 @@
|
||||||
source: crates/ruff_db/src/diagnostic/render/github.rs
|
source: crates/ruff_db/src/diagnostic/render/github.rs
|
||||||
expression: env.render_diagnostics(&diagnostics)
|
expression: env.render_diagnostics(&diagnostics)
|
||||||
---
|
---
|
||||||
::error title=Ruff (F401),file=fib.py,line=1,col=8,endLine=1,endColumn=10::fib.py:1:8: F401 `os` imported but unused
|
::error title=ty (F401),file=fib.py,line=1,col=8,endLine=1,endColumn=10::fib.py:1:8: F401 `os` imported but unused
|
||||||
::error title=Ruff (F841),file=fib.py,line=6,col=5,endLine=6,endColumn=6::fib.py:6:5: F841 Local variable `x` is assigned to but never used
|
::error title=ty (F841),file=fib.py,line=6,col=5,endLine=6,endColumn=6::fib.py:6:5: F841 Local variable `x` is assigned to but never used
|
||||||
::error title=Ruff (F821),file=undef.py,line=1,col=4,endLine=1,endColumn=5::undef.py:1:4: F821 Undefined name `a`
|
::error title=ty (F821),file=undef.py,line=1,col=4,endLine=1,endColumn=5::undef.py:1:4: F821 Undefined name `a`
|
||||||
|
|
|
@ -2,5 +2,5 @@
|
||||||
source: crates/ruff_db/src/diagnostic/render/github.rs
|
source: crates/ruff_db/src/diagnostic/render/github.rs
|
||||||
expression: env.render_diagnostics(&diagnostics)
|
expression: env.render_diagnostics(&diagnostics)
|
||||||
---
|
---
|
||||||
::error title=Ruff (invalid-syntax),file=syntax_errors.py,line=1,col=15,endLine=2,endColumn=1::syntax_errors.py:1:15: invalid-syntax: Expected one or more symbol names after import
|
::error title=ty (invalid-syntax),file=syntax_errors.py,line=1,col=15,endLine=2,endColumn=1::syntax_errors.py:1:15: invalid-syntax: Expected one or more symbol names after import
|
||||||
::error title=Ruff (invalid-syntax),file=syntax_errors.py,line=3,col=12,endLine=4,endColumn=1::syntax_errors.py:3:12: invalid-syntax: Expected ')', found newline
|
::error title=ty (invalid-syntax),file=syntax_errors.py,line=3,col=12,endLine=4,endColumn=1::syntax_errors.py:3:12: invalid-syntax: Expected ')', found newline
|
||||||
|
|
1
crates/ty/docs/cli.md
generated
1
crates/ty/docs/cli.md
generated
|
@ -63,6 +63,7 @@ over all configuration files.</p>
|
||||||
<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>
|
<li><code>gitlab</code>: Print diagnostics in the JSON format expected by GitLab Code Quality reports</li>
|
||||||
|
<li><code>github</code>: Print diagnostics in the format used by GitHub Actions workflow error annotations</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>
|
||||||
|
|
|
@ -324,6 +324,9 @@ pub enum OutputFormat {
|
||||||
/// Print diagnostics in the JSON format expected by GitLab Code Quality reports.
|
/// Print diagnostics in the JSON format expected by GitLab Code Quality reports.
|
||||||
#[value(name = "gitlab")]
|
#[value(name = "gitlab")]
|
||||||
Gitlab,
|
Gitlab,
|
||||||
|
#[value(name = "github")]
|
||||||
|
/// Print diagnostics in the format used by GitHub Actions workflow error annotations.
|
||||||
|
Github,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<OutputFormat> for ty_project::metadata::options::OutputFormat {
|
impl From<OutputFormat> for ty_project::metadata::options::OutputFormat {
|
||||||
|
@ -332,6 +335,7 @@ impl From<OutputFormat> for ty_project::metadata::options::OutputFormat {
|
||||||
OutputFormat::Full => Self::Full,
|
OutputFormat::Full => Self::Full,
|
||||||
OutputFormat::Concise => Self::Concise,
|
OutputFormat::Concise => Self::Concise,
|
||||||
OutputFormat::Gitlab => Self::Gitlab,
|
OutputFormat::Gitlab => Self::Gitlab,
|
||||||
|
OutputFormat::Github => Self::Github,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -683,6 +683,30 @@ fn gitlab_diagnostics() -> anyhow::Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn github_diagnostics() -> anyhow::Result<()> {
|
||||||
|
let case = CliTest::with_file(
|
||||||
|
"test.py",
|
||||||
|
r#"
|
||||||
|
print(x) # [unresolved-reference]
|
||||||
|
print(4[1]) # [non-subscriptable]
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
assert_cmd_snapshot!(case.command().arg("--output-format=github").arg("--warn").arg("unresolved-reference"), @r"
|
||||||
|
success: false
|
||||||
|
exit_code: 1
|
||||||
|
----- stdout -----
|
||||||
|
::warning title=ty (unresolved-reference),file=test.py,line=2,col=7,endLine=2,endColumn=8::test.py:2:7: unresolved-reference: Name `x` used when not defined
|
||||||
|
::error title=ty (non-subscriptable),file=test.py,line=3,col=7,endLine=3,endColumn=8::test.py:3:7: non-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
|
||||||
|
|
||||||
|
----- 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
|
||||||
|
|
|
@ -1077,6 +1077,10 @@ pub enum OutputFormat {
|
||||||
///
|
///
|
||||||
/// [Code Quality]: https://docs.gitlab.com/ci/testing/code_quality/#code-quality-report-format
|
/// [Code Quality]: https://docs.gitlab.com/ci/testing/code_quality/#code-quality-report-format
|
||||||
Gitlab,
|
Gitlab,
|
||||||
|
/// Print diagnostics in the format used by [GitHub Actions] workflow error annotations.
|
||||||
|
///
|
||||||
|
/// [GitHub Actions]: https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands#setting-an-error-message
|
||||||
|
Github,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OutputFormat {
|
impl OutputFormat {
|
||||||
|
@ -1096,6 +1100,7 @@ impl From<OutputFormat> for DiagnosticFormat {
|
||||||
OutputFormat::Full => Self::Full,
|
OutputFormat::Full => Self::Full,
|
||||||
OutputFormat::Concise => Self::Concise,
|
OutputFormat::Concise => Self::Concise,
|
||||||
OutputFormat::Gitlab => Self::Gitlab,
|
OutputFormat::Gitlab => Self::Gitlab,
|
||||||
|
OutputFormat::Github => Self::Github,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
7
ty.schema.json
generated
7
ty.schema.json
generated
|
@ -171,6 +171,13 @@
|
||||||
"enum": [
|
"enum": [
|
||||||
"gitlab"
|
"gitlab"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Print diagnostics in the format used by [GitHub Actions] workflow error annotations.\n\n[GitHub Actions]: https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands#setting-an-error-message",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"github"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue