rust-split 0.2.2

A parser-based god-file splitter for Rust source.
Documentation
# rust-split

`rust-split` is a parser-based helper for breaking up large Rust source files.
It is meant for the mechanical part of a split: find top-level items, preserve
their attached comments and attributes, group related items, and write a module
layout that stays under a requested LOC ceiling.

It does not decide when a file should be split. Use your project's own rule for
that decision, then run this tool when you want the mechanical carve-out.

## Suggested Split Policy

`rust-split` ships with a suggested policy for teams and agents that do not
already have one. Treat it as operating guidance, not as behavior enforced by
the CLI:

- Around 1,000 LOC, review the file's cohesion and decide whether it should be
  split. This is a soft review trigger, not an automatic split at line 1,001.
- When a split is warranted, target responsibility owners below 500 LOC.
- 500 LOC is a ceiling, not a packing target. Prefer cohesive smaller files over
  filling files toward 499 lines.
- Split earlier when a smaller file becomes a dumping ground for unrelated
  concepts.
- Prefer planning ownership boundaries before implementation so large-file
  churn does not become a separate refactor.

See [SplitPolicy.md](docs/SplitPolicy.md) for the full suggested rule.

## For agents

Reach for this instead of hand-editing. Moving items by cut-and-paste is O(n²) in
edits and silently orphans doc-comments and `#[attrs]`; `explode` + `split` is
O(n) and verifiable. (This tool decides *how*, never *when* — your repo's rules
own that.)

Loop:

1. `explode` to a temp dir, then **diff the concatenated chunks against the
   original and confirm it is byte-identical** before trusting the split.
2. Read `manifest.toml` and adjust grouping there — don't re-cluster by hand.
3. `split --out <tempdir>` (never in place first), then copy in deliberately.
4. Let the compiler enumerate the fallout: run the crate's build and fix the
   exact `use` / `pub(crate)` errors it reports. Don't predict visibility by
   reading.
5. Proven when the crate's existing tests stay green on a pure-move diff. **Do
   not run a formatter** — it destroys the pure-move diff; formatting is a
   separate pass.

