Skip to content

Commit 9852a31

Browse files
snitin315fasttime
andauthored
fix: deep merge behavior in flat config (#18065)
* fix: replicate eslintrc merge behavior in flat config * do not merge arrays * Remove unused code * test for undefined properties * fix an edge case with non-enumerable properties (cherry picked from commit f182114) Co-authored-by: Francesco Trotta <github@fasttime.org>
1 parent dca7d0f commit 9852a31

File tree

2 files changed

+157
-56
lines changed

2 files changed

+157
-56
lines changed

lib/config/flat-config-schema.js

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,15 @@ function isNonNullObject(value) {
5353
return typeof value === "object" && value !== null;
5454
}
5555

56+
/**
57+
* Check if a value is a non-null non-array object.
58+
* @param {any} value The value to check.
59+
* @returns {boolean} `true` if the value is a non-null non-array object.
60+
*/
61+
function isNonArrayObject(value) {
62+
return isNonNullObject(value) && !Array.isArray(value);
63+
}
64+
5665
/**
5766
* Check if a value is undefined.
5867
* @param {any} value The value to check.
@@ -62,25 +71,14 @@ function isUndefined(value) {
6271
return typeof value === "undefined";
6372
}
6473

65-
// A unique empty object to be used internally as a mapping key in `deepMerge`.
66-
const EMPTY_OBJECT = {};
67-
6874
/**
69-
* Deeply merges two objects.
75+
* Deeply merges two non-array objects.
7076
* @param {Object} first The base object.
71-
* @param {any} second The overrides value.
77+
* @param {Object} second The overrides object.
7278
* @param {Map<string, Map<string, Object>>} [mergeMap] Maps the combination of first and second arguments to a merged result.
7379
* @returns {Object} An object with properties from both first and second.
7480
*/
75-
function deepMerge(first, second = {}, mergeMap = new Map()) {
76-
77-
/*
78-
* If the second value is an array, just return it. We don't merge
79-
* arrays because order matters and we can't know the correct order.
80-
*/
81-
if (Array.isArray(second)) {
82-
return second;
83-
}
81+
function deepMerge(first, second, mergeMap = new Map()) {
8482

8583
let secondMergeMap = mergeMap.get(first);
8684

@@ -98,7 +96,7 @@ function deepMerge(first, second = {}, mergeMap = new Map()) {
9896
}
9997

10098
/*
101-
* First create a result object where properties from the second value
99+
* First create a result object where properties from the second object
102100
* overwrite properties from the first. This sets up a baseline to use
103101
* later rather than needing to inspect and change every property
104102
* individually.
@@ -108,27 +106,25 @@ function deepMerge(first, second = {}, mergeMap = new Map()) {
108106
...second
109107
};
110108

109+
delete result.__proto__; // eslint-disable-line no-proto -- don't merge own property "__proto__"
110+
111111
// Store the pending result for this combination of first and second arguments.
112112
secondMergeMap.set(second, result);
113113

114114
for (const key of Object.keys(second)) {
115115

116116
// avoid hairy edge case
117-
if (key === "__proto__") {
117+
if (key === "__proto__" || !Object.prototype.propertyIsEnumerable.call(first, key)) {
118118
continue;
119119
}
120120

121121
const firstValue = first[key];
122122
const secondValue = second[key];
123123

124-
if (isNonNullObject(firstValue)) {
124+
if (isNonArrayObject(firstValue) && isNonArrayObject(secondValue)) {
125125
result[key] = deepMerge(firstValue, secondValue, mergeMap);
126-
} else if (isUndefined(firstValue)) {
127-
if (isNonNullObject(secondValue)) {
128-
result[key] = deepMerge(EMPTY_OBJECT, secondValue, mergeMap);
129-
} else if (!isUndefined(secondValue)) {
130-
result[key] = secondValue;
131-
}
126+
} else if (isUndefined(secondValue)) {
127+
result[key] = firstValue;
132128
}
133129
}
134130

tests/lib/config/flat-config-schema.js

Lines changed: 138 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,28 @@
77

88
const { flatConfigSchema } = require("../../../lib/config/flat-config-schema");
99
const { assert } = require("chai");
10+
const { Legacy: { ConfigArray } } = require("@eslint/eslintrc");
11+
12+
/**
13+
* This function checks the result of merging two values in eslintrc config.
14+
* It uses deep strict equality to compare the actual and the expected results.
15+
* This is useful to ensure that the flat config merge logic behaves similarly to the old logic.
16+
* When eslintrc is removed, this function and its invocations can be also removed.
17+
* @param {Object} [first] The base object.
18+
* @param {Object} [second] The overrides object.
19+
* @param {Object} [expectedResult] The expected reults of merging first and second values.
20+
* @returns {void}
21+
*/
22+
function confirmLegacyMergeResult(first, second, expectedResult) {
23+
const configArray = new ConfigArray(
24+
{ settings: first },
25+
{ settings: second }
26+
);
27+
const config = configArray.extractConfig("/file");
28+
const actualResult = config.settings;
29+
30+
assert.deepStrictEqual(actualResult, expectedResult);
31+
}
1032

1133
describe("merge", () => {
1234

@@ -18,36 +40,14 @@ describe("merge", () => {
1840
const result = merge(first, second);
1941

2042
assert.deepStrictEqual(result, { ...first, ...second });
21-
});
22-
23-
it("overrides an object with an array", () => {
24-
const first = { foo: 42 };
25-
const second = ["bar", "baz"];
26-
const result = merge(first, second);
27-
28-
assert.strictEqual(result, second);
29-
});
30-
31-
it("merges an array with an object", () => {
32-
const first = ["foo", "bar"];
33-
const second = { baz: 42 };
34-
const result = merge(first, second);
35-
36-
assert.deepStrictEqual(result, { 0: "foo", 1: "bar", baz: 42 });
37-
});
38-
39-
it("overrides an array with another array", () => {
40-
const first = ["foo", "bar"];
41-
const second = ["baz", "qux"];
42-
const result = merge(first, second);
43-
44-
assert.strictEqual(result, second);
43+
confirmLegacyMergeResult(first, second, result);
4544
});
4645

4746
it("returns an emtpy object if both values are undefined", () => {
4847
const result = merge(void 0, void 0);
4948

5049
assert.deepStrictEqual(result, {});
50+
confirmLegacyMergeResult(void 0, void 0, result);
5151
});
5252

5353
it("returns an object equal to the first one if the second one is undefined", () => {
@@ -56,6 +56,7 @@ describe("merge", () => {
5656

5757
assert.deepStrictEqual(result, first);
5858
assert.notStrictEqual(result, first);
59+
confirmLegacyMergeResult(first, void 0, result);
5960
});
6061

6162
it("returns an object equal to the second one if the first one is undefined", () => {
@@ -64,6 +65,16 @@ describe("merge", () => {
6465

6566
assert.deepStrictEqual(result, second);
6667
assert.notStrictEqual(result, second);
68+
confirmLegacyMergeResult(void 0, second, result);
69+
});
70+
71+
it("does not preserve the type of merged objects", () => {
72+
const first = new Set(["foo", "bar"]);
73+
const second = new Set(["baz"]);
74+
const result = merge(first, second);
75+
76+
assert.deepStrictEqual(result, {});
77+
confirmLegacyMergeResult(first, second, result);
6778
});
6879

6980
it("merges two objects in a property", () => {
@@ -72,6 +83,34 @@ describe("merge", () => {
7283
const result = merge(first, second);
7384

7485
assert.deepStrictEqual(result, { foo: { bar: "baz", qux: 42 } });
86+
confirmLegacyMergeResult(first, second, result);
87+
});
88+
89+
it("overwrites an object in a property with an array", () => {
90+
const first = { someProperty: { 1: "foo", bar: "baz" } };
91+
const second = { someProperty: ["qux"] };
92+
const result = merge(first, second);
93+
94+
assert.deepStrictEqual(result, second);
95+
assert.strictEqual(result.someProperty, second.someProperty);
96+
});
97+
98+
it("overwrites an array in a property with another array", () => {
99+
const first = { someProperty: ["foo", "bar", void 0, "baz"] };
100+
const second = { someProperty: ["qux", void 0, 42] };
101+
const result = merge(first, second);
102+
103+
assert.deepStrictEqual(result, second);
104+
assert.strictEqual(result.someProperty, second.someProperty);
105+
});
106+
107+
it("overwrites an array in a property with an object", () => {
108+
const first = { foo: ["foobar"] };
109+
const second = { foo: { 1: "qux", bar: "baz" } };
110+
const result = merge(first, second);
111+
112+
assert.deepStrictEqual(result, second);
113+
assert.strictEqual(result.foo, second.foo);
75114
});
76115

77116
it("does not override a value in a property with undefined", () => {
@@ -81,6 +120,7 @@ describe("merge", () => {
81120

82121
assert.deepStrictEqual(result, first);
83122
assert.notStrictEqual(result, first);
123+
confirmLegacyMergeResult(first, second, result);
84124
});
85125

86126
it("does not change the prototype of a merged object", () => {
@@ -89,39 +129,104 @@ describe("merge", () => {
89129
const result = merge(first, second);
90130

91131
assert.strictEqual(Object.getPrototypeOf(result), Object.prototype);
132+
confirmLegacyMergeResult(first, second, result);
92133
});
93134

94135
it("does not merge the '__proto__' property", () => {
95136
const first = { ["__proto__"]: { foo: 42 } };
96137
const second = { ["__proto__"]: { bar: "baz" } };
97138
const result = merge(first, second);
98139

99-
assert.deepStrictEqual(result, second);
100-
assert.notStrictEqual(result, second);
140+
assert.deepStrictEqual(result, {});
141+
confirmLegacyMergeResult(first, second, result);
101142
});
102143

103-
it("throws an error if a value in a property is overriden with null", () => {
144+
it("overrides a value in a property with null", () => {
104145
const first = { foo: { bar: "baz" } };
105146
const second = { foo: null };
147+
const result = merge(first, second);
106148

107-
assert.throws(() => merge(first, second), TypeError);
149+
assert.deepStrictEqual(result, second);
150+
assert.notStrictEqual(result, second);
151+
confirmLegacyMergeResult(first, second, result);
108152
});
109153

110-
it("does not override a value in a property with a primitive", () => {
154+
it("overrides a value in a property with a non-nullish primitive", () => {
111155
const first = { foo: { bar: "baz" } };
112156
const second = { foo: 42 };
113157
const result = merge(first, second);
114158

115-
assert.deepStrictEqual(result, first);
116-
assert.notStrictEqual(result, first);
159+
assert.deepStrictEqual(result, second);
160+
assert.notStrictEqual(result, second);
161+
confirmLegacyMergeResult(first, second, result);
117162
});
118163

119-
it("merges an object in a property with a string", () => {
164+
it("overrides an object in a property with a string", () => {
120165
const first = { foo: { bar: "baz" } };
121166
const second = { foo: "qux" };
122167
const result = merge(first, second);
123168

124-
assert.deepStrictEqual(result, { foo: { 0: "q", 1: "u", 2: "x", bar: "baz" } });
169+
assert.deepStrictEqual(result, second);
170+
assert.notStrictEqual(result, first);
171+
confirmLegacyMergeResult(first, second, result);
172+
});
173+
174+
it("overrides a value in a property with a function", () => {
175+
const first = { someProperty: { foo: 42 } };
176+
const second = { someProperty() {} };
177+
const result = merge(first, second);
178+
179+
assert.deepStrictEqual(result, second);
180+
assert.notProperty(result.someProperty, "foo");
181+
confirmLegacyMergeResult(first, second, result);
182+
});
183+
184+
it("overrides a function in a property with an object", () => {
185+
const first = { someProperty: Object.assign(() => {}, { foo: "bar" }) };
186+
const second = { someProperty: { baz: "qux" } };
187+
const result = merge(first, second);
188+
189+
assert.deepStrictEqual(result, second);
190+
assert.notProperty(result.someProperty, "foo");
191+
confirmLegacyMergeResult(first, second, result);
192+
});
193+
194+
it("sets properties to undefined", () => {
195+
const first = { foo: void 0, bar: void 0 };
196+
const second = { foo: void 0, baz: void 0 };
197+
const result = merge(first, second);
198+
199+
assert.deepStrictEqual(result, { foo: void 0, bar: void 0, baz: void 0 });
200+
});
201+
202+
it("considers only own enumerable properties", () => {
203+
const first = {
204+
__proto__: { inherited1: "A" }, // non-own properties are not considered
205+
included1: "B",
206+
notMerged1: { first: true }
207+
};
208+
const second = {
209+
__proto__: { inherited2: "C" }, // non-own properties are not considered
210+
included2: "D",
211+
notMerged2: { second: true }
212+
};
213+
214+
// non-enumerable properties are not considered
215+
Object.defineProperty(first, "notMerged2", { enumerable: false, value: { first: true } });
216+
Object.defineProperty(second, "notMerged1", { enumerable: false, value: { second: true } });
217+
218+
const result = merge(first, second);
219+
220+
assert.deepStrictEqual(
221+
result,
222+
{
223+
included1: "B",
224+
included2: "D",
225+
notMerged1: { first: true },
226+
notMerged2: { second: true }
227+
}
228+
);
229+
confirmLegacyMergeResult(first, second, result);
125230
});
126231

127232
it("merges objects with self-references", () => {

0 commit comments

Comments
 (0)