RFA-610 · Case file with fixtures · Case 582 of 694 · Compiler evidence
Rust Restricted Visibility Paths Require pub(in ...) Syntax
Restricted visibility is lexical module policy. Use pub(crate), pub(super), pub(self), or pub(in ancestor), and keep ownership boundaries deliberate.
- Reviewed
- Rust
- Rust 1.98.1, edition 2024
- Targets
- all Rust targets
- Profiles
- dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- A custom module name was placed in shorthand visibility syntax even though Rust privacy follows explicit lexical ancestor scopes.
- First discriminating check
- Choose private, crate, parent, or one valid ancestor scope and spell a custom restriction as pub(in path).
Rust has dedicated shorthand for crate, parent, and current-module visibility. A custom module path uses pub(in path). The failing fixture writes pub(storage) and receives E0704 because that is not a valid restriction form.
Visibility syntax encodes lexical scope
The accepted forms include ordinary pub, pub(crate), pub(super), pub(self), and pub(in path). The path restriction must refer to an allowed ancestor module rather than an arbitrary friend module elsewhere.
The official E0704 explanation shows adding in for a module path. Rust's visibility reference defines the full restrictions and path rules.
This is not only grammar. Rust privacy follows the module tree, so restricted access communicates which enclosing layer owns an item.
The repair uses an explicit ancestor path
The repaired fixture declares pub(in crate::storage) struct Handle. That scope is effectively local to the module in this small example, and a public open function uses the handle internally.
If the intent is simply current-module visibility, leaving off pub is clearer. The verbose form is most useful when a nested submodule exposes an item to a specific ancestor but not to the whole crate.
I select the narrowest visibility that matches real collaboration. pub(crate) is common for application internals shared across top-level modules. Public library APIs use full pub only when downstream crates should rely on them.
Privacy can enforce architectural direction
A storage implementation may expose a stable repository capability upward while keeping connection handles and query builders inside its subtree. Restricted visibility prevents other modules from bypassing policy through low-level types.
This makes module boundaries part of compile-time architecture. It is not absolute security—code in the same crate can be changed—but it stops accidental dependencies and makes reviews easier.
I avoid a “utilities” ancestor that everything can access. Broad internal visibility becomes another public-like surface without documentation. Items should live near the invariant they support.
Module moves can invalidate restrictions
Because pub(in path) names lexical ancestry, moving a module may require updating visibility. That compile failure is useful: the new location may change which layer should own access.
I do not immediately replace a broken precise path with pub(crate). I review the architecture after the move and expose a façade if the old relationship no longer fits.
In edition 2018 and later, restricted paths follow current path rules and are normally rooted with crate, self, or super as appropriate. Generated code should emit edition-compatible paths based on the invocation context.
Visibility and re-export are separate
An item needs sufficient underlying visibility before a re-export can make it reachable through another path. Re-exporting does not bypass a private or narrower item. E0704 fixes declaration syntax, but public façade tests should verify the intended final path.
Fields and methods have their own visibility decisions. A restricted struct does not automatically make all fields available at the same scope if they are more private. I inspect the complete construction and behaviour surface.
Unit tests nested inside a module can see private ancestors differently from integration tests compiled as downstream crates. I use both to validate internal invariants and public usability.
A useful review question
When I see a restricted item, I ask which operation would become invalid if an unrelated module called it. This makes the boundary concrete. A parser may expose unchecked construction only to its validation subtree, while the rest of the crate receives validated values. The syntax then records an invariant, not merely a preference about tidiness. If nobody can name the protected invariant, the restriction may be accidental complexity and a smaller private helper or a documented crate API may be clearer.
My E0704 checklist
- Is the code using one of Rust's accepted visibility forms?
- Does a custom module path include the
inkeyword? - Is that path a valid lexical ancestor under the current edition?
- Would private, super, or crate visibility express the intent more simply?
- What invariant is protected by keeping the item inside a subtree?
- Did a module move change the correct ownership boundary?
- Does a re-export have enough underlying visibility?
- Do integration tests confirm only the intended downstream surface?
The core principle is that visibility follows the module ownership tree, not arbitrary friend lists. E0704 catches invalid syntax, then the real work is choosing the smallest ancestor scope that keeps collaboration possible without dissolving the architecture.