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.
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 an item lives in a top-level private module and is not re-exported by the crate root, use
pub(crate)— barepubthere is misleading because the item can never escape the crate - if the crate root re-exports the item via
pub use, the source must be barepub(E0364) - 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 is probably wrong;cargo mendrejects this form as a hard error so the structural problem surfaces instead of getting papered over
cargo mend flags places where the written visibility is broader, vaguer, or more global than
the code relationship actually is.
Hard errors:
pub(crate)is forbidden by default. Two narrow exceptions: at the crate root of a library crate (item stays crate-internal but outside the public library API), and inside a top-level private module with a private parent (any crate kind)pub(in crate::...)is forbidden — a code smell that signals the module tree is wrong; relocate the item to a better common parent insteadpub 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
The tool looks for mend.toml at the target root.
[visibility]
allow_pub_mod = [
"mcp/src/brp_tools/tools/mod.rs",
]
allow_pub_items = [
"src/example/private_child.rs::SomeIntentionalFacadeItem",
]Use the allowlists sparingly. The default assumption should be that the code structure is wrong before the policy is wrong.
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.
Install the rustc-dev component, then install cargo-mend with the stable toolchain plus
RUSTC_BOOTSTRAP=1. Nightly-built binaries can fail against stable projects with E0514
because cargo-mend links against rustc_driver.
rustup component add rustc-dev
RUSTC_BOOTSTRAP=1 cargo +stable install --path .
RUSTC_BOOTSTRAP=1 cargo +stable install cargo-mend --version <VERSION>cargo mend
cargo mend --fail-on-warn
cargo mend --fix
cargo mend --json
cargo mend --version
cargo mend --build-info
cargo mend --manifest-path path/to/Cargo.tomlBehavior:
- 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
--lib, --bin <NAME>, --example <NAME>, --test <NAME>, --bench <NAME>, and
--all-targets only narrow what gets printed. They do not change what gets analyzed —
mend always compiles every target (lib, bins, tests, examples, benches).
Why: whether a pub fn is "really used" depends on the whole crate. If you analyze only the
lib, a function called solely by an integration test or a #[cfg(test)] helper looks dead
and mend would suggest narrowing or removing it. That suggestion would break the test build.
By always compiling everything, mend sees the full call graph and gives correct answers.
So cargo mend --lib is "show me only the lib-file findings"; the analysis behind those
findings still considered every target.
Caveat: this only handles #[cfg(test)]. #[cfg(feature = "x")] items reached only under a
non-default feature still need an explicit --features <set> invocation to be visible.
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 all callers are inside the defining module subtree, make it private
- if callers live in sibling modules, try
pub(super)in a nested module - if the item lives in a top-level private module and is not re-exported by the crate root,
use
pub(crate)—cargo mend --fixwill narrow barepubfor you here - 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
pub(crate) lets any module in the crate touch the item, regardless of where the item lives.
In a deep module tree that usually weakens the module boundaries the layout was meant to
enforce.
cargo mend forbids pub(crate) by default. Two narrow exceptions:
- Library crate root — the item should stay crate-internal but outside the public library API.
- Top-level private module with a private parent — the item should be reachable anywhere in the crate but kept out of the public boundary. Applies to library and binary crates; integration tests never qualify.
Otherwise, 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, feature is a parent module and helpers.rs exists only to support it. The
question is whether the helper should be available to the whole crate, or just to feature.
// src/feature/mod.rs
mod helpers;
// src/feature/helpers.rs
pub(crate) fn helper() {}helper here looks reasonable — any caller in the crate can use it — but that is the problem.
The helper now ignores the feature module boundary. A better version:
// src/feature/helpers.rs
pub(super) fn helper() {}helper is now available to feature and nowhere else.
Exception 1 — library crate root:
// src/lib.rs
pub(crate) type InternalDrawPhase = ();Usable anywhere inside the crate, but not part of the external library API.
Exception 2 — top-level private module:
// src/lib.rs
mod internals;
// src/internals.rs
pub(crate) fn helper() {}internals is private to the crate, so pub(crate) inside it cannot leak. The item is
reachable anywhere inside the crate; the public boundary still holds.
pub(in crate::...) is a code smell: the visibility path has to reach outward to describe the
real boundary, which means the item lives too deep in the module tree. cargo mend rejects this
form as a hard error so the structural problem surfaces.
// src/feature/deep/helper.rs
pub(in crate::feature::subtree) fn helper() {}Pick one:
pub(super)when the current layout is already correct- relocate the item upward so the boundary is local, then mark it
pub(super)
pub mod is disallowed by default — it publishes the module path as part of the crate's public
API. Override per path via allow_pub_mod in mend.toml when the public path is intentional
(e.g. macro or codegen constraints).
// src/lib.rs
pub mod tools; // module path is now part of the crate's public APIA nested private module can declare pub struct Helper;, but if any parent module on the path
is private, Helper cannot escape the crate — the bare pub is broader than the boundary the
file actually participates in.
// src/lib.rs
mod support;
// src/support/mod.rs
mod helpers;
// src/support/helpers.rs
pub struct Helper;Helper is pub, but support is private, so Helper is unreachable from outside the crate.
The declared visibility doesn't match the actual reach.
Resolutions:
- make the item private
- change it to
pub(super) - move it to a better common parent if it is genuinely shared across the crate
This warning does not fire at a top-level private module — Narrow pub to
pub(crate) covers that case. At the top level, bare pub is only
correct when the crate root re-exports the item via pub use; otherwise, narrow it to
pub(crate).
When the parent module re-exports the child item, the child pub is intentional and the
warning is suppressed:
// src/private_parent/mod.rs
mod child;
pub use child::Helper;The exception applies whether the parent boundary is a mod.rs file or an ordinary file module
like markdown_file.rs. If nothing outside the parent subtree uses that re-export, the warning
still fires — and the compiler usually emits a paired unused import warning on the parent.
cargo mend --fix-pub-use is designed to repair that paired case.
When a pub item is only used inside its defining module subtree, the modifier grants no useful
access. Private visibility already lets the defining module and its descendants call the item.
// src/lib.rs
mod renderer;
// src/renderer/mod.rs
mod tests;
pub fn normalize_label(label: &str) -> String {
label.trim().to_string()
}
// src/renderer/tests.rs
fn example() {
let _ = super::normalize_label(" title ");
}The item does not need to be visible to the parent or sibling modules:
fn normalize_label(label: &str) -> String {
label.trim().to_string()
}This warning does not fire for pub items in a library crate root, for items reached from outside
their defining module subtree, or for items structurally exposed through public signatures.
cargo mend --fix can remove these pub annotations automatically.
This warning detects direct function imports and suggests importing the parent module instead, then calling the function with module qualification.
Example:
// Before:
use crate::error::report_to_mcp_error;
fn example() {
let error = report_to_mcp_error(&err);
}
// After:
use crate::error;
fn example() {
let error = error::report_to_mcp_error(&err);
}cargo mend --fix can rewrite these cases automatically. It rewrites the use statement and
qualifies all bare references in the file.
This warning detects types used with inline path qualification — both intra-crate
(crate::module::MyType, super::module::MyType) and external-crate
(ratatui::Frame, std::collections::BTreeMap, notify::WatcherKind::Variant) —
and suggests adding a use import at the top of the file instead. Trait paths in
impl Trait for Type are also covered.
Example:
// Before:
fn example() -> crate::module::MyType {
crate::module::MyType::new()
}
fn render(frame: &mut ratatui::Frame<'_>) {}
impl crate::pane::Hittable for ToastManager { /* ... */ }
// After:
use crate::module::MyType;
use crate::pane::Hittable;
use ratatui::Frame;
fn example() -> MyType {
MyType::new()
}
fn render(frame: &mut Frame<'_>) {}
impl Hittable for ToastManager { /* ... */ }cargo mend --fix can rewrite these cases automatically. It adds the use import and replaces
all inline occurrences with the bare type name. The fix is skipped when adding the import would
shadow a name the file already uses (e.g. it won't add use io::Result; if the file relies on
the prelude Result via Result::ok).
A crate::a::b::c::* import that crosses no module boundary makes the path look more global
than the relationship is. When the importer and the imported module share a parent, prefer the
local-relative form.
// src/app_tools/support/process.rs
// flagged — `cargo_detector` is a peer of `process` under `support`
use crate::app_tools::support::cargo_detector::TargetType;
// preferred
use super::cargo_detector::TargetType;cargo mend --fix rewrites these cases automatically. It preserves the original use
visibility (use, pub use, pub(crate) use, etc.) and rolls the edits back if the follow-up
cargo check fails.
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 super::super::ResolvedWidths;
// preferred — named crate path
use crate::tui::ResolvedWidths;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.
This warning is about parent facade modules that re-export everything from a child with *.
That makes the boundary harder to read because the parent module no longer says what it is actually exporting.
Prefer:
pub use child::{Helper, OtherHelper};instead of:
pub use child::*;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
mod child;
pub use child::Helper;
// src/private_parent/sibling.rs
fn use_helper() {
let _ = std::mem::size_of::<super::Helper>();
}In this example, 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.
This warning flags bare pub items that can't actually escape the crate. Writing pub(crate) at
the definition makes the real reach visible at a glance, instead of forcing the reader to walk up
the module tree.
It fires in two situations:
The crate root doesn't re-export the item.
// src/lib.rs
mod helpers;
pub use helpers::exported_fn;
// src/helpers.rs
pub fn exported_fn() {} // re-exported → must stay `pub`
pub fn internal_fn() {} // NOT re-exported → should be `pub(crate)`The parent re-exports the item as pub(crate) use. The pub(crate) use already caps reach at
the crate boundary, so the source modifier should match.
// src/keyboard/mod.rs
mod keys;
pub(crate) use keys::send_keys_handler;
// src/keyboard/keys.rs
pub fn send_keys_handler() {} // → should be `pub(crate)`Glob re-exports (pub(crate) use foo::*) are ignored — they neither trigger nor block this lint.
Items widened by a pub use somewhere in the chain are left alone.
Run cargo mend --fix to auto-fix these items to pub(crate).
This warning flags struct, union, or enum-variant fields with a pub or pub(crate) annotation
on a fully private type (a type with no pub annotation of its own). The field annotation
cannot grant any access because the containing type itself isn't visible — the annotation is
dead.
The lint deliberately does not fire on the conventional pattern of pub fields on
pub(crate) or pub(super) structs:
// Allowed — `pub` on fields of a `pub(crate)` struct is idiomatic Rust shorthand
pub(crate) struct GhRun {
pub id: u64,
pub node_id: String,
}Most large Rust codebases (rustc, cargo, tokio, serde, ratatui) write pub fields on
pub(crate) types and rely on the type to cap the reach. Flagging that pattern would push
toward a less idiomatic style.
What does get flagged: a pub field on a struct that has no visibility annotation at all.
// inside a private module
struct Hidden {
pub leaked: u32, // dead — Hidden is private, `pub` grants nothing
}After cargo mend --fix:
struct Hidden {
leaked: u32,
}Run cargo mend --fix to auto-remove dead field annotations.
This warning flags use statements written inside function bodies, closures, and other block
expressions. They should live at the top of the enclosing file or the enclosing inline
mod { ... } block instead.
// before
fn example() {
use crate::movable::Movable;
let m = Movable::default();
}// after
use crate::movable::Movable;
fn example() {
let m = Movable::default();
}cargo mend --fix lifts the use to the top of the enclosing file or inline module. The
fix is conservative:
usestatements with any attribute (most importantly#[cfg(...)]) are left in place because lifting them could change what's in scope under a different configuration.- Glob imports (
use foo::*;) inside a body are left in place; they may shadow arbitrary names at the destination. - When the bare name the in-body
useintroduces is already bound at the top of the destination — by anotherusewith a different full path, or by a struct/enum/fn/etc. defined at that level — the in-bodyuseis left in place to avoid anE0255collision. - When the bare name and full path already match an existing top-level
use, the in-body duplicate is deleted.
Run cargo mend --fix to auto-lift use statements.
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.