rust-split 0.2.2

A parser-based god-file splitter for Rust source.
Documentation
use syn::spanned::Spanned;

use super::*;

/// Raise a private moved item — and its struct fields / inherent impl members,
/// which may be referenced across the new module boundaries — to `vis`
/// (`pub(super)` below a nested module, `pub(crate)` below a crate root), so
/// the original module's re-exports and the sibling parts can see them.
/// Span-driven (parse the chunk, insert at exact offsets), so it is
/// attribute-safe and handles `fn` modifiers, multi-line attrs, etc. Items
/// that already carry a visibility are left alone. `vis` is exactly the scope
/// that could see the private item before the move, so nothing outside it gains
/// access.
pub(super) fn bump_visibility(chunk_text: &str, vis: &str) -> String {
    let Ok(item) = syn::parse_str::<syn::Item>(chunk_text) else {
        return chunk_text.to_owned();
    };
    let mut offsets = Vec::new();
    collect_vis_offsets(&item, &mut offsets);
    apply_inserts(chunk_text, &offsets, vis)
}

pub(super) fn is_inherited(vis: &syn::Visibility) -> bool {
    matches!(vis, syn::Visibility::Inherited)
}

fn start<T: Spanned>(node: &T) -> usize {
    node.span().byte_range().start
}

/// Byte offset where a function's visibility belongs: before any
/// `const`/`async`/`unsafe`/`extern` modifier, else before `fn`.
fn fn_sig_start(sig: &syn::Signature) -> usize {
    let mut min = start(&sig.fn_token);
    if let Some(t) = &sig.constness {
        min = min.min(start(t));
    }
    if let Some(t) = &sig.asyncness {
        min = min.min(start(t));
    }
    if let Some(t) = &sig.unsafety {
        min = min.min(start(t));
    }
    if let Some(abi) = &sig.abi {
        min = min.min(start(&abi.extern_token));
    }
    min
}

fn field_offsets(fields: &syn::Fields, offsets: &mut Vec<usize>) {
    for field in fields {
        if is_inherited(&field.vis) {
            match &field.ident {
                Some(ident) => offsets.push(start(ident)),
                None => offsets.push(start(&field.ty)),
            }
        }
    }
}

fn collect_vis_offsets(item: &syn::Item, offsets: &mut Vec<usize>) {
    use syn::Item;
    match item {
        Item::Fn(f) => {
            if is_inherited(&f.vis) {
                offsets.push(fn_sig_start(&f.sig));
            }
        }
        Item::Struct(s) => {
            if is_inherited(&s.vis) {
                offsets.push(start(&s.struct_token));
            }
            field_offsets(&s.fields, offsets);
        }
        Item::Enum(e) => {
            if is_inherited(&e.vis) {
                offsets.push(start(&e.enum_token));
            }
        }
        Item::Union(u) => {
            if is_inherited(&u.vis) {
                offsets.push(start(&u.union_token));
            }
            field_offsets(&syn::Fields::Named(u.fields.clone()), offsets);
        }
        Item::Const(c) if is_inherited(&c.vis) => offsets.push(start(&c.const_token)),
        Item::Static(s) if is_inherited(&s.vis) => offsets.push(start(&s.static_token)),
        Item::Type(t) if is_inherited(&t.vis) => offsets.push(start(&t.type_token)),
        Item::Trait(t) if is_inherited(&t.vis) => offsets.push(start(&t.trait_token)),
        Item::TraitAlias(t) if is_inherited(&t.vis) => offsets.push(start(&t.trait_token)),
        // Only inherent impls (`impl Type`) allow member visibility; trait-impl
        // members (`impl Trait for Type`) inherit the trait's and must not.
        Item::Impl(i) if i.trait_.is_none() => {
            for member in &i.items {
                match member {
                    syn::ImplItem::Fn(f) if is_inherited(&f.vis) => {
                        offsets.push(fn_sig_start(&f.sig))
                    }
                    syn::ImplItem::Const(c) if is_inherited(&c.vis) => {
                        offsets.push(start(&c.const_token))
                    }
                    syn::ImplItem::Type(t) if is_inherited(&t.vis) => {
                        offsets.push(start(&t.type_token))
                    }
                    _ => {}
                }
            }
        }
        _ => {}
    }
}

