Jumping in a a little late, Ive been bitten by the current odd behavior multiple times in the last 2 months while refactoring and am hugely in favor of this pitch.
Thank you all for the feedback. I've updated the text of the proposal in this PR to address some of the points raised in this thread. The most significant change is that I revamped the Detailed Design section to describe in detail which modules are considered "visible" in a given source file:
Detailed design
A reference to a member in a source file will only be accepted if that member is declared in a module that is contained in the set of visible modules for that source file. A module is in the set of visible modules if any of the following statements are true:
- The module is directly imported. In other words, some import statement in the source file names the module explicitly.
- The module is directly imported from the bridging header.
- The module is in the set of modules that is re-exported by any module that is either directly imported in the file or directly imported in the bridging header.
A Swift module re-exports any modules that have been imported using the
@_exported
attribute. Clang modules list the modules that they re-export in their modulemap files, and it is common for a Clang module to re-export every module it imports usingexport *
. Re-exports are also transitive, so if moduleA
re-exports moduleB
, and moduleB
re-exports moduleC
, then declarations fromA
,B
, andC
are all in scope in a file that only importsA
directly.Note that there are some imports that are added to every source file implicitly by the compiler for normal programs. The implicitly imported modules include the standard library and the module being compiled. As a subtle consequence implicitly importing the current module, any module that is
@_exported
in any source file is also considered visible in every other source file because it is a re-export of a direct import.
After doing some work to improve the quality of the diagnostics, I'm relatively confident that we can make the experience of updating to the proposed language mode a straightforward task that mostly involves accepting fix-its.
Edit: Fixed the description of Clang module re-exports to fix an over-simplification pointed out by @allevato
Minor nit, but this is only true if the Clang module's module map contains export *
, right? It's just that most Clang modules in the world (at least, the ones in Apple's SDKs) use export *
because it mirrors the transitive behavior of traditional header inclusion.
Or are you proposing that this be the case for all Clang modules?
(Motivation: We also use export *
when we generate module maps in Bazel for Objective-C libraries, and I'd like to drop those re-exports one day if proposals like this one can get us to a point where users are much more explicit about imports and we have tooling to support that.)
You're right, I mistakenly simplified this because it's how all the Clang modules I'm used to working with behave. It is not my intent to change anything about how Clang re-exports work, only to document how they work in relation to this feature. I'll try to make this more accurate.
Update: fixed in the original post.
After gaining some experience with improving how the compiler handles diagnosing the missing imports, I'm hopeful that this won't actually be a problem. The first attempt at name lookup will filter out all member declarations that are inaccessible because they belong to modules that are not imported. If this lookup fails to resolve to a candidate, then the compiler will perform a second lookup that relaxes the import visibility constraint, effectively falling back to the lookup behavior we have today. If this second lookup succeeds, the compiler will emit a diagnostic about the missing import but compilation will proceed as if the member declaration were visible, preventing unnecessary knock-on diagnostics.