rust-split 0.2.2

A parser-based god-file splitter for Rust source.
Documentation
use super::*;

#[test]
fn splits_a_flat_bin_root_into_cohesive_under_budget_modules() {
    // four cohesive groups (g{k}_0 -> g{k}_1 -> g{k}_2 -> g{k}_3); main calls g0_0
    let mut src = String::from("use std::fmt;\n\n");
    for k in 0..4 {
        for n in 0..4 {
            let call = if n < 3 {
                format!("    g{k}_{}();\n", n + 1)
            } else {
                String::new()
            };
            src.push_str(&format!("fn g{k}_{n}() {{\n{call}}}\n\n"));
        }
    }
    src.push_str("fn main() {\n    g0_0();\n}\n");
    let exploded = explode(&src).unwrap();
    let max_loc = 60;
    let out = split_bin(&exploded, max_loc, "main");

    assert!(out.still_oversized.is_empty());
    for f in &out.files {
        assert!(f.loc < max_loc, "{} over budget: {}", f.path, f.loc);
    }
    let root = out.files.iter().find(|f| f.path == "main.rs").unwrap();
    assert!(root.contents.contains("fn main"), "main stays at root");
    assert!(root.contents.contains("pub(crate) use "));
    let part = out.files.iter().find(|f| f.path != "main.rs").unwrap();
    assert!(part.contents.contains("use crate::*;"));
    assert!(part.contents.contains("pub(crate) fn "));
    // cohesion: the whole g0 group lands in ONE module
    let g0 = out
        .files
        .iter()
        .find(|f| f.contents.contains("fn g0_1"))
        .unwrap();
    for n in 0..4 {
        assert!(
            g0.contents.contains(&format!("fn g0_{n}")),
            "g0 group together"
        );
    }
}

// ----- issue 1: a plain `foo.rs` file module places sub-modules in `foo/` ---

#[test]
fn file_module_places_submodules_in_a_subdir() {
    // A plain `foo.rs` file module (stem != "mod"): `mod bar;` in it resolves
    // to `foo/bar.rs`, so parts must be written under a `foo/` subdir, not as
    // siblings of `foo.rs` (which would not compile — E0583).
    let mut src = String::new();
    for k in 0..3 {
        // three items that reference nothing -> three separate parts
        src.push_str(&format!("pub fn item{k}() -> i32 {{\n    {k}\n}}\n\n"));
    }
    let exploded = explode(&src).unwrap();
    let out = split_mod(&exploded, 10_000, "wstest");

    let root = out.files.iter().find(|f| f.path == "wstest.rs").unwrap();
    assert!(
        root.contents.contains("mod item0;"),
        "root file-module declares the sub-modules"
    );
    let subs: Vec<&str> = out
        .files
        .iter()
        .map(|f| f.path.as_str())
        .filter(|p| *p != "wstest.rs")
        .collect();
    assert!(!subs.is_empty(), "produced sub-module files");
    for path in &subs {
        assert!(
            path.starts_with("wstest/"),
            "sub-module {path} must live under wstest/ (file-module subdir)"
        );
    }
}

#[test]
fn mod_rs_keeps_submodules_as_siblings() {
    // The `foo/mod.rs` case (stem == "mod"): sub-modules stay siblings, no subdir.
    let mut src = String::new();
    for k in 0..3 {
        src.push_str(&format!("pub fn item{k}() -> i32 {{\n    {k}\n}}\n\n"));
    }
    let exploded = explode(&src).unwrap();
    let out = split_mod(&exploded, 10_000, "mod");
    for f in &out.files {
        assert!(
            !f.path.contains('/'),
            "mod.rs sub-modules are siblings, not in a subdir: {}",
            f.path
        );
    }
}

// ----- preamble (inner `//!`/`#![…]`) stays at the root, not copied to parts -

#[test]
fn crate_preamble_stays_at_root_not_copied_into_parts() {
    // `#![allow]` / `//!` belong at the crate root (and `#![feature]` is a
    // hard error anywhere else); a crate-root attr already covers sub-modules.
    let src = "//! crate docs\n#![allow(dead_code)]\n\npub fn alpha() -> i32 {\n    beta()\n}\n\nfn beta() -> i32 {\n    1\n}\n";
    let exploded = explode(src).unwrap();
    let out = split_bin(&exploded, 10_000, "lib");

    let root = out.files.iter().find(|f| f.path == "lib.rs").unwrap();
    assert!(
        root.contents.contains("#![allow(dead_code)]"),
        "preamble stays at the root"
    );
    for f in &out.files {
        if f.path != "lib.rs" {
            assert!(
                !f.contents.contains("#!["),
                "part {} must not carry crate-level inner attrs",
                f.path
            );
            assert!(
                !f.contents.contains("//! crate docs"),
                "part {} must not duplicate the crate docs",
                f.path
            );
        }
    }
}

// ----- razel regression 1: source re-exports must stay `pub use` at root --

#[test]
fn source_pub_use_reexports_stay_pub_use_at_the_root() {
    // The original file's `pub use` lines ARE the crate's public API.
    // Treating them as droppable header imports (or demoting them to
    // `pub(crate) use`) silently breaks every dependent's paths.
    let src = "mod helpers;\n\npub use helpers::Helper;\n\nuse std::fmt::Display;\n\n\
               pub fn api(d: &dyn Display) {\n    let _ = d;\n}\n";
    let exploded = explode(src).unwrap();
    let out = split_bin(&exploded, 10_000, "lib");

    let root = out.files.iter().find(|f| f.path == "lib.rs").unwrap();
    assert!(
        root.contents.contains("mod helpers;"),
        "content-less mod decl stays at the root (it binds helpers.rs):\n{}",
        root.contents
    );
    assert!(
        root.contents.contains("pub use helpers::Helper;"),
        "source re-export stays verbatim at the root:\n{}",
        root.contents
    );
    assert!(
        !root.contents.contains("pub(crate) use helpers"),
        "a source re-export must never be demoted"
    );
    for f in &out.files {
        if f.path != "lib.rs" {
            assert!(
                !f.contents.contains("pub use helpers::Helper;"),
                "{} must not carry the root's re-export",
                f.path
            );
            assert!(
                !f.contents.contains("mod helpers;"),
                "{} must not carry the root's mod decl (would look for a \
                 sibling subdir file)",
                f.path
            );
        }
    }
}

#[test]
fn part_colliding_with_an_extracted_mod_name_is_renamed() {
    // `fn tests` (value namespace) and `mod tests` (type namespace) can
    // coexist in the source; the extracted mod owns `tests.rs`, so the
    // part named after the fn must yield.
    let src = "fn tests() -> i32 {\n    1\n}\n\n\
               mod tests {\n    fn smoke() {}\n}\n";
    let exploded = explode(src).unwrap();
    let out = split_bin(&exploded, 10_000, "lib");

    let paths: Vec<&str> = out.files.iter().map(|f| f.path.as_str()).collect();
    assert!(paths.contains(&"tests.rs"), "extracted mod file: {paths:?}");
    assert!(paths.contains(&"tests_.rs"), "renamed part file: {paths:?}");
    let root = out.files.iter().find(|f| f.path == "lib.rs").unwrap();
    assert!(root.contents.contains("mod tests_;"));
    assert!(root.contents.contains("mod tests;"));
}