zutil_cloned/
lib.rs

1//! Macros for `zutil-cloned`
2
3// Features
4#![feature(if_let_guard, try_blocks)]
5
6// Imports
7use {
8	core::iter,
9	proc_macro::TokenStream,
10	quote::ToTokens,
11	syn::{punctuated::Punctuated, spanned::Spanned},
12};
13
14#[proc_macro_attribute]
15pub fn cloned(attr: TokenStream, input: TokenStream) -> TokenStream {
16	let attrs = syn::parse_macro_input!(attr as Attrs);
17	let mut input = syn::parse_macro_input!(input as Input);
18
19	let clones = attrs
20		.clones
21		.into_iter()
22		.map(|attr| {
23			let ident = attr.ident;
24			let expr = match attr.expr {
25				Some(expr) => expr,
26				None => self::ident_to_expr(ident.clone()),
27			};
28			syn::parse_quote! {
29				let #ident = #expr.clone();
30			}
31		})
32		.collect::<Vec<syn::Stmt>>();
33
34	// Wraps the expression in the clones
35	let wrap_expr = |expr: &mut syn::Expr, trailing_semi: Option<syn::Token![;]>| {
36		*expr = syn::parse_quote_spanned! {expr.span() => {
37			#![allow(clippy::semicolon_outside_block)]
38			#![allow(clippy::semicolon_if_nothing_returned)]
39
40			#( #clones )*
41			#expr
42			#trailing_semi
43		}};
44	};
45
46	// Find the expression to replace
47	match &mut input {
48		Input::Stmt { stmt } => match stmt {
49			syn::Stmt::Local(local) => match &mut local.init {
50				// Note: this expression is an initializer, so it never needs the trailing semi
51				Some(init) => wrap_expr(&mut init.expr, None),
52				None => self::cannot_attach("uninitialized let binding"),
53			},
54			syn::Stmt::Item(_) => self::cannot_attach("item"),
55
56			// Statement expressions also just carry their previous trailing semicolon
57			syn::Stmt::Expr(expr, trailing_semi) => wrap_expr(expr, *trailing_semi),
58			syn::Stmt::Macro(_) => self::cannot_attach("macro call"),
59		},
60
61		// Normal expressions are the only place we need the user to tell us whether to use a semi or not
62		Input::Expr { expr } => wrap_expr(expr, attrs.semi),
63	};
64
65	// Then output it.
66	let output = match input {
67		Input::Stmt { stmt } => stmt.to_token_stream(),
68		Input::Expr { expr } => expr.to_token_stream(),
69	};
70
71	TokenStream::from(output)
72}
73
74
75/// Input
76enum Input {
77	Stmt { stmt: syn::Stmt },
78	Expr { expr: syn::Expr },
79}
80
81impl syn::parse::Parse for Input {
82	fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
83		// Try to parse ourselves as a statement first
84		// TODO: The documentation warns against this specifically, but how can we do better?
85		let is_stmt = input.fork().parse::<syn::Stmt>().is_ok();
86		if is_stmt {
87			let stmt = input.parse::<syn::Stmt>()?;
88			return Ok(Self::Stmt { stmt });
89		}
90
91		// Otherwise, parse an expression
92		let expr = input.parse::<syn::Expr>()?;
93
94		// Allow trailing commas so we can parse function arguments
95		let _trailing_comma = input.parse::<Option<syn::Token![,]>>()?;
96
97		Ok(Self::Expr { expr })
98	}
99}
100
101/// Attributes
102struct Attrs {
103	/// Whether to semi a semicolon at the end
104	semi: Option<syn::Token![;]>,
105
106	// All of the clones
107	clones: Vec<Attr>,
108}
109
110impl syn::parse::Parse for Attrs {
111	fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
112		let clones = Punctuated::<_, syn::Token![,]>::parse_terminated_with(input, |input| {
113			let attr = input.parse::<Attr>()?;
114			let semi = input.parse::<Option<syn::Token![;]>>()?;
115
116			Ok((attr, semi))
117		})?;
118
119		let mut semi = None::<syn::Token![;]>;
120		let clones = clones
121			.into_iter()
122			.map(|(attr, new_semi)| {
123				if let Some(new_semi) = new_semi {
124					match semi {
125						Some(old_semi) =>
126							return Err(syn::Error::new(
127								old_semi.span,
128								"Unexpected `;`, only allowed to be trailing",
129							)),
130						None => semi = Some(new_semi),
131					}
132				}
133
134				Ok(attr)
135			})
136			.collect::<Result<_, _>>()?;
137
138		Ok(Self { semi, clones })
139	}
140}
141
142/// Attribute
143struct Attr {
144	ident: syn::Ident,
145	expr:  Option<syn::Expr>,
146}
147
148impl syn::parse::Parse for Attr {
149	fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
150		let ident = input.parse()?;
151		let expr = match input.parse::<Option<syn::Token![=]>>()? {
152			Some(_eq) => {
153				let expr = input.parse::<syn::Expr>()?;
154				Some(expr)
155			},
156			None => None,
157		};
158
159		Ok(Self { ident, expr })
160	}
161}
162
163/// Converts an identifier to an expression
164fn ident_to_expr(ident: syn::Ident) -> syn::Expr {
165	syn::Expr::Path(syn::ExprPath {
166		attrs: Vec::new(),
167		qself: None,
168		path:  syn::Path {
169			leading_colon: None,
170			segments:      iter::once(syn::PathSegment {
171				ident,
172				arguments: syn::PathArguments::None,
173			})
174			.collect(),
175		},
176	})
177}
178
179// Panics with `cannot attach #[cloned] to <kind>`
180fn cannot_attach(kind: &str) -> ! {
181	panic!("Cannot attach `#[cloned]` to {kind}");
182}