mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-02 06:41:23 +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 ruff_db::diagnostic::{
|
||||
Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig, DisplayDiagnostics, SecondaryCode,
|
||||
Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig, DisplayDiagnostics,
|
||||
DisplayGithubDiagnostics, GithubRenderer, SecondaryCode,
|
||||
};
|
||||
use ruff_linter::fs::relativize_path;
|
||||
use ruff_linter::logging::LogLevel;
|
||||
|
@ -224,32 +225,26 @@ impl Printer {
|
|||
let context = EmitterContext::new(&diagnostics.notebook_indexes);
|
||||
let fixables = FixableStatistics::try_from(diagnostics, self.unsafe_fixes);
|
||||
|
||||
let config = DisplayDiagnosticConfig::default().preview(preview);
|
||||
|
||||
match self.format {
|
||||
OutputFormat::Json => {
|
||||
let config = DisplayDiagnosticConfig::default()
|
||||
.format(DiagnosticFormat::Json)
|
||||
.preview(preview);
|
||||
let config = config.format(DiagnosticFormat::Json);
|
||||
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
|
||||
write!(writer, "{value}")?;
|
||||
}
|
||||
OutputFormat::Rdjson => {
|
||||
let config = DisplayDiagnosticConfig::default()
|
||||
.format(DiagnosticFormat::Rdjson)
|
||||
.preview(preview);
|
||||
let config = config.format(DiagnosticFormat::Rdjson);
|
||||
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
|
||||
write!(writer, "{value}")?;
|
||||
}
|
||||
OutputFormat::JsonLines => {
|
||||
let config = DisplayDiagnosticConfig::default()
|
||||
.format(DiagnosticFormat::JsonLines)
|
||||
.preview(preview);
|
||||
let config = config.format(DiagnosticFormat::JsonLines);
|
||||
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
|
||||
write!(writer, "{value}")?;
|
||||
}
|
||||
OutputFormat::Junit => {
|
||||
let config = DisplayDiagnosticConfig::default()
|
||||
.format(DiagnosticFormat::Junit)
|
||||
.preview(preview);
|
||||
let config = config.format(DiagnosticFormat::Junit);
|
||||
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
|
||||
write!(writer, "{value}")?;
|
||||
}
|
||||
|
@ -288,30 +283,22 @@ impl Printer {
|
|||
self.write_summary_text(writer, diagnostics)?;
|
||||
}
|
||||
OutputFormat::Github => {
|
||||
let config = DisplayDiagnosticConfig::default()
|
||||
.format(DiagnosticFormat::Github)
|
||||
.preview(preview);
|
||||
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
|
||||
let renderer = GithubRenderer::new(&context, "Ruff");
|
||||
let value = DisplayGithubDiagnostics::new(&renderer, &diagnostics.inner);
|
||||
write!(writer, "{value}")?;
|
||||
}
|
||||
OutputFormat::Gitlab => {
|
||||
let config = DisplayDiagnosticConfig::default()
|
||||
.format(DiagnosticFormat::Gitlab)
|
||||
.preview(preview);
|
||||
let config = config.format(DiagnosticFormat::Gitlab);
|
||||
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
|
||||
write!(writer, "{value}")?;
|
||||
}
|
||||
OutputFormat::Pylint => {
|
||||
let config = DisplayDiagnosticConfig::default()
|
||||
.format(DiagnosticFormat::Pylint)
|
||||
.preview(preview);
|
||||
let config = config.format(DiagnosticFormat::Pylint);
|
||||
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
|
||||
write!(writer, "{value}")?;
|
||||
}
|
||||
OutputFormat::Azure => {
|
||||
let config = DisplayDiagnosticConfig::default()
|
||||
.format(DiagnosticFormat::Azure)
|
||||
.preview(preview);
|
||||
let config = config.format(DiagnosticFormat::Azure);
|
||||
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
|
||||
write!(writer, "{value}")?;
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ use ruff_text_size::{Ranged, TextRange, TextSize};
|
|||
|
||||
pub use self::render::{
|
||||
DisplayDiagnostic, DisplayDiagnostics, FileResolver, Input, ceil_char_boundary,
|
||||
github::{DisplayGithubDiagnostics, GithubRenderer},
|
||||
};
|
||||
use crate::{Db, files::File};
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ use pylint::PylintRenderer;
|
|||
mod azure;
|
||||
mod concise;
|
||||
mod full;
|
||||
mod github;
|
||||
pub mod github;
|
||||
#[cfg(feature = "serde")]
|
||||
mod gitlab;
|
||||
#[cfg(feature = "serde")]
|
||||
|
@ -145,7 +145,7 @@ impl std::fmt::Display for DisplayDiagnostics<'_> {
|
|||
gitlab::GitlabRenderer::new(self.resolver).render(f, self.diagnostics)?;
|
||||
}
|
||||
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,
|
||||
program: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> GithubRenderer<'a> {
|
||||
pub(super) fn new(resolver: &'a dyn FileResolver) -> Self {
|
||||
Self { resolver }
|
||||
pub fn new(resolver: &'a dyn FileResolver, program: &'a str) -> Self {
|
||||
Self { resolver, program }
|
||||
}
|
||||
|
||||
pub(super) fn render(
|
||||
|
@ -15,9 +16,15 @@ impl<'a> GithubRenderer<'a> {
|
|||
diagnostics: &[Diagnostic],
|
||||
) -> std::fmt::Result {
|
||||
for diagnostic in diagnostics {
|
||||
let severity = match diagnostic.severity() {
|
||||
Severity::Info => "notice",
|
||||
Severity::Warning => "warning",
|
||||
Severity::Error | Severity::Fatal => "error",
|
||||
};
|
||||
write!(
|
||||
f,
|
||||
"::error title=Ruff ({code})",
|
||||
"::{severity} title={program} ({code})",
|
||||
program = self.program,
|
||||
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)]
|
||||
mod tests {
|
||||
use crate::diagnostic::{
|
||||
|
@ -103,7 +130,7 @@ mod tests {
|
|||
|
||||
insta::assert_snapshot!(
|
||||
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
|
||||
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=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=Ruff (F821),file=undef.py,line=1,col=4,endLine=1,endColumn=5::undef.py:1:4: F821 Undefined name `a`
|
||||
::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=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=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
|
||||
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=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=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=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>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>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>
|
||||
<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>
|
||||
|
|
|
@ -324,6 +324,9 @@ pub enum OutputFormat {
|
|||
/// Print diagnostics in the JSON format expected by GitLab Code Quality reports.
|
||||
#[value(name = "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 {
|
||||
|
@ -332,6 +335,7 @@ impl From<OutputFormat> for ty_project::metadata::options::OutputFormat {
|
|||
OutputFormat::Full => Self::Full,
|
||||
OutputFormat::Concise => Self::Concise,
|
||||
OutputFormat::Gitlab => Self::Gitlab,
|
||||
OutputFormat::Github => Self::Github,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -683,6 +683,30 @@ fn gitlab_diagnostics() -> anyhow::Result<()> {
|
|||
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 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
|
||||
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 {
|
||||
|
@ -1096,6 +1100,7 @@ impl From<OutputFormat> for DiagnosticFormat {
|
|||
OutputFormat::Full => Self::Full,
|
||||
OutputFormat::Concise => Self::Concise,
|
||||
OutputFormat::Gitlab => Self::Gitlab,
|
||||
OutputFormat::Github => Self::Github,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
7
ty.schema.json
generated
7
ty.schema.json
generated
|
@ -171,6 +171,13 @@
|
|||
"enum": [
|
||||
"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