diff --git a/crates/ruff/src/printer.rs b/crates/ruff/src/printer.rs index 79eda760d5..bf18f66f41 100644 --- a/crates/ruff/src/printer.rs +++ b/crates/ruff/src/printer.rs @@ -14,9 +14,7 @@ use ruff_db::diagnostic::{ }; use ruff_linter::fs::relativize_path; use ruff_linter::logging::LogLevel; -use ruff_linter::message::{ - Emitter, EmitterContext, GithubEmitter, GroupedEmitter, SarifEmitter, TextEmitter, -}; +use ruff_linter::message::{Emitter, EmitterContext, GroupedEmitter, SarifEmitter, TextEmitter}; use ruff_linter::notify_user; use ruff_linter::settings::flags::{self}; use ruff_linter::settings::types::{OutputFormat, UnsafeFixes}; @@ -290,7 +288,11 @@ impl Printer { self.write_summary_text(writer, diagnostics)?; } OutputFormat::Github => { - GithubEmitter.emit(writer, &diagnostics.inner, &context)?; + let config = DisplayDiagnosticConfig::default() + .format(DiagnosticFormat::Github) + .preview(preview); + let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner); + write!(writer, "{value}")?; } OutputFormat::Gitlab => { let config = DisplayDiagnosticConfig::default() diff --git a/crates/ruff_db/src/diagnostic/mod.rs b/crates/ruff_db/src/diagnostic/mod.rs index 7979cafc68..ff8d8939d3 100644 --- a/crates/ruff_db/src/diagnostic/mod.rs +++ b/crates/ruff_db/src/diagnostic/mod.rs @@ -1447,6 +1447,11 @@ pub enum DiagnosticFormat { /// [Code Quality]: https://docs.gitlab.com/ci/testing/code_quality/#code-quality-report-format #[cfg(feature = "serde")] 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, } /// A representation of the kinds of messages inside a diagnostic. diff --git a/crates/ruff_db/src/diagnostic/render.rs b/crates/ruff_db/src/diagnostic/render.rs index 903b481d99..6eb87b8b7f 100644 --- a/crates/ruff_db/src/diagnostic/render.rs +++ b/crates/ruff_db/src/diagnostic/render.rs @@ -25,11 +25,13 @@ use super::{ use azure::AzureRenderer; use concise::ConciseRenderer; +use github::GithubRenderer; use pylint::PylintRenderer; mod azure; mod concise; mod full; +mod github; #[cfg(feature = "serde")] mod gitlab; #[cfg(feature = "serde")] @@ -142,6 +144,9 @@ impl std::fmt::Display for DisplayDiagnostics<'_> { DiagnosticFormat::Gitlab => { gitlab::GitlabRenderer::new(self.resolver).render(f, self.diagnostics)?; } + DiagnosticFormat::Github => { + GithubRenderer::new(self.resolver).render(f, self.diagnostics)?; + } } Ok(()) diff --git a/crates/ruff_db/src/diagnostic/render/github.rs b/crates/ruff_db/src/diagnostic/render/github.rs new file mode 100644 index 0000000000..da857c85d8 --- /dev/null +++ b/crates/ruff_db/src/diagnostic/render/github.rs @@ -0,0 +1,109 @@ +use crate::diagnostic::{Diagnostic, FileResolver}; + +pub(super) struct GithubRenderer<'a> { + resolver: &'a dyn FileResolver, +} + +impl<'a> GithubRenderer<'a> { + pub(super) fn new(resolver: &'a dyn FileResolver) -> Self { + Self { resolver } + } + + pub(super) fn render( + &self, + f: &mut std::fmt::Formatter, + diagnostics: &[Diagnostic], + ) -> std::fmt::Result { + for diagnostic in diagnostics { + write!( + f, + "::error title=Ruff ({code})", + code = diagnostic.secondary_code_or_id() + )?; + + if let Some(span) = diagnostic.primary_span() { + let file = span.file(); + write!(f, ",file={file}", file = file.path(self.resolver))?; + + let (start_location, end_location) = if self.resolver.is_notebook(file) { + // We can't give a reasonable location for the structured formats, + // so we show one that's clearly a fallback + None + } else { + let diagnostic_source = file.diagnostic_source(self.resolver); + let source_code = diagnostic_source.as_source_code(); + + span.range().map(|range| { + ( + source_code.line_column(range.start()), + source_code.line_column(range.end()), + ) + }) + } + .unwrap_or_default(); + + write!( + f, + ",line={row},col={column},endLine={end_row},endColumn={end_column}::", + row = start_location.line, + column = start_location.column, + end_row = end_location.line, + end_column = end_location.column, + )?; + + write!( + f, + "{path}:{row}:{column}: ", + path = file.relative_path(self.resolver).display(), + row = start_location.line, + column = start_location.column, + )?; + } else { + write!(f, "::")?; + } + + if let Some(code) = diagnostic.secondary_code() { + write!(f, "{code}")?; + } else { + write!(f, "{id}:", id = diagnostic.id())?; + } + + writeln!(f, " {}", diagnostic.body())?; + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::diagnostic::{ + DiagnosticFormat, + render::tests::{TestEnvironment, create_diagnostics, create_syntax_error_diagnostics}, + }; + + #[test] + fn output() { + let (env, diagnostics) = create_diagnostics(DiagnosticFormat::Github); + insta::assert_snapshot!(env.render_diagnostics(&diagnostics)); + } + + #[test] + fn syntax_errors() { + let (env, diagnostics) = create_syntax_error_diagnostics(DiagnosticFormat::Github); + insta::assert_snapshot!(env.render_diagnostics(&diagnostics)); + } + + #[test] + fn missing_file() { + let mut env = TestEnvironment::new(); + env.format(DiagnosticFormat::Github); + + let diag = env.err().build(); + + insta::assert_snapshot!( + env.render(&diag), + @"::error title=Ruff (test-diagnostic)::test-diagnostic: main diagnostic message", + ); + } +} diff --git a/crates/ruff_linter/src/message/snapshots/ruff_linter__message__github__tests__output.snap b/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__github__tests__output.snap similarity index 78% rename from crates/ruff_linter/src/message/snapshots/ruff_linter__message__github__tests__output.snap rename to crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__github__tests__output.snap index c816059368..fcb1ea24ef 100644 --- a/crates/ruff_linter/src/message/snapshots/ruff_linter__message__github__tests__output.snap +++ b/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__github__tests__output.snap @@ -1,7 +1,6 @@ --- -source: crates/ruff_linter/src/message/github.rs -expression: content -snapshot_kind: text +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 diff --git a/crates/ruff_linter/src/message/snapshots/ruff_linter__message__github__tests__syntax_errors.snap b/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__github__tests__syntax_errors.snap similarity index 77% rename from crates/ruff_linter/src/message/snapshots/ruff_linter__message__github__tests__syntax_errors.snap rename to crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__github__tests__syntax_errors.snap index 55436236c1..76f824c2ef 100644 --- a/crates/ruff_linter/src/message/snapshots/ruff_linter__message__github__tests__syntax_errors.snap +++ b/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__github__tests__syntax_errors.snap @@ -1,6 +1,6 @@ --- -source: crates/ruff_linter/src/message/github.rs -expression: content +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 diff --git a/crates/ruff_linter/src/message/github.rs b/crates/ruff_linter/src/message/github.rs deleted file mode 100644 index 9b4eebfcb5..0000000000 --- a/crates/ruff_linter/src/message/github.rs +++ /dev/null @@ -1,90 +0,0 @@ -use std::io::Write; - -use ruff_db::diagnostic::Diagnostic; -use ruff_source_file::LineColumn; - -use crate::fs::relativize_path; -use crate::message::{Emitter, EmitterContext}; - -/// Generate error workflow command in GitHub Actions format. -/// See: [GitHub documentation](https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message) -#[derive(Default)] -pub struct GithubEmitter; - -impl Emitter for GithubEmitter { - fn emit( - &mut self, - writer: &mut dyn Write, - diagnostics: &[Diagnostic], - context: &EmitterContext, - ) -> anyhow::Result<()> { - for diagnostic in diagnostics { - let source_location = diagnostic.ruff_start_location().unwrap_or_default(); - let filename = diagnostic.expect_ruff_filename(); - let location = if context.is_notebook(&filename) { - // We can't give a reasonable location for the structured formats, - // so we show one that's clearly a fallback - LineColumn::default() - } else { - source_location - }; - - let end_location = diagnostic.ruff_end_location().unwrap_or_default(); - - write!( - writer, - "::error title=Ruff ({code}),file={file},line={row},col={column},endLine={end_row},endColumn={end_column}::", - code = diagnostic.secondary_code_or_id(), - file = filename, - row = source_location.line, - column = source_location.column, - end_row = end_location.line, - end_column = end_location.column, - )?; - - write!( - writer, - "{path}:{row}:{column}:", - path = relativize_path(&filename), - row = location.line, - column = location.column, - )?; - - if let Some(code) = diagnostic.secondary_code() { - write!(writer, " {code}")?; - } else { - write!(writer, " {id}:", id = diagnostic.id())?; - } - - writeln!(writer, " {}", diagnostic.body())?; - } - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use insta::assert_snapshot; - - use crate::message::GithubEmitter; - use crate::message::tests::{ - capture_emitter_output, create_diagnostics, create_syntax_error_diagnostics, - }; - - #[test] - fn output() { - let mut emitter = GithubEmitter; - let content = capture_emitter_output(&mut emitter, &create_diagnostics()); - - assert_snapshot!(content); - } - - #[test] - fn syntax_errors() { - let mut emitter = GithubEmitter; - let content = capture_emitter_output(&mut emitter, &create_syntax_error_diagnostics()); - - assert_snapshot!(content); - } -} diff --git a/crates/ruff_linter/src/message/mod.rs b/crates/ruff_linter/src/message/mod.rs index fe97c1ca52..994dc81296 100644 --- a/crates/ruff_linter/src/message/mod.rs +++ b/crates/ruff_linter/src/message/mod.rs @@ -9,7 +9,6 @@ use ruff_db::diagnostic::{ }; use ruff_db::files::File; -pub use github::GithubEmitter; pub use grouped::GroupedEmitter; use ruff_notebook::NotebookIndex; use ruff_source_file::SourceFile; @@ -20,7 +19,6 @@ pub use text::TextEmitter; use crate::Fix; use crate::registry::Rule; -mod github; mod grouped; mod sarif; mod text;