diff --git a/lib/config/flat-config-array.js b/lib/config/flat-config-array.js index e09fdcef0e5..4312066d18d 100644 --- a/lib/config/flat-config-array.js +++ b/lib/config/flat-config-array.js @@ -79,7 +79,53 @@ function getObjectId(object) { return name; } +/** + * Wraps a config error with details about where the error occurred. + * @param {Error} error The original error. + * @param {number} originalLength The original length of the config array. + * @param {number} baseLength The length of the base config. + * @returns {TypeError} The new error with details. + */ +function wrapConfigErrorWithDetails(error, originalLength, baseLength) { + + let location = "user-defined"; + let configIndex = error.index; + + /* + * A config array is set up in this order: + * 1. Base config + * 2. Original configs + * 3. User-defined configs + * 4. CLI-defined configs + * + * So we need to adjust the index to account for the base config. + * + * - If the index is less than the base length, it's in the base config + * (as specified by `baseConfig` argument to `FlatConfigArray` constructor). + * - If the index is greater than the base length but less than the original + * length + base length, it's in the original config. The original config + * is passed to the `FlatConfigArray` constructor as the first argument. + * - Otherwise, it's in the user-defined config, which is loaded from the + * config file and merged with any command-line options. + */ + if (error.index < baseLength) { + location = "base"; + } else if (error.index < originalLength + baseLength) { + location = "original"; + configIndex = error.index - baseLength; + } else { + configIndex = error.index - originalLength - baseLength; + } + + return new TypeError( + `${error.message.slice(0, -1)} at ${location} index ${configIndex}.`, + { cause: error } + ); +} + const originalBaseConfig = Symbol("originalBaseConfig"); +const originalLength = Symbol("originalLength"); +const baseLength = Symbol("baseLength"); //----------------------------------------------------------------------------- // Exports @@ -106,12 +152,24 @@ class FlatConfigArray extends ConfigArray { schema: flatConfigSchema }); + /** + * The original length of the array before any modifications. + * @type {number} + */ + this[originalLength] = this.length; + if (baseConfig[Symbol.iterator]) { this.unshift(...baseConfig); } else { this.unshift(baseConfig); } + /** + * The length of the array after applying the base config. + * @type {number} + */ + this[baseLength] = this.length - this[originalLength]; + /** * The base config used to build the config array. * @type {Array} @@ -129,6 +187,49 @@ class FlatConfigArray extends ConfigArray { Object.defineProperty(this, "shouldIgnore", { writable: false }); } + /** + * Normalizes the array by calling the superclass method and catching/rethrowing + * any ConfigError exceptions with additional details. + * @param {any} [context] The context to use to normalize the array. + * @returns {Promise} A promise that resolves when the array is normalized. + */ + normalize(context) { + return super.normalize(context) + .catch(error => { + if (error.name === "ConfigError") { + throw wrapConfigErrorWithDetails(error, this[originalLength], this[baseLength]); + } + + throw error; + + }); + } + + /** + * Normalizes the array by calling the superclass method and catching/rethrowing + * any ConfigError exceptions with additional details. + * @param {any} [context] The context to use to normalize the array. + * @returns {FlatConfigArray} The current instance. + * @throws {TypeError} If the config is invalid. + */ + normalizeSync(context) { + + try { + + return super.normalizeSync(context); + + } catch (error) { + + if (error.name === "ConfigError") { + throw wrapConfigErrorWithDetails(error, this[originalLength], this[baseLength]); + } + + throw error; + + } + + } + /* eslint-disable class-methods-use-this -- Desired as instance method */ /** * Replaces a config with another config to allow us to put strings diff --git a/package.json b/package.json index 366974e51b2..c3b04b5f8b2 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^3.0.2", "@eslint/js": "9.0.0", - "@humanwhocodes/config-array": "^0.12.3", + "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.2.3", "@nodelib/fs.walk": "^1.2.8", diff --git a/tests/lib/config/flat-config-array.js b/tests/lib/config/flat-config-array.js index a8bff24417a..9da4d050ddf 100644 --- a/tests/lib/config/flat-config-array.js +++ b/tests/lib/config/flat-config-array.js @@ -191,9 +191,9 @@ async function assertMergedResult(values, result) { async function assertInvalidConfig(values, message) { const configs = createFlatConfigArray(values); - await configs.normalize(); assert.throws(() => { + configs.normalizeSync(); configs.getConfig("foo.js"); }, message); } @@ -719,14 +719,162 @@ describe("FlatConfigArray", () => { describe("Config array elements", () => { it("should error on 'eslint:recommended' string config", async () => { - await assertInvalidConfig(["eslint:recommended"], "All arguments must be objects."); + await assertInvalidConfig(["eslint:recommended"], "Config (unnamed): Unexpected non-object config at original index 0."); }); it("should error on 'eslint:all' string config", async () => { - await assertInvalidConfig(["eslint:all"], "All arguments must be objects."); + await assertInvalidConfig(["eslint:all"], "Config (unnamed): Unexpected non-object config at original index 0."); + }); + + + it("should throw an error when undefined original config is normalized", () => { + + const configs = new FlatConfigArray([void 0]); + + assert.throws(() => { + configs.normalizeSync(); + }, "Config (unnamed): Unexpected undefined config at original index 0."); + + }); + + it("should throw an error when undefined original config is normalized asynchronously", async () => { + + const configs = new FlatConfigArray([void 0]); + + try { + await configs.normalize(); + assert.fail("Error not thrown"); + } catch (error) { + assert.strictEqual(error.message, "Config (unnamed): Unexpected undefined config at original index 0."); + } + + }); + + it("should throw an error when null original config is normalized", () => { + + const configs = new FlatConfigArray([null]); + + assert.throws(() => { + configs.normalizeSync(); + }, "Config (unnamed): Unexpected null config at original index 0."); + + }); + + it("should throw an error when null original config is normalized asynchronously", async () => { + + const configs = new FlatConfigArray([null]); + + try { + await configs.normalize(); + assert.fail("Error not thrown"); + } catch (error) { + assert.strictEqual(error.message, "Config (unnamed): Unexpected null config at original index 0."); + } + + }); + + it("should throw an error when undefined base config is normalized", () => { + + const configs = new FlatConfigArray([], { baseConfig: [void 0] }); + + assert.throws(() => { + configs.normalizeSync(); + }, "Config (unnamed): Unexpected undefined config at base index 0."); + + }); + + it("should throw an error when undefined base config is normalized asynchronously", async () => { + + const configs = new FlatConfigArray([], { baseConfig: [void 0] }); + + try { + await configs.normalize(); + assert.fail("Error not thrown"); + } catch (error) { + assert.strictEqual(error.message, "Config (unnamed): Unexpected undefined config at base index 0."); + } + + }); + + it("should throw an error when null base config is normalized", () => { + + const configs = new FlatConfigArray([], { baseConfig: [null] }); + + assert.throws(() => { + configs.normalizeSync(); + }, "Config (unnamed): Unexpected null config at base index 0."); + + }); + + it("should throw an error when null base config is normalized asynchronously", async () => { + + const configs = new FlatConfigArray([], { baseConfig: [null] }); + + try { + await configs.normalize(); + assert.fail("Error not thrown"); + } catch (error) { + assert.strictEqual(error.message, "Config (unnamed): Unexpected null config at base index 0."); + } + }); + it("should throw an error when undefined user-defined config is normalized", () => { + + const configs = new FlatConfigArray([]); + + configs.push(void 0); + + assert.throws(() => { + configs.normalizeSync(); + }, "Config (unnamed): Unexpected undefined config at user-defined index 0."); + + }); + + it("should throw an error when undefined user-defined config is normalized asynchronously", async () => { + + const configs = new FlatConfigArray([]); + + configs.push(void 0); + + try { + await configs.normalize(); + assert.fail("Error not thrown"); + } catch (error) { + assert.strictEqual(error.message, "Config (unnamed): Unexpected undefined config at user-defined index 0."); + } + + }); + + it("should throw an error when null user-defined config is normalized", () => { + + const configs = new FlatConfigArray([]); + + configs.push(null); + + assert.throws(() => { + configs.normalizeSync(); + }, "Config (unnamed): Unexpected null config at user-defined index 0."); + + }); + + it("should throw an error when null user-defined config is normalized asynchronously", async () => { + + const configs = new FlatConfigArray([]); + + configs.push(null); + + try { + await configs.normalize(); + assert.fail("Error not thrown"); + } catch (error) { + assert.strictEqual(error.message, "Config (unnamed): Unexpected null config at user-defined index 0."); + } + + }); + + }); describe("Config Properties", () => {