rust-split 0.2.2

A parser-based god-file splitter for Rust source.
Documentation
use std::collections::BTreeSet;

use super::*;

// ----- issue 5: include! paths resolve against the containing file --------

#[test]
fn include_paths_in_moved_bodies_are_rebased_one_directory_deeper() {
    let src = r#"pub fn doc() -> &'static str {
    include_str!("../docs/a.md")
}

pub fn blob() -> &'static [u8] {
    include_bytes!("fixtures/b.bin")
}

pub fn built() -> &'static str {
    include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/x"))
}

pub fn rooted() -> &'static str {
    include_str!("/etc/x")
}
"#;
    let exploded = explode(src).unwrap();
    let out = split_mod(&exploded, 10_000, "foo");

    let moved: String = out
        .files
        .iter()
        .filter(|f| f.path != "foo.rs")
        .map(|f| f.contents.as_str())
        .collect();
    assert!(
        moved.contains(r#"include_str!("../../docs/a.md")"#),
        "a relative include! path gains one `../`:\n{moved}"
    );
    assert!(
        moved.contains(r#"include_bytes!("../fixtures/b.bin")"#),
        "a bare relative path is prefixed too:\n{moved}"
    );
    assert!(
        moved.contains(r#"include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/x"))"#),
        "a non-literal argument is byte-unchanged:\n{moved}"
    );
    assert!(
        moved.contains(r#"include_str!("/etc/x")"#),
        "an absolute path is byte-unchanged:\n{moved}"
    );
}

#[test]
fn include_paths_are_untouched_when_the_file_stays_in_its_directory() {
    // A crate root's parts are siblings of the root file, so no body moved
    // between directories and no include! path may change.
    let src = "fn worker() -> &'static str {\n    include_str!(\"../docs/w.md\")\n}\n\n\
               fn main() {\n    let _ = include_str!(\"../docs/m.md\");\n    \
               let _ = worker();\n}\n";
    let exploded = explode(src).unwrap();
    let out = split_bin(&exploded, 10_000, "main");

    for f in &out.files {
        assert!(
            !f.contents.contains("../../docs/"),
            "{} stayed in the source directory:\n{}",
            f.path,
            f.contents
        );
    }
    let root = out.files.iter().find(|f| f.path == "main.rs").unwrap();
    assert!(root.contents.contains(r#"include_str!("../docs/m.md")"#));
}

// ----- issue 4b: relative paths *inside* a moved body move with it --------

#[test]
fn review_body_paths_preserve_opaque_macro_and_attribute_tokens() {
    let src = r#"#[metadata(super::Thing)]
pub fn probe() -> super::Thing {
    macro_rules! data { () => { self::Thing }; }
    let _ = stringify!(super::Thing);
    let _ = stringify! { self::Thing };
    let _ = custom!(super::Thing, (self::Thing), stringify!(super::Thing));
    let _ = super::factory!(self::Thing);
    <super::Thing as self::Trait>::make()
}
"#;
    let expected = src
        .replace("-> super::Thing", "-> super::super::Thing")
        .replace("super::factory!", "super::super::factory!")
        .replace(
            "<super::Thing as self::Trait>",
            "<super::super::Thing as super::Trait>",
        );
    assert_eq!(rebase_body_paths(src, Rebase::Nested), expected);
    assert_eq!(rebase_body_paths(src, Rebase::Keep), src);
}

#[test]
fn review_standard_expression_macro_arguments_rebase_paths() {
    let src = r#"fn probe() {
    assert_eq!(super::Thing::value(), self::value(), "{}", super::message());
    let _ = format!("{}", super::Thing::value());
    let _ = custom!(super::Thing, self::Thing);
    let _ = custom::assert_eq!(super::Thing, self::Thing);
    let _ = stringify!(super::Thing);
}
"#;
    let expected = src
        .replace("super::Thing::value()", "super::super::Thing::value()")
        .replace("self::value()", "super::value()")
        .replace("super::message()", "super::super::message()");
    assert_eq!(rebase_body_paths(src, Rebase::Nested), expected);
}

#[test]
fn review_renamed_use_anchors_rebase_in_headers_and_bodies() {
    for (source, nested, root) in [
        (
            "super as parent",
            "super::super as parent",
            "super as parent",
        ),
        ("self as current", "super as current", "crate as current"),
        (
            "{super as parent, self as current, super::{self as p, Thing}}",
            "{super::super as parent, super as current, super::super::{self as p, Thing}}",
            "{super as parent, crate as current, super::{self as p, Thing}}",
        ),
        ("crate as root", "crate as root", "crate as root"),
    ] {
        for (rebase, expected) in [
            (Rebase::Nested, nested),
            (Rebase::CrateRoot, root),
            (Rebase::Keep, source),
        ] {
            let import = format!("// keep this comment\n#[allow(unused_imports)]\nuse {source};\n");
            let expected = import.replace(source, expected);
            assert_eq!(rebase_import(&import, rebase, &BTreeSet::new()), expected);
            assert_eq!(
                rebase_body_paths(&format!("fn probe() {{\n{import}}}"), rebase),
                format!("fn probe() {{\n{expected}}}"),
            );
        }
    }
}

#[test]
fn review_include_literals_accept_one_optional_trailing_comma() {
    for name in PATH_MACROS {
        for literal in [r#""docs/file""#, r##"r#"docs/file"#"##] {
            let src = format!("fn probe() {{ {name}!({literal} /* keep */,); }}");
            assert_eq!(
                rebase_includes(&src, 2, "foo/part.rs"),
                src.replace("docs/file", "../../docs/file"),
            );
            for suffix in [",,", ", other", " + other"] {
                let src = format!("fn probe() {{ {name}!({literal}{suffix}); }}");
                assert_eq!(rebase_includes(&src, 1, "foo/part.rs"), src);
            }
        }
    }
}

#[test]
fn review_include_absolute_windows_paths_are_host_independent() {
    for name in PATH_MACROS {
        for literal in [
            r#""C:\\docs\\file""#,
            r#""z:/docs/file""#,
            r#"r"C:\docs\file""#,
            r#""\\\\server\\share\\file""#,
            r#"r"\\server\share\file""#,
            r#"r"\\?\C:\docs\file""#,
            r#""//server/share/file""#,
            r#""/docs/file""#,
        ] {
            let src = format!("fn probe() {{ {name}!({literal}); }}");
            assert_eq!(rebase_includes(&src, 1, "foo/part.rs"), src);
        }
        let src = format!("fn probe() {{ {name}!(\"C:relative\"); }}");
        assert_eq!(
            rebase_includes(&src, 1, "foo/part.rs"),
            src.replace("C:relative", "../C:relative"),
        );
    }
}

#[test]
fn moved_body_paths_gain_one_super_level() {
    // The header is not the only place a relative path hides: expression and
    // type paths, and a function-local `use`, travel inside the item.
    let src = r#"pub fn probe() -> i32 {
    use super::alpha::Beta;
    let _ = Beta;
    let _ = super::filter::parse("x");
    let _ = super::super::grandparent::thing();
    let _ = self::local::helper();
    let _: super::sibling::Kind = super::sibling::make();
    let _ = super::sibling::Record {
        meta: super::meta::build("req"),
    };
    crate::top::value()
}
"#;
    let exploded = explode(src).unwrap();
    let out = split_mod(&exploded, 10_000, "foo");

    let part = out.files.iter().find(|f| f.path == "foo/probe.rs").unwrap();
    for expected in [
        "use super::super::alpha::Beta;",
        r#"super::super::filter::parse("x")"#,
        // Only the leading segment moves: two levels become three, not four.
        "super::super::super::grandparent::thing()",
        "super::local::helper()",
        // A lone `:` — a type annotation, a struct field init — is not a
        // path separator, so the segment after it still starts a path.
        "let _: super::super::sibling::Kind = super::super::sibling::make();",
        "meta: super::super::meta::build(\"req\"),",
        "crate::top::value()",
    ] {
        assert!(
            part.contents.contains(expected),
            "moved body must carry `{expected}`:\n{}",
            part.contents
        );
    }
}

#[test]
fn moved_body_leaves_self_receivers_and_use_groups_alone() {
    // `self` is only a module anchor when a `::` follows it: a receiver, a
    // `use …::{self, …}` group member and a `pub(super)` restriction are all
    // something else. (Re-anchoring a `pub(super)` restriction — which does
    // narrow when the item moves — is out of scope here.)
    let src = r#"pub struct Widget {
    pub n: i32,
}

impl Widget {
    pub fn get(&self) -> i32 {
        use std::collections::{self, HashMap};
        let _: Option<HashMap<u8, u8>> = None;
        let _ = collections::BTreeMap::<u8, u8>::new();
        let _ = Self::make();
        self.n
    }

    pub fn make() -> Self {
        Self { n: 1 }
    }
}

pub(super) fn gated() -> i32 {
    1
}
"#;
    let exploded = explode(src).unwrap();
    let out = split_mod(&exploded, 10_000, "foo");
    let moved: String = out
        .files
        .iter()
        .filter(|f| f.path != "foo.rs")
        .map(|f| f.contents.as_str())
        .collect();

    for expected in [
        "use std::collections::{self, HashMap};",
        "Self::make()",
        "self.n",
        "pub(super) fn gated",
    ] {
        assert!(
            moved.contains(expected),
            "`{expected}` is not a module anchor and must not move:\n{moved}"
        );
    }
    assert!(
        !moved.contains("pub(super::super)"),
        "a `pub(super)` restriction must never be mangled:\n{moved}"
    );
}

#[test]
fn moved_body_paths_below_a_crate_root_rewrite_self_to_crate() {
    let src = "pub fn entry() -> i32 {\n    let _ = self::helpers::setup();\n    \
               crate::other::value()\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("crate::helpers::setup()"),
        "`self::` below a crate root is `crate::`:\n{}",
        part.contents
    );
    assert!(part.contents.contains("crate::other::value()"));
}

#[test]
fn explode_never_rewrites_include_paths() {
    // Re-basing belongs to `split`; `explode` stays byte-exact.
    let src = "pub fn doc() -> &'static str {\n    include_str!(\"../docs/a.md\")\n}\n";
    let exploded = explode(src).unwrap();
    let joined: String = exploded.chunks.iter().map(|c| c.text.as_str()).collect();
    assert_eq!(joined, src, "explode must stay lossless");
}