diff --git a/crates/ruff_db/src/diagnostic/render.rs b/crates/ruff_db/src/diagnostic/render.rs index 43c676856d..eba3253c96 100644 --- a/crates/ruff_db/src/diagnostic/render.rs +++ b/crates/ruff_db/src/diagnostic/render.rs @@ -710,7 +710,11 @@ impl<'r> RenderableAnnotation<'r> { /// lifetime parameter here refers to the lifetime of the resolver that /// created the given `ResolvedAnnotation`. fn new(snippet_start: TextSize, ann: &'_ ResolvedAnnotation<'r>) -> RenderableAnnotation<'r> { - let range = ann.range - snippet_start; + // This should only ever saturate if a BOM is present _and_ the annotation range points + // before the BOM (i.e. at offset 0). In Ruff this typically results from the use of + // `TextRange::default()` for a diagnostic range instead of a range relative to file + // contents. + let range = ann.range.checked_sub(snippet_start).unwrap_or(ann.range); RenderableAnnotation { range, message: ann.message, diff --git a/crates/ruff_db/src/diagnostic/render/full.rs b/crates/ruff_db/src/diagnostic/render/full.rs index 300e370705..0eee73e543 100644 --- a/crates/ruff_db/src/diagnostic/render/full.rs +++ b/crates/ruff_db/src/diagnostic/render/full.rs @@ -512,6 +512,27 @@ print() "); } + #[test] + fn bom_with_default_range() { + let mut env = TestEnvironment::new(); + env.add("example.py", "\u{feff}import foo"); + env.format(DiagnosticFormat::Full); + + let mut diagnostic = env.err().build(); + let span = env.path("example.py").with_range(TextRange::default()); + let annotation = Annotation::primary(span); + diagnostic.annotate(annotation); + + insta::assert_snapshot!(env.render(&diagnostic), @r" + error[test-diagnostic]: main diagnostic message + --> example.py:1:1 + | + 1 | import foo + | ^ + | + "); + } + /// We previously rendered this correctly, but the header was falling back to 1:1 for ranges /// pointing to the final newline in a file. Like Ruff, we now use the offset of the first /// character in the nonexistent final line in the header.