Skip to content

Commit 03068f1

Browse files
nzakasfasttime
andauthored
feat: Provide helpful error message for nullish configs (#18357)
* feat: Provide helpful error message for nullish configs fixes #18259 * Update lib/config/flat-config-array.js Co-authored-by: Francesco Trotta <github@fasttime.org> * Update lib/config/flat-config-array.js Co-authored-by: Francesco Trotta <github@fasttime.org> --------- Co-authored-by: Francesco Trotta <github@fasttime.org>
1 parent 50d406d commit 03068f1

3 files changed

Lines changed: 253 additions & 4 deletions

File tree

‎lib/config/flat-config-array.js‎

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,53 @@ function getObjectId(object) {
7979
return name;
8080
}
8181

82+
/**
83+
* Wraps a config error with details about where the error occurred.
84+
* @param {Error} error The original error.
85+
* @param {number} originalLength The original length of the config array.
86+
* @param {number} baseLength The length of the base config.
87+
* @returns {TypeError} The new error with details.
88+
*/
89+
function wrapConfigErrorWithDetails(error, originalLength, baseLength) {
90+
91+
let location = "user-defined";
92+
let configIndex = error.index;
93+
94+
/*
95+
* A config array is set up in this order:
96+
* 1. Base config
97+
* 2. Original configs
98+
* 3. User-defined configs
99+
* 4. CLI-defined configs
100+
*
101+
* So we need to adjust the index to account for the base config.
102+
*
103+
* - If the index is less than the base length, it's in the base config
104+
* (as specified by `baseConfig` argument to `FlatConfigArray` constructor).
105+
* - If the index is greater than the base length but less than the original
106+
* length + base length, it's in the original config. The original config
107+
* is passed to the `FlatConfigArray` constructor as the first argument.
108+
* - Otherwise, it's in the user-defined config, which is loaded from the
109+
* config file and merged with any command-line options.
110+
*/
111+
if (error.index < baseLength) {
112+
location = "base";
113+
} else if (error.index < originalLength + baseLength) {
114+
location = "original";
115+
configIndex = error.index - baseLength;
116+
} else {
117+
configIndex = error.index - originalLength - baseLength;
118+
}
119+
120+
return new TypeError(
121+
`${error.message.slice(0, -1)} at ${location} index ${configIndex}.`,
122+
{ cause: error }
123+
);
124+
}
125+
82126
const originalBaseConfig = Symbol("originalBaseConfig");
127+
const originalLength = Symbol("originalLength");
128+
const baseLength = Symbol("baseLength");
83129

84130
//-----------------------------------------------------------------------------
85131
// Exports
@@ -106,12 +152,24 @@ class FlatConfigArray extends ConfigArray {
106152
schema: flatConfigSchema
107153
});
108154

155+
/**
156+
* The original length of the array before any modifications.
157+
* @type {number}
158+
*/
159+
this[originalLength] = this.length;
160+
109161
if (baseConfig[Symbol.iterator]) {
110162
this.unshift(...baseConfig);
111163
} else {
112164
this.unshift(baseConfig);
113165
}
114166

