Skip to content

Commit

Permalink
fix: allow circular references in config (#17752)
Browse files Browse the repository at this point in the history
* fix for circular references in config

* restore current logic
  • Loading branch information
fasttime committed Dec 20, 2023
1 parent cc0c9f7 commit b577e8a
Show file tree
Hide file tree
Showing 3 changed files with 321 additions and 8 deletions.
35 changes: 27 additions & 8 deletions lib/config/flat-config-schema.js
Expand Up @@ -62,13 +62,17 @@ function isUndefined(value) {
return typeof value === "undefined";
}

// A unique empty object to be used internally as a mapping key in `deepMerge`.
const EMPTY_OBJECT = {};

/**
* Deeply merges two objects.
* @param {Object} first The base object.
* @param {Object} second The overrides object.
* @param {any} second The overrides value.
* @param {Map<string, Map<string, Object>>} [mergeMap] Maps the combination of first and second arguments to a merged result.
* @returns {Object} An object with properties from both first and second.
*/
function deepMerge(first = {}, second = {}) {
function deepMerge(first, second = {}, mergeMap = new Map()) {

/*
* If the second value is an array, just return it. We don't merge
Expand All @@ -78,8 +82,23 @@ function deepMerge(first = {}, second = {}) {
return second;
}

let secondMergeMap = mergeMap.get(first);

if (secondMergeMap) {
const result = secondMergeMap.get(second);

if (result) {

// If this combination of first and second arguments has been already visited, return the previously created result.
return result;
}
} else {
secondMergeMap = new Map();
mergeMap.set(first, secondMergeMap);
}

/*
* First create a result object where properties from the second object
* First create a result object where properties from the second value
* overwrite properties from the first. This sets up a baseline to use
* later rather than needing to inspect and change every property
* individually.
Expand All @@ -89,6 +108,9 @@ function deepMerge(first = {}, second = {}) {
...second
};

// Store the pending result for this combination of first and second arguments.
secondMergeMap.set(second, result);

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

// avoid hairy edge case
Expand All @@ -100,13 +122,10 @@ function deepMerge(first = {}, second = {}) {
const secondValue = second[key];

if (isNonNullObject(firstValue)) {
result[key] = deepMerge(firstValue, secondValue);
result[key] = deepMerge(firstValue, secondValue, mergeMap);
} else if (isUndefined(firstValue)) {
if (isNonNullObject(secondValue)) {
result[key] = deepMerge(
Array.isArray(secondValue) ? [] : {},
secondValue
);
result[key] = deepMerge(EMPTY_OBJECT, secondValue, mergeMap);
} else if (!isUndefined(secondValue)) {
result[key] = secondValue;
}
Expand Down
228 changes: 228 additions & 0 deletions tests/lib/config/flat-config-schema.js
@@ -0,0 +1,228 @@
/**
* @fileoverview Tests for flatConfigSchema
* @author Francesco Trotta
*/

"use strict";

const { flatConfigSchema } = require("../../../lib/config/flat-config-schema");
const { assert } = require("chai");

describe("merge", () => {

const { merge } = flatConfigSchema.settings;

it("merges two objects", () => {
const first = { foo: 42 };
const second = { bar: "baz" };
const result = merge(first, second);

assert.deepStrictEqual(result, { ...first, ...second });
});

it("overrides an object with an array", () => {
const first = { foo: 42 };
const second = ["bar", "baz"];
const result = merge(first, second);

assert.strictEqual(result, second);
});

it("merges an array with an object", () => {
const first = ["foo", "bar"];
const second = { baz: 42 };
const result = merge(first, second);

assert.deepStrictEqual(result, { 0: "foo", 1: "bar", baz: 42 });
});

it("overrides an array with another array", () => {
const first = ["foo", "bar"];
const second = ["baz", "qux"];
const result = merge(first, second);

assert.strictEqual(result, second);
});

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

assert.deepStrictEqual(result, {});
});

it("returns an object equal to the first one if the second one is undefined", () => {
const first = { foo: 42, bar: "baz" };
const result = merge(first, void 0);

assert.deepStrictEqual(result, first);
assert.notStrictEqual(result, first);
});

it("returns an object equal to the second one if the first one is undefined", () => {
const second = { foo: 42, bar: "baz" };
const result = merge(void 0, second);

assert.deepStrictEqual(result, second);
assert.notStrictEqual(result, second);
});

it("merges two objects in a property", () => {
const first = { foo: { bar: "baz" } };
const second = { foo: { qux: 42 } };
const result = merge(first, second);

assert.deepStrictEqual(result, { foo: { bar: "baz", qux: 42 } });
});

it("does not override a value in a property with undefined", () => {
const first = { foo: { bar: "baz" } };
const second = { foo: void 0 };
const result = merge(first, second);

assert.deepStrictEqual(result, first);
assert.notStrictEqual(result, first);
});

it("does not change the prototype of a merged object", () => {
const first = { foo: 42 };
const second = { bar: "baz", ["__proto__"]: { qux: true } };
const result = merge(first, second);

assert.strictEqual(Object.getPrototypeOf(result), Object.prototype);
});

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

assert.deepStrictEqual(result, second);
assert.notStrictEqual(result, second);
});

it("throws an error if a value in a property is overriden with null", () => {
const first = { foo: { bar: "baz" } };
const second = { foo: null };

assert.throws(() => merge(first, second), TypeError);
});

it("does not override a value in a property with a primitive", () => {
const first = { foo: { bar: "baz" } };
const second = { foo: 42 };
const result = merge(first, second);

assert.deepStrictEqual(result, first);
assert.notStrictEqual(result, first);
});

it("merges an object in a property with a string", () => {
const first = { foo: { bar: "baz" } };
const second = { foo: "qux" };
const result = merge(first, second);

assert.deepStrictEqual(result, { foo: { 0: "q", 1: "u", 2: "x", bar: "baz" } });
});

it("merges objects with self-references", () => {
const first = { foo: 42 };

first.first = first;
const second = { bar: "baz" };

second.second = second;
const result = merge(first, second);

assert.strictEqual(result.first, first);
assert.deepStrictEqual(result.second, second);

const expected = { foo: 42, bar: "baz" };

expected.first = first;
expected.second = second;
assert.deepStrictEqual(result, expected);
});

it("merges objects with overlapping self-references", () => {
const first = { foo: 42 };

first.reference = first;
const second = { bar: "baz" };

second.reference = second;

const result = merge(first, second);

assert.strictEqual(result.reference, result);

const expected = { foo: 42, bar: "baz" };

expected.reference = expected;
assert.deepStrictEqual(result, expected);
});

it("merges objects with cross-references", () => {
const first = { foo: 42 };
const second = { bar: "baz" };

first.second = second;
second.first = first;

const result = merge(first, second);

assert.deepStrictEqual(result.first, first);
assert.strictEqual(result.second, second);

const expected = { foo: 42, bar: "baz" };

expected.first = first;
expected.second = second;
assert.deepStrictEqual(result, expected);
});

it("merges objects with overlapping cross-references", () => {
const first = { foo: 42 };
const second = { bar: "baz" };

first.reference = second;
second.reference = first;

const result = merge(first, second);

assert.strictEqual(result, result.reference.reference);

const expected = { foo: 42, bar: "baz", reference: { foo: 42, bar: "baz" } };

expected.reference.reference = expected;
assert.deepStrictEqual(result, expected);
});

it("produces the same results for the same combinations of property values", () => {
const firstCommon = { foo: 42 };
const secondCommon = { bar: "baz" };
const first = {
a: firstCommon,
b: firstCommon,
c: { foo: "different" },
d: firstCommon
};
const second = {
a: secondCommon,
b: { bar: "something else" },
c: secondCommon,
d: secondCommon
};
const result = merge(first, second);

assert.deepStrictEqual(result.a, result.d);

const expected = {
a: { foo: 42, bar: "baz" },
b: { foo: 42, bar: "something else" },
c: { foo: "different", bar: "baz" },
d: { foo: 42, bar: "baz" }
};

assert.deepStrictEqual(result, expected);
});
});
66 changes: 66 additions & 0 deletions tests/lib/eslint/flat-eslint.js
Expand Up @@ -6322,6 +6322,72 @@ describe("FlatESLint", () => {
});
}

describe("config with circular references", () => {
it("in 'settings'", async () => {
let resolvedSettings = null;

const circular = {};

circular.self = circular;

const eslint = new FlatESLint({
overrideConfigFile: true,
baseConfig: {
settings: {
sharedData: circular
},
rules: {
"test-plugin/test-rule": 1
}
},
plugins: {
"test-plugin": {
rules: {
"test-rule": {
create(context) {
resolvedSettings = context.settings;
return { };
}
}
}
}
}
});

await eslint.lintText("debugger;");

assert.deepStrictEqual(resolvedSettings.sharedData, circular);
});

it("in 'parserOptions'", async () => {
let resolvedParserOptions = null;

const circular = {};

circular.self = circular;

const eslint = new FlatESLint({
overrideConfigFile: true,
baseConfig: {
languageOptions: {
parser: {
parse(text, parserOptions) {
resolvedParserOptions = parserOptions;
}
},
parserOptions: {
testOption: circular
}
}
}
});

await eslint.lintText("debugger;");

assert.deepStrictEqual(resolvedParserOptions.testOption, circular);
});
});

});

describe("shouldUseFlatConfig", () => {
Expand Down

0 comments on commit b577e8a

Please sign in to comment.