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