Skip to main content

tusks_macro/
lib.rs

1use proc_macro::TokenStream;
2use syn::{parse_macro_input, ItemMod};
3use quote::quote;
4use tusks_lib::TusksModule;
5use tusks_lib::AttributeCheck;
6use tusks_lib::CliCodegen;
7use tusks_lib::HandleMatchesCodegen;
8use tusks_lib::ParametersCodegen;
9use tusks_lib::attribute::models::TusksAttr;
10use tusks_lib::tasks::functions::add_execute_task_function;
11use tusks_lib::tasks::functions::add_show_help_for_task;
12use tusks_lib::tasks::functions::add_use_statements;
13use tusks_lib::tasks::functions::set_allow_external_subcommands;
14
15#[proc_macro_attribute]
16pub fn tusks(_attr: TokenStream, item: TokenStream) -> TokenStream {
17    // 1. Validate that it's called on a module
18    let mut module = parse_macro_input!(item as ItemMod);
19
20    let mut args = parse_macro_input!(_attr as TusksAttr);
21
22    add_use_statements(&mut module);
23
24    // if tasks configuration exists add necessary functions
25    if let Some(tasks_config) = &args.tasks {
26        set_allow_external_subcommands(&mut module);
27        add_execute_task_function(&mut module, tasks_config);
28        add_show_help_for_task(&mut module, tasks_config);
29    }
30
31    args.debug = args.debug || cfg!(feature = "debug");
32    
33    // 2. Parse with TusksModule::from_module
34    let mut tusks_module = match TusksModule::from_module(module.clone(), args.root, true) {
35        Ok(Some(tm)) => tm,
36        Ok(None) => return TokenStream::from(quote! {#module}),
37        Err(e) => return e.to_compile_error().into(),
38    };
39
40    // Add missing Parameters structs and connect them via super_ field
41    if let Err(e) = tusks_module.supplement_parameters(
42        &mut module,
43        args.root,
44        args.derive_debug_for_parameters
45    ) {
46        return e.to_compile_error().into();
47    }
48    
49    // 3. Clean the original module from #[arg] and #[parameters] attributes
50    let cleaned_module = clean_attributes_from_module(module);
51    
52    // 4. Insert __internal_tusks_module with cli
53    let extended_module = insert_internal_module(cleaned_module, &tusks_module, &args);
54    
55    if args.debug {
56        eprintln!("Parsed TusksModule: {:#?}", tusks_module);
57    }
58    
59    // Return the final module
60    TokenStream::from(quote! {
61        #extended_module
62    })
63}
64
65/// Remove #[arg] and #[parameters] attributes from a module and all its items
66fn clean_attributes_from_module(mut module: ItemMod) -> ItemMod {
67    // Don't clean module-level attributes
68    
69    // Clean attributes in module content
70    clean_module_attributes(&mut module);
71    if let Some((brace, ref mut items)) = module.content {
72        for item in items.iter_mut() {
73            clean_item_attributes(item);
74        }
75        module.content = Some((brace, items.clone()));
76    }
77    
78    module
79}
80
81fn clean_module_attributes(module: &mut ItemMod) {
82    module.attrs.retain(
83        |attr|
84            !attr.path().is_ident("command")
85            && !attr.path().is_ident("subcommands")
86            && !attr.path().is_ident("external_subcommands")
87    );
88}
89
90/// Recursively clean attributes from an item
91fn clean_item_attributes(item: &mut syn::Item) {
92    match item {
93        syn::Item::Struct(s) => {
94            if s.has_attr("skip") {
95                s.attrs.retain(|attr| !attr.path().is_ident("skip"));
96            }
97            else {
98                // Clean #[arg] from field attributes
99                for field in s.fields.iter_mut() {
100                    field.attrs.retain(|attr| !attr.path().is_ident("arg"));
101                }
102            }
103        }
104        syn::Item::Fn(f) => {
105            if f.has_attr("skip") {
106                f.attrs.retain(|attr| !attr.path().is_ident("skip"));
107            }
108            else {
109                f.attrs.retain(
110                    |attr| !attr.path().is_ident("command")
111                    && !attr.path().is_ident("default")
112                );
113
114                // Clean #[arg] from parameter attributes
115                for input in f.sig.inputs.iter_mut() {
116                    if let syn::FnArg::Typed(pat_type) = input {
117                        pat_type.attrs.retain(|attr| !attr.path().is_ident("arg"));
118                    }
119                }
120            }
121        }
122        syn::Item::Mod(m) => {
123            if m.has_attr("skip") {
124                m.attrs.retain(|attr| !attr.path().is_ident("skip"));
125            }
126            else {
127                clean_module_attributes(m);
128
129                // Recursively clean submodules
130                if let Some((brace, ref mut items)) = m.content {
131                    for subitem in items.iter_mut() {
132                        clean_item_attributes(subitem);
133                    }
134                    m.content = Some((brace, items.clone()));
135                }
136            }
137        }
138        syn::Item::Use(u) => {
139            if u.has_attr("skip") {
140                u.attrs.retain(|attr| !attr.path().is_ident("skip"));
141            }
142            else {
143                u.attrs.retain(|attr| !attr.path().is_ident("command"));
144            }
145        }
146        _ => {
147            // Don't clean other items
148        }
149    }
150}
151
152/// Insert the __internal_tusks_module with cli into the cleaned module
153fn insert_internal_module(
154    mut module: ItemMod,
155    tusks_module: &TusksModule,
156    attr: &TusksAttr
157) -> ItemMod {
158    // Generate the cli module content
159    let cli_content = tusks_module.build_cli(Vec::new(), attr.debug);
160    let handle_matches = tusks_module.build_handle_matches(attr.root);
161
162    let completions_check = if cfg!(feature = "completions") {
163        quote! {
164            // Check for --completions <SHELL> before normal dispatch
165            {
166                let args: Vec<String> = std::env::args().collect();
167                if let Some(pos) = args.iter().position(|a| a == "--completions") {
168                    if let Some(shell_str) = args.get(pos + 1) {
169                        use ::tusks::clap::CommandFactory;
170                        let shell: ::tusks::clap_complete::Shell = shell_str.parse()
171                            .expect("invalid shell for --completions (try: bash, zsh, fish, elvish, powershell)");
172                        let mut cmd = cli::Cli::command();
173                        let name = cmd.get_name().to_string();
174                        ::tusks::clap_complete::generate(shell, &mut cmd, name, &mut std::io::stdout());
175                        return Some(0);
176                    } else {
177                        eprintln!("--completions requires a shell argument (bash, zsh, fish, elvish, powershell)");
178                        return Some(1);
179                    }
180                }
181            }
182        }
183    } else {
184        quote! {}
185    };
186
187    let handle_call = if cfg!(feature = "async") {
188        quote! {
189            ::tusks::tokio::runtime::Runtime::new()
190                .expect("failed to create tokio runtime")
191                .block_on(handle_matches(&cli))
192        }
193    } else {
194        quote! { handle_matches(&cli) }
195    };
196
197    let exec_cli = if !attr.root {
198        quote! {}
199    } else {
200        quote! {
201            pub fn exec_cli() -> Option<u8> {
202                use ::tusks::clap::Parser;
203
204                #completions_check
205
206                let cli = cli::Cli::parse();
207                #handle_call
208            }
209        }
210    };
211    
212    // Build the __internal_tusks_module
213    let internal_module = quote! {
214        pub mod __internal_tusks_module {
215            // CLI structure
216            pub mod cli {
217                #cli_content
218            }
219            
220            #handle_matches
221
222            #exec_cli
223        }
224    };
225    
226    // Parse the internal module as an Item
227    let internal_item: syn::Item = syn::parse2(internal_module)
228        .expect("Failed to parse internal module");
229    
230    // Add it to the module content
231    if let Some((brace, ref mut items)) = module.content {
232        items.push(internal_item);
233
234
235        if attr.root {
236            let exec_cli_outer = quote! {
237                pub fn exec_cli() -> Option<u8> {
238                    __internal_tusks_module::exec_cli()
239                }
240            };
241
242            let exec_cli_outer: syn::Item = syn::parse2(exec_cli_outer)
243                .expect("Failed to parse outer exec cli");
244
245            items.push(exec_cli_outer);
246        }
247
248
249        module.content = Some((brace, items.clone()));
250    }
251    
252    module
253}