rust-split 0.2.2

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

/// Extract an unconditional inline module using parser spans. Keep leading
/// comments, attributes and visibility on its declaration. Conditional modules
/// remain braced and inline: extracting them to a name-only file would merge
/// mutually exclusive bodies and leave a gate on an unbraced declaration.
/// Conservatively retain all `cfg_attr` modules, including nested predicates.
pub(super) fn extract_mod(chunk_text: &str) -> Option<(String, String)> {
    let Ok(syn::Item::Mod(item)) = syn::parse_str::<syn::Item>(chunk_text) else {
        return None;
    };
    if item
        .attrs
        .iter()
        .any(|attr| attr.path().is_ident("cfg") || attr.path().is_ident("cfg_attr"))
    {
        return None;
    }
    let (brace, _) = item.content.as_ref()?;
    let ident_end = item.ident.span().byte_range().end;
    let decl = format!("{};\n", &chunk_text[..ident_end]);

    let open_end = brace.span.open().byte_range().end;
    let close_start = brace.span.close().byte_range().start;
    let inner = &chunk_text[open_end..close_start];
    // Drop the newline that followed `{` and the closing brace's indentation.
    let inner = inner.strip_prefix('\n').unwrap_or(inner);
    let mut inner = inner.trim_end_matches([' ', '\t']).to_owned();
    if !inner.is_empty() && !inner.ends_with('\n') {
        inner.push('\n');
    }
    Some((decl, inner))
}

