Mehdi Akiki
Rust Failure Atlas / Language and diagnostics

RFA-624 · Case file with fixtures · Case 596 of 694 · Compiler evidence

Restricted Visibility Can Name Only an Ancestor Module

Rust restricted visibility defines a lexical subtree, not a friend module. Move the item under the owning ancestor or expose a narrow operation through a shared parent.

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
Restricted visibility was treated as an arbitrary friend grant even though Rust uses one lexical ancestor subtree as the audience.
First discriminating check
Move the item under its real owner or expose one narrow operation through the smallest common ancestor instead of widening to the crate.

Rust privacy follows the module tree. A visibility restriction chooses a region rooted at an ancestor of the item. It is not a list of unrelated modules allowed to peek inside. The failing fixture places an item in land but restricts it to sibling sea, so rustc reports E0742.

pub(in path) describes a subtree

The official E0742 explanation repairs the example by moving the item beneath the module named by its restriction. The Reference lists the accepted visibility forms: public, crate, self, super, or an explicit ancestor path.

This gives access rules a lexical shape. The named ancestor and its descendants can use the item, subject to the rest of the path being accessible. A sibling is outside that subtree, even if the two modules collaborate closely.

I find this constraint healthy because it forces one ownership question: where does the shared capability belong?

Move ownership or expose behaviour

The repaired fixture moves Shark into sea, making crate::sea its ancestor. In a real codebase, moving the item is correct only if sea owns its invariant.

If a sibling needs one operation, I usually keep the data private and expose that operation at the smallest common ancestor. The parent coordinates both children without giving either broad access to the other’s representation. A narrow method also leaves room to validate input, update metrics, or change storage later.

Using pub(crate) simply to silence E0742 expands access to every module. It may be appropriate for an intentional crate-wide service, but it should not be the automatic sibling-sharing workaround.

Rust has no arbitrary friend list

Some languages let a class grant access to named friends. Rust instead encourages modules to group code sharing an invariant. Descendant test modules can see private parent items, and parent modules can build façades for children.

When two distant modules repeatedly need each other’s internals, I treat that as architecture feedback. They may belong under one subsystem, may need a shared domain type, or may be coupled through responsibilities that should become an explicit interface.

The Book’s section on separating modules into files is useful because filesystem layout can distract from the semantic tree. mod relationships determine ancestry; directories alone do not grant visibility.

Re-exports do not erase the underlying boundary

A public or restricted re-export creates another usable path only when the underlying item has sufficient visibility. It does not turn a private sibling detail into accessible state. I check both the definition’s visibility and every ancestor along the exported path.

This matters after refactors. Moving a file, changing an inline module to an external file, or introducing a façade can alter paths without changing intended ownership. Compiler failures should trigger a boundary review rather than a broad mechanical replacement.

Integration tests compile as external users and verify the public surface. Unit tests nested in descendants verify internal behaviour. I use both because a child test can access details that downstream code correctly cannot.

Generated code needs an invocation-relative plan

Macros that emit pub(in crate::somewhere) assume a specific module topology. They can fail when invoked from another branch. A macro should accept the relevant path, generate private helpers in a local child module, or expose only items whose scope is stable across invocation sites.

I test macros from several module depths and, when exported, from another crate. This catches accidental reliance on the defining crate’s tree and on edition-specific path interpretation.

Comments should say what invariant the restricted item protects. Without that explanation, future maintainers may respond to the next move by broadening visibility until compilation succeeds.

My E0742 checklist

  • What module contains the restricted item after expansion and file loading?
  • Is the path in pub(in path) an actual ancestor of that module?
  • Which subsystem owns the state or invariant behind the item?
  • Should the item move under that owner instead of widening access?
  • Does the sibling need raw representation or only one narrow operation?
  • Is the smallest common parent the correct façade location?
  • Do re-exports and every ancestor path preserve the intended scope?
  • Have unit, integration, and macro invocation tests checked the real audience?

The core principle is that restricted visibility marks an ownership subtree, not an arbitrary friendship. E0742 exposes a mismatch between code location and intended audience. I repair that mismatch by placing responsibility correctly or exposing a narrow parent-owned interface.