diff --git a/CHANGELOG.md b/CHANGELOG.md index f9975f2..e2ed748 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.4.2] - 2026-02-15 + +### Fixed +- **EventParser**: Fixed bug where types were not discovered when moving emitter to external function + ## [0.4.1] - 2026-02-05 ### Added diff --git a/Cargo.toml b/Cargo.toml index 17457c3..74841bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-typegen" -version = "0.4.1" +version = "0.4.2" authors = [ "Stefan Poindl" ] description = "A rust crate that automatically generates TypeScript models and bindings from your Tauri commands" edition = "2021" diff --git a/src/analysis/event_parser.rs b/src/analysis/event_parser.rs index 35c97ca..b31e28f 100644 --- a/src/analysis/event_parser.rs +++ b/src/analysis/event_parser.rs @@ -1,12 +1,16 @@ use crate::analysis::type_resolver::TypeResolver; use crate::models::EventInfo; +use std::collections::HashMap; use std::path::Path; -use syn::{Expr, ExprMethodCall, File as SynFile, Lit}; +use syn::{Expr, ExprMethodCall, File as SynFile, FnArg, Lit, Pat, Type}; /// Parser for Tauri event emissions #[derive(Debug)] pub struct EventParser; +/// Simple symbol table to track variable names to their types +type SymbolTable = HashMap; + impl EventParser { pub fn new() -> Self { Self @@ -28,12 +32,17 @@ impl EventParser { // Visit all items in the AST looking for emit calls for item in &ast.items { if let syn::Item::Fn(func) = item { - // Search within function bodies + // Build symbol table from function parameters + let mut symbols = SymbolTable::new(); + self.extract_param_types(&func.sig.inputs, &mut symbols); + + // Search within function bodies with symbol context self.extract_events_from_block( &func.block.stmts, file_path, type_resolver, &mut events, + &mut symbols, ); } } @@ -41,6 +50,42 @@ impl EventParser { Ok(events) } + /// Extract parameter types from function signature into symbol table + fn extract_param_types( + &self, + inputs: &syn::punctuated::Punctuated, + symbols: &mut SymbolTable, + ) { + for arg in inputs { + if let FnArg::Typed(pat_type) = arg { + if let Pat::Ident(pat_ident) = &*pat_type.pat { + let param_name = pat_ident.ident.to_string(); + let param_type = self.extract_type_name(&pat_type.ty); + symbols.insert(param_name, param_type); + } + } + } + } + + /// Extract the type name from a Type, handling references and generic wrappers + fn extract_type_name(&self, ty: &Type) -> String { + match ty { + Type::Reference(type_ref) => { + // Handle &T and &mut T - extract the inner type + self.extract_type_name(&type_ref.elem) + } + Type::Path(type_path) => { + // Get the last segment of the path (the actual type name) + if let Some(segment) = type_path.path.segments.last() { + segment.ident.to_string() + } else { + "unknown".to_string() + } + } + _ => "unknown".to_string(), + } + } + /// Recursively search through statements for emit calls fn extract_events_from_block( &self, @@ -48,9 +93,10 @@ impl EventParser { file_path: &Path, type_resolver: &mut TypeResolver, events: &mut Vec, + symbols: &mut SymbolTable, ) { for stmt in stmts { - self.extract_events_from_stmt(stmt, file_path, type_resolver, events); + self.extract_events_from_stmt(stmt, file_path, type_resolver, events, symbols); } } @@ -61,18 +107,92 @@ impl EventParser { file_path: &Path, type_resolver: &mut TypeResolver, events: &mut Vec, + symbols: &mut SymbolTable, ) { match stmt { syn::Stmt::Expr(expr, _) => { - self.extract_events_from_expr(expr, file_path, type_resolver, events); + self.extract_events_from_expr(expr, file_path, type_resolver, events, symbols); } syn::Stmt::Local(local) => { + // Track let bindings with explicit types + self.extract_local_binding(local, symbols); + if let Some(init) = &local.init { - self.extract_events_from_expr(&init.expr, file_path, type_resolver, events); + self.extract_events_from_expr( + &init.expr, + file_path, + type_resolver, + events, + symbols, + ); + } + } + _ => {} + } + } + + /// Extract variable binding from let statement + fn extract_local_binding(&self, local: &syn::Local, symbols: &mut SymbolTable) { + // Handle let var: Type = ... + if let Pat::Ident(pat_ident) = &local.pat { + let var_name = pat_ident.ident.to_string(); + + // If there's an explicit type annotation, use it + if let Some(local_init) = &local.init { + // Try to infer type from the initialization expression + let inferred_type = self.infer_type_from_init(&local_init.expr, symbols); + if inferred_type != "unknown" { + symbols.insert(var_name, inferred_type); + } + } + } + + // Handle let var: Type (with type annotation via local.ty if it were available) + // syn's Local doesn't have direct type annotation in newer versions, + // but we can handle patterns with type annotations + if let Pat::Type(pat_type) = &local.pat { + if let Pat::Ident(pat_ident) = &*pat_type.pat { + let var_name = pat_ident.ident.to_string(); + let var_type = self.extract_type_name(&pat_type.ty); + symbols.insert(var_name, var_type); + } + } + } + + /// Try to infer type from initialization expression + fn infer_type_from_init(&self, expr: &Expr, symbols: &SymbolTable) -> String { + match expr { + Expr::Struct(expr_struct) => { + // Struct construction: Type { ... } + if let Some(segment) = expr_struct.path.segments.last() { + return segment.ident.to_string(); + } + } + Expr::Call(call) => { + // Function call like Type::new() or Type::default() + if let Expr::Path(path) = &*call.func { + // Check for Type::method() pattern + if path.path.segments.len() >= 2 { + return path.path.segments[0].ident.to_string(); + } } } + Expr::Path(path) => { + // Variable reference - look up in symbol table + if let Some(ident) = path.path.get_ident() { + let name = ident.to_string(); + if let Some(typ) = symbols.get(&name) { + return typ.clone(); + } + } + } + Expr::Reference(expr_ref) => { + // &expr - recurse into inner expression + return self.infer_type_from_init(&expr_ref.expr, symbols); + } _ => {} } + "unknown".to_string() } /// Extract events from an expression @@ -82,10 +202,11 @@ impl EventParser { file_path: &Path, type_resolver: &mut TypeResolver, events: &mut Vec, + symbols: &mut SymbolTable, ) { match expr { Expr::MethodCall(method_call) => { - self.handle_method_call(method_call, file_path, type_resolver, events); + self.handle_method_call(method_call, file_path, type_resolver, events, symbols); } Expr::Block(block) => { self.extract_events_from_block( @@ -93,6 +214,7 @@ impl EventParser { file_path, type_resolver, events, + symbols, ); } Expr::If(expr_if) => { @@ -101,14 +223,27 @@ impl EventParser { file_path, type_resolver, events, + symbols, ); if let Some((_, else_branch)) = &expr_if.else_branch { - self.extract_events_from_expr(else_branch, file_path, type_resolver, events); + self.extract_events_from_expr( + else_branch, + file_path, + type_resolver, + events, + symbols, + ); } } Expr::Match(expr_match) => { for arm in &expr_match.arms { - self.extract_events_from_expr(&arm.body, file_path, type_resolver, events); + self.extract_events_from_expr( + &arm.body, + file_path, + type_resolver, + events, + symbols, + ); } } Expr::Loop(expr_loop) => { @@ -117,6 +252,7 @@ impl EventParser { file_path, type_resolver, events, + symbols, ); } Expr::While(expr_while) => { @@ -125,6 +261,7 @@ impl EventParser { file_path, type_resolver, events, + symbols, ); } Expr::ForLoop(expr_for) => { @@ -133,13 +270,26 @@ impl EventParser { file_path, type_resolver, events, + symbols, ); } Expr::Await(expr_await) => { - self.extract_events_from_expr(&expr_await.base, file_path, type_resolver, events); + self.extract_events_from_expr( + &expr_await.base, + file_path, + type_resolver, + events, + symbols, + ); } Expr::Try(expr_try) => { - self.extract_events_from_expr(&expr_try.expr, file_path, type_resolver, events); + self.extract_events_from_expr( + &expr_try.expr, + file_path, + type_resolver, + events, + symbols, + ); } _ => {} } @@ -152,20 +302,27 @@ impl EventParser { file_path: &Path, type_resolver: &mut TypeResolver, events: &mut Vec, + symbols: &mut SymbolTable, ) { let method_name = method_call.method.to_string(); if method_name == "emit" || method_name == "emit_to" { // Check if the receiver looks like app/window (basic heuristic) if self.is_likely_tauri_emitter(&method_call.receiver) { - self.extract_emit_event(method_call, file_path, type_resolver, events); + self.extract_emit_event(method_call, file_path, type_resolver, events, symbols); } } // Recursively check receiver and arguments for nested emits - self.extract_events_from_expr(&method_call.receiver, file_path, type_resolver, events); + self.extract_events_from_expr( + &method_call.receiver, + file_path, + type_resolver, + events, + symbols, + ); for arg in &method_call.args { - self.extract_events_from_expr(arg, file_path, type_resolver, events); + self.extract_events_from_expr(arg, file_path, type_resolver, events, symbols); } } @@ -229,6 +386,7 @@ impl EventParser { file_path: &Path, type_resolver: &mut TypeResolver, events: &mut Vec, + symbols: &SymbolTable, ) { let method_name = method_call.method.to_string(); let args = &method_call.args; @@ -251,7 +409,7 @@ impl EventParser { if let Some(event_name) = event_name { let payload_type = if let Some(payload_expr) = payload_expr { - Self::infer_payload_type(payload_expr) + self.infer_payload_type(payload_expr, symbols) } else { "()".to_string() }; @@ -280,13 +438,13 @@ impl EventParser { } /// Infer the type of the payload expression - /// This is a best-effort heuristic based on the expression structure - fn infer_payload_type(expr: &Expr) -> String { + /// Uses symbol table to resolve variable names to their types + fn infer_payload_type(&self, expr: &Expr, symbols: &SymbolTable) -> String { match expr { // Reference to a variable: &some_var Expr::Reference(expr_ref) => { // Try to infer from the inner expression - Self::infer_payload_type(&expr_ref.expr) + self.infer_payload_type(&expr_ref.expr, symbols) } // Struct construction: User { ... } Expr::Struct(expr_struct) => { @@ -297,6 +455,16 @@ impl EventParser { } // Variable or path: some_var, module::Type Expr::Path(path) => { + if let Some(ident) = path.path.get_ident() { + let name = ident.to_string(); + // Look up variable in symbol table + if let Some(typ) = symbols.get(&name) { + return typ.clone(); + } + // Fallback: might be a type name used directly (like Status::Active) + return name; + } + // For qualified paths, return the last segment if let Some(segment) = path.path.segments.last() { return segment.ident.to_string(); } @@ -318,8 +486,18 @@ impl EventParser { Lit::Bool(_) => "bool".to_string(), _ => "unknown".to_string(), }, - // Method or function calls - Expr::Call(_) | Expr::MethodCall(_) => { + // Clone call: var.clone() + Expr::MethodCall(method_call) => { + let method_name = method_call.method.to_string(); + if method_name == "clone" { + // Try to infer type from the receiver + return self.infer_payload_type(&method_call.receiver, symbols); + } + // Can't easily infer return type without type checker + "unknown".to_string() + } + // Function calls + Expr::Call(_) => { // Can't easily infer return type without type checker "unknown".to_string() } @@ -346,37 +524,25 @@ mod tests { #[test] fn test_extract_from_string_literal() { let parser = EventParser::new(); - let expr: Expr = parse_quote!("user-login"); - - let result = parser.extract_string_literal(&expr); - assert_eq!(result, Some("user-login".to_string())); - } - - #[test] - fn test_extract_from_empty_string() { - let parser = EventParser::new(); - let expr: Expr = parse_quote!(""); - - let result = parser.extract_string_literal(&expr); - assert_eq!(result, Some("".to_string())); + let expr: Expr = parse_quote!("hello"); + assert_eq!( + parser.extract_string_literal(&expr), + Some("hello".to_string()) + ); } #[test] fn test_extract_from_non_string() { let parser = EventParser::new(); let expr: Expr = parse_quote!(42); - - let result = parser.extract_string_literal(&expr); - assert!(result.is_none()); + assert_eq!(parser.extract_string_literal(&expr), None); } #[test] fn test_extract_from_variable() { let parser = EventParser::new(); - let expr: Expr = parse_quote!(event_name); - - let result = parser.extract_string_literal(&expr); - assert!(result.is_none()); + let expr: Expr = parse_quote!(some_var); + assert_eq!(parser.extract_string_literal(&expr), None); } } @@ -385,78 +551,139 @@ mod tests { use super::*; #[test] - fn test_infer_string_literal() { - let expr: Expr = parse_quote!("hello"); - assert_eq!(EventParser::infer_payload_type(&expr), "String"); + fn test_infer_from_struct_construction() { + let parser = EventParser::new(); + let symbols = SymbolTable::new(); + let expr: Expr = parse_quote!(User { + name: "test".to_string() + }); + assert_eq!(parser.infer_payload_type(&expr, &symbols), "User"); } #[test] - fn test_infer_int_literal() { - let expr: Expr = parse_quote!(42); - assert_eq!(EventParser::infer_payload_type(&expr), "i32"); + fn test_infer_from_variable_with_symbol_table() { + let parser = EventParser::new(); + let mut symbols = SymbolTable::new(); + symbols.insert("update".to_string(), "ProgressUpdate".to_string()); + + let expr: Expr = parse_quote!(update); + assert_eq!(parser.infer_payload_type(&expr, &symbols), "ProgressUpdate"); } #[test] - fn test_infer_float_literal() { - let expr: Expr = parse_quote!(3.14); - assert_eq!(EventParser::infer_payload_type(&expr), "f64"); + fn test_infer_from_reference_with_symbol_table() { + let parser = EventParser::new(); + let mut symbols = SymbolTable::new(); + symbols.insert("update".to_string(), "ProgressUpdate".to_string()); + + let expr: Expr = parse_quote!(&update); + assert_eq!(parser.infer_payload_type(&expr, &symbols), "ProgressUpdate"); } #[test] - fn test_infer_bool_literal() { - let expr: Expr = parse_quote!(true); - assert_eq!(EventParser::infer_payload_type(&expr), "bool"); + fn test_infer_from_clone_with_symbol_table() { + let parser = EventParser::new(); + let mut symbols = SymbolTable::new(); + symbols.insert("update".to_string(), "ProgressUpdate".to_string()); + + let expr: Expr = parse_quote!(update.clone()); + assert_eq!(parser.infer_payload_type(&expr, &symbols), "ProgressUpdate"); } #[test] - fn test_infer_struct_construction() { - let expr: Expr = parse_quote!(User { - id: 1, - name: "Alice" - }); - assert_eq!(EventParser::infer_payload_type(&expr), "User"); + fn test_infer_from_variable_without_symbol() { + let parser = EventParser::new(); + let symbols = SymbolTable::new(); + let expr: Expr = parse_quote!(some_var); + // Without symbol table entry, returns the variable name + assert_eq!(parser.infer_payload_type(&expr, &symbols), "some_var"); } #[test] - fn test_infer_qualified_struct() { - let expr: Expr = parse_quote!(models::User { id: 1 }); - assert_eq!(EventParser::infer_payload_type(&expr), "User"); + fn test_infer_from_string_literal() { + let parser = EventParser::new(); + let symbols = SymbolTable::new(); + let expr: Expr = parse_quote!("hello"); + assert_eq!(parser.infer_payload_type(&expr, &symbols), "String"); } #[test] - fn test_infer_variable_path() { - let expr: Expr = parse_quote!(user_data); - assert_eq!(EventParser::infer_payload_type(&expr), "user_data"); + fn test_infer_from_integer_literal() { + let parser = EventParser::new(); + let symbols = SymbolTable::new(); + let expr: Expr = parse_quote!(42); + assert_eq!(parser.infer_payload_type(&expr, &symbols), "i32"); } #[test] - fn test_infer_reference() { - let expr: Expr = parse_quote!(&data); - assert_eq!(EventParser::infer_payload_type(&expr), "data"); + fn test_infer_from_bool_literal() { + let parser = EventParser::new(); + let symbols = SymbolTable::new(); + let expr: Expr = parse_quote!(true); + assert_eq!(parser.infer_payload_type(&expr, &symbols), "bool"); } #[test] - fn test_infer_empty_tuple() { + fn test_infer_from_empty_tuple() { + let parser = EventParser::new(); + let symbols = SymbolTable::new(); let expr: Expr = parse_quote!(()); - assert_eq!(EventParser::infer_payload_type(&expr), "()"); + assert_eq!(parser.infer_payload_type(&expr, &symbols), "()"); + } + } + + // extract_param_types tests + mod extract_param_types { + use super::*; + + #[test] + fn test_extract_simple_param() { + let parser = EventParser::new(); + let func: syn::ItemFn = parse_quote! { + fn test(name: String) {} + }; + let mut symbols = SymbolTable::new(); + parser.extract_param_types(&func.sig.inputs, &mut symbols); + + assert_eq!(symbols.get("name"), Some(&"String".to_string())); } #[test] - fn test_infer_non_empty_tuple() { - let expr: Expr = parse_quote!((1, 2, 3)); - assert_eq!(EventParser::infer_payload_type(&expr), "tuple"); + fn test_extract_reference_param() { + let parser = EventParser::new(); + let func: syn::ItemFn = parse_quote! { + fn test(update: &ProgressUpdate) {} + }; + let mut symbols = SymbolTable::new(); + parser.extract_param_types(&func.sig.inputs, &mut symbols); + + assert_eq!(symbols.get("update"), Some(&"ProgressUpdate".to_string())); } #[test] - fn test_infer_method_call() { - let expr: Expr = parse_quote!(get_user()); - assert_eq!(EventParser::infer_payload_type(&expr), "unknown"); + fn test_extract_mutable_reference_param() { + let parser = EventParser::new(); + let func: syn::ItemFn = parse_quote! { + fn test(update: &mut ProgressUpdate) {} + }; + let mut symbols = SymbolTable::new(); + parser.extract_param_types(&func.sig.inputs, &mut symbols); + + assert_eq!(symbols.get("update"), Some(&"ProgressUpdate".to_string())); } #[test] - fn test_infer_function_call() { - let expr: Expr = parse_quote!(calculate(x, y)); - assert_eq!(EventParser::infer_payload_type(&expr), "unknown"); + fn test_extract_multiple_params() { + let parser = EventParser::new(); + let func: syn::ItemFn = parse_quote! { + fn test(app: AppHandle, update: &ProgressUpdate, count: i32) {} + }; + let mut symbols = SymbolTable::new(); + parser.extract_param_types(&func.sig.inputs, &mut symbols); + + assert_eq!(symbols.get("app"), Some(&"AppHandle".to_string())); + assert_eq!(symbols.get("update"), Some(&"ProgressUpdate".to_string())); + assert_eq!(symbols.get("count"), Some(&"i32".to_string())); } } @@ -465,290 +692,146 @@ mod tests { use super::*; #[test] - fn test_recognizes_app_identifier() { + fn test_app_identifier() { let parser = EventParser::new(); let expr: Expr = parse_quote!(app); assert!(parser.is_likely_tauri_emitter(&expr)); } #[test] - fn test_recognizes_window_identifier() { + fn test_window_identifier() { let parser = EventParser::new(); let expr: Expr = parse_quote!(window); assert!(parser.is_likely_tauri_emitter(&expr)); } #[test] - fn test_recognizes_webview_identifier() { + fn test_webview_identifier() { let parser = EventParser::new(); let expr: Expr = parse_quote!(webview); assert!(parser.is_likely_tauri_emitter(&expr)); } #[test] - fn test_recognizes_qualified_app_handle() { + fn test_qualified_tauri_app_handle() { let parser = EventParser::new(); let expr: Expr = parse_quote!(tauri::AppHandle); assert!(parser.is_likely_tauri_emitter(&expr)); } #[test] - fn test_recognizes_qualified_window() { + fn test_qualified_tauri_window() { let parser = EventParser::new(); let expr: Expr = parse_quote!(tauri::Window); assert!(parser.is_likely_tauri_emitter(&expr)); } #[test] - fn test_recognizes_qualified_webview_window() { - let parser = EventParser::new(); - let expr: Expr = parse_quote!(tauri::WebviewWindow); - assert!(parser.is_likely_tauri_emitter(&expr)); - } - - #[test] - fn test_recognizes_field_access() { + fn test_self_app_field() { let parser = EventParser::new(); let expr: Expr = parse_quote!(self.app); assert!(parser.is_likely_tauri_emitter(&expr)); } #[test] - fn test_recognizes_method_call_receiver() { - let parser = EventParser::new(); - // Method calls are considered permissive emitters (could return AppHandle) - let expr: Expr = parse_quote!(obj.method()); - assert!(parser.is_likely_tauri_emitter(&expr)); - } - - #[test] - fn test_rejects_user_variable() { - let parser = EventParser::new(); - let expr: Expr = parse_quote!(my_data); - assert!(!parser.is_likely_tauri_emitter(&expr)); - } - - #[test] - fn test_rejects_user_type() { + fn test_random_variable_not_emitter() { let parser = EventParser::new(); - let expr: Expr = parse_quote!(User); + let expr: Expr = parse_quote!(some_var); assert!(!parser.is_likely_tauri_emitter(&expr)); } #[test] - fn test_recognizes_app_handle_in_path() { + fn test_method_call_is_permissive() { let parser = EventParser::new(); - // Complex path with AppHandle segment - let expr: Expr = parse_quote!(tauri::AppHandle); + // Method calls like self.get_app() are permissive + let expr: Expr = parse_quote!(self.get_app()); assert!(parser.is_likely_tauri_emitter(&expr)); } - - #[test] - fn test_recognizes_qualified_tauri_window() { - let parser = EventParser::new(); - let expr: Expr = parse_quote!(tauri::Window); - assert!(parser.is_likely_tauri_emitter(&expr)); - } - - #[test] - fn test_rejects_function_call() { - let parser = EventParser::new(); - // Function calls (not method calls) are not automatically emitters - let expr: Expr = parse_quote!(get_app()); - assert!(!parser.is_likely_tauri_emitter(&expr)); - } } - // Integration tests with AST parsing - mod ast_parsing { + // Integration tests for event extraction + mod event_extraction { use super::*; - use std::path::PathBuf; - - #[test] - fn test_extract_simple_emit() { - let parser = EventParser::new(); - let mut type_resolver = TypeResolver::new(); - let ast: SynFile = parse_quote! { - fn notify_user() { - app.emit("user-login", "Alice"); - } - }; - let path = PathBuf::from("test.rs"); - - let events = parser - .extract_events_from_ast(&ast, &path, &mut type_resolver) - .unwrap(); - - assert_eq!(events.len(), 1); - assert_eq!(events[0].event_name, "user-login"); - assert_eq!(events[0].payload_type, "String"); - } + use crate::analysis::type_resolver::TypeResolver; #[test] - fn test_extract_emit_with_struct() { + fn test_extract_event_with_struct_payload() { let parser = EventParser::new(); let mut type_resolver = TypeResolver::new(); - let ast: SynFile = parse_quote! { - fn notify_user() { - app.emit("user-updated", User { id: 1, name: "Alice" }); - } - }; - let path = PathBuf::from("test.rs"); - let events = parser - .extract_events_from_ast(&ast, &path, &mut type_resolver) - .unwrap(); - - assert_eq!(events.len(), 1); - assert_eq!(events[0].event_name, "user-updated"); - assert_eq!(events[0].payload_type, "User"); - } - - #[test] - fn test_extract_emit_to() { - let parser = EventParser::new(); - let mut type_resolver = TypeResolver::new(); - let ast: SynFile = parse_quote! { - fn notify_window() { - app.emit_to("main", "progress", 50); + let file: SynFile = parse_quote! { + fn emit_progress(app: AppHandle) { + app.emit("progress", ProgressUpdate { value: 50 }).unwrap(); } }; - let path = PathBuf::from("test.rs"); let events = parser - .extract_events_from_ast(&ast, &path, &mut type_resolver) + .extract_events_from_ast(&file, Path::new("test.rs"), &mut type_resolver) .unwrap(); assert_eq!(events.len(), 1); assert_eq!(events[0].event_name, "progress"); - assert_eq!(events[0].payload_type, "i32"); + assert_eq!(events[0].payload_type, "ProgressUpdate"); } #[test] - fn test_extract_multiple_emits() { + fn test_extract_event_with_variable_payload_from_param() { let parser = EventParser::new(); let mut type_resolver = TypeResolver::new(); - let ast: SynFile = parse_quote! { - fn notify_all() { - app.emit("event1", "data1"); - window.emit("event2", 42); - } - }; - let path = PathBuf::from("test.rs"); - - let events = parser - .extract_events_from_ast(&ast, &path, &mut type_resolver) - .unwrap(); - - assert_eq!(events.len(), 2); - assert_eq!(events[0].event_name, "event1"); - assert_eq!(events[1].event_name, "event2"); - } - - #[test] - fn test_extract_emit_in_if_block() { - let parser = EventParser::new(); - let mut type_resolver = TypeResolver::new(); - let ast: SynFile = parse_quote! { - fn conditional_notify() { - if condition { - app.emit("success", true); - } else { - app.emit("failure", false); - } - } - }; - let path = PathBuf::from("test.rs"); - - let events = parser - .extract_events_from_ast(&ast, &path, &mut type_resolver) - .unwrap(); - assert_eq!(events.len(), 2); - assert_eq!(events[0].event_name, "success"); - assert_eq!(events[1].event_name, "failure"); - } - - #[test] - fn test_extract_emit_in_loop() { - let parser = EventParser::new(); - let mut type_resolver = TypeResolver::new(); - let ast: SynFile = parse_quote! { - fn loop_notify() { - for i in 0..10 { - app.emit("iteration", i); - } + let file: SynFile = parse_quote! { + fn emit_externally(app: AppHandle, update: &ProgressUpdate) { + app.emit("progress-update", update).unwrap(); } }; - let path = PathBuf::from("test.rs"); let events = parser - .extract_events_from_ast(&ast, &path, &mut type_resolver) + .extract_events_from_ast(&file, Path::new("test.rs"), &mut type_resolver) .unwrap(); assert_eq!(events.len(), 1); - assert_eq!(events[0].event_name, "iteration"); + assert_eq!(events[0].event_name, "progress-update"); + assert_eq!(events[0].payload_type, "ProgressUpdate"); } #[test] - fn test_extract_emit_in_match() { + fn test_extract_emit_to_with_variable_payload() { let parser = EventParser::new(); let mut type_resolver = TypeResolver::new(); - let ast: SynFile = parse_quote! { - fn match_notify() { - match result { - Ok(val) => app.emit("success", val), - Err(e) => app.emit("error", e), - } - } - }; - let path = PathBuf::from("test.rs"); - let events = parser - .extract_events_from_ast(&ast, &path, &mut type_resolver) - .unwrap(); - - assert_eq!(events.len(), 2); - assert_eq!(events[0].event_name, "success"); - assert_eq!(events[1].event_name, "error"); - } - - #[test] - fn test_no_events_in_non_emit_function() { - let parser = EventParser::new(); - let mut type_resolver = TypeResolver::new(); - let ast: SynFile = parse_quote! { - fn regular_function() { - let x = 42; - println!("Hello"); + let file: SynFile = parse_quote! { + fn emit_to_window(app: AppHandle, update: &ProgressUpdate) { + app.emit_to("main", "progress-update", update).unwrap(); } }; - let path = PathBuf::from("test.rs"); let events = parser - .extract_events_from_ast(&ast, &path, &mut type_resolver) + .extract_events_from_ast(&file, Path::new("test.rs"), &mut type_resolver) .unwrap(); - assert_eq!(events.len(), 0); + assert_eq!(events.len(), 1); + assert_eq!(events[0].event_name, "progress-update"); + assert_eq!(events[0].payload_type, "ProgressUpdate"); } #[test] - fn test_ignores_user_emit_method() { + fn test_extract_event_with_cloned_variable() { let parser = EventParser::new(); let mut type_resolver = TypeResolver::new(); - let ast: SynFile = parse_quote! { - fn user_emit() { - my_object.emit("not-a-tauri-event", data); + + let file: SynFile = parse_quote! { + fn emit_cloned(app: AppHandle, update: ProgressUpdate) { + app.emit("progress", update.clone()).unwrap(); } }; - let path = PathBuf::from("test.rs"); let events = parser - .extract_events_from_ast(&ast, &path, &mut type_resolver) + .extract_events_from_ast(&file, Path::new("test.rs"), &mut type_resolver) .unwrap(); - // Should not detect this as a Tauri event since my_object is not a Tauri emitter - assert_eq!(events.len(), 0); + assert_eq!(events.len(), 1); + assert_eq!(events[0].event_name, "progress"); + assert_eq!(events[0].payload_type, "ProgressUpdate"); } } } diff --git a/tests/integration_e2e.rs b/tests/integration_e2e.rs index 417933e..755fa0d 100644 --- a/tests/integration_e2e.rs +++ b/tests/integration_e2e.rs @@ -433,3 +433,81 @@ fn test_deeply_nested_types_full_pipeline() { || commands_file.contains("{ [key: string]: User }") ); } + +/// Test event payload type discovery when emitting from helper functions +/// Verifies that variable types are correctly inferred from function parameters +#[test] +fn test_event_payload_discovery_from_helper_function() { + let project = TestProject::new(); + + project.write_file( + "main.rs", + r#" + use tauri::{AppHandle, Manager}; + use serde::{Deserialize, Serialize}; + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct ProgressUpdate { + pub task_id: String, + pub progress: f64, + pub message: String, + } + + /// Command that uses a helper function to emit events + #[tauri::command] + pub async fn process_task(app: AppHandle, task_id: String) -> Result { + let update = ProgressUpdate { + task_id: task_id.clone(), + progress: 50.0, + message: "Processing".to_string(), + }; + emit_progress(app, &update); + Ok(format!("Task {} completed", task_id)) + } + + /// Helper function that emits the event with a reference parameter + pub fn emit_progress(app: AppHandle, update: &ProgressUpdate) { + app.emit("progress-update", update).unwrap(); + } + "#, + ); + + let (analyzer, commands) = project.analyze(); + let events = analyzer.get_discovered_events(); + + // Verify the command was found + assert_eq!(commands.len(), 1); + + // Verify the event was discovered with correct payload type + assert_eq!(events.len(), 1, "Should discover one event"); + assert_eq!(events[0].event_name, "progress-update"); + assert_eq!( + events[0].payload_type, "ProgressUpdate", + "Should infer ProgressUpdate type from function parameter, got: {}", + events[0].payload_type + ); + + // Generate code and verify struct is included + let generator = TestGenerator::new(); + generator.generate( + &commands, + analyzer.get_discovered_structs(), + &analyzer, + Some("zod"), + None, + ); + + let types = generator.read_file("types.ts"); + assert!( + types.contains("ProgressUpdate"), + "Should include ProgressUpdate in types.ts. Got:\n{}", + types + ); + + let events_file = generator.read_file("events.ts"); + assert!( + events_file.contains("ProgressUpdate"), + "Should reference ProgressUpdate in events.ts. Got:\n{}", + events_file + ); +}