1use crate::{Exploded, SplitPlan, plan_split};
29use syn::parse::Parser;
30use syn::spanned::Spanned;
31
32const PART_OVERHEAD: usize = 3;
35
36pub fn split_bin(exploded: &Exploded, max_loc: usize, root_stem: &str) -> SplitOutput {
42 let header_loc: usize = exploded
43 .manifest
44 .rows
45 .iter()
46 .filter(|r| matches!(r.kind.as_str(), "use" | "preamble" | "extern_crate"))
47 .map(|r| r.loc)
48 .sum();
49 let item_budget = max_loc.saturating_sub(header_loc + PART_OVERHEAD).max(1);
50 let plan = plan_split(exploded, item_budget);
51 reassemble_bin(exploded, &plan, root_stem, max_loc)
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct OutputFile {
57 pub path: String,
59 pub contents: String,
60 pub loc: usize,
61}
62
63#[derive(Debug, Clone)]
65pub struct SplitOutput {
66 pub files: Vec<OutputFile>,
67 pub still_oversized: Vec<String>,
69}
70
71impl SplitOutput {
72 pub fn max_loc(&self) -> usize {
75 self.files.iter().map(|f| f.loc).max().unwrap_or(0)
76 }
77}
78
79fn loc(text: &str) -> usize {
80 if text.is_empty() {
81 0
82 } else {
83 text.lines().count().max(1)
84 }
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum Topology {
93 Bin,
96 Mod,
102}
103
104pub fn split_mod(exploded: &Exploded, max_loc: usize, root_stem: &str) -> SplitOutput {
109 let header_loc: usize = exploded
110 .manifest
111 .rows
112 .iter()
113 .filter(|r| matches!(r.kind.as_str(), "use" | "preamble" | "extern_crate"))
114 .map(|r| r.loc)
115 .sum();
116 let item_budget = max_loc.saturating_sub(header_loc + PART_OVERHEAD).max(1);
117 let plan = plan_split(exploded, item_budget);
118 reassemble(exploded, &plan, root_stem, Topology::Mod, max_loc)
119}
120
121pub fn reassemble_bin(
123 exploded: &Exploded,
124 plan: &SplitPlan,
125 root_stem: &str,
126 max_loc: usize,
127) -> SplitOutput {
128 reassemble(exploded, plan, root_stem, Topology::Bin, max_loc)
129}
130
131fn reassemble(
135 exploded: &Exploded,
136 plan: &SplitPlan,
137 root_stem: &str,
138 topology: Topology,
139 max_loc: usize,
140) -> SplitOutput {
141 let sibling = match topology {
142 Topology::Bin => "crate",
143 Topology::Mod => "super",
144 };
145 let part_rebase = match topology {
148 Topology::Bin => Rebase::CrateRoot,
149 Topology::Mod => Rebase::Nested,
150 };
151 let chunk_text = |i: usize| exploded.chunks[i].text.as_str();
152 let row = |i: usize| {
153 exploded
154 .manifest
155 .rows
156 .iter()
157 .find(|r| r.chunk_index == i)
158 .unwrap()
159 };
160
161 let sub_prefix = match topology {
165 Topology::Bin => String::new(),
166 Topology::Mod if matches!(root_stem, "mod" | "lib" | "main") => String::new(),
167 Topology::Mod => format!("{root_stem}/"),
168 };
169
170 let preamble: String = plan
178 .header
179 .iter()
180 .filter(|&&i| row(i).kind == "preamble")
181 .map(|&i| chunk_text(i))
182 .collect();
183 let import_chunks: Vec<usize> = plan
184 .header
185 .iter()
186 .copied()
187 .filter(|&i| row(i).kind != "preamble")
188 .collect();
189
190 let root_kept: String = plan.root_items.iter().map(|&i| chunk_text(i)).collect();
194
195 let mut reserved: std::collections::BTreeSet<&str> =
199 plan.mods.iter().map(|&i| row(i).name.as_str()).collect();
200 reserved.insert(root_stem);
201 for &i in &plan.root_items {
202 if row(i).kind == "mod" {
203 reserved.insert(row(i).name.as_str());
204 }
205 }
206
207 let mut files = Vec::new();
208 let mut mod_decls = String::new();
209 let mut reexports = String::new();
210 let mut still_oversized = Vec::new();
211 let mut root_refs = Refs::default();
214
215 for part in &plan.parts {
217 let module = if reserved.contains(part.name.as_str()) {
218 format!("{}_", part.name)
219 } else {
220 part.name.clone()
221 };
222 let mut body = String::new();
223 let mut root_main = None;
224 let mut export_vis = ExportVis::None;
226 let mut refs = Refs::default();
228 for &i in &part.chunk_indices {
229 let r = row(i);
230 if r.kind == "fn" && r.name == "main" {
232 root_main = Some(i);
233 continue;
234 }
235 export_vis = export_vis.max(item_export_vis(chunk_text(i)));
236 refs.add(chunk_text(i));
237 body.push_str(&rebase_body_paths(
238 &bump_visibility(chunk_text(i)),
239 part_rebase,
240 ));
241 }
242 if body.trim().is_empty() {
244 if let Some(i) = root_main {
245 files.push(OutputFile {
247 path: String::from("__root_main__"),
248 contents: chunk_text(i).to_owned(),
249 loc: 0,
250 });
251 }
252 continue;
253 }
254 if let Some(i) = root_main {
255 files.push(OutputFile {
256 path: String::from("__root_main__"),
257 contents: chunk_text(i).to_owned(),
258 loc: 0,
259 });
260 }
261 let part_imports = select_imports(&import_chunks, &exploded.chunks, &refs, part_rebase);
262 let contents = part_file(&part_imports, sibling, &body);
263 mod_decls.push_str(&format!("mod {module};\n"));
264 if let Some(line) = reexport_line(export_vis, &module) {
272 reexports.push_str(&line);
273 }
274 let l = loc(&contents);
275 files.push(OutputFile {
276 path: format!("{sub_prefix}{module}.rs"),
277 contents,
278 loc: l,
279 });
280 }
281
282 for &mod_index in &plan.mods {
289 let chunk = chunk_text(mod_index);
290 let name = row(mod_index).name.as_str();
291 let Some((decl, inner)) = extract_mod(chunk) else {
292 mod_decls.push_str(chunk);
295 continue;
296 };
297 mod_decls.push_str(&decl);
298 root_refs.add_source(&inner);
301 let inner_loc = loc(&inner);
302 if inner_loc < max_loc {
303 files.push(OutputFile {
304 path: format!("{sub_prefix}{name}.rs"),
305 contents: inner,
306 loc: inner_loc,
307 });
308 } else {
309 let (nested, nested_still) = split_nested_mod(name, &inner, max_loc);
310 files.extend(nested.into_iter().map(|mut f| {
311 f.path = format!("{sub_prefix}{}", f.path);
312 f
313 }));
314 still_oversized.extend(nested_still);
315 }
316 }
317
318 for over in &plan.oversized {
320 still_oversized.push(format!(
321 "{} ({} LOC {}, manual extraction)",
322 over.name, over.loc, over.kind
323 ));
324 }
325
326 let mut root_main = String::new();
328 files.retain(|f| {
329 if f.path == "__root_main__" {
330 root_main = f.contents.clone();
331 false
332 } else {
333 true
334 }
335 });
336
337 if !root_main.trim().is_empty() {
342 root_refs.add(&root_main);
343 }
344 let root_imports = if root_refs.is_empty() {
345 String::new()
346 } else {
347 select_imports(&import_chunks, &exploded.chunks, &root_refs, Rebase::Keep)
350 };
351
352 let mut root = String::new();
353 root.push_str(&preamble);
354 root.push_str(&root_imports);
355 root.push_str(&root_kept);
356 root.push('\n');
357 root.push_str(&mod_decls);
358 root.push('\n');
359 root.push_str(&reexports);
360 if !root_main.is_empty() {
361 root.push('\n');
362 root.push_str(&root_main);
363 }
364 let root_loc = loc(&root);
365 files.insert(
366 0,
367 OutputFile {
368 path: format!("{root_stem}.rs"),
369 contents: root,
370 loc: root_loc,
371 },
372 );
373
374 for file in &mut files {
380 let levels = file.path.matches('/').count();
381 if levels == 0 || !file.contents.contains("include") {
382 continue;
383 }
384 file.contents = rebase_includes(&file.contents, levels, &file.path);
385 }
386
387 SplitOutput {
388 files,
389 still_oversized,
390 }
391}
392
393const PATH_MACROS: [&str; 3] = ["include", "include_str", "include_bytes"];
396
397fn rebase_includes(contents: &str, levels: usize, path: &str) -> String {
404 let Ok(stream) = contents.parse::<proc_macro2::TokenStream>() else {
405 return contents.to_owned();
406 };
407 let mut edits = Vec::new();
408 let prefix = "../".repeat(levels);
409 collect_include_edits(stream, &prefix, contents, path, &mut edits);
410 apply_replacements(contents, &edits)
411}
412
413fn collect_include_edits<'a>(
417 stream: proc_macro2::TokenStream,
418 prefix: &'a str,
419 contents: &str,
420 path: &str,
421 edits: &mut Vec<(std::ops::Range<usize>, &'a str)>,
422) {
423 let trees: Vec<proc_macro2::TokenTree> = stream.into_iter().collect();
424 for (index, tree) in trees.iter().enumerate() {
425 let proc_macro2::TokenTree::Group(group) = tree else {
426 continue;
427 };
428 if !is_path_macro_call(&trees, index) {
429 collect_include_edits(group.stream(), prefix, contents, path, edits);
430 continue;
431 }
432 match literal_insert_point(&group.stream()) {
433 Some(Some(offset)) => edits.push((offset..offset, prefix)),
434 Some(None) => {}
436 None => {
437 let line = contents[..group.span().byte_range().start].lines().count();
438 eprintln!(
439 "rust-split: {path}:{line}: {}! argument is not a plain string \
440 literal — its path was not re-based",
441 trees[index - 2]
442 );
443 }
444 }
445 }
446}
447
448fn is_path_macro_call(trees: &[proc_macro2::TokenTree], index: usize) -> bool {
451 if index < 2 {
452 return false;
453 }
454 let proc_macro2::TokenTree::Ident(name) = &trees[index - 2] else {
455 return false;
456 };
457 let proc_macro2::TokenTree::Punct(bang) = &trees[index - 1] else {
458 return false;
459 };
460 bang.as_char() == '!' && PATH_MACROS.iter().any(|m| name == m)
461}
462
463fn literal_insert_point(stream: &proc_macro2::TokenStream) -> Option<Option<usize>> {
468 let mut trees = stream.clone().into_iter();
469 let proc_macro2::TokenTree::Literal(literal) = trees.next()? else {
470 return None;
471 };
472 if let Some(token) = trees.next()
473 && (!matches!(token, proc_macro2::TokenTree::Punct(p) if p.as_char() == ',')
474 || trees.next().is_some())
475 {
476 return None;
477 }
478 let text = literal.to_string();
479 let value = syn::parse_str::<syn::LitStr>(&text).ok()?.value();
480 let windows_drive =
482 matches!(value.as_bytes(), [drive, b':', b'/' | b'\\', ..] if drive.is_ascii_alphabetic());
483 if value.is_empty() || value.starts_with('/') || value.starts_with(r"\\") || windows_drive {
484 return Some(None);
485 }
486 let quote = text.find('"')?;
489 Some(Some(literal.span().byte_range().start + quote + 1))
490}
491
492fn bump_visibility(chunk_text: &str) -> String {
499 let Ok(item) = syn::parse_str::<syn::Item>(chunk_text) else {
500 return chunk_text.to_owned();
501 };
502 let mut offsets = Vec::new();
503 collect_vis_offsets(&item, &mut offsets);
504 apply_inserts(chunk_text, &offsets)
505}
506
507fn is_inherited(vis: &syn::Visibility) -> bool {
508 matches!(vis, syn::Visibility::Inherited)
509}
510
511fn start<T: Spanned>(node: &T) -> usize {
512 node.span().byte_range().start
513}
514
515fn fn_sig_start(sig: &syn::Signature) -> usize {
518 let mut min = start(&sig.fn_token);
519 if let Some(t) = &sig.constness {
520 min = min.min(start(t));
521 }
522 if let Some(t) = &sig.asyncness {
523 min = min.min(start(t));
524 }
525 if let Some(t) = &sig.unsafety {
526 min = min.min(start(t));
527 }
528 if let Some(abi) = &sig.abi {
529 min = min.min(start(&abi.extern_token));
530 }
531 min
532}
533
534fn field_offsets(fields: &syn::Fields, offsets: &mut Vec<usize>) {
535 for field in fields {
536 if is_inherited(&field.vis) {
537 match &field.ident {
538 Some(ident) => offsets.push(start(ident)),
539 None => offsets.push(start(&field.ty)),
540 }
541 }
542 }
543}
544
545fn collect_vis_offsets(item: &syn::Item, offsets: &mut Vec<usize>) {
546 use syn::Item;
547 match item {
548 Item::Fn(f) => {
549 if is_inherited(&f.vis) {
550 offsets.push(fn_sig_start(&f.sig));
551 }
552 }
553 Item::Struct(s) => {
554 if is_inherited(&s.vis) {
555 offsets.push(start(&s.struct_token));
556 }
557 field_offsets(&s.fields, offsets);
558 }
559 Item::Enum(e) => {
560 if is_inherited(&e.vis) {
561 offsets.push(start(&e.enum_token));
562 }
563 }
564 Item::Union(u) => {
565 if is_inherited(&u.vis) {
566 offsets.push(start(&u.union_token));
567 }
568 field_offsets(&syn::Fields::Named(u.fields.clone()), offsets);
569 }
570 Item::Const(c) if is_inherited(&c.vis) => offsets.push(start(&c.const_token)),
571 Item::Static(s) if is_inherited(&s.vis) => offsets.push(start(&s.static_token)),
572 Item::Type(t) if is_inherited(&t.vis) => offsets.push(start(&t.type_token)),
573 Item::Trait(t) if is_inherited(&t.vis) => offsets.push(start(&t.trait_token)),
574 Item::TraitAlias(t) if is_inherited(&t.vis) => offsets.push(start(&t.trait_token)),
575 Item::Impl(i) if i.trait_.is_none() => {
578 for member in &i.items {
579 match member {
580 syn::ImplItem::Fn(f) if is_inherited(&f.vis) => {
581 offsets.push(fn_sig_start(&f.sig))
582 }
583 syn::ImplItem::Const(c) if is_inherited(&c.vis) => {
584 offsets.push(start(&c.const_token))
585 }
586 syn::ImplItem::Type(t) if is_inherited(&t.vis) => {
587 offsets.push(start(&t.type_token))
588 }
589 _ => {}
590 }
591 }
592 }
593 _ => {}
594 }
595}
596
597fn apply_inserts(text: &str, offsets: &[usize]) -> String {
598 let mut offsets: Vec<usize> = offsets.to_vec();
599 offsets.sort_unstable();
600 offsets.dedup();
601 let mut out = String::with_capacity(text.len() + offsets.len() * 11);
602 let mut prev = 0;
603 for &offset in &offsets {
604 out.push_str(&text[prev..offset]);
605 out.push_str("pub(crate) ");
606 prev = offset;
607 }
608 out.push_str(&text[prev..]);
609 out
610}
611
612fn part_file(imports: &str, sibling: &str, body: &str) -> String {
615 if imports.trim().is_empty() {
616 format!("use {sibling}::*;\n\n{body}")
617 } else {
618 format!("{imports}\nuse {sibling}::*;\n\n{body}")
619 }
620}
621
622fn reexport_line(vis: ExportVis, module: &str) -> Option<String> {
626 match vis {
627 ExportVis::None => None,
628 ExportVis::Crate => Some(format!("pub(crate) use {module}::*;\n")),
629 ExportVis::Pub => Some(format!("pub use {module}::*;\n")),
630 }
631}
632
633#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
636enum ExportVis {
637 None,
639 Crate,
642 Pub,
644}
645
646fn item_export_vis(chunk_text: &str) -> ExportVis {
650 use syn::{Item, Visibility};
651 let Ok(item) = syn::parse_str::<syn::Item>(chunk_text) else {
652 return ExportVis::None;
655 };
656 let (vis, bumped) = match &item {
657 Item::Fn(f) => (&f.vis, true),
658 Item::Struct(s) => (&s.vis, true),
659 Item::Enum(e) => (&e.vis, true),
660 Item::Const(c) => (&c.vis, true),
661 Item::Static(s) => (&s.vis, true),
662 Item::Trait(t) => (&t.vis, true),
663 Item::TraitAlias(t) => (&t.vis, true),
664 Item::Type(t) => (&t.vis, true),
665 Item::Union(u) => (&u.vis, true),
666 Item::Mod(m) => (&m.vis, false),
667 Item::Use(u) => (&u.vis, false),
668 _ => return ExportVis::None,
670 };
671 match vis {
672 Visibility::Public(_) => ExportVis::Pub,
673 Visibility::Restricted(_) => ExportVis::Crate,
674 Visibility::Inherited if bumped => ExportVis::Crate,
675 Visibility::Inherited => ExportVis::None,
676 }
677}
678
679#[derive(Default)]
683struct Refs {
684 names: std::collections::BTreeSet<String>,
685 keep_all: bool,
686}
687
688impl Refs {
689 fn add(&mut self, chunk_text: &str) {
690 match crate::referenced_idents(chunk_text) {
691 Some(names) => self.names.extend(names),
692 None => self.keep_all = true,
693 }
694 }
695
696 fn add_source(&mut self, src: &str) {
699 match crate::referenced_idents_in_source(src) {
700 Some(names) => self.names.extend(names),
701 None => self.keep_all = true,
702 }
703 }
704
705 fn is_empty(&self) -> bool {
707 self.names.is_empty() && !self.keep_all
708 }
709
710 fn keeps(&self, provided: &Option<std::collections::BTreeSet<String>>) -> bool {
714 match provided {
715 None => true,
716 Some(names) => self.keep_all || names.iter().any(|n| self.names.contains(n)),
717 }
718 }
719}
720
721#[derive(Debug, Clone, Copy, PartialEq, Eq)]
724enum Rebase {
725 Keep,
728 Nested,
731 CrateRoot,
734}
735
736fn rebase_import(chunk_text: &str, rebase: Rebase) -> String {
742 if matches!(rebase, Rebase::Keep) {
743 return chunk_text.to_owned();
744 }
745 let Ok(syn::Item::Use(item)) = syn::parse_str::<syn::Item>(chunk_text) else {
746 return chunk_text.to_owned();
747 };
748 if !is_inherited(&item.vis) || item.leading_colon.is_some() {
751 return chunk_text.to_owned();
752 }
753 let mut edits = Vec::new();
754 collect_rebase_edits(&item.tree, rebase, &mut edits);
755 apply_replacements(chunk_text, &edits)
756}
757
758fn collect_rebase_edits(
763 tree: &syn::UseTree,
764 rebase: Rebase,
765 edits: &mut Vec<(std::ops::Range<usize>, &'static str)>,
766) {
767 match tree {
768 syn::UseTree::Path(path) => collect_anchor_edit(&path.ident, rebase, edits),
769 syn::UseTree::Rename(rename) => collect_anchor_edit(&rename.ident, rebase, edits),
770 syn::UseTree::Group(group) => {
771 for item in &group.items {
772 collect_rebase_edits(item, rebase, edits);
773 }
774 }
775 _ => {}
776 }
777}
778
779fn collect_anchor_edit(
780 ident: &syn::Ident,
781 rebase: Rebase,
782 edits: &mut Vec<(std::ops::Range<usize>, &'static str)>,
783) {
784 let replacement = match (ident.to_string().as_str(), rebase) {
785 ("super", Rebase::Nested) => "super::super",
786 ("self", Rebase::Nested) => "super",
787 ("self", Rebase::CrateRoot) => "crate",
788 _ => return,
789 };
790 edits.push((ident.span().byte_range(), replacement));
791}
792
793fn apply_replacements(text: &str, edits: &[(std::ops::Range<usize>, &str)]) -> String {
796 let mut edits: Vec<(std::ops::Range<usize>, &str)> = edits.to_vec();
797 edits.sort_by_key(|(range, _)| range.start);
798 let mut out = String::with_capacity(text.len() + edits.len() * 8);
799 let mut prev = 0;
800 for (range, replacement) in &edits {
801 if range.start < prev {
802 continue;
803 }
804 out.push_str(&text[prev..range.start]);
805 out.push_str(replacement);
806 prev = range.end;
807 }
808 out.push_str(&text[prev..]);
809 out
810}
811
812fn rebase_body_paths(body: &str, rebase: Rebase) -> String {
819 if matches!(rebase, Rebase::Keep) {
820 return body.to_owned();
821 }
822 let Ok(file) = syn::parse_file(body) else {
823 return body.to_owned();
824 };
825 let mut visitor = BodyPathEdits {
826 rebase,
827 edits: Vec::new(),
828 };
829 syn::visit::Visit::visit_file(&mut visitor, &file);
830 apply_replacements(body, &visitor.edits)
831}
832
833struct BodyPathEdits {
834 rebase: Rebase,
835 edits: Vec<(std::ops::Range<usize>, &'static str)>,
836}
837
838impl<'ast> syn::visit::Visit<'ast> for BodyPathEdits {
839 fn visit_path(&mut self, path: &'ast syn::Path) {
840 if path.leading_colon.is_none() && path.segments.len() > 1 {
841 collect_anchor_edit(&path.segments[0].ident, self.rebase, &mut self.edits);
842 }
843 syn::visit::visit_path(self, path);
844 }
845
846 fn visit_item_use(&mut self, item: &'ast syn::ItemUse) {
847 if item.leading_colon.is_none() {
848 collect_rebase_edits(&item.tree, self.rebase, &mut self.edits);
849 }
850 }
851
852 fn visit_macro(&mut self, mac: &'ast syn::Macro) {
853 syn::visit::Visit::visit_path(self, &mac.path);
854 let Some(name) = mac.path.segments.last().map(|segment| &segment.ident) else {
855 return;
856 };
857 let is_standard_path = mac.path.segments.len() == 1
858 || (mac.path.segments.len() == 2
859 && mac
860 .path
861 .segments
862 .first()
863 .is_some_and(|segment| segment.ident == "std" || segment.ident == "core"));
864 if !is_standard_path
865 || !matches!(
866 name.to_string().as_str(),
867 "assert"
868 | "assert_eq"
869 | "assert_ne"
870 | "debug_assert"
871 | "debug_assert_eq"
872 | "debug_assert_ne"
873 | "format"
874 | "format_args"
875 | "format_args_nl"
876 | "panic"
877 | "print"
878 | "println"
879 | "eprint"
880 | "eprintln"
881 | "write"
882 | "writeln"
883 )
884 {
885 return;
886 }
887 let parser = syn::punctuated::Punctuated::<syn::Expr, syn::Token![,]>::parse_terminated;
890 let Ok(arguments) = parser.parse2(mac.tokens.clone()) else {
891 return;
892 };
893 for argument in &arguments {
894 syn::visit::Visit::visit_expr(self, argument);
895 }
896 }
897
898 fn visit_attribute(&mut self, _: &'ast syn::Attribute) {}
899
900 fn visit_visibility(&mut self, _: &'ast syn::Visibility) {}
901}
902
903fn select_imports(
909 import_chunks: &[usize],
910 chunks: &[crate::Chunk],
911 refs: &Refs,
912 rebase: Rebase,
913) -> String {
914 let mut out = String::new();
915 for &i in import_chunks {
916 let text = chunks[i].text.as_str();
917 if refs.keeps(&import_provided_names(text)) {
918 out.push_str(&rebase_import(text, rebase));
919 }
920 }
921 out
922}
923
924fn import_provided_names(chunk_text: &str) -> Option<std::collections::BTreeSet<String>> {
928 match syn::parse_str::<syn::Item>(chunk_text).ok()? {
929 syn::Item::Use(u) => {
930 let mut names = std::collections::BTreeSet::new();
931 let mut name_less = false;
932 collect_use_names(&u.tree, &mut names, &mut name_less);
933 if name_less { None } else { Some(names) }
934 }
935 syn::Item::ExternCrate(c) => {
936 let name = c
937 .rename
938 .map(|(_, id)| id.to_string())
939 .unwrap_or_else(|| c.ident.to_string());
940 Some(std::collections::BTreeSet::from([name]))
941 }
942 _ => None,
944 }
945}
946
947fn collect_use_names(
950 tree: &syn::UseTree,
951 names: &mut std::collections::BTreeSet<String>,
952 name_less: &mut bool,
953) {
954 match tree {
955 syn::UseTree::Path(p) => collect_use_names(&p.tree, names, name_less),
956 syn::UseTree::Name(n) => {
957 names.insert(n.ident.to_string());
958 }
959 syn::UseTree::Rename(r) => {
960 if r.rename == "_" {
961 *name_less = true;
962 } else {
963 names.insert(r.rename.to_string());
964 }
965 }
966 syn::UseTree::Glob(_) => *name_less = true,
967 syn::UseTree::Group(g) => {
968 for item in &g.items {
969 collect_use_names(item, names, name_less);
970 }
971 }
972 }
973}
974
975fn extract_mod(chunk_text: &str) -> Option<(String, String)> {
983 let Ok(syn::Item::Mod(item)) = syn::parse_str::<syn::Item>(chunk_text) else {
984 return None;
985 };
986 let (brace, _) = item.content.as_ref()?;
987 let ident_end = item.ident.span().byte_range().end;
988 let decl = format!("{};\n", &chunk_text[..ident_end]);
989
990 let open_end = brace.span.open().byte_range().end;
991 let close_start = brace.span.close().byte_range().start;
992 let inner = &chunk_text[open_end..close_start];
993 let inner = inner.strip_prefix('\n').unwrap_or(inner);
995 let mut inner = inner.trim_end_matches([' ', '\t']).to_owned();
996 if !inner.is_empty() && !inner.ends_with('\n') {
997 inner.push('\n');
998 }
999 Some((decl, inner))
1000}
1001
1002fn split_nested_mod(name: &str, inner: &str, max_loc: usize) -> (Vec<OutputFile>, Vec<String>) {
1011 let Ok(exploded) = crate::explode(inner) else {
1012 return (
1013 vec![OutputFile {
1014 path: format!("{name}.rs"),
1015 contents: inner.to_owned(),
1016 loc: loc(inner),
1017 }],
1018 vec![format!("{name} (could not parse for nested split)")],
1019 );
1020 };
1021 let row = |i: usize| {
1022 exploded
1023 .manifest
1024 .rows
1025 .iter()
1026 .find(|r| r.chunk_index == i)
1027 .unwrap()
1028 };
1029
1030 let header_estimate: usize = exploded
1037 .manifest
1038 .rows
1039 .iter()
1040 .filter(|r| matches!(r.kind.as_str(), "use" | "preamble" | "extern_crate"))
1041 .map(|r| r.loc)
1042 .sum();
1043 let item_budget = max_loc
1044 .saturating_sub(header_estimate + PART_OVERHEAD)
1045 .max(1);
1046 let plan = plan_split(&exploded, item_budget);
1047 let is_super_glob = |t: &str| t.replace(' ', "").contains("usesuper::*;");
1048 let group_header: String = plan
1051 .header
1052 .iter()
1053 .map(|&i| exploded.chunks[i].text.as_str())
1054 .filter(|t| !is_super_glob(t))
1055 .map(|t| rebase_import(t, Rebase::Nested))
1056 .collect();
1057
1058 let mut files = Vec::new();
1059 let mut decls = String::new();
1060 let mut reexports = String::new();
1061 let mut still = Vec::new();
1062 for (idx, part) in plan.parts.iter().enumerate() {
1063 let module = format!("g{idx:02}");
1064 let mut body = String::new();
1065 for &ci in &part.chunk_indices {
1066 body.push_str(&rebase_body_paths(
1067 &bump_visibility(exploded.chunks[ci].text.as_str()),
1068 Rebase::Nested,
1069 ));
1070 }
1071 let contents = format!("{group_header}\nuse super::*;\n\n{body}");
1072 files.push(OutputFile {
1073 path: format!("{name}/{module}.rs"),
1074 loc: loc(&contents),
1075 contents,
1076 });
1077 decls.push_str(&format!("mod {module};\n"));
1078 reexports.push_str(&format!("pub(crate) use {module}::*;\n"));
1079 }
1080 for &mod_index in &plan.mods {
1081 let chunk = exploded.chunks[mod_index].text.as_str();
1082 let mod_name = row(mod_index).name.as_str();
1083 let Some((decl, mod_inner)) = extract_mod(chunk) else {
1084 decls.push_str(chunk);
1085 continue;
1086 };
1087 decls.push_str(&decl);
1088 let mod_loc = loc(&mod_inner);
1089 if mod_loc < max_loc {
1090 files.push(OutputFile {
1091 path: format!("{name}/{mod_name}.rs"),
1092 contents: mod_inner,
1093 loc: mod_loc,
1094 });
1095 } else {
1096 let (nested, nested_still) = split_nested_mod(mod_name, &mod_inner, max_loc);
1097 files.extend(nested.into_iter().map(|mut f| {
1098 f.path = format!("{name}/{}", f.path);
1099 f
1100 }));
1101 still.extend(
1102 nested_still
1103 .into_iter()
1104 .map(|message| format!("{name}::{message}")),
1105 );
1106 }
1107 }
1108 for over in &plan.oversized {
1109 still.push(format!(
1110 "{name}::{} ({} LOC {}, leaf needs manual extraction)",
1111 over.name, over.loc, over.kind
1112 ));
1113 }
1114
1115 let root_kept: String = plan
1116 .root_items
1117 .iter()
1118 .map(|&i| exploded.chunks[i].text.as_str())
1119 .collect();
1120 let modrs = format!("pub(crate) use super::*;\n\n{root_kept}{decls}\n{reexports}");
1121 files.insert(
1122 0,
1123 OutputFile {
1124 path: format!("{name}/mod.rs"),
1125 loc: loc(&modrs),
1126 contents: modrs,
1127 },
1128 );
1129 (files, still)
1130}
1131
1132#[cfg(test)]
1133mod tests {
1134 use super::*;
1135 use crate::{explode, plan_split};
1136
1137 #[test]
1138 fn splits_a_flat_bin_root_into_cohesive_under_budget_modules() {
1139 let mut src = String::from("use std::fmt;\n\n");
1141 for k in 0..4 {
1142 for n in 0..4 {
1143 let call = if n < 3 {
1144 format!(" g{k}_{}();\n", n + 1)
1145 } else {
1146 String::new()
1147 };
1148 src.push_str(&format!("fn g{k}_{n}() {{\n{call}}}\n\n"));
1149 }
1150 }
1151 src.push_str("fn main() {\n g0_0();\n}\n");
1152 let exploded = explode(&src).unwrap();
1153 let max_loc = 60;
1154 let out = split_bin(&exploded, max_loc, "main");
1155
1156 assert!(out.still_oversized.is_empty());
1157 for f in &out.files {
1158 assert!(f.loc < max_loc, "{} over budget: {}", f.path, f.loc);
1159 }
1160 let root = out.files.iter().find(|f| f.path == "main.rs").unwrap();
1161 assert!(root.contents.contains("fn main"), "main stays at root");
1162 assert!(root.contents.contains("pub(crate) use "));
1163 let part = out.files.iter().find(|f| f.path != "main.rs").unwrap();
1164 assert!(part.contents.contains("use crate::*;"));
1165 assert!(part.contents.contains("pub(crate) fn "));
1166 let g0 = out
1168 .files
1169 .iter()
1170 .find(|f| f.contents.contains("fn g0_1"))
1171 .unwrap();
1172 for n in 0..4 {
1173 assert!(
1174 g0.contents.contains(&format!("fn g0_{n}")),
1175 "g0 group together"
1176 );
1177 }
1178 }
1179
1180 #[test]
1181 fn oversized_mod_is_extracted_to_its_own_file() {
1182 let mut inner = String::new();
1183 for i in 0..10 {
1184 inner.push_str(&format!(" fn t{i}() {{}}\n"));
1185 }
1186 let src = format!("fn main() {{}}\n\n#[cfg(test)]\nmod tests {{\n{inner}}}\n");
1187 let exploded = explode(&src).unwrap();
1188 let plan = plan_split(&exploded, 6);
1189 let out = reassemble_bin(&exploded, &plan, "main", 1000);
1191 let tests = out.files.iter().find(|f| f.path == "tests.rs").unwrap();
1192 assert!(tests.contents.contains("fn t0()"));
1193 assert!(!tests.contents.contains("mod tests"));
1194 let root = out.files.iter().find(|f| f.path == "main.rs").unwrap();
1195 assert!(root.contents.contains("#[cfg(test)]\nmod tests;"));
1196 }
1197
1198 #[test]
1199 fn an_over_ceiling_test_mod_is_nested_into_a_subdir() {
1200 let mut inner =
1202 String::from(" use super::*;\n\n fn helper() -> i32 {\n 1\n }\n");
1203 for i in 0..30 {
1204 inner.push_str(&format!(
1205 "\n #[test]\n fn t{i}() {{\n assert_eq!(helper(), 1);\n }}\n"
1206 ));
1207 }
1208 let src = format!(
1209 "pub fn helper() -> i32 {{\n 1\n}}\n\nfn main() {{}}\n\n#[cfg(test)]\nmod tests {{\n{inner}}}\n"
1210 );
1211 let exploded = explode(&src).unwrap();
1212 let out = split_bin(&exploded, 60, "main");
1213
1214 let mod_rs = out.files.iter().find(|f| f.path == "tests/mod.rs").unwrap();
1216 assert!(
1217 mod_rs.contents.contains("pub(crate) use super::*;"),
1218 "forwards the parent"
1219 );
1220 assert!(mod_rs.contents.contains("mod g00;"));
1221 assert!(out.files.iter().any(|f| f.path == "tests/g00.rs"));
1222 for f in &out.files {
1223 assert!(f.loc < 60, "{} over ceiling: {}", f.path, f.loc);
1224 }
1225 let root = out.files.iter().find(|f| f.path == "main.rs").unwrap();
1226 assert!(root.contents.contains("#[cfg(test)]\nmod tests;"));
1227 }
1228
1229 #[test]
1230 fn split_mod_uses_super_and_preserves_pub_api() {
1231 let src = "use std::fmt;\n\npub fn alpha() -> i32 {\n beta()\n}\n\nfn beta() -> i32 {\n 1\n}\n";
1233 let exploded = explode(src).unwrap();
1234 let out = split_mod(&exploded, 10_000, "mod");
1235
1236 let root = out.files.iter().find(|f| f.path == "mod.rs").unwrap();
1237 assert!(
1238 root.contents.contains("pub use "),
1239 "pub re-export preserves the API"
1240 );
1241 assert!(
1242 !root.contents.contains("pub(crate) use "),
1243 "a single `pub use *` caps each item at its own visibility — no \
1244 redundant second `pub(crate) use` line"
1245 );
1246
1247 let sub = out.files.iter().find(|f| f.path != "mod.rs").unwrap();
1248 assert!(
1249 sub.contents.contains("use super::*;"),
1250 "siblings reached via super, not crate"
1251 );
1252 assert!(!sub.contents.contains("use crate::*;"));
1253 assert!(sub.contents.contains("pub fn alpha"), "pub item stays pub");
1254 assert!(
1255 sub.contents.contains("pub(crate) fn beta"),
1256 "private bumped to pub(crate)"
1257 );
1258 }
1259
1260 #[test]
1263 fn file_module_places_submodules_in_a_subdir() {
1264 let mut src = String::new();
1268 for k in 0..3 {
1269 src.push_str(&format!("pub fn item{k}() -> i32 {{\n {k}\n}}\n\n"));
1271 }
1272 let exploded = explode(&src).unwrap();
1273 let out = split_mod(&exploded, 10_000, "wstest");
1274
1275 let root = out.files.iter().find(|f| f.path == "wstest.rs").unwrap();
1276 assert!(
1277 root.contents.contains("mod item0;"),
1278 "root file-module declares the sub-modules"
1279 );
1280 let subs: Vec<&str> = out
1281 .files
1282 .iter()
1283 .map(|f| f.path.as_str())
1284 .filter(|p| *p != "wstest.rs")
1285 .collect();
1286 assert!(!subs.is_empty(), "produced sub-module files");
1287 for path in &subs {
1288 assert!(
1289 path.starts_with("wstest/"),
1290 "sub-module {path} must live under wstest/ (file-module subdir)"
1291 );
1292 }
1293 }
1294
1295 #[test]
1296 fn mod_rs_keeps_submodules_as_siblings() {
1297 let mut src = String::new();
1299 for k in 0..3 {
1300 src.push_str(&format!("pub fn item{k}() -> i32 {{\n {k}\n}}\n\n"));
1301 }
1302 let exploded = explode(&src).unwrap();
1303 let out = split_mod(&exploded, 10_000, "mod");
1304 for f in &out.files {
1305 assert!(
1306 !f.path.contains('/'),
1307 "mod.rs sub-modules are siblings, not in a subdir: {}",
1308 f.path
1309 );
1310 }
1311 }
1312
1313 #[test]
1316 fn private_only_module_reexports_pub_crate_not_pub() {
1317 let src = "fn helper() -> i32 {\n leaf()\n}\n\nfn leaf() -> i32 {\n 1\n}\n";
1320 let exploded = explode(src).unwrap();
1321 let out = split_mod(&exploded, 10_000, "mod");
1322 let root = out.files.iter().find(|f| f.path == "mod.rs").unwrap();
1323 assert!(
1324 root.contents.contains("pub(crate) use "),
1325 "all-private module re-exports pub(crate):\n{}",
1326 root.contents
1327 );
1328 assert!(
1329 !root.contents.contains("pub use "),
1330 "no `pub use` for a module with no public item"
1331 );
1332 }
1333
1334 #[test]
1335 fn impl_only_module_is_declared_but_not_reexported() {
1336 let src = "pub fn api() -> i32 {\n 1\n}\n\nimpl Worker for External {\n fn run(&self) {}\n}\n";
1340 let exploded = explode(src).unwrap();
1341 let out = split_mod(&exploded, 10_000, "mod");
1342 let root = out.files.iter().find(|f| f.path == "mod.rs").unwrap();
1343
1344 let reexports = root
1346 .contents
1347 .lines()
1348 .filter(|l| l.contains(" use ") && l.trim_end().ends_with("::*;"))
1349 .count();
1350 assert_eq!(
1351 reexports, 1,
1352 "only the nameable module is re-exported:\n{}",
1353 root.contents
1354 );
1355 assert!(root.contents.contains("pub use api::*;"));
1356 assert_eq!(
1357 root.contents.matches("mod ").count(),
1358 2,
1359 "both modules are still declared:\n{}",
1360 root.contents
1361 );
1362 }
1363
1364 #[test]
1367 fn bin_public_entry_fn_is_reexported_pub_so_it_stays_crate_visible() {
1368 let src = "fn helper() -> i32 {\n 1\n}\n\npub fn run() -> i32 {\n helper()\n}\n";
1372 let exploded = explode(src).unwrap();
1373 let out = split_bin(&exploded, 10_000, "lib");
1374
1375 let root = out.files.iter().find(|f| f.path == "lib.rs").unwrap();
1376 assert!(
1377 root.contents.contains("pub use "),
1378 "public `run` re-exported at pub keeps `crate::run` visible:\n{}",
1379 root.contents
1380 );
1381 assert!(
1382 !root.contents.contains("pub(crate) use "),
1383 "the module has a pub item, so one `pub use *` suffices (no double line)"
1384 );
1385 }
1386
1387 #[test]
1390 fn crate_preamble_stays_at_root_not_copied_into_parts() {
1391 let src = "//! crate docs\n#![allow(dead_code)]\n\npub fn alpha() -> i32 {\n beta()\n}\n\nfn beta() -> i32 {\n 1\n}\n";
1394 let exploded = explode(src).unwrap();
1395 let out = split_bin(&exploded, 10_000, "lib");
1396
1397 let root = out.files.iter().find(|f| f.path == "lib.rs").unwrap();
1398 assert!(
1399 root.contents.contains("#![allow(dead_code)]"),
1400 "preamble stays at the root"
1401 );
1402 for f in &out.files {
1403 if f.path != "lib.rs" {
1404 assert!(
1405 !f.contents.contains("#!["),
1406 "part {} must not carry crate-level inner attrs",
1407 f.path
1408 );
1409 assert!(
1410 !f.contents.contains("//! crate docs"),
1411 "part {} must not duplicate the crate docs",
1412 f.path
1413 );
1414 }
1415 }
1416 }
1417
1418 #[test]
1421 fn parts_copy_only_the_imports_they_reference() {
1422 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";
1425 let exploded = explode(src).unwrap();
1426 let out = split_mod(&exploded, 10_000, "mod");
1427
1428 let alpha = out
1429 .files
1430 .iter()
1431 .find(|f| f.contents.contains("fn alpha"))
1432 .unwrap();
1433 let bravo = out
1434 .files
1435 .iter()
1436 .find(|f| f.contents.contains("fn bravo"))
1437 .unwrap();
1438 assert!(
1439 alpha.contents.contains("use std::fmt::Display;"),
1440 "alpha references Display:\n{}",
1441 alpha.contents
1442 );
1443 assert!(
1444 !alpha.contents.contains("HashMap"),
1445 "alpha never names HashMap -> import dropped:\n{}",
1446 alpha.contents
1447 );
1448 assert!(
1449 !bravo.contents.contains("use std::fmt::Display;"),
1450 "bravo references nothing from the header:\n{}",
1451 bravo.contents
1452 );
1453 assert!(!bravo.contents.contains("HashMap"));
1454 }
1455
1456 #[test]
1457 fn globs_and_anonymous_trait_imports_are_always_copied() {
1458 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";
1461 let exploded = explode(src).unwrap();
1462 let out = split_mod(&exploded, 10_000, "mod");
1463 for f in &out.files {
1464 if f.path == "mod.rs" {
1465 continue;
1466 }
1467 assert!(
1468 f.contents.contains("use std::fmt::Write as _;"),
1469 "{} must keep the `as _` trait import:\n{}",
1470 f.path,
1471 f.contents
1472 );
1473 assert!(
1474 f.contents.contains("use std::prelude::v1::*;"),
1475 "{} must keep the glob import:\n{}",
1476 f.path,
1477 f.contents
1478 );
1479 }
1480 }
1481
1482 #[test]
1483 fn item_less_root_keeps_no_imports() {
1484 let src = "use std::collections::HashMap;\n\npub fn solo(m: HashMap<u8, u8>) {\n let _ = m;\n}\n";
1487 let exploded = explode(src).unwrap();
1488 let out = split_mod(&exploded, 10_000, "mod");
1489
1490 let root = out.files.iter().find(|f| f.path == "mod.rs").unwrap();
1491 assert!(
1492 !root.contents.contains("use std::collections::HashMap;"),
1493 "item-less root drops imports it doesn't use:\n{}",
1494 root.contents
1495 );
1496 let part = out.files.iter().find(|f| f.path != "mod.rs").unwrap();
1497 assert!(
1498 part.contents.contains("use std::collections::HashMap;"),
1499 "the part that uses HashMap keeps it"
1500 );
1501 }
1502
1503 #[test]
1506 fn source_pub_use_reexports_stay_pub_use_at_the_root() {
1507 let src = "mod helpers;\n\npub use helpers::Helper;\n\nuse std::fmt::Display;\n\n\
1511 pub fn api(d: &dyn Display) {\n let _ = d;\n}\n";
1512 let exploded = explode(src).unwrap();
1513 let out = split_bin(&exploded, 10_000, "lib");
1514
1515 let root = out.files.iter().find(|f| f.path == "lib.rs").unwrap();
1516 assert!(
1517 root.contents.contains("mod helpers;"),
1518 "content-less mod decl stays at the root (it binds helpers.rs):\n{}",
1519 root.contents
1520 );
1521 assert!(
1522 root.contents.contains("pub use helpers::Helper;"),
1523 "source re-export stays verbatim at the root:\n{}",
1524 root.contents
1525 );
1526 assert!(
1527 !root.contents.contains("pub(crate) use helpers"),
1528 "a source re-export must never be demoted"
1529 );
1530 for f in &out.files {
1531 if f.path != "lib.rs" {
1532 assert!(
1533 !f.contents.contains("pub use helpers::Helper;"),
1534 "{} must not carry the root's re-export",
1535 f.path
1536 );
1537 assert!(
1538 !f.contents.contains("mod helpers;"),
1539 "{} must not carry the root's mod decl (would look for a \
1540 sibling subdir file)",
1541 f.path
1542 );
1543 }
1544 }
1545 }
1546
1547 #[test]
1550 fn under_budget_cfg_test_mod_is_extracted_whole_with_its_gate() {
1551 let src = "pub fn alpha() -> i32 {\n 1\n}\n\n\
1556 #[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n \
1557 fn smoke() {\n assert_eq!(alpha(), 1);\n }\n}\n";
1558 let exploded = explode(src).unwrap();
1559 let out = split_bin(&exploded, 10_000, "lib");
1561
1562 let root = out.files.iter().find(|f| f.path == "lib.rs").unwrap();
1563 assert!(
1564 root.contents.contains("#[cfg(test)]\nmod tests;"),
1565 "the gate must travel to the root declaration:\n{}",
1566 root.contents
1567 );
1568 assert!(
1569 !root.contents.contains("use tests::*"),
1570 "an extracted mod is declared, not glob re-exported:\n{}",
1571 root.contents
1572 );
1573 let tests = out.files.iter().find(|f| f.path == "tests.rs").unwrap();
1574 assert!(tests.contents.contains("fn smoke()"));
1575 assert!(
1576 !tests.contents.contains("mod tests"),
1577 "the body is unwrapped — no double nesting:\n{}",
1578 tests.contents
1579 );
1580 }
1581
1582 #[test]
1583 fn pub_mod_keeps_its_visibility_on_the_extracted_decl() {
1584 let src = "pub fn api() -> i32 {\n 1\n}\n\n\
1585 /// Config store.\npub mod config {\n pub fn get() -> i32 {\n 2\n }\n}\n";
1586 let exploded = explode(src).unwrap();
1587 let out = split_bin(&exploded, 10_000, "lib");
1588
1589 let root = out.files.iter().find(|f| f.path == "lib.rs").unwrap();
1590 assert!(
1591 root.contents.contains("/// Config store.\npub mod config;"),
1592 "docs and `pub` travel to the declaration:\n{}",
1593 root.contents
1594 );
1595 let config = out.files.iter().find(|f| f.path == "config.rs").unwrap();
1596 assert!(config.contents.contains("pub fn get()"));
1597 }
1598
1599 #[test]
1600 fn part_colliding_with_an_extracted_mod_name_is_renamed() {
1601 let src = "fn tests() -> i32 {\n 1\n}\n\n\
1605 #[cfg(test)]\nmod tests {\n fn smoke() {}\n}\n";
1606 let exploded = explode(src).unwrap();
1607 let out = split_bin(&exploded, 10_000, "lib");
1608
1609 let paths: Vec<&str> = out.files.iter().map(|f| f.path.as_str()).collect();
1610 assert!(paths.contains(&"tests.rs"), "extracted mod file: {paths:?}");
1611 assert!(paths.contains(&"tests_.rs"), "renamed part file: {paths:?}");
1612 let root = out.files.iter().find(|f| f.path == "lib.rs").unwrap();
1613 assert!(root.contents.contains("mod tests_;"));
1614 assert!(root.contents.contains("#[cfg(test)]\nmod tests;"));
1615 }
1616
1617 #[test]
1618 fn root_keeps_imports_an_extracted_mod_body_references() {
1619 let src = "use std::collections::HashMap;\n\n\
1622 pub fn alpha() -> i32 {\n 1\n}\n\n\
1623 #[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n \
1624 fn smoke() {\n let _m: HashMap<u8, u8> = HashMap::new();\n \
1625 assert_eq!(alpha(), 1);\n }\n}\n";
1626 let exploded = explode(src).unwrap();
1627 let out = split_bin(&exploded, 10_000, "lib");
1628
1629 let root = out.files.iter().find(|f| f.path == "lib.rs").unwrap();
1630 assert!(
1631 root.contents.contains("use std::collections::HashMap;"),
1632 "root must keep imports the extracted body names:\n{}",
1633 root.contents
1634 );
1635 }
1636
1637 #[test]
1638 fn bin_root_keeps_only_imports_main_references() {
1639 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";
1642 let exploded = explode(src).unwrap();
1643 let out = split_bin(&exploded, 10_000, "main");
1644
1645 let root = out.files.iter().find(|f| f.path == "main.rs").unwrap();
1646 assert!(
1647 root.contents.contains("use std::process::exit;"),
1648 "main references exit:\n{}",
1649 root.contents
1650 );
1651 assert!(
1652 !root.contents.contains("HashMap"),
1653 "main never names HashMap (worker does) -> dropped from root:\n{}",
1654 root.contents
1655 );
1656 }
1657
1658 #[test]
1661 fn nested_module_part_imports_gain_one_super_level() {
1662 let src = r#"use super::sibling::Thing;
1666use super::{alpha, beta::Gamma};
1667use self::local::X;
1668use super::*;
1669use crate::top::Absolute;
1670use std::fmt::Display;
1671
1672pub fn one(t: Thing, g: Gamma, x: X, d: &dyn Display) -> i32 {
1673 let _: Option<Absolute> = None;
1674 let _ = (t, g, x, d);
1675 alpha()
1676}
1677
1678#[cfg(test)]
1679mod tests {
1680 use super::*;
1681
1682 #[test]
1683 fn smoke() {
1684 let _ = Thing;
1685 }
1686}
1687"#;
1688 let exploded = explode(src).unwrap();
1689 let out = split_mod(&exploded, 10_000, "foo");
1690
1691 let part = out.files.iter().find(|f| f.path == "foo/one.rs").unwrap();
1692 for expected in [
1693 "use super::super::sibling::Thing;",
1694 "use super::super::{alpha, beta::Gamma};",
1695 "use super::local::X;",
1696 "use super::super::*;",
1697 "use crate::top::Absolute;",
1699 "use std::fmt::Display;",
1700 ] {
1701 assert!(
1702 part.contents.contains(expected),
1703 "part must carry `{expected}`:\n{}",
1704 part.contents
1705 );
1706 }
1707 assert_eq!(
1710 part.contents
1711 .lines()
1712 .filter(|l| l.trim() == "use super::*;")
1713 .count(),
1714 1,
1715 "exactly one tool-added sibling glob:\n{}",
1716 part.contents
1717 );
1718
1719 let root = out.files.iter().find(|f| f.path == "foo.rs").unwrap();
1721 assert!(
1722 root.contents.contains("use super::sibling::Thing;"),
1723 "root header stays byte-unchanged:\n{}",
1724 root.contents
1725 );
1726 assert!(
1727 !root.contents.contains("super::super"),
1728 "the root did not move — nothing to re-anchor:\n{}",
1729 root.contents
1730 );
1731 }
1732
1733 #[test]
1734 fn crate_root_part_imports_rewrite_self_to_crate() {
1735 let src = "use self::helpers::Helper;\nuse std::fmt::Display;\n\n\
1738 pub fn api(h: Helper, d: &dyn Display) {\n let _ = (h, d);\n}\n";
1739 let exploded = explode(src).unwrap();
1740 let out = split_bin(&exploded, 10_000, "lib");
1741
1742 let part = out.files.iter().find(|f| f.path != "lib.rs").unwrap();
1743 assert!(
1744 part.contents.contains("use crate::helpers::Helper;"),
1745 "`self::` re-anchors to `crate::` below a crate root:\n{}",
1746 part.contents
1747 );
1748 assert!(part.contents.contains("use std::fmt::Display;"));
1749 }
1750
1751 #[test]
1752 fn nested_split_group_headers_gain_one_super_level() {
1753 let mut inner = String::from(
1756 " use super::outer::Thing;\n use self::inner::Y;\n\n\
1757 \x20 fn helper() -> i32 {\n 1\n }\n",
1758 );
1759 for i in 0..30 {
1760 inner.push_str(&format!(
1761 "\n #[test]\n fn t{i}() {{\n \
1762 let _: Option<Thing> = None;\n let _: Option<Y> = None;\n \
1763 assert_eq!(helper(), 1);\n }}\n"
1764 ));
1765 }
1766 let src = format!(
1767 "pub fn helper() -> i32 {{\n 1\n}}\n\nfn main() {{}}\n\n\
1768 #[cfg(test)]\nmod tests {{\n{inner}}}\n"
1769 );
1770 let exploded = explode(&src).unwrap();
1771 let out = split_bin(&exploded, 60, "main");
1772
1773 let group = out.files.iter().find(|f| f.path == "tests/g00.rs").unwrap();
1774 assert!(
1775 group.contents.contains("use super::super::outer::Thing;"),
1776 "group header `super::` gains a level:\n{}",
1777 group.contents
1778 );
1779 assert!(
1780 group.contents.contains("use super::inner::Y;"),
1781 "group header `self::` becomes `super::`:\n{}",
1782 group.contents
1783 );
1784 }
1785
1786 #[test]
1787 fn extracted_inline_mod_keeps_its_own_super_paths() {
1788 let src = "pub fn helper() -> i32 {\n 1\n}\n\n\
1792 #[cfg(test)]\nmod tests {\n use super::helper;\n\n #[test]\n \
1793 fn smoke() {\n assert_eq!(helper(), 1);\n }\n}\n";
1794 let exploded = explode(src).unwrap();
1795 let out = split_mod(&exploded, 10_000, "foo");
1796
1797 let tests = out.files.iter().find(|f| f.path == "foo/tests.rs").unwrap();
1798 assert!(
1799 tests.contents.contains("use super::helper;"),
1800 "an extracted mod keeps its own module level:\n{}",
1801 tests.contents
1802 );
1803 assert!(
1804 !tests.contents.contains("super::super"),
1805 "the extracted body did not change module level:\n{}",
1806 tests.contents
1807 );
1808 }
1809
1810 #[test]
1813 fn include_paths_in_moved_bodies_are_rebased_one_directory_deeper() {
1814 let src = r#"pub fn doc() -> &'static str {
1815 include_str!("../docs/a.md")
1816}
1817
1818pub fn blob() -> &'static [u8] {
1819 include_bytes!("fixtures/b.bin")
1820}
1821
1822pub fn built() -> &'static str {
1823 include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/x"))
1824}
1825
1826pub fn rooted() -> &'static str {
1827 include_str!("/etc/x")
1828}
1829"#;
1830 let exploded = explode(src).unwrap();
1831 let out = split_mod(&exploded, 10_000, "foo");
1832
1833 let moved: String = out
1834 .files
1835 .iter()
1836 .filter(|f| f.path != "foo.rs")
1837 .map(|f| f.contents.as_str())
1838 .collect();
1839 assert!(
1840 moved.contains(r#"include_str!("../../docs/a.md")"#),
1841 "a relative include! path gains one `../`:\n{moved}"
1842 );
1843 assert!(
1844 moved.contains(r#"include_bytes!("../fixtures/b.bin")"#),
1845 "a bare relative path is prefixed too:\n{moved}"
1846 );
1847 assert!(
1848 moved.contains(r#"include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/x"))"#),
1849 "a non-literal argument is byte-unchanged:\n{moved}"
1850 );
1851 assert!(
1852 moved.contains(r#"include_str!("/etc/x")"#),
1853 "an absolute path is byte-unchanged:\n{moved}"
1854 );
1855 }
1856
1857 #[test]
1858 fn include_paths_are_untouched_when_the_file_stays_in_its_directory() {
1859 let src = "fn worker() -> &'static str {\n include_str!(\"../docs/w.md\")\n}\n\n\
1862 fn main() {\n let _ = include_str!(\"../docs/m.md\");\n \
1863 let _ = worker();\n}\n";
1864 let exploded = explode(src).unwrap();
1865 let out = split_bin(&exploded, 10_000, "main");
1866
1867 for f in &out.files {
1868 assert!(
1869 !f.contents.contains("../../docs/"),
1870 "{} stayed in the source directory:\n{}",
1871 f.path,
1872 f.contents
1873 );
1874 }
1875 let root = out.files.iter().find(|f| f.path == "main.rs").unwrap();
1876 assert!(root.contents.contains(r#"include_str!("../docs/m.md")"#));
1877 }
1878
1879 #[test]
1882 fn review_body_paths_preserve_opaque_macro_and_attribute_tokens() {
1883 let src = r#"#[metadata(super::Thing)]
1884pub fn probe() -> super::Thing {
1885 macro_rules! data { () => { self::Thing }; }
1886 let _ = stringify!(super::Thing);
1887 let _ = stringify! { self::Thing };
1888 let _ = custom!(super::Thing, (self::Thing), stringify!(super::Thing));
1889 let _ = super::factory!(self::Thing);
1890 <super::Thing as self::Trait>::make()
1891}
1892"#;
1893 let expected = src
1894 .replace("-> super::Thing", "-> super::super::Thing")
1895 .replace("super::factory!", "super::super::factory!")
1896 .replace(
1897 "<super::Thing as self::Trait>",
1898 "<super::super::Thing as super::Trait>",
1899 );
1900 assert_eq!(rebase_body_paths(src, Rebase::Nested), expected);
1901 assert_eq!(rebase_body_paths(src, Rebase::Keep), src);
1902 }
1903
1904 #[test]
1905 fn review_standard_expression_macro_arguments_rebase_paths() {
1906 let src = r#"fn probe() {
1907 assert_eq!(super::Thing::value(), self::value(), "{}", super::message());
1908 let _ = format!("{}", super::Thing::value());
1909 let _ = custom!(super::Thing, self::Thing);
1910 let _ = custom::assert_eq!(super::Thing, self::Thing);
1911 let _ = stringify!(super::Thing);
1912}
1913"#;
1914 let expected = src
1915 .replace("super::Thing::value()", "super::super::Thing::value()")
1916 .replace("self::value()", "super::value()")
1917 .replace("super::message()", "super::super::message()");
1918 assert_eq!(rebase_body_paths(src, Rebase::Nested), expected);
1919 }
1920
1921 #[test]
1922 fn review_renamed_use_anchors_rebase_in_headers_and_bodies() {
1923 for (source, nested, root) in [
1924 (
1925 "super as parent",
1926 "super::super as parent",
1927 "super as parent",
1928 ),
1929 ("self as current", "super as current", "crate as current"),
1930 (
1931 "{super as parent, self as current, super::{self as p, Thing}}",
1932 "{super::super as parent, super as current, super::super::{self as p, Thing}}",
1933 "{super as parent, crate as current, super::{self as p, Thing}}",
1934 ),
1935 ("crate as root", "crate as root", "crate as root"),
1936 ] {
1937 for (rebase, expected) in [
1938 (Rebase::Nested, nested),
1939 (Rebase::CrateRoot, root),
1940 (Rebase::Keep, source),
1941 ] {
1942 let import =
1943 format!("// keep this comment\n#[allow(unused_imports)]\nuse {source};\n");
1944 let expected = import.replace(source, expected);
1945 assert_eq!(rebase_import(&import, rebase), expected);
1946 assert_eq!(
1947 rebase_body_paths(&format!("fn probe() {{\n{import}}}"), rebase),
1948 format!("fn probe() {{\n{expected}}}"),
1949 );
1950 }
1951 }
1952 }
1953
1954 #[test]
1955 fn review_include_literals_accept_one_optional_trailing_comma() {
1956 for name in PATH_MACROS {
1957 for literal in [r#""docs/file""#, r##"r#"docs/file"#"##] {
1958 let src = format!("fn probe() {{ {name}!({literal} /* keep */,); }}");
1959 assert_eq!(
1960 rebase_includes(&src, 2, "foo/part.rs"),
1961 src.replace("docs/file", "../../docs/file"),
1962 );
1963 for suffix in [",,", ", other", " + other"] {
1964 let src = format!("fn probe() {{ {name}!({literal}{suffix}); }}");
1965 assert_eq!(rebase_includes(&src, 1, "foo/part.rs"), src);
1966 }
1967 }
1968 }
1969 }
1970
1971 #[test]
1972 fn review_include_absolute_windows_paths_are_host_independent() {
1973 for name in PATH_MACROS {
1974 for literal in [
1975 r#""C:\\docs\\file""#,
1976 r#""z:/docs/file""#,
1977 r#"r"C:\docs\file""#,
1978 r#""\\\\server\\share\\file""#,
1979 r#"r"\\server\share\file""#,
1980 r#"r"\\?\C:\docs\file""#,
1981 r#""//server/share/file""#,
1982 r#""/docs/file""#,
1983 ] {
1984 let src = format!("fn probe() {{ {name}!({literal}); }}");
1985 assert_eq!(rebase_includes(&src, 1, "foo/part.rs"), src);
1986 }
1987 let src = format!("fn probe() {{ {name}!(\"C:relative\"); }}");
1988 assert_eq!(
1989 rebase_includes(&src, 1, "foo/part.rs"),
1990 src.replace("C:relative", "../C:relative"),
1991 );
1992 }
1993 }
1994
1995 #[test]
1996 fn moved_body_paths_gain_one_super_level() {
1997 let src = r#"pub fn probe() -> i32 {
2000 use super::alpha::Beta;
2001 let _ = Beta;
2002 let _ = super::filter::parse("x");
2003 let _ = super::super::grandparent::thing();
2004 let _ = self::local::helper();
2005 let _: super::sibling::Kind = super::sibling::make();
2006 let _ = super::sibling::Record {
2007 meta: super::meta::build("req"),
2008 };
2009 crate::top::value()
2010}
2011"#;
2012 let exploded = explode(src).unwrap();
2013 let out = split_mod(&exploded, 10_000, "foo");
2014
2015 let part = out.files.iter().find(|f| f.path == "foo/probe.rs").unwrap();
2016 for expected in [
2017 "use super::super::alpha::Beta;",
2018 r#"super::super::filter::parse("x")"#,
2019 "super::super::super::grandparent::thing()",
2021 "super::local::helper()",
2022 "let _: super::super::sibling::Kind = super::super::sibling::make();",
2025 "meta: super::super::meta::build(\"req\"),",
2026 "crate::top::value()",
2027 ] {
2028 assert!(
2029 part.contents.contains(expected),
2030 "moved body must carry `{expected}`:\n{}",
2031 part.contents
2032 );
2033 }
2034 }
2035
2036 #[test]
2037 fn moved_body_leaves_self_receivers_and_use_groups_alone() {
2038 let src = r#"pub struct Widget {
2043 pub n: i32,
2044}
2045
2046impl Widget {
2047 pub fn get(&self) -> i32 {
2048 use std::collections::{self, HashMap};
2049 let _: Option<HashMap<u8, u8>> = None;
2050 let _ = collections::BTreeMap::<u8, u8>::new();
2051 let _ = Self::make();
2052 self.n
2053 }
2054
2055 pub fn make() -> Self {
2056 Self { n: 1 }
2057 }
2058}
2059
2060pub(super) fn gated() -> i32 {
2061 1
2062}
2063"#;
2064 let exploded = explode(src).unwrap();
2065 let out = split_mod(&exploded, 10_000, "foo");
2066 let moved: String = out
2067 .files
2068 .iter()
2069 .filter(|f| f.path != "foo.rs")
2070 .map(|f| f.contents.as_str())
2071 .collect();
2072
2073 for expected in [
2074 "use std::collections::{self, HashMap};",
2075 "Self::make()",
2076 "self.n",
2077 "pub(super) fn gated",
2078 ] {
2079 assert!(
2080 moved.contains(expected),
2081 "`{expected}` is not a module anchor and must not move:\n{moved}"
2082 );
2083 }
2084 assert!(
2085 !moved.contains("pub(super::super)"),
2086 "a `pub(super)` restriction must never be mangled:\n{moved}"
2087 );
2088 }
2089
2090 #[test]
2091 fn moved_body_paths_below_a_crate_root_rewrite_self_to_crate() {
2092 let src = "pub fn entry() -> i32 {\n let _ = self::helpers::setup();\n \
2093 crate::other::value()\n}\n";
2094 let exploded = explode(src).unwrap();
2095 let out = split_bin(&exploded, 10_000, "lib");
2096
2097 let part = out.files.iter().find(|f| f.path != "lib.rs").unwrap();
2098 assert!(
2099 part.contents.contains("crate::helpers::setup()"),
2100 "`self::` below a crate root is `crate::`:\n{}",
2101 part.contents
2102 );
2103 assert!(part.contents.contains("crate::other::value()"));
2104 }
2105
2106 #[test]
2107 fn explode_never_rewrites_include_paths() {
2108 let src = "pub fn doc() -> &'static str {\n include_str!(\"../docs/a.md\")\n}\n";
2110 let exploded = explode(src).unwrap();
2111 let joined: String = exploded.chunks.iter().map(|c| c.text.as_str()).collect();
2112 assert_eq!(joined, src, "explode must stay lossless");
2113 }
2114}