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#[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 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 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 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 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 let line_number = func.sig.ident.span().start().line;
87
88 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(), serde_rename_all,
104 })
105 }
106
107 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 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 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 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 if segments.len() >= 2 {
156 if segments[0].ident == "tauri" {
158 if segments.len() == 2 {
159 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 let third = &segments[2].ident;
169 return third == "Request" || third == "Channel";
170 }
171 }
172 }
173
174 if let Some(last_segment) = segments.last() {
176 let type_ident = &last_segment.ident;
177
178 if type_ident == "AppHandle" || type_ident == "WebviewWindow" {
181 return true;
182 }
183
184 if type_ident == "Channel"
186 && matches!(
187 last_segment.arguments,
188 syn::PathArguments::AngleBracketed(_)
189 )
190 {
191 return true;
192 }
193
194 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 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 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 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 let _ = parser;
292 }
293
294 #[test]
295 fn test_default_impl() {
296 let parser = CommandParser::default();
297 let _ = parser;
299 }
300
301 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 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 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 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 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 let ty: Type = parse_quote!(Window);
540 assert!(!parser.is_tauri_parameter_type(&ty));
541 }
542 }
543
544 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 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 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 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 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 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}