Repository: dtolnay/thiserror (rust)

Issue:
Escaped field names do not work
```rust
use thiserror::Error;

#[derive(Debug, Error)]
pub enum Error {
    #[error("invalid field type {r#type:?} for {field:?}")]
    InvalidField {
        field: String,
        r#type: String
    }
}
```
```text
invalid format string: expected `'}'`, found `'#'`
  --> src\lib.rs:17:51
   |
17 |     #[error("the field {field:?} is not of type {r#type:?}")]
   |                                                 - ^ expected `}` in format string
   |                                                 |
   |                                                 because of this opening brace
   |
   = note: if you intended to print `{`, you can escape it using `{{`
```

Pull request:
Support raw-identifier fields in format strings
Fixes #96 by stripping the "r#" prefix from format variables.

## Background

Previously, given a type like
```rust
#[derive(Error, Debug)]
#[error("a raw ident: {r#reserved}")]`
struct RawError {
    r#reserved: String,
}
```
`thiserror` would output something like
```rust
// ...
Self { r#reserved } = self;
write!("a raw ident: {r#reserved}", r#reserved = r#reserved);
// ...
```
This would fail during `write!()` expansion because the character `#` isn't allowed in named `format!()` parameters.

## Proposed solution

This PR replaces the `r#` prefix with `raw_field_` when generating named parameters, producing output like
```rust
// ...
Self { r#reserved } = self;
write!("a raw ident: {raw_field_reserved}", raw_field_reserved = r#reserved);
// ...
```

### Caveat: Possible name collision

The generated named parameters could collide with user-defined named parameters. For example, the following compiles but produces potentially-unexpected output: 
```rust
#[derive(Error, Debug)]
#[error("a raw ident: {r#reserved}; and a named parameter: {raw_field_reserved}", raw_field_reserved = "some value")]`
struct RawError {
    r#reserved: String,
}

println!("{}", RawError { r#reserved: "some other value" });
```
This kind of collision is already possible in `thiserror`. Existing code maps tuple fields to named parameters of the form `field_0`. These collisions could be avoided by instead mapping field references to anonymous parameters (`{}`), which is actually what the documentation claims is done already; however, I think that's outside the scope of this PR.

## Further work

A similar but disjoint issue: `thiserror` assumes all user-defined named parameters are valid identifiers, and panics if it encounters an exception. `format!()` itself does not require this. I think fixing this is outside the scope of this PR.

Patch:
diff --git a/impl/src/fmt.rs b/impl/src/fmt.rs
index ea1b518..5e12c62 100644
--- a/impl/src/fmt.rs
+++ b/impl/src/fmt.rs
@@ -5,7 +5,7 @@ use quote::{format_ident, quote_spanned};
 use std::collections::HashSet as Set;
 use syn::ext::IdentExt;
 use syn::parse::{ParseStream, Parser};
-use syn::{Ident, Index, LitStr, Member, Result, Token};
+use syn::{parse_str, Ident, Index, LitStr, Member, Result, Token};
 
 impl Display<'_> {
     // Transform `"error {var}"` to `"error {}", var`.
@@ -55,7 +55,9 @@ impl Display<'_> {
                 }
                 'a'..='z' | 'A'..='Z' | '_' => {
                     let ident = take_ident(&mut read);
-                    Member::Named(Ident::new(&ident, span))
+                    let mut ident = parse_str::<Ident>(&ident).unwrap();
+                    ident.set_span(span);
+                    Member::Named(ident)
                 }
                 _ => continue,
             };
@@ -64,6 +66,10 @@ impl Display<'_> {
                 Member::Named(ident) => ident.clone(),
             };
             let mut formatvar = local.clone();
+            if formatvar.to_string().starts_with("r#") {
+                // Replace the "r#" prefix.
+                formatvar = format_ident!("raw_field_{}", formatvar);
+            }
             if formatvar.to_string().starts_with('_') {
                 // Work around leading underscore being rejected by 1.40 and
                 // older compilers. https://github.com/rust-lang/rust/pull/66847
@@ -125,6 +131,10 @@ fn take_int(read: &mut &str) -> String {
 
 fn take_ident(read: &mut &str) -> String {
     let mut ident = String::new();
+    if let Some(rest) = read.strip_prefix("r#") {
+        ident.push_str("r#");
+        *read = rest;
+    }
     for (i, ch) in read.char_indices() {
         match ch {
             'a'..='z' | 'A'..='Z' | '0'..='9' | '_' => ident.push(ch),


Test patch:
diff --git a/tests/test_display.rs b/tests/test_display.rs
index 9bc385a..84beee3 100644
--- a/tests/test_display.rs
+++ b/tests/test_display.rs
@@ -230,3 +230,51 @@ fn test_macro_rules() {
     assert("0", Error0::Repro(0));
     assert("0", Error1::Repro(0));
 }
+
+#[test]
+fn test_raw() {
+    #[derive(Error, Debug)]
+    #[error("braced raw error: {r#fn}")]
+    struct Error {
+        r#fn: String,
+    }
+
+    assert(
+        "braced raw error: T",
+        Error {
+            r#fn: "T".to_owned(),
+        },
+    );
+}
+
+#[test]
+fn test_raw_enum() {
+    #[derive(Error, Debug)]
+    enum Error {
+        #[error("braced raw error: {r#fn}")]
+        Braced { r#fn: String },
+    }
+
+    assert(
+        "braced raw error: T",
+        Error::Braced {
+            r#fn: "T".to_owned(),
+        },
+    );
+}
+
+#[test]
+fn test_raw_conflict() {
+    #[derive(Error, Debug)]
+    enum Error {
+        #[error("braced raw error: {r#func}, {func}", func = "U")]
+        Braced { r#func: String },
+    }
+
+    assert(
+        "braced raw error: T, U",
+        Error::Braced {
+            r#func: "T".to_owned(),
+        },
+    );
+}