rust-split 0.2.2

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

// ----- issue 3a: copy only the imports each file actually references -------

#[test]
fn parts_copy_only_the_imports_they_reference() {
    // alpha names `Display`; bravo names nothing from the header. Each part
    // gets only the imports it references.
    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";
    let exploded = explode(src).unwrap();
    let out = split_mod(&exploded, 10_000, "mod");

    let alpha = out
        .files
        .iter()
        .find(|f| f.contents.contains("fn alpha"))
        .unwrap();
    let bravo = out
        .files
        .iter()
        .find(|f| f.contents.contains("fn bravo"))
        .unwrap();
    assert!(
        alpha.contents.contains("use std::fmt::Display;"),
        "alpha references Display:\n{}",
        alpha.contents
    );
    assert!(
        !alpha.contents.contains("HashMap"),
        "alpha never names HashMap -> import dropped:\n{}",
        alpha.contents
    );
    assert!(
        !bravo.contents.contains("use std::fmt::Display;"),
        "bravo references nothing from the header:\n{}",
        bravo.contents
    );
    assert!(!bravo.contents.contains("HashMap"));
}

#[test]
fn globs_and_anonymous_trait_imports_are_always_copied() {
    // `use ...::*` and `use Trait as _` expose no name to match, so every
    // part keeps them — the safe choice for a trait used only via methods.
    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";
    let exploded = explode(src).unwrap();
    let out = split_mod(&exploded, 10_000, "mod");
    for f in &out.files {
        if f.path == "mod.rs" {
            continue;
        }
        assert!(
            f.contents.contains("use std::fmt::Write as _;"),
            "{} must keep the `as _` trait import:\n{}",
            f.path,
            f.contents
        );
        assert!(
            f.contents.contains("use std::prelude::v1::*;"),
            "{} must keep the glob import:\n{}",
            f.path,
            f.contents
        );
    }
}

#[test]
fn item_less_root_keeps_no_imports() {
    // A module root whose items all moved out is just decls + re-exports; it
    // needs no imports, while the part that uses one keeps it.
    let src =
        "use std::collections::HashMap;\n\npub fn solo(m: HashMap<u8, u8>) {\n    let _ = m;\n}\n";
    let exploded = explode(src).unwrap();
    let out = split_mod(&exploded, 10_000, "mod");

    let root = out.files.iter().find(|f| f.path == "mod.rs").unwrap();
    assert!(
        !root.contents.contains("use std::collections::HashMap;"),
        "item-less root drops imports it doesn't use:\n{}",
        root.contents
    );
    let part = out.files.iter().find(|f| f.path != "mod.rs").unwrap();
    assert!(
        part.contents.contains("use std::collections::HashMap;"),
        "the part that uses HashMap keeps it"
    );
}

#[test]
fn root_keeps_imports_an_extracted_mod_body_references() {
    // The extracted tests mod reaches the root's imports via
    // `use super::*;` — dropping HashMap from the root would break it.
    let src = "use std::collections::HashMap;\n\n\
               pub fn alpha() -> i32 {\n    1\n}\n\n\
               #[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    \
               fn smoke() {\n        let _m: HashMap<u8, u8> = HashMap::new();\n        \
               assert_eq!(alpha(), 1);\n    }\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("use std::collections::HashMap;"),
        "root must keep imports the extracted body names:\n{}",
        root.contents
    );
}

#[test]
fn bin_root_keeps_only_imports_main_references() {
    // `main` names `exit` but not `HashMap` (the moved `worker` does), so the
    // root keeps `exit` and drops `HashMap`.
    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";
    let exploded = explode(src).unwrap();
    let out = split_bin(&exploded, 10_000, "main");

    let root = out.files.iter().find(|f| f.path == "main.rs").unwrap();
    assert!(
        root.contents.contains("use std::process::exit;"),
        "main references exit:\n{}",
        root.contents
    );
    assert!(
        !root.contents.contains("HashMap"),
        "main never names HashMap (worker does) -> dropped from root:\n{}",
        root.contents
    );
}

// ----- issue 4: a part sits one module level below the file it came from --

