Source code

Revision control

Copy as Markdown

Other Tools

//! Implementations of [`fmt`]-like derive macros.
//!
//! [`fmt`]: std::fmt
#[cfg(feature = "debug")]
pub(crate) mod debug;
#[cfg(feature = "display")]
pub(crate) mod display;
mod parsing;
use proc_macro2::TokenStream;
use quote::{format_ident, quote, ToTokens};
use syn::{
ext::IdentExt as _,
parse::{Parse, ParseStream},
parse_quote,
punctuated::Punctuated,
spanned::Spanned as _,
token,
};
use crate::{
parsing::Expr,
utils::{attr, Either, Spanning},
};
/// Representation of a `bound` macro attribute, expressing additional trait bounds.
///
/// ```rust,ignore
/// #[<attribute>(bound(<where-predicates>))]
/// #[<attribute>(bounds(<where-predicates>))]
/// #[<attribute>(where(<where-predicates>))]
/// ```
#[derive(Debug, Default)]
struct BoundsAttribute(Punctuated<syn::WherePredicate, token::Comma>);
impl Parse for BoundsAttribute {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
Self::check_legacy_fmt(input)?;
let _ = input.parse::<syn::Path>().and_then(|p| {
if ["bound", "bounds", "where"]
.into_iter()
.any(|i| p.is_ident(i))
{
Ok(p)
} else {
Err(syn::Error::new(
p.span(),
"unknown attribute argument, expected `bound(...)`",
))
}
})?;
let content;
syn::parenthesized!(content in input);
content
.parse_terminated(syn::WherePredicate::parse, token::Comma)
.map(Self)
}
}
impl BoundsAttribute {
/// Errors in case legacy syntax is encountered: `bound = "..."`.
fn check_legacy_fmt(input: ParseStream<'_>) -> syn::Result<()> {
let fork = input.fork();
let path = fork
.parse::<syn::Path>()
.and_then(|path| fork.parse::<token::Eq>().map(|_| path));
match path {
Ok(path) if path.is_ident("bound") => fork
.parse::<syn::Lit>()
.ok()
.and_then(|lit| match lit {
syn::Lit::Str(s) => Some(s.value()),
_ => None,
})
.map_or(Ok(()), |bound| {
Err(syn::Error::new(
input.span(),
format!("legacy syntax, use `bound({bound})` instead"),
))
}),
Ok(_) | Err(_) => Ok(()),
}
}
}
/// Representation of a [`fmt`]-like attribute.
///
/// ```rust,ignore
/// #[<attribute>("<fmt-literal>", <fmt-args>)]
/// ```
///
/// [`fmt`]: std::fmt
#[derive(Debug)]
struct FmtAttribute {
/// Interpolation [`syn::LitStr`].
///
/// [`syn::LitStr`]: struct@syn::LitStr
lit: syn::LitStr,
/// Optional [`token::Comma`].
///
/// [`token::Comma`]: struct@token::Comma
comma: Option<token::Comma>,
/// Interpolation arguments.
args: Punctuated<FmtArgument, token::Comma>,
}
impl Parse for FmtAttribute {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
Self::check_legacy_fmt(input)?;
let mut parsed = Self {
lit: input.parse()?,
comma: input
.peek(token::Comma)
.then(|| input.parse())
.transpose()?,
args: input.parse_terminated(FmtArgument::parse, token::Comma)?,
};
parsed.args.pop_punct();
Ok(parsed)
}
}
impl attr::ParseMultiple for FmtAttribute {}
impl ToTokens for FmtAttribute {
fn to_tokens(&self, tokens: &mut TokenStream) {
self.lit.to_tokens(tokens);
self.comma.to_tokens(tokens);
self.args.to_tokens(tokens);
}
}
impl FmtAttribute {
/// Checks whether this [`FmtAttribute`] can be replaced with a transparent delegation (calling
/// a formatting trait directly instead of interpolation syntax).
///
/// If such transparent call is possible, then returns an [`Ident`] of the delegated trait and
/// the [`Expr`] to pass into the call, otherwise [`None`].
///
/// [`Ident`]: struct@syn::Ident
fn transparent_call(&self) -> Option<(Expr, syn::Ident)> {
// `FmtAttribute` is transparent when:
// (1) There is exactly one formatting parameter.
let lit = self.lit.value();
let param =
parsing::format(&lit).and_then(|(more, p)| more.is_empty().then_some(p))?;
// (2) And the formatting parameter doesn't contain any modifiers.
if param
.spec
.map(|s| {
s.align.is_some()
|| s.sign.is_some()
|| s.alternate.is_some()
|| s.zero_padding.is_some()
|| s.width.is_some()
|| s.precision.is_some()
|| !s.ty.is_trivial()
})
.unwrap_or_default()
{
return None;
}
let expr = match param.arg {
// (3) And either exactly one positional argument is specified.
Some(parsing::Argument::Integer(_)) | None => (self.args.len() == 1)
.then(|| self.args.first())
.flatten()
.map(|a| a.expr.clone()),
// (4) Or the formatting parameter's name refers to some outer binding.
Some(parsing::Argument::Identifier(name)) if self.args.is_empty() => {
Some(format_ident!("{name}").into())
}
// (5) Or exactly one named argument is specified for the formatting parameter's name.
Some(parsing::Argument::Identifier(name)) => (self.args.len() == 1)
.then(|| self.args.first())
.flatten()
.filter(|a| a.alias.as_ref().map(|a| a.0 == name).unwrap_or_default())
.map(|a| a.expr.clone()),
}?;
let trait_name = param
.spec
.map(|s| s.ty)
.unwrap_or(parsing::Type::Display)
.trait_name();
Some((expr, format_ident!("{trait_name}")))
}
/// Same as [`transparent_call()`], but additionally checks the returned [`Expr`] whether it's
/// one of the [`fmt_args_idents`] of the provided [`syn::Fields`], and makes it suitable for
/// passing directly into the transparent call of the delegated formatting trait.
///
/// [`fmt_args_idents`]: FieldsExt::fmt_args_idents
/// [`transparent_call()`]: FmtAttribute::transparent_call
fn transparent_call_on_fields(
&self,
fields: &syn::Fields,
) -> Option<(Expr, syn::Ident)> {
self.transparent_call().map(|(expr, trait_ident)| {
let expr = if let Some(field) = fields
.fmt_args_idents()
.find(|field| expr == *field || expr == field.unraw())
{
field.into()
} else {
parse_quote! { &(#expr) }
};
(expr, trait_ident)
})
}
/// Returns an [`Iterator`] over bounded [`syn::Type`]s (and correspondent trait names) by this
/// [`FmtAttribute`].
fn bounded_types<'a>(
&'a self,
fields: &'a syn::Fields,
) -> impl Iterator<Item = (&'a syn::Type, &'static str)> {
let placeholders = Placeholder::parse_fmt_string(&self.lit.value());
// We ignore unknown fields, as compiler will produce better error messages.
placeholders.into_iter().filter_map(move |placeholder| {
let name = match placeholder.arg {
Parameter::Named(name) => self
.args
.iter()
.find_map(|a| (a.alias()? == &name).then_some(&a.expr))
.map_or(Some(name), |expr| expr.ident().map(ToString::to_string))?,
Parameter::Positional(i) => self
.args
.iter()
.nth(i)
.and_then(|a| a.expr.ident().filter(|_| a.alias.is_none()))?
.to_string(),
};
let unnamed = name.strip_prefix('_').and_then(|s| s.parse().ok());
let ty = match (&fields, unnamed) {
(syn::Fields::Unnamed(f), Some(i)) => {
f.unnamed.iter().nth(i).map(|f| &f.ty)
}
(syn::Fields::Named(f), None) => f.named.iter().find_map(|f| {
f.ident
.as_ref()
.filter(|s| s.unraw() == name)
.map(|_| &f.ty)
}),
_ => None,
}?;
Some((ty, placeholder.trait_name))
})
}
#[cfg(feature = "display")]
/// Checks whether this [`FmtAttribute`] contains an argument with the provided `name` (either
/// in its direct [`FmtArgument`]s or inside [`Placeholder`]s).
fn contains_arg(&self, name: &str) -> bool {
self.placeholders_by_arg(name).next().is_some()
}
#[cfg(feature = "display")]
/// Returns an [`Iterator`] over [`Placeholder`]s using an argument with the provided `name`
/// (either in its direct [`FmtArgument`]s of this [`FmtAttribute`] or inside the
/// [`Placeholder`] itself).
fn placeholders_by_arg<'a>(
&'a self,
name: &'a str,
) -> impl Iterator<Item = Placeholder> + 'a {
let placeholders = Placeholder::parse_fmt_string(&self.lit.value());
placeholders.into_iter().filter(move |placeholder| {
match &placeholder.arg {
Parameter::Named(name) => self
.args
.iter()
.find_map(|a| (a.alias()? == name).then_some(&a.expr))
.map_or(Some(name.clone()), |expr| {
expr.ident().map(ToString::to_string)
}),
Parameter::Positional(i) => self
.args
.iter()
.nth(*i)
.and_then(|a| a.expr.ident().filter(|_| a.alias.is_none()))
.map(ToString::to_string),
}
.as_deref()
== Some(name)
})
}
/// Returns an [`Iterator`] over the additional formatting arguments doing the dereferencing
/// replacement in this [`FmtAttribute`] for those [`Placeholder`] representing the provided
/// [`syn::Fields`] and requiring it ([`fmt::Pointer`] ones).
///
/// [`fmt::Pointer`]: std::fmt::Pointer
fn additional_deref_args<'fmt: 'ret, 'fields: 'ret, 'ret>(
&'fmt self,
fields: &'fields syn::Fields,
) -> impl Iterator<Item = TokenStream> + 'ret {
let used_args = Placeholder::parse_fmt_string(&self.lit.value())
.into_iter()
.filter_map(|placeholder| match placeholder.arg {
Parameter::Named(name) if placeholder.trait_name == "Pointer" => {
Some(name)
}
_ => None,
})
.collect::<Vec<_>>();
fields.fmt_args_idents().filter_map(move |field_name| {
(used_args.iter().any(|arg| field_name.unraw() == arg)
&& !self.args.iter().any(|arg| {
arg.alias.as_ref().is_some_and(|(n, _)| n == &field_name)
}))
.then(|| quote! { #field_name = *#field_name })
})
}
/// Errors in case legacy syntax is encountered: `fmt = "...", (arg),*`.
fn check_legacy_fmt(input: ParseStream<'_>) -> syn::Result<()> {
let fork = input.fork();
let path = fork
.parse::<syn::Path>()
.and_then(|path| fork.parse::<token::Eq>().map(|_| path));
match path {
Ok(path) if path.is_ident("fmt") => (|| {
let args = fork
.parse_terminated(
<Either<syn::Lit, syn::Ident>>::parse,
token::Comma,
)
.ok()?
.into_iter()
.enumerate()
.filter_map(|(i, arg)| match arg {
Either::Left(syn::Lit::Str(str)) => Some(if i == 0 {
format!("\"{}\"", str.value())
} else {
str.value()
}),
Either::Right(ident) => Some(ident.to_string()),
_ => None,
})
.collect::<Vec<_>>();
(!args.is_empty()).then_some(args)
})()
.map_or(Ok(()), |fmt| {
Err(syn::Error::new(
input.span(),
format!(
"legacy syntax, remove `fmt =` and use `{}` instead",
fmt.join(", "),
),
))
}),
Ok(_) | Err(_) => Ok(()),
}
}
}
/// Representation of a [named parameter][1] (`identifier '=' expression`) in a [`FmtAttribute`].
///
#[derive(Debug)]
struct FmtArgument {
/// `identifier =` [`Ident`].
///
/// [`Ident`]: struct@syn::Ident
alias: Option<(syn::Ident, token::Eq)>,
/// `expression` [`Expr`].
expr: Expr,
}
impl FmtArgument {
/// Returns an `identifier` of the [named parameter][1].
///
fn alias(&self) -> Option<&syn::Ident> {
self.alias.as_ref().map(|(ident, _)| ident)
}
}
impl Parse for FmtArgument {
fn parse(input: ParseStream) -> syn::Result<Self> {
Ok(Self {
alias: (input.peek(syn::Ident) && input.peek2(token::Eq))
.then(|| Ok::<_, syn::Error>((input.parse()?, input.parse()?)))
.transpose()?,
expr: input.parse()?,
})
}
}
impl ToTokens for FmtArgument {
fn to_tokens(&self, tokens: &mut TokenStream) {
if let Some((ident, eq)) = &self.alias {
ident.to_tokens(tokens);
eq.to_tokens(tokens);
}
self.expr.to_tokens(tokens);
}
}
/// Representation of a [parameter][1] used in a [`Placeholder`].
///
#[derive(Debug, Eq, PartialEq)]
enum Parameter {
/// [Positional parameter][1].
///
Positional(usize),
/// [Named parameter][1].
///
Named(String),
}
impl<'a> From<parsing::Argument<'a>> for Parameter {
fn from(arg: parsing::Argument<'a>) -> Self {
match arg {
parsing::Argument::Integer(i) => Self::Positional(i),
parsing::Argument::Identifier(i) => Self::Named(i.to_owned()),
}
}
}
/// Representation of a formatting placeholder.
#[derive(Debug, Eq, PartialEq)]
struct Placeholder {
/// Formatting argument (either named or positional) to be used by this [`Placeholder`].
arg: Parameter,
/// Indicator whether this [`Placeholder`] has any formatting modifiers.
has_modifiers: bool,
/// Name of [`std::fmt`] trait to be used for rendering this [`Placeholder`].
trait_name: &'static str,
}
impl Placeholder {
/// Parses [`Placeholder`]s from the provided formatting string.
fn parse_fmt_string(s: &str) -> Vec<Self> {
let mut n = 0;
parsing::format_string(s)
.into_iter()
.flat_map(|f| f.formats)
.map(|format| {
let (maybe_arg, ty) = (
format.arg,
format.spec.map(|s| s.ty).unwrap_or(parsing::Type::Display),
);
let position = maybe_arg.map(Into::into).unwrap_or_else(|| {
// Assign "the next argument".
n += 1;
Parameter::Positional(n - 1)
});
Self {
arg: position,
has_modifiers: format
.spec
.map(|s| {
s.align.is_some()
|| s.sign.is_some()
|| s.alternate.is_some()
|| s.zero_padding.is_some()
|| s.width.is_some()
|| s.precision.is_some()
|| !s.ty.is_trivial()
})
.unwrap_or_default(),
trait_name: ty.trait_name(),
}
})
.collect()
}
}
/// Representation of a [`fmt::Display`]-like derive macro attributes placed on a container (struct
/// or enum variant).
///
/// ```rust,ignore
/// #[<attribute>("<fmt-literal>", <fmt-args>)]
/// #[<attribute>(bound(<where-predicates>))]
/// ```
///
/// `#[<attribute>(...)]` can be specified only once, while multiple `#[<attribute>(bound(...))]`
/// are allowed.
///
/// [`fmt::Display`]: std::fmt::Display
#[derive(Debug, Default)]
struct ContainerAttributes {
/// Interpolation [`FmtAttribute`].
fmt: Option<FmtAttribute>,
/// Addition trait bounds.
bounds: BoundsAttribute,
}
impl Parse for ContainerAttributes {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
// We do check `FmtAttribute::check_legacy_fmt` eagerly here, because `Either` will swallow
// any error of the `Either::Left` if the `Either::Right` succeeds.
FmtAttribute::check_legacy_fmt(input)?;
<Either<FmtAttribute, BoundsAttribute>>::parse(input).map(|v| match v {
Either::Left(fmt) => Self {
bounds: BoundsAttribute::default(),
fmt: Some(fmt),
},
Either::Right(bounds) => Self { bounds, fmt: None },
})
}
}
impl attr::ParseMultiple for ContainerAttributes {
fn merge_attrs(
prev: Spanning<Self>,
new: Spanning<Self>,
name: &syn::Ident,
) -> syn::Result<Spanning<Self>> {
let Spanning {
span: prev_span,
item: mut prev,
} = prev;
let Spanning {
span: new_span,
item: new,
} = new;
if new.fmt.and_then(|n| prev.fmt.replace(n)).is_some() {
return Err(syn::Error::new(
new_span,
format!("multiple `#[{name}(\"...\", ...)]` attributes aren't allowed"),
));
}
prev.bounds.0.extend(new.bounds.0);
Ok(Spanning::new(
prev,
prev_span.join(new_span).unwrap_or(prev_span),
))
}
}
/// Matches the provided `trait_name` to appropriate [`FmtAttribute`]'s argument name.
fn trait_name_to_attribute_name<T>(trait_name: T) -> &'static str
where
T: for<'a> PartialEq<&'a str>,
{
match () {
_ if trait_name == "Binary" => "binary",
_ if trait_name == "Debug" => "debug",
_ if trait_name == "Display" => "display",
_ if trait_name == "LowerExp" => "lower_exp",
_ if trait_name == "LowerHex" => "lower_hex",
_ if trait_name == "Octal" => "octal",
_ if trait_name == "Pointer" => "pointer",
_ if trait_name == "UpperExp" => "upper_exp",
_ if trait_name == "UpperHex" => "upper_hex",
_ => unimplemented!(),
}
}
/// Extension of a [`syn::Type`] and a [`syn::Path`] allowing to travers its type parameters.
trait ContainsGenericsExt {
/// Checks whether this definition contains any of the provided `type_params`.
fn contains_generics(&self, type_params: &[&syn::Ident]) -> bool;
}
impl ContainsGenericsExt for syn::Type {
fn contains_generics(&self, type_params: &[&syn::Ident]) -> bool {
if type_params.is_empty() {
return false;
}
match self {
Self::Path(syn::TypePath { qself, path }) => {
if let Some(qself) = qself {
if qself.ty.contains_generics(type_params) {
return true;
}
}
if let Some(ident) = path.get_ident() {
type_params.iter().any(|param| *param == ident)
} else {
path.contains_generics(type_params)
}
}
Self::Array(syn::TypeArray { elem, .. })
| Self::Group(syn::TypeGroup { elem, .. })
| Self::Paren(syn::TypeParen { elem, .. })
| Self::Ptr(syn::TypePtr { elem, .. })
| Self::Reference(syn::TypeReference { elem, .. })
| Self::Slice(syn::TypeSlice { elem, .. }) => {
elem.contains_generics(type_params)
}
Self::BareFn(syn::TypeBareFn { inputs, output, .. }) => {
inputs
.iter()
.any(|arg| arg.ty.contains_generics(type_params))
|| match output {
syn::ReturnType::Default => false,
syn::ReturnType::Type(_, ty) => {
ty.contains_generics(type_params)
}
}
}
Self::Tuple(syn::TypeTuple { elems, .. }) => {
elems.iter().any(|ty| ty.contains_generics(type_params))
}
Self::TraitObject(syn::TypeTraitObject { bounds, .. }) => {
bounds.iter().any(|bound| match bound {
syn::TypeParamBound::Trait(syn::TraitBound { path, .. }) => {
path.contains_generics(type_params)
}
syn::TypeParamBound::Lifetime(..)
| syn::TypeParamBound::Verbatim(..) => false,
_ => unimplemented!(
"syntax is not supported by `derive_more`, please report a bug",
),
})
}
Self::ImplTrait(..)
| Self::Infer(..)
| Self::Macro(..)
| Self::Never(..)
| Self::Verbatim(..) => false,
_ => unimplemented!(
"syntax is not supported by `derive_more`, please report a bug",
),
}
}
}
impl ContainsGenericsExt for syn::Path {
fn contains_generics(&self, type_params: &[&syn::Ident]) -> bool {
if type_params.is_empty() {
return false;
}
self.segments
.iter()
.enumerate()
.any(|(n, segment)| match &segment.arguments {
syn::PathArguments::None => {
// `TypeParam::AssocType` case.
(n == 0) && type_params.contains(&&segment.ident)
}
syn::PathArguments::AngleBracketed(
syn::AngleBracketedGenericArguments { args, .. },
) => args.iter().any(|generic| match generic {
syn::GenericArgument::Type(ty)
| syn::GenericArgument::AssocType(syn::AssocType { ty, .. }) => {
ty.contains_generics(type_params)
}
syn::GenericArgument::Lifetime(..)
| syn::GenericArgument::Const(..)
| syn::GenericArgument::AssocConst(..)
| syn::GenericArgument::Constraint(..) => false,
_ => unimplemented!(
"syntax is not supported by `derive_more`, please report a bug",
),
}),
syn::PathArguments::Parenthesized(
syn::ParenthesizedGenericArguments { inputs, output, .. },
) => {
inputs.iter().any(|ty| ty.contains_generics(type_params))
|| match output {
syn::ReturnType::Default => false,
syn::ReturnType::Type(_, ty) => {
ty.contains_generics(type_params)
}
}
}
})
}
}
/// Extension of [`syn::Fields`] providing helpers for a [`FmtAttribute`].
trait FieldsExt {
/// Returns an [`Iterator`] over [`syn::Ident`]s representing these [`syn::Fields`] in a
/// [`FmtAttribute`] as [`FmtArgument`]s or named [`Placeholder`]s.
///
/// [`syn::Ident`]: struct@syn::Ident
fn fmt_args_idents(&self) -> impl Iterator<Item = syn::Ident> + '_;
}
impl FieldsExt for syn::Fields {
fn fmt_args_idents(&self) -> impl Iterator<Item = syn::Ident> + '_ {
self.iter()
.enumerate()
.map(|(i, f)| f.ident.clone().unwrap_or_else(|| format_ident!("_{i}")))
}
}
#[cfg(test)]
mod fmt_attribute_spec {
use itertools::Itertools as _;
use quote::ToTokens;
use super::FmtAttribute;
fn assert<'a>(input: &'a str, parsed: impl AsRef<[&'a str]>) {
let parsed = parsed.as_ref();
let attr = syn::parse_str::<FmtAttribute>(&format!("\"\", {}", input)).unwrap();
let fmt_args = attr
.args
.into_iter()
.map(|arg| arg.into_token_stream().to_string())
.collect::<Vec<String>>();
fmt_args.iter().zip_eq(parsed).enumerate().for_each(
|(i, (found, expected))| {
assert_eq!(
*expected, found,
"Mismatch at index {i}\n\
Expected: {parsed:?}\n\
Found: {fmt_args:?}",
);
},
);
}
#[test]
fn cases() {
let cases = [
"ident",
"alias = ident",
"[a , b , c , d]",
"counter += 1",
"async { fut . await }",
"a < b",
"a > b",
"{ let x = (a , b) ; }",
"invoke (a , b)",
"foo as f64",
"| a , b | a + b",
"obj . k",
"for pat in expr { break pat ; }",
"if expr { true } else { false }",
"vector [2]",
"1",
"\"foo\"",
"loop { break i ; }",
"format ! (\"{}\" , q)",
"match n { Some (n) => { } , None => { } }",
"x . foo ::< T > (a , b)",
"x . foo ::< T < [T < T >; if a < b { 1 } else { 2 }] >, { a < b } > (a , b)",
"(a + b)",
"i32 :: MAX",
"1 .. 2",
"& a",
"[0u8 ; N]",
"(a , b , c , d)",
"< Ty as Trait > :: T",
"< Ty < Ty < T >, { a < b } > as Trait < T > > :: T",
];
assert("", []);
for i in 1..4 {
for permutations in cases.into_iter().permutations(i) {
let mut input = permutations.clone().join(",");
assert(&input, &permutations);
input.push(',');
assert(&input, &permutations);
}
}
}
}
#[cfg(test)]
mod placeholder_parse_fmt_string_spec {
use super::{Parameter, Placeholder};
#[test]
fn indicates_position_and_trait_name_for_each_fmt_placeholder() {
let fmt_string = "{},{:?},{{}},{{{1:0$}}}-{2:.1$x}{par:#?}{:width$}";
assert_eq!(
Placeholder::parse_fmt_string(fmt_string),
vec![
Placeholder {
arg: Parameter::Positional(0),
has_modifiers: false,
trait_name: "Display",
},
Placeholder {
arg: Parameter::Positional(1),
has_modifiers: false,
trait_name: "Debug",
},
Placeholder {
arg: Parameter::Positional(1),
has_modifiers: true,
trait_name: "Display",
},
Placeholder {
arg: Parameter::Positional(2),
has_modifiers: true,
trait_name: "LowerHex",
},
Placeholder {
arg: Parameter::Named("par".to_owned()),
has_modifiers: true,
trait_name: "Debug",
},
Placeholder {
arg: Parameter::Positional(2),
has_modifiers: true,
trait_name: "Display",
},
],
);
}
}