1use ryo_analysis::{ASTRegistry, CodeGraphV2, SymbolId, SymbolKind};
54use ryo_source::pure::ModScope;
55use ryo_source::pure::{PureAttribute, PureFile, PureItem, PureMod, PureVis, ToSynError};
56use ryo_symbol::{CrateName, SymbolPath, SymbolRegistry};
57use std::collections::{BTreeMap, HashMap, HashSet};
58
59type ModSymbolKey = (String, Vec<String>, bool);
61
62type ModSymbolValue = (bool, Vec<PureAttribute>);
64
65type CrateKey = (String, bool);
67
68type SymbolEntry = (SymbolId, SymbolPath, PureItem);
70
71struct MultiFileOpts<'a> {
73 is_main: bool,
75 module_use_items: &'a HashMap<ModSymbolKey, Vec<PureItem>>,
77 mod_symbols: &'a HashMap<ModSymbolKey, ModSymbolValue>,
79 inline_module_paths: &'a HashSet<String>,
81 affected_file_keys: Option<&'a HashSet<String>>,
83}
84
85#[derive(Debug, Clone)]
90pub struct GeneratedCrate {
91 pub crate_name: String,
93
94 pub files: HashMap<String, GeneratedFile>,
106}
107
108#[derive(Debug, Clone)]
110pub struct GeneratedFile {
111 pub source: String,
113 pub pure_file: PureFile,
115}
116
117#[derive(Debug, Clone, Default)]
143pub struct GeneratedWorkspace {
144 pub crates: HashMap<String, GeneratedCrate>,
146}
147
148impl GeneratedWorkspace {
149 pub fn total_files(&self) -> usize {
151 self.crates.values().map(|c| c.files.len()).sum()
152 }
153
154 pub fn iter_files(&self) -> impl Iterator<Item = (&str, &str, &GeneratedFile)> {
156 self.crates.iter().flat_map(|(crate_name, crate_data)| {
157 crate_data
158 .files
159 .iter()
160 .map(move |(path, file)| (crate_name.as_str(), path.as_str(), file))
161 })
162 }
163}
164
165#[derive(Debug, Clone, Default)]
195pub struct RegistryGenerator {
196 pub single_file: bool,
198 pub use_mod_rs: bool,
200 existing_file_keys_per_crate: HashMap<CrateName, HashSet<String>>,
210 existing_file_keys_workspace_wide: HashSet<String>,
214 lib_crates: HashSet<CrateName>,
227}
228
229impl RegistryGenerator {
230 pub fn single_file() -> Self {
232 Self {
233 single_file: true,
234 use_mod_rs: false,
235 existing_file_keys_per_crate: HashMap::new(),
236 existing_file_keys_workspace_wide: HashSet::new(),
237 lib_crates: HashSet::new(),
238 }
239 }
240
241 pub fn multi_file() -> Self {
243 Self {
244 single_file: false,
245 use_mod_rs: false,
246 existing_file_keys_per_crate: HashMap::new(),
247 existing_file_keys_workspace_wide: HashSet::new(),
248 lib_crates: HashSet::new(),
249 }
250 }
251
252 pub fn multi_file_mod_rs() -> Self {
254 Self {
255 single_file: false,
256 use_mod_rs: true,
257 existing_file_keys_per_crate: HashMap::new(),
258 existing_file_keys_workspace_wide: HashSet::new(),
259 lib_crates: HashSet::new(),
260 }
261 }
262
263 pub fn with_existing_files<I>(mut self, paths: I) -> Self
272 where
273 I: IntoIterator,
274 I::Item: Into<String>,
275 {
276 self.existing_file_keys_workspace_wide = paths.into_iter().map(Into::into).collect();
277 self
278 }
279
280 pub fn with_lib_crates(mut self, crates: HashSet<CrateName>) -> Self {
294 self.lib_crates = crates;
295 self
296 }
297
298 pub fn with_existing_files_per_crate(
307 mut self,
308 map: HashMap<CrateName, HashSet<String>>,
309 ) -> Self {
310 self.existing_file_keys_per_crate = map;
311 self
312 }
313
314 fn existing_keys_for_crate(&self, crate_name: Option<&CrateName>) -> &HashSet<String> {
319 if let Some(name) = crate_name {
320 if let Some(set) = self.existing_file_keys_per_crate.get(name) {
321 return set;
322 }
323 }
324 &self.existing_file_keys_workspace_wide
325 }
326
327 pub fn generate(
332 &self,
333 ast_registry: &ASTRegistry,
334 symbol_registry: &SymbolRegistry,
335 ) -> Result<GeneratedWorkspace, ToSynError> {
336 self.generate_internal(ast_registry, symbol_registry, None, None)
337 }
338
339 fn generate_internal(
347 &self,
348 ast_registry: &ASTRegistry,
349 symbol_registry: &SymbolRegistry,
350 affected_file_keys: Option<&HashSet<String>>,
351 affected_crates: Option<&HashSet<CrateName>>,
352 ) -> Result<GeneratedWorkspace, ToSynError> {
353 let mut inline_module_paths: HashSet<String> = HashSet::new();
358 for id in ast_registry.inline_module_ids() {
359 if let Some(path) = symbol_registry.path(id) {
360 inline_module_paths.insert(path.to_string());
361 }
362 }
363
364 let mut crates: HashMap<CrateKey, Vec<SymbolEntry>> = HashMap::new();
367
368 for (id, item) in ast_registry.iter() {
369 let kind = symbol_registry.kind(id);
375 if matches!(
376 kind,
377 Some(SymbolKind::Impl | SymbolKind::Method | SymbolKind::Field)
378 ) {
379 continue;
380 }
381
382 if let Some(path) = symbol_registry.path(id) {
383 if path.segments().any(|s| s.starts_with("<impl ")) {
386 continue;
387 }
388
389 let path_str = path.to_string();
393 let is_child_of_inline = inline_module_paths.iter().any(|inline_path| {
394 let prefix_with_sep = format!("{}::", inline_path);
398 path_str.starts_with(&prefix_with_sep)
399 });
400 if is_child_of_inline {
401 continue;
402 }
403
404 let (crate_name, is_main) = if path.is_main_symbol() {
405 (
407 path.main_target_crate()
408 .unwrap_or(path.crate_name())
409 .to_string(),
410 true,
411 )
412 } else {
413 (path.crate_name().to_string(), false)
415 };
416
417 crates.entry((crate_name, is_main)).or_default().push((
418 id,
419 path.clone(),
420 item.clone(),
421 ));
422 }
423 }
424
425 let mut module_use_items: HashMap<ModSymbolKey, Vec<PureItem>> = HashMap::new();
428
429 for (module_id, items) in ast_registry.iter_module_items() {
430 tracing::debug!(
431 "iter_module_items: module_id={:?}, items_count={}, has_use={}",
432 module_id,
433 items.len(),
434 items.iter().any(|i| matches!(i, PureItem::Use(_)))
435 );
436 if let Some(path) = symbol_registry.path(module_id) {
437 let (crate_name, is_main) = if path.is_main_symbol() {
438 (
439 path.main_target_crate()
440 .unwrap_or(path.crate_name())
441 .to_string(),
442 true,
443 )
444 } else {
445 (path.crate_name().to_string(), false)
446 };
447
448 let segments: Vec<String> = path
453 .mod_path()
454 .iter()
455 .map(|s| s.name())
456 .filter(|name| !name.starts_with("<impl "))
457 .map(|s| s.to_string())
458 .collect();
459
460 let file_level_items: Vec<PureItem> = items
464 .iter()
465 .filter(|item| matches!(item, PureItem::Use(_) | PureItem::Impl(_)))
466 .cloned()
467 .collect();
468
469 if !file_level_items.is_empty() {
470 tracing::debug!(
471 "Adding {} file_level_items to module_use_items for path={}, segments={:?}",
472 file_level_items.len(),
473 path,
474 segments
475 );
476 module_use_items
477 .entry((crate_name, segments, is_main))
478 .or_default()
479 .extend(file_level_items);
480 }
481 }
482 }
483
484 for (crate_name, segments, is_main) in module_use_items.keys() {
487 if segments.is_empty() {
488 crates.entry((crate_name.clone(), *is_main)).or_default();
490 }
491 }
492
493 let mut mod_symbols: HashMap<ModSymbolKey, ModSymbolValue> = HashMap::new();
496 for (id, path) in symbol_registry.iter() {
497 let Some(kind) = symbol_registry.kind(id) else {
498 continue;
499 };
500 if kind != SymbolKind::Mod {
501 continue;
502 }
503 if ast_registry.is_inline_module(id) {
507 continue;
508 }
509 let mod_attrs = if let Some(PureItem::Mod(m)) = ast_registry.get(id) {
510 m.attrs.clone()
511 } else {
512 Vec::new()
513 };
514
515 let segments: Vec<&str> = path.segments().collect();
516 let (crate_name, is_main) = if path.is_main_symbol() {
517 (
518 path.main_target_crate()
519 .unwrap_or(path.crate_name())
520 .to_string(),
521 true,
522 )
523 } else {
524 (path.crate_name().to_string(), false)
525 };
526
527 if segments.len() <= 1 || (path.is_main_symbol() && segments.len() <= 2) {
530 mod_symbols.insert(
532 (crate_name.clone(), vec![], is_main),
533 (true, mod_attrs), );
535 crates.entry((crate_name, is_main)).or_default();
536 continue;
537 }
538
539 let skip_count = if is_main { 2 } else { 1 };
542 let module_segments: Vec<String> = segments[skip_count..]
543 .iter()
544 .filter(|s| !s.starts_with("<impl "))
545 .map(|s| s.to_string())
546 .collect();
547
548 let is_pub = symbol_registry
550 .visibility(id)
551 .map(|v| matches!(v, ryo_symbol::Visibility::Public))
552 .unwrap_or(false);
553
554 mod_symbols.insert(
555 (crate_name.clone(), module_segments, is_main),
556 (is_pub, mod_attrs),
557 );
558
559 crates.entry((crate_name, is_main)).or_default();
561 }
562
563 let mut workspace = GeneratedWorkspace::default();
565
566 for ((crate_name, is_main), symbols) in crates {
567 if !is_main && !self.lib_crates.is_empty() {
578 if let Ok(name) = CrateName::new(crate_name.as_str()) {
579 if !self.lib_crates.contains(&name) {
580 continue;
581 }
582 }
583 }
584
585 if let Some(crates_filter) = affected_crates {
594 if let Ok(name) = CrateName::new(crate_name.as_str()) {
595 if !crates_filter.contains(&name) {
596 continue;
597 }
598 }
599 }
600
601 let entry = workspace
602 .crates
603 .entry(crate_name.clone())
604 .or_insert_with(|| GeneratedCrate {
605 crate_name: crate_name.clone(),
606 files: HashMap::new(),
607 });
608
609 let generated_files = if self.single_file {
610 self.generate_single_file_inner(
611 &crate_name,
612 symbols,
613 is_main,
614 &module_use_items,
615 &mod_symbols,
616 )
617 } else {
618 self.generate_multi_file_inner(
619 &crate_name,
620 symbols,
621 MultiFileOpts {
622 is_main,
623 module_use_items: &module_use_items,
624 mod_symbols: &mod_symbols,
625 inline_module_paths: &inline_module_paths,
626 affected_file_keys,
627 },
628 )
629 };
630
631 entry.files.extend(generated_files?);
632 }
633
634 Ok(workspace)
635 }
636
637 pub fn generate_affected(
689 &self,
690 ast_registry: &ASTRegistry,
691 symbol_registry: &SymbolRegistry,
692 modified_symbols: &[SymbolId],
693 _metadata: &ryo_symbol::CargoMetadataProvider,
694 ) -> Result<GeneratedWorkspace, ToSynError> {
695 if modified_symbols.is_empty() {
696 return Ok(GeneratedWorkspace::default());
697 }
698
699 let mut affected_file_keys: HashSet<String> = modified_symbols
713 .iter()
714 .filter_map(|&id| {
715 let path = symbol_registry.path(id)?;
716 let resolved =
717 Self::resolve_to_file_level_symbol(path, symbol_registry, ast_registry);
718 Some(self.symbol_path_to_file_key(&resolved))
719 })
720 .collect();
721
722 for &id in modified_symbols {
728 if symbol_registry.kind(id) != Some(SymbolKind::Mod) {
729 continue;
730 }
731 if ast_registry.is_inline_module(id) {
732 continue;
733 }
734 if let Some(path) = symbol_registry.path(id) {
735 let parent_key = self.symbol_path_to_file_key(path);
737 let crate_name_opt = CrateName::new(path.crate_name()).ok();
738 if let Some(mod_name) = path.segments().last() {
739 let mod_file_key =
740 self.derive_child_path(&parent_key, mod_name, crate_name_opt.as_ref());
741 affected_file_keys.insert(mod_file_key);
742 }
743 }
744 }
745
746 tracing::debug!(
747 "generate_affected: {} modified symbols → {} affected file keys: {:?}",
748 modified_symbols.len(),
749 affected_file_keys.len(),
750 affected_file_keys,
751 );
752
753 let affected_crates: HashSet<CrateName> = modified_symbols
762 .iter()
763 .filter_map(|&id| {
764 let path = symbol_registry.path(id)?;
765 CrateName::new(path.crate_name()).ok()
766 })
767 .collect();
768
769 self.generate_internal(
774 ast_registry,
775 symbol_registry,
776 Some(&affected_file_keys),
777 Some(&affected_crates),
778 )
779 }
780
781 fn resolve_to_file_level_symbol(
799 path: &SymbolPath,
800 registry: &SymbolRegistry,
801 ast_registry: &ASTRegistry,
802 ) -> SymbolPath {
803 let mut current = path.clone();
804 loop {
805 if let Some(parent) = current.parent() {
806 if let Some(parent_id) = registry.lookup(&parent) {
807 if registry.kind(parent_id) == Some(SymbolKind::Mod) {
808 if ast_registry.is_inline_module(parent_id) {
811 current = parent;
812 continue;
813 }
814 return current;
815 }
816 }
817 current = parent;
818 } else {
819 return current;
820 }
821 }
822 }
823
824 fn symbol_path_to_file_key(&self, path: &SymbolPath) -> String {
835 let segments: Vec<&str> = path.segments().collect();
836 let is_main = path.is_main_symbol();
837 let skip_count = if is_main { 2 } else { 1 };
838
839 let item_segments = if segments.len() > skip_count {
853 &segments[skip_count..segments.len() - 1]
854 } else {
855 &[]
856 };
857
858 let module_segments: Vec<&str> = item_segments
860 .iter()
861 .filter(|s| !s.starts_with("<impl "))
862 .copied()
863 .collect();
864
865 let root_file = if is_main { "src/main.rs" } else { "src/lib.rs" };
866
867 let crate_name_opt = CrateName::new(path.crate_name()).ok();
869
870 if module_segments.is_empty() {
871 root_file.to_string()
872 } else {
873 let mut current = root_file.to_string();
875 for seg in &module_segments {
876 current = self.derive_child_path(¤t, seg, crate_name_opt.as_ref());
877 }
878 current
879 }
880 }
881
882 pub fn generate_with_graph(
893 &self,
894 code_graph: &CodeGraphV2,
895 ast_registry: &ASTRegistry,
896 symbol_registry: &SymbolRegistry,
897 ) -> Result<GeneratedWorkspace, ToSynError> {
898 let mut workspace = GeneratedWorkspace::default();
899
900 let mut crate_children: HashMap<(String, bool), Vec<SymbolId>> = HashMap::new();
903
904 for &root_id in code_graph.crate_roots() {
905 if let Some(path) = symbol_registry.path(root_id) {
907 let (crate_name, is_main) = if path.is_main_symbol() {
908 (
909 path.main_target_crate()
910 .unwrap_or(path.crate_name())
911 .to_string(),
912 true,
913 )
914 } else {
915 (path.crate_name().to_string(), false)
916 };
917
918 let children: Vec<SymbolId> = code_graph.children_of(root_id).collect();
920 crate_children
921 .entry((crate_name, is_main))
922 .or_default()
923 .extend(children);
924 }
925 }
926
927 for ((crate_name, is_main), root_ids) in crate_children {
929 let entry = workspace
930 .crates
931 .entry(crate_name.clone())
932 .or_insert_with(|| GeneratedCrate {
933 crate_name: crate_name.clone(),
934 files: HashMap::new(),
935 });
936
937 let generated_files = if self.single_file {
938 self.generate_single_file_from_graph(
939 &crate_name,
940 &root_ids,
941 is_main,
942 code_graph,
943 ast_registry,
944 symbol_registry,
945 )
946 } else {
947 self.generate_multi_file_from_graph(
948 &crate_name,
949 &root_ids,
950 is_main,
951 code_graph,
952 ast_registry,
953 symbol_registry,
954 )
955 };
956
957 entry.files.extend(generated_files?);
958 }
959
960 Ok(workspace)
961 }
962
963 fn generate_single_file_from_graph(
965 &self,
966 _crate_name: &str,
967 root_ids: &[SymbolId],
968 is_main: bool,
969 code_graph: &CodeGraphV2,
970 ast_registry: &ASTRegistry,
971 symbol_registry: &SymbolRegistry,
972 ) -> Result<HashMap<String, GeneratedFile>, ToSynError> {
973 let items =
974 self.collect_items_from_graph(root_ids, code_graph, ast_registry, symbol_registry);
975
976 let file_attrs = self.extract_file_attrs_from_ids(root_ids, ast_registry, symbol_registry);
978
979 let pure_file = PureFile {
980 attrs: file_attrs,
981 items,
982 };
983 let source = pure_file.to_source()?;
984
985 let root_file = if is_main { "src/main.rs" } else { "src/lib.rs" };
986
987 let mut files = HashMap::new();
988 files.insert(root_file.to_string(), GeneratedFile { source, pure_file });
989 Ok(files)
990 }
991
992 fn generate_multi_file_from_graph(
994 &self,
995 _crate_name: &str,
996 root_ids: &[SymbolId],
997 is_main: bool,
998 code_graph: &CodeGraphV2,
999 ast_registry: &ASTRegistry,
1000 symbol_registry: &SymbolRegistry,
1001 ) -> Result<HashMap<String, GeneratedFile>, ToSynError> {
1002 let root_file = if is_main { "src/main.rs" } else { "src/lib.rs" };
1003
1004 let mut files = HashMap::new();
1005 self.generate_file_from_graph(
1006 root_file,
1007 root_ids,
1008 None, code_graph,
1010 ast_registry,
1011 symbol_registry,
1012 &mut files,
1013 )?;
1014 Ok(files)
1015 }
1016
1017 fn collect_items_from_graph(
1019 &self,
1020 ids: &[SymbolId],
1021 code_graph: &CodeGraphV2,
1022 ast_registry: &ASTRegistry,
1023 symbol_registry: &SymbolRegistry,
1024 ) -> Vec<PureItem> {
1025 let mut items = Vec::new();
1026
1027 let mut mods: Vec<SymbolId> = Vec::new();
1029 let mut non_mods: Vec<SymbolId> = Vec::new();
1030
1031 for &id in ids {
1032 if let Some(kind) = symbol_registry.kind(id) {
1033 if kind == SymbolKind::Mod {
1034 if let Some(path) = symbol_registry.path(id) {
1037 if path.is_crate_root() {
1038 non_mods.push(id);
1041 } else {
1042 mods.push(id);
1045 }
1046 } else {
1047 mods.push(id);
1048 }
1049 } else {
1050 non_mods.push(id);
1051 }
1052 }
1053 }
1054
1055 for id in non_mods {
1058 if symbol_registry.kind(id) == Some(SymbolKind::Impl) {
1060 continue;
1061 }
1062 if let Some(item) = ast_registry.get(id) {
1063 items.push(item.clone());
1064 }
1065 }
1066
1067 for &id in ids {
1071 if let Some(module_items) = ast_registry.get_module_items(id) {
1072 for item in module_items {
1073 if matches!(item, PureItem::Use(_) | PureItem::Impl(_)) {
1074 items.push(item.clone());
1075 }
1076 }
1077 }
1078 }
1079
1080 for mod_id in mods {
1082 let children: Vec<SymbolId> = code_graph.children_of(mod_id).collect();
1083 let child_items =
1084 self.collect_items_from_graph(&children, code_graph, ast_registry, symbol_registry);
1085
1086 if let Some(path) = symbol_registry.path(mod_id) {
1088 let mod_name = path.segments().last().unwrap_or("unknown").to_string();
1089
1090 let vis = if child_items.iter().any(is_public) {
1092 PureVis::Public
1093 } else {
1094 PureVis::Private
1095 };
1096
1097 let mut all_items = Vec::new();
1099 if let Some(module_items) = ast_registry.get_module_items(mod_id) {
1100 for item in module_items {
1101 if let PureItem::Use(_) = item {
1102 all_items.push(item.clone());
1103 }
1104 }
1105 }
1106 all_items.extend(child_items);
1107
1108 let mod_attrs = ast_registry
1110 .get(mod_id)
1111 .and_then(|item| {
1112 if let PureItem::Mod(m) = item {
1113 Some(m.attrs.clone())
1114 } else {
1115 None
1116 }
1117 })
1118 .unwrap_or_default();
1119
1120 items.push(PureItem::Mod(PureMod {
1121 attrs: mod_attrs,
1122 vis,
1123 name: mod_name,
1124 items: all_items,
1125 scope: Default::default(),
1126 }));
1127 }
1128 }
1129
1130 sort_items(&mut items);
1133 items
1134 }
1135
1136 #[allow(clippy::too_many_arguments)]
1143 fn generate_file_from_graph(
1144 &self,
1145 current_path: &str,
1146 ids: &[SymbolId],
1147 containing_mod_id: Option<SymbolId>,
1148 code_graph: &CodeGraphV2,
1149 ast_registry: &ASTRegistry,
1150 symbol_registry: &SymbolRegistry,
1151 files: &mut HashMap<String, GeneratedFile>,
1152 ) -> Result<(), ToSynError> {
1153 let mut items = Vec::new();
1154
1155 let mut mods: Vec<SymbolId> = Vec::new();
1157 let mut non_mods: Vec<SymbolId> = Vec::new();
1158
1159 for &id in ids {
1160 if let Some(kind) = symbol_registry.kind(id) {
1161 if kind == SymbolKind::Mod {
1162 if let Some(path) = symbol_registry.path(id) {
1165 if path.is_crate_root() {
1166 non_mods.push(id);
1169 } else {
1170 mods.push(id);
1173 }
1174 } else {
1175 mods.push(id);
1176 }
1177 } else {
1178 non_mods.push(id);
1179 }
1180 }
1181 }
1182
1183 for id in non_mods {
1186 if symbol_registry.kind(id) == Some(SymbolKind::Impl) {
1188 continue;
1189 }
1190 if let Some(item) = ast_registry.get(id) {
1191 items.push(item.clone());
1192 }
1193 }
1194
1195 for &id in ids {
1198 if let Some(module_items) = ast_registry.get_module_items(id) {
1199 for item in module_items {
1200 if matches!(item, PureItem::Use(_) | PureItem::Impl(_)) {
1201 items.push(item.clone());
1202 }
1203 }
1204 }
1205 }
1206
1207 let mut inline_mod_ids: Vec<SymbolId> = Vec::new();
1210
1211 for &mod_id in &mods {
1212 if let Some(path) = symbol_registry.path(mod_id) {
1213 let mod_name = path.segments().last().unwrap_or("unknown").to_string();
1214
1215 if let Some(PureItem::Mod(m)) = ast_registry.get(mod_id) {
1217 if !m.items.is_empty() {
1218 items.push(PureItem::Mod(m.clone()));
1220 inline_mod_ids.push(mod_id);
1221 continue;
1222 }
1223 }
1224
1225 let (vis, attrs) = ast_registry
1229 .get(mod_id)
1230 .and_then(|item| {
1231 if let PureItem::Mod(m) = item {
1232 Some((m.vis.clone(), m.attrs.clone()))
1233 } else {
1234 None
1235 }
1236 })
1237 .unwrap_or((PureVis::Private, vec![]));
1238
1239 items.push(PureItem::Mod(PureMod {
1240 attrs,
1241 vis,
1242 name: mod_name,
1243 items: vec![], scope: Default::default(),
1245 }));
1246 }
1247 }
1248
1249 sort_items(&mut items);
1256
1257 let file_attrs = self.extract_file_attrs_from_ids_with_container(
1262 ids,
1263 containing_mod_id,
1264 ast_registry,
1265 symbol_registry,
1266 );
1267
1268 let pure_file = PureFile {
1269 attrs: file_attrs,
1270 items,
1271 };
1272 let source = pure_file.to_source()?;
1273 files.insert(
1274 current_path.to_string(),
1275 GeneratedFile { source, pure_file },
1276 );
1277
1278 for mod_id in mods {
1280 if inline_mod_ids.contains(&mod_id) {
1282 continue;
1283 }
1284
1285 if let Some(path) = symbol_registry.path(mod_id) {
1286 let mod_name = path.segments().last().unwrap_or("unknown").to_string();
1287 let crate_name_opt = CrateName::new(path.crate_name()).ok();
1288 let child_path =
1289 self.derive_child_path(current_path, &mod_name, crate_name_opt.as_ref());
1290 let children: Vec<SymbolId> = code_graph.children_of(mod_id).collect();
1291 self.generate_file_from_graph(
1292 &child_path,
1293 &children,
1294 Some(mod_id), code_graph,
1296 ast_registry,
1297 symbol_registry,
1298 files,
1299 )?;
1300 }
1301 }
1302 Ok(())
1303 }
1304
1305 fn generate_single_file_inner(
1309 &self,
1310 crate_name: &str,
1311 symbols: Vec<(SymbolId, SymbolPath, PureItem)>,
1312 is_main: bool,
1313 module_use_items: &HashMap<ModSymbolKey, Vec<PureItem>>,
1314 mod_symbols: &HashMap<ModSymbolKey, ModSymbolValue>,
1315 ) -> Result<HashMap<String, GeneratedFile>, ToSynError> {
1316 let mut tree = self.build_module_tree(crate_name, &symbols, is_main);
1318
1319 self.add_mod_symbols_to_tree(&mut tree, crate_name, is_main, mod_symbols);
1321
1322 self.add_use_items_to_tree(&mut tree, crate_name, is_main, module_use_items);
1324
1325 let pure_file = self.tree_to_pure_file(&tree);
1327 let source = pure_file.to_source()?;
1328
1329 let root_file = if is_main { "src/main.rs" } else { "src/lib.rs" };
1330
1331 let mut files = HashMap::new();
1332 files.insert(root_file.to_string(), GeneratedFile { source, pure_file });
1333 Ok(files)
1334 }
1335
1336 fn generate_multi_file_inner(
1341 &self,
1342 crate_name: &str,
1343 symbols: Vec<SymbolEntry>,
1344 opts: MultiFileOpts<'_>,
1345 ) -> Result<HashMap<String, GeneratedFile>, ToSynError> {
1346 let MultiFileOpts {
1347 is_main,
1348 module_use_items,
1349 mod_symbols,
1350 inline_module_paths,
1351 affected_file_keys,
1352 } = opts;
1353
1354 let mut tree = self.build_module_tree(crate_name, &symbols, is_main);
1356
1357 self.add_mod_symbols_to_tree(&mut tree, crate_name, is_main, mod_symbols);
1359
1360 self.add_use_items_to_tree(&mut tree, crate_name, is_main, module_use_items);
1362
1363 let root_file = if is_main { "src/main.rs" } else { "src/lib.rs" };
1364
1365 let crate_prefix = format!("{}::", crate_name);
1372 let inline_mod_paths: HashSet<Vec<String>> = inline_module_paths
1373 .iter()
1374 .filter(|p| p.starts_with(&crate_prefix))
1375 .filter_map(|p| p.strip_prefix(&crate_prefix))
1376 .map(|s| s.split("::").map(|seg| seg.to_string()).collect())
1377 .collect();
1378
1379 let current_crate = CrateName::new(crate_name).ok();
1382
1383 let mut files = HashMap::new();
1385 self.generate_files_from_tree(
1386 &tree,
1387 root_file,
1388 &[],
1389 &mut files,
1390 &inline_mod_paths,
1391 affected_file_keys,
1392 current_crate.as_ref(),
1393 )?;
1394 Ok(files)
1395 }
1396
1397 fn add_use_items_to_tree(
1399 &self,
1400 tree: &mut ModuleNode,
1401 crate_name: &str,
1402 is_main: bool,
1403 module_use_items: &HashMap<ModSymbolKey, Vec<PureItem>>,
1404 ) {
1405 tracing::debug!(
1406 "add_use_items_to_tree: crate_name={}, is_main={}, module_use_items_count={}",
1407 crate_name,
1408 is_main,
1409 module_use_items.len()
1410 );
1411 let mut items_to_add: Vec<(Vec<String>, Vec<PureItem>)> = Vec::new();
1413
1414 for ((item_crate, segments, item_is_main), use_items) in module_use_items {
1415 if item_crate != crate_name || *item_is_main != is_main {
1416 continue;
1417 }
1418 tracing::debug!(
1419 " Matched: segments={:?}, use_items_count={}",
1420 segments,
1421 use_items.len()
1422 );
1423 items_to_add.push((segments.clone(), use_items.clone()));
1424 }
1425
1426 for (segments, use_items) in items_to_add {
1428 let target_node = if segments.is_empty() {
1429 tree as &mut ModuleNode
1430 } else {
1431 let seg_refs: Vec<&str> = segments.iter().map(|s| s.as_str()).collect();
1432 tree.get_or_create_path(&seg_refs)
1433 };
1434
1435 for use_item in use_items {
1437 target_node.items.push(use_item);
1438 }
1439 }
1440 }
1441
1442 fn add_mod_symbols_to_tree(
1449 &self,
1450 tree: &mut ModuleNode,
1451 crate_name: &str,
1452 is_main: bool,
1453 mod_symbols: &HashMap<ModSymbolKey, ModSymbolValue>,
1454 ) {
1455 for ((item_crate, segments, item_is_main), (is_pub, attrs)) in mod_symbols {
1456 if item_crate != crate_name || *item_is_main != is_main {
1457 continue;
1458 }
1459 let node = if segments.is_empty() {
1461 tree as &mut ModuleNode
1463 } else {
1464 let seg_refs: Vec<&str> = segments.iter().map(|s| s.as_str()).collect();
1465 tree.get_or_create_path(&seg_refs)
1466 };
1467
1468 if !segments.is_empty() && node.is_pub.is_none() {
1470 node.is_pub = Some(*is_pub);
1471 }
1472 if node.attrs.is_empty() && !attrs.is_empty() {
1474 node.attrs = attrs.clone();
1475 }
1476 }
1477 }
1478
1479 fn build_module_tree(
1483 &self,
1484 crate_name: &str,
1485 symbols: &[(SymbolId, SymbolPath, PureItem)],
1486 is_main: bool,
1487 ) -> ModuleNode {
1488 let mut root = ModuleNode::new(crate_name.to_string());
1489
1490 for (_id, path, item) in symbols {
1491 let segments: Vec<&str> = path.segments().collect();
1493
1494 let skip_count = if is_main { 2 } else { 1 }; if segments.len() <= skip_count {
1499 continue;
1501 }
1502
1503 let module_segments: Vec<&str> = segments[skip_count..segments.len() - 1]
1506 .iter()
1507 .filter(|s| !s.starts_with("<impl "))
1508 .copied()
1509 .collect();
1510
1511 let target_module = root.get_or_create_path(&module_segments);
1513
1514 target_module.items.push(item.clone());
1516 }
1517
1518 root
1519 }
1520
1521 fn tree_to_pure_file(&self, tree: &ModuleNode) -> PureFile {
1523 let items = self.collect_items_recursive(tree, true);
1524 let file_attrs: Vec<PureAttribute> =
1526 tree.attrs.iter().filter(|a| a.is_inner).cloned().collect();
1527 PureFile {
1528 attrs: file_attrs,
1529 items,
1530 }
1531 }
1532
1533 fn collect_items_recursive(&self, node: &ModuleNode, _is_root: bool) -> Vec<PureItem> {
1535 let mut items = Vec::new();
1536
1537 let mut direct_items: Vec<PureItem> = node
1545 .items
1546 .iter()
1547 .filter(|item| match item {
1548 PureItem::Mod(m) => !m.items.is_empty() || m.scope == ModScope::Test,
1549 _ => true,
1550 })
1551 .cloned()
1552 .collect();
1553 sort_items(&mut direct_items);
1556 items.extend(direct_items);
1557
1558 for (name, child) in &node.children {
1560 let child_items = self.collect_items_recursive(child, false);
1561
1562 let vis = if let Some(is_pub) = child.is_pub {
1564 if is_pub {
1565 PureVis::Public
1566 } else {
1567 PureVis::Private
1568 }
1569 } else if child.items.iter().any(is_public) {
1570 PureVis::Public
1571 } else {
1572 PureVis::Private
1573 };
1574
1575 let mod_attrs: Vec<PureAttribute> = child
1577 .attrs
1578 .iter()
1579 .filter(|a| !a.is_inner)
1580 .cloned()
1581 .collect();
1582
1583 items.push(PureItem::Mod(PureMod {
1584 attrs: mod_attrs,
1585 vis,
1586 name: name.clone(),
1587 items: child_items,
1588 scope: Default::default(),
1589 }));
1590 }
1591
1592 items
1593 }
1594
1595 #[allow(clippy::too_many_arguments)]
1612 fn generate_files_from_tree(
1613 &self,
1614 tree: &ModuleNode,
1615 current_path: &str,
1616 mod_segments: &[String],
1617 files: &mut HashMap<String, GeneratedFile>,
1618 known_inline_mods: &HashSet<Vec<String>>,
1619 affected_file_keys: Option<&HashSet<String>>,
1620 current_crate: Option<&CrateName>,
1621 ) -> Result<(), ToSynError> {
1622 tracing::debug!(
1623 "Generating file: {} (module: {}, mod_segments={:?}, {} children, {} items, known_inline_mods={:?})",
1624 current_path,
1625 tree.name,
1626 mod_segments,
1627 tree.children.len(),
1628 tree.items.len(),
1629 known_inline_mods
1630 );
1631
1632 let child_path = |name: &str| -> Vec<String> {
1634 let mut p = Vec::with_capacity(mod_segments.len() + 1);
1635 p.extend_from_slice(mod_segments);
1636 p.push(name.to_string());
1637 p
1638 };
1639
1640 let mut items = Vec::new();
1642
1643 let mut direct_items: Vec<PureItem> = tree
1649 .items
1650 .iter()
1651 .filter(|item| match item {
1652 PureItem::Mod(m) => {
1653 known_inline_mods.contains(&child_path(&m.name))
1657 }
1658 _ => true,
1659 })
1660 .cloned()
1661 .collect();
1662 sort_items(&mut direct_items);
1665
1666 items.extend(direct_items);
1667
1668 let mut external_mod_decls: Vec<PureItem> = Vec::new();
1679 for (name, child) in &tree.children {
1680 if known_inline_mods.contains(&child_path(name)) {
1681 continue;
1682 }
1683
1684 let original_mod = tree.items.iter().find_map(|item| {
1687 if let PureItem::Mod(m) = item {
1688 if &m.name == name {
1689 return Some(m);
1690 }
1691 }
1692 None
1693 });
1694
1695 let vis = if let Some(m) = original_mod {
1697 m.vis.clone()
1698 } else if let Some(is_pub) = child.is_pub {
1699 if is_pub {
1700 PureVis::Public
1701 } else {
1702 PureVis::Private
1703 }
1704 } else {
1705 PureVis::Private
1706 };
1707
1708 let mod_attrs: Vec<PureAttribute> = if let Some(m) = original_mod {
1710 m.attrs.iter().filter(|a| !a.is_inner).cloned().collect()
1711 } else {
1712 child
1713 .attrs
1714 .iter()
1715 .filter(|a| !a.is_inner)
1716 .cloned()
1717 .collect()
1718 };
1719
1720 external_mod_decls.push(PureItem::Mod(PureMod {
1721 attrs: mod_attrs,
1722 vis,
1723 name: name.clone(),
1724 items: vec![], scope: Default::default(),
1726 }));
1727 }
1728
1729 if !external_mod_decls.is_empty() {
1737 let mut combined = external_mod_decls;
1738 combined.append(&mut items);
1739 items = combined;
1740 }
1741
1742 let is_affected = affected_file_keys
1747 .map(|keys| keys.contains(current_path))
1748 .unwrap_or(true);
1749
1750 if is_affected {
1751 let file_attrs: Vec<PureAttribute> =
1752 tree.attrs.iter().filter(|a| a.is_inner).cloned().collect();
1753 let pure_file = PureFile {
1754 attrs: file_attrs,
1755 items,
1756 };
1757 let source = pure_file.to_source()?;
1758 files.insert(
1759 current_path.to_string(),
1760 GeneratedFile { source, pure_file },
1761 );
1762 } else {
1763 tracing::debug!("Skipping unaffected file: {}", current_path);
1764 }
1765
1766 for (name, child) in &tree.children {
1769 let child_segments = child_path(name);
1770 if known_inline_mods.contains(&child_segments) {
1771 tracing::debug!("Skipping inline module {} (no separate file)", name);
1772 continue;
1773 }
1774 let child_file_path = self.derive_child_path(current_path, name, current_crate);
1775 tracing::debug!(" → Child module: {} at {}", name, child_file_path);
1776 self.generate_files_from_tree(
1777 child,
1778 &child_file_path,
1779 &child_segments,
1780 files,
1781 known_inline_mods,
1782 affected_file_keys,
1783 current_crate,
1784 )?;
1785 }
1786 Ok(())
1787 }
1788
1789 fn derive_child_path(
1805 &self,
1806 parent_path: &str,
1807 child_name: &str,
1808 current_crate: Option<&CrateName>,
1809 ) -> String {
1810 let parent_dir = if parent_path.ends_with("/lib.rs") || parent_path.ends_with("/main.rs") {
1812 parent_path
1814 .trim_end_matches("lib.rs")
1815 .trim_end_matches("main.rs")
1816 } else if parent_path.ends_with("/mod.rs") {
1817 parent_path.trim_end_matches("mod.rs")
1819 } else {
1820 parent_path.trim_end_matches(".rs")
1822 };
1823
1824 let mod_rs_candidate = format!("{}{}/mod.rs", parent_dir, child_name);
1825 let flat_candidate =
1826 if parent_path.ends_with("/lib.rs") || parent_path.ends_with("/main.rs") {
1827 format!("{}{}.rs", parent_dir, child_name)
1828 } else {
1829 format!("{}/{}.rs", parent_dir, child_name)
1830 };
1831
1832 let keys = self.existing_keys_for_crate(current_crate);
1836 if keys.contains(&mod_rs_candidate) {
1837 return mod_rs_candidate;
1838 }
1839 if keys.contains(&flat_candidate) {
1840 return flat_candidate;
1841 }
1842
1843 if self.use_mod_rs {
1845 mod_rs_candidate
1846 } else {
1847 flat_candidate
1848 }
1849 }
1850
1851 fn extract_file_attrs_from_ids(
1856 &self,
1857 ids: &[SymbolId],
1858 ast_registry: &ASTRegistry,
1859 symbol_registry: &SymbolRegistry,
1860 ) -> Vec<PureAttribute> {
1861 self.extract_file_attrs_from_ids_with_container(ids, None, ast_registry, symbol_registry)
1862 }
1863
1864 fn extract_file_attrs_from_ids_with_container(
1880 &self,
1881 ids: &[SymbolId],
1882 containing_mod_id: Option<SymbolId>,
1883 ast_registry: &ASTRegistry,
1884 symbol_registry: &SymbolRegistry,
1885 ) -> Vec<PureAttribute> {
1886 if let Some(mod_id) = containing_mod_id {
1887 if let Some(PureItem::Mod(m)) = ast_registry.get(mod_id) {
1888 return m.attrs.iter().filter(|a| a.is_inner).cloned().collect();
1889 }
1890 }
1891
1892 for &id in ids {
1893 if let Some(kind) = symbol_registry.kind(id) {
1895 if kind == SymbolKind::Mod {
1896 if let Some(path) = symbol_registry.path(id) {
1897 if path.is_crate_root() {
1898 if let Some(PureItem::Mod(m)) = ast_registry.get(id) {
1900 return m.attrs.iter().filter(|a| a.is_inner).cloned().collect();
1901 }
1902 }
1903 }
1904 }
1905 }
1906 }
1907 Vec::new()
1908 }
1909}
1910
1911#[derive(Debug, Clone, Default)]
1913struct ModuleNode {
1914 name: String,
1915 items: Vec<PureItem>,
1916 children: BTreeMap<String, ModuleNode>,
1917 is_pub: Option<bool>,
1919 attrs: Vec<PureAttribute>,
1923}
1924
1925impl ModuleNode {
1926 fn new(name: String) -> Self {
1927 Self {
1928 name,
1929 items: Vec::new(),
1930 children: BTreeMap::new(),
1931 is_pub: None,
1932 attrs: Vec::new(),
1933 }
1934 }
1935
1936 fn get_or_create_path(&mut self, segments: &[&str]) -> &mut ModuleNode {
1937 if segments.is_empty() {
1938 return self;
1939 }
1940
1941 let child = self
1942 .children
1943 .entry(segments[0].to_string())
1944 .or_insert_with(|| ModuleNode::new(segments[0].to_string()));
1945
1946 child.get_or_create_path(&segments[1..])
1947 }
1948}
1949
1950fn sort_items(items: &mut [PureItem]) {
1960 items.sort_by(|a, b| {
1961 let kind_a = item_sort_key(a);
1962 let kind_b = item_sort_key(b);
1963
1964 match kind_a.cmp(&kind_b) {
1965 std::cmp::Ordering::Equal => {
1966 let name_cmp = item_name(a).cmp(item_name(b));
1967 if name_cmp != std::cmp::Ordering::Equal {
1968 return name_cmp;
1969 }
1970 match (a, b) {
1973 (PureItem::Impl(a_impl), PureItem::Impl(b_impl)) => {
1974 match (&a_impl.trait_, &b_impl.trait_) {
1975 (None, None) => std::cmp::Ordering::Equal,
1976 (None, Some(_)) => std::cmp::Ordering::Less,
1977 (Some(_), None) => std::cmp::Ordering::Greater,
1978 (Some(a_trait), Some(b_trait)) => a_trait.cmp(b_trait),
1979 }
1980 }
1981 _ => std::cmp::Ordering::Equal,
1982 }
1983 }
1984 other => other,
1985 }
1986 });
1987}
1988
1989fn item_sort_key(item: &PureItem) -> u8 {
1990 match item {
2002 PureItem::Mod(m) if m.name == "tests" => 12, PureItem::Mod(_) => 0, PureItem::Use(_) => 1,
2005 PureItem::Const(_) => 2,
2006 PureItem::Static(_) => 3,
2007 PureItem::Type(_) => 4,
2008 PureItem::Struct(_) => 5,
2009 PureItem::Enum(_) => 6,
2010 PureItem::Trait(_) => 7,
2011 PureItem::Fn(_) => 8,
2012 PureItem::Impl(_) => 9,
2013 PureItem::Macro(_) => 10,
2014 PureItem::Other(_) => 11,
2015 PureItem::Verbatim(_) => 13,
2016 }
2017}
2018
2019fn item_name(item: &PureItem) -> &str {
2020 match item {
2021 PureItem::Struct(s) => &s.name,
2022 PureItem::Enum(e) => &e.name,
2023 PureItem::Fn(f) => &f.name,
2024 PureItem::Trait(t) => &t.name,
2025 PureItem::Impl(i) => &i.self_ty,
2026 PureItem::Const(c) => &c.name,
2027 PureItem::Static(s) => &s.name,
2028 PureItem::Type(t) => &t.name,
2029 PureItem::Mod(m) => &m.name,
2030 PureItem::Macro(m) => &m.path,
2031 _ => "",
2032 }
2033}
2034
2035fn is_public(item: &PureItem) -> bool {
2036 match item {
2037 PureItem::Struct(s) => matches!(s.vis, PureVis::Public),
2038 PureItem::Enum(e) => matches!(e.vis, PureVis::Public),
2039 PureItem::Fn(f) => matches!(f.vis, PureVis::Public),
2040 PureItem::Trait(t) => matches!(t.vis, PureVis::Public),
2041 PureItem::Const(c) => matches!(c.vis, PureVis::Public),
2042 PureItem::Static(s) => matches!(s.vis, PureVis::Public),
2043 PureItem::Type(t) => matches!(t.vis, PureVis::Public),
2044 PureItem::Mod(m) => matches!(m.vis, PureVis::Public),
2045 _ => false,
2046 }
2047}
2048
2049#[cfg(test)]
2050mod tests {
2051 use super::*;
2052 use ryo_source::pure::{
2053 PureBlock, PureEnum, PureFields, PureFn, PureGenerics, PureStruct, PureVariant,
2054 };
2055 use ryo_symbol::SymbolKind;
2056
2057 fn make_struct(name: &str) -> PureItem {
2058 PureItem::Struct(PureStruct {
2059 attrs: vec![],
2060 vis: PureVis::Public,
2061 name: name.to_string(),
2062 generics: PureGenerics::default(),
2063 fields: PureFields::Unit,
2064 })
2065 }
2066
2067 fn make_fn(name: &str) -> PureItem {
2068 PureItem::Fn(PureFn {
2069 attrs: vec![],
2070 vis: PureVis::Public,
2071 is_async: false,
2072 is_async_inferred: false,
2073 is_const: false,
2074 is_unsafe: false,
2075 abi: None,
2076 name: name.to_string(),
2077 generics: PureGenerics::default(),
2078 params: vec![],
2079 ret: None,
2080 body: PureBlock::default(),
2081 })
2082 }
2083
2084 fn make_path(s: &str) -> SymbolPath {
2085 SymbolPath::parse(s).unwrap()
2086 }
2087
2088 #[test]
2089 fn test_derive_child_path_default_flat() {
2090 let gen = RegistryGenerator::multi_file();
2092 assert_eq!(
2093 gen.derive_child_path("src/lib.rs", "models", None),
2094 "src/models.rs"
2095 );
2096 assert_eq!(
2097 gen.derive_child_path("src/models.rs", "user", None),
2098 "src/models/user.rs"
2099 );
2100 }
2101
2102 #[test]
2103 fn test_derive_child_path_respects_existing_mod_rs() {
2104 let gen = RegistryGenerator::multi_file()
2107 .with_existing_files(vec!["src/models/mod.rs".to_string()]);
2108 assert_eq!(
2109 gen.derive_child_path("src/lib.rs", "models", None),
2110 "src/models/mod.rs",
2111 "existing mod.rs must be preferred over flat sibling"
2112 );
2113 assert_eq!(
2115 gen.derive_child_path("src/lib.rs", "config", None),
2116 "src/config.rs"
2117 );
2118 }
2119
2120 #[test]
2121 fn test_derive_child_path_respects_existing_flat() {
2122 let gen = RegistryGenerator::multi_file_mod_rs()
2126 .with_existing_files(vec!["src/models.rs".to_string()]);
2127 assert_eq!(
2128 gen.derive_child_path("src/lib.rs", "models", None),
2129 "src/models.rs",
2130 "existing flat layout must override use_mod_rs default"
2131 );
2132 assert_eq!(
2134 gen.derive_child_path("src/lib.rs", "config", None),
2135 "src/config/mod.rs"
2136 );
2137 }
2138
2139 #[test]
2140 fn test_derive_child_path_per_crate_isolation() {
2141 let crate_a = CrateName::new("ryo-executor").unwrap();
2151 let crate_b = CrateName::new("ryo-agent").unwrap();
2152
2153 let mut per_crate: HashMap<CrateName, HashSet<String>> = HashMap::new();
2154 per_crate
2155 .entry(crate_a.clone())
2156 .or_default()
2157 .insert("src/executor/mod.rs".to_string());
2158 per_crate
2159 .entry(crate_b.clone())
2160 .or_default()
2161 .insert("src/executor.rs".to_string());
2162
2163 let gen = RegistryGenerator::multi_file().with_existing_files_per_crate(per_crate);
2164
2165 assert_eq!(
2166 gen.derive_child_path("src/lib.rs", "executor", Some(&crate_a)),
2167 "src/executor/mod.rs",
2168 "crate A must keep mod.rs layout from its own key set",
2169 );
2170 assert_eq!(
2171 gen.derive_child_path("src/lib.rs", "executor", Some(&crate_b)),
2172 "src/executor.rs",
2173 "crate B must use flat layout — crate A's mod.rs key MUST NOT bleed here",
2174 );
2175 }
2176
2177 #[test]
2178 fn test_derive_child_path_per_crate_falls_back_to_workspace_wide() {
2179 let crate_a = CrateName::new("ryo-executor").unwrap();
2183 let crate_unknown = CrateName::new("unknown-crate").unwrap();
2184
2185 let mut per_crate: HashMap<CrateName, HashSet<String>> = HashMap::new();
2186 per_crate
2187 .entry(crate_a)
2188 .or_default()
2189 .insert("src/executor/mod.rs".to_string());
2190
2191 let gen = RegistryGenerator::multi_file()
2192 .with_existing_files(vec!["src/storage/mod.rs".to_string()])
2193 .with_existing_files_per_crate(per_crate);
2194
2195 assert_eq!(
2198 gen.derive_child_path("src/lib.rs", "storage", Some(&crate_unknown)),
2199 "src/storage/mod.rs",
2200 "absent crate must fall back to workspace-wide set",
2201 );
2202 assert_eq!(
2204 gen.derive_child_path("src/lib.rs", "storage", None),
2205 "src/storage/mod.rs",
2206 "None current_crate must fall back to workspace-wide set",
2207 );
2208 }
2209
2210 #[test]
2211 fn test_single_file_basic() {
2212 let mut ast_registry = ASTRegistry::new();
2213 let mut symbol_registry = SymbolRegistry::new();
2214
2215 let config_path = make_path("my_crate::Config");
2217 let config_id = symbol_registry
2218 .register(config_path, SymbolKind::Struct)
2219 .unwrap();
2220 ast_registry.set(config_id, make_struct("Config"));
2221
2222 let helper_path = make_path("my_crate::helper");
2223 let helper_id = symbol_registry
2224 .register(helper_path, SymbolKind::Function)
2225 .unwrap();
2226 ast_registry.set(helper_id, make_fn("helper"));
2227
2228 let generator = RegistryGenerator::single_file();
2230 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
2231
2232 assert_eq!(workspace.crates.len(), 1);
2233 let crate_data = workspace.crates.get("my_crate").unwrap();
2234 assert_eq!(crate_data.files.len(), 1);
2235
2236 let lib = crate_data.files.get("src/lib.rs").unwrap();
2237 assert!(lib.source.contains("struct Config"));
2238 assert!(lib.source.contains("fn helper"));
2239 }
2240
2241 #[test]
2242 fn test_single_file_nested_modules() {
2243 let mut ast_registry = ASTRegistry::new();
2244 let mut symbol_registry = SymbolRegistry::new();
2245
2246 let config_path = make_path("my_crate::Config");
2248 let config_id = symbol_registry
2249 .register(config_path, SymbolKind::Struct)
2250 .unwrap();
2251 ast_registry.set(config_id, make_struct("Config"));
2252
2253 let user_path = make_path("my_crate::models::User");
2255 let user_id = symbol_registry
2256 .register(user_path, SymbolKind::Struct)
2257 .unwrap();
2258 ast_registry.set(user_id, make_struct("User"));
2259
2260 let dto_path = make_path("my_crate::models::dto::UserDto");
2262 let dto_id = symbol_registry
2263 .register(dto_path, SymbolKind::Struct)
2264 .unwrap();
2265 ast_registry.set(dto_id, make_struct("UserDto"));
2266
2267 let generator = RegistryGenerator::single_file();
2269 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
2270
2271 let lib = workspace
2272 .crates
2273 .get("my_crate")
2274 .unwrap()
2275 .files
2276 .get("src/lib.rs")
2277 .unwrap();
2278
2279 assert!(lib.source.contains("struct Config"));
2281 assert!(lib.source.contains("mod models"));
2282 assert!(lib.source.contains("struct User"));
2283 assert!(lib.source.contains("mod dto"));
2284 assert!(lib.source.contains("struct UserDto"));
2285
2286 syn::parse_str::<syn::File>(&lib.source)
2288 .unwrap_or_else(|_| panic!("Should be valid Rust:\n{}", lib.source));
2289 }
2290
2291 #[test]
2292 fn test_multi_file_submodule_preserves_inner_attrs() {
2293 let mut ast_registry = ASTRegistry::new();
2299 let mut symbol_registry = SymbolRegistry::new();
2300
2301 let models_mod = PureMod {
2303 attrs: vec![PureAttribute {
2304 path: "allow".to_string(),
2305 meta: PureAttrMeta::List("unused".to_string()),
2306 is_inner: true,
2307 }],
2308 vis: PureVis::Public,
2309 name: "models".to_string(),
2310 items: vec![],
2311 scope: Default::default(),
2312 };
2313 let models_id = symbol_registry
2314 .register(make_path("my_crate::models"), SymbolKind::Mod)
2315 .unwrap();
2316 ast_registry.set(models_id, PureItem::Mod(models_mod));
2317
2318 let user_id = symbol_registry
2320 .register(make_path("my_crate::models::User"), SymbolKind::Struct)
2321 .unwrap();
2322 ast_registry.set(user_id, make_struct("User"));
2323
2324 let generator = RegistryGenerator::multi_file();
2326 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
2327 let crate_data = workspace.crates.get("my_crate").unwrap();
2328 let models = crate_data
2329 .files
2330 .get("src/models.rs")
2331 .expect("models.rs should be generated");
2332
2333 assert!(
2335 !models.pure_file.attrs.is_empty(),
2336 "submodule file lost its inner attrs (regression of D-2 `//!` drop)"
2337 );
2338 assert!(
2339 models.pure_file.attrs.iter().all(|a| a.is_inner),
2340 "all file-level attrs must be inner: {:?}",
2341 models.pure_file.attrs
2342 );
2343 assert_eq!(models.pure_file.attrs[0].path, "allow");
2344 assert!(models.source.contains("allow(unused)"));
2345 }
2346
2347 #[test]
2348 fn test_multi_file_modern_style() {
2349 let mut ast_registry = ASTRegistry::new();
2350 let mut symbol_registry = SymbolRegistry::new();
2351
2352 let config_path = make_path("my_crate::Config");
2354 let config_id = symbol_registry
2355 .register(config_path, SymbolKind::Struct)
2356 .unwrap();
2357 ast_registry.set(config_id, make_struct("Config"));
2358
2359 let user_path = make_path("my_crate::models::User");
2361 let user_id = symbol_registry
2362 .register(user_path, SymbolKind::Struct)
2363 .unwrap();
2364 ast_registry.set(user_id, make_struct("User"));
2365
2366 let generator = RegistryGenerator::multi_file();
2368 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
2369
2370 let crate_data = workspace.crates.get("my_crate").unwrap();
2371
2372 assert_eq!(crate_data.files.len(), 2);
2374 assert!(crate_data.files.contains_key("src/lib.rs"));
2375 assert!(crate_data.files.contains_key("src/models.rs"));
2376
2377 let lib = crate_data.files.get("src/lib.rs").unwrap();
2379 assert!(lib.source.contains("struct Config"));
2380 assert!(lib.source.contains("mod models;"));
2381
2382 let models = crate_data.files.get("src/models.rs").unwrap();
2384 assert!(models.source.contains("struct User"));
2385 }
2386
2387 #[test]
2388 fn test_multi_crate() {
2389 let mut ast_registry = ASTRegistry::new();
2390 let mut symbol_registry = SymbolRegistry::new();
2391
2392 let foo_path = make_path("crate_a::Foo");
2394 let foo_id = symbol_registry
2395 .register(foo_path, SymbolKind::Struct)
2396 .unwrap();
2397 ast_registry.set(foo_id, make_struct("Foo"));
2398
2399 let bar_path = make_path("crate_b::Bar");
2401 let bar_id = symbol_registry
2402 .register(bar_path, SymbolKind::Struct)
2403 .unwrap();
2404 ast_registry.set(bar_id, make_struct("Bar"));
2405
2406 let generator = RegistryGenerator::single_file();
2408 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
2409
2410 assert_eq!(workspace.crates.len(), 2);
2412 assert!(workspace.crates.contains_key("crate_a"));
2413 assert!(workspace.crates.contains_key("crate_b"));
2414
2415 let crate_a = workspace.crates.get("crate_a").unwrap();
2417 assert!(crate_a
2418 .files
2419 .get("src/lib.rs")
2420 .unwrap()
2421 .source
2422 .contains("struct Foo"));
2423
2424 let crate_b = workspace.crates.get("crate_b").unwrap();
2425 assert!(crate_b
2426 .files
2427 .get("src/lib.rs")
2428 .unwrap()
2429 .source
2430 .contains("struct Bar"));
2431 }
2432
2433 #[test]
2434 fn test_all_generated_valid_rust() {
2435 let mut ast_registry = ASTRegistry::new();
2436 let mut symbol_registry = SymbolRegistry::new();
2437
2438 for (path, name) in [
2440 ("app::Config", "Config"),
2441 ("app::models::User", "User"),
2442 ("app::models::Post", "Post"),
2443 ("app::models::dto::UserDto", "UserDto"),
2444 ("app::utils::helpers::format", "format"),
2445 ] {
2446 let sp = make_path(path);
2447 let kind = if name == "format" {
2448 SymbolKind::Function
2449 } else {
2450 SymbolKind::Struct
2451 };
2452 let id = symbol_registry.register(sp, kind).unwrap();
2453 if name == "format" {
2454 ast_registry.set(id, make_fn(name));
2455 } else {
2456 ast_registry.set(id, make_struct(name));
2457 }
2458 }
2459
2460 for generator in [
2462 RegistryGenerator::single_file(),
2463 RegistryGenerator::multi_file(),
2464 RegistryGenerator::multi_file_mod_rs(),
2465 ] {
2466 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
2467
2468 for (crate_name, path, file) in workspace.iter_files() {
2469 syn::parse_str::<syn::File>(&file.source).unwrap_or_else(|_| {
2470 panic!(
2471 "[{}] {} should be valid Rust:\n{}",
2472 crate_name, path, file.source
2473 )
2474 });
2475 }
2476 }
2477 }
2478
2479 #[test]
2488 fn test_add_mod_no_span_required() {
2489 let mut ast_registry = ASTRegistry::new();
2490 let mut symbol_registry = SymbolRegistry::new();
2491
2492 let config_id = symbol_registry
2494 .register(make_path("app::Config"), SymbolKind::Struct)
2495 .unwrap();
2496 ast_registry.set(config_id, make_struct("Config"));
2497
2498 let generator = RegistryGenerator::multi_file();
2500 let before = generator.generate(&ast_registry, &symbol_registry).unwrap();
2501
2502 assert_eq!(before.crates.get("app").unwrap().files.len(), 1);
2504 assert!(before
2505 .crates
2506 .get("app")
2507 .unwrap()
2508 .files
2509 .contains_key("src/lib.rs"));
2510
2511 let user_id = symbol_registry
2514 .register(make_path("app::models::User"), SymbolKind::Struct)
2515 .unwrap();
2516 ast_registry.set(user_id, make_struct("User"));
2517
2518 let post_id = symbol_registry
2519 .register(make_path("app::models::Post"), SymbolKind::Struct)
2520 .unwrap();
2521 ast_registry.set(post_id, make_struct("Post"));
2522
2523 let after = generator.generate(&ast_registry, &symbol_registry).unwrap();
2525
2526 let app = after.crates.get("app").unwrap();
2528 assert_eq!(app.files.len(), 2);
2529 assert!(app.files.contains_key("src/lib.rs"));
2530 assert!(app.files.contains_key("src/models.rs")); let lib = app.files.get("src/lib.rs").unwrap();
2534 assert!(lib.source.contains("struct Config"));
2535 assert!(lib.source.contains("mod models;"));
2536
2537 let models = app.files.get("src/models.rs").unwrap();
2539 assert!(models.source.contains("struct User"));
2540 assert!(models.source.contains("struct Post"));
2541
2542 syn::parse_str::<syn::File>(&lib.source).unwrap();
2544 syn::parse_str::<syn::File>(&models.source).unwrap();
2545 }
2546
2547 #[test]
2549 fn test_add_nested_mod_no_span_required() {
2550 let mut ast_registry = ASTRegistry::new();
2551 let mut symbol_registry = SymbolRegistry::new();
2552
2553 let config_id = symbol_registry
2555 .register(make_path("app::Config"), SymbolKind::Struct)
2556 .unwrap();
2557 ast_registry.set(config_id, make_struct("Config"));
2558
2559 let handler_id = symbol_registry
2561 .register(
2562 make_path("app::api::v1::handlers::UserHandler"),
2563 SymbolKind::Struct,
2564 )
2565 .unwrap();
2566 ast_registry.set(handler_id, make_struct("UserHandler"));
2567
2568 let generator = RegistryGenerator::multi_file();
2570 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
2571
2572 let app = workspace.crates.get("app").unwrap();
2573
2574 assert_eq!(app.files.len(), 4);
2576 assert!(app.files.contains_key("src/lib.rs"));
2577 assert!(app.files.contains_key("src/api.rs"));
2578 assert!(app.files.contains_key("src/api/v1.rs"));
2579 assert!(app.files.contains_key("src/api/v1/handlers.rs"));
2580
2581 assert!(app
2583 .files
2584 .get("src/lib.rs")
2585 .unwrap()
2586 .source
2587 .contains("mod api;"));
2588 assert!(app
2589 .files
2590 .get("src/api.rs")
2591 .unwrap()
2592 .source
2593 .contains("mod v1;"));
2594 assert!(app
2595 .files
2596 .get("src/api/v1.rs")
2597 .unwrap()
2598 .source
2599 .contains("mod handlers;"));
2600 assert!(app
2601 .files
2602 .get("src/api/v1/handlers.rs")
2603 .unwrap()
2604 .source
2605 .contains("struct UserHandler"));
2606
2607 for file in app.files.values() {
2609 syn::parse_str::<syn::File>(&file.source).unwrap();
2610 }
2611 }
2612
2613 #[test]
2618 fn test_remove_mod_no_span_required() {
2619 let mut ast_registry = ASTRegistry::new();
2620 let mut symbol_registry = SymbolRegistry::new();
2621
2622 let config_id = symbol_registry
2624 .register(make_path("app::Config"), SymbolKind::Struct)
2625 .unwrap();
2626 ast_registry.set(config_id, make_struct("Config"));
2627
2628 let user_id = symbol_registry
2629 .register(make_path("app::models::User"), SymbolKind::Struct)
2630 .unwrap();
2631 ast_registry.set(user_id, make_struct("User"));
2632
2633 let generator = RegistryGenerator::multi_file();
2635 let before = generator.generate(&ast_registry, &symbol_registry).unwrap();
2636
2637 assert_eq!(before.crates.get("app").unwrap().files.len(), 2);
2639
2640 ast_registry.remove(user_id);
2643 symbol_registry.remove(user_id);
2644
2645 let after = generator.generate(&ast_registry, &symbol_registry).unwrap();
2647
2648 let app = after.crates.get("app").unwrap();
2650 assert_eq!(app.files.len(), 1);
2651 assert!(app.files.contains_key("src/lib.rs"));
2652 assert!(!app.files.contains_key("src/models.rs")); let lib = app.files.get("src/lib.rs").unwrap();
2656 assert!(lib.source.contains("struct Config"));
2657 assert!(!lib.source.contains("mod models")); syn::parse_str::<syn::File>(&lib.source).unwrap();
2660 }
2661
2662 #[test]
2664 fn test_partial_remove_mod() {
2665 let mut ast_registry = ASTRegistry::new();
2666 let mut symbol_registry = SymbolRegistry::new();
2667
2668 let config_id = symbol_registry
2670 .register(make_path("app::Config"), SymbolKind::Struct)
2671 .unwrap();
2672 ast_registry.set(config_id, make_struct("Config"));
2673
2674 let user_id = symbol_registry
2675 .register(make_path("app::models::User"), SymbolKind::Struct)
2676 .unwrap();
2677 ast_registry.set(user_id, make_struct("User"));
2678
2679 let post_id = symbol_registry
2680 .register(make_path("app::models::Post"), SymbolKind::Struct)
2681 .unwrap();
2682 ast_registry.set(post_id, make_struct("Post"));
2683
2684 ast_registry.remove(user_id);
2686 symbol_registry.remove(user_id);
2687
2688 let generator = RegistryGenerator::multi_file();
2690 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
2691
2692 let app = workspace.crates.get("app").unwrap();
2693
2694 assert_eq!(app.files.len(), 2);
2696 assert!(app.files.contains_key("src/models.rs"));
2697
2698 let models = app.files.get("src/models.rs").unwrap();
2699 assert!(!models.source.contains("struct User")); assert!(models.source.contains("struct Post")); syn::parse_str::<syn::File>(&models.source).unwrap();
2703 }
2704
2705 #[test]
2711 fn test_main_entry_point() {
2712 let mut ast_registry = ASTRegistry::new();
2713 let mut symbol_registry = SymbolRegistry::new();
2714
2715 let config_id = symbol_registry
2717 .register(make_path("my_crate::Config"), SymbolKind::Struct)
2718 .unwrap();
2719 ast_registry.set(config_id, make_struct("Config"));
2720
2721 let main_id = symbol_registry
2723 .register(make_path("main::my_crate::main"), SymbolKind::Function)
2724 .unwrap();
2725 ast_registry.set(main_id, make_fn("main"));
2726
2727 let generator = RegistryGenerator::multi_file();
2729 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
2730
2731 assert_eq!(workspace.crates.len(), 1);
2733 let crate_data = workspace.crates.get("my_crate").unwrap();
2734
2735 assert!(crate_data.files.contains_key("src/lib.rs"));
2736 assert!(crate_data.files.contains_key("src/main.rs"));
2737
2738 let lib = crate_data.files.get("src/lib.rs").unwrap();
2740 assert!(lib.source.contains("struct Config"));
2741 assert!(!lib.source.contains("fn main"));
2742
2743 let main = crate_data.files.get("src/main.rs").unwrap();
2745 assert!(main.source.contains("fn main"));
2746 assert!(!main.source.contains("struct Config"));
2747
2748 syn::parse_str::<syn::File>(&lib.source).unwrap();
2750 syn::parse_str::<syn::File>(&main.source).unwrap();
2751 }
2752
2753 #[test]
2755 fn test_main_with_modules() {
2756 let mut ast_registry = ASTRegistry::new();
2757 let mut symbol_registry = SymbolRegistry::new();
2758
2759 let main_id = symbol_registry
2761 .register(make_path("main::my_crate::main"), SymbolKind::Function)
2762 .unwrap();
2763 ast_registry.set(main_id, make_fn("main"));
2764
2765 let args_id = symbol_registry
2767 .register(make_path("main::my_crate::cli::Args"), SymbolKind::Struct)
2768 .unwrap();
2769 ast_registry.set(args_id, make_struct("Args"));
2770
2771 let generator = RegistryGenerator::single_file();
2773 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
2774
2775 let crate_data = workspace.crates.get("my_crate").unwrap();
2776 let main = crate_data.files.get("src/main.rs").unwrap();
2777
2778 assert!(main.source.contains("fn main"));
2780 assert!(main.source.contains("mod cli"));
2781 assert!(main.source.contains("struct Args"));
2782
2783 syn::parse_str::<syn::File>(&main.source).unwrap();
2784 }
2785
2786 #[test]
2788 fn test_main_multi_file_modules() {
2789 let mut ast_registry = ASTRegistry::new();
2790 let mut symbol_registry = SymbolRegistry::new();
2791
2792 let main_id = symbol_registry
2794 .register(make_path("main::my_crate::main"), SymbolKind::Function)
2795 .unwrap();
2796 ast_registry.set(main_id, make_fn("main"));
2797
2798 let args_id = symbol_registry
2800 .register(make_path("main::my_crate::cli::Args"), SymbolKind::Struct)
2801 .unwrap();
2802 ast_registry.set(args_id, make_struct("Args"));
2803
2804 let generator = RegistryGenerator::multi_file();
2806 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
2807
2808 let crate_data = workspace.crates.get("my_crate").unwrap();
2809
2810 assert!(crate_data.files.contains_key("src/main.rs"));
2812 assert!(crate_data.files.contains_key("src/cli.rs"));
2813
2814 let main = crate_data.files.get("src/main.rs").unwrap();
2816 assert!(main.source.contains("fn main"));
2817 assert!(main.source.contains("mod cli;"));
2818
2819 let cli = crate_data.files.get("src/cli.rs").unwrap();
2821 assert!(cli.source.contains("struct Args"));
2822
2823 syn::parse_str::<syn::File>(&main.source).unwrap();
2825 syn::parse_str::<syn::File>(&cli.source).unwrap();
2826 }
2827
2828 #[test]
2830 fn test_lib_and_main_separate_modules() {
2831 let mut ast_registry = ASTRegistry::new();
2832 let mut symbol_registry = SymbolRegistry::new();
2833
2834 let lib_id = symbol_registry
2836 .register(
2837 make_path("my_crate::lib_mod::LibStruct"),
2838 SymbolKind::Struct,
2839 )
2840 .unwrap();
2841 ast_registry.set(lib_id, make_struct("LibStruct"));
2842
2843 let bin_id = symbol_registry
2845 .register(
2846 make_path("main::my_crate::bin_mod::BinStruct"),
2847 SymbolKind::Struct,
2848 )
2849 .unwrap();
2850 ast_registry.set(bin_id, make_struct("BinStruct"));
2851
2852 let generator = RegistryGenerator::multi_file();
2854 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
2855
2856 let crate_data = workspace.crates.get("my_crate").unwrap();
2857
2858 assert_eq!(crate_data.files.len(), 4);
2860 assert!(crate_data.files.contains_key("src/lib.rs"));
2861 assert!(crate_data.files.contains_key("src/lib_mod.rs"));
2862 assert!(crate_data.files.contains_key("src/main.rs"));
2863 assert!(crate_data.files.contains_key("src/bin_mod.rs"));
2864
2865 let lib = crate_data.files.get("src/lib.rs").unwrap();
2867 assert!(lib.source.contains("mod lib_mod;"));
2868 assert!(!lib.source.contains("mod bin_mod"));
2869
2870 let main = crate_data.files.get("src/main.rs").unwrap();
2872 assert!(main.source.contains("mod bin_mod;"));
2873 assert!(!main.source.contains("mod lib_mod"));
2874
2875 assert!(crate_data
2877 .files
2878 .get("src/lib_mod.rs")
2879 .unwrap()
2880 .source
2881 .contains("struct LibStruct"));
2882 assert!(crate_data
2883 .files
2884 .get("src/bin_mod.rs")
2885 .unwrap()
2886 .source
2887 .contains("struct BinStruct"));
2888
2889 for file in crate_data.files.values() {
2891 syn::parse_str::<syn::File>(&file.source).unwrap();
2892 }
2893 }
2894
2895 #[test]
2897 fn test_move_symbol_between_modules() {
2898 let mut ast_registry = ASTRegistry::new();
2899 let mut symbol_registry = SymbolRegistry::new();
2900
2901 let config_id = symbol_registry
2903 .register(make_path("app::Config"), SymbolKind::Struct)
2904 .unwrap();
2905 ast_registry.set(config_id, make_struct("Config"));
2906
2907 let user_id = symbol_registry
2908 .register(make_path("app::models::User"), SymbolKind::Struct)
2909 .unwrap();
2910 ast_registry.set(user_id, make_struct("User"));
2911
2912 let generator = RegistryGenerator::multi_file();
2913
2914 let before = generator.generate(&ast_registry, &symbol_registry).unwrap();
2916 assert!(before
2917 .crates
2918 .get("app")
2919 .unwrap()
2920 .files
2921 .contains_key("src/models.rs"));
2922
2923 ast_registry.remove(user_id);
2925 symbol_registry.remove(user_id);
2926
2927 let new_user_id = symbol_registry
2928 .register(make_path("app::entities::User"), SymbolKind::Struct)
2929 .unwrap();
2930 ast_registry.set(new_user_id, make_struct("User"));
2931
2932 let after = generator.generate(&ast_registry, &symbol_registry).unwrap();
2934 let app = after.crates.get("app").unwrap();
2935
2936 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();
2940 assert!(entities.source.contains("struct User"));
2941
2942 let lib = app.files.get("src/lib.rs").unwrap();
2944 assert!(!lib.source.contains("mod models"));
2945 assert!(lib.source.contains("mod entities;"));
2946
2947 syn::parse_str::<syn::File>(&lib.source).unwrap();
2948 syn::parse_str::<syn::File>(&entities.source).unwrap();
2949 }
2950
2951 use ryo_analysis::{CodeEdgeV2, CodeGraphV2};
2958
2959 fn setup_registries_with_graph() -> (ASTRegistry, SymbolRegistry, CodeGraphV2) {
2960 let mut ast_registry = ASTRegistry::new();
2961 let mut symbol_registry = SymbolRegistry::new();
2962 let mut code_graph = CodeGraphV2::new();
2963
2964 let crate_id = symbol_registry
2966 .register(make_path("my_crate"), SymbolKind::Mod)
2967 .unwrap();
2968 code_graph.add_node(crate_id);
2969 code_graph.add_crate_root(crate_id);
2970
2971 let config_id = symbol_registry
2973 .register(make_path("my_crate::Config"), SymbolKind::Struct)
2974 .unwrap();
2975 ast_registry.set(config_id, make_struct("Config"));
2976 code_graph.add_node(config_id);
2977 code_graph.add_edge(crate_id, config_id, CodeEdgeV2::Contains);
2978
2979 let models_id = symbol_registry
2981 .register(make_path("my_crate::models"), SymbolKind::Mod)
2982 .unwrap();
2983 code_graph.add_node(models_id);
2984 code_graph.add_edge(crate_id, models_id, CodeEdgeV2::Contains);
2985
2986 let user_id = symbol_registry
2988 .register(make_path("my_crate::models::User"), SymbolKind::Struct)
2989 .unwrap();
2990 ast_registry.set(user_id, make_struct("User"));
2991 code_graph.add_node(user_id);
2992 code_graph.add_edge(models_id, user_id, CodeEdgeV2::Contains);
2993
2994 (ast_registry, symbol_registry, code_graph)
2995 }
2996
2997 #[test]
2998 fn test_generate_with_graph_single_file() {
2999 let (ast_registry, symbol_registry, code_graph) = setup_registries_with_graph();
3000
3001 let generator = RegistryGenerator::single_file();
3002 let workspace = generator
3003 .generate_with_graph(&code_graph, &ast_registry, &symbol_registry)
3004 .unwrap();
3005
3006 assert_eq!(workspace.crates.len(), 1);
3007 let crate_data = workspace.crates.get("my_crate").unwrap();
3008 assert_eq!(crate_data.files.len(), 1);
3009
3010 let lib = crate_data.files.get("src/lib.rs").unwrap();
3011 assert!(lib.source.contains("struct Config"), "Should have Config");
3012 assert!(lib.source.contains("mod models"), "Should have mod models");
3013 assert!(
3014 lib.source.contains("struct User"),
3015 "Should have User in nested mod"
3016 );
3017
3018 syn::parse_str::<syn::File>(&lib.source)
3020 .unwrap_or_else(|_| panic!("Should be valid Rust:\n{}", lib.source));
3021 }
3022
3023 #[test]
3024 fn test_generate_with_graph_multi_file() {
3025 let (ast_registry, symbol_registry, code_graph) = setup_registries_with_graph();
3026
3027 let generator = RegistryGenerator::multi_file();
3028 let workspace = generator
3029 .generate_with_graph(&code_graph, &ast_registry, &symbol_registry)
3030 .unwrap();
3031
3032 assert_eq!(workspace.crates.len(), 1);
3033 let crate_data = workspace.crates.get("my_crate").unwrap();
3034 assert_eq!(
3035 crate_data.files.len(),
3036 2,
3037 "Should have lib.rs and models.rs"
3038 );
3039
3040 let lib = crate_data.files.get("src/lib.rs").unwrap();
3041 assert!(
3042 lib.source.contains("struct Config"),
3043 "lib.rs should have Config"
3044 );
3045 assert!(
3046 lib.source.contains("mod models;"),
3047 "lib.rs should have mod declaration"
3048 );
3049
3050 let models = crate_data.files.get("src/models.rs").unwrap();
3051 assert!(
3052 models.source.contains("struct User"),
3053 "models.rs should have User"
3054 );
3055
3056 syn::parse_str::<syn::File>(&lib.source).unwrap();
3058 syn::parse_str::<syn::File>(&models.source).unwrap();
3059 }
3060
3061 #[test]
3062 fn test_generate_with_graph_matches_original() {
3063 let (ast_registry, symbol_registry, code_graph) = setup_registries_with_graph();
3064
3065 let generator = RegistryGenerator::single_file();
3066
3067 let original = generator.generate(&ast_registry, &symbol_registry).unwrap();
3069 let with_graph = generator
3070 .generate_with_graph(&code_graph, &ast_registry, &symbol_registry)
3071 .unwrap();
3072
3073 assert_eq!(original.crates.len(), with_graph.crates.len());
3075
3076 for crate_name in original.crates.keys() {
3078 assert!(with_graph.crates.contains_key(crate_name));
3079 }
3080 }
3081
3082 use ryo_source::pure::{
3087 PureAttrMeta, PureAttribute, PureField, PureImpl, PureImplItem, PureTupleField, PureType,
3088 PureUse, PureUseTree,
3089 };
3090
3091 fn make_inline_test_module() -> PureMod {
3097 PureMod {
3098 attrs: vec![],
3099 vis: PureVis::Private,
3100 name: "tests".to_string(),
3101 items: vec![
3102 PureItem::Use(PureUse {
3103 vis: PureVis::Private,
3104 tree: PureUseTree::Path {
3105 path: "super".to_string(),
3106 tree: Box::new(PureUseTree::Glob),
3107 },
3108 }),
3109 PureItem::Fn(PureFn {
3110 attrs: vec![PureAttribute {
3111 path: "test".to_string(),
3112 meta: PureAttrMeta::Path,
3113 is_inner: false,
3114 }],
3115 vis: PureVis::Private,
3116 is_async: false,
3117 is_async_inferred: false,
3118 is_const: false,
3119 is_unsafe: false,
3120 abi: None,
3121 name: "test_config".to_string(),
3122 generics: PureGenerics::default(),
3123 params: vec![],
3124 ret: None,
3125 body: PureBlock::default(),
3126 }),
3127 ],
3128 scope: ModScope::Test,
3129 }
3130 }
3131
3132 #[test]
3134 fn test_inline_test_module_idempotent_output() {
3135 let mut ast_registry = ASTRegistry::new();
3136 let mut symbol_registry = SymbolRegistry::new();
3137
3138 symbol_registry
3140 .register(make_path("my_crate"), SymbolKind::Mod)
3141 .unwrap();
3142
3143 let config_id = symbol_registry
3145 .register(make_path("my_crate::Config"), SymbolKind::Struct)
3146 .unwrap();
3147 ast_registry.set(config_id, make_struct("Config"));
3148
3149 let tests_id = symbol_registry
3151 .register(make_path("my_crate::tests"), SymbolKind::Mod)
3152 .unwrap();
3153 ast_registry.set(tests_id, PureItem::Mod(make_inline_test_module()));
3154 ast_registry.mark_inline_module(tests_id);
3155
3156 let generator = RegistryGenerator::single_file();
3158 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
3159
3160 let crate_data = workspace.crates.get("my_crate").unwrap();
3161 let lib = crate_data.files.get("src/lib.rs").unwrap();
3162
3163 syn::parse_str::<syn::File>(&lib.source)
3165 .unwrap_or_else(|_| panic!("Should be valid Rust:\n{}", lib.source));
3166
3167 assert!(
3169 lib.source.contains("struct Config"),
3170 "Output should contain Config struct"
3171 );
3172
3173 assert!(
3175 lib.source.contains("mod tests {"),
3176 "Output should have inline mod tests, got:\n{}",
3177 lib.source
3178 );
3179
3180 assert!(
3182 lib.source.contains("#[cfg(test)]"),
3183 "Output should preserve #[cfg(test)] attribute, got:\n{}",
3184 lib.source
3185 );
3186
3187 assert!(
3189 lib.source.contains("fn test_config"),
3190 "Output should contain test_config function"
3191 );
3192
3193 assert!(
3195 lib.source.contains("#[test]"),
3196 "Output should preserve #[test] attribute, got:\n{}",
3197 lib.source
3198 );
3199
3200 let workspace2 = generator.generate(&ast_registry, &symbol_registry).unwrap();
3202 let lib2 = workspace2
3203 .crates
3204 .get("my_crate")
3205 .unwrap()
3206 .files
3207 .get("src/lib.rs")
3208 .unwrap();
3209
3210 assert_eq!(
3211 lib.source, lib2.source,
3212 "Idempotent: same AST should produce identical output"
3213 );
3214 }
3215
3216 #[test]
3222 fn test_empty_cfg_test_mod_preserved_in_single_file() {
3223 let mut ast_registry = ASTRegistry::new();
3224 let mut symbol_registry = SymbolRegistry::new();
3225
3226 symbol_registry
3227 .register(make_path("my_crate"), SymbolKind::Mod)
3228 .unwrap();
3229
3230 let config_id = symbol_registry
3232 .register(make_path("my_crate::Config"), SymbolKind::Struct)
3233 .unwrap();
3234 ast_registry.set(config_id, make_struct("Config"));
3235
3236 let tests_mod = PureMod {
3239 attrs: vec![],
3240 vis: PureVis::Private,
3241 name: "tests".to_string(),
3242 items: vec![],
3243 scope: ModScope::Test,
3244 };
3245 let tests_id = symbol_registry
3246 .register(make_path("my_crate::tests"), SymbolKind::Mod)
3247 .unwrap();
3248 ast_registry.set(tests_id, PureItem::Mod(tests_mod));
3249 ast_registry.mark_inline_module(tests_id);
3250
3251 let generator = RegistryGenerator::single_file();
3252 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
3253
3254 let lib = workspace
3255 .crates
3256 .get("my_crate")
3257 .unwrap()
3258 .files
3259 .get("src/lib.rs")
3260 .unwrap();
3261
3262 syn::parse_str::<syn::File>(&lib.source)
3263 .unwrap_or_else(|_| panic!("Should be valid Rust:\n{}", lib.source));
3264
3265 let has_mod_tests = lib
3267 .pure_file
3268 .items
3269 .iter()
3270 .any(|item| matches!(item, PureItem::Mod(m) if m.name == "tests"));
3271 assert!(
3272 has_mod_tests,
3273 "Empty #[cfg(test)] mod tests; must be preserved in single-file output, got:\n{}",
3274 lib.source
3275 );
3276
3277 assert!(
3279 lib.source.contains("#[cfg(test)]"),
3280 "Output should preserve #[cfg(test)] attribute, got:\n{}",
3281 lib.source
3282 );
3283 }
3284
3285 #[test]
3287 fn test_inline_module_with_impl_idempotent_output() {
3288 let mut ast_registry = ASTRegistry::new();
3289 let mut symbol_registry = SymbolRegistry::new();
3290
3291 symbol_registry
3292 .register(make_path("my_crate"), SymbolKind::Mod)
3293 .unwrap();
3294
3295 let config_id = symbol_registry
3296 .register(make_path("my_crate::Config"), SymbolKind::Struct)
3297 .unwrap();
3298 ast_registry.set(config_id, make_struct("Config"));
3299
3300 let tests_mod = PureMod {
3302 attrs: vec![PureAttribute {
3303 path: "cfg".to_string(),
3304 meta: PureAttrMeta::List("test".to_string()),
3305 is_inner: false,
3306 }],
3307 vis: PureVis::Private,
3308 name: "tests".to_string(),
3309 items: vec![
3310 PureItem::Struct(PureStruct {
3311 attrs: vec![],
3312 vis: PureVis::Private,
3313 name: "TestHelper".to_string(),
3314 generics: PureGenerics::default(),
3315 fields: PureFields::Named(vec![PureField {
3316 attrs: vec![],
3317 vis: PureVis::Private,
3318 name: "value".to_string(),
3319 ty: PureType::Path("i32".to_string()),
3320 }]),
3321 }),
3322 PureItem::Impl(PureImpl {
3323 attrs: vec![],
3324 generics: PureGenerics::default(),
3325 is_unsafe: false,
3326 trait_: None,
3327 self_ty: "TestHelper".to_string(),
3328 items: vec![PureImplItem::Fn(PureFn {
3329 attrs: vec![],
3330 vis: PureVis::Private,
3331 is_async: false,
3332 is_async_inferred: false,
3333 is_const: false,
3334 is_unsafe: false,
3335 abi: None,
3336 name: "new".to_string(),
3337 generics: PureGenerics::default(),
3338 params: vec![],
3339 ret: Some(PureType::Path("Self".to_string())),
3340 body: PureBlock::default(),
3341 })],
3342 }),
3343 PureItem::Fn(PureFn {
3344 attrs: vec![PureAttribute {
3345 path: "test".to_string(),
3346 meta: PureAttrMeta::Path,
3347 is_inner: false,
3348 }],
3349 vis: PureVis::Private,
3350 is_async: false,
3351 is_async_inferred: false,
3352 is_const: false,
3353 is_unsafe: false,
3354 abi: None,
3355 name: "test_with_helper".to_string(),
3356 generics: PureGenerics::default(),
3357 params: vec![],
3358 ret: None,
3359 body: PureBlock::default(),
3360 }),
3361 ],
3362 scope: ModScope::Test,
3363 };
3364
3365 let tests_id = symbol_registry
3366 .register(make_path("my_crate::tests"), SymbolKind::Mod)
3367 .unwrap();
3368 ast_registry.set(tests_id, PureItem::Mod(tests_mod));
3369 ast_registry.mark_inline_module(tests_id);
3370
3371 let generator = RegistryGenerator::single_file();
3372 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
3373
3374 let crate_data = workspace.crates.get("my_crate").unwrap();
3375 let lib = crate_data.files.get("src/lib.rs").unwrap();
3376
3377 syn::parse_str::<syn::File>(&lib.source)
3379 .unwrap_or_else(|_| panic!("Should be valid Rust:\n{}", lib.source));
3380
3381 assert!(lib.source.contains("struct Config"));
3383 assert!(lib.source.contains("#[cfg(test)]"));
3384 assert!(lib.source.contains("mod tests {"));
3385 assert!(lib.source.contains("struct TestHelper"));
3386 assert!(lib.source.contains("impl TestHelper"));
3387 assert!(lib.source.contains("fn new"));
3388 assert!(lib.source.contains("fn test_with_helper"));
3389
3390 let workspace2 = generator.generate(&ast_registry, &symbol_registry).unwrap();
3392 let lib2 = workspace2
3393 .crates
3394 .get("my_crate")
3395 .unwrap()
3396 .files
3397 .get("src/lib.rs")
3398 .unwrap();
3399 assert_eq!(lib.source, lib2.source, "Idempotent output");
3400 }
3401
3402 #[test]
3404 fn test_inline_module_multi_file_no_separate_file() {
3405 let mut ast_registry = ASTRegistry::new();
3406 let mut symbol_registry = SymbolRegistry::new();
3407
3408 symbol_registry
3409 .register(make_path("my_crate"), SymbolKind::Mod)
3410 .unwrap();
3411
3412 let config_id = symbol_registry
3413 .register(make_path("my_crate::Config"), SymbolKind::Struct)
3414 .unwrap();
3415 ast_registry.set(config_id, make_struct("Config"));
3416
3417 let utils_id = symbol_registry
3419 .register(make_path("my_crate::utils"), SymbolKind::Mod)
3420 .unwrap();
3421 ast_registry.set(
3423 utils_id,
3424 PureItem::Mod(PureMod {
3425 attrs: vec![],
3426 vis: PureVis::Public,
3427 name: "utils".to_string(),
3428 items: vec![], scope: Default::default(),
3430 }),
3431 );
3432
3433 let helper_id = symbol_registry
3434 .register(make_path("my_crate::utils::helper"), SymbolKind::Function)
3435 .unwrap();
3436 ast_registry.set(helper_id, make_fn("helper"));
3437
3438 let tests_id = symbol_registry
3440 .register(make_path("my_crate::tests"), SymbolKind::Mod)
3441 .unwrap();
3442 ast_registry.set(tests_id, PureItem::Mod(make_inline_test_module()));
3443 ast_registry.mark_inline_module(tests_id);
3444
3445 let generator = RegistryGenerator::multi_file();
3447 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
3448
3449 let crate_data = workspace.crates.get("my_crate").unwrap();
3450
3451 assert!(
3453 crate_data.files.contains_key("src/lib.rs"),
3454 "Should have lib.rs"
3455 );
3456 assert!(
3457 crate_data.files.contains_key("src/utils.rs"),
3458 "Should have utils.rs (external module)"
3459 );
3460 assert!(
3461 !crate_data.files.contains_key("src/tests.rs"),
3462 "Should NOT have tests.rs (inline module stays inline)"
3463 );
3464
3465 let lib = crate_data.files.get("src/lib.rs").unwrap();
3466
3467 assert!(
3469 lib.source.contains("mod tests {"),
3470 "tests should be inline, got:\n{}",
3471 lib.source
3472 );
3473
3474 assert!(
3476 lib.source.contains("#[cfg(test)]"),
3477 "Should preserve #[cfg(test)], got:\n{}",
3478 lib.source
3479 );
3480
3481 assert!(
3483 lib.source.contains("mod utils;"),
3484 "Should have mod utils; declaration, got:\n{}",
3485 lib.source
3486 );
3487
3488 for (path, file) in &crate_data.files {
3490 syn::parse_str::<syn::File>(&file.source)
3491 .unwrap_or_else(|_| panic!("[{}] Should be valid Rust:\n{}", path, file.source));
3492 }
3493 }
3494
3495 #[test]
3508 fn test_inline_sibling_last_segment_collision() {
3509 let mut ast_registry = ASTRegistry::new();
3510 let mut symbol_registry = SymbolRegistry::new();
3511
3512 symbol_registry
3513 .register(make_path("my_crate"), SymbolKind::Mod)
3514 .unwrap();
3515
3516 let foo_id = symbol_registry
3518 .register(make_path("my_crate::foo"), SymbolKind::Mod)
3519 .unwrap();
3520 ast_registry.set(
3521 foo_id,
3522 PureItem::Mod(PureMod {
3523 attrs: vec![],
3524 vis: PureVis::Public,
3525 name: "foo".to_string(),
3526 items: vec![],
3527 scope: Default::default(),
3528 }),
3529 );
3530
3531 let foo_tests_id = symbol_registry
3533 .register(make_path("my_crate::foo::tests"), SymbolKind::Mod)
3534 .unwrap();
3535 ast_registry.set(foo_tests_id, PureItem::Mod(make_inline_test_module()));
3536 ast_registry.mark_inline_module(foo_tests_id);
3537
3538 let bar_id = symbol_registry
3540 .register(make_path("my_crate::bar"), SymbolKind::Mod)
3541 .unwrap();
3542 ast_registry.set(
3543 bar_id,
3544 PureItem::Mod(PureMod {
3545 attrs: vec![],
3546 vis: PureVis::Public,
3547 name: "bar".to_string(),
3548 items: vec![],
3549 scope: Default::default(),
3550 }),
3551 );
3552
3553 let bar_tests_id = symbol_registry
3555 .register(make_path("my_crate::bar::tests"), SymbolKind::Mod)
3556 .unwrap();
3557 ast_registry.set(
3558 bar_tests_id,
3559 PureItem::Mod(PureMod {
3560 attrs: vec![],
3561 vis: PureVis::Private,
3562 name: "tests".to_string(),
3563 items: vec![],
3564 scope: Default::default(),
3565 }),
3566 );
3567
3568 let helper_id = symbol_registry
3570 .register(
3571 make_path("my_crate::bar::tests::helper"),
3572 SymbolKind::Function,
3573 )
3574 .unwrap();
3575 ast_registry.set(helper_id, make_fn("helper"));
3576
3577 let generator = RegistryGenerator::multi_file();
3578 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
3579 let crate_data = workspace.crates.get("my_crate").unwrap();
3580
3581 assert!(
3583 !crate_data.files.contains_key("src/foo/tests.rs"),
3584 "foo::tests is inline; src/foo/tests.rs must not be generated. Files: {:?}",
3585 crate_data.files.keys().collect::<Vec<_>>()
3586 );
3587
3588 let bar_tests_key = if crate_data.files.contains_key("src/bar/tests.rs") {
3591 "src/bar/tests.rs"
3592 } else if crate_data.files.contains_key("src/bar/tests/mod.rs") {
3593 "src/bar/tests/mod.rs"
3594 } else {
3595 panic!(
3596 "bar::tests is external; expected src/bar/tests.rs or src/bar/tests/mod.rs. Files: {:?}",
3597 crate_data.files.keys().collect::<Vec<_>>()
3598 );
3599 };
3600 let bar_tests = crate_data.files.get(bar_tests_key).unwrap();
3601 assert!(
3602 bar_tests.source.contains("fn helper"),
3603 "bar::tests file should contain helper fn, got:\n{}",
3604 bar_tests.source
3605 );
3606
3607 let bar_key = if crate_data.files.contains_key("src/bar.rs") {
3610 "src/bar.rs"
3611 } else if crate_data.files.contains_key("src/bar/mod.rs") {
3612 "src/bar/mod.rs"
3613 } else {
3614 panic!(
3615 "bar parent module file missing. Files: {:?}",
3616 crate_data.files.keys().collect::<Vec<_>>()
3617 );
3618 };
3619 let bar_src = &crate_data.files.get(bar_key).unwrap().source;
3620 assert!(
3621 bar_src.contains("mod tests"),
3622 "{} should declare `mod tests`, got:\n{}",
3623 bar_key,
3624 bar_src
3625 );
3626 assert!(
3627 !bar_src.contains("mod tests {"),
3628 "{} must NOT inline tests (external module); got:\n{}",
3629 bar_key,
3630 bar_src
3631 );
3632
3633 let foo_key = if crate_data.files.contains_key("src/foo.rs") {
3635 "src/foo.rs"
3636 } else if crate_data.files.contains_key("src/foo/mod.rs") {
3637 "src/foo/mod.rs"
3638 } else {
3639 panic!(
3640 "foo parent module file missing. Files: {:?}",
3641 crate_data.files.keys().collect::<Vec<_>>()
3642 );
3643 };
3644 let foo_src = &crate_data.files.get(foo_key).unwrap().source;
3645 assert!(
3646 foo_src.contains("mod tests {"),
3647 "{} should inline `mod tests {{ ... }}`, got:\n{}",
3648 foo_key,
3649 foo_src
3650 );
3651
3652 for (path, file) in &crate_data.files {
3654 syn::parse_str::<syn::File>(&file.source)
3655 .unwrap_or_else(|_| panic!("[{}] Should be valid Rust:\n{}", path, file.source));
3656 }
3657 }
3658
3659 #[test]
3661 fn test_external_mod_preserves_attrs() {
3662 let mut ast_registry = ASTRegistry::new();
3663 let mut symbol_registry = SymbolRegistry::new();
3664
3665 symbol_registry
3667 .register(make_path("my_crate"), SymbolKind::Mod)
3668 .unwrap();
3669
3670 let tests_mod = PureMod {
3672 attrs: vec![PureAttribute {
3673 path: "cfg".to_string(),
3674 meta: PureAttrMeta::List("test".to_string()),
3675 is_inner: false, }],
3677 vis: PureVis::Private,
3678 name: "tests".to_string(),
3679 items: vec![], scope: ModScope::Test,
3681 };
3682
3683 let tests_id = symbol_registry
3684 .register(make_path("my_crate::tests"), SymbolKind::Mod)
3685 .unwrap();
3686 ast_registry.set(tests_id, PureItem::Mod(tests_mod));
3687 let helper_id = symbol_registry
3691 .register(make_path("my_crate::tests::TestHelper"), SymbolKind::Struct)
3692 .unwrap();
3693 ast_registry.set(helper_id, make_struct("TestHelper"));
3694
3695 let generator = RegistryGenerator::multi_file();
3697 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
3698
3699 let crate_data = workspace.crates.get("my_crate").unwrap();
3700
3701 let lib = crate_data.files.get("src/lib.rs").unwrap();
3703
3704 assert_eq!(
3706 lib.pure_file.attrs.len(),
3707 0,
3708 "lib.rs should have no file-level attrs, got: {:?}",
3709 lib.pure_file.attrs
3710 );
3711
3712 let tests_mod_item = lib
3714 .pure_file
3715 .items
3716 .iter()
3717 .find_map(|item| {
3718 if let PureItem::Mod(m) = item {
3719 if m.name == "tests" {
3720 return Some(m);
3721 }
3722 }
3723 None
3724 })
3725 .expect("Should have mod tests in lib.rs");
3726
3727 assert!(
3729 tests_mod_item.items.is_empty(),
3730 "External mod should have empty items"
3731 );
3732
3733 assert_eq!(
3735 tests_mod_item.attrs.len(),
3736 1,
3737 "mod tests should have exactly 1 attribute, got: {:?}",
3738 tests_mod_item.attrs
3739 );
3740
3741 let attr = &tests_mod_item.attrs[0];
3743 assert_eq!(attr.path, "cfg", "Attr path should be 'cfg'");
3744 assert!(!attr.is_inner, "Should be outer attribute");
3745 assert!(
3746 matches!(&attr.meta, PureAttrMeta::List(s) if s == "test"),
3747 "Attr meta should be List(\"test\"), got: {:?}",
3748 attr.meta
3749 );
3750
3751 assert!(
3753 crate_data.files.contains_key("src/tests.rs"),
3754 "Should have tests.rs for external module"
3755 );
3756
3757 for (path, file) in &crate_data.files {
3759 syn::parse_str::<syn::File>(&file.source)
3760 .unwrap_or_else(|_| panic!("[{}] Should be valid Rust:\n{}", path, file.source));
3761 }
3762 }
3763
3764 #[test]
3767 fn test_inner_attrs_at_file_level() {
3768 let mut ast_registry = ASTRegistry::new();
3769 let mut symbol_registry = SymbolRegistry::new();
3770
3771 let crate_mod = PureMod {
3773 attrs: vec![
3774 PureAttribute {
3775 path: "allow".to_string(),
3776 meta: PureAttrMeta::List("dead_code".to_string()),
3777 is_inner: true, },
3779 PureAttribute {
3780 path: "warn".to_string(),
3781 meta: PureAttrMeta::List("unused_variables".to_string()),
3782 is_inner: true,
3783 },
3784 ],
3785 vis: PureVis::Public,
3786 name: "my_crate".to_string(),
3787 items: vec![],
3788 scope: Default::default(),
3789 };
3790
3791 let crate_id = symbol_registry
3792 .register(make_path("my_crate"), SymbolKind::Mod)
3793 .unwrap();
3794 ast_registry.set(crate_id, PureItem::Mod(crate_mod));
3795
3796 let config_id = symbol_registry
3798 .register(make_path("my_crate::Config"), SymbolKind::Struct)
3799 .unwrap();
3800 ast_registry.set(config_id, make_struct("Config"));
3801
3802 let generator = RegistryGenerator::single_file();
3804 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
3805
3806 let crate_data = workspace.crates.get("my_crate").unwrap();
3807 let lib = crate_data.files.get("src/lib.rs").unwrap();
3808
3809 assert_eq!(
3811 lib.pure_file.attrs.len(),
3812 2,
3813 "lib.rs should have exactly 2 file-level attrs, got: {:?}",
3814 lib.pure_file.attrs
3815 );
3816
3817 for attr in &lib.pure_file.attrs {
3819 assert!(attr.is_inner, "File-level attr should be inner: {:?}", attr);
3820 }
3821
3822 let attr0 = &lib.pure_file.attrs[0];
3824 assert_eq!(attr0.path, "allow");
3825 assert!(matches!(&attr0.meta, PureAttrMeta::List(s) if s == "dead_code"));
3826
3827 let attr1 = &lib.pure_file.attrs[1];
3829 assert_eq!(attr1.path, "warn");
3830 assert!(matches!(&attr1.meta, PureAttrMeta::List(s) if s == "unused_variables"));
3831
3832 syn::parse_str::<syn::File>(&lib.source)
3834 .unwrap_or_else(|_| panic!("Should be valid Rust:\n{}", lib.source));
3835 }
3836
3837 #[test]
3841 fn test_mixed_outer_inner_attrs_separation() {
3842 let mut ast_registry = ASTRegistry::new();
3843 let mut symbol_registry = SymbolRegistry::new();
3844
3845 symbol_registry
3847 .register(make_path("my_crate"), SymbolKind::Mod)
3848 .unwrap();
3849
3850 let utils_mod = PureMod {
3852 attrs: vec![
3853 PureAttribute {
3855 path: "doc".to_string(),
3856 meta: PureAttrMeta::NameValue("\"Utils module\"".to_string()),
3857 is_inner: false,
3858 },
3859 PureAttribute {
3861 path: "allow".to_string(),
3862 meta: PureAttrMeta::List("dead_code".to_string()),
3863 is_inner: true,
3864 },
3865 PureAttribute {
3867 path: "cfg".to_string(),
3868 meta: PureAttrMeta::List("feature = \"utils\"".to_string()),
3869 is_inner: false,
3870 },
3871 ],
3872 vis: PureVis::Public,
3873 name: "utils".to_string(),
3874 items: vec![], scope: Default::default(),
3876 };
3877
3878 let utils_id = symbol_registry
3879 .register(make_path("my_crate::utils"), SymbolKind::Mod)
3880 .unwrap();
3881 ast_registry.set(utils_id, PureItem::Mod(utils_mod));
3882
3883 let helper_fn_id = symbol_registry
3885 .register(make_path("my_crate::utils::helper"), SymbolKind::Function)
3886 .unwrap();
3887 ast_registry.set(helper_fn_id, make_fn("helper"));
3888
3889 let generator = RegistryGenerator::multi_file();
3891 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
3892
3893 let crate_data = workspace.crates.get("my_crate").unwrap();
3894
3895 let lib = crate_data.files.get("src/lib.rs").unwrap();
3897 let utils_mod_decl = lib
3898 .pure_file
3899 .items
3900 .iter()
3901 .find_map(|item| {
3902 if let PureItem::Mod(m) = item {
3903 if m.name == "utils" {
3904 return Some(m);
3905 }
3906 }
3907 None
3908 })
3909 .expect("Should have mod utils in lib.rs");
3910
3911 assert_eq!(
3913 utils_mod_decl.attrs.len(),
3914 2,
3915 "mod utils declaration should have 2 outer attrs, got: {:?}",
3916 utils_mod_decl.attrs
3917 );
3918
3919 for attr in &utils_mod_decl.attrs {
3921 assert!(
3922 !attr.is_inner,
3923 "mod declaration attrs should be outer: {:?}",
3924 attr
3925 );
3926 }
3927
3928 assert!(
3930 utils_mod_decl.attrs.iter().any(|a| a.path == "doc"),
3931 "Should have doc attr"
3932 );
3933 assert!(
3934 utils_mod_decl.attrs.iter().any(|a| a.path == "cfg"),
3935 "Should have cfg attr"
3936 );
3937
3938 let utils_file = crate_data
3940 .files
3941 .get("src/utils.rs")
3942 .expect("Should have utils.rs");
3943
3944 assert_eq!(
3945 utils_file.pure_file.attrs.len(),
3946 1,
3947 "utils.rs should have 1 file-level attr (inner), got: {:?}",
3948 utils_file.pure_file.attrs
3949 );
3950
3951 let file_attr = &utils_file.pure_file.attrs[0];
3952 assert!(file_attr.is_inner, "File attr should be inner");
3953 assert_eq!(file_attr.path, "allow");
3954 assert!(matches!(&file_attr.meta, PureAttrMeta::List(s) if s == "dead_code"));
3955
3956 for (path, file) in &crate_data.files {
3958 syn::parse_str::<syn::File>(&file.source)
3959 .unwrap_or_else(|_| panic!("[{}] Should be valid Rust:\n{}", path, file.source));
3960 }
3961 }
3962
3963 #[test]
3966 fn test_no_spurious_attrs() {
3967 let mut ast_registry = ASTRegistry::new();
3968 let mut symbol_registry = SymbolRegistry::new();
3969
3970 let crate_mod = PureMod {
3972 attrs: vec![], vis: PureVis::Public,
3974 name: "my_crate".to_string(),
3975 items: vec![],
3976 scope: Default::default(),
3977 };
3978
3979 let crate_id = symbol_registry
3980 .register(make_path("my_crate"), SymbolKind::Mod)
3981 .unwrap();
3982 ast_registry.set(crate_id, PureItem::Mod(crate_mod));
3983
3984 let utils_mod = PureMod {
3986 attrs: vec![], vis: PureVis::Public,
3988 name: "utils".to_string(),
3989 items: vec![],
3990 scope: Default::default(),
3991 };
3992
3993 let utils_id = symbol_registry
3994 .register(make_path("my_crate::utils"), SymbolKind::Mod)
3995 .unwrap();
3996 ast_registry.set(utils_id, PureItem::Mod(utils_mod));
3997
3998 let config_id = symbol_registry
4000 .register(make_path("my_crate::utils::Config"), SymbolKind::Struct)
4001 .unwrap();
4002 ast_registry.set(config_id, make_struct("Config"));
4003
4004 let generator = RegistryGenerator::multi_file();
4006 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
4007
4008 let crate_data = workspace.crates.get("my_crate").unwrap();
4009
4010 let lib = crate_data.files.get("src/lib.rs").unwrap();
4012 assert_eq!(
4013 lib.pure_file.attrs.len(),
4014 0,
4015 "lib.rs should have no attrs, got: {:?}",
4016 lib.pure_file.attrs
4017 );
4018
4019 let utils_mod_decl = lib
4021 .pure_file
4022 .items
4023 .iter()
4024 .find_map(|item| {
4025 if let PureItem::Mod(m) = item {
4026 if m.name == "utils" {
4027 return Some(m);
4028 }
4029 }
4030 None
4031 })
4032 .expect("Should have mod utils");
4033
4034 assert_eq!(
4035 utils_mod_decl.attrs.len(),
4036 0,
4037 "mod utils declaration should have no attrs, got: {:?}",
4038 utils_mod_decl.attrs
4039 );
4040
4041 let utils_file = crate_data.files.get("src/utils.rs").unwrap();
4043 assert_eq!(
4044 utils_file.pure_file.attrs.len(),
4045 0,
4046 "utils.rs should have no attrs, got: {:?}",
4047 utils_file.pure_file.attrs
4048 );
4049 }
4050
4051 #[test]
4060 fn test_external_module_with_items_not_inlined() {
4061 let mut ast_registry = ASTRegistry::new();
4062 let mut symbol_registry = SymbolRegistry::new();
4063
4064 symbol_registry
4066 .register(make_path("my_crate"), SymbolKind::Mod)
4067 .unwrap();
4068
4069 let config_id = symbol_registry
4071 .register(make_path("my_crate::Config"), SymbolKind::Struct)
4072 .unwrap();
4073 ast_registry.set(config_id, make_struct("Config"));
4074
4075 let filter_mod = PureMod {
4079 attrs: vec![],
4080 vis: PureVis::Public,
4081 name: "filter".to_string(),
4082 items: vec![
4083 PureItem::Enum(PureEnum {
4085 attrs: vec![],
4086 vis: PureVis::Public,
4087 name: "Filter".to_string(),
4088 generics: PureGenerics::default(),
4089 variants: vec![
4090 PureVariant {
4091 attrs: vec![],
4092 name: "Identity".to_string(),
4093 fields: PureFields::Unit,
4094 discriminant: None,
4095 },
4096 PureVariant {
4097 attrs: vec![],
4098 name: "Field".to_string(),
4099 fields: PureFields::Tuple(vec![PureTupleField {
4100 attrs: vec![],
4101 vis: PureVis::Private,
4102 ty: PureType::Path("String".to_string()),
4103 }]),
4104 discriminant: None,
4105 },
4106 ],
4107 }),
4108 ],
4109 scope: Default::default(),
4110 };
4111
4112 let filter_id = symbol_registry
4113 .register(make_path("my_crate::filter"), SymbolKind::Mod)
4114 .unwrap();
4115 ast_registry.set(filter_id, PureItem::Mod(filter_mod));
4116 let filter_enum_id = symbol_registry
4121 .register(make_path("my_crate::filter::Filter"), SymbolKind::Enum)
4122 .unwrap();
4123 ast_registry.set(
4124 filter_enum_id,
4125 PureItem::Enum(PureEnum {
4126 attrs: vec![],
4127 vis: PureVis::Public,
4128 name: "Filter".to_string(),
4129 generics: PureGenerics::default(),
4130 variants: vec![
4131 PureVariant {
4132 attrs: vec![],
4133 name: "Identity".to_string(),
4134 fields: PureFields::Unit,
4135 discriminant: None,
4136 },
4137 PureVariant {
4138 attrs: vec![],
4139 name: "Field".to_string(),
4140 fields: PureFields::Tuple(vec![PureTupleField {
4141 attrs: vec![],
4142 vis: PureVis::Private,
4143 ty: PureType::Path("String".to_string()),
4144 }]),
4145 discriminant: None,
4146 },
4147 ],
4148 }),
4149 );
4150
4151 let generator = RegistryGenerator::multi_file();
4153 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
4154
4155 let crate_data = workspace.crates.get("my_crate").unwrap();
4156
4157 assert!(
4161 crate_data.files.contains_key("src/filter.rs"),
4162 "External module 'filter' should have its own file (filter.rs). Got files: {:?}",
4163 crate_data.files.keys().collect::<Vec<_>>()
4164 );
4165
4166 let lib = crate_data.files.get("src/lib.rs").unwrap();
4168 assert!(
4169 lib.source.contains("mod filter;"),
4170 "lib.rs should have 'mod filter;' declaration, got:\n{}",
4171 lib.source
4172 );
4173
4174 assert!(
4176 !lib.source.contains("mod filter {"),
4177 "lib.rs should NOT have inline 'mod filter {{...}}', got:\n{}",
4178 lib.source
4179 );
4180
4181 assert!(
4183 !lib.source.contains("enum Filter"),
4184 "lib.rs should NOT contain Filter enum (should be in filter.rs), got:\n{}",
4185 lib.source
4186 );
4187
4188 let filter_file = crate_data.files.get("src/filter.rs").unwrap();
4190 assert!(
4191 filter_file.source.contains("pub enum Filter"),
4192 "filter.rs should contain 'pub enum Filter', got:\n{}",
4193 filter_file.source
4194 );
4195
4196 for (path, file) in &crate_data.files {
4198 syn::parse_str::<syn::File>(&file.source)
4199 .unwrap_or_else(|_| panic!("[{}] Should be valid Rust:\n{}", path, file.source));
4200 }
4201 }
4202
4203 #[test]
4208 fn test_marked_inline_module_stays_inline() {
4209 use ryo_source::pure::{PureBlock, PureFn};
4210
4211 let mut ast_registry = ASTRegistry::new();
4212 let mut symbol_registry = SymbolRegistry::new();
4213
4214 symbol_registry
4216 .register(make_path("my_crate"), SymbolKind::Mod)
4217 .unwrap();
4218
4219 let config_id = symbol_registry
4221 .register(make_path("my_crate::Config"), SymbolKind::Struct)
4222 .unwrap();
4223 ast_registry.set(config_id, make_struct("Config"));
4224
4225 let tests_mod = PureMod {
4227 attrs: vec![PureAttribute {
4228 path: "cfg".to_string(),
4229 meta: PureAttrMeta::List("test".to_string()),
4230 is_inner: false,
4231 }],
4232 vis: PureVis::Private,
4233 name: "tests".to_string(),
4234 items: vec![PureItem::Fn(PureFn {
4235 attrs: vec![PureAttribute {
4236 path: "test".to_string(),
4237 meta: PureAttrMeta::Path,
4238 is_inner: false,
4239 }],
4240 vis: PureVis::Private,
4241 is_async: false,
4242 is_async_inferred: false,
4243 is_const: false,
4244 is_unsafe: false,
4245 abi: None,
4246 name: "test_something".to_string(),
4247 generics: PureGenerics::default(),
4248 params: vec![],
4249 ret: None,
4250 body: PureBlock::default(),
4251 })],
4252 scope: ModScope::Test,
4253 };
4254
4255 let tests_id = symbol_registry
4256 .register(make_path("my_crate::tests"), SymbolKind::Mod)
4257 .unwrap();
4258 ast_registry.set(tests_id, PureItem::Mod(tests_mod));
4259 ast_registry.mark_inline_module(tests_id);
4261
4262 let generator = RegistryGenerator::multi_file();
4264 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
4265
4266 let crate_data = workspace.crates.get("my_crate").unwrap();
4267
4268 assert!(
4272 !crate_data.files.contains_key("src/tests.rs"),
4273 "Inline module 'tests' should NOT have its own file. Got files: {:?}",
4274 crate_data.files.keys().collect::<Vec<_>>()
4275 );
4276
4277 let lib = crate_data.files.get("src/lib.rs").unwrap();
4279 assert!(
4280 lib.source.contains("mod tests {"),
4281 "lib.rs should have inline 'mod tests {{...}}', got:\n{}",
4282 lib.source
4283 );
4284
4285 assert!(
4287 lib.source.contains("fn test_something"),
4288 "lib.rs should contain test_something function, got:\n{}",
4289 lib.source
4290 );
4291
4292 assert!(
4294 lib.source.contains("#[cfg(test)]"),
4295 "lib.rs should have #[cfg(test)] attribute, got:\n{}",
4296 lib.source
4297 );
4298
4299 for (path, file) in &crate_data.files {
4301 syn::parse_str::<syn::File>(&file.source)
4302 .unwrap_or_else(|_| panic!("[{}] Should be valid Rust:\n{}", path, file.source));
4303 }
4304 }
4305
4306 #[test]
4314 fn test_external_module_visibility_preserved() {
4315 let mut ast_registry = ASTRegistry::new();
4316 let mut symbol_registry = SymbolRegistry::new();
4317
4318 let crate_id = symbol_registry
4320 .register(make_path("my_crate"), SymbolKind::Mod)
4321 .unwrap();
4322 ast_registry.set(
4323 crate_id,
4324 PureItem::Mod(PureMod {
4325 attrs: vec![],
4326 vis: PureVis::Public,
4327 name: "my_crate".to_string(),
4328 items: vec![],
4329 scope: Default::default(),
4330 }),
4331 );
4332
4333 let error_mod = PureMod {
4336 attrs: vec![],
4337 vis: PureVis::Private, name: "error".to_string(),
4339 items: vec![
4340 PureItem::Enum(PureEnum {
4342 attrs: vec![],
4343 vis: PureVis::Public, name: "Error".to_string(),
4345 generics: PureGenerics::default(),
4346 variants: vec![PureVariant {
4347 attrs: vec![],
4348 name: "IoError".to_string(),
4349 fields: PureFields::Unit,
4350 discriminant: None,
4351 }],
4352 }),
4353 ],
4354 scope: Default::default(),
4355 };
4356
4357 let error_mod_id = symbol_registry
4358 .register(make_path("my_crate::error"), SymbolKind::Mod)
4359 .unwrap();
4360 ast_registry.set(error_mod_id, PureItem::Mod(error_mod));
4361 let _ = symbol_registry.set_visibility(error_mod_id, ryo_symbol::Visibility::Private);
4363 let error_enum_id = symbol_registry
4367 .register(make_path("my_crate::error::Error"), SymbolKind::Enum)
4368 .unwrap();
4369 ast_registry.set(
4370 error_enum_id,
4371 PureItem::Enum(PureEnum {
4372 attrs: vec![],
4373 vis: PureVis::Public,
4374 name: "Error".to_string(),
4375 generics: PureGenerics::default(),
4376 variants: vec![PureVariant {
4377 attrs: vec![],
4378 name: "IoError".to_string(),
4379 fields: PureFields::Unit,
4380 discriminant: None,
4381 }],
4382 }),
4383 );
4384
4385 let generator = RegistryGenerator::multi_file();
4387 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
4388
4389 let crate_data = workspace.crates.get("my_crate").unwrap();
4390
4391 let lib = crate_data.files.get("src/lib.rs").unwrap();
4393
4394 let error_mod_decl = lib
4396 .pure_file
4397 .items
4398 .iter()
4399 .find_map(|item| {
4400 if let PureItem::Mod(m) = item {
4401 if m.name == "error" {
4402 return Some(m);
4403 }
4404 }
4405 None
4406 })
4407 .expect("Should have mod error in lib.rs");
4408
4409 assert!(
4411 matches!(error_mod_decl.vis, PureVis::Private),
4412 "mod error should be PRIVATE (original visibility), but got: {:?}\nlib.rs:\n{}",
4413 error_mod_decl.vis,
4414 lib.source
4415 );
4416
4417 assert!(
4419 lib.source.contains("mod error;") && !lib.source.contains("pub mod error;"),
4420 "lib.rs should have 'mod error;' (private), not 'pub mod error;'\nGot:\n{}",
4421 lib.source
4422 );
4423 }
4424
4425 #[test]
4432 fn test_external_module_visibility_from_pure_mod() {
4433 let mut ast_registry = ASTRegistry::new();
4434 let mut symbol_registry = SymbolRegistry::new();
4435
4436 let crate_id = symbol_registry
4438 .register(make_path("my_crate"), SymbolKind::Mod)
4439 .unwrap();
4440 ast_registry.set(
4441 crate_id,
4442 PureItem::Mod(PureMod {
4443 attrs: vec![],
4444 vis: PureVis::Public,
4445 name: "my_crate".to_string(),
4446 items: vec![],
4447 scope: Default::default(),
4448 }),
4449 );
4450
4451 let error_mod = PureMod {
4454 attrs: vec![],
4455 vis: PureVis::Private, name: "error".to_string(),
4457 items: vec![
4458 PureItem::Enum(PureEnum {
4460 attrs: vec![],
4461 vis: PureVis::Public, name: "Error".to_string(),
4463 generics: PureGenerics::default(),
4464 variants: vec![PureVariant {
4465 attrs: vec![],
4466 name: "IoError".to_string(),
4467 fields: PureFields::Unit,
4468 discriminant: None,
4469 }],
4470 }),
4471 ],
4472 scope: Default::default(),
4473 };
4474
4475 let error_mod_id = symbol_registry
4476 .register(make_path("my_crate::error"), SymbolKind::Mod)
4477 .unwrap();
4478 ast_registry.set(error_mod_id, PureItem::Mod(error_mod));
4479 let error_enum_id = symbol_registry
4484 .register(make_path("my_crate::error::Error"), SymbolKind::Enum)
4485 .unwrap();
4486 ast_registry.set(
4487 error_enum_id,
4488 PureItem::Enum(PureEnum {
4489 attrs: vec![],
4490 vis: PureVis::Public,
4491 name: "Error".to_string(),
4492 generics: PureGenerics::default(),
4493 variants: vec![PureVariant {
4494 attrs: vec![],
4495 name: "IoError".to_string(),
4496 fields: PureFields::Unit,
4497 discriminant: None,
4498 }],
4499 }),
4500 );
4501
4502 let generator = RegistryGenerator::multi_file();
4504 let workspace = generator.generate(&ast_registry, &symbol_registry).unwrap();
4505
4506 let crate_data = workspace.crates.get("my_crate").unwrap();
4507
4508 let lib = crate_data.files.get("src/lib.rs").unwrap();
4510
4511 let error_mod_decl = lib
4513 .pure_file
4514 .items
4515 .iter()
4516 .find_map(|item| {
4517 if let PureItem::Mod(m) = item {
4518 if m.name == "error" {
4519 return Some(m);
4520 }
4521 }
4522 None
4523 })
4524 .expect("Should have mod error in lib.rs");
4525
4526 assert!(
4528 matches!(error_mod_decl.vis, PureVis::Private),
4529 "mod error should be PRIVATE (from PureMod.vis), but got: {:?}\n\
4530 This indicates visibility is being inferred from child items instead of PureMod.vis\n\
4531 lib.rs:\n{}",
4532 error_mod_decl.vis,
4533 lib.source
4534 );
4535 }
4536
4537 #[test]
4544 fn test_external_module_visibility_with_graph() {
4545 use ryo_analysis::{CodeEdgeV2, CodeGraphV2};
4546
4547 let mut ast_registry = ASTRegistry::new();
4548 let mut symbol_registry = SymbolRegistry::new();
4549 let mut code_graph = CodeGraphV2::new();
4550
4551 let crate_id = symbol_registry
4553 .register(make_path("my_crate"), SymbolKind::Mod)
4554 .unwrap();
4555 ast_registry.set(
4556 crate_id,
4557 PureItem::Mod(PureMod {
4558 attrs: vec![],
4559 vis: PureVis::Public,
4560 name: "my_crate".to_string(),
4561 items: vec![],
4562 scope: Default::default(),
4563 }),
4564 );
4565 code_graph.add_node(crate_id);
4566 code_graph.add_crate_root(crate_id);
4567
4568 let error_mod = PureMod {
4570 attrs: vec![],
4571 vis: PureVis::Private, name: "error".to_string(),
4573 items: vec![], scope: Default::default(),
4575 };
4576
4577 let error_mod_id = symbol_registry
4578 .register(make_path("my_crate::error"), SymbolKind::Mod)
4579 .unwrap();
4580 ast_registry.set(error_mod_id, PureItem::Mod(error_mod));
4581 code_graph.add_node(error_mod_id);
4582 code_graph.add_edge(crate_id, error_mod_id, CodeEdgeV2::Contains);
4583
4584 let error_enum_id = symbol_registry
4586 .register(make_path("my_crate::error::Error"), SymbolKind::Enum)
4587 .unwrap();
4588 ast_registry.set(
4589 error_enum_id,
4590 PureItem::Enum(PureEnum {
4591 attrs: vec![],
4592 vis: PureVis::Public, name: "Error".to_string(),
4594 generics: PureGenerics::default(),
4595 variants: vec![PureVariant {
4596 attrs: vec![],
4597 name: "IoError".to_string(),
4598 fields: PureFields::Unit,
4599 discriminant: None,
4600 }],
4601 }),
4602 );
4603 code_graph.add_node(error_enum_id);
4604 code_graph.add_edge(error_mod_id, error_enum_id, CodeEdgeV2::Contains);
4605
4606 let generator = RegistryGenerator::multi_file();
4608 let workspace = generator
4609 .generate_with_graph(&code_graph, &ast_registry, &symbol_registry)
4610 .unwrap();
4611
4612 let crate_data = workspace.crates.get("my_crate").unwrap();
4613 let lib = crate_data.files.get("src/lib.rs").unwrap();
4614
4615 let error_mod_decl = lib
4617 .pure_file
4618 .items
4619 .iter()
4620 .find_map(|item| {
4621 if let PureItem::Mod(m) = item {
4622 if m.name == "error" {
4623 return Some(m);
4624 }
4625 }
4626 None
4627 })
4628 .expect("Should have mod error in lib.rs");
4629
4630 assert!(
4632 matches!(error_mod_decl.vis, PureVis::Private),
4633 "mod error should be PRIVATE (from PureMod.vis), but got: {:?}\n\
4634 Bug: generate_file_from_graph is inferring visibility from children instead of PureMod.vis\n\
4635 lib.rs:\n{}",
4636 error_mod_decl.vis,
4637 lib.source
4638 );
4639 }
4640
4641 #[test]
4646 fn test_symbol_path_to_file_key_root_items() {
4647 let gen = RegistryGenerator::multi_file();
4648
4649 let path = SymbolPath::parse("my_crate::Config").unwrap();
4651 assert_eq!(gen.symbol_path_to_file_key(&path), "src/lib.rs");
4652
4653 let path = SymbolPath::parse("my_crate").unwrap();
4655 assert_eq!(gen.symbol_path_to_file_key(&path), "src/lib.rs");
4656 }
4657
4658 #[test]
4659 fn test_symbol_path_to_file_key_module_items() {
4660 let gen = RegistryGenerator::multi_file();
4661
4662 let path = SymbolPath::parse("my_crate::models::User").unwrap();
4664 assert_eq!(gen.symbol_path_to_file_key(&path), "src/models.rs");
4665 }
4666
4667 #[test]
4668 fn test_symbol_path_to_file_key_nested_modules() {
4669 let gen = RegistryGenerator::multi_file();
4670
4671 let path = SymbolPath::parse("my_crate::models::sub::Foo").unwrap();
4673 assert_eq!(gen.symbol_path_to_file_key(&path), "src/models/sub.rs");
4674 }
4675
4676 #[test]
4677 fn test_symbol_path_to_file_key_main_symbol() {
4678 let gen = RegistryGenerator::multi_file();
4679
4680 let path = SymbolPath::parse("main::my_app::Config").unwrap();
4682 assert_eq!(gen.symbol_path_to_file_key(&path), "src/main.rs");
4683
4684 let path = SymbolPath::parse("main::my_app::cli::Args").unwrap();
4686 assert_eq!(gen.symbol_path_to_file_key(&path), "src/cli.rs");
4687 }
4688
4689 #[test]
4690 fn test_generate_affected_empty_symbols_returns_empty() {
4691 let ast_registry = ASTRegistry::new();
4692 let symbol_registry = SymbolRegistry::new();
4693 let gen = RegistryGenerator::multi_file();
4694
4695 let workspace = gen
4697 .generate_internal(&ast_registry, &symbol_registry, Some(&HashSet::new()), None)
4698 .unwrap();
4699 assert_eq!(workspace.total_files(), 0);
4700 }
4701
4702 #[test]
4703 fn test_generate_affected_only_affected_files() {
4704 let mut ast_registry = ASTRegistry::new();
4706 let mut symbol_registry = SymbolRegistry::new();
4707
4708 let root_id = symbol_registry
4710 .register(SymbolPath::parse("test_crate").unwrap(), SymbolKind::Mod)
4711 .unwrap();
4712
4713 let models_mod_id = symbol_registry
4715 .register(
4716 SymbolPath::parse("test_crate::models").unwrap(),
4717 SymbolKind::Mod,
4718 )
4719 .unwrap();
4720 let user_id = symbol_registry
4721 .register(
4722 SymbolPath::parse("test_crate::models::User").unwrap(),
4723 SymbolKind::Struct,
4724 )
4725 .unwrap();
4726
4727 let handlers_mod_id = symbol_registry
4729 .register(
4730 SymbolPath::parse("test_crate::handlers").unwrap(),
4731 SymbolKind::Mod,
4732 )
4733 .unwrap();
4734 let process_id = symbol_registry
4735 .register(
4736 SymbolPath::parse("test_crate::handlers::process").unwrap(),
4737 SymbolKind::Function,
4738 )
4739 .unwrap();
4740
4741 ast_registry.set(root_id, make_mod("test_crate"));
4743 ast_registry.set(models_mod_id, make_mod("models"));
4744 ast_registry.set(user_id, make_struct("User"));
4745 ast_registry.set(handlers_mod_id, make_mod("handlers"));
4746 ast_registry.set(process_id, make_fn("process"));
4747
4748 let gen = RegistryGenerator::multi_file();
4749
4750 let full = gen.generate(&ast_registry, &symbol_registry).unwrap();
4752 let full_crate = full.crates.get("test_crate").unwrap();
4753 assert!(
4754 full_crate.files.len() >= 3,
4755 "Full gen should have lib.rs, models.rs, handlers.rs but got: {:?}",
4756 full_crate.files.keys().collect::<Vec<_>>()
4757 );
4758
4759 let workspace = gen
4761 .generate_internal(
4762 &ast_registry,
4763 &symbol_registry,
4764 Some(&HashSet::from(["src/models.rs".to_string()])),
4765 None,
4766 )
4767 .unwrap();
4768 let affected_crate = workspace.crates.get("test_crate").unwrap();
4769 assert_eq!(
4770 affected_crate.files.len(),
4771 1,
4772 "Affected gen should only have models.rs but got: {:?}",
4773 affected_crate.files.keys().collect::<Vec<_>>()
4774 );
4775 assert!(affected_crate.files.contains_key("src/models.rs"));
4776 assert!(!affected_crate.files.contains_key("src/lib.rs"));
4777 assert!(!affected_crate.files.contains_key("src/handlers.rs"));
4778 }
4779
4780 fn make_mod(name: &str) -> PureItem {
4781 PureItem::Mod(PureMod {
4782 attrs: vec![],
4783 vis: PureVis::Public,
4784 name: name.to_string(),
4785 items: vec![],
4786 scope: Default::default(),
4787 })
4788 }
4789
4790 #[test]
4796 fn test_generate_affected_no_duplicate_mod_rs_when_flat_exists() {
4797 let mut ast_registry = ASTRegistry::new();
4798 let mut symbol_registry = SymbolRegistry::new();
4799
4800 let root_id = symbol_registry
4801 .register(SymbolPath::parse("ryo_app").unwrap(), SymbolKind::Mod)
4802 .unwrap();
4803 let storage_mod_id = symbol_registry
4804 .register(
4805 SymbolPath::parse("ryo_app::storage").unwrap(),
4806 SymbolKind::Mod,
4807 )
4808 .unwrap();
4809 let item_id = symbol_registry
4810 .register(
4811 SymbolPath::parse("ryo_app::storage::NewItem").unwrap(),
4812 SymbolKind::Struct,
4813 )
4814 .unwrap();
4815
4816 ast_registry.set(root_id, make_mod("ryo_app"));
4817 ast_registry.set(storage_mod_id, make_mod("storage"));
4818 ast_registry.set(item_id, make_struct("NewItem"));
4819
4820 let gen =
4822 RegistryGenerator::multi_file().with_existing_files(vec!["src/storage.rs".to_string()]);
4823
4824 let modified = [storage_mod_id, item_id];
4829
4830 let mut affected_file_keys: std::collections::HashSet<String> = modified
4832 .iter()
4833 .filter_map(|&id| {
4834 let path = symbol_registry.path(id)?;
4835 let resolved = RegistryGenerator::resolve_to_file_level_symbol(
4836 path,
4837 &symbol_registry,
4838 &ast_registry,
4839 );
4840 Some(gen.symbol_path_to_file_key(&resolved))
4841 })
4842 .collect();
4843
4844 for &id in &modified {
4846 if symbol_registry.kind(id) != Some(SymbolKind::Mod) {
4847 continue;
4848 }
4849 if ast_registry.is_inline_module(id) {
4850 continue;
4851 }
4852 if let Some(path) = symbol_registry.path(id) {
4853 let parent_key = gen.symbol_path_to_file_key(path);
4854 let crate_name_opt = CrateName::new(path.crate_name()).ok();
4855 if let Some(mod_name) = path.segments().last() {
4856 let mod_file_key =
4857 gen.derive_child_path(&parent_key, mod_name, crate_name_opt.as_ref());
4858 affected_file_keys.insert(mod_file_key);
4859 }
4860 }
4861 }
4862
4863 assert!(
4865 !affected_file_keys.contains("src/storage/mod.rs"),
4866 "Must not emit src/storage/mod.rs when flat src/storage.rs already exists. \
4867 Got affected_file_keys = {:?}",
4868 affected_file_keys
4869 );
4870 assert!(
4871 affected_file_keys.contains("src/storage.rs"),
4872 "Must emit src/storage.rs. Got = {:?}",
4873 affected_file_keys
4874 );
4875
4876 let workspace = gen
4878 .generate_internal(
4879 &ast_registry,
4880 &symbol_registry,
4881 Some(&affected_file_keys),
4882 None,
4883 )
4884 .unwrap();
4885 let crate_data = workspace.crates.get("ryo_app").unwrap();
4886 assert!(
4887 !crate_data.files.contains_key("src/storage/mod.rs"),
4888 "Generated workspace must not contain src/storage/mod.rs. Files = {:?}",
4889 crate_data.files.keys().collect::<Vec<_>>()
4890 );
4891 }
4892}