Repository: dtolnay/proc-macro2 (Rust)

Issue:
`Span::source_text` panics with multibyte source
```rust
    #[test]
    fn proc_macro2_source_text_is_correct_for_single_byte() {
        let source_code = "const FOO: () = ();";
        let ast = syn::parse_str::<syn::ItemConst>(source_code).unwrap();
        assert_eq!("FOO", ast.ident.span().source_text().unwrap())
    }

    #[test]
    #[should_panic = "is not a char boundary"]
    fn proc_macro2_source_text_is_incorrect_for_multibyte() {
        let source_code = "const 𓀕: () = ();";
        let ast = syn::parse_str::<syn::ItemConst>(source_code).unwrap();
        let reported = ast.ident.span().source_text(); // boom
        assert_eq!("𓀕", reported.unwrap())
    }
```

Note this is just _calling the method_, not unwrapping the `Option`:
```
thread 'tests::proc_macro2_source_text_is_incorrect_for_multibyte' panicked at 'byte index 7 is not a char boundary; it is inside '𓀕' (bytes 6..10) of `const 𓀕: () = ();`', /home/aatif/.cargo/registry/src/index.crates.io-6f17d22bba15001f/proc-macro2-1.0.67/src/fallback.rs:369:9
...
   6: <alloc::string::String as core::ops::index::Index<core::ops::range::Range<usize>>>::index
             at /rustc/5680fa18feaa87f3ff04063800aec256c3d4b4be/library/alloc/src/string.rs:2350:10
   7: proc_macro2::fallback::FileInfo::source_text
             at /home/aatif/.cargo/registry/src/index.crates.io-6f17d22bba15001f/proc-macro2-1.0.67/src/fallback.rs:369:9
   8: proc_macro2::fallback::Span::source_text::{{closure}}
             at /home/aatif/.cargo/registry/src/index.crates.io-6f17d22bba15001f/proc-macro2-1.0.67/src/fallback.rs:569:43
   9: std::thread::local::LocalKey<T>::try_with
             at /rustc/5680fa18feaa87f3ff04063800aec256c3d4b4be/library/std/src/thread/local.rs:270:16
  10: std::thread::local::LocalKey<T>::with
             at /rustc/5680fa18feaa87f3ff04063800aec256c3d4b4be/library/std/src/thread/local.rs:246:9
  11: proc_macro2::fallback::Span::source_text
             at /home/aatif/.cargo/registry/src/index.crates.io-6f17d22bba15001f/proc-macro2-1.0.67/src/fallback.rs:569:22
  12: proc_macro2::Span::source_text
             at /home/aatif/.cargo/registry/src/index.crates.io-6f17d22bba15001f/proc-macro2-1.0.67/src/lib.rs:522:9
  13: synsert::tests::proc_macro2_source_text_is_incorrect_for_multibyte
             at ./src/lib.rs:165:24
  14: synsert::tests::proc_macro2_source_text_is_incorrect_for_multibyte::{{closure}}
             at ./src/lib.rs:162:61
...
```

`proc-macro2 1.0.67` (latest)
Not called from a `proc-macro` context


Discussion:
I have published a fix in proc-macro2 1.0.68.



Patch:
diff --git a/src/fallback.rs b/src/fallback.rs
index db52959..352b459 100644
--- a/src/fallback.rs
+++ b/src/fallback.rs
@@ -364,8 +364,13 @@ impl FileInfo {
 
     fn source_text(&self, span: Span) -> String {
         let lo = (span.lo - self.span.lo) as usize;
-        let hi = (span.hi - self.span.lo) as usize;
-        self.source_text[lo..hi].to_owned()
+        let trunc_lo = &self.source_text[lo..];
+        let char_len = (span.hi - span.lo) as usize;
+        let source_text = match trunc_lo.char_indices().nth(char_len) {
+            Some((offset, _ch)) => &trunc_lo[..offset],
+            None => trunc_lo,
+        };
+        source_text.to_owned()
     }
 }
 


Test patch:
diff --git a/tests/test.rs b/tests/test.rs
index 8e47b46..4a6ca75 100644
--- a/tests/test.rs
+++ b/tests/test.rs
@@ -325,6 +325,15 @@ fn literal_span() {
     assert!(positive.subspan(1..4).is_none());
 }
 
+#[cfg(span_locations)]
+#[test]
+fn source_text() {
+    let input = "    𓀕    ";
+    let tokens = input.parse::<proc_macro2::TokenStream>().unwrap();
+    let ident = tokens.into_iter().next().unwrap();
+    assert_eq!("𓀕", ident.span().source_text().unwrap());
+}
+
 #[test]
 fn roundtrip() {
     fn roundtrip(p: &str) {