# 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**.
```bash
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::X` →
`super::super::X` and `self::X` → `super::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):
| `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](LICENSE-APACHE) or [MIT](LICENSE-MIT) at your option.
[`semver`]: https://crates.io/crates/semver
[`bytes`]: https://crates.io/crates/bytes