1use crate::analysis::type_resolver::TypeResolver;
2use crate::models::EventInfo;
3use std::path::Path;
4use syn::{Expr, ExprMethodCall, File as SynFile, Lit};
5
6#[derive(Debug)]
8pub struct EventParser;
9
10impl EventParser {
11 pub fn new() -> Self {
12 Self
13 }
14
15 pub fn extract_events_from_ast(
21 &self,
22 ast: &SynFile,
23 file_path: &Path,
24 type_resolver: &mut TypeResolver,
25 ) -> Result<Vec<EventInfo>, Box<dyn std::error::Error>> {
26 let mut events = Vec::new();
27
28 for item in &ast.items {
30 if let syn::Item::Fn(func) = item {
31 self.extract_events_from_block(
33 &func.block.stmts,
34 file_path,
35 type_resolver,
36 &mut events,
37 );
38 }
39 }
40
41 Ok(events)
42 }
43
44 fn extract_events_from_block(
46 &self,
47 stmts: &[syn::Stmt],
48 file_path: &Path,
49 type_resolver: &mut TypeResolver,
50 events: &mut Vec<EventInfo>,
51 ) {
52 for stmt in stmts {
53 self.extract_events_from_stmt(stmt, file_path, type_resolver, events);
54 }
55 }
56
57 fn extract_events_from_stmt(
59 &self,
60 stmt: &syn::Stmt,
61 file_path: &Path,
62 type_resolver: &mut TypeResolver,
63 events: &mut Vec<EventInfo>,
64 ) {
65 match stmt {
66 syn::Stmt::Expr(expr, _) => {
67 self.extract_events_from_expr(expr, file_path, type_resolver, events);
68 }
69 syn::Stmt::Local(local) => {
70 if let Some(init) = &local.init {
71 self.extract_events_from_expr(&init.expr, file_path, type_resolver, events);
72 }
73 }
74 _ => {}
75 }
76 }
77
78 fn extract_events_from_expr(
80 &self,
81 expr: &Expr,
82 file_path: &Path,
83 type_resolver: &mut TypeResolver,
84 events: &mut Vec<EventInfo>,
85 ) {
86 match expr {
87 Expr::MethodCall(method_call) => {
88 self.handle_method_call(method_call, file_path, type_resolver, events);
89 }
90 Expr::Block(block) => {
91 self.extract_events_from_block(
92 &block.block.stmts,
93 file_path,
94 type_resolver,
95 events,
96 );
97 }
98 Expr::If(expr_if) => {
99 self.extract_events_from_block(
100 &expr_if.then_branch.stmts,
101 file_path,
102 type_resolver,
103 events,
104 );
105 if let Some((_, else_branch)) = &expr_if.else_branch {
106 self.extract_events_from_expr(else_branch, file_path, type_resolver, events);
107 }
108 }
109 Expr::Match(expr_match) => {
110 for arm in &expr_match.arms {
111 self.extract_events_from_expr(&arm.body, file_path, type_resolver, events);
112 }
113 }
114 Expr::Loop(expr_loop) => {
115 self.extract_events_from_block(
116 &expr_loop.body.stmts,
117 file_path,
118 type_resolver,
119 events,
120 );
121 }
122 Expr::While(expr_while) => {
123 self.extract_events_from_block(
124 &expr_while.body.stmts,
125 file_path,
126 type_resolver,
127 events,
128 );
129 }
130 Expr::ForLoop(expr_for) => {
131 self.extract_events_from_block(
132 &expr_for.body.stmts,
133 file_path,
134 type_resolver,
135 events,
136 );
137 }
138 Expr::Await(expr_await) => {
139 self.extract_events_from_expr(&expr_await.base, file_path, type_resolver, events);
140 }
141 Expr::Try(expr_try) => {
142 self.extract_events_from_expr(&expr_try.expr, file_path, type_resolver, events);
143 }
144 _ => {}
145 }
146 }
147
148 fn handle_method_call(
150 &self,
151 method_call: &ExprMethodCall,
152 file_path: &Path,
153 type_resolver: &mut TypeResolver,
154 events: &mut Vec<EventInfo>,
155 ) {
156 let method_name = method_call.method.to_string();
157
158 if method_name == "emit" || method_name == "emit_to" {
159 if self.is_likely_tauri_emitter(&method_call.receiver) {
161 self.extract_emit_event(method_call, file_path, type_resolver, events);
162 }
163 }
164
165 self.extract_events_from_expr(&method_call.receiver, file_path, type_resolver, events);
167 for arg in &method_call.args {
168 self.extract_events_from_expr(arg, file_path, type_resolver, events);
169 }
170 }
171
172 fn is_likely_tauri_emitter(&self, receiver: &Expr) -> bool {
175 match receiver {
176 Expr::Path(path) => {
177 let segments = &path.path.segments;
179
180 if segments.len() >= 2 && segments[0].ident == "tauri" {
182 let second = &segments[1].ident;
183 return second == "AppHandle"
184 || second == "Window"
185 || second == "WebviewWindow";
186 }
187
188 if let Some(ident) = path.path.get_ident() {
191 let name = ident.to_string();
192 return name == "app" || name == "window" || name == "webview";
195 }
196
197 for segment in segments {
199 let seg_name = segment.ident.to_string();
200 if seg_name == "AppHandle" || seg_name == "WebviewWindow" {
201 return true;
202 }
203 }
204
205 false
206 }
207 Expr::Field(field_expr) => {
208 if let syn::Member::Named(ident) = &field_expr.member {
210 let name = ident.to_string();
211 return name == "app" || name == "window" || name == "webview";
213 }
214 false
215 }
216 Expr::MethodCall(_) => {
217 true
220 }
221 _ => false,
222 }
223 }
224
225 fn extract_emit_event(
227 &self,
228 method_call: &ExprMethodCall,
229 file_path: &Path,
230 type_resolver: &mut TypeResolver,
231 events: &mut Vec<EventInfo>,
232 ) {
233 let method_name = method_call.method.to_string();
234 let args = &method_call.args;
235
236 let (event_name, payload_expr) = if method_name == "emit_to" {
237 if args.len() >= 3 {
239 (self.extract_string_literal(&args[1]), Some(&args[2]))
240 } else {
241 return;
242 }
243 } else {
244 if args.len() >= 2 {
246 (self.extract_string_literal(&args[0]), Some(&args[1]))
247 } else {
248 return;
249 }
250 };
251
252 if let Some(event_name) = event_name {
253 let payload_type = if let Some(payload_expr) = payload_expr {
254 Self::infer_payload_type(payload_expr)
255 } else {
256 "()".to_string()
257 };
258
259 let line_number = method_call.method.span().start().line;
260 let payload_type_structure = type_resolver.parse_type_structure(&payload_type);
261
262 events.push(EventInfo {
263 event_name,
264 payload_type,
265 payload_type_structure,
266 file_path: file_path.to_string_lossy().to_string(),
267 line_number,
268 });
269 }
270 }
271
272 fn extract_string_literal(&self, expr: &Expr) -> Option<String> {
274 if let Expr::Lit(expr_lit) = expr {
275 if let Lit::Str(lit_str) = &expr_lit.lit {
276 return Some(lit_str.value());
277 }
278 }
279 None
280 }
281
282 fn infer_payload_type(expr: &Expr) -> String {
285 match expr {
286 Expr::Reference(expr_ref) => {
288 Self::infer_payload_type(&expr_ref.expr)
290 }
291 Expr::Struct(expr_struct) => {
293 if let Some(segment) = expr_struct.path.segments.last() {
294 return segment.ident.to_string();
295 }
296 "unknown".to_string()
297 }
298 Expr::Path(path) => {
300 if let Some(segment) = path.path.segments.last() {
301 return segment.ident.to_string();
302 }
303 "unknown".to_string()
304 }
305 Expr::Tuple(tuple) => {
307 if tuple.elems.is_empty() {
308 return "()".to_string();
309 }
310 "tuple".to_string()
312 }
313 Expr::Lit(lit) => match &lit.lit {
315 Lit::Str(_) => "String".to_string(),
316 Lit::Int(_) => "i32".to_string(),
317 Lit::Float(_) => "f64".to_string(),
318 Lit::Bool(_) => "bool".to_string(),
319 _ => "unknown".to_string(),
320 },
321 Expr::Call(_) | Expr::MethodCall(_) => {
323 "unknown".to_string()
325 }
326 _ => "unknown".to_string(),
327 }
328 }
329}
330
331impl Default for EventParser {
332 fn default() -> Self {
333 Self::new()
334 }
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340 use syn::parse_quote;
341
342 mod extract_string_literal {
344 use super::*;
345
346 #[test]
347 fn test_extract_from_string_literal() {
348 let parser = EventParser::new();
349 let expr: Expr = parse_quote!("user-login");
350
351 let result = parser.extract_string_literal(&expr);
352 assert_eq!(result, Some("user-login".to_string()));
353 }
354
355 #[test]
356 fn test_extract_from_empty_string() {
357 let parser = EventParser::new();
358 let expr: Expr = parse_quote!("");
359
360 let result = parser.extract_string_literal(&expr);
361 assert_eq!(result, Some("".to_string()));
362 }
363
364 #[test]
365 fn test_extract_from_non_string() {
366 let parser = EventParser::new();
367 let expr: Expr = parse_quote!(42);
368
369 let result = parser.extract_string_literal(&expr);
370 assert!(result.is_none());
371 }
372
373 #[test]
374 fn test_extract_from_variable() {
375 let parser = EventParser::new();
376 let expr: Expr = parse_quote!(event_name);
377
378 let result = parser.extract_string_literal(&expr);
379 assert!(result.is_none());
380 }
381 }
382
383 mod infer_payload_type {
385 use super::*;
386
387 #[test]
388 fn test_infer_string_literal() {
389 let expr: Expr = parse_quote!("hello");
390 assert_eq!(EventParser::infer_payload_type(&expr), "String");
391 }
392
393 #[test]
394 fn test_infer_int_literal() {
395 let expr: Expr = parse_quote!(42);
396 assert_eq!(EventParser::infer_payload_type(&expr), "i32");
397 }
398
399 #[test]
400 fn test_infer_float_literal() {
401 let expr: Expr = parse_quote!(3.14);
402 assert_eq!(EventParser::infer_payload_type(&expr), "f64");
403 }
404
405 #[test]
406 fn test_infer_bool_literal() {
407 let expr: Expr = parse_quote!(true);
408 assert_eq!(EventParser::infer_payload_type(&expr), "bool");
409 }
410
411 #[test]
412 fn test_infer_struct_construction() {
413 let expr: Expr = parse_quote!(User {
414 id: 1,
415 name: "Alice"
416 });
417 assert_eq!(EventParser::infer_payload_type(&expr), "User");
418 }
419
420 #[test]
421 fn test_infer_qualified_struct() {
422 let expr: Expr = parse_quote!(models::User { id: 1 });
423 assert_eq!(EventParser::infer_payload_type(&expr), "User");
424 }
425
426 #[test]
427 fn test_infer_variable_path() {
428 let expr: Expr = parse_quote!(user_data);
429 assert_eq!(EventParser::infer_payload_type(&expr), "user_data");
430 }
431
432 #[test]
433 fn test_infer_reference() {
434 let expr: Expr = parse_quote!(&data);
435 assert_eq!(EventParser::infer_payload_type(&expr), "data");
436 }
437
438 #[test]
439 fn test_infer_empty_tuple() {
440 let expr: Expr = parse_quote!(());
441 assert_eq!(EventParser::infer_payload_type(&expr), "()");
442 }
443
444 #[test]
445 fn test_infer_non_empty_tuple() {
446 let expr: Expr = parse_quote!((1, 2, 3));
447 assert_eq!(EventParser::infer_payload_type(&expr), "tuple");
448 }
449
450 #[test]
451 fn test_infer_method_call() {
452 let expr: Expr = parse_quote!(get_user());
453 assert_eq!(EventParser::infer_payload_type(&expr), "unknown");
454 }
455
456 #[test]
457 fn test_infer_function_call() {
458 let expr: Expr = parse_quote!(calculate(x, y));
459 assert_eq!(EventParser::infer_payload_type(&expr), "unknown");
460 }
461 }
462
463 mod is_likely_tauri_emitter {
465 use super::*;
466
467 #[test]
468 fn test_recognizes_app_identifier() {
469 let parser = EventParser::new();
470 let expr: Expr = parse_quote!(app);
471 assert!(parser.is_likely_tauri_emitter(&expr));
472 }
473
474 #[test]
475 fn test_recognizes_window_identifier() {
476 let parser = EventParser::new();
477 let expr: Expr = parse_quote!(window);
478 assert!(parser.is_likely_tauri_emitter(&expr));
479 }
480
481 #[test]
482 fn test_recognizes_webview_identifier() {
483 let parser = EventParser::new();
484 let expr: Expr = parse_quote!(webview);
485 assert!(parser.is_likely_tauri_emitter(&expr));
486 }
487
488 #[test]
489 fn test_recognizes_qualified_app_handle() {
490 let parser = EventParser::new();
491 let expr: Expr = parse_quote!(tauri::AppHandle);
492 assert!(parser.is_likely_tauri_emitter(&expr));
493 }
494
495 #[test]
496 fn test_recognizes_qualified_window() {
497 let parser = EventParser::new();
498 let expr: Expr = parse_quote!(tauri::Window);
499 assert!(parser.is_likely_tauri_emitter(&expr));
500 }
501
502 #[test]
503 fn test_recognizes_qualified_webview_window() {
504 let parser = EventParser::new();
505 let expr: Expr = parse_quote!(tauri::WebviewWindow);
506 assert!(parser.is_likely_tauri_emitter(&expr));
507 }
508
509 #[test]
510 fn test_recognizes_field_access() {
511 let parser = EventParser::new();
512 let expr: Expr = parse_quote!(self.app);
513 assert!(parser.is_likely_tauri_emitter(&expr));
514 }
515
516 #[test]
517 fn test_recognizes_method_call_receiver() {
518 let parser = EventParser::new();
519 let expr: Expr = parse_quote!(obj.method());
521 assert!(parser.is_likely_tauri_emitter(&expr));
522 }
523
524 #[test]
525 fn test_rejects_user_variable() {
526 let parser = EventParser::new();
527 let expr: Expr = parse_quote!(my_data);
528 assert!(!parser.is_likely_tauri_emitter(&expr));
529 }
530
531 #[test]
532 fn test_rejects_user_type() {
533 let parser = EventParser::new();
534 let expr: Expr = parse_quote!(User);
535 assert!(!parser.is_likely_tauri_emitter(&expr));
536 }
537
538 #[test]
539 fn test_recognizes_app_handle_in_path() {
540 let parser = EventParser::new();
541 let expr: Expr = parse_quote!(tauri::AppHandle);
543 assert!(parser.is_likely_tauri_emitter(&expr));
544 }
545
546 #[test]
547 fn test_recognizes_qualified_tauri_window() {
548 let parser = EventParser::new();
549 let expr: Expr = parse_quote!(tauri::Window);
550 assert!(parser.is_likely_tauri_emitter(&expr));
551 }
552
553 #[test]
554 fn test_rejects_function_call() {
555 let parser = EventParser::new();
556 let expr: Expr = parse_quote!(get_app());
558 assert!(!parser.is_likely_tauri_emitter(&expr));
559 }
560 }
561
562 mod ast_parsing {
564 use super::*;
565 use std::path::PathBuf;
566
567 #[test]
568 fn test_extract_simple_emit() {
569 let parser = EventParser::new();
570 let mut type_resolver = TypeResolver::new();
571 let ast: SynFile = parse_quote! {
572 fn notify_user() {
573 app.emit("user-login", "Alice");
574 }
575 };
576 let path = PathBuf::from("test.rs");
577
578 let events = parser
579 .extract_events_from_ast(&ast, &path, &mut type_resolver)
580 .unwrap();
581
582 assert_eq!(events.len(), 1);
583 assert_eq!(events[0].event_name, "user-login");
584 assert_eq!(events[0].payload_type, "String");
585 }
586
587 #[test]
588 fn test_extract_emit_with_struct() {
589 let parser = EventParser::new();
590 let mut type_resolver = TypeResolver::new();
591 let ast: SynFile = parse_quote! {
592 fn notify_user() {
593 app.emit("user-updated", User { id: 1, name: "Alice" });
594 }
595 };
596 let path = PathBuf::from("test.rs");
597
598 let events = parser
599 .extract_events_from_ast(&ast, &path, &mut type_resolver)
600 .unwrap();
601
602 assert_eq!(events.len(), 1);
603 assert_eq!(events[0].event_name, "user-updated");
604 assert_eq!(events[0].payload_type, "User");
605 }
606
607 #[test]
608 fn test_extract_emit_to() {
609 let parser = EventParser::new();
610 let mut type_resolver = TypeResolver::new();
611 let ast: SynFile = parse_quote! {
612 fn notify_window() {
613 app.emit_to("main", "progress", 50);
614 }
615 };
616 let path = PathBuf::from("test.rs");
617
618 let events = parser
619 .extract_events_from_ast(&ast, &path, &mut type_resolver)
620 .unwrap();
621
622 assert_eq!(events.len(), 1);
623 assert_eq!(events[0].event_name, "progress");
624 assert_eq!(events[0].payload_type, "i32");
625 }
626
627 #[test]
628 fn test_extract_multiple_emits() {
629 let parser = EventParser::new();
630 let mut type_resolver = TypeResolver::new();
631 let ast: SynFile = parse_quote! {
632 fn notify_all() {
633 app.emit("event1", "data1");
634 window.emit("event2", 42);
635 }
636 };
637 let path = PathBuf::from("test.rs");
638
639 let events = parser
640 .extract_events_from_ast(&ast, &path, &mut type_resolver)
641 .unwrap();
642
643 assert_eq!(events.len(), 2);
644 assert_eq!(events[0].event_name, "event1");
645 assert_eq!(events[1].event_name, "event2");
646 }
647
648 #[test]
649 fn test_extract_emit_in_if_block() {
650 let parser = EventParser::new();
651 let mut type_resolver = TypeResolver::new();
652 let ast: SynFile = parse_quote! {
653 fn conditional_notify() {
654 if condition {
655 app.emit("success", true);
656 } else {
657 app.emit("failure", false);
658 }
659 }
660 };
661 let path = PathBuf::from("test.rs");
662
663 let events = parser
664 .extract_events_from_ast(&ast, &path, &mut type_resolver)
665 .unwrap();
666
667 assert_eq!(events.len(), 2);
668 assert_eq!(events[0].event_name, "success");
669 assert_eq!(events[1].event_name, "failure");
670 }
671
672 #[test]
673 fn test_extract_emit_in_loop() {
674 let parser = EventParser::new();
675 let mut type_resolver = TypeResolver::new();
676 let ast: SynFile = parse_quote! {
677 fn loop_notify() {
678 for i in 0..10 {
679 app.emit("iteration", i);
680 }
681 }
682 };
683 let path = PathBuf::from("test.rs");
684
685 let events = parser
686 .extract_events_from_ast(&ast, &path, &mut type_resolver)
687 .unwrap();
688
689 assert_eq!(events.len(), 1);
690 assert_eq!(events[0].event_name, "iteration");
691 }
692
693 #[test]
694 fn test_extract_emit_in_match() {
695 let parser = EventParser::new();
696 let mut type_resolver = TypeResolver::new();
697 let ast: SynFile = parse_quote! {
698 fn match_notify() {
699 match result {
700 Ok(val) => app.emit("success", val),
701 Err(e) => app.emit("error", e),
702 }
703 }
704 };
705 let path = PathBuf::from("test.rs");
706
707 let events = parser
708 .extract_events_from_ast(&ast, &path, &mut type_resolver)
709 .unwrap();
710
711 assert_eq!(events.len(), 2);
712 assert_eq!(events[0].event_name, "success");
713 assert_eq!(events[1].event_name, "error");
714 }
715
716 #[test]
717 fn test_no_events_in_non_emit_function() {
718 let parser = EventParser::new();
719 let mut type_resolver = TypeResolver::new();
720 let ast: SynFile = parse_quote! {
721 fn regular_function() {
722 let x = 42;
723 println!("Hello");
724 }
725 };
726 let path = PathBuf::from("test.rs");
727
728 let events = parser
729 .extract_events_from_ast(&ast, &path, &mut type_resolver)
730 .unwrap();
731
732 assert_eq!(events.len(), 0);
733 }
734
735 #[test]
736 fn test_ignores_user_emit_method() {
737 let parser = EventParser::new();
738 let mut type_resolver = TypeResolver::new();
739 let ast: SynFile = parse_quote! {
740 fn user_emit() {
741 my_object.emit("not-a-tauri-event", data);
742 }
743 };
744 let path = PathBuf::from("test.rs");
745
746 let events = parser
747 .extract_events_from_ast(&ast, &path, &mut type_resolver)
748 .unwrap();
749
750 assert_eq!(events.len(), 0);
752 }
753 }
754}