1use ryo_analysis::{ASTRegistry, CodeGraphV2, SymbolId, SymbolKind};
54use ryo_source::pure::{PureAttribute, PureFile, PureItem, PureMod, PureVis, ToSynError};
55use ryo_symbol::{SymbolPath, SymbolRegistry};
56use std::collections::{BTreeMap, HashMap, HashSet};
57
58type ModSymbolKey = (String, Vec<String>, bool);
60
61type ModSymbolValue = (bool, Vec<PureAttribute>);
63
64type CrateKey = (String, bool);
66
67type SymbolEntry = (SymbolId, SymbolPath, PureItem);
69
70struct MultiFileOpts<'a> {
72 is_main: bool,
74 module_use_items: &'a HashMap<ModSymbolKey, Vec<PureItem>>,
76 mod_symbols: &'a HashMap<ModSymbolKey, ModSymbolValue>,
78 inline_module_paths: &'a HashSet<String>,
80 affected_file_keys: Option<&'a HashSet<String>>,
82}
83
84#[derive(Debug, Clone)]
89pub struct GeneratedCrate {
90 pub crate_name: String,
92
93 pub files: HashMap<String, GeneratedFile>,
105}
106
107#[derive(Debug, Clone)]
109pub struct GeneratedFile {
110 pub source: String,
112 pub pure_file: PureFile,
114}
115
116#[derive(Debug, Clone, Default)]
142pub struct GeneratedWorkspace {
143 pub crates: HashMap<String, GeneratedCrate>,
145}
146
147impl GeneratedWorkspace {
148 pub fn total_files(&self) -> usize {
150 self.crates.values().map(|c| c.files.len()).sum()
151 }
152
153 pub fn iter_files(&self) -> impl Iterator<Item = (&str, &str, &GeneratedFile)> {
155 self.crates.iter().flat_map(|(crate_name, crate_data)| {
156 crate_data
157 .files
158 .iter()
159 .map(move |(path, file)| (crate_name.as_str(), path.as_str(), file))
160 })
161 }
162}
163
164#[derive(Debug, Clone, Default)]
194pub struct RegistryGenerator {
195 pub single_file: bool,
197 pub use_mod_rs: bool,
199}
200
201impl RegistryGenerator {
202 pub fn single_file() -> Self {
204 Self {
205 single_file: true,
206 use_mod_rs: false,
207 }
208 }
209
210 pub fn multi_file() -> Self {
212 Self {
213 single_file: false,
214 use_mod_rs: false,
215 }
216 }
217
218 pub fn multi_file_mod_rs() -> Self {
220 Self {
221 single_file: false,
222 use_mod_rs: true,
223 }
224 }
225
226 pub fn generate(
231 &self,
232 ast_registry: &ASTRegistry,
233 symbol_registry: &SymbolRegistry,
234 ) -> Result<GeneratedWorkspace, ToSynError> {
235 self.generate_internal(ast_registry, symbol_registry, None)
236 }
237
238 fn generate_internal(
246 &self,
247 ast_registry: &ASTRegistry,
248 symbol_registry: &SymbolRegistry,
249 affected_file_keys: Option<&HashSet<String>>,
250 ) -> Result<GeneratedWorkspace, ToSynError> {
251 let mut inline_module_paths: HashSet<String> = HashSet::new();
256 for id in ast_registry.inline_module_ids() {
257 if let Some(path) = symbol_registry.path(id) {
258 inline_module_paths.insert(path.to_string());
259 }
260 }
261
262 let mut crates: HashMap<CrateKey, Vec<SymbolEntry>> = HashMap::new();
265
266 for (id, item) in ast_registry.iter() {
267 let kind = symbol_registry.kind(id);
273 if matches!(
274 kind,
275 Some(SymbolKind::Impl | SymbolKind::Method | SymbolKind::Field)
276 ) {
277 continue;
278 }
279
280 if let Some(path) = symbol_registry.path(id) {
281 if path.segments().any(|s| s.starts_with("<impl ")) {
284 continue;
285 }
286
287 let path_str = path.to_string();
291 let is_child_of_inline = inline_module_paths.iter().any(|inline_path| {
292 let prefix_with_sep = format!("{}::", inline_path);
296 path_str.starts_with(&prefix_with_sep)
297 });
298 if is_child_of_inline {
299 continue;
300 }
301
302 let (crate_name, is_main) = if path.is_main_symbol() {
303 (
305 path.main_target_crate()
306 .unwrap_or(path.crate_name())
307 .to_string(),
308 true,
309 )
310 } else {
311 (path.crate_name().to_string(), false)
313 };
314
315 crates.entry((crate_name, is_main)).or_default().push((
316 id,
317 path.clone(),
318 item.clone(),
319 ));
320 }
321 }
322
323 let mut module_use_items: HashMap<ModSymbolKey, Vec<PureItem>> = HashMap::new();
326
327 for (module_id, items) in ast_registry.iter_module_items() {
328 tracing::debug!(
329 "iter_module_items: module_id={:?}, items_count={}, has_use={}",
330 module_id,
331 items.len(),
332 items.iter().any(|i| matches!(i, PureItem::Use(_)))
333 );
334 if let Some(path) = symbol_registry.path(module_id) {
335 let (crate_name, is_main) = if path.is_main_symbol() {
336 (
337 path.main_target_crate()
338 .unwrap_or(path.crate_name())
339 .to_string(),
340 true,
341 )
342 } else {
343 (path.crate_name().to_string(), false)
344 };
345
346 let segments: Vec<String> = path
351 .mod_path()
352 .iter()
353 .map(|s| s.name())
354 .filter(|name| !name.starts_with("<impl "))
355 .map(|s| s.to_string())
356 .collect();
357
358 let file_level_items: Vec<PureItem> = items
362 .iter()
363 .filter(|item| matches!(item, PureItem::Use(_) | PureItem::Impl(_)))
364 .cloned()
365 .collect();
366
367 if !file_level_items.is_empty() {
368 tracing::debug!(
369 "Adding {} file_level_items to module_use_items for path={}, segments={:?}",
370 file_level_items.len(),
371 path,
372 segments
373 );
374 module_use_items
375 .entry((crate_name, segments, is_main))
376 .or_default()
377 .extend(file_level_items);
378 }
379 }
380 }
381
382 for (crate_name, segments, is_main) in module_use_items.keys() {
385 if segments.is_empty() {
386 crates.entry((crate_name.clone(), *is_main)).or_default();
388 }
389 }
390
391 let mut mod_symbols: HashMap<ModSymbolKey, ModSymbolValue> = HashMap::new();
394 for (id, path) in symbol_registry.iter() {
395 let Some(kind) = symbol_registry.kind(id) else {
396 continue;
397 };
398 if kind != SymbolKind::Mod {
399 continue;
400 }
401 if ast_registry.is_inline_module(id) {
405 continue;
406 }
407 let mod_attrs = if let Some(PureItem::Mod(m)) = ast_registry.get(id) {
408 m.attrs.clone()
409 } else {
410 Vec::new()
411 };
412
413 let segments: Vec<&str> = path.segments().collect();
414 let (crate_name, is_main) = if path.is_main_symbol() {
415 (
416 path.main_target_crate()
417 .unwrap_or(path.crate_name())
418 .to_string(),
419 true,
420 )
421 } else {
422 (path.crate_name().to_string(), false)
423 };
424
425 if segments.len() <= 1 || (path.is_main_symbol() && segments.len() <= 2) {
428 mod_symbols.insert(
430 (crate_name.clone(), vec![], is_main),
431 (true, mod_attrs), );
433 crates.entry((crate_name, is_main)).or_default();
434 continue;
435 }
436
437 let skip_count = if is_main { 2 } else { 1 };
440 let module_segments: Vec<String> = segments[skip_count..]
441 .iter()
442 .filter(|s| !s.starts_with("<impl "))
443 .map(|s| s.to_string())
444 .collect();
445
446 let is_pub = symbol_registry
448 .visibility(id)
449 .map(|v| matches!(v, ryo_symbol::Visibility::Public))
450 .unwrap_or(false);
451
452 mod_symbols.insert(
453 (crate_name.clone(), module_segments, is_main),
454 (is_pub, mod_attrs),
455 );
456
457 crates.entry((crate_name, is_main)).or_default();
459 }
460
461 let mut workspace = GeneratedWorkspace::default();
463
464 for ((crate_name, is_main), symbols) in crates {
465 let entry = workspace
466 .crates
467 .entry(crate_name.clone())
468 .or_insert_with(|| GeneratedCrate {
469 crate_name: crate_name.clone(),
470 files: HashMap::new(),
471 });
472
473 let generated_files = if self.single_file {
474 self.generate_single_file_inner(
475 &crate_name,
476 symbols,
477 is_main,
478 &module_use_items,
479 &mod_symbols,
480 )
481 } else {
482 self.generate_multi_file_inner(
483 &crate_name,
484 symbols,
485 MultiFileOpts {
486 is_main,
487 module_use_items: &module_use_items,
488 mod_symbols: &mod_symbols,
489 inline_module_paths: &inline_module_paths,
490 affected_file_keys,
491 },
492 )
493 };
494
495 entry.files.extend(generated_files?);
496 }
497
498 Ok(workspace)
499 }
500
501 pub fn generate_affected(
553 &self,
554 ast_registry: &ASTRegistry,
555 symbol_registry: &SymbolRegistry,
556 modified_symbols: &[SymbolId],
557 _metadata: &ryo_symbol::CargoMetadataProvider,
558 ) -> Result<GeneratedWorkspace, ToSynError> {
559 if modified_symbols.is_empty() {
560 return Ok(GeneratedWorkspace::default());
561 }
562
563 let mut affected_file_keys: HashSet<String> = modified_symbols
577 .iter()
578 .filter_map(|&id| {
579 let path = symbol_registry.path(id)?;
580 let resolved =
581 Self::resolve_to_file_level_symbol(path, symbol_registry, ast_registry);
582 Some(self.symbol_path_to_file_key(&resolved))
583 })
584 .collect();
585
586 for &id in modified_symbols {
592 if symbol_registry.kind(id) != Some(SymbolKind::Mod) {
593 continue;
594 }
595 if ast_registry.is_inline_module(id) {
596 continue;
597 }
598 if let Some(path) = symbol_registry.path(id) {
599 let parent_key = self.symbol_path_to_file_key(path);
601 if let Some(mod_name) = path.segments().last() {
602 let mod_file_key = self.derive_child_path(&parent_key, mod_name);
603 affected_file_keys.insert(mod_file_key);
604 }
605 }
606 }
607
608 tracing::debug!(
609 "generate_affected: {} modified symbols → {} affected file keys: {:?}",
610 modified_symbols.len(),
611 affected_file_keys.len(),
612 affected_file_keys,
613 );
614
615 self.generate_internal(ast_registry, symbol_registry, Some(&affected_file_keys))
620 }
621
622 fn resolve_to_file_level_symbol(
640 path: &SymbolPath,
641 registry: &SymbolRegistry,
642 ast_registry: &ASTRegistry,
643 ) -> SymbolPath {
644 let mut current = path.clone();
645 loop {
646 if let Some(parent) = current.parent() {
647 if let Some(parent_id) = registry.lookup(&parent) {
648 if registry.kind(parent_id) == Some(SymbolKind::Mod) {
649 if ast_registry.is_inline_module(parent_id) {
652 current = parent;
653 continue;
654 }
655 return current;
656 }
657 }
658 current = parent;
659 } else {
660 return current;
661 }
662 }
663 }
664
665 fn symbol_path_to_file_key(&self, path: &SymbolPath) -> String {
676 let segments: Vec<&str> = path.segments().collect();
677 let is_main = path.is_main_symbol();
678 let skip_count = if is_main { 2 } else { 1 };
679
680 let item_segments = if segments.len() > skip_count {
694 &segments[skip_count..segments.len() - 1]
695 } else {
696 &[]
697 };
698
699 let module_segments: Vec<&str> = item_segments
701 .iter()
702 .filter(|s| !s.starts_with("<impl "))
703 .copied()
704 .collect();
705
706 let root_file = if is_main { "src/main.rs" } else { "src/lib.rs" };
707
708 if module_segments.is_empty() {
709 root_file.to_string()
710 } else {
711 let mut current = root_file.to_string();
713 for seg in &module_segments {
714 current = self.derive_child_path(¤t, seg);
715 }
716 current
717 }
718 }
719
720 pub fn generate_with_graph(
731 &self,
732 code_graph: &CodeGraphV2,
733 ast_registry: &ASTRegistry,
734 symbol_registry: &SymbolRegistry,
735 ) -> Result<GeneratedWorkspace, ToSynError> {
736 let mut workspace = GeneratedWorkspace::default();
737
738 let mut crate_children: HashMap<(String, bool), Vec<SymbolId>> = HashMap::new();
741
742 for &root_id in code_graph.crate_roots() {
743 if let Some(path) = symbol_registry.path(root_id) {
745 let (crate_name, is_main) = if path.is_main_symbol() {
746 (
747 path.main_target_crate()
748 .unwrap_or(path.crate_name())
749 .to_string(),
750 true,
751 )
752 } else {
753 (path.crate_name().to_string(), false)
754 };
755
756 let children: Vec<SymbolId> = code_graph.children_of(root_id).collect();
758 crate_children
759 .entry((crate_name, is_main))
760 .or_default()
761 .extend(children);
762 }
763 }
764
765 for ((crate_name, is_main), root_ids) in crate_children {
767 let entry = workspace
768 .crates
769 .entry(crate_name.clone())
770 .or_insert_with(|| GeneratedCrate {
771 crate_name: crate_name.clone(),
772 files: HashMap::new(),
773 });
774
775 let generated_files = if self.single_file {
776 self.generate_single_file_from_graph(
777 &crate_name,
778 &root_ids,
779 is_main,
780 code_graph,
781 ast_registry,
782 symbol_registry,
783 )
784 } else {
785 self.generate_multi_file_from_graph(
786 &crate_name,
787 &root_ids,
788 is_main,
789 code_graph,
790 ast_registry,
791 symbol_registry,
792 )
793 };
794
795 entry.files.extend(generated_files?);
796 }
797
798 Ok(workspace)
799 }
800
801 fn generate_single_file_from_graph(
803 &self,
804 _crate_name: &str,
805 root_ids: &[SymbolId],
806 is_main: bool,
807 code_graph: &CodeGraphV2,
808 ast_registry: &ASTRegistry,
809 symbol_registry: &SymbolRegistry,
810 ) -> Result<HashMap<String, GeneratedFile>, ToSynError> {
811 let items =
812 self.collect_items_from_graph(root_ids, code_graph, ast_registry, symbol_registry);
813
814 let file_attrs = self.extract_file_attrs_from_ids(root_ids, ast_registry, symbol_registry);
816
817 let pure_file = PureFile {
818 attrs: file_attrs,
819 items,
820 };
821 let source = pure_file.to_source()?;
822
823 let root_file = if is_main { "src/main.rs" } else { "src/lib.rs" };
824
825 let mut files = HashMap::new();
826 files.insert(root_file.to_string(), GeneratedFile { source, pure_file });
827 Ok(files)
828 }
829
830 fn generate_multi_file_from_graph(
832 &self,
833 _crate_name: &str,
834 root_ids: &[SymbolId],
835 is_main: bool,
836 code_graph: &CodeGraphV2,
837 ast_registry: &ASTRegistry,
838 symbol_registry: &SymbolRegistry,
839 ) -> Result<HashMap<String, GeneratedFile>, ToSynError> {
840 let root_file = if is_main { "src/main.rs" } else { "src/lib.rs" };
841
842 let mut files = HashMap::new();
843 self.generate_file_from_graph(
844 root_file,
845 root_ids,
846 code_graph,
847 ast_registry,
848 symbol_registry,
849 &mut files,
850 )?;
851 Ok(files)
852 }
853
854 fn collect_items_from_graph(
856 &self,
857 ids: &[SymbolId],
858 code_graph: &CodeGraphV2,
859 ast_registry: &ASTRegistry,
860 symbol_registry: &SymbolRegistry,
861 ) -> Vec<PureItem> {
862 let mut items = Vec::new();
863
864 let mut mods: Vec<SymbolId> = Vec::new();
866 let mut non_mods: Vec<SymbolId> = Vec::new();
867
868 for &id in ids {
869 if let Some(kind) = symbol_registry.kind(id) {
870 if kind == SymbolKind::Mod {
871 if let Some(path) = symbol_registry.path(id) {
874 if path.is_crate_root() {
875 non_mods.push(id);
878 } else {
879 mods.push(id);
882 }
883 } else {
884 mods.push(id);
885 }
886 } else {
887 non_mods.push(id);
888 }
889 }
890 }
891
892 for id in non_mods {
895 if symbol_registry.kind(id) == Some(SymbolKind::Impl) {
897 continue;
898 }
899 if let Some(item) = ast_registry.get(id) {
900 items.push(item.clone());
901 }
902 }
903
904 for &id in ids {
908 if let Some(module_items) = ast_registry.get_module_items(id) {
909 for item in module_items {
910 if matches!(item, PureItem::Use(_) | PureItem::Impl(_)) {
911 items.push(item.clone());
912 }
913 }
914 }
915 }
916
917 for mod_id in mods {
919 let children: Vec<SymbolId> = code_graph.children_of(mod_id).collect();
920 let child_items =
921 self.collect_items_from_graph(&children, code_graph, ast_registry, symbol_registry);
922
923 if let Some(path) = symbol_registry.path(mod_id) {
925 let mod_name = path.segments().last().unwrap_or("unknown").to_string();
926
927 let vis = if child_items.iter().any(is_public) {
929 PureVis::Public
930 } else {
931 PureVis::Private
932 };
933
934 let mut all_items = Vec::new();
936 if let Some(module_items) = ast_registry.get_module_items(mod_id) {
937 for item in module_items {
938 if let PureItem::Use(_) = item {
939 all_items.push(item.clone());
940 }
941 }
942 }
943 all_items.extend(child_items);
944
945 let mod_attrs = ast_registry
947 .get(mod_id)
948 .and_then(|item| {
949 if let PureItem::Mod(m) = item {
950 Some(m.attrs.clone())
951 } else {
952 None
953 }
954 })
955 .unwrap_or_default();
956
957 items.push(PureItem::Mod(PureMod {
958 attrs: mod_attrs,
959 vis,
960 name: mod_name,
961 items: all_items,
962 }));
963 }
964 }
965
966 sort_items(&mut items);
969 items
970 }
971
972 fn generate_file_from_graph(
974 &self,
975 current_path: &str,
976 ids: &[SymbolId],
977 code_graph: &CodeGraphV2,
978 ast_registry: &ASTRegistry,
979 symbol_registry: &SymbolRegistry,
980 files: &mut HashMap<String, GeneratedFile>,
981 ) -> Result<(), ToSynError> {
982 let mut items = Vec::new();
983
984 let mut mods: Vec<SymbolId> = Vec::new();
986 let mut non_mods: Vec<SymbolId> = Vec::new();
987
988 for &id in ids {
989 if let Some(kind) = symbol_registry.kind(id) {
990 if kind == SymbolKind::Mod {
991 if let Some(path) = symbol_registry.path(id) {
994 if path.is_crate_root() {
995 non_mods.push(id);
998 } else {
999 mods.push(id);
1002 }
1003 } else {
1004 mods.push(id);
1005 }
1006 } else {
1007 non_mods.push(id);
1008 }
1009 }
1010 }
1011
1012 for id in non_mods {
1015 if symbol_registry.kind(id) == Some(SymbolKind::Impl) {
1017 continue;
1018 }
1019 if let Some(item) = ast_registry.get(id) {
1020 items.push(item.clone());
1021 }
1022 }
1023
1024 for &id in ids {
1027 if let Some(module_items) = ast_registry.get_module_items(id) {
1028 for item in module_items {
1029 if matches!(item, PureItem::Use(_) | PureItem::Impl(_)) {
1030 items.push(item.clone());
1031 }
1032 }
1033 }
1034 }
1035
1036 let mut inline_mod_ids: Vec<SymbolId> = Vec::new();
1039
1040 for &mod_id in &mods {
1041 if let Some(path) = symbol_registry.path(mod_id) {
1042 let mod_name = path.segments().last().unwrap_or("unknown").to_string();
1043
1044 if let Some(PureItem::Mod(m)) = ast_registry.get(mod_id) {
1046 if !m.items.is_empty() {
1047 items.push(PureItem::Mod(m.clone()));
1049 inline_mod_ids.push(mod_id);
1050 continue;
1051 }
1052 }
1053
1054 let (vis, attrs) = ast_registry
1058 .get(mod_id)
1059 .and_then(|item| {
1060 if let PureItem::Mod(m) = item {
1061 Some((m.vis.clone(), m.attrs.clone()))
1062 } else {
1063 None
1064 }
1065 })
1066 .unwrap_or((PureVis::Private, vec![]));
1067
1068 items.push(PureItem::Mod(PureMod {
1069 attrs,
1070 vis,
1071 name: mod_name,
1072 items: vec![], }));
1074 }
1075 }
1076
1077 sort_items(&mut items);
1084
1085 let file_attrs = self.extract_file_attrs_from_ids(ids, ast_registry, symbol_registry);
1087
1088 let pure_file = PureFile {
1089 attrs: file_attrs,
1090 items,
1091 };
1092 let source = pure_file.to_source()?;
1093 files.insert(
1094 current_path.to_string(),
1095 GeneratedFile { source, pure_file },
1096 );
1097
1098 for mod_id in mods {
1100 if inline_mod_ids.contains(&mod_id) {
1102 continue;
1103 }
1104
1105 if let Some(path) = symbol_registry.path(mod_id) {
1106 let mod_name = path.segments().last().unwrap_or("unknown").to_string();
1107 let child_path = self.derive_child_path(current_path, &mod_name);
1108 let children: Vec<SymbolId> = code_graph.children_of(mod_id).collect();
1109 self.generate_file_from_graph(
1110 &child_path,
1111 &children,
1112 code_graph,
1113 ast_registry,
1114 symbol_registry,
1115 files,
1116 )?;
1117 }
1118 }
1119 Ok(())
1120 }
1121
1122 fn generate_single_file_inner(
1126 &self,
1127 crate_name: &str,
1128 symbols: Vec<(SymbolId, SymbolPath, PureItem)>,
1129 is_main: bool,
1130 module_use_items: &HashMap<ModSymbolKey, Vec<PureItem>>,
1131 mod_symbols: &HashMap<ModSymbolKey, ModSymbolValue>,
1132 ) -> Result<HashMap<String, GeneratedFile>, ToSynError> {
1133 let mut tree = self.build_module_tree(crate_name, &symbols, is_main);
1135
1136 self.add_mod_symbols_to_tree(&mut tree, crate_name, is_main, mod_symbols);
1138
1139 self.add_use_items_to_tree(&mut tree, crate_name, is_main, module_use_items);
1141
1142 let pure_file = self.tree_to_pure_file(&tree);
1144 let source = pure_file.to_source()?;
1145
1146 let root_file = if is_main { "src/main.rs" } else { "src/lib.rs" };
1147
1148 let mut files = HashMap::new();
1149 files.insert(root_file.to_string(), GeneratedFile { source, pure_file });
1150 Ok(files)
1151 }
1152
1153 fn generate_multi_file_inner(
1158 &self,
1159 crate_name: &str,
1160 symbols: Vec<SymbolEntry>,
1161 opts: MultiFileOpts<'_>,
1162 ) -> Result<HashMap<String, GeneratedFile>, ToSynError> {
1163 let MultiFileOpts {
1164 is_main,
1165 module_use_items,
1166 mod_symbols,
1167 inline_module_paths,
1168 affected_file_keys,
1169 } = opts;
1170
1171 let mut tree = self.build_module_tree(crate_name, &symbols, is_main);
1173
1174 self.add_mod_symbols_to_tree(&mut tree, crate_name, is_main, mod_symbols);
1176
1177 self.add_use_items_to_tree(&mut tree, crate_name, is_main, module_use_items);
1179
1180 let root_file = if is_main { "src/main.rs" } else { "src/lib.rs" };
1181
1182 let crate_prefix = format!("{}::", crate_name);
1184 let inline_mod_names: HashSet<String> = inline_module_paths
1185 .iter()
1186 .filter(|p| p.starts_with(&crate_prefix))
1187 .filter_map(|p| p.strip_prefix(&crate_prefix))
1188 .map(|s| {
1189 s.split("::").last().unwrap_or(s).to_string()
1191 })
1192 .collect();
1193
1194 let mut files = HashMap::new();
1196 self.generate_files_from_tree(
1197 &tree,
1198 root_file,
1199 &mut files,
1200 &inline_mod_names,
1201 affected_file_keys,
1202 )?;
1203 Ok(files)
1204 }
1205
1206 fn add_use_items_to_tree(
1208 &self,
1209 tree: &mut ModuleNode,
1210 crate_name: &str,
1211 is_main: bool,
1212 module_use_items: &HashMap<ModSymbolKey, Vec<PureItem>>,
1213 ) {
1214 tracing::debug!(
1215 "add_use_items_to_tree: crate_name={}, is_main={}, module_use_items_count={}",
1216 crate_name,
1217 is_main,
1218 module_use_items.len()
1219 );
1220 let mut items_to_add: Vec<(Vec<String>, Vec<PureItem>)> = Vec::new();
1222
1223 for ((item_crate, segments, item_is_main), use_items) in module_use_items {
1224 if item_crate != crate_name || *item_is_main != is_main {
1225 continue;
1226 }
1227 tracing::debug!(
1228 " Matched: segments={:?}, use_items_count={}",
1229 segments,
1230 use_items.len()
1231 );
1232 items_to_add.push((segments.clone(), use_items.clone()));
1233 }
1234
1235 for (segments, use_items) in items_to_add {
1237 let target_node = if segments.is_empty() {
1238 tree as &mut ModuleNode
1239 } else {
1240 let seg_refs: Vec<&str> = segments.iter().map(|s| s.as_str()).collect();
1241 tree.get_or_create_path(&seg_refs)
1242 };
1243
1244 for use_item in use_items {
1246 target_node.items.push(use_item);
1247 }
1248 }
1249 }
1250
1251 fn add_mod_symbols_to_tree(
1258 &self,
1259 tree: &mut ModuleNode,
1260 crate_name: &str,
1261 is_main: bool,
1262 mod_symbols: &HashMap<ModSymbolKey, ModSymbolValue>,
1263 ) {
1264 for ((item_crate, segments, item_is_main), (is_pub, attrs)) in mod_symbols {
1265 if item_crate != crate_name || *item_is_main != is_main {
1266 continue;
1267 }
1268 let node = if segments.is_empty() {
1270 tree as &mut ModuleNode
1272 } else {
1273 let seg_refs: Vec<&str> = segments.iter().map(|s| s.as_str()).collect();
1274 tree.get_or_create_path(&seg_refs)
1275 };
1276
1277 if !segments.is_empty() && node.is_pub.is_none() {
1279 node.is_pub = Some(*is_pub);
1280 }
1281 if node.attrs.is_empty() && !attrs.is_empty() {
1283 node.attrs = attrs.clone();
1284 }
1285 }
1286 }
1287
1288 fn build_module_tree(
1292 &self,
1293 crate_name: &str,
1294 symbols: &[(SymbolId, SymbolPath, PureItem)],
1295 is_main: bool,
1296 ) -> ModuleNode {
1297 let mut root = ModuleNode::new(crate_name.to_string());
1298
1299 for (_id, path, item) in symbols {
1300 let segments: Vec<&str> = path.segments().collect();
1302
1303 let skip_count = if is_main { 2 } else { 1 }; if segments.len() <= skip_count {
1308 continue;
1310 }
1311
1312 let module_segments: Vec<&str> = segments[skip_count..segments.len() - 1]
1315 .iter()
1316 .filter(|s| !s.starts_with("<impl "))
1317 .copied()
1318 .collect();
1319
1320 let target_module = root.get_or_create_path(&module_segments);
1322
1323 target_module.items.push(item.clone());
1325 }
1326
1327 root
1328 }
1329
1330 fn tree_to_pure_file(&self, tree: &ModuleNode) -> PureFile {
1332 let items = self.collect_items_recursive(tree, true);
1333 let file_attrs: Vec<PureAttribute> =
1335 tree.attrs.iter().filter(|a| a.is_inner).cloned().collect();
1336 PureFile {
1337 attrs: file_attrs,
1338 items,
1339 }
1340 }
1341
1342 fn collect_items_recursive(&self, node: &ModuleNode, _is_root: bool) -> Vec<PureItem> {
1344 let mut items = Vec::new();
1345
1346 let mut direct_items: Vec<PureItem> = node
1350 .items
1351 .iter()
1352 .filter(|item| match item {
1353 PureItem::Mod(m) => !m.items.is_empty(), _ => true,
1355 })
1356 .cloned()
1357 .collect();
1358 sort_items(&mut direct_items);
1361 items.extend(direct_items);
1362
1363 for (name, child) in &node.children {
1365 let child_items = self.collect_items_recursive(child, false);
1366
1367 let vis = if let Some(is_pub) = child.is_pub {
1369 if is_pub {
1370 PureVis::Public
1371 } else {
1372 PureVis::Private
1373 }
1374 } else if child.items.iter().any(is_public) {
1375 PureVis::Public
1376 } else {
1377 PureVis::Private
1378 };
1379
1380 let mod_attrs: Vec<PureAttribute> = child
1382 .attrs
1383 .iter()
1384 .filter(|a| !a.is_inner)
1385 .cloned()
1386 .collect();
1387
1388 items.push(PureItem::Mod(PureMod {
1389 attrs: mod_attrs,
1390 vis,
1391 name: name.clone(),
1392 items: child_items,
1393 }));
1394 }
1395
1396 items
1397 }
1398
1399 fn generate_files_from_tree(
1408 &self,
1409 tree: &ModuleNode,
1410 current_path: &str,
1411 files: &mut HashMap<String, GeneratedFile>,
1412 known_inline_mods: &HashSet<String>,
1413 affected_file_keys: Option<&HashSet<String>>,
1414 ) -> Result<(), ToSynError> {
1415 tracing::debug!(
1416 "Generating file: {} (module: {}, {} children, {} items, known_inline_mods={:?})",
1417 current_path,
1418 tree.name,
1419 tree.children.len(),
1420 tree.items.len(),
1421 known_inline_mods
1422 );
1423
1424 let mut items = Vec::new();
1426
1427 let mut direct_items: Vec<PureItem> = tree
1433 .items
1434 .iter()
1435 .filter(|item| match item {
1436 PureItem::Mod(m) => {
1437 known_inline_mods.contains(&m.name)
1439 }
1440 _ => true,
1441 })
1442 .cloned()
1443 .collect();
1444 sort_items(&mut direct_items);
1447
1448 let inline_mod_names: HashSet<String> = known_inline_mods.clone();
1452
1453 items.extend(direct_items);
1454
1455 for (name, child) in &tree.children {
1458 if inline_mod_names.contains(name) {
1459 continue;
1460 }
1461
1462 let original_mod = tree.items.iter().find_map(|item| {
1465 if let PureItem::Mod(m) = item {
1466 if &m.name == name {
1467 return Some(m);
1468 }
1469 }
1470 None
1471 });
1472
1473 let vis = if let Some(m) = original_mod {
1475 m.vis.clone()
1476 } else if let Some(is_pub) = child.is_pub {
1477 if is_pub {
1478 PureVis::Public
1479 } else {
1480 PureVis::Private
1481 }
1482 } else {
1483 PureVis::Private
1484 };
1485
1486 let mod_attrs: Vec<PureAttribute> = if let Some(m) = original_mod {
1488 m.attrs.iter().filter(|a| !a.is_inner).cloned().collect()
1489 } else {
1490 child
1491 .attrs
1492 .iter()
1493 .filter(|a| !a.is_inner)
1494 .cloned()
1495 .collect()
1496 };
1497
1498 items.push(PureItem::Mod(PureMod {
1499 attrs: mod_attrs,
1500 vis,
1501 name: name.clone(),
1502 items: vec![], }));
1504 }
1505
1506 let is_affected = affected_file_keys
1511 .map(|keys| keys.contains(current_path))
1512 .unwrap_or(true);
1513
1514 if is_affected {
1515 let file_attrs: Vec<PureAttribute> =
1516 tree.attrs.iter().filter(|a| a.is_inner).cloned().collect();
1517 let pure_file = PureFile {
1518 attrs: file_attrs,
1519 items,
1520 };
1521 let source = pure_file.to_source()?;
1522 files.insert(
1523 current_path.to_string(),
1524 GeneratedFile { source, pure_file },
1525 );
1526 } else {
1527 tracing::debug!("Skipping unaffected file: {}", current_path);
1528 }
1529
1530 for (name, child) in &tree.children {
1533 if inline_mod_names.contains(name) {
1534 tracing::debug!("Skipping inline module {} (no separate file)", name);
1535 continue;
1536 }
1537 let child_path = self.derive_child_path(current_path, name);
1538 tracing::debug!(" → Child module: {} at {}", name, child_path);
1539 self.generate_files_from_tree(
1540 child,
1541 &child_path,
1542 files,
1543 known_inline_mods,
1544 affected_file_keys,
1545 )?;
1546 }
1547 Ok(())
1548 }
1549
1550 fn derive_child_path(&self, parent_path: &str, child_name: &str) -> String {
1552 let parent_dir = if parent_path.ends_with("/lib.rs") || parent_path.ends_with("/main.rs") {
1554 parent_path
1556 .trim_end_matches("lib.rs")
1557 .trim_end_matches("main.rs")
1558 } else if parent_path.ends_with("/mod.rs") {
1559 parent_path.trim_end_matches("mod.rs")
1561 } else {
1562 parent_path.trim_end_matches(".rs")
1564 };
1565
1566 if self.use_mod_rs {
1567 format!("{}{}/mod.rs", parent_dir, child_name)
1569 } else {
1570 if parent_path.ends_with("/lib.rs") || parent_path.ends_with("/main.rs") {
1572 format!("{}{}.rs", parent_dir, child_name)
1573 } else {
1574 format!("{}/{}.rs", parent_dir, child_name)
1575 }
1576 }
1577 }
1578
1579 fn extract_file_attrs_from_ids(
1584 &self,
1585 ids: &[SymbolId],
1586 ast_registry: &ASTRegistry,
1587 symbol_registry: &SymbolRegistry,
1588 ) -> Vec<PureAttribute> {
1589 for &id in ids {
1590 if let Some(kind) = symbol_registry.kind(id) {
1592 if kind == SymbolKind::Mod {
1593 if let Some(path) = symbol_registry.path(id) {
1594 if path.is_crate_root() {
1595 if let Some(PureItem::Mod(m)) = ast_registry.get(id) {
1597 return m.attrs.iter().filter(|a| a.is_inner).cloned().collect();
1598 }
1599 }
1600 }
1601 }
1602 }
1603 }
1604 Vec::new()
1605 }
1606}
1607
1608#[derive(Debug, Clone, Default)]
1610struct ModuleNode {
1611 name: String,
1612 items: Vec<PureItem>,
1613 children: BTreeMap<String, ModuleNode>,
1614 is_pub: Option<bool>,
1616 attrs: Vec<PureAttribute>,
1620}
1621
1622impl ModuleNode {
1623 fn new(name: String) -> Self {
1624 Self {
1625 name,
1626 items: Vec::new(),
1627 children: BTreeMap::new(),
1628 is_pub: None,
1629 attrs: Vec::new(),
1630 }
1631 }
1632
1633 fn get_or_create_path(&mut self, segments: &[&str]) -> &mut ModuleNode {
1634 if segments.is_empty() {
1635 return self;
1636 }
1637
1638 let child = self
1639 .children
1640 .entry(segments[0].to_string())
1641 .or_insert_with(|| ModuleNode::new(segments[0].to_string()));
1642
1643 child.get_or_create_path(&segments[1..])
1644 }
1645}
1646
1647fn sort_items(items: &mut [PureItem]) {
1657 items.sort_by(|a, b| {
1658 let kind_a = item_sort_key(a);
1659 let kind_b = item_sort_key(b);
1660
1661 match kind_a.cmp(&kind_b) {
1662 std::cmp::Ordering::Equal => {
1663 let name_cmp = item_name(a).cmp(item_name(b));
1664 if name_cmp != std::cmp::Ordering::Equal {
1665 return name_cmp;
1666 }
1667 match (a, b) {
1670 (PureItem::Impl(a_impl), PureItem::Impl(b_impl)) => {
1671 match (&a_impl.trait_, &b_impl.trait_) {
1672 (None, None) => std::cmp::Ordering::Equal,
1673 (None, Some(_)) => std::cmp::Ordering::Less,
1674 (Some(_), None) => std::cmp::Ordering::Greater,
1675 (Some(a_trait), Some(b_trait)) => a_trait.cmp(b_trait),
1676 }
1677 }
1678 _ => std::cmp::Ordering::Equal,
1679 }
1680 }
1681 other => other,
1682 }
1683 });
1684}
1685
1686fn item_sort_key(item: &PureItem) -> u8 {
1687 match item {
1688 PureItem::Use(_) => 0,
1689 PureItem::Const(_) => 1,
1690 PureItem::Static(_) => 2,
1691 PureItem::Type(_) => 3,
1692 PureItem::Struct(_) => 4,
1693 PureItem::Enum(_) => 5,
1694 PureItem::Trait(_) => 6,
1695 PureItem::Fn(_) => 7,
1696 PureItem::Impl(_) => 8,
1697 PureItem::Mod(m) if m.name == "tests" => 11, PureItem::Mod(_) => 9,
1699 PureItem::Macro(_) => 10,
1700 PureItem::Other(_) => 12,
1701 }
1702}
1703
1704fn item_name(item: &PureItem) -> &str {
1705 match item {
1706 PureItem::Struct(s) => &s.name,
1707 PureItem::Enum(e) => &e.name,
1708 PureItem::Fn(f) => &f.name,
1709 PureItem::Trait(t) => &t.name,
1710 PureItem::Impl(i) => &i.self_ty,
1711 PureItem::Const(c) => &c.name,
1712 PureItem::Static(s) => &s.name,
1713 PureItem::Type(t) => &t.name,
1714 PureItem::Mod(m) => &m.name,
1715 PureItem::Macro(m) => &m.path,
1716 _ => "",
1717 }
1718}
1719
1720fn is_public(item: &PureItem) -> bool {
1721 match item {
1722 PureItem::Struct(s) => matches!(s.vis, PureVis::Public),
1723 PureItem::Enum(e) => matches!(e.vis, PureVis::Public),
1724 PureItem::Fn(f) => matches!(f.vis, PureVis::Public),
1725 PureItem::Trait(t) => matches!(t.vis, PureVis::Public),
1726 PureItem::Const(c) => matches!(c.vis, PureVis::Public),
1727 PureItem::Static(s) => matches!(s.vis, PureVis::Public),
1728 PureItem::Type(t) => matches!(t.vis, PureVis::Public),
1729 PureItem::Mod(m) => matches!(m.vis, PureVis::Public),
1730 _ => false,
1731 }
1732}
1733
1734#[cfg(test)]
1735mod tests {
1736 use super::*;
1737 use ryo_source::pure::{
1738 PureBlock, PureEnum, PureFields, PureFn, PureGenerics, PureStruct, PureVariant,
1739 };
1740 use ryo_symbol::SymbolKind;
1741
1742 fn make_struct(name: &str) -> PureItem {
1743 PureItem::Struct(PureStruct {
1744 attrs: vec![],
1745 vis: PureVis::Public,
1746 name: name.to_string(),
1747 generics: PureGenerics::default(),
1748 fields: PureFields::Unit,
1749 })
1750 }
1751
1752 fn make_fn(name: &str) -> PureItem {
1753 PureItem::Fn(PureFn {
1754 attrs: vec![],
1755 vis: PureVis::Public,
1756 is_async: false,
1757 is_async_inferred: false,
1758 is_const: false,
1759 is_unsafe: false,
1760 abi: None,
1761 name: name.to_string(),
1762 generics: PureGenerics::default(),
1763 params: vec![],
1764 ret: None,
1765 body: PureBlock::default(),
1766 })
1767 }
1768
1769 fn make_path(s: &str) -> SymbolPath {
1770 SymbolPath::parse(s).unwrap()
1771 }
1772
1773 #[test]
1774 fn test_single_file_basic() {
1775 let mut ast_registry = ASTRegistry::new();
1776 let mut symbol_registry = SymbolRegistry::new();
1777
1778 let config_path = make_path("my_crate::Config");
1780 let config_id = symbol_registry
1781 .register(config_path, SymbolKind::Struct)
1782 .unwrap();
1783 ast_registry.set(config_id, make_struct("Config"));
1784
1785 let helper_path = make_path("my_crate::helper");
1786 let helper_id = symbol_registry
1787 .register(helper_path, SymbolKind::Function)
1788 .unwrap();
1789 ast_registry.set(helper_id, make_fn("helper"));
1790
1791 let generator = RegistryGenerator::single_file();
1793 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
1794
1795 assert_eq!(workspace.crates.len(), 1);
1796 let crate_data = workspace.crates.get("my_crate").unwrap();
1797 assert_eq!(crate_data.files.len(), 1);
1798
1799 let lib = crate_data.files.get("src/lib.rs").unwrap();
1800 assert!(lib.source.contains("struct Config"));
1801 assert!(lib.source.contains("fn helper"));
1802 }
1803
1804 #[test]
1805 fn test_single_file_nested_modules() {
1806 let mut ast_registry = ASTRegistry::new();
1807 let mut symbol_registry = SymbolRegistry::new();
1808
1809 let config_path = make_path("my_crate::Config");
1811 let config_id = symbol_registry
1812 .register(config_path, SymbolKind::Struct)
1813 .unwrap();
1814 ast_registry.set(config_id, make_struct("Config"));
1815
1816 let user_path = make_path("my_crate::models::User");
1818 let user_id = symbol_registry
1819 .register(user_path, SymbolKind::Struct)
1820 .unwrap();
1821 ast_registry.set(user_id, make_struct("User"));
1822
1823 let dto_path = make_path("my_crate::models::dto::UserDto");
1825 let dto_id = symbol_registry
1826 .register(dto_path, SymbolKind::Struct)
1827 .unwrap();
1828 ast_registry.set(dto_id, make_struct("UserDto"));
1829
1830 let generator = RegistryGenerator::single_file();
1832 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
1833
1834 let lib = workspace
1835 .crates
1836 .get("my_crate")
1837 .unwrap()
1838 .files
1839 .get("src/lib.rs")
1840 .unwrap();
1841
1842 assert!(lib.source.contains("struct Config"));
1844 assert!(lib.source.contains("mod models"));
1845 assert!(lib.source.contains("struct User"));
1846 assert!(lib.source.contains("mod dto"));
1847 assert!(lib.source.contains("struct UserDto"));
1848
1849 syn::parse_str::<syn::File>(&lib.source)
1851 .unwrap_or_else(|_| panic!("Should be valid Rust:\n{}", lib.source));
1852 }
1853
1854 #[test]
1855 fn test_multi_file_modern_style() {
1856 let mut ast_registry = ASTRegistry::new();
1857 let mut symbol_registry = SymbolRegistry::new();
1858
1859 let config_path = make_path("my_crate::Config");
1861 let config_id = symbol_registry
1862 .register(config_path, SymbolKind::Struct)
1863 .unwrap();
1864 ast_registry.set(config_id, make_struct("Config"));
1865
1866 let user_path = make_path("my_crate::models::User");
1868 let user_id = symbol_registry
1869 .register(user_path, SymbolKind::Struct)
1870 .unwrap();
1871 ast_registry.set(user_id, make_struct("User"));
1872
1873 let generator = RegistryGenerator::multi_file();
1875 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
1876
1877 let crate_data = workspace.crates.get("my_crate").unwrap();
1878
1879 assert_eq!(crate_data.files.len(), 2);
1881 assert!(crate_data.files.contains_key("src/lib.rs"));
1882 assert!(crate_data.files.contains_key("src/models.rs"));
1883
1884 let lib = crate_data.files.get("src/lib.rs").unwrap();
1886 assert!(lib.source.contains("struct Config"));
1887 assert!(lib.source.contains("mod models;"));
1888
1889 let models = crate_data.files.get("src/models.rs").unwrap();
1891 assert!(models.source.contains("struct User"));
1892 }
1893
1894 #[test]
1895 fn test_multi_crate() {
1896 let mut ast_registry = ASTRegistry::new();
1897 let mut symbol_registry = SymbolRegistry::new();
1898
1899 let foo_path = make_path("crate_a::Foo");
1901 let foo_id = symbol_registry
1902 .register(foo_path, SymbolKind::Struct)
1903 .unwrap();
1904 ast_registry.set(foo_id, make_struct("Foo"));
1905
1906 let bar_path = make_path("crate_b::Bar");
1908 let bar_id = symbol_registry
1909 .register(bar_path, SymbolKind::Struct)
1910 .unwrap();
1911 ast_registry.set(bar_id, make_struct("Bar"));
1912
1913 let generator = RegistryGenerator::single_file();
1915 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
1916
1917 assert_eq!(workspace.crates.len(), 2);
1919 assert!(workspace.crates.contains_key("crate_a"));
1920 assert!(workspace.crates.contains_key("crate_b"));
1921
1922 let crate_a = workspace.crates.get("crate_a").unwrap();
1924 assert!(crate_a
1925 .files
1926 .get("src/lib.rs")
1927 .unwrap()
1928 .source
1929 .contains("struct Foo"));
1930
1931 let crate_b = workspace.crates.get("crate_b").unwrap();
1932 assert!(crate_b
1933 .files
1934 .get("src/lib.rs")
1935 .unwrap()
1936 .source
1937 .contains("struct Bar"));
1938 }
1939
1940 #[test]
1941 fn test_all_generated_valid_rust() {
1942 let mut ast_registry = ASTRegistry::new();
1943 let mut symbol_registry = SymbolRegistry::new();
1944
1945 for (path, name) in [
1947 ("app::Config", "Config"),
1948 ("app::models::User", "User"),
1949 ("app::models::Post", "Post"),
1950 ("app::models::dto::UserDto", "UserDto"),
1951 ("app::utils::helpers::format", "format"),
1952 ] {
1953 let sp = make_path(path);
1954 let kind = if name == "format" {
1955 SymbolKind::Function
1956 } else {
1957 SymbolKind::Struct
1958 };
1959 let id = symbol_registry.register(sp, kind).unwrap();
1960 if name == "format" {
1961 ast_registry.set(id, make_fn(name));
1962 } else {
1963 ast_registry.set(id, make_struct(name));
1964 }
1965 }
1966
1967 for generator in [
1969 RegistryGenerator::single_file(),
1970 RegistryGenerator::multi_file(),
1971 RegistryGenerator::multi_file_mod_rs(),
1972 ] {
1973 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
1974
1975 for (crate_name, path, file) in workspace.iter_files() {
1976 syn::parse_str::<syn::File>(&file.source).unwrap_or_else(|_| {
1977 panic!(
1978 "[{}] {} should be valid Rust:\n{}",
1979 crate_name, path, file.source
1980 )
1981 });
1982 }
1983 }
1984 }
1985
1986 #[test]
1995 fn test_add_mod_no_span_required() {
1996 let mut ast_registry = ASTRegistry::new();
1997 let mut symbol_registry = SymbolRegistry::new();
1998
1999 let config_id = symbol_registry
2001 .register(make_path("app::Config"), SymbolKind::Struct)
2002 .unwrap();
2003 ast_registry.set(config_id, make_struct("Config"));
2004
2005 let generator = RegistryGenerator::multi_file();
2007 let before = generator.generate(&ast_registry, &symbol_registry).unwrap();
2008
2009 assert_eq!(before.crates.get("app").unwrap().files.len(), 1);
2011 assert!(before
2012 .crates
2013 .get("app")
2014 .unwrap()
2015 .files
2016 .contains_key("src/lib.rs"));
2017
2018 let user_id = symbol_registry
2021 .register(make_path("app::models::User"), SymbolKind::Struct)
2022 .unwrap();
2023 ast_registry.set(user_id, make_struct("User"));
2024
2025 let post_id = symbol_registry
2026 .register(make_path("app::models::Post"), SymbolKind::Struct)
2027 .unwrap();
2028 ast_registry.set(post_id, make_struct("Post"));
2029
2030 let after = generator.generate(&ast_registry, &symbol_registry).unwrap();
2032
2033 let app = after.crates.get("app").unwrap();
2035 assert_eq!(app.files.len(), 2);
2036 assert!(app.files.contains_key("src/lib.rs"));
2037 assert!(app.files.contains_key("src/models.rs")); let lib = app.files.get("src/lib.rs").unwrap();
2041 assert!(lib.source.contains("struct Config"));
2042 assert!(lib.source.contains("mod models;"));
2043
2044 let models = app.files.get("src/models.rs").unwrap();
2046 assert!(models.source.contains("struct User"));
2047 assert!(models.source.contains("struct Post"));
2048
2049 syn::parse_str::<syn::File>(&lib.source).unwrap();
2051 syn::parse_str::<syn::File>(&models.source).unwrap();
2052 }
2053
2054 #[test]
2056 fn test_add_nested_mod_no_span_required() {
2057 let mut ast_registry = ASTRegistry::new();
2058 let mut symbol_registry = SymbolRegistry::new();
2059
2060 let config_id = symbol_registry
2062 .register(make_path("app::Config"), SymbolKind::Struct)
2063 .unwrap();
2064 ast_registry.set(config_id, make_struct("Config"));
2065
2066 let handler_id = symbol_registry
2068 .register(
2069 make_path("app::api::v1::handlers::UserHandler"),
2070 SymbolKind::Struct,
2071 )
2072 .unwrap();
2073 ast_registry.set(handler_id, make_struct("UserHandler"));
2074
2075 let generator = RegistryGenerator::multi_file();
2077 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
2078
2079 let app = workspace.crates.get("app").unwrap();
2080
2081 assert_eq!(app.files.len(), 4);
2083 assert!(app.files.contains_key("src/lib.rs"));
2084 assert!(app.files.contains_key("src/api.rs"));
2085 assert!(app.files.contains_key("src/api/v1.rs"));
2086 assert!(app.files.contains_key("src/api/v1/handlers.rs"));
2087
2088 assert!(app
2090 .files
2091 .get("src/lib.rs")
2092 .unwrap()
2093 .source
2094 .contains("mod api;"));
2095 assert!(app
2096 .files
2097 .get("src/api.rs")
2098 .unwrap()
2099 .source
2100 .contains("mod v1;"));
2101 assert!(app
2102 .files
2103 .get("src/api/v1.rs")
2104 .unwrap()
2105 .source
2106 .contains("mod handlers;"));
2107 assert!(app
2108 .files
2109 .get("src/api/v1/handlers.rs")
2110 .unwrap()
2111 .source
2112 .contains("struct UserHandler"));
2113
2114 for file in app.files.values() {
2116 syn::parse_str::<syn::File>(&file.source).unwrap();
2117 }
2118 }
2119
2120 #[test]
2125 fn test_remove_mod_no_span_required() {
2126 let mut ast_registry = ASTRegistry::new();
2127 let mut symbol_registry = SymbolRegistry::new();
2128
2129 let config_id = symbol_registry
2131 .register(make_path("app::Config"), SymbolKind::Struct)
2132 .unwrap();
2133 ast_registry.set(config_id, make_struct("Config"));
2134
2135 let user_id = symbol_registry
2136 .register(make_path("app::models::User"), SymbolKind::Struct)
2137 .unwrap();
2138 ast_registry.set(user_id, make_struct("User"));
2139
2140 let generator = RegistryGenerator::multi_file();
2142 let before = generator.generate(&ast_registry, &symbol_registry).unwrap();
2143
2144 assert_eq!(before.crates.get("app").unwrap().files.len(), 2);
2146
2147 ast_registry.remove(user_id);
2150 symbol_registry.remove(user_id);
2151
2152 let after = generator.generate(&ast_registry, &symbol_registry).unwrap();
2154
2155 let app = after.crates.get("app").unwrap();
2157 assert_eq!(app.files.len(), 1);
2158 assert!(app.files.contains_key("src/lib.rs"));
2159 assert!(!app.files.contains_key("src/models.rs")); let lib = app.files.get("src/lib.rs").unwrap();
2163 assert!(lib.source.contains("struct Config"));
2164 assert!(!lib.source.contains("mod models")); syn::parse_str::<syn::File>(&lib.source).unwrap();
2167 }
2168
2169 #[test]
2171 fn test_partial_remove_mod() {
2172 let mut ast_registry = ASTRegistry::new();
2173 let mut symbol_registry = SymbolRegistry::new();
2174
2175 let config_id = symbol_registry
2177 .register(make_path("app::Config"), SymbolKind::Struct)
2178 .unwrap();
2179 ast_registry.set(config_id, make_struct("Config"));
2180
2181 let user_id = symbol_registry
2182 .register(make_path("app::models::User"), SymbolKind::Struct)
2183 .unwrap();
2184 ast_registry.set(user_id, make_struct("User"));
2185
2186 let post_id = symbol_registry
2187 .register(make_path("app::models::Post"), SymbolKind::Struct)
2188 .unwrap();
2189 ast_registry.set(post_id, make_struct("Post"));
2190
2191 ast_registry.remove(user_id);
2193 symbol_registry.remove(user_id);
2194
2195 let generator = RegistryGenerator::multi_file();
2197 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
2198
2199 let app = workspace.crates.get("app").unwrap();
2200
2201 assert_eq!(app.files.len(), 2);
2203 assert!(app.files.contains_key("src/models.rs"));
2204
2205 let models = app.files.get("src/models.rs").unwrap();
2206 assert!(!models.source.contains("struct User")); assert!(models.source.contains("struct Post")); syn::parse_str::<syn::File>(&models.source).unwrap();
2210 }
2211
2212 #[test]
2218 fn test_main_entry_point() {
2219 let mut ast_registry = ASTRegistry::new();
2220 let mut symbol_registry = SymbolRegistry::new();
2221
2222 let config_id = symbol_registry
2224 .register(make_path("my_crate::Config"), SymbolKind::Struct)
2225 .unwrap();
2226 ast_registry.set(config_id, make_struct("Config"));
2227
2228 let main_id = symbol_registry
2230 .register(make_path("main::my_crate::main"), SymbolKind::Function)
2231 .unwrap();
2232 ast_registry.set(main_id, make_fn("main"));
2233
2234 let generator = RegistryGenerator::multi_file();
2236 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
2237
2238 assert_eq!(workspace.crates.len(), 1);
2240 let crate_data = workspace.crates.get("my_crate").unwrap();
2241
2242 assert!(crate_data.files.contains_key("src/lib.rs"));
2243 assert!(crate_data.files.contains_key("src/main.rs"));
2244
2245 let lib = crate_data.files.get("src/lib.rs").unwrap();
2247 assert!(lib.source.contains("struct Config"));
2248 assert!(!lib.source.contains("fn main"));
2249
2250 let main = crate_data.files.get("src/main.rs").unwrap();
2252 assert!(main.source.contains("fn main"));
2253 assert!(!main.source.contains("struct Config"));
2254
2255 syn::parse_str::<syn::File>(&lib.source).unwrap();
2257 syn::parse_str::<syn::File>(&main.source).unwrap();
2258 }
2259
2260 #[test]
2262 fn test_main_with_modules() {
2263 let mut ast_registry = ASTRegistry::new();
2264 let mut symbol_registry = SymbolRegistry::new();
2265
2266 let main_id = symbol_registry
2268 .register(make_path("main::my_crate::main"), SymbolKind::Function)
2269 .unwrap();
2270 ast_registry.set(main_id, make_fn("main"));
2271
2272 let args_id = symbol_registry
2274 .register(make_path("main::my_crate::cli::Args"), SymbolKind::Struct)
2275 .unwrap();
2276 ast_registry.set(args_id, make_struct("Args"));
2277
2278 let generator = RegistryGenerator::single_file();
2280 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
2281
2282 let crate_data = workspace.crates.get("my_crate").unwrap();
2283 let main = crate_data.files.get("src/main.rs").unwrap();
2284
2285 assert!(main.source.contains("fn main"));
2287 assert!(main.source.contains("mod cli"));
2288 assert!(main.source.contains("struct Args"));
2289
2290 syn::parse_str::<syn::File>(&main.source).unwrap();
2291 }
2292
2293 #[test]
2295 fn test_main_multi_file_modules() {
2296 let mut ast_registry = ASTRegistry::new();
2297 let mut symbol_registry = SymbolRegistry::new();
2298
2299 let main_id = symbol_registry
2301 .register(make_path("main::my_crate::main"), SymbolKind::Function)
2302 .unwrap();
2303 ast_registry.set(main_id, make_fn("main"));
2304
2305 let args_id = symbol_registry
2307 .register(make_path("main::my_crate::cli::Args"), SymbolKind::Struct)
2308 .unwrap();
2309 ast_registry.set(args_id, make_struct("Args"));
2310
2311 let generator = RegistryGenerator::multi_file();
2313 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
2314
2315 let crate_data = workspace.crates.get("my_crate").unwrap();
2316
2317 assert!(crate_data.files.contains_key("src/main.rs"));
2319 assert!(crate_data.files.contains_key("src/cli.rs"));
2320
2321 let main = crate_data.files.get("src/main.rs").unwrap();
2323 assert!(main.source.contains("fn main"));
2324 assert!(main.source.contains("mod cli;"));
2325
2326 let cli = crate_data.files.get("src/cli.rs").unwrap();
2328 assert!(cli.source.contains("struct Args"));
2329
2330 syn::parse_str::<syn::File>(&main.source).unwrap();
2332 syn::parse_str::<syn::File>(&cli.source).unwrap();
2333 }
2334
2335 #[test]
2337 fn test_lib_and_main_separate_modules() {
2338 let mut ast_registry = ASTRegistry::new();
2339 let mut symbol_registry = SymbolRegistry::new();
2340
2341 let lib_id = symbol_registry
2343 .register(
2344 make_path("my_crate::lib_mod::LibStruct"),
2345 SymbolKind::Struct,
2346 )
2347 .unwrap();
2348 ast_registry.set(lib_id, make_struct("LibStruct"));
2349
2350 let bin_id = symbol_registry
2352 .register(
2353 make_path("main::my_crate::bin_mod::BinStruct"),
2354 SymbolKind::Struct,
2355 )
2356 .unwrap();
2357 ast_registry.set(bin_id, make_struct("BinStruct"));
2358
2359 let generator = RegistryGenerator::multi_file();
2361 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
2362
2363 let crate_data = workspace.crates.get("my_crate").unwrap();
2364
2365 assert_eq!(crate_data.files.len(), 4);
2367 assert!(crate_data.files.contains_key("src/lib.rs"));
2368 assert!(crate_data.files.contains_key("src/lib_mod.rs"));
2369 assert!(crate_data.files.contains_key("src/main.rs"));
2370 assert!(crate_data.files.contains_key("src/bin_mod.rs"));
2371
2372 let lib = crate_data.files.get("src/lib.rs").unwrap();
2374 assert!(lib.source.contains("mod lib_mod;"));
2375 assert!(!lib.source.contains("mod bin_mod"));
2376
2377 let main = crate_data.files.get("src/main.rs").unwrap();
2379 assert!(main.source.contains("mod bin_mod;"));
2380 assert!(!main.source.contains("mod lib_mod"));
2381
2382 assert!(crate_data
2384 .files
2385 .get("src/lib_mod.rs")
2386 .unwrap()
2387 .source
2388 .contains("struct LibStruct"));
2389 assert!(crate_data
2390 .files
2391 .get("src/bin_mod.rs")
2392 .unwrap()
2393 .source
2394 .contains("struct BinStruct"));
2395
2396 for file in crate_data.files.values() {
2398 syn::parse_str::<syn::File>(&file.source).unwrap();
2399 }
2400 }
2401
2402 #[test]
2404 fn test_move_symbol_between_modules() {
2405 let mut ast_registry = ASTRegistry::new();
2406 let mut symbol_registry = SymbolRegistry::new();
2407
2408 let config_id = symbol_registry
2410 .register(make_path("app::Config"), SymbolKind::Struct)
2411 .unwrap();
2412 ast_registry.set(config_id, make_struct("Config"));
2413
2414 let user_id = symbol_registry
2415 .register(make_path("app::models::User"), SymbolKind::Struct)
2416 .unwrap();
2417 ast_registry.set(user_id, make_struct("User"));
2418
2419 let generator = RegistryGenerator::multi_file();
2420
2421 let before = generator.generate(&ast_registry, &symbol_registry).unwrap();
2423 assert!(before
2424 .crates
2425 .get("app")
2426 .unwrap()
2427 .files
2428 .contains_key("src/models.rs"));
2429
2430 ast_registry.remove(user_id);
2432 symbol_registry.remove(user_id);
2433
2434 let new_user_id = symbol_registry
2435 .register(make_path("app::entities::User"), SymbolKind::Struct)
2436 .unwrap();
2437 ast_registry.set(new_user_id, make_struct("User"));
2438
2439 let after = generator.generate(&ast_registry, &symbol_registry).unwrap();
2441 let app = after.crates.get("app").unwrap();
2442
2443 assert!(!app.files.contains_key("src/models.rs")); assert!(app.files.contains_key("src/entities.rs")); let entities = app.files.get("src/entities.rs").unwrap();
2447 assert!(entities.source.contains("struct User"));
2448
2449 let lib = app.files.get("src/lib.rs").unwrap();
2451 assert!(!lib.source.contains("mod models"));
2452 assert!(lib.source.contains("mod entities;"));
2453
2454 syn::parse_str::<syn::File>(&lib.source).unwrap();
2455 syn::parse_str::<syn::File>(&entities.source).unwrap();
2456 }
2457
2458 use ryo_analysis::{CodeEdgeV2, CodeGraphV2};
2465
2466 fn setup_registries_with_graph() -> (ASTRegistry, SymbolRegistry, CodeGraphV2) {
2467 let mut ast_registry = ASTRegistry::new();
2468 let mut symbol_registry = SymbolRegistry::new();
2469 let mut code_graph = CodeGraphV2::new();
2470
2471 let crate_id = symbol_registry
2473 .register(make_path("my_crate"), SymbolKind::Mod)
2474 .unwrap();
2475 code_graph.add_node(crate_id);
2476 code_graph.add_crate_root(crate_id);
2477
2478 let config_id = symbol_registry
2480 .register(make_path("my_crate::Config"), SymbolKind::Struct)
2481 .unwrap();
2482 ast_registry.set(config_id, make_struct("Config"));
2483 code_graph.add_node(config_id);
2484 code_graph.add_edge(crate_id, config_id, CodeEdgeV2::Contains);
2485
2486 let models_id = symbol_registry
2488 .register(make_path("my_crate::models"), SymbolKind::Mod)
2489 .unwrap();
2490 code_graph.add_node(models_id);
2491 code_graph.add_edge(crate_id, models_id, CodeEdgeV2::Contains);
2492
2493 let user_id = symbol_registry
2495 .register(make_path("my_crate::models::User"), SymbolKind::Struct)
2496 .unwrap();
2497 ast_registry.set(user_id, make_struct("User"));
2498 code_graph.add_node(user_id);
2499 code_graph.add_edge(models_id, user_id, CodeEdgeV2::Contains);
2500
2501 (ast_registry, symbol_registry, code_graph)
2502 }
2503
2504 #[test]
2505 fn test_generate_with_graph_single_file() {
2506 let (ast_registry, symbol_registry, code_graph) = setup_registries_with_graph();
2507
2508 let generator = RegistryGenerator::single_file();
2509 let workspace = generator
2510 .generate_with_graph(&code_graph, &ast_registry, &symbol_registry)
2511 .unwrap();
2512
2513 assert_eq!(workspace.crates.len(), 1);
2514 let crate_data = workspace.crates.get("my_crate").unwrap();
2515 assert_eq!(crate_data.files.len(), 1);
2516
2517 let lib = crate_data.files.get("src/lib.rs").unwrap();
2518 assert!(lib.source.contains("struct Config"), "Should have Config");
2519 assert!(lib.source.contains("mod models"), "Should have mod models");
2520 assert!(
2521 lib.source.contains("struct User"),
2522 "Should have User in nested mod"
2523 );
2524
2525 syn::parse_str::<syn::File>(&lib.source)
2527 .unwrap_or_else(|_| panic!("Should be valid Rust:\n{}", lib.source));
2528 }
2529
2530 #[test]
2531 fn test_generate_with_graph_multi_file() {
2532 let (ast_registry, symbol_registry, code_graph) = setup_registries_with_graph();
2533
2534 let generator = RegistryGenerator::multi_file();
2535 let workspace = generator
2536 .generate_with_graph(&code_graph, &ast_registry, &symbol_registry)
2537 .unwrap();
2538
2539 assert_eq!(workspace.crates.len(), 1);
2540 let crate_data = workspace.crates.get("my_crate").unwrap();
2541 assert_eq!(
2542 crate_data.files.len(),
2543 2,
2544 "Should have lib.rs and models.rs"
2545 );
2546
2547 let lib = crate_data.files.get("src/lib.rs").unwrap();
2548 assert!(
2549 lib.source.contains("struct Config"),
2550 "lib.rs should have Config"
2551 );
2552 assert!(
2553 lib.source.contains("mod models;"),
2554 "lib.rs should have mod declaration"
2555 );
2556
2557 let models = crate_data.files.get("src/models.rs").unwrap();
2558 assert!(
2559 models.source.contains("struct User"),
2560 "models.rs should have User"
2561 );
2562
2563 syn::parse_str::<syn::File>(&lib.source).unwrap();
2565 syn::parse_str::<syn::File>(&models.source).unwrap();
2566 }
2567
2568 #[test]
2569 fn test_generate_with_graph_matches_original() {
2570 let (ast_registry, symbol_registry, code_graph) = setup_registries_with_graph();
2571
2572 let generator = RegistryGenerator::single_file();
2573
2574 let original = generator.generate(&ast_registry, &symbol_registry).unwrap();
2576 let with_graph = generator
2577 .generate_with_graph(&code_graph, &ast_registry, &symbol_registry)
2578 .unwrap();
2579
2580 assert_eq!(original.crates.len(), with_graph.crates.len());
2582
2583 for crate_name in original.crates.keys() {
2585 assert!(with_graph.crates.contains_key(crate_name));
2586 }
2587 }
2588
2589 use ryo_source::pure::{
2594 PureAttrMeta, PureAttribute, PureField, PureImpl, PureImplItem, PureType, PureUse,
2595 PureUseTree,
2596 };
2597
2598 fn make_inline_test_module() -> PureMod {
2600 PureMod {
2601 attrs: vec![PureAttribute {
2602 path: "cfg".to_string(),
2603 meta: PureAttrMeta::List("test".to_string()),
2604 is_inner: false,
2605 }],
2606 vis: PureVis::Private,
2607 name: "tests".to_string(),
2608 items: vec![
2609 PureItem::Use(PureUse {
2610 vis: PureVis::Private,
2611 tree: PureUseTree::Path {
2612 path: "super".to_string(),
2613 tree: Box::new(PureUseTree::Glob),
2614 },
2615 }),
2616 PureItem::Fn(PureFn {
2617 attrs: vec![PureAttribute {
2618 path: "test".to_string(),
2619 meta: PureAttrMeta::Path,
2620 is_inner: false,
2621 }],
2622 vis: PureVis::Private,
2623 is_async: false,
2624 is_async_inferred: false,
2625 is_const: false,
2626 is_unsafe: false,
2627 abi: None,
2628 name: "test_config".to_string(),
2629 generics: PureGenerics::default(),
2630 params: vec![],
2631 ret: None,
2632 body: PureBlock::default(),
2633 }),
2634 ],
2635 }
2636 }
2637
2638 #[test]
2640 fn test_inline_test_module_idempotent_output() {
2641 let mut ast_registry = ASTRegistry::new();
2642 let mut symbol_registry = SymbolRegistry::new();
2643
2644 symbol_registry
2646 .register(make_path("my_crate"), SymbolKind::Mod)
2647 .unwrap();
2648
2649 let config_id = symbol_registry
2651 .register(make_path("my_crate::Config"), SymbolKind::Struct)
2652 .unwrap();
2653 ast_registry.set(config_id, make_struct("Config"));
2654
2655 let tests_id = symbol_registry
2657 .register(make_path("my_crate::tests"), SymbolKind::Mod)
2658 .unwrap();
2659 ast_registry.set(tests_id, PureItem::Mod(make_inline_test_module()));
2660 ast_registry.mark_inline_module(tests_id);
2661
2662 let generator = RegistryGenerator::single_file();
2664 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
2665
2666 let crate_data = workspace.crates.get("my_crate").unwrap();
2667 let lib = crate_data.files.get("src/lib.rs").unwrap();
2668
2669 syn::parse_str::<syn::File>(&lib.source)
2671 .unwrap_or_else(|_| panic!("Should be valid Rust:\n{}", lib.source));
2672
2673 assert!(
2675 lib.source.contains("struct Config"),
2676 "Output should contain Config struct"
2677 );
2678
2679 assert!(
2681 lib.source.contains("mod tests {"),
2682 "Output should have inline mod tests, got:\n{}",
2683 lib.source
2684 );
2685
2686 assert!(
2688 lib.source.contains("#[cfg(test)]"),
2689 "Output should preserve #[cfg(test)] attribute, got:\n{}",
2690 lib.source
2691 );
2692
2693 assert!(
2695 lib.source.contains("fn test_config"),
2696 "Output should contain test_config function"
2697 );
2698
2699 assert!(
2701 lib.source.contains("#[test]"),
2702 "Output should preserve #[test] attribute, got:\n{}",
2703 lib.source
2704 );
2705
2706 let workspace2 = generator.generate(&ast_registry, &symbol_registry).unwrap();
2708 let lib2 = workspace2
2709 .crates
2710 .get("my_crate")
2711 .unwrap()
2712 .files
2713 .get("src/lib.rs")
2714 .unwrap();
2715
2716 assert_eq!(
2717 lib.source, lib2.source,
2718 "Idempotent: same AST should produce identical output"
2719 );
2720 }
2721
2722 #[test]
2724 fn test_inline_module_with_impl_idempotent_output() {
2725 let mut ast_registry = ASTRegistry::new();
2726 let mut symbol_registry = SymbolRegistry::new();
2727
2728 symbol_registry
2729 .register(make_path("my_crate"), SymbolKind::Mod)
2730 .unwrap();
2731
2732 let config_id = symbol_registry
2733 .register(make_path("my_crate::Config"), SymbolKind::Struct)
2734 .unwrap();
2735 ast_registry.set(config_id, make_struct("Config"));
2736
2737 let tests_mod = PureMod {
2739 attrs: vec![PureAttribute {
2740 path: "cfg".to_string(),
2741 meta: PureAttrMeta::List("test".to_string()),
2742 is_inner: false,
2743 }],
2744 vis: PureVis::Private,
2745 name: "tests".to_string(),
2746 items: vec![
2747 PureItem::Struct(PureStruct {
2748 attrs: vec![],
2749 vis: PureVis::Private,
2750 name: "TestHelper".to_string(),
2751 generics: PureGenerics::default(),
2752 fields: PureFields::Named(vec![PureField {
2753 attrs: vec![],
2754 vis: PureVis::Private,
2755 name: "value".to_string(),
2756 ty: PureType::Path("i32".to_string()),
2757 }]),
2758 }),
2759 PureItem::Impl(PureImpl {
2760 attrs: vec![],
2761 generics: PureGenerics::default(),
2762 is_unsafe: false,
2763 trait_: None,
2764 self_ty: "TestHelper".to_string(),
2765 items: vec![PureImplItem::Fn(PureFn {
2766 attrs: vec![],
2767 vis: PureVis::Private,
2768 is_async: false,
2769 is_async_inferred: false,
2770 is_const: false,
2771 is_unsafe: false,
2772 abi: None,
2773 name: "new".to_string(),
2774 generics: PureGenerics::default(),
2775 params: vec![],
2776 ret: Some(PureType::Path("Self".to_string())),
2777 body: PureBlock::default(),
2778 })],
2779 }),
2780 PureItem::Fn(PureFn {
2781 attrs: vec![PureAttribute {
2782 path: "test".to_string(),
2783 meta: PureAttrMeta::Path,
2784 is_inner: false,
2785 }],
2786 vis: PureVis::Private,
2787 is_async: false,
2788 is_async_inferred: false,
2789 is_const: false,
2790 is_unsafe: false,
2791 abi: None,
2792 name: "test_with_helper".to_string(),
2793 generics: PureGenerics::default(),
2794 params: vec![],
2795 ret: None,
2796 body: PureBlock::default(),
2797 }),
2798 ],
2799 };
2800
2801 let tests_id = symbol_registry
2802 .register(make_path("my_crate::tests"), SymbolKind::Mod)
2803 .unwrap();
2804 ast_registry.set(tests_id, PureItem::Mod(tests_mod));
2805 ast_registry.mark_inline_module(tests_id);
2806
2807 let generator = RegistryGenerator::single_file();
2808 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
2809
2810 let crate_data = workspace.crates.get("my_crate").unwrap();
2811 let lib = crate_data.files.get("src/lib.rs").unwrap();
2812
2813 syn::parse_str::<syn::File>(&lib.source)
2815 .unwrap_or_else(|_| panic!("Should be valid Rust:\n{}", lib.source));
2816
2817 assert!(lib.source.contains("struct Config"));
2819 assert!(lib.source.contains("#[cfg(test)]"));
2820 assert!(lib.source.contains("mod tests {"));
2821 assert!(lib.source.contains("struct TestHelper"));
2822 assert!(lib.source.contains("impl TestHelper"));
2823 assert!(lib.source.contains("fn new"));
2824 assert!(lib.source.contains("fn test_with_helper"));
2825
2826 let workspace2 = generator.generate(&ast_registry, &symbol_registry).unwrap();
2828 let lib2 = workspace2
2829 .crates
2830 .get("my_crate")
2831 .unwrap()
2832 .files
2833 .get("src/lib.rs")
2834 .unwrap();
2835 assert_eq!(lib.source, lib2.source, "Idempotent output");
2836 }
2837
2838 #[test]
2840 fn test_inline_module_multi_file_no_separate_file() {
2841 let mut ast_registry = ASTRegistry::new();
2842 let mut symbol_registry = SymbolRegistry::new();
2843
2844 symbol_registry
2845 .register(make_path("my_crate"), SymbolKind::Mod)
2846 .unwrap();
2847
2848 let config_id = symbol_registry
2849 .register(make_path("my_crate::Config"), SymbolKind::Struct)
2850 .unwrap();
2851 ast_registry.set(config_id, make_struct("Config"));
2852
2853 let utils_id = symbol_registry
2855 .register(make_path("my_crate::utils"), SymbolKind::Mod)
2856 .unwrap();
2857 ast_registry.set(
2859 utils_id,
2860 PureItem::Mod(PureMod {
2861 attrs: vec![],
2862 vis: PureVis::Public,
2863 name: "utils".to_string(),
2864 items: vec![], }),
2866 );
2867
2868 let helper_id = symbol_registry
2869 .register(make_path("my_crate::utils::helper"), SymbolKind::Function)
2870 .unwrap();
2871 ast_registry.set(helper_id, make_fn("helper"));
2872
2873 let tests_id = symbol_registry
2875 .register(make_path("my_crate::tests"), SymbolKind::Mod)
2876 .unwrap();
2877 ast_registry.set(tests_id, PureItem::Mod(make_inline_test_module()));
2878 ast_registry.mark_inline_module(tests_id);
2879
2880 let generator = RegistryGenerator::multi_file();
2882 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
2883
2884 let crate_data = workspace.crates.get("my_crate").unwrap();
2885
2886 assert!(
2888 crate_data.files.contains_key("src/lib.rs"),
2889 "Should have lib.rs"
2890 );
2891 assert!(
2892 crate_data.files.contains_key("src/utils.rs"),
2893 "Should have utils.rs (external module)"
2894 );
2895 assert!(
2896 !crate_data.files.contains_key("src/tests.rs"),
2897 "Should NOT have tests.rs (inline module stays inline)"
2898 );
2899
2900 let lib = crate_data.files.get("src/lib.rs").unwrap();
2901
2902 assert!(
2904 lib.source.contains("mod tests {"),
2905 "tests should be inline, got:\n{}",
2906 lib.source
2907 );
2908
2909 assert!(
2911 lib.source.contains("#[cfg(test)]"),
2912 "Should preserve #[cfg(test)], got:\n{}",
2913 lib.source
2914 );
2915
2916 assert!(
2918 lib.source.contains("mod utils;"),
2919 "Should have mod utils; declaration, got:\n{}",
2920 lib.source
2921 );
2922
2923 for (path, file) in &crate_data.files {
2925 syn::parse_str::<syn::File>(&file.source)
2926 .unwrap_or_else(|_| panic!("[{}] Should be valid Rust:\n{}", path, file.source));
2927 }
2928 }
2929
2930 #[test]
2932 fn test_external_mod_preserves_attrs() {
2933 let mut ast_registry = ASTRegistry::new();
2934 let mut symbol_registry = SymbolRegistry::new();
2935
2936 symbol_registry
2938 .register(make_path("my_crate"), SymbolKind::Mod)
2939 .unwrap();
2940
2941 let tests_mod = PureMod {
2943 attrs: vec![PureAttribute {
2944 path: "cfg".to_string(),
2945 meta: PureAttrMeta::List("test".to_string()),
2946 is_inner: false, }],
2948 vis: PureVis::Private,
2949 name: "tests".to_string(),
2950 items: vec![], };
2952
2953 let tests_id = symbol_registry
2954 .register(make_path("my_crate::tests"), SymbolKind::Mod)
2955 .unwrap();
2956 ast_registry.set(tests_id, PureItem::Mod(tests_mod));
2957 let helper_id = symbol_registry
2961 .register(make_path("my_crate::tests::TestHelper"), SymbolKind::Struct)
2962 .unwrap();
2963 ast_registry.set(helper_id, make_struct("TestHelper"));
2964
2965 let generator = RegistryGenerator::multi_file();
2967 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
2968
2969 let crate_data = workspace.crates.get("my_crate").unwrap();
2970
2971 let lib = crate_data.files.get("src/lib.rs").unwrap();
2973
2974 assert_eq!(
2976 lib.pure_file.attrs.len(),
2977 0,
2978 "lib.rs should have no file-level attrs, got: {:?}",
2979 lib.pure_file.attrs
2980 );
2981
2982 let tests_mod_item = lib
2984 .pure_file
2985 .items
2986 .iter()
2987 .find_map(|item| {
2988 if let PureItem::Mod(m) = item {
2989 if m.name == "tests" {
2990 return Some(m);
2991 }
2992 }
2993 None
2994 })
2995 .expect("Should have mod tests in lib.rs");
2996
2997 assert!(
2999 tests_mod_item.items.is_empty(),
3000 "External mod should have empty items"
3001 );
3002
3003 assert_eq!(
3005 tests_mod_item.attrs.len(),
3006 1,
3007 "mod tests should have exactly 1 attribute, got: {:?}",
3008 tests_mod_item.attrs
3009 );
3010
3011 let attr = &tests_mod_item.attrs[0];
3013 assert_eq!(attr.path, "cfg", "Attr path should be 'cfg'");
3014 assert!(!attr.is_inner, "Should be outer attribute");
3015 assert!(
3016 matches!(&attr.meta, PureAttrMeta::List(s) if s == "test"),
3017 "Attr meta should be List(\"test\"), got: {:?}",
3018 attr.meta
3019 );
3020
3021 assert!(
3023 crate_data.files.contains_key("src/tests.rs"),
3024 "Should have tests.rs for external module"
3025 );
3026
3027 for (path, file) in &crate_data.files {
3029 syn::parse_str::<syn::File>(&file.source)
3030 .unwrap_or_else(|_| panic!("[{}] Should be valid Rust:\n{}", path, file.source));
3031 }
3032 }
3033
3034 #[test]
3037 fn test_inner_attrs_at_file_level() {
3038 let mut ast_registry = ASTRegistry::new();
3039 let mut symbol_registry = SymbolRegistry::new();
3040
3041 let crate_mod = PureMod {
3043 attrs: vec![
3044 PureAttribute {
3045 path: "allow".to_string(),
3046 meta: PureAttrMeta::List("dead_code".to_string()),
3047 is_inner: true, },
3049 PureAttribute {
3050 path: "warn".to_string(),
3051 meta: PureAttrMeta::List("unused_variables".to_string()),
3052 is_inner: true,
3053 },
3054 ],
3055 vis: PureVis::Public,
3056 name: "my_crate".to_string(),
3057 items: vec![],
3058 };
3059
3060 let crate_id = symbol_registry
3061 .register(make_path("my_crate"), SymbolKind::Mod)
3062 .unwrap();
3063 ast_registry.set(crate_id, PureItem::Mod(crate_mod));
3064
3065 let config_id = symbol_registry
3067 .register(make_path("my_crate::Config"), SymbolKind::Struct)
3068 .unwrap();
3069 ast_registry.set(config_id, make_struct("Config"));
3070
3071 let generator = RegistryGenerator::single_file();
3073 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
3074
3075 let crate_data = workspace.crates.get("my_crate").unwrap();
3076 let lib = crate_data.files.get("src/lib.rs").unwrap();
3077
3078 assert_eq!(
3080 lib.pure_file.attrs.len(),
3081 2,
3082 "lib.rs should have exactly 2 file-level attrs, got: {:?}",
3083 lib.pure_file.attrs
3084 );
3085
3086 for attr in &lib.pure_file.attrs {
3088 assert!(attr.is_inner, "File-level attr should be inner: {:?}", attr);
3089 }
3090
3091 let attr0 = &lib.pure_file.attrs[0];
3093 assert_eq!(attr0.path, "allow");
3094 assert!(matches!(&attr0.meta, PureAttrMeta::List(s) if s == "dead_code"));
3095
3096 let attr1 = &lib.pure_file.attrs[1];
3098 assert_eq!(attr1.path, "warn");
3099 assert!(matches!(&attr1.meta, PureAttrMeta::List(s) if s == "unused_variables"));
3100
3101 syn::parse_str::<syn::File>(&lib.source)
3103 .unwrap_or_else(|_| panic!("Should be valid Rust:\n{}", lib.source));
3104 }
3105
3106 #[test]
3110 fn test_mixed_outer_inner_attrs_separation() {
3111 let mut ast_registry = ASTRegistry::new();
3112 let mut symbol_registry = SymbolRegistry::new();
3113
3114 symbol_registry
3116 .register(make_path("my_crate"), SymbolKind::Mod)
3117 .unwrap();
3118
3119 let utils_mod = PureMod {
3121 attrs: vec![
3122 PureAttribute {
3124 path: "doc".to_string(),
3125 meta: PureAttrMeta::NameValue("\"Utils module\"".to_string()),
3126 is_inner: false,
3127 },
3128 PureAttribute {
3130 path: "allow".to_string(),
3131 meta: PureAttrMeta::List("dead_code".to_string()),
3132 is_inner: true,
3133 },
3134 PureAttribute {
3136 path: "cfg".to_string(),
3137 meta: PureAttrMeta::List("feature = \"utils\"".to_string()),
3138 is_inner: false,
3139 },
3140 ],
3141 vis: PureVis::Public,
3142 name: "utils".to_string(),
3143 items: vec![], };
3145
3146 let utils_id = symbol_registry
3147 .register(make_path("my_crate::utils"), SymbolKind::Mod)
3148 .unwrap();
3149 ast_registry.set(utils_id, PureItem::Mod(utils_mod));
3150
3151 let helper_fn_id = symbol_registry
3153 .register(make_path("my_crate::utils::helper"), SymbolKind::Function)
3154 .unwrap();
3155 ast_registry.set(helper_fn_id, make_fn("helper"));
3156
3157 let generator = RegistryGenerator::multi_file();
3159 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
3160
3161 let crate_data = workspace.crates.get("my_crate").unwrap();
3162
3163 let lib = crate_data.files.get("src/lib.rs").unwrap();
3165 let utils_mod_decl = lib
3166 .pure_file
3167 .items
3168 .iter()
3169 .find_map(|item| {
3170 if let PureItem::Mod(m) = item {
3171 if m.name == "utils" {
3172 return Some(m);
3173 }
3174 }
3175 None
3176 })
3177 .expect("Should have mod utils in lib.rs");
3178
3179 assert_eq!(
3181 utils_mod_decl.attrs.len(),
3182 2,
3183 "mod utils declaration should have 2 outer attrs, got: {:?}",
3184 utils_mod_decl.attrs
3185 );
3186
3187 for attr in &utils_mod_decl.attrs {
3189 assert!(
3190 !attr.is_inner,
3191 "mod declaration attrs should be outer: {:?}",
3192 attr
3193 );
3194 }
3195
3196 assert!(
3198 utils_mod_decl.attrs.iter().any(|a| a.path == "doc"),
3199 "Should have doc attr"
3200 );
3201 assert!(
3202 utils_mod_decl.attrs.iter().any(|a| a.path == "cfg"),
3203 "Should have cfg attr"
3204 );
3205
3206 let utils_file = crate_data
3208 .files
3209 .get("src/utils.rs")
3210 .expect("Should have utils.rs");
3211
3212 assert_eq!(
3213 utils_file.pure_file.attrs.len(),
3214 1,
3215 "utils.rs should have 1 file-level attr (inner), got: {:?}",
3216 utils_file.pure_file.attrs
3217 );
3218
3219 let file_attr = &utils_file.pure_file.attrs[0];
3220 assert!(file_attr.is_inner, "File attr should be inner");
3221 assert_eq!(file_attr.path, "allow");
3222 assert!(matches!(&file_attr.meta, PureAttrMeta::List(s) if s == "dead_code"));
3223
3224 for (path, file) in &crate_data.files {
3226 syn::parse_str::<syn::File>(&file.source)
3227 .unwrap_or_else(|_| panic!("[{}] Should be valid Rust:\n{}", path, file.source));
3228 }
3229 }
3230
3231 #[test]
3234 fn test_no_spurious_attrs() {
3235 let mut ast_registry = ASTRegistry::new();
3236 let mut symbol_registry = SymbolRegistry::new();
3237
3238 let crate_mod = PureMod {
3240 attrs: vec![], vis: PureVis::Public,
3242 name: "my_crate".to_string(),
3243 items: vec![],
3244 };
3245
3246 let crate_id = symbol_registry
3247 .register(make_path("my_crate"), SymbolKind::Mod)
3248 .unwrap();
3249 ast_registry.set(crate_id, PureItem::Mod(crate_mod));
3250
3251 let utils_mod = PureMod {
3253 attrs: vec![], vis: PureVis::Public,
3255 name: "utils".to_string(),
3256 items: vec![],
3257 };
3258
3259 let utils_id = symbol_registry
3260 .register(make_path("my_crate::utils"), SymbolKind::Mod)
3261 .unwrap();
3262 ast_registry.set(utils_id, PureItem::Mod(utils_mod));
3263
3264 let config_id = symbol_registry
3266 .register(make_path("my_crate::utils::Config"), SymbolKind::Struct)
3267 .unwrap();
3268 ast_registry.set(config_id, make_struct("Config"));
3269
3270 let generator = RegistryGenerator::multi_file();
3272 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
3273
3274 let crate_data = workspace.crates.get("my_crate").unwrap();
3275
3276 let lib = crate_data.files.get("src/lib.rs").unwrap();
3278 assert_eq!(
3279 lib.pure_file.attrs.len(),
3280 0,
3281 "lib.rs should have no attrs, got: {:?}",
3282 lib.pure_file.attrs
3283 );
3284
3285 let utils_mod_decl = lib
3287 .pure_file
3288 .items
3289 .iter()
3290 .find_map(|item| {
3291 if let PureItem::Mod(m) = item {
3292 if m.name == "utils" {
3293 return Some(m);
3294 }
3295 }
3296 None
3297 })
3298 .expect("Should have mod utils");
3299
3300 assert_eq!(
3301 utils_mod_decl.attrs.len(),
3302 0,
3303 "mod utils declaration should have no attrs, got: {:?}",
3304 utils_mod_decl.attrs
3305 );
3306
3307 let utils_file = crate_data.files.get("src/utils.rs").unwrap();
3309 assert_eq!(
3310 utils_file.pure_file.attrs.len(),
3311 0,
3312 "utils.rs should have no attrs, got: {:?}",
3313 utils_file.pure_file.attrs
3314 );
3315 }
3316
3317 #[test]
3326 fn test_external_module_with_items_not_inlined() {
3327 let mut ast_registry = ASTRegistry::new();
3328 let mut symbol_registry = SymbolRegistry::new();
3329
3330 symbol_registry
3332 .register(make_path("my_crate"), SymbolKind::Mod)
3333 .unwrap();
3334
3335 let config_id = symbol_registry
3337 .register(make_path("my_crate::Config"), SymbolKind::Struct)
3338 .unwrap();
3339 ast_registry.set(config_id, make_struct("Config"));
3340
3341 let filter_mod = PureMod {
3345 attrs: vec![],
3346 vis: PureVis::Public,
3347 name: "filter".to_string(),
3348 items: vec![
3349 PureItem::Enum(PureEnum {
3351 attrs: vec![],
3352 vis: PureVis::Public,
3353 name: "Filter".to_string(),
3354 generics: PureGenerics::default(),
3355 variants: vec![
3356 PureVariant {
3357 attrs: vec![],
3358 name: "Identity".to_string(),
3359 fields: PureFields::Unit,
3360 discriminant: None,
3361 },
3362 PureVariant {
3363 attrs: vec![],
3364 name: "Field".to_string(),
3365 fields: PureFields::Tuple(vec![PureType::Path("String".to_string())]),
3366 discriminant: None,
3367 },
3368 ],
3369 }),
3370 ],
3371 };
3372
3373 let filter_id = symbol_registry
3374 .register(make_path("my_crate::filter"), SymbolKind::Mod)
3375 .unwrap();
3376 ast_registry.set(filter_id, PureItem::Mod(filter_mod));
3377 let filter_enum_id = symbol_registry
3382 .register(make_path("my_crate::filter::Filter"), SymbolKind::Enum)
3383 .unwrap();
3384 ast_registry.set(
3385 filter_enum_id,
3386 PureItem::Enum(PureEnum {
3387 attrs: vec![],
3388 vis: PureVis::Public,
3389 name: "Filter".to_string(),
3390 generics: PureGenerics::default(),
3391 variants: vec![
3392 PureVariant {
3393 attrs: vec![],
3394 name: "Identity".to_string(),
3395 fields: PureFields::Unit,
3396 discriminant: None,
3397 },
3398 PureVariant {
3399 attrs: vec![],
3400 name: "Field".to_string(),
3401 fields: PureFields::Tuple(vec![PureType::Path("String".to_string())]),
3402 discriminant: None,
3403 },
3404 ],
3405 }),
3406 );
3407
3408 let generator = RegistryGenerator::multi_file();
3410 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
3411
3412 let crate_data = workspace.crates.get("my_crate").unwrap();
3413
3414 assert!(
3418 crate_data.files.contains_key("src/filter.rs"),
3419 "External module 'filter' should have its own file (filter.rs). Got files: {:?}",
3420 crate_data.files.keys().collect::<Vec<_>>()
3421 );
3422
3423 let lib = crate_data.files.get("src/lib.rs").unwrap();
3425 assert!(
3426 lib.source.contains("mod filter;"),
3427 "lib.rs should have 'mod filter;' declaration, got:\n{}",
3428 lib.source
3429 );
3430
3431 assert!(
3433 !lib.source.contains("mod filter {"),
3434 "lib.rs should NOT have inline 'mod filter {{...}}', got:\n{}",
3435 lib.source
3436 );
3437
3438 assert!(
3440 !lib.source.contains("enum Filter"),
3441 "lib.rs should NOT contain Filter enum (should be in filter.rs), got:\n{}",
3442 lib.source
3443 );
3444
3445 let filter_file = crate_data.files.get("src/filter.rs").unwrap();
3447 assert!(
3448 filter_file.source.contains("pub enum Filter"),
3449 "filter.rs should contain 'pub enum Filter', got:\n{}",
3450 filter_file.source
3451 );
3452
3453 for (path, file) in &crate_data.files {
3455 syn::parse_str::<syn::File>(&file.source)
3456 .unwrap_or_else(|_| panic!("[{}] Should be valid Rust:\n{}", path, file.source));
3457 }
3458 }
3459
3460 #[test]
3465 fn test_marked_inline_module_stays_inline() {
3466 use ryo_source::pure::{PureBlock, PureFn};
3467
3468 let mut ast_registry = ASTRegistry::new();
3469 let mut symbol_registry = SymbolRegistry::new();
3470
3471 symbol_registry
3473 .register(make_path("my_crate"), SymbolKind::Mod)
3474 .unwrap();
3475
3476 let config_id = symbol_registry
3478 .register(make_path("my_crate::Config"), SymbolKind::Struct)
3479 .unwrap();
3480 ast_registry.set(config_id, make_struct("Config"));
3481
3482 let tests_mod = PureMod {
3484 attrs: vec![PureAttribute {
3485 path: "cfg".to_string(),
3486 meta: PureAttrMeta::List("test".to_string()),
3487 is_inner: false,
3488 }],
3489 vis: PureVis::Private,
3490 name: "tests".to_string(),
3491 items: vec![PureItem::Fn(PureFn {
3492 attrs: vec![PureAttribute {
3493 path: "test".to_string(),
3494 meta: PureAttrMeta::Path,
3495 is_inner: false,
3496 }],
3497 vis: PureVis::Private,
3498 is_async: false,
3499 is_async_inferred: false,
3500 is_const: false,
3501 is_unsafe: false,
3502 abi: None,
3503 name: "test_something".to_string(),
3504 generics: PureGenerics::default(),
3505 params: vec![],
3506 ret: None,
3507 body: PureBlock::default(),
3508 })],
3509 };
3510
3511 let tests_id = symbol_registry
3512 .register(make_path("my_crate::tests"), SymbolKind::Mod)
3513 .unwrap();
3514 ast_registry.set(tests_id, PureItem::Mod(tests_mod));
3515 ast_registry.mark_inline_module(tests_id);
3517
3518 let generator = RegistryGenerator::multi_file();
3520 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
3521
3522 let crate_data = workspace.crates.get("my_crate").unwrap();
3523
3524 assert!(
3528 !crate_data.files.contains_key("src/tests.rs"),
3529 "Inline module 'tests' should NOT have its own file. Got files: {:?}",
3530 crate_data.files.keys().collect::<Vec<_>>()
3531 );
3532
3533 let lib = crate_data.files.get("src/lib.rs").unwrap();
3535 assert!(
3536 lib.source.contains("mod tests {"),
3537 "lib.rs should have inline 'mod tests {{...}}', got:\n{}",
3538 lib.source
3539 );
3540
3541 assert!(
3543 lib.source.contains("fn test_something"),
3544 "lib.rs should contain test_something function, got:\n{}",
3545 lib.source
3546 );
3547
3548 assert!(
3550 lib.source.contains("#[cfg(test)]"),
3551 "lib.rs should have #[cfg(test)] attribute, got:\n{}",
3552 lib.source
3553 );
3554
3555 for (path, file) in &crate_data.files {
3557 syn::parse_str::<syn::File>(&file.source)
3558 .unwrap_or_else(|_| panic!("[{}] Should be valid Rust:\n{}", path, file.source));
3559 }
3560 }
3561
3562 #[test]
3570 fn test_external_module_visibility_preserved() {
3571 let mut ast_registry = ASTRegistry::new();
3572 let mut symbol_registry = SymbolRegistry::new();
3573
3574 let crate_id = symbol_registry
3576 .register(make_path("my_crate"), SymbolKind::Mod)
3577 .unwrap();
3578 ast_registry.set(
3579 crate_id,
3580 PureItem::Mod(PureMod {
3581 attrs: vec![],
3582 vis: PureVis::Public,
3583 name: "my_crate".to_string(),
3584 items: vec![],
3585 }),
3586 );
3587
3588 let error_mod = PureMod {
3591 attrs: vec![],
3592 vis: PureVis::Private, name: "error".to_string(),
3594 items: vec![
3595 PureItem::Enum(PureEnum {
3597 attrs: vec![],
3598 vis: PureVis::Public, name: "Error".to_string(),
3600 generics: PureGenerics::default(),
3601 variants: vec![PureVariant {
3602 attrs: vec![],
3603 name: "IoError".to_string(),
3604 fields: PureFields::Unit,
3605 discriminant: None,
3606 }],
3607 }),
3608 ],
3609 };
3610
3611 let error_mod_id = symbol_registry
3612 .register(make_path("my_crate::error"), SymbolKind::Mod)
3613 .unwrap();
3614 ast_registry.set(error_mod_id, PureItem::Mod(error_mod));
3615 let _ = symbol_registry.set_visibility(error_mod_id, ryo_symbol::Visibility::Private);
3617 let error_enum_id = symbol_registry
3621 .register(make_path("my_crate::error::Error"), SymbolKind::Enum)
3622 .unwrap();
3623 ast_registry.set(
3624 error_enum_id,
3625 PureItem::Enum(PureEnum {
3626 attrs: vec![],
3627 vis: PureVis::Public,
3628 name: "Error".to_string(),
3629 generics: PureGenerics::default(),
3630 variants: vec![PureVariant {
3631 attrs: vec![],
3632 name: "IoError".to_string(),
3633 fields: PureFields::Unit,
3634 discriminant: None,
3635 }],
3636 }),
3637 );
3638
3639 let generator = RegistryGenerator::multi_file();
3641 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
3642
3643 let crate_data = workspace.crates.get("my_crate").unwrap();
3644
3645 let lib = crate_data.files.get("src/lib.rs").unwrap();
3647
3648 let error_mod_decl = lib
3650 .pure_file
3651 .items
3652 .iter()
3653 .find_map(|item| {
3654 if let PureItem::Mod(m) = item {
3655 if m.name == "error" {
3656 return Some(m);
3657 }
3658 }
3659 None
3660 })
3661 .expect("Should have mod error in lib.rs");
3662
3663 assert!(
3665 matches!(error_mod_decl.vis, PureVis::Private),
3666 "mod error should be PRIVATE (original visibility), but got: {:?}\nlib.rs:\n{}",
3667 error_mod_decl.vis,
3668 lib.source
3669 );
3670
3671 assert!(
3673 lib.source.contains("mod error;") && !lib.source.contains("pub mod error;"),
3674 "lib.rs should have 'mod error;' (private), not 'pub mod error;'\nGot:\n{}",
3675 lib.source
3676 );
3677 }
3678
3679 #[test]
3686 fn test_external_module_visibility_from_pure_mod() {
3687 let mut ast_registry = ASTRegistry::new();
3688 let mut symbol_registry = SymbolRegistry::new();
3689
3690 let crate_id = symbol_registry
3692 .register(make_path("my_crate"), SymbolKind::Mod)
3693 .unwrap();
3694 ast_registry.set(
3695 crate_id,
3696 PureItem::Mod(PureMod {
3697 attrs: vec![],
3698 vis: PureVis::Public,
3699 name: "my_crate".to_string(),
3700 items: vec![],
3701 }),
3702 );
3703
3704 let error_mod = PureMod {
3707 attrs: vec![],
3708 vis: PureVis::Private, name: "error".to_string(),
3710 items: vec![
3711 PureItem::Enum(PureEnum {
3713 attrs: vec![],
3714 vis: PureVis::Public, name: "Error".to_string(),
3716 generics: PureGenerics::default(),
3717 variants: vec![PureVariant {
3718 attrs: vec![],
3719 name: "IoError".to_string(),
3720 fields: PureFields::Unit,
3721 discriminant: None,
3722 }],
3723 }),
3724 ],
3725 };
3726
3727 let error_mod_id = symbol_registry
3728 .register(make_path("my_crate::error"), SymbolKind::Mod)
3729 .unwrap();
3730 ast_registry.set(error_mod_id, PureItem::Mod(error_mod));
3731 let error_enum_id = symbol_registry
3736 .register(make_path("my_crate::error::Error"), SymbolKind::Enum)
3737 .unwrap();
3738 ast_registry.set(
3739 error_enum_id,
3740 PureItem::Enum(PureEnum {
3741 attrs: vec![],
3742 vis: PureVis::Public,
3743 name: "Error".to_string(),
3744 generics: PureGenerics::default(),
3745 variants: vec![PureVariant {
3746 attrs: vec![],
3747 name: "IoError".to_string(),
3748 fields: PureFields::Unit,
3749 discriminant: None,
3750 }],
3751 }),
3752 );
3753
3754 let generator = RegistryGenerator::multi_file();
3756 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
3757
3758 let crate_data = workspace.crates.get("my_crate").unwrap();
3759
3760 let lib = crate_data.files.get("src/lib.rs").unwrap();
3762
3763 let error_mod_decl = lib
3765 .pure_file
3766 .items
3767 .iter()
3768 .find_map(|item| {
3769 if let PureItem::Mod(m) = item {
3770 if m.name == "error" {
3771 return Some(m);
3772 }
3773 }
3774 None
3775 })
3776 .expect("Should have mod error in lib.rs");
3777
3778 assert!(
3780 matches!(error_mod_decl.vis, PureVis::Private),
3781 "mod error should be PRIVATE (from PureMod.vis), but got: {:?}\n\
3782 This indicates visibility is being inferred from child items instead of PureMod.vis\n\
3783 lib.rs:\n{}",
3784 error_mod_decl.vis,
3785 lib.source
3786 );
3787 }
3788
3789 #[test]
3796 fn test_external_module_visibility_with_graph() {
3797 use ryo_analysis::{CodeEdgeV2, CodeGraphV2};
3798
3799 let mut ast_registry = ASTRegistry::new();
3800 let mut symbol_registry = SymbolRegistry::new();
3801 let mut code_graph = CodeGraphV2::new();
3802
3803 let crate_id = symbol_registry
3805 .register(make_path("my_crate"), SymbolKind::Mod)
3806 .unwrap();
3807 ast_registry.set(
3808 crate_id,
3809 PureItem::Mod(PureMod {
3810 attrs: vec![],
3811 vis: PureVis::Public,
3812 name: "my_crate".to_string(),
3813 items: vec![],
3814 }),
3815 );
3816 code_graph.add_node(crate_id);
3817 code_graph.add_crate_root(crate_id);
3818
3819 let error_mod = PureMod {
3821 attrs: vec![],
3822 vis: PureVis::Private, name: "error".to_string(),
3824 items: vec![], };
3826
3827 let error_mod_id = symbol_registry
3828 .register(make_path("my_crate::error"), SymbolKind::Mod)
3829 .unwrap();
3830 ast_registry.set(error_mod_id, PureItem::Mod(error_mod));
3831 code_graph.add_node(error_mod_id);
3832 code_graph.add_edge(crate_id, error_mod_id, CodeEdgeV2::Contains);
3833
3834 let error_enum_id = symbol_registry
3836 .register(make_path("my_crate::error::Error"), SymbolKind::Enum)
3837 .unwrap();
3838 ast_registry.set(
3839 error_enum_id,
3840 PureItem::Enum(PureEnum {
3841 attrs: vec![],
3842 vis: PureVis::Public, name: "Error".to_string(),
3844 generics: PureGenerics::default(),
3845 variants: vec![PureVariant {
3846 attrs: vec![],
3847 name: "IoError".to_string(),
3848 fields: PureFields::Unit,
3849 discriminant: None,
3850 }],
3851 }),
3852 );
3853 code_graph.add_node(error_enum_id);
3854 code_graph.add_edge(error_mod_id, error_enum_id, CodeEdgeV2::Contains);
3855
3856 let generator = RegistryGenerator::multi_file();
3858 let workspace = generator
3859 .generate_with_graph(&code_graph, &ast_registry, &symbol_registry)
3860 .unwrap();
3861
3862 let crate_data = workspace.crates.get("my_crate").unwrap();
3863 let lib = crate_data.files.get("src/lib.rs").unwrap();
3864
3865 let error_mod_decl = lib
3867 .pure_file
3868 .items
3869 .iter()
3870 .find_map(|item| {
3871 if let PureItem::Mod(m) = item {
3872 if m.name == "error" {
3873 return Some(m);
3874 }
3875 }
3876 None
3877 })
3878 .expect("Should have mod error in lib.rs");
3879
3880 assert!(
3882 matches!(error_mod_decl.vis, PureVis::Private),
3883 "mod error should be PRIVATE (from PureMod.vis), but got: {:?}\n\
3884 Bug: generate_file_from_graph is inferring visibility from children instead of PureMod.vis\n\
3885 lib.rs:\n{}",
3886 error_mod_decl.vis,
3887 lib.source
3888 );
3889 }
3890
3891 #[test]
3896 fn test_symbol_path_to_file_key_root_items() {
3897 let gen = RegistryGenerator::multi_file();
3898
3899 let path = SymbolPath::parse("my_crate::Config").unwrap();
3901 assert_eq!(gen.symbol_path_to_file_key(&path), "src/lib.rs");
3902
3903 let path = SymbolPath::parse("my_crate").unwrap();
3905 assert_eq!(gen.symbol_path_to_file_key(&path), "src/lib.rs");
3906 }
3907
3908 #[test]
3909 fn test_symbol_path_to_file_key_module_items() {
3910 let gen = RegistryGenerator::multi_file();
3911
3912 let path = SymbolPath::parse("my_crate::models::User").unwrap();
3914 assert_eq!(gen.symbol_path_to_file_key(&path), "src/models.rs");
3915 }
3916
3917 #[test]
3918 fn test_symbol_path_to_file_key_nested_modules() {
3919 let gen = RegistryGenerator::multi_file();
3920
3921 let path = SymbolPath::parse("my_crate::models::sub::Foo").unwrap();
3923 assert_eq!(gen.symbol_path_to_file_key(&path), "src/models/sub.rs");
3924 }
3925
3926 #[test]
3927 fn test_symbol_path_to_file_key_main_symbol() {
3928 let gen = RegistryGenerator::multi_file();
3929
3930 let path = SymbolPath::parse("main::my_app::Config").unwrap();
3932 assert_eq!(gen.symbol_path_to_file_key(&path), "src/main.rs");
3933
3934 let path = SymbolPath::parse("main::my_app::cli::Args").unwrap();
3936 assert_eq!(gen.symbol_path_to_file_key(&path), "src/cli.rs");
3937 }
3938
3939 #[test]
3940 fn test_generate_affected_empty_symbols_returns_empty() {
3941 let ast_registry = ASTRegistry::new();
3942 let symbol_registry = SymbolRegistry::new();
3943 let gen = RegistryGenerator::multi_file();
3944
3945 let workspace = gen
3947 .generate_internal(&ast_registry, &symbol_registry, Some(&HashSet::new()))
3948 .unwrap();
3949 assert_eq!(workspace.total_files(), 0);
3950 }
3951
3952 #[test]
3953 fn test_generate_affected_only_affected_files() {
3954 let mut ast_registry = ASTRegistry::new();
3956 let mut symbol_registry = SymbolRegistry::new();
3957
3958 let root_id = symbol_registry
3960 .register(SymbolPath::parse("test_crate").unwrap(), SymbolKind::Mod)
3961 .unwrap();
3962
3963 let models_mod_id = symbol_registry
3965 .register(
3966 SymbolPath::parse("test_crate::models").unwrap(),
3967 SymbolKind::Mod,
3968 )
3969 .unwrap();
3970 let user_id = symbol_registry
3971 .register(
3972 SymbolPath::parse("test_crate::models::User").unwrap(),
3973 SymbolKind::Struct,
3974 )
3975 .unwrap();
3976
3977 let handlers_mod_id = symbol_registry
3979 .register(
3980 SymbolPath::parse("test_crate::handlers").unwrap(),
3981 SymbolKind::Mod,
3982 )
3983 .unwrap();
3984 let process_id = symbol_registry
3985 .register(
3986 SymbolPath::parse("test_crate::handlers::process").unwrap(),
3987 SymbolKind::Function,
3988 )
3989 .unwrap();
3990
3991 ast_registry.set(root_id, make_mod("test_crate"));
3993 ast_registry.set(models_mod_id, make_mod("models"));
3994 ast_registry.set(user_id, make_struct("User"));
3995 ast_registry.set(handlers_mod_id, make_mod("handlers"));
3996 ast_registry.set(process_id, make_fn("process"));
3997
3998 let gen = RegistryGenerator::multi_file();
3999
4000 let full = gen.generate(&ast_registry, &symbol_registry).unwrap();
4002 let full_crate = full.crates.get("test_crate").unwrap();
4003 assert!(
4004 full_crate.files.len() >= 3,
4005 "Full gen should have lib.rs, models.rs, handlers.rs but got: {:?}",
4006 full_crate.files.keys().collect::<Vec<_>>()
4007 );
4008
4009 let workspace = gen
4011 .generate_internal(
4012 &ast_registry,
4013 &symbol_registry,
4014 Some(&HashSet::from(["src/models.rs".to_string()])),
4015 )
4016 .unwrap();
4017 let affected_crate = workspace.crates.get("test_crate").unwrap();
4018 assert_eq!(
4019 affected_crate.files.len(),
4020 1,
4021 "Affected gen should only have models.rs but got: {:?}",
4022 affected_crate.files.keys().collect::<Vec<_>>()
4023 );
4024 assert!(affected_crate.files.contains_key("src/models.rs"));
4025 assert!(!affected_crate.files.contains_key("src/lib.rs"));
4026 assert!(!affected_crate.files.contains_key("src/handlers.rs"));
4027 }
4028
4029 fn make_mod(name: &str) -> PureItem {
4030 PureItem::Mod(PureMod {
4031 attrs: vec![],
4032 vis: PureVis::Public,
4033 name: name.to_string(),
4034 items: vec![],
4035 })
4036 }
4037}