Skip to content

Commit fd335eb

Browse files
authored
Move fix suggestion to subdiagnostic (#19464)
Summary -- This PR tweaks Ruff's internal usage of the new diagnostic model to more closely match the intended use, as I understand it. Specifically, it moves the fix/help suggestion from the primary annotation's message to a subdiagnostic. In turn, it adds the secondary/noqa code as the new primary annotation message. As shown in the new `ruff_db` tests, this more closely mirrors Ruff's current diagnostic output. I also added `Severity::Help` to render the fix suggestion with a `help:` prefix instead of `info:`. These changes don't have any external impact now but should help a bit with #19415. Test Plan -- New full output format tests in `ruff_db` Rendered Diagnostics -- Full diagnostic output from `annotate-snippets` in this PR: ``` error[unused-import]: `os` imported but unused --> fib.py:1:8 | 1 | import os | ^^ | help: Remove unused import: `os` ``` Current Ruff output for the same code: ``` fib.py:1:8: F401 [*] `os` imported but unused | 1 | import os | ^^ F401 | = help: Remove unused import: `os` ``` Proposed final output after #19415: ``` F401 [*] `os` imported but unused --> fib.py:1:8 | 1 | import os | ^^ | help: Remove unused import: `os` ``` These are slightly updated from #19464 (comment) below to remove the extra noqa codes in the primary annotation messages for the first and third cases.
1 parent c82fa94 commit fd335eb

20 files changed

Lines changed: 234 additions & 105 deletions

File tree

crates/ruff/src/cache.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,7 @@ impl LintCacheData {
454454
CacheMessage {
455455
rule,
456456
body: msg.body().to_string(),
457-
suggestion: msg.suggestion().map(ToString::to_string),
457+
suggestion: msg.first_help_text().map(ToString::to_string),
458458
range: msg.expect_range(),
459459
parent: msg.parent(),
460460
fix: msg.fix().cloned(),

crates/ruff_db/src/diagnostic/mod.rs

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,14 @@ impl Diagnostic {
122122
/// directly. If callers want or need to avoid cloning the diagnostic
123123
/// message, then they can also pass a `DiagnosticMessage` directly.
124124
pub fn info<'a>(&mut self, message: impl IntoDiagnosticMessage + 'a) {
125-
self.sub(SubDiagnostic::new(Severity::Info, message));
125+
self.sub(SubDiagnostic::new(SubDiagnosticSeverity::Info, message));
126+
}
127+
128+
/// Adds a "help" sub-diagnostic with the given message.
129+
///
130+
/// See the closely related [`Diagnostic::info`] method for more details.
131+
pub fn help<'a>(&mut self, message: impl IntoDiagnosticMessage + 'a) {
132+
self.sub(SubDiagnostic::new(SubDiagnosticSeverity::Help, message));
126133
}
127134

128135
/// Adds a "sub" diagnostic to this diagnostic.
@@ -377,9 +384,15 @@ impl Diagnostic {
377384
self.primary_message()
378385
}
379386

380-
/// Returns the fix suggestion for the violation.
381-
pub fn suggestion(&self) -> Option<&str> {
382-
self.primary_annotation()?.get_message()
387+
/// Returns the message of the first sub-diagnostic with a `Help` severity.
388+
///
389+
/// Note that this is used as the fix title/suggestion for some of Ruff's output formats, but in
390+
/// general this is not the guaranteed meaning of such a message.
391+
pub fn first_help_text(&self) -> Option<&str> {
392+
self.sub_diagnostics()
393+
.iter()
394+
.find(|sub| matches!(sub.inner.severity, SubDiagnosticSeverity::Help))
395+
.map(|sub| sub.inner.message.as_str())
383396
}
384397

385398
/// Returns the URL for the rule documentation, if it exists.
@@ -565,7 +578,10 @@ impl SubDiagnostic {
565578
/// Callers can pass anything that implements `std::fmt::Display`
566579
/// directly. If callers want or need to avoid cloning the diagnostic
567580
/// message, then they can also pass a `DiagnosticMessage` directly.
568-
pub fn new<'a>(severity: Severity, message: impl IntoDiagnosticMessage + 'a) -> SubDiagnostic {
581+
pub fn new<'a>(
582+
severity: SubDiagnosticSeverity,
583+
message: impl IntoDiagnosticMessage + 'a,
584+
) -> SubDiagnostic {
569585
let inner = Box::new(SubDiagnosticInner {
570586
severity,
571587
message: message.into_diagnostic_message(),
@@ -643,7 +659,7 @@ impl SubDiagnostic {
643659

644660
#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)]
645661
struct SubDiagnosticInner {
646-
severity: Severity,
662+
severity: SubDiagnosticSeverity,
647663
message: DiagnosticMessage,
648664
annotations: Vec<Annotation>,
649665
}
@@ -1170,6 +1186,30 @@ impl Severity {
11701186
}
11711187
}
11721188

1189+
/// Like [`Severity`] but exclusively for sub-diagnostics.
1190+
///
1191+
/// This supports an additional `Help` severity that may not be needed in main diagnostics.
1192+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, get_size2::GetSize)]
1193+
pub enum SubDiagnosticSeverity {
1194+
Help,
1195+
Info,
1196+
Warning,
1197+
Error,
1198+
Fatal,
1199+
}
1200+
1201+
impl SubDiagnosticSeverity {
1202+
fn to_annotate(self) -> AnnotateLevel {
1203+
match self {
1204+
SubDiagnosticSeverity::Help => AnnotateLevel::Help,
1205+
SubDiagnosticSeverity::Info => AnnotateLevel::Info,
1206+
SubDiagnosticSeverity::Warning => AnnotateLevel::Warning,
1207+
SubDiagnosticSeverity::Error => AnnotateLevel::Error,
1208+
SubDiagnosticSeverity::Fatal => AnnotateLevel::Error,
1209+
}
1210+
}
1211+
}
1212+
11731213
/// Configuration for rendering diagnostics.
11741214
#[derive(Clone, Debug)]
11751215
pub struct DisplayDiagnosticConfig {

crates/ruff_db/src/diagnostic/render.rs

Lines changed: 37 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use azure::AzureRenderer;
2626
use pylint::PylintRenderer;
2727

2828
mod azure;
29+
mod full;
2930
#[cfg(feature = "serde")]
3031
mod json;
3132
#[cfg(feature = "serde")]
@@ -256,7 +257,7 @@ impl<'a> Resolved<'a> {
256257
/// both.)
257258
#[derive(Debug)]
258259
struct ResolvedDiagnostic<'a> {
259-
severity: Severity,
260+
level: AnnotateLevel,
260261
id: Option<String>,
261262
message: String,
262263
annotations: Vec<ResolvedAnnotation<'a>>,
@@ -281,7 +282,7 @@ impl<'a> ResolvedDiagnostic<'a> {
281282
let id = Some(diag.inner.id.to_string());
282283
let message = diag.inner.message.as_str().to_string();
283284
ResolvedDiagnostic {
284-
severity: diag.inner.severity,
285+
level: diag.inner.severity.to_annotate(),
285286
id,
286287
message,
287288
annotations,
@@ -304,7 +305,7 @@ impl<'a> ResolvedDiagnostic<'a> {
304305
})
305306
.collect();
306307
ResolvedDiagnostic {
307-
severity: diag.inner.severity,
308+
level: diag.inner.severity.to_annotate(),
308309
id: None,
309310
message: diag.inner.message.as_str().to_string(),
310311
annotations,
@@ -371,7 +372,7 @@ impl<'a> ResolvedDiagnostic<'a> {
371372
snippets_by_input
372373
.sort_by(|snips1, snips2| snips1.has_primary.cmp(&snips2.has_primary).reverse());
373374
RenderableDiagnostic {
374-
severity: self.severity,
375+
level: self.level,
375376
id: self.id.as_deref(),
376377
message: &self.message,
377378
snippets_by_input,
@@ -459,7 +460,7 @@ struct Renderable<'r> {
459460
#[derive(Debug)]
460461
struct RenderableDiagnostic<'r> {
461462
/// The severity of the diagnostic.
462-
severity: Severity,
463+
level: AnnotateLevel,
463464
/// The ID of the diagnostic. The ID can usually be used on the CLI or in a
464465
/// config file to change the severity of a lint.
465466
///
@@ -478,15 +479,14 @@ struct RenderableDiagnostic<'r> {
478479
impl RenderableDiagnostic<'_> {
479480
/// Convert this to an "annotate" snippet.
480481
fn to_annotate(&self) -> AnnotateMessage<'_> {
481-
let level = self.severity.to_annotate();
482482
let snippets = self.snippets_by_input.iter().flat_map(|snippets| {
483483
let path = snippets.path;
484484
snippets
485485
.snippets
486486
.iter()
487487
.map(|snippet| snippet.to_annotate(path))
488488
});
489-
let mut message = level.title(self.message);
489+
let mut message = self.level.title(self.message);
490490
if let Some(id) = self.id {
491491
message = message.id(id);
492492
}
@@ -864,7 +864,10 @@ mod tests {
864864

865865
use ruff_diagnostics::{Edit, Fix};
866866

867-
use crate::diagnostic::{Annotation, DiagnosticId, SecondaryCode, Severity, Span};
867+
use crate::diagnostic::{
868+
Annotation, DiagnosticId, IntoDiagnosticMessage, SecondaryCode, Severity, Span,
869+
SubDiagnosticSeverity,
870+
};
868871
use crate::files::system_path_to_file;
869872
use crate::system::{DbWithWritableSystem, SystemPath};
870873
use crate::tests::TestDb;
@@ -1548,7 +1551,7 @@ watermelon
15481551

15491552
let mut diag = env.err().primary("animals", "3", "3", "").build();
15501553
diag.sub(
1551-
env.sub_builder(Severity::Info, "this is a helpful note")
1554+
env.sub_builder(SubDiagnosticSeverity::Info, "this is a helpful note")
15521555
.build(),
15531556
);
15541557
insta::assert_snapshot!(
@@ -1577,15 +1580,15 @@ watermelon
15771580

15781581
let mut diag = env.err().primary("animals", "3", "3", "").build();
15791582
diag.sub(
1580-
env.sub_builder(Severity::Info, "this is a helpful note")
1583+
env.sub_builder(SubDiagnosticSeverity::Info, "this is a helpful note")
15811584
.build(),
15821585
);
15831586
diag.sub(
1584-
env.sub_builder(Severity::Info, "another helpful note")
1587+
env.sub_builder(SubDiagnosticSeverity::Info, "another helpful note")
15851588
.build(),
15861589
);
15871590
diag.sub(
1588-
env.sub_builder(Severity::Info, "and another helpful note")
1591+
env.sub_builder(SubDiagnosticSeverity::Info, "and another helpful note")
15891592
.build(),
15901593
);
15911594
insta::assert_snapshot!(
@@ -2370,7 +2373,7 @@ watermelon
23702373
/// sub-diagnostic with "error" severity and canned values for
23712374
/// its identifier and message.
23722375
fn sub_warn(&mut self) -> SubDiagnosticBuilder<'_> {
2373-
self.sub_builder(Severity::Warning, "sub-diagnostic message")
2376+
self.sub_builder(SubDiagnosticSeverity::Warning, "sub-diagnostic message")
23742377
}
23752378

23762379
/// Returns a builder for tersely constructing diagnostics.
@@ -2391,7 +2394,11 @@ watermelon
23912394
}
23922395

23932396
/// Returns a builder for tersely constructing sub-diagnostics.
2394-
fn sub_builder(&mut self, severity: Severity, message: &str) -> SubDiagnosticBuilder<'_> {
2397+
fn sub_builder(
2398+
&mut self,
2399+
severity: SubDiagnosticSeverity,
2400+
message: &str,
2401+
) -> SubDiagnosticBuilder<'_> {
23952402
let subdiag = SubDiagnostic::new(severity, message);
23962403
SubDiagnosticBuilder { env: self, subdiag }
23972404
}
@@ -2494,6 +2501,12 @@ watermelon
24942501
self.diag.set_noqa_offset(noqa_offset);
24952502
self
24962503
}
2504+
2505+
/// Adds a "help" sub-diagnostic with the given message.
2506+
fn help(mut self, message: impl IntoDiagnosticMessage) -> DiagnosticBuilder<'e> {
2507+
self.diag.help(message);
2508+
self
2509+
}
24972510
}
24982511

24992512
/// A helper builder for tersely populating a `SubDiagnostic`.
@@ -2600,7 +2613,8 @@ def fibonacci(n):
26002613

26012614
let diagnostics = vec![
26022615
env.builder("unused-import", Severity::Error, "`os` imported but unused")
2603-
.primary("fib.py", "1:7", "1:9", "Remove unused import: `os`")
2616+
.primary("fib.py", "1:7", "1:9", "")
2617+
.help("Remove unused import: `os`")
26042618
.secondary_code("F401")
26052619
.fix(Fix::unsafe_edit(Edit::range_deletion(TextRange::new(
26062620
TextSize::from(0),
@@ -2613,12 +2627,8 @@ def fibonacci(n):
26132627
Severity::Error,
26142628
"Local variable `x` is assigned to but never used",
26152629
)
2616-
.primary(
2617-
"fib.py",
2618-
"6:4",
2619-
"6:5",
2620-
"Remove assignment to unused variable `x`",
2621-
)
2630+
.primary("fib.py", "6:4", "6:5", "")
2631+
.help("Remove assignment to unused variable `x`")
26222632
.secondary_code("F841")
26232633
.fix(Fix::unsafe_edit(Edit::deletion(
26242634
TextSize::from(94),
@@ -2720,7 +2730,8 @@ if call(foo
27202730

27212731
let diagnostics = vec![
27222732
env.builder("unused-import", Severity::Error, "`os` imported but unused")
2723-
.primary("notebook.ipynb", "2:7", "2:9", "Remove unused import: `os`")
2733+
.primary("notebook.ipynb", "2:7", "2:9", "")
2734+
.help("Remove unused import: `os`")
27242735
.secondary_code("F401")
27252736
.fix(Fix::safe_edit(Edit::range_deletion(TextRange::new(
27262737
TextSize::from(9),
@@ -2733,12 +2744,8 @@ if call(foo
27332744
Severity::Error,
27342745
"`math` imported but unused",
27352746
)
2736-
.primary(
2737-
"notebook.ipynb",
2738-
"4:7",
2739-
"4:11",
2740-
"Remove unused import: `math`",
2741-
)
2747+
.primary("notebook.ipynb", "4:7", "4:11", "")
2748+
.help("Remove unused import: `math`")
27422749
.secondary_code("F401")
27432750
.fix(Fix::safe_edit(Edit::range_deletion(TextRange::new(
27442751
TextSize::from(28),
@@ -2751,12 +2758,8 @@ if call(foo
27512758
Severity::Error,
27522759
"Local variable `x` is assigned to but never used",
27532760
)
2754-
.primary(
2755-
"notebook.ipynb",
2756-
"10:4",
2757-
"10:5",
2758-
"Remove assignment to unused variable `x`",
2759-
)
2761+
.primary("notebook.ipynb", "10:4", "10:5", "")
2762+
.help("Remove assignment to unused variable `x`")
27602763
.secondary_code("F841")
27612764
.fix(Fix::unsafe_edit(Edit::range_deletion(TextRange::new(
27622765
TextSize::from(94),
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
#[cfg(test)]
2+
mod tests {
3+
use crate::diagnostic::{
4+
DiagnosticFormat,
5+
render::tests::{create_diagnostics, create_syntax_error_diagnostics},
6+
};
7+
8+
#[test]
9+
fn output() {
10+
let (env, diagnostics) = create_diagnostics(DiagnosticFormat::Full);
11+
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r#"
12+
error[unused-import]: `os` imported but unused
13+
--> fib.py:1:8
14+
|
15+
1 | import os
16+
| ^^
17+
|
18+
help: Remove unused import: `os`
19+
20+
error[unused-variable]: Local variable `x` is assigned to but never used
21+
--> fib.py:6:5
22+
|
23+
4 | def fibonacci(n):
24+
5 | """Compute the nth number in the Fibonacci sequence."""
25+
6 | x = 1
26+
| ^
27+
7 | if n == 0:
28+
8 | return 0
29+
|
30+
help: Remove assignment to unused variable `x`
31+
32+
error[undefined-name]: Undefined name `a`
33+
--> undef.py:1:4
34+
|
35+
1 | if a == 1: pass
36+
| ^
37+
|
38+
"#);
39+
}
40+
41+
#[test]
42+
fn syntax_errors() {
43+
let (env, diagnostics) = create_syntax_error_diagnostics(DiagnosticFormat::Full);
44+
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
45+
error[invalid-syntax]: SyntaxError: Expected one or more symbol names after import
46+
--> syntax_errors.py:1:15
47+
|
48+
1 | from os import
49+
| ^
50+
2 |
51+
3 | if call(foo
52+
|
53+
54+
error[invalid-syntax]: SyntaxError: Expected ')', found newline
55+
--> syntax_errors.py:3:12
56+
|
57+
1 | from os import
58+
2 |
59+
3 | if call(foo
60+
| ^
61+
4 | def bar():
62+
5 | pass
63+
|
64+
");
65+
}
66+
}

crates/ruff_db/src/diagnostic/render/json.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ pub(super) fn diagnostic_to_json<'a>(
8787

8888
let fix = diagnostic.fix().map(|fix| JsonFix {
8989
applicability: fix.applicability(),
90-
message: diagnostic.suggestion(),
90+
message: diagnostic.first_help_text(),
9191
edits: ExpandedEdits {
9292
edits: fix.edits(),
9393
notebook_index,

crates/ruff_linter/src/message/mod.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,12 @@ where
7575
);
7676

7777
let span = Span::from(file).with_range(range);
78-
let mut annotation = Annotation::primary(span);
78+
let annotation = Annotation::primary(span);
79+
diagnostic.annotate(annotation);
80+
7981
if let Some(suggestion) = suggestion {
80-
annotation = annotation.message(suggestion);
82+
diagnostic.help(suggestion);
8183
}
82-
diagnostic.annotate(annotation);
8384

8485
if let Some(fix) = fix {
8586
diagnostic.set_fix(fix);

0 commit comments

Comments
 (0)