Swift’s permissive nature when it comes to module dependencies has always annoyed me and made my life a bit harder than I expected. Of course, I’m talking about transitive module imports.
You know the situation: there is a ModuleA which is a dependency of ModuleB, and if you declare ModuleB as a dependency of ModuleC, ModuleC can import ModuleA even though it never declared ModuleA as its own direct dependency.
There are a lot of reasons why you might not want that, from making dependencies harder to visualize to enforcing a certain module structure, and so on.
Unfortunately, Swift itself does not provide a mechanism to enforce direct module dependencies—or at least I don’t know of one.
Layering check via rules_swift
One of the reasons I like working with Bazel is the flexibility it provides. One example of that is a feature in rules_swift called layering check, which prevents importing modules that aren’t declared as direct dependencies.
It was introduced a couple of months ago in #1780.
So, for example:
swift_library(
name = "Bottom",
srcs = ["Bottom.swift"],
module_name = "Bottom",
)
swift_library(
name = "Middle",
srcs = ["Middle.swift"],
module_name = "Middle",
deps = [":Bottom"],
)
swift_library(
name = "Top",
srcs = ["Top.swift"],
module_name = "Top",
deps = [":Middle"],
)
Now let’s say Top.swift tries to do this:
import Middle
import Bottom
Even though Bottom is available transitively through Middle, Top never declared it as a direct dependency.
With the Swift layering check enabled, building it:
bazel build //:Top --features=swift.layering_check_swift
should fail with an error resembling:
Layering violation in //path/to/package:Top
The following modules were imported, but they are not direct dependencies:
Bottom
Please add the correct 'deps' ...
And that’s exactly what I want.
To fix it, simply declare Bottom as a direct Bazel dependency:
swift_library(
name = "Top",
srcs = ["Top.swift"],
module_name = "Top",
deps = [
":Bottom",
":Middle",
],
)
Conclusion
This is one of those features I wish more people knew about and used. It gives you much tighter control over your dependency graph and makes it harder for accidental dependencies to get imported.