diff --git a/lib/config/flat-config-schema.js b/lib/config/flat-config-schema.js index df850995d87..911d159d93b 100644 --- a/lib/config/flat-config-schema.js +++ b/lib/config/flat-config-schema.js @@ -5,6 +5,16 @@ "use strict"; +//----------------------------------------------------------------------------- +// Requirements +//----------------------------------------------------------------------------- + +/* + * Note: This can be removed in ESLint v9 because structuredClone is available globally + * starting in Node.js v17. + */ +const structuredClone = require("@ungap/structured-clone").default; + //----------------------------------------------------------------------------- // Type Definitions //----------------------------------------------------------------------------- @@ -119,7 +129,7 @@ function normalizeRuleOptions(ruleOptions) { : [ruleOptions]; finalOptions[0] = ruleSeverities.get(finalOptions[0]); - return finalOptions; + return structuredClone(finalOptions); } //----------------------------------------------------------------------------- @@ -378,48 +388,57 @@ const rulesSchema = { ...second }; - for (const ruleId of Object.keys(result)) { - - // avoid hairy edge case - if (ruleId === "__proto__") { - - /* eslint-disable-next-line no-proto -- Though deprecated, may still be present */ - delete result.__proto__; - continue; - } - - result[ruleId] = normalizeRuleOptions(result[ruleId]); - - /* - * If either rule config is missing, then the correct - * config is already present and we just need to normalize - * the severity. - */ - if (!(ruleId in first) || !(ruleId in second)) { - continue; - } - const firstRuleOptions = normalizeRuleOptions(first[ruleId]); - const secondRuleOptions = normalizeRuleOptions(second[ruleId]); + for (const ruleId of Object.keys(result)) { - /* - * If the second rule config only has a severity (length of 1), - * then use that severity and keep the rest of the options from - * the first rule config. - */ - if (secondRuleOptions.length === 1) { - result[ruleId] = [secondRuleOptions[0], ...firstRuleOptions.slice(1)]; - continue; + try { + + // avoid hairy edge case + if (ruleId === "__proto__") { + + /* eslint-disable-next-line no-proto -- Though deprecated, may still be present */ + delete result.__proto__; + continue; + } + + result[ruleId] = normalizeRuleOptions(result[ruleId]); + + /* + * If either rule config is missing, then the correct + * config is already present and we just need to normalize + * the severity. + */ + if (!(ruleId in first) || !(ruleId in second)) { + continue; + } + + const firstRuleOptions = normalizeRuleOptions(first[ruleId]); + const secondRuleOptions = normalizeRuleOptions(second[ruleId]); + + /* + * If the second rule config only has a severity (length of 1), + * then use that severity and keep the rest of the options from + * the first rule config. + */ + if (secondRuleOptions.length === 1) { + result[ruleId] = [secondRuleOptions[0], ...firstRuleOptions.slice(1)]; + continue; + } + + /* + * In any other situation, then the second rule config takes + * precedence. That means the value at `result[ruleId]` is + * already correct and no further work is necessary. + */ + } catch (ex) { + throw new Error(`Key "${ruleId}": ${ex.message}`, { cause: ex }); } - /* - * In any other situation, then the second rule config takes - * precedence. That means the value at `result[ruleId]` is - * already correct and no further work is necessary. - */ } return result; + + }, validate(value) { diff --git a/package.json b/package.json index ece3957419e..8f4a112e3cd 100644 --- a/package.json +++ b/package.json @@ -64,9 +64,10 @@ "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.2", "@eslint/js": "8.51.0", - "@humanwhocodes/config-array": "^0.11.11", + "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", diff --git a/tests/lib/config/flat-config-array.js b/tests/lib/config/flat-config-array.js index 728b3e93785..b0dbfec93f7 100644 --- a/tests/lib/config/flat-config-array.js +++ b/tests/lib/config/flat-config-array.js @@ -1987,4 +1987,71 @@ describe("FlatConfigArray", () => { }); }); + + // https://github.com/eslint/eslint/issues/12592 + describe("Shared references between rule configs", () => { + + it("shared rule config should not cause a rule validation error", () => { + + const ruleConfig = ["error", {}]; + + const configs = new FlatConfigArray([{ + rules: { + camelcase: ruleConfig, + "default-case": ruleConfig + } + }]); + + configs.normalizeSync(); + + const config = configs.getConfig("foo.js"); + + assert.deepStrictEqual(config.rules, { + camelcase: [2, { + ignoreDestructuring: false, + ignoreGlobals: false, + ignoreImports: false + }], + "default-case": [2, {}] + }); + + }); + + + it("should throw rule validation error for camelcase", async () => { + + const ruleConfig = ["error", {}]; + + const configs = new FlatConfigArray([ + { + rules: { + camelcase: ruleConfig + } + }, + { + rules: { + "default-case": ruleConfig, + + + camelcase: [ + "error", + { + ignoreDestructuring: Date + } + + ] + } + } + ]); + + configs.normalizeSync(); + + // exact error may differ based on structuredClone implementation so just test prefix + assert.throws(() => { + configs.getConfig("foo.js"); + }, /Key "rules": Key "camelcase":/u); + + }); + + }); });