Skip to content

Commit d02f914

Browse files
feat: merge rule.meta.defaultOptions before validation (#166)
* feat: merge rule.meta.defaultOptions before validation * Switch from ?. to && * Apply suggestions from code review Co-authored-by: Francesco Trotta <[email protected]> * switch from undefined to void 0 in deep-merge-arrays --------- Co-authored-by: Francesco Trotta <[email protected]>
1 parent 2b0aa3a commit d02f914

File tree

4 files changed

+227
-1
lines changed

4 files changed

+227
-1
lines changed

lib/shared/config-validator.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import util from "util";
1919
import * as ConfigOps from "./config-ops.js";
2020
import { emitDeprecationWarning } from "./deprecation-warnings.js";
2121
import ajvOrig from "./ajv.js";
22+
import { deepMergeArrays } from "./deep-merge-arrays.js";
2223
import configSchema from "../../conf/config-schema.js";
2324
import BuiltInEnvironments from "../../conf/environments.js";
2425

@@ -148,7 +149,10 @@ export default class ConfigValidator {
148149
const validateRule = ruleValidators.get(rule);
149150

150151
if (validateRule) {
151-
validateRule(localOptions);
152+
const mergedOptions = deepMergeArrays(rule.meta?.defaultOptions, localOptions);
153+
154+
validateRule(mergedOptions);
155+
152156
if (validateRule.errors) {
153157
throw new Error(validateRule.errors.map(
154158
error => `\tValue ${JSON.stringify(error.data)} ${error.message}.\n`

lib/shared/deep-merge-arrays.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* @fileoverview Applies default rule options
3+
* @author JoshuaKGoldberg
4+
*/
5+
6+
/**
7+
* Check if the variable contains an object strictly rejecting arrays
8+
* @param {unknown} value an object
9+
* @returns {boolean} Whether value is an object
10+
*/
11+
function isObjectNotArray(value) {
12+
return typeof value === "object" && value !== null && !Array.isArray(value);
13+
}
14+
15+
/**
16+
* Deeply merges second on top of first, creating a new {} object if needed.
17+
* @param {T} first Base, default value.
18+
* @param {U} second User-specified value.
19+
* @returns {T | U | (T & U)} Merged equivalent of second on top of first.
20+
*/
21+
function deepMergeObjects(first, second) {
22+
if (second === void 0) {
23+
return first;
24+
}
25+
26+
if (!isObjectNotArray(first) || !isObjectNotArray(second)) {
27+
return second;
28+
}
29+
30+
const result = { ...first, ...second };
31+
32+
for (const key of Object.keys(second)) {
33+
if (Object.prototype.propertyIsEnumerable.call(first, key)) {
34+
result[key] = deepMergeObjects(first[key], second[key]);
35+
}
36+
}
37+
38+
return result;
39+
}
40+
41+
/**
42+
* Deeply merges second on top of first, creating a new [] array if needed.
43+
* @param {T[] | undefined} first Base, default values.
44+
* @param {U[] | undefined} second User-specified values.
45+
* @returns {(T | U | (T & U))[]} Merged equivalent of second on top of first.
46+
*/
47+
function deepMergeArrays(first, second) {
48+
if (!first || !second) {
49+
return second || first || [];
50+
}
51+
52+
return [
53+
...first.map((value, i) => deepMergeObjects(value, second[i])),
54+
...second.slice(first.length)
55+
];
56+
}
57+
58+
export { deepMergeArrays };

tests/lib/shared/config-validator.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,21 @@ const mockInvalidJSONSchemaRule = {
7373
}
7474
};
7575

76+
const mockMaxPropertiesSchema = {
77+
meta: {
78+
defaultOptions: [{
79+
foo: 42
80+
}],
81+
schema: [{
82+
type: "object",
83+
maxProperties: 2
84+
}]
85+
},
86+
create() {
87+
return {};
88+
}
89+
};
90+
7691
//------------------------------------------------------------------------------
7792
// Tests
7893
//------------------------------------------------------------------------------
@@ -253,4 +268,19 @@ describe("ConfigValidator", () => {
253268
});
254269

255270
});
271+
272+
describe("validateRuleSchema", () => {
273+
274+
it("should throw when rule options are invalid after defaults are applied", () => {
275+
const fn = validator.validateRuleSchema.bind(validator, mockMaxPropertiesSchema, [{ bar: 6, baz: 7 }]);
276+
277+
nodeAssert.throws(
278+
fn,
279+
{
280+
message: '\tValue {"foo":42,"bar":6,"baz":7} should NOT have more than 2 properties.\n'
281+
}
282+
);
283+
});
284+
285+
});
256286
});

tests/lib/shared/deep-merge-arrays.js

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
//------------------------------------------------------------------------------
2+
// Requirements
3+
//------------------------------------------------------------------------------
4+
5+
import assert from "node:assert";
6+
7+
import { deepMergeArrays } from "../../../lib/shared/deep-merge-arrays.js";
8+
9+
//------------------------------------------------------------------------------
10+
// Tests
11+
//------------------------------------------------------------------------------
12+
13+
/**
14+
* Turns a value into its string equivalent for a test name.
15+
* @param {unknown} value Value to be stringified.
16+
* @returns {string} String equivalent of the value.
17+
*/
18+
function toTestCaseName(value) {
19+
return typeof value === "object" ? JSON.stringify(value) : `${value}`;
20+
}
21+
22+
describe("deepMerge", () => {
23+
for (const [first, second, result] of [
24+
[void 0, void 0, []],
25+
[[], void 0, []],
26+
[["abc"], void 0, ["abc"]],
27+
[void 0, ["abc"], ["abc"]],
28+
[[], ["abc"], ["abc"]],
29+
[[void 0], ["abc"], ["abc"]],
30+
[[void 0, void 0], ["abc"], ["abc", void 0]],
31+
[[void 0, void 0], ["abc", "def"], ["abc", "def"]],
32+
[[void 0, null], ["abc"], ["abc", null]],
33+
[[void 0, null], ["abc", "def"], ["abc", "def"]],
34+
[[null], ["abc"], ["abc"]],
35+
[[123], [void 0], [123]],
36+
[[123], [null], [null]],
37+
[[123], [{ a: 0 }], [{ a: 0 }]],
38+
[["abc"], [void 0], ["abc"]],
39+
[["abc"], [null], [null]],
40+
[["abc"], ["def"], ["def"]],
41+
[["abc"], [{ a: 0 }], [{ a: 0 }]],
42+
[[["abc"]], [null], [null]],
43+
[[["abc"]], ["def"], ["def"]],
44+
[[["abc"]], [{ a: 0 }], [{ a: 0 }]],
45+
[[{ abc: true }], ["def"], ["def"]],
46+
[[{ abc: true }], [["def"]], [["def"]]],
47+
[[null], [{ abc: true }], [{ abc: true }]],
48+
[[{ a: void 0 }], [{ a: 0 }], [{ a: 0 }]],
49+
[[{ a: null }], [{ a: 0 }], [{ a: 0 }]],
50+
[[{ a: null }], [{ a: { b: 0 } }], [{ a: { b: 0 } }]],
51+
[[{ a: 0 }], [{ a: 1 }], [{ a: 1 }]],
52+
[[{ a: 0 }], [{ a: null }], [{ a: null }]],
53+
[[{ a: 0 }], [{ a: void 0 }], [{ a: 0 }]],
54+
[[{ a: 0 }], ["abc"], ["abc"]],
55+
[[{ a: 0 }], [123], [123]],
56+
[[[{ a: 0 }]], [123], [123]],
57+
[
58+
[{ a: ["b"] }],
59+
[{ a: ["c"] }],
60+
[{ a: ["c"] }]
61+
],
62+
[
63+
[{ a: [{ b: "c" }] }],
64+
[{ a: [{ d: "e" }] }],
65+
[{ a: [{ d: "e" }] }]
66+
],
67+
[
68+
[{ a: { b: "c" }, d: true }],
69+
[{ a: { e: "f" } }],
70+
[{ a: { b: "c", e: "f" }, d: true }]
71+
],
72+
[
73+
[{ a: { b: "c" } }],
74+
[{ a: { e: "f" }, d: true }],
75+
[{ a: { b: "c", e: "f" }, d: true }]
76+
],
77+
[
78+
[{ a: { b: "c" } }, { d: true }],
79+
[{ a: { e: "f" } }, { f: 123 }],
80+
[{ a: { b: "c", e: "f" } }, { d: true, f: 123 }]
81+
],
82+
[
83+
[{ hasOwnProperty: true }],
84+
[{}],
85+
[{ hasOwnProperty: true }]
86+
],
87+
[
88+
[{ hasOwnProperty: false }],
89+
[{}],
90+
[{ hasOwnProperty: false }]
91+
],
92+
[
93+
[{ hasOwnProperty: null }],
94+
[{}],
95+
[{ hasOwnProperty: null }]
96+
],
97+
[
98+
[{ hasOwnProperty: void 0 }],
99+
[{}],
100+
[{ hasOwnProperty: void 0 }]
101+
],
102+
[
103+
[{}],
104+
[{ hasOwnProperty: null }],
105+
[{ hasOwnProperty: null }]
106+
],
107+
[
108+
[{}],
109+
[{ hasOwnProperty: void 0 }],
110+
[{ hasOwnProperty: void 0 }]
111+
],
112+
[
113+
[{
114+
allow: [],
115+
ignoreDestructuring: false,
116+
ignoreGlobals: false,
117+
ignoreImports: false,
118+
properties: "always"
119+
}],
120+
[],
121+
[{
122+
allow: [],
123+
ignoreDestructuring: false,
124+
ignoreGlobals: false,
125+
ignoreImports: false,
126+
properties: "always"
127+
}]
128+
]
129+
]) {
130+
it(`${toTestCaseName(first)}, ${toTestCaseName(second)}`, () => {
131+
assert.deepStrictEqual(deepMergeArrays(first, second), result);
132+
});
133+
}
134+
});

0 commit comments

Comments
 (0)