rust-split 0.2.2

A parser-based god-file splitter for Rust source.
Documentation
//! Deterministic responsibility hints, ownership, and preservation contracts.
use std::fs;
use std::process::Command;

use rust_split::{Exploded, SplitOutput, explode, plan_split, split_bin, split_mod};

fn text_at<'a>(out: &'a SplitOutput, path: &str) -> &'a str {
    &out.files.iter().find(|f| f.path == path).unwrap().contents
}

fn assert_plan_partition(exploded: &Exploded, ceiling: usize) {
    let plan = plan_split(exploded, ceiling);
    for _ in 0..20 {
        assert_eq!(plan, plan_split(exploded, ceiling));
    }
    let mut indices = plan.header.clone();
    indices.extend(&plan.root_items);
    indices.extend(&plan.mods);
    indices.extend(plan.oversized.iter().map(|o| o.chunk_index));
    for part in &plan.parts {
        assert!(part.loc < ceiling, "{part:?}");
        indices.extend(&part.chunk_indices);
    }
    indices.sort_unstable();
    assert_eq!(indices, (0..exploded.chunks.len()).collect::<Vec<_>>());
}

#[test]
fn banners_bound_cross_references_even_through_a_shared_helper() {
    let src = "fn shared() {}\n\n\
        // ---- Parsing ----\n\n\
        fn parse() { shared(); emit(); }\n\n\
        fn parse_more() { parse(); }\n\n\
        // ---- Output ----\n\n\
        fn emit() { shared(); parse(); }\n\n\
        fn emit_more() { emit(); }\n\n\
        // ---- Parsing ----\n\n\
        fn parse_again() { parse(); }\n";
    let exploded = explode(src).unwrap();
    assert_eq!(
        exploded
            .chunks
            .iter()
            .map(|c| c.text.as_str())
            .collect::<String>(),
        src
    );
    let plan = plan_split(&exploded, 1000);
    assert_eq!(plan.parts.len(), 4, "helper + three explicit sections");
    let out = split_mod(&exploded, 1000, "mod");
    assert!(text_at(&out, "parsing.rs").contains("fn parse_more"));
    assert!(!text_at(&out, "parsing.rs").contains("fn emit"));
    assert!(text_at(&out, "output.rs").contains("fn emit_more"));
    assert!(text_at(&out, "parsing_2.rs").contains("fn parse_again"));
    for ceiling in [1, 5, 10, 30, 1000] {
        assert_plan_partition(&exploded, ceiling);
    }
}

const SHARED: &str = "use std::collections::BTreeMap;\n\n\
    /// Shared across phases.\n\
    #[derive(Default)]\n\
    pub struct State<T> {\n    values: BTreeMap<u8, T>,\n}\n\n\
    pub enum Mode { Ready }\n\n\
    union Bits { value: u32 }\n\n\
    // ---- Collection ----\n\n\
    impl<T> self::State<T> {\n\
        pub fn new(value: T) -> Self {\n\
            Self { values: BTreeMap::from([(0, value)]) }\n\
        }\n\
    }\n\n\
    // ---- Reading ----\n\n\
    impl<T> State<T> {\n\
        fn read(&self) -> &T {\n\
            self.values.get(&0).unwrap()\n\
        }\n\
    }\n\n\
    impl Mode { fn ready() -> Self { Self::Ready } }\n\n\
    impl Bits { fn new() -> Self { Self { value: 1 } } }\n\n\
    impl std::fmt::Display for State<u8> {\n\
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n\
            write!(f, \"{}\", self.read())\n\
        }\n\
    }\n\n\
    pub fn run() -> String {\n\
        let _ = Mode::ready();\n\
        let _ = Bits::new();\n\
        State::new(7u8).to_string()\n\
    }\n";

fn compile_and_run(out: &SplitOutput, module: bool) {
    let dir = tempfile::tempdir().unwrap();
    for file in &out.files {
        let path = dir.path().join(&file.path);
        fs::create_dir_all(path.parent().unwrap()).unwrap();
        fs::write(path, &file.contents).unwrap();
    }
    let entry = if module {
        "mod foo;\nfn main() { assert_eq!(foo::run(), \"7\"); }\n"
    } else {
        "include!(\"lib.rs\");\nfn main() { assert_eq!(run(), \"7\"); }\n"
    };
    fs::write(dir.path().join("main.rs"), entry).unwrap();
    let exe = dir
        .path()
        .join(format!("check{}", std::env::consts::EXE_SUFFIX));
    let compiled = Command::new("rustc")
        .current_dir(dir.path())
        .args(["--edition=2024", "main.rs", "-o"])
        .arg(&exe)
        .output()
        .unwrap();
    assert!(
        compiled.status.success(),
        "{}",
        String::from_utf8_lossy(&compiled.stderr)
    );
    assert!(Command::new(exe).status().unwrap().success());
}