#[test]
fn nested_module_part_imports_gain_one_super_level() {
    // A part of `foo.rs` is module `foo::<part>`, one level below `foo`, so a
    // copied `use super::X` would name `foo` instead of `foo`'s parent and a
    // copied `use self::X` would name the part itself.
    let src = r#"use super::sibling::Thing;
use super::{alpha, beta::Gamma};
use self::local::X;
use super::*;
use crate::top::Absolute;
use std::fmt::Display;

pub fn one(t: Thing, g: Gamma, x: X, d: &dyn Display) -> i32 {
    let _: Option<Absolute> = None;
    let _ = (t, g, x, d);
    alpha()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn smoke() {
        let _ = Thing;
    }
}
"#;
    let exploded = explode(src).unwrap();
    let out = split_mod(&exploded, 10_000, "foo");

    let part = out.files.iter().find(|f| f.path == "foo/one.rs").unwrap();
    for expected in [
        "use super::super::sibling::Thing;",
        "use super::super::{alpha, beta::Gamma};",
        "use super::local::X;",
        "use super::super::*;",
        // `crate::` and external paths are absolute — untouched.
        "use crate::top::Absolute;",
        "use std::fmt::Display;",
    ] {
        assert!(
            part.contents.contains(expected),
            "part must carry `{expected}`:\n{}",
            part.contents
        );
    }
    // The tool's own sibling glob is still there, exactly once, and is not
    // confused with the header's rewritten `use super::*;`.
    assert_eq!(
        part.contents
            .lines()
            .filter(|l| l.trim() == "use super::*;")
            .count(),
        1,
        "exactly one tool-added sibling glob:\n{}",
        part.contents
    );

    // The root is still the original module — its header must not move.
    let root = out.files.iter().find(|f| f.path == "foo.rs").unwrap();
    assert!(
        root.contents.contains("use super::sibling::Thing;"),
        "root header stays byte-unchanged:\n{}",
        root.contents
    );
    assert!(
        !root.contents.contains("super::super"),
        "the root did not move — nothing to re-anchor:\n{}",
        root.contents
    );
}

#[test]
fn crate_root_part_imports_rewrite_self_to_crate() {
    // Below a crate root `self::X` names the part, not the crate root;
    // `super::` cannot occur at a crate root, so nothing else changes.
    let src = "use self::helpers::Helper;\nuse std::fmt::Display;\n\n\
               pub fn api(h: Helper, d: &dyn Display) {\n    let _ = (h, d);\n}\n";
    let exploded = explode(src).unwrap();
    let out = split_bin(&exploded, 10_000, "lib");

    let part = out.files.iter().find(|f| f.path != "lib.rs").unwrap();
    assert!(
        part.contents.contains("use crate::helpers::Helper;"),
        "`self::` re-anchors to `crate::` below a crate root:\n{}",
        part.contents
    );
    assert!(part.contents.contains("use std::fmt::Display;"));
}

#[test]
fn nested_split_group_headers_gain_one_super_level() {
    // `tests/gNN.rs` is module `tests::gNN`, one below the extracted `tests`
    // body the group header was copied from.
    let mut inner = String::from(
        "    use super::outer::Thing;\n    use self::inner::Y;\n\n\
         \x20   fn helper() -> i32 {\n        1\n    }\n",
    );
    for i in 0..30 {
        inner.push_str(&format!(
            "\n    #[test]\n    fn t{i}() {{\n        \
             let _: Option<Thing> = None;\n        let _: Option<Y> = None;\n        \
             assert_eq!(helper(), 1);\n    }}\n"
        ));
    }
    let src = format!(
        "pub fn helper() -> i32 {{\n    1\n}}\n\nfn main() {{}}\n\n\
         mod tests {{\n{inner}}}\n"
    );
    let exploded = explode(&src).unwrap();
    let out = split_bin(&exploded, 60, "main");

    let group = out.files.iter().find(|f| f.path == "tests/g00.rs").unwrap();
    assert!(
        group.contents.contains("use super::super::outer::Thing;"),
        "group header `super::` gains a level:\n{}",
        group.contents
    );
    assert!(
        group.contents.contains("use super::inner::Y;"),
        "group header `self::` becomes `super::`:\n{}",
        group.contents
    );
}

#[test]
fn extracted_inline_mod_keeps_its_own_super_paths() {
    // An inline `mod tests` extracted out of `foo.rs` lands at `foo/tests.rs`
    // — still module `foo::tests`, the level it already had — so its own
    // `use super::helper;` still names `foo` and must NOT be re-anchored.
    let src = "pub fn helper() -> i32 {\n    1\n}\n\n\
               mod tests {\n    use super::helper;\n\n    #[test]\n    \
               fn smoke() {\n        assert_eq!(helper(), 1);\n    }\n}\n";
    let exploded = explode(src).unwrap();
    let out = split_mod(&exploded, 10_000, "foo");

    let tests = out.files.iter().find(|f| f.path == "foo/tests.rs").unwrap();
    assert!(
        tests.contents.contains("use super::helper;"),
        "an extracted mod keeps its own module level:\n{}",
        tests.contents
    );
    assert!(
        !tests.contents.contains("super::super"),
        "the extracted body did not change module level:\n{}",
        tests.contents
    );
}