diff --git a/eslint.config.mjs b/eslint.config.mjs index 6ffdaa9831a..135fbc4260d 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -391,6 +391,7 @@ export default tseslint.config( }, settings: { vitest: { typecheck: true } }, }, + { files: ['packages/*/tests/**/vitest-custom-matchers.d.ts'], name: 'vitest-custom-matchers-declaration-files', @@ -403,6 +404,7 @@ export default tseslint.config( '@typescript-eslint/no-explicit-any': 'off', }, }, + // plugin rule tests { files: [ diff --git a/packages/typescript-eslint/package.json b/packages/typescript-eslint/package.json index e59a531f543..9eb27120657 100644 --- a/packages/typescript-eslint/package.json +++ b/packages/typescript-eslint/package.json @@ -43,8 +43,7 @@ ], "scripts": { "build": "tsc -b tsconfig.build.json", - "clean": "tsc -b tsconfig.build.json --clean", - "postclean": "rimraf dist/ coverage/", + "clean": "rimraf dist/ coverage/", "format": "prettier --write \"./**/*.{ts,mts,cts,tsx,js,mjs,cjs,jsx,json,md,css}\" --ignore-path ../../.prettierignore", "lint": "nx lint", "test": "vitest --run --config=$INIT_CWD/vitest.config.mts", diff --git a/packages/typescript-eslint/tests/config-helper.test.ts b/packages/typescript-eslint/tests/config-helper.test.ts index 2def1d4256c..748f6762626 100644 --- a/packages/typescript-eslint/tests/config-helper.test.ts +++ b/packages/typescript-eslint/tests/config-helper.test.ts @@ -1,6 +1,6 @@ import type { TSESLint } from '@typescript-eslint/utils'; -import tseslint from '../src/index'; +import tseslint from '../src/index.js'; describe('config helper', () => { it('works without extends', () => { @@ -10,7 +10,7 @@ describe('config helper', () => { ignores: ['ignored'], rules: { rule: 'error' }, }), - ).toEqual([ + ).toStrictEqual([ { files: ['file'], ignores: ['ignored'], @@ -25,7 +25,7 @@ describe('config helper', () => { extends: [{ rules: { rule1: 'error' } }, { rules: { rule2: 'error' } }], rules: { rule: 'error' }, }), - ).toEqual([ + ).toStrictEqual([ { rules: { rule1: 'error' } }, { rules: { rule2: 'error' } }, { rules: { rule: 'error' } }, @@ -40,7 +40,7 @@ describe('config helper', () => { ignores: ['common-ignored'], rules: { rule: 'error' }, }), - ).toEqual([ + ).toStrictEqual([ { files: ['common-file'], ignores: ['common-ignored'], @@ -62,7 +62,7 @@ describe('config helper', () => { it('throws error containing config name when some extensions are undefined', () => { const extension: TSESLint.FlatConfig.Config = { rules: { rule1: 'error' } }; - expect(() => + expect(() => { tseslint.config( { extends: [extension], @@ -79,8 +79,8 @@ describe('config helper', () => { name: 'my-config-2', rules: { rule: 'error' }, }, - ), - ).toThrow( + ); + }).toThrow( 'tseslint.config(): Config at index 1, named "my-config-2", contains non-object ' + 'extensions at the following indices: 0, 2', ); @@ -89,7 +89,7 @@ describe('config helper', () => { it('throws error without config name when some extensions are undefined', () => { const extension: TSESLint.FlatConfig.Config = { rules: { rule1: 'error' } }; - expect(() => + expect(() => { tseslint.config( { extends: [extension], @@ -105,8 +105,8 @@ describe('config helper', () => { ignores: ['common-ignored'], rules: { rule: 'error' }, }, - ), - ).toThrow( + ); + }).toThrow( 'tseslint.config(): Config at index 1 (anonymous) contains non-object extensions at ' + 'the following indices: 0, 2', ); @@ -121,7 +121,7 @@ describe('config helper', () => { name: 'my-config', rules: { rule: 'error' }, }), - ).toEqual([ + ).toStrictEqual([ { files: ['common-file'], ignores: ['common-ignored'], @@ -154,7 +154,7 @@ describe('config helper', () => { ignores: ['common-ignored'], rules: { rule: 'error' }, }), - ).toEqual([ + ).toStrictEqual([ { files: ['common-file'], ignores: ['common-ignored'], @@ -186,7 +186,7 @@ describe('config helper', () => { name: 'my-config', rules: { rule: 'error' }, }), - ).toEqual([ + ).toStrictEqual([ { files: ['common-file'], ignores: ['common-ignored'], @@ -217,7 +217,7 @@ describe('config helper', () => { [[[{ rules: { rule4: 'error' } }]]], [[[[{ rules: { rule5: 'error' } }]]]], ), - ).toEqual([ + ).toStrictEqual([ { rules: { rule1: 'error' } }, { rules: { rule2: 'error' } }, { rules: { rule3: 'error' } }, @@ -238,7 +238,7 @@ describe('config helper', () => { ], rules: { rule: 'error' }, }), - ).toEqual([ + ).toStrictEqual([ { rules: { rule1: 'error' } }, { rules: { rule2: 'error' } }, { rules: { rule3: 'error' } }, @@ -254,7 +254,7 @@ describe('config helper', () => { ignores: ['ignored'], }); - expect(configWithIgnores).toEqual([ + expect(configWithIgnores).toStrictEqual([ { ignores: ['ignored'], rules: { rule1: 'error' } }, { ignores: ['ignored'], rules: { rule2: 'error' } }, ]); @@ -272,7 +272,7 @@ describe('config helper', () => { name: 'my-config', }); - expect(configWithMetadata).toEqual([ + expect(configWithMetadata).toStrictEqual([ { files: ['file'], ignores: ['ignored'], @@ -301,7 +301,7 @@ describe('config helper', () => { extends: [{ rules: { rule1: 'error' } }, {}], ignores: ['ignored'], }), - ).toEqual([ + ).toStrictEqual([ { ignores: ['ignored'], rules: { rule1: 'error' } }, // Should not create global ignores {}, @@ -314,23 +314,23 @@ describe('config helper', () => { extends: [{ ignores: ['files/**/*'], name: 'global-ignore-stuff' }], ignores: ['ignored'], }), - ).toEqual([{ ignores: ['files/**/*'], name: 'global-ignore-stuff' }]); + ).toStrictEqual([{ ignores: ['files/**/*'], name: 'global-ignore-stuff' }]); }); it('throws error when extends is not an array', () => { - expect(() => + expect(() => { tseslint.config({ // @ts-expect-error purposely testing invalid values extends: 42, - }), - ).toThrow( + }); + }).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])( + it.for([[undefined], [null], ['not a config object'], [42]] as const)( 'passes invalid arguments through unchanged', - config => { + ([config], { expect }) => { expect( tseslint.config( // @ts-expect-error purposely testing invalid values @@ -341,12 +341,12 @@ describe('config helper', () => { ); it('gives a special error message for string extends', () => { - expect(() => + expect(() => { tseslint.config({ // @ts-expect-error purposely testing invalid values extends: ['some-string'], - }), - ).toThrow( + }); + }).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.', @@ -360,17 +360,17 @@ describe('config helper', () => { extends: null, files: ['files'], }), - ).toEqual([{ files: ['files'] }]); + ).toStrictEqual([{ files: ['files'] }]); }); it('complains when given an object with an invalid name', () => { - expect(() => + expect(() => { tseslint.config({ extends: [], // @ts-expect-error purposely testing invalid values name: 42, - }), - ).toThrow( + }); + }).toThrow( "tseslint.config(): Config at index 0 has a 'name' property that is not a string.", ); }); diff --git a/packages/typescript-eslint/tests/configs.test.ts b/packages/typescript-eslint/tests/configs.test.ts index 3be135ea1e5..fea7a41196f 100644 --- a/packages/typescript-eslint/tests/configs.test.ts +++ b/packages/typescript-eslint/tests/configs.test.ts @@ -5,7 +5,7 @@ import type { import rules from '@typescript-eslint/eslint-plugin/use-at-your-own-risk/rules'; -import tseslint from '../src/index'; +import tseslint from '../src/index.js'; const RULE_NAME_PREFIX = '@typescript-eslint/'; const EXTENSION_RULES = Object.entries(rules) @@ -23,8 +23,9 @@ const EXTENSION_RULES = Object.entries(rules) function filterRules( values: FlatConfig.Rules | undefined, ): [string, FlatConfig.RuleEntry][] { - expect(values).toBeDefined(); - return Object.entries(values!) + assert.isDefined(values); + + return Object.entries(values) .filter((pair): pair is [string, FlatConfig.RuleEntry] => pair[1] != null) .filter(([name]) => name.startsWith(RULE_NAME_PREFIX)); } @@ -88,227 +89,298 @@ function filterAndMapRuleConfigs({ }); } -function itHasBaseRulesOverriden( - unfilteredConfigRules: FlatConfig.Rules | undefined, -): void { - it('has the base rules overriden by the appropriate extension rules', () => { - expect(unfilteredConfigRules).toBeDefined(); - const ruleNames = new Set(Object.keys(unfilteredConfigRules!)); - EXTENSION_RULES.forEach(([ruleName, extRuleName]) => { - if (ruleNames.has(ruleName)) { - // this looks a little weird, but it provides the cleanest test output style - expect(unfilteredConfigRules).toMatchObject({ - ...unfilteredConfigRules, - [extRuleName]: 'off', - }); - } - }); - }); -} +const localTest = test.extend<{ + unfilteredConfigRules: FlatConfig.Rules | undefined; + expectedOverrides: Record; + configRulesObject: Record; +}>({ + configRulesObject: [ + async ({ unfilteredConfigRules }, use) => { + const configRules = filterRules(unfilteredConfigRules); -describe('all.ts', () => { - const unfilteredConfigRules = tseslint.configs.all[2]?.rules; + const configRulesObject = Object.fromEntries(configRules); + + await use(configRulesObject); + }, + { auto: false }, + ], + + expectedOverrides: [ + async ({ unfilteredConfigRules }, use) => { + assert.isDefined(unfilteredConfigRules); + + const ruleNames = new Set(Object.keys(unfilteredConfigRules)); + + const expectedOverrides = Object.fromEntries( + EXTENSION_RULES.filter(([ruleName]) => ruleNames.has(ruleName)).map( + ([, extRuleName]) => [extRuleName, 'off'] as const, + ), + ); + + await use(expectedOverrides); + }, + { auto: false }, + ], - it('contains all of the rules', () => { - const configRules = filterRules(unfilteredConfigRules); + unfilteredConfigRules: [tseslint.configs.all[2]?.rules, { auto: true }], +}); + +describe('all.ts', () => { + localTest('contains all of the rules', ({ configRulesObject }) => { // note: exclude deprecated rules, this config is allowed to change between minor versions const ruleConfigs = filterAndMapRuleConfigs({ excludeDeprecated: true, }); - expect(Object.fromEntries(ruleConfigs)).toEqual( - Object.fromEntries(configRules), - ); + expect(Object.fromEntries(ruleConfigs)).toStrictEqual(configRulesObject); }); - itHasBaseRulesOverriden(unfilteredConfigRules); + localTest( + 'has the base rules overridden by the appropriate extension rules', + ({ expectedOverrides, unfilteredConfigRules }) => { + expect(unfilteredConfigRules).toMatchObject(expectedOverrides); + }, + ); }); describe('disable-type-checked.ts', () => { - const unfilteredConfigRules = tseslint.configs.disableTypeChecked.rules; - - it('disables all type checked rules', () => { - const configRules = filterRules(unfilteredConfigRules); + localTest.scoped({ + unfilteredConfigRules: tseslint.configs.disableTypeChecked.rules, + }); - const ruleConfigs: [string, string][] = Object.entries(rules) + localTest('disables all type checked rules', ({ configRulesObject }) => { + const ruleConfigs = Object.entries(rules) .filter(([, rule]) => rule.meta.docs.requiresTypeChecking) - .map(([name]) => [`${RULE_NAME_PREFIX}${name}`, 'off']); + .map(([name]) => [`${RULE_NAME_PREFIX}${name}`, 'off'] as const); - expect(Object.fromEntries(ruleConfigs)).toEqual( - Object.fromEntries(configRules), - ); + expect(Object.fromEntries(ruleConfigs)).toStrictEqual(configRulesObject); }); }); describe('recommended.ts', () => { - const unfilteredConfigRules = tseslint.configs.recommended[2]?.rules; - - it('contains all recommended rules, excluding type checked ones', () => { - const configRules = filterRules(unfilteredConfigRules); - // note: include deprecated rules so that the config doesn't change between major bumps - const ruleConfigs = filterAndMapRuleConfigs({ - recommendations: ['recommended'], - typeChecked: 'exclude', - }); - - expect(Object.fromEntries(ruleConfigs)).toEqual( - Object.fromEntries(configRules), - ); + localTest.scoped({ + unfilteredConfigRules: tseslint.configs.recommended[2]?.rules, }); - itHasBaseRulesOverriden(unfilteredConfigRules); + localTest( + 'contains all recommended rules, excluding type checked ones', + ({ configRulesObject }) => { + // note: include deprecated rules so that the config doesn't change between major bumps + const ruleConfigs = filterAndMapRuleConfigs({ + recommendations: ['recommended'], + typeChecked: 'exclude', + }); + + expect(Object.fromEntries(ruleConfigs)).toStrictEqual(configRulesObject); + }, + ); + + localTest( + 'has the base rules overridden by the appropriate extension rules', + ({ expectedOverrides, unfilteredConfigRules }) => { + expect(unfilteredConfigRules).toMatchObject(expectedOverrides); + }, + ); }); describe('recommended-type-checked.ts', () => { - const unfilteredConfigRules = - tseslint.configs.recommendedTypeChecked[2]?.rules; + localTest.scoped({ + unfilteredConfigRules: tseslint.configs.recommendedTypeChecked[2]?.rules, + }); - it('contains all recommended rules', () => { - const configRules = filterRules(unfilteredConfigRules); + localTest('contains all recommended rules', ({ configRulesObject }) => { // note: include deprecated rules so that the config doesn't change between major bumps const ruleConfigs = filterAndMapRuleConfigs({ recommendations: ['recommended'], }); - expect(Object.fromEntries(ruleConfigs)).toEqual( - Object.fromEntries(configRules), - ); + expect(Object.fromEntries(ruleConfigs)).toStrictEqual(configRulesObject); }); - itHasBaseRulesOverriden(unfilteredConfigRules); + localTest( + 'has the base rules overridden by the appropriate extension rules', + ({ expectedOverrides, unfilteredConfigRules }) => { + expect(unfilteredConfigRules).toMatchObject(expectedOverrides); + }, + ); }); describe('recommended-type-checked-only.ts', () => { - const unfilteredConfigRules = - tseslint.configs.recommendedTypeCheckedOnly[2]?.rules; - - it('contains only type-checked recommended rules', () => { - const configRules = filterRules(unfilteredConfigRules); - // note: include deprecated rules so that the config doesn't change between major bumps - const ruleConfigs = filterAndMapRuleConfigs({ - recommendations: ['recommended'], - typeChecked: 'include-only', - }).filter(([ruleName]) => ruleName); - - expect(Object.fromEntries(ruleConfigs)).toEqual( - Object.fromEntries(configRules), - ); + localTest.scoped({ + unfilteredConfigRules: + tseslint.configs.recommendedTypeCheckedOnly[2]?.rules, }); - itHasBaseRulesOverriden(unfilteredConfigRules); + localTest( + 'contains only type-checked recommended rules', + ({ configRulesObject }) => { + // note: include deprecated rules so that the config doesn't change between major bumps + const ruleConfigs = filterAndMapRuleConfigs({ + recommendations: ['recommended'], + typeChecked: 'include-only', + }).filter(([ruleName]) => ruleName); + + expect(Object.fromEntries(ruleConfigs)).toStrictEqual(configRulesObject); + }, + ); + + localTest( + 'has the base rules overridden by the appropriate extension rules', + ({ expectedOverrides, unfilteredConfigRules }) => { + expect(unfilteredConfigRules).toMatchObject(expectedOverrides); + }, + ); }); describe('strict.ts', () => { - const unfilteredConfigRules = tseslint.configs.strict[2]?.rules; - - it('contains all strict rules, excluding type checked ones', () => { - const configRules = filterRules(unfilteredConfigRules); - // note: exclude deprecated rules, this config is allowed to change between minor versions - const ruleConfigs = filterAndMapRuleConfigs({ - excludeDeprecated: true, - recommendations: ['recommended', 'strict'], - typeChecked: 'exclude', - }); - - expect(Object.fromEntries(ruleConfigs)).toEqual( - Object.fromEntries(configRules), - ); + localTest.scoped({ + unfilteredConfigRules: tseslint.configs.strict[2]?.rules, }); - itHasBaseRulesOverriden(unfilteredConfigRules); + localTest( + 'contains all strict rules, excluding type checked ones', + ({ configRulesObject }) => { + // note: exclude deprecated rules, this config is allowed to change between minor versions + const ruleConfigs = filterAndMapRuleConfigs({ + excludeDeprecated: true, + recommendations: ['recommended', 'strict'], + typeChecked: 'exclude', + }); + + expect(Object.fromEntries(ruleConfigs)).toStrictEqual(configRulesObject); + }, + ); + + localTest( + 'has the base rules overridden by the appropriate extension rules', + ({ expectedOverrides, unfilteredConfigRules }) => { + expect(unfilteredConfigRules).toMatchObject(expectedOverrides); + }, + ); }); describe('strict-type-checked.ts', () => { - const unfilteredConfigRules = tseslint.configs.strictTypeChecked[2]?.rules; + localTest.scoped({ + unfilteredConfigRules: tseslint.configs.strictTypeChecked[2]?.rules, + }); - it('contains all strict rules', () => { - const configRules = filterRules(unfilteredConfigRules); + localTest('contains all strict rules', ({ configRulesObject }) => { // note: exclude deprecated rules, this config is allowed to change between minor versions const ruleConfigs = filterAndMapRuleConfigs({ excludeDeprecated: true, recommendations: ['recommended', 'strict'], }); - expect(Object.fromEntries(ruleConfigs)).toEqual( - Object.fromEntries(configRules), - ); + expect(Object.fromEntries(ruleConfigs)).toStrictEqual(configRulesObject); }); - itHasBaseRulesOverriden(unfilteredConfigRules); + localTest( + 'has the base rules overridden by the appropriate extension rules', + ({ expectedOverrides, unfilteredConfigRules }) => { + expect(unfilteredConfigRules).toMatchObject(expectedOverrides); + }, + ); }); describe('strict-type-checked-only.ts', () => { - const unfilteredConfigRules = - tseslint.configs.strictTypeCheckedOnly[2]?.rules; - - it('contains only type-checked strict rules', () => { - const configRules = filterRules(unfilteredConfigRules); - // note: exclude deprecated rules, this config is allowed to change between minor versions - const ruleConfigs = filterAndMapRuleConfigs({ - excludeDeprecated: true, - recommendations: ['recommended', 'strict'], - typeChecked: 'include-only', - }).filter(([ruleName]) => ruleName); - - expect(Object.fromEntries(ruleConfigs)).toEqual( - Object.fromEntries(configRules), - ); + localTest.scoped({ + unfilteredConfigRules: tseslint.configs.strictTypeCheckedOnly[2]?.rules, }); - itHasBaseRulesOverriden(unfilteredConfigRules); + localTest( + 'contains only type-checked strict rules', + ({ configRulesObject }) => { + // note: exclude deprecated rules, this config is allowed to change between minor versions + const ruleConfigs = filterAndMapRuleConfigs({ + excludeDeprecated: true, + recommendations: ['recommended', 'strict'], + typeChecked: 'include-only', + }).filter(([ruleName]) => ruleName); + + expect(Object.fromEntries(ruleConfigs)).toStrictEqual(configRulesObject); + }, + ); + + localTest( + 'has the base rules overridden by the appropriate extension rules', + ({ expectedOverrides, unfilteredConfigRules }) => { + expect(unfilteredConfigRules).toMatchObject(expectedOverrides); + }, + ); }); describe('stylistic.ts', () => { - const unfilteredConfigRules = tseslint.configs.stylistic[2]?.rules; - - it('contains all stylistic rules, excluding deprecated or type checked ones', () => { - const configRules = filterRules(unfilteredConfigRules); - // note: include deprecated rules so that the config doesn't change between major bumps - const ruleConfigs = filterAndMapRuleConfigs({ - recommendations: ['stylistic'], - typeChecked: 'exclude', - }); - - expect(Object.fromEntries(ruleConfigs)).toEqual( - Object.fromEntries(configRules), - ); + localTest.scoped({ + unfilteredConfigRules: tseslint.configs.stylistic[2]?.rules, }); - itHasBaseRulesOverriden(unfilteredConfigRules); + localTest( + 'contains all stylistic rules, excluding deprecated or type checked ones', + ({ configRulesObject }) => { + // note: include deprecated rules so that the config doesn't change between major bumps + const ruleConfigs = filterAndMapRuleConfigs({ + recommendations: ['stylistic'], + typeChecked: 'exclude', + }); + + expect(Object.fromEntries(ruleConfigs)).toStrictEqual(configRulesObject); + }, + ); + + localTest( + 'has the base rules overridden by the appropriate extension rules', + ({ expectedOverrides, unfilteredConfigRules }) => { + expect(unfilteredConfigRules).toMatchObject(expectedOverrides); + }, + ); }); describe('stylistic-type-checked.ts', () => { - const unfilteredConfigRules = tseslint.configs.stylisticTypeChecked[2]?.rules; - const configRules = filterRules(unfilteredConfigRules); - // note: include deprecated rules so that the config doesn't change between major bumps - const ruleConfigs = filterAndMapRuleConfigs({ - recommendations: ['stylistic'], + localTest.scoped({ + unfilteredConfigRules: tseslint.configs.stylisticTypeChecked[2]?.rules, }); - it('contains all stylistic rules, excluding deprecated ones', () => { - expect(Object.fromEntries(ruleConfigs)).toEqual( - Object.fromEntries(configRules), - ); - }); + localTest( + 'contains all stylistic rules, excluding deprecated ones', + ({ configRulesObject }) => { + // note: include deprecated rules so that the config doesn't change between major bumps + const ruleConfigs = filterAndMapRuleConfigs({ + recommendations: ['stylistic'], + }); - itHasBaseRulesOverriden(unfilteredConfigRules); + expect(Object.fromEntries(ruleConfigs)).toStrictEqual(configRulesObject); + }, + ); + + localTest( + 'has the base rules overridden by the appropriate extension rules', + ({ expectedOverrides, unfilteredConfigRules }) => { + expect(unfilteredConfigRules).toMatchObject(expectedOverrides); + }, + ); }); describe('stylistic-type-checked-only.ts', () => { - const unfilteredConfigRules = - tseslint.configs.stylisticTypeCheckedOnly[2]?.rules; - - it('contains only type-checked stylistic rules', () => { - const configRules = filterRules(unfilteredConfigRules); - // note: include deprecated rules so that the config doesn't change between major bumps - const ruleConfigs = filterAndMapRuleConfigs({ - recommendations: ['stylistic'], - typeChecked: 'include-only', - }).filter(([ruleName]) => ruleName); - - expect(Object.fromEntries(ruleConfigs)).toEqual( - Object.fromEntries(configRules), - ); + localTest.scoped({ + unfilteredConfigRules: tseslint.configs.stylisticTypeCheckedOnly[2]?.rules, }); - itHasBaseRulesOverriden(unfilteredConfigRules); + localTest( + 'contains only type-checked stylistic rules', + ({ configRulesObject }) => { + // note: include deprecated rules so that the config doesn't change between major bumps + const ruleConfigs = filterAndMapRuleConfigs({ + recommendations: ['stylistic'], + typeChecked: 'include-only', + }).filter(([ruleName]) => ruleName); + + expect(Object.fromEntries(ruleConfigs)).toStrictEqual(configRulesObject); + }, + ); + + localTest( + 'has the base rules overridden by the appropriate extension rules', + ({ expectedOverrides, unfilteredConfigRules }) => { + expect(unfilteredConfigRules).toMatchObject(expectedOverrides); + }, + ); });