Repository: rust-lang/annotate-snippets-rs
Pull request #219 (2025-06-24 19:26:53)

fix: Ensure empty sources have one "line"
`rustc` will occasionally give us a `Snippet` with an empty source and an `Annotation` with the span `0..0`, which causes `annotate-snippets` to panic. The root cause of the panic is that a few places in `SourceMap` expect at least one `LineInfo`, when none are created for an empty source. To fix this, I made it so when we are given an empty source, we add a single "empty" `LineInfo`, with the appropriate `line_index`.

<hr>

When looking into how to fix this, I noticed that `rustc` [doesn't show the annotation](https://github.com/rust-lang/rust/blob/36b21637e93b038453924d3c66821089e71d8baa/tests/ui/include-macros/mismatched-types.stderr#L1-L4) even though there is one that could be displayed. Because of this, I felt it was best to diverge from how `rustc` would render this and show the annotation (and the correct line). 

<details>
  <summary>Original Test</summary>

  ```rust
  #[test]
  fn mismatched_types1() {
      // tests/ui/include-macros/mismatched-types.rs
  
      let file_txt_source = r#""#;
  
      let rust_source = r#"fn main() {
      let b: &[u8] = include_str!("file.txt");    //~ ERROR mismatched types
      let s: &str = include_bytes!("file.txt");   //~ ERROR mismatched types
  }"#;
  
      let input = Level::ERROR.header("mismatched types").id("E0308").group(
          Group::new()
              .element(
                  Snippet::source(file_txt_source)
                      .fold(true)
                      .line_start(3)
                      .origin("$DIR/file.txt")
                      .annotation(
                          AnnotationKind::Primary
                              .span(0..0)
                              .label("expected `&[u8]`, found `&str`"),
                      ),
              )
              .element(
                  Snippet::source(rust_source)
                      .origin("$DIR/mismatched-types.rs")
                      .fold(true)
                      .annotation(
                          AnnotationKind::Context
                              .span(23..28)
                              .label("expected due to this"),
                      )
                      .annotation(
                          AnnotationKind::Context
                              .span(31..55)
                              .label("in this macro invocation"),
                      ),
              )
              .element(
                  Level::NOTE.title("expected reference `&[u8]`\n   found reference `&'static str`"),
              ),
      );

      let expected = str![[r#"
  error[E0308]: mismatched types
    --> $DIR/file.txt:0:1
     |
     |
    ::: $DIR/mismatched-types.rs:2:12
     |
  LL |     let b: &[u8] = include_str!("file.txt");    //~ ERROR mismatched types
     |            -----   ------------------------ in this macro invocation
     |            |
     |            expected due to this
     |
     = note: expected reference `&[u8]`
                found reference `&'static str`
  "#]];
      let renderer = Renderer::plain().anonymized_line_numbers(true);
      assert_data_eq!(renderer.render(input), expected);
  }
  ```
  
</details>


<details>
  <summary>Proposed</summary>
  
  ```rust
  #[test]
  fn mismatched_types1() {
      // tests/ui/include-macros/mismatched-types.rs
  
      let file_txt_source = r#""#;
  
      let rust_source = r#"fn main() {
      let b: &[u8] = include_str!("file.txt");    //~ ERROR mismatched types
      let s: &str = include_bytes!("file.txt");   //~ ERROR mismatched types
  }"#;
  
      let input = Level::ERROR.header("mismatched types").id("E0308").group(
          Group::new()
              .element(
                  Snippet::source(file_txt_source)
                      .fold(true)
                      .line_start(3)
                      .origin("$DIR/file.txt")
                      .annotation(
                          AnnotationKind::Primary
                              .span(0..0)
                              .label("expected `&[u8]`, found `&str`"),
                      ),
              )
              .element(
                  Snippet::source(rust_source)
                      .origin("$DIR/mismatched-types.rs")
                      .fold(true)
                      .annotation(
                          AnnotationKind::Context
                              .span(23..28)
                              .label("expected due to this"),
                      )
                      .annotation(
                          AnnotationKind::Context
                              .span(31..55)
                              .label("in this macro invocation"),
                      ),
              )
              .element(
                  Level::NOTE.title("expected reference `&[u8]`\n   found reference `&'static str`"),
              ),
      );
  
      let expected = str![[r#"
  error[E0308]: mismatched types
    --> $DIR/file.txt:3:1
     |
  LL |
     | ^ expected `&[u8]`, found `&str`
     |
    ::: $DIR/mismatched-types.rs:2:12
     |
  LL |     let b: &[u8] = include_str!("file.txt");    //~ ERROR mismatched types
     |            -----   ------------------------ in this macro invocation
     |            |
     |            expected due to this
     |
     = note: expected reference `&[u8]`
                found reference `&'static str`
  "#]];
      let renderer = Renderer::plain().anonymized_line_numbers(true);
      assert_data_eq!(renderer.render(input), expected);
  }
  ```
  
</details>


Note: I will try to backport this change to `rustc` so that this is only a temporary divergence in output.


Patch:
diff --git a/src/renderer/source_map.rs b/src/renderer/source_map.rs
index d5f91a1..7401fb9 100644
--- a/src/renderer/source_map.rs
+++ b/src/renderer/source_map.rs
@@ -11,6 +11,21 @@ pub(crate) struct SourceMap<'a> {
 
 impl<'a> SourceMap<'a> {
     pub(crate) fn new(source: &'a str, line_start: usize) -> Self {
+        // Empty sources do have a "line", but it is empty, so we need to add
+        // a line with an empty string to the source map.
+        if source.is_empty() {
+            return Self {
+                lines: vec![LineInfo {
+                    line: "",
+                    line_index: line_start,
+                    start_byte: 0,
+                    end_byte: 0,
+                    end_line_size: 0,
+                }],
+                source,
+            };
+        }
+
         let mut current_index = 0;
 
         let mut mapping = vec![];


Test patch:
diff --git a/tests/formatter.rs b/tests/formatter.rs
index e5647b8..61277ec 100644
--- a/tests/formatter.rs
+++ b/tests/formatter.rs
@@ -303,6 +303,7 @@ fn test_only_source() {
 error: 
  --> file.rs
   |
+1 |
 "#]];
     let renderer = Renderer::plain();
     assert_data_eq!(renderer.render(input), expected);
diff --git a/tests/rustc_tests.rs b/tests/rustc_tests.rs
index 0a1c531..b1505d7 100644
--- a/tests/rustc_tests.rs
+++ b/tests/rustc_tests.rs
@@ -2680,3 +2680,116 @@ LL | | */
     let renderer = Renderer::plain().anonymized_line_numbers(true);
     assert_data_eq!(renderer.render(input), expected);
 }
+
+#[test]
+fn mismatched_types1() {
+    // tests/ui/include-macros/mismatched-types.rs
+
+    let file_txt_source = r#""#;
+
+    let rust_source = r#"fn main() {
+    let b: &[u8] = include_str!("file.txt");    //~ ERROR mismatched types
+    let s: &str = include_bytes!("file.txt");   //~ ERROR mismatched types
+}"#;
+
+    let input = Level::ERROR.header("mismatched types").id("E0308").group(
+        Group::new()
+            .element(
+                Snippet::source(file_txt_source)
+                    .fold(true)
+                    .line_start(3)
+                    .origin("$DIR/file.txt")
+                    .annotation(
+                        AnnotationKind::Primary
+                            .span(0..0)
+                            .label("expected `&[u8]`, found `&str`"),
+                    ),
+            )
+            .element(
+                Snippet::source(rust_source)
+                    .origin("$DIR/mismatched-types.rs")
+                    .fold(true)
+                    .annotation(
+                        AnnotationKind::Context
+                            .span(23..28)
+                            .label("expected due to this"),
+                    )
+                    .annotation(
+                        AnnotationKind::Context
+                            .span(31..55)
+                            .label("in this macro invocation"),
+                    ),
+            )
+            .element(
+                Level::NOTE.title("expected reference `&[u8]`\n   found reference `&'static str`"),
+            ),
+    );
+
+    let expected = str![[r#"
+error[E0308]: mismatched types
+  --> $DIR/file.txt:3:1
+   |
+LL |
+   | ^ expected `&[u8]`, found `&str`
+   |
+  ::: $DIR/mismatched-types.rs:2:12
+   |
+LL |     let b: &[u8] = include_str!("file.txt");    //~ ERROR mismatched types
+   |            -----   ------------------------ in this macro invocation
+   |            |
+   |            expected due to this
+   |
+   = note: expected reference `&[u8]`
+              found reference `&'static str`
+"#]];
+    let renderer = Renderer::plain().anonymized_line_numbers(true);
+    assert_data_eq!(renderer.render(input), expected);
+}
+
+#[test]
+fn mismatched_types2() {
+    // tests/ui/include-macros/mismatched-types.rs
+
+    let source = r#"fn main() {
+    let b: &[u8] = include_str!("file.txt");    //~ ERROR mismatched types
+    let s: &str = include_bytes!("file.txt");   //~ ERROR mismatched types
+}"#;
+
+    let input = Level::ERROR.header("mismatched types").id("E0308").group(
+        Group::new()
+            .element(
+                Snippet::source(source)
+                    .origin("$DIR/mismatched-types.rs")
+                    .fold(true)
+                    .annotation(
+                        AnnotationKind::Primary
+                            .span(105..131)
+                            .label("expected `&str`, found `&[u8; 0]`"),
+                    )
+                    .annotation(
+                        AnnotationKind::Context
+                            .span(98..102)
+                            .label("expected due to this"),
+                    ),
+            )
+            .element(
+                Level::NOTE
+                    .title("expected reference `&str`\n   found reference `&'static [u8; 0]`"),
+            ),
+    );
+
+    let expected = str![[r#"
+error[E0308]: mismatched types
+  --> $DIR/mismatched-types.rs:3:19
+   |
+LL |     let s: &str = include_bytes!("file.txt");   //~ ERROR mismatched types
+   |            ----   ^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `&str`, found `&[u8; 0]`
+   |            |
+   |            expected due to this
+   |
+   = note: expected reference `&str`
+              found reference `&'static [u8; 0]`
+"#]];
+    let renderer = Renderer::plain().anonymized_line_numbers(true);
+    assert_data_eq!(renderer.render(input), expected);
+}