aboutsummaryrefslogtreecommitdiff
path: root/mingling_macros/src/attr/completion.rs
diff options
context:
space:
mode:
Diffstat (limited to 'mingling_macros/src/attr/completion.rs')
-rw-r--r--mingling_macros/src/attr/completion.rs250
1 files changed, 250 insertions, 0 deletions
diff --git a/mingling_macros/src/attr/completion.rs b/mingling_macros/src/attr/completion.rs
new file mode 100644
index 0000000..3ced091
--- /dev/null
+++ b/mingling_macros/src/attr/completion.rs
@@ -0,0 +1,250 @@
+use crate::res_injection::{ResourceInjection, generate_immut_resource_bindings};
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::spanned::Spanned;
+use syn::{FnArg, Ident, ItemFn, Pat, PatType, Type, TypePath, parse_macro_input};
+
+#[cfg(feature = "comp")]
+pub(crate) fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
+ // Parse the attribute arguments such as HelloEntry or crate::EntryFine from #[completion(crate::EntryFine)]
+ use crate::get_global_set;
+ let previous_type_path: TypePath = if attr.is_empty() {
+ return syn::Error::new(
+ proc_macro2::Span::call_site(),
+ "completion attribute requires a previous type argument, e.g. #[completion(HelloEntry)]",
+ )
+ .to_compile_error()
+ .into();
+ } else {
+ parse_macro_input!(attr as TypePath)
+ };
+ let previous_type_ident = &previous_type_path.path.segments.last().unwrap().ident;
+
+ // Parse the function item
+ let input_fn = parse_macro_input!(item as ItemFn);
+
+ // Validate the function is not async
+ if input_fn.sig.asyncness.is_some() {
+ return syn::Error::new(input_fn.sig.span(), "Completion function cannot be async")
+ .to_compile_error()
+ .into();
+ }
+
+ // Get the function signature parts
+ let sig = &input_fn.sig;
+ let inputs = &sig.inputs;
+ let output = &sig.output;
+
+ // Must have at least one parameter ctx
+ if inputs.is_empty() {
+ return syn::Error::new(
+ inputs.span(),
+ "Completion function must have at least one parameter: `ctx: &ShellContext`",
+ )
+ .to_compile_error()
+ .into();
+ }
+
+ // Extract the first param pattern and type for the ctx parameter
+ let first_arg = &inputs[0];
+ let _ctx_type = match first_arg {
+ FnArg::Typed(PatType { ty, .. }) => (**ty).clone(),
+ FnArg::Receiver(_) => {
+ return syn::Error::new(
+ first_arg.span(),
+ "Completion function cannot have self parameter",
+ )
+ .to_compile_error()
+ .into();
+ }
+ };
+ let fixed_ctx: Pat = syn::parse_quote!(ctx);
+
+ // Extract resources from params 2 through N, skipping ctx
+ let resources = match extract_resources_from_args(sig, 1) {
+ Ok(r) => r,
+ Err(e) => return e.to_compile_error().into(),
+ };
+
+ // Get the function body
+ let fn_body = &input_fn.block;
+
+ // Get function attributes excluding the completion attribute
+ let mut fn_attrs = input_fn.attrs.clone();
+ fn_attrs.retain(|attr| !attr.path().is_ident("completion"));
+
+ // Get function visibility
+ let vis = &input_fn.vis;
+
+ // Get function name
+ let fn_name = &sig.ident;
+
+ // Generate internal name from function name using snake_case
+ let internal_name = format!(
+ "__internal_completion_{}",
+ just_fmt::snake_case!(fn_name.to_string())
+ );
+ let struct_name = Ident::new(&internal_name, fn_name.span());
+
+ let program_type = crate::default_program_path();
+ let has_resources = !resources.is_empty();
+ let mut_resources: Vec<_> = resources.iter().filter(|r| r.is_mut).collect();
+
+ // Generate immutable resource bindings
+ let immut_resource_stmts = generate_immut_resource_bindings(resources.iter(), &program_type);
+
+ // Build the call to the original function with resource arguments injected
+ let resource_args: Vec<_> = resources
+ .iter()
+ .map(|res| {
+ let var_name = &res.var_name;
+ quote! { #var_name }
+ })
+ .collect();
+
+ let fn_call = if has_resources {
+ quote! { #fn_name(#fixed_ctx, #(#resource_args),*) }
+ } else {
+ quote! { #fn_name(#fixed_ctx) }
+ };
+
+ // Wrap the function call with modify_res for mutable resources
+ let inner_call = if mut_resources.is_empty() {
+ fn_call
+ } else {
+ let mut wrapped = fn_call;
+ for res in mut_resources.iter().rev() {
+ let var_name = &res.var_name;
+ let inner_type = &res.inner_type;
+ wrapped = quote! {
+ ::mingling::this::<#program_type>().modify_res(|#var_name: &mut #inner_type| {
+ #wrapped
+ })
+ };
+ }
+ wrapped
+ };
+
+ let comp_body = if has_resources {
+ quote! {
+ #(#immut_resource_stmts)*
+ #inner_call
+ }
+ } else {
+ quote! { #inner_call }
+ };
+
+ // Generate the struct and implementation
+ // The `comp` trait method only takes `ctx` as the first parameter; resources are injected internally
+
+ let expanded = quote! {
+ #(#fn_attrs)*
+ #[doc(hidden)]
+ #[allow(non_camel_case_types)]
+ #vis struct #struct_name;
+
+ impl ::mingling::Completion for #struct_name {
+ type Previous = #previous_type_path;
+
+ fn comp(#fixed_ctx: &::mingling::ShellContext) #output {
+ #comp_body
+ }
+ }
+
+ // Keep the original function for internal use
+ #(#fn_attrs)*
+ #vis fn #fn_name(#inputs) #output {
+ #fn_body
+ }
+ };
+
+ let completion_entry = quote! {
+ Self::#previous_type_ident => <#struct_name as ::mingling::Completion>::comp(ctx),
+ };
+
+ let mut completions = get_global_set(&crate::COMPLETIONS).lock().unwrap();
+ let completion_str = completion_entry.to_string();
+
+ // Check for duplicate variant before inserting
+ let variant_name = previous_type_ident.to_string();
+ if let Err(err) = crate::check_duplicate_variant(
+ &completions,
+ &completion_str,
+ &variant_name,
+ "completion",
+ previous_type_path.span(),
+ ) {
+ return err.into();
+ }
+
+ completions.insert(completion_str);
+
+ expanded.into()
+}
+
+/// Extract resource injection parameters from function arguments (skipping the first N params).
+fn extract_resources_from_args(
+ sig: &syn::Signature,
+ skip: usize,
+) -> syn::Result<Vec<ResourceInjection>> {
+ let mut resources = Vec::new();
+ for arg in sig.inputs.iter().skip(skip) {
+ match arg {
+ FnArg::Typed(PatType { pat, ty, .. }) => {
+ let var_name = match &**pat {
+ Pat::Ident(pat_ident) => pat_ident.ident.clone(),
+ _ => {
+ return Err(syn::Error::new(
+ pat.span(),
+ "Resource injection parameter must be a simple identifier",
+ ));
+ }
+ };
+
+ let full_type = *(*ty).clone();
+
+ let (inner_type, is_ref, is_mut) = match &full_type {
+ Type::Reference(ref_type) => match &*ref_type.elem {
+ Type::Path(type_path) => {
+ let is_mut = ref_type.mutability.is_some();
+ (type_path.clone(), true, is_mut)
+ }
+ _ => {
+ return Err(syn::Error::new(
+ ty.span(),
+ "Reference resource type must be a type path",
+ ));
+ }
+ },
+ Type::Path(_) => {
+ return Err(syn::Error::new(
+ ty.span(),
+ "Resource injection parameter must be a reference (`&T` or `&mut T`)",
+ ));
+ }
+ _ => {
+ return Err(syn::Error::new(
+ ty.span(),
+ "Resource injection type must be a type path or reference",
+ ));
+ }
+ };
+
+ resources.push(ResourceInjection {
+ var_name,
+ full_type,
+ inner_type,
+ is_ref,
+ is_mut,
+ });
+ }
+ FnArg::Receiver(_) => {
+ return Err(syn::Error::new(
+ arg.span(),
+ "Resource injection parameter cannot be self",
+ ));
+ }
+ }
+ }
+ Ok(resources)
+}