diff --git a/packages/typescript-eslint/src/config-helper.ts b/packages/typescript-eslint/src/config-helper.ts index 6f8f5b9bfb27..ce5d4ea806ed 100644 --- a/packages/typescript-eslint/src/config-helper.ts +++ b/packages/typescript-eslint/src/config-helper.ts @@ -92,70 +92,123 @@ export type ConfigArray = TSESLint.FlatConfig.ConfigArray; export function config( ...configs: InfiniteDepthConfigWithExtends[] ): ConfigArray { - const flattened = - // @ts-expect-error -- intentionally an infinite type - configs.flat(Infinity) as ConfigWithExtends[]; - return flattened.flatMap((configWithExtends, configIndex) => { - const { extends: extendsArr, ...config } = configWithExtends; - if (extendsArr == null || extendsArr.length === 0) { - return config; - } - const extendsArrFlattened = extendsArr.flat( - Infinity, - ) as ConfigWithExtends[]; - - const undefinedExtensions = extendsArrFlattened.reduce( - (acc, extension, extensionIndex) => { - const maybeExtension = extension as - | TSESLint.FlatConfig.Config - | undefined; - if (maybeExtension == null) { - acc.push(extensionIndex); + return configImpl(...configs); +} + +// Implementation of the config function without assuming the runtime type of +// the input. +function configImpl(...configs: unknown[]): ConfigArray { + const flattened = configs.flat(Infinity); + return flattened.flatMap( + ( + configWithExtends, + configIndex, + ): TSESLint.FlatConfig.Config | TSESLint.FlatConfig.Config[] => { + if ( + configWithExtends == null || + typeof configWithExtends !== 'object' || + !('extends' in configWithExtends) + ) { + // Unless the object is a config object with extends key, just forward it + // along to eslint. + return configWithExtends as TSESLint.FlatConfig.Config; + } + + const { extends: extendsArr, ..._config } = configWithExtends; + const config = _config as { + name?: unknown; + extends?: unknown; + files?: unknown; + ignores?: unknown; + }; + + if (extendsArr == null) { + // If the extends value is nullish, just forward along the rest of the + // config object to eslint. + return config as TSESLint.FlatConfig.Config; + } + + const name = ((): string | undefined => { + if ('name' in configWithExtends && configWithExtends.name != null) { + if (typeof configWithExtends.name !== 'string') { + throw new Error( + `tseslint.config(): Config at index ${configIndex} has a 'name' property that is not a string.`, + ); + } + return configWithExtends.name; } - return acc; - }, - [], - ); - if (undefinedExtensions.length) { - const configName = - configWithExtends.name != null - ? `, named "${configWithExtends.name}",` - : ' (anonymous)'; - const extensionIndices = undefinedExtensions.join(', '); - throw new Error( - `Your config at index ${configIndex}${configName} contains undefined` + - ` extensions at the following indices: ${extensionIndices}.`, - ); - } - - const configArray = []; - - for (const extension of extendsArrFlattened) { - const name = [config.name, extension.name].filter(Boolean).join('__'); - if (isPossiblyGlobalIgnores(extension)) { - // If it's a global ignores, then just pass it along - configArray.push({ - ...extension, - ...(name && { name }), - }); - } else { - configArray.push({ - ...extension, - ...(config.files && { files: config.files }), - ...(config.ignores && { ignores: config.ignores }), - ...(name && { name }), - }); + return undefined; + })(); + const nameErrorPhrase = + name != null ? `, named "${name}",` : ' (anonymous)'; + + if (!Array.isArray(extendsArr)) { + throw new TypeError( + `tseslint.config(): Config at index ${configIndex}${nameErrorPhrase} has an 'extends' property that is not an array.`, + ); } - } - // If the base config could form a global ignores object, then we mustn't include - // it in the output. Otherwise, we must add it in order for it to have effect. - if (!isPossiblyGlobalIgnores(config)) { - configArray.push(config); - } + const extendsArrFlattened = (extendsArr as unknown[]).flat(Infinity); + + const nonObjectExtensions = []; + for (const [extensionIndex, extension] of extendsArrFlattened.entries()) { + // special error message to be clear we don't support eslint's stringly typed extends. + // https://eslint.org/docs/latest/use/configure/configuration-files#extending-configurations + if (typeof extension === 'string') { + throw new Error( + `tseslint.config(): Config at index ${configIndex}${nameErrorPhrase} has an 'extends' array that contains a string (${JSON.stringify(extension)}) at index ${extensionIndex}.` + + " This is a feature of eslint's `defineConfig()` helper and is not supported by typescript-eslint." + + ' Please provide a config object instead.', + ); + } + if (extension == null || typeof extension !== 'object') { + nonObjectExtensions.push(extensionIndex); + } + } + if (nonObjectExtensions.length > 0) { + const extensionIndices = nonObjectExtensions.join(', '); + throw new Error( + `tseslint.config(): Config at index ${configIndex}${nameErrorPhrase} contains non-object` + + ` extensions at the following indices: ${extensionIndices}.`, + ); + } + + const configArray = []; + + for (const _extension of extendsArrFlattened) { + const extension = _extension as { + name?: unknown; + files?: unknown; + ignores?: unknown; + }; + const resolvedConfigName = [name, extension.name] + .filter(Boolean) + .join('__'); + if (isPossiblyGlobalIgnores(extension)) { + // If it's a global ignores, then just pass it along + configArray.push({ + ...extension, + ...(resolvedConfigName !== '' ? { name: resolvedConfigName } : {}), + }); + } else { + configArray.push({ + ...extension, + ...(config.files ? { files: config.files } : {}), + ...(config.ignores ? { ignores: config.ignores } : {}), + ...(resolvedConfigName !== '' ? { name: resolvedConfigName } : {}), + }); + } + } + + // If the base config could form a global ignores object, then we mustn't include + // it in the output. Otherwise, we must add it in order for it to have effect. + if (!isPossiblyGlobalIgnores(config)) { + configArray.push(config); + } - return configArray; - }); + return configArray as ConfigArray; + }, + ); } /** diff --git a/packages/typescript-eslint/tests/config-helper.test.ts b/packages/typescript-eslint/tests/config-helper.test.ts index 1564267090ea..2def1d4256c4 100644 --- a/packages/typescript-eslint/tests/config-helper.test.ts +++ b/packages/typescript-eslint/tests/config-helper.test.ts @@ -1,11 +1,11 @@ import type { TSESLint } from '@typescript-eslint/utils'; -import plugin from '../src/index'; +import tseslint from '../src/index'; describe('config helper', () => { it('works without extends', () => { expect( - plugin.config({ + tseslint.config({ files: ['file'], ignores: ['ignored'], rules: { rule: 'error' }, @@ -21,7 +21,7 @@ describe('config helper', () => { it('flattens extended configs', () => { expect( - plugin.config({ + tseslint.config({ extends: [{ rules: { rule1: 'error' } }, { rules: { rule2: 'error' } }], rules: { rule: 'error' }, }), @@ -34,7 +34,7 @@ describe('config helper', () => { it('flattens extended configs with files and ignores', () => { expect( - plugin.config({ + tseslint.config({ extends: [{ rules: { rule1: 'error' } }, { rules: { rule2: 'error' } }], files: ['common-file'], ignores: ['common-ignored'], @@ -63,7 +63,7 @@ describe('config helper', () => { const extension: TSESLint.FlatConfig.Config = { rules: { rule1: 'error' } }; expect(() => - plugin.config( + tseslint.config( { extends: [extension], files: ['common-file'], @@ -81,7 +81,7 @@ describe('config helper', () => { }, ), ).toThrow( - 'Your config at index 1, named "my-config-2", contains undefined ' + + 'tseslint.config(): Config at index 1, named "my-config-2", contains non-object ' + 'extensions at the following indices: 0, 2', ); }); @@ -90,7 +90,7 @@ describe('config helper', () => { const extension: TSESLint.FlatConfig.Config = { rules: { rule1: 'error' } }; expect(() => - plugin.config( + tseslint.config( { extends: [extension], files: ['common-file'], @@ -107,14 +107,14 @@ describe('config helper', () => { }, ), ).toThrow( - 'Your config at index 1 (anonymous) contains undefined extensions at ' + + 'tseslint.config(): Config at index 1 (anonymous) contains non-object extensions at ' + 'the following indices: 0, 2', ); }); it('flattens extended configs with config name', () => { expect( - plugin.config({ + tseslint.config({ extends: [{ rules: { rule1: 'error' } }, { rules: { rule2: 'error' } }], files: ['common-file'], ignores: ['common-ignored'], @@ -145,7 +145,7 @@ describe('config helper', () => { it('flattens extended configs with names if base config is unnamed', () => { expect( - plugin.config({ + tseslint.config({ extends: [ { name: 'extension-1', rules: { rule1: 'error' } }, { rules: { rule2: 'error' } }, @@ -176,7 +176,7 @@ describe('config helper', () => { it('merges config items names', () => { expect( - plugin.config({ + tseslint.config({ extends: [ { name: 'extension-1', rules: { rule1: 'error' } }, { rules: { rule2: 'error' } }, @@ -210,7 +210,7 @@ describe('config helper', () => { it('allows nested arrays in the config function', () => { expect( - plugin.config( + tseslint.config( { rules: { rule1: 'error' } }, [{ rules: { rule2: 'error' } }], [[{ rules: { rule3: 'error' } }]], @@ -228,7 +228,7 @@ describe('config helper', () => { it('allows nested arrays in extends', () => { expect( - plugin.config({ + tseslint.config({ extends: [ { rules: { rule1: 'error' } }, [{ rules: { rule2: 'error' } }], @@ -249,7 +249,7 @@ describe('config helper', () => { }); it('does not create global ignores in extends', () => { - const configWithIgnores = plugin.config({ + const configWithIgnores = tseslint.config({ extends: [{ rules: { rule1: 'error' } }, { rules: { rule2: 'error' } }], ignores: ['ignored'], }); @@ -265,7 +265,7 @@ describe('config helper', () => { }); it('creates noop config in extends', () => { - const configWithMetadata = plugin.config({ + const configWithMetadata = tseslint.config({ extends: [{ rules: { rule1: 'error' } }, { rules: { rule2: 'error' } }], files: ['file'], ignores: ['ignored'], @@ -297,7 +297,7 @@ describe('config helper', () => { it('does not create global ignores when extending empty configs', () => { expect( - plugin.config({ + tseslint.config({ extends: [{ rules: { rule1: 'error' } }, {}], ignores: ['ignored'], }), @@ -310,10 +310,68 @@ describe('config helper', () => { it('handles name field when global-ignoring in extension', () => { expect( - plugin.config({ + tseslint.config({ extends: [{ ignores: ['files/**/*'], name: 'global-ignore-stuff' }], ignores: ['ignored'], }), ).toEqual([{ ignores: ['files/**/*'], name: 'global-ignore-stuff' }]); }); + + it('throws error when extends is not an array', () => { + expect(() => + tseslint.config({ + // @ts-expect-error purposely testing invalid values + extends: 42, + }), + ).toThrow( + "tseslint.config(): Config at index 0 (anonymous) has an 'extends' property that is not an array.", + ); + }); + + it.each([undefined, null, 'not a config object', 42])( + 'passes invalid arguments through unchanged', + config => { + expect( + tseslint.config( + // @ts-expect-error purposely testing invalid values + config, + ), + ).toStrictEqual([config]); + }, + ); + + it('gives a special error message for string extends', () => { + expect(() => + tseslint.config({ + // @ts-expect-error purposely testing invalid values + extends: ['some-string'], + }), + ).toThrow( + 'tseslint.config(): Config at index 0 (anonymous) has an \'extends\' array that contains a string ("some-string") at index 0. ' + + "This is a feature of eslint's `defineConfig()` helper and is not supported by typescript-eslint. " + + 'Please provide a config object instead.', + ); + }); + + it('strips nullish extends arrays from the config object', () => { + expect( + tseslint.config({ + // @ts-expect-error purposely testing invalid values + extends: null, + files: ['files'], + }), + ).toEqual([{ files: ['files'] }]); + }); + + it('complains when given an object with an invalid name', () => { + expect(() => + tseslint.config({ + extends: [], + // @ts-expect-error purposely testing invalid values + name: 42, + }), + ).toThrow( + "tseslint.config(): Config at index 0 has a 'name' property that is not a string.", + ); + }); }); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy