#, "?style=flat-square&logo=rust)](https://crates.io/crates/", env!("CARGO_PKG_NAME"), ")")]
#, "?style=flat-square&logo=docs.rs)](https://docs.rs/", env!("CARGO_PKG_NAME"), ")")]
#"]
#, "-blue?style=flat-square&logo=rust)")]
#, ")](https://github.com/nik-rev/", env!("CARGO_PKG_NAME"), ")")]
#![doc = concat!(env!("CARGO_PKG_NAME"), " = ", "\"", env!("CARGO_PKG_VERSION_MAJOR"), ".", env!("CARGO_PKG_VERSION_MINOR"), "\"")]
use proc_macro::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream, TokenTree};
#[proc_macro_attribute]
pub fn display(args: TokenStream, ts: TokenStream) -> TokenStream {
let mut compile_errors = TokenStream::new();
let mut args = args.into_iter();
let generate_doc_comments = match args.next() {
Some(TokenTree::Ident(ident)) if ident.to_string() == "doc" => {
if let Some(next) = args.next() {
compile_errors.extend(CompileError::new(next.span(), "unexpected token"));
}
true
}
Some(tt) => {
compile_errors.extend(CompileError::new(tt.span(), "unexpected token"));
false
}
None => false,
};
let mut output = TokenStream::new();
let mut ts = ts.into_iter().peekable();
loop {
let Some(tt) = ts.next() else {
return CompileError::new(Span::call_site(), "expected an `enum` item")
.into_iter()
.collect();
};
match tt {
TokenTree::Punct(punct) if punct == '#' => {
output.extend([TokenTree::Punct(punct)]);
output.extend(ts.next());
}
TokenTree::Ident(ident) if ident.to_string() == "enum" => {
output.extend([TokenTree::Ident(ident)]);
break;
}
tt => {
output.extend([tt]);
}
}
}
let enum_ident = match ts.next() {
Some(TokenTree::Ident(ident)) => ident,
_ => unreachable!("`enum` is always followed by an identifier"),
};
let generics = match ts.peek() {
Some(TokenTree::Punct(punct)) if *punct == '<' => {
let mut generics = TokenStream::new();
generics.extend(ts.next());
loop {
match ts.next() {
Some(TokenTree::Punct(punct)) if punct == '>' => {
generics.extend([TokenTree::Punct(punct)]);
break;
}
tt => {
generics.extend(tt);
}
}
}
generics
}
_ => TokenStream::new(),
};
let where_clause = match ts.peek() {
Some(TokenTree::Ident(ident)) if ident.to_string() == "where" => {
let mut where_clause = TokenStream::new();
where_clause.extend(ts.next());
loop {
match ts.peek() {
Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Brace => break,
_ => {
where_clause.extend(ts.next());
}
}
}
where_clause
}
_ => TokenStream::new(),
};
let mut enum_body = match ts.next() {
Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Brace => {
group.stream().into_iter().peekable()
}
_ => unreachable!("enum has braces"),
};
let mut variants = TokenStream::new();
let mut arms = TokenStream::new();
loop {
if enum_body.peek().is_none() {
break;
}
loop {
match enum_body.peek() {
Some(TokenTree::Punct(punct)) if *punct == '#' => {
variants.extend(enum_body.next());
variants.extend(enum_body.next());
}
_ => break,
}
}
let mut variant = TokenStream::new();
match enum_body.peek() {
Some(TokenTree::Ident(ident)) if ident.to_string() == "pub" => {
variant.extend(enum_body.next());
match enum_body.peek() {
Some(TokenTree::Group(group))
if group.delimiter() == Delimiter::Parenthesis =>
{
variant.extend(enum_body.next());
}
_ => (),
}
}
_ => (),
}
let variant_ident = match enum_body.next() {
Some(TokenTree::Ident(ident)) => {
variant.extend([TokenTree::Ident(ident.clone())]);
ident
}
_ => unreachable!("identifier must appear in this position"),
};
match enum_body.next() {
Some(TokenTree::Group(fields)) if fields.delimiter() == Delimiter::Parenthesis => {
variant.extend([TokenTree::Group(fields.clone())]);
let mut fields = fields.stream().into_iter().peekable();
let is_zero_variants = fields.peek().is_none();
let mut commas = 0;
while let Some(tt) = fields.next() {
match tt {
TokenTree::Punct(punct) if punct == ',' => {
if fields.peek().is_some() {
commas += 1;
}
}
_ => (),
}
}
let variant_count = if is_zero_variants { 0 } else { 1 + commas };
let destructure = (0..variant_count)
.flat_map(|i| {
[
TokenTree::Ident(Ident::new(&format!("_{i}"), Span::call_site())),
TokenTree::Punct(Punct::new(',', Spacing::Joint)),
]
})
.collect();
let destructure = TokenTree::Group(Group::new(Delimiter::Parenthesis, destructure));
match extract_eq_string(&mut enum_body, variant_ident.span()) {
Ok((string, stream)) => {
if generate_doc_comments {
variants.extend(doc_comment(&string.to_string()));
}
arms.extend(generate_arm(
&variant_ident.to_string(),
destructure,
string,
stream,
));
}
Err(compile_error) => {
arms.extend(generate_arm(
&variant_ident.to_string(),
destructure,
Literal::string(""),
TokenStream::new(),
));
compile_errors.extend(compile_error);
}
};
match enum_body.peek() {
Some(TokenTree::Punct(punct)) if *punct == ',' => {
variant.extend(enum_body.next());
}
_ => (),
}
}
Some(TokenTree::Group(fields)) if fields.delimiter() == Delimiter::Brace => {
variant.extend([TokenTree::Group(fields.clone())]);
let mut is_inside_type = false;
let mut fields = fields.stream().into_iter().peekable();
let mut destructure = TokenStream::new();
loop {
let current = fields.next();
match fields.peek() {
Some(TokenTree::Punct(punct)) if *punct == ':' && !is_inside_type => {
destructure.extend(current);
destructure.extend([TokenTree::Punct(Punct::new(',', Spacing::Joint))]);
is_inside_type = true;
}
Some(TokenTree::Punct(punct)) if *punct == ',' && is_inside_type => {
is_inside_type = false;
}
Some(_) => (),
None => break,
}
}
let destructure = TokenTree::Group(Group::new(Delimiter::Brace, destructure));
match extract_eq_string(&mut enum_body, variant_ident.span()) {
Ok((string, stream)) => {
if generate_doc_comments {
variants.extend(doc_comment(&string.to_string()));
}
arms.extend(generate_arm(
&variant_ident.to_string(),
destructure,
string,
stream,
));
}
Err(compile_error) => {
arms.extend(generate_arm(
&variant_ident.to_string(),
destructure,
Literal::string(""),
TokenStream::new(),
));
compile_errors.extend(compile_error);
}
};
match enum_body.peek() {
Some(TokenTree::Punct(punct)) if *punct == ',' => {
variant.extend(enum_body.next());
}
_ => (),
}
}
Some(TokenTree::Punct(punct)) if punct == '=' => {
match extract_string(&mut enum_body) {
Ok((string, stream)) => {
if generate_doc_comments {
variants.extend(doc_comment(&string.to_string()));
}
arms.extend(generate_arm(
&variant_ident.to_string(),
TokenTree::Group(Group::new(Delimiter::Brace, TokenStream::new())),
string,
stream,
));
}
Err(compile_error) => {
compile_errors.extend(compile_error);
arms.extend(generate_arm(
&variant_ident.to_string(),
TokenTree::Group(Group::new(Delimiter::Brace, TokenStream::new())),
Literal::string(""),
TokenStream::new(),
));
}
}
match enum_body.peek() {
Some(TokenTree::Punct(punct)) if *punct == ',' => {
variant.extend(enum_body.next());
}
_ => (),
}
}
Some(TokenTree::Punct(punct)) if punct == ',' => {
variant.extend([TokenTree::Punct(punct.clone())]);
compile_errors.extend(CompileError::new(
variant_ident.span(),
"expected this variant to have a string discriminant: `= \"...\"`",
));
arms.extend(generate_arm(
&variant_ident.to_string(),
TokenTree::Group(Group::new(Delimiter::Brace, TokenStream::new())),
Literal::string(""),
TokenStream::new(),
));
}
None => {
compile_errors.extend(CompileError::new(
variant_ident.span(),
"expected this variant to have a string discriminant: `= \"...\"`",
));
arms.extend(generate_arm(
&variant_ident.to_string(),
TokenTree::Group(Group::new(Delimiter::Brace, TokenStream::new())),
Literal::string(""),
TokenStream::new(),
));
break;
}
Some(_) => unreachable!("no other token is valid in this position"),
}
variants.extend(variant);
}
let original_enum = output
.into_iter()
.chain([TokenTree::Ident(enum_ident.clone())])
.chain(generics)
.chain(where_clause)
.chain([TokenTree::Group(Group::new(Delimiter::Brace, variants))]);
let display_impl = TokenStream::from_iter([
TokenTree::Ident(Ident::new("impl", Span::call_site())),
TokenTree::Punct(Punct::new(':', Spacing::Joint)),
TokenTree::Punct(Punct::new(':', Spacing::Joint)),
TokenTree::Ident(Ident::new("core", Span::call_site())),
TokenTree::Punct(Punct::new(':', Spacing::Joint)),
TokenTree::Punct(Punct::new(':', Spacing::Joint)),
TokenTree::Ident(Ident::new("fmt", Span::call_site())),
TokenTree::Punct(Punct::new(':', Spacing::Joint)),
TokenTree::Punct(Punct::new(':', Spacing::Joint)),
TokenTree::Ident(Ident::new("Display", Span::call_site())),
TokenTree::Ident(Ident::new("for", Span::call_site())),
TokenTree::Ident(enum_ident),
TokenTree::Group(Group::new(
Delimiter::Brace,
TokenStream::from_iter([
TokenTree::Ident(Ident::new("fn", Span::call_site())),
TokenTree::Ident(Ident::new("fmt", Span::call_site())),
TokenTree::Group(Group::new(
Delimiter::Parenthesis,
TokenStream::from_iter([
TokenTree::Punct(Punct::new('&', Spacing::Joint)),
TokenTree::Ident(Ident::new("self", Span::call_site())),
TokenTree::Punct(Punct::new(',', Spacing::Joint)),
TokenTree::Ident(Ident::new("f", Span::call_site())),
TokenTree::Punct(Punct::new(':', Spacing::Joint)),
TokenTree::Punct(Punct::new('&', Spacing::Joint)),
TokenTree::Ident(Ident::new("mut", Span::call_site())),
TokenTree::Punct(Punct::new(':', Spacing::Joint)),
TokenTree::Punct(Punct::new(':', Spacing::Joint)),
TokenTree::Ident(Ident::new("core", Span::call_site())),
TokenTree::Punct(Punct::new(':', Spacing::Joint)),
TokenTree::Punct(Punct::new(':', Spacing::Joint)),
TokenTree::Ident(Ident::new("fmt", Span::call_site())),
TokenTree::Punct(Punct::new(':', Spacing::Joint)),
TokenTree::Punct(Punct::new(':', Spacing::Joint)),
TokenTree::Ident(Ident::new("Formatter", Span::call_site())),
]),
)),
TokenTree::Punct(Punct::new('-', Spacing::Joint)),
TokenTree::Punct(Punct::new('>', Spacing::Joint)),
TokenTree::Punct(Punct::new(':', Spacing::Joint)),
TokenTree::Punct(Punct::new(':', Spacing::Joint)),
TokenTree::Ident(Ident::new("core", Span::call_site())),
TokenTree::Punct(Punct::new(':', Spacing::Joint)),
TokenTree::Punct(Punct::new(':', Spacing::Joint)),
TokenTree::Ident(Ident::new("fmt", Span::call_site())),
TokenTree::Punct(Punct::new(':', Spacing::Joint)),
TokenTree::Punct(Punct::new(':', Spacing::Joint)),
TokenTree::Ident(Ident::new("Result", Span::call_site())),
TokenTree::Group(Group::new(
Delimiter::Brace,
TokenStream::from_iter([
TokenTree::Ident(Ident::new("match", Span::call_site())),
TokenTree::Ident(Ident::new("self", Span::call_site())),
TokenTree::Group(Group::new(Delimiter::Brace, arms)),
]),
)),
]),
)),
]);
original_enum
.chain(compile_errors)
.chain(display_impl)
.collect()
}
#[allow(clippy::result_large_err)]
fn extract_eq_string(
ts: &mut std::iter::Peekable<proc_macro::token_stream::IntoIter>,
variant_ident_span: Span,
) -> Result<(Literal, TokenStream), CompileError> {
match ts.next() {
Some(TokenTree::Punct(punct)) if punct == '=' => {
extract_string(ts)
}
_ => Err(CompileError::new(
variant_ident_span,
"expected this variant to have a string discriminant: `= \"...\"`",
)),
}
}
fn doc_comment(content: &str) -> [TokenTree; 2] {
[
TokenTree::Punct(Punct::new('#', Spacing::Joint)),
TokenTree::Group(Group::new(
Delimiter::Bracket,
TokenStream::from_iter([
TokenTree::Ident(Ident::new("doc", Span::call_site())),
TokenTree::Punct(Punct::new('=', Spacing::Joint)),
TokenTree::Literal(Literal::string(content)),
]),
)),
]
}
fn extract_string(
ts: &mut std::iter::Peekable<proc_macro::token_stream::IntoIter>,
) -> Result<(Literal, TokenStream), CompileError> {
let next = ts.next();
match next {
Some(TokenTree::Literal(string)) => {
Ok((string, TokenStream::new()))
}
Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Parenthesis => {
let mut stream = group.stream().into_iter();
match stream.next() {
Some(TokenTree::Literal(string)) => Ok((string, stream.collect())),
Some(tt) => Err(CompileError::new(tt.span(), "expected string literal")),
None => Err(CompileError::new(group.span(), "expected string literal")),
}
}
Some(tt) => Err(CompileError::new(tt.span(), "expected string literal")),
None => unreachable!("`=` is always followed by an expression"),
}
}
type DisplayArm = [TokenTree; 12];
fn generate_arm(
variant: &str,
destructure: TokenTree,
string: Literal,
stream: TokenStream,
) -> DisplayArm {
[
TokenTree::Ident(Ident::new("Self", Span::call_site())),
TokenTree::Punct(Punct::new(':', Spacing::Joint)),
TokenTree::Punct(Punct::new(':', Spacing::Joint)),
TokenTree::Ident(Ident::new(variant, Span::call_site())),
destructure,
TokenTree::Punct(Punct::new('=', Spacing::Joint)),
TokenTree::Punct(Punct::new('>', Spacing::Joint)),
TokenTree::Ident(Ident::new("f", Span::call_site())),
TokenTree::Punct(Punct::new('.', Spacing::Joint)),
TokenTree::Ident(Ident::new("write_fmt", Span::call_site())),
TokenTree::Group(Group::new(
Delimiter::Parenthesis,
TokenStream::from_iter([
TokenTree::Punct(Punct::new(':', Spacing::Joint)),
TokenTree::Punct(Punct::new(':', Spacing::Joint)),
TokenTree::Ident(Ident::new("core", Span::call_site())),
TokenTree::Punct(Punct::new(':', Spacing::Joint)),
TokenTree::Punct(Punct::new(':', Spacing::Joint)),
TokenTree::Ident(Ident::new("format_args", Span::call_site())),
TokenTree::Punct(Punct::new('!', Spacing::Joint)),
TokenTree::Group(Group::new(
Delimiter::Parenthesis,
[TokenTree::Literal(string)]
.into_iter()
.chain(stream)
.collect(),
)),
]),
)),
TokenTree::Punct(Punct::new(',', Spacing::Joint)),
]
}
struct CompileError {
pub span: Span,
pub message: String,
}
impl CompileError {
pub fn new(span: Span, message: impl AsRef<str>) -> Self {
Self {
span,
message: message.as_ref().to_string(),
}
}
}
impl IntoIterator for CompileError {
type Item = TokenTree;
type IntoIter = std::array::IntoIter<Self::Item, 3>;
fn into_iter(self) -> Self::IntoIter {
[
TokenTree::Ident(Ident::new("compile_error", self.span)),
TokenTree::Punct({
let mut punct = Punct::new('!', Spacing::Alone);
punct.set_span(self.span);
punct
}),
TokenTree::Group({
let mut group = Group::new(Delimiter::Brace, {
TokenStream::from_iter(vec![TokenTree::Literal({
let mut string = Literal::string(&self.message);
string.set_span(self.span);
string
})])
});
group.set_span(self.span);
group
}),
]
.into_iter()
}
}