mirror of
https://github.com/astral-sh/ruff.git
synced 2025-12-23 09:19:58 +00:00
Try to render code snippets with annotations
This commit is contained in:
parent
6e94d19624
commit
d32b2ed46f
5 changed files with 183 additions and 48 deletions
|
|
@ -18,9 +18,15 @@ exit_code: 1
|
|||
<testsuites name="ruff" tests="1" failures="1" errors="0">
|
||||
<testsuite name="[TMP]/input.py" tests="1" disabled="0" errors="0" failures="1" package="org.ruff">
|
||||
<testcase name="org.ruff.unformatted" classname="[TMP]/input" line="1" column="1">
|
||||
<failure message="File would be reformatted">
|
||||
line 1, col 1, File would be reformatted
|
||||
|
||||
<failure message="File would be reformatted"[TMP]/input.py:1:1
|
||||
|
|
||||
1 |
|
||||
| ^
|
||||
2 | from test import say_hy
|
||||
3 |
|
||||
4 | if __name__ == "__main__":
|
||||
5 | say_hy("dear Ruff contributor")
|
||||
|
|
||||
|
||||
</failure>
|
||||
</testcase>
|
||||
|
|
|
|||
|
|
@ -20,23 +20,38 @@ exit_code: 1
|
|||
<testsuites name="ruff" tests="3" failures="3" errors="0">
|
||||
<testsuite name="[TMP]/input.py" tests="3" disabled="0" errors="0" failures="3" package="org.ruff">
|
||||
<testcase name="org.ruff.F401" classname="[TMP]/input" line="1" column="8">
|
||||
<failure message="`os` imported but unused">
|
||||
line 1, col 8, `os` imported but unused
|
||||
|
||||
Help: Remove unused import: `os`
|
||||
<failure message="`os` imported but unused"[TMP]/input.py:1:8
|
||||
|
|
||||
1 | import os # F401
|
||||
| ^^
|
||||
2 | x = y # F821
|
||||
3 | match 42: # invalid-syntax
|
||||
4 | case _: ...
|
||||
|
|
||||
help: Remove unused import: `os`
|
||||
</failure>
|
||||
</testcase>
|
||||
<testcase name="org.ruff.F821" classname="[TMP]/input" line="2" column="5">
|
||||
<failure message="Undefined name `y`">
|
||||
line 2, col 5, Undefined name `y`
|
||||
|
||||
<failure message="Undefined name `y`"[TMP]/input.py:2:5
|
||||
|
|
||||
1 | import os # F401
|
||||
2 | x = y # F821
|
||||
| ^
|
||||
3 | match 42: # invalid-syntax
|
||||
4 | case _: ...
|
||||
|
|
||||
|
||||
</failure>
|
||||
</testcase>
|
||||
<testcase name="org.ruff.invalid-syntax" classname="[TMP]/input" line="3" column="1">
|
||||
<failure message="Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)">
|
||||
line 3, col 1, Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
|
||||
|
||||
<failure message="Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)"[TMP]/input.py:3:1
|
||||
|
|
||||
1 | import os # F401
|
||||
2 | x = y # F821
|
||||
3 | match 42: # invalid-syntax
|
||||
| ^^^^^
|
||||
4 | case _: ...
|
||||
|
|
||||
|
||||
</failure>
|
||||
</testcase>
|
||||
|
|
|
|||
|
|
@ -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::<Vec<String>>()
|
||||
.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), // <failure> 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), // <failure> 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<usize>) -> 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::<Vec<_>>()
|
||||
.join("\n")
|
||||
} else {
|
||||
rendered
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(brent) this and `group_diagnostics_by_filename` are also used by the `grouped` output
|
||||
|
|
|
|||
|
|
@ -7,24 +7,58 @@ expression: env.render_diagnostics(&diagnostics)
|
|||
<testsuite name="/fib.py" tests="2" disabled="0" errors="0" failures="2" package="org.ruff">
|
||||
<testcase name="org.ruff.F401" classname="/fib" line="1" column="8">
|
||||
<failure message="`os` imported but unused">
|
||||
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`
|
||||
</failure>
|
||||
</testcase>
|
||||
<testcase name="org.ruff.F841" classname="/fib" line="6" column="5">
|
||||
<failure message="Local variable `x` is assigned to but never used">
|
||||
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`
|
||||
</failure>
|
||||
</testcase>
|
||||
</testsuite>
|
||||
<testsuite name="/undef.py" tests="1" disabled="0" errors="0" failures="1" package="org.ruff">
|
||||
<testcase name="org.ruff.F821" classname="/undef" line="1" column="4">
|
||||
<failure message="Undefined name `a`">
|
||||
line 1, col 4, Undefined name `a`
|
||||
|
||||
error: Undefined name `a`
|
||||
--> /undef.py:1:4
|
||||
|
|
||||
1 | if a == 1: pass
|
||||
| ^
|
||||
|
|
||||
|
||||
</failure>
|
||||
</testcase>
|
||||
|
|
|
|||
|
|
@ -7,15 +7,31 @@ expression: env.render_diagnostics(&diagnostics)
|
|||
<testsuite name="/syntax_errors.py" tests="2" disabled="0" errors="0" failures="2" package="org.ruff">
|
||||
<testcase name="org.ruff.invalid-syntax" classname="/syntax_errors" line="1" column="15">
|
||||
<failure message="Expected one or more symbol names after import">
|
||||
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
|
||||
|
|
||||
|
||||
</failure>
|
||||
</testcase>
|
||||
<testcase name="org.ruff.invalid-syntax" classname="/syntax_errors" line="3" column="12">
|
||||
<failure message="Expected ')', found newline">
|
||||
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
|
||||
|
|
||||
|
||||
</failure>
|
||||
</testcase>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue