From 23988449fd09ae16ab29b4a1ed84bb657a0375f9 Mon Sep 17 00:00:00 2001 From: Maxime Gerbe Date: Tue, 16 Jan 2024 15:18:52 +0100 Subject: [PATCH] feat: prefer importing jest globals [new rule] - Fix #1101 Issue: https://github.com/jest-community/eslint-plugin-jest/issues/1101 --- README.md | 1 + docs/rules/prefer-importing-jest-globals.md | 44 ++++++++++++++ .../__snapshots__/rules.test.ts.snap | 1 + src/__tests__/rules.test.ts | 2 +- .../prefer-importing-jest-globals.test.ts | 38 +++++++++++++ src/rules/prefer-importing-jest-globals.ts | 57 +++++++++++++++++++ 6 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 docs/rules/prefer-importing-jest-globals.md create mode 100644 src/rules/__tests__/prefer-importing-jest-globals.test.ts create mode 100644 src/rules/prefer-importing-jest-globals.ts diff --git a/README.md b/README.md index e9622e747..f2945c19a 100644 --- a/README.md +++ b/README.md @@ -248,6 +248,7 @@ set to warn in.\ | [prefer-expect-resolves](docs/rules/prefer-expect-resolves.md) | Prefer `await expect(...).resolves` over `expect(await ...)` syntax | | | 🔧 | | | | [prefer-hooks-in-order](docs/rules/prefer-hooks-in-order.md) | Prefer having hooks in a consistent order | | | | | | | [prefer-hooks-on-top](docs/rules/prefer-hooks-on-top.md) | Suggest having hooks before any test cases | | | | | | +| [prefer-importing-jest-globals](docs/rules/prefer-importing-jest-globals.md) | Prefer importing Jest globals | | | | | | | [prefer-lowercase-title](docs/rules/prefer-lowercase-title.md) | Enforce lowercase test names | | | 🔧 | | | | [prefer-mock-promise-shorthand](docs/rules/prefer-mock-promise-shorthand.md) | Prefer mock resolved/rejected shorthands for promises | | | 🔧 | | | | [prefer-snapshot-hint](docs/rules/prefer-snapshot-hint.md) | Prefer including a hint with external snapshots | | | | | | diff --git a/docs/rules/prefer-importing-jest-globals.md b/docs/rules/prefer-importing-jest-globals.md new file mode 100644 index 000000000..f270a6d9c --- /dev/null +++ b/docs/rules/prefer-importing-jest-globals.md @@ -0,0 +1,44 @@ +# Prefer importing Jest globals (`prefer-importing-jest-globals`) + + + +This rule aims to enforce explicit imports from `@jest/globals`. + +1. This is useful for ensuring that the Jest APIs are imported the same way in + the codebase. +2. When you can't modify Jest's + [`injectGlobals`](https://jestjs.io/docs/configuration#injectglobals-boolean) + configuration property, this rule can help to ensure that the Jest globals + are imported explicitly and facilitate a migration to `@jest/globals`. + +## Rule details + +Examples of **incorrect** code for this rule + +```js +/* eslint jest/prefer-importing-jest-globals: "error" */ + +describe('foo', () => { + it('accepts this input', () => { + // ... + }); +}); +``` + +Examples of **correct** code for this rule + +```js +/* eslint jest/prefer-importing-jest-globals: "error" */ + +import { describe, it } from '@jest/globals'; + +describe('foo', () => { + it('accepts this input', () => { + // ... + }); +}); +``` + +## Further Reading + +- [Documentation](https://jestjs.io/docs/api) diff --git a/src/__tests__/__snapshots__/rules.test.ts.snap b/src/__tests__/__snapshots__/rules.test.ts.snap index 7d2de014b..3cb45fdfb 100644 --- a/src/__tests__/__snapshots__/rules.test.ts.snap +++ b/src/__tests__/__snapshots__/rules.test.ts.snap @@ -45,6 +45,7 @@ exports[`rules should export configs that refer to actual rules 1`] = ` "jest/prefer-expect-resolves": "error", "jest/prefer-hooks-in-order": "error", "jest/prefer-hooks-on-top": "error", + "jest/prefer-importing-jest-globals": "error", "jest/prefer-lowercase-title": "error", "jest/prefer-mock-promise-shorthand": "error", "jest/prefer-snapshot-hint": "error", diff --git a/src/__tests__/rules.test.ts b/src/__tests__/rules.test.ts index 2948c2414..1cceca55a 100644 --- a/src/__tests__/rules.test.ts +++ b/src/__tests__/rules.test.ts @@ -2,7 +2,7 @@ import { existsSync } from 'fs'; import { resolve } from 'path'; import plugin from '../'; -const numberOfRules = 53; +const numberOfRules = 54; const ruleNames = Object.keys(plugin.rules); const deprecatedRules = Object.entries(plugin.rules) .filter(([, rule]) => rule.meta.deprecated) diff --git a/src/rules/__tests__/prefer-importing-jest-globals.test.ts b/src/rules/__tests__/prefer-importing-jest-globals.test.ts new file mode 100644 index 000000000..98a01d52e --- /dev/null +++ b/src/rules/__tests__/prefer-importing-jest-globals.test.ts @@ -0,0 +1,38 @@ +import { TSESLint } from '@typescript-eslint/utils'; +import dedent from 'dedent'; +import rule from '../prefer-importing-jest-globals'; +import { espreeParser } from './test-utils'; + +const ruleTester = new TSESLint.RuleTester({ + parser: espreeParser, + parserOptions: { + ecmaVersion: 2015, + sourceType: 'module', + }, +}); + +ruleTester.run('prefer-importing-jest-globals', rule, { + valid: [ + { + code: dedent` + import { test, expect } from '@jest/globals'; + + test('should pass', () => { + expect(true).toBeDefined(); + }); + `, + parserOptions: { sourceType: 'module' }, + }, + ], + invalid: [ + { + code: dedent` + it("foo"); + `, + parserOptions: { sourceType: 'module' }, + errors: [ + { endColumn: 11, column: 1, messageId: 'preferImportingJestGlobal' }, + ], + }, + ], +}); diff --git a/src/rules/prefer-importing-jest-globals.ts b/src/rules/prefer-importing-jest-globals.ts new file mode 100644 index 000000000..4a06b8170 --- /dev/null +++ b/src/rules/prefer-importing-jest-globals.ts @@ -0,0 +1,57 @@ +import globalsJson from '../globals.json'; +import { createRule, parseJestFnCall } from './utils'; + +export default createRule({ + name: __filename, + meta: { + docs: { + category: 'Best Practices', + description: 'Prefer importing Jest globals', + recommended: false, + }, + messages: { + preferImportingJestGlobal: + "Jest function \"{{ jestFunction }} is used but not imported from '@jest/globals'", + }, + type: 'problem', + schema: [], + }, + defaultOptions: [], + create(context) { + const jestGlobalFunctions = Object.keys(globalsJson); + const importedJestFunctions: string[] = []; + const usedJestFunctions = new Set(); + + return { + CallExpression(node) { + const jestFnCall = parseJestFnCall(node, context); + + if (!jestFnCall) { + return; + } + if ( + jestFnCall.head.type === 'import' && + jestGlobalFunctions.includes(jestFnCall.name) + ) { + importedJestFunctions.push(jestFnCall.name); + } + + /* istanbul ignore else */ + if (jestGlobalFunctions.includes(jestFnCall.name)) { + usedJestFunctions.add(jestFnCall.name); + } + }, + 'Program:exit'() { + usedJestFunctions.forEach(jestFunction => { + if (!importedJestFunctions.includes(jestFunction)) { + context.report({ + node: context.getSourceCode().ast, + messageId: 'preferImportingJestGlobal', + data: { jestFunction }, + }); + } + }); + }, + }; + }, +});