Repository navigation
scripts: add split_rust_module.py for mechanical, verifiable Rust module splits - #562
Conversation
Splitting a large Rust file into child modules by hand is slow to review and painful to rebase. This tool does it from a short spec that names the items each new child module gets: - `list FILE` prints every top-level item with the selector a spec uses. - `apply SPEC` moves the named items verbatim, with their doc comments and attributes and in their original order, into new child modules. The parent keeps `mod child;` and a glob re-export, so every existing path still resolves and no caller changes. - `verify [BASE]` proves BASE...HEAD is such a move. It re-applies the same edits to every item at the merge base and requires an exact match at HEAD, ignoring whitespace and trailing commas but not string literals. The only edits to moved text are the ones that keep name resolution and reach exactly as they were: - Visibility on items, inherent-impl members and struct fields: private becomes `pub(super)` and `pub(super)` becomes `pub(in <grandparent>)`, which is the same reach one module deeper. - `super::` paths gain one `super::`. - Relative `include_str!`/`include_bytes!` paths gain `../` when the children sit one directory below the parent file. Each child starts with `use super::*;`, so the parent's imports, traits and macros resolve as before. Because the spec names items rather than lines, a split can be regenerated on a newer main instead of being rebased.
A large file is often one big inherent impl, so whole-item moves cannot shrink it. A selector of the form 'impl <header> :: fn <name>' now moves chosen members into an impl block with the same attributes and header in the child module. The parent's block keeps the rest and disappears once it is empty. The impl's doc comments stay with the parent's block, or go to the first child that receives members when none are left. Trait impls still move whole. verify compares inherent impls member by member, keyed by the impl's attributes and header, so a split impl is proved the same way as a whole item. apply now also refuses a parent file loaded through #[path], whose children would resolve elsewhere, and reports archcheck pattern-ratchet matches each new module would hold, so their baseline entries can move with the code.
Moving every item of 19 large files into child modules compiled and passed clippy, doctests and the affected crates' tests apart from these cases, now handled: - `const _` takes no visibility; giving it pub(super) is a warning. - A macro_rules! that emits pub(super) or pub(self) means something else when it is expanded one module deeper, so apply refuses to move code that invokes it. - verify ignores the trailing comma rustfmt adds inside generics when a visibility pushes a signature past the line width. - The #[path] check ignores attributes written in comments.
File headers (//! docs and #![...] inner attributes) are not items, so verify did not compare them, and an added #![cfg(...)] or #![allow(...)] would have passed. A modified file's header must now equal its base, and a new child module's header may hold only //! docs.
…arden apply
Review found that verify's item matching accepted changes that are not
moves: a dropped mod or re-export line, an import added next to the
generated ones, a header edit, an edit a whitespace-insensitive
comparison erases ((x,) to (x), a newline inside a string), doc comments
swapped between impls, and items reordered in the parent.
verify now proves something simpler and stronger: it derives the spec
from the diff (which items each new child module holds), regenerates
the split from the merge base with apply, and compares every parent and
child file byte for byte after rustfmt. Any other Rust change, a
symlink, or a diff with no split in it fails.
apply fixes from the same review and from a rustc-built test crate:
- Pick the glob re-export's visibility from each item's reach, so
pub(in crate::a) items are no longer re-exported more narrowly, and
re-export modules of macro invocations at pub, which a glob clamps.
- Count angle brackets only directly inside a struct body and skip
const-generic blocks when finding a body or impl brace, so fields
after [u8; 1 << 3] and impls with where T: Tr<{ 1 + 1 }> are widened.
- Match extern "C" members, treat src/bin/*.rs as crate roots and a
nested main.rs as an ordinary module, keep cfg text byte for byte on
the new mod lines, rewrite super:: in attributes copied onto split
impl blocks, and rebase plain include! paths.
- Refuse instead of guessing: a macro name defined twice, macro chains
that emit pub(super), nested relative visibility, inline modules that
name super, self::super, super:: inside string literals, other
include arguments, keyword or import-colliding module names, and an
attribute sharing a line with its item.
- glob_import = false omits use super::*; for a module that needs
nothing from its parent. The reexport option is gone, so a dropped
re-export cannot pass verify.
- The #[path] scan stays inside the crate.
Tests: 63, including rejection cases for each false accept above and a
compile oracle that builds and runs a small crate with rustc before and
after a split, with callers at every visibility boundary.
verify:
- Format only the regenerated side and compare it with HEAD's bytes.
Comparing two rustfmt outputs let an edit rustfmt erases (a trailing
comma inside a macro call) pass.
- Find a new file's parent from the same layout rules apply uses, so a
split of a src/bin/ crate root round-trips.
- The glob import is never optional: a module whose items need nothing
from the parent sets allow_unused_glob, which keeps `use super::*;`
under #[allow(unused_imports)]. Removing the import, which can change
what a name resolves to, now fails verify.
apply now refuses, rather than changing meaning:
- moved code that invokes a parent macro whose expansion names a
super/self path, directly or through another macro;
- moved code that invokes a macro the parent defines only later in the
file (it bound to an outer macro of that name);
- top-level macro invocations, unless the module sets
allow_macro_items (their items keep visibilities the tool cannot
remap);
- inline modules nested inside moved code that names super;
- stringify! of a super path with any delimiter;
- an include inside a split impl's header or attributes;
- an attribute sharing a line with an impl member.
apply also deepens every path-leading super (use super as x, not only
super::), rebases include paths written with {} or [] delimiters,
wraps generated //! docs at 100 columns, and counts the two kinds of
visibility edit separately.
…ure move) sandbox/worker.rs is keyed by emitter: every sandbox-manager feature appends its metric family here. Move each family into its own child module: guest_bridge, route_authorization, net_gateway, teardown_suspend, auto_lifecycle, cleanup, quarantine_recovery, inventory, health and ingress. New families then get a module of their own instead of growing this file. Generated by scripts/split_rust_module.py (#562) from the spec in the pull request. Items move verbatim. The only edits keep each item's reach: 10 private items, fields or methods become pub(super). The parent re-exports the moved items, so no caller changes.
verify: - Read file bytes without newline translation, and reject a carriage return. rustc ends a // comment only at \n, so a lone CR after a comment turns the next line into comment text that Python's newline translation would hide. - Map every impl, not only inherent ones, back to its base selector, and derive a whole-impl selector when one child holds all of an impl's members. A whole move of an impl with super:: in its header or #[doc = include_str!] on it no longer fails as a false reject. - Write the derived spec without ASCII escaping, so non-ASCII docs round-trip. - Resolve the repository root, so verify works from a subdirectory. - List the non-Rust files a change also touches, which verify does not check. apply: - Refuse include! of Rust code, whose paths resolve from the module it is expanded in. include_str!/include_bytes! paths are still rebased. - Refuse a file with a carriage return. - Refuse a src/bin/<name>.rs crate root: its children would sit in src/bin/, where Cargo builds every file as its own binary. - Refuse a module name the parent already uses as a path (a crate or a glob-imported module), which the new module would shadow. - Ignore #[...] inside block comments when reading an item's attributes, and refuse a multi-line attribute that ends on its item's line. - Leave const _ out of the re-export choice, so a module of only impls and const _ gets no unused glob re-export. Remove dead ranking code. Tests: 82, with message checks on the rejection tests and cases for each finding above.
facets.rs holds the facet registry and dispatch together with every facet's implementation. Keep the registry, dispatch and shared publish types in facets.rs and move each facet into its own child module: network_policy, template_desired_state, sandbox_desired_state, cluster_deleted_eviction, port_exposure, inventory_publish and the local_state reads they share. Generated by scripts/split_rust_module.py (#562) from the spec in the pull request. Items move verbatim. The only edits keep each item's reach: 101 private items, fields or methods become pub(super). The parent re-exports the moved items, so no caller changes.
…ure move) sandbox/worker.rs is keyed by emitter: every sandbox-manager feature appends its metric family here. Move each family into its own child module: guest_bridge, route_authorization, net_gateway, teardown_suspend, auto_lifecycle, cleanup, quarantine_recovery, inventory, health and ingress. New families then get a module of their own instead of growing this file. Generated by scripts/split_rust_module.py (#562) from the spec in the pull request. Items move verbatim. The only edits keep each item's reach: 10 private items, fields or methods become pub(super). The parent re-exports the moved items, so no caller changes.
data_plane_sessions.rs serves five unrelated ClusterApi RPC families: session route resolution, process binding and reconnects, revocation, runtime access, and the ECW sandbox service route with its reset and credential checks. Move each family, and the route-row and route-token helpers they share, into its own child module: session_route, route_rows, route_token, service_route, service_route_reset, ecw_authorization, process_binding, revocation and runtime_access. Generated by scripts/split_rust_module.py (#562) from the spec in the pull request. Items move verbatim. The only edits keep each item's reach: 132 private items, fields or methods become pub(super); 8 pub(super) ones become pub(crate). The parent re-exports the moved items, so no caller changes.
facets.rs holds the facet registry and dispatch together with every facet's implementation. Keep the registry, dispatch and shared publish types in facets.rs and move each facet into its own child module: network_policy, template_desired_state, sandbox_desired_state, cluster_deleted_eviction, port_exposure, inventory_publish and the local_state reads they share. Generated by scripts/split_rust_module.py (#562) from the spec in the pull request. Items move verbatim. The only edits keep each item's reach: 101 private items, fields or methods become pub(super). The parent re-exports the moved items, so no caller changes.
bootstrap.rs collects five unrelated facilities that services use at startup: gRPC request parameter recording and logging, Unix socket binding, the HTTP trace layer, the gRPC trace layer and shutdown signal handling. Keep the gRPC server builder and health service in bootstrap.rs and move each facility into its own child module: grpc_request_params, unix_socket, http_trace, grpc_trace and shutdown. Generated by scripts/split_rust_module.py (#562) from the spec in the pull request. Items move verbatim. The only edits keep each item's reach: 42 private items, fields or methods become pub(super). The parent re-exports the moved items, so no caller changes.
executor.rs is the largest production file in the workspace. Besides the transfer executor it holds the page-cache writeback coordinator, the decode and decrypt sinks, sparse-shadow comparison, replica provenance, cgroup codec sizing and the pull path. Move those, which no open pull request edits, into child modules: codec_concurrency, artifact_role, transfer_metrics, sparse_shadow, replica_provenance, stream_pull_timing, writeback_coordinator, stream_writeback, decode_sinks and pull. Payload, crypto staging, the artifact job and push stay for a later step. Generated by scripts/split_rust_module.py (#562) from the spec in the pull request. Items move verbatim. The only edits keep each item's reach: 207 private items, fields or methods become pub(super). The parent re-exports the moved items, so no caller changes.
node_maintenance.rs serves three unrelated admin route families beside the Direct node registry: node status, health and reconcile; node template pull, delete and list through the node-api; and the node-api client plumbing they share. Keep the registry handlers and the shared node row and response types in node_maintenance.rs and move the rest into child modules: node_api, node_status and node_templates. Every line that touches a Direct-only table stays in node_maintenance.rs, so archcheck's direct_tables ratchet is unchanged. Generated by scripts/split_rust_module.py (#562) from the spec in the pull request. Items move verbatim. The only edits keep each item's reach: 137 private items, fields or methods become pub(super). The parent re-exports the moved items, so no caller changes.
substrate/mod.rs holds the SubstrateAdapter trait and its dispatch, and also a dozen unrelated groups of types and helpers. Move six of them, which no open pull request edits, into child modules: node_api_client, pool_claims, local_artifact, execution_fence, recovery_charge and sandbox_state_probe. The trait, dispatch, errors, capabilities and request types stay for a later step. Generated by scripts/split_rust_module.py (#562) from the spec in the pull request. Items move verbatim. The only edits keep each item's reach: 20 private items, fields or methods become pub(super); 2 pub(super) ones become pub(in crate::controllers). The parent re-exports the moved items, so no caller changes.
- Check macro textual scope transitively: a moved item that reaches a parent macro through another parent macro (relay!() expanding to value!()) is refused when that macro is defined later in the parent or more than once, like a direct invocation. - Detect a terminal super (use super as x) in parent macro bodies, as in moved code. - Read the attributes copied onto split impl blocks from masked text, so a #[cfg(..)] inside a block comment is not copied as a real one. - Refuse a multi-line impl-member attribute that ends on its member's line, which would otherwise attach the member to the next one. Tests: 86.
node_maintenance.rs serves three admin route families beside the Direct node registry: node status, health and reconcile; node template pull and delete through the node-api, and listing from the cached template inventory; and the node-api client plumbing the registry and template routes use. Keep the registry handlers, the shared node row and response types, and the node reads in node_maintenance.rs, and move the rest into child modules: node_api, node_status and node_templates. Every line that touches a Direct-only table stays in node_maintenance.rs, so archcheck's direct_tables ratchet is unchanged. Generated by scripts/split_rust_module.py (#562) from the spec in the pull request. Items move verbatim. The only edits keep each item's reach: 137 private items, fields or methods become pub(super). The parent re-exports the moved items, so no caller changes.
- Deepen super passed as a macro argument (call!(super)); the
visibility check already excludes pub(super).
- Refuse a raw-identifier inline module (mod r#i { .. }) naming super,
like any inline module.
- Treat #[macro_use] items like macro_rules! when placing the new mod
lines, so children still see the macros they bring in.
- Require allow_macro_items to move a macro invocation that is an
inherent-impl member, as for top-level invocations.
- Scan macro_rules! definitions elsewhere in the crate (and use-aliases
of them) for super/self paths, not only the parent's.
- Look for a new module name used as a path in the parent's existing
child modules too, so it cannot shadow a crate they name.
- Refuse a code block in a module doc, which rustdoc would run.
- Remove dead code.
Tests: 93.
A module whose moved items are named only inside the module (and its tests) makes the parent's private use child::*; unused in normal builds, which -D warnings rejects. Dropping the re-export is not safe, because a sibling's glob import could then resolve the name to a different item. allow_unused_reexport = true keeps the re-export under #[allow(unused_imports)]; verify derives it from the head like the other module options.
executor.rs is the largest production file in the workspace. Besides the transfer executor it holds the page-cache writeback coordinator, the decode and decrypt sinks, sparse-shadow comparison, replica provenance, cgroup codec sizing and the pull path. Move those, which no open pull request edits, into child modules: codec_concurrency, artifact_role, transfer_metrics, sparse_shadow, replica_provenance, stream_pull_timing, writeback_coordinator, stream_writeback, decode_sinks and pull. Payload, crypto staging, the artifact job and push stay for a later step. Generated by scripts/split_rust_module.py (#562) from the spec in the pull request. Items move verbatim. The only edits keep each item's reach: 210 private items, fields or methods become pub(super). The parent re-exports the moved items, so no caller changes.
…rea (pure move) lib.rs holds the whole typed client: its types, the call macro and constructors, and 40 methods across four guest services. Keep the types, the macro and the constructors in lib.rs and move the methods into impl blocks in five child modules: meta, filesystem, file_watch, process and networking, together with the helpers only those methods use. Generated by scripts/split_rust_module.py (#562) from the spec in the pull request. Items move verbatim. The only edits keep each item's reach: 5 private items, fields or methods become pub(super). The parent re-exports the moved items, so no caller changes.
data_plane_sessions.rs serves five unrelated ClusterApi RPC families: session route resolution, process binding and reconnects, revocation, runtime access, and the ECW sandbox service route with its reset and credential checks. Move each family, and the route-row and route-token helpers they share, into its own child module: session_route, route_rows, route_token, service_route, service_route_reset, ecw_authorization, process_binding, revocation and runtime_access. Generated by scripts/split_rust_module.py (#562) from the spec in the pull request. Items move verbatim. The only edits keep each item's reach: 132 private items, fields or methods become pub(super); 8 pub(super) ones become pub(crate). The parent re-exports the moved items, so no caller changes.
The fourth review round found the remaining meaning changes all come from macros, which the tool cannot expand. Refuse them instead: - Treat a macro_rules! anywhere in the crate as relative when its body names a super/self path or relative visibility, or reaches such a macro through other macros or a use-alias, and refuse moved code that invokes one, bare or path-qualified. - Refuse passing super in a user macro's arguments: the macro can match or stringify those tokens, so deepening them may change its expansion. format-, assert- and tracing-style macros stay allowed. - Refuse any split of a parent with a #[macro_use] item, whose textual scope the new mod lines would change. - Scan the parent's child modules recursively when checking that a new module name is not already used as a path. The workspace has no #[macro_use] and no super passed to a user macro, so no real split loses anything. Docstring updated; tests: 98.
substrate/mod.rs holds the SubstrateAdapter trait and its dispatch, and also a dozen unrelated groups of types and helpers. Move six of them, which no open pull request edits, into child modules: node_api_client, pool_claims, local_artifact, execution_fence, recovery_charge and sandbox_state_probe. The trait, dispatch, errors, capabilities and request types stay for a later step. Generated by scripts/split_rust_module.py (#562) from the spec in the pull request. Items move verbatim. The only edits keep each item's reach: 20 private items, fields or methods become pub(super); 2 pub(super) ones become pub(in crate::controllers). The parent re-exports the moved items, so no caller changes.
Review: 7 findingsMedium
Low
Verified against What I ran:
|
the-gigi-pplx
left a comment
There was a problem hiding this comment.
Approved. 7 non-blocking findings in the review comment above.
- Refuse super passed to a path-qualified user macro (crate::m!, $crate::m!, other::m!), and to a crate macro_rules! that shares a name with a known format/assert/tracing/anyhow macro: the exemption now applies only when the crate defines no macro of that name. - Keep plain comments (a license or SPDX line) above the module's inner docs in the parent header, and recognize /*! */ inner docs. - Refuse moved code whose doc comments or #[doc] attributes name a super:: or self:: path: an intra-doc link would point one module off. - Place the new mod lines above the first fn when the parent defines no types, instead of after a trailing mod tests;. - verify fails on non-Rust changes inside the split crate (a manifest, data or build input) unless --allow-non-rust is given. - Document that macros from other crates, and proc and attribute macros, are not analyzed; fix the comment on split-impl doc placement. All eight open split branches still verify identical with this tool. Tests: 108.
|
Thanks for the careful read. All seven are fixed in a097b25, each with a test that fails on the previous head and passes now (108 tests):
The split PRs are unaffected: with this version, all eight (#563–#569, #571) still regenerate byte-identically at their current heads, none of their parents starts with a plain comment, and every non-Rust file they touch is outside the split crate. |
…erplexityai#569) * api: move six groups out of controllers/substrate/mod.rs (pure move) substrate/mod.rs holds the SubstrateAdapter trait and its dispatch, and also a dozen unrelated groups of types and helpers. Move six of them, which no open pull request edits, into child modules: node_api_client, pool_claims, local_artifact, execution_fence, recovery_charge and sandbox_state_probe. The trait, dispatch, errors, capabilities and request types stay for a later step. Generated by scripts/split_rust_module.py (perplexityai#562) from the spec in the pull request. Items move verbatim. The only edits keep each item's reach: 20 private items, fields or methods become pub(super); 2 pub(super) ones become pub(in crate::controllers). The parent re-exports the moved items, so no caller changes. * docs: point references at the split mod.rs modules
…rea (pure move) (perplexityai#571) lib.rs holds the whole typed client: its types, the call macro and constructors, and 40 methods across four guest services. Keep the types, the macro and the constructors in lib.rs and move the methods into impl blocks in five child modules: meta, filesystem, file_watch, process and networking, together with the helpers only those methods use. Generated by scripts/split_rust_module.py (perplexityai#562) from the spec in the pull request. Items move verbatim. The only edits keep each item's reach: 5 private items, fields or methods become pub(super). The parent re-exports the moved items, so no caller changes.
…ve) (perplexityai#563) facets.rs holds the facet registry and dispatch together with every facet's implementation. Keep the registry, dispatch and shared publish types in facets.rs and move each facet into its own child module: network_policy, template_desired_state, sandbox_desired_state, cluster_deleted_eviction, port_exposure, inventory_publish and the local_state reads they share. Generated by scripts/split_rust_module.py (perplexityai#562) from the spec in the pull request. Items move verbatim. The only edits keep each item's reach: 101 private items, fields or methods become pub(super). The parent re-exports the moved items, so no caller changes.
) executor.rs is the largest production file in the workspace. Besides the transfer executor it holds the page-cache writeback coordinator, the decode and decrypt sinks, sparse-shadow comparison, replica provenance, cgroup codec sizing and the pull path. Move those, which no open pull request edits, into child modules: codec_concurrency, artifact_role, transfer_metrics, sparse_shadow, replica_provenance, stream_pull_timing, writeback_coordinator, stream_writeback, decode_sinks and pull. Payload, crypto staging, the artifact job and push stay for a later step. Generated by scripts/split_rust_module.py (perplexityai#562) from the spec in the pull request. Items move verbatim. The only edits keep each item's reach: 210 private items, fields or methods become pub(super). The parent re-exports the moved items, so no caller changes.
…ure move) (perplexityai#565) sandbox/worker.rs is keyed by emitter: every sandbox-manager feature appends its metric family here. Move each family into its own child module: guest_bridge, route_authorization, net_gateway, teardown_suspend, auto_lifecycle, cleanup, quarantine_recovery, inventory, health and ingress. New families then get a module of their own instead of growing this file. Generated by scripts/split_rust_module.py (perplexityai#562) from the spec in the pull request. Items move verbatim. The only edits keep each item's reach: 10 private items, fields or methods become pub(super). The parent re-exports the moved items, so no caller changes.
…erplexityai#566) * internal-api: split data_plane_sessions.rs by RPC family (pure move) data_plane_sessions.rs serves five unrelated ClusterApi RPC families: session route resolution, process binding and reconnects, revocation, runtime access, and the ECW sandbox service route with its reset and credential checks. Move each family, and the route-row and route-token helpers they share, into its own child module: session_route, route_rows, route_token, service_route, service_route_reset, ecw_authorization, process_binding, revocation and runtime_access. Generated by scripts/split_rust_module.py (perplexityai#562) from the spec in the pull request. Items move verbatim. The only edits keep each item's reach: 132 private items, fields or methods become pub(super); 8 pub(super) ones become pub(crate). The parent re-exports the moved items, so no caller changes. * archcheck: re-key the direct_tables entry for the split data_plane_sessions.rs The split moves all seven direct_tables matches in data_plane_sessions.rs into data_plane_sessions/route_rows.rs. Re-key the ratchet entry to the new file with the same cap of 7. * docs: point references at the split data_plane_sessions.rs modules node_api_backend_endpoint now lives in data_plane_sessions/route_rows.rs; repoint the network policy comment that cites it.
) * common-svc: split bootstrap.rs by facility (pure move) bootstrap.rs collects five unrelated facilities that services use at startup: gRPC request parameter recording and logging, Unix socket binding, the HTTP trace layer, the gRPC trace layer and shutdown signal handling. Keep the gRPC server builder and health service in bootstrap.rs and move each facility into its own child module: grpc_request_params, unix_socket, http_trace, grpc_trace and shutdown. Generated by scripts/split_rust_module.py (perplexityai#562) from the spec in the pull request. Items move verbatim. The only edits keep each item's reach: 42 private items, fields or methods become pub(super). The parent re-exports the moved items, so no caller changes. * docs: point references at the split bootstrap.rs modules Two chart comments cite bootstrap.rs for replacing a node-local socket by rename. That code now lives in bootstrap/unix_socket.rs.
…plexityai#568) * api: split admin node_maintenance.rs by route family (pure move) node_maintenance.rs serves three admin route families beside the Direct node registry: node status, health and reconcile; node template pull and delete through the node-api, and listing from the cached template inventory; and the node-api client plumbing the registry and template routes use. Keep the registry handlers, the shared node row and response types, and the node reads in node_maintenance.rs, and move the rest into child modules: node_api, node_status and node_templates. Every line that touches a Direct-only table stays in node_maintenance.rs, so archcheck's direct_tables ratchet is unchanged. Generated by scripts/split_rust_module.py (perplexityai#562) from the spec in the pull request. Items move verbatim. The only edits keep each item's reach: 137 private items, fields or methods become pub(super). The parent re-exports the moved items, so no caller changes. * docs: point references at the split node_maintenance.rs modules The Kind node-api chart comment cites pull_node_template by file. It now lives in node_maintenance/node_templates.rs; require_admin_node_registry stays in node_maintenance.rs.
What
scripts/split_rust_module.pysplits a rustfmt-formatted Rust file into child modules from a short spec, and proves afterwards that the result is a pure move.list FILEprints each top-level item, and each member of an inherent impl, with the selector a spec uses.apply SPECmoves the selected items into new child modules. Each item keeps its doc comments and attributes, its original order, and its text. The parent keepsmod child;and a glob re-export, so every existing path still resolves and no caller changes. Each child starts withuse super::*;, so the parent's imports, traits and macros resolve as before.verify [BASE]proves that BASE...HEAD's Rust changes are exactly whatapplygenerates. It derives the spec from the diff (which items each new child module holds), regenerates the split from the merge base, rustfmts the result, and requires HEAD's parent and child files to equal it byte for byte. Any other Rust change fails it: a missing or alteredmod/useline, a dropped or reordered item, an edit inside moved code or a comment, a symlink, or a diff with no split. So does a non-Rust change inside the split crate (a manifest, data or build input) unless--allow-non-rustis given; non-Rust changes elsewhere are listed.--print-specshows the derived spec so it can be compared with the one in the PR.A spec names items, not lines:
impl <header> :: fn <name>moves chosen members of an inherent impl into a block with the same attributes and header in the child; the parent's block keeps the rest. Trait impls move whole.Why
Large files are slow to review and change, for people and for agents. Splitting them by hand takes long and is hard to review. A split also goes stale against main within hours, and rebasing a 1 → N move is mostly manual, because git's rename detection doesn't follow it.
With this tool a split is a spec plus a regenerate. If main moves, rerun
applyon the new main instead of resolving conflicts. Reviewers check the grouping in the spec and rerunverify, instead of reading thousands of moved lines.verifychecks the generator's output, not its semantics. Those rest on the edits below being reach-preserving, on the refusals, and on compiling and testing the result like any change.The only edits it makes to moved text
Each keeps name resolution and reach exactly as they were:
pub(self)→pub(super)on items, inherent-impl members and struct fieldspub(super)reaches exactly the old module and its descendants.pub(super)→pub(in crate::<grandparent>)(orpub(crate))super::→super::super::include!/include_str!/include_bytes!paths gain../foo.rs→foo/).It refuses, rather than guesses, when a move needs a hand edit. Most refusals are about macros, which the tool cannot expand:
use,mod x;,externblocks ormacro_rules!items, and any split of a parent with a#[macro_use]item. Macros stay in the parent, above the newmodlines, so children still see them.super/selfpaths or emitspub(super)/pub(self). This covers direct calls, calls through other macros, path-qualified calls anduse .. asaliases.superpassed in a user macro's arguments, bare or path-qualified.format!/assert!/tracing-style macros are fine, unless the crate defines its own macro of the same name;stringify!ofsuper, andinclude!;#[doc]attribute in moved code naming asuper::/self::path: an intra-doc link would point one module off;allow_macro_items;pub(in super::..), apub(super)nested inside a moved item, an inline module namingsuper,self::super, or asuper::path inside a string literal;#[path], or#[path]inside moved code;src/bin/<name>.rscrate root, or a code block in a module doc.Module options, each reproduced by
verify:allow_unused_globkeepsuse super::*;under#[allow(unused_imports)]for a module that needs nothing from its parent.allow_unused_reexportdoes the same for the parent's re-export when nothing outside the module names its items.allow_macro_itemsopts in to moving macro invocations.applyalso reports per new module thetracingevents without an explicittarget:(their target becomes the new module path),sql!locators, and lines matching an archcheckpattern_ratchet, so a PR can list them and move ratchet entries with the code.Evidence
scripts/tests/test_split_rust_module.py(108 tests):verifymust reject;rustc --test -D warningsbuilds and runs a small crate before and after a split, with callers in the parent, a sibling and the grandparent reaching private,pub(super)and field-level items.cargo check --workspace --all-targets --all-featuresand CI's clippy invocations with-D warningspassed.--no-default-featuresbuilds of the 14 packages, doctests, and the nextest runs of four packages all matched main.verify. 92 regenerate identically; the other 10 stop at a refusal (macro invocations without the opt-in,include!, a parent macro emittingpub(super), nested relative visibility, an inline module namingsuper).verifydesign, which led to the regeneration check, andapplycases that changed meaning; each is fixed with a test. Round 4 found only macro forms the tool now refuses. A maintainer review then found seven non-blocking gaps; each is fixed with a test, and the eight split PRs still regenerate identically.Limits
verifyproves the commit isapply's rustfmt'd output for the derived spec. Thatapplypreserves meaning rests on the edit rules, the refusals, and compiling and testing the result. Macros are where that argument is weakest, hence the conservative refusals.pub(super)or asuper::path would expand one module deeper without a refusal; compiling and testing the result is the check.allow_macro_itemsemits keeps the visibility the macro gives it; compile errors are the check.docs/rust/space-conventions.md("Log targets") covers it, andapplycounts them.🤖 Generated with Claude Code