Skip to main content

tauri_typegen/analysis/
command_parser.rs

1use crate::analysis::serde_parser::SerdeParser;
2use crate::analysis::type_resolver::TypeResolver;
3use crate::models::{CommandInfo, ParameterInfo};
4use std::path::Path;
5use syn::{File as SynFile, FnArg, ItemFn, PatType, ReturnType, Type};
6
7/// Parser for Tauri command functions
8#[derive(Debug)]
9pub struct CommandParser {
10    serde_parser: SerdeParser,
11}
12
13impl CommandParser {
14    pub fn new() -> Self {
15        Self {
16            serde_parser: SerdeParser::new(),
17        }
18    }
19
20    /// Extract commands from a cached AST (including nested modules)
21    pub fn extract_commands_from_ast(
22        &self,
23        ast: &SynFile,
24        file_path: &Path,
25        type_resolver: &mut TypeResolver,
26    ) -> Result<Vec<CommandInfo>, Box<dyn std::error::Error>> {
27        let mut commands = Vec::new();
28        self.extract_commands_from_items(&ast.items, file_path, type_resolver, &mut commands);
29        Ok(commands)
30    }
31
32    /// Recursively extract commands from items
33    fn extract_commands_from_items(
34        &self,
35        items: &[syn::Item],
36        file_path: &Path,
37        type_resolver: &mut TypeResolver,
38        commands: &mut Vec<CommandInfo>,
39    ) {
40        for item in items {
41            match item {
42                syn::Item::Fn(func) => {
43                    if self.is_tauri_command(func) {
44                        if let Some(info) =
45                            self.extract_command_info(func, file_path, type_resolver)
46                        {
47                            commands.push(info);
48                        }
49                    }
50                }
51                syn::Item::Mod(item_mod) => {
52                    if let Some((_, items)) = &item_mod.content {
53                        self.extract_commands_from_items(items, file_path, type_resolver, commands);
54                    }
55                }
56                _ => {}
57            }
58        }
59    }
60
61    /// Check if a function is a Tauri command
62    fn is_tauri_command(&self, func: &ItemFn) -> bool {
63        func.attrs.iter().any(|attr| {
64            attr.path().segments.len() == 2
65                && attr.path().segments[0].ident == "tauri"
66                && attr.path().segments[1].ident == "command"
67                || attr.path().is_ident("command")
68        })
69    }
70
71    /// Extract command information from a function
72    fn extract_command_info(
73        &self,
74        func: &ItemFn,
75        file_path: &Path,
76        type_resolver: &mut TypeResolver,
77    ) -> Option<CommandInfo> {
78        let name = func.sig.ident.to_string();
79
80        let parameters = self.extract_parameters(&func.sig.inputs, type_resolver);
81        let return_type = self.extract_return_type(&func.sig.output);
82        let return_type_structure = type_resolver.parse_type_structure(&return_type);
83        let is_async = func.sig.asyncness.is_some();
84
85        // Get line number from the function's span
86        let line_number = func.sig.ident.span().start().line;
87
88        // Parse serde rename_all attribute from function attributes
89        let serde_rename_all = self
90            .serde_parser
91            .parse_struct_serde_attrs(&func.attrs)
92            .rename_all;
93
94        Some(CommandInfo {
95            name,
96            parameters,
97            return_type,
98            return_type_structure,
99            file_path: file_path.to_string_lossy().to_string(),
100            line_number,
101            is_async,
102            channels: Vec::new(), // Will be populated by channel_parser
103            serde_rename_all,
104        })
105    }
106
107    /// Extract parameters from function signature
108    fn extract_parameters(
109        &self,
110        inputs: &syn::punctuated::Punctuated<FnArg, syn::token::Comma>,
111        type_resolver: &mut TypeResolver,
112    ) -> Vec<ParameterInfo> {
113        inputs
114            .iter()
115            .filter_map(|input| {
116                if let FnArg::Typed(PatType { pat, ty, attrs, .. }) = input {
117                    if let syn::Pat::Ident(pat_ident) = pat.as_ref() {
118                        let name = pat_ident.ident.to_string();
119
120                        // Skip Tauri-specific parameters
121                        if self.is_tauri_parameter_type(ty) {
122                            return None;
123                        }
124
125                        let rust_type = Self::type_to_string(ty);
126                        let type_structure = type_resolver.parse_type_structure(&rust_type);
127                        let is_optional = self.is_optional_type(ty);
128
129                        // Parse serde rename attribute from parameter attributes
130                        let serde_rename = self.serde_parser.parse_field_serde_attrs(attrs).rename;
131
132                        return Some(ParameterInfo {
133                            name,
134                            rust_type,
135                            is_optional,
136                            type_structure,
137                            serde_rename,
138                        });
139                    }
140                }
141                None
142            })
143            .collect()
144    }
145
146    /// Check if a parameter type is a Tauri-specific type that should be skipped
147    /// This checks the actual syn::Type to properly handle both imported and fully-qualified types
148    fn is_tauri_parameter_type(&self, ty: &Type) -> bool {
149        if let Type::Path(type_path) = ty {
150            let segments = &type_path.path.segments;
151
152            // Check various patterns:
153            // 1. Fully qualified: tauri::AppHandle, tauri::State<T>, tauri::ipc::Request
154            // 2. Imported: AppHandle, State<T>, Window<T>
155            if segments.len() >= 2 {
156                // Check for tauri::* or tauri::ipc::*
157                if segments[0].ident == "tauri" {
158                    if segments.len() == 2 {
159                        // tauri::AppHandle, tauri::Window, etc.
160                        let second = &segments[1].ident;
161                        return second == "AppHandle"
162                            || second == "Window"
163                            || second == "WebviewWindow"
164                            || second == "State"
165                            || second == "Manager";
166                    } else if segments.len() == 3 && segments[1].ident == "ipc" {
167                        // tauri::ipc::Request, tauri::ipc::Channel
168                        let third = &segments[2].ident;
169                        return third == "Request" || third == "Channel";
170                    }
171                }
172            }
173
174            // Check for imported types (single segment)
175            if let Some(last_segment) = segments.last() {
176                let type_ident = &last_segment.ident;
177
178                // Only match specific Tauri types that are commonly imported
179                // Be careful not to match user types with similar names
180                if type_ident == "AppHandle" || type_ident == "WebviewWindow" {
181                    return true;
182                }
183
184                // Channel should be filtered if it has generic parameters (indicating it's the Tauri IPC channel)
185                if type_ident == "Channel"
186                    && matches!(
187                        last_segment.arguments,
188                        syn::PathArguments::AngleBracketed(_)
189                    )
190                {
191                    return true;
192                }
193
194                // State and Window are common names, only match if they have generic params
195                // (Tauri's State and Window types always have generics like State<T>, Window<R>)
196                if (type_ident == "State" || type_ident == "Window")
197                    && !last_segment.arguments.is_empty()
198                {
199                    return true;
200                }
201            }
202        }
203
204        false
205    }
206
207    /// Extract return type from function signature - returns rust_type only
208    fn extract_return_type(&self, output: &ReturnType) -> String {
209        match output {
210            ReturnType::Default => "()".to_string(),
211            ReturnType::Type(_, ty) => Self::type_to_string(ty),
212        }
213    }
214
215    /// Convert a Type to its string representation
216    fn type_to_string(ty: &Type) -> String {
217        match ty {
218            Type::Path(type_path) => {
219                let segments: Vec<String> = type_path
220                    .path
221                    .segments
222                    .iter()
223                    .map(|segment| {
224                        if segment.arguments.is_empty() {
225                            segment.ident.to_string()
226                        } else {
227                            match &segment.arguments {
228                                syn::PathArguments::AngleBracketed(args) => {
229                                    let inner_types: Vec<String> = args
230                                        .args
231                                        .iter()
232                                        .filter_map(|arg| {
233                                            if let syn::GenericArgument::Type(inner_ty) = arg {
234                                                Some(Self::type_to_string(inner_ty))
235                                            } else {
236                                                None
237                                            }
238                                        })
239                                        .collect();
240                                    format!("{}<{}>", segment.ident, inner_types.join(", "))
241                                }
242                                _ => segment.ident.to_string(),
243                            }
244                        }
245                    })
246                    .collect();
247                segments.join("::")
248            }
249            Type::Reference(type_ref) => {
250                format!("&{}", Self::type_to_string(&type_ref.elem))
251            }
252            Type::Tuple(type_tuple) => {
253                if type_tuple.elems.is_empty() {
254                    "()".to_string()
255                } else {
256                    let types: Vec<String> =
257                        type_tuple.elems.iter().map(Self::type_to_string).collect();
258                    format!("({})", types.join(", "))
259                }
260            }
261            _ => "unknown".to_string(),
262        }
263    }
264
265    /// Check if a type is Option<T>
266    fn is_optional_type(&self, ty: &Type) -> bool {
267        if let Type::Path(type_path) = ty {
268            if let Some(segment) = type_path.path.segments.last() {
269                return segment.ident == "Option";
270            }
271        }
272        false
273    }
274}
275
276impl Default for CommandParser {
277    fn default() -> Self {
278        Self::new()
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285    use syn::parse_quote;
286
287    #[test]
288    fn test_new_command_parser() {
289        let parser = CommandParser::new();
290        // Just verify it constructs without panicking
291        let _ = parser;
292    }
293
294    #[test]
295    fn test_default_impl() {
296        let parser = CommandParser::default();
297        // Just verify default works
298        let _ = parser;
299    }
300
301    // is_tauri_command tests
302    mod is_tauri_command {
303        use super::*;
304
305        #[test]
306        fn test_recognizes_tauri_command_attribute() {
307            let parser = CommandParser::new();
308            let func: ItemFn = parse_quote! {
309                #[tauri::command]
310                fn greet(name: String) -> String {
311                    format!("Hello, {}!", name)
312                }
313            };
314
315            assert!(parser.is_tauri_command(&func));
316        }
317
318        #[test]
319        fn test_recognizes_command_attribute() {
320            let parser = CommandParser::new();
321            let func: ItemFn = parse_quote! {
322                #[command]
323                fn greet(name: String) -> String {
324                    format!("Hello, {}!", name)
325                }
326            };
327
328            assert!(parser.is_tauri_command(&func));
329        }
330
331        #[test]
332        fn test_rejects_non_command_function() {
333            let parser = CommandParser::new();
334            let func: ItemFn = parse_quote! {
335                fn greet(name: String) -> String {
336                    format!("Hello, {}!", name)
337                }
338            };
339
340            assert!(!parser.is_tauri_command(&func));
341        }
342
343        #[test]
344        fn test_rejects_other_attributes() {
345            let parser = CommandParser::new();
346            let func: ItemFn = parse_quote! {
347                #[derive(Debug)]
348                fn greet(name: String) -> String {
349                    format!("Hello, {}!", name)
350                }
351            };
352
353            assert!(!parser.is_tauri_command(&func));
354        }
355    }
356
357    // type_to_string tests
358    mod type_to_string {
359        use super::*;
360
361        #[test]
362        fn test_simple_type() {
363            let ty: Type = parse_quote!(String);
364            assert_eq!(CommandParser::type_to_string(&ty), "String");
365        }
366
367        #[test]
368        fn test_generic_type() {
369            let ty: Type = parse_quote!(Vec<String>);
370            assert_eq!(CommandParser::type_to_string(&ty), "Vec<String>");
371        }
372
373        #[test]
374        fn test_nested_generic() {
375            let ty: Type = parse_quote!(Vec<Option<String>>);
376            assert_eq!(CommandParser::type_to_string(&ty), "Vec<Option<String>>");
377        }
378
379        #[test]
380        fn test_multiple_generics() {
381            let ty: Type = parse_quote!(HashMap<String, i32>);
382            assert_eq!(CommandParser::type_to_string(&ty), "HashMap<String, i32>");
383        }
384
385        #[test]
386        fn test_reference_type() {
387            let ty: Type = parse_quote!(&str);
388            assert_eq!(CommandParser::type_to_string(&ty), "&str");
389        }
390
391        #[test]
392        fn test_empty_tuple() {
393            let ty: Type = parse_quote!(());
394            assert_eq!(CommandParser::type_to_string(&ty), "()");
395        }
396
397        #[test]
398        fn test_tuple_with_elements() {
399            let ty: Type = parse_quote!((String, i32));
400            assert_eq!(CommandParser::type_to_string(&ty), "(String, i32)");
401        }
402
403        #[test]
404        fn test_qualified_path() {
405            let ty: Type = parse_quote!(std::collections::HashMap<String, i32>);
406            assert_eq!(
407                CommandParser::type_to_string(&ty),
408                "std::collections::HashMap<String, i32>"
409            );
410        }
411    }
412
413    // is_optional_type tests
414    mod is_optional_type {
415        use super::*;
416
417        #[test]
418        fn test_recognizes_option() {
419            let parser = CommandParser::new();
420            let ty: Type = parse_quote!(Option<String>);
421            assert!(parser.is_optional_type(&ty));
422        }
423
424        #[test]
425        fn test_recognizes_nested_option() {
426            let parser = CommandParser::new();
427            let ty: Type = parse_quote!(Option<Vec<String>>);
428            assert!(parser.is_optional_type(&ty));
429        }
430
431        #[test]
432        fn test_rejects_non_option() {
433            let parser = CommandParser::new();
434            let ty: Type = parse_quote!(String);
435            assert!(!parser.is_optional_type(&ty));
436        }
437
438        #[test]
439        fn test_rejects_vec() {
440            let parser = CommandParser::new();
441            let ty: Type = parse_quote!(Vec<String>);
442            assert!(!parser.is_optional_type(&ty));
443        }
444    }
445
446    // is_tauri_parameter_type tests
447    mod is_tauri_parameter_type {
448        use super::*;
449
450        #[test]
451        fn test_recognizes_app_handle() {
452            let parser = CommandParser::new();
453            let ty: Type = parse_quote!(tauri::AppHandle);
454            assert!(parser.is_tauri_parameter_type(&ty));
455        }
456
457        #[test]
458        fn test_recognizes_imported_app_handle() {
459            let parser = CommandParser::new();
460            let ty: Type = parse_quote!(AppHandle);
461            assert!(parser.is_tauri_parameter_type(&ty));
462        }
463
464        #[test]
465        fn test_recognizes_window_with_generics() {
466            let parser = CommandParser::new();
467            let ty: Type = parse_quote!(Window<R>);
468            assert!(parser.is_tauri_parameter_type(&ty));
469        }
470
471        #[test]
472        fn test_recognizes_state_with_generics() {
473            let parser = CommandParser::new();
474            let ty: Type = parse_quote!(State<AppState>);
475            assert!(parser.is_tauri_parameter_type(&ty));
476        }
477
478        #[test]
479        fn test_recognizes_webview_window() {
480            let parser = CommandParser::new();
481            let ty: Type = parse_quote!(tauri::WebviewWindow);
482            assert!(parser.is_tauri_parameter_type(&ty));
483        }
484
485        #[test]
486        fn test_recognizes_imported_webview_window() {
487            let parser = CommandParser::new();
488            let ty: Type = parse_quote!(WebviewWindow);
489            assert!(parser.is_tauri_parameter_type(&ty));
490        }
491
492        #[test]
493        fn test_recognizes_ipc_request() {
494            let parser = CommandParser::new();
495            let ty: Type = parse_quote!(tauri::ipc::Request);
496            assert!(parser.is_tauri_parameter_type(&ty));
497        }
498
499        #[test]
500        fn test_recognizes_ipc_channel() {
501            let parser = CommandParser::new();
502            let ty: Type = parse_quote!(tauri::ipc::Channel<String>);
503            assert!(parser.is_tauri_parameter_type(&ty));
504        }
505
506        #[test]
507        fn test_recognizes_channel_with_generics() {
508            let parser = CommandParser::new();
509            let ty: Type = parse_quote!(Channel<ProgressUpdate>);
510            assert!(parser.is_tauri_parameter_type(&ty));
511        }
512
513        #[test]
514        fn test_rejects_user_string_type() {
515            let parser = CommandParser::new();
516            let ty: Type = parse_quote!(String);
517            assert!(!parser.is_tauri_parameter_type(&ty));
518        }
519
520        #[test]
521        fn test_rejects_user_custom_type() {
522            let parser = CommandParser::new();
523            let ty: Type = parse_quote!(User);
524            assert!(!parser.is_tauri_parameter_type(&ty));
525        }
526
527        #[test]
528        fn test_rejects_state_without_generics() {
529            let parser = CommandParser::new();
530            // User might have their own State type without generics
531            let ty: Type = parse_quote!(State);
532            assert!(!parser.is_tauri_parameter_type(&ty));
533        }
534
535        #[test]
536        fn test_rejects_window_without_generics() {
537            let parser = CommandParser::new();
538            // User might have their own Window type without generics
539            let ty: Type = parse_quote!(Window);
540            assert!(!parser.is_tauri_parameter_type(&ty));
541        }
542    }
543
544    // extract_return_type tests
545    mod extract_return_type {
546        use super::*;
547
548        #[test]
549        fn test_extract_simple_return() {
550            let parser = CommandParser::new();
551            let output: ReturnType = parse_quote!(-> String);
552            assert_eq!(parser.extract_return_type(&output), "String");
553        }
554
555        #[test]
556        fn test_extract_generic_return() {
557            let parser = CommandParser::new();
558            let output: ReturnType = parse_quote!(-> Vec<String>);
559            assert_eq!(parser.extract_return_type(&output), "Vec<String>");
560        }
561
562        #[test]
563        fn test_extract_result_return() {
564            let parser = CommandParser::new();
565            let output: ReturnType = parse_quote!(-> Result<String, Error>);
566            assert_eq!(parser.extract_return_type(&output), "Result<String, Error>");
567        }
568
569        #[test]
570        fn test_extract_default_return() {
571            let parser = CommandParser::new();
572            let output: ReturnType = parse_quote!();
573            assert_eq!(parser.extract_return_type(&output), "()");
574        }
575    }
576
577    // extract_parameters tests
578    mod extract_parameters {
579        use super::*;
580
581        #[test]
582        fn test_extract_simple_parameter() {
583            let parser = CommandParser::new();
584            let mut type_resolver = TypeResolver::new();
585            let inputs = parse_quote!(name: String);
586
587            let params = parser.extract_parameters(&inputs, &mut type_resolver);
588
589            assert_eq!(params.len(), 1);
590            assert_eq!(params[0].name, "name");
591            assert_eq!(params[0].rust_type, "String");
592            assert!(!params[0].is_optional);
593        }
594
595        #[test]
596        fn test_extract_optional_parameter() {
597            let parser = CommandParser::new();
598            let mut type_resolver = TypeResolver::new();
599            let inputs = parse_quote!(email: Option<String>);
600
601            let params = parser.extract_parameters(&inputs, &mut type_resolver);
602
603            assert_eq!(params.len(), 1);
604            assert_eq!(params[0].name, "email");
605            assert!(params[0].is_optional);
606        }
607
608        #[test]
609        fn test_extract_multiple_parameters() {
610            let parser = CommandParser::new();
611            let mut type_resolver = TypeResolver::new();
612            let inputs = parse_quote!(name: String, age: i32);
613
614            let params = parser.extract_parameters(&inputs, &mut type_resolver);
615
616            assert_eq!(params.len(), 2);
617            assert_eq!(params[0].name, "name");
618            assert_eq!(params[1].name, "age");
619        }
620
621        #[test]
622        fn test_filters_app_handle() {
623            let parser = CommandParser::new();
624            let mut type_resolver = TypeResolver::new();
625            let inputs = parse_quote!(app: AppHandle, name: String);
626
627            let params = parser.extract_parameters(&inputs, &mut type_resolver);
628
629            // AppHandle should be filtered out
630            assert_eq!(params.len(), 1);
631            assert_eq!(params[0].name, "name");
632        }
633
634        #[test]
635        fn test_filters_state() {
636            let parser = CommandParser::new();
637            let mut type_resolver = TypeResolver::new();
638            let inputs = parse_quote!(state: State<AppState>, name: String);
639
640            let params = parser.extract_parameters(&inputs, &mut type_resolver);
641
642            // State should be filtered out
643            assert_eq!(params.len(), 1);
644            assert_eq!(params[0].name, "name");
645        }
646
647        #[test]
648        fn test_filters_channel() {
649            let parser = CommandParser::new();
650            let mut type_resolver = TypeResolver::new();
651            let inputs = parse_quote!(progress: Channel<u32>, name: String);
652
653            let params = parser.extract_parameters(&inputs, &mut type_resolver);
654
655            // Channel should be filtered out
656            assert_eq!(params.len(), 1);
657            assert_eq!(params[0].name, "name");
658        }
659
660        #[test]
661        fn test_empty_parameters() {
662            let parser = CommandParser::new();
663            let mut type_resolver = TypeResolver::new();
664            let inputs = parse_quote!();
665
666            let params = parser.extract_parameters(&inputs, &mut type_resolver);
667
668            assert_eq!(params.len(), 0);
669        }
670    }
671
672    // extract_command_info tests
673    mod extract_command_info {
674        use super::*;
675        use std::path::PathBuf;
676
677        #[test]
678        fn test_extract_simple_command() {
679            let parser = CommandParser::new();
680            let mut type_resolver = TypeResolver::new();
681            let func: ItemFn = parse_quote! {
682                #[tauri::command]
683                fn greet(name: String) -> String {
684                    format!("Hello, {}!", name)
685                }
686            };
687            let path = PathBuf::from("test.rs");
688
689            let info = parser.extract_command_info(&func, &path, &mut type_resolver);
690
691            assert!(info.is_some());
692            let info = info.unwrap();
693            assert_eq!(info.name, "greet");
694            assert_eq!(info.parameters.len(), 1);
695            assert_eq!(info.return_type, "String");
696            assert!(!info.is_async);
697        }
698
699        #[test]
700        fn test_extract_async_command() {
701            let parser = CommandParser::new();
702            let mut type_resolver = TypeResolver::new();
703            let func: ItemFn = parse_quote! {
704                #[tauri::command]
705                async fn fetch_data() -> Result<String, Error> {
706                    Ok("data".to_string())
707                }
708            };
709            let path = PathBuf::from("test.rs");
710
711            let info = parser.extract_command_info(&func, &path, &mut type_resolver);
712
713            assert!(info.is_some());
714            let info = info.unwrap();
715            assert!(info.is_async);
716            assert_eq!(info.return_type, "Result<String, Error>");
717        }
718
719        #[test]
720        fn test_extract_command_with_no_return() {
721            let parser = CommandParser::new();
722            let mut type_resolver = TypeResolver::new();
723            let func: ItemFn = parse_quote! {
724                #[tauri::command]
725                fn log_message(msg: String) {
726                    println!("{}", msg);
727                }
728            };
729            let path = PathBuf::from("test.rs");
730
731            let info = parser.extract_command_info(&func, &path, &mut type_resolver);
732
733            assert!(info.is_some());
734            let info = info.unwrap();
735            assert_eq!(info.return_type, "()");
736        }
737    }
738}