Cyclic Module Dependencies
ID |
javascript.cyclic_package_dependencies |
Severity |
high |
Remediation Complexity |
hard |
Remediation Risk |
medium |
Remediation Effort |
high |
Resource |
Reliability |
Language |
JavaScript |
Tags |
code-style, reliability |
Description
Reports cycles in the directed graph of in-project module imports. An edge A → B is established when module A contains an import statement whose target resolves to module B on disk. When two or more modules mutually depend on each other (directly or transitively), they form a cycle.
// alpha.js
import { thing } from "./beta"; // alpha -> beta
// beta.js
import { other } from "./alpha"; // beta -> alpha (cycle!)
Rationale
Cyclic imports between modules create real runtime hazards in JavaScript:
-
whichever module finishes evaluating first sees the other as a partially-initialised object —
undefinedexports, missing functions, or stale class references — depending on which side `import`s first, -
code splitters and bundlers bundle the entire cycle into a single chunk, defeating tree-shaking,
-
unit tests cannot exercise either side in isolation,
-
refactors that split or move a module ripple through the entire cycle.
Cycles often signal that two modules should be merged or that a shared abstraction should be extracted into a third module.
Remediation
Break the cycle by:
-
Extracting an interface module — move the types or values both sides agree on into a separate module that both depend on.
-
Inverting the dependency — have the lower-level module declare an abstraction the higher-level one implements.
-
Merging modules — if two modules always change together, the cycle is evidence they were already one logical module.
// shared.js (new shared module)
export const SHARED_KEY = "...";
// alpha.js -> shared.js (no cycle)
// beta.js -> shared.js
Notes
The rule resolves only relative-path imports (./foo, ../bar); bare specifiers like lodash and scoped specifiers like @scope/pkg are external and ignored. Each scanned source file is treated as one node, identified by its canonical path with the source extension stripped — so import './foo' matches a scanned file at <dir>/foo.js, <dir>/foo.ts, etc. Imports that resolve to a directory’s index.* file are not auto-resolved (a known limitation for v1).
Mirrors java.cyclic_package_dependencies. The Tarjan SCC implementation is iterative and survives large module graphs without blowing the JVM call stack.