oxc-project/oxc · error · OxcDiagnostic
Dependency cycle detected
Error message
Dependency cycle detected
What it means
Diagnostic from the oxlint rule import/no-cycle (restriction category). It reports that the current module can be reached again by following its own imports: the linter walks the resolved module graph and, when a path returns to the current file, flags the import statement that starts the cycle, attaching a note that prints the full cycle with resolved paths. Cycles are a real hazard — it is common to import an `undefined` binding because the other module has not finished evaluating. Options: `maxDepth` (default unlimited), `ignoreTypes` (default true, type-only imports ignored), `ignoreExternal` (default false), `allowUnsafeDynamicCyclicDependency` (default false).
Source
Thrown at crates/oxc_linter/src/rules/import/no_cycle.rs:25
use cow_utils::CowUtils;
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::Span;
use oxc_str::CompactStr;
use rustc_hash::FxHashSet;
use schemars::JsonSchema;
use serde::Deserialize;
use crate::{
ModuleRecord,
context::LintContext,
module_graph_visitor::{ModuleGraphVisitorBuilder, ModuleGraphVisitorEvent, VisitFoldWhile},
rule::{DefaultRuleConfig, Rule},
};
fn no_cycle_diagnostic(span: Span, stack: &[(CompactStr, PathBuf)], cwd: &Path) -> OxcDiagnostic {
let cycle_description = format_cycle(stack, cwd);
OxcDiagnostic::warn("Dependency cycle detected")
.with_help("Refactor to remove the cycle. Consider extracting shared code into a separate module that both files can import.")
.with_note(format!("These paths form a cycle:\n{cycle_description}"))
.with_label(span)
}
fn self_referencing_cycle_diagnostic(span: Span, is_import: bool) -> OxcDiagnostic {
OxcDiagnostic::warn("Dependency cycle detected")
.with_help(if is_import {
"Remove the self-referencing import."
} else {
"Remove the self-referencing export and consider using a named export instead."
})
.with_label(span.primary_label("this module references itself"))
}
fn format_cycle(stack: &[(CompactStr, PathBuf)], cwd: &Path) -> String {
let mut lines = Vec::with_capacity(stack.len() * 2 + 1);
View on GitHub (pinned to e1e7af627c)
Solutions
- Break the cycle structurally: extract the shared code both modules need into a third module both import
- Convert value imports that only carry types to `import type { ... }` — ignoreTypes defaults to true so those no longer count
- Defer one direction with a dynamic `await import(...)` at the use site; if acceptable, set `allowUnsafeDynamicCyclicDependency: true`
- For noisy node_modules or deep graphs, set `ignoreExternal: true` and/or a bounded `maxDepth`
Example fix
// before (dep-a.js)
import { b } from './dep-b.js';
export function a() { return b(); }
// dep-b.js
import './dep-a.js';
export function b() { return 1; }
// after (dep-b.js no longer imports dep-a.js)
export function b() { return 1; } Defensive patterns
Strategy: validation
Validate before calling
// independent pre-check with madge (catches cycles oxlint's resolver might miss)
// npx madge --circular --extensions js,ts,tsx src/
// .oxlintrc.json guardrails: { "rules": { "import/no-cycle": ["warn", { "ignoreExternal": true, "maxDepth": 50 }] } } Prevention
- Avoid barrel index modules that siblings import back from; export directly instead
- Use `import type` for type-only cross-references (ignoreTypes defaults to true)
- Run madge --circular in CI as a second detector
- When adding a back-edge feels necessary, prefer dynamic import() at the use site
When it happens
Trigger: A imports B and B (directly or transitively) imports A; the diagnostic is emitted on A's first import statement of that chain, with the stack printed via format_cycle. Reported from run_once in crates/oxc_linter/src/rules/import/no_cycle.rs:25 when the module-graph visitor finds resolved_absolute_path equal to the current file.
Common situations: Barrel files (index.ts re-exports siblings that also import from the index); mutual imports created during refactoring (utils imports constants, constants imports utils); type-only cycles that developers expect to be ignored (covered by the default ignoreTypes:true only when `import type` is actually used).
Related errors
- Relative imports from parent directories are not allowed
- A module importing itself is not allowed
- Could not find the reuseWorker option in ${path}
- Do not assign to imported bindings
- Expected '{curr_kind}' syntax before '{prev_kind}' syntax.
AI-assisted analysis of oxc-project/oxc@e1e7af627c (2026-08-20).
Data as JSON: /api/errors/cc5b83285e7454e9.
Report an issue: GitHub.