Content-Length: 1148400 | pFad | http://github.com/phaux/typescript-eslint/commit/78e7cf48907f089724f84b25669e8ad9f200903d

D9 fix(typescript-eslint): gracefully handle invalid flat config objects… · phaux/typescript-eslint@78e7cf4 · GitHub
Skip to content

Commit 78e7cf4

Browse files
kirkwaiblingerphaux
authored andcommitted
fix(typescript-eslint): gracefully handle invalid flat config objects in config helper (typescript-eslint#11070)
redo from main
1 parent baa4b47 commit 78e7cf4

File tree

2 files changed

+188
-77
lines changed

2 files changed

+188
-77
lines changed

packages/typescript-eslint/src/config-helper.ts

Lines changed: 113 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -92,70 +92,123 @@ export type ConfigArray = TSESLint.FlatConfig.ConfigArray;
9292
export function config(
9393
...configs: InfiniteDepthConfigWithExtends[]
9494
): ConfigArray {
95-
const flattened =
96-
// @ts-expect-error -- intentionally an infinite type
97-
configs.flat(Infinity) as ConfigWithExtends[];
98-
return flattened.flatMap((configWithExtends, configIndex) => {
99-
const { extends: extendsArr, ...config } = configWithExtends;
100-
if (extendsArr == null || extendsArr.length === 0) {
101-
return config;
102-
}
103-
const extendsArrFlattened = extendsArr.flat(
104-
Infinity,
105-
) as ConfigWithExtends[];
106-
107-
const undefinedExtensions = extendsArrFlattened.reduce<number[]>(
108-
(acc, extension, extensionIndex) => {
109-
const maybeExtension = extension as
110-
| TSESLint.FlatConfig.Config
111-
| undefined;
112-
if (maybeExtension == null) {
113-
acc.push(extensionIndex);
95+
return configImpl(...configs);
96+
}
97+
98+
// Implementation of the config function without assuming the runtime type of
99+
// the input.
100+
function configImpl(...configs: unknown[]): ConfigArray {
101+
const flattened = configs.flat(Infinity);
102+
return flattened.flatMap(
103+
(
104+
configWithExtends,
105+
configIndex,
106+
): TSESLint.FlatConfig.Config | TSESLint.FlatConfig.Config[] => {
107+
if (
108+
configWithExtends == null ||
109+
typeof configWithExtends !== 'object' ||
110+
!('extends' in configWithExtends)
111+
) {
112+
// Unless the object is a config object with extends key, just forward it
113+
// along to eslint.
114+
return configWithExtends as TSESLint.FlatConfig.Config;
115+
}
116+
117+
const { extends: extendsArr, ..._config } = configWithExtends;
118+
const config = _config as {
119+
name?: unknown;
120+
extends?: unknown;
121+
files?: unknown;
122+
ignores?: unknown;
123+
};
124+
125+
if (extendsArr == null) {
126+
// If the extends value is nullish, just forward along the rest of the
127+
// config object to eslint.
128+
return config as TSESLint.FlatConfig.Config;
129+
}
130+
131+
const name = ((): string | undefined => {
132+
if ('name' in configWithExtends && configWithExtends.name != null) {
133+
if (typeof configWithExtends.name !== 'string') {
134+
throw new Error(
135+
`tseslint.config(): Config at index ${configIndex} has a 'name' property that is not a string.`,
136+
);
137+
}
138+
return configWithExtends.name;
114139
}
115-
return acc;
116-
},
117-
[],
118-
);
119-
if (undefinedExtensions.length) {
120-
const configName =
121-
configWithExtends.name != null
122-
? `, named "${configWithExtends.name}",`
123-
: ' (anonymous)';
124-
const extensionIndices = undefinedExtensions.join(', ');
125-
throw new Error(
126-
`Your config at index ${configIndex}${configName} contains undefined` +
127-
` extensions at the following indices: ${extensionIndices}.`,
128-
);
129-
}
130-
131-
const configArray = [];
132-
133-
for (const extension of extendsArrFlattened) {
134-
const name = [config.name, extension.name].filter(Boolean).join('__');
135-
if (isPossiblyGlobalIgnores(extension)) {
136-
// If it's a global ignores, then just pass it along
137-
configArray.push({
138-
...extension,
139-
...(name && { name }),
140-
});
141-
} else {
142-
configArray.push({
143-
...extension,
144-
...(config.files && { files: config.files }),
145-
...(config.ignores && { ignores: config.ignores }),
146-
...(name && { name }),
147-
});
140+
return undefined;
141+
})();
142+
const nameErrorPhrase =
143+
name != null ? `, named "${name}",` : ' (anonymous)';
144+
145+
if (!Array.isArray(extendsArr)) {
146+
throw new TypeError(
147+
`tseslint.config(): Config at index ${configIndex}${nameErrorPhrase} has an 'extends' property that is not an array.`,
148+
);
148149
}
149-
}
150150

151-
// If the base config could form a global ignores object, then we mustn't include
152-
// it in the output. Otherwise, we must add it in order for it to have effect.
153-
if (!isPossiblyGlobalIgnores(config)) {
154-
configArray.push(config);
155-
}
151+
const extendsArrFlattened = (extendsArr as unknown[]).flat(Infinity);
152+
153+
const nonObjectExtensions = [];
154+
for (const [extensionIndex, extension] of extendsArrFlattened.entries()) {
155+
// special error message to be clear we don't support eslint's stringly typed extends.
156+
// https://eslint.org/docs/latest/use/configure/configuration-files#extending-configurations
157+
if (typeof extension === 'string') {
158+
throw new Error(
159+
`tseslint.config(): Config at index ${configIndex}${nameErrorPhrase} has an 'extends' array that contains a string (${JSON.stringify(extension)}) at index ${extensionIndex}.` +
160+
" This is a feature of eslint's `defineConfig()` helper and is not supported by typescript-eslint." +
161+
' Please provide a config object instead.',
162+
);
163+
}
164+
if (extension == null || typeof extension !== 'object') {
165+
nonObjectExtensions.push(extensionIndex);
166+
}
167+
}
168+
if (nonObjectExtensions.length > 0) {
169+
const extensionIndices = nonObjectExtensions.join(', ');
170+
throw new Error(
171+
`tseslint.config(): Config at index ${configIndex}${nameErrorPhrase} contains non-object` +
172+
` extensions at the following indices: ${extensionIndices}.`,
173+
);
174+
}
175+
176+
const configArray = [];
177+
178+
for (const _extension of extendsArrFlattened) {
179+
const extension = _extension as {
180+
name?: unknown;
181+
files?: unknown;
182+
ignores?: unknown;
183+
};
184+
const resolvedConfigName = [name, extension.name]
185+
.filter(Boolean)
186+
.join('__');
187+
if (isPossiblyGlobalIgnores(extension)) {
188+
// If it's a global ignores, then just pass it along
189+
configArray.push({
190+
...extension,
191+
...(resolvedConfigName !== '' ? { name: resolvedConfigName } : {}),
192+
});
193+
} else {
194+
configArray.push({
195+
...extension,
196+
...(config.files ? { files: config.files } : {}),
197+
...(config.ignores ? { ignores: config.ignores } : {}),
198+
...(resolvedConfigName !== '' ? { name: resolvedConfigName } : {}),
199+
});
200+
}
201+
}
202+
203+
// If the base config could form a global ignores object, then we mustn't include
204+
// it in the output. Otherwise, we must add it in order for it to have effect.
205+
if (!isPossiblyGlobalIgnores(config)) {
206+
configArray.push(config);
207+
}
156208

157-
return configArray;
158-
});
209+
return configArray as ConfigArray;
210+
},
211+
);
159212
}
160213

161214
/**

packages/typescript-eslint/tests/config-helper.test.ts

Lines changed: 75 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import type { TSESLint } from '@typescript-eslint/utils';
22

3-
import plugin from '../src/index';
3+
import tseslint from '../src/index';
44

55
describe('config helper', () => {
66
it('works without extends', () => {
77
expect(
8-
plugin.config({
8+
tseslint.config({
99
files: ['file'],
1010
ignores: ['ignored'],
1111
rules: { rule: 'error' },
@@ -21,7 +21,7 @@ describe('config helper', () => {
2121

2222
it('flattens extended configs', () => {
2323
expect(
24-
plugin.config({
24+
tseslint.config({
2525
extends: [{ rules: { rule1: 'error' } }, { rules: { rule2: 'error' } }],
2626
rules: { rule: 'error' },
2727
}),
@@ -34,7 +34,7 @@ describe('config helper', () => {
3434

3535
it('flattens extended configs with files and ignores', () => {
3636
expect(
37-
plugin.config({
37+
tseslint.config({
3838
extends: [{ rules: { rule1: 'error' } }, { rules: { rule2: 'error' } }],
3939
files: ['common-file'],
4040
ignores: ['common-ignored'],
@@ -63,7 +63,7 @@ describe('config helper', () => {
6363
const extension: TSESLint.FlatConfig.Config = { rules: { rule1: 'error' } };
6464

6565
expect(() =>
66-
plugin.config(
66+
tseslint.config(
6767
{
6868
extends: [extension],
6969
files: ['common-file'],
@@ -81,7 +81,7 @@ describe('config helper', () => {
8181
},
8282
),
8383
).toThrow(
84-
'Your config at index 1, named "my-config-2", contains undefined ' +
84+
'tseslint.config(): Config at index 1, named "my-config-2", contains non-object ' +
8585
'extensions at the following indices: 0, 2',
8686
);
8787
});
@@ -90,7 +90,7 @@ describe('config helper', () => {
9090
const extension: TSESLint.FlatConfig.Config = { rules: { rule1: 'error' } };
9191

9292
expect(() =>
93-
plugin.config(
93+
tseslint.config(
9494
{
9595
extends: [extension],
9696
files: ['common-file'],
@@ -107,14 +107,14 @@ describe('config helper', () => {
107107
},
108108
),
109109
).toThrow(
110-
'Your config at index 1 (anonymous) contains undefined extensions at ' +
110+
'tseslint.config(): Config at index 1 (anonymous) contains non-object extensions at ' +
111111
'the following indices: 0, 2',
112112
);
113113
});
114114

115115
it('flattens extended configs with config name', () => {
116116
expect(
117-
plugin.config({
117+
tseslint.config({
118118
extends: [{ rules: { rule1: 'error' } }, { rules: { rule2: 'error' } }],
119119
files: ['common-file'],
120120
ignores: ['common-ignored'],
@@ -145,7 +145,7 @@ describe('config helper', () => {
145145

146146
it('flattens extended configs with names if base config is unnamed', () => {
147147
expect(
148-
plugin.config({
148+
tseslint.config({
149149
extends: [
150150
{ name: 'extension-1', rules: { rule1: 'error' } },
151151
{ rules: { rule2: 'error' } },
@@ -176,7 +176,7 @@ describe('config helper', () => {
176176

177177
it('merges config items names', () => {
178178
expect(
179-
plugin.config({
179+
tseslint.config({
180180
extends: [
181181
{ name: 'extension-1', rules: { rule1: 'error' } },
182182
{ rules: { rule2: 'error' } },
@@ -210,7 +210,7 @@ describe('config helper', () => {
210210

211211
it('allows nested arrays in the config function', () => {
212212
expect(
213-
plugin.config(
213+
tseslint.config(
214214
{ rules: { rule1: 'error' } },
215215
[{ rules: { rule2: 'error' } }],
216216
[[{ rules: { rule3: 'error' } }]],
@@ -228,7 +228,7 @@ describe('config helper', () => {
228228

229229
it('allows nested arrays in extends', () => {
230230
expect(
231-
plugin.config({
231+
tseslint.config({
232232
extends: [
233233
{ rules: { rule1: 'error' } },
234234
[{ rules: { rule2: 'error' } }],
@@ -249,7 +249,7 @@ describe('config helper', () => {
249249
});
250250

251251
it('does not create global ignores in extends', () => {
252-
const configWithIgnores = plugin.config({
252+
const configWithIgnores = tseslint.config({
253253
extends: [{ rules: { rule1: 'error' } }, { rules: { rule2: 'error' } }],
254254
ignores: ['ignored'],
255255
});
@@ -265,7 +265,7 @@ describe('config helper', () => {
265265
});
266266

267267
it('creates noop config in extends', () => {
268-
const configWithMetadata = plugin.config({
268+
const configWithMetadata = tseslint.config({
269269
extends: [{ rules: { rule1: 'error' } }, { rules: { rule2: 'error' } }],
270270
files: ['file'],
271271
ignores: ['ignored'],
@@ -297,7 +297,7 @@ describe('config helper', () => {
297297

298298
it('does not create global ignores when extending empty configs', () => {
299299
expect(
300-
plugin.config({
300+
tseslint.config({
301301
extends: [{ rules: { rule1: 'error' } }, {}],
302302
ignores: ['ignored'],
303303
}),
@@ -310,10 +310,68 @@ describe('config helper', () => {
310310

311311
it('handles name field when global-ignoring in extension', () => {
312312
expect(
313-
plugin.config({
313+
tseslint.config({
314314
extends: [{ ignores: ['files/**/*'], name: 'global-ignore-stuff' }],
315315
ignores: ['ignored'],
316316
}),
317317
).toEqual([{ ignores: ['files/**/*'], name: 'global-ignore-stuff' }]);
318318
});
319+
320+
it('throws error when extends is not an array', () => {
321+
expect(() =>
322+
tseslint.config({
323+
// @ts-expect-error purposely testing invalid values
324+
extends: 42,
325+
}),
326+
).toThrow(
327+
"tseslint.config(): Config at index 0 (anonymous) has an 'extends' property that is not an array.",
328+
);
329+
});
330+
331+
it.each([undefined, null, 'not a config object', 42])(
332+
'passes invalid arguments through unchanged',
333+
config => {
334+
expect(
335+
tseslint.config(
336+
// @ts-expect-error purposely testing invalid values
337+
config,
338+
),
339+
).toStrictEqual([config]);
340+
},
341+
);
342+
343+
it('gives a special error message for string extends', () => {
344+
expect(() =>
345+
tseslint.config({
346+
// @ts-expect-error purposely testing invalid values
347+
extends: ['some-string'],
348+
}),
349+
).toThrow(
350+
'tseslint.config(): Config at index 0 (anonymous) has an \'extends\' array that contains a string ("some-string") at index 0. ' +
351+
"This is a feature of eslint's `defineConfig()` helper and is not supported by typescript-eslint. " +
352+
'Please provide a config object instead.',
353+
);
354+
});
355+
356+
it('strips nullish extends arrays from the config object', () => {
357+
expect(
358+
tseslint.config({
359+
// @ts-expect-error purposely testing invalid values
360+
extends: null,
361+
files: ['files'],
362+
}),
363+
).toEqual([{ files: ['files'] }]);
364+
});
365+
366+
it('complains when given an object with an invalid name', () => {
367+
expect(() =>
368+
tseslint.config({
369+
extends: [],
370+
// @ts-expect-error purposely testing invalid values
371+
name: 42,
372+
}),
373+
).toThrow(
374+
"tseslint.config(): Config at index 0 has a 'name' property that is not a string.",
375+
);
376+
});
319377
});

0 commit comments

Comments
 (0)








ApplySandwichStrip

pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

Fetched URL: http://github.com/phaux/typescript-eslint/commit/78e7cf48907f089724f84b25669e8ad9f200903d

Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy