Repository: dtolnay/thiserror (rust)

Issue:
Associated type equality constraints interfere with display conversion
The following code fails to compile. Similar code compiles if the associated type's name is different from the struct field's name, although these two things should have no bearing on one another.

```rust
#![allow(non_snake_case)]

use std::fmt::Display;
use std::path::PathBuf;
use thiserror::Error;

trait Trait<T>: Display {
    type A;
}

impl<T> Trait<T> for i32 {
    type A = i32;
}

#[derive(Error, Debug)]
#[error("{A} {b}", b = &0 as &dyn Trait<i32, A = i32>)]
pub struct Error {
    pub A: PathBuf,
}
```

```console
error[E0277]: `PathBuf` doesn't implement `std::fmt::Display`
  --> src/main.rs:16:10
   |
15 | #[derive(Error, Debug)]
   |          ----- in this derive macro expansion
16 | #[error("{A} {b}", b = &0 as &dyn Trait<i32, A = i32>)]
   |          ^^^ `PathBuf` cannot be formatted with the default formatter; call `.display()` on it
   |
   = help: the trait `std::fmt::Display` is not implemented for `PathBuf`
   = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
   = note: call `.display()` or `.to_string_lossy()` to safely print paths, as they may contain non-Unicode data
   = note: this error originates in the macro `$crate::format_args` which comes from the expansion of the derive macro `Error` (in Nightly builds, run with -Z macro-backtrace for more info)
```

Pull request:
More robust scanning for fmt argument expressions
Fixes #335.

Patch:
diff --git a/impl/Cargo.toml b/impl/Cargo.toml
index 5acbe8f..1d65e9c 100644
--- a/impl/Cargo.toml
+++ b/impl/Cargo.toml
@@ -14,7 +14,7 @@ proc-macro = true
 [dependencies]
 proc-macro2 = "1.0.74"
 quote = "1.0.35"
-syn = "2.0.86"
+syn = "2.0.87"
 
 [package.metadata.docs.rs]
 targets = ["x86_64-unknown-linux-gnu"]
diff --git a/impl/src/fmt.rs b/impl/src/fmt.rs
index c036536..029870b 100644
--- a/impl/src/fmt.rs
+++ b/impl/src/fmt.rs
@@ -1,5 +1,6 @@
 use crate::ast::Field;
 use crate::attr::{Display, Trait};
+use crate::scan_expr::scan_expr;
 use proc_macro2::TokenTree;
 use quote::{format_ident, quote_spanned};
 use std::collections::{BTreeSet as Set, HashMap as Map};
