From 38eb74aab7c98425f42d4a93252723143ed4d223 Mon Sep 17 00:00:00 2001 From: Tom Quist Date: Fri, 26 Apr 2024 15:19:13 +0200 Subject: [PATCH] feat: prefer importing jest globals for specific types Accessing the `jest` global in ESM must be done either through `import.meta.jest` or by importing it from `@jest/globals`. The latter is useful while migrating to ESM because the former is not accessible in non-ESM. This adds an option to specify the types of globals for which we want to enforce the import. --- docs/rules/prefer-importing-jest-globals.md | 36 +++++++++++++++ .../prefer-importing-jest-globals.test.ts | 46 +++++++++++++++++++ src/rules/prefer-importing-jest-globals.ts | 39 ++++++++++++++-- src/rules/utils/misc.ts | 14 ++++++ 4 files changed, 132 insertions(+), 3 deletions(-) diff --git a/docs/rules/prefer-importing-jest-globals.md b/docs/rules/prefer-importing-jest-globals.md index b7020a40f..49cde56af 100644 --- a/docs/rules/prefer-importing-jest-globals.md +++ b/docs/rules/prefer-importing-jest-globals.md @@ -42,6 +42,42 @@ describe('foo', () => { }); ``` +## Options + +This rule can be configured as follows + +```json5 +{ + type: 'object', + properties: { + types: { + type: 'array', + items: { + type: 'string', + enum: ['hook', 'describe', 'test', 'expect', 'jest', 'unknown'], + }, + }, + }, + additionalProperties: false, +} +``` + +#### types + +A list of Jest global types to enforce explicit imports for. By default, all +Jest globals are enforced. + +This option is useful when you only want to enforce explicit imports for a +subset of Jest globals. For instance, when migrating to ESM, you might want to +enforce explicit imports only for the `jest` global, as of +[Jest's ESM documentation](https://jestjs.io/docs/ecmascript-modules#differences-between-esm-and-commonjs). + +```json5 +{ + 'jest/prefer-importing-jest-globals': ['error', { types: ['jest'] }], +} +``` + ## Further Reading - [Documentation](https://jestjs.io/docs/api) diff --git a/src/rules/__tests__/prefer-importing-jest-globals.test.ts b/src/rules/__tests__/prefer-importing-jest-globals.test.ts index 0bfa96c7c..19bc24b8b 100644 --- a/src/rules/__tests__/prefer-importing-jest-globals.test.ts +++ b/src/rules/__tests__/prefer-importing-jest-globals.test.ts @@ -22,6 +22,25 @@ ruleTester.run('prefer-importing-jest-globals', rule, { `, parserOptions: { sourceType: 'module' }, }, + { + code: dedent` + test('should pass', () => { + expect(true).toBeDefined(); + }); + `, + options: [{ types: ['jest'] }], + parserOptions: { sourceType: 'module' }, + }, + { + code: dedent` + const { it } = require('@jest/globals'); + it('should pass', () => { + expect(true).toBeDefined(); + }); + `, + options: [{ types: ['test'] }], + parserOptions: { sourceType: 'module' }, + }, { code: dedent` // with require @@ -85,6 +104,33 @@ ruleTester.run('prefer-importing-jest-globals', rule, { }, ], }, + { + code: dedent` + jest.useFakeTimers(); + describe("suite", () => { + test("foo"); + expect(true).toBeDefined(); + }) + `, + output: dedent` + import { jest } from '@jest/globals'; + jest.useFakeTimers(); + describe("suite", () => { + test("foo"); + expect(true).toBeDefined(); + }) + `, + options: [{ types: ['jest'] }], + parserOptions: { sourceType: 'module' }, + errors: [ + { + endColumn: 5, + column: 1, + line: 1, + messageId: 'preferImportingJestGlobal', + }, + ], + }, { code: dedent` import React from 'react'; diff --git a/src/rules/prefer-importing-jest-globals.ts b/src/rules/prefer-importing-jest-globals.ts index 49f2b794f..e7f0b0d66 100644 --- a/src/rules/prefer-importing-jest-globals.ts +++ b/src/rules/prefer-importing-jest-globals.ts @@ -1,6 +1,8 @@ import { AST_NODE_TYPES, type TSESTree } from '@typescript-eslint/utils'; import { + type JestFnType, createRule, + exhaustiveStringTuple, getAccessorValue, getSourceCode, isIdentifier, @@ -20,6 +22,15 @@ const createFixerImports = ( : `const { ${allImportsFormatted} } = require('@jest/globals');`; }; +const allJestFnTypes = exhaustiveStringTuple()( + 'hook', + 'describe', + 'test', + 'expect', + 'jest', + 'unknown', +); + export default createRule({ name: __filename, meta: { @@ -31,10 +42,29 @@ export default createRule({ }, fixable: 'code', type: 'problem', - schema: [], + schema: [ + { + type: 'object', + properties: { + types: { + type: 'array', + items: { + type: 'string', + enum: allJestFnTypes, + }, + }, + }, + additionalProperties: false, + }, + ], }, - defaultOptions: [], + defaultOptions: [ + { + types: allJestFnTypes as JestFnType[], + }, + ], create(context) { + const { types = allJestFnTypes } = context.options[0] || {}; const importedFunctionsWithSource: Record = {}; const functionsToImport = new Set(); let reportingNode: TSESTree.Node; @@ -55,7 +85,10 @@ export default createRule({ return; } - if (jestFnCall.head.type !== 'import') { + if ( + jestFnCall.head.type !== 'import' && + types.includes(jestFnCall.type) + ) { functionsToImport.add(jestFnCall.name); reportingNode ||= jestFnCall.head.node; } diff --git a/src/rules/utils/misc.ts b/src/rules/utils/misc.ts index 15c886487..ce5bffbc1 100644 --- a/src/rules/utils/misc.ts +++ b/src/rules/utils/misc.ts @@ -266,3 +266,17 @@ export const getDeclaredVariables = ( context.getDeclaredVariables(node) ); }; + +type AtLeastOne = [T, ...T[]]; +export const exhaustiveTuple = + () => + >( + ...x: L extends any + ? Exclude extends never + ? L + : Array> + : never + ) => + x; +export const exhaustiveStringTuple = () => + exhaustiveTuple();