Repository: dtolnay/thiserror (Rust)

Issue:
Inappropriate inferred trait bounds when format var name overlaps with field name
This code expands very sensibly to `impl<T: Display> Display for Error<T>`:

```rust
#[derive(Error, Debug)]
#[error("{thing}")]
pub struct Error<T> {
    thing: T,
}
```

This one expands to `impl<T> Display for Error<T>` with no inferred bound on T:

```rust
#[derive(Error, Debug)]
#[error("{var}", var = "...")]
pub struct Error<T> {
    thing: T,
}
```

But this one does not make sense. It expands to `impl<T: Display> Display for Error<T>` even though self.thing is not used in the error message and T does not need to be bound with Display.

```rust
#[derive(Error, Debug)]
#[error("{thing}", thing = "...")]
pub struct Error<T> {
    thing: T,
}
```


Discussion:

Patch:
diff --git a/impl/src/fmt.rs b/impl/src/fmt.rs
index 8b571d9..d555c24 100644
--- a/impl/src/fmt.rs
+++ b/impl/src/fmt.rs
@@ -69,6 +69,20 @@ impl Display<'_> {
                 }
                 _ => continue,
             };
+            let formatvar = match &member {
+                MemberUnraw::Unnamed(index) => IdentUnraw::new(format_ident!("_{}", index)),
+                MemberUnraw::Named(ident) => ident.clone(),
+            };
+            out += &formatvar.to_string();
+            if !named_args.insert(formatvar.clone()) {
+                // Already specified in the format argument list.
+                continue;
+            }
+            if !has_trailing_comma {
+                args.extend(quote_spanned!(span=> ,));
+            }
+            let local = formatvar.to_local();
+            args.extend(quote_spanned!(span=> #formatvar = #local));
             if let Some(&field) = member_index.get(&member) {
                 let end_spec = match read.find('}') {
                     Some(end_spec) => end_spec,
@@ -83,28 +97,15 @@ impl Display<'_> {
                     Some('b') => Trait::Binary,
                     Some('e') => Trait::LowerExp,
                     Some('E') => Trait::UpperExp,
-                    Some(_) | None => Trait::Display,
+                    Some(_) => Trait::Display,
+                    None => {
+                        has_bonus_display = true;
+                        args.extend(quote_spanned!(span=> .as_display()));
+                        Trait::Display
+                    }
                 };
                 implied_bounds.insert((field, bound));
             }
-            let formatvar = match &member {
-                MemberUnraw::Unnamed(index) => IdentUnraw::new(format_ident!("_{}", index)),
-                MemberUnraw::Named(ident) => ident.clone(),
-            };
-            out += &formatvar.to_string();
-            if !named_args.insert(formatvar.clone()) {
-                // Already specified in the format argument list.
-                continue;
-            }
-            if !has_trailing_comma {
-                args.extend(quote_spanned!(span=> ,));
-            }
-            let local = formatvar.to_local();
-            args.extend(quote_spanned!(span=> #formatvar = #local));
-            if read.starts_with('}') && member_index.contains_key(&member) {
-                has_bonus_display = true;
-                args.extend(quote_spanned!(span=> .as_display()));
-            }
             has_trailing_comma = false;
         }
 


Test patch:
diff --git a/tests/test_generics.rs b/tests/test_generics.rs
index d7790e2..4d49edf 100644
--- a/tests/test_generics.rs
+++ b/tests/test_generics.rs
@@ -159,3 +159,16 @@ pub struct StructFromGeneric<E> {
 #[derive(Error, Debug)]
 #[error(transparent)]
 pub struct StructTransparentGeneric<E>(pub E);
+
+// Regression test for https://github.com/dtolnay/thiserror/issues/345
+#[test]
+fn test_no_bound_on_named_fmt() {
+    #[derive(Error, Debug)]
+    #[error("{thing}", thing = "...")]
+    struct Error<T> {
+        thing: T,
+    }
+
+    let error = Error { thing: DebugOnly };
+    assert_eq!(error.to_string(), "...");
+}