git.delta.rocks / jrsonnet / refs/commits / 8d2d63334ec3

difftreelog

source

crates/jrsonnet-macros/src/lib.rs17.1 KiBsourcehistory
1use std::string::String;23use proc_macro2::TokenStream;4use quote::quote;5use syn::{6	parenthesized,7	parse::{Parse, ParseStream},8	parse_macro_input,9	punctuated::Punctuated,10	spanned::Spanned,11	token::{self, Comma},12	Attribute, DeriveInput, Error, Expr, FnArg, GenericArgument, Ident, ItemFn, LitStr, Pat, Path,13	PathArguments, Result, ReturnType, Token, Type,14};1516fn parse_attr<A: Parse, I>(attrs: &[Attribute], ident: I) -> Result<Option<A>>17where18	Ident: PartialEq<I>,19{20	let attrs = attrs21		.iter()22		.filter(|a| a.path().is_ident(&ident))23		.collect::<Vec<_>>();24	if attrs.len() > 1 {25		return Err(Error::new(26			attrs[1].span(),27			"this attribute may be specified only once",28		));29	} else if attrs.is_empty() {30		return Ok(None);31	}32	let attr = attrs[0];33	let attr = attr.parse_args::<A>()?;3435	Ok(Some(attr))36}3738fn path_is(path: &Path, needed: &str) -> bool {39	path.leading_colon.is_none()40		&& !path.segments.is_empty()41		&& path.segments.iter().last().unwrap().ident == needed42}4344fn type_is_path<'ty>(ty: &'ty Type, needed: &str) -> Option<&'ty PathArguments> {45	match ty {46		Type::Path(path) if path.qself.is_none() && path_is(&path.path, needed) => {47			let args = &path.path.segments.iter().last().unwrap().arguments;48			Some(args)49		}50		_ => None,51	}52}5354fn extract_type_from_option(ty: &Type) -> Result<Option<&Type>> {55	let Some(args) = type_is_path(ty, "Option") else {56		return Ok(None);57	};58	// It should have only on angle-bracketed param ("<String>"):59	let PathArguments::AngleBracketed(params) = args else {60		return Err(Error::new(args.span(), "missing option generic"));61	};62	let generic_arg = params.args.iter().next().unwrap();63	// This argument must be a type:64	let GenericArgument::Type(ty) = generic_arg else {65		return Err(Error::new(66			generic_arg.span(),67			"option generic should be a type",68		));69	};70	Ok(Some(ty))71}7273struct Field {74	attrs: Vec<Attribute>,75	name: Ident,76	_colon: Token![:],77	ty: Type,78}79impl Parse for Field {80	fn parse(input: ParseStream) -> syn::Result<Self> {81		Ok(Self {82			attrs: input.call(Attribute::parse_outer)?,83			name: input.parse()?,84			_colon: input.parse()?,85			ty: input.parse()?,86		})87	}88}8990mod kw {91	syn::custom_keyword!(fields);92	syn::custom_keyword!(rename);93	syn::custom_keyword!(flatten);94	syn::custom_keyword!(add);95	syn::custom_keyword!(hide);96	syn::custom_keyword!(ok);97}9899struct EmptyAttr;100impl Parse for EmptyAttr {101	fn parse(_input: ParseStream) -> Result<Self> {102		Ok(Self)103	}104}105106struct BuiltinAttrs {107	fields: Vec<Field>,108}109impl Parse for BuiltinAttrs {110	fn parse(input: ParseStream) -> syn::Result<Self> {111		if input.is_empty() {112			return Ok(Self { fields: Vec::new() });113		}114		input.parse::<kw::fields>()?;115		let fields;116		parenthesized!(fields in input);117		let p = Punctuated::<Field, Comma>::parse_terminated(&fields)?;118		Ok(Self {119			fields: p.into_iter().collect(),120		})121	}122}123124enum ArgInfo {125	Normal {126		ty: Box<Type>,127		is_option: bool,128		name: Option<String>,129		cfg_attrs: Vec<Attribute>,130	},131	Lazy {132		is_option: bool,133		name: Option<String>,134	},135	Context,136	Location,137	This,138}139140impl ArgInfo {141	fn parse(name: &str, arg: &FnArg) -> Result<Self> {142		let FnArg::Typed(arg) = arg else {143			unreachable!()144		};145		let ident = match &arg.pat as &Pat {146			Pat::Ident(i) => Some(i.ident.clone()),147			_ => None,148		};149		let ty = &arg.ty;150		if type_is_path(ty, "Context").is_some() {151			return Ok(Self::Context);152		} else if type_is_path(ty, "CallLocation").is_some() {153			return Ok(Self::Location);154		} else if type_is_path(ty, "Thunk").is_some() {155			return Ok(Self::Lazy {156				is_option: false,157				name: ident.map(|v| v.to_string()),158			});159		}160161		match ty as &Type {162			Type::Reference(r) if type_is_path(&r.elem, name).is_some() => return Ok(Self::This),163			_ => {}164		}165166		let (is_option, ty) = if let Some(ty) = extract_type_from_option(ty)? {167			if type_is_path(ty, "Thunk").is_some() {168				return Ok(Self::Lazy {169					is_option: true,170					name: ident.map(|v| v.to_string()),171				});172			}173174			(true, Box::new(ty.clone()))175		} else {176			(false, ty.clone())177		};178179		let cfg_attrs = arg180			.attrs181			.iter()182			.filter(|a| a.path().is_ident("cfg"))183			.cloned()184			.collect();185186		Ok(Self::Normal {187			ty,188			is_option,189			name: ident.map(|v| v.to_string()),190			cfg_attrs,191		})192	}193}194195#[proc_macro_attribute]196pub fn builtin(197	attr: proc_macro::TokenStream,198	item: proc_macro::TokenStream,199) -> proc_macro::TokenStream {200	let attr = parse_macro_input!(attr as BuiltinAttrs);201	let item_fn = item.clone();202	let item_fn: ItemFn = parse_macro_input!(item_fn);203204	match builtin_inner(attr, item_fn, item.into()) {205		Ok(v) => v.into(),206		Err(e) => e.into_compile_error().into(),207	}208}209210#[allow(clippy::too_many_lines)]211fn builtin_inner(212	attr: BuiltinAttrs,213	fun: ItemFn,214	item: proc_macro2::TokenStream,215) -> syn::Result<TokenStream> {216	let ReturnType::Type(_, result) = &fun.sig.output else {217		return Err(Error::new(218			fun.sig.span(),219			"builtin should return something",220		));221	};222223	let name = fun.sig.ident.to_string();224	let args = fun225		.sig226		.inputs227		.iter()228		.map(|arg| ArgInfo::parse(&name, arg))229		.collect::<Result<Vec<_>>>()?;230231	let params_desc = args.iter().filter_map(|a| match a {232		ArgInfo::Normal {233			is_option,234			name,235			cfg_attrs,236			..237		} => {238			let name = name239				.as_ref()240				.map_or_else(|| quote! {None}, |n| quote! {ParamName::new_static(#n)});241			Some(quote! {242				#(#cfg_attrs)*243				BuiltinParam::new(#name, #is_option),244			})245		}246		ArgInfo::Lazy { is_option, name } => {247			let name = name248				.as_ref()249				.map_or_else(|| quote! {None}, |n| quote! {ParamName::new_static(#n)});250			Some(quote! {251				BuiltinParam::new(#name, #is_option),252			})253		}254		ArgInfo::Context | ArgInfo::Location | ArgInfo::This => None,255	});256257	let mut id = 0usize;258	let pass = args259		.iter()260		.map(|a| match a {261			ArgInfo::Normal { .. } | ArgInfo::Lazy { .. } => {262				let cid = id;263				id += 1;264				(quote! {#cid}, a)265			}266			ArgInfo::Context | ArgInfo::Location | ArgInfo::This => {267				(quote! {compile_error!("should not use id")}, a)268			}269		})270		.map(|(id, a)| match a {271			ArgInfo::Normal {272				ty,273				is_option,274				name,275				cfg_attrs,276			} => {277				let name = name.as_ref().map_or("<unnamed>", String::as_str);278				let eval = quote! {jrsonnet_evaluator::State::push_description(279					|| format!("argument <{}> evaluation", #name),280					|| <#ty>::from_untyped(value.evaluate()?),281				)?};282				let value = if *is_option {283					quote! {if let Some(value) = &parsed[#id] {284						Some(#eval)285					} else {286						None287					},}288				} else {289					quote! {{290						let value = parsed[#id].as_ref().expect("args shape is checked");291						#eval292					},}293				};294				quote! {295					#(#cfg_attrs)*296					#value297				}298			}299			ArgInfo::Lazy { is_option, .. } => {300				if *is_option {301					quote! {if let Some(value) = &parsed[#id] {302						Some(value.clone())303					} else {304						None305					}}306				} else {307					quote! {308						parsed[#id].as_ref().expect("args shape is correct").clone(),309					}310				}311			}312			ArgInfo::Context => quote! {ctx.clone(),},313			ArgInfo::Location => quote! {location,},314			ArgInfo::This => quote! {self,},315		});316317	let fields = attr.fields.iter().map(|field| {318		let attrs = &field.attrs;319		let name = &field.name;320		let ty = &field.ty;321		quote! {322			#(#attrs)*323			pub #name: #ty,324		}325	});326327	let name = &fun.sig.ident;328	let vis = &fun.vis;329	let static_ext = if attr.fields.is_empty() {330		quote! {331			impl #name {332				pub const INST: &'static dyn StaticBuiltin = &#name {};333			}334			impl StaticBuiltin for #name {}335		}336	} else {337		quote! {}338	};339	let static_derive_copy = if attr.fields.is_empty() {340		quote! {, Copy}341	} else {342		quote! {}343	};344345	Ok(quote! {346		#item347348		#[doc(hidden)]349		#[allow(non_camel_case_types)]350		#[derive(Clone, jrsonnet_gcmodule::Trace #static_derive_copy)]351		#vis struct #name {352			#(#fields)*353		}354		const _: () = {355			use ::jrsonnet_evaluator::{356				State, Val,357				function::{builtin::{Builtin, StaticBuiltin, BuiltinParam, ParamName}, CallLocation, ArgsLike, parse::parse_builtin_call},358				Result, Context, typed::Typed,359				parser::ExprLocation,360			};361			const PARAMS: &'static [BuiltinParam] = &[362				#(#params_desc)*363			];364365			#static_ext366			impl Builtin for #name367			where368				Self: 'static369			{370				fn name(&self) -> &str {371					stringify!(#name)372				}373				fn params(&self) -> &[BuiltinParam] {374					PARAMS375				}376				#[allow(unused_variable)]377				fn call(&self, ctx: Context, location: CallLocation, args: &dyn ArgsLike) -> Result<Val> {378					let parsed = parse_builtin_call(ctx.clone(), &PARAMS, args, false)?;379380					let result: #result = #name(#(#pass)*);381					<_ as Typed>::into_result(result)382				}383				fn as_any(&self) -> &dyn ::std::any::Any {384					self385				}386			}387		};388	})389}390391#[derive(Default)]392#[allow(clippy::struct_excessive_bools)]393struct TypedAttr {394	rename: Option<String>,395	flatten: bool,396	/// flatten(ok) strategy for flattened optionals397	/// field would be None in case of any parsing error (as in serde)398	flatten_ok: bool,399	// Should it be `field+:` instead of `field:`400	add: bool,401	// Should it be `field::` instead of `field:`402	hide: bool,403}404impl Parse for TypedAttr {405	fn parse(input: ParseStream) -> syn::Result<Self> {406		let mut out = Self::default();407		loop {408			let lookahead = input.lookahead1();409			if lookahead.peek(kw::rename) {410				input.parse::<kw::rename>()?;411				input.parse::<Token![=]>()?;412				let name = input.parse::<LitStr>()?;413				if out.rename.is_some() {414					return Err(Error::new(415						name.span(),416						"rename attribute may only be specified once",417					));418				}419				out.rename = Some(name.value());420			} else if lookahead.peek(kw::flatten) {421				input.parse::<kw::flatten>()?;422				out.flatten = true;423				if input.peek(token::Paren) {424					let content;425					parenthesized!(content in input);426					let lookahead = content.lookahead1();427					if lookahead.peek(kw::ok) {428						content.parse::<kw::ok>()?;429						out.flatten_ok = true;430					} else {431						return Err(lookahead.error());432					}433				}434			} else if lookahead.peek(kw::add) {435				input.parse::<kw::add>()?;436				out.add = true;437			} else if lookahead.peek(kw::hide) {438				input.parse::<kw::hide>()?;439				out.hide = true;440			} else if input.is_empty() {441				break;442			} else {443				return Err(lookahead.error());444			}445			if input.peek(Token![,]) {446				input.parse::<Token![,]>()?;447			} else {448				break;449			}450		}451		Ok(out)452	}453}454455struct TypedField {456	attr: TypedAttr,457	ident: Ident,458	ty: Type,459	is_option: bool,460}461impl TypedField {462	fn parse(field: &syn::Field) -> Result<Self> {463		let attr = parse_attr::<TypedAttr, _>(&field.attrs, "typed")?.unwrap_or_default();464		let Some(ident) = field.ident.clone() else {465			return Err(Error::new(466				field.span(),467				"this field should appear in output object, but it has no visible name",468			));469		};470		let (is_option, ty) = extract_type_from_option(&field.ty)?471			.map_or_else(|| (false, field.ty.clone()), |ty| (true, ty.clone()));472		if is_option && attr.flatten {473			if !attr.flatten_ok {474				return Err(Error::new(475					field.span(),476					"strategy should be set when flattening Option",477				));478			}479		} else if attr.flatten_ok {480			return Err(Error::new(481				field.span(),482				"flatten(ok) is only useable on optional fields",483			));484		}485486		Ok(Self {487			attr,488			ident,489			ty,490			is_option,491		})492	}493	/// None if this field is flattened in jsonnet output494	fn name(&self) -> Option<String> {495		if self.attr.flatten {496			return None;497		}498		Some(499			self.attr500				.rename501				.clone()502				.unwrap_or_else(|| self.ident.to_string()),503		)504	}505506	fn expand_field(&self) -> Option<TokenStream> {507		if self.is_option {508			return None;509		}510		let name = self.name()?;511		let ty = &self.ty;512		Some(quote! {513			(#name, <#ty as Typed>::TYPE)514		})515	}516	fn expand_parse(&self) -> TokenStream {517		let ident = &self.ident;518		let ty = &self.ty;519		if self.attr.flatten {520			// optional flatten is handled in same way as serde521			return if self.is_option {522				quote! {523					#ident: <#ty as TypedObj>::parse(&obj).ok(),524				}525			} else {526				quote! {527					#ident: <#ty as TypedObj>::parse(&obj)?,528				}529			};530		};531532		let name = self.name().unwrap();533		let value = if self.is_option {534			quote! {535				if let Some(value) = obj.get(#name.into())? {536					Some(<#ty as Typed>::from_untyped(value)?)537				} else {538					None539				}540			}541		} else {542			quote! {543				<#ty as Typed>::from_untyped(obj.get(#name.into())?.ok_or_else(|| ErrorKind::NoSuchField(#name.into(), vec![]))?)?544			}545		};546547		quote! {548			#ident: #value,549		}550	}551	fn expand_serialize(&self) -> TokenStream {552		let ident = &self.ident;553		let ty = &self.ty;554		self.name().map_or_else(555			|| {556				if self.is_option {557					quote! {558						if let Some(value) = self.#ident {559							<#ty as TypedObj>::serialize(value, out)?;560						}561					}562				} else {563					quote! {564						<#ty as TypedObj>::serialize(self.#ident, out)?;565					}566				}567			},568			|name| {569				let hide = if self.attr.hide {570					quote! {.hide()}571				} else {572					quote! {}573				};574				let add = if self.attr.add {575					quote! {.add()}576				} else {577					quote! {}578				};579				if self.is_option {580					quote! {581						if let Some(value) = self.#ident {582							out.field(#name)583								#hide584								#add585								.try_value(<#ty as Typed>::into_untyped(value)?)?;586						}587					}588				} else {589					quote! {590						out.field(#name)591							#hide592							#add593							.try_value(<#ty as Typed>::into_untyped(self.#ident)?)?;594					}595				}596			},597		)598	}599}600601#[proc_macro_derive(Typed, attributes(typed))]602pub fn derive_typed(item: proc_macro::TokenStream) -> proc_macro::TokenStream {603	let input = parse_macro_input!(item as DeriveInput);604605	match derive_typed_inner(input) {606		Ok(v) => v.into(),607		Err(e) => e.to_compile_error().into(),608	}609}610611fn derive_typed_inner(input: DeriveInput) -> Result<TokenStream> {612	let syn::Data::Struct(data) = &input.data else {613		return Err(Error::new(input.span(), "only structs supported"));614	};615616	let ident = &input.ident;617	let fields = data618		.fields619		.iter()620		.map(TypedField::parse)621		.collect::<Result<Vec<_>>>()?;622623	let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();624625	let typed = {626		let fields = fields627			.iter()628			.filter_map(TypedField::expand_field)629			.collect::<Vec<_>>();630		quote! {631			impl #impl_generics Typed for #ident #ty_generics #where_clause {632				const TYPE: &'static ComplexValType = &ComplexValType::ObjectRef(&[633					#(#fields,)*634				]);635636				fn from_untyped(value: Val) -> JrResult<Self> {637					let obj = value.as_obj().expect("shape is correct");638					Self::parse(&obj)639				}640641				fn into_untyped(value: Self) -> JrResult<Val> {642					let mut out = ObjValueBuilder::new();643					value.serialize(&mut out)?;644					Ok(Val::Obj(out.build()))645				}646647			}648		}649	};650651	let fields_parse = fields.iter().map(TypedField::expand_parse);652	let fields_serialize = fields653		.iter()654		.map(TypedField::expand_serialize)655		.collect::<Vec<_>>();656657	Ok(quote! {658		const _: () = {659			use ::jrsonnet_evaluator::{660				typed::{ComplexValType, Typed, TypedObj, CheckType},661				Val, State,662				error::{ErrorKind, Result as JrResult},663				ObjValueBuilder, ObjValue,664			};665666			#typed667668			impl #impl_generics TypedObj for #ident #ty_generics #where_clause {669				fn serialize(self, out: &mut ObjValueBuilder) -> JrResult<()> {670					#(#fields_serialize)*671672					Ok(())673				}674				fn parse(obj: &ObjValue) -> JrResult<Self> {675					Ok(Self {676						#(#fields_parse)*677					})678				}679			}680		};681	})682}683684struct FormatInput {685	formatting: LitStr,686	arguments: Vec<Expr>,687}688impl Parse for FormatInput {689	fn parse(input: ParseStream) -> Result<Self> {690		let formatting = input.parse()?;691		let mut arguments = Vec::new();692693		while input.peek(Token![,]) {694			input.parse::<Token![,]>()?;695			if input.is_empty() {696				// Trailing comma697				break;698			}699			let expr = input.parse()?;700			arguments.push(expr);701		}702703		if !input.is_empty() {704			return Err(syn::Error::new(input.span(), "unexpected trailing input"));705		}706707		Ok(Self {708			formatting,709			arguments,710		})711	}712}713fn is_format_str(i: &str) -> bool {714	let mut is_plain = true;715	// -1 = {716	// +1 = }717	let mut is_bracket = 0i8;718	for ele in i.chars() {719		match ele {720			'{' if is_bracket == -1 => {721				is_bracket = 0;722			}723			'}' if is_bracket == -1 => {724				is_plain = false;725				break;726			}727			'}' if is_bracket == 1 => {728				is_bracket = 0;729			}730			'{' if is_bracket == 1 => {731				is_plain = false;732				break;733			}734			'{' => {735				is_bracket = -1;736			}737			'}' => {738				is_bracket = 1;739			}740			_ if is_bracket != 0 => {741				is_plain = false;742				break;743			}744			_ => {}745		}746	}747	!is_plain || is_bracket != 0748}749impl FormatInput {750	fn expand(self) -> TokenStream {751		let format = self.formatting;752		if is_format_str(&format.value()) {753			let args = self.arguments;754			quote! {755				::jrsonnet_evaluator::IStr::from(format!(#format #(, #args)*))756			}757		} else {758			if let Some(first) = self.arguments.first() {759				return syn::Error::new(760					first.span(),761					"string has no formatting codes, it should not have the arguments",762				)763				.into_compile_error();764			}765			quote! {766				::jrsonnet_evaluator::IStr::from(#format)767			}768		}769	}770}771772/// `IStr` formatting helper773///774/// Using `format!("literal with no codes").into()` is slower than just `"literal with no codes".into()`775/// This macro looks for formatting codes in the input string, and uses776/// `format!()` only when necessary777#[proc_macro]778pub fn format_istr(input: proc_macro::TokenStream) -> proc_macro::TokenStream {779	let input = parse_macro_input!(input as FormatInput);780	input.expand().into()781}