cargo-mend 0.3.1

Opinionated visibility auditing for Rust crates and workspaces
cargo-mend-0.3.1 is not a library.

cargo-mend

Crates.io MIT/Apache 2.0 Crates.io CI

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 pub in 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 pub can 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 pub may 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 pub but 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 forbidden
  • pub mod requires an explicit allowlist entry

Warnings:

  • pub in a nested child file where compiler analysis shows the item should probably be narrower than pub
  • parent module pub use * re-exports that should be explicit

If you are new to Rust visibility, the important idea is this:

  • pub does 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 pub and still not actually be reachable from outside the crate

Config

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 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

rustup component add rustc-dev --toolchain nightly
cargo +nightly install cargo-mend

Option B: stable toolchain with bootstrap override

RUSTC_BOOTSTRAP=1 cargo install cargo-mend

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:

rustup component add rustc-dev

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

cargo mend
cargo mend --fail-on-warn
cargo mend --fix
cargo mend --json
cargo mend --manifest-path path/to/Cargo.toml

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-path to choose an explicit crate or workspace root
  • --fix only rewrites the import-shortening cases that cargo-mend can prove are safe
  • if a --fix run would leave the crate failing cargo check, cargo-mend restores the original files automatically
  • if there is nothing fixable, cargo-mend says so after the report summary

Intended workflow

Use this as a migration aid and CI guard:

  1. fail immediately on forbidden visibility forms
  2. review suspicious pub
  3. let cargo mend --fix rewrite the straightforward local-import paths it knows how to fix
  4. keep repo-specific exceptions small and explicit

The usual review flow is:

  1. ask whether the item is truly part of the module's API
  2. if not, try private or pub(super) in a nested module
  3. if the item lives in a top-level private module, plain pub may already be the correct crate-internal boundary
  4. if pub(super) is too narrow, move the item to a better common parent
  5. 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
mod helpers;

// src/feature/helpers.rs
pub(crate) fn helper() {}

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(super) fn helper() {}

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(crate) 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
mod internals;

// src/internals.rs
pub(crate) fn helper() {}

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(in crate::feature::subtree) fn helper() {}

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(super) fn helper() {}

or:

// src/feature/helper.rs
pub(super) fn helper() {}

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 tools;

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.

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
mod support;

// src/support/mod.rs
mod helpers;

// src/support/helpers.rs
pub struct Helper;

If you are new to Rust, it is easy to read pub struct Helper; and think:

  • "Helper is public, so other crates can use it"

But Rust does not work that way. The full path must be public too.

In this example:

  • Helper is marked pub
  • but support is private
  • so Helper is 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.rs file
  • or an ordinary file module like markdown_file.rs

For example:

// src/private_parent/mod.rs
mod child;
pub use child::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 use is stale
  • cargo-mend warns 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(super) struct Helper;

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 child::*;

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 use is 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 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 child::{Helper, OtherHelper};

instead of:

pub use child::*;

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.rs
  • process.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 crate::app_tools::support::cargo_detector::TargetType;

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.rs and cargo_detector.rs are peers under support
  • the import is not crossing to a different domain
  • the shorter local-relative path is clearer

A better import is:

use super::cargo_detector::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 check fails

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 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.

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 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.

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:
fn example() -> crate::module::MyType {
    crate::module::MyType::new()
}

// After:
use crate::module::MyType;

fn example() -> MyType {
    MyType::new()
}

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

at your option.