diff --git a/apps/oxlint/src-js/js_config.ts b/apps/oxlint/src-js/js_config.ts index 6ddcc43073b63..d91ed5de5eada 100644 --- a/apps/oxlint/src-js/js_config.ts +++ b/apps/oxlint/src-js/js_config.ts @@ -12,6 +12,47 @@ type LoadJsConfigsResult = | { Failures: { path: string; error: string }[] } | { Error: string }; +function validateConfigExtends(root: object): void { + const visited = new Set(); + const stack = new Set(); + + const visit = (config: object): 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."); + } + stack.add(config); + + const maybeExtends = (config as Record).extends; + if (maybeExtends !== undefined) { + if (!Array.isArray(maybeExtends)) { + throw new Error( + "`extends` must be an array of config objects (strings/paths are not supported).", + ); + } + for (let i = 0; i < maybeExtends.length; i++) { + const item = maybeExtends[i]; + if (typeof item !== "object" || item === null || Array.isArray(item)) { + throw new Error( + `\`extends[${i}]\` must be a config object (strings/paths are not supported).`, + ); + } + if (stack.has(item)) { + throw new Error("`extends` contains a circular reference."); + } + visit(item); + } + } + + stack.delete(config); + }; + + visit(root); +} + /** * Load JavaScript config files in parallel. * @@ -43,6 +84,8 @@ export async function loadJsConfigs(paths: string[]): Promise { ); } + validateConfigExtends(config as object); + return { path, config }; }), ); diff --git a/apps/oxlint/test/fixtures/js_config_extends_cycle/files/test.js b/apps/oxlint/test/fixtures/js_config_extends_cycle/files/test.js new file mode 100644 index 0000000000000..eab74692130a6 --- /dev/null +++ b/apps/oxlint/test/fixtures/js_config_extends_cycle/files/test.js @@ -0,0 +1 @@ +debugger; 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 new file mode 100644 index 0000000000000..ae0d0ec92e8a1 --- /dev/null +++ b/apps/oxlint/test/fixtures/js_config_extends_cycle/output.snap.md @@ -0,0 +1,15 @@ +# Exit code +1 + +# stdout +``` +Failed to parse oxlint configuration file. + + x Failed to load config: /oxlint.config.ts + | + | Error: `extends` contains a circular reference. +``` + +# stderr +``` +``` diff --git a/apps/oxlint/test/fixtures/js_config_extends_cycle/oxlint.config.ts b/apps/oxlint/test/fixtures/js_config_extends_cycle/oxlint.config.ts new file mode 100644 index 0000000000000..469bf3331c54f --- /dev/null +++ b/apps/oxlint/test/fixtures/js_config_extends_cycle/oxlint.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "#oxlint"; + +const a = defineConfig({}) as any; +const b = defineConfig({ extends: [a] }) as any; +a.extends = [b]; + +export default defineConfig({ + extends: [a], +}); diff --git a/apps/oxlint/test/fixtures/js_config_extends_invalid_non_array/files/test.js b/apps/oxlint/test/fixtures/js_config_extends_invalid_non_array/files/test.js new file mode 100644 index 0000000000000..eab74692130a6 --- /dev/null +++ b/apps/oxlint/test/fixtures/js_config_extends_invalid_non_array/files/test.js @@ -0,0 +1 @@ +debugger; diff --git a/apps/oxlint/test/fixtures/js_config_extends_invalid_non_array/output.snap.md b/apps/oxlint/test/fixtures/js_config_extends_invalid_non_array/output.snap.md new file mode 100644 index 0000000000000..bcbb36a3affbb --- /dev/null +++ b/apps/oxlint/test/fixtures/js_config_extends_invalid_non_array/output.snap.md @@ -0,0 +1,15 @@ +# Exit code +1 + +# stdout +``` +Failed to parse oxlint configuration file. + + x Failed to load config: /oxlint.config.ts + | + | Error: `extends` must be an array of config objects (strings/paths are not supported). +``` + +# stderr +``` +``` diff --git a/apps/oxlint/test/fixtures/js_config_extends_invalid_non_array/oxlint.config.ts b/apps/oxlint/test/fixtures/js_config_extends_invalid_non_array/oxlint.config.ts new file mode 100644 index 0000000000000..5e0273d57e801 --- /dev/null +++ b/apps/oxlint/test/fixtures/js_config_extends_invalid_non_array/oxlint.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from "#oxlint"; + +export default defineConfig({ + // @ts-expect-error - we are testing invalid config + extends: {}, +}); diff --git a/apps/oxlint/test/fixtures/js_config_extends_invalid_string/files/test.js b/apps/oxlint/test/fixtures/js_config_extends_invalid_string/files/test.js new file mode 100644 index 0000000000000..eab74692130a6 --- /dev/null +++ b/apps/oxlint/test/fixtures/js_config_extends_invalid_string/files/test.js @@ -0,0 +1 @@ +debugger; diff --git a/apps/oxlint/test/fixtures/js_config_extends_invalid_string/output.snap.md b/apps/oxlint/test/fixtures/js_config_extends_invalid_string/output.snap.md new file mode 100644 index 0000000000000..bacb2f99d5b1d --- /dev/null +++ b/apps/oxlint/test/fixtures/js_config_extends_invalid_string/output.snap.md @@ -0,0 +1,15 @@ +# Exit code +1 + +# stdout +``` +Failed to parse oxlint configuration file. + + x Failed to load config: /oxlint.config.ts + | + | Error: `extends[0]` must be a config object (strings/paths are not supported). +``` + +# stderr +``` +``` diff --git a/apps/oxlint/test/fixtures/js_config_extends_invalid_string/oxlint.config.ts b/apps/oxlint/test/fixtures/js_config_extends_invalid_string/oxlint.config.ts new file mode 100644 index 0000000000000..a89e3c5538a89 --- /dev/null +++ b/apps/oxlint/test/fixtures/js_config_extends_invalid_string/oxlint.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from "#oxlint"; + +export default defineConfig({ + // @ts-expect-error - we are testing invalid config + extends: ["./base.ts"], +});