Skip to main content

rust_split/
reassemble.rs

1//! Tier 2 step 2 — reassemble a planned split into output module files.
2//!
3//! Target: a **binary/crate-root** file (e.g. `src/main.rs`) whose items all
4//! live in one module and reference each other by bare name. Splitting them
5//! into sibling modules breaks those bare references, so the scheme is:
6//!
7//! - the crate root keeps the file preamble + imports, the source's own
8//!   re-exports (`pub use ...` — dropping or demoting one would narrow the
9//!   crate's public API) and content-less `mod name;` declarations verbatim,
10//!   declares each part module, re-exports each part with a single glob whose
11//!   visibility matches the part's widest item (`pub use` when the part has a
12//!   public item, else `pub(crate) use`, and nothing for an `impl`-only part),
13//!   and keeps `fn main` (the entry point must stay at the root);
14//! - each part file copies the imports it references by name (preamble stays at
15//!   the root), does `use crate::*` to see its siblings via the root re-exports,
16//!   and carries its items with `pub(crate)` visibility bumped on so the
17//!   re-exports can see them;
18//! - a body-carrying `mod` (e.g. the test module) of **any size** is extracted
19//!   whole to its own file — its attributes (`#[cfg(test)]` must keep gating
20//!   it) and visibility travel to the root declaration, and its `super::*`
21//!   keeps pointing at the root; one over the ceiling is split further into a
22//!   `{name}/` subdirectory.
23//!
24//! Residual import/visibility that this mechanical scheme misses is left for the
25//! compiler to enumerate (the documented O(n) finish) — but the LOC budget and
26//! the byte-exact moves are done here.
27
28use crate::{Exploded, SplitPlan, plan_split};
29use syn::spanned::Spanned;
30
31/// Per-part overhead beyond the imports: the `use crate::*;` sibling glob plus
32/// blank-line separators.
33const PART_OVERHEAD: usize = 3;
34
35/// Plan and reassemble a binary crate-root split so that **every output file is
36/// `< max_loc`**. The item budget reserves the full header LOC and the per-part
37/// overhead; since a part now copies only the imports it references (a subset of
38/// the header), this reservation is conservative — a packed part stays safely
39/// under budget.
40pub fn split_bin(exploded: &Exploded, max_loc: usize, root_stem: &str) -> SplitOutput {
41    let header_loc: usize = exploded
42        .manifest
43        .rows
44        .iter()
45        .filter(|r| matches!(r.kind.as_str(), "use" | "preamble" | "extern_crate"))
46        .map(|r| r.loc)
47        .sum();
48    let item_budget = max_loc.saturating_sub(header_loc + PART_OVERHEAD).max(1);
49    let plan = plan_split(exploded, item_budget);
50    reassemble_bin(exploded, &plan, root_stem, max_loc)
51}
52
53/// A file the split would write.
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct OutputFile {
56    /// Path relative to the source file's directory (e.g. `main.rs`, `p00.rs`).
57    pub path: String,
58    pub contents: String,
59    pub loc: usize,
60}
61
62/// The reassembled output plus the parts of the plan that could not be met.
63#[derive(Debug, Clone)]
64pub struct SplitOutput {
65    pub files: Vec<OutputFile>,
66    /// Names of items still at or above budget (e.g. an un-nested test mod).
67    pub still_oversized: Vec<String>,
68}
69
70impl SplitOutput {
71    /// The largest output file's LOC (the budget is met iff this is `< max_loc`
72    /// and `still_oversized` is empty).
73    pub fn max_loc(&self) -> usize {
74        self.files.iter().map(|f| f.loc).max().unwrap_or(0)
75    }
76}
77
78fn loc(text: &str) -> usize {
79    if text.is_empty() {
80        0
81    } else {
82        text.lines().count().max(1)
83    }
84}
85
86/// How the split file sits in the module tree — fixes the sibling path prefix
87/// and where sub-module files go. Re-export visibility is per part in both
88/// topologies: `pub use` when the part has a public item (preserving the
89/// crate's public API), else `pub(crate) use`.
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub enum Topology {
92    /// A binary or library crate root (`main.rs`, `lib.rs`): siblings reached
93    /// via `crate::`, `fn main` (if any) kept at the root.
94    Bin,
95    /// A nested library module (`foo/mod.rs`, or a plain `foo.rs` file module):
96    /// sub-modules reached via `super::` and re-exported with a single per-module
97    /// glob — `pub use` to preserve a public API, else `pub(crate) use`. A plain
98    /// `foo.rs` file module resolves `mod bar;` to `foo/bar.rs`, so its
99    /// sub-modules are written into a `foo/` subdir.
100    Mod,
101}
102
103/// Plan and reassemble a library module so every output file is `< max_loc`;
104/// sub-modules see each other via `super::*`. Handles both a directory-owning
105/// `foo/mod.rs` (sub-modules become siblings) and a plain `foo.rs` file module
106/// (sub-modules go in a `foo/` subdir). `root_stem` is the source file's stem.
107pub fn split_mod(exploded: &Exploded, max_loc: usize, root_stem: &str) -> SplitOutput {
108    let header_loc: usize = exploded
109        .manifest
110        .rows
111        .iter()
112        .filter(|r| matches!(r.kind.as_str(), "use" | "preamble" | "extern_crate"))
113        .map(|r| r.loc)
114        .sum();
115    let item_budget = max_loc.saturating_sub(header_loc + PART_OVERHEAD).max(1);
116    let plan = plan_split(exploded, item_budget);
117    reassemble(exploded, &plan, root_stem, Topology::Mod, max_loc)
118}
119
120/// Reassemble a binary crate-root split (back-compat wrapper).
121pub fn reassemble_bin(
122    exploded: &Exploded,
123    plan: &SplitPlan,
124    root_stem: &str,
125    max_loc: usize,
126) -> SplitOutput {
127    reassemble(exploded, plan, root_stem, Topology::Bin, max_loc)
128}
129
130/// Reassemble a split for the given module topology. `root_stem` is the source
131/// file's stem (`"main"` for `main.rs`, `"mod"` for `foo/mod.rs`). `max_loc` is
132/// the ceiling, needed to recursively split an oversized test module.
133fn reassemble(
134    exploded: &Exploded,
135    plan: &SplitPlan,
136    root_stem: &str,
137    topology: Topology,
138    max_loc: usize,
139) -> SplitOutput {
140    let sibling = match topology {
141        Topology::Bin => "crate",
142        Topology::Mod => "super",
143    };
144    let chunk_text = |i: usize| exploded.chunks[i].text.as_str();
145    let row = |i: usize| {
146        exploded
147            .manifest
148            .rows
149            .iter()
150            .find(|r| r.chunk_index == i)
151            .unwrap()
152    };
153
154    // Where sub-module files go. A directory-owning module file
155    // (`main.rs`/`lib.rs`/`foo/mod.rs`) keeps them as siblings; a plain `foo.rs`
156    // file module resolves `mod bar;` to `foo/bar.rs`, so its parts go in `foo/`.
157    let sub_prefix = match topology {
158        Topology::Bin => String::new(),
159        Topology::Mod if matches!(root_stem, "mod" | "lib" | "main") => String::new(),
160        Topology::Mod => format!("{root_stem}/"),
161    };
162
163    // The crate/module preamble (inner `//!` docs / `#![...]` attrs) stays at
164    // the root only — a crate-root `#![feature]` is a hard error in a sub-module
165    // file, and a crate-root `#![allow]` already covers descendants. The import
166    // chunks (`use`/`extern crate`) are copied into each file, but only the ones
167    // that file references by name (see `select_imports`). Re-exports
168    // (`pub use ...`) are not imports — the planner routes them to
169    // `plan.root_items`, kept at the root verbatim.
170    let preamble: String = plan
171        .header
172        .iter()
173        .filter(|&&i| row(i).kind == "preamble")
174        .map(|&i| chunk_text(i))
175        .collect();
176    let import_chunks: Vec<usize> = plan
177        .header
178        .iter()
179        .copied()
180        .filter(|&i| row(i).kind != "preamble")
181        .collect();
182
183    // Source chunks that stay at the root verbatim: `pub use` re-exports (the
184    // crate's public surface) and content-less `mod name;` declarations (which
185    // bind files relative to the root's directory).
186    let root_kept: String = plan.root_items.iter().map(|&i| chunk_text(i)).collect();
187
188    // Module names fixed by the source — extracted body-mods and `mod name;`
189    // declarations keep their own names — plus the root's stem; a generated
190    // part that would collide is renamed with a `_` suffix.
191    let mut reserved: std::collections::BTreeSet<&str> =
192        plan.mods.iter().map(|&i| row(i).name.as_str()).collect();
193    reserved.insert(root_stem);
194    for &i in &plan.root_items {
195        if row(i).kind == "mod" {
196            reserved.insert(row(i).name.as_str());
197        }
198    }
199
200    let mut files = Vec::new();
201    let mut mod_decls = String::new();
202    let mut reexports = String::new();
203    let mut still_oversized = Vec::new();
204    // Names referenced from the root file itself (`fn main`, extracted module
205    // bodies whose `use super::*` reaches the root's imports).
206    let mut root_refs = Refs::default();
207
208    // Part modules, named after their cluster's dominant item.
209    for part in &plan.parts {
210        let module = if reserved.contains(part.name.as_str()) {
211            format!("{}_", part.name)
212        } else {
213            part.name.clone()
214        };
215        let mut body = String::new();
216        let mut root_main = None;
217        // Widest visibility any moved item contributes to a glob re-export.
218        let mut export_vis = ExportVis::None;
219        // Names this part references, so only the imports it uses are copied in.
220        let mut refs = Refs::default();
221        for &i in &part.chunk_indices {
222            let r = row(i);
223            // `fn main` must stay at the crate root, not move into a submodule.
224            if r.kind == "fn" && r.name == "main" {
225                root_main = Some(i);
226                continue;
227            }
228            export_vis = export_vis.max(item_export_vis(chunk_text(i)));
229            refs.add(chunk_text(i));
230            body.push_str(&bump_visibility(chunk_text(i)));
231        }
232        // If the part was only `fn main`, it produced no module file.
233        if body.trim().is_empty() {
234            if let Some(i) = root_main {
235                // stash main to emit at root via a sentinel part with no module
236                files.push(OutputFile {
237                    path: String::from("__root_main__"),
238                    contents: chunk_text(i).to_owned(),
239                    loc: 0,
240                });
241            }
242            continue;
243        }
244        if let Some(i) = root_main {
245            files.push(OutputFile {
246                path: String::from("__root_main__"),
247                contents: chunk_text(i).to_owned(),
248                loc: 0,
249            });
250        }
251        let part_imports = select_imports(&import_chunks, &exploded.chunks, &refs);
252        let contents = part_file(&part_imports, sibling, &body);
253        mod_decls.push_str(&format!("mod {module};\n"));
254        // One glob re-export per module, visibility chosen by the module's widest
255        // item: `pub use` when it has a public item (a `pub use *` caps each item
256        // at its own visibility, so it covers internal `pub(crate)` items in the
257        // same line); else `pub(crate) use`; and nothing when the module exposes
258        // no nameable item (e.g. a bare `impl` — a glob re-export would only warn
259        // "doesn't reexport anything"). This keeps the crate's public surface
260        // (`pub use`) reachable and avoids the redundant double re-export.
261        if let Some(line) = reexport_line(export_vis, &module) {
262            reexports.push_str(&line);
263        }
264        let l = loc(&contents);
265        files.push(OutputFile {
266            path: format!("{sub_prefix}{module}.rs"),
267            contents,
268            loc: l,
269        });
270    }
271
272    // Body-carrying mods are extracted whole, whatever their size: packing a
273    // `#[cfg(test)] mod tests { ... }` into a cluster would nest it inside a
274    // part module and leave the root declaration ungated. The declaration —
275    // leading comments, attributes, visibility — travels to the root; the
276    // unwrapped body becomes the module's file, split further into a `{name}/`
277    // subdir when it is itself over the ceiling.
278    for &mod_index in &plan.mods {
279        let chunk = chunk_text(mod_index);
280        let name = row(mod_index).name.as_str();
281        let Some((decl, inner)) = extract_mod(chunk) else {
282            // Should not happen (the planner classified it by parsing); keep
283            // the chunk intact at the root rather than lose it.
284            mod_decls.push_str(chunk);
285            continue;
286        };
287        mod_decls.push_str(&decl);
288        // The body's `use super::*;` reaches the root's imports, so the root
289        // must keep the imports the body names.
290        root_refs.add_source(&inner);
291        let inner_loc = loc(&inner);
292        if inner_loc < max_loc {
293            files.push(OutputFile {
294                path: format!("{sub_prefix}{name}.rs"),
295                contents: inner,
296                loc: inner_loc,
297            });
298        } else {
299            let (nested, nested_still) = split_nested_mod(name, &inner, max_loc);
300            files.extend(nested.into_iter().map(|mut f| {
301                f.path = format!("{sub_prefix}{}", f.path);
302                f
303            }));
304            still_oversized.extend(nested_still);
305        }
306    }
307
308    // Oversized leaves can't be moved mechanically.
309    for over in &plan.oversized {
310        still_oversized.push(format!(
311            "{} ({} LOC {}, manual extraction)",
312            over.name, over.loc, over.kind
313        ));
314    }
315
316    // Pull the stashed `fn main` (if any) and build the root file.
317    let mut root_main = String::new();
318    files.retain(|f| {
319        if f.path == "__root_main__" {
320            root_main = f.contents.clone();
321            false
322        } else {
323            true
324        }
325    });
326
327    // The root keeps the preamble, then only the imports its own retained code
328    // references — `fn main` (if any) plus extracted module bodies, whose
329    // `use super::*;` resolves against the root. A root that is just module
330    // declarations and re-exports needs no imports at all.
331    if !root_main.trim().is_empty() {
332        root_refs.add(&root_main);
333    }
334    let root_imports = if root_refs.is_empty() {
335        String::new()
336    } else {
337        select_imports(&import_chunks, &exploded.chunks, &root_refs)
338    };
339
340    let mut root = String::new();
341    root.push_str(&preamble);
342    root.push_str(&root_imports);
343    root.push_str(&root_kept);
344    root.push('\n');
345    root.push_str(&mod_decls);
346    root.push('\n');
347    root.push_str(&reexports);
348    if !root_main.is_empty() {
349        root.push('\n');
350        root.push_str(&root_main);
351    }
352    let root_loc = loc(&root);
353    files.insert(
354        0,
355        OutputFile {
356            path: format!("{root_stem}.rs"),
357            contents: root,
358            loc: root_loc,
359        },
360    );
361
362    SplitOutput {
363        files,
364        still_oversized,
365    }
366}
367
368/// Raise a moved item — and the struct fields / impl members referenced across
369/// the new module boundaries — to `pub(crate)`, so the crate-root re-exports can
370/// see them. Span-driven (parse the chunk, insert at exact offsets), so it is
371/// attribute-safe and handles `fn` modifiers, multi-line attrs, etc. Items
372/// already `pub`/`pub(crate)` are left alone. Over-exposes within the crate,
373/// which is the mechanical, behavior-preserving choice.
374fn bump_visibility(chunk_text: &str) -> String {
375    let Ok(item) = syn::parse_str::<syn::Item>(chunk_text) else {
376        return chunk_text.to_owned();
377    };
378    let mut offsets = Vec::new();
379    collect_vis_offsets(&item, &mut offsets);
380    apply_inserts(chunk_text, &offsets)
381}
382
383fn is_inherited(vis: &syn::Visibility) -> bool {
384    matches!(vis, syn::Visibility::Inherited)
385}
386
387fn start<T: Spanned>(node: &T) -> usize {
388    node.span().byte_range().start
389}
390
391/// Byte offset where a function's visibility belongs: before any
392/// `const`/`async`/`unsafe`/`extern` modifier, else before `fn`.
393fn fn_sig_start(sig: &syn::Signature) -> usize {
394    let mut min = start(&sig.fn_token);
395    if let Some(t) = &sig.constness {
396        min = min.min(start(t));
397    }
398    if let Some(t) = &sig.asyncness {
399        min = min.min(start(t));
400    }
401    if let Some(t) = &sig.unsafety {
402        min = min.min(start(t));
403    }
404    if let Some(abi) = &sig.abi {
405        min = min.min(start(&abi.extern_token));
406    }
407    min
408}
409
410fn field_offsets(fields: &syn::Fields, offsets: &mut Vec<usize>) {
411    for field in fields {
412        if is_inherited(&field.vis) {
413            match &field.ident {
414                Some(ident) => offsets.push(start(ident)),
415                None => offsets.push(start(&field.ty)),
416            }
417        }
418    }
419}
420
421fn collect_vis_offsets(item: &syn::Item, offsets: &mut Vec<usize>) {
422    use syn::Item;
423    match item {
424        Item::Fn(f) => {
425            if is_inherited(&f.vis) {
426                offsets.push(fn_sig_start(&f.sig));
427            }
428        }
429        Item::Struct(s) => {
430            if is_inherited(&s.vis) {
431                offsets.push(start(&s.struct_token));
432            }
433            field_offsets(&s.fields, offsets);
434        }
435        Item::Enum(e) => {
436            if is_inherited(&e.vis) {
437                offsets.push(start(&e.enum_token));
438            }
439        }
440        Item::Union(u) => {
441            if is_inherited(&u.vis) {
442                offsets.push(start(&u.union_token));
443            }
444            field_offsets(&syn::Fields::Named(u.fields.clone()), offsets);
445        }
446        Item::Const(c) if is_inherited(&c.vis) => offsets.push(start(&c.const_token)),
447        Item::Static(s) if is_inherited(&s.vis) => offsets.push(start(&s.static_token)),
448        Item::Type(t) if is_inherited(&t.vis) => offsets.push(start(&t.type_token)),
449        Item::Trait(t) if is_inherited(&t.vis) => offsets.push(start(&t.trait_token)),
450        Item::TraitAlias(t) if is_inherited(&t.vis) => offsets.push(start(&t.trait_token)),
451        // Only inherent impls (`impl Type`) allow member visibility; trait-impl
452        // members (`impl Trait for Type`) inherit the trait's and must not.
453        Item::Impl(i) if i.trait_.is_none() => {
454            for member in &i.items {
455                match member {
456                    syn::ImplItem::Fn(f) if is_inherited(&f.vis) => {
457                        offsets.push(fn_sig_start(&f.sig))
458                    }
459                    syn::ImplItem::Const(c) if is_inherited(&c.vis) => {
460                        offsets.push(start(&c.const_token))
461                    }
462                    syn::ImplItem::Type(t) if is_inherited(&t.vis) => {
463                        offsets.push(start(&t.type_token))
464                    }
465                    _ => {}
466                }
467            }
468        }
469        _ => {}
470    }
471}
472
473fn apply_inserts(text: &str, offsets: &[usize]) -> String {
474    let mut offsets: Vec<usize> = offsets.to_vec();
475    offsets.sort_unstable();
476    offsets.dedup();
477    let mut out = String::with_capacity(text.len() + offsets.len() * 11);
478    let mut prev = 0;
479    for &offset in &offsets {
480        out.push_str(&text[prev..offset]);
481        out.push_str("pub(crate) ");
482        prev = offset;
483    }
484    out.push_str(&text[prev..]);
485    out
486}
487
488/// Assemble a part file: its selected imports, the sibling glob so it can reach
489/// the rest of the module via the root re-exports, then the moved items.
490fn part_file(imports: &str, sibling: &str, body: &str) -> String {
491    if imports.trim().is_empty() {
492        format!("use {sibling}::*;\n\n{body}")
493    } else {
494        format!("{imports}\nuse {sibling}::*;\n\n{body}")
495    }
496}
497
498/// The glob re-export line for a module given the widest visibility of its
499/// items — or `None` when it exposes nothing nameable (re-exporting an
500/// `impl`-only module would only warn).
501fn reexport_line(vis: ExportVis, module: &str) -> Option<String> {
502    match vis {
503        ExportVis::None => None,
504        ExportVis::Crate => Some(format!("pub(crate) use {module}::*;\n")),
505        ExportVis::Pub => Some(format!("pub use {module}::*;\n")),
506    }
507}
508
509/// What a moved item contributes to its module's glob re-export. Ordered so the
510/// widest contribution across a module's items picks the re-export visibility.
511#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
512enum ExportVis {
513    /// No nameable export (a bare `impl`, a macro invocation, parse failure).
514    None,
515    /// A crate-visible name: a private item (bumped to `pub(crate)`), or one
516    /// already `pub(crate)`/`pub(super)`/`pub(in …)`.
517    Crate,
518    /// A fully public name — part of the crate/module's public surface.
519    Pub,
520}
521
522/// Classify one chunk's contribution to its module's glob re-export, mirroring
523/// `bump_visibility`: items it raises to `pub(crate)` count as crate-visible;
524/// `mod`/`use` are not raised, so a private one is invisible to the re-export.
525fn item_export_vis(chunk_text: &str) -> ExportVis {
526    use syn::{Item, Visibility};
527    let Ok(item) = syn::parse_str::<syn::Item>(chunk_text) else {
528        // Unparseable: bump_visibility also left it private, so a glob re-export
529        // could not see it either — contribute nothing.
530        return ExportVis::None;
531    };
532    let (vis, bumped) = match &item {
533        Item::Fn(f) => (&f.vis, true),
534        Item::Struct(s) => (&s.vis, true),
535        Item::Enum(e) => (&e.vis, true),
536        Item::Const(c) => (&c.vis, true),
537        Item::Static(s) => (&s.vis, true),
538        Item::Trait(t) => (&t.vis, true),
539        Item::TraitAlias(t) => (&t.vis, true),
540        Item::Type(t) => (&t.vis, true),
541        Item::Union(u) => (&u.vis, true),
542        Item::Mod(m) => (&m.vis, false),
543        Item::Use(u) => (&u.vis, false),
544        // impl / macro / foreign_mod / extern_crate / verbatim: nothing nameable.
545        _ => return ExportVis::None,
546    };
547    match vis {
548        Visibility::Public(_) => ExportVis::Pub,
549        Visibility::Restricted(_) => ExportVis::Crate,
550        Visibility::Inherited if bumped => ExportVis::Crate,
551        Visibility::Inherited => ExportVis::None,
552    }
553}
554
555/// The identifiers a set of moved items references — the basis for copying only
556/// the imports they use. An item that does not re-parse flips `keep_all`, so the
557/// caller falls back to copying every import rather than dropping a needed one.
558#[derive(Default)]
559struct Refs {
560    names: std::collections::BTreeSet<String>,
561    keep_all: bool,
562}
563
564impl Refs {
565    fn add(&mut self, chunk_text: &str) {
566        match crate::referenced_idents(chunk_text) {
567            Some(names) => self.names.extend(names),
568            None => self.keep_all = true,
569        }
570    }
571
572    /// Add every identifier a multi-item source fragment (an extracted module
573    /// body) references.
574    fn add_source(&mut self, src: &str) {
575        match crate::referenced_idents_in_source(src) {
576            Some(names) => self.names.extend(names),
577            None => self.keep_all = true,
578        }
579    }
580
581    /// True when nothing was added — no import can be needed.
582    fn is_empty(&self) -> bool {
583        self.names.is_empty() && !self.keep_all
584    }
585
586    /// Whether an import providing `names` should be copied in. A name-less
587    /// import (`None`: a `*` glob or `as _`) is always kept, since its usage
588    /// can't be detected by name.
589    fn keeps(&self, provided: &Option<std::collections::BTreeSet<String>>) -> bool {
590        match provided {
591            None => true,
592            Some(names) => self.keep_all || names.iter().any(|n| self.names.contains(n)),
593        }
594    }
595}
596
597/// Concatenate, in source order, the import chunks referenced by `refs` (plus
598/// name-less imports like `*` globs and `as _`, always kept). This is the
599/// aggressive policy: an import whose every name is unreferenced is dropped —
600/// including an extension trait used only via its methods, whose `use` the
601/// compiler will then ask to be re-added.
602fn select_imports(import_chunks: &[usize], chunks: &[crate::Chunk], refs: &Refs) -> String {
603    let mut out = String::new();
604    for &i in import_chunks {
605        let text = chunks[i].text.as_str();
606        if refs.keeps(&import_provided_names(text)) {
607            out.push_str(text);
608        }
609    }
610    out
611}
612
613/// The names a `use`/`extern crate` chunk introduces, or `None` when it exposes
614/// nothing referenceable by name — a `*` glob or an `as _` import — which is
615/// then kept in every file rather than dropped.
616fn import_provided_names(chunk_text: &str) -> Option<std::collections::BTreeSet<String>> {
617    match syn::parse_str::<syn::Item>(chunk_text).ok()? {
618        syn::Item::Use(u) => {
619            let mut names = std::collections::BTreeSet::new();
620            let mut name_less = false;
621            collect_use_names(&u.tree, &mut names, &mut name_less);
622            if name_less { None } else { Some(names) }
623        }
624        syn::Item::ExternCrate(c) => {
625            let name = c
626                .rename
627                .map(|(_, id)| id.to_string())
628                .unwrap_or_else(|| c.ident.to_string());
629            Some(std::collections::BTreeSet::from([name]))
630        }
631        // An unparseable header chunk has no detectable name -> keep it.
632        _ => None,
633    }
634}
635
636/// Collect the bound names a `use` tree introduces. `name_less` is set for a
637/// `*` glob or an `as _` rename, which bind no referenceable name.
638fn collect_use_names(
639    tree: &syn::UseTree,
640    names: &mut std::collections::BTreeSet<String>,
641    name_less: &mut bool,
642) {
643    match tree {
644        syn::UseTree::Path(p) => collect_use_names(&p.tree, names, name_less),
645        syn::UseTree::Name(n) => {
646            names.insert(n.ident.to_string());
647        }
648        syn::UseTree::Rename(r) => {
649            if r.rename == "_" {
650                *name_less = true;
651            } else {
652                names.insert(r.rename.to_string());
653            }
654        }
655        syn::UseTree::Glob(_) => *name_less = true,
656        syn::UseTree::Group(g) => {
657            for item in &g.items {
658                collect_use_names(item, names, name_less);
659            }
660        }
661    }
662}
663
664/// Split a `mod NAME { ... }` chunk into the root-side declaration and the
665/// unwrapped inner body for the extracted file. The declaration is everything
666/// up to the mod's identifier — leading comments, attributes (a `#[cfg(test)]`
667/// gate must keep gating the declaration), and visibility (`pub mod` stays
668/// `pub mod`) — closed with `;`. Span-driven on the re-parsed chunk, so it is
669/// exact where the old line-based scan dropped the visibility. `None` when the
670/// chunk does not re-parse as a body-carrying mod.
671fn extract_mod(chunk_text: &str) -> Option<(String, String)> {
672    let Ok(syn::Item::Mod(item)) = syn::parse_str::<syn::Item>(chunk_text) else {
673        return None;
674    };
675    let (brace, _) = item.content.as_ref()?;
676    let ident_end = item.ident.span().byte_range().end;
677    let decl = format!("{};\n", &chunk_text[..ident_end]);
678
679    let open_end = brace.span.open().byte_range().end;
680    let close_start = brace.span.close().byte_range().start;
681    let inner = &chunk_text[open_end..close_start];
682    // Drop the newline that followed `{` and the closing brace's indentation.
683    let inner = inner.strip_prefix('\n').unwrap_or(inner);
684    let mut inner = inner.trim_end_matches([' ', '\t']).to_owned();
685    if !inner.is_empty() && !inner.ends_with('\n') {
686        inner.push('\n');
687    }
688    Some((decl, inner))
689}
690
691/// Recursively split an over-budget module's inner body into a `{name}/`
692/// subdirectory: `{name}/mod.rs` forwards the enclosing module's items
693/// (`pub(crate) use super::*`), keeps the body's own re-exports and `mod`
694/// declarations verbatim, and re-exports the clusters; each `{name}/gNN.rs`
695/// holds a cohesive cluster and reaches everything via `super::*`. A nested
696/// body-carrying mod is extracted whole (attributes and visibility on its
697/// declaration), recursing when it is itself over the ceiling. Used for big
698/// test modules.
699fn split_nested_mod(name: &str, inner: &str, max_loc: usize) -> (Vec<OutputFile>, Vec<String>) {
700    let Ok(exploded) = crate::explode(inner) else {
701        return (
702            vec![OutputFile {
703                path: format!("{name}.rs"),
704                contents: inner.to_owned(),
705                loc: loc(inner),
706            }],
707            vec![format!("{name} (could not parse for nested split)")],
708        );
709    };
710    let row = |i: usize| {
711        exploded
712            .manifest
713            .rows
714            .iter()
715            .find(|r| r.chunk_index == i)
716            .unwrap()
717    };
718
719    // Budget from a conservative header estimate (every use/preamble/extern
720    // chunk), then plan once. The copied group header is the plan's header —
721    // the module's own plain imports — minus `use super::*` (the parent glob,
722    // which `{name}/mod.rs` forwards instead). Re-exports and `mod`
723    // declarations are in `root_items`, kept in `{name}/mod.rs`; a nested
724    // `mod x;` still resolves to `{name}/x.rs` after the move.
725    let header_estimate: usize = exploded
726        .manifest
727        .rows
728        .iter()
729        .filter(|r| matches!(r.kind.as_str(), "use" | "preamble" | "extern_crate"))
730        .map(|r| r.loc)
731        .sum();
732    let item_budget = max_loc
733        .saturating_sub(header_estimate + PART_OVERHEAD)
734        .max(1);
735    let plan = plan_split(&exploded, item_budget);
736    let is_super_glob = |t: &str| t.replace(' ', "").contains("usesuper::*;");
737    let group_header: String = plan
738        .header
739        .iter()
740        .map(|&i| exploded.chunks[i].text.as_str())
741        .filter(|t| !is_super_glob(t))
742        .collect();
743
744    let mut files = Vec::new();
745    let mut decls = String::new();
746    let mut reexports = String::new();
747    let mut still = Vec::new();
748    for (idx, part) in plan.parts.iter().enumerate() {
749        let module = format!("g{idx:02}");
750        let mut body = String::new();
751        for &ci in &part.chunk_indices {
752            body.push_str(&bump_visibility(exploded.chunks[ci].text.as_str()));
753        }
754        let contents = format!("{group_header}\nuse super::*;\n\n{body}");
755        files.push(OutputFile {
756            path: format!("{name}/{module}.rs"),
757            loc: loc(&contents),
758            contents,
759        });
760        decls.push_str(&format!("mod {module};\n"));
761        reexports.push_str(&format!("pub(crate) use {module}::*;\n"));
762    }
763    for &mod_index in &plan.mods {
764        let chunk = exploded.chunks[mod_index].text.as_str();
765        let mod_name = row(mod_index).name.as_str();
766        let Some((decl, mod_inner)) = extract_mod(chunk) else {
767            decls.push_str(chunk);
768            continue;
769        };
770        decls.push_str(&decl);
771        let mod_loc = loc(&mod_inner);
772        if mod_loc < max_loc {
773            files.push(OutputFile {
774                path: format!("{name}/{mod_name}.rs"),
775                contents: mod_inner,
776                loc: mod_loc,
777            });
778        } else {
779            let (nested, nested_still) = split_nested_mod(mod_name, &mod_inner, max_loc);
780            files.extend(nested.into_iter().map(|mut f| {
781                f.path = format!("{name}/{}", f.path);
782                f
783            }));
784            still.extend(
785                nested_still
786                    .into_iter()
787                    .map(|message| format!("{name}::{message}")),
788            );
789        }
790    }
791    for over in &plan.oversized {
792        still.push(format!(
793            "{name}::{} ({} LOC {}, leaf needs manual extraction)",
794            over.name, over.loc, over.kind
795        ));
796    }
797
798    let root_kept: String = plan
799        .root_items
800        .iter()
801        .map(|&i| exploded.chunks[i].text.as_str())
802        .collect();
803    let modrs = format!("pub(crate) use super::*;\n\n{root_kept}{decls}\n{reexports}");
804    files.insert(
805        0,
806        OutputFile {
807            path: format!("{name}/mod.rs"),
808            loc: loc(&modrs),
809            contents: modrs,
810        },
811    );
812    (files, still)
813}
814
815#[cfg(test)]
816mod tests {
817    use super::*;
818    use crate::{explode, plan_split};
819
820    #[test]
821    fn splits_a_flat_bin_root_into_cohesive_under_budget_modules() {
822        // four cohesive groups (g{k}_0 -> g{k}_1 -> g{k}_2 -> g{k}_3); main calls g0_0
823        let mut src = String::from("use std::fmt;\n\n");
824        for k in 0..4 {
825            for n in 0..4 {
826                let call = if n < 3 {
827                    format!("    g{k}_{}();\n", n + 1)
828                } else {
829                    String::new()
830                };
831                src.push_str(&format!("fn g{k}_{n}() {{\n{call}}}\n\n"));
832            }
833        }
834        src.push_str("fn main() {\n    g0_0();\n}\n");
835        let exploded = explode(&src).unwrap();
836        let max_loc = 60;
837        let out = split_bin(&exploded, max_loc, "main");
838
839        assert!(out.still_oversized.is_empty());
840        for f in &out.files {
841            assert!(f.loc < max_loc, "{} over budget: {}", f.path, f.loc);
842        }
843        let root = out.files.iter().find(|f| f.path == "main.rs").unwrap();
844        assert!(root.contents.contains("fn main"), "main stays at root");
845        assert!(root.contents.contains("pub(crate) use "));
846        let part = out.files.iter().find(|f| f.path != "main.rs").unwrap();
847        assert!(part.contents.contains("use crate::*;"));
848        assert!(part.contents.contains("pub(crate) fn "));
849        // cohesion: the whole g0 group lands in ONE module
850        let g0 = out
851            .files
852            .iter()
853            .find(|f| f.contents.contains("fn g0_1"))
854            .unwrap();
855        for n in 0..4 {
856            assert!(
857                g0.contents.contains(&format!("fn g0_{n}")),
858                "g0 group together"
859            );
860        }
861    }
862
863    #[test]
864    fn oversized_mod_is_extracted_to_its_own_file() {
865        let mut inner = String::new();
866        for i in 0..10 {
867            inner.push_str(&format!("    fn t{i}() {{}}\n"));
868        }
869        let src = format!("fn main() {{}}\n\n#[cfg(test)]\nmod tests {{\n{inner}}}\n");
870        let exploded = explode(&src).unwrap();
871        let plan = plan_split(&exploded, 6);
872        // ceiling large enough that the test mod fits whole -> single tests.rs
873        let out = reassemble_bin(&exploded, &plan, "main", 1000);
874        let tests = out.files.iter().find(|f| f.path == "tests.rs").unwrap();
875        assert!(tests.contents.contains("fn t0()"));
876        assert!(!tests.contents.contains("mod tests"));
877        let root = out.files.iter().find(|f| f.path == "main.rs").unwrap();
878        assert!(root.contents.contains("#[cfg(test)]\nmod tests;"));
879    }
880
881    #[test]
882    fn an_over_ceiling_test_mod_is_nested_into_a_subdir() {
883        // 30 test fns referencing a shared helper -> test mod far over a tiny ceiling
884        let mut inner =
885            String::from("    use super::*;\n\n    fn helper() -> i32 {\n        1\n    }\n");
886        for i in 0..30 {
887            inner.push_str(&format!(
888                "\n    #[test]\n    fn t{i}() {{\n        assert_eq!(helper(), 1);\n    }}\n"
889            ));
890        }
891        let src = format!(
892            "pub fn helper() -> i32 {{\n    1\n}}\n\nfn main() {{}}\n\n#[cfg(test)]\nmod tests {{\n{inner}}}\n"
893        );
894        let exploded = explode(&src).unwrap();
895        let out = split_bin(&exploded, 60, "main");
896
897        // the test mod became a `tests/` subdir, every file under the ceiling
898        let mod_rs = out.files.iter().find(|f| f.path == "tests/mod.rs").unwrap();
899        assert!(
900            mod_rs.contents.contains("pub(crate) use super::*;"),
901            "forwards the parent"
902        );
903        assert!(mod_rs.contents.contains("mod g00;"));
904        assert!(out.files.iter().any(|f| f.path == "tests/g00.rs"));
905        for f in &out.files {
906            assert!(f.loc < 60, "{} over ceiling: {}", f.path, f.loc);
907        }
908        let root = out.files.iter().find(|f| f.path == "main.rs").unwrap();
909        assert!(root.contents.contains("#[cfg(test)]\nmod tests;"));
910    }
911
912    #[test]
913    fn split_mod_uses_super_and_preserves_pub_api() {
914        // alpha (pub, the module's API) calls beta (private helper) -> one cluster
915        let src = "use std::fmt;\n\npub fn alpha() -> i32 {\n    beta()\n}\n\nfn beta() -> i32 {\n    1\n}\n";
916        let exploded = explode(src).unwrap();
917        let out = split_mod(&exploded, 10_000, "mod");
918
919        let root = out.files.iter().find(|f| f.path == "mod.rs").unwrap();
920        assert!(
921            root.contents.contains("pub use "),
922            "pub re-export preserves the API"
923        );
924        assert!(
925            !root.contents.contains("pub(crate) use "),
926            "a single `pub use *` caps each item at its own visibility — no \
927             redundant second `pub(crate) use` line"
928        );
929
930        let sub = out.files.iter().find(|f| f.path != "mod.rs").unwrap();
931        assert!(
932            sub.contents.contains("use super::*;"),
933            "siblings reached via super, not crate"
934        );
935        assert!(!sub.contents.contains("use crate::*;"));
936        assert!(sub.contents.contains("pub fn alpha"), "pub item stays pub");
937        assert!(
938            sub.contents.contains("pub(crate) fn beta"),
939            "private bumped to pub(crate)"
940        );
941    }
942
943    // ----- issue 1: a plain `foo.rs` file module places sub-modules in `foo/` ---
944
945    #[test]
946    fn file_module_places_submodules_in_a_subdir() {
947        // A plain `foo.rs` file module (stem != "mod"): `mod bar;` in it resolves
948        // to `foo/bar.rs`, so parts must be written under a `foo/` subdir, not as
949        // siblings of `foo.rs` (which would not compile — E0583).
950        let mut src = String::new();
951        for k in 0..3 {
952            // three items that reference nothing -> three separate parts
953            src.push_str(&format!("pub fn item{k}() -> i32 {{\n    {k}\n}}\n\n"));
954        }
955        let exploded = explode(&src).unwrap();
956        let out = split_mod(&exploded, 10_000, "wstest");
957
958        let root = out.files.iter().find(|f| f.path == "wstest.rs").unwrap();
959        assert!(
960            root.contents.contains("mod item0;"),
961            "root file-module declares the sub-modules"
962        );
963        let subs: Vec<&str> = out
964            .files
965            .iter()
966            .map(|f| f.path.as_str())
967            .filter(|p| *p != "wstest.rs")
968            .collect();
969        assert!(!subs.is_empty(), "produced sub-module files");
970        for path in &subs {
971            assert!(
972                path.starts_with("wstest/"),
973                "sub-module {path} must live under wstest/ (file-module subdir)"
974            );
975        }
976    }
977
978    #[test]
979    fn mod_rs_keeps_submodules_as_siblings() {
980        // The `foo/mod.rs` case (stem == "mod"): sub-modules stay siblings, no subdir.
981        let mut src = String::new();
982        for k in 0..3 {
983            src.push_str(&format!("pub fn item{k}() -> i32 {{\n    {k}\n}}\n\n"));
984        }
985        let exploded = explode(&src).unwrap();
986        let out = split_mod(&exploded, 10_000, "mod");
987        for f in &out.files {
988            assert!(
989                !f.path.contains('/'),
990                "mod.rs sub-modules are siblings, not in a subdir: {}",
991                f.path
992            );
993        }
994    }
995
996    // ----- issue 2: per-module pub vs pub(crate) vs no re-export decision -------
997
998    #[test]
999    fn private_only_module_reexports_pub_crate_not_pub() {
1000        // No `pub` item: `pub use *` would warn "nothing public enough", so the
1001        // re-export must be `pub(crate) use`.
1002        let src = "fn helper() -> i32 {\n    leaf()\n}\n\nfn leaf() -> i32 {\n    1\n}\n";
1003        let exploded = explode(src).unwrap();
1004        let out = split_mod(&exploded, 10_000, "mod");
1005        let root = out.files.iter().find(|f| f.path == "mod.rs").unwrap();
1006        assert!(
1007            root.contents.contains("pub(crate) use "),
1008            "all-private module re-exports pub(crate):\n{}",
1009            root.contents
1010        );
1011        assert!(
1012            !root.contents.contains("pub use "),
1013            "no `pub use` for a module with no public item"
1014        );
1015    }
1016
1017    #[test]
1018    fn impl_only_module_is_declared_but_not_reexported() {
1019        // `api` is a normal part; the bare impl references no sibling, so it
1020        // clusters alone with nothing nameable -> declared, but no glob re-export
1021        // (re-exporting it would only warn "doesn't reexport anything").
1022        let src = "pub fn api() -> i32 {\n    1\n}\n\nimpl Worker for External {\n    fn run(&self) {}\n}\n";
1023        let exploded = explode(src).unwrap();
1024        let out = split_mod(&exploded, 10_000, "mod");
1025        let root = out.files.iter().find(|f| f.path == "mod.rs").unwrap();
1026
1027        // exactly one glob re-export line (for `api`) though two modules declared
1028        let reexports = root
1029            .contents
1030            .lines()
1031            .filter(|l| l.contains(" use ") && l.trim_end().ends_with("::*;"))
1032            .count();
1033        assert_eq!(
1034            reexports, 1,
1035            "only the nameable module is re-exported:\n{}",
1036            root.contents
1037        );
1038        assert!(root.contents.contains("pub use api::*;"));
1039        assert_eq!(
1040            root.contents.matches("mod ").count(),
1041            2,
1042            "both modules are still declared:\n{}",
1043            root.contents
1044        );
1045    }
1046
1047    // ----- issue 3: crate-public entry fn is preserved as `pub use` -------------
1048
1049    #[test]
1050    fn bin_public_entry_fn_is_reexported_pub_so_it_stays_crate_visible() {
1051        // A lib crate root where `run` is the public entry (the bin calls
1052        // `crate_name::run`). It must be re-exported `pub`, not `pub(crate)`, or
1053        // `crate_name::run` is no longer visible to main.rs after the split.
1054        let src = "fn helper() -> i32 {\n    1\n}\n\npub fn run() -> i32 {\n    helper()\n}\n";
1055        let exploded = explode(src).unwrap();
1056        let out = split_bin(&exploded, 10_000, "lib");
1057
1058        let root = out.files.iter().find(|f| f.path == "lib.rs").unwrap();
1059        assert!(
1060            root.contents.contains("pub use "),
1061            "public `run` re-exported at pub keeps `crate::run` visible:\n{}",
1062            root.contents
1063        );
1064        assert!(
1065            !root.contents.contains("pub(crate) use "),
1066            "the module has a pub item, so one `pub use *` suffices (no double line)"
1067        );
1068    }
1069
1070    // ----- preamble (inner `//!`/`#![…]`) stays at the root, not copied to parts -
1071
1072    #[test]
1073    fn crate_preamble_stays_at_root_not_copied_into_parts() {
1074        // `#![allow]` / `//!` belong at the crate root (and `#![feature]` is a
1075        // hard error anywhere else); a crate-root attr already covers sub-modules.
1076        let src = "//! crate docs\n#![allow(dead_code)]\n\npub fn alpha() -> i32 {\n    beta()\n}\n\nfn beta() -> i32 {\n    1\n}\n";
1077        let exploded = explode(src).unwrap();
1078        let out = split_bin(&exploded, 10_000, "lib");
1079
1080        let root = out.files.iter().find(|f| f.path == "lib.rs").unwrap();
1081        assert!(
1082            root.contents.contains("#![allow(dead_code)]"),
1083            "preamble stays at the root"
1084        );
1085        for f in &out.files {
1086            if f.path != "lib.rs" {
1087                assert!(
1088                    !f.contents.contains("#!["),
1089                    "part {} must not carry crate-level inner attrs",
1090                    f.path
1091                );
1092                assert!(
1093                    !f.contents.contains("//! crate docs"),
1094                    "part {} must not duplicate the crate docs",
1095                    f.path
1096                );
1097            }
1098        }
1099    }
1100
1101    // ----- issue 3a: copy only the imports each file actually references -------
1102
1103    #[test]
1104    fn parts_copy_only_the_imports_they_reference() {
1105        // alpha names `Display`; bravo names nothing from the header. Each part
1106        // gets only the imports it references.
1107        let src = "use std::fmt::Display;\nuse std::collections::HashMap;\n\npub fn alpha(d: &dyn Display) {\n    let _ = d;\n}\n\npub fn bravo() -> i32 {\n    1\n}\n";
1108        let exploded = explode(src).unwrap();
1109        let out = split_mod(&exploded, 10_000, "mod");
1110
1111        let alpha = out
1112            .files
1113            .iter()
1114            .find(|f| f.contents.contains("fn alpha"))
1115            .unwrap();
1116        let bravo = out
1117            .files
1118            .iter()
1119            .find(|f| f.contents.contains("fn bravo"))
1120            .unwrap();
1121        assert!(
1122            alpha.contents.contains("use std::fmt::Display;"),
1123            "alpha references Display:\n{}",
1124            alpha.contents
1125        );
1126        assert!(
1127            !alpha.contents.contains("HashMap"),
1128            "alpha never names HashMap -> import dropped:\n{}",
1129            alpha.contents
1130        );
1131        assert!(
1132            !bravo.contents.contains("use std::fmt::Display;"),
1133            "bravo references nothing from the header:\n{}",
1134            bravo.contents
1135        );
1136        assert!(!bravo.contents.contains("HashMap"));
1137    }
1138
1139    #[test]
1140    fn globs_and_anonymous_trait_imports_are_always_copied() {
1141        // `use ...::*` and `use Trait as _` expose no name to match, so every
1142        // part keeps them — the safe choice for a trait used only via methods.
1143        let src = "use std::fmt::Write as _;\nuse std::prelude::v1::*;\n\npub fn writes(s: &mut String) {\n    let _ = write!(s, \"x\");\n}\n\npub fn other() -> i32 {\n    2\n}\n";
1144        let exploded = explode(src).unwrap();
1145        let out = split_mod(&exploded, 10_000, "mod");
1146        for f in &out.files {
1147            if f.path == "mod.rs" {
1148                continue;
1149            }
1150            assert!(
1151                f.contents.contains("use std::fmt::Write as _;"),
1152                "{} must keep the `as _` trait import:\n{}",
1153                f.path,
1154                f.contents
1155            );
1156            assert!(
1157                f.contents.contains("use std::prelude::v1::*;"),
1158                "{} must keep the glob import:\n{}",
1159                f.path,
1160                f.contents
1161            );
1162        }
1163    }
1164
1165    #[test]
1166    fn item_less_root_keeps_no_imports() {
1167        // A module root whose items all moved out is just decls + re-exports; it
1168        // needs no imports, while the part that uses one keeps it.
1169        let src = "use std::collections::HashMap;\n\npub fn solo(m: HashMap<u8, u8>) {\n    let _ = m;\n}\n";
1170        let exploded = explode(src).unwrap();
1171        let out = split_mod(&exploded, 10_000, "mod");
1172
1173        let root = out.files.iter().find(|f| f.path == "mod.rs").unwrap();
1174        assert!(
1175            !root.contents.contains("use std::collections::HashMap;"),
1176            "item-less root drops imports it doesn't use:\n{}",
1177            root.contents
1178        );
1179        let part = out.files.iter().find(|f| f.path != "mod.rs").unwrap();
1180        assert!(
1181            part.contents.contains("use std::collections::HashMap;"),
1182            "the part that uses HashMap keeps it"
1183        );
1184    }
1185
1186    // ----- razel regression 1: source re-exports must stay `pub use` at root --
1187
1188    #[test]
1189    fn source_pub_use_reexports_stay_pub_use_at_the_root() {
1190        // The original file's `pub use` lines ARE the crate's public API.
1191        // Treating them as droppable header imports (or demoting them to
1192        // `pub(crate) use`) silently breaks every dependent's paths.
1193        let src = "mod helpers;\n\npub use helpers::Helper;\n\nuse std::fmt::Display;\n\n\
1194                   pub fn api(d: &dyn Display) {\n    let _ = d;\n}\n";
1195        let exploded = explode(src).unwrap();
1196        let out = split_bin(&exploded, 10_000, "lib");
1197
1198        let root = out.files.iter().find(|f| f.path == "lib.rs").unwrap();
1199        assert!(
1200            root.contents.contains("mod helpers;"),
1201            "content-less mod decl stays at the root (it binds helpers.rs):\n{}",
1202            root.contents
1203        );
1204        assert!(
1205            root.contents.contains("pub use helpers::Helper;"),
1206            "source re-export stays verbatim at the root:\n{}",
1207            root.contents
1208        );
1209        assert!(
1210            !root.contents.contains("pub(crate) use helpers"),
1211            "a source re-export must never be demoted"
1212        );
1213        for f in &out.files {
1214            if f.path != "lib.rs" {
1215                assert!(
1216                    !f.contents.contains("pub use helpers::Helper;"),
1217                    "{} must not carry the root's re-export",
1218                    f.path
1219                );
1220                assert!(
1221                    !f.contents.contains("mod helpers;"),
1222                    "{} must not carry the root's mod decl (would look for a \
1223                     sibling subdir file)",
1224                    f.path
1225                );
1226            }
1227        }
1228    }
1229
1230    // ----- razel regression 2: the cfg(test) gate travels with the tests mod --
1231
1232    #[test]
1233    fn under_budget_cfg_test_mod_is_extracted_whole_with_its_gate() {
1234        // The razel failure: the tests mod fit under the item budget, so it was
1235        // CLUSTERED — wrapped inside a part module with an ungated root decl —
1236        // instead of extracted. A body mod must always move whole, its
1237        // `#[cfg(test)]` gating the root declaration.
1238        let src = "pub fn alpha() -> i32 {\n    1\n}\n\n\
1239                   #[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    \
1240                   fn smoke() {\n        assert_eq!(alpha(), 1);\n    }\n}\n";
1241        let exploded = explode(src).unwrap();
1242        // Budget far above the tests mod's size: it would have been clustered.
1243        let out = split_bin(&exploded, 10_000, "lib");
1244
1245        let root = out.files.iter().find(|f| f.path == "lib.rs").unwrap();
1246        assert!(
1247            root.contents.contains("#[cfg(test)]\nmod tests;"),
1248            "the gate must travel to the root declaration:\n{}",
1249            root.contents
1250        );
1251        assert!(
1252            !root.contents.contains("use tests::*"),
1253            "an extracted mod is declared, not glob re-exported:\n{}",
1254            root.contents
1255        );
1256        let tests = out.files.iter().find(|f| f.path == "tests.rs").unwrap();
1257        assert!(tests.contents.contains("fn smoke()"));
1258        assert!(
1259            !tests.contents.contains("mod tests"),
1260            "the body is unwrapped — no double nesting:\n{}",
1261            tests.contents
1262        );
1263    }
1264
1265    #[test]
1266    fn pub_mod_keeps_its_visibility_on_the_extracted_decl() {
1267        let src = "pub fn api() -> i32 {\n    1\n}\n\n\
1268                   /// Config store.\npub mod config {\n    pub fn get() -> i32 {\n        2\n    }\n}\n";
1269        let exploded = explode(src).unwrap();
1270        let out = split_bin(&exploded, 10_000, "lib");
1271
1272        let root = out.files.iter().find(|f| f.path == "lib.rs").unwrap();
1273        assert!(
1274            root.contents.contains("/// Config store.\npub mod config;"),
1275            "docs and `pub` travel to the declaration:\n{}",
1276            root.contents
1277        );
1278        let config = out.files.iter().find(|f| f.path == "config.rs").unwrap();
1279        assert!(config.contents.contains("pub fn get()"));
1280    }
1281
1282    #[test]
1283    fn part_colliding_with_an_extracted_mod_name_is_renamed() {
1284        // `fn tests` (value namespace) and `mod tests` (type namespace) can
1285        // coexist in the source; the extracted mod owns `tests.rs`, so the
1286        // part named after the fn must yield.
1287        let src = "fn tests() -> i32 {\n    1\n}\n\n\
1288                   #[cfg(test)]\nmod tests {\n    fn smoke() {}\n}\n";
1289        let exploded = explode(src).unwrap();
1290        let out = split_bin(&exploded, 10_000, "lib");
1291
1292        let paths: Vec<&str> = out.files.iter().map(|f| f.path.as_str()).collect();
1293        assert!(paths.contains(&"tests.rs"), "extracted mod file: {paths:?}");
1294        assert!(paths.contains(&"tests_.rs"), "renamed part file: {paths:?}");
1295        let root = out.files.iter().find(|f| f.path == "lib.rs").unwrap();
1296        assert!(root.contents.contains("mod tests_;"));
1297        assert!(root.contents.contains("#[cfg(test)]\nmod tests;"));
1298    }
1299
1300    #[test]
1301    fn root_keeps_imports_an_extracted_mod_body_references() {
1302        // The extracted tests mod reaches the root's imports via
1303        // `use super::*;` — dropping HashMap from the root would break it.
1304        let src = "use std::collections::HashMap;\n\n\
1305                   pub fn alpha() -> i32 {\n    1\n}\n\n\
1306                   #[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    \
1307                   fn smoke() {\n        let _m: HashMap<u8, u8> = HashMap::new();\n        \
1308                   assert_eq!(alpha(), 1);\n    }\n}\n";
1309        let exploded = explode(src).unwrap();
1310        let out = split_bin(&exploded, 10_000, "lib");
1311
1312        let root = out.files.iter().find(|f| f.path == "lib.rs").unwrap();
1313        assert!(
1314            root.contents.contains("use std::collections::HashMap;"),
1315            "root must keep imports the extracted body names:\n{}",
1316            root.contents
1317        );
1318    }
1319
1320    #[test]
1321    fn bin_root_keeps_only_imports_main_references() {
1322        // `main` names `exit` but not `HashMap` (the moved `worker` does), so the
1323        // root keeps `exit` and drops `HashMap`.
1324        let src = "use std::process::exit;\nuse std::collections::HashMap;\n\nfn worker() -> HashMap<u8, u8> {\n    HashMap::new()\n}\n\nfn main() {\n    let _ = worker();\n    exit(0);\n}\n";
1325        let exploded = explode(src).unwrap();
1326        let out = split_bin(&exploded, 10_000, "main");
1327
1328        let root = out.files.iter().find(|f| f.path == "main.rs").unwrap();
1329        assert!(
1330            root.contents.contains("use std::process::exit;"),
1331            "main references exit:\n{}",
1332            root.contents
1333        );
1334        assert!(
1335            !root.contents.contains("HashMap"),
1336            "main never names HashMap (worker does) -> dropped from root:\n{}",
1337            root.contents
1338        );
1339    }
1340}