`split` extracts an inline `#[cfg(test)] mod tests { … }` to its own file
itself, the gate traveling to the root declaration. When you hand-finish from
`explode` chunks instead, that is on you: keep the wrapper's attributes with the
moved module and declare it as a top-level `mod` in the destination file (a
file-module's submodule resolves to a subdir).

One thing the tool won't fix for you: registration blocks
(`#[starlark_module]`, framework macros) need re-wrapping into N blocks and
re-registering by hand.

## Workflow

The tool works in two passes. The first (`explode`) is lossless: concatenating
the generated chunk files in order must reproduce the original file exactly. The
second (`split`) rewrites the module graph, so it intentionally adds module
declarations, re-exports, imports, and some `pub(super)` / `pub(crate)`
visibility.

## Install

Install with Cargo:

```sh
cargo install rust-split
```

Install the latest release on macOS or Linux:

```sh
curl --proto '=https' --tlsv1.2 -LsSf \
  https://github.com/owebeeone/rust-split/releases/latest/download/rust-split-installer.sh | sh
```

Install the latest release on Windows PowerShell:

```powershell
powershell -ExecutionPolicy Bypass -c "irm https://github.com/owebeeone/rust-split/releases/latest/download/rust-split-installer.ps1 | iex"
```

The `latest` URLs point at the newest non-prerelease GitHub Release. If you want
a pinned install, replace `latest` with a concrete tag such as `v0.1.1`:

```text
https://github.com/owebeeone/rust-split/releases/download/v0.1.1/rust-split-installer.sh
```

Users who already have Rust can install from source:

```sh
cargo install --git https://github.com/owebeeone/rust-split
```

### Smoke Test Installers

Test the Unix installer without modifying `PATH`:

```sh
tmp="$(mktemp -d)"

curl --proto '=https' --tlsv1.2 -LsSf \
  https://github.com/owebeeone/rust-split/releases/latest/download/rust-split-installer.sh \
  -o "${tmp}/rust-split-installer.sh"

RUST_SPLIT_UNMANAGED_INSTALL="${tmp}/bin" \
RUST_SPLIT_NO_MODIFY_PATH=1 \
sh "${tmp}/rust-split-installer.sh"

"${tmp}/bin/rust-split" --version
"${tmp}/bin/rust-split" --help
```

Test the Windows installer without modifying `PATH`:

```powershell
$ErrorActionPreference = "Stop"

$tmp = Join-Path $env:TEMP "rust-split-test-$([guid]::NewGuid())"
New-Item -ItemType Directory -Force -Path $tmp | Out-Null

$installer = Join-Path $tmp "rust-split-installer.ps1"
Invoke-WebRequest `
  "https://github.com/owebeeone/rust-split/releases/latest/download/rust-split-installer.ps1" `
  -OutFile $installer

$env:RUST_SPLIT_UNMANAGED_INSTALL = Join-Path $tmp "bin"
$env:RUST_SPLIT_NO_MODIFY_PATH = "1"

Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
& $installer

$exe = Join-Path $env:RUST_SPLIT_UNMANAGED_INSTALL "rust-split.exe"
& $exe --version
& $exe --help
```

Release assets are checksummed and have GitHub artifact attestations. The
installers are convenience scripts; users who want stronger verification should
download the release asset, verify the attestation, compare the SHA-256 checksum,
and then install.

## Commands

Build a local development binary:

```sh
cargo build
./target/debug/rust-split --help
```

Or install the current checkout onto your Cargo bin path:

```sh
cargo install --path .
rust-split --help
```

While developing the tool, any `rust-split ...` command below can also be run as
`cargo run -- ...`.

Explode a file into chunks:

```sh
rust-split explode path/to/file.rs --out /tmp/file-chunks
```

This writes:

- `chunk-000.rs`, `chunk-001.rs`, ...
- `manifest.toml`

Split a binary crate root, such as `src/main.rs`:

```sh
rust-split split src/main.rs --max-loc 500 --out /tmp/split-main
```

Split a nested module file, such as `src/workspace_ops/mod.rs`:

```sh
rust-split split src/workspace_ops/mod.rs --module --max-loc 500 --out /tmp/split-workspace-ops
```

Omit `--out` to write in place next to the source file. For real work, prefer
using `--out` first, reviewing the generated layout, then copying the result
into the repo deliberately.

## How It Splits

`explode` parses the file with `syn` and records one manifest row per top-level
item. Each row includes:

- item name and kind
- byte range and line span
- LOC
- sibling identifier references as `adjacency_hint`

Comments between two items are divided at the first run of blank lines after
the earlier item: comment lines directly below an item stay with it, and
everything after the blank lines leads the next item. A section banner set off
by blank lines (`// ---- Linking ----`) therefore travels with the item below
it. Doc comments and attributes always belong to their item.

`split` treats plain `use`/`extern crate` imports and the file preamble as a
shared header. These chunk classes never enter clustering:

- **Local types with inherent impls** (`struct`, `enum`, `union`) stay verbatim
  in the parent module. Child implementation modules can access the parent's
  private fields, so the type and its fields need no visibility changes. An
  unqualified or `self::Type` impl identifies a local owner, including generic
  and oversized impls; trait-only impls and paths through other modules do not.
- **Owner-local impls** with explicit `pub(self)` or `pub(in self)` members
  stay in the parent, preserving their restrictions and comments verbatim.
- **Local macros needed by retained types or owner-local impls** stay with their
  syntactic macro consumers and helper macros in the parent, in source order. This preserves
  lexical resolution across macro redefinitions without expanding macros.
  Matching identifiers conservatively may retain additional declarations or
  inline modules; the resulting parent can remain above budget.
- **Re-exports** (`pub use ...`, and any visibility-qualified import) are the
  file's API surface and stay at the root verbatim — never dropped, never
  demoted to `pub(crate)`.
- **`mod name;` declarations** bind files relative to the root's directory and
  stay at the root verbatim.
- **Inline modules** (`mod name { ... }`, any size) are already module
  boundaries: each is extracted whole to its own file, with its attributes
  (`#[cfg(test)]` keeps gating the declaration), doc comments, and visibility
  traveling to the root's `mod name;`.

Section banners start responsibility boundaries lasting until the next banner;
even repeated titles start distinct sections. Reference edges never join items
across these boundaries, including indirectly through a shared helper. Within
each section (or throughout an unbannered file), remaining items are clustered
by sibling-reference adjacency. Keeping shared types in the parent removes
their reference edges, so independent impl blocks no longer cluster just
because they implement the same type. Unrelated items within a section still
stay separate; a banner does not force them into one file.

The LOC ceiling is a hard upper bound but **not a packing target**: a
transitively related group larger than half the ceiling is partitioned into
roughly equal cohesive parts
(strongest reference edges bond first, so cuts fall on the weakest edges),
leaning toward more, smaller files rather than one file grazing the ceiling.
Unrelated items are left separate rather than packed together just to reduce
file count.

A generated module is named after the first section banner heading one of its
items (`// ---- Syntax helpers ----` gives `syntax_helpers.rs`), else after its
largest named item. A name that would shadow a crate inside the module (`core`,
`std`, `alloc`, `proc_macro`, `test`), a keyword, or a name the root already
binds gets a `_` suffix (`core_.rs`).

The file preamble (inner `//!` docs and `#![...]` attributes) stays at the root.
Each generated file copies only the imports it references by name; `*` globs and
`as _` trait imports, which expose no name, are kept everywhere. Modules reach
their siblings through the generated root:

- binary roots use `use crate::*`
- nested module splits use `use super::*`

A copied import that starts at a module or enum declared in the split file
(`use model::*;` beside `mod model;`, `use Kind::*;` beside `enum Kind`) is
anchored explicitly — `use super::model::*;` in a nested split, `use
crate::model::*;` below a crate root — rather than resolving only through the
sibling glob.

Moved private items, fields of moved structs, and inherent impl members are
raised just far enough for the sibling modules and the root to see them:
`pub(super)` in a `--module` split and in the groups of a nested split,
`pub(crate)` at a crate
root, where the two are the same scope. Items that already carry a visibility
keep it.

The root re-exports each module with a single glob whose visibility matches the
module's widest item: `pub use` when it has a public item (preserving the crate's
public surface), `pub(crate) use` for crate-visible items, and for items that
were private either `pub(crate) use` (crate root) or a private `use` (nested
module, whose children still see it). A module that exposes no nameable item
(such as a bare `impl`) gets no re-export. A re-export is never wider than the
items behind it, which would draw rustc's "glob import doesn't reexport
anything" warning.

