From 4cc2a1b680507ff006b5d2b02fa6d262584bb263 Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Sat, 17 Feb 2024 06:37:22 +1300 Subject: [PATCH] feat: add should-be-fine support for flat configs (#1505) * feat: support flat config * fix: redo flat config * docs: add note about flat config support * fix: handle snapshot processor * chore: remove unneeded cast * chore: exclude flat configs from docs * test: use property matches to reduce snapshot size --- .eslint-doc-generatorrc.js | 8 +- README.md | 123 +++++++++++- .../__snapshots__/rules.test.ts.snap | 176 ++++++++++++++++++ src/__tests__/rules.test.ts | 23 ++- src/index.ts | 72 +++++-- 5 files changed, 376 insertions(+), 26 deletions(-) diff --git a/.eslint-doc-generatorrc.js b/.eslint-doc-generatorrc.js index ddab92b57..0a06c3fec 100644 --- a/.eslint-doc-generatorrc.js +++ b/.eslint-doc-generatorrc.js @@ -3,7 +3,13 @@ const { prettier: prettierRC } = require('./package.json'); /** @type {import('eslint-doc-generator').GenerateOptions} */ const config = { - ignoreConfig: ['all'], + ignoreConfig: [ + 'all', + 'flat/all', + 'flat/recommended', + 'flat/style', + 'flat/snapshots', + ], ruleDocTitleFormat: 'desc-parens-name', ruleDocSectionInclude: ['Rule details'], ruleListColumns: [ diff --git a/README.md b/README.md index e9622e747..c996614cf 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,15 @@ yarn add --dev eslint eslint-plugin-jest ## Usage +> [!NOTE] +> +> `eslint.config.js` is supported, though most of the plugin documentation still +> currently uses `.eslintrc` syntax. +> +> Refer to the +> [ESLint documentation on the new configuration file format](https://eslint.org/docs/latest/use/configure/configuration-files-new) +> for more. + Add `jest` to the plugins section of your `.eslintrc` configuration file. You can omit the `eslint-plugin-` prefix: @@ -85,7 +94,7 @@ test-related. This means it's generally not suitable to include them in your top-level configuration as that applies to all files being linted which can include source files. -You can use +For `.eslintrc` configs you can use [overrides](https://eslint.org/docs/user-guide/configuring/configuration-files#how-do-overrides-work) to have ESLint apply additional rules to specific files: @@ -106,6 +115,30 @@ to have ESLint apply additional rules to specific files: } ``` +For `eslint.config.js` you can use +[`files` and `ignores`](https://eslint.org/docs/latest/use/configure/configuration-files-new#specifying-files-and-ignores): + +```js +const jest = require('eslint-plugin-jest'); + +module.exports = [ + ...require('@eslint/js').configs.recommended, + { + files: ['test/**'], + ...jest.configs['flat/recommended'], + rules: { + ...jest.configs['flat/recommended'], + 'jest/prefer-expect-assertions': 'off', + }, + }, + // you can also configure jest rules in other objects, so long as some of the `files` match + { + files: ['test/**'], + rules: { 'jest/prefer-expect-assertions': 'off' }, + }, +]; +``` + ### Jest `version` setting The behaviour of some rules (specifically [`no-deprecated-functions`][]) change @@ -145,13 +178,18 @@ module.exports = { ## Shareable configurations +> [!NOTE] +> +> `eslint.config.js` compatible versions of configs are available prefixed with +> `flat/` and may be subject to small breaking changes while ESLint v9 is being +> finalized. + ### Recommended This plugin exports a recommended configuration that enforces good testing practices. -To enable this configuration use the `extends` property in your `.eslintrc` -config file: +To enable this configuration with `.eslintrc`, use the `extends` property: ```json { @@ -159,6 +197,22 @@ config file: } ``` +To enable this configuration with `eslint.config.js`, use +`jest.configs['flat/recommended']`: + +```js +const jest = require('eslint-plugin-jest'); + +module.exports = [ + { + files: [ + /* glob matching your test files */ + ], + ...jest.configs['flat/recommended'], + }, +]; +``` + ### Style This plugin also exports a configuration named `style`, which adds some @@ -174,9 +228,21 @@ config file: } ``` -See -[ESLint documentation](https://eslint.org/docs/user-guide/configuring/configuration-files#extending-configuration-files) -for more information about extending configuration files. +To enable this configuration with `eslint.config.js`, use +`jest.configs['flat/style']`: + +```js +const jest = require('eslint-plugin-jest'); + +module.exports = [ + { + files: [ + /* glob matching your test files */ + ], + ...jest.configs['flat/style'], + }, +]; +``` ### All @@ -189,10 +255,55 @@ If you want to enable all rules instead of only some you can do so by adding the } ``` +To enable this configuration with `eslint.config.js`, use +`jest.configs['flat/all']`: + +```js +const jest = require('eslint-plugin-jest'); + +module.exports = [ + { + files: [ + /* glob matching your test files */ + ], + ...jest.configs['flat/all'], + }, +]; +``` + While the `recommended` and `style` configurations only change in major versions the `all` configuration may change in any release and is thus unsuited for installations requiring long-term consistency. +## Snapshot processing + +> [!NOTE] +> +> This is only relevant for `eslint.config.js` + +This plugin provides a +[custom processor](https://eslint.org/docs/latest/extend/custom-processors) to +allow rules to "lint" snapshot files. + +For `.eslintrc` based configs, this is automatically enabled out of the box but +must be opted into for `eslint.config.js` using the `flat/snapshots` config: + +```js +const jest = require('eslint-plugin-jest'); + +module.exports = [ + { + ...jest.configs['flat/snapshots'], + rules: { + 'jest/no-large-snapshots': ['error', { maxSize: 1 }], + }, + }, +]; +``` + +Unlike other configs, this includes a `files` array that matches `.snap` files +meaning you can use it directly + ## Rules diff --git a/src/__tests__/__snapshots__/rules.test.ts.snap b/src/__tests__/__snapshots__/rules.test.ts.snap index 7d2de014b..9bd68055d 100644 --- a/src/__tests__/__snapshots__/rules.test.ts.snap +++ b/src/__tests__/__snapshots__/rules.test.ts.snap @@ -64,6 +64,182 @@ exports[`rules should export configs that refer to actual rules 1`] = ` "jest/valid-title": "error", }, }, + "flat/all": { + "languageOptions": { + "globals": { + "afterAll": false, + "afterEach": false, + "beforeAll": false, + "beforeEach": false, + "describe": false, + "expect": false, + "fit": false, + "it": false, + "jest": false, + "test": false, + "xdescribe": false, + "xit": false, + "xtest": false, + }, + }, + "plugins": { + "jest": ObjectContaining { + "meta": { + "name": "eslint-plugin-jest", + "version": "27.8.0", + }, + }, + }, + "rules": { + "jest/consistent-test-it": "error", + "jest/expect-expect": "error", + "jest/max-expects": "error", + "jest/max-nested-describe": "error", + "jest/no-alias-methods": "error", + "jest/no-commented-out-tests": "error", + "jest/no-conditional-expect": "error", + "jest/no-conditional-in-test": "error", + "jest/no-confusing-set-timeout": "error", + "jest/no-deprecated-functions": "error", + "jest/no-disabled-tests": "error", + "jest/no-done-callback": "error", + "jest/no-duplicate-hooks": "error", + "jest/no-export": "error", + "jest/no-focused-tests": "error", + "jest/no-hooks": "error", + "jest/no-identical-title": "error", + "jest/no-interpolation-in-snapshots": "error", + "jest/no-jasmine-globals": "error", + "jest/no-large-snapshots": "error", + "jest/no-mocks-import": "error", + "jest/no-restricted-jest-methods": "error", + "jest/no-restricted-matchers": "error", + "jest/no-standalone-expect": "error", + "jest/no-test-prefixes": "error", + "jest/no-test-return-statement": "error", + "jest/no-untyped-mock-factory": "error", + "jest/prefer-called-with": "error", + "jest/prefer-comparison-matcher": "error", + "jest/prefer-each": "error", + "jest/prefer-equality-matcher": "error", + "jest/prefer-expect-assertions": "error", + "jest/prefer-expect-resolves": "error", + "jest/prefer-hooks-in-order": "error", + "jest/prefer-hooks-on-top": "error", + "jest/prefer-lowercase-title": "error", + "jest/prefer-mock-promise-shorthand": "error", + "jest/prefer-snapshot-hint": "error", + "jest/prefer-spy-on": "error", + "jest/prefer-strict-equal": "error", + "jest/prefer-to-be": "error", + "jest/prefer-to-contain": "error", + "jest/prefer-to-have-length": "error", + "jest/prefer-todo": "error", + "jest/require-hook": "error", + "jest/require-to-throw-message": "error", + "jest/require-top-level-describe": "error", + "jest/unbound-method": "error", + "jest/valid-describe-callback": "error", + "jest/valid-expect": "error", + "jest/valid-expect-in-promise": "error", + "jest/valid-title": "error", + }, + }, + "flat/recommended": { + "languageOptions": { + "globals": { + "afterAll": false, + "afterEach": false, + "beforeAll": false, + "beforeEach": false, + "describe": false, + "expect": false, + "fit": false, + "it": false, + "jest": false, + "test": false, + "xdescribe": false, + "xit": false, + "xtest": false, + }, + }, + "plugins": { + "jest": ObjectContaining { + "meta": { + "name": "eslint-plugin-jest", + "version": "27.8.0", + }, + }, + }, + "rules": { + "jest/expect-expect": "warn", + "jest/no-alias-methods": "error", + "jest/no-commented-out-tests": "warn", + "jest/no-conditional-expect": "error", + "jest/no-deprecated-functions": "error", + "jest/no-disabled-tests": "warn", + "jest/no-done-callback": "error", + "jest/no-export": "error", + "jest/no-focused-tests": "error", + "jest/no-identical-title": "error", + "jest/no-interpolation-in-snapshots": "error", + "jest/no-jasmine-globals": "error", + "jest/no-mocks-import": "error", + "jest/no-standalone-expect": "error", + "jest/no-test-prefixes": "error", + "jest/valid-describe-callback": "error", + "jest/valid-expect": "error", + "jest/valid-expect-in-promise": "error", + "jest/valid-title": "error", + }, + }, + "flat/snapshots": { + "files": [ + "**/*.snap", + ], + "plugins": { + "jest": ObjectContaining { + "meta": { + "name": "eslint-plugin-jest", + "version": "27.8.0", + }, + }, + }, + "processor": "jest/snapshots", + }, + "flat/style": { + "languageOptions": { + "globals": { + "afterAll": false, + "afterEach": false, + "beforeAll": false, + "beforeEach": false, + "describe": false, + "expect": false, + "fit": false, + "it": false, + "jest": false, + "test": false, + "xdescribe": false, + "xit": false, + "xtest": false, + }, + }, + "plugins": { + "jest": ObjectContaining { + "meta": { + "name": "eslint-plugin-jest", + "version": "27.8.0", + }, + }, + }, + "rules": { + "jest/no-alias-methods": "warn", + "jest/prefer-to-be": "error", + "jest/prefer-to-contain": "error", + "jest/prefer-to-have-length": "error", + }, + }, "recommended": { "env": { "jest/globals": true, diff --git a/src/__tests__/rules.test.ts b/src/__tests__/rules.test.ts index 2948c2414..bdc589b0d 100644 --- a/src/__tests__/rules.test.ts +++ b/src/__tests__/rules.test.ts @@ -48,19 +48,38 @@ describe('rules', () => { }); it('should export configs that refer to actual rules', () => { + const expectJestPlugin = expect.objectContaining({ + meta: { + name: 'eslint-plugin-jest', + version: '27.8.0', + }, + }); + const recommendedConfigs = plugin.configs; - expect(recommendedConfigs).toMatchSnapshot(); + expect(recommendedConfigs).toMatchSnapshot({ + 'flat/recommended': { plugins: { jest: expectJestPlugin } }, + 'flat/style': { plugins: { jest: expectJestPlugin } }, + 'flat/all': { plugins: { jest: expectJestPlugin } }, + 'flat/snapshots': { plugins: { jest: expectJestPlugin } }, + }); expect(Object.keys(recommendedConfigs)).toEqual([ 'all', 'recommended', 'style', + 'flat/all', + 'flat/recommended', + 'flat/style', + 'flat/snapshots', ]); expect(Object.keys(recommendedConfigs.all.rules)).toHaveLength( ruleNames.length - deprecatedRules.length, ); + expect(Object.keys(recommendedConfigs['flat/all'].rules)).toHaveLength( + ruleNames.length - deprecatedRules.length, + ); const allConfigRules = Object.values(recommendedConfigs) - .map(config => Object.keys(config.rules)) + .map(config => Object.keys(config.rules ?? {})) .reduce((previousValue, currentValue) => [ ...previousValue, ...currentValue, diff --git a/src/index.ts b/src/index.ts index f1956d1e2..2e51e605b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -79,31 +79,69 @@ const allRules = Object.fromEntries( .map(([name]) => [`jest/${name}`, 'error']), ); -const createConfig = (rules: Record) => ({ - plugins: ['jest'], - env: { 'jest/globals': true }, - rules, -}); - -export = { +const plugin = { meta: { name: packageName, version: packageVersion }, - configs: { - all: createConfig(allRules), - recommended: createConfig(recommendedRules), - style: createConfig({ - 'jest/no-alias-methods': 'warn', - 'jest/prefer-to-be': 'error', - 'jest/prefer-to-contain': 'error', - 'jest/prefer-to-have-length': 'error', - }), - }, + // ugly cast for now to keep TypeScript happy since + // we don't have types for flat config yet + configs: {} as Record< + | 'all' + | 'recommended' + | 'style' + | 'flat/all' + | 'flat/recommended' + | 'flat/style' + | 'flat/snapshots', + Pick, 'rules'> + >, environments: { globals: { globals, }, }, processors: { + snapshots: snapshotProcessor, '.snap': snapshotProcessor, }, rules, }; + +const createRCConfig = (rules: Record) => ({ + plugins: ['jest'], + env: { 'jest/globals': true }, + rules, +}); + +const createFlatConfig = ( + rules: Record, +) => ({ + plugins: { jest: plugin }, + languageOptions: { globals }, + rules, +}); + +plugin.configs = { + all: createRCConfig(allRules), + recommended: createRCConfig(recommendedRules), + style: createRCConfig({ + 'jest/no-alias-methods': 'warn', + 'jest/prefer-to-be': 'error', + 'jest/prefer-to-contain': 'error', + 'jest/prefer-to-have-length': 'error', + }), + 'flat/all': createFlatConfig(allRules), + 'flat/recommended': createFlatConfig(recommendedRules), + 'flat/style': createFlatConfig({ + 'jest/no-alias-methods': 'warn', + 'jest/prefer-to-be': 'error', + 'jest/prefer-to-contain': 'error', + 'jest/prefer-to-have-length': 'error', + }), + 'flat/snapshots': { + // @ts-expect-error this is introduced in flat config + files: ['**/*.snap'], + plugins: { jest: plugin }, + processor: 'jest/snapshots', + }, +}; + +export = plugin;