diff --git a/Cargo.toml b/Cargo.toml index 96a87e8..e107eef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thiserror" -version = "1.0.66" +version = "1.0.67" authors = ["David Tolnay "] categories = ["rust-patterns"] description = "derive(Error)" @@ -12,7 +12,7 @@ repository = "https://github.com/dtolnay/thiserror" rust-version = "1.61" [dependencies] -thiserror-impl = { version = "=1.0.66", path = "impl" } +thiserror-impl = { version = "=1.0.67", path = "impl" } [dev-dependencies] anyhow = "1.0.73" diff --git a/impl/Cargo.toml b/impl/Cargo.toml index 5acbe8f..1b474cb 100644 --- a/impl/Cargo.toml +++ b/impl/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thiserror-impl" -version = "1.0.66" +version = "1.0.67" authors = ["David Tolnay "] description = "Implementation detail of the `thiserror` crate" edition = "2021" @@ -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/attr.rs b/impl/src/attr.rs index a3746b0..c28761c 100644 --- a/impl/src/attr.rs +++ b/impl/src/attr.rs @@ -142,6 +142,13 @@ fn parse_error_attribute<'a>(attrs: &mut Attrs<'a>, attr: &'a Attribute) -> Resu fn parse_token_expr(input: ParseStream, mut begin_expr: bool) -> Result { let mut tokens = Vec::new(); while !input.is_empty() { + if input.peek(token::Group) { + let group: TokenTree = input.parse()?; + tokens.push(group); + begin_expr = false; + continue; + } + if begin_expr && input.peek(Token![.]) { if input.peek2(Ident) { input.parse::()?; diff --git a/impl/src/fmt.rs b/impl/src/fmt.rs index b38b7bf..153a423 100644 --- a/impl/src/fmt.rs +++ b/impl/src/fmt.rs @@ -1,11 +1,13 @@ use crate::ast::Field; use crate::attr::{Display, Trait}; -use proc_macro2::TokenTree; -use quote::{format_ident, quote_spanned}; +use crate::scan_expr; +use proc_macro2::{TokenStream, TokenTree}; +use quote::{format_ident, quote, quote_spanned}; use std::collections::{BTreeSet as Set, HashMap as Map}; use syn::ext::IdentExt; +use syn::parse::discouraged::Speculative; use syn::parse::{ParseStream, Parser}; -use syn::{Ident, Index, LitStr, Member, Result, Token}; +use syn::{Expr, Ident, Index, LitStr, Member, Result, Token}; impl Display<'_> { // Transform `"error {var}"` to `"error {}", var`. @@ -93,11 +95,6 @@ impl Display<'_> { if formatvar.to_string().starts_with("r#") { formatvar = format_ident!("r_{}", 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 - formatvar = format_ident!("field_{}", formatvar); - } out += &formatvar.to_string(); if !named_args.insert(formatvar.clone()) { // Already specified in the format argument list. @@ -122,7 +119,50 @@ impl Display<'_> { } } +#[allow(clippy::unnecessary_wraps)] fn explicit_named_args(input: ParseStream) -> Result> { + let ahead = input.fork(); + if let Ok(set) = try_explicit_named_args(&ahead) { + input.advance_to(&ahead); + return Ok(set); + } + + let ahead = input.fork(); + if let Ok(set) = fallback_explicit_named_args(&ahead) { + input.advance_to(&ahead); + return Ok(set); + } + + input.parse::().unwrap(); + Ok(Set::new()) +} + +fn try_explicit_named_args(input: ParseStream) -> Result> { + let scan_expr = if is_syn_full() { + |input: ParseStream| input.parse::().map(drop) + } else { + scan_expr::scan_expr + }; + + let mut named_args = Set::new(); + + while !input.is_empty() { + 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); + } + scan_expr(input)?; + } + + Ok(named_args) +} + +fn fallback_explicit_named_args(input: ParseStream) -> Result> { let mut named_args = Set::new(); while !input.is_empty() { @@ -131,14 +171,30 @@ fn explicit_named_args(input: ParseStream) -> Result> { let ident = input.call(Ident::parse_any)?; input.parse::()?; named_args.insert(ident); - } else { - input.parse::()?; } } Ok(named_args) } +fn is_syn_full() -> bool { + // Expr::Block contains syn::Block which contains Vec. In the + // current version of Syn, syn::Stmt is exhaustive and could only plausibly + // represent `trait Trait {}` in Stmt::Item which contains syn::Item. Most + // of the point of syn's non-"full" mode is to avoid compiling Item and the + // entire expansive syntax tree it comprises. So the following expression + // being parsed to Expr::Block is a reliable indication that "full" is + // enabled. + let test = quote!({ + trait Trait {} + }); + match syn::parse2(test) { + Ok(Expr::Verbatim(_)) | Err(_) => false, + Ok(Expr::Block(_)) => true, + Ok(_) => unreachable!(), + } +} + fn take_int(read: &mut &str) -> String { let mut int = String::new(); for (i, ch) in read.char_indices() { diff --git a/impl/src/generics.rs b/impl/src/generics.rs index 95592a7..254c2ed 100644 --- a/impl/src/generics.rs +++ b/impl/src/generics.rs @@ -57,7 +57,6 @@ impl InferredBounds { } } - #[allow(clippy::type_repetition_in_bounds, clippy::trait_duplication_in_bounds)] // clippy bug: https://github.com/rust-lang/rust-clippy/issues/8771 pub fn insert(&mut self, ty: impl ToTokens, bound: impl ToTokens) { let ty = ty.to_token_stream(); let bound = bound.to_token_stream(); 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::>()?.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")); + } +} diff --git a/src/lib.rs b/src/lib.rs index b7afdb7..8437394 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -258,7 +258,7 @@ //! //! [`anyhow`]: https://github.com/dtolnay/anyhow -#![doc(html_root_url = "https://docs.rs/thiserror/1.0.66")] +#![doc(html_root_url = "https://docs.rs/thiserror/1.0.67")] #![allow( clippy::module_name_repetitions, clippy::needless_lifetimes, diff --git a/tests/test_expr.rs b/tests/test_expr.rs index c5e3b4b..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 @@ -50,6 +51,7 @@ pub enum RustupError { }, } +#[track_caller] fn assert(expected: &str, value: T) { assert_eq!(expected, value.to_string()); } @@ -86,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("..."), + }, + ); +}