167+
/**
168+
* The length of the array after applying the base config.
169+
* @type {number}
170+
*/
171+
this[baseLength] = this.length - this[originalLength];
172+
115173
/**
116174
* The base config used to build the config array.
117175
* @type {Array<FlatConfig>}
@@ -129,6 +187,49 @@ class FlatConfigArray extends ConfigArray {
129187
Object.defineProperty(this, "shouldIgnore", { writable: false });
130188
}
131189

190+
/**
191+
* Normalizes the array by calling the superclass method and catching/rethrowing
192+
* any ConfigError exceptions with additional details.
193+
* @param {any} [context] The context to use to normalize the array.
194+
* @returns {Promise<FlatConfigArray>} A promise that resolves when the array is normalized.
195+
*/
196+
normalize(context) {
197+
return super.normalize(context)
198+
.catch(error => {
199+
if (error.name === "ConfigError") {
200+
throw wrapConfigErrorWithDetails(error, this[originalLength], this[baseLength]);
201+
}
202+
203+
throw error;
204+
205+
});
206+
}
207+
208+
/**
209+
* Normalizes the array by calling the superclass method and catching/rethrowing
210+
* any ConfigError exceptions with additional details.
211+
* @param {any} [context] The context to use to normalize the array.
212+
* @returns {FlatConfigArray} The current instance.
213+
* @throws {TypeError} If the config is invalid.
214+
*/
215+
normalizeSync(context) {
216+
217+
try {
218+
219+
return super.normalizeSync(context);
220+
221+
} catch (error) {
222+
223+
if (error.name === "ConfigError") {
224+
throw wrapConfigErrorWithDetails(error, this[originalLength], this[baseLength]);
225+
}
226+
227+
throw error;
228+
229+
}
230+
231+
}
232+
132233
/* eslint-disable class-methods-use-this -- Desired as instance method */
133234
/**
134235
* Replaces a config with another config to allow us to put strings

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070
"@eslint-community/regexpp": "^4.6.1",
7171
"@eslint/eslintrc": "^3.0.2",
7272
"@eslint/js": "9.0.0",
73-
"@humanwhocodes/config-array": "^0.12.3",
73+
"@humanwhocodes/config-array": "^0.13.0",
7474
"@humanwhocodes/module-importer": "^1.0.1",
7575
"@humanwhocodes/retry": "^0.2.3",
7676
"@nodelib/fs.walk": "^1.2.8",

‎tests/lib/config/flat-config-array.js‎

Lines changed: 151 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,9 +191,9 @@ async function assertMergedResult(values, result) {
191191
async function assertInvalidConfig(values, message) {
192192
const configs = createFlatConfigArray(values);
193193

194-
await configs.normalize();
195194

196195
assert.throws(() => {
196+
configs.normalizeSync();
197197
configs.getConfig("foo.js");
198198
}, message);
199199
}
@@ -719,14 +719,162 @@ describe("FlatConfigArray", () => {
719719
describe("Config array elements", () => {
720720
it("should error on 'eslint:recommended' string config", async () => {
721721

722-
await assertInvalidConfig(["eslint:recommended"], "All arguments must be objects.");
722+
await assertInvalidConfig(["eslint:recommended"], "Config (unnamed): Unexpected non-object config at original index 0.");
723723
});
724724

725725
it("should error on 'eslint:all' string config", async () => {
726726

727-
await assertInvalidConfig(["eslint:all"], "All arguments must be objects.");
727+
await assertInvalidConfig(["eslint:all"], "Config (unnamed): Unexpected non-object config at original index 0.");
728+
});
729+
730+
731+
it("should throw an error when undefined original config is normalized", () => {
732+
733+
const configs = new FlatConfigArray([void 0]);
734+
735+
assert.throws(() => {
736+
configs.normalizeSync();
737+
}, "Config (unnamed): Unexpected undefined config at original index 0.");
738+
739+
});
740+
741+
it("should throw an error when undefined original config is normalized asynchronously", async () => {
742+
743+
const configs = new FlatConfigArray([void 0]);
744+
745+
try {
746+
await configs.normalize();
747+
assert.fail("Error not thrown");
748+
} catch (error) {
749+
assert.strictEqual(error.message, "Config (unnamed): Unexpected undefined config at original index 0.");
750+
}
751+
752+
});
753+
754+
it("should throw an error when null original config is normalized", () => {
755+
756+
const configs = new FlatConfigArray([null]);
757+
758+
assert.throws(() => {
759+
configs.normalizeSync();
760+
}, "Config (unnamed): Unexpected null config at original index 0.");
761+
762+
});
763+
764+
it("should throw an error when null original config is normalized asynchronously", async () => {
765+
766+
const configs = new FlatConfigArray([null]);
767+
768+
try {
769+
await configs.normalize();
770+
assert.fail("Error not thrown");
771+
} catch (error) {
772+
assert.strictEqual(error.message, "Config (unnamed): Unexpected null config at original index 0.");
773+
}
774+
775+
});
776+
777+
it("should throw an error when undefined base config is normalized", () => {
778+
779+
const configs = new FlatConfigArray([], { baseConfig: [void 0] });
780+
781+
assert.throws(() => {
782+
configs.normalizeSync();
783+
}, "Config (unnamed): Unexpected undefined config at base index 0.");
784+
785+
});
786+
787+
it("should throw an error when undefined base config is normalized asynchronously", async () => {
788+
789+
const configs = new FlatConfigArray([], { baseConfig: [void 0] });
790+
791+
try {
792+
await configs.normalize();
793+
assert.fail("Error not thrown");
794+
} catch (error) {
795+
assert.strictEqual(error.message, "Config (unnamed): Unexpected undefined config at base index 0.");
796+
}
797+
798+
});
799+
800+
it("should throw an error when null base config is normalized", () => {
801+
802+
const configs = new FlatConfigArray([], { baseConfig: [null] });
803+
804+
assert.throws(() => {
805+
configs.normalizeSync();
806+
}, "Config (unnamed): Unexpected null config at base index 0.");
807+
808+
});
809+
810+
it("should throw an error when null base config is normalized asynchronously", async () => {
811+
812+
const configs = new FlatConfigArray([], { baseConfig: [null] });
813+
814+
try {
815+
await configs.normalize();
816+
assert.fail("Error not thrown");
817+
} catch (error) {
818+
assert.strictEqual(error.message, "Config (unnamed): Unexpected null config at base index 0.");
819+
}
820+
728821
});
729822

823+
it("should throw an error when undefined user-defined config is normalized", () => {
824+
825+
const configs = new FlatConfigArray([]);
826+
827+
configs.push(void 0);
828+
829+
assert.throws(() => {
830+
configs.normalizeSync();
831+
}, "Config (unnamed): Unexpected undefined config at user-defined index 0.");
832+
833+
});
834+
835+
it("should throw an error when undefined user-defined config is normalized asynchronously", async () => {
836+
837+
const configs = new FlatConfigArray([]);
838+
839+
configs.push(void 0);
840+
841+
try {
842+
await configs.normalize();
843+
assert.fail("Error not thrown");
844+
} catch (error) {
845+
assert.strictEqual(error.message, "Config (unnamed): Unexpected undefined config at user-defined index 0.");
846+
}
847+
848+
});
849+
850+
it("should throw an error when null user-defined config is normalized", () => {
851+
852+
const configs = new FlatConfigArray([]);
853+
854+
configs.push(null);
855+
856+
assert.throws(() => {
857+
configs.normalizeSync();
858+
}, "Config (unnamed): Unexpected null config at user-defined index 0.");
859+
860+
});
861+
862+
it("should throw an error when null user-defined config is normalized asynchronously", async () => {
863+
864+
const configs = new FlatConfigArray([]);
865+
866+
configs.push(null);
867+
868+
try {
869+
await configs.normalize();
870+
assert.fail("Error not thrown");
871+
} catch (error) {
872+
assert.strictEqual(error.message, "Config (unnamed): Unexpected null config at user-defined index 0.");
873+
}
874+
875+
});
876+
877+
730878
});
731879

732880
describe("Config Properties", () => {

0 commit comments

Comments
 (0)