diff --git a/apps/oxlint/src-js/js_config.ts b/apps/oxlint/src-js/js_config.ts index d91ed5de5eada..d1a867b001088 100644 --- a/apps/oxlint/src-js/js_config.ts +++ b/apps/oxlint/src-js/js_config.ts @@ -13,18 +13,35 @@ type LoadJsConfigsResult = | { Error: string }; function validateConfigExtends(root: object): void { - const visited = new Set(); - const stack = new Set(); + const visited = new WeakSet(); + const inStack = new WeakSet(); + const stackObjects: object[] = []; + const stackPaths: string[] = []; + + const formatCycleError = (refPath: string, cycleStart: string, idx: number): string => { + const cycle = + idx === -1 + ? `${cycleStart} -> ${cycleStart}` + : [...stackPaths.slice(idx), cycleStart].join(" -> "); + + return ( + "`extends` contains a circular reference.\n\n" + + `${refPath} points back to ${cycleStart}\n` + + `Cycle: ${cycle}` + ); + }; - const visit = (config: object): void => { + const visit = (config: object, path: string): void => { if (visited.has(config)) return; - visited.add(config); - - if (stack.has(config)) { - // Defensive: this should never happen because we check before recursing. - throw new Error("`extends` contains a circular reference."); + if (inStack.has(config)) { + const idx = stackObjects.indexOf(config); + const cycleStart = idx === -1 ? "" : stackPaths[idx]; + throw new Error(formatCycleError(path, cycleStart, idx)); } - stack.add(config); + + inStack.add(config); + stackObjects.push(config); + stackPaths.push(path); const maybeExtends = (config as Record).extends; if (maybeExtends !== undefined) { @@ -40,17 +57,25 @@ function validateConfigExtends(root: object): void { `\`extends[${i}]\` must be a config object (strings/paths are not supported).`, ); } - if (stack.has(item)) { - throw new Error("`extends` contains a circular reference."); + + const itemPath = `${path}.extends[${i}]`; + if (inStack.has(item)) { + const idx = stackObjects.indexOf(item); + const cycleStart = idx === -1 ? "" : stackPaths[idx]; + throw new Error(formatCycleError(itemPath, cycleStart, idx)); } - visit(item); + + visit(item, itemPath); } } - stack.delete(config); + inStack.delete(config); + stackObjects.pop(); + stackPaths.pop(); + visited.add(config); }; - visit(root); + visit(root, ""); } /** diff --git a/apps/oxlint/test/fixtures/js_config_extends_cycle/output.snap.md b/apps/oxlint/test/fixtures/js_config_extends_cycle/output.snap.md index ae0d0ec92e8a1..d12d88e047685 100644 --- a/apps/oxlint/test/fixtures/js_config_extends_cycle/output.snap.md +++ b/apps/oxlint/test/fixtures/js_config_extends_cycle/output.snap.md @@ -8,6 +8,9 @@ Failed to parse oxlint configuration file. x Failed to load config: /oxlint.config.ts | | Error: `extends` contains a circular reference. + | + | .extends[0].extends[0].extends[0] points back to .extends[0] + | Cycle: .extends[0] -> .extends[0].extends[0] -> .extends[0] ``` # stderr