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::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 so that **every output file is
37/// `< max_loc`**. The item budget reserves the full header LOC and the per-part
38/// overhead; since a part now copies only the imports it references (a subset of
39/// the header), this reservation is conservative — a packed part stays safely
40/// under budget.
41pub fn split_bin(exploded: &Exploded, max_loc: usize, root_stem: &str) -> SplitOutput {
42    let header_loc: usize = exploded
43        .manifest
44        .rows
45        .iter()
46        .filter(|r| matches!(r.kind.as_str(), "use" | "preamble" | "extern_crate"))
47        .map(|r| r.loc)
48        .sum();
49    let item_budget = max_loc.saturating_sub(header_loc + PART_OVERHEAD).max(1);
50    let plan = plan_split(exploded, item_budget);
51    reassemble_bin(exploded, &plan, root_stem, max_loc)
52}
53
54/// A file the split would write.
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct OutputFile {
57    /// Path relative to the source file's directory (e.g. `main.rs`, `p00.rs`).
58    pub path: String,
59    pub contents: String,
60    pub loc: usize,
61}
62
63/// The reassembled output plus the parts of the plan that could not be met.
64#[derive(Debug, Clone)]
65pub struct SplitOutput {
66    pub files: Vec<OutputFile>,
67    /// Names of items still at or above budget (e.g. an un-nested test mod).
68    pub still_oversized: Vec<String>,
69}
70
71impl SplitOutput {
72    /// The largest output file's LOC (the budget is met iff this is `< max_loc`
73    /// and `still_oversized` is empty).
74    pub fn max_loc(&self) -> usize {
75        self.files.iter().map(|f| f.loc).max().unwrap_or(0)
76    }
77}
78
79fn loc(text: &str) -> usize {
80    if text.is_empty() {
81        0
82    } else {
83        text.lines().count().max(1)
84    }
85}
86
87/// How the split file sits in the module tree — fixes the sibling path prefix
88/// and where sub-module files go. Re-export visibility is per part in both
89/// topologies: `pub use` when the part has a public item (preserving the
90/// crate's public API), else `pub(crate) use`.
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum Topology {
93    /// A binary or library crate root (`main.rs`, `lib.rs`): siblings reached
94    /// via `crate::`, `fn main` (if any) kept at the root.
95    Bin,
96    /// A nested library module (`foo/mod.rs`, or a plain `foo.rs` file module):
97    /// sub-modules reached via `super::` and re-exported with a single per-module
98    /// glob — `pub use` to preserve a public API, else `pub(crate) use`. A plain
99    /// `foo.rs` file module resolves `mod bar;` to `foo/bar.rs`, so its
100    /// sub-modules are written into a `foo/` subdir.
101    Mod,
102}
103
104/// Plan and reassemble a library module so every output file is `< max_loc`;
105/// sub-modules see each other via `super::*`. Handles both a directory-owning
106/// `foo/mod.rs` (sub-modules become siblings) and a plain `foo.rs` file module
107/// (sub-modules go in a `foo/` subdir). `root_stem` is the source file's stem.
108pub fn split_mod(exploded: &Exploded, max_loc: usize, root_stem: &str) -> SplitOutput {
109    let header_loc: usize = exploded
110        .manifest
111        .rows
112        .iter()
113        .filter(|r| matches!(r.kind.as_str(), "use" | "preamble" | "extern_crate"))
114        .map(|r| r.loc)
115        .sum();
116    let item_budget = max_loc.saturating_sub(header_loc + PART_OVERHEAD).max(1);
117    let plan = plan_split(exploded, item_budget);
118    reassemble(exploded, &plan, root_stem, Topology::Mod, max_loc)
119}
120
121/// Reassemble a binary crate-root split (back-compat wrapper).
122pub fn reassemble_bin(
123    exploded: &Exploded,
124    plan: &SplitPlan,
125    root_stem: &str,
126    max_loc: usize,
127) -> SplitOutput {
128    reassemble(exploded, plan, root_stem, Topology::Bin, max_loc)
129}
130
131/// Reassemble a split for the given module topology. `root_stem` is the source
132/// file's stem (`"main"` for `main.rs`, `"mod"` for `foo/mod.rs`). `max_loc` is
133/// the ceiling, needed to recursively split an oversized test module.
134fn reassemble(
135    exploded: &Exploded,
136    plan: &SplitPlan,
137    root_stem: &str,
138    topology: Topology,
139    max_loc: usize,
140) -> SplitOutput {
141    let sibling = match topology {
142        Topology::Bin => "crate",
143        Topology::Mod => "super",
144    };
145    // A part is a module *below* the file it came from, whichever topology, so
146    // the header imports it copies have to be re-anchored one level up.
147    let part_rebase = match topology {
148        Topology::Bin => Rebase::CrateRoot,
149        Topology::Mod => Rebase::Nested,
150    };
151    let chunk_text = |i: usize| exploded.chunks[i].text.as_str();
152    let row = |i: usize| {
153        exploded
154            .manifest
155            .rows
156            .iter()
157            .find(|r| r.chunk_index == i)
158            .unwrap()
159    };
160
161    // Where sub-module files go. A directory-owning module file
162    // (`main.rs`/`lib.rs`/`foo/mod.rs`) keeps them as siblings; a plain `foo.rs`
163    // file module resolves `mod bar;` to `foo/bar.rs`, so its parts go in `foo/`.
164    let sub_prefix = match topology {
165        Topology::Bin => String::new(),
166        Topology::Mod if matches!(root_stem, "mod" | "lib" | "main") => String::new(),
167        Topology::Mod => format!("{root_stem}/"),
168    };
169
170    // The crate/module preamble (inner `//!` docs / `#![...]` attrs) stays at
171    // the root only — a crate-root `#![feature]` is a hard error in a sub-module
172    // file, and a crate-root `#![allow]` already covers descendants. The import
173    // chunks (`use`/`extern crate`) are copied into each file, but only the ones
174    // that file references by name (see `select_imports`). Re-exports
175    // (`pub use ...`) are not imports — the planner routes them to
176    // `plan.root_items`, kept at the root verbatim.
177    let preamble: String = plan
178        .header
179        .iter()
180        .filter(|&&i| row(i).kind == "preamble")
181        .map(|&i| chunk_text(i))
182        .collect();
183    let import_chunks: Vec<usize> = plan
184        .header
185        .iter()
186        .copied()
187        .filter(|&i| row(i).kind != "preamble")
188        .collect();
189
190    // Source chunks that stay at the root verbatim: `pub use` re-exports (the
191    // crate's public surface) and content-less `mod name;` declarations (which
192    // bind files relative to the root's directory).
193    let root_kept: String = plan.root_items.iter().map(|&i| chunk_text(i)).collect();
194
195    // Module names fixed by the source — extracted body-mods and `mod name;`
196    // declarations keep their own names — plus the root's stem; a generated
197    // part that would collide is renamed with a `_` suffix.
198    let mut reserved: std::collections::BTreeSet<&str> =
199        plan.mods.iter().map(|&i| row(i).name.as_str()).collect();
200    reserved.insert(root_stem);
201    for &i in &plan.root_items {
202        if row(i).kind == "mod" {
203            reserved.insert(row(i).name.as_str());
204        }
205    }
206
207    let mut files = Vec::new();
208    let mut mod_decls = String::new();
209    let mut reexports = String::new();
210    let mut still_oversized = Vec::new();
211    // Names referenced from the root file itself (`fn main`, extracted module
212    // bodies whose `use super::*` reaches the root's imports).
213    let mut root_refs = Refs::default();
214
215    // Part modules, named after their cluster's dominant item.
216    for part in &plan.parts {
217        let module = if reserved.contains(part.name.as_str()) {
218            format!("{}_", part.name)
219        } else {
220            part.name.clone()
221        };
222        let mut body = String::new();
223        let mut root_main = None;
224        // Widest visibility any moved item contributes to a glob re-export.
225        let mut export_vis = ExportVis::None;
226        // Names this part references, so only the imports it uses are copied in.
227        let mut refs = Refs::default();
228        for &i in &part.chunk_indices {
229            let r = row(i);
230            // `fn main` must stay at the crate root, not move into a submodule.
231            if r.kind == "fn" && r.name == "main" {
232                root_main = Some(i);
233                continue;
234            }
235            export_vis = export_vis.max(item_export_vis(chunk_text(i)));
236            refs.add(chunk_text(i));
237            body.push_str(&rebase_body_paths(
238                &bump_visibility(chunk_text(i)),
239                part_rebase,
240            ));
241        }
242        // If the part was only `fn main`, it produced no module file.
243        if body.trim().is_empty() {
244            if let Some(i) = root_main {
245                // stash main to emit at root via a sentinel part with no module
246                files.push(OutputFile {
247                    path: String::from("__root_main__"),
248                    contents: chunk_text(i).to_owned(),
249                    loc: 0,
250                });
251            }
252            continue;
253        }
254        if let Some(i) = root_main {
255            files.push(OutputFile {
256                path: String::from("__root_main__"),
257                contents: chunk_text(i).to_owned(),
258                loc: 0,
259            });
260        }
261        let part_imports = select_imports(&import_chunks, &exploded.chunks, &refs, part_rebase);
262        let contents = part_file(&part_imports, sibling, &body);
263        mod_decls.push_str(&format!("mod {module};\n"));
264        // One glob re-export per module, visibility chosen by the module's widest
265        // item: `pub use` when it has a public item (a `pub use *` caps each item
266        // at its own visibility, so it covers internal `pub(crate)` items in the
267        // same line); else `pub(crate) use`; and nothing when the module exposes
268        // no nameable item (e.g. a bare `impl` — a glob re-export would only warn
269        // "doesn't reexport anything"). This keeps the crate's public surface
270        // (`pub use`) reachable and avoids the redundant double re-export.
271        if let Some(line) = reexport_line(export_vis, &module) {
272            reexports.push_str(&line);
273        }
274        let l = loc(&contents);
275        files.push(OutputFile {
276            path: format!("{sub_prefix}{module}.rs"),
277            contents,
278            loc: l,
279        });
280    }
281
282    // Body-carrying mods are extracted whole, whatever their size: packing a
283    // `#[cfg(test)] mod tests { ... }` into a cluster would nest it inside a
284    // part module and leave the root declaration ungated. The declaration —
285    // leading comments, attributes, visibility — travels to the root; the
286    // unwrapped body becomes the module's file, split further into a `{name}/`
287    // subdir when it is itself over the ceiling.
288    for &mod_index in &plan.mods {
289        let chunk = chunk_text(mod_index);
290        let name = row(mod_index).name.as_str();
291        let Some((decl, inner)) = extract_mod(chunk) else {
292            // Should not happen (the planner classified it by parsing); keep
293            // the chunk intact at the root rather than lose it.
294            mod_decls.push_str(chunk);
295            continue;
296        };
297        mod_decls.push_str(&decl);
298        // The body's `use super::*;` reaches the root's imports, so the root
299        // must keep the imports the body names.
300        root_refs.add_source(&inner);
301        let inner_loc = loc(&inner);
302        if inner_loc < max_loc {
303            files.push(OutputFile {
304                path: format!("{sub_prefix}{name}.rs"),
305                contents: inner,
306                loc: inner_loc,
307            });
308        } else {
309            let (nested, nested_still) = split_nested_mod(name, &inner, max_loc);
310            files.extend(nested.into_iter().map(|mut f| {
311                f.path = format!("{sub_prefix}{}", f.path);
312                f
313            }));
314            still_oversized.extend(nested_still);
315        }
316    }
317
318    // Oversized leaves can't be moved mechanically.
319    for over in &plan.oversized {
320        still_oversized.push(format!(
321            "{} ({} LOC {}, manual extraction)",
322            over.name, over.loc, over.kind
323        ));
324    }
325
326    // Pull the stashed `fn main` (if any) and build the root file.
327    let mut root_main = String::new();
328    files.retain(|f| {
329        if f.path == "__root_main__" {
330            root_main = f.contents.clone();
331            false
332        } else {
333            true
334        }
335    });
336
337    // The root keeps the preamble, then only the imports its own retained code
338    // references — `fn main` (if any) plus extracted module bodies, whose
339    // `use super::*;` resolves against the root. A root that is just module
340    // declarations and re-exports needs no imports at all.
341    if !root_main.trim().is_empty() {
342        root_refs.add(&root_main);
343    }
344    let root_imports = if root_refs.is_empty() {
345        String::new()
346    } else {
347        // The root file *is* the original module: its own imports still mean
348        // what they meant.
349        select_imports(&import_chunks, &exploded.chunks, &root_refs, Rebase::Keep)
350    };
351
352    let mut root = String::new();
353    root.push_str(&preamble);
354    root.push_str(&root_imports);
355    root.push_str(&root_kept);
356    root.push('\n');
357    root.push_str(&mod_decls);
358    root.push('\n');
359    root.push_str(&reexports);
360    if !root_main.is_empty() {
361        root.push('\n');
362        root.push_str(&root_main);
363    }
364    let root_loc = loc(&root);
365    files.insert(
366        0,
367        OutputFile {
368            path: format!("{root_stem}.rs"),
369            contents: root,
370            loc: root_loc,
371        },
372    );
373
374    // `include!`/`include_str!`/`include_bytes!` resolve their argument against
375    // the directory of the file that contains them, so a body written into a
376    // subdirectory needs one `../` per level it moved down. The root file did
377    // not move (no `/` in its path), and neither did a part that stayed a
378    // sibling. Inserting `../` adds no lines, so `loc` is unaffected.
379    for file in &mut files {
380        let levels = file.path.matches('/').count();
381        if levels == 0 || !file.contents.contains("include") {
382            continue;
383        }
384        file.contents = rebase_includes(&file.contents, levels, &file.path);
385    }
386
387    SplitOutput {
388        files,
389        still_oversized,
390    }
391}
392
393/// Macros whose single argument is a path resolved relative to the *file* that
394/// contains them — the one construct a byte-exact move cannot leave alone.
395const PATH_MACROS: [&str; 3] = ["include", "include_str", "include_bytes"];
396
397/// Re-base the relative path arguments of the `include*!` macros in a file
398/// written `levels` directories below the source file. Token-driven, so a path
399/// spelled inside a comment or an unrelated string is never touched. An
400/// absolute path is left alone; so is a non-literal argument (`concat!`,
401/// `env!`, a macro, a constant), which gets one warning line on stderr naming
402/// the generated file and line so the user knows where to look.
403fn rebase_includes(contents: &str, levels: usize, path: &str) -> String {
404    let Ok(stream) = contents.parse::<proc_macro2::TokenStream>() else {
405        return contents.to_owned();
406    };
407    let mut edits = Vec::new();
408    let prefix = "../".repeat(levels);
409    collect_include_edits(stream, &prefix, contents, path, &mut edits);
410    apply_replacements(contents, &edits)
411}
412
413/// Walk a token stream for `include*!(...)` calls, recording where a `../`
414/// prefix must be inserted and warning about the arguments that cannot be
415/// re-based mechanically.
416fn collect_include_edits<'a>(
417    stream: proc_macro2::TokenStream,
418    prefix: &'a str,
419    contents: &str,
420    path: &str,
421    edits: &mut Vec<(std::ops::Range<usize>, &'a str)>,
422) {
423    let trees: Vec<proc_macro2::TokenTree> = stream.into_iter().collect();
424    for (index, tree) in trees.iter().enumerate() {
425        let proc_macro2::TokenTree::Group(group) = tree else {
426            continue;
427        };
428        if !is_path_macro_call(&trees, index) {
429            collect_include_edits(group.stream(), prefix, contents, path, edits);
430            continue;
431        }
432        match literal_insert_point(&group.stream()) {
433            Some(Some(offset)) => edits.push((offset..offset, prefix)),
434            // A literal that must not move (absolute path): nothing to do.
435            Some(None) => {}
436            None => {
437                let line = contents[..group.span().byte_range().start].lines().count();
438                eprintln!(
439                    "rust-split: {path}:{line}: {}! argument is not a plain string \
440                     literal — its path was not re-based",
441                    trees[index - 2]
442                );
443            }
444        }
445    }
446}
447
448/// Whether the group at `index` is the argument list of an `include*!` call,
449/// i.e. the two preceding tokens are the macro name and its `!`.
450fn is_path_macro_call(trees: &[proc_macro2::TokenTree], index: usize) -> bool {
451    if index < 2 {
452        return false;
453    }
454    let proc_macro2::TokenTree::Ident(name) = &trees[index - 2] else {
455        return false;
456    };
457    let proc_macro2::TokenTree::Punct(bang) = &trees[index - 1] else {
458        return false;
459    };
460    bang.as_char() == '!' && PATH_MACROS.iter().any(|m| name == m)
461}
462
463/// For an `include*!` argument list: `Some(Some(offset))` is the byte offset
464/// just inside the opening quote of a re-basable relative path literal,
465/// `Some(None)` a literal that must stay put (an absolute path), and `None` an
466/// argument that is not a single plain string literal with an optional comma.
467fn literal_insert_point(stream: &proc_macro2::TokenStream) -> Option<Option<usize>> {
468    let mut trees = stream.clone().into_iter();
469    let proc_macro2::TokenTree::Literal(literal) = trees.next()? else {
470        return None;
471    };
472    if let Some(token) = trees.next()
473        && (!matches!(token, proc_macro2::TokenTree::Punct(p) if p.as_char() == ',')
474            || trees.next().is_some())
475    {
476        return None;
477    }
478    let text = literal.to_string();
479    let value = syn::parse_str::<syn::LitStr>(&text).ok()?.value();
480    // An absolute path does not move with the file; an empty one is not a path.
481    let windows_drive =
482        matches!(value.as_bytes(), [drive, b':', b'/' | b'\\', ..] if drive.is_ascii_alphabetic());
483    if value.is_empty() || value.starts_with('/') || value.starts_with(r"\\") || windows_drive {
484        return Some(None);
485    }
486    // Insert just inside the opening quote — `"…"`, `r"…"` and `r#"…"#` alike —
487    // so the literal's own escaping is preserved byte-for-byte.
488    let quote = text.find('"')?;
489    Some(Some(literal.span().byte_range().start + quote + 1))
490}
491
492/// Raise a moved item — and the struct fields / impl members referenced across
493/// the new module boundaries — to `pub(crate)`, so the crate-root re-exports can
494/// see them. Span-driven (parse the chunk, insert at exact offsets), so it is
495/// attribute-safe and handles `fn` modifiers, multi-line attrs, etc. Items
496/// already `pub`/`pub(crate)` are left alone. Over-exposes within the crate,
497/// which is the mechanical, behavior-preserving choice.
498fn bump_visibility(chunk_text: &str) -> String {
499    let Ok(item) = syn::parse_str::<syn::Item>(chunk_text) else {
500        return chunk_text.to_owned();
501    };
502    let mut offsets = Vec::new();
503    collect_vis_offsets(&item, &mut offsets);
504    apply_inserts(chunk_text, &offsets)
505}
506
507fn is_inherited(vis: &syn::Visibility) -> bool {
508    matches!(vis, syn::Visibility::Inherited)
509}
510
511fn start<T: Spanned>(node: &T) -> usize {
512    node.span().byte_range().start
513}
514
515/// Byte offset where a function's visibility belongs: before any
516/// `const`/`async`/`unsafe`/`extern` modifier, else before `fn`.
517fn fn_sig_start(sig: &syn::Signature) -> usize {
518    let mut min = start(&sig.fn_token);
519    if let Some(t) = &sig.constness {
520        min = min.min(start(t));
521    }
522    if let Some(t) = &sig.asyncness {
523        min = min.min(start(t));
524    }
525    if let Some(t) = &sig.unsafety {
526        min = min.min(start(t));
527    }
528    if let Some(abi) = &sig.abi {
529        min = min.min(start(&abi.extern_token));
530    }
531    min
532}
533
534fn field_offsets(fields: &syn::Fields, offsets: &mut Vec<usize>) {
535    for field in fields {
536        if is_inherited(&field.vis) {
537            match &field.ident {
538                Some(ident) => offsets.push(start(ident)),
539                None => offsets.push(start(&field.ty)),
540            }
541        }
542    }
543}
544
545fn collect_vis_offsets(item: &syn::Item, offsets: &mut Vec<usize>) {
546    use syn::Item;
547    match item {
548        Item::Fn(f) => {
549            if is_inherited(&f.vis) {
550                offsets.push(fn_sig_start(&f.sig));
551            }
552        }
553        Item::Struct(s) => {
554            if is_inherited(&s.vis) {
555                offsets.push(start(&s.struct_token));
556            }
557            field_offsets(&s.fields, offsets);
558        }
559        Item::Enum(e) => {
560            if is_inherited(&e.vis) {
561                offsets.push(start(&e.enum_token));
562            }
563        }
564        Item::Union(u) => {
565            if is_inherited(&u.vis) {
566                offsets.push(start(&u.union_token));
567            }
568            field_offsets(&syn::Fields::Named(u.fields.clone()), offsets);
569        }
570        Item::Const(c) if is_inherited(&c.vis) => offsets.push(start(&c.const_token)),
571        Item::Static(s) if is_inherited(&s.vis) => offsets.push(start(&s.static_token)),
572        Item::Type(t) if is_inherited(&t.vis) => offsets.push(start(&t.type_token)),
573        Item::Trait(t) if is_inherited(&t.vis) => offsets.push(start(&t.trait_token)),
574        Item::TraitAlias(t) if is_inherited(&t.vis) => offsets.push(start(&t.trait_token)),
575        // Only inherent impls (`impl Type`) allow member visibility; trait-impl
576        // members (`impl Trait for Type`) inherit the trait's and must not.
577        Item::Impl(i) if i.trait_.is_none() => {
578            for member in &i.items {
579                match member {
580                    syn::ImplItem::Fn(f) if is_inherited(&f.vis) => {
581                        offsets.push(fn_sig_start(&f.sig))
582                    }
583                    syn::ImplItem::Const(c) if is_inherited(&c.vis) => {
584                        offsets.push(start(&c.const_token))
585                    }
586                    syn::ImplItem::Type(t) if is_inherited(&t.vis) => {
587                        offsets.push(start(&t.type_token))
588                    }
589                    _ => {}
590                }
591            }
592        }
593        _ => {}
594    }
595}
596
597fn apply_inserts(text: &str, offsets: &[usize]) -> String {
598    let mut offsets: Vec<usize> = offsets.to_vec();
599    offsets.sort_unstable();
600    offsets.dedup();
601    let mut out = String::with_capacity(text.len() + offsets.len() * 11);
602    let mut prev = 0;
603    for &offset in &offsets {
604        out.push_str(&text[prev..offset]);
605        out.push_str("pub(crate) ");
606        prev = offset;
607    }
608    out.push_str(&text[prev..]);
609    out
610}
611
612/// Assemble a part file: its selected imports, the sibling glob so it can reach
613/// the rest of the module via the root re-exports, then the moved items.
614fn part_file(imports: &str, sibling: &str, body: &str) -> String {
615    if imports.trim().is_empty() {
616        format!("use {sibling}::*;\n\n{body}")
617    } else {
618        format!("{imports}\nuse {sibling}::*;\n\n{body}")
619    }
620}
621
622/// The glob re-export line for a module given the widest visibility of its
623/// items — or `None` when it exposes nothing nameable (re-exporting an
624/// `impl`-only module would only warn).
625fn reexport_line(vis: ExportVis, module: &str) -> Option<String> {
626    match vis {
627        ExportVis::None => None,
628        ExportVis::Crate => Some(format!("pub(crate) use {module}::*;\n")),
629        ExportVis::Pub => Some(format!("pub use {module}::*;\n")),
630    }
631}
632
633/// What a moved item contributes to its module's glob re-export. Ordered so the
634/// widest contribution across a module's items picks the re-export visibility.
635#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
636enum ExportVis {
637    /// No nameable export (a bare `impl`, a macro invocation, parse failure).
638    None,
639    /// A crate-visible name: a private item (bumped to `pub(crate)`), or one
640    /// already `pub(crate)`/`pub(super)`/`pub(in …)`.
641    Crate,
642    /// A fully public name — part of the crate/module's public surface.
643    Pub,
644}
645
646/// Classify one chunk's contribution to its module's glob re-export, mirroring
647/// `bump_visibility`: items it raises to `pub(crate)` count as crate-visible;
648/// `mod`/`use` are not raised, so a private one is invisible to the re-export.
649fn item_export_vis(chunk_text: &str) -> ExportVis {
650    use syn::{Item, Visibility};
651    let Ok(item) = syn::parse_str::<syn::Item>(chunk_text) else {
652        // Unparseable: bump_visibility also left it private, so a glob re-export
653        // could not see it either — contribute nothing.
654        return ExportVis::None;
655    };
656    let (vis, bumped) = match &item {
657        Item::Fn(f) => (&f.vis, true),
658        Item::Struct(s) => (&s.vis, true),
659        Item::Enum(e) => (&e.vis, true),
660        Item::Const(c) => (&c.vis, true),
661        Item::Static(s) => (&s.vis, true),
662        Item::Trait(t) => (&t.vis, true),
663        Item::TraitAlias(t) => (&t.vis, true),
664        Item::Type(t) => (&t.vis, true),
665        Item::Union(u) => (&u.vis, true),
666        Item::Mod(m) => (&m.vis, false),
667        Item::Use(u) => (&u.vis, false),
668        // impl / macro / foreign_mod / extern_crate / verbatim: nothing nameable.
669        _ => return ExportVis::None,
670    };
671    match vis {
672        Visibility::Public(_) => ExportVis::Pub,
673        Visibility::Restricted(_) => ExportVis::Crate,
674        Visibility::Inherited if bumped => ExportVis::Crate,
675        Visibility::Inherited => ExportVis::None,
676    }
677}
678
679/// The identifiers a set of moved items references — the basis for copying only
680/// the imports they use. An item that does not re-parse flips `keep_all`, so the
681/// caller falls back to copying every import rather than dropping a needed one.
682#[derive(Default)]
683struct Refs {
684    names: std::collections::BTreeSet<String>,
685    keep_all: bool,
686}
687
688impl Refs {
689    fn add(&mut self, chunk_text: &str) {
690        match crate::referenced_idents(chunk_text) {
691            Some(names) => self.names.extend(names),
692            None => self.keep_all = true,
693        }
694    }
695
696    /// Add every identifier a multi-item source fragment (an extracted module
697    /// body) references.
698    fn add_source(&mut self, src: &str) {
699        match crate::referenced_idents_in_source(src) {
700            Some(names) => self.names.extend(names),
701            None => self.keep_all = true,
702        }
703    }
704
705    /// True when nothing was added — no import can be needed.
706    fn is_empty(&self) -> bool {
707        self.names.is_empty() && !self.keep_all
708    }
709
710    /// Whether an import providing `names` should be copied in. A name-less
711    /// import (`None`: a `*` glob or `as _`) is always kept, since its usage
712    /// can't be detected by name.
713    fn keeps(&self, provided: &Option<std::collections::BTreeSet<String>>) -> bool {
714        match provided {
715            None => true,
716            Some(names) => self.keep_all || names.iter().any(|n| self.names.contains(n)),
717        }
718    }
719}
720
721/// How a copied `use` path must be re-anchored, given where the copy lands
722/// relative to the module the import was written in.
723#[derive(Debug, Clone, Copy, PartialEq, Eq)]
724enum Rebase {
725    /// Same module — the paths already mean what they meant (the root file, and
726    /// any body extracted to the module level it already had).
727    Keep,
728    /// One module level below a nested module: `super::` needs one more
729    /// `super::`, and `self::` (the old module) becomes `super::`.
730    Nested,
731    /// One module level below a crate root: `self::` becomes `crate::`. A
732    /// crate root has no `super::`, so nothing else can move.
733    CrateRoot,
734}
735
736/// Re-anchor a copied `use` chunk for a file one module level below the one it
737/// was written in. Only the leading segment moves: `crate::`, `::`-rooted and
738/// external paths are untouched, and a re-export (`pub use …`) is never
739/// rewritten. Span-driven on the re-parsed chunk, so leading comments,
740/// attributes and formatting survive byte-exactly.
741fn rebase_import(chunk_text: &str, rebase: Rebase) -> String {
742    if matches!(rebase, Rebase::Keep) {
743        return chunk_text.to_owned();
744    }
745    let Ok(syn::Item::Use(item)) = syn::parse_str::<syn::Item>(chunk_text) else {
746        return chunk_text.to_owned();
747    };
748    // A re-export is the module's public surface and stays at the root verbatim;
749    // a `::`-rooted path is already absolute.
750    if !is_inherited(&item.vis) || item.leading_colon.is_some() {
751        return chunk_text.to_owned();
752    }
753    let mut edits = Vec::new();
754    collect_rebase_edits(&item.tree, rebase, &mut edits);
755    apply_replacements(chunk_text, &edits)
756}
757
758/// Collect the byte range and replacement text for each leading `super`/`self`
759/// segment of a `use` tree. A top-level group (`use {super::a, self::b};`)
760/// anchors each of its items separately, so it recurses; a group *after* a path
761/// segment does not (`use super::{self, a};` is anchored by the `super`).
762fn collect_rebase_edits(
763    tree: &syn::UseTree,
764    rebase: Rebase,
765    edits: &mut Vec<(std::ops::Range<usize>, &'static str)>,
766) {
767    match tree {
768        syn::UseTree::Path(path) => collect_anchor_edit(&path.ident, rebase, edits),
769        syn::UseTree::Rename(rename) => collect_anchor_edit(&rename.ident, rebase, edits),
770        syn::UseTree::Group(group) => {
771            for item in &group.items {
772                collect_rebase_edits(item, rebase, edits);
773            }
774        }
775        _ => {}
776    }
777}
778
779fn collect_anchor_edit(
780    ident: &syn::Ident,
781    rebase: Rebase,
782    edits: &mut Vec<(std::ops::Range<usize>, &'static str)>,
783) {
784    let replacement = match (ident.to_string().as_str(), rebase) {
785        ("super", Rebase::Nested) => "super::super",
786        ("self", Rebase::Nested) => "super",
787        ("self", Rebase::CrateRoot) => "crate",
788        _ => return,
789    };
790    edits.push((ident.span().byte_range(), replacement));
791}
792
793/// Replace the given (non-overlapping, possibly empty) byte ranges in `text`.
794/// An empty range is an insertion point.
795fn apply_replacements(text: &str, edits: &[(std::ops::Range<usize>, &str)]) -> String {
796    let mut edits: Vec<(std::ops::Range<usize>, &str)> = edits.to_vec();
797    edits.sort_by_key(|(range, _)| range.start);
798    let mut out = String::with_capacity(text.len() + edits.len() * 8);
799    let mut prev = 0;
800    for (range, replacement) in &edits {
801        if range.start < prev {
802            continue;
803        }
804        out.push_str(&text[prev..range.start]);
805        out.push_str(replacement);
806        prev = range.end;
807    }
808    out.push_str(&text[prev..]);
809    out
810}
811
812/// Re-anchor the `super::` / `self::` paths *inside* a moved item — expression
813/// and type paths, function-local `use` items, and macro invocation paths — for
814/// a body that lands one module level below its original file. Attribute and
815/// arbitrary macro input tokens are opaque; arguments to common standard
816/// expression macros are parsed before their paths are rewritten. Comments,
817/// strings, receivers, and visibility restrictions stay byte-exact.
818fn rebase_body_paths(body: &str, rebase: Rebase) -> String {
819    if matches!(rebase, Rebase::Keep) {
820        return body.to_owned();
821    }
822    let Ok(file) = syn::parse_file(body) else {
823        return body.to_owned();
824    };
825    let mut visitor = BodyPathEdits {
826        rebase,
827        edits: Vec::new(),
828    };
829    syn::visit::Visit::visit_file(&mut visitor, &file);
830    apply_replacements(body, &visitor.edits)
831}
832
833struct BodyPathEdits {
834    rebase: Rebase,
835    edits: Vec<(std::ops::Range<usize>, &'static str)>,
836}
837
838impl<'ast> syn::visit::Visit<'ast> for BodyPathEdits {
839    fn visit_path(&mut self, path: &'ast syn::Path) {
840        if path.leading_colon.is_none() && path.segments.len() > 1 {
841            collect_anchor_edit(&path.segments[0].ident, self.rebase, &mut self.edits);
842        }
843        syn::visit::visit_path(self, path);
844    }
845
846    fn visit_item_use(&mut self, item: &'ast syn::ItemUse) {
847        if item.leading_colon.is_none() {
848            collect_rebase_edits(&item.tree, self.rebase, &mut self.edits);
849        }
850    }
851
852    fn visit_macro(&mut self, mac: &'ast syn::Macro) {
853        syn::visit::Visit::visit_path(self, &mac.path);
854        let Some(name) = mac.path.segments.last().map(|segment| &segment.ident) else {
855            return;
856        };
857        let is_standard_path = mac.path.segments.len() == 1
858            || (mac.path.segments.len() == 2
859                && mac
860                    .path
861                    .segments
862                    .first()
863                    .is_some_and(|segment| segment.ident == "std" || segment.ident == "core"));
864        if !is_standard_path
865            || !matches!(
866                name.to_string().as_str(),
867                "assert"
868                    | "assert_eq"
869                    | "assert_ne"
870                    | "debug_assert"
871                    | "debug_assert_eq"
872                    | "debug_assert_ne"
873                    | "format"
874                    | "format_args"
875                    | "format_args_nl"
876                    | "panic"
877                    | "print"
878                    | "println"
879                    | "eprint"
880                    | "eprintln"
881                    | "write"
882                    | "writeln"
883            )
884        {
885            return;
886        }
887        // These standard macros accept comma-separated Rust expressions.
888        // Unknown macro grammars remain opaque even when their tokens look Rust-like.
889        let parser = syn::punctuated::Punctuated::<syn::Expr, syn::Token![,]>::parse_terminated;
890        let Ok(arguments) = parser.parse2(mac.tokens.clone()) else {
891            return;
892        };
893        for argument in &arguments {
894            syn::visit::Visit::visit_expr(self, argument);
895        }
896    }
897
898    fn visit_attribute(&mut self, _: &'ast syn::Attribute) {}
899
900    fn visit_visibility(&mut self, _: &'ast syn::Visibility) {}
901}
902
903/// Concatenate, in source order, the import chunks referenced by `refs` (plus
904/// name-less imports like `*` globs and `as _`, always kept), each re-anchored
905/// for where it lands. This is the aggressive policy: an import whose every name
906/// is unreferenced is dropped — including an extension trait used only via its
907/// methods, whose `use` the compiler will then ask to be re-added.
908fn select_imports(
909    import_chunks: &[usize],
910    chunks: &[crate::Chunk],
911    refs: &Refs,
912    rebase: Rebase,
913) -> String {
914    let mut out = String::new();
915    for &i in import_chunks {
916        let text = chunks[i].text.as_str();
917        if refs.keeps(&import_provided_names(text)) {
918            out.push_str(&rebase_import(text, rebase));
919        }
920    }
921    out
922}
923
924/// The names a `use`/`extern crate` chunk introduces, or `None` when it exposes
925/// nothing referenceable by name — a `*` glob or an `as _` import — which is
926/// then kept in every file rather than dropped.
927fn import_provided_names(chunk_text: &str) -> Option<std::collections::BTreeSet<String>> {
928    match syn::parse_str::<syn::Item>(chunk_text).ok()? {
929        syn::Item::Use(u) => {
930            let mut names = std::collections::BTreeSet::new();
931            let mut name_less = false;
932            collect_use_names(&u.tree, &mut names, &mut name_less);
933            if name_less { None } else { Some(names) }
934        }
935        syn::Item::ExternCrate(c) => {
936            let name = c
937                .rename
938                .map(|(_, id)| id.to_string())
939                .unwrap_or_else(|| c.ident.to_string());
940            Some(std::collections::BTreeSet::from([name]))
941        }
942        // An unparseable header chunk has no detectable name -> keep it.
943        _ => None,
944    }
945}
946
947/// Collect the bound names a `use` tree introduces. `name_less` is set for a
948/// `*` glob or an `as _` rename, which bind no referenceable name.
949fn collect_use_names(
950    tree: &syn::UseTree,
951    names: &mut std::collections::BTreeSet<String>,
952    name_less: &mut bool,
953) {
954    match tree {
955        syn::UseTree::Path(p) => collect_use_names(&p.tree, names, name_less),
956        syn::UseTree::Name(n) => {
957            names.insert(n.ident.to_string());
958        }
959        syn::UseTree::Rename(r) => {
960            if r.rename == "_" {
961                *name_less = true;
962            } else {
963                names.insert(r.rename.to_string());
964            }
965        }
966        syn::UseTree::Glob(_) => *name_less = true,
967        syn::UseTree::Group(g) => {
968            for item in &g.items {
969                collect_use_names(item, names, name_less);
970            }
971        }
972    }
973}
974
975/// Split a `mod NAME { ... }` chunk into the root-side declaration and the
976/// unwrapped inner body for the extracted file. The declaration is everything
977/// up to the mod's identifier — leading comments, attributes (a `#[cfg(test)]`
978/// gate must keep gating the declaration), and visibility (`pub mod` stays
979/// `pub mod`) — closed with `;`. Span-driven on the re-parsed chunk, so it is
980/// exact where the old line-based scan dropped the visibility. `None` when the
981/// chunk does not re-parse as a body-carrying mod.
982fn extract_mod(chunk_text: &str) -> Option<(String, String)> {
983    let Ok(syn::Item::Mod(item)) = syn::parse_str::<syn::Item>(chunk_text) else {
984        return None;
985    };
986    let (brace, _) = item.content.as_ref()?;
987    let ident_end = item.ident.span().byte_range().end;
988    let decl = format!("{};\n", &chunk_text[..ident_end]);
989
990    let open_end = brace.span.open().byte_range().end;
991    let close_start = brace.span.close().byte_range().start;
992    let inner = &chunk_text[open_end..close_start];
993    // Drop the newline that followed `{` and the closing brace's indentation.
994    let inner = inner.strip_prefix('\n').unwrap_or(inner);
995    let mut inner = inner.trim_end_matches([' ', '\t']).to_owned();
996    if !inner.is_empty() && !inner.ends_with('\n') {
997        inner.push('\n');
998    }
999    Some((decl, inner))
1000}
1001
1002/// Recursively split an over-budget module's inner body into a `{name}/`
1003/// subdirectory: `{name}/mod.rs` forwards the enclosing module's items
1004/// (`pub(crate) use super::*`), keeps the body's own re-exports and `mod`
1005/// declarations verbatim, and re-exports the clusters; each `{name}/gNN.rs`
1006/// holds a cohesive cluster and reaches everything via `super::*`. A nested
1007/// body-carrying mod is extracted whole (attributes and visibility on its
1008/// declaration), recursing when it is itself over the ceiling. Used for big
1009/// test modules.
1010fn split_nested_mod(name: &str, inner: &str, max_loc: usize) -> (Vec<OutputFile>, Vec<String>) {
1011    let Ok(exploded) = crate::explode(inner) else {
1012        return (
1013            vec![OutputFile {
1014                path: format!("{name}.rs"),
1015                contents: inner.to_owned(),
1016                loc: loc(inner),
1017            }],
1018            vec![format!("{name} (could not parse for nested split)")],
1019        );
1020    };
1021    let row = |i: usize| {
1022        exploded
1023            .manifest
1024            .rows
1025            .iter()
1026            .find(|r| r.chunk_index == i)
1027            .unwrap()
1028    };
1029
1030    // Budget from a conservative header estimate (every use/preamble/extern
1031    // chunk), then plan once. The copied group header is the plan's header —
1032    // the module's own plain imports — minus `use super::*` (the parent glob,
1033    // which `{name}/mod.rs` forwards instead). Re-exports and `mod`
1034    // declarations are in `root_items`, kept in `{name}/mod.rs`; a nested
1035    // `mod x;` still resolves to `{name}/x.rs` after the move.
1036    let header_estimate: usize = exploded
1037        .manifest
1038        .rows
1039        .iter()
1040        .filter(|r| matches!(r.kind.as_str(), "use" | "preamble" | "extern_crate"))
1041        .map(|r| r.loc)
1042        .sum();
1043    let item_budget = max_loc
1044        .saturating_sub(header_estimate + PART_OVERHEAD)
1045        .max(1);
1046    let plan = plan_split(&exploded, item_budget);
1047    let is_super_glob = |t: &str| t.replace(' ', "").contains("usesuper::*;");
1048    // `{name}/gNN.rs` is a module below `{name}`, so the body's own imports are
1049    // re-anchored the same way a part's are.
1050    let group_header: String = plan
1051        .header
1052        .iter()
1053        .map(|&i| exploded.chunks[i].text.as_str())
1054        .filter(|t| !is_super_glob(t))
1055        .map(|t| rebase_import(t, Rebase::Nested))
1056        .collect();
1057
1058    let mut files = Vec::new();
1059    let mut decls = String::new();
1060    let mut reexports = String::new();
1061    let mut still = Vec::new();
1062    for (idx, part) in plan.parts.iter().enumerate() {
1063        let module = format!("g{idx:02}");
1064        let mut body = String::new();
1065        for &ci in &part.chunk_indices {
1066            body.push_str(&rebase_body_paths(
1067                &bump_visibility(exploded.chunks[ci].text.as_str()),
1068                Rebase::Nested,
1069            ));
1070        }
1071        let contents = format!("{group_header}\nuse super::*;\n\n{body}");
1072        files.push(OutputFile {
1073            path: format!("{name}/{module}.rs"),
1074            loc: loc(&contents),
1075            contents,
1076        });
1077        decls.push_str(&format!("mod {module};\n"));
1078        reexports.push_str(&format!("pub(crate) use {module}::*;\n"));
1079    }
1080    for &mod_index in &plan.mods {
1081        let chunk = exploded.chunks[mod_index].text.as_str();
1082        let mod_name = row(mod_index).name.as_str();
1083        let Some((decl, mod_inner)) = extract_mod(chunk) else {
1084            decls.push_str(chunk);
1085            continue;
1086        };
1087        decls.push_str(&decl);
1088        let mod_loc = loc(&mod_inner);
1089        if mod_loc < max_loc {
1090            files.push(OutputFile {
1091                path: format!("{name}/{mod_name}.rs"),
1092                contents: mod_inner,
1093                loc: mod_loc,
1094            });
1095        } else {
1096            let (nested, nested_still) = split_nested_mod(mod_name, &mod_inner, max_loc);
1097            files.extend(nested.into_iter().map(|mut f| {
1098                f.path = format!("{name}/{}", f.path);
1099                f
1100            }));
1101            still.extend(
1102                nested_still
1103                    .into_iter()
1104                    .map(|message| format!("{name}::{message}")),
1105            );
1106        }
1107    }
1108    for over in &plan.oversized {
1109        still.push(format!(
1110            "{name}::{} ({} LOC {}, leaf needs manual extraction)",
1111            over.name, over.loc, over.kind
1112        ));
1113    }
1114
1115    let root_kept: String = plan
1116        .root_items
1117        .iter()
1118        .map(|&i| exploded.chunks[i].text.as_str())
1119        .collect();
1120    let modrs = format!("pub(crate) use super::*;\n\n{root_kept}{decls}\n{reexports}");
1121    files.insert(
1122        0,
1123        OutputFile {
1124            path: format!("{name}/mod.rs"),
1125            loc: loc(&modrs),
1126            contents: modrs,
1127        },
1128    );
1129    (files, still)
1130}
1131
1132#[cfg(test)]
1133mod tests {
1134    use super::*;
1135    use crate::{explode, plan_split};
1136
1137    #[test]
1138    fn splits_a_flat_bin_root_into_cohesive_under_budget_modules() {
1139        // four cohesive groups (g{k}_0 -> g{k}_1 -> g{k}_2 -> g{k}_3); main calls g0_0
1140        let mut src = String::from("use std::fmt;\n\n");
1141        for k in 0..4 {
1142            for n in 0..4 {
1143                let call = if n < 3 {
1144                    format!("    g{k}_{}();\n", n + 1)
1145                } else {
1146                    String::new()
1147                };
1148                src.push_str(&format!("fn g{k}_{n}() {{\n{call}}}\n\n"));
1149            }
1150        }
1151        src.push_str("fn main() {\n    g0_0();\n}\n");
1152        let exploded = explode(&src).unwrap();
1153        let max_loc = 60;
1154        let out = split_bin(&exploded, max_loc, "main");
1155
1156        assert!(out.still_oversized.is_empty());
1157        for f in &out.files {
1158            assert!(f.loc < max_loc, "{} over budget: {}", f.path, f.loc);
1159        }
1160        let root = out.files.iter().find(|f| f.path == "main.rs").unwrap();
1161        assert!(root.contents.contains("fn main"), "main stays at root");
1162        assert!(root.contents.contains("pub(crate) use "));
1163        let part = out.files.iter().find(|f| f.path != "main.rs").unwrap();
1164        assert!(part.contents.contains("use crate::*;"));
1165        assert!(part.contents.contains("pub(crate) fn "));
1166        // cohesion: the whole g0 group lands in ONE module
1167        let g0 = out
1168            .files
1169            .iter()
1170            .find(|f| f.contents.contains("fn g0_1"))
1171            .unwrap();
1172        for n in 0..4 {
1173            assert!(
1174                g0.contents.contains(&format!("fn g0_{n}")),
1175                "g0 group together"
1176            );
1177        }
1178    }
1179
1180    #[test]
1181    fn oversized_mod_is_extracted_to_its_own_file() {
1182        let mut inner = String::new();
1183        for i in 0..10 {
1184            inner.push_str(&format!("    fn t{i}() {{}}\n"));
1185        }
1186        let src = format!("fn main() {{}}\n\n#[cfg(test)]\nmod tests {{\n{inner}}}\n");
1187        let exploded = explode(&src).unwrap();
1188        let plan = plan_split(&exploded, 6);
1189        // ceiling large enough that the test mod fits whole -> single tests.rs
1190        let out = reassemble_bin(&exploded, &plan, "main", 1000);
1191        let tests = out.files.iter().find(|f| f.path == "tests.rs").unwrap();
1192        assert!(tests.contents.contains("fn t0()"));
1193        assert!(!tests.contents.contains("mod tests"));
1194        let root = out.files.iter().find(|f| f.path == "main.rs").unwrap();
1195        assert!(root.contents.contains("#[cfg(test)]\nmod tests;"));
1196    }
1197
1198    #[test]
1199    fn an_over_ceiling_test_mod_is_nested_into_a_subdir() {
1200        // 30 test fns referencing a shared helper -> test mod far over a tiny ceiling
1201        let mut inner =
1202            String::from("    use super::*;\n\n    fn helper() -> i32 {\n        1\n    }\n");
1203        for i in 0..30 {
1204            inner.push_str(&format!(
1205                "\n    #[test]\n    fn t{i}() {{\n        assert_eq!(helper(), 1);\n    }}\n"
1206            ));
1207        }
1208        let src = format!(
1209            "pub fn helper() -> i32 {{\n    1\n}}\n\nfn main() {{}}\n\n#[cfg(test)]\nmod tests {{\n{inner}}}\n"
1210        );
1211        let exploded = explode(&src).unwrap();
1212        let out = split_bin(&exploded, 60, "main");
1213
1214        // the test mod became a `tests/` subdir, every file under the ceiling
1215        let mod_rs = out.files.iter().find(|f| f.path == "tests/mod.rs").unwrap();
1216        assert!(
1217            mod_rs.contents.contains("pub(crate) use super::*;"),
1218            "forwards the parent"
1219        );
1220        assert!(mod_rs.contents.contains("mod g00;"));
1221        assert!(out.files.iter().any(|f| f.path == "tests/g00.rs"));
1222        for f in &out.files {
1223            assert!(f.loc < 60, "{} over ceiling: {}", f.path, f.loc);
1224        }
1225        let root = out.files.iter().find(|f| f.path == "main.rs").unwrap();
1226        assert!(root.contents.contains("#[cfg(test)]\nmod tests;"));
1227    }
1228
1229    #[test]
1230    fn split_mod_uses_super_and_preserves_pub_api() {
1231        // alpha (pub, the module's API) calls beta (private helper) -> one cluster
1232        let src = "use std::fmt;\n\npub fn alpha() -> i32 {\n    beta()\n}\n\nfn beta() -> i32 {\n    1\n}\n";
1233        let exploded = explode(src).unwrap();
1234        let out = split_mod(&exploded, 10_000, "mod");
1235
1236        let root = out.files.iter().find(|f| f.path == "mod.rs").unwrap();
1237        assert!(
1238            root.contents.contains("pub use "),
1239            "pub re-export preserves the API"
1240        );
1241        assert!(
1242            !root.contents.contains("pub(crate) use "),
1243            "a single `pub use *` caps each item at its own visibility — no \
1244             redundant second `pub(crate) use` line"
1245        );
1246
1247        let sub = out.files.iter().find(|f| f.path != "mod.rs").unwrap();
1248        assert!(
1249            sub.contents.contains("use super::*;"),
1250            "siblings reached via super, not crate"
1251        );
1252        assert!(!sub.contents.contains("use crate::*;"));
1253        assert!(sub.contents.contains("pub fn alpha"), "pub item stays pub");
1254        assert!(
1255            sub.contents.contains("pub(crate) fn beta"),
1256            "private bumped to pub(crate)"
1257        );
1258    }
1259
1260    // ----- issue 1: a plain `foo.rs` file module places sub-modules in `foo/` ---
1261
1262    #[test]
1263    fn file_module_places_submodules_in_a_subdir() {
1264        // A plain `foo.rs` file module (stem != "mod"): `mod bar;` in it resolves
1265        // to `foo/bar.rs`, so parts must be written under a `foo/` subdir, not as
1266        // siblings of `foo.rs` (which would not compile — E0583).
1267        let mut src = String::new();
1268        for k in 0..3 {
1269            // three items that reference nothing -> three separate parts
1270            src.push_str(&format!("pub fn item{k}() -> i32 {{\n    {k}\n}}\n\n"));
1271        }
1272        let exploded = explode(&src).unwrap();
1273        let out = split_mod(&exploded, 10_000, "wstest");
1274
1275        let root = out.files.iter().find(|f| f.path == "wstest.rs").unwrap();
1276        assert!(
1277            root.contents.contains("mod item0;"),
1278            "root file-module declares the sub-modules"
1279        );
1280        let subs: Vec<&str> = out
1281            .files
1282            .iter()
1283            .map(|f| f.path.as_str())
1284            .filter(|p| *p != "wstest.rs")
1285            .collect();
1286        assert!(!subs.is_empty(), "produced sub-module files");
1287        for path in &subs {
1288            assert!(
1289                path.starts_with("wstest/"),
1290                "sub-module {path} must live under wstest/ (file-module subdir)"
1291            );
1292        }
1293    }
1294
1295    #[test]
1296    fn mod_rs_keeps_submodules_as_siblings() {
1297        // The `foo/mod.rs` case (stem == "mod"): sub-modules stay siblings, no subdir.
1298        let mut src = String::new();
1299        for k in 0..3 {
1300            src.push_str(&format!("pub fn item{k}() -> i32 {{\n    {k}\n}}\n\n"));
1301        }
1302        let exploded = explode(&src).unwrap();
1303        let out = split_mod(&exploded, 10_000, "mod");
1304        for f in &out.files {
1305            assert!(
1306                !f.path.contains('/'),
1307                "mod.rs sub-modules are siblings, not in a subdir: {}",
1308                f.path
1309            );
1310        }
1311    }
1312
1313    // ----- issue 2: per-module pub vs pub(crate) vs no re-export decision -------
1314
1315    #[test]
1316    fn private_only_module_reexports_pub_crate_not_pub() {
1317        // No `pub` item: `pub use *` would warn "nothing public enough", so the
1318        // re-export must be `pub(crate) use`.
1319        let src = "fn helper() -> i32 {\n    leaf()\n}\n\nfn leaf() -> i32 {\n    1\n}\n";
1320        let exploded = explode(src).unwrap();
1321        let out = split_mod(&exploded, 10_000, "mod");
1322        let root = out.files.iter().find(|f| f.path == "mod.rs").unwrap();
1323        assert!(
1324            root.contents.contains("pub(crate) use "),
1325            "all-private module re-exports pub(crate):\n{}",
1326            root.contents
1327        );
1328        assert!(
1329            !root.contents.contains("pub use "),
1330            "no `pub use` for a module with no public item"
1331        );
1332    }
1333
1334    #[test]
1335    fn impl_only_module_is_declared_but_not_reexported() {
1336        // `api` is a normal part; the bare impl references no sibling, so it
1337        // clusters alone with nothing nameable -> declared, but no glob re-export
1338        // (re-exporting it would only warn "doesn't reexport anything").
1339        let src = "pub fn api() -> i32 {\n    1\n}\n\nimpl Worker for External {\n    fn run(&self) {}\n}\n";
1340        let exploded = explode(src).unwrap();
1341        let out = split_mod(&exploded, 10_000, "mod");
1342        let root = out.files.iter().find(|f| f.path == "mod.rs").unwrap();
1343
1344        // exactly one glob re-export line (for `api`) though two modules declared
1345        let reexports = root
1346            .contents
1347            .lines()
1348            .filter(|l| l.contains(" use ") && l.trim_end().ends_with("::*;"))
1349            .count();
1350        assert_eq!(
1351            reexports, 1,
1352            "only the nameable module is re-exported:\n{}",
1353            root.contents
1354        );
1355        assert!(root.contents.contains("pub use api::*;"));
1356        assert_eq!(
1357            root.contents.matches("mod ").count(),
1358            2,
1359            "both modules are still declared:\n{}",
1360            root.contents
1361        );
1362    }
1363
1364    // ----- issue 3: crate-public entry fn is preserved as `pub use` -------------
1365
1366    #[test]
1367    fn bin_public_entry_fn_is_reexported_pub_so_it_stays_crate_visible() {
1368        // A lib crate root where `run` is the public entry (the bin calls
1369        // `crate_name::run`). It must be re-exported `pub`, not `pub(crate)`, or
1370        // `crate_name::run` is no longer visible to main.rs after the split.
1371        let src = "fn helper() -> i32 {\n    1\n}\n\npub fn run() -> i32 {\n    helper()\n}\n";
1372        let exploded = explode(src).unwrap();
1373        let out = split_bin(&exploded, 10_000, "lib");
1374
1375        let root = out.files.iter().find(|f| f.path == "lib.rs").unwrap();
1376        assert!(
1377            root.contents.contains("pub use "),
1378            "public `run` re-exported at pub keeps `crate::run` visible:\n{}",
1379            root.contents
1380        );
1381        assert!(
1382            !root.contents.contains("pub(crate) use "),
1383            "the module has a pub item, so one `pub use *` suffices (no double line)"
1384        );
1385    }
1386
1387    // ----- preamble (inner `//!`/`#![…]`) stays at the root, not copied to parts -
1388
1389    #[test]
1390    fn crate_preamble_stays_at_root_not_copied_into_parts() {
1391        // `#![allow]` / `//!` belong at the crate root (and `#![feature]` is a
1392        // hard error anywhere else); a crate-root attr already covers sub-modules.
1393        let src = "//! crate docs\n#![allow(dead_code)]\n\npub fn alpha() -> i32 {\n    beta()\n}\n\nfn beta() -> i32 {\n    1\n}\n";
1394        let exploded = explode(src).unwrap();
1395        let out = split_bin(&exploded, 10_000, "lib");
1396
1397        let root = out.files.iter().find(|f| f.path == "lib.rs").unwrap();
1398        assert!(
1399            root.contents.contains("#![allow(dead_code)]"),
1400            "preamble stays at the root"
1401        );
1402        for f in &out.files {
1403            if f.path != "lib.rs" {
1404                assert!(
1405                    !f.contents.contains("#!["),
1406                    "part {} must not carry crate-level inner attrs",
1407                    f.path
1408                );
1409                assert!(
1410                    !f.contents.contains("//! crate docs"),
1411                    "part {} must not duplicate the crate docs",
1412                    f.path
1413                );
1414            }
1415        }
1416    }
1417
1418    // ----- issue 3a: copy only the imports each file actually references -------
1419
1420    #[test]
1421    fn parts_copy_only_the_imports_they_reference() {
1422        // alpha names `Display`; bravo names nothing from the header. Each part
1423        // gets only the imports it references.
1424        let src = "use std::fmt::Display;\nuse std::collections::HashMap;\n\npub fn alpha(d: &dyn Display) {\n    let _ = d;\n}\n\npub fn bravo() -> i32 {\n    1\n}\n";
1425        let exploded = explode(src).unwrap();
1426        let out = split_mod(&exploded, 10_000, "mod");
1427
1428        let alpha = out
1429            .files
1430            .iter()
1431            .find(|f| f.contents.contains("fn alpha"))
1432            .unwrap();
1433        let bravo = out
1434            .files
1435            .iter()
1436            .find(|f| f.contents.contains("fn bravo"))
1437            .unwrap();
1438        assert!(
1439            alpha.contents.contains("use std::fmt::Display;"),
1440            "alpha references Display:\n{}",
1441            alpha.contents
1442        );
1443        assert!(
1444            !alpha.contents.contains("HashMap"),
1445            "alpha never names HashMap -> import dropped:\n{}",
1446            alpha.contents
1447        );
1448        assert!(
1449            !bravo.contents.contains("use std::fmt::Display;"),
1450            "bravo references nothing from the header:\n{}",
1451            bravo.contents
1452        );
1453        assert!(!bravo.contents.contains("HashMap"));
1454    }
1455
1456    #[test]
1457    fn globs_and_anonymous_trait_imports_are_always_copied() {
1458        // `use ...::*` and `use Trait as _` expose no name to match, so every
1459        // part keeps them — the safe choice for a trait used only via methods.
1460        let src = "use std::fmt::Write as _;\nuse std::prelude::v1::*;\n\npub fn writes(s: &mut String) {\n    let _ = write!(s, \"x\");\n}\n\npub fn other() -> i32 {\n    2\n}\n";
1461        let exploded = explode(src).unwrap();
1462        let out = split_mod(&exploded, 10_000, "mod");
1463        for f in &out.files {
1464            if f.path == "mod.rs" {
1465                continue;
1466            }
1467            assert!(
1468                f.contents.contains("use std::fmt::Write as _;"),
1469                "{} must keep the `as _` trait import:\n{}",
1470                f.path,
1471                f.contents
1472            );
1473            assert!(
1474                f.contents.contains("use std::prelude::v1::*;"),
1475                "{} must keep the glob import:\n{}",
1476                f.path,
1477                f.contents
1478            );
1479        }
1480    }
1481
1482    #[test]
1483    fn item_less_root_keeps_no_imports() {
1484        // A module root whose items all moved out is just decls + re-exports; it
1485        // needs no imports, while the part that uses one keeps it.
1486        let src = "use std::collections::HashMap;\n\npub fn solo(m: HashMap<u8, u8>) {\n    let _ = m;\n}\n";
1487        let exploded = explode(src).unwrap();
1488        let out = split_mod(&exploded, 10_000, "mod");
1489
1490        let root = out.files.iter().find(|f| f.path == "mod.rs").unwrap();
1491        assert!(
1492            !root.contents.contains("use std::collections::HashMap;"),
1493            "item-less root drops imports it doesn't use:\n{}",
1494            root.contents
1495        );
1496        let part = out.files.iter().find(|f| f.path != "mod.rs").unwrap();
1497        assert!(
1498            part.contents.contains("use std::collections::HashMap;"),
1499            "the part that uses HashMap keeps it"
1500        );
1501    }
1502
1503    // ----- razel regression 1: source re-exports must stay `pub use` at root --
1504
1505    #[test]
1506    fn source_pub_use_reexports_stay_pub_use_at_the_root() {
1507        // The original file's `pub use` lines ARE the crate's public API.
1508        // Treating them as droppable header imports (or demoting them to
1509        // `pub(crate) use`) silently breaks every dependent's paths.
1510        let src = "mod helpers;\n\npub use helpers::Helper;\n\nuse std::fmt::Display;\n\n\
1511                   pub fn api(d: &dyn Display) {\n    let _ = d;\n}\n";
1512        let exploded = explode(src).unwrap();
1513        let out = split_bin(&exploded, 10_000, "lib");
1514
1515        let root = out.files.iter().find(|f| f.path == "lib.rs").unwrap();
1516        assert!(
1517            root.contents.contains("mod helpers;"),
1518            "content-less mod decl stays at the root (it binds helpers.rs):\n{}",
1519            root.contents
1520        );
1521        assert!(
1522            root.contents.contains("pub use helpers::Helper;"),
1523            "source re-export stays verbatim at the root:\n{}",
1524            root.contents
1525        );
1526        assert!(
1527            !root.contents.contains("pub(crate) use helpers"),
1528            "a source re-export must never be demoted"
1529        );
1530        for f in &out.files {
1531            if f.path != "lib.rs" {
1532                assert!(
1533                    !f.contents.contains("pub use helpers::Helper;"),
1534                    "{} must not carry the root's re-export",
1535                    f.path
1536                );
1537                assert!(
1538                    !f.contents.contains("mod helpers;"),
1539                    "{} must not carry the root's mod decl (would look for a \
1540                     sibling subdir file)",
1541                    f.path
1542                );
1543            }
1544        }
1545    }
1546
1547    // ----- razel regression 2: the cfg(test) gate travels with the tests mod --
1548
1549    #[test]
1550    fn under_budget_cfg_test_mod_is_extracted_whole_with_its_gate() {
1551        // The razel failure: the tests mod fit under the item budget, so it was
1552        // CLUSTERED — wrapped inside a part module with an ungated root decl —
1553        // instead of extracted. A body mod must always move whole, its
1554        // `#[cfg(test)]` gating the root declaration.
1555        let src = "pub fn alpha() -> i32 {\n    1\n}\n\n\
1556                   #[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    \
1557                   fn smoke() {\n        assert_eq!(alpha(), 1);\n    }\n}\n";
1558        let exploded = explode(src).unwrap();
1559        // Budget far above the tests mod's size: it would have been clustered.
1560        let out = split_bin(&exploded, 10_000, "lib");
1561
1562        let root = out.files.iter().find(|f| f.path == "lib.rs").unwrap();
1563        assert!(
1564            root.contents.contains("#[cfg(test)]\nmod tests;"),
1565            "the gate must travel to the root declaration:\n{}",
1566            root.contents
1567        );
1568        assert!(
1569            !root.contents.contains("use tests::*"),
1570            "an extracted mod is declared, not glob re-exported:\n{}",
1571            root.contents
1572        );
1573        let tests = out.files.iter().find(|f| f.path == "tests.rs").unwrap();
1574        assert!(tests.contents.contains("fn smoke()"));
1575        assert!(
1576            !tests.contents.contains("mod tests"),
1577            "the body is unwrapped — no double nesting:\n{}",
1578            tests.contents
1579        );
1580    }
1581
1582    #[test]
1583    fn pub_mod_keeps_its_visibility_on_the_extracted_decl() {
1584        let src = "pub fn api() -> i32 {\n    1\n}\n\n\
1585                   /// Config store.\npub mod config {\n    pub fn get() -> i32 {\n        2\n    }\n}\n";
1586        let exploded = explode(src).unwrap();
1587        let out = split_bin(&exploded, 10_000, "lib");
1588
1589        let root = out.files.iter().find(|f| f.path == "lib.rs").unwrap();
1590        assert!(
1591            root.contents.contains("/// Config store.\npub mod config;"),
1592            "docs and `pub` travel to the declaration:\n{}",
1593            root.contents
1594        );
1595        let config = out.files.iter().find(|f| f.path == "config.rs").unwrap();
1596        assert!(config.contents.contains("pub fn get()"));
1597    }
1598
1599    #[test]
1600    fn part_colliding_with_an_extracted_mod_name_is_renamed() {
1601        // `fn tests` (value namespace) and `mod tests` (type namespace) can
1602        // coexist in the source; the extracted mod owns `tests.rs`, so the
1603        // part named after the fn must yield.
1604        let src = "fn tests() -> i32 {\n    1\n}\n\n\
1605                   #[cfg(test)]\nmod tests {\n    fn smoke() {}\n}\n";
1606        let exploded = explode(src).unwrap();
1607        let out = split_bin(&exploded, 10_000, "lib");
1608
1609        let paths: Vec<&str> = out.files.iter().map(|f| f.path.as_str()).collect();
1610        assert!(paths.contains(&"tests.rs"), "extracted mod file: {paths:?}");
1611        assert!(paths.contains(&"tests_.rs"), "renamed part file: {paths:?}");
1612        let root = out.files.iter().find(|f| f.path == "lib.rs").unwrap();
1613        assert!(root.contents.contains("mod tests_;"));
1614        assert!(root.contents.contains("#[cfg(test)]\nmod tests;"));
1615    }
1616
1617    #[test]
1618    fn root_keeps_imports_an_extracted_mod_body_references() {
1619        // The extracted tests mod reaches the root's imports via
1620        // `use super::*;` — dropping HashMap from the root would break it.
1621        let src = "use std::collections::HashMap;\n\n\
1622                   pub fn alpha() -> i32 {\n    1\n}\n\n\
1623                   #[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    \
1624                   fn smoke() {\n        let _m: HashMap<u8, u8> = HashMap::new();\n        \
1625                   assert_eq!(alpha(), 1);\n    }\n}\n";
1626        let exploded = explode(src).unwrap();
1627        let out = split_bin(&exploded, 10_000, "lib");
1628
1629        let root = out.files.iter().find(|f| f.path == "lib.rs").unwrap();
1630        assert!(
1631            root.contents.contains("use std::collections::HashMap;"),
1632            "root must keep imports the extracted body names:\n{}",
1633            root.contents
1634        );
1635    }
1636
1637    #[test]
1638    fn bin_root_keeps_only_imports_main_references() {
1639        // `main` names `exit` but not `HashMap` (the moved `worker` does), so the
1640        // root keeps `exit` and drops `HashMap`.
1641        let src = "use std::process::exit;\nuse std::collections::HashMap;\n\nfn worker() -> HashMap<u8, u8> {\n    HashMap::new()\n}\n\nfn main() {\n    let _ = worker();\n    exit(0);\n}\n";
1642        let exploded = explode(src).unwrap();
1643        let out = split_bin(&exploded, 10_000, "main");
1644
1645        let root = out.files.iter().find(|f| f.path == "main.rs").unwrap();
1646        assert!(
1647            root.contents.contains("use std::process::exit;"),
1648            "main references exit:\n{}",
1649            root.contents
1650        );
1651        assert!(
1652            !root.contents.contains("HashMap"),
1653            "main never names HashMap (worker does) -> dropped from root:\n{}",
1654            root.contents
1655        );
1656    }
1657
1658    // ----- issue 4: a part sits one module level below the file it came from --
1659
1660    #[test]
1661    fn nested_module_part_imports_gain_one_super_level() {
1662        // A part of `foo.rs` is module `foo::<part>`, one level below `foo`, so a
1663        // copied `use super::X` would name `foo` instead of `foo`'s parent and a
1664        // copied `use self::X` would name the part itself.
1665        let src = r#"use super::sibling::Thing;
1666use super::{alpha, beta::Gamma};
1667use self::local::X;
1668use super::*;
1669use crate::top::Absolute;
1670use std::fmt::Display;
1671
1672pub fn one(t: Thing, g: Gamma, x: X, d: &dyn Display) -> i32 {
1673    let _: Option<Absolute> = None;
1674    let _ = (t, g, x, d);
1675    alpha()
1676}
1677
1678#[cfg(test)]
1679mod tests {
1680    use super::*;
1681
1682    #[test]
1683    fn smoke() {
1684        let _ = Thing;
1685    }
1686}
1687"#;
1688        let exploded = explode(src).unwrap();
1689        let out = split_mod(&exploded, 10_000, "foo");
1690
1691        let part = out.files.iter().find(|f| f.path == "foo/one.rs").unwrap();
1692        for expected in [
1693            "use super::super::sibling::Thing;",
1694            "use super::super::{alpha, beta::Gamma};",
1695            "use super::local::X;",
1696            "use super::super::*;",
1697            // `crate::` and external paths are absolute — untouched.
1698            "use crate::top::Absolute;",
1699            "use std::fmt::Display;",
1700        ] {
1701            assert!(
1702                part.contents.contains(expected),
1703                "part must carry `{expected}`:\n{}",
1704                part.contents
1705            );
1706        }
1707        // The tool's own sibling glob is still there, exactly once, and is not
1708        // confused with the header's rewritten `use super::*;`.
1709        assert_eq!(
1710            part.contents
1711                .lines()
1712                .filter(|l| l.trim() == "use super::*;")
1713                .count(),
1714            1,
1715            "exactly one tool-added sibling glob:\n{}",
1716            part.contents
1717        );
1718
1719        // The root is still the original module — its header must not move.
1720        let root = out.files.iter().find(|f| f.path == "foo.rs").unwrap();
1721        assert!(
1722            root.contents.contains("use super::sibling::Thing;"),
1723            "root header stays byte-unchanged:\n{}",
1724            root.contents
1725        );
1726        assert!(
1727            !root.contents.contains("super::super"),
1728            "the root did not move — nothing to re-anchor:\n{}",
1729            root.contents
1730        );
1731    }
1732
1733    #[test]
1734    fn crate_root_part_imports_rewrite_self_to_crate() {
1735        // Below a crate root `self::X` names the part, not the crate root;
1736        // `super::` cannot occur at a crate root, so nothing else changes.
1737        let src = "use self::helpers::Helper;\nuse std::fmt::Display;\n\n\
1738                   pub fn api(h: Helper, d: &dyn Display) {\n    let _ = (h, d);\n}\n";
1739        let exploded = explode(src).unwrap();
1740        let out = split_bin(&exploded, 10_000, "lib");
1741
1742        let part = out.files.iter().find(|f| f.path != "lib.rs").unwrap();
1743        assert!(
1744            part.contents.contains("use crate::helpers::Helper;"),
1745            "`self::` re-anchors to `crate::` below a crate root:\n{}",
1746            part.contents
1747        );
1748        assert!(part.contents.contains("use std::fmt::Display;"));
1749    }
1750
1751    #[test]
1752    fn nested_split_group_headers_gain_one_super_level() {
1753        // `tests/gNN.rs` is module `tests::gNN`, one below the extracted `tests`
1754        // body the group header was copied from.
1755        let mut inner = String::from(
1756            "    use super::outer::Thing;\n    use self::inner::Y;\n\n\
1757             \x20   fn helper() -> i32 {\n        1\n    }\n",
1758        );
1759        for i in 0..30 {
1760            inner.push_str(&format!(
1761                "\n    #[test]\n    fn t{i}() {{\n        \
1762                 let _: Option<Thing> = None;\n        let _: Option<Y> = None;\n        \
1763                 assert_eq!(helper(), 1);\n    }}\n"
1764            ));
1765        }
1766        let src = format!(
1767            "pub fn helper() -> i32 {{\n    1\n}}\n\nfn main() {{}}\n\n\
1768             #[cfg(test)]\nmod tests {{\n{inner}}}\n"
1769        );
1770        let exploded = explode(&src).unwrap();
1771        let out = split_bin(&exploded, 60, "main");
1772
1773        let group = out.files.iter().find(|f| f.path == "tests/g00.rs").unwrap();
1774        assert!(
1775            group.contents.contains("use super::super::outer::Thing;"),
1776            "group header `super::` gains a level:\n{}",
1777            group.contents
1778        );
1779        assert!(
1780            group.contents.contains("use super::inner::Y;"),
1781            "group header `self::` becomes `super::`:\n{}",
1782            group.contents
1783        );
1784    }
1785
1786    #[test]
1787    fn extracted_inline_mod_keeps_its_own_super_paths() {
1788        // An inline `mod tests` extracted out of `foo.rs` lands at `foo/tests.rs`
1789        // — still module `foo::tests`, the level it already had — so its own
1790        // `use super::helper;` still names `foo` and must NOT be re-anchored.
1791        let src = "pub fn helper() -> i32 {\n    1\n}\n\n\
1792                   #[cfg(test)]\nmod tests {\n    use super::helper;\n\n    #[test]\n    \
1793                   fn smoke() {\n        assert_eq!(helper(), 1);\n    }\n}\n";
1794        let exploded = explode(src).unwrap();
1795        let out = split_mod(&exploded, 10_000, "foo");
1796
1797        let tests = out.files.iter().find(|f| f.path == "foo/tests.rs").unwrap();
1798        assert!(
1799            tests.contents.contains("use super::helper;"),
1800            "an extracted mod keeps its own module level:\n{}",
1801            tests.contents
1802        );
1803        assert!(
1804            !tests.contents.contains("super::super"),
1805            "the extracted body did not change module level:\n{}",
1806            tests.contents
1807        );
1808    }
1809
1810    // ----- issue 5: include! paths resolve against the containing file --------
1811
1812    #[test]
1813    fn include_paths_in_moved_bodies_are_rebased_one_directory_deeper() {
1814        let src = r#"pub fn doc() -> &'static str {
1815    include_str!("../docs/a.md")
1816}
1817
1818pub fn blob() -> &'static [u8] {
1819    include_bytes!("fixtures/b.bin")
1820}
1821
1822pub fn built() -> &'static str {
1823    include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/x"))
1824}
1825
1826pub fn rooted() -> &'static str {
1827    include_str!("/etc/x")
1828}
1829"#;
1830        let exploded = explode(src).unwrap();
1831        let out = split_mod(&exploded, 10_000, "foo");
1832
1833        let moved: String = out
1834            .files
1835            .iter()
1836            .filter(|f| f.path != "foo.rs")
1837            .map(|f| f.contents.as_str())
1838            .collect();
1839        assert!(
1840            moved.contains(r#"include_str!("../../docs/a.md")"#),
1841            "a relative include! path gains one `../`:\n{moved}"
1842        );
1843        assert!(
1844            moved.contains(r#"include_bytes!("../fixtures/b.bin")"#),
1845            "a bare relative path is prefixed too:\n{moved}"
1846        );
1847        assert!(
1848            moved.contains(r#"include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/x"))"#),
1849            "a non-literal argument is byte-unchanged:\n{moved}"
1850        );
1851        assert!(
1852            moved.contains(r#"include_str!("/etc/x")"#),
1853            "an absolute path is byte-unchanged:\n{moved}"
1854        );
1855    }
1856
1857    #[test]
1858    fn include_paths_are_untouched_when_the_file_stays_in_its_directory() {
1859        // A crate root's parts are siblings of the root file, so no body moved
1860        // between directories and no include! path may change.
1861        let src = "fn worker() -> &'static str {\n    include_str!(\"../docs/w.md\")\n}\n\n\
1862                   fn main() {\n    let _ = include_str!(\"../docs/m.md\");\n    \
1863                   let _ = worker();\n}\n";
1864        let exploded = explode(src).unwrap();
1865        let out = split_bin(&exploded, 10_000, "main");
1866
1867        for f in &out.files {
1868            assert!(
1869                !f.contents.contains("../../docs/"),
1870                "{} stayed in the source directory:\n{}",
1871                f.path,
1872                f.contents
1873            );
1874        }
1875        let root = out.files.iter().find(|f| f.path == "main.rs").unwrap();
1876        assert!(root.contents.contains(r#"include_str!("../docs/m.md")"#));
1877    }
1878
1879    // ----- issue 4b: relative paths *inside* a moved body move with it --------
1880
1881    #[test]
1882    fn review_body_paths_preserve_opaque_macro_and_attribute_tokens() {
1883        let src = r#"#[metadata(super::Thing)]
1884pub fn probe() -> super::Thing {
1885    macro_rules! data { () => { self::Thing }; }
1886    let _ = stringify!(super::Thing);
1887    let _ = stringify! { self::Thing };
1888    let _ = custom!(super::Thing, (self::Thing), stringify!(super::Thing));
1889    let _ = super::factory!(self::Thing);
1890    <super::Thing as self::Trait>::make()
1891}
1892"#;
1893        let expected = src
1894            .replace("-> super::Thing", "-> super::super::Thing")
1895            .replace("super::factory!", "super::super::factory!")
1896            .replace(
1897                "<super::Thing as self::Trait>",
1898                "<super::super::Thing as super::Trait>",
1899            );
1900        assert_eq!(rebase_body_paths(src, Rebase::Nested), expected);
1901        assert_eq!(rebase_body_paths(src, Rebase::Keep), src);
1902    }
1903
1904    #[test]
1905    fn review_standard_expression_macro_arguments_rebase_paths() {
1906        let src = r#"fn probe() {
1907    assert_eq!(super::Thing::value(), self::value(), "{}", super::message());
1908    let _ = format!("{}", super::Thing::value());
1909    let _ = custom!(super::Thing, self::Thing);
1910    let _ = custom::assert_eq!(super::Thing, self::Thing);
1911    let _ = stringify!(super::Thing);
1912}
1913"#;
1914        let expected = src
1915            .replace("super::Thing::value()", "super::super::Thing::value()")
1916            .replace("self::value()", "super::value()")
1917            .replace("super::message()", "super::super::message()");
1918        assert_eq!(rebase_body_paths(src, Rebase::Nested), expected);
1919    }
1920
1921    #[test]
1922    fn review_renamed_use_anchors_rebase_in_headers_and_bodies() {
1923        for (source, nested, root) in [
1924            (
1925                "super as parent",
1926                "super::super as parent",
1927                "super as parent",
1928            ),
1929            ("self as current", "super as current", "crate as current"),
1930            (
1931                "{super as parent, self as current, super::{self as p, Thing}}",
1932                "{super::super as parent, super as current, super::super::{self as p, Thing}}",
1933                "{super as parent, crate as current, super::{self as p, Thing}}",
1934            ),
1935            ("crate as root", "crate as root", "crate as root"),
1936        ] {
1937            for (rebase, expected) in [
1938                (Rebase::Nested, nested),
1939                (Rebase::CrateRoot, root),
1940                (Rebase::Keep, source),
1941            ] {
1942                let import =
1943                    format!("// keep this comment\n#[allow(unused_imports)]\nuse {source};\n");
1944                let expected = import.replace(source, expected);
1945                assert_eq!(rebase_import(&import, rebase), expected);
1946                assert_eq!(
1947                    rebase_body_paths(&format!("fn probe() {{\n{import}}}"), rebase),
1948                    format!("fn probe() {{\n{expected}}}"),
1949                );
1950            }
1951        }
1952    }
1953
1954    #[test]
1955    fn review_include_literals_accept_one_optional_trailing_comma() {
1956        for name in PATH_MACROS {
1957            for literal in [r#""docs/file""#, r##"r#"docs/file"#"##] {
1958                let src = format!("fn probe() {{ {name}!({literal} /* keep */,); }}");
1959                assert_eq!(
1960                    rebase_includes(&src, 2, "foo/part.rs"),
1961                    src.replace("docs/file", "../../docs/file"),
1962                );
1963                for suffix in [",,", ", other", " + other"] {
1964                    let src = format!("fn probe() {{ {name}!({literal}{suffix}); }}");
1965                    assert_eq!(rebase_includes(&src, 1, "foo/part.rs"), src);
1966                }
1967            }
1968        }
1969    }
1970
1971    #[test]
1972    fn review_include_absolute_windows_paths_are_host_independent() {
1973        for name in PATH_MACROS {
1974            for literal in [
1975                r#""C:\\docs\\file""#,
1976                r#""z:/docs/file""#,
1977                r#"r"C:\docs\file""#,
1978                r#""\\\\server\\share\\file""#,
1979                r#"r"\\server\share\file""#,
1980                r#"r"\\?\C:\docs\file""#,
1981                r#""//server/share/file""#,
1982                r#""/docs/file""#,
1983            ] {
1984                let src = format!("fn probe() {{ {name}!({literal}); }}");
1985                assert_eq!(rebase_includes(&src, 1, "foo/part.rs"), src);
1986            }
1987            let src = format!("fn probe() {{ {name}!(\"C:relative\"); }}");
1988            assert_eq!(
1989                rebase_includes(&src, 1, "foo/part.rs"),
1990                src.replace("C:relative", "../C:relative"),
1991            );
1992        }
1993    }
1994
1995    #[test]
1996    fn moved_body_paths_gain_one_super_level() {
1997        // The header is not the only place a relative path hides: expression and
1998        // type paths, and a function-local `use`, travel inside the item.
1999        let src = r#"pub fn probe() -> i32 {
2000    use super::alpha::Beta;
2001    let _ = Beta;
2002    let _ = super::filter::parse("x");
2003    let _ = super::super::grandparent::thing();
2004    let _ = self::local::helper();
2005    let _: super::sibling::Kind = super::sibling::make();
2006    let _ = super::sibling::Record {
2007        meta: super::meta::build("req"),
2008    };
2009    crate::top::value()
2010}
2011"#;
2012        let exploded = explode(src).unwrap();
2013        let out = split_mod(&exploded, 10_000, "foo");
2014
2015        let part = out.files.iter().find(|f| f.path == "foo/probe.rs").unwrap();
2016        for expected in [
2017            "use super::super::alpha::Beta;",
2018            r#"super::super::filter::parse("x")"#,
2019            // Only the leading segment moves: two levels become three, not four.
2020            "super::super::super::grandparent::thing()",
2021            "super::local::helper()",
2022            // A lone `:` — a type annotation, a struct field init — is not a
2023            // path separator, so the segment after it still starts a path.
2024            "let _: super::super::sibling::Kind = super::super::sibling::make();",
2025            "meta: super::super::meta::build(\"req\"),",
2026            "crate::top::value()",
2027        ] {
2028            assert!(
2029                part.contents.contains(expected),
2030                "moved body must carry `{expected}`:\n{}",
2031                part.contents
2032            );
2033        }
2034    }
2035
2036    #[test]
2037    fn moved_body_leaves_self_receivers_and_use_groups_alone() {
2038        // `self` is only a module anchor when a `::` follows it: a receiver, a
2039        // `use …::{self, …}` group member and a `pub(super)` restriction are all
2040        // something else. (Re-anchoring a `pub(super)` restriction — which does
2041        // narrow when the item moves — is out of scope here.)
2042        let src = r#"pub struct Widget {
2043    pub n: i32,
2044}
2045
2046impl Widget {
2047    pub fn get(&self) -> i32 {
2048        use std::collections::{self, HashMap};
2049        let _: Option<HashMap<u8, u8>> = None;
2050        let _ = collections::BTreeMap::<u8, u8>::new();
2051        let _ = Self::make();
2052        self.n
2053    }
2054
2055    pub fn make() -> Self {
2056        Self { n: 1 }
2057    }
2058}
2059
2060pub(super) fn gated() -> i32 {
2061    1
2062}
2063"#;
2064        let exploded = explode(src).unwrap();
2065        let out = split_mod(&exploded, 10_000, "foo");
2066        let moved: String = out
2067            .files
2068            .iter()
2069            .filter(|f| f.path != "foo.rs")
2070            .map(|f| f.contents.as_str())
2071            .collect();
2072
2073        for expected in [
2074            "use std::collections::{self, HashMap};",
2075            "Self::make()",
2076            "self.n",
2077            "pub(super) fn gated",
2078        ] {
2079            assert!(
2080                moved.contains(expected),
2081                "`{expected}` is not a module anchor and must not move:\n{moved}"
2082            );
2083        }
2084        assert!(
2085            !moved.contains("pub(super::super)"),
2086            "a `pub(super)` restriction must never be mangled:\n{moved}"
2087        );
2088    }
2089
2090    #[test]
2091    fn moved_body_paths_below_a_crate_root_rewrite_self_to_crate() {
2092        let src = "pub fn entry() -> i32 {\n    let _ = self::helpers::setup();\n    \
2093                   crate::other::value()\n}\n";
2094        let exploded = explode(src).unwrap();
2095        let out = split_bin(&exploded, 10_000, "lib");
2096
2097        let part = out.files.iter().find(|f| f.path != "lib.rs").unwrap();
2098        assert!(
2099            part.contents.contains("crate::helpers::setup()"),
2100            "`self::` below a crate root is `crate::`:\n{}",
2101            part.contents
2102        );
2103        assert!(part.contents.contains("crate::other::value()"));
2104    }
2105
2106    #[test]
2107    fn explode_never_rewrites_include_paths() {
2108        // Re-basing belongs to `split`; `explode` stays byte-exact.
2109        let src = "pub fn doc() -> &'static str {\n    include_str!(\"../docs/a.md\")\n}\n";
2110        let exploded = explode(src).unwrap();
2111        let joined: String = exploded.chunks.iter().map(|c| c.text.as_str()).collect();
2112        assert_eq!(joined, src, "explode must stay lossless");
2113    }
2114}