You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Boundary rules do not follow re-export chains (docs say they do) #2939
The docs say boundary checks follow re-export chains:
Fallow follows re-export chains when it checks boundaries. If shared/index.ts re-exports a symbol from core/, fallow treats an import of that symbol as an import from core/, not shared/.
(https://docs.fallow.tools/explanations/fallow-vs-linters#architecture-boundary-violations; the comparison table on the same page also says "Fallow: ... follows re-export chains")
In 3.29.0 this does not happen. A ui file that imports a core symbol through a shared barrel gets no boundary-violation. A direct ui -> core import in the same project is flagged. The result is the same for named re-exports and export *, and for relative and tsconfig-alias specifiers.
Fallow does resolve the chain for dead-code analysis. dead-code --trace lists the ui files as direct references of the core export, re-exported through the shared barrels. Only the boundary pass ignores the chain.
Five boundary violations: the direct control import, plus the four ui files that consume a core symbol through a shared re-export. The docs say these should count as imports from core.
The re-export edges do resolve. If I add { "from": "shared", "allow": [] }, the four shared -> core re-export lines are reported, for both the alias and the relative forms. Nothing is reported as unresolved or unused, and --trace shows that fallow knows who consumes the symbol:
$ npx -y fallow@3.29.0 dead-code --trace src/core/secret.ts:coreValue
USED coreValue in src/core/secret.ts
Reason: Used by 2 file(s), re-exported through 4 barrel(s)
2 direct reference(s):
-> src/ui/via-named-alias.ts (named import)
-> src/ui/via-named-relative.ts (named import)
Re-exported through:
-> src/shared/named-alias.ts as 'coreValue' (1 ref(s))
-> src/shared/named-relative.ts as 'coreValue' (1 ref(s))
-> src/shared/star-alias.ts as 'coreValue' (0 ref(s))
-> src/shared/star-relative.ts as 'coreValue' (0 ref(s))
Environment
fallow 3.29.0 (the npm prebuilt, @fallow-cli/darwin-arm64, signature verified)
macOS arm64
node_modules is not installed in the fixture (fallow warns about this). The fixture has no package imports.
I also checked the source at the v3.30.0 release commit: crates/core/src/analyze/boundary.rs did not change in that release, so 3.30.0 should behave the same.
Cause (from reading the source)
find_boundary_violations in crates/core/src/analyze/boundary.rs loops over graph.outgoing_edge_summaries(node.file_id) and classifies only the direct edge.target. It never looks at the target's re_exports (ReExportEdge { source_file, imported_name, exported_name, .. }). An import of a barrel is therefore judged by the barrel's zone. No test in crates/core/tests/integration_test/boundary_violations.rs or in the tests/fixtures/boundary-* fixtures covers a re-export chain.
Possible fix
For each edge whose target module re-exports the imported symbols, follow each symbol through the target's re-export edges (using the resolution the unused-exports pass already does, including export * and cycle guards) to the module that defines it. Then run is_import_allowed(from_zone, origin_zone) on the origin as well as on the direct target. Report the violation at the import line, and possibly name the barrel in the message, e.g. ui → core (via src/shared/named-alias.ts).
Two points to consider:
Unzoned barrels. Support auto-discovery of architecture boundaries #368 says top-level features/index.ts barrels "stay unclassified by design so barrels can re-export children without false positives". With chain-following, a sibling feature that imports features/index.ts would be attributed to the child feature it uses. That is probably the intended result, but it changes findings for existing bulletproof users, so it may need a changelog note, or an opt-in or opt-out setting at first.
Type-only re-exports (export type { X } from) should keep allowTypeOnly semantics along the chain.
If the current behaviour is intended, the docs should be corrected instead. The "follows re-export chains" claim on the fallow-vs-linters page is the main thing that separates fallow from eslint-plugin-boundaries and no-restricted-paths in that comparison.
With fallow 3.31.0 (npm install fallow@3.31.0), a ui file that imports a core symbol through a shared barrel gets a boundary-violation. Fallow now judges a named or default import against the zone of the module that declares the symbol, so the behavior matches the docs.
The finding names the origin module in to_path and to_zone. The new optional via_path field names the barrel, and the human, SARIF, CodeClimate, markdown and LSP messages name it too. Baseline keys stay from_path->to_path. Fallow still judges namespace and side-effect imports by the direct target. This can add findings to an existing configuration, and --save-baseline keeps the old result.
Summary
The docs say boundary checks follow re-export chains:
In 3.29.0 this does not happen. A
uifile that imports acoresymbol through asharedbarrel gets noboundary-violation. A directui -> coreimport in the same project is flagged. The result is the same for named re-exports andexport *, and for relative and tsconfig-alias specifiers.Fallow does resolve the chain for dead-code analysis.
dead-code --tracelists theuifiles as direct references of thecoreexport, re-exported through thesharedbarrels. Only the boundary pass ignores the chain.Reproduction
Directory layout:
package.json{ "name": "reexport-boundaries-repro", "version": "0.0.0", "private": true, "type": "module", "main": "src/index.ts" }tsconfig.json{ "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "Bundler", "strict": true, "baseUrl": ".", "paths": { "@/*": ["./src/*"] } }, "include": ["src"] }.fallowrc.json: the same zones and rule as the docs example, pluscore:{ "entry": ["src/index.ts"], "boundaries": { "zones": [ { "name": "ui", "patterns": ["src/ui/**"] }, { "name": "core", "patterns": ["src/core/**"] }, { "name": "shared", "patterns": ["src/shared/**"] } ], "rules": [{ "from": "ui", "allow": ["shared"] }] } }src/core/secret.tssrc/core/direct.tssrc/shared/util.tssrc/shared/named-relative.tssrc/shared/star-relative.tssrc/shared/named-alias.tssrc/shared/star-alias.tssrc/ui/control-direct.ts: the control case, which is flagged as expectedsrc/ui/allowed-shared.ts: a genuinesharedimport that is allowedsrc/ui/via-named-relative.tssrc/ui/via-star-relative.tssrc/ui/via-named-alias.tssrc/ui/via-star-alias.tssrc/index.tsCommand:
Expected
Five boundary violations: the direct control import, plus the four
uifiles that consume acoresymbol through asharedre-export. The docs say these should count as imports fromcore.src/ui/allowed-shared.tsshould stay clean.Actual
Only the direct import is reported:
The re-export edges do resolve. If I add
{ "from": "shared", "allow": [] }, the fourshared -> corere-export lines are reported, for both the alias and the relative forms. Nothing is reported as unresolved or unused, and--traceshows that fallow knows who consumes the symbol:Environment
@fallow-cli/darwin-arm64, signature verified)node_modulesis not installed in the fixture (fallow warns about this). The fixture has no package imports.crates/core/src/analyze/boundary.rsdid not change in that release, so 3.30.0 should behave the same.Cause (from reading the source)
find_boundary_violationsincrates/core/src/analyze/boundary.rsloops overgraph.outgoing_edge_summaries(node.file_id)and classifies only the directedge.target. It never looks at the target'sre_exports(ReExportEdge { source_file, imported_name, exported_name, .. }). An import of a barrel is therefore judged by the barrel's zone. No test incrates/core/tests/integration_test/boundary_violations.rsor in thetests/fixtures/boundary-*fixtures covers a re-export chain.Possible fix
For each edge whose target module re-exports the imported symbols, follow each symbol through the target's re-export edges (using the resolution the unused-exports pass already does, including
export *and cycle guards) to the module that defines it. Then runis_import_allowed(from_zone, origin_zone)on the origin as well as on the direct target. Report the violation at the import line, and possibly name the barrel in the message, e.g.ui → core (via src/shared/named-alias.ts).Two points to consider:
features/index.tsbarrels "stay unclassified by design so barrels can re-export children without false positives". With chain-following, a sibling feature that importsfeatures/index.tswould be attributed to the child feature it uses. That is probably the intended result, but it changes findings for existingbulletproofusers, so it may need a changelog note, or an opt-in or opt-out setting at first.export type { X } from) should keepallowTypeOnlysemantics along the chain.If the current behaviour is intended, the docs should be corrected instead. The "follows re-export chains" claim on the fallow-vs-linters page is the main thing that separates fallow from
eslint-plugin-boundariesandno-restricted-pathsin that comparison.Related