Skip to content

Commit bb1eb97

Browse files
committed
feat(linter): improve diagnostic message for circular configs (oxc-project#18947)
1 parent 3184f36 commit bb1eb97

2 files changed

Lines changed: 42 additions & 14 deletions

File tree

apps/oxlint/src-js/js_config.ts

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,35 @@ type LoadJsConfigsResult =
1313
| { Error: string };
1414

1515
function validateConfigExtends(root: object): void {
16-
const visited = new Set<object>();
17-
const stack = new Set<object>();
16+
const visited = new WeakSet<object>();
17+
const inStack = new WeakSet<object>();
18+
const stackObjects: object[] = [];
19+
const stackPaths: string[] = [];
20+
21+
const formatCycleError = (refPath: string, cycleStart: string, idx: number): string => {
22+
const cycle =
23+
idx === -1
24+
? `${cycleStart} -> ${cycleStart}`
25+
: [...stackPaths.slice(idx), cycleStart].join(" -> ");
26+
27+
return (
28+
"`extends` contains a circular reference.\n\n" +
29+
`${refPath} points back to ${cycleStart}\n` +
30+
`Cycle: ${cycle}`
31+
);
32+
};
1833

19-
const visit = (config: object): void => {
34+
const visit = (config: object, path: string): void => {
2035
if (visited.has(config)) return;
21-
visited.add(config);
22-
23-
if (stack.has(config)) {
24-
// Defensive: this should never happen because we check before recursing.
25-
throw new Error("`extends` contains a circular reference.");
36+
if (inStack.has(config)) {
37+
const idx = stackObjects.indexOf(config);
38+
const cycleStart = idx === -1 ? "<unknown>" : stackPaths[idx];
39+
throw new Error(formatCycleError(path, cycleStart, idx));
2640
}
27-
stack.add(config);
41+
42+
inStack.add(config);
43+
stackObjects.push(config);
44+
stackPaths.push(path);
2845

2946
const maybeExtends = (config as Record<string, unknown>).extends;
3047
if (maybeExtends !== undefined) {
@@ -40,17 +57,25 @@ function validateConfigExtends(root: object): void {
4057
`\`extends[${i}]\` must be a config object (strings/paths are not supported).`,
4158
);
4259
}
43-
if (stack.has(item)) {
44-
throw new Error("`extends` contains a circular reference.");
60+
61+
const itemPath = `${path}.extends[${i}]`;
62+
if (inStack.has(item)) {
63+
const idx = stackObjects.indexOf(item);
64+
const cycleStart = idx === -1 ? "<unknown>" : stackPaths[idx];
65+
throw new Error(formatCycleError(itemPath, cycleStart, idx));
4566
}
46-
visit(item);
67+
68+
visit(item, itemPath);
4769
}
4870
}
4971

50-
stack.delete(config);
72+
inStack.delete(config);
73+
stackObjects.pop();
74+
stackPaths.pop();
75+
visited.add(config);
5176
};
5277

53-
visit(root);
78+
visit(root, "<root>");
5479
}
5580

5681
/**

apps/oxlint/test/fixtures/js_config_extends_cycle/output.snap.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ Failed to parse oxlint configuration file.
88
x Failed to load config: <fixture>/oxlint.config.ts
99
|
1010
| Error: `extends` contains a circular reference.
11+
|
12+
| <root>.extends[0].extends[0].extends[0] points back to <root>.extends[0]
13+
| Cycle: <root>.extends[0] -> <root>.extends[0].extends[0] -> <root>.extends[0]
1114
```
1215

1316
# stderr

0 commit comments

Comments
 (0)