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//! - unconditional body-carrying modules are extracted whole, with attributes
19//!   and visibility on their declarations; large bodies are split recursively;
20//! - conditional modules keep their braced boundaries inline, and oversized
21//!   leaves stay intact in their original module. An unmet budget never
22//!   justifies dropping a declaration.
23//!
24//! Residual import/visibility that this mechanical scheme misses is left for the
25//! compiler to enumerate (the documented O(n) finish). Unmet LOC budgets are
26//! reported alongside the preserved output.
27
28use crate::{Exploded, SplitPlan, plan_split};
29use syn::parse::Parser;
30use syn::spanned::Spanned;
31
32/// Per-part overhead beyond the imports: the `use crate::*;` sibling glob plus
33/// blank-line separators.
34const PART_OVERHEAD: usize = 3;
35
36/// Plan and reassemble a binary crate-root split targeting files `< max_loc`.
37/// Indivisible items and conditional modules stay in their original scope;
38/// consult `still_oversized` and `max_loc()` for unmet budget constraints.
39/// The item budget reserves the full header LOC and the per-part overhead; since a part now copies only the imports it references (a subset of
40/// the header), this reservation is conservative — a packed part stays safely
41/// under budget.
42pub 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/// A file the split would write.
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct OutputFile {
58    /// Path relative to the source file's directory (e.g. `main.rs`, `p00.rs`).
59    pub path: String,
60    pub contents: String,
61    pub loc: usize,
62}
63
64/// The reassembled output plus the parts of the plan that could not be met.
65#[derive(Debug, Clone)]
66pub struct SplitOutput {
67    pub files: Vec<OutputFile>,
68    /// Retained oversized items and output files at or above the LOC budget.
69    pub still_oversized: Vec<String>,
70}
71
72impl SplitOutput {
73    /// The largest output file's LOC (the budget is met iff this is `< max_loc`
74    /// and `still_oversized` is empty).
75    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/// How the split file sits in the module tree — fixes the sibling path prefix
89/// and where sub-module files go. Re-export visibility is per part in both
90/// topologies: `pub use` when the part has a public item (preserving the
91/// crate's public API), else `pub(crate) use`.
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub enum Topology {
94    /// A binary or library crate root (`main.rs`, `lib.rs`): siblings reached
95    /// via `crate::`, `fn main` (if any) kept at the root.
96    Bin,
97    /// A nested library module (`foo/mod.rs`, or a plain `foo.rs` file module):
98    /// sub-modules reached via `super::` and re-exported with a single per-module
99    /// glob — `pub use` to preserve a public API, else `pub(crate) use`. A plain
100    /// `foo.rs` file module resolves `mod bar;` to `foo/bar.rs`, so its
101    /// sub-modules are written into a `foo/` subdir.
102    Mod,
103}
104
105/// Plan and reassemble a library module targeting files `< max_loc`;
106/// sub-modules see each other via `super::*`. Unmet budgets are reported
107/// without discarding declarations. Handles both a directory-owning
108/// `foo/mod.rs` (sub-modules become siblings) and a plain `foo.rs` file module
109/// (sub-modules go in a `foo/` subdir). `root_stem` is the source file's stem.
110pub 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
123/// Reassemble a binary crate-root split (back-compat wrapper).
124pub 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
133/// Reassemble a split for the given module topology. `root_stem` is the source
134/// file's stem (`"main"` for `main.rs`, `"mod"` for `foo/mod.rs`). `max_loc` is
135/// the ceiling, needed to recursively split an oversized test module.
136fn 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    // A part is a module *below* the file it came from, whichever topology, so
148    // the header imports it copies have to be re-anchored one level up.
149    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    // Where sub-module files go. A directory-owning module file
164    // (`main.rs`/`lib.rs`/`foo/mod.rs`) keeps them as siblings; a plain `foo.rs`
165    // file module resolves `mod bar;` to `foo/bar.rs`, so its parts go in `foo/`.
166    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    // The crate/module preamble (inner `//!` docs / `#![...]` attrs) stays at
173    // the root only — a crate-root `#![feature]` is a hard error in a sub-module
174    // file, and a crate-root `#![allow]` already covers descendants. The import
175    // chunks (`use`/`extern crate`) are copied into each file, but only the ones
176    // that file references by name (see `select_imports`). Re-exports
177    // (`pub use ...`) are not imports — the planner routes them to
178    // `plan.root_items`, kept at the root verbatim.
179    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    // Source chunks that stay at the root verbatim: `pub use` re-exports (the
193    // crate's public surface) and content-less `mod name;` declarations (which
194    // bind files relative to the root's directory).
195    let root_kept: String = plan.root_items.iter().map(|&i| chunk_text(i)).collect();
196
197    // Module names fixed by the source — extracted body-mods and `mod name;`
198    // declarations keep their own names — plus the root's stem; a generated
199    // part that would collide is renamed with a `_` suffix.
200    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    // Names referenced from the root file itself (`fn main`, extracted module
214    // bodies whose `use super::*` reaches the root's imports).
215    let mut root_refs = Refs::default();
216
217    // Part modules, named after their cluster's dominant item.
218    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        // Widest visibility any moved item contributes to a glob re-export.
227        let mut export_vis = ExportVis::None;
228        // Names this part references, so only the imports it uses are copied in.
229        let mut refs = Refs::default();
230        for &i in &part.chunk_indices {
231            let r = row(i);
232            // `fn main` must stay at the crate root, not move into a submodule.
233            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 the part was only `fn main`, it produced no module file.
245        if body.trim().is_empty() {
246            if let Some(i) = root_main {
247                // stash main to emit at root via a sentinel part with no module
248                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        // One glob re-export per module, visibility chosen by the module's widest
267        // item: `pub use` when it has a public item (a `pub use *` caps each item
268        // at its own visibility, so it covers internal `pub(crate)` items in the
269        // same line); else `pub(crate) use`; and nothing when the module exposes
270        // no nameable item (e.g. a bare `impl` — a glob re-export would only warn
271        // "doesn't reexport anything"). This keeps the crate's public surface
272        // (`pub use`) reachable and avoids the redundant double re-export.
273        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    // Unconditional modules are extracted; conditional modules keep their
285    // original braced scope, including mutually exclusive same-named bodies.
286    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            // Conditional or unparseable modules remain intact at the root.
291            root_refs.add(chunk);
292            mod_decls.push_str(chunk);
293            continue;
294        };
295        mod_decls.push_str(&decl);
296        // The body's `use super::*;` reaches the root's imports, so the root
297        // must keep the imports the body names.
298        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    // Keep oversized leaves at their original scope, without visibility or
317    // path changes. Retain imports they reference just as for `main`.
318    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    // Pull the stashed `fn main` (if any) and build the root file.
330    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    // The root keeps the preamble, then only the imports its own retained code
341    // references — `fn main` (if any) plus extracted module bodies, whose
342    // `use super::*;` resolves against the root. A root that is just module
343    // declarations and re-exports needs no imports at all.
344    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        // The root file *is* the original module: its own imports still mean
351        // what they meant.
352        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    // `include!`/`include_str!`/`include_bytes!` resolve their argument against
379    // the directory of the file that contains them, so a body written into a
380    // subdirectory needs one `../` per level it moved down. The root file did
381    // not move (no `/` in its path), and neither did a part that stayed a
382    // sibling. Inserting `../` adds no lines, so `loc` is unaffected.
383    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
406/// Macros whose single argument is a path resolved relative to the *file* that
407/// contains them — the one construct a byte-exact move cannot leave alone.
408const PATH_MACROS: [&str; 3] = ["include", "include_str", "include_bytes"];
409
410/// Re-base the relative path arguments of the `include*!` macros in a file
411/// written `levels` directories below the source file. Token-driven, so a path
412/// spelled inside a comment or an unrelated string is never touched. An
413/// absolute path is left alone; so is a non-literal argument (`concat!`,
414/// `env!`, a macro, a constant), which gets one warning line on stderr naming
415/// the generated file and line so the user knows where to look.
416fn 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
426/// Walk a token stream for `include*!(...)` calls, recording where a `../`
427/// prefix must be inserted and warning about the arguments that cannot be
428/// re-based mechanically.
429fn 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            // A literal that must not move (absolute path): nothing to do.
448            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
461/// Whether the group at `index` is the argument list of an `include*!` call,
462/// i.e. the two preceding tokens are the macro name and its `!`.
463fn 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
476/// For an `include*!` argument list: `Some(Some(offset))` is the byte offset
477/// just inside the opening quote of a re-basable relative path literal,
478/// `Some(None)` a literal that must stay put (an absolute path), and `None` an
479/// argument that is not a single plain string literal with an optional comma.
480fn 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    // An absolute path does not move with the file; an empty one is not a path.
494    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    // Insert just inside the opening quote — `"…"`, `r"…"` and `r#"…"#` alike —
500    // so the literal's own escaping is preserved byte-for-byte.
501    let quote = text.find('"')?;
502    Some(Some(literal.span().byte_range().start + quote + 1))
503}
504
505/// Raise a moved item — and the struct fields / impl members referenced across
506/// the new module boundaries — to `pub(crate)`, so the crate-root re-exports can
507/// see them. Span-driven (parse the chunk, insert at exact offsets), so it is
508/// attribute-safe and handles `fn` modifiers, multi-line attrs, etc. Items
509/// already `pub`/`pub(crate)` are left alone. Over-exposes within the crate,
510/// which is the mechanical, behavior-preserving choice.
511fn 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
528/// Byte offset where a function's visibility belongs: before any
529/// `const`/`async`/`unsafe`/`extern` modifier, else before `fn`.
530fn 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        // Only inherent impls (`impl Type`) allow member visibility; trait-impl
589        // members (`impl Trait for Type`) inherit the trait's and must not.
590        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
625/// Assemble a part file: its selected imports, the sibling glob so it can reach
626/// the rest of the module via the root re-exports, then the moved items.
627fn 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
635/// The glob re-export line for a module given the widest visibility of its
636/// items — or `None` when it exposes nothing nameable (re-exporting an
637/// `impl`-only module would only warn).
638fn 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/// What a moved item contributes to its module's glob re-export. Ordered so the
647/// widest contribution across a module's items picks the re-export visibility.
648#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
649enum ExportVis {
650    /// No nameable export (a bare `impl`, a macro invocation, parse failure).
651    None,
652    /// A crate-visible name: a private item (bumped to `pub(crate)`), or one
653    /// already `pub(crate)`/`pub(super)`/`pub(in …)`.
654    Crate,
655    /// A fully public name — part of the crate/module's public surface.
656    Pub,
657}
658
659/// Classify one chunk's contribution to its module's glob re-export, mirroring
660/// `bump_visibility`: items it raises to `pub(crate)` count as crate-visible;
661/// `mod`/`use` are not raised, so a private one is invisible to the re-export.
662fn 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        // Unparseable: bump_visibility also left it private, so a glob re-export
666        // could not see it either — contribute nothing.
667        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        // impl / macro / foreign_mod / extern_crate / verbatim: nothing nameable.
682        _ => 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/// The identifiers a set of moved items references — the basis for copying only
693/// the imports they use. An item that does not re-parse flips `keep_all`, so the
694/// caller falls back to copying every import rather than dropping a needed one.
695#[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    /// Add every identifier a multi-item source fragment (an extracted module
710    /// body) references.
711    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    /// True when nothing was added — no import can be needed.
719    fn is_empty(&self) -> bool {
720        self.names.is_empty() && !self.keep_all
721    }
722
723    /// Whether an import providing `names` should be copied in. A name-less
724    /// import (`None`: a `*` glob or `as _`) is always kept, since its usage
725    /// can't be detected by name.
726    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/// How a copied `use` path must be re-anchored, given where the copy lands
735/// relative to the module the import was written in.
736#[derive(Debug, Clone, Copy, PartialEq, Eq)]
737enum Rebase {
738    /// Same module — the paths already mean what they meant (the root file, and
739    /// any body extracted to the module level it already had).
740    Keep,
741    /// One module level below a nested module: `super::` needs one more
742    /// `super::`, and `self::` (the old module) becomes `super::`.
743    Nested,
744    /// One module level below a crate root: `self::` becomes `crate::`. A
745    /// crate root has no `super::`, so nothing else can move.
746    CrateRoot,
747}
748
749/// Re-anchor a copied `use` chunk for a file one module level below the one it
750/// was written in. Only the leading segment moves: `crate::`, `::`-rooted and
751/// external paths are untouched, and a re-export (`pub use …`) is never
752/// rewritten. Span-driven on the re-parsed chunk, so leading comments,
753/// attributes and formatting survive byte-exactly.
754fn 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    // A re-export is the module's public surface and stays at the root verbatim;
762    // a `::`-rooted path is already absolute.
763    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
771/// Collect the byte range and replacement text for each leading `super`/`self`
772/// segment of a `use` tree. A top-level group (`use {super::a, self::b};`)
773/// anchors each of its items separately, so it recurses; a group *after* a path
774/// segment does not (`use super::{self, a};` is anchored by the `super`).
775fn 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
806/// Replace the given (non-overlapping, possibly empty) byte ranges in `text`.
807/// An empty range is an insertion point.
808fn 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
825/// Re-anchor the `super::` / `self::` paths *inside* a moved item — expression
826/// and type paths, function-local `use` items, and macro invocation paths — for
827/// a body that lands one module level below its original file. Attribute and
828/// arbitrary macro input tokens are opaque; arguments to common standard
829/// expression macros are parsed before their paths are rewritten. Comments,
830/// strings, receivers, and visibility restrictions stay byte-exact.
831fn 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        // These standard macros accept comma-separated Rust expressions.
901        // Unknown macro grammars remain opaque even when their tokens look Rust-like.
902        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
916/// Concatenate, in source order, the import chunks referenced by `refs` (plus
917/// name-less imports like `*` globs and `as _`, always kept), each re-anchored
918/// for where it lands. This is the aggressive policy: an import whose every name
919/// is unreferenced is dropped — including an extension trait used only via its
920/// methods, whose `use` the compiler will then ask to be re-added.
921fn 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
937/// The names a `use`/`extern crate` chunk introduces, or `None` when it exposes
938/// nothing referenceable by name — a `*` glob or an `as _` import — which is
939/// then kept in every file rather than dropped.
940fn 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        // An unparseable header chunk has no detectable name -> keep it.
956        _ => None,
957    }
958}
959
960/// Collect the bound names a `use` tree introduces. `name_less` is set for a
961/// `*` glob or an `as _` rename, which bind no referenceable name.
962fn 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
988/// Extract an unconditional inline module using parser spans. Keep leading
989/// comments, attributes and visibility on its declaration. Conditional modules
990/// remain braced and inline: extracting them to a name-only file would merge
991/// mutually exclusive bodies and leave a gate on an unbraced declaration.
992/// Conservatively retain all `cfg_attr` modules, including nested predicates.
993fn 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    // Drop the newline that followed `{` and the closing brace's indentation.
1012    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
1020/// Recursively split an over-budget module's inner body into a `{name}/`
1021/// subdirectory: `{name}/mod.rs` forwards the enclosing module's items
1022/// (`pub(crate) use super::*`), keeps the body's own re-exports and `mod`
1023/// declarations verbatim, and re-exports the clusters; each `{name}/gNN.rs`
1024/// holds a cohesive cluster and reaches everything via `super::*`. A nested
1025/// unconditional module is extracted whole, recursing above the ceiling;
1026/// conditional modules stay braced and inline. Oversized leaves stay in
1027/// `{name}/mod.rs` with their original attributes, visibility and imports.
1028fn 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    // Budget from a conservative header estimate (every use/preamble/extern
1049    // chunk), then plan once. The copied group header is the plan's header —
1050    // the module's own plain imports — minus `use super::*` (the parent glob,
1051    // which `{name}/mod.rs` forwards instead). Re-exports and `mod`
1052    // declarations are in `root_items`, kept in `{name}/mod.rs`; a nested
1053    // `mod x;` still resolves to `{name}/x.rs` after the move.
1054    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    // `{name}/gNN.rs` is a module below `{name}`, so the body's own imports are
1067    // re-anchored the same way a part's are.
1068    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    // Keep the original preamble and imports in the owning module as well:
1142    // retained leaves and conditional bodies still resolve names here.
1143    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        // four cohesive groups (g{k}_0 -> g{k}_1 -> g{k}_2 -> g{k}_3); main calls g0_0
1170        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        // cohesion: the whole g0 group lands in ONE module
1197        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        // ceiling large enough that the test mod fits whole -> single tests.rs
1220        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        // 30 test fns referencing a shared helper -> test mod far over a tiny ceiling
1231        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        // the test mod became a `tests/` subdir, every file under the ceiling
1245        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        // alpha (pub, the module's API) calls beta (private helper) -> one cluster
1262        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    // ----- issue 1: a plain `foo.rs` file module places sub-modules in `foo/` ---
1291
1292    #[test]
1293    fn file_module_places_submodules_in_a_subdir() {
1294        // A plain `foo.rs` file module (stem != "mod"): `mod bar;` in it resolves
1295        // to `foo/bar.rs`, so parts must be written under a `foo/` subdir, not as
1296        // siblings of `foo.rs` (which would not compile — E0583).
1297        let mut src = String::new();
1298        for k in 0..3 {
1299            // three items that reference nothing -> three separate parts
1300            src.push_str(&format!("pub fn item{k}() -> i32 {{\n    {k}\n}}\n\n"));
1301        }
1302        let exploded = explode(&src).unwrap();
1303        let out = split_mod(&exploded, 10_000, "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        // The `foo/mod.rs` case (stem == "mod"): sub-modules stay siblings, no subdir.
1328        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    // ----- issue 2: per-module pub vs pub(crate) vs no re-export decision -------
1344
1345    #[test]
1346    fn private_only_module_reexports_pub_crate_not_pub() {
1347        // No `pub` item: `pub use *` would warn "nothing public enough", so the
1348        // re-export must be `pub(crate) use`.
1349        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        // `api` is a normal part; the bare impl references no sibling, so it
1367        // clusters alone with nothing nameable -> declared, but no glob re-export
1368        // (re-exporting it would only warn "doesn't reexport anything").
1369        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        // exactly one glob re-export line (for `api`) though two modules declared
1375        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    // ----- issue 3: crate-public entry fn is preserved as `pub use` -------------
1395
1396    #[test]
1397    fn bin_public_entry_fn_is_reexported_pub_so_it_stays_crate_visible() {
1398        // A lib crate root where `run` is the public entry (the bin calls
1399        // `crate_name::run`). It must be re-exported `pub`, not `pub(crate)`, or
1400        // `crate_name::run` is no longer visible to main.rs after the split.
1401        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    // ----- preamble (inner `//!`/`#![…]`) stays at the root, not copied to parts -
1418
1419    #[test]
1420    fn crate_preamble_stays_at_root_not_copied_into_parts() {
1421        // `#![allow]` / `//!` belong at the crate root (and `#![feature]` is a
1422        // hard error anywhere else); a crate-root attr already covers sub-modules.
1423        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    // ----- issue 3a: copy only the imports each file actually references -------
1449
1450    #[test]
1451    fn parts_copy_only_the_imports_they_reference() {
1452        // alpha names `Display`; bravo names nothing from the header. Each part
1453        // gets only the imports it references.
1454        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        // `use ...::*` and `use Trait as _` expose no name to match, so every
1489        // part keeps them — the safe choice for a trait used only via methods.
1490        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        // A module root whose items all moved out is just decls + re-exports; it
1515        // needs no imports, while the part that uses one keeps it.
1516        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    // ----- razel regression 1: source re-exports must stay `pub use` at root --
1534
1535    #[test]
1536    fn source_pub_use_reexports_stay_pub_use_at_the_root() {
1537        // The original file's `pub use` lines ARE the crate's public API.
1538        // Treating them as droppable header imports (or demoting them to
1539        // `pub(crate) use`) silently breaks every dependent's paths.
1540        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    // ----- razel regression 2: the cfg(test) gate travels with the tests mod --
1578
1579    #[test]
1580    fn under_budget_cfg_test_mod_keeps_its_braced_gate() {
1581        // The razel failure: the tests mod fit under the item budget, so it was
1582        // CLUSTERED — wrapped inside a part module with an ungated root decl —
1583        // instead of preserved. A conditional module must keep its braced
1584        // scope and its gate in the original module.
1585        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        // Budget far above the tests mod's size: it would have been clustered.
1590        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        // `fn tests` (value namespace) and `mod tests` (type namespace) can
1627        // coexist in the source; the extracted mod owns `tests.rs`, so the
1628        // part named after the fn must yield.
1629        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        // The extracted tests mod reaches the root's imports via
1645        // `use super::*;` — dropping HashMap from the root would break it.
1646        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        // `main` names `exit` but not `HashMap` (the moved `worker` does), so the
1665        // root keeps `exit` and drops `HashMap`.
1666        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    // ----- issue 4: a part sits one module level below the file it came from --
1684
1685    #[test]
1686    fn nested_module_part_imports_gain_one_super_level() {
1687        // A part of `foo.rs` is module `foo::<part>`, one level below `foo`, so a
1688        // copied `use super::X` would name `foo` instead of `foo`'s parent and a
1689        // copied `use self::X` would name the part itself.
1690        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            // `crate::` and external paths are absolute — untouched.
1723            "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        // The tool's own sibling glob is still there, exactly once, and is not
1733        // confused with the header's rewritten `use super::*;`.
1734        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        // The root is still the original module — its header must not move.
1745        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        // Below a crate root `self::X` names the part, not the crate root;
1761        // `super::` cannot occur at a crate root, so nothing else changes.
1762        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        // `tests/gNN.rs` is module `tests::gNN`, one below the extracted `tests`
1779        // body the group header was copied from.
1780        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        // An inline `mod tests` extracted out of `foo.rs` lands at `foo/tests.rs`
1814        // — still module `foo::tests`, the level it already had — so its own
1815        // `use super::helper;` still names `foo` and must NOT be re-anchored.
1816        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    // ----- issue 5: include! paths resolve against the containing file --------
1836
1837    #[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        // A crate root's parts are siblings of the root file, so no body moved
1885        // between directories and no include! path may change.
1886        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    // ----- issue 4b: relative paths *inside* a moved body move with it --------
1905
1906    #[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        // The header is not the only place a relative path hides: expression and
2023        // type paths, and a function-local `use`, travel inside the item.
2024        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            // Only the leading segment moves: two levels become three, not four.
2045            "super::super::super::grandparent::thing()",
2046            "super::local::helper()",
2047            // A lone `:` — a type annotation, a struct field init — is not a
2048            // path separator, so the segment after it still starts a path.
2049            "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        // `self` is only a module anchor when a `::` follows it: a receiver, a
2064        // `use …::{self, …}` group member and a `pub(super)` restriction are all
2065        // something else. (Re-anchoring a `pub(super)` restriction — which does
2066        // narrow when the item moves — is out of scope here.)
2067        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        // Re-basing belongs to `split`; `explode` stays byte-exact.
2134        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}