diff --git a/crates/ruff/tests/cli/snapshots/cli__format__output_format_junit.snap b/crates/ruff/tests/cli/snapshots/cli__format__output_format_junit.snap index 4eb5f66693..2037bc8d05 100644 --- a/crates/ruff/tests/cli/snapshots/cli__format__output_format_junit.snap +++ b/crates/ruff/tests/cli/snapshots/cli__format__output_format_junit.snap @@ -18,9 +18,15 @@ exit_code: 1 - - line 1, col 1, File would be reformatted - + diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__output_format_junit.snap b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_junit.snap index 16fda42bd3..2aee51e199 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__output_format_junit.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_junit.snap @@ -20,23 +20,38 @@ exit_code: 1 - - line 1, col 8, `os` imported but unused - - Help: Remove unused import: `os` + - - line 2, col 5, Undefined name `y` - + - - line 3, col 1, Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10) - + diff --git a/crates/ruff_db/src/diagnostic/render/junit.rs b/crates/ruff_db/src/diagnostic/render/junit.rs index bbc4efb8d3..02b6602843 100644 --- a/crates/ruff_db/src/diagnostic/render/junit.rs +++ b/crates/ruff_db/src/diagnostic/render/junit.rs @@ -3,6 +3,7 @@ use std::{collections::BTreeMap, ops::Deref}; use quick_junit::{NonSuccessKind, Report, TestCase, TestCaseStatus, TestSuite, XmlString}; +use ruff_annotate_snippets::{Level, Renderer, Snippet}; use ruff_source_file::LineColumn; use crate::diagnostic::{Diagnostic, SecondaryCode, render::FileResolver}; @@ -49,14 +50,14 @@ impl<'a> JunitRenderer<'a> { .extra .insert(XmlString::new("package"), XmlString::new("org.ruff")); + let indent = " ".repeat(4 * 4); for diagnostic in diagnostics { let DiagnosticWithLocation { diagnostic, start_location: location, } = diagnostic; - let indent = " ".repeat(4 * 4); - let mut sub_diags = diagnostic + let mut output = diagnostic .sub_diagnostics() .iter() .map(|sub_diagnostic| { @@ -64,7 +65,8 @@ impl<'a> JunitRenderer<'a> { // parent and formats it for rendering into the XML structure let message = sub_diagnostic.concise_message().to_string(); - let severity = sub_diagnostic.inner.severity; + let severity = + format!("{:?}", sub_diagnostic.inner.severity).to_lowercase(); let mut annotations = vec![]; // Add function/method/etc definition location @@ -99,23 +101,17 @@ impl<'a> JunitRenderer<'a> { ); } annotations.push(format!( - "{indent}{severity:?}: {sub_message} → {path}{sub_diag_loc}", + "{indent}{severity}: {sub_message} → {path}{sub_diag_loc}", )); } if annotations.is_empty() { - format!("{indent}{severity:?}: {message}") - } else { - annotations.join("\n") + annotations.push(format!("{indent}{severity}: {message}")); } + annotations.join("\n") }) .collect::>() .join("\n"); - if !sub_diags.is_empty() { - // Add some space between the body and sub_diags if there are some - sub_diags = format!("\n\n{sub_diags}"); - } - let code = diagnostic .secondary_code() .map_or_else(|| diagnostic.name(), SecondaryCode::as_str); @@ -124,8 +120,12 @@ impl<'a> JunitRenderer<'a> { let classname = Path::new(filename).with_extension(""); case.set_classname(classname.to_str().unwrap()); case.status.set_message(diagnostic.body()); + case.status.set_description(format!( + "\n{snippet}\n{output}\n{after_indent}", + snippet = self.render_snippet(diagnostic, Some(indent.len())), + after_indent = " ".repeat(4 * 3), // tag closes one indent less + )); - let mut diagnostic_loc = String::new(); if let Some(location) = location { case.extra.insert( XmlString::new("line"), @@ -135,17 +135,8 @@ impl<'a> JunitRenderer<'a> { XmlString::new("column"), XmlString::new(location.column.to_string()), ); - diagnostic_loc = format!( - "line {row}, col {col}, ", - row = location.line, - col = location.column, - ); } - case.status.set_description(format!( - "\n{indent}{diagnostic_loc}{body}{sub_diags}\n{after_indent}", - after_indent = " ".repeat(4 * 3), // tag closes one indent less - body = diagnostic.body(), - )); + test_suite.add_test_case(case); } report.add_test_suite(test_suite); @@ -155,6 +146,79 @@ impl<'a> JunitRenderer<'a> { let adapter = FmtAdapter { fmt: f }; report.serialize(adapter).map_err(|_| std::fmt::Error) } + + fn render_snippet(&self, diagnostic: &Diagnostic, indentation: Option) -> String { + let (source_text, filename) = if let Some(span) = diagnostic.primary_span_ref() { + let file = span.file(); + let source = file.diagnostic_source(self.resolver); + let filename = match file { + crate::diagnostic::UnifiedFile::Ty(file) => self.resolver.path(*file), + crate::diagnostic::UnifiedFile::Ruff(file) => file.name(), + }; + ( + source.as_source_code().text().to_string(), + filename.to_string(), + ) + } else { + return String::new(); + }; + + let mut snippet = Snippet::source(&source_text) + .line_start(1) + .origin(&filename); + + let mut annotations = vec![]; + if let Some(primary) = diagnostic.primary_annotation() { + if let Some(range) = primary.get_span().range() { + annotations.push( + Level::Error + .span(range.into()) + // Message next to the location of the problem + .label(primary.get_message().unwrap_or_default()), + ); + } + } + + for secondary in diagnostic.secondary_annotations() { + if let Some(range) = secondary.get_span().range() { + annotations.push( + Level::Info + .span(range.into()) + // Message at related location involved in the problem + .label(secondary.get_message().unwrap_or_default()), + ); + } + } + + for sub in diagnostic.sub_diagnostics() { + if let Some(primary) = sub.primary_annotation() { + if let Some(range) = primary.get_span().range() { + annotations.push( + Level::Help + .span(range.into()) + // Help message goes here usually + .label(primary.get_message().unwrap_or_default()), + ); + } + } + } + snippet = snippet.annotations(annotations); + + let message = Level::Error.title(diagnostic.body()).snippet(snippet); + let renderer = Renderer::plain(); + let rendered = renderer.render(message).to_string(); + + if let Some(indentation) = indentation { + let indent = " ".repeat(indentation); + rendered + .lines() + .map(|line| format!("{indent}{line}")) + .collect::>() + .join("\n") + } else { + rendered + } + } } // TODO(brent) this and `group_diagnostics_by_filename` are also used by the `grouped` output diff --git a/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__junit__tests__output.snap b/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__junit__tests__output.snap index e13f8fc822..f8c6dcdd32 100644 --- a/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__junit__tests__output.snap +++ b/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__junit__tests__output.snap @@ -7,24 +7,58 @@ expression: env.render_diagnostics(&diagnostics) - line 1, col 8, `os` imported but unused - - Help: Remove unused import: `os` + error: `os` imported but unused + --> /fib.py:1:8 + | + 1 | import os + | ^^ + 2 | + 3 | + 4 | def fibonacci(n): + 5 | """Compute the nth number in the Fibonacci sequence.""" + 6 | x = 1 + 7 | if n == 0: + 8 | return 0 + 9 | elif n == 1: + 10 | return 1 + 11 | else: + 12 | return fibonacci(n - 1) + fibonacci(n - 2) + | + help: Remove unused import: `os` - line 6, col 5, Local variable `x` is assigned to but never used - - Help: Remove assignment to unused variable `x` + error: Local variable `x` is assigned to but never used + --> /fib.py:6:5 + | + 1 | import os + 2 | + 3 | + 4 | def fibonacci(n): + 5 | """Compute the nth number in the Fibonacci sequence.""" + 6 | x = 1 + | ^ + 7 | if n == 0: + 8 | return 0 + 9 | elif n == 1: + 10 | return 1 + 11 | else: + 12 | return fibonacci(n - 1) + fibonacci(n - 2) + | + help: Remove assignment to unused variable `x` - line 1, col 4, Undefined name `a` - + error: Undefined name `a` + --> /undef.py:1:4 + | + 1 | if a == 1: pass + | ^ + | diff --git a/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__junit__tests__syntax_errors.snap b/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__junit__tests__syntax_errors.snap index fbd7ce2062..a1674ec701 100644 --- a/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__junit__tests__syntax_errors.snap +++ b/crates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__junit__tests__syntax_errors.snap @@ -7,15 +7,31 @@ expression: env.render_diagnostics(&diagnostics) - line 1, col 15, Expected one or more symbol names after import - + error: Expected one or more symbol names after import + --> /syntax_errors.py:1:15 + | + 1 | from os import + | ^ + 2 | + 3 | if call(foo + 4 | def bar(): + 5 | pass + | - line 3, col 12, Expected ')', found newline - + error: Expected ')', found newline + --> /syntax_errors.py:3:12 + | + 1 | from os import + 2 | + 3 | if call(foo + | ^ + 4 | def bar(): + 5 | pass + |