1use crate::{Exploded, SplitPlan, plan_split};
29use syn::spanned::Spanned;
30
31const PART_OVERHEAD: usize = 3;
34
35pub fn split_bin(exploded: &Exploded, max_loc: usize, root_stem: &str) -> SplitOutput {
41 let header_loc: usize = exploded
42 .manifest
43 .rows
44 .iter()
45 .filter(|r| matches!(r.kind.as_str(), "use" | "preamble" | "extern_crate"))
46 .map(|r| r.loc)
47 .sum();
48 let item_budget = max_loc.saturating_sub(header_loc + PART_OVERHEAD).max(1);
49 let plan = plan_split(exploded, item_budget);
50 reassemble_bin(exploded, &plan, root_stem, max_loc)
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct OutputFile {
56 pub path: String,
58 pub contents: String,
59 pub loc: usize,
60}
61
62#[derive(Debug, Clone)]
64pub struct SplitOutput {
65 pub files: Vec<OutputFile>,
66 pub still_oversized: Vec<String>,
68}
69
70impl SplitOutput {
71 pub fn max_loc(&self) -> usize {
74 self.files.iter().map(|f| f.loc).max().unwrap_or(0)
75 }
76}
77
78fn loc(text: &str) -> usize {
79 if text.is_empty() {
80 0
81 } else {
82 text.lines().count().max(1)
83 }
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub enum Topology {
92 Bin,
95 Mod,
101}
102
103pub fn split_mod(exploded: &Exploded, max_loc: usize, root_stem: &str) -> SplitOutput {
108 let header_loc: usize = exploded
109 .manifest
110 .rows
111 .iter()
112 .filter(|r| matches!(r.kind.as_str(), "use" | "preamble" | "extern_crate"))
113 .map(|r| r.loc)
114 .sum();
115 let item_budget = max_loc.saturating_sub(header_loc + PART_OVERHEAD).max(1);
116 let plan = plan_split(exploded, item_budget);
117 reassemble(exploded, &plan, root_stem, Topology::Mod, max_loc)
118}
119
120pub fn reassemble_bin(
122 exploded: &Exploded,
123 plan: &SplitPlan,
124 root_stem: &str,
125 max_loc: usize,
126) -> SplitOutput {
127 reassemble(exploded, plan, root_stem, Topology::Bin, max_loc)
128}
129
130fn reassemble(
134 exploded: &Exploded,
135 plan: &SplitPlan,
136 root_stem: &str,
137 topology: Topology,
138 max_loc: usize,
139) -> SplitOutput {
140 let sibling = match topology {
141 Topology::Bin => "crate",
142 Topology::Mod => "super",
143 };
144 let chunk_text = |i: usize| exploded.chunks[i].text.as_str();
145 let row = |i: usize| {
146 exploded
147 .manifest
148 .rows
149 .iter()
150 .find(|r| r.chunk_index == i)
151 .unwrap()
152 };
153
154 let sub_prefix = match topology {
158 Topology::Bin => String::new(),
159 Topology::Mod if matches!(root_stem, "mod" | "lib" | "main") => String::new(),
160 Topology::Mod => format!("{root_stem}/"),
161 };
162
163 let preamble: String = plan
171 .header
172 .iter()
173 .filter(|&&i| row(i).kind == "preamble")
174 .map(|&i| chunk_text(i))
175 .collect();
176 let import_chunks: Vec<usize> = plan
177 .header
178 .iter()
179 .copied()
180 .filter(|&i| row(i).kind != "preamble")
181 .collect();
182
183 let root_kept: String = plan.root_items.iter().map(|&i| chunk_text(i)).collect();
187
188 let mut reserved: std::collections::BTreeSet<&str> =
192 plan.mods.iter().map(|&i| row(i).name.as_str()).collect();
193 reserved.insert(root_stem);
194 for &i in &plan.root_items {
195 if row(i).kind == "mod" {
196 reserved.insert(row(i).name.as_str());
197 }
198 }
199
200 let mut files = Vec::new();
201 let mut mod_decls = String::new();
202 let mut reexports = String::new();
203 let mut still_oversized = Vec::new();
204 let mut root_refs = Refs::default();
207
208 for part in &plan.parts {
210 let module = if reserved.contains(part.name.as_str()) {
211 format!("{}_", part.name)
212 } else {
213 part.name.clone()
214 };
215 let mut body = String::new();
216 let mut root_main = None;
217 let mut export_vis = ExportVis::None;
219 let mut refs = Refs::default();
221 for &i in &part.chunk_indices {
222 let r = row(i);
223 if r.kind == "fn" && r.name == "main" {
225 root_main = Some(i);
226 continue;
227 }
228 export_vis = export_vis.max(item_export_vis(chunk_text(i)));
229 refs.add(chunk_text(i));
230 body.push_str(&bump_visibility(chunk_text(i)));
231 }
232 if body.trim().is_empty() {
234 if let Some(i) = root_main {
235 files.push(OutputFile {
237 path: String::from("__root_main__"),
238 contents: chunk_text(i).to_owned(),
239 loc: 0,
240 });
241 }
242 continue;
243 }
244 if let Some(i) = root_main {
245 files.push(OutputFile {
246 path: String::from("__root_main__"),
247 contents: chunk_text(i).to_owned(),
248 loc: 0,
249 });
250 }
251 let part_imports = select_imports(&import_chunks, &exploded.chunks, &refs);
252 let contents = part_file(&part_imports, sibling, &body);
253 mod_decls.push_str(&format!("mod {module};\n"));
254 if let Some(line) = reexport_line(export_vis, &module) {
262 reexports.push_str(&line);
263 }
264 let l = loc(&contents);
265 files.push(OutputFile {
266 path: format!("{sub_prefix}{module}.rs"),
267 contents,
268 loc: l,
269 });
270 }
271
272 for &mod_index in &plan.mods {
279 let chunk = chunk_text(mod_index);
280 let name = row(mod_index).name.as_str();
281 let Some((decl, inner)) = extract_mod(chunk) else {
282 mod_decls.push_str(chunk);
285 continue;
286 };
287 mod_decls.push_str(&decl);
288 root_refs.add_source(&inner);
291 let inner_loc = loc(&inner);
292 if inner_loc < max_loc {
293 files.push(OutputFile {
294 path: format!("{sub_prefix}{name}.rs"),
295 contents: inner,
296 loc: inner_loc,
297 });
298 } else {
299 let (nested, nested_still) = split_nested_mod(name, &inner, max_loc);
300 files.extend(nested.into_iter().map(|mut f| {
301 f.path = format!("{sub_prefix}{}", f.path);
302 f
303 }));
304 still_oversized.extend(nested_still);
305 }
306 }
307
308 for over in &plan.oversized {
310 still_oversized.push(format!(
311 "{} ({} LOC {}, manual extraction)",
312 over.name, over.loc, over.kind
313 ));
314 }
315
316 let mut root_main = String::new();
318 files.retain(|f| {
319 if f.path == "__root_main__" {
320 root_main = f.contents.clone();
321 false
322 } else {
323 true
324 }
325 });
326
327 if !root_main.trim().is_empty() {
332 root_refs.add(&root_main);
333 }
334 let root_imports = if root_refs.is_empty() {
335 String::new()
336 } else {
337 select_imports(&import_chunks, &exploded.chunks, &root_refs)
338 };
339
340 let mut root = String::new();
341 root.push_str(&preamble);
342 root.push_str(&root_imports);
343 root.push_str(&root_kept);
344 root.push('\n');
345 root.push_str(&mod_decls);
346 root.push('\n');
347 root.push_str(&reexports);
348 if !root_main.is_empty() {
349 root.push('\n');
350 root.push_str(&root_main);
351 }
352 let root_loc = loc(&root);
353 files.insert(
354 0,
355 OutputFile {
356 path: format!("{root_stem}.rs"),
357 contents: root,
358 loc: root_loc,
359 },
360 );
361
362 SplitOutput {
363 files,
364 still_oversized,
365 }
366}
367
368fn bump_visibility(chunk_text: &str) -> String {
375 let Ok(item) = syn::parse_str::<syn::Item>(chunk_text) else {
376 return chunk_text.to_owned();
377 };
378 let mut offsets = Vec::new();
379 collect_vis_offsets(&item, &mut offsets);
380 apply_inserts(chunk_text, &offsets)
381}
382
383fn is_inherited(vis: &syn::Visibility) -> bool {
384 matches!(vis, syn::Visibility::Inherited)
385}
386
387fn start<T: Spanned>(node: &T) -> usize {
388 node.span().byte_range().start
389}
390
391fn fn_sig_start(sig: &syn::Signature) -> usize {
394 let mut min = start(&sig.fn_token);
395 if let Some(t) = &sig.constness {
396 min = min.min(start(t));
397 }
398 if let Some(t) = &sig.asyncness {
399 min = min.min(start(t));
400 }
401 if let Some(t) = &sig.unsafety {
402 min = min.min(start(t));
403 }
404 if let Some(abi) = &sig.abi {
405 min = min.min(start(&abi.extern_token));
406 }
407 min
408}
409
410fn field_offsets(fields: &syn::Fields, offsets: &mut Vec<usize>) {
411 for field in fields {
412 if is_inherited(&field.vis) {
413 match &field.ident {
414 Some(ident) => offsets.push(start(ident)),
415 None => offsets.push(start(&field.ty)),
416 }
417 }
418 }
419}
420
421fn collect_vis_offsets(item: &syn::Item, offsets: &mut Vec<usize>) {
422 use syn::Item;
423 match item {
424 Item::Fn(f) => {
425 if is_inherited(&f.vis) {
426 offsets.push(fn_sig_start(&f.sig));
427 }
428 }
429 Item::Struct(s) => {
430 if is_inherited(&s.vis) {
431 offsets.push(start(&s.struct_token));
432 }
433 field_offsets(&s.fields, offsets);
434 }
435 Item::Enum(e) => {
436 if is_inherited(&e.vis) {
437 offsets.push(start(&e.enum_token));
438 }
439 }
440 Item::Union(u) => {
441 if is_inherited(&u.vis) {
442 offsets.push(start(&u.union_token));
443 }
444 field_offsets(&syn::Fields::Named(u.fields.clone()), offsets);
445 }
446 Item::Const(c) if is_inherited(&c.vis) => offsets.push(start(&c.const_token)),
447 Item::Static(s) if is_inherited(&s.vis) => offsets.push(start(&s.static_token)),
448 Item::Type(t) if is_inherited(&t.vis) => offsets.push(start(&t.type_token)),
449 Item::Trait(t) if is_inherited(&t.vis) => offsets.push(start(&t.trait_token)),
450 Item::TraitAlias(t) if is_inherited(&t.vis) => offsets.push(start(&t.trait_token)),
451 Item::Impl(i) if i.trait_.is_none() => {
454 for member in &i.items {
455 match member {
456 syn::ImplItem::Fn(f) if is_inherited(&f.vis) => {
457 offsets.push(fn_sig_start(&f.sig))
458 }
459 syn::ImplItem::Const(c) if is_inherited(&c.vis) => {
460 offsets.push(start(&c.const_token))
461 }
462 syn::ImplItem::Type(t) if is_inherited(&t.vis) => {
463 offsets.push(start(&t.type_token))
464 }
465 _ => {}
466 }
467 }
468 }
469 _ => {}
470 }
471}
472
473fn apply_inserts(text: &str, offsets: &[usize]) -> String {
474 let mut offsets: Vec<usize> = offsets.to_vec();
475 offsets.sort_unstable();
476 offsets.dedup();
477 let mut out = String::with_capacity(text.len() + offsets.len() * 11);
478 let mut prev = 0;
479 for &offset in &offsets {
480 out.push_str(&text[prev..offset]);
481 out.push_str("pub(crate) ");
482 prev = offset;
483 }
484 out.push_str(&text[prev..]);
485 out
486}
487
488fn part_file(imports: &str, sibling: &str, body: &str) -> String {
491 if imports.trim().is_empty() {
492 format!("use {sibling}::*;\n\n{body}")
493 } else {
494 format!("{imports}\nuse {sibling}::*;\n\n{body}")
495 }
496}
497
498fn reexport_line(vis: ExportVis, module: &str) -> Option<String> {
502 match vis {
503 ExportVis::None => None,
504 ExportVis::Crate => Some(format!("pub(crate) use {module}::*;\n")),
505 ExportVis::Pub => Some(format!("pub use {module}::*;\n")),
506 }
507}
508
509#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
512enum ExportVis {
513 None,
515 Crate,
518 Pub,
520}
521
522fn item_export_vis(chunk_text: &str) -> ExportVis {
526 use syn::{Item, Visibility};
527 let Ok(item) = syn::parse_str::<syn::Item>(chunk_text) else {
528 return ExportVis::None;
531 };
532 let (vis, bumped) = match &item {
533 Item::Fn(f) => (&f.vis, true),
534 Item::Struct(s) => (&s.vis, true),
535 Item::Enum(e) => (&e.vis, true),
536 Item::Const(c) => (&c.vis, true),
537 Item::Static(s) => (&s.vis, true),
538 Item::Trait(t) => (&t.vis, true),
539 Item::TraitAlias(t) => (&t.vis, true),
540 Item::Type(t) => (&t.vis, true),
541 Item::Union(u) => (&u.vis, true),
542 Item::Mod(m) => (&m.vis, false),
543 Item::Use(u) => (&u.vis, false),
544 _ => return ExportVis::None,
546 };
547 match vis {
548 Visibility::Public(_) => ExportVis::Pub,
549 Visibility::Restricted(_) => ExportVis::Crate,
550 Visibility::Inherited if bumped => ExportVis::Crate,
551 Visibility::Inherited => ExportVis::None,
552 }
553}
554
555#[derive(Default)]
559struct Refs {
560 names: std::collections::BTreeSet<String>,
561 keep_all: bool,
562}
563
564impl Refs {
565 fn add(&mut self, chunk_text: &str) {
566 match crate::referenced_idents(chunk_text) {
567 Some(names) => self.names.extend(names),
568 None => self.keep_all = true,
569 }
570 }
571
572 fn add_source(&mut self, src: &str) {
575 match crate::referenced_idents_in_source(src) {
576 Some(names) => self.names.extend(names),
577 None => self.keep_all = true,
578 }
579 }
580
581 fn is_empty(&self) -> bool {
583 self.names.is_empty() && !self.keep_all
584 }
585
586 fn keeps(&self, provided: &Option<std::collections::BTreeSet<String>>) -> bool {
590 match provided {
591 None => true,
592 Some(names) => self.keep_all || names.iter().any(|n| self.names.contains(n)),
593 }
594 }
595}
596
597fn select_imports(import_chunks: &[usize], chunks: &[crate::Chunk], refs: &Refs) -> String {
603 let mut out = String::new();
604 for &i in import_chunks {
605 let text = chunks[i].text.as_str();
606 if refs.keeps(&import_provided_names(text)) {
607 out.push_str(text);
608 }
609 }
610 out
611}
612
613fn import_provided_names(chunk_text: &str) -> Option<std::collections::BTreeSet<String>> {
617 match syn::parse_str::<syn::Item>(chunk_text).ok()? {
618 syn::Item::Use(u) => {
619 let mut names = std::collections::BTreeSet::new();
620 let mut name_less = false;
621 collect_use_names(&u.tree, &mut names, &mut name_less);
622 if name_less { None } else { Some(names) }
623 }
624 syn::Item::ExternCrate(c) => {
625 let name = c
626 .rename
627 .map(|(_, id)| id.to_string())
628 .unwrap_or_else(|| c.ident.to_string());
629 Some(std::collections::BTreeSet::from([name]))
630 }
631 _ => None,
633 }
634}
635
636fn collect_use_names(
639 tree: &syn::UseTree,
640 names: &mut std::collections::BTreeSet<String>,
641 name_less: &mut bool,
642) {
643 match tree {
644 syn::UseTree::Path(p) => collect_use_names(&p.tree, names, name_less),
645 syn::UseTree::Name(n) => {
646 names.insert(n.ident.to_string());
647 }
648 syn::UseTree::Rename(r) => {
649 if r.rename == "_" {
650 *name_less = true;
651 } else {
652 names.insert(r.rename.to_string());
653 }
654 }
655 syn::UseTree::Glob(_) => *name_less = true,
656 syn::UseTree::Group(g) => {
657 for item in &g.items {
658 collect_use_names(item, names, name_less);
659 }
660 }
661 }
662}
663
664fn extract_mod(chunk_text: &str) -> Option<(String, String)> {
672 let Ok(syn::Item::Mod(item)) = syn::parse_str::<syn::Item>(chunk_text) else {
673 return None;
674 };
675 let (brace, _) = item.content.as_ref()?;
676 let ident_end = item.ident.span().byte_range().end;
677 let decl = format!("{};\n", &chunk_text[..ident_end]);
678
679 let open_end = brace.span.open().byte_range().end;
680 let close_start = brace.span.close().byte_range().start;
681 let inner = &chunk_text[open_end..close_start];
682 let inner = inner.strip_prefix('\n').unwrap_or(inner);
684 let mut inner = inner.trim_end_matches([' ', '\t']).to_owned();
685 if !inner.is_empty() && !inner.ends_with('\n') {
686 inner.push('\n');
687 }
688 Some((decl, inner))
689}
690
691fn split_nested_mod(name: &str, inner: &str, max_loc: usize) -> (Vec<OutputFile>, Vec<String>) {
700 let Ok(exploded) = crate::explode(inner) else {
701 return (
702 vec![OutputFile {
703 path: format!("{name}.rs"),
704 contents: inner.to_owned(),
705 loc: loc(inner),
706 }],
707 vec![format!("{name} (could not parse for nested split)")],
708 );
709 };
710 let row = |i: usize| {
711 exploded
712 .manifest
713 .rows
714 .iter()
715 .find(|r| r.chunk_index == i)
716 .unwrap()
717 };
718
719 let header_estimate: usize = exploded
726 .manifest
727 .rows
728 .iter()
729 .filter(|r| matches!(r.kind.as_str(), "use" | "preamble" | "extern_crate"))
730 .map(|r| r.loc)
731 .sum();
732 let item_budget = max_loc
733 .saturating_sub(header_estimate + PART_OVERHEAD)
734 .max(1);
735 let plan = plan_split(&exploded, item_budget);
736 let is_super_glob = |t: &str| t.replace(' ', "").contains("usesuper::*;");
737 let group_header: String = plan
738 .header
739 .iter()
740 .map(|&i| exploded.chunks[i].text.as_str())
741 .filter(|t| !is_super_glob(t))
742 .collect();
743
744 let mut files = Vec::new();
745 let mut decls = String::new();
746 let mut reexports = String::new();
747 let mut still = Vec::new();
748 for (idx, part) in plan.parts.iter().enumerate() {
749 let module = format!("g{idx:02}");
750 let mut body = String::new();
751 for &ci in &part.chunk_indices {
752 body.push_str(&bump_visibility(exploded.chunks[ci].text.as_str()));
753 }
754 let contents = format!("{group_header}\nuse super::*;\n\n{body}");
755 files.push(OutputFile {
756 path: format!("{name}/{module}.rs"),
757 loc: loc(&contents),
758 contents,
759 });
760 decls.push_str(&format!("mod {module};\n"));
761 reexports.push_str(&format!("pub(crate) use {module}::*;\n"));
762 }
763 for &mod_index in &plan.mods {
764 let chunk = exploded.chunks[mod_index].text.as_str();
765 let mod_name = row(mod_index).name.as_str();
766 let Some((decl, mod_inner)) = extract_mod(chunk) else {
767 decls.push_str(chunk);
768 continue;
769 };
770 decls.push_str(&decl);
771 let mod_loc = loc(&mod_inner);
772 if mod_loc < max_loc {
773 files.push(OutputFile {
774 path: format!("{name}/{mod_name}.rs"),
775 contents: mod_inner,
776 loc: mod_loc,
777 });
778 } else {
779 let (nested, nested_still) = split_nested_mod(mod_name, &mod_inner, max_loc);
780 files.extend(nested.into_iter().map(|mut f| {
781 f.path = format!("{name}/{}", f.path);
782 f
783 }));
784 still.extend(
785 nested_still
786 .into_iter()
787 .map(|message| format!("{name}::{message}")),
788 );
789 }
790 }
791 for over in &plan.oversized {
792 still.push(format!(
793 "{name}::{} ({} LOC {}, leaf needs manual extraction)",
794 over.name, over.loc, over.kind
795 ));
796 }
797
798 let root_kept: String = plan
799 .root_items
800 .iter()
801 .map(|&i| exploded.chunks[i].text.as_str())
802 .collect();
803 let modrs = format!("pub(crate) use super::*;\n\n{root_kept}{decls}\n{reexports}");
804 files.insert(
805 0,
806 OutputFile {
807 path: format!("{name}/mod.rs"),
808 loc: loc(&modrs),
809 contents: modrs,
810 },
811 );
812 (files, still)
813}
814
815#[cfg(test)]
816mod tests {
817 use super::*;
818 use crate::{explode, plan_split};
819
820 #[test]
821 fn splits_a_flat_bin_root_into_cohesive_under_budget_modules() {
822 let mut src = String::from("use std::fmt;\n\n");
824 for k in 0..4 {
825 for n in 0..4 {
826 let call = if n < 3 {
827 format!(" g{k}_{}();\n", n + 1)
828 } else {
829 String::new()
830 };
831 src.push_str(&format!("fn g{k}_{n}() {{\n{call}}}\n\n"));
832 }
833 }
834 src.push_str("fn main() {\n g0_0();\n}\n");
835 let exploded = explode(&src).unwrap();
836 let max_loc = 60;
837 let out = split_bin(&exploded, max_loc, "main");
838
839 assert!(out.still_oversized.is_empty());
840 for f in &out.files {
841 assert!(f.loc < max_loc, "{} over budget: {}", f.path, f.loc);
842 }
843 let root = out.files.iter().find(|f| f.path == "main.rs").unwrap();
844 assert!(root.contents.contains("fn main"), "main stays at root");
845 assert!(root.contents.contains("pub(crate) use "));
846 let part = out.files.iter().find(|f| f.path != "main.rs").unwrap();
847 assert!(part.contents.contains("use crate::*;"));
848 assert!(part.contents.contains("pub(crate) fn "));
849 let g0 = out
851 .files
852 .iter()
853 .find(|f| f.contents.contains("fn g0_1"))
854 .unwrap();
855 for n in 0..4 {
856 assert!(
857 g0.contents.contains(&format!("fn g0_{n}")),
858 "g0 group together"
859 );
860 }
861 }
862
863 #[test]
864 fn oversized_mod_is_extracted_to_its_own_file() {
865 let mut inner = String::new();
866 for i in 0..10 {
867 inner.push_str(&format!(" fn t{i}() {{}}\n"));
868 }
869 let src = format!("fn main() {{}}\n\n#[cfg(test)]\nmod tests {{\n{inner}}}\n");
870 let exploded = explode(&src).unwrap();
871 let plan = plan_split(&exploded, 6);
872 let out = reassemble_bin(&exploded, &plan, "main", 1000);
874 let tests = out.files.iter().find(|f| f.path == "tests.rs").unwrap();
875 assert!(tests.contents.contains("fn t0()"));
876 assert!(!tests.contents.contains("mod tests"));
877 let root = out.files.iter().find(|f| f.path == "main.rs").unwrap();
878 assert!(root.contents.contains("#[cfg(test)]\nmod tests;"));
879 }
880
881 #[test]
882 fn an_over_ceiling_test_mod_is_nested_into_a_subdir() {
883 let mut inner =
885 String::from(" use super::*;\n\n fn helper() -> i32 {\n 1\n }\n");
886 for i in 0..30 {
887 inner.push_str(&format!(
888 "\n #[test]\n fn t{i}() {{\n assert_eq!(helper(), 1);\n }}\n"
889 ));
890 }
891 let src = format!(
892 "pub fn helper() -> i32 {{\n 1\n}}\n\nfn main() {{}}\n\n#[cfg(test)]\nmod tests {{\n{inner}}}\n"
893 );
894 let exploded = explode(&src).unwrap();
895 let out = split_bin(&exploded, 60, "main");
896
897 let mod_rs = out.files.iter().find(|f| f.path == "tests/mod.rs").unwrap();
899 assert!(
900 mod_rs.contents.contains("pub(crate) use super::*;"),
901 "forwards the parent"
902 );
903 assert!(mod_rs.contents.contains("mod g00;"));
904 assert!(out.files.iter().any(|f| f.path == "tests/g00.rs"));
905 for f in &out.files {
906 assert!(f.loc < 60, "{} over ceiling: {}", f.path, f.loc);
907 }
908 let root = out.files.iter().find(|f| f.path == "main.rs").unwrap();
909 assert!(root.contents.contains("#[cfg(test)]\nmod tests;"));
910 }
911
912 #[test]
913 fn split_mod_uses_super_and_preserves_pub_api() {
914 let src = "use std::fmt;\n\npub fn alpha() -> i32 {\n beta()\n}\n\nfn beta() -> i32 {\n 1\n}\n";
916 let exploded = explode(src).unwrap();
917 let out = split_mod(&exploded, 10_000, "mod");
918
919 let root = out.files.iter().find(|f| f.path == "mod.rs").unwrap();
920 assert!(
921 root.contents.contains("pub use "),
922 "pub re-export preserves the API"
923 );
924 assert!(
925 !root.contents.contains("pub(crate) use "),
926 "a single `pub use *` caps each item at its own visibility — no \
927 redundant second `pub(crate) use` line"
928 );
929
930 let sub = out.files.iter().find(|f| f.path != "mod.rs").unwrap();
931 assert!(
932 sub.contents.contains("use super::*;"),
933 "siblings reached via super, not crate"
934 );
935 assert!(!sub.contents.contains("use crate::*;"));
936 assert!(sub.contents.contains("pub fn alpha"), "pub item stays pub");
937 assert!(
938 sub.contents.contains("pub(crate) fn beta"),
939 "private bumped to pub(crate)"
940 );
941 }
942
943 #[test]
946 fn file_module_places_submodules_in_a_subdir() {
947 let mut src = String::new();
951 for k in 0..3 {
952 src.push_str(&format!("pub fn item{k}() -> i32 {{\n {k}\n}}\n\n"));
954 }
955 let exploded = explode(&src).unwrap();
956 let out = split_mod(&exploded, 10_000, "wstest");
957
958 let root = out.files.iter().find(|f| f.path == "wstest.rs").unwrap();
959 assert!(
960 root.contents.contains("mod item0;"),
961 "root file-module declares the sub-modules"
962 );
963 let subs: Vec<&str> = out
964 .files
965 .iter()
966 .map(|f| f.path.as_str())
967 .filter(|p| *p != "wstest.rs")
968 .collect();
969 assert!(!subs.is_empty(), "produced sub-module files");
970 for path in &subs {
971 assert!(
972 path.starts_with("wstest/"),
973 "sub-module {path} must live under wstest/ (file-module subdir)"
974 );
975 }
976 }
977
978 #[test]
979 fn mod_rs_keeps_submodules_as_siblings() {
980 let mut src = String::new();
982 for k in 0..3 {
983 src.push_str(&format!("pub fn item{k}() -> i32 {{\n {k}\n}}\n\n"));
984 }
985 let exploded = explode(&src).unwrap();
986 let out = split_mod(&exploded, 10_000, "mod");
987 for f in &out.files {
988 assert!(
989 !f.path.contains('/'),
990 "mod.rs sub-modules are siblings, not in a subdir: {}",
991 f.path
992 );
993 }
994 }
995
996 #[test]
999 fn private_only_module_reexports_pub_crate_not_pub() {
1000 let src = "fn helper() -> i32 {\n leaf()\n}\n\nfn leaf() -> i32 {\n 1\n}\n";
1003 let exploded = explode(src).unwrap();
1004 let out = split_mod(&exploded, 10_000, "mod");
1005 let root = out.files.iter().find(|f| f.path == "mod.rs").unwrap();
1006 assert!(
1007 root.contents.contains("pub(crate) use "),
1008 "all-private module re-exports pub(crate):\n{}",
1009 root.contents
1010 );
1011 assert!(
1012 !root.contents.contains("pub use "),
1013 "no `pub use` for a module with no public item"
1014 );
1015 }
1016
1017 #[test]
1018 fn impl_only_module_is_declared_but_not_reexported() {
1019 let src = "pub fn api() -> i32 {\n 1\n}\n\nimpl Worker for External {\n fn run(&self) {}\n}\n";
1023 let exploded = explode(src).unwrap();
1024 let out = split_mod(&exploded, 10_000, "mod");
1025 let root = out.files.iter().find(|f| f.path == "mod.rs").unwrap();
1026
1027 let reexports = root
1029 .contents
1030 .lines()
1031 .filter(|l| l.contains(" use ") && l.trim_end().ends_with("::*;"))
1032 .count();
1033 assert_eq!(
1034 reexports, 1,
1035 "only the nameable module is re-exported:\n{}",
1036 root.contents
1037 );
1038 assert!(root.contents.contains("pub use api::*;"));
1039 assert_eq!(
1040 root.contents.matches("mod ").count(),
1041 2,
1042 "both modules are still declared:\n{}",
1043 root.contents
1044 );
1045 }
1046
1047 #[test]
1050 fn bin_public_entry_fn_is_reexported_pub_so_it_stays_crate_visible() {
1051 let src = "fn helper() -> i32 {\n 1\n}\n\npub fn run() -> i32 {\n helper()\n}\n";
1055 let exploded = explode(src).unwrap();
1056 let out = split_bin(&exploded, 10_000, "lib");
1057
1058 let root = out.files.iter().find(|f| f.path == "lib.rs").unwrap();
1059 assert!(
1060 root.contents.contains("pub use "),
1061 "public `run` re-exported at pub keeps `crate::run` visible:\n{}",
1062 root.contents
1063 );
1064 assert!(
1065 !root.contents.contains("pub(crate) use "),
1066 "the module has a pub item, so one `pub use *` suffices (no double line)"
1067 );
1068 }
1069
1070 #[test]
1073 fn crate_preamble_stays_at_root_not_copied_into_parts() {
1074 let src = "//! crate docs\n#![allow(dead_code)]\n\npub fn alpha() -> i32 {\n beta()\n}\n\nfn beta() -> i32 {\n 1\n}\n";
1077 let exploded = explode(src).unwrap();
1078 let out = split_bin(&exploded, 10_000, "lib");
1079
1080 let root = out.files.iter().find(|f| f.path == "lib.rs").unwrap();
1081 assert!(
1082 root.contents.contains("#![allow(dead_code)]"),
1083 "preamble stays at the root"
1084 );
1085 for f in &out.files {
1086 if f.path != "lib.rs" {
1087 assert!(
1088 !f.contents.contains("#!["),
1089 "part {} must not carry crate-level inner attrs",
1090 f.path
1091 );
1092 assert!(
1093 !f.contents.contains("//! crate docs"),
1094 "part {} must not duplicate the crate docs",
1095 f.path
1096 );
1097 }
1098 }
1099 }
1100
1101 #[test]
1104 fn parts_copy_only_the_imports_they_reference() {
1105 let src = "use std::fmt::Display;\nuse std::collections::HashMap;\n\npub fn alpha(d: &dyn Display) {\n let _ = d;\n}\n\npub fn bravo() -> i32 {\n 1\n}\n";
1108 let exploded = explode(src).unwrap();
1109 let out = split_mod(&exploded, 10_000, "mod");
1110
1111 let alpha = out
1112 .files
1113 .iter()
1114 .find(|f| f.contents.contains("fn alpha"))
1115 .unwrap();
1116 let bravo = out
1117 .files
1118 .iter()
1119 .find(|f| f.contents.contains("fn bravo"))
1120 .unwrap();
1121 assert!(
1122 alpha.contents.contains("use std::fmt::Display;"),
1123 "alpha references Display:\n{}",
1124 alpha.contents
1125 );
1126 assert!(
1127 !alpha.contents.contains("HashMap"),
1128 "alpha never names HashMap -> import dropped:\n{}",
1129 alpha.contents
1130 );
1131 assert!(
1132 !bravo.contents.contains("use std::fmt::Display;"),
1133 "bravo references nothing from the header:\n{}",
1134 bravo.contents
1135 );
1136 assert!(!bravo.contents.contains("HashMap"));
1137 }
1138
1139 #[test]
1140 fn globs_and_anonymous_trait_imports_are_always_copied() {
1141 let src = "use std::fmt::Write as _;\nuse std::prelude::v1::*;\n\npub fn writes(s: &mut String) {\n let _ = write!(s, \"x\");\n}\n\npub fn other() -> i32 {\n 2\n}\n";
1144 let exploded = explode(src).unwrap();
1145 let out = split_mod(&exploded, 10_000, "mod");
1146 for f in &out.files {
1147 if f.path == "mod.rs" {
1148 continue;
1149 }
1150 assert!(
1151 f.contents.contains("use std::fmt::Write as _;"),
1152 "{} must keep the `as _` trait import:\n{}",
1153 f.path,
1154 f.contents
1155 );
1156 assert!(
1157 f.contents.contains("use std::prelude::v1::*;"),
1158 "{} must keep the glob import:\n{}",
1159 f.path,
1160 f.contents
1161 );
1162 }
1163 }
1164
1165 #[test]
1166 fn item_less_root_keeps_no_imports() {
1167 let src = "use std::collections::HashMap;\n\npub fn solo(m: HashMap<u8, u8>) {\n let _ = m;\n}\n";
1170 let exploded = explode(src).unwrap();
1171 let out = split_mod(&exploded, 10_000, "mod");
1172
1173 let root = out.files.iter().find(|f| f.path == "mod.rs").unwrap();
1174 assert!(
1175 !root.contents.contains("use std::collections::HashMap;"),
1176 "item-less root drops imports it doesn't use:\n{}",
1177 root.contents
1178 );
1179 let part = out.files.iter().find(|f| f.path != "mod.rs").unwrap();
1180 assert!(
1181 part.contents.contains("use std::collections::HashMap;"),
1182 "the part that uses HashMap keeps it"
1183 );
1184 }
1185
1186 #[test]
1189 fn source_pub_use_reexports_stay_pub_use_at_the_root() {
1190 let src = "mod helpers;\n\npub use helpers::Helper;\n\nuse std::fmt::Display;\n\n\
1194 pub fn api(d: &dyn Display) {\n let _ = d;\n}\n";
1195 let exploded = explode(src).unwrap();
1196 let out = split_bin(&exploded, 10_000, "lib");
1197
1198 let root = out.files.iter().find(|f| f.path == "lib.rs").unwrap();
1199 assert!(
1200 root.contents.contains("mod helpers;"),
1201 "content-less mod decl stays at the root (it binds helpers.rs):\n{}",
1202 root.contents
1203 );
1204 assert!(
1205 root.contents.contains("pub use helpers::Helper;"),
1206 "source re-export stays verbatim at the root:\n{}",
1207 root.contents
1208 );
1209 assert!(
1210 !root.contents.contains("pub(crate) use helpers"),
1211 "a source re-export must never be demoted"
1212 );
1213 for f in &out.files {
1214 if f.path != "lib.rs" {
1215 assert!(
1216 !f.contents.contains("pub use helpers::Helper;"),
1217 "{} must not carry the root's re-export",
1218 f.path
1219 );
1220 assert!(
1221 !f.contents.contains("mod helpers;"),
1222 "{} must not carry the root's mod decl (would look for a \
1223 sibling subdir file)",
1224 f.path
1225 );
1226 }
1227 }
1228 }
1229
1230 #[test]
1233 fn under_budget_cfg_test_mod_is_extracted_whole_with_its_gate() {
1234 let src = "pub fn alpha() -> i32 {\n 1\n}\n\n\
1239 #[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n \
1240 fn smoke() {\n assert_eq!(alpha(), 1);\n }\n}\n";
1241 let exploded = explode(src).unwrap();
1242 let out = split_bin(&exploded, 10_000, "lib");
1244
1245 let root = out.files.iter().find(|f| f.path == "lib.rs").unwrap();
1246 assert!(
1247 root.contents.contains("#[cfg(test)]\nmod tests;"),
1248 "the gate must travel to the root declaration:\n{}",
1249 root.contents
1250 );
1251 assert!(
1252 !root.contents.contains("use tests::*"),
1253 "an extracted mod is declared, not glob re-exported:\n{}",
1254 root.contents
1255 );
1256 let tests = out.files.iter().find(|f| f.path == "tests.rs").unwrap();
1257 assert!(tests.contents.contains("fn smoke()"));
1258 assert!(
1259 !tests.contents.contains("mod tests"),
1260 "the body is unwrapped — no double nesting:\n{}",
1261 tests.contents
1262 );
1263 }
1264
1265 #[test]
1266 fn pub_mod_keeps_its_visibility_on_the_extracted_decl() {
1267 let src = "pub fn api() -> i32 {\n 1\n}\n\n\
1268 /// Config store.\npub mod config {\n pub fn get() -> i32 {\n 2\n }\n}\n";
1269 let exploded = explode(src).unwrap();
1270 let out = split_bin(&exploded, 10_000, "lib");
1271
1272 let root = out.files.iter().find(|f| f.path == "lib.rs").unwrap();
1273 assert!(
1274 root.contents.contains("/// Config store.\npub mod config;"),
1275 "docs and `pub` travel to the declaration:\n{}",
1276 root.contents
1277 );
1278 let config = out.files.iter().find(|f| f.path == "config.rs").unwrap();
1279 assert!(config.contents.contains("pub fn get()"));
1280 }
1281
1282 #[test]
1283 fn part_colliding_with_an_extracted_mod_name_is_renamed() {
1284 let src = "fn tests() -> i32 {\n 1\n}\n\n\
1288 #[cfg(test)]\nmod tests {\n fn smoke() {}\n}\n";
1289 let exploded = explode(src).unwrap();
1290 let out = split_bin(&exploded, 10_000, "lib");
1291
1292 let paths: Vec<&str> = out.files.iter().map(|f| f.path.as_str()).collect();
1293 assert!(paths.contains(&"tests.rs"), "extracted mod file: {paths:?}");
1294 assert!(paths.contains(&"tests_.rs"), "renamed part file: {paths:?}");
1295 let root = out.files.iter().find(|f| f.path == "lib.rs").unwrap();
1296 assert!(root.contents.contains("mod tests_;"));
1297 assert!(root.contents.contains("#[cfg(test)]\nmod tests;"));
1298 }
1299
1300 #[test]
1301 fn root_keeps_imports_an_extracted_mod_body_references() {
1302 let src = "use std::collections::HashMap;\n\n\
1305 pub fn alpha() -> i32 {\n 1\n}\n\n\
1306 #[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n \
1307 fn smoke() {\n let _m: HashMap<u8, u8> = HashMap::new();\n \
1308 assert_eq!(alpha(), 1);\n }\n}\n";
1309 let exploded = explode(src).unwrap();
1310 let out = split_bin(&exploded, 10_000, "lib");
1311
1312 let root = out.files.iter().find(|f| f.path == "lib.rs").unwrap();
1313 assert!(
1314 root.contents.contains("use std::collections::HashMap;"),
1315 "root must keep imports the extracted body names:\n{}",
1316 root.contents
1317 );
1318 }
1319
1320 #[test]
1321 fn bin_root_keeps_only_imports_main_references() {
1322 let src = "use std::process::exit;\nuse std::collections::HashMap;\n\nfn worker() -> HashMap<u8, u8> {\n HashMap::new()\n}\n\nfn main() {\n let _ = worker();\n exit(0);\n}\n";
1325 let exploded = explode(src).unwrap();
1326 let out = split_bin(&exploded, 10_000, "main");
1327
1328 let root = out.files.iter().find(|f| f.path == "main.rs").unwrap();
1329 assert!(
1330 root.contents.contains("use std::process::exit;"),
1331 "main references exit:\n{}",
1332 root.contents
1333 );
1334 assert!(
1335 !root.contents.contains("HashMap"),
1336 "main never names HashMap (worker does) -> dropped from root:\n{}",
1337 root.contents
1338 );
1339 }
1340}