cargo-mend
Warning: This project is pre-1.0 and under active development. Diagnostics, config format,
and CLI flags may change without notice between releases. The --fix flag modifies source files
in place (it rolls back on cargo check failure, but always review the diff before committing).
Use at your own risk.
cargo-mend provides the cargo mend subcommand for enforcing an opinionated Rust
visibility style across a crate or workspace.
The tool is meant for codebases that want visibility to describe real module boundaries.
Guiding Principle
The goal is that you should be able to read a Rust file in place and understand what each item's visibility is trying to say.
In practice, that means:
- if you see
pubin a leaf module, it should suggest that the item is part of that module's intended API surface - if an item is only meant for its parent module or peer modules under the same parent,
pub(super)should say that directly - if you are in a top-level private module, plain
pubcan still be the right way to mark that module's crate-internal boundary API - if an item is only local implementation detail, it should stay private
The more the code says this directly, the less a reader has to reconstruct the real boundary by mentally walking the whole module tree.
That is the design pressure behind this tool. It tries to catch places where the written visibility is broader, vaguer, or more global than the code relationship really is.
In practice, that usually means:
- if an item is only meant for its parent module in a nested private subtree, use
pub(super) - if an item lives in a top-level private module and is part of that module's crate-internal API,
plain
pubmay be correct - if an item is only local implementation detail, keep it private
- if an item seems to need a deeply nested visibility like
pub(in crate::feature::subtree), the module tree may be wrong - if an item is marked
pubbut is not actually used outside its intended module boundary, that is probably a design smell
V1 policy
Hard errors:
pub(crate)is forbidden in binaries and in nested modules- library crates may use
pub(crate)at the crate root when the intent is to keep an item crate-internal rather than part of the external library API - top-level private modules in library crates may also use
pub(crate)when the intent is to keep an item crate-internal and prevent accidental exposure through the public library boundary pub(in crate::...)is forbiddenpub modrequires an explicit allowlist entry
Warnings:
pubin a nested child file where compiler analysis shows the item should probably be narrower thanpub- parent module
pub use *re-exports that should be explicit
If you are new to Rust visibility, the important idea is this:
pubdoes not automatically make an item part of the crate's real outward API- every parent module on the path also has to be visible
- if a parent module is private, a child item can be written as
puband still not actually be reachable from outside the crate
Config
The tool looks for mend.toml at the target root.
[]
= [
"mcp/src/brp_tools/tools/mod.rs",
]
= [
"src/example/private_child.rs::SomeIntentionalFacadeItem",
]
Use the allowlists sparingly. The default assumption should be that the code shape is wrong before the policy is wrong.
Installation
cargo-mend uses #![feature(rustc_private)] to access compiler internals for visibility
analysis after macro expansion. This is a permanently unstable feature — it is how tools like
clippy and miri access the compiler, but it means the compiler's internal crates have no
stability guarantee and cargo-mend is sensitive to the exact rustc version used to build it.
| rustc | cargo-mend |
|---|---|
| 1.94.0 | 0.1.0+ |
| 1.93.1 | 0.1.0 |
Plain cargo install cargo-mend on a stable toolchain will fail because the compiler rejects
#![feature(rustc_private)] on stable. You need one of the following:
Option A: nightly toolchain
Option B: stable toolchain with bootstrap override
RUSTC_BOOTSTRAP=1
RUSTC_BOOTSTRAP=1 tells the compiler to accept unstable features on a stable toolchain. This
is the same mechanism the Rust project uses internally to build its own tools. You also need the
rustc-dev component:
CI installation
For GitHub Actions or similar CI, install the rustc-dev component and use RUSTC_BOOTSTRAP:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
components: rust-src, rustc-dev, llvm-tools-preview
- name: Install cargo-mend
run: cargo install cargo-mend
env:
RUSTC_BOOTSTRAP: 1
- name: Run cargo-mend
run: cargo mend --fail-on-warn
After installation
Once installed, cargo mend runs on any project regardless of that project's toolchain. The
toolchain requirement is only for compiling cargo-mend itself.
After a Rust toolchain update, rerun cargo mend on a known repo and check the table above
if results regress.
Usage
Behavior:
- run it at a workspace root to audit all workspace members
- run it in a member crate directory to audit just that package
- pass
--manifest-pathto choose an explicit crate or workspace root --fixonly rewrites the import-shortening cases thatcargo-mendcan prove are safe- if a
--fixrun would leave the crate failingcargo check,cargo-mendrestores the original files automatically - if there is nothing fixable,
cargo-mendsays so after the report summary
Intended workflow
Use this as a migration aid and CI guard:
- fail immediately on forbidden visibility forms
- review suspicious
pub - let
cargo mend --fixrewrite the straightforward local-import paths it knows how to fix - keep repo-specific exceptions small and explicit
The usual review flow is:
- ask whether the item is truly part of the module's API
- if not, try private or
pub(super)in a nested module - if the item lives in a top-level private module, plain
pubmay already be the correct crate-internal boundary - if
pub(super)is too narrow, move the item to a better common parent - only keep broader visibility when the module structure genuinely requires it
Diagnostic Reference
Forbidden pub(crate)
pub(crate) is broad enough to be easy to reach for, but in many codebases it weakens module
boundaries more than intended.
This tool treats it as forbidden in binaries and in nested modules.
There is one narrow exception:
- at the crate root of a library crate, when the item should stay crate-internal and not become part of the external library API
- in a library crate
- inside a top-level private module
- when the point is to keep something crate-internal and prevent accidental leakage through the public library boundary
Prefer:
- private items when they are local implementation details
pub(super)when the parent module owns the boundary- moving the item to a better common parent when
pub(super)is too narrow
In this example, there is a parent module named feature, and helpers.rs exists only to support
that parent module.
The question is whether the helper should be available to the whole crate, or just to feature.
Example:
// src/feature/mod.rs
// src/feature/helpers.rs
pub
At first glance, helper looks reasonable: the whole crate can use it.
But that is exactly the problem. The helper now ignores the feature module boundary.
A better shape is:
// src/feature/helpers.rs
pub
Now helper is available to feature, but not to unrelated parts of the crate.
One exception is the crate root of a library crate, for example:
// src/lib.rs
pub type InternalDrawPhase = ;
That can be acceptable when the intent is:
- usable anywhere inside the crate
- but not part of the external library API
Another exception is a library crate with a top-level private module, for example:
// src/lib.rs
// src/internals.rs
pub
That can be acceptable when the intent is:
- usable anywhere inside the crate
- but never part of the external library API
Forbidden pub(in crate::...)
pub(in crate::...) often means the item lives too deep in the module tree.
This tool treats it as a design-review signal, not a normal visibility tool.
Prefer:
pub(super)when the current module shape is already correct- moving the item to the nearest common parent as its own file
In this example, a helper lives under src/feature/deep/, but the desired sharing boundary is
somewhere higher up than that file.
The example is showing what it looks like when the visibility path has to reach outward to describe the real boundary.
Example:
// src/feature/deep/helper.rs
pub
This tells you the visibility boundary is somewhere far away from the item.
That usually means one of two things:
- the item should just be
pub(super) - the item should move upward so the right boundary is local and obvious
A better shape is usually either:
// src/feature/deep/helper.rs
pub
or:
// src/feature/helper.rs
pub
Review pub mod
pub mod requires explicit review or allowlisting.
Keep it only when:
- the module path itself is intentionally part of the API
- macro or code-generation constraints make it a deliberate exception
In this example, the code is at the crate root.
The important thing to notice is that pub mod does not just declare a child module. It also
publishes that module path as part of the crate API.
Example:
// src/lib.rs
pub mod does two things at once:
- it declares a child module
- it makes that module path part of the public API
That is sometimes exactly what you want. It is also easy to do by accident.
This tool asks you to review that choice explicitly instead of letting it slip in unnoticed.
Narrow pub to pub(crate)
This warning flags pub items in top-level private modules that are not re-exported by the crate
root (lib.rs or main.rs).
In a top-level private module, pub and pub(crate) have the same effect — but pub misleadingly
suggests the item is part of the public API. Using pub(crate) makes the intent explicit: the item
is shared within the crate, not exported.
Example:
// src/lib.rs
// private module — not `pub mod`
pub use publicly_exported_fn;
// src/helpers.rs
// re-exported → must stay `pub`
// NOT re-exported → should be `pub(crate)`
Run cargo mend --fix to auto-fix these items to pub(crate).
Suspicious pub
This warning is about a Rust visibility trap in nested private modules:
- an item can be written as
pub - but still be broader than the boundary that file actually lives under
That happens when one of its parent modules is private and the file is not itself sitting at the top-level private boundary.
In this example, there is a private parent module named support, and helpers.rs lives under
that private boundary.
The code in helpers.rs marks Helper as pub, but the example is specifically showing a case
where that still does not make Helper part of the crate's public API.
Example:
// src/lib.rs
// src/support/mod.rs
// src/support/helpers.rs
;
If you are new to Rust, it is easy to read pub struct Helper; and think:
- "
Helperis public, so other crates can use it"
But Rust does not work that way. The full path must be public too.
In this example:
Helperis markedpub- but
supportis private - so
Helperis not reachable from outside the crate
That is why this tool warns here. In a nested private module like support/helpers.rs, the
declared visibility (pub) is broader than the boundary that file is actually participating in.
Possible resolutions:
- make the item private
- change it to
pub(super) - move it to a better common parent if it is truly shared
There is one important allowed case.
If the parent boundary module is intentionally acting as a facade, it may re-export the child item. That boundary can be either:
- a
mod.rsfile - or an ordinary file module like
markdown_file.rs
For example:
// src/private_parent/mod.rs
pub use Helper;
If code outside private_parent actually uses private_parent::Helper, then keeping Helper
as pub in child.rs is intentional and this warning should not fire.
If the parent boundary module re-exports Helper but nothing outside the parent subtree ever uses that
re-export, then the child pub is still broader than the boundary the code is actually using.
In that case this warning should still appear.
In practice, Rust itself will often warn on the parent mod.rs too:
warning: unused import: ...
cargo-mend does not duplicate that parent warning. Instead, it warns on the child item and points
back to the compiler's unused import warning so you can see the pair together:
- the compiler warns that the parent
pub useis stale cargo-mendwarns that the child item is still broader than needed
That is also the case that cargo mend --fix-pub-use is designed to repair.
For example:
// src/support/helpers.rs
pub ;
Now the code says what it actually means: Helper is shared with its parent module, not with the
outside world.
This warning does not apply the same way to a top-level private module. At the top level, plain
pub can still be the right way to say "this belongs to this module's crate-internal API."
Parent facade re-exports should also be explicit.
If a parent boundary module does this:
pub use *;
cargo-mend treats that as a separate problem. Use explicit re-exports instead so the parent
facade states exactly which child items it is exporting.
Internal parent pub use facade
This warning is about a parent boundary module that is being used as an internal namespace facade inside its own subtree.
In other words:
- the parent
pub useis not part of the outward boundary - but code inside the subtree is still referring to the parent path directly
- that makes the parent boundary part of the implementation structure, not just the facade
Example:
// src/private_parent/mod.rs
pub use Helper;
// src/private_parent/sibling.rs
In this shape, super::Helper is using the parent boundary itself as an internal facade.
That can be intentional, but it is worth review because it usually means one of two things:
- the parent boundary is acting as an internal namespace and should stay that way intentionally
- or the subtree should import the child module directly instead of routing through the parent
cargo-mend does not auto-fix this case.
Wildcard parent pub use
This warning is about parent facade modules that re-export everything from a child with *.
That shape makes the boundary harder to read because the parent module no longer says what it is actually exporting.
Prefer:
pub use ;
instead of:
pub use *;
Shorten local crate import
This warning is about import paths that are technically correct, but more global than the code relationship actually is.
In this example, there are two peer modules under the same private parent module:
cargo_detector.rsprocess.rs
The code in process.rs wants to import TargetType from its peer module cargo_detector.rs.
Example:
// src/app_tools/support/process.rs
use crateTargetType;
If you are reading process.rs, that import path makes TargetType look more global than it
really is.
But the real relationship is local:
process.rsandcargo_detector.rsare peers undersupport- the import is not crossing to a different domain
- the shorter local-relative path is clearer
A better import is:
use TargetType;
cargo mend --fix can rewrite these straightforward cases automatically.
Today, that auto-fix mode is intentionally narrow:
- it only rewrites local import-shortening cases
- it preserves the original import visibility (
use,pub use,pub(crate) use, and so on) - it rolls the edits back automatically if the follow-up
cargo checkfails
Replace deep super:: import
super::super:: and deeper chains force the reader to count hops to figure out where the import
lands. When a single super:: is not enough, a named crate:: path is immediately clear.
Example:
// src/tui/columns/render.rs
// flagged — deep super chain
use ResolvedWidths;
// preferred — named crate path
use crateResolvedWidths;
This applies at any depth: super::super::super:: and beyond are all rewritten to the equivalent
crate:: path.
cargo mend --fix can rewrite these cases automatically.
Prefer module import
This warning detects direct function imports and suggests importing the parent module instead, then calling the function with module qualification.
Example:
// Before:
use cratereport_to_mcp_error;
// After:
use crateerror;
cargo mend --fix can rewrite these cases automatically. It rewrites the use statement and
qualifies all bare references in the file.
Inline path-qualified type
This warning detects types used with inline path qualification (like crate::module::MyType)
and suggests adding a use import at the top of the file instead.
Example:
// Before:
// After:
use crateMyType;
cargo mend --fix can rewrite these cases automatically. It adds the use import and replaces
all inline occurrences with the bare type name.
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.