cargo-split-modules 0.1.0

Split large Rust source files into one-item-per-file modules, preserving comments and the public API (verified by the compiler).
Documentation

cargo-split-modules

Split large Rust source files into one-item-per-file submodules — preserving comments and the public API, with the compiler as the safety net.

cargo install cargo-split-modules

cargo split-modules src/big.rs          # split one file
cargo split-modules --recursive src     # split every oversized file in a crate
cargo split-modules -n src/big.rs       # dry run: show what would happen

Turn this:

src/parser.rs        # 1500 lines: 12 structs, 30 fns, 20 impls

into this:

src/parser.rs        # module index: `mod` decls + `pub use` re-exports
src/parser/
    token.rs         # struct Token + its impls
    lexer.rs         # struct Lexer + its impls
    parse_expr.rs    # fn parse_expr
    ...

…and your crate still compiles and passes its tests, unchanged.

Why it's safe

Most "move code around" tools risk breaking your build. This one is built so it cannot leave your project in a broken state:

  1. The public API is preserved by construction. Each item moves into a child file, and the parent module re-exports it at its original visibility (pub use child::Foo;, pub(crate) use …, private use …). Every path anywhere in your project that referenced crate::parser::Token still resolves — no call sites are rewritten.

  2. Children see everything via use super::*;. All the original use imports stay in the parent, and child modules glob-import them along with their siblings. No import analysis, no guessing.

  3. Member visibility is widened safely. Moving a struct deeper would hide its private fields from sibling modules that relied on the old nesting, so private members are widened to pub(crate) — a superset of any in-crate audience, which can never break compiling code and never changes the external API.

  4. Module-relative paths are rewritten with scope awareness. super::Xsuper::super::X and self::Xsuper::X, but only at the item's own module depth (paths inside nested mod {} blocks are left alone).

  5. The compiler verifies every split. After writing files, cargo split-modules runs cargo check. If anything fails to compile, it rolls back the entire split, restoring the original file byte-for-byte and removing generated files. You either get a working split or no change at all.

This has been validated by splitting real crates (e.g. semver, bytes) end to end and confirming their full test suites still pass.

What gets preserved

  • Doc-comments (///, //!) and #[derive]/attribute lines — they're part of each item's span and move with it.
  • Plain // comments directly above an item, and trailing same-line comments.
  • #[cfg(...)] attributes — replicated onto the generated re-export.
  • Generics, unsafe, async, lifetimes, where clauses — the item's source text is sliced verbatim, never reformatted away.

How items are grouped

One file per item, named after it (snake_case):

Item Goes to
struct / enum / union / type / trait name.rs
free fn name.rs
const / static name.rs
impl Foo / impl Trait for Foo co-located in foo.rs (with Foo)

A same-named const, type alias, and struct merge into one file. impl blocks for an external/complex self type land in impls.rs.

Things that stay in the parent: use, mod, extern crate, macro_rules!, anonymous (const _) and _-prefixed side-effect items.

File layout

  • foo.rs → a sibling foo/ directory is created and foo.rs becomes the module index.
  • lib.rs / main.rs / mod.rs → generated files go in the same directory (these already own a directory module).

Options

cargo split-modules <PATH> [OPTIONS]

  PATH                 A .rs file to split, or a directory/crate to process recursively.

  -r, --recursive      Process a directory recursively (implied when PATH is a directory).
                       Splits every file that would yield 2+ module files.
  -n, --dry-run        Show what would happen without writing anything.
      --no-verify      Skip the cargo check + rollback safety step (faster, not advised).
      --no-fmt         Don't run rustfmt on generated files.
      --min-groups N   Minimum number of resulting module files for a split (default 2).

Known limitations (handled by safe rollback)

A file is safely skipped (rolled back, never broken) when a split would not compile — in practice this means paths hidden inside macro token streams (some_macro!(super::X)), or other constructs the AST can't see. You lose nothing: the file is left exactly as it was, and the tool tells you which files it skipped.

License

Licensed under either of Apache-2.0 or MIT at your option.