1pub mod ast_cache;
2pub mod channel_parser;
3pub mod command_parser;
4pub mod dependency_graph;
5pub mod event_parser;
6pub mod serde_parser;
7pub mod struct_parser;
8pub mod type_resolver;
9pub mod validator_parser;
10
11use crate::models::{ChannelInfo, CommandInfo, EventInfo, StructInfo};
12use std::collections::{HashMap, HashSet};
13use std::env;
14use std::fs;
15use std::path::{Path, PathBuf};
16
17use ast_cache::AstCache;
18use channel_parser::ChannelParser;
19use command_parser::CommandParser;
20use dependency_graph::TypeDependencyGraph;
21use event_parser::EventParser;
22use struct_parser::StructParser;
23use type_resolver::TypeResolver;
24
25pub struct CommandAnalyzer {
27 ast_cache: AstCache,
29 command_parser: CommandParser,
31 channel_parser: ChannelParser,
33 event_parser: EventParser,
35 struct_parser: StructParser,
37 type_resolver: TypeResolver,
39 dependency_graph: TypeDependencyGraph,
41 discovered_structs: HashMap<String, StructInfo>,
43 discovered_events: Vec<EventInfo>,
45 unresolved_types: Vec<String>,
50 external_type_lookup_cache: HashMap<String, Option<PathBuf>>,
57}
58
59impl CommandAnalyzer {
60 pub fn new() -> Self {
61 Self {
62 ast_cache: AstCache::new(),
63 command_parser: CommandParser::new(),
64 channel_parser: ChannelParser::new(),
65 event_parser: EventParser::new(),
66 struct_parser: StructParser::new(),
67 type_resolver: TypeResolver::new(),
68 dependency_graph: TypeDependencyGraph::new(),
69 discovered_structs: HashMap::new(),
70 discovered_events: Vec::new(),
71 unresolved_types: Vec::new(),
72 external_type_lookup_cache: HashMap::new(),
73 }
74 }
75
76 pub fn add_type_mappings(&mut self, mappings: &HashMap<String, String>) {
78 for (rust_type, ts_type) in mappings {
79 self.type_resolver
80 .add_type_mapping(rust_type.clone(), ts_type.clone());
81 }
82 }
83
84 pub fn unresolved_types(&self) -> &[String] {
93 &self.unresolved_types
94 }
95
96 pub fn seed_external_type_cache(&mut self, index: HashMap<String, Option<PathBuf>>) {
102 self.external_type_lookup_cache = index;
103 }
104
105 pub fn external_type_lookup_cache(&self) -> &HashMap<String, Option<PathBuf>> {
109 &self.external_type_lookup_cache
110 }
111
112 pub fn analyze_project(
114 &mut self,
115 project_path: &str,
116 ) -> Result<Vec<CommandInfo>, Box<dyn std::error::Error>> {
117 self.analyze_project_with_verbose(project_path, false)
118 }
119
120 pub fn analyze_project_with_verbose(
122 &mut self,
123 project_path: &str,
124 verbose: bool,
125 ) -> Result<Vec<CommandInfo>, Box<dyn std::error::Error>> {
126 self.ast_cache
128 .parse_and_cache_all_files(project_path, verbose)?;
129
130 let mut file_paths: Vec<PathBuf> = self.ast_cache.keys().cloned().collect();
132 file_paths.sort_unstable();
133 let mut commands = Vec::new();
134 let mut type_names_to_discover = HashSet::new();
135
136 for file_path in file_paths {
138 if let Some(parsed_file) = self.ast_cache.get_cloned(&file_path) {
139 if verbose {
140 println!("🔍 Analyzing file: {}", parsed_file.path.display());
141 }
142
143 let mut file_commands = self.command_parser.extract_commands_from_ast(
145 &parsed_file.ast,
146 parsed_file.path.as_path(),
147 &mut self.type_resolver,
148 )?;
149
150 for command in &mut file_commands {
152 if let Some(func) = self.find_function_in_ast(&parsed_file.ast, &command.name) {
153 let channels = self.channel_parser.extract_channels_from_command(
154 func,
155 &command.name,
156 parsed_file.path.as_path(),
157 &mut self.type_resolver,
158 )?;
159
160 channels.iter().for_each(|ch| {
162 self.extract_type_names(&ch.message_type, &mut type_names_to_discover);
163 });
164
165 command.channels = channels;
166 }
167 }
168
169 let file_events = self.event_parser.extract_events_from_ast(
171 &parsed_file.ast,
172 parsed_file.path.as_path(),
173 &mut self.type_resolver,
174 )?;
175
176 file_commands.iter().for_each(|cmd| {
178 cmd.parameters.iter().for_each(|param| {
179 self.extract_type_names(¶m.rust_type, &mut type_names_to_discover);
180 });
181 self.extract_type_names(&cmd.return_type, &mut type_names_to_discover);
183 });
184
185 file_events.iter().for_each(|event| {
187 self.extract_type_names(&event.payload_type, &mut type_names_to_discover);
188 });
189
190 commands.extend(file_commands);
191 self.discovered_events.extend(file_events);
192
193 self.index_type_definitions(&parsed_file.ast, parsed_file.path.as_path());
195 }
196 }
197
198 if verbose {
199 println!("🔍 Type names to discover: {:?}", type_names_to_discover);
200 }
201
202 self.resolve_types_lazily(&type_names_to_discover)?;
204
205 if verbose {
206 println!(
207 "🏗️ Discovered {} structs total",
208 self.discovered_structs.len()
209 );
210 for (name, info) in &self.discovered_structs {
211 println!(" - {}: {} fields", name, info.fields.len());
212 }
213 println!(
214 "📡 Discovered {} events total",
215 self.discovered_events.len()
216 );
217 for event in &self.discovered_events {
218 println!(" - '{}': {}", event.event_name, event.payload_type);
219 }
220 let all_channels = self.get_all_discovered_channels(&commands);
221 println!("📞 Discovered {} channels total", all_channels.len());
222 for channel in &all_channels {
223 println!(
224 " - '{}' in {}: {}",
225 channel.parameter_name, channel.command_name, channel.message_type
226 );
227 }
228 }
229
230 if !self.unresolved_types.is_empty() {
235 eprintln!(
236 "⚠️ tauri-typegen: {} referenced type(s) could not be resolved in the \
237 project or the Cargo registry; their bindings will be missing from the output:",
238 self.unresolved_types.len()
239 );
240 for name in &self.unresolved_types {
241 eprintln!(" - {}", name);
242 }
243 eprintln!(
244 " This is usually caused by a missing/empty/relocated Cargo registry \
245 (CARGO_HOME) or a vendored dependency layout. See issue #84."
246 );
247 }
248
249 Ok(commands)
250 }
251
252 pub fn analyze_file(
254 &mut self,
255 file_path: &std::path::Path,
256 ) -> Result<Vec<CommandInfo>, Box<dyn std::error::Error>> {
257 let path_buf = file_path.to_path_buf();
258
259 match self.ast_cache.parse_and_cache_file(&path_buf) {
261 Ok(_) => {
262 if let Some(parsed_file) = self.ast_cache.get_cloned(&path_buf) {
264 let file_events = self.event_parser.extract_events_from_ast(
266 &parsed_file.ast,
267 path_buf.as_path(),
268 &mut self.type_resolver,
269 )?;
270 self.discovered_events.extend(file_events);
271
272 let mut commands = self.command_parser.extract_commands_from_ast(
274 &parsed_file.ast,
275 path_buf.as_path(),
276 &mut self.type_resolver,
277 )?;
278
279 for command in &mut commands {
281 if let Some(func) =
282 self.find_function_in_ast(&parsed_file.ast, &command.name)
283 {
284 let channels = self.channel_parser.extract_channels_from_command(
285 func,
286 &command.name,
287 path_buf.as_path(),
288 &mut self.type_resolver,
289 )?;
290
291 command.channels = channels;
292 }
293 }
294
295 Ok(commands)
296 } else {
297 Ok(vec![])
298 }
299 }
300 Err(_) => {
301 Ok(vec![])
303 }
304 }
305 }
306
307 fn index_type_definitions(&mut self, ast: &syn::File, file_path: &Path) {
309 self.index_items(&ast.items, file_path);
310 }
311
312 fn index_items(&mut self, items: &[syn::Item], file_path: &Path) {
314 for item in items {
315 match item {
316 syn::Item::Struct(item_struct) => {
317 if self.struct_parser.should_include_struct(item_struct) {
318 let struct_name = item_struct.ident.to_string();
319 self.dependency_graph
320 .add_type_definition(struct_name, file_path.to_path_buf());
321 }
322 }
323 syn::Item::Enum(item_enum) => {
324 if self.struct_parser.should_include_enum(item_enum) {
325 let enum_name = item_enum.ident.to_string();
326 self.dependency_graph
327 .add_type_definition(enum_name, file_path.to_path_buf());
328 }
329 }
330 syn::Item::Mod(item_mod) => {
331 if let Some((_, items)) = &item_mod.content {
332 self.index_items(items, file_path);
333 }
334 }
335 _ => {}
336 }
337 }
338 }
339
340 fn resolve_types_lazily(
342 &mut self,
343 initial_types: &HashSet<String>,
344 ) -> Result<(), Box<dyn std::error::Error>> {
345 let mut types_to_resolve: Vec<String> = initial_types.iter().cloned().collect();
346 let mut resolved_types = HashSet::new();
347 let mut unresolved_types: Vec<String> = Vec::new();
351
352 while let Some(type_name) = types_to_resolve.pop() {
353 if resolved_types.contains(&type_name)
355 || self.discovered_structs.contains_key(&type_name)
356 {
357 continue;
358 }
359
360 if let Some(file_path) = self
362 .dependency_graph
363 .get_type_definition_path(&type_name)
364 .cloned()
365 .or_else(|| {
366 if let Some(ext_path) = self.find_external_type_path_cached(&type_name) {
368 self.dependency_graph
370 .add_type_definition(type_name.clone(), ext_path.clone());
371 let _ = self.ast_cache.parse_and_cache_file(&ext_path);
372 Some(ext_path)
373 } else {
374 None
375 }
376 })
377 {
378 if let Some(parsed_file) = self.ast_cache.get_cloned(&file_path) {
379 self.index_items(&parsed_file.ast.items, &file_path);
380 if let Some(struct_info) = self.extract_type_from_ast(
382 &parsed_file.ast,
383 &type_name,
384 file_path.as_path(),
385 ) {
386 let mut type_dependencies = HashSet::new();
388 for field in &struct_info.fields {
389 self.extract_type_names(&field.rust_type, &mut type_dependencies);
390 }
391
392 if let Some(variants) = &struct_info.enum_variants {
394 for variant in variants {
395 match &variant.kind {
396 crate::models::EnumVariantKind::Unit => {}
397 crate::models::EnumVariantKind::Tuple(types) => {
398 for type_struct in types {
399 let mut variant_types = HashSet::new();
400 crate::generators::TypeCollector::collect_referenced_types_from_structure(
401 type_struct,
402 &mut variant_types,
403 );
404 type_dependencies.extend(variant_types);
405 }
406 }
407 crate::models::EnumVariantKind::Struct(fields) => {
408 for field in fields {
409 self.extract_type_names(
410 &field.rust_type,
411 &mut type_dependencies,
412 );
413 }
414 }
415 }
416 }
417 }
418
419 for dep_type in &type_dependencies {
421 if !resolved_types.contains(dep_type)
422 && !self.discovered_structs.contains_key(dep_type)
423 && (self.dependency_graph.has_type_definition(dep_type)
424 || self.find_external_type_path_cached(dep_type).is_some())
425 {
426 types_to_resolve.push(dep_type.clone());
427 }
428 }
429
430 self.dependency_graph
432 .add_dependencies(type_name.clone(), type_dependencies.clone());
433 self.dependency_graph
434 .add_resolved_type(type_name.clone(), struct_info.clone());
435 self.discovered_structs
436 .insert(type_name.clone(), struct_info);
437 resolved_types.insert(type_name);
438 }
439 }
440 } else {
441 unresolved_types.push(type_name);
446 }
447 }
448
449 let mut seen: HashSet<String> = HashSet::new();
451 unresolved_types.retain(|name| seen.insert(name.clone()));
452 self.unresolved_types = unresolved_types;
453
454 Ok(())
455 }
456
457 fn find_external_type_path_cached(&mut self, type_name: &str) -> Option<PathBuf> {
473 if let Some(cached) = self.external_type_lookup_cache.get(type_name) {
474 return cached.clone();
475 }
476 let found = self.find_external_type_path_uncached(type_name);
477 self.external_type_lookup_cache
478 .insert(type_name.to_string(), found.clone());
479 found
480 }
481
482 fn find_external_type_path_uncached(&self, type_name: &str) -> Option<PathBuf> {
494 let cargo_home: PathBuf = match env::var("CARGO_HOME") {
498 Ok(dir) => PathBuf::from(dir),
499 Err(_) => {
500 let home: String = env::var("HOME").or(env::var("USERPROFILE")).ok()?;
501 PathBuf::from(home).join(".cargo")
502 }
503 };
504 let src_dir: PathBuf = cargo_home.join("registry/src");
505
506 let needle: String = type_name.to_string();
511
512 let mut dirs: Vec<PathBuf> = vec![src_dir];
514 while let Some(dir) = dirs.pop() {
515 let entries: fs::ReadDir = fs::read_dir(&dir).ok()?;
516 for entry in entries.filter_map(Result::ok) {
517 let path: PathBuf = entry.path();
518 if path.is_dir() {
519 dirs.push(path);
520 continue;
521 }
522 if path.extension().and_then(|s| s.to_str()) != Some("rs") {
523 continue;
524 }
525 let content: String = match fs::read_to_string(&path) {
527 Ok(c) => c,
528 Err(_) => continue,
529 };
530 if !content.contains(&needle) {
531 continue;
532 }
533 if Self::file_declares_type(&content, type_name) {
535 return Some(path);
536 }
537 }
538 }
539 None
540 }
541
542 fn file_declares_type(source: &str, type_name: &str) -> bool {
548 let file: syn::File = match syn::parse_file(source) {
549 Ok(f) => f,
550 Err(_) => return false,
551 };
552 Self::items_declare_type(&file.items, type_name)
553 }
554
555 fn items_declare_type(items: &[syn::Item], type_name: &str) -> bool {
556 for item in items {
557 let declares = match item {
558 syn::Item::Struct(s) => s.ident == type_name,
559 syn::Item::Enum(e) => e.ident == type_name,
560 syn::Item::Mod(m) => match &m.content {
563 Some((_, inner)) => Self::items_declare_type(inner, type_name),
564 None => false,
565 },
566 _ => false,
567 };
568 if declares {
569 return true;
570 }
571 }
572 false
573 }
574
575 fn extract_type_from_ast(
577 &mut self,
578 ast: &syn::File,
579 type_name: &str,
580 file_path: &Path,
581 ) -> Option<StructInfo> {
582 self.find_type_in_items(&ast.items, type_name, file_path)
583 }
584
585 fn find_type_in_items(
587 &mut self,
588 items: &[syn::Item],
589 type_name: &str,
590 file_path: &Path,
591 ) -> Option<StructInfo> {
592 for item in items {
593 match item {
594 syn::Item::Struct(item_struct) => {
595 if item_struct.ident == type_name
596 && self.struct_parser.should_include_struct(item_struct)
597 {
598 return self.struct_parser.parse_struct(
599 item_struct,
600 file_path,
601 &mut self.type_resolver,
602 );
603 }
604 }
605 syn::Item::Enum(item_enum) => {
606 if item_enum.ident == type_name
607 && self.struct_parser.should_include_enum(item_enum)
608 {
609 return self.struct_parser.parse_enum(
610 item_enum,
611 file_path,
612 &mut self.type_resolver,
613 );
614 }
615 }
616 syn::Item::Mod(item_mod) => {
617 if let Some((_, items)) = &item_mod.content {
618 if let Some(info) = self.find_type_in_items(items, type_name, file_path) {
619 return Some(info);
620 }
621 }
622 }
623 _ => {}
624 }
625 }
626 None
627 }
628
629 pub fn extract_type_names(&self, rust_type: &str, type_names: &mut HashSet<String>) {
631 self.extract_type_names_recursive(rust_type, type_names);
632 }
633
634 fn extract_type_names_recursive(&self, rust_type: &str, type_names: &mut HashSet<String>) {
636 let rust_type = rust_type.trim();
637
638 if rust_type.starts_with('&') {
640 let without_ref = rust_type.trim_start_matches('&');
641 self.extract_type_names_recursive(without_ref, type_names);
642 return;
643 }
644
645 let stripped = Self::strip_module_prefix(rust_type);
648
649 if stripped.starts_with("Result<") {
651 if let Some(inner) = stripped
652 .strip_prefix("Result<")
653 .and_then(|s| s.strip_suffix(">"))
654 {
655 if let Some(comma_pos) = inner.find(',') {
656 let ok_type = inner[..comma_pos].trim();
657 let err_type = inner[comma_pos + 1..].trim();
658 self.extract_type_names_recursive(ok_type, type_names);
659 self.extract_type_names_recursive(err_type, type_names);
660 }
661 }
662 return;
663 }
664
665 if stripped.starts_with("Option<") {
667 if let Some(inner) = stripped
668 .strip_prefix("Option<")
669 .and_then(|s| s.strip_suffix(">"))
670 {
671 self.extract_type_names_recursive(inner, type_names);
672 }
673 return;
674 }
675
676 if stripped.starts_with("Vec<") {
678 if let Some(inner) = stripped
679 .strip_prefix("Vec<")
680 .and_then(|s| s.strip_suffix(">"))
681 {
682 self.extract_type_names_recursive(inner, type_names);
683 }
684 return;
685 }
686
687 if stripped.starts_with("HashMap<") || stripped.starts_with("BTreeMap<") {
689 let prefix = if stripped.starts_with("HashMap<") {
690 "HashMap<"
691 } else {
692 "BTreeMap<"
693 };
694 if let Some(inner) = stripped
695 .strip_prefix(prefix)
696 .and_then(|s| s.strip_suffix(">"))
697 {
698 if let Some(comma_pos) = inner.find(',') {
699 let key_type = inner[..comma_pos].trim();
700 let value_type = inner[comma_pos + 1..].trim();
701 self.extract_type_names_recursive(key_type, type_names);
702 self.extract_type_names_recursive(value_type, type_names);
703 }
704 }
705 return;
706 }
707
708 if stripped.starts_with("HashSet<") || stripped.starts_with("BTreeSet<") {
710 let prefix = if stripped.starts_with("HashSet<") {
711 "HashSet<"
712 } else {
713 "BTreeSet<"
714 };
715 if let Some(inner) = stripped
716 .strip_prefix(prefix)
717 .and_then(|s| s.strip_suffix(">"))
718 {
719 self.extract_type_names_recursive(inner, type_names);
720 }
721 return;
722 }
723
724 if rust_type.starts_with('(') && rust_type.ends_with(')') && rust_type != "()" {
726 let inner = &rust_type[1..rust_type.len() - 1];
727 for part in inner.split(',') {
728 self.extract_type_names_recursive(part.trim(), type_names);
729 }
730 return;
731 }
732
733 if !rust_type.is_empty()
735 && !self.type_resolver.get_type_set().contains(rust_type)
736 && !rust_type.starts_with(char::is_lowercase) && rust_type.chars().next().is_some_and(char::is_alphabetic)
738 && !rust_type.contains('<')
739 {
741 let type_name = Self::extract_simple_type_name(rust_type);
743 type_names.insert(type_name);
744 }
745 }
746
747 fn strip_module_prefix(rust_type: &str) -> &str {
750 if let Some(last_double_colon) = rust_type.rfind("::") {
752 let after_colon = &rust_type[last_double_colon + 2..];
754 if after_colon.contains('<') {
755 return after_colon;
756 }
757 }
758 rust_type
759 }
760
761 fn extract_simple_type_name(rust_type: &str) -> String {
764 if let Some(last_double_colon) = rust_type.rfind("::") {
766 rust_type[last_double_colon + 2..].to_string()
767 } else {
768 rust_type.to_string()
769 }
770 }
771
772 pub fn get_discovered_structs(&self) -> &HashMap<String, StructInfo> {
774 &self.discovered_structs
775 }
776
777 pub fn get_discovered_events(&self) -> &[EventInfo] {
779 &self.discovered_events
780 }
781
782 pub fn get_type_resolver(&self) -> std::cell::RefCell<&TypeResolver> {
784 std::cell::RefCell::new(&self.type_resolver)
785 }
786
787 pub fn get_all_discovered_channels(&self, commands: &[CommandInfo]) -> Vec<ChannelInfo> {
789 commands
790 .iter()
791 .flat_map(|cmd| cmd.channels.clone())
792 .collect()
793 }
794
795 fn find_function_in_ast<'a>(
797 &self,
798 ast: &'a syn::File,
799 function_name: &str,
800 ) -> Option<&'a syn::ItemFn> {
801 self.find_function_in_items(&ast.items, function_name)
802 }
803
804 fn find_function_in_items<'a>(
806 &self,
807 items: &'a [syn::Item],
808 function_name: &str,
809 ) -> Option<&'a syn::ItemFn> {
810 for item in items {
811 match item {
812 syn::Item::Fn(func) => {
813 if func.sig.ident == function_name {
814 return Some(func);
815 }
816 }
817 syn::Item::Mod(item_mod) => {
818 if let Some((_, items)) = &item_mod.content {
819 if let Some(func) = self.find_function_in_items(items, function_name) {
820 return Some(func);
821 }
822 }
823 }
824 _ => {}
825 }
826 }
827 None
828 }
829
830 pub fn get_dependency_graph(&self) -> &TypeDependencyGraph {
832 &self.dependency_graph
833 }
834
835 pub fn topological_sort_types(&self, types: &HashSet<String>) -> Vec<String> {
837 self.dependency_graph.topological_sort_types(types)
838 }
839
840 pub fn visualize_dependencies(&self, commands: &[CommandInfo]) -> String {
842 self.dependency_graph.visualize_dependencies(commands)
843 }
844
845 pub fn generate_dot_graph(&self, commands: &[CommandInfo]) -> String {
847 self.dependency_graph.generate_dot_graph(commands)
848 }
849}
850
851impl Default for CommandAnalyzer {
852 fn default() -> Self {
853 Self::new()
854 }
855}
856
857#[cfg(test)]
858mod tests {
859 use super::*;
860 use std::collections::HashSet;
861
862 fn analyzer() -> CommandAnalyzer {
863 CommandAnalyzer::new()
864 }
865
866 mod initialization {
867 use super::*;
868
869 #[test]
870 fn test_new_creates_analyzer() {
871 let analyzer = CommandAnalyzer::new();
872 assert!(analyzer.get_discovered_structs().is_empty());
873 assert!(analyzer.get_discovered_events().is_empty());
874 }
875
876 #[test]
877 fn test_default_creates_analyzer() {
878 let analyzer = CommandAnalyzer::default();
879 assert!(analyzer.get_discovered_structs().is_empty());
880 assert!(analyzer.get_discovered_events().is_empty());
881 }
882 }
883
884 mod type_name_extraction {
885 use super::*;
886
887 #[test]
888 fn test_extract_simple_type() {
889 let analyzer = analyzer();
890 let mut types = HashSet::new();
891 analyzer.extract_type_names("User", &mut types);
892 assert_eq!(types.len(), 1);
893 assert!(types.contains("User"));
894 }
895
896 #[test]
897 fn test_extract_option_type() {
898 let analyzer = analyzer();
899 let mut types = HashSet::new();
900 analyzer.extract_type_names("Option<User>", &mut types);
901 assert_eq!(types.len(), 1);
902 assert!(types.contains("User"));
903 }
904
905 #[test]
906 fn test_extract_vec_type() {
907 let analyzer = analyzer();
908 let mut types = HashSet::new();
909 analyzer.extract_type_names("Vec<Product>", &mut types);
910 assert_eq!(types.len(), 1);
911 assert!(types.contains("Product"));
912 }
913
914 #[test]
915 fn test_extract_result_type() {
916 let analyzer = analyzer();
917 let mut types = HashSet::new();
918 analyzer.extract_type_names("Result<User, AppError>", &mut types);
919 assert_eq!(types.len(), 2);
920 assert!(types.contains("User"));
921 assert!(types.contains("AppError"));
922 }
923
924 #[test]
925 fn test_extract_hashmap_type() {
926 let analyzer = analyzer();
927 let mut types = HashSet::new();
928 analyzer.extract_type_names("HashMap<String, User>", &mut types);
929 assert_eq!(types.len(), 1);
931 assert!(types.contains("User"));
932 }
933
934 #[test]
935 fn test_extract_btreemap_type() {
936 let analyzer = analyzer();
937 let mut types = HashSet::new();
938 analyzer.extract_type_names("BTreeMap<UserId, Profile>", &mut types);
939 assert_eq!(types.len(), 2);
940 assert!(types.contains("UserId"));
941 assert!(types.contains("Profile"));
942 }
943
944 #[test]
945 fn test_extract_hashset_type() {
946 let analyzer = analyzer();
947 let mut types = HashSet::new();
948 analyzer.extract_type_names("HashSet<User>", &mut types);
949 assert_eq!(types.len(), 1);
950 assert!(types.contains("User"));
951 }
952
953 #[test]
954 fn test_extract_btreeset_type() {
955 let analyzer = analyzer();
956 let mut types = HashSet::new();
957 analyzer.extract_type_names("BTreeSet<Tag>", &mut types);
958 assert_eq!(types.len(), 1);
959 assert!(types.contains("Tag"));
960 }
961
962 #[test]
963 fn test_extract_tuple_type() {
964 let analyzer = analyzer();
965 let mut types = HashSet::new();
966 analyzer.extract_type_names("(User, Product, Order)", &mut types);
967 assert_eq!(types.len(), 3);
968 assert!(types.contains("User"));
969 assert!(types.contains("Product"));
970 assert!(types.contains("Order"));
971 }
972
973 #[test]
974 fn test_extract_reference_type() {
975 let analyzer = analyzer();
976 let mut types = HashSet::new();
977 analyzer.extract_type_names("&User", &mut types);
978 assert_eq!(types.len(), 1);
979 assert!(types.contains("User"));
980 }
981
982 #[test]
983 fn test_extract_nested_types() {
984 let analyzer = analyzer();
985 let mut types = HashSet::new();
986 analyzer.extract_type_names("Vec<Option<User>>", &mut types);
987 assert_eq!(types.len(), 1);
988 assert!(types.contains("User"));
989 }
990
991 #[test]
992 fn test_extract_deeply_nested_types() {
993 let analyzer = analyzer();
994 let mut types = HashSet::new();
995 analyzer.extract_type_names("HashMap<String, Vec<Option<Product>>>", &mut types);
996 assert_eq!(types.len(), 1);
997 assert!(types.contains("Product"));
998 }
999
1000 #[test]
1001 fn test_skips_primitive_types() {
1002 let analyzer = analyzer();
1003 let mut types = HashSet::new();
1004 analyzer.extract_type_names("String", &mut types);
1005 assert_eq!(types.len(), 0);
1006 }
1007
1008 #[test]
1009 fn test_skips_built_in_types() {
1010 let analyzer = analyzer();
1011 let mut types = HashSet::new();
1012 analyzer.extract_type_names("i32", &mut types);
1013 assert_eq!(types.len(), 0);
1014 }
1015
1016 #[test]
1017 fn test_skips_empty_type() {
1018 let analyzer = analyzer();
1019 let mut types = HashSet::new();
1020 analyzer.extract_type_names("", &mut types);
1021 assert_eq!(types.len(), 0);
1022 }
1023
1024 #[test]
1025 fn test_skips_unit_type() {
1026 let analyzer = analyzer();
1027 let mut types = HashSet::new();
1028 analyzer.extract_type_names("()", &mut types);
1029 assert_eq!(types.len(), 0);
1030 }
1031
1032 #[test]
1033 fn test_multiple_calls_accumulate() {
1034 let analyzer = analyzer();
1035 let mut types = HashSet::new();
1036 analyzer.extract_type_names("User", &mut types);
1037 analyzer.extract_type_names("Product", &mut types);
1038 assert_eq!(types.len(), 2);
1039 assert!(types.contains("User"));
1040 assert!(types.contains("Product"));
1041 }
1042
1043 #[test]
1044 fn test_duplicate_types_deduped() {
1045 let analyzer = analyzer();
1046 let mut types = HashSet::new();
1047 analyzer.extract_type_names("User", &mut types);
1048 analyzer.extract_type_names("User", &mut types);
1049 assert_eq!(types.len(), 1);
1050 }
1051 }
1052
1053 mod getters {
1054 use super::*;
1055
1056 #[test]
1057 fn test_get_discovered_structs_empty() {
1058 let analyzer = analyzer();
1059 let structs = analyzer.get_discovered_structs();
1060 assert!(structs.is_empty());
1061 }
1062
1063 #[test]
1064 fn test_get_discovered_events_empty() {
1065 let analyzer = analyzer();
1066 let events = analyzer.get_discovered_events();
1067 assert!(events.is_empty());
1068 }
1069
1070 #[test]
1071 fn test_get_type_resolver() {
1072 let analyzer = analyzer();
1073 let resolver = analyzer.get_type_resolver();
1074 assert!(!resolver.borrow().get_type_set().is_empty());
1076 }
1077
1078 #[test]
1079 fn test_get_dependency_graph() {
1080 let analyzer = analyzer();
1081 let graph = analyzer.get_dependency_graph();
1082 assert!(graph.get_resolved_types().is_empty());
1084 }
1085
1086 #[test]
1087 fn test_get_all_discovered_channels_empty() {
1088 let analyzer = analyzer();
1089 let commands = vec![];
1090 let channels = analyzer.get_all_discovered_channels(&commands);
1091 assert!(channels.is_empty());
1092 }
1093
1094 #[test]
1095 fn test_get_all_discovered_channels_with_commands() {
1096 let analyzer = analyzer();
1097 let command = CommandInfo::new_for_test(
1098 "test_cmd",
1099 "test.rs",
1100 1,
1101 vec![],
1102 "void",
1103 false,
1104 vec![
1105 ChannelInfo::new_for_test("ch1", "Message1", "test_cmd", "test.rs", 10),
1106 ChannelInfo::new_for_test("ch2", "Message2", "test_cmd", "test.rs", 20),
1107 ],
1108 );
1109
1110 let commands = vec![command];
1111 let channels = analyzer.get_all_discovered_channels(&commands);
1112 assert_eq!(channels.len(), 2);
1113 }
1114 }
1115
1116 mod topological_sort {
1117 use super::*;
1118
1119 #[test]
1120 fn test_topological_sort_empty() {
1121 let analyzer = analyzer();
1122 let types = HashSet::new();
1123 let sorted = analyzer.topological_sort_types(&types);
1124 assert!(sorted.is_empty());
1125 }
1126
1127 #[test]
1128 fn test_topological_sort_single_type() {
1129 let mut analyzer = analyzer();
1130 let path = PathBuf::from("test.rs");
1131 analyzer
1132 .dependency_graph
1133 .add_type_definition("User".to_string(), path);
1134
1135 let mut types = HashSet::new();
1136 types.insert("User".to_string());
1137
1138 let sorted = analyzer.topological_sort_types(&types);
1139 assert_eq!(sorted.len(), 1);
1140 assert_eq!(sorted[0], "User");
1141 }
1142 }
1143
1144 mod ast_helpers {
1145 use super::*;
1146 use syn::{parse_quote, File as SynFile};
1147
1148 #[test]
1149 fn test_find_function_in_ast() {
1150 let analyzer = analyzer();
1151 let ast: SynFile = parse_quote! {
1152 #[tauri::command]
1153 fn my_command() -> String {
1154 "test".to_string()
1155 }
1156
1157 fn other_function() {}
1158 };
1159
1160 let result = analyzer.find_function_in_ast(&ast, "my_command");
1161 assert!(result.is_some());
1162 assert_eq!(result.unwrap().sig.ident, "my_command");
1163 }
1164
1165 #[test]
1166 fn test_find_function_in_ast_not_found() {
1167 let analyzer = analyzer();
1168 let ast: SynFile = parse_quote! {
1169 fn my_command() {}
1170 };
1171
1172 let result = analyzer.find_function_in_ast(&ast, "non_existent");
1173 assert!(result.is_none());
1174 }
1175
1176 #[test]
1177 fn test_find_function_in_ast_empty() {
1178 let analyzer = analyzer();
1179 let ast: SynFile = parse_quote! {};
1180
1181 let result = analyzer.find_function_in_ast(&ast, "any_function");
1182 assert!(result.is_none());
1183 }
1184 }
1185
1186 mod index_type_definitions {
1187 use super::*;
1188 use syn::{parse_quote, File as SynFile};
1189
1190 #[test]
1191 fn test_index_struct() {
1192 let mut analyzer = analyzer();
1193 let ast: SynFile = parse_quote! {
1194 #[derive(Serialize)]
1195 pub struct User {
1196 name: String,
1197 }
1198 };
1199 let path = Path::new("test.rs");
1200
1201 analyzer.index_type_definitions(&ast, path);
1202
1203 assert!(analyzer.dependency_graph.has_type_definition("User"));
1204 }
1205
1206 #[test]
1207 fn test_index_enum() {
1208 let mut analyzer = analyzer();
1209 let ast: SynFile = parse_quote! {
1210 #[derive(Serialize)]
1211 pub enum Status {
1212 Active,
1213 Inactive,
1214 }
1215 };
1216 let path = Path::new("test.rs");
1217
1218 analyzer.index_type_definitions(&ast, path);
1219
1220 assert!(analyzer.dependency_graph.has_type_definition("Status"));
1221 }
1222
1223 #[test]
1224 fn test_skips_non_serde_types() {
1225 let mut analyzer = analyzer();
1226 let ast: SynFile = parse_quote! {
1227 #[derive(Debug, Clone)]
1228 pub struct User {
1229 name: String,
1230 }
1231 };
1232 let path = Path::new("test.rs");
1233
1234 analyzer.index_type_definitions(&ast, path);
1235
1236 assert!(!analyzer.dependency_graph.has_type_definition("User"));
1237 }
1238 }
1239
1240 mod extract_type_from_ast {
1241 use super::*;
1242 use syn::{parse_quote, File as SynFile};
1243
1244 #[test]
1245 fn test_extract_struct_from_ast() {
1246 let mut analyzer = analyzer();
1247 let ast: SynFile = parse_quote! {
1248 #[derive(Serialize)]
1249 pub struct User {
1250 pub name: String,
1251 }
1252 };
1253 let path = Path::new("test.rs");
1254
1255 let result = analyzer.extract_type_from_ast(&ast, "User", path);
1256 assert!(result.is_some());
1257 let struct_info = result.unwrap();
1258 assert_eq!(struct_info.name, "User");
1259 assert_eq!(struct_info.fields.len(), 1);
1260 }
1261
1262 #[test]
1263 fn test_extract_enum_from_ast() {
1264 let mut analyzer = analyzer();
1265 let ast: SynFile = parse_quote! {
1266 #[derive(Serialize)]
1267 pub enum Status {
1268 Active,
1269 Inactive,
1270 }
1271 };
1272 let path = Path::new("test.rs");
1273
1274 let result = analyzer.extract_type_from_ast(&ast, "Status", path);
1275 assert!(result.is_some());
1276 let enum_info = result.unwrap();
1277 assert_eq!(enum_info.name, "Status");
1278 assert!(enum_info.is_enum);
1279 }
1280
1281 #[test]
1282 fn test_extract_type_not_found() {
1283 let mut analyzer = analyzer();
1284 let ast: SynFile = parse_quote! {
1285 #[derive(Serialize)]
1286 pub struct User {
1287 name: String,
1288 }
1289 };
1290 let path = Path::new("test.rs");
1291
1292 let result = analyzer.extract_type_from_ast(&ast, "Product", path);
1293 assert!(result.is_none());
1294 }
1295
1296 #[test]
1297 fn test_extract_type_without_serde() {
1298 let mut analyzer = analyzer();
1299 let ast: SynFile = parse_quote! {
1300 #[derive(Debug)]
1301 pub struct User {
1302 name: String,
1303 }
1304 };
1305 let path = Path::new("test.rs");
1306
1307 let result = analyzer.extract_type_from_ast(&ast, "User", path);
1308 assert!(result.is_none());
1309 }
1310 }
1311
1312 mod visualization {
1313 use super::*;
1314
1315 #[test]
1316 fn test_visualize_dependencies() {
1317 let analyzer = analyzer();
1318 let commands = vec![];
1319 let viz = analyzer.visualize_dependencies(&commands);
1320 assert!(viz.contains("Dependency Graph"));
1322 }
1323
1324 #[test]
1325 fn test_generate_dot_graph() {
1326 let analyzer = analyzer();
1327 let commands = vec![];
1328 let dot = analyzer.generate_dot_graph(&commands);
1329 assert!(dot.contains("digraph"));
1331 }
1332 }
1333
1334 mod external_type_lookup {
1335 use super::*;
1336 use serial_test::serial;
1337 use std::env;
1338 use std::fs;
1339 use std::path::PathBuf;
1340
1341 struct FakeRegistry {
1346 _root: PathBuf,
1347 cargo_home: PathBuf,
1348 }
1349
1350 impl FakeRegistry {
1351 fn new(files: &[(&str, &str)]) -> (Self, CommandAnalyzer) {
1352 let root: PathBuf = std::env::temp_dir().join(format!(
1353 "tauri_typegen_external_test_{}",
1354 std::time::SystemTime::now()
1355 .duration_since(std::time::UNIX_EPOCH)
1356 .unwrap()
1357 .as_nanos(),
1358 ));
1359 let _ = std::fs::remove_dir_all(&root);
1360 let cargo_home: PathBuf = root.join(".cargo");
1361 let src_dir: PathBuf = cargo_home.join("registry/src");
1362 let crate_dir: PathBuf = src_dir.join("dummy-0.1.0");
1363 fs::create_dir_all(&crate_dir).expect("create temp crate dir");
1364 for (name, content) in files {
1365 fs::write(crate_dir.join(name), content).expect("write file");
1366 }
1367 env::set_var("CARGO_HOME", &cargo_home);
1368 let fake = FakeRegistry {
1369 _root: root.clone(),
1370 cargo_home,
1371 };
1372 (fake, CommandAnalyzer::default())
1373 }
1374 }
1375
1376 impl Drop for FakeRegistry {
1377 fn drop(&mut self) {
1378 let _ = std::fs::remove_dir_all(&self._root);
1379 }
1380 }
1381
1382 #[test]
1383 #[serial]
1384 fn test_find_external_type_path_pub_struct() {
1385 let (reg, mut analyzer) = FakeRegistry::new(&[("lib.rs", "pub struct ExternalFoo;")]);
1386 let found = analyzer.find_external_type_path_cached("ExternalFoo");
1387 let expected = reg.cargo_home.join("registry/src/dummy-0.1.0/lib.rs");
1388 assert_eq!(found.unwrap(), expected, "pub struct should be located");
1389 }
1390
1391 #[test]
1394 #[serial]
1395 fn test_find_external_type_path_pub_crate_visibility() {
1396 let (reg, mut analyzer) =
1397 FakeRegistry::new(&[("lib.rs", "pub(crate) struct VisCrate;")]);
1398 let found = analyzer.find_external_type_path_cached("VisCrate");
1399 let expected = reg.cargo_home.join("registry/src/dummy-0.1.0/lib.rs");
1400 assert_eq!(
1401 found.unwrap(),
1402 expected,
1403 "pub(crate) struct should be located"
1404 );
1405 }
1406
1407 #[test]
1410 #[serial]
1411 fn test_find_external_type_path_with_derive_attributes() {
1412 let (reg, mut analyzer) = FakeRegistry::new(&[(
1413 "lib.rs",
1414 "#[derive(Debug, Clone)]\npub struct WithDerives { field: i32 }",
1415 )]);
1416 let found = analyzer.find_external_type_path_cached("WithDerives");
1417 let expected = reg.cargo_home.join("registry/src/dummy-0.1.0/lib.rs");
1418 assert_eq!(
1419 found.unwrap(),
1420 expected,
1421 "#[derive(...)] pub struct should be located"
1422 );
1423 }
1424
1425 #[test]
1428 #[serial]
1429 fn test_find_external_type_path_with_generics() {
1430 let (reg, mut analyzer) =
1431 FakeRegistry::new(&[("lib.rs", "pub struct Generic<T, U> { a: T, b: U }")]);
1432 let found = analyzer.find_external_type_path_cached("Generic");
1433 let expected = reg.cargo_home.join("registry/src/dummy-0.1.0/lib.rs");
1434 assert_eq!(
1435 found.unwrap(),
1436 expected,
1437 "generic pub struct should be located"
1438 );
1439 }
1440
1441 #[test]
1445 #[serial]
1446 fn test_find_external_type_path_multiline() {
1447 let (reg, mut analyzer) =
1448 FakeRegistry::new(&[("lib.rs", "pub\n struct\n Multiline\n{\n x: i32,\n }")]);
1449 let found = analyzer.find_external_type_path_cached("Multiline");
1450 let expected = reg.cargo_home.join("registry/src/dummy-0.1.0/lib.rs");
1451 assert_eq!(
1452 found.unwrap(),
1453 expected,
1454 "multi-line struct should be located"
1455 );
1456 }
1457
1458 #[test]
1460 #[serial]
1461 fn test_find_external_type_path_enum_with_attributes() {
1462 let (reg, mut analyzer) =
1463 FakeRegistry::new(&[("lib.rs", "#[derive(Debug)]\npub enum EnumAttr { A, B }")]);
1464 let found = analyzer.find_external_type_path_cached("EnumAttr");
1465 let expected = reg.cargo_home.join("registry/src/dummy-0.1.0/lib.rs");
1466 assert_eq!(
1467 found.unwrap(),
1468 expected,
1469 "pub enum with derive should be located"
1470 );
1471 }
1472
1473 #[test]
1475 #[serial]
1476 fn test_find_external_type_path_nested_module() {
1477 let (reg, mut analyzer) =
1478 FakeRegistry::new(&[("lib.rs", "mod inner {\n pub struct Nested;\n}\n")]);
1479 let found = analyzer.find_external_type_path_cached("Nested");
1480 let expected = reg.cargo_home.join("registry/src/dummy-0.1.0/lib.rs");
1481 assert_eq!(
1482 found.unwrap(),
1483 expected,
1484 "struct inside an inline mod should be located"
1485 );
1486 }
1487
1488 #[test]
1493 #[serial]
1494 fn test_find_external_type_path_no_false_positive_prefix() {
1495 let (_reg, mut analyzer) =
1496 FakeRegistry::new(&[("lib.rs", "pub struct ExternalFooBar;")]);
1497 let found = analyzer.find_external_type_path_cached("ExternalFoo");
1498 assert!(
1499 found.is_none(),
1500 "a prefix-named struct must not match the shorter identifier"
1501 );
1502 }
1503
1504 #[test]
1506 #[serial]
1507 fn test_find_external_type_path_missing() {
1508 let (_reg, mut analyzer) =
1509 FakeRegistry::new(&[("lib.rs", "pub struct SomethingElse;")]);
1510 let found = analyzer.find_external_type_path_cached("ExternalFoo");
1511 assert!(found.is_none(), "a missing type must resolve to None");
1512 }
1513
1514 #[test]
1516 #[serial]
1517 fn test_find_external_type_path_skips_unparseable_file() {
1518 let (_reg, mut analyzer) =
1519 FakeRegistry::new(&[("lib.rs", "this is not valid rust !!!")]);
1520 let found = analyzer.find_external_type_path_cached("ExternalFoo");
1521 assert!(
1522 found.is_none(),
1523 "an unparseable file must be skipped without panicking"
1524 );
1525 }
1526 }
1527
1528 mod unresolved_type_reporting {
1529 use super::*;
1530 use serial_test::serial;
1531 use std::env;
1532 use std::fs;
1533 use std::path::PathBuf;
1534
1535 #[test]
1537 fn fresh_analyzer_reports_no_unresolved_types() {
1538 let analyzer = CommandAnalyzer::default();
1539 assert!(
1540 analyzer.unresolved_types().is_empty(),
1541 "a fresh analyzer must report no unresolved types",
1542 );
1543 }
1544
1545 #[test]
1549 #[serial]
1550 fn records_unresolved_type_when_registry_is_empty() {
1551 let tmp_root: PathBuf = std::env::temp_dir().join(format!(
1553 "tauri_typegen_unresolved_empty_{}",
1554 std::time::SystemTime::now()
1555 .duration_since(std::time::UNIX_EPOCH)
1556 .unwrap()
1557 .as_nanos(),
1558 ));
1559 let _ = std::fs::remove_dir_all(&tmp_root);
1560 let cargo_home: PathBuf = tmp_root.join(".cargo");
1561 fs::create_dir_all(&cargo_home).expect("create empty cargo home");
1562 env::set_var("CARGO_HOME", &cargo_home);
1563
1564 let mut analyzer = CommandAnalyzer::default();
1565 let mut initial: HashSet<String> = HashSet::new();
1566 initial.insert("DefinitelyMissing".to_string());
1567
1568 analyzer
1569 .resolve_types_lazily(&initial)
1570 .expect("resolve pass");
1571
1572 let unresolved = analyzer.unresolved_types();
1573 assert!(
1574 unresolved.contains(&"DefinitelyMissing".to_string()),
1575 "an absent type must be recorded as unresolved, got: {:?}",
1576 unresolved,
1577 );
1578
1579 let _ = std::fs::remove_dir_all(&tmp_root);
1580 }
1581
1582 #[test]
1585 #[serial]
1586 fn records_unresolved_type_when_registry_dir_absent() {
1587 let tmp_root: PathBuf = std::env::temp_dir().join(format!(
1588 "tauri_typegen_unresolved_absent_{}",
1589 std::time::SystemTime::now()
1590 .duration_since(std::time::UNIX_EPOCH)
1591 .unwrap()
1592 .as_nanos(),
1593 ));
1594 let _ = std::fs::remove_dir_all(&tmp_root);
1595 fs::create_dir_all(&tmp_root).expect("create cargo home root");
1597 env::set_var("CARGO_HOME", &tmp_root);
1598
1599 let mut analyzer = CommandAnalyzer::default();
1600 let mut initial: HashSet<String> = HashSet::new();
1601 initial.insert("NoRegistryHere".to_string());
1602
1603 analyzer
1604 .resolve_types_lazily(&initial)
1605 .expect("resolve pass");
1606
1607 assert!(
1608 analyzer
1609 .unresolved_types()
1610 .contains(&"NoRegistryHere".to_string()),
1611 "a missing registry dir must still yield an unresolved report",
1612 );
1613
1614 let _ = std::fs::remove_dir_all(&tmp_root);
1615 }
1616
1617 #[test]
1622 #[serial]
1623 fn resolved_type_is_not_reported_unresolved() {
1624 let tmp_root: PathBuf = std::env::temp_dir().join(format!(
1625 "tauri_typegen_unresolved_resolved_{}",
1626 std::time::SystemTime::now()
1627 .duration_since(std::time::UNIX_EPOCH)
1628 .unwrap()
1629 .as_nanos(),
1630 ));
1631 let _ = std::fs::remove_dir_all(&tmp_root);
1632 let cargo_home: PathBuf = tmp_root.join(".cargo");
1633 fs::create_dir_all(&cargo_home).expect("create cargo home");
1634 env::set_var("CARGO_HOME", &cargo_home);
1635
1636 let src_file: PathBuf = tmp_root.join("types.rs");
1640 fs::write(
1641 &src_file,
1642 "use serde::Serialize;\n#[derive(Serialize)]\npub struct ResolvedType { a: i32 }",
1643 )
1644 .expect("write source");
1645
1646 let mut analyzer = CommandAnalyzer::default();
1647 analyzer
1649 .ast_cache
1650 .parse_and_cache_file(&src_file)
1651 .expect("parse source file");
1652 analyzer
1654 .dependency_graph
1655 .add_type_definition("ResolvedType".to_string(), src_file.clone());
1656
1657 let mut initial: HashSet<String> = HashSet::new();
1658 initial.insert("ResolvedType".to_string());
1659
1660 analyzer
1661 .resolve_types_lazily(&initial)
1662 .expect("resolve pass");
1663
1664 assert!(
1665 analyzer.discovered_structs.contains_key("ResolvedType"),
1666 "the in-project type should have been resolved",
1667 );
1668 assert!(
1669 !analyzer
1670 .unresolved_types()
1671 .contains(&"ResolvedType".to_string()),
1672 "a resolved in-project type must not be reported unresolved",
1673 );
1674
1675 let _ = std::fs::remove_dir_all(&tmp_root);
1676 }
1677
1678 #[test]
1682 #[serial]
1683 fn cargo_home_fallback_uses_userprofile_when_home_absent() {
1684 let tmp_root: PathBuf = std::env::temp_dir().join(format!(
1685 "tauri_typegen_userprofile_{}",
1686 std::time::SystemTime::now()
1687 .duration_since(std::time::UNIX_EPOCH)
1688 .unwrap()
1689 .as_nanos(),
1690 ));
1691 let _ = std::fs::remove_dir_all(&tmp_root);
1692 let src_dir: PathBuf = tmp_root.join(".cargo").join("registry/src");
1694 let crate_dir: PathBuf = src_dir.join("windep-0.1.0");
1695 fs::create_dir_all(&crate_dir).expect("create registry crate dir");
1696 let file_path: PathBuf = crate_dir.join("lib.rs");
1697 fs::write(&file_path, "pub struct WinOnly;").expect("write registry file");
1698
1699 env::remove_var("CARGO_HOME");
1700 env::remove_var("HOME");
1701 env::set_var("USERPROFILE", &tmp_root);
1702
1703 let mut analyzer = CommandAnalyzer::default();
1704 let found = analyzer.find_external_type_path_cached("WinOnly");
1705 assert_eq!(
1706 found.unwrap(),
1707 file_path,
1708 "USERPROFILE fallback must locate the type without HOME/CARGO_HOME",
1709 );
1710
1711 let _ = std::fs::remove_dir_all(&tmp_root);
1712 }
1713 }
1714
1715 mod external_type_lookup_caching {
1716 use super::*;
1717 use serial_test::serial;
1718 use std::env;
1719 use std::fs;
1720 use std::path::PathBuf;
1721
1722 fn registry_with(files: &[(&str, &str)]) -> (PathBuf, CommandAnalyzer) {
1726 let root: PathBuf = std::env::temp_dir().join(format!(
1727 "tauri_typegen_caching_{}",
1728 std::time::SystemTime::now()
1729 .duration_since(std::time::UNIX_EPOCH)
1730 .unwrap()
1731 .as_nanos(),
1732 ));
1733 let _ = std::fs::remove_dir_all(&root);
1734 let cargo_home: PathBuf = root.join(".cargo");
1735 let crate_dir: PathBuf = cargo_home.join("registry/src/dummy-0.1.0");
1736 fs::create_dir_all(&crate_dir).expect("create registry crate dir");
1737 for (name, content) in files {
1738 fs::write(crate_dir.join(name), content).expect("write file");
1739 }
1740 env::set_var("CARGO_HOME", &cargo_home);
1741 (root, CommandAnalyzer::default())
1742 }
1743
1744 #[test]
1747 #[serial]
1748 fn caches_positive_result() {
1749 let (root, mut analyzer) = registry_with(&[("lib.rs", "pub struct CachedFoo;")]);
1750
1751 let first = analyzer.find_external_type_path_cached("CachedFoo");
1752 assert!(first.is_some(), "first lookup should find the type");
1753 assert_eq!(
1755 analyzer.external_type_lookup_cache.get("CachedFoo"),
1756 Some(&first.clone()),
1757 "positive result must be memoized in external_type_lookup_cache",
1758 );
1759
1760 let second = analyzer.find_external_type_path_cached("CachedFoo");
1761 assert_eq!(first, second, "second lookup must return the cached value",);
1762
1763 let _ = std::fs::remove_dir_all(&root);
1764 }
1765
1766 #[test]
1770 #[serial]
1771 fn caches_negative_result() {
1772 let (root, mut analyzer) = registry_with(&[("lib.rs", "pub struct SomethingElse;")]);
1773
1774 let first = analyzer.find_external_type_path_cached("MissingType");
1775 assert!(first.is_none(), "first lookup should miss");
1776 assert_eq!(
1778 analyzer.external_type_lookup_cache.get("MissingType"),
1779 Some(&None),
1780 "negative result must be memoized as Some(None)",
1781 );
1782
1783 let second = analyzer.find_external_type_path_cached("MissingType");
1784 assert!(
1785 second.is_none(),
1786 "second lookup for a cached-miss must still return None",
1787 );
1788
1789 let _ = std::fs::remove_dir_all(&root);
1790 }
1791
1792 #[test]
1794 #[serial]
1795 fn caches_distinct_types_independently() {
1796 let (root, mut analyzer) =
1797 registry_with(&[("lib.rs", "pub struct Alpha;\npub struct Beta;\n")]);
1798
1799 let alpha = analyzer.find_external_type_path_cached("Alpha");
1800 let beta = analyzer.find_external_type_path_cached("Beta");
1801 let gamma = analyzer.find_external_type_path_cached("Gamma");
1803
1804 assert!(alpha.is_some() && beta.is_some());
1805 assert!(gamma.is_none());
1806 assert_eq!(analyzer.external_type_lookup_cache.len(), 3);
1807 assert!(analyzer.external_type_lookup_cache.contains_key("Alpha"));
1808 assert!(analyzer.external_type_lookup_cache.contains_key("Beta"));
1809 assert!(analyzer.external_type_lookup_cache.contains_key("Gamma"));
1810
1811 let _ = std::fs::remove_dir_all(&root);
1812 }
1813 }
1814
1815 mod external_type_cache_seeding {
1816 use super::*;
1817 use serial_test::serial;
1818 use std::env;
1819 use std::fs;
1820 use std::path::PathBuf;
1821
1822 #[test]
1826 #[serial]
1827 fn seeded_positive_entry_skips_walk() {
1828 let tmp_root: PathBuf = std::env::temp_dir().join(format!(
1829 "tauri_typegen_seeding_pos_{}",
1830 std::time::SystemTime::now()
1831 .duration_since(std::time::UNIX_EPOCH)
1832 .unwrap()
1833 .as_nanos(),
1834 ));
1835 let _ = std::fs::remove_dir_all(&tmp_root);
1836 let cargo_home: PathBuf = tmp_root.join(".cargo");
1838 fs::create_dir_all(&cargo_home).expect("create empty cargo home");
1839 env::set_var("CARGO_HOME", &cargo_home);
1840
1841 let mut analyzer = CommandAnalyzer::default();
1842 let mut seed: HashMap<String, Option<PathBuf>> = HashMap::new();
1843 seed.insert(
1844 "SeededType".to_string(),
1845 Some(PathBuf::from("/fake/registry/seeded.rs")),
1846 );
1847 analyzer.seed_external_type_cache(seed);
1848
1849 let found = analyzer.find_external_type_path_cached("SeededType");
1850 assert_eq!(
1851 found,
1852 Some(PathBuf::from("/fake/registry/seeded.rs")),
1853 "a seeded positive entry must be returned without walking",
1854 );
1855
1856 let _ = std::fs::remove_dir_all(&tmp_root);
1857 }
1858
1859 #[test]
1865 #[serial]
1866 fn seeded_negative_entry_skips_walk() {
1867 let tmp_root: PathBuf = std::env::temp_dir().join(format!(
1868 "tauri_typegen_seeding_neg_{}",
1869 std::time::SystemTime::now()
1870 .duration_since(std::time::UNIX_EPOCH)
1871 .unwrap()
1872 .as_nanos(),
1873 ));
1874 let _ = std::fs::remove_dir_all(&tmp_root);
1875 let cargo_home: PathBuf = tmp_root.join(".cargo");
1876 let crate_dir: PathBuf = cargo_home.join("registry/src/dummy-0.1.0");
1877 fs::create_dir_all(&crate_dir).expect("create registry crate dir");
1878 fs::write(crate_dir.join("lib.rs"), "pub struct ActuallyHere;").expect("write file");
1880 env::set_var("CARGO_HOME", &cargo_home);
1881
1882 let mut analyzer = CommandAnalyzer::default();
1883 let mut seed: HashMap<String, Option<PathBuf>> = HashMap::new();
1884 seed.insert("ActuallyHere".to_string(), None);
1885 analyzer.seed_external_type_cache(seed);
1886
1887 let found = analyzer.find_external_type_path_cached("ActuallyHere");
1888 assert!(
1889 found.is_none(),
1890 "a seeded negative entry must short-circuit and return None",
1891 );
1892
1893 let _ = std::fs::remove_dir_all(&tmp_root);
1894 }
1895
1896 #[test]
1899 #[serial]
1900 fn unseeded_type_falls_back_to_walk() {
1901 let tmp_root: PathBuf = std::env::temp_dir().join(format!(
1902 "tauri_typegen_seeding_fallback_{}",
1903 std::time::SystemTime::now()
1904 .duration_since(std::time::UNIX_EPOCH)
1905 .unwrap()
1906 .as_nanos(),
1907 ));
1908 let _ = std::fs::remove_dir_all(&tmp_root);
1909 let cargo_home: PathBuf = tmp_root.join(".cargo");
1910 let crate_dir: PathBuf = cargo_home.join("registry/src/dummy-0.1.0");
1911 fs::create_dir_all(&crate_dir).expect("create registry crate dir");
1912 let file_path: PathBuf = crate_dir.join("lib.rs");
1913 fs::write(&file_path, "pub struct WalkResolved;").expect("write file");
1914 env::set_var("CARGO_HOME", &cargo_home);
1915
1916 let mut analyzer = CommandAnalyzer::default();
1917 analyzer.seed_external_type_cache(HashMap::new());
1919
1920 let found = analyzer.find_external_type_path_cached("WalkResolved");
1921 assert_eq!(
1922 found.unwrap(),
1923 file_path,
1924 "an unseeded type must be found via the walk",
1925 );
1926 assert!(
1927 analyzer
1928 .external_type_lookup_cache()
1929 .contains_key("WalkResolved"),
1930 "the walk result must be memoized after resolving",
1931 );
1932
1933 let _ = std::fs::remove_dir_all(&tmp_root);
1934 }
1935 }
1936}