diff --git a/packages/typescript-eslint/src/config-helper.ts b/packages/typescript-eslint/src/config-helper.ts index 606ddaebf5ba..6f8f5b9bfb27 100644 --- a/packages/typescript-eslint/src/config-helper.ts +++ b/packages/typescript-eslint/src/config-helper.ts @@ -128,17 +128,42 @@ export function config( ); } - return [ - ...extendsArrFlattened.map(extension => { - const name = [config.name, extension.name].filter(Boolean).join('__'); - return { + 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 }), - }; - }), - config, - ]; + }); + } + } + + // 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; }); } + +/** + * This utility function returns false if the config objects contains any field + * that would prevent it from being considered a global ignores object and true + * otherwise. Note in particular that the `ignores` field may not be present and + * the return value can still be true. + */ +function isPossiblyGlobalIgnores(config: object): boolean { + return Object.keys(config).every(key => ['name', 'ignores'].includes(key)); +} diff --git a/packages/typescript-eslint/tests/config-helper.test.ts b/packages/typescript-eslint/tests/config-helper.test.ts index b30ba7958658..1564267090ea 100644 --- a/packages/typescript-eslint/tests/config-helper.test.ts +++ b/packages/typescript-eslint/tests/config-helper.test.ts @@ -247,4 +247,73 @@ describe('config helper', () => { { rules: { rule: 'error' } }, ]); }); + + it('does not create global ignores in extends', () => { + const configWithIgnores = plugin.config({ + extends: [{ rules: { rule1: 'error' } }, { rules: { rule2: 'error' } }], + ignores: ['ignored'], + }); + + expect(configWithIgnores).toEqual([ + { ignores: ['ignored'], rules: { rule1: 'error' } }, + { ignores: ['ignored'], rules: { rule2: 'error' } }, + ]); + expect(configWithIgnores).not.toContainEqual( + // Should not create global ignores + { ignores: ['ignored'] }, + ); + }); + + it('creates noop config in extends', () => { + const configWithMetadata = plugin.config({ + extends: [{ rules: { rule1: 'error' } }, { rules: { rule2: 'error' } }], + files: ['file'], + ignores: ['ignored'], + name: 'my-config', + }); + + expect(configWithMetadata).toEqual([ + { + files: ['file'], + ignores: ['ignored'], + name: 'my-config', + rules: { rule1: 'error' }, + }, + { + files: ['file'], + ignores: ['ignored'], + name: 'my-config', + rules: { rule2: 'error' }, + }, + // it would also be ok for this not to be present, but we want to align + // with the eslint `defineConfig()` behavior. + { + files: ['file'], + ignores: ['ignored'], + name: 'my-config', + }, + ]); + }); + + it('does not create global ignores when extending empty configs', () => { + expect( + plugin.config({ + extends: [{ rules: { rule1: 'error' } }, {}], + ignores: ['ignored'], + }), + ).toEqual([ + { ignores: ['ignored'], rules: { rule1: 'error' } }, + // Should not create global ignores + {}, + ]); + }); + + it('handles name field when global-ignoring in extension', () => { + expect( + plugin.config({ + extends: [{ ignores: ['files/**/*'], name: 'global-ignore-stuff' }], + ignores: ['ignored'], + }), + ).toEqual([{ ignores: ['files/**/*'], name: 'global-ignore-stuff' }]); + }); });