/// Recursively split an over-budget module's inner body into a `{name}/`
/// subdirectory: `{name}/mod.rs` forwards the enclosing module's items
/// (`pub(crate) use super::*`), keeps the body's own re-exports and `mod`
/// declarations verbatim, and re-exports the clusters as the top level does;
/// each `{name}/gNN.rs` holds a cohesive cluster, raises private items to
/// `pub(super)`, and reaches everything via `super::*`. A nested
/// unconditional module is extracted whole, recursing above the ceiling;
/// conditional modules stay braced and inline. Oversized leaves stay in
/// `{name}/mod.rs` with their original attributes, visibility and imports.
pub(super) fn split_nested_mod(
    name: &str,
    inner: &str,
    max_loc: usize,
) -> (Vec<OutputFile>, Vec<String>) {
    let Ok(exploded) = crate::explode(inner) else {
        return (
            vec![OutputFile {
                path: format!("{name}.rs"),
                contents: inner.to_owned(),
                loc: loc(inner),
            }],
            vec![format!("{name} (could not parse for nested split)")],
        );
    };
    let row = |i: usize| {
        exploded
            .manifest
            .rows
            .iter()
            .find(|r| r.chunk_index == i)
            .unwrap()
    };

    // Budget from a conservative header estimate (every use/preamble/extern
    // chunk), then plan once. The copied group header is the plan's header —
    // the module's own plain imports — minus `use super::*` (the parent glob,
    // which `{name}/mod.rs` forwards instead). Re-exports and `mod`
    // declarations are in `root_items`, kept in `{name}/mod.rs`; a nested
    // `mod x;` still resolves to `{name}/x.rs` after the move.
    let header_estimate: usize = exploded
        .manifest
        .rows
        .iter()
        .filter(|r| matches!(r.kind.as_str(), "use" | "preamble" | "extern_crate"))
        .map(|r| r.loc)
        .sum();
    let item_budget = max_loc
        .saturating_sub(header_estimate + PART_OVERHEAD)
        .max(1);
    let plan = plan_split(&exploded, item_budget);
    let is_super_glob = |t: &str| t.replace(' ', "").contains("usesuper::*;");
    // `{name}/gNN.rs` is a module below `{name}`, so the body's own imports are
    // re-anchored the same way a part's are.
    let local = local_path_roots(&exploded);
    let group_header: String = plan
        .header
        .iter()
        .filter(|&&i| row(i).kind != "preamble")
        .map(|&i| exploded.chunks[i].text.as_str())
        .filter(|t| !is_super_glob(t))
        .map(|t| rebase_import(t, Rebase::Nested, &local))
        .collect();

    let mut files = Vec::new();
    let mut decls = String::new();
    let mut reexports = String::new();
    let mut still = Vec::new();
    let mut reserved = std::collections::BTreeSet::new();
    for &i in plan
        .root_items
        .iter()
        .chain(&plan.mods)
        .chain(&plan.header)
        .chain(plan.oversized.iter().map(|o| &o.chunk_index))
    {
        let r = row(i);
        if matches!(
            r.kind.as_str(),
            "mod" | "struct" | "enum" | "union" | "type" | "trait" | "trait_alias"
        ) {
            reserved.insert(r.name.clone());
        } else if let Some(names) = import_provided_names(&exploded.chunks[i].text) {
            reserved.extend(names);
        }
    }
    for (idx, part) in plan.parts.iter().enumerate() {
        let mut module = format!("g{idx:02}");
        while !reserved.insert(module.clone()) {
            module.push('_');
        }
        let mut body = String::new();
        let mut export_vis = ExportVis::None;
        for &ci in &part.chunk_indices {
            let text = exploded.chunks[ci].text.as_str();
            export_vis = export_vis.max(item_export_vis(text));
            // The groups are children of `{name}`, never of a crate root.
            body.push_str(&rebase_body_paths(
                &bump_visibility(text, NESTED_SIBLING_VIS),
                Rebase::Nested,
            ));
        }
        let contents = format!("{group_header}\nuse super::*;\n\n{body}");
        files.push(OutputFile {
            path: format!("{name}/{module}.rs"),
            loc: loc(&contents),
            contents,
        });
        decls.push_str(&format!("mod {module};\n"));
        if let Some(line) = reexport_line(export_vis, &module, Topology::Mod) {
            reexports.push_str(&line);
        }
    }
    for &mod_index in &plan.mods {
        let chunk = exploded.chunks[mod_index].text.as_str();
        let mod_name = row(mod_index).name.as_str();
        let Some((decl, mod_inner)) = extract_mod(chunk) else {
            decls.push_str(chunk);
            continue;
        };
        decls.push_str(&decl);
        let mod_loc = loc(&mod_inner);
        if mod_loc < max_loc {
            files.push(OutputFile {
                path: format!("{name}/{mod_name}.rs"),
                contents: mod_inner,
                loc: mod_loc,
            });
        } else {
            let (nested, nested_still) = split_nested_mod(mod_name, &mod_inner, max_loc);
            files.extend(nested.into_iter().map(|mut f| {
                f.path = format!("{name}/{}", f.path);
                f
            }));
            still.extend(
                nested_still
                    .into_iter()
                    .map(|message| format!("{name}::{message}")),
            );
        }
    }
    let mut oversized_kept = String::new();
    for over in &plan.oversized {
        oversized_kept.push_str(&exploded.chunks[over.chunk_index].text);
        still.push(format!(
            "{name}::{} ({} LOC {}, leaf needs manual extraction)",
            over.name, over.loc, over.kind
        ));
    }

    let root_kept: String = plan
        .root_items
        .iter()
        .map(|&i| exploded.chunks[i].text.as_str())
        .collect();
    // Keep the original preamble and imports in the owning module as well:
    // retained leaves and conditional bodies still resolve names here.
    let root_header: String = plan
        .header
        .iter()
        .map(|&i| exploded.chunks[i].text.as_str())
        .collect();
    let modrs = format!(
        "{root_header}\npub(crate) use super::*;\n\n{root_kept}{decls}\n{reexports}{oversized_kept}"
    );
    files.insert(
        0,
        OutputFile {
            path: format!("{name}/mod.rs"),
            loc: loc(&modrs),
            contents: modrs,
        },
    );
    (files, still)
}