From 561e29eb809d78f918b90d624e9617931c4194dd Mon Sep 17 00:00:00 2001 From: David Tolnay Date: Sat, 2 Nov 2024 09:39:37 -0700 Subject: [PATCH 1/4] Add regression test for issue 335 error[E0277]: `PathBuf` doesn't implement `std::fmt::Display` --> tests/test_expr.rs:105:14 | 104 | #[derive(Error, Debug)] | ----- in this derive macro expansion 105 | #[error("{A} {b}", b = &0 as &dyn Trait)] | ^^^ `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) --- tests/test_expr.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) 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: Display { + type A; + } + + impl Trait for i32 { + type A = i32; + } + + #[derive(Error, Debug)] + #[error("{A} {b}", b = &0 as &dyn Trait)] + pub struct Error { + pub A: PathBuf, + } + + assert( + "... 0", + Error { + A: PathBuf::from("..."), + }, + ); +} From c0050558f7638e1a5c23f2997b5bd6dcaac88a76 Mon Sep 17 00:00:00 2001 From: David Tolnay Date: Sat, 2 Nov 2024 09:31:56 -0700 Subject: [PATCH 2/4] Import expr scanner from syn 2.0.87 --- impl/Cargo.toml | 2 +- impl/src/lib.rs | 1 + impl/src/scan_expr.rs | 264 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 impl/src/scan_expr.rs 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/lib.rs b/impl/src/lib.rs index 58f4bb5..48075eb 100644 --- a/impl/src/lib.rs +++ b/impl/src/lib.rs @@ -23,6 +23,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::>()?.is_some(), + Input::ConsumeBinOp => input.parse::().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::>()?.is_some(), + Input::ConsumeLifetime => input.parse::>()?.is_some(), + Input::ConsumeLiteral => input.parse::>()?.is_some(), + Input::ExpectPath => { + input.parse::()?; + true + } + Input::ExpectTurbofish => { + if input.peek(Token![::]) { + input.parse::()?; + } + 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")); + } +} From 2585669fa1a44c2ce1456e91fdcd38c2f1e747a5 Mon Sep 17 00:00:00 2001 From: David Tolnay Date: Sat, 2 Nov 2024 09:36:05 -0700 Subject: [PATCH 3/4] More robust scanning for fmt argument expressions --- impl/src/fmt.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) 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> { let mut named_args = Set::new(); while !input.is_empty() { - if input.peek(Token![,]) && input.peek2(Ident::peek_any) && input.peek3(Token![=]) { - input.parse::()?; + input.parse::()?; + 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::()?; named_args.insert(ident); - } else { - input.parse::()?; } + scan_expr(input)?; } Ok(named_args) From 45e18f53dfc8ff742e16bff0b0818b615b317cf0 Mon Sep 17 00:00:00 2001 From: David Tolnay Date: Sat, 2 Nov 2024 09:45:25 -0700 Subject: [PATCH 4/4] Ignore enum_glob_use pedantic clippy lint warning: usage of wildcard import for enum variants --> impl/src/scan_expr.rs:1:12 | 1 | use self::{Action::*, Input::*}; | ^^^^^^^^^ help: try: `Action::{DecDepth, Finish, IncDepth, SetState}` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#enum_glob_use = note: `-W clippy::enum-glob-use` implied by `-W clippy::pedantic` = help: to override `-W clippy::pedantic` add `#[allow(clippy::enum_glob_use)]` warning: usage of wildcard import for enum variants --> impl/src/scan_expr.rs:1:23 | 1 | use self::{Action::*, Input::*}; | ^^^^^^^^ help: try: `Input::{CanBeginExpr, ConsumeAny, ConsumeBinOp, ConsumeBrace, ConsumeDelimiter, ConsumeIdent, ConsumeLifetime, ConsumeLiteral, ConsumeNestedBrace, Empty, ExpectPath, ExpectTurbofish, ExpectType, Keyword, Otherwise, Punct}` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#enum_glob_use --- impl/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/impl/src/lib.rs b/impl/src/lib.rs index 48075eb..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,