fn apply_inserts(text: &str, offsets: &[usize], vis: &str) -> String {
    let mut offsets: Vec<usize> = offsets.to_vec();
    offsets.sort_unstable();
    offsets.dedup();
    let mut out = String::with_capacity(text.len() + offsets.len() * vis.len());
    let mut prev = 0;
    for &offset in &offsets {
        out.push_str(&text[prev..offset]);
        out.push_str(vis);
        prev = offset;
    }
    out.push_str(&text[prev..]);
    out
}

/// The glob re-export line for a module given the widest visibility of its
/// items — or `None` when it exposes nothing nameable (re-exporting an
/// `impl`-only module would only warn). The line is never wider than the items
/// behind it: rustc warns that a glob "doesn't reexport anything" when no item
/// is public enough for it.
pub(super) fn reexport_line(vis: ExportVis, module: &str, topology: Topology) -> Option<String> {
    match (vis, topology) {
        (ExportVis::None, _) => None,
        // Private items of a nested module, raised to `pub(super)`: a private
        // glob in the original module reaches its children's `use super::*;`.
        (ExportVis::Private, Topology::Mod) => Some(format!("use {module}::*;\n")),
        // A crate root's private items were visible crate-wide.
        (ExportVis::Private, Topology::Bin) | (ExportVis::Crate, _) => {
            Some(format!("pub(crate) use {module}::*;\n"))
        }
        (ExportVis::Pub, _) => Some(format!("pub use {module}::*;\n")),
    }
}

/// What a moved item contributes to its module's glob re-export. Ordered so the
/// widest contribution across a module's items picks the re-export visibility.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(super) enum ExportVis {
    /// No nameable export (a bare `impl`, a macro invocation, parse failure).
    None,
    /// A name only the original module could see: a private item (raised to
    /// the topology's sibling visibility), or a `pub(self)`/`pub(super)`/
    /// `pub(in self|super…)` restriction relative to it.
    Private,
    /// A crate-visible name: `pub(crate)` or `pub(in crate…)`.
    Crate,
    /// A fully public name — part of the crate/module's public surface.
    Pub,
}

/// Classify one chunk's contribution to its module's glob re-export, mirroring
/// `bump_visibility`: items it raises count as module-private; `mod`/`use` are
/// not raised, so a private one is invisible to the re-export.
pub(super) fn item_export_vis(chunk_text: &str) -> ExportVis {
    use syn::{Item, Visibility};
    let Ok(item) = syn::parse_str::<syn::Item>(chunk_text) else {
        // Unparseable: bump_visibility also left it private, so a glob re-export
        // could not see it either — contribute nothing.
        return ExportVis::None;
    };
    let (vis, bumped) = match &item {
        Item::Fn(f) => (&f.vis, true),
        Item::Struct(s) => (&s.vis, true),
        Item::Enum(e) => (&e.vis, true),
        Item::Const(c) => (&c.vis, true),
        Item::Static(s) => (&s.vis, true),
        Item::Trait(t) => (&t.vis, true),
        Item::TraitAlias(t) => (&t.vis, true),
        Item::Type(t) => (&t.vis, true),
        Item::Union(u) => (&u.vis, true),
        Item::Mod(m) => (&m.vis, false),
        Item::Use(u) => (&u.vis, false),
        // impl / macro / foreign_mod / extern_crate / verbatim: nothing nameable.
        _ => return ExportVis::None,
    };
    match vis {
        Visibility::Public(_) => ExportVis::Pub,
        Visibility::Restricted(restricted)
            if restricted
                .path
                .segments
                .first()
                .is_some_and(|segment| segment.ident == "crate") =>
        {
            ExportVis::Crate
        }
        Visibility::Restricted(_) => ExportVis::Private,
        Visibility::Inherited if bumped => ExportVis::Private,
        Visibility::Inherited => ExportVis::None,
    }
}