| 1 | //! Implementations of [`fmt`]-like derive macros. |
| 2 | //! |
| 3 | //! [`fmt`]: std::fmt |
| 4 | |
| 5 | #[cfg (feature = "debug" )] |
| 6 | pub(crate) mod debug; |
| 7 | #[cfg (feature = "display" )] |
| 8 | pub(crate) mod display; |
| 9 | mod parsing; |
| 10 | |
| 11 | use proc_macro2::TokenStream; |
| 12 | use quote::{format_ident, quote, ToTokens}; |
| 13 | use syn::{ |
| 14 | ext::IdentExt as _, |
| 15 | parse::{Parse, ParseStream}, |
| 16 | parse_quote, |
| 17 | punctuated::Punctuated, |
| 18 | spanned::Spanned as _, |
| 19 | token, |
| 20 | }; |
| 21 | |
| 22 | use crate::{ |
| 23 | parsing::Expr, |
| 24 | utils::{attr, Either, Spanning}, |
| 25 | }; |
| 26 | |
| 27 | /// Representation of a `bound` macro attribute, expressing additional trait bounds. |
| 28 | /// |
| 29 | /// ```rust,ignore |
| 30 | /// #[<attribute>(bound(<where-predicates>))] |
| 31 | /// #[<attribute>(bounds(<where-predicates>))] |
| 32 | /// #[<attribute>(where(<where-predicates>))] |
| 33 | /// ``` |
| 34 | #[derive (Debug, Default)] |
| 35 | struct BoundsAttribute(Punctuated<syn::WherePredicate, token::Comma>); |
| 36 | |
| 37 | impl Parse for BoundsAttribute { |
| 38 | fn parse(input: ParseStream<'_>) -> syn::Result<Self> { |
| 39 | Self::check_legacy_fmt(input)?; |
| 40 | |
| 41 | let _ = input.parse::<syn::Path>().and_then(|p| { |
| 42 | if ["bound" , "bounds" , "where" ] |
| 43 | .into_iter() |
| 44 | .any(|i| p.is_ident(i)) |
| 45 | { |
| 46 | Ok(p) |
| 47 | } else { |
| 48 | Err(syn::Error::new( |
| 49 | p.span(), |
| 50 | "unknown attribute argument, expected `bound(...)`" , |
| 51 | )) |
| 52 | } |
| 53 | })?; |
| 54 | |
| 55 | let content; |
| 56 | syn::parenthesized!(content in input); |
| 57 | |
| 58 | content |
| 59 | .parse_terminated(syn::WherePredicate::parse, token::Comma) |
| 60 | .map(Self) |
| 61 | } |
| 62 | } |
| 63 | |
| 64 | impl BoundsAttribute { |
| 65 | /// Errors in case legacy syntax is encountered: `bound = "..."`. |
| 66 | fn check_legacy_fmt(input: ParseStream<'_>) -> syn::Result<()> { |
| 67 | let fork = input.fork(); |
| 68 | |
| 69 | let path = fork |
| 70 | .parse::<syn::Path>() |
| 71 | .and_then(|path| fork.parse::<token::Eq>().map(|_| path)); |
| 72 | match path { |
| 73 | Ok(path) if path.is_ident("bound" ) => fork |
| 74 | .parse::<syn::Lit>() |
| 75 | .ok() |
| 76 | .and_then(|lit| match lit { |
| 77 | syn::Lit::Str(s) => Some(s.value()), |
| 78 | _ => None, |
| 79 | }) |
| 80 | .map_or(Ok(()), |bound| { |
| 81 | Err(syn::Error::new( |
| 82 | input.span(), |
| 83 | format!("legacy syntax, use `bound( {bound})` instead" ), |
| 84 | )) |
| 85 | }), |
| 86 | Ok(_) | Err(_) => Ok(()), |
| 87 | } |
| 88 | } |
| 89 | } |
| 90 | |
| 91 | /// Representation of a [`fmt`]-like attribute. |
| 92 | /// |
| 93 | /// ```rust,ignore |
| 94 | /// #[<attribute>("<fmt-literal>" , <fmt-args>)] |
| 95 | /// ``` |
| 96 | /// |
| 97 | /// [`fmt`]: std::fmt |
| 98 | #[derive (Debug)] |
| 99 | struct FmtAttribute { |
| 100 | /// Interpolation [`syn::LitStr`]. |
| 101 | /// |
| 102 | /// [`syn::LitStr`]: struct@syn::LitStr |
| 103 | lit: syn::LitStr, |
| 104 | |
| 105 | /// Optional [`token::Comma`]. |
| 106 | /// |
| 107 | /// [`token::Comma`]: struct@token::Comma |
| 108 | comma: Option<token::Comma>, |
| 109 | |
| 110 | /// Interpolation arguments. |
| 111 | args: Punctuated<FmtArgument, token::Comma>, |
| 112 | } |
| 113 | |
| 114 | impl Parse for FmtAttribute { |
| 115 | fn parse(input: ParseStream<'_>) -> syn::Result<Self> { |
| 116 | Self::check_legacy_fmt(input)?; |
| 117 | |
| 118 | let mut parsed: FmtAttribute = Self { |
| 119 | lit: input.parse()?, |
| 120 | comma: inputOption> |
| 121 | .peek(token:token::Comma) |
| 122 | .then(|| input.parse()) |
| 123 | .transpose()?, |
| 124 | args: input.parse_terminated(parser:FmtArgument::parse, separator:token::Comma)?, |
| 125 | }; |
| 126 | parsed.args.pop_punct(); |
| 127 | Ok(parsed) |
| 128 | } |
| 129 | } |
| 130 | |
| 131 | impl attr::ParseMultiple for FmtAttribute {} |
| 132 | |
| 133 | impl ToTokens for FmtAttribute { |
| 134 | fn to_tokens(&self, tokens: &mut TokenStream) { |
| 135 | self.lit.to_tokens(tokens); |
| 136 | self.comma.to_tokens(tokens); |
| 137 | self.args.to_tokens(tokens); |
| 138 | } |
| 139 | } |
| 140 | |
| 141 | impl FmtAttribute { |
| 142 | /// Checks whether this [`FmtAttribute`] can be replaced with a transparent delegation (calling |
| 143 | /// a formatting trait directly instead of interpolation syntax). |
| 144 | /// |
| 145 | /// If such transparent call is possible, then returns an [`Ident`] of the delegated trait and |
| 146 | /// the [`Expr`] to pass into the call, otherwise [`None`]. |
| 147 | /// |
| 148 | /// [`Ident`]: struct@syn::Ident |
| 149 | fn transparent_call(&self) -> Option<(Expr, syn::Ident)> { |
| 150 | // `FmtAttribute` is transparent when: |
| 151 | |
| 152 | // (1) There is exactly one formatting parameter. |
| 153 | let lit = self.lit.value(); |
| 154 | let param = |
| 155 | parsing::format(&lit).and_then(|(more, p)| more.is_empty().then_some(p))?; |
| 156 | |
| 157 | // (2) And the formatting parameter doesn't contain any modifiers. |
| 158 | if param |
| 159 | .spec |
| 160 | .map(|s| { |
| 161 | s.align.is_some() |
| 162 | || s.sign.is_some() |
| 163 | || s.alternate.is_some() |
| 164 | || s.zero_padding.is_some() |
| 165 | || s.width.is_some() |
| 166 | || s.precision.is_some() |
| 167 | || !s.ty.is_trivial() |
| 168 | }) |
| 169 | .unwrap_or_default() |
| 170 | { |
| 171 | return None; |
| 172 | } |
| 173 | |
| 174 | let expr = match param.arg { |
| 175 | // (3) And either exactly one positional argument is specified. |
| 176 | Some(parsing::Argument::Integer(_)) | None => (self.args.len() == 1) |
| 177 | .then(|| self.args.first()) |
| 178 | .flatten() |
| 179 | .map(|a| a.expr.clone()), |
| 180 | |
| 181 | // (4) Or the formatting parameter's name refers to some outer binding. |
| 182 | Some(parsing::Argument::Identifier(name)) if self.args.is_empty() => { |
| 183 | Some(format_ident!(" {name}" ).into()) |
| 184 | } |
| 185 | |
| 186 | // (5) Or exactly one named argument is specified for the formatting parameter's name. |
| 187 | Some(parsing::Argument::Identifier(name)) => (self.args.len() == 1) |
| 188 | .then(|| self.args.first()) |
| 189 | .flatten() |
| 190 | .filter(|a| a.alias.as_ref().map(|a| a.0 == name).unwrap_or_default()) |
| 191 | .map(|a| a.expr.clone()), |
| 192 | }?; |
| 193 | |
| 194 | let trait_name = param |
| 195 | .spec |
| 196 | .map(|s| s.ty) |
| 197 | .unwrap_or(parsing::Type::Display) |
| 198 | .trait_name(); |
| 199 | |
| 200 | Some((expr, format_ident!(" {trait_name}" ))) |
| 201 | } |
| 202 | |
| 203 | /// Same as [`transparent_call()`], but additionally checks the returned [`Expr`] whether it's |
| 204 | /// one of the [`fmt_args_idents`] of the provided [`syn::Fields`], and makes it suitable for |
| 205 | /// passing directly into the transparent call of the delegated formatting trait. |
| 206 | /// |
| 207 | /// [`fmt_args_idents`]: FieldsExt::fmt_args_idents |
| 208 | /// [`transparent_call()`]: FmtAttribute::transparent_call |
| 209 | fn transparent_call_on_fields( |
| 210 | &self, |
| 211 | fields: &syn::Fields, |
| 212 | ) -> Option<(Expr, syn::Ident)> { |
| 213 | self.transparent_call().map(|(expr, trait_ident)| { |
| 214 | let expr = if let Some(field) = fields |
| 215 | .fmt_args_idents() |
| 216 | .find(|field| expr == *field || expr == field.unraw()) |
| 217 | { |
| 218 | field.into() |
| 219 | } else { |
| 220 | parse_quote! { &(#expr) } |
| 221 | }; |
| 222 | |
| 223 | (expr, trait_ident) |
| 224 | }) |
| 225 | } |
| 226 | |
| 227 | /// Returns an [`Iterator`] over bounded [`syn::Type`]s (and correspondent trait names) by this |
| 228 | /// [`FmtAttribute`]. |
| 229 | fn bounded_types<'a>( |
| 230 | &'a self, |
| 231 | fields: &'a syn::Fields, |
| 232 | ) -> impl Iterator<Item = (&'a syn::Type, &'static str)> { |
| 233 | let placeholders = Placeholder::parse_fmt_string(&self.lit.value()); |
| 234 | |
| 235 | // We ignore unknown fields, as compiler will produce better error messages. |
| 236 | placeholders.into_iter().filter_map(move |placeholder| { |
| 237 | let name = match placeholder.arg { |
| 238 | Parameter::Named(name) => self |
| 239 | .args |
| 240 | .iter() |
| 241 | .find_map(|a| (a.alias()? == &name).then_some(&a.expr)) |
| 242 | .map_or(Some(name), |expr| expr.ident().map(ToString::to_string))?, |
| 243 | Parameter::Positional(i) => self |
| 244 | .args |
| 245 | .iter() |
| 246 | .nth(i) |
| 247 | .and_then(|a| a.expr.ident().filter(|_| a.alias.is_none()))? |
| 248 | .to_string(), |
| 249 | }; |
| 250 | |
| 251 | let unnamed = name.strip_prefix('_' ).and_then(|s| s.parse().ok()); |
| 252 | let ty = match (&fields, unnamed) { |
| 253 | (syn::Fields::Unnamed(f), Some(i)) => { |
| 254 | f.unnamed.iter().nth(i).map(|f| &f.ty) |
| 255 | } |
| 256 | (syn::Fields::Named(f), None) => f.named.iter().find_map(|f| { |
| 257 | f.ident |
| 258 | .as_ref() |
| 259 | .filter(|s| s.unraw() == name) |
| 260 | .map(|_| &f.ty) |
| 261 | }), |
| 262 | _ => None, |
| 263 | }?; |
| 264 | |
| 265 | Some((ty, placeholder.trait_name)) |
| 266 | }) |
| 267 | } |
| 268 | |
| 269 | #[cfg (feature = "display" )] |
| 270 | /// Checks whether this [`FmtAttribute`] contains an argument with the provided `name` (either |
| 271 | /// in its direct [`FmtArgument`]s or inside [`Placeholder`]s). |
| 272 | fn contains_arg(&self, name: &str) -> bool { |
| 273 | self.placeholders_by_arg(name).next().is_some() |
| 274 | } |
| 275 | |
| 276 | #[cfg (feature = "display" )] |
| 277 | /// Returns an [`Iterator`] over [`Placeholder`]s using an argument with the provided `name` |
| 278 | /// (either in its direct [`FmtArgument`]s of this [`FmtAttribute`] or inside the |
| 279 | /// [`Placeholder`] itself). |
| 280 | fn placeholders_by_arg<'a>( |
| 281 | &'a self, |
| 282 | name: &'a str, |
| 283 | ) -> impl Iterator<Item = Placeholder> + 'a { |
| 284 | let placeholders = Placeholder::parse_fmt_string(&self.lit.value()); |
| 285 | |
| 286 | placeholders.into_iter().filter(move |placeholder| { |
| 287 | match &placeholder.arg { |
| 288 | Parameter::Named(name) => self |
| 289 | .args |
| 290 | .iter() |
| 291 | .find_map(|a| (a.alias()? == name).then_some(&a.expr)) |
| 292 | .map_or(Some(name.clone()), |expr| { |
| 293 | expr.ident().map(ToString::to_string) |
| 294 | }), |
| 295 | Parameter::Positional(i) => self |
| 296 | .args |
| 297 | .iter() |
| 298 | .nth(*i) |
| 299 | .and_then(|a| a.expr.ident().filter(|_| a.alias.is_none())) |
| 300 | .map(ToString::to_string), |
| 301 | } |
| 302 | .as_deref() |
| 303 | == Some(name) |
| 304 | }) |
| 305 | } |
| 306 | |
| 307 | /// Returns an [`Iterator`] over the additional formatting arguments doing the dereferencing |
| 308 | /// replacement in this [`FmtAttribute`] for those [`Placeholder`] representing the provided |
| 309 | /// [`syn::Fields`] and requiring it ([`fmt::Pointer`] ones). |
| 310 | /// |
| 311 | /// [`fmt::Pointer`]: std::fmt::Pointer |
| 312 | fn additional_deref_args<'fmt: 'ret, 'fields: 'ret, 'ret>( |
| 313 | &'fmt self, |
| 314 | fields: &'fields syn::Fields, |
| 315 | ) -> impl Iterator<Item = TokenStream> + 'ret { |
| 316 | let used_args = Placeholder::parse_fmt_string(&self.lit.value()) |
| 317 | .into_iter() |
| 318 | .filter_map(|placeholder| match placeholder.arg { |
| 319 | Parameter::Named(name) if placeholder.trait_name == "Pointer" => { |
| 320 | Some(name) |
| 321 | } |
| 322 | _ => None, |
| 323 | }) |
| 324 | .collect::<Vec<_>>(); |
| 325 | |
| 326 | fields.fmt_args_idents().filter_map(move |field_name| { |
| 327 | (used_args.iter().any(|arg| field_name.unraw() == arg) |
| 328 | && !self.args.iter().any(|arg| { |
| 329 | arg.alias.as_ref().is_some_and(|(n, _)| n == &field_name) |
| 330 | })) |
| 331 | .then(|| quote! { #field_name = *#field_name }) |
| 332 | }) |
| 333 | } |
| 334 | |
| 335 | /// Errors in case legacy syntax is encountered: `fmt = "...", (arg),*`. |
| 336 | fn check_legacy_fmt(input: ParseStream<'_>) -> syn::Result<()> { |
| 337 | let fork = input.fork(); |
| 338 | |
| 339 | let path = fork |
| 340 | .parse::<syn::Path>() |
| 341 | .and_then(|path| fork.parse::<token::Eq>().map(|_| path)); |
| 342 | match path { |
| 343 | Ok(path) if path.is_ident("fmt" ) => (|| { |
| 344 | let args = fork |
| 345 | .parse_terminated( |
| 346 | <Either<syn::Lit, syn::Ident>>::parse, |
| 347 | token::Comma, |
| 348 | ) |
| 349 | .ok()? |
| 350 | .into_iter() |
| 351 | .enumerate() |
| 352 | .filter_map(|(i, arg)| match arg { |
| 353 | Either::Left(syn::Lit::Str(str)) => Some(if i == 0 { |
| 354 | format!(" \"{}\"" , str.value()) |
| 355 | } else { |
| 356 | str.value() |
| 357 | }), |
| 358 | Either::Right(ident) => Some(ident.to_string()), |
| 359 | _ => None, |
| 360 | }) |
| 361 | .collect::<Vec<_>>(); |
| 362 | (!args.is_empty()).then_some(args) |
| 363 | })() |
| 364 | .map_or(Ok(()), |fmt| { |
| 365 | Err(syn::Error::new( |
| 366 | input.span(), |
| 367 | format!( |
| 368 | "legacy syntax, remove `fmt =` and use ` {}` instead" , |
| 369 | fmt.join(", " ), |
| 370 | ), |
| 371 | )) |
| 372 | }), |
| 373 | Ok(_) | Err(_) => Ok(()), |
| 374 | } |
| 375 | } |
| 376 | } |
| 377 | |
| 378 | /// Representation of a [named parameter][1] (`identifier '=' expression`) in a [`FmtAttribute`]. |
| 379 | /// |
| 380 | /// [1]: https://doc.rust-lang.org/stable/std/fmt/index.html#named-parameters |
| 381 | #[derive (Debug)] |
| 382 | struct FmtArgument { |
| 383 | /// `identifier =` [`Ident`]. |
| 384 | /// |
| 385 | /// [`Ident`]: struct@syn::Ident |
| 386 | alias: Option<(syn::Ident, token::Eq)>, |
| 387 | |
| 388 | /// `expression` [`Expr`]. |
| 389 | expr: Expr, |
| 390 | } |
| 391 | |
| 392 | impl FmtArgument { |
| 393 | /// Returns an `identifier` of the [named parameter][1]. |
| 394 | /// |
| 395 | /// [1]: https://doc.rust-lang.org/stable/std/fmt/index.html#named-parameters |
| 396 | fn alias(&self) -> Option<&syn::Ident> { |
| 397 | self.alias.as_ref().map(|(ident: &Ident, _)| ident) |
| 398 | } |
| 399 | } |
| 400 | |
| 401 | impl Parse for FmtArgument { |
| 402 | fn parse(input: ParseStream) -> syn::Result<Self> { |
| 403 | Ok(Self { |
| 404 | alias: (input.peek(token:syn::Ident) && input.peek2(token:token::Eq)) |
| 405 | .then(|| Ok::<_, syn::Error>((input.parse()?, input.parse()?))) |
| 406 | .transpose()?, |
| 407 | expr: input.parse()?, |
| 408 | }) |
| 409 | } |
| 410 | } |
| 411 | |
| 412 | impl ToTokens for FmtArgument { |
| 413 | fn to_tokens(&self, tokens: &mut TokenStream) { |
| 414 | if let Some((ident: &Ident, eq: &Eq)) = &self.alias { |
| 415 | ident.to_tokens(tokens); |
| 416 | eq.to_tokens(tokens); |
| 417 | } |
| 418 | self.expr.to_tokens(tokens); |
| 419 | } |
| 420 | } |
| 421 | |
| 422 | /// Representation of a [parameter][1] used in a [`Placeholder`]. |
| 423 | /// |
| 424 | /// [1]: https://doc.rust-lang.org/stable/std/fmt/index.html#formatting-parameters |
| 425 | #[derive (Debug, Eq, PartialEq)] |
| 426 | enum Parameter { |
| 427 | /// [Positional parameter][1]. |
| 428 | /// |
| 429 | /// [1]: https://doc.rust-lang.org/stable/std/fmt/index.html#positional-parameters |
| 430 | Positional(usize), |
| 431 | |
| 432 | /// [Named parameter][1]. |
| 433 | /// |
| 434 | /// [1]: https://doc.rust-lang.org/stable/std/fmt/index.html#named-parameters |
| 435 | Named(String), |
| 436 | } |
| 437 | |
| 438 | impl<'a> From<parsing::Argument<'a>> for Parameter { |
| 439 | fn from(arg: parsing::Argument<'a>) -> Self { |
| 440 | match arg { |
| 441 | parsing::Argument::Integer(i: usize) => Self::Positional(i), |
| 442 | parsing::Argument::Identifier(i: &str) => Self::Named(i.to_owned()), |
| 443 | } |
| 444 | } |
| 445 | } |
| 446 | |
| 447 | /// Representation of a formatting placeholder. |
| 448 | #[derive (Debug, Eq, PartialEq)] |
| 449 | struct Placeholder { |
| 450 | /// Formatting argument (either named or positional) to be used by this [`Placeholder`]. |
| 451 | arg: Parameter, |
| 452 | |
| 453 | /// Indicator whether this [`Placeholder`] has any formatting modifiers. |
| 454 | has_modifiers: bool, |
| 455 | |
| 456 | /// Name of [`std::fmt`] trait to be used for rendering this [`Placeholder`]. |
| 457 | trait_name: &'static str, |
| 458 | } |
| 459 | |
| 460 | impl Placeholder { |
| 461 | /// Parses [`Placeholder`]s from the provided formatting string. |
| 462 | fn parse_fmt_string(s: &str) -> Vec<Self> { |
| 463 | let mut n = 0; |
| 464 | parsing::format_string(s) |
| 465 | .into_iter() |
| 466 | .flat_map(|f| f.formats) |
| 467 | .map(|format| { |
| 468 | let (maybe_arg, ty) = ( |
| 469 | format.arg, |
| 470 | format.spec.map(|s| s.ty).unwrap_or(parsing::Type::Display), |
| 471 | ); |
| 472 | let position = maybe_arg.map(Into::into).unwrap_or_else(|| { |
| 473 | // Assign "the next argument". |
| 474 | // https://doc.rust-lang.org/stable/std/fmt/index.html#positional-parameters |
| 475 | n += 1; |
| 476 | Parameter::Positional(n - 1) |
| 477 | }); |
| 478 | |
| 479 | Self { |
| 480 | arg: position, |
| 481 | has_modifiers: format |
| 482 | .spec |
| 483 | .map(|s| { |
| 484 | s.align.is_some() |
| 485 | || s.sign.is_some() |
| 486 | || s.alternate.is_some() |
| 487 | || s.zero_padding.is_some() |
| 488 | || s.width.is_some() |
| 489 | || s.precision.is_some() |
| 490 | || !s.ty.is_trivial() |
| 491 | }) |
| 492 | .unwrap_or_default(), |
| 493 | trait_name: ty.trait_name(), |
| 494 | } |
| 495 | }) |
| 496 | .collect() |
| 497 | } |
| 498 | } |
| 499 | |
| 500 | /// Representation of a [`fmt::Display`]-like derive macro attributes placed on a container (struct |
| 501 | /// or enum variant). |
| 502 | /// |
| 503 | /// ```rust,ignore |
| 504 | /// #[<attribute>("<fmt-literal>" , <fmt-args>)] |
| 505 | /// #[<attribute>(bound(<where-predicates>))] |
| 506 | /// ``` |
| 507 | /// |
| 508 | /// `#[<attribute>(...)]` can be specified only once, while multiple `#[<attribute>(bound(...))]` |
| 509 | /// are allowed. |
| 510 | /// |
| 511 | /// [`fmt::Display`]: std::fmt::Display |
| 512 | #[derive (Debug, Default)] |
| 513 | struct ContainerAttributes { |
| 514 | /// Interpolation [`FmtAttribute`]. |
| 515 | fmt: Option<FmtAttribute>, |
| 516 | |
| 517 | /// Addition trait bounds. |
| 518 | bounds: BoundsAttribute, |
| 519 | } |
| 520 | |
| 521 | impl Parse for ContainerAttributes { |
| 522 | fn parse(input: ParseStream<'_>) -> syn::Result<Self> { |
| 523 | // We do check `FmtAttribute::check_legacy_fmt` eagerly here, because `Either` will swallow |
| 524 | // any error of the `Either::Left` if the `Either::Right` succeeds. |
| 525 | FmtAttribute::check_legacy_fmt(input)?; |
| 526 | <Either<FmtAttribute, BoundsAttribute>>::parse(input).map(|v: Either| match v { |
| 527 | Either::Left(fmt: FmtAttribute) => Self { |
| 528 | bounds: BoundsAttribute::default(), |
| 529 | fmt: Some(fmt), |
| 530 | }, |
| 531 | Either::Right(bounds: BoundsAttribute) => Self { bounds, fmt: None }, |
| 532 | }) |
| 533 | } |
| 534 | } |
| 535 | |
| 536 | impl attr::ParseMultiple for ContainerAttributes { |
| 537 | fn merge_attrs( |
| 538 | prev: Spanning<Self>, |
| 539 | new: Spanning<Self>, |
| 540 | name: &syn::Ident, |
| 541 | ) -> syn::Result<Spanning<Self>> { |
| 542 | let Spanning { |
| 543 | span: prev_span, |
| 544 | item: mut prev, |
| 545 | } = prev; |
| 546 | let Spanning { |
| 547 | span: new_span, |
| 548 | item: new, |
| 549 | } = new; |
| 550 | |
| 551 | if new.fmt.and_then(|n| prev.fmt.replace(n)).is_some() { |
| 552 | return Err(syn::Error::new( |
| 553 | new_span, |
| 554 | format!("multiple `#[ {name}( \"... \", ...)]` attributes aren't allowed" ), |
| 555 | )); |
| 556 | } |
| 557 | prev.bounds.0.extend(new.bounds.0); |
| 558 | |
| 559 | Ok(Spanning::new( |
| 560 | prev, |
| 561 | prev_span.join(new_span).unwrap_or(prev_span), |
| 562 | )) |
| 563 | } |
| 564 | } |
| 565 | |
| 566 | /// Matches the provided `trait_name` to appropriate [`FmtAttribute`]'s argument name. |
| 567 | fn trait_name_to_attribute_name<T>(trait_name: T) -> &'static str |
| 568 | where |
| 569 | T: for<'a> dynPartialEq<&'a str>, |
| 570 | { |
| 571 | match () { |
| 572 | _ if trait_name == "Binary" => "binary" , |
| 573 | _ if trait_name == "Debug" => "debug" , |
| 574 | _ if trait_name == "Display" => "display" , |
| 575 | _ if trait_name == "LowerExp" => "lower_exp" , |
| 576 | _ if trait_name == "LowerHex" => "lower_hex" , |
| 577 | _ if trait_name == "Octal" => "octal" , |
| 578 | _ if trait_name == "Pointer" => "pointer" , |
| 579 | _ if trait_name == "UpperExp" => "upper_exp" , |
| 580 | _ if trait_name == "UpperHex" => "upper_hex" , |
| 581 | _ => unimplemented!(), |
| 582 | } |
| 583 | } |
| 584 | |
| 585 | /// Extension of a [`syn::Type`] and a [`syn::Path`] allowing to travers its type parameters. |
| 586 | trait ContainsGenericsExt { |
| 587 | /// Checks whether this definition contains any of the provided `type_params`. |
| 588 | fn contains_generics(&self, type_params: &[&syn::Ident]) -> bool; |
| 589 | } |
| 590 | |
| 591 | impl ContainsGenericsExt for syn::Type { |
| 592 | fn contains_generics(&self, type_params: &[&syn::Ident]) -> bool { |
| 593 | if type_params.is_empty() { |
| 594 | return false; |
| 595 | } |
| 596 | match self { |
| 597 | Self::Path(syn::TypePath { qself, path }) => { |
| 598 | if let Some(qself) = qself { |
| 599 | if qself.ty.contains_generics(type_params) { |
| 600 | return true; |
| 601 | } |
| 602 | } |
| 603 | |
| 604 | if let Some(ident) = path.get_ident() { |
| 605 | type_params.iter().any(|param| *param == ident) |
| 606 | } else { |
| 607 | path.contains_generics(type_params) |
| 608 | } |
| 609 | } |
| 610 | |
| 611 | Self::Array(syn::TypeArray { elem, .. }) |
| 612 | | Self::Group(syn::TypeGroup { elem, .. }) |
| 613 | | Self::Paren(syn::TypeParen { elem, .. }) |
| 614 | | Self::Ptr(syn::TypePtr { elem, .. }) |
| 615 | | Self::Reference(syn::TypeReference { elem, .. }) |
| 616 | | Self::Slice(syn::TypeSlice { elem, .. }) => { |
| 617 | elem.contains_generics(type_params) |
| 618 | } |
| 619 | |
| 620 | Self::BareFn(syn::TypeBareFn { inputs, output, .. }) => { |
| 621 | inputs |
| 622 | .iter() |
| 623 | .any(|arg| arg.ty.contains_generics(type_params)) |
| 624 | || match output { |
| 625 | syn::ReturnType::Default => false, |
| 626 | syn::ReturnType::Type(_, ty) => { |
| 627 | ty.contains_generics(type_params) |
| 628 | } |
| 629 | } |
| 630 | } |
| 631 | |
| 632 | Self::Tuple(syn::TypeTuple { elems, .. }) => { |
| 633 | elems.iter().any(|ty| ty.contains_generics(type_params)) |
| 634 | } |
| 635 | |
| 636 | Self::TraitObject(syn::TypeTraitObject { bounds, .. }) => { |
| 637 | bounds.iter().any(|bound| match bound { |
| 638 | syn::TypeParamBound::Trait(syn::TraitBound { path, .. }) => { |
| 639 | path.contains_generics(type_params) |
| 640 | } |
| 641 | syn::TypeParamBound::Lifetime(..) |
| 642 | | syn::TypeParamBound::Verbatim(..) => false, |
| 643 | _ => unimplemented!( |
| 644 | "syntax is not supported by `derive_more`, please report a bug" , |
| 645 | ), |
| 646 | }) |
| 647 | } |
| 648 | |
| 649 | Self::ImplTrait(..) |
| 650 | | Self::Infer(..) |
| 651 | | Self::Macro(..) |
| 652 | | Self::Never(..) |
| 653 | | Self::Verbatim(..) => false, |
| 654 | _ => unimplemented!( |
| 655 | "syntax is not supported by `derive_more`, please report a bug" , |
| 656 | ), |
| 657 | } |
| 658 | } |
| 659 | } |
| 660 | |
| 661 | impl ContainsGenericsExt for syn::Path { |
| 662 | fn contains_generics(&self, type_params: &[&syn::Ident]) -> bool { |
| 663 | if type_params.is_empty() { |
| 664 | return false; |
| 665 | } |
| 666 | self.segments |
| 667 | .iter() |
| 668 | .enumerate() |
| 669 | .any(|(n, segment)| match &segment.arguments { |
| 670 | syn::PathArguments::None => { |
| 671 | // `TypeParam::AssocType` case. |
| 672 | (n == 0) && type_params.contains(&&segment.ident) |
| 673 | } |
| 674 | syn::PathArguments::AngleBracketed( |
| 675 | syn::AngleBracketedGenericArguments { args, .. }, |
| 676 | ) => args.iter().any(|generic| match generic { |
| 677 | syn::GenericArgument::Type(ty) |
| 678 | | syn::GenericArgument::AssocType(syn::AssocType { ty, .. }) => { |
| 679 | ty.contains_generics(type_params) |
| 680 | } |
| 681 | |
| 682 | syn::GenericArgument::Lifetime(..) |
| 683 | | syn::GenericArgument::Const(..) |
| 684 | | syn::GenericArgument::AssocConst(..) |
| 685 | | syn::GenericArgument::Constraint(..) => false, |
| 686 | _ => unimplemented!( |
| 687 | "syntax is not supported by `derive_more`, please report a bug" , |
| 688 | ), |
| 689 | }), |
| 690 | syn::PathArguments::Parenthesized( |
| 691 | syn::ParenthesizedGenericArguments { inputs, output, .. }, |
| 692 | ) => { |
| 693 | inputs.iter().any(|ty| ty.contains_generics(type_params)) |
| 694 | || match output { |
| 695 | syn::ReturnType::Default => false, |
| 696 | syn::ReturnType::Type(_, ty) => { |
| 697 | ty.contains_generics(type_params) |
| 698 | } |
| 699 | } |
| 700 | } |
| 701 | }) |
| 702 | } |
| 703 | } |
| 704 | |
| 705 | /// Extension of [`syn::Fields`] providing helpers for a [`FmtAttribute`]. |
| 706 | trait FieldsExt { |
| 707 | /// Returns an [`Iterator`] over [`syn::Ident`]s representing these [`syn::Fields`] in a |
| 708 | /// [`FmtAttribute`] as [`FmtArgument`]s or named [`Placeholder`]s. |
| 709 | /// |
| 710 | /// [`syn::Ident`]: struct@syn::Ident |
| 711 | fn fmt_args_idents(&self) -> impl Iterator<Item = syn::Ident> + '_; |
| 712 | } |
| 713 | |
| 714 | impl FieldsExt for syn::Fields { |
| 715 | fn fmt_args_idents(&self) -> impl Iterator<Item = syn::Ident> + '_ { |
| 716 | self.iter() |
| 717 | .enumerate() |
| 718 | .map(|(i: usize, f: &Field)| f.ident.clone().unwrap_or_else(|| format_ident!("_ {i}" ))) |
| 719 | } |
| 720 | } |
| 721 | |
| 722 | #[cfg (test)] |
| 723 | mod fmt_attribute_spec { |
| 724 | use itertools::Itertools as _; |
| 725 | use quote::ToTokens; |
| 726 | |
| 727 | use super::FmtAttribute; |
| 728 | |
| 729 | fn assert<'a>(input: &'a str, parsed: impl AsRef<[&'a str]>) { |
| 730 | let parsed = parsed.as_ref(); |
| 731 | let attr = syn::parse_str::<FmtAttribute>(&format!(" \"\", {}" , input)).unwrap(); |
| 732 | let fmt_args = attr |
| 733 | .args |
| 734 | .into_iter() |
| 735 | .map(|arg| arg.into_token_stream().to_string()) |
| 736 | .collect::<Vec<String>>(); |
| 737 | fmt_args.iter().zip_eq(parsed).enumerate().for_each( |
| 738 | |(i, (found, expected))| { |
| 739 | assert_eq!( |
| 740 | *expected, found, |
| 741 | "Mismatch at index {i} \n\ |
| 742 | Expected: {parsed:?} \n\ |
| 743 | Found: {fmt_args:?}" , |
| 744 | ); |
| 745 | }, |
| 746 | ); |
| 747 | } |
| 748 | |
| 749 | #[test ] |
| 750 | fn cases() { |
| 751 | let cases = [ |
| 752 | "ident" , |
| 753 | "alias = ident" , |
| 754 | "[a , b , c , d]" , |
| 755 | "counter += 1" , |
| 756 | "async { fut . await }" , |
| 757 | "a < b" , |
| 758 | "a > b" , |
| 759 | "{ let x = (a , b) ; }" , |
| 760 | "invoke (a , b)" , |
| 761 | "foo as f64" , |
| 762 | "| a , b | a + b" , |
| 763 | "obj . k" , |
| 764 | "for pat in expr { break pat ; }" , |
| 765 | "if expr { true } else { false }" , |
| 766 | "vector [2]" , |
| 767 | "1" , |
| 768 | " \"foo \"" , |
| 769 | "loop { break i ; }" , |
| 770 | "format ! ( \"{} \" , q)" , |
| 771 | "match n { Some (n) => { } , None => { } }" , |
| 772 | "x . foo ::< T > (a , b)" , |
| 773 | "x . foo ::< T < [T < T >; if a < b { 1 } else { 2 }] >, { a < b } > (a , b)" , |
| 774 | "(a + b)" , |
| 775 | "i32 :: MAX" , |
| 776 | "1 .. 2" , |
| 777 | "& a" , |
| 778 | "[0u8 ; N]" , |
| 779 | "(a , b , c , d)" , |
| 780 | "< Ty as Trait > :: T" , |
| 781 | "< Ty < Ty < T >, { a < b } > as Trait < T > > :: T" , |
| 782 | ]; |
| 783 | |
| 784 | assert("" , []); |
| 785 | for i in 1..4 { |
| 786 | for permutations in cases.into_iter().permutations(i) { |
| 787 | let mut input = permutations.clone().join("," ); |
| 788 | assert(&input, &permutations); |
| 789 | input.push(',' ); |
| 790 | assert(&input, &permutations); |
| 791 | } |
| 792 | } |
| 793 | } |
| 794 | } |
| 795 | |
| 796 | #[cfg (test)] |
| 797 | mod placeholder_parse_fmt_string_spec { |
| 798 | use super::{Parameter, Placeholder}; |
| 799 | |
| 800 | #[test ] |
| 801 | fn indicates_position_and_trait_name_for_each_fmt_placeholder() { |
| 802 | let fmt_string = "{},{:?},{{}},{{{1:0$}}}-{2:.1$x}{par:#?}{:width$}" ; |
| 803 | assert_eq!( |
| 804 | Placeholder::parse_fmt_string(fmt_string), |
| 805 | vec![ |
| 806 | Placeholder { |
| 807 | arg: Parameter::Positional(0), |
| 808 | has_modifiers: false, |
| 809 | trait_name: "Display" , |
| 810 | }, |
| 811 | Placeholder { |
| 812 | arg: Parameter::Positional(1), |
| 813 | has_modifiers: false, |
| 814 | trait_name: "Debug" , |
| 815 | }, |
| 816 | Placeholder { |
| 817 | arg: Parameter::Positional(1), |
| 818 | has_modifiers: true, |
| 819 | trait_name: "Display" , |
| 820 | }, |
| 821 | Placeholder { |
| 822 | arg: Parameter::Positional(2), |
| 823 | has_modifiers: true, |
| 824 | trait_name: "LowerHex" , |
| 825 | }, |
| 826 | Placeholder { |
| 827 | arg: Parameter::Named("par" .to_owned()), |
| 828 | has_modifiers: true, |
| 829 | trait_name: "Debug" , |
| 830 | }, |
| 831 | Placeholder { |
| 832 | arg: Parameter::Positional(2), |
| 833 | has_modifiers: true, |
| 834 | trait_name: "Display" , |
| 835 | }, |
| 836 | ], |
| 837 | ); |
| 838 | } |
| 839 | } |
| 840 | |