#[test]
fn shared_generic_types_keep_comments_attributes_imports_and_private_fields() {
    let exploded = explode(SHARED).unwrap();
    for ceiling in [1, 10, 30, 1000] {
        assert_plan_partition(&exploded, ceiling);
    }
    for (module, stem) in [(true, "foo"), (false, "lib")] {
        let out = if module {
            split_mod(&exploded, 100, stem)
        } else {
            split_bin(&exploded, 100, stem)
        };
        assert!(out.still_oversized.is_empty(), "{:?}", out.still_oversized);
        let root = text_at(&out, &format!("{stem}.rs"));
        for expected in [
            "/// Shared across phases.\n#[derive(Default)]\npub struct State<T> {\n    values:",
            "pub enum Mode { Ready }",
            "union Bits { value: u32 }",
            "use std::collections::BTreeMap;",
        ] {
            assert!(root.contains(expected), "missing {expected}:\n{root}");
        }
        for file in out.files.iter().skip(1) {
            assert!(!file.contents.contains("struct State"));
            assert!(!file.contents.contains("enum Mode"));
            assert!(!file.contents.contains("union Bits"));
        }
        compile_and_run(&out, module);
    }
}

#[test]
fn oversized_impls_still_anchor_their_type_and_preserve_unmet_budgets() {
    let src = "struct State { value: usize }\n\nimpl State {\n\
        fn value(&self) -> usize {\n        self.value\n    }\n}\n";
    let exploded = explode(src).unwrap();
    let plan = plan_split(&exploded, 4);
    assert_eq!(plan.root_items.len(), 1);
    assert_eq!(plan.oversized.len(), 1);
    let out = split_mod(&exploded, 7, "foo");
    assert!(!out.still_oversized.is_empty());
    for chunk in &exploded.chunks {
        assert!(text_at(&out, "foo.rs").contains(chunk.text.trim()));
    }
}

#[test]
fn trait_only_and_foreign_path_impls_do_not_claim_a_local_type() {
    let src = "struct Local;\n\n\
        impl std::fmt::Display for Local {\n\
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n\
                write!(f, \"local\")\n\
            }\n\
        }\n\n\
        mod other { pub struct Local; }\n\n\
        impl other::Local { fn new() -> Self { Self } }\n";
    let exploded = explode(src).unwrap();
    let plan = plan_split(&exploded, 1000);
    assert!(plan.root_items.is_empty());
    assert_plan_partition(&exploded, 1000);
}

#[test]
fn recursively_split_modules_keep_types_in_their_own_parent() {
    let padding = "    let _ = 0;\n".repeat(30);
    let inner = SHARED.replace("pub fn run()", "pub fn execute()");
    let src = format!(
        "mod inner {{\n{inner}\nfn padding() {{\n{padding}}}\n}}\n\npub fn run() -> String {{ inner::execute() }}\n"
    );
    let out = split_mod(&explode(&src).unwrap(), 60, "foo");
    assert!(out.still_oversized.is_empty(), "{:?}", out.still_oversized);
    let owner = text_at(&out, "foo/inner/mod.rs");
    assert!(owner.contains("pub struct State<T> {\n    values:"));
    assert!(owner.contains("union Bits { value: u32 }"));
    compile_and_run(&out, true);
}

#[test]
fn parent_type_names_are_reserved_for_generated_modules() {
    let src = "#![allow(non_camel_case_types)]\n\n\
        struct worker;\n\nstruct worker_;\n\n\
        // ---- Worker ----\n\n\
        impl worker { fn value() -> u8 { 7 } }\n\n\
        impl worker_ { fn value() -> u8 { worker::value() } }\n\n\
        pub fn run() -> String { worker_::value().to_string() }\n";
    let out = split_mod(&explode(src).unwrap(), 100, "foo");
    assert!(text_at(&out, "foo.rs").contains("mod worker__;"));
    compile_and_run(&out, true);
}

#[test]
fn many_shared_types_report_an_oversized_parent_without_moving_or_losing_them() {
    let src: String = (0..20)
        .map(|i| {
            format!(
                "/// Shared owner {i}.\nstruct State{i} {{ value: u8 }}\n\nimpl State{i} {{}}\n\n"
            )
        })
        .collect();
    let exploded = explode(&src).unwrap();
    let out = split_mod(&exploded, 30, "mod");
    assert!(out.still_oversized.iter().any(|s| s.starts_with("mod.rs ")));
    for row in exploded.manifest.rows.iter().filter(|r| r.kind == "struct") {
        assert!(text_at(&out, "mod.rs").contains(&exploded.chunks[row.chunk_index].text));
    }
    assert_plan_partition(&exploded, 30);
}