A plain `foo.rs` file module places its sub-modules in a `foo/` subdir — where
`mod bar;` resolves — so the layout compiles without a manual move; a `foo/mod.rs`
keeps its sub-modules as siblings.

Because a generated part is a module *below* the file it came from, the imports
copied into it are re-anchored: a `use super::…` gains one `super::`, and a
`use self::…` becomes `use super::…` (or `use crate::…` below a crate root).
`crate::`, `::`-rooted and external paths are untouched, and the root file — still
the original module — keeps its header verbatim. For the same reason, a relative
`include!` / `include_str!` / `include_bytes!` path in a body that moved into a
subdirectory gains one `../` per level.

## Verification

Run the tool's own checks with:

```sh
cargo test
cargo clippy --all-targets --all-features -- -D warnings
```

After applying a generated split to another crate, run that crate's normal
verification commands. `rust-split` handles the mechanical move, but the compiler
is still the authority for import paths, visibility, macro edge cases, and public
API preservation.

## Limitations

- `adjacency_hint` is syntactic. It records sibling identifier references, not a
  full semantic call graph, so shadowing and macro expansion can affect grouping.
- Responsibility grouping uses explicit banners and local inherent-type
  ownership, not semantic analysis. Without those hints, reference adjacency
  still decides grouping. Aliases, qualified paths other than `self::Type`,
  macro-generated impls, and trait-only impls do not establish parent ownership.
  Many retained types can leave the parent over budget; this is reported rather
  than moving shared ownership to an arbitrary child. Review the layout and
  use the skill's explode-then-assemble workflow when manual boundaries fit better.
- A moved `pub(super)` item keeps its restriction, which now names the
  generated module's parent — the original module — rather than the original
  module's parent.
- Registration macros or framework-specific blocks may need manual treatment.
  Attribute tokens and unknown macro input are left verbatim. Paths in parsed
  expression arguments to common standard assertion, formatting, and output
  macros are re-based.
- Large leaf items cannot be split internally; they are reported as still
  oversized.
- A `#[path = "…"]` declaration in the parent changes where rustc looks for the
  generated sub-modules, so a `#[path]`-declared module must be split with the
  attribute removed (one line in the parent) or the output moved by hand.
- `include!` / `include_str!` / `include_bytes!` paths are re-based only when the
  argument is a single plain string literal (with an optional trailing comma)
  holding a relative path. An absolute path is left alone, and a computed
  argument — `concat!(env!("CARGO_MANIFEST_DIR"),
  …)`, a macro, a constant — cannot be re-based mechanically: it is left verbatim
  and reported on stderr with the generated file and line to check.