Mehdi Akiki
Rust Failure Atlas / Language and diagnostics

RFA-564 · Case file with fixtures · Case 536 of 694 · Compiler evidence

Rust Restricted Visibility Paths Must Name Ancestor Modules

Visibility follows the module tree, not arbitrary type ownership. Place the item in the boundary module and restrict it to a valid ancestor path.

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
Rust privacy follows lexical module ancestry and cannot use an arbitrary type as a friend-like access scope.
First discriminating check
Place the item under the module owning its invariant and restrict it to self, super, crate, or another valid ancestor path.

pub(in path) does not grant access to a type, trait, or conceptual subsystem. It defines where an item is visible in the module tree. If the path resolves to an enum, Rust reports E0577.

The failing fixture declares enum Boundary and uses crate::Boundary as the visibility scope for Token.

Privacy is organised by modules

Rust visibility answers which modules may name an item. Types do not contain lexical visibility scopes in the way classes in some languages contain private nested members.

The official E0577 page says the restricted path must identify a module. It also notes the module must be an ancestor of the item.

The Reference's visibility and privacy chapter defines public, crate-visible, parent-visible, and restricted forms.

The repaired item lives inside the boundary module

The repaired fixture creates module boundary and places Token inside it. pub(in crate::boundary) now names a valid ancestor.

The module's public issue function can use the token internally while outside modules cannot name it. This makes the boundary behavioural: callers ask the module to perform a supported operation instead of constructing its internal token.

The restriction cannot point sideways

An item cannot be declared visible only to an unrelated sibling module with pub(in crate::other). Restricted visibility scopes must be ancestors, forming progressively wider rings around the item.

If one sibling needs access, I can move shared internals into their common parent, expose a pub(super) helper, or design a narrow pub(crate) interface. The module tree should reflect the collaboration boundary.

Trying to encode an arbitrary friend relationship in a path fights Rust's privacy model.

pub(crate) is often simpler

When all workspace code inside one crate may use the item, pub(crate) communicates this directly. pub(super) exposes it to the parent module. pub(self) is equivalent to private in the current module.

I use the explicit long path when several nested modules exist and the intended ancestor is not obvious from one super. This can make future moves fail loudly rather than silently changing visibility.

The module Reference helps map inline and out-of-line modules into that ancestor tree.

Visibility is not capability security

Rust privacy is checked at compile time within the crate graph. It does not authenticate runtime users or prevent unsafe code, filesystem access, or process inspection.

I use it to maintain architectural APIs and invariants. Security boundaries still require process, protocol, and authorization controls.

This distinction prevents a private token type from being described as a security mechanism it is not.

Re-exports can intentionally widen access

A restricted item cannot be publicly re-exported beyond its own visibility. If a re-export fails, I compare both visibility scopes rather than making the original item fully public by habit.

Sometimes the correct public surface is a different wrapper or trait. Internal types can remain restricted while public operations translate at the module boundary.

Tests can enforce the intended privacy ring

Compile-pass tests inside the permitted ancestor prove internal collaborators still work, while compile-fail documentation examples can show that external construction is unavailable. I avoid testing privacy only from the item's own module because that scope always has the broadest local access. When reorganising modules, I run these boundary tests before adjusting visibility. A move that breaks pub(in path) may be telling me the ownership boundary changed; replacing it immediately with pub(crate) can silently expose more implementation detail than the refactor intended.

My E0577 checklist

  • Does the pub(in ...) path resolve to a module?
  • Is that module an ancestor of the declared item?
  • Would pub(super) or pub(crate) state the intent more clearly?
  • Should shared sibling internals move under a common parent?
  • Can a public function preserve the invariant without exposing the type?
  • Will moving the module accidentally change a relative restriction?
  • Is a re-export trying to exceed the item's visibility?
  • Am I treating compile-time privacy as runtime security?

The core principle is that visibility follows lexical module ancestry. I place internal items under the module that owns their invariant and expose the smallest behavioural surface needed outside it.