Skip to content

Commit b324ae1

Browse files
authored
Hide empty snippets for full-file diagnostics (#19653)
Summary -- This is the other commit I wanted to spin off from #19415, currently stacked on #19644. This PR suppresses blank snippets for empty ranges at the very beginning of a file, and for empty ranges in non-existent files. Ruff includes empty ranges for IO errors, for example. https://github.com/astral-sh/ruff/blob/f4e93b63351bfe035b1fc5a7bc605a52979e7841/crates/ruff_linter/src/message/text.rs#L100-L110 The diagnostics now look like this (new snapshot test): ``` error[test-diagnostic]: main diagnostic message --> example.py:1:1 ``` Instead of [^*] ``` error[test-diagnostic]: main diagnostic message --> example.py:1:1 | | ``` Test Plan -- A new `ruff_db` test showing the expected output format [^*]: This doesn't correspond precisely to the example in the PR because of some details of the diagnostic builder helper methods in `ruff_db`, but you can see another example in the current version of the summary in #19415.
1 parent 2db4e5d commit b324ae1

File tree

6 files changed

+88
-5
lines changed

6 files changed

+88
-5
lines changed

crates/ruff_annotate_snippets/src/renderer/display_list.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1183,6 +1183,21 @@ fn format_snippet<'m>(
11831183
let main_range = snippet.annotations.first().map(|x| x.range.start);
11841184
let origin = snippet.origin;
11851185
let need_empty_header = origin.is_some() || is_first;
1186+
1187+
let is_file_level = snippet.annotations.iter().any(|ann| ann.is_file_level);
1188+
if is_file_level {
1189+
assert!(
1190+
snippet.source.is_empty(),
1191+
"Non-empty file-level snippet that won't be rendered: {:?}",
1192+
snippet.source
1193+
);
1194+
let header = format_header(origin, main_range, &[], is_first);
1195+
return DisplaySet {
1196+
display_lines: header.map_or_else(Vec::new, |header| vec![header]),
1197+
margin: Margin::new(0, 0, 0, 0, term_width, 0),
1198+
};
1199+
}
1200+
11861201
let mut body = format_body(
11871202
snippet,
11881203
need_empty_header,

crates/ruff_annotate_snippets/src/snippet.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,13 +124,19 @@ pub struct Annotation<'a> {
124124
pub(crate) range: Range<usize>,
125125
pub(crate) label: Option<&'a str>,
126126
pub(crate) level: Level,
127+
pub(crate) is_file_level: bool,
127128
}
128129

129130
impl<'a> Annotation<'a> {
130131
pub fn label(mut self, label: &'a str) -> Self {
131132
self.label = Some(label);
132133
self
133134
}
135+
136+
pub fn is_file_level(mut self, yes: bool) -> Self {
137+
self.is_file_level = yes;
138+
self
139+
}
134140
}
135141

136142
/// Types of annotations.
@@ -165,6 +171,7 @@ impl Level {
165171
range: span,
166172
label: None,
167173
level: self,
174+
is_file_level: false,
168175
}
169176
}
170177
}

crates/ruff_db/src/diagnostic/mod.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,11 @@ pub struct Annotation {
712712
is_primary: bool,
713713
/// The diagnostic tags associated with this annotation.
714714
tags: Vec<DiagnosticTag>,
715+
/// Whether this annotation is a file-level or full-file annotation.
716+
///
717+
/// When set, rendering will only include the file's name and (optional) range. Everything else
718+
/// is omitted, including any file snippet or message.
719+
is_file_level: bool,
715720
}
716721

717722
impl Annotation {
@@ -730,6 +735,7 @@ impl Annotation {
730735
message: None,
731736
is_primary: true,
732737
tags: Vec::new(),
738+
is_file_level: false,
733739
}
734740
}
735741

@@ -746,6 +752,7 @@ impl Annotation {
746752
message: None,
747753
is_primary: false,
748754
tags: Vec::new(),
755+
is_file_level: false,
749756
}
750757
}
751758

@@ -811,6 +818,21 @@ impl Annotation {
811818
pub fn push_tag(&mut self, tag: DiagnosticTag) {
812819
self.tags.push(tag);
813820
}
821+
822+
/// Set whether or not this annotation is file-level.
823+
///
824+
/// File-level annotations are only rendered with their file name and range, if available. This
825+
/// is intended for backwards compatibility with Ruff diagnostics, which historically used
826+
/// `TextRange::default` to indicate a file-level diagnostic. In the new diagnostic model, a
827+
/// [`Span`] with a range of `None` should be used instead, as mentioned in the `Span`
828+
/// documentation.
829+
///
830+
/// TODO(brent) update this usage in Ruff and remove `is_file_level` entirely. See
831+
/// <https://github.com/astral-sh/ruff/issues/19688>, especially my first comment, for more
832+
/// details.
833+
pub fn set_file_level(&mut self, yes: bool) {
834+
self.is_file_level = yes;
835+
}
814836
}
815837

