My general philosophy is that, while documentation is great and necessary, it tends to be better if you can get the compiler to enforce a clear set of semantics that are used by multiple people frequently, as long as the semantics themselves are as simple as is reasonable. I have tried to do multiple things to enforce these kinds of boundaries in my own code, but I always end up having trouble glancing through the resulting code (with excessively long item visibilites), leading to me often adding the wrong visibilities, or at least slowing me down on my own code within a single crate. When I try to work with multiple (private) crates, I both have to worry about the publishing workflow, and often find that I'm supporting a bunch of dead code, as there is no lint or analysis that would check that all of the pub exports in a "private" crate are used elsewhere in the workspace (and that would tend to be a hard lint to write, IIUC).
This RFC is what I came up with after some thinking. Modules are (in some cases) used for the same purpose of defining an API boundary in multiple projects, and I would imagine being able to statically enforce this encapsulation would at least reduce the maintenance burden for a bunch of projects.
To answer your question directly: The problem that vis assignments would not solve is escaping of items outside of the module boundary. The following would not cause any compile errors and/or warnings with your proposal:
// in src/parent_mod/my_mod.rs
// We want only the things in the api visibility range to be available in the rest of the crate. Other
// items should not be exported from this module
vis api = vis!(crate);
// This is great: It will have the expected visibility.
vis(api) fn exported_fn() { /* ... */ }
// Also fine. Does not escape my_mod.rs
fn local_private_fn() { /* ... */ }
// Problematic: These items escape, even though it's not part of the module boundary
pub fn why_am_i_public() { /* ... */ }
pub(crate) fn better_but_no_cigar() { /* ... */ }
pub(in crate::parent_mod) fn use_me_other_mod() { /* ... */ }
Correct me if I'm wrong, but all of the above should theoretically compile with vis assignments. The problem is now there are some kinds of migrations and refactorings that are more difficult.
First, if better_but_no_cigar() is not part of our expected API (perhaps forgotten as part of a previous migration, where it was supposed to be made module private), and then we decide to delete the code, or modify the signature, we can find that there is other code elsewhere in the crate that uses it. This means that you have to read the code that's using it, and migrate that before making the change. This is annoying if you're the sole developer on a project, and could even be actively painful in a team setting, where the the code using it is not owned by you.
Second, if we want to move the module to another path, the use_me_other_mod() may no longer be accurate. We will update the vis api = ... declaration, but we will forget the other. If this is a large piece of code, this may not see that the visibility needs to be updated, and may be left as it is, leaving a trap for future maintenance.
Third, if we want to extract this to a crate, the better_but_no_cigar() declaration will compile naturally, but it isn't actually to the crate that previously depended on it, so it would create a build failure in the new crate (with the above migration issues). The use_me_other_mod() decl would just fail to build, as there is no crate::parent_mod path in this crate.
Fourth (and possibly least), the why_am_i_public() decl will be (potentially) exported from the crate. This will often be found via lint, but if by mistake someone ends up pub useing it somewhere, then it will become part of your public API without being intended.
The critical check is that those problematic visibilities all escape from the current module, which from my understanding of the compiler internals, is reasonably easy to do at the moment (since all visibilities are reduced to either pub, or pub(in <path>)). With my pub(api) proposal:
// in src/parent_mod/my_mod.rs
// We want only the things in the api visibility range to be available in the rest of the crate. Other
// items should not be exported from this module
// Mark this module as an API boundary, used by `pub(api)`
#![api_boundary]
// This is great: It will have the expected visibility.
pub(api) fn exported_fn() { /* ... */ }
// Also fine. Does not escape my_mod.rs
fn local_private_fn() { /* ... */ }
// ERRORS: These items would escape, so we cause errors
// `pub` item escapes from `crate::parent_mod::my_mod`
pub fn why_am_i_public() { /* ... */ }
// Path `crate` is not equal to or under `crate::parent_mod::my_mod`
pub(crate) fn better_but_no_cigar() { /* ... */ }
// Path `crate::parent_mod` is not equal to or under `crate::parent_mod::my_mod`
pub(in crate::parent_mod) fn use_me_other_mod() { /* ... */ }
// Resolves to path `crate::parent_mod`, which is not equal to or under
// `crate::parent_mod::my_mod`
pub(super) fn use_me_parent() { /* ... */ }
So while you can certainly make some complex visibility patterns more usable with vis assignments, these refactoring and migration hazards are still possible.