@@ -121,14 +122,16 @@ fn explicit_named_args(input: ParseStream) -> Result<Set<Ident>> {
     let mut named_args = Set::new();
 
     while !input.is_empty() {
-        if input.peek(Token![,]) && input.peek2(Ident::peek_any) && input.peek3(Token![=]) {
-            input.parse::<Token![,]>()?;
+        input.parse::<Token![,]>()?;
+        if input.is_empty() {
+            break;
+        }
+        if input.peek(Ident::peek_any) && input.peek2(Token![=]) && !input.peek2(Token![==]) {
             let ident = input.call(Ident::parse_any)?;
             input.parse::<Token![=]>()?;
             named_args.insert(ident);
-        } else {
-            input.parse::<TokenTree>()?;
         }
+        scan_expr(input)?;
     }
 
     Ok(named_args)
diff --git a/impl/src/lib.rs b/impl/src/lib.rs
index 58f4bb5..7d7c6e3 100644
--- a/impl/src/lib.rs
+++ b/impl/src/lib.rs
@@ -2,6 +2,7 @@
     clippy::blocks_in_conditions,
     clippy::cast_lossless,
     clippy::cast_possible_truncation,
+    clippy::enum_glob_use,
     clippy::manual_find,
     clippy::manual_let_else,
     clippy::manual_map,
@@ -23,6 +24,7 @@ mod expand;
 mod fmt;
 mod generics;
 mod prop;
+mod scan_expr;
 mod span;
 mod valid;
 
diff --git a/impl/src/scan_expr.rs b/impl/src/scan_expr.rs
new file mode 100644
index 0000000..155b5b6
--- /dev/null
+++ b/impl/src/scan_expr.rs
@@ -0,0 +1,264 @@
+use self::{Action::*, Input::*};
+use proc_macro2::{Delimiter, Ident, Spacing, TokenTree};
+use syn::parse::{ParseStream, Result};
+use syn::{AngleBracketedGenericArguments, BinOp, Expr, ExprPath, Lifetime, Lit, Token, Type};
+
+enum Input {
+    Keyword(&'static str),
+    Punct(&'static str),
+    ConsumeAny,
+    ConsumeBinOp,
+    ConsumeBrace,
+    ConsumeDelimiter,
+    ConsumeIdent,
+    ConsumeLifetime,
+    ConsumeLiteral,
+    ConsumeNestedBrace,
+    ExpectPath,
+    ExpectTurbofish,
+    ExpectType,
+    CanBeginExpr,
+    Otherwise,
+    Empty,
+}
+
+enum Action {
+    SetState(&'static [(Input, Action)]),
+    IncDepth,
+    DecDepth,
+    Finish,
+}
+
+static INIT: [(Input, Action); 28] = [
+    (ConsumeDelimiter, SetState(&POSTFIX)),
+    (Keyword("async"), SetState(&ASYNC)),
+    (Keyword("break"), SetState(&BREAK_LABEL)),
+    (Keyword("const"), SetState(&CONST)),
+    (Keyword("continue"), SetState(&CONTINUE)),
+    (Keyword("for"), SetState(&FOR)),
+    (Keyword("if"), IncDepth),
+    (Keyword("let"), SetState(&PATTERN)),
+    (Keyword("loop"), SetState(&BLOCK)),
+    (Keyword("match"), IncDepth),
+    (Keyword("move"), SetState(&CLOSURE)),
+    (Keyword("return"), SetState(&RETURN)),
+    (Keyword("static"), SetState(&CLOSURE)),
+    (Keyword("unsafe"), SetState(&BLOCK)),
+    (Keyword("while"), IncDepth),
+    (Keyword("yield"), SetState(&RETURN)),
+    (Keyword("_"), SetState(&POSTFIX)),
+    (Punct("!"), SetState(&INIT)),
+    (Punct("#"), SetState(&[(ConsumeDelimiter, SetState(&INIT))])),
+    (Punct("&"), SetState(&REFERENCE)),
+    (Punct("*"), SetState(&INIT)),
+    (Punct("-"), SetState(&INIT)),
+    (Punct("..="), SetState(&INIT)),
+    (Punct(".."), SetState(&RANGE)),
+    (Punct("|"), SetState(&CLOSURE_ARGS)),
+    (ConsumeLifetime, SetState(&[(Punct(":"), SetState(&INIT))])),
+    (ConsumeLiteral, SetState(&POSTFIX)),
+    (ExpectPath, SetState(&PATH)),
+];
+
+static POSTFIX: [(Input, Action); 10] = [
+    (Keyword("as"), SetState(&[(ExpectType, SetState(&POSTFIX))])),
+    (Punct("..="), SetState(&INIT)),
+    (Punct(".."), SetState(&RANGE)),
+    (Punct("."), SetState(&DOT)),
+    (Punct("?"), SetState(&POSTFIX)),
+    (ConsumeBinOp, SetState(&INIT)),
+    (Punct("="), SetState(&INIT)),
+    (ConsumeNestedBrace, SetState(&IF_THEN)),
+    (ConsumeDelimiter, SetState(&POSTFIX)),
+    (Empty, Finish),
+];
+
+static ASYNC: [(Input, Action); 3] = [
+    (Keyword("move"), SetState(&ASYNC)),
+    (Punct("|"), SetState(&CLOSURE_ARGS)),
+    (ConsumeBrace, SetState(&POSTFIX)),
+];
+
+static BLOCK: [(Input, Action); 1] = [(ConsumeBrace, SetState(&POSTFIX))];
+
+static BREAK_LABEL: [(Input, Action); 2] = [
+    (ConsumeLifetime, SetState(&BREAK_VALUE)),
+    (Otherwise, SetState(&BREAK_VALUE)),
+];
+
+static BREAK_VALUE: [(Input, Action); 3] = [
+    (ConsumeNestedBrace, SetState(&IF_THEN)),
+    (CanBeginExpr, SetState(&INIT)),
+    (Otherwise, SetState(&POSTFIX)),
+];
+
+static CLOSURE: [(Input, Action); 6] = [
+    (Keyword("async"), SetState(&CLOSURE)),
+    (Keyword("move"), SetState(&CLOSURE)),
+    (Punct(","), SetState(&CLOSURE)),
+    (Punct(">"), SetState(&CLOSURE)),
+    (Punct("|"), SetState(&CLOSURE_ARGS)),
+    (ConsumeLifetime, SetState(&CLOSURE)),
+];
+
+static CLOSURE_ARGS: [(Input, Action); 2] = [
+    (Punct("|"), SetState(&CLOSURE_RET)),
+    (ConsumeAny, SetState(&CLOSURE_ARGS)),
+];
+
+static CLOSURE_RET: [(Input, Action); 2] = [
+    (Punct("->"), SetState(&[(ExpectType, SetState(&BLOCK))])),
+    (Otherwise, SetState(&INIT)),
+];
+
+static CONST: [(Input, Action); 2] = [
+    (Punct("|"), SetState(&CLOSURE_ARGS)),
+    (ConsumeBrace, SetState(&POSTFIX)),
+];
+
+static CONTINUE: [(Input, Action); 2] = [
+    (ConsumeLifetime, SetState(&POSTFIX)),
+    (Otherwise, SetState(&POSTFIX)),
+];
+
+static DOT: [(Input, Action); 3] = [
+    (Keyword("await"), SetState(&POSTFIX)),
+    (ConsumeIdent, SetState(&METHOD)),
+    (ConsumeLiteral, SetState(&POSTFIX)),
+];
+
+static FOR: [(Input, Action); 2] = [
+    (Punct("<"), SetState(&CLOSURE)),
+    (Otherwise, SetState(&PATTERN)),
+];
+
+static IF_ELSE: [(Input, Action); 2] = [(Keyword("if"), SetState(&INIT)), (ConsumeBrace, DecDepth)];
+static IF_THEN: [(Input, Action); 2] =
+    [(Keyword("else"), SetState(&IF_ELSE)), (Otherwise, DecDepth)];
+
+static METHOD: [(Input, Action); 1] = [(ExpectTurbofish, SetState(&POSTFIX))];
+
+static PATH: [(Input, Action); 4] = [
+    (Punct("!="), SetState(&INIT)),
+    (Punct("!"), SetState(&INIT)),
+    (ConsumeNestedBrace, SetState(&IF_THEN)),
+    (Otherwise, SetState(&POSTFIX)),
+];
+
+static PATTERN: [(Input, Action); 15] = [
+    (ConsumeDelimiter, SetState(&PATTERN)),
+    (Keyword("box"), SetState(&PATTERN)),
+    (Keyword("in"), IncDepth),
+    (Keyword("mut"), SetState(&PATTERN)),
+    (Keyword("ref"), SetState(&PATTERN)),
+    (Keyword("_"), SetState(&PATTERN)),
+    (Punct("!"), SetState(&PATTERN)),
+    (Punct("&"), SetState(&PATTERN)),
+    (Punct("..="), SetState(&PATTERN)),
+    (Punct(".."), SetState(&PATTERN)),
+    (Punct("="), SetState(&INIT)),
+    (Punct("@"), SetState(&PATTERN)),
+    (Punct("|"), SetState(&PATTERN)),
+    (ConsumeLiteral, SetState(&PATTERN)),
+    (ExpectPath, SetState(&PATTERN)),
+];
+
+static RANGE: [(Input, Action); 6] = [
+    (Punct("..="), SetState(&INIT)),
+    (Punct(".."), SetState(&RANGE)),
+    (Punct("."), SetState(&DOT)),
+    (ConsumeNestedBrace, SetState(&IF_THEN)),
+    (Empty, Finish),
+    (Otherwise, SetState(&INIT)),
+];
+
+static RAW: [(Input, Action); 3] = [
+    (Keyword("const"), SetState(&INIT)),
+    (Keyword("mut"), SetState(&INIT)),
+    (Otherwise, SetState(&POSTFIX)),
+];
+
+static REFERENCE: [(Input, Action); 3] = [
+    (Keyword("mut"), SetState(&INIT)),
+    (Keyword("raw"), SetState(&RAW)),
+    (Otherwise, SetState(&INIT)),
+];
+
+static RETURN: [(Input, Action); 2] = [
+    (CanBeginExpr, SetState(&INIT)),
+    (Otherwise, SetState(&POSTFIX)),
+];
+
+pub(crate) fn scan_expr(input: ParseStream) -> Result<()> {
+    let mut state = INIT.as_slice();
+    let mut depth = 0usize;
+    'table: loop {
+        for rule in state {
+            if match rule.0 {
+                Input::Keyword(expected) => input.step(|cursor| match cursor.ident() {
+                    Some((ident, rest)) if ident == expected => Ok((true, rest)),
+                    _ => Ok((false, *cursor)),
+                })?,
+                Input::Punct(expected) => input.step(|cursor| {
+                    let begin = *cursor;
+                    let mut cursor = begin;
+                    for (i, ch) in expected.chars().enumerate() {
+                        match cursor.punct() {
+                            Some((punct, _)) if punct.as_char() != ch => break,
+                            Some((_, rest)) if i == expected.len() - 1 => {
+                                return Ok((true, rest));
+                            }
+                            Some((punct, rest)) if punct.spacing() == Spacing::Joint => {
+                                cursor = rest;
+                            }
+                            _ => break,
+                        }
+                    }
+                    Ok((false, begin))
+                })?,
+                Input::ConsumeAny => input.parse::<Option<TokenTree>>()?.is_some(),
+                Input::ConsumeBinOp => input.parse::<BinOp>().is_ok(),
+                Input::ConsumeBrace | Input::ConsumeNestedBrace => {
+                    (matches!(rule.0, Input::ConsumeBrace) || depth > 0)
+                        && input.step(|cursor| match cursor.group(Delimiter::Brace) {
+                            Some((_inside, _span, rest)) => Ok((true, rest)),
+                            None => Ok((false, *cursor)),
+                        })?
+                }
+                Input::ConsumeDelimiter => input.step(|cursor| match cursor.any_group() {
+                    Some((_inside, _delimiter, _span, rest)) => Ok((true, rest)),
+                    None => Ok((false, *cursor)),
+                })?,
+                Input::ConsumeIdent => input.parse::<Option<Ident>>()?.is_some(),
+                Input::ConsumeLifetime => input.parse::<Option<Lifetime>>()?.is_some(),
+                Input::ConsumeLiteral => input.parse::<Option<Lit>>()?.is_some(),
+                Input::ExpectPath => {
+                    input.parse::<ExprPath>()?;
+                    true
+                }
+                Input::ExpectTurbofish => {
+                    if input.peek(Token![::]) {
+                        input.parse::<AngleBracketedGenericArguments>()?;
+                    }
+                    true
+                }
+                Input::ExpectType => {
+                    Type::without_plus(input)?;
+                    true
+                }
+                Input::CanBeginExpr => Expr::peek(input),
+                Input::Otherwise => true,
+                Input::Empty => input.is_empty() || input.peek(Token![,]),
+            } {
+                state = match rule.1 {
+                    Action::SetState(next) => next,
+                    Action::IncDepth => (depth += 1, &INIT).1,
+                    Action::DecDepth => (depth -= 1, &POSTFIX).1,
+                    Action::Finish => return if depth == 0 { Ok(()) } else { break },
+                };
+                continue 'table;
+            }
+        }
+        return Err(input.error("unsupported expression"));
+    }
+}


Test patch:
diff --git a/tests/test_expr.rs b/tests/test_expr.rs
index e369ba0..4252280 100644
--- a/tests/test_expr.rs
+++ b/tests/test_expr.rs
@@ -1,6 +1,7 @@
 #![allow(clippy::iter_cloned_collect, clippy::uninlined_format_args)]
 
 use core::fmt::Display;
+use std::path::PathBuf;
 use thiserror::Error;
 
 // Some of the elaborate cases from the rcc codebase, which is a C compiler in
@@ -87,3 +88,29 @@ fn test_rustup() {
         },
     );
 }
+
+// Regression test for https://github.com/dtolnay/thiserror/issues/335
+#[test]
+#[allow(non_snake_case)]
+fn test_assoc_type_equality_constraint() {
+    pub trait Trait<T>: Display {
+        type A;
+    }
+
+    impl<T> Trait<T> for i32 {
+        type A = i32;
+    }
+
+    #[derive(Error, Debug)]
+    #[error("{A} {b}", b = &0 as &dyn Trait<i32, A = i32>)]
+    pub struct Error {
+        pub A: PathBuf,
+    }
+
+    assert(
+        "... 0",
+        Error {
+            A: PathBuf::from("..."),
+        },
+    );
+}