816838
/// Tags that can be associated with an annotation.

crates/ruff_db/src/diagnostic/render.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,7 @@ struct ResolvedAnnotation<'a> {
387387
line_end: OneIndexed,
388388
message: Option<&'a str>,
389389
is_primary: bool,
390+
is_file_level: bool,
390391
}
391392

392393
impl<'a> ResolvedAnnotation<'a> {
@@ -432,6 +433,7 @@ impl<'a> ResolvedAnnotation<'a> {
432433
line_end,
433434
message: ann.get_message(),
434435
is_primary: ann.is_primary,
436+
is_file_level: ann.is_file_level,
435437
})
436438
}
437439
}
@@ -653,6 +655,8 @@ struct RenderableAnnotation<'r> {
653655
message: Option<&'r str>,
654656
/// Whether this annotation is considered "primary" or not.
655657
is_primary: bool,
658+
/// Whether this annotation applies to an entire file, rather than a snippet within it.
659+
is_file_level: bool,
656660
}
657661

658662
impl<'r> RenderableAnnotation<'r> {
@@ -670,6 +674,7 @@ impl<'r> RenderableAnnotation<'r> {
670674
range,
671675
message: ann.message,
672676
is_primary: ann.is_primary,
677+
is_file_level: ann.is_file_level,
673678
}
674679
}
675680

@@ -695,7 +700,7 @@ impl<'r> RenderableAnnotation<'r> {
695700
if let Some(message) = self.message {
696701
ann = ann.label(message);
697702
}
698-
ann
703+
ann.is_file_level(self.is_file_level)
699704
}
700705
}
701706

@@ -2551,7 +2556,12 @@ watermelon
25512556
/// of the corresponding line minus one. (The "minus one" is because
25522557
/// otherwise, the span will end where the next line begins, and this
25532558
/// confuses `ruff_annotate_snippets` as of 2025-03-13.)
2554-
fn span(&self, path: &str, line_offset_start: &str, line_offset_end: &str) -> Span {
2559+
pub(super) fn span(
2560+
&self,
2561+
path: &str,
2562+
line_offset_start: &str,
2563+
line_offset_end: &str,
2564+
) -> Span {
25552565
let span = self.path(path);
25562566

25572567
let file = span.expect_ty_file();
@@ -2574,7 +2584,7 @@ watermelon
25742584
}
25752585

25762586
/// Like `span`, but only attaches a file path.
2577-
fn path(&self, path: &str) -> Span {
2587+
pub(super) fn path(&self, path: &str) -> Span {
25782588
let file = system_path_to_file(&self.db, path).unwrap();
25792589
Span::from(file)
25802590
}

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

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
#[cfg(test)]
22
mod tests {
33
use ruff_diagnostics::Applicability;
4+
use ruff_text_size::TextRange;
45

56
use crate::diagnostic::{
6-
DiagnosticFormat, Severity,
7+
Annotation, DiagnosticFormat, Severity,
78
render::tests::{TestEnvironment, create_diagnostics, create_syntax_error_diagnostics},
89
};
910

@@ -264,4 +265,24 @@ print()
264265
|
265266
");
266267
}
268+
269+
/// For file-level diagnostics, we expect to see the header line with the diagnostic information
270+
/// and the `-->` line with the file information but no lines of source code.
271+
#[test]
272+
fn file_level() {
273+
let mut env = TestEnvironment::new();
274+
env.add("example.py", "");
275+
env.format(DiagnosticFormat::Full);
276+
277+
let mut diagnostic = env.err().build();
278+
let span = env.path("example.py").with_range(TextRange::default());
279+
let mut annotation = Annotation::primary(span);
280+
annotation.set_file_level(true);
281+
diagnostic.annotate(annotation);
282+
283+
insta::assert_snapshot!(env.render(&diagnostic), @r"
284+
error[test-diagnostic]: main diagnostic message
285+
--> example.py:1:1
286+
");
287+
}
267288
}

crates/ruff_linter/src/message/mod.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,15 @@ where
7070
);
7171

7272
let span = Span::from(file).with_range(range);
73-
let annotation = Annotation::primary(span);
73+
let mut annotation = Annotation::primary(span);
74+
// The `0..0` range is used to highlight file-level diagnostics.
75+
//
76+
// TODO(brent) We should instead set this flag on annotations for individual lint rules that
77+
// actually need it, but we need to be able to cache the new diagnostic model first. See
78+
// https://github.com/astral-sh/ruff/issues/19688.
79+
if range == TextRange::default() {
80+
annotation.set_file_level(true);
81+
}
7482
diagnostic.annotate(annotation);
7583

7684
if let Some(suggestion) = suggestion {

0 commit comments

Comments
 (0)