use crate::{Exploded, SplitPlan, plan_split};
use syn::spanned::Spanned;
const PART_OVERHEAD: usize = 3;
pub fn split_bin(exploded: &Exploded, max_loc: usize, root_stem: &str) -> SplitOutput {
let header_loc: usize = exploded
.manifest
.rows
.iter()
.filter(|r| matches!(r.kind.as_str(), "use" | "preamble" | "extern_crate"))
.map(|r| r.loc)
.sum();
let item_budget = max_loc.saturating_sub(header_loc + PART_OVERHEAD).max(1);
let plan = plan_split(exploded, item_budget);
reassemble_bin(exploded, &plan, root_stem, max_loc)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OutputFile {
pub path: String,
pub contents: String,
pub loc: usize,
}
#[derive(Debug, Clone)]
pub struct SplitOutput {
pub files: Vec<OutputFile>,
pub still_oversized: Vec<String>,
}
impl SplitOutput {
pub fn max_loc(&self) -> usize {
self.files.iter().map(|f| f.loc).max().unwrap_or(0)
}
}
fn loc(text: &str) -> usize {
if text.is_empty() {
0
} else {
text.lines().count().max(1)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Topology {
Bin,
Mod,
}
pub fn split_mod(exploded: &Exploded, max_loc: usize, root_stem: &str) -> SplitOutput {
let header_loc: usize = exploded
.manifest
.rows
.iter()
.filter(|r| matches!(r.kind.as_str(), "use" | "preamble" | "extern_crate"))
.map(|r| r.loc)
.sum();
let item_budget = max_loc.saturating_sub(header_loc + PART_OVERHEAD).max(1);
let plan = plan_split(exploded, item_budget);
reassemble(exploded, &plan, root_stem, Topology::Mod, max_loc)
}
pub fn reassemble_bin(
exploded: &Exploded,
plan: &SplitPlan,
root_stem: &str,
max_loc: usize,
) -> SplitOutput {
reassemble(exploded, plan, root_stem, Topology::Bin, max_loc)
}
fn reassemble(
exploded: &Exploded,
plan: &SplitPlan,
root_stem: &str,
topology: Topology,
max_loc: usize,
) -> SplitOutput {
let sibling = match topology {
Topology::Bin => "crate",
Topology::Mod => "super",
};
let chunk_text = |i: usize| exploded.chunks[i].text.as_str();
let row = |i: usize| {
exploded
.manifest
.rows
.iter()
.find(|r| r.chunk_index == i)
.unwrap()
};
let sub_prefix = match topology {
Topology::Bin => String::new(),
Topology::Mod if matches!(root_stem, "mod" | "lib" | "main") => String::new(),
Topology::Mod => format!("{root_stem}/"),
};
let preamble: String = plan
.header
.iter()
.filter(|&&i| row(i).kind == "preamble")
.map(|&i| chunk_text(i))
.collect();
let import_chunks: Vec<usize> = plan
.header
.iter()
.copied()
.filter(|&i| row(i).kind != "preamble")
.collect();
let root_kept: String = plan.root_items.iter().map(|&i| chunk_text(i)).collect();
let mut reserved: std::collections::BTreeSet<&str> =
plan.mods.iter().map(|&i| row(i).name.as_str()).collect();
reserved.insert(root_stem);
for &i in &plan.root_items {
if row(i).kind == "mod" {
reserved.insert(row(i).name.as_str());
}
}
let mut files = Vec::new();
let mut mod_decls = String::new();
let mut reexports = String::new();
let mut still_oversized = Vec::new();
let mut root_refs = Refs::default();
for part in &plan.parts {
let module = if reserved.contains(part.name.as_str()) {
format!("{}_", part.name)
} else {
part.name.clone()
};
let mut body = String::new();
let mut root_main = None;
let mut export_vis = ExportVis::None;
let mut refs = Refs::default();
for &i in &part.chunk_indices {
let r = row(i);
if r.kind == "fn" && r.name == "main" {
root_main = Some(i);
continue;
}
export_vis = export_vis.max(item_export_vis(chunk_text(i)));
refs.add(chunk_text(i));
body.push_str(&bump_visibility(chunk_text(i)));
}
if body.trim().is_empty() {
if let Some(i) = root_main {
files.push(OutputFile {
path: String::from("__root_main__"),
contents: chunk_text(i).to_owned(),
loc: 0,
});
}
continue;
}
if let Some(i) = root_main {
files.push(OutputFile {
path: String::from("__root_main__"),
contents: chunk_text(i).to_owned(),
loc: 0,
});
}
let part_imports = select_imports(&import_chunks, &exploded.chunks, &refs);
let contents = part_file(&part_imports, sibling, &body);
mod_decls.push_str(&format!("mod {module};\n"));
if let Some(line) = reexport_line(export_vis, &module) {
reexports.push_str(&line);
}
let l = loc(&contents);
files.push(OutputFile {
path: format!("{sub_prefix}{module}.rs"),
contents,
loc: l,
});
}
for &mod_index in &plan.mods {
let chunk = chunk_text(mod_index);
let name = row(mod_index).name.as_str();
let Some((decl, inner)) = extract_mod(chunk) else {
mod_decls.push_str(chunk);
continue;
};
mod_decls.push_str(&decl);
root_refs.add_source(&inner);
let inner_loc = loc(&inner);
if inner_loc < max_loc {
files.push(OutputFile {
path: format!("{sub_prefix}{name}.rs"),
contents: inner,
loc: inner_loc,
});
} else {
let (nested, nested_still) = split_nested_mod(name, &inner, max_loc);
files.extend(nested.into_iter().map(|mut f| {
f.path = format!("{sub_prefix}{}", f.path);
f
}));
still_oversized.extend(nested_still);
}
}
for over in &plan.oversized {
still_oversized.push(format!(
"{} ({} LOC {}, manual extraction)",
over.name, over.loc, over.kind
));
}
let mut root_main = String::new();
files.retain(|f| {
if f.path == "__root_main__" {
root_main = f.contents.clone();
false
} else {
true
}
});
if !root_main.trim().is_empty() {
root_refs.add(&root_main);
}
let root_imports = if root_refs.is_empty() {
String::new()
} else {
select_imports(&import_chunks, &exploded.chunks, &root_refs)
};
let mut root = String::new();
root.push_str(&preamble);
root.push_str(&root_imports);
root.push_str(&root_kept);
root.push('\n');
root.push_str(&mod_decls);
root.push('\n');
root.push_str(&reexports);
if !root_main.is_empty() {
root.push('\n');
root.push_str(&root_main);
}
let root_loc = loc(&root);
files.insert(
0,
OutputFile {
path: format!("{root_stem}.rs"),
contents: root,
loc: root_loc,
},
);
SplitOutput {
files,
still_oversized,
}
}
fn bump_visibility(chunk_text: &str) -> String {
let Ok(item) = syn::parse_str::<syn::Item>(chunk_text) else {
return chunk_text.to_owned();
};
let mut offsets = Vec::new();
collect_vis_offsets(&item, &mut offsets);
apply_inserts(chunk_text, &offsets)
}
fn is_inherited(vis: &syn::Visibility) -> bool {
matches!(vis, syn::Visibility::Inherited)
}
fn start<T: Spanned>(node: &T) -> usize {
node.span().byte_range().start
}
fn fn_sig_start(sig: &syn::Signature) -> usize {
let mut min = start(&sig.fn_token);
if let Some(t) = &sig.constness {
min = min.min(start(t));
}
if let Some(t) = &sig.asyncness {
min = min.min(start(t));
}
if let Some(t) = &sig.unsafety {
min = min.min(start(t));
}
if let Some(abi) = &sig.abi {
min = min.min(start(&abi.extern_token));
}
min
}
fn field_offsets(fields: &syn::Fields, offsets: &mut Vec<usize>) {
for field in fields {
if is_inherited(&field.vis) {
match &field.ident {
Some(ident) => offsets.push(start(ident)),
None => offsets.push(start(&field.ty)),
}
}
}
}
fn collect_vis_offsets(item: &syn::Item, offsets: &mut Vec<usize>) {
use syn::Item;
match item {
Item::Fn(f) => {
if is_inherited(&f.vis) {
offsets.push(fn_sig_start(&f.sig));
}
}
Item::Struct(s) => {
if is_inherited(&s.vis) {
offsets.push(start(&s.struct_token));
}
field_offsets(&s.fields, offsets);
}
Item::Enum(e) => {
if is_inherited(&e.vis) {
offsets.push(start(&e.enum_token));
}
}
Item::Union(u) => {
if is_inherited(&u.vis) {
offsets.push(start(&u.union_token));
}
field_offsets(&syn::Fields::Named(u.fields.clone()), offsets);
}
Item::Const(c) if is_inherited(&c.vis) => offsets.push(start(&c.const_token)),
Item::Static(s) if is_inherited(&s.vis) => offsets.push(start(&s.static_token)),
Item::Type(t) if is_inherited(&t.vis) => offsets.push(start(&t.type_token)),
Item::Trait(t) if is_inherited(&t.vis) => offsets.push(start(&t.trait_token)),
Item::TraitAlias(t) if is_inherited(&t.vis) => offsets.push(start(&t.trait_token)),
Item::Impl(i) if i.trait_.is_none() => {
for member in &i.items {
match member {
syn::ImplItem::Fn(f) if is_inherited(&f.vis) => {
offsets.push(fn_sig_start(&f.sig))
}
syn::ImplItem::Const(c) if is_inherited(&c.vis) => {
offsets.push(start(&c.const_token))
}
syn::ImplItem::Type(t) if is_inherited(&t.vis) => {
offsets.push(start(&t.type_token))
}
_ => {}
}
}
}
_ => {}
}
}
fn apply_inserts(text: &str, offsets: &[usize]) -> String {
let mut offsets: Vec<usize> = offsets.to_vec();
offsets.sort_unstable();
offsets.dedup();
let mut out = String::with_capacity(text.len() + offsets.len() * 11);
let mut prev = 0;
for &offset in &offsets {
out.push_str(&text[prev..offset]);
out.push_str("pub(crate) ");
prev = offset;
}
out.push_str(&text[prev..]);
out
}
fn part_file(imports: &str, sibling: &str, body: &str) -> String {
if imports.trim().is_empty() {
format!("use {sibling}::*;\n\n{body}")
} else {
format!("{imports}\nuse {sibling}::*;\n\n{body}")
}
}
fn reexport_line(vis: ExportVis, module: &str) -> Option<String> {
match vis {
ExportVis::None => None,
ExportVis::Crate => Some(format!("pub(crate) use {module}::*;\n")),
ExportVis::Pub => Some(format!("pub use {module}::*;\n")),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum ExportVis {
None,
Crate,
Pub,
}
fn item_export_vis(chunk_text: &str) -> ExportVis {
use syn::{Item, Visibility};
let Ok(item) = syn::parse_str::<syn::Item>(chunk_text) else {
return ExportVis::None;
};
let (vis, bumped) = match &item {
Item::Fn(f) => (&f.vis, true),
Item::Struct(s) => (&s.vis, true),
Item::Enum(e) => (&e.vis, true),
Item::Const(c) => (&c.vis, true),
Item::Static(s) => (&s.vis, true),
Item::Trait(t) => (&t.vis, true),
Item::TraitAlias(t) => (&t.vis, true),
Item::Type(t) => (&t.vis, true),
Item::Union(u) => (&u.vis, true),
Item::Mod(m) => (&m.vis, false),
Item::Use(u) => (&u.vis, false),
_ => return ExportVis::None,
};
match vis {
Visibility::Public(_) => ExportVis::Pub,
Visibility::Restricted(_) => ExportVis::Crate,
Visibility::Inherited if bumped => ExportVis::Crate,
Visibility::Inherited => ExportVis::None,
}
}
#[derive(Default)]
struct Refs {
names: std::collections::BTreeSet<String>,
keep_all: bool,
}
impl Refs {
fn add(&mut self, chunk_text: &str) {
match crate::referenced_idents(chunk_text) {
Some(names) => self.names.extend(names),
None => self.keep_all = true,
}
}
fn add_source(&mut self, src: &str) {
match crate::referenced_idents_in_source(src) {
Some(names) => self.names.extend(names),
None => self.keep_all = true,
}
}
fn is_empty(&self) -> bool {
self.names.is_empty() && !self.keep_all
}
fn keeps(&self, provided: &Option<std::collections::BTreeSet<String>>) -> bool {
match provided {
None => true,
Some(names) => self.keep_all || names.iter().any(|n| self.names.contains(n)),
}
}
}
fn select_imports(import_chunks: &[usize], chunks: &[crate::Chunk], refs: &Refs) -> String {
let mut out = String::new();
for &i in import_chunks {
let text = chunks[i].text.as_str();
if refs.keeps(&import_provided_names(text)) {
out.push_str(text);
}
}
out
}
fn import_provided_names(chunk_text: &str) -> Option<std::collections::BTreeSet<String>> {
match syn::parse_str::<syn::Item>(chunk_text).ok()? {
syn::Item::Use(u) => {
let mut names = std::collections::BTreeSet::new();
let mut name_less = false;
collect_use_names(&u.tree, &mut names, &mut name_less);
if name_less { None } else { Some(names) }
}
syn::Item::ExternCrate(c) => {
let name = c
.rename
.map(|(_, id)| id.to_string())
.unwrap_or_else(|| c.ident.to_string());
Some(std::collections::BTreeSet::from([name]))
}
_ => None,
}
}
fn collect_use_names(
tree: &syn::UseTree,
names: &mut std::collections::BTreeSet<String>,
name_less: &mut bool,
) {
match tree {
syn::UseTree::Path(p) => collect_use_names(&p.tree, names, name_less),
syn::UseTree::Name(n) => {
names.insert(n.ident.to_string());
}
syn::UseTree::Rename(r) => {
if r.rename == "_" {
*name_less = true;
} else {
names.insert(r.rename.to_string());
}
}
syn::UseTree::Glob(_) => *name_less = true,
syn::UseTree::Group(g) => {
for item in &g.items {
collect_use_names(item, names, name_less);
}
}
}
}
fn extract_mod(chunk_text: &str) -> Option<(String, String)> {
let Ok(syn::Item::Mod(item)) = syn::parse_str::<syn::Item>(chunk_text) else {
return None;
};
let (brace, _) = item.content.as_ref()?;
let ident_end = item.ident.span().byte_range().end;
let decl = format!("{};\n", &chunk_text[..ident_end]);
let open_end = brace.span.open().byte_range().end;
let close_start = brace.span.close().byte_range().start;
let inner = &chunk_text[open_end..close_start];
let inner = inner.strip_prefix('\n').unwrap_or(inner);
let mut inner = inner.trim_end_matches([' ', '\t']).to_owned();
if !inner.is_empty() && !inner.ends_with('\n') {
inner.push('\n');
}
Some((decl, inner))
}
fn split_nested_mod(name: &str, inner: &str, max_loc: usize) -> (Vec<OutputFile>, Vec<String>) {
let Ok(exploded) = crate::explode(inner) else {
return (
vec![OutputFile {
path: format!("{name}.rs"),
contents: inner.to_owned(),
loc: loc(inner),
}],
vec![format!("{name} (could not parse for nested split)")],
);
};
let row = |i: usize| {
exploded
.manifest
.rows
.iter()
.find(|r| r.chunk_index == i)
.unwrap()
};
let header_estimate: usize = exploded
.manifest
.rows
.iter()
.filter(|r| matches!(r.kind.as_str(), "use" | "preamble" | "extern_crate"))
.map(|r| r.loc)
.sum();
let item_budget = max_loc
.saturating_sub(header_estimate + PART_OVERHEAD)
.max(1);
let plan = plan_split(&exploded, item_budget);
let is_super_glob = |t: &str| t.replace(' ', "").contains("usesuper::*;");
let group_header: String = plan
.header
.iter()
.map(|&i| exploded.chunks[i].text.as_str())
.filter(|t| !is_super_glob(t))
.collect();
let mut files = Vec::new();
let mut decls = String::new();
let mut reexports = String::new();
let mut still = Vec::new();
for (idx, part) in plan.parts.iter().enumerate() {
let module = format!("g{idx:02}");
let mut body = String::new();
for &ci in &part.chunk_indices {
body.push_str(&bump_visibility(exploded.chunks[ci].text.as_str()));
}
let contents = format!("{group_header}\nuse super::*;\n\n{body}");
files.push(OutputFile {
path: format!("{name}/{module}.rs"),
loc: loc(&contents),
contents,
});
decls.push_str(&format!("mod {module};\n"));
reexports.push_str(&format!("pub(crate) use {module}::*;\n"));
}
for &mod_index in &plan.mods {
let chunk = exploded.chunks[mod_index].text.as_str();
let mod_name = row(mod_index).name.as_str();
let Some((decl, mod_inner)) = extract_mod(chunk) else {
decls.push_str(chunk);
continue;
};
decls.push_str(&decl);
let mod_loc = loc(&mod_inner);
if mod_loc < max_loc {
files.push(OutputFile {
path: format!("{name}/{mod_name}.rs"),
contents: mod_inner,
loc: mod_loc,
});
} else {
let (nested, nested_still) = split_nested_mod(mod_name, &mod_inner, max_loc);
files.extend(nested.into_iter().map(|mut f| {
f.path = format!("{name}/{}", f.path);
f
}));
still.extend(
nested_still
.into_iter()
.map(|message| format!("{name}::{message}")),
);
}
}
for over in &plan.oversized {
still.push(format!(
"{name}::{} ({} LOC {}, leaf needs manual extraction)",
over.name, over.loc, over.kind
));
}
let root_kept: String = plan
.root_items
.iter()
.map(|&i| exploded.chunks[i].text.as_str())
.collect();
let modrs = format!("pub(crate) use super::*;\n\n{root_kept}{decls}\n{reexports}");
files.insert(
0,
OutputFile {
path: format!("{name}/mod.rs"),
loc: loc(&modrs),
contents: modrs,
},
);
(files, still)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{explode, plan_split};
#[test]
fn splits_a_flat_bin_root_into_cohesive_under_budget_modules() {
let mut src = String::from("use std::fmt;\n\n");
for k in 0..4 {
for n in 0..4 {
let call = if n < 3 {
format!(" g{k}_{}();\n", n + 1)
} else {
String::new()
};
src.push_str(&format!("fn g{k}_{n}() {{\n{call}}}\n\n"));
}
}
src.push_str("fn main() {\n g0_0();\n}\n");
let exploded = explode(&src).unwrap();
let max_loc = 60;
let out = split_bin(&exploded, max_loc, "main");
assert!(out.still_oversized.is_empty());
for f in &out.files {
assert!(f.loc < max_loc, "{} over budget: {}", f.path, f.loc);
}
let root = out.files.iter().find(|f| f.path == "main.rs").unwrap();
assert!(root.contents.contains("fn main"), "main stays at root");
assert!(root.contents.contains("pub(crate) use "));
let part = out.files.iter().find(|f| f.path != "main.rs").unwrap();
assert!(part.contents.contains("use crate::*;"));
assert!(part.contents.contains("pub(crate) fn "));
let g0 = out
.files
.iter()
.find(|f| f.contents.contains("fn g0_1"))
.unwrap();
for n in 0..4 {
assert!(
g0.contents.contains(&format!("fn g0_{n}")),
"g0 group together"
);
}
}
#[test]
fn oversized_mod_is_extracted_to_its_own_file() {
let mut inner = String::new();
for i in 0..10 {
inner.push_str(&format!(" fn t{i}() {{}}\n"));
}
let src = format!("fn main() {{}}\n\n#[cfg(test)]\nmod tests {{\n{inner}}}\n");
let exploded = explode(&src).unwrap();
let plan = plan_split(&exploded, 6);
let out = reassemble_bin(&exploded, &plan, "main", 1000);
let tests = out.files.iter().find(|f| f.path == "tests.rs").unwrap();
assert!(tests.contents.contains("fn t0()"));
assert!(!tests.contents.contains("mod tests"));
let root = out.files.iter().find(|f| f.path == "main.rs").unwrap();
assert!(root.contents.contains("#[cfg(test)]\nmod tests;"));
}
#[test]
fn an_over_ceiling_test_mod_is_nested_into_a_subdir() {
let mut inner =
String::from(" use super::*;\n\n fn helper() -> i32 {\n 1\n }\n");
for i in 0..30 {
inner.push_str(&format!(
"\n #[test]\n fn t{i}() {{\n assert_eq!(helper(), 1);\n }}\n"
));
}
let src = format!(
"pub fn helper() -> i32 {{\n 1\n}}\n\nfn main() {{}}\n\n#[cfg(test)]\nmod tests {{\n{inner}}}\n"
);
let exploded = explode(&src).unwrap();
let out = split_bin(&exploded, 60, "main");
let mod_rs = out.files.iter().find(|f| f.path == "tests/mod.rs").unwrap();
assert!(
mod_rs.contents.contains("pub(crate) use super::*;"),
"forwards the parent"
);
assert!(mod_rs.contents.contains("mod g00;"));
assert!(out.files.iter().any(|f| f.path == "tests/g00.rs"));
for f in &out.files {
assert!(f.loc < 60, "{} over ceiling: {}", f.path, f.loc);
}
let root = out.files.iter().find(|f| f.path == "main.rs").unwrap();
assert!(root.contents.contains("#[cfg(test)]\nmod tests;"));
}
#[test]
fn split_mod_uses_super_and_preserves_pub_api() {
let src = "use std::fmt;\n\npub fn alpha() -> i32 {\n beta()\n}\n\nfn beta() -> i32 {\n 1\n}\n";
let exploded = explode(src).unwrap();
let out = split_mod(&exploded, 10_000, "mod");
let root = out.files.iter().find(|f| f.path == "mod.rs").unwrap();
assert!(
root.contents.contains("pub use "),
"pub re-export preserves the API"
);
assert!(
!root.contents.contains("pub(crate) use "),
"a single `pub use *` caps each item at its own visibility — no \
redundant second `pub(crate) use` line"
);
let sub = out.files.iter().find(|f| f.path != "mod.rs").unwrap();
assert!(
sub.contents.contains("use super::*;"),
"siblings reached via super, not crate"
);
assert!(!sub.contents.contains("use crate::*;"));
assert!(sub.contents.contains("pub fn alpha"), "pub item stays pub");
assert!(
sub.contents.contains("pub(crate) fn beta"),
"private bumped to pub(crate)"
);
}
#[test]
fn file_module_places_submodules_in_a_subdir() {
let mut src = String::new();
for k in 0..3 {
src.push_str(&format!("pub fn item{k}() -> i32 {{\n {k}\n}}\n\n"));
}
let exploded = explode(&src).unwrap();
let out = split_mod(&exploded, 10_000, "wstest");
let root = out.files.iter().find(|f| f.path == "wstest.rs").unwrap();
assert!(
root.contents.contains("mod item0;"),
"root file-module declares the sub-modules"
);
let subs: Vec<&str> = out
.files
.iter()
.map(|f| f.path.as_str())
.filter(|p| *p != "wstest.rs")
.collect();
assert!(!subs.is_empty(), "produced sub-module files");
for path in &subs {
assert!(
path.starts_with("wstest/"),
"sub-module {path} must live under wstest/ (file-module subdir)"
);
}
}
#[test]
fn mod_rs_keeps_submodules_as_siblings() {
let mut src = String::new();
for k in 0..3 {
src.push_str(&format!("pub fn item{k}() -> i32 {{\n {k}\n}}\n\n"));
}
let exploded = explode(&src).unwrap();
let out = split_mod(&exploded, 10_000, "mod");
for f in &out.files {
assert!(
!f.path.contains('/'),
"mod.rs sub-modules are siblings, not in a subdir: {}",
f.path
);
}
}
#[test]
fn private_only_module_reexports_pub_crate_not_pub() {
let src = "fn helper() -> i32 {\n leaf()\n}\n\nfn leaf() -> i32 {\n 1\n}\n";
let exploded = explode(src).unwrap();
let out = split_mod(&exploded, 10_000, "mod");
let root = out.files.iter().find(|f| f.path == "mod.rs").unwrap();
assert!(
root.contents.contains("pub(crate) use "),
"all-private module re-exports pub(crate):\n{}",
root.contents
);
assert!(
!root.contents.contains("pub use "),
"no `pub use` for a module with no public item"
);
}
#[test]
fn impl_only_module_is_declared_but_not_reexported() {
let src = "pub fn api() -> i32 {\n 1\n}\n\nimpl Worker for External {\n fn run(&self) {}\n}\n";
let exploded = explode(src).unwrap();
let out = split_mod(&exploded, 10_000, "mod");
let root = out.files.iter().find(|f| f.path == "mod.rs").unwrap();
let reexports = root
.contents
.lines()
.filter(|l| l.contains(" use ") && l.trim_end().ends_with("::*;"))
.count();
assert_eq!(
reexports, 1,
"only the nameable module is re-exported:\n{}",
root.contents
);
assert!(root.contents.contains("pub use api::*;"));
assert_eq!(
root.contents.matches("mod ").count(),
2,
"both modules are still declared:\n{}",
root.contents
);
}
#[test]
fn bin_public_entry_fn_is_reexported_pub_so_it_stays_crate_visible() {
let src = "fn helper() -> i32 {\n 1\n}\n\npub fn run() -> i32 {\n helper()\n}\n";
let exploded = explode(src).unwrap();
let out = split_bin(&exploded, 10_000, "lib");
let root = out.files.iter().find(|f| f.path == "lib.rs").unwrap();
assert!(
root.contents.contains("pub use "),
"public `run` re-exported at pub keeps `crate::run` visible:\n{}",
root.contents
);
assert!(
!root.contents.contains("pub(crate) use "),
"the module has a pub item, so one `pub use *` suffices (no double line)"
);
}
#[test]
fn crate_preamble_stays_at_root_not_copied_into_parts() {
let src = "//! crate docs\n#![allow(dead_code)]\n\npub fn alpha() -> i32 {\n beta()\n}\n\nfn beta() -> i32 {\n 1\n}\n";
let exploded = explode(src).unwrap();
let out = split_bin(&exploded, 10_000, "lib");
let root = out.files.iter().find(|f| f.path == "lib.rs").unwrap();
assert!(
root.contents.contains("#![allow(dead_code)]"),
"preamble stays at the root"
);
for f in &out.files {
if f.path != "lib.rs" {
assert!(
!f.contents.contains("#!["),
"part {} must not carry crate-level inner attrs",
f.path
);
assert!(
!f.contents.contains("//! crate docs"),
"part {} must not duplicate the crate docs",
f.path
);
}
}
}
#[test]
fn parts_copy_only_the_imports_they_reference() {
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";
let exploded = explode(src).unwrap();
let out = split_mod(&exploded, 10_000, "mod");
let alpha = out
.files
.iter()
.find(|f| f.contents.contains("fn alpha"))
.unwrap();
let bravo = out
.files
.iter()
.find(|f| f.contents.contains("fn bravo"))
.unwrap();
assert!(
alpha.contents.contains("use std::fmt::Display;"),
"alpha references Display:\n{}",
alpha.contents
);
assert!(
!alpha.contents.contains("HashMap"),
"alpha never names HashMap -> import dropped:\n{}",
alpha.contents
);
assert!(
!bravo.contents.contains("use std::fmt::Display;"),
"bravo references nothing from the header:\n{}",
bravo.contents
);
assert!(!bravo.contents.contains("HashMap"));
}
#[test]
fn globs_and_anonymous_trait_imports_are_always_copied() {
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";
let exploded = explode(src).unwrap();
let out = split_mod(&exploded, 10_000, "mod");
for f in &out.files {
if f.path == "mod.rs" {
continue;
}
assert!(
f.contents.contains("use std::fmt::Write as _;"),
"{} must keep the `as _` trait import:\n{}",
f.path,
f.contents
);
assert!(
f.contents.contains("use std::prelude::v1::*;"),
"{} must keep the glob import:\n{}",
f.path,
f.contents
);
}
}
#[test]
fn item_less_root_keeps_no_imports() {
let src = "use std::collections::HashMap;\n\npub fn solo(m: HashMap<u8, u8>) {\n let _ = m;\n}\n";
let exploded = explode(src).unwrap();
let out = split_mod(&exploded, 10_000, "mod");
let root = out.files.iter().find(|f| f.path == "mod.rs").unwrap();
assert!(
!root.contents.contains("use std::collections::HashMap;"),
"item-less root drops imports it doesn't use:\n{}",
root.contents
);
let part = out.files.iter().find(|f| f.path != "mod.rs").unwrap();
assert!(
part.contents.contains("use std::collections::HashMap;"),
"the part that uses HashMap keeps it"
);
}
#[test]
fn source_pub_use_reexports_stay_pub_use_at_the_root() {
let src = "mod helpers;\n\npub use helpers::Helper;\n\nuse std::fmt::Display;\n\n\
pub fn api(d: &dyn Display) {\n let _ = d;\n}\n";
let exploded = explode(src).unwrap();
let out = split_bin(&exploded, 10_000, "lib");
let root = out.files.iter().find(|f| f.path == "lib.rs").unwrap();
assert!(
root.contents.contains("mod helpers;"),
"content-less mod decl stays at the root (it binds helpers.rs):\n{}",
root.contents
);
assert!(
root.contents.contains("pub use helpers::Helper;"),
"source re-export stays verbatim at the root:\n{}",
root.contents
);
assert!(
!root.contents.contains("pub(crate) use helpers"),
"a source re-export must never be demoted"
);
for f in &out.files {
if f.path != "lib.rs" {
assert!(
!f.contents.contains("pub use helpers::Helper;"),
"{} must not carry the root's re-export",
f.path
);
assert!(
!f.contents.contains("mod helpers;"),
"{} must not carry the root's mod decl (would look for a \
sibling subdir file)",
f.path
);
}
}
}
#[test]
fn under_budget_cfg_test_mod_is_extracted_whole_with_its_gate() {
let src = "pub fn alpha() -> i32 {\n 1\n}\n\n\
#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n \
fn smoke() {\n assert_eq!(alpha(), 1);\n }\n}\n";
let exploded = explode(src).unwrap();
let out = split_bin(&exploded, 10_000, "lib");
let root = out.files.iter().find(|f| f.path == "lib.rs").unwrap();
assert!(
root.contents.contains("#[cfg(test)]\nmod tests;"),
"the gate must travel to the root declaration:\n{}",
root.contents
);
assert!(
!root.contents.contains("use tests::*"),
"an extracted mod is declared, not glob re-exported:\n{}",
root.contents
);
let tests = out.files.iter().find(|f| f.path == "tests.rs").unwrap();
assert!(tests.contents.contains("fn smoke()"));
assert!(
!tests.contents.contains("mod tests"),
"the body is unwrapped — no double nesting:\n{}",
tests.contents
);
}
#[test]
fn pub_mod_keeps_its_visibility_on_the_extracted_decl() {
let src = "pub fn api() -> i32 {\n 1\n}\n\n\
/// Config store.\npub mod config {\n pub fn get() -> i32 {\n 2\n }\n}\n";
let exploded = explode(src).unwrap();
let out = split_bin(&exploded, 10_000, "lib");
let root = out.files.iter().find(|f| f.path == "lib.rs").unwrap();
assert!(
root.contents.contains("/// Config store.\npub mod config;"),
"docs and `pub` travel to the declaration:\n{}",
root.contents
);
let config = out.files.iter().find(|f| f.path == "config.rs").unwrap();
assert!(config.contents.contains("pub fn get()"));
}
#[test]
fn part_colliding_with_an_extracted_mod_name_is_renamed() {
let src = "fn tests() -> i32 {\n 1\n}\n\n\
#[cfg(test)]\nmod tests {\n fn smoke() {}\n}\n";
let exploded = explode(src).unwrap();
let out = split_bin(&exploded, 10_000, "lib");
let paths: Vec<&str> = out.files.iter().map(|f| f.path.as_str()).collect();
assert!(paths.contains(&"tests.rs"), "extracted mod file: {paths:?}");
assert!(paths.contains(&"tests_.rs"), "renamed part file: {paths:?}");
let root = out.files.iter().find(|f| f.path == "lib.rs").unwrap();
assert!(root.contents.contains("mod tests_;"));
assert!(root.contents.contains("#[cfg(test)]\nmod tests;"));
}
#[test]
fn root_keeps_imports_an_extracted_mod_body_references() {
let src = "use std::collections::HashMap;\n\n\
pub fn alpha() -> i32 {\n 1\n}\n\n\
#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n \
fn smoke() {\n let _m: HashMap<u8, u8> = HashMap::new();\n \
assert_eq!(alpha(), 1);\n }\n}\n";
let exploded = explode(src).unwrap();
let out = split_bin(&exploded, 10_000, "lib");
let root = out.files.iter().find(|f| f.path == "lib.rs").unwrap();
assert!(
root.contents.contains("use std::collections::HashMap;"),
"root must keep imports the extracted body names:\n{}",
root.contents
);
}
#[test]
fn bin_root_keeps_only_imports_main_references() {
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";
let exploded = explode(src).unwrap();
let out = split_bin(&exploded, 10_000, "main");
let root = out.files.iter().find(|f| f.path == "main.rs").unwrap();
assert!(
root.contents.contains("use std::process::exit;"),
"main references exit:\n{}",
root.contents
);
assert!(
!root.contents.contains("HashMap"),
"main never names HashMap (worker does) -> dropped from root:\n{}",
root.contents
);
}
}