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 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 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 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 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 let cleaned_module = clean_attributes_from_module(module);
51
52 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 TokenStream::from(quote! {
61 #extended_module
62 })
63}
64
65fn clean_attributes_from_module(mut module: ItemMod) -> ItemMod {
67 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
90fn 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 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 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 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 }
149 }
150}
151
152fn insert_internal_module(
154 mut module: ItemMod,
155 tusks_module: &TusksModule,
156 attr: &TusksAttr
157) -> ItemMod {
158 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 {
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 let internal_module = quote! {
214 pub mod __internal_tusks_module {
215 pub mod cli {
217 #cli_content
218 }
219
220 #handle_matches
221
222 #exec_cli
223 }
224 };
225
226 let internal_item: syn::Item = syn::parse2(internal_module)
228 .expect("Failed to parse internal module");
229
230 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}