tauri_typegen/analysis/
event_parser.rs

1use crate::analysis::type_resolver::TypeResolver;
2use crate::models::EventInfo;
3use std::path::Path;
4use syn::{Expr, ExprMethodCall, File as SynFile, Lit};
5
6/// Parser for Tauri event emissions
7#[derive(Debug)]
8pub struct EventParser;
9
10impl EventParser {
11    pub fn new() -> Self {
12        Self
13    }
14
15    /// Extract event emissions from a cached AST
16    /// Looks for patterns like:
17    /// - app.emit("event-name", payload)
18    /// - window.emit("event-name", payload)
19    /// - app.emit_to("label", "event-name", payload)
20    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        // Visit all items in the AST looking for emit calls
29        for item in &ast.items {
30            if let syn::Item::Fn(func) = item {
31                // Search within function bodies
32                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    /// Recursively search through statements for emit calls
45    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    /// Extract events from a single statement
58    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    /// Extract events from an expression
79    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    /// Handle method call expressions, looking for emit() and emit_to()
149    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            // Check if the receiver looks like app/window (basic heuristic)
160            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        // Recursively check receiver and arguments for nested emits
166        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    /// Check if the receiver expression is likely a Tauri emitter (app/window)
173    /// This checks the expression pattern to identify Tauri framework types
174    fn is_likely_tauri_emitter(&self, receiver: &Expr) -> bool {
175        match receiver {
176            Expr::Path(path) => {
177                // Check if this is a path that could be a Tauri type
178                let segments = &path.path.segments;
179
180                // Check for fully qualified paths: tauri::AppHandle, tauri::WebviewWindow
181                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                // Check for simple identifiers - be conservative
189                // Only match very specific common patterns for Tauri types
190                if let Some(ident) = path.path.get_ident() {
191                    let name = ident.to_string();
192                    // Only match exact common parameter names used in Tauri commands
193                    // Avoid matching user variables with similar names
194                    return name == "app" || name == "window" || name == "webview";
195                }
196
197                // For complex paths, check if any segment looks like a Tauri type
198                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                // Check field access like self.app
209                if let syn::Member::Named(ident) = &field_expr.member {
210                    let name = ident.to_string();
211                    // Only match exact common field names
212                    return name == "app" || name == "window" || name == "webview";
213                }
214                false
215            }
216            Expr::MethodCall(_) => {
217                // Could be something like get_app().emit()
218                // Be permissive here since method calls that return handles are common
219                true
220            }
221            _ => false,
222        }
223    }
224
225    /// Extract event information from an emit or emit_to call
226    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            // emit_to(label, event_name, payload)
238            if args.len() >= 3 {
239                (self.extract_string_literal(&args[1]), Some(&args[2]))
240            } else {
241                return;
242            }
243        } else {
244            // emit(event_name, payload)
245            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    /// Extract a string literal from an expression
273    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    /// Infer the type of the payload expression
283    /// This is a best-effort heuristic based on the expression structure
284    fn infer_payload_type(expr: &Expr) -> String {
285        match expr {
286            // Reference to a variable: &some_var
287            Expr::Reference(expr_ref) => {
288                // Try to infer from the inner expression
289                Self::infer_payload_type(&expr_ref.expr)
290            }
291            // Struct construction: User { ... }
292            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            // Variable or path: some_var, module::Type
299            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            // Tuple: (a, b, c)
306            Expr::Tuple(tuple) => {
307                if tuple.elems.is_empty() {
308                    return "()".to_string();
309                }
310                // For now, just mark as tuple
311                "tuple".to_string()
312            }
313            // Literal values
314            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            // Method or function calls
322            Expr::Call(_) | Expr::MethodCall(_) => {
323                // Can't easily infer return type without type checker
324                "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    // extract_string_literal tests
343    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    // infer_payload_type tests
384    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    // is_likely_tauri_emitter tests
464    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            // Method calls are considered permissive emitters (could return AppHandle)
520            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            // Complex path with AppHandle segment
542            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            // Function calls (not method calls) are not automatically emitters
557            let expr: Expr = parse_quote!(get_app());
558            assert!(!parser.is_likely_tauri_emitter(&expr));
559        }
560    }
561
562    // Integration tests with AST parsing
563    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            // Should not detect this as a Tauri event since my_object is not a Tauri emitter
751            assert_eq!(events.len(), 0);
752        }
753    }
754}