#![allow(missing_docs, reason = "Not all docs are written yet, see #3492.")]
#![cfg_attr(any(docsrs, docsrs_dep), feature(doc_cfg))]
#![expect(
clippy::result_large_err,
reason = "The error variant intentionally holds detailed diagnostic information."
)]
use proc_macro::TokenStream;
use quote::quote;
use unsynn::{TokenStream as TokenStream2, format_ident, *};
unsynn! {
keyword KDoc = "doc";
keyword KFakeVariadic = "fake_variadic";
struct AllTuplesParsed {
fake_variadic: Option<FakeVariadicAttr>,
macro_ident: Ident,
_comma1: Comma,
start: LiteralInteger,
_comma2: Comma,
end: LiteralInteger,
_comma3: Comma,
idents: CommaDelimitedVec<Ident>,
}
struct FakeVariadicAttr {
_hash: Pound,
_bracket: BracketGroupContaining::<(KDoc, ParenthesisGroupContaining::<KFakeVariadic>)>,
}
}
struct AllTuples {
fake_variadic: bool,
macro_ident: Ident,
start: usize,
end: usize,
idents: Vec<Ident>,
}
#[proc_macro]
pub fn all_tuples(input: TokenStream) -> TokenStream {
let input = match parse_all_tuples(input) {
Ok(input) => input,
Err(err) => {
return err;
}
};
let ident_tuples = build_ident_tuples(&input);
let macro_ident = &input.macro_ident;
let invocations = make_invocation_range(&input).map(|n| {
let ident_tuples = choose_ident_tuples(&input, &ident_tuples, n);
let attrs = attrs(&input, n);
quote! { #macro_ident!(#attrs #ident_tuples); }
});
TokenStream::from(quote! { #(#invocations)* })
}
#[proc_macro]
pub fn all_tuples_enumerated(input: TokenStream) -> TokenStream {
let input = match parse_all_tuples(input) {
Ok(input) => input,
Err(err) => {
return err;
}
};
let ident_tuples = build_ident_tuples_enumerated(&input);
let macro_ident = &input.macro_ident;
let invocations = make_invocation_range(&input).map(|n| {
let ident_tuples = choose_ident_tuples_enumerated(&input, &ident_tuples, n);
let attrs = attrs(&input, n);
quote! { #macro_ident!(#attrs #ident_tuples); }
});
TokenStream::from(quote! { #(#invocations)* })
}
#[proc_macro]
pub fn all_tuples_with_size(input: TokenStream) -> TokenStream {
let input = match parse_all_tuples(input) {
Ok(input) => input,
Err(err) => {
return err;
}
};
let ident_tuples = build_ident_tuples(&input);
let macro_ident = &input.macro_ident;
let invocations = make_invocation_range(&input).map(|n| {
let ident_tuples = choose_ident_tuples(&input, &ident_tuples, n);
let attrs = attrs(&input, n);
quote! { #macro_ident!(#n, #attrs #ident_tuples); }
});
TokenStream::from(quote! { #(#invocations)* })
}
fn parse_all_tuples(input: TokenStream) -> std::result::Result<AllTuples, TokenStream> {
let ts: TokenStream2 = input.into();
let mut iter = ts.to_token_iter();
let tuples = AllTuplesParsed::parse(&mut iter).map_err(pretty_print_error)?;
let start: usize = match tuples.start.value().try_into() {
Ok(start) => start,
Err(_) => {
return Err(span_error(
tuples.start,
"`start` should be in the range of 0..usize::MAX",
));
}
};
let end: usize = match tuples.end.value().try_into() {
Ok(end) => end,
Err(_) => {
return Err(span_error(
tuples.end,
"`end` should be in the range of 0..usize::MAX",
));
}
};
if end < start {
return Err(span_error(tuples.end, "`start` should <= `end`"));
}
Ok(AllTuples {
fake_variadic: tuples.fake_variadic.is_some(),
macro_ident: tuples.macro_ident,
start,
end,
idents: tuples.idents.iter().map(|i| i.value.clone()).collect(),
})
}
fn pretty_print_error(err: Error) -> TokenStream {
let span = err
.failed_at()
.map(|tt| tt.span())
.unwrap_or_else(Span::call_site);
let msg = match err.kind {
ErrorKind::Other { reason } => reason,
ErrorKind::UnexpectedToken => format!("expected {}", err.expected_type_name()),
_ => err.to_string(),
};
quote::quote_spanned! { span => compile_error!(#msg); }.into()
}
fn span_error(tokens: impl ToTokens, msg: &str) -> TokenStream {
let span = tokens
.to_token_iter()
.next()
.map(|tt| tt.span())
.unwrap_or_else(Span::call_site);
quote::quote_spanned! { span => compile_error!(#msg); }.into()
}
fn build_ident_tuples(input: &AllTuples) -> Vec<TokenStream2> {
(0..input.end)
.map(|i| {
let idents = input
.idents
.iter()
.map(|ident| format_ident!("{}{}", ident, i));
to_ident_tuple(idents, input.idents.len())
})
.collect()
}
fn build_ident_tuples_enumerated(input: &AllTuples) -> Vec<TokenStream2> {
(0..input.end)
.map(|i| {
let idents = input
.idents
.iter()
.map(|ident| format_ident!("{}{}", ident, i));
to_ident_tuple_enumerated(idents, i)
})
.collect()
}
fn make_invocation_range(input: &AllTuples) -> impl Iterator<Item = usize> {
let base = input.start..=input.end;
let extra: Vec<usize> = if input.fake_variadic && input.start > 1 {
vec![1]
} else {
vec![]
};
base.chain(extra)
}
fn choose_ident_tuples(input: &AllTuples, ident_tuples: &[TokenStream2], n: usize) -> TokenStream2 {
if input.fake_variadic && n == 1 {
let ident_tuple = to_ident_tuple(input.idents.iter().cloned(), input.idents.len());
quote! { #ident_tuple }
} else {
let ident_tuples = &ident_tuples[..n];
quote! { #(#ident_tuples),* }
}
}
fn choose_ident_tuples_enumerated(
input: &AllTuples,
ident_tuples: &[TokenStream2],
n: usize,
) -> TokenStream2 {
if input.fake_variadic && n == 1 {
let ident_tuple = to_ident_tuple_enumerated(input.idents.iter().cloned(), 0);
quote! { #ident_tuple }
} else {
let ident_tuples = &ident_tuples[..n];
quote! { #(#ident_tuples),* }
}
}
fn to_ident_tuple(idents: impl Iterator<Item = Ident>, generic_num: usize) -> TokenStream2 {
if generic_num < 2 {
quote! { #(#idents)* }
} else {
quote! { (#(#idents),*) }
}
}
fn to_ident_tuple_enumerated(idents: impl Iterator<Item = Ident>, idx: usize) -> TokenStream2 {
let idx = Literal::usize_unsuffixed(idx);
quote! { (#idx, #(#idents),*) }
}
fn attrs(input: &AllTuples, n: usize) -> TokenStream2 {
if !input.fake_variadic {
return TokenStream2::default();
}
match n {
0 => TokenStream2::default(),
n => {
let cfg = quote! { any(docsrs, docsrs_dep) };
if n == 1 {
let doc = Literal::string(&format!(
"This trait is implemented for tuple{s1} {range} item{s2} long.",
range = if input.start == input.end {
format!("exactly {}", input.start)
} else {
format!(
"{down}up to {up}",
down = if input.start != 0 {
format!("down to {} ", input.start)
} else {
"".to_string()
},
up = input.end
)
},
s1 = if input.end > input.start { "s" } else { "" },
s2 = if input.end >= input.start && input.end > 1 {
"s"
} else {
""
}
));
if input.start <= 1 && input.end >= 1 {
quote! {
#[cfg_attr(#cfg, doc(fake_variadic))]
#[cfg_attr(#cfg, doc = #doc)]
}
} else {
quote! {
#[cfg(#cfg)]
#[doc(fake_variadic)]
#[doc = #doc]
}
}
} else {
quote! { #[cfg_attr(#cfg, doc(hidden))] }
}
}
}
}