Repository: rust-lang/annotate-snippets-rs
Pull request #216 (2025-06-12 02:30:17)

fix: Match how rustc handles annotating newlines
Note: This is another change related to matching `rustc`'s output


Patch:
diff --git a/src/renderer/source_map.rs b/src/renderer/source_map.rs
index eca057e..403c1c6 100644
--- a/src/renderer/source_map.rs
+++ b/src/renderer/source_map.rs
@@ -73,9 +73,9 @@ impl<'a> SourceMap<'a> {
         let end_info = self
             .lines
             .iter()
-            .find(|info| info.end_byte > span.end.saturating_sub(1))
+            .find(|info| span.end >= info.start_byte && span.end < info.end_byte)
             .unwrap_or(self.lines.last().unwrap());
-        let (mut end_char_pos, end_display_pos) = end_info.line
+        let (end_char_pos, end_display_pos) = end_info.line
             [0..(span.end - end_info.start_byte).min(end_info.line.len())]
             .chars()
             .fold((0, 0), |(char_pos, byte_pos), c| {
@@ -83,10 +83,6 @@ impl<'a> SourceMap<'a> {
                 (char_pos + 1, byte_pos + display)
             });
 
-        // correct the char pos if we are highlighting the end of a line
-        if (span.end - end_info.start_byte).saturating_sub(end_info.line.len()) > 0 {
-            end_char_pos += 1;
-        }
         let mut end = Loc {
             line: end_info.line_index,
             char: end_char_pos,


Test patch:
diff --git a/tests/color/ann_multiline2.term.svg b/tests/color/ann_multiline2.term.svg
index 24827f6..2f7eb90 100644
--- a/tests/color/ann_multiline2.term.svg
+++ b/tests/color/ann_multiline2.term.svg
@@ -1,4 +1,4 @@
-<svg width="740px" height="146px" xmlns="http://www.w3.org/2000/svg">
+<svg width="740px" height="164px" xmlns="http://www.w3.org/2000/svg">
   <style>
     .fg { fill: #AAAAAA }
     .bg { background: #000000 }
@@ -25,13 +25,15 @@
 </tspan>
     <tspan x="10px" y="64px"><tspan>   </tspan><tspan class="fg-bright-blue bold">|</tspan>
 </tspan>
-    <tspan x="10px" y="82px"><tspan class="fg-bright-blue bold">26</tspan><tspan> </tspan><tspan class="fg-bright-blue bold">|</tspan><tspan> This is an example</tspan>
+    <tspan x="10px" y="82px"><tspan class="fg-bright-blue bold">26</tspan><tspan> </tspan><tspan class="fg-bright-blue bold">|</tspan><tspan>   This is an example</tspan>
 </tspan>
-    <tspan x="10px" y="100px"><tspan>   </tspan><tspan class="fg-bright-blue bold">|</tspan><tspan>            </tspan><tspan class="fg-bright-red bold">^^^^^^^</tspan><tspan> </tspan><tspan class="fg-bright-red bold">this should not be on separate lines</tspan>
+    <tspan x="10px" y="100px"><tspan>   </tspan><tspan class="fg-bright-blue bold">|</tspan><tspan> </tspan><tspan class="fg-bright-red bold"> ____________^</tspan>
 </tspan>
-    <tspan x="10px" y="118px"><tspan class="fg-bright-blue bold">27</tspan><tspan> </tspan><tspan class="fg-bright-blue bold">|</tspan><tspan> of an edge case of an annotation overflowing</tspan>
+    <tspan x="10px" y="118px"><tspan class="fg-bright-blue bold">27</tspan><tspan> </tspan><tspan class="fg-bright-blue bold">|</tspan><tspan> </tspan><tspan class="fg-bright-red bold">|</tspan><tspan> of an edge case of an annotation overflowing</tspan>
 </tspan>
-    <tspan x="10px" y="136px"><tspan class="fg-bright-blue bold">28</tspan><tspan> </tspan><tspan class="fg-bright-blue bold">|</tspan><tspan> to exactly one character on next line.</tspan>
+    <tspan x="10px" y="136px"><tspan>   </tspan><tspan class="fg-bright-blue bold">|</tspan><tspan> </tspan><tspan class="fg-bright-red bold">|_^</tspan><tspan> </tspan><tspan class="fg-bright-red bold">this should not be on separate lines</tspan>
+</tspan>
+    <tspan x="10px" y="154px"><tspan class="fg-bright-blue bold">28</tspan><tspan> </tspan><tspan class="fg-bright-blue bold">|</tspan><tspan>   to exactly one character on next line.</tspan>
 </tspan>
   </text>
 
diff --git a/tests/color/simple.term.svg b/tests/color/simple.term.svg
index b849cf4..76aa393 100644
--- a/tests/color/simple.term.svg
+++ b/tests/color/simple.term.svg
@@ -1,4 +1,4 @@
-<svg width="740px" height="164px" xmlns="http://www.w3.org/2000/svg">
+<svg width="740px" height="182px" xmlns="http://www.w3.org/2000/svg">
   <style>
     .fg { fill: #AAAAAA }
     .bg { background: #000000 }
@@ -25,15 +25,17 @@
 </tspan>
     <tspan x="10px" y="64px"><tspan>    </tspan><tspan class="fg-bright-blue bold">|</tspan>
 </tspan>
-    <tspan x="10px" y="82px"><tspan class="fg-bright-blue bold">169</tspan><tspan> </tspan><tspan class="fg-bright-blue bold">|</tspan><tspan>         })</tspan>
+    <tspan x="10px" y="82px"><tspan class="fg-bright-blue bold">169</tspan><tspan> </tspan><tspan class="fg-bright-blue bold">|</tspan><tspan>           })</tspan>
 </tspan>
-    <tspan x="10px" y="100px"><tspan>    </tspan><tspan class="fg-bright-blue bold">|</tspan><tspan>           </tspan><tspan class="fg-bright-blue bold">-</tspan><tspan> </tspan><tspan class="fg-bright-blue bold">expected one of `.`, `;`, `?`, or an operator here</tspan>
+    <tspan x="10px" y="100px"><tspan>    </tspan><tspan class="fg-bright-blue bold">|</tspan><tspan> </tspan><tspan class="fg-bright-blue bold"> ___________-</tspan>
 </tspan>
-    <tspan x="10px" y="118px"><tspan class="fg-bright-blue bold">170</tspan><tspan> </tspan><tspan class="fg-bright-blue bold">|</tspan>
+    <tspan x="10px" y="118px"><tspan class="fg-bright-blue bold">170</tspan><tspan> </tspan><tspan class="fg-bright-blue bold">|</tspan><tspan> </tspan><tspan class="fg-bright-blue bold">|</tspan>
 </tspan>
-    <tspan x="10px" y="136px"><tspan class="fg-bright-blue bold">171</tspan><tspan> </tspan><tspan class="fg-bright-blue bold">|</tspan><tspan>         for line in &amp;self.body {</tspan>
+    <tspan x="10px" y="136px"><tspan>    </tspan><tspan class="fg-bright-blue bold">|</tspan><tspan> </tspan><tspan class="fg-bright-blue bold">|_-</tspan><tspan> </tspan><tspan class="fg-bright-blue bold">expected one of `.`, `;`, `?`, or an operator here</tspan>
 </tspan>
-    <tspan x="10px" y="154px"><tspan>    </tspan><tspan class="fg-bright-blue bold">|</tspan><tspan>         </tspan><tspan class="fg-bright-red bold">^^^</tspan><tspan> </tspan><tspan class="fg-bright-red bold">unexpected token</tspan>
+    <tspan x="10px" y="154px"><tspan class="fg-bright-blue bold">171</tspan><tspan> </tspan><tspan class="fg-bright-blue bold">|</tspan><tspan>           for line in &amp;self.body {</tspan>
+</tspan>
+    <tspan x="10px" y="172px"><tspan>    </tspan><tspan class="fg-bright-blue bold">|</tspan><tspan>           </tspan><tspan class="fg-bright-red bold">^^^</tspan><tspan> </tspan><tspan class="fg-bright-red bold">unexpected token</tspan>
 </tspan>
   </text>
 
diff --git a/tests/formatter.rs b/tests/formatter.rs
index bf99853..e5647b8 100644
--- a/tests/formatter.rs
+++ b/tests/formatter.rs
@@ -415,9 +415,9 @@ fn char_eol_annotate_char() {
 error: 
  --> file/path:3:1
   |
-3 | a
-  | ^
-4 | b
+3 | / a
+4 | | b
+  | |_^
 "#]];
     let renderer = Renderer::plain().anonymized_line_numbers(false);
     assert_data_eq!(renderer.render(input), expected);
@@ -437,10 +437,11 @@ fn char_eol_annotate_char_double_width() {
 error: 
  --> <current file>:1:2
   |
-1 | こん
-  |   ^^
-2 | にちは
-3 | 世界
+1 |   こん
+  |  ___^
+2 | | にちは
+  | |_^
+3 |   世界
 "#]];
 
     let renderer = Renderer::plain();
@@ -485,9 +486,10 @@ fn annotate_eol2() {
 error: 
  --> file/path:3:2
   |
-3 | a
-  |  ^
-4 | b
+3 |   a
+  |  __^
+4 | | b
+  | |_^
 "#]];
     let renderer = Renderer::plain().anonymized_line_numbers(false);
     assert_data_eq!(renderer.render(input), expected);
@@ -508,9 +510,10 @@ fn annotate_eol3() {
 error: 
  --> file/path:3:3
   |
-3 | a
-  |  ^
-4 | b
+3 |   a
+  |  __^
+4 | | b
+  | |_^
 "#]];
     let renderer = Renderer::plain().anonymized_line_numbers(false);
     assert_data_eq!(renderer.render(input), expected);
@@ -553,10 +556,11 @@ fn annotate_eol_double_width() {
 error: 
  --> <current file>:1:4
   |
-1 | こん
-  |     ^
-2 | にちは
-3 | 世界
+1 |   こん
+  |  _____^
+2 | | にちは
+  | |_^
+3 |   世界
 "#]];
 
     let renderer = Renderer::plain();
@@ -678,8 +682,8 @@ error:
 3 |   a
   |  __^
 4 | | b
-  | |__^
-5 |   c
+5 | | c
+  | |_^
 "#]];
     let renderer = Renderer::plain().anonymized_line_numbers(false);
     assert_data_eq!(renderer.render(input), expected);
@@ -728,8 +732,8 @@ error:
 3 |   a
   |  __^
 4 | | b
-  | |__^
-5 |   c
+5 | | c
+  | |_^
 "#]];
     let renderer = Renderer::plain().anonymized_line_numbers(false);
     assert_data_eq!(renderer.render(input), expected);
@@ -1532,6 +1536,7 @@ LL -     T
 LL -     :
 LL -     ?
 LL -     Sized
+LL + {
    |
 "#]];
     let renderer = Renderer::plain().anonymized_line_numbers(true);
@@ -1640,8 +1645,12 @@ LL | struct Wrapper<T>(T);
    |                this could be changed to `T: ?Sized`...
 help: consider removing the `?Sized` bound to make the type parameter `Sized`
    |
-LL ~ and 
-LL ~ + Send{
+LL - and where
+LL -     T
+LL -     :
+LL -     ?
+LL -     Sized
+LL + and + Send{
    |
 "#]];
     let renderer = Renderer::plain().anonymized_line_numbers(true);
diff --git a/tests/rustc_tests.rs b/tests/rustc_tests.rs
index cf33969..4979e77 100644
--- a/tests/rustc_tests.rs
+++ b/tests/rustc_tests.rs
@@ -2111,3 +2111,358 @@ LL +     x.iter_mut().for_each(|items| {
     let renderer = Renderer::plain().anonymized_line_numbers(true);
     assert_data_eq!(renderer.render(input), expected);
 }
+
+#[test]
+fn bad_char_literals() {
+    // tests/ui/parser/bad-char-literals.rs
+
+    let source = r#"// ignore-tidy-cr
+// ignore-tidy-tab
+
+fn main() {
+    // these literals are just silly.
+    ''';
+    //~^ ERROR: character constant must be escaped: `'`
+
+    // note that this is a literal "\n" byte
+    '
+';
+    //~^^ ERROR: character constant must be escaped: `\n`
+
+    // note that this is a literal "\r" byte
+; //~ ERROR: character constant must be escaped: `\r`
+
+    // note that this is a literal NULL
+    '--'; //~ ERROR: character literal may only contain one codepoint
+
+    // note that this is a literal tab character here
+    '  ';
+    //~^ ERROR: character constant must be escaped: `\t`
+}
+"#;
+
+    let input = Level::ERROR
+        .header("character constant must be escaped: `\\n`")
+        .group(
+            Group::new().element(
+                Snippet::source(source)
+                    .origin("$DIR/bad-char-literals.rs")
+                    .fold(true)
+                    .annotation(AnnotationKind::Primary.span(204..205)),
+            ),
+        )
+        .group(
+            Group::new()
+                .element(Level::HELP.title("escape the character"))
+                .element(
+                    Snippet::source(source)
+                        .origin("$DIR/bad-char-literals.rs")
+                        .line_start(1)
+                        .fold(true)
+                        .patch(Patch::new(204..205, r#"\n"#)),
+                ),
+        );
+    let expected = str![[r#"
+error: character constant must be escaped: `/n`
+  --> $DIR/bad-char-literals.rs:10:6
+   |
+LL |       '
+   |  ______^
+LL | | ';
+   | |_^
+   |
+help: escape the character
+   |
+LL |     '/n';
+   |      ++
+"#]];
+    let renderer = Renderer::plain().anonymized_line_numbers(true);
+    assert_data_eq!(renderer.render(input), expected);
+}
+
+#[test]
+fn unclosed_1() {
+    // tests/ui/frontmatter/unclosed-1.rs
+
+    let source = r#"----cargo
+//~^ ERROR: unclosed frontmatter
+
+// This test checks that the #! characters can help us recover a frontmatter
+// close. There should not be a "missing `main` function" error as the rest
+// are properly parsed.
+
+#![feature(frontmatter)]
+
+fn main() {}
+"#;
+
+    let input = Level::ERROR
+        .header("unclosed frontmatter")
+        .group(
+            Group::new().element(
+                Snippet::source(source)
+                    .origin("$DIR/unclosed-1.rs")
+                    .fold(true)
+                    .annotation(AnnotationKind::Primary.span(0..221)),
+            ),
+        )
+        .group(
+            Group::new()
+                .element(Level::NOTE.title("frontmatter opening here was not closed"))
+                .element(
+                    Snippet::source(source)
+                        .origin("$DIR/unclosed-1.rs")
+                        .fold(true)
+                        .annotation(AnnotationKind::Primary.span(0..4)),
+                ),
+        );
+    let expected = str![[r#"
+error: unclosed frontmatter
+  --> $DIR/unclosed-1.rs:1:1
+   |
+LL | / ----cargo
+...  |
+LL | |
+   | |_^
+   |
+note: frontmatter opening here was not closed
+  --> $DIR/unclosed-1.rs:1:1
+   |
+LL | ----cargo
+   | ^^^^
+"#]];
+    let renderer = Renderer::plain().anonymized_line_numbers(true);
+    assert_data_eq!(renderer.render(input), expected);
+}
+
+#[test]
+fn unclosed_2() {
+    // tests/ui/frontmatter/unclosed-2.rs
+
+    let source = r#"----cargo
+//~^ ERROR: unclosed frontmatter
+//~| ERROR: frontmatters are experimental
+
+//@ compile-flags: --crate-type lib
+
+// Leading whitespace on the feature line prevents recovery. However
+// the dashes quoted will not be used for recovery and the entire file
+// should be treated as within the frontmatter block.
+
+ #![feature(frontmatter)]
+
+fn foo() -> &str {
+    "----"
+}
+"#;
+
+    let input = Level::ERROR
+        .header("unclosed frontmatter")
+        .group(
+            Group::new().element(
+                Snippet::source(source)
+                    .origin("$DIR/unclosed-2.rs")
+                    .fold(true)
+                    .annotation(AnnotationKind::Primary.span(0..377)),
+            ),
+        )
+        .group(
+            Group::new()
+                .element(Level::NOTE.title("frontmatter opening here was not closed"))
+                .element(
+                    Snippet::source(source)
+                        .origin("$DIR/unclosed-2.rs")
+                        .fold(true)
+                        .annotation(AnnotationKind::Primary.span(0..4)),
+                ),
+        );
+    let expected = str![[r#"
+error: unclosed frontmatter
+  --> $DIR/unclosed-2.rs:1:1
+   |
+LL | / ----cargo
+...  |
+LL | |     "----"
+LL | | }
+   | |__^
+   |
+note: frontmatter opening here was not closed
+  --> $DIR/unclosed-2.rs:1:1
+   |
+LL | ----cargo
+   | ^^^^
+"#]];
+    let renderer = Renderer::plain().anonymized_line_numbers(true);
+    assert_data_eq!(renderer.render(input), expected);
+}
+
+#[test]
+fn unclosed_3() {
+    // tests/ui/frontmatter/unclosed-3.rs
+
+    let source = r#"----cargo
+//~^ ERROR: frontmatter close does not match the opening
+
+//@ compile-flags: --crate-type lib
+
+// Unfortunate recovery situation. Not really preventable with improving the
+// recovery strategy, but this type of code is rare enough already.
+
+ #![feature(frontmatter)]
+
+fn foo(x: i32) -> i32 {
+    ---x
+    //~^ ERROR: invalid preceding whitespace for frontmatter close
+    //~| ERROR: extra characters after frontmatter close are not allowed
+}
+//~^ ERROR: unexpected closing delimiter: `}`
+"#;
+
+    let input = Level::ERROR
+        .header("invalid preceding whitespace for frontmatter close")
+        .group(
+            Group::new().element(
+                Snippet::source(source)
+                    .origin("$DIR/unclosed-3.rs")
+                    .fold(true)
+                    .annotation(AnnotationKind::Primary.span(302..310)),
+            ),
+        )
+        .group(
+            Group::new()
+                .element(
+                    Level::NOTE.title("frontmatter close should not be preceded by whitespace"),
+                )
+                .element(
+                    Snippet::source(source)
+                        .origin("$DIR/unclosed-3.rs")
+                        .fold(true)
+                        .annotation(AnnotationKind::Primary.span(302..306)),
+                ),
+        );
+    let expected = str![[r#"
+error: invalid preceding whitespace for frontmatter close
+  --> $DIR/unclosed-3.rs:12:1
+   |
+LL |     ---x
+   | ^^^^^^^^
+   |
+note: frontmatter close should not be preceded by whitespace
+  --> $DIR/unclosed-3.rs:12:1
+   |
+LL |     ---x
+   | ^^^^
+"#]];
+    let renderer = Renderer::plain().anonymized_line_numbers(true);
+    assert_data_eq!(renderer.render(input), expected);
+}
+
+#[test]
+fn unclosed_4() {
+    // tests/ui/frontmatter/unclosed-4.rs
+
+    let source = r#"----cargo
+//~^ ERROR: unclosed frontmatter
+
+//! Similarly, a module-level content should allow for recovery as well (as
+//! per unclosed-1.rs)
+
+#![feature(frontmatter)]
+
+fn main() {}
+"#;
+
+    let input = Level::ERROR
+        .header("unclosed frontmatter")
+        .group(
+            Group::new().element(
+                Snippet::source(source)
+                    .origin("$DIR/unclosed-4.rs")
+                    .fold(true)
+                    .annotation(AnnotationKind::Primary.span(0..43)),
+            ),
+        )
+        .group(
+            Group::new()
+                .element(Level::NOTE.title("frontmatter opening here was not closed"))
+                .element(
+                    Snippet::source(source)
+                        .origin("$DIR/unclosed-4.rs")
+                        .fold(true)
+                        .annotation(AnnotationKind::Primary.span(0..4)),
+                ),
+        );
+    let expected = str![[r#"
+error: unclosed frontmatter
+  --> $DIR/unclosed-4.rs:1:1
+   |
+LL | / ----cargo
+LL | | //~^ ERROR: unclosed frontmatter
+LL | |
+   | |_^
+   |
+note: frontmatter opening here was not closed
+  --> $DIR/unclosed-4.rs:1:1
+   |
+LL | ----cargo
+   | ^^^^
+"#]];
+    let renderer = Renderer::plain().anonymized_line_numbers(true);
+    assert_data_eq!(renderer.render(input), expected);
+}
+
+#[test]
+fn unclosed_5() {
+    // tests/ui/frontmatter/unclosed-5.rs
+
+    let source = r#"----cargo
+//~^ ERROR: unclosed frontmatter
+//~| ERROR: frontmatters are experimental
+
+// Similarly, a use statement should allow for recovery as well (as
+// per unclosed-1.rs)
+
+use std::env;
+
+fn main() {}
+"#;
+
+    let input = Level::ERROR
+        .header("unclosed frontmatter")
+        .group(
+            Group::new().element(
+                Snippet::source(source)
+                    .origin("$DIR/unclosed-5.rs")
+                    .fold(true)
+                    .annotation(AnnotationKind::Primary.span(0..176)),
+            ),
+        )
+        .group(
+            Group::new()
+                .element(Level::NOTE.title("frontmatter opening here was not closed"))
+                .element(
+                    Snippet::source(source)
+                        .origin("$DIR/unclosed-5.rs")
+                        .fold(true)
+                        .annotation(AnnotationKind::Primary.span(0..4)),
+                ),
+        );
+
+    let expected = str![[r#"
+error: unclosed frontmatter
+  --> $DIR/unclosed-5.rs:1:1
+   |
+LL | / ----cargo
+...  |
+LL | |
+   | |_^
+   |
+note: frontmatter opening here was not closed
+  --> $DIR/unclosed-5.rs:1:1
+   |
+LL | ----cargo
+   | ^^^^
+"#]];
+    let renderer = Renderer::plain().anonymized_line_numbers(true);
+    assert_data_eq!(renderer.render(input), expected);
+}