Repository: dtolnay/thiserror
Pull request #347 (2024-11-04 20:23:07)

Delete support for raw identifiers inside format string
`format_args!("{r#keyword}")` is not allowed by core::fmt's format string syntax. Thiserror shouldn't allow it either.


Patch:
diff --git a/impl/src/fmt.rs b/impl/src/fmt.rs
index 3d1394c..5d026d1 100644
--- a/impl/src/fmt.rs
+++ b/impl/src/fmt.rs
@@ -1,7 +1,7 @@
 use crate::ast::Field;
 use crate::attr::{Display, Trait};
 use crate::scan_expr::scan_expr;
-use proc_macro2::{TokenStream, TokenTree};
+use proc_macro2::{Span, TokenStream, TokenTree};
 use quote::{format_ident, quote, quote_spanned};
 use std::collections::{BTreeSet as Set, HashMap as Map};
 use syn::ext::IdentExt;
@@ -87,14 +87,10 @@ impl Display<'_> {
                 };
                 implied_bounds.insert((field, bound));
             }
-            let local = match &member {
+            let formatvar = match &member {
                 Member::Unnamed(index) => format_ident!("_{}", index),
                 Member::Named(ident) => ident.clone(),
             };
-            let mut formatvar = local.clone();
-            if formatvar.to_string().starts_with("r#") {
-                formatvar = format_ident!("r_{}", formatvar);
-            }
             out += &formatvar.to_string();
             if !named_args.insert(formatvar.clone()) {
                 // Already specified in the format argument list.
@@ -103,6 +99,7 @@ impl Display<'_> {
             if !has_trailing_comma {
                 args.extend(quote_spanned!(span=> ,));
             }
+            let local = raw_if_needed(&formatvar);
             args.extend(quote_spanned!(span=> #formatvar = #local));
             if read.starts_with('}') && member_index.contains_key(&member) {
                 has_bonus_display = true;
@@ -233,11 +230,6 @@ fn take_int(read: &mut &str) -> String {
 
 fn take_ident(read: &mut &str) -> Ident {
     let mut ident = String::new();
-    let raw = read.starts_with("r#");
-    if raw {
-        ident.push_str("r#");
-        *read = &read[2..];
-    }
     for (i, ch) in read.char_indices() {
         match ch {
             'a'..='z' | 'A'..='Z' | '0'..='9' | '_' => ident.push(ch),
@@ -247,5 +239,17 @@ fn take_ident(read: &mut &str) -> Ident {
             }
         }
     }
-    Ident::parse_any.parse_str(&ident).unwrap()
+    Ident::new(&ident, Span::call_site())
+}
+
+fn raw_if_needed(ident: &Ident) -> Ident {
+    let repr = ident.to_string();
+    if syn::parse_str::<Ident>(&repr).is_err() {
+        if let "_" | "super" | "self" | "Self" | "crate" = repr.as_str() {
+            // Some identifiers are never allowed to appear as raw, like r#self and r#_.
+        } else {
+            return Ident::new_raw(&repr, Span::call_site());
+        }
+    }
+    ident.clone()
 }


Test patch:
diff --git a/tests/test_display.rs b/tests/test_display.rs
index 7fad386..27bb72c 100644
--- a/tests/test_display.rs
+++ b/tests/test_display.rs
@@ -279,7 +279,7 @@ fn test_macro_rules() {
 #[test]
 fn test_raw() {
     #[derive(Error, Debug)]
-    #[error("braced raw error: {r#fn}")]
+    #[error("braced raw error: {fn}")]
     struct Error {
         r#fn: &'static str,
     }
@@ -291,24 +291,13 @@ fn test_raw() {
 fn test_raw_enum() {
     #[derive(Error, Debug)]
     enum Error {
-        #[error("braced raw error: {r#fn}")]
+        #[error("braced raw error: {fn}")]
         Braced { r#fn: &'static str },
     }
 
     assert("braced raw error: T", Error::Braced { r#fn: "T" });
 }
 
-#[test]
-fn test_raw_conflict() {
-    #[derive(Error, Debug)]
-    enum Error {
-        #[error("braced raw error: {r#func}, {func}", func = "U")]
-        Braced { r#func: &'static str },
-    }
-
-    assert("braced raw error: T, U", Error::Braced { r#func: "T" });
-}
-
 #[test]
 fn test_keyword() {
     #[derive(Error, Debug)]
diff --git a/tests/ui/raw-identifier.rs b/tests/ui/raw-identifier.rs
new file mode 100644
index 0000000..e7e66d0
--- /dev/null
+++ b/tests/ui/raw-identifier.rs
@@ -0,0 +1,12 @@
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+#[error("error: {r#fn}")]
+pub struct Error {
+    r#fn: &'static str,
+}
+
+fn main() {
+    let r#fn = "...";
+    let _ = format!("error: {r#fn}");
+}
diff --git a/tests/ui/raw-identifier.stderr b/tests/ui/raw-identifier.stderr
new file mode 100644
index 0000000..a3ce94d
--- /dev/null
+++ b/tests/ui/raw-identifier.stderr
@@ -0,0 +1,21 @@
+error: invalid format string: raw identifiers are not supported
+ --> tests/ui/raw-identifier.rs:4:18
+  |
+4 | #[error("error: {r#fn}")]
+  |                  --^^
+  |                  |
+  |                  raw identifier used here in format string
+  |                  help: remove the `r#`
+  |
+  = note: identifiers in format strings can be keywords and don't need to be prefixed with `r#`
+
+error: invalid format string: raw identifiers are not supported
+  --> tests/ui/raw-identifier.rs:11:30
+   |
+11 |     let _ = format!("error: {r#fn}");
+   |                              --^^
+   |                              |
+   |                              raw identifier used here in format string
+   |                              help: remove the `r#`
+   |
+   = note: identifiers in format strings can be keywords and don't need to be prefixed with `r#`