Skip to content

Commit 38eb74a

Browse files
committed
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.
1 parent 20c8703 commit 38eb74a

File tree

4 files changed

+132
-3
lines changed

4 files changed

+132
-3
lines changed

docs/rules/prefer-importing-jest-globals.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,42 @@ describe('foo', () => {
4242
});
4343
```
4444

45+
## Options
46+
47+
This rule can be configured as follows
48+
49+
```json5
50+
{
51+
type: 'object',
52+
properties: {
53+
types: {
54+
type: 'array',
55+
items: {
56+
type: 'string',
57+
enum: ['hook', 'describe', 'test', 'expect', 'jest', 'unknown'],
58+
},
59+
},
60+
},
61+
additionalProperties: false,
62+
}
63+
```
64+
65+
#### types
66+
67+
A list of Jest global types to enforce explicit imports for. By default, all
68+
Jest globals are enforced.
69+
70+
This option is useful when you only want to enforce explicit imports for a
71+
subset of Jest globals. For instance, when migrating to ESM, you might want to
72+
enforce explicit imports only for the `jest` global, as of
73+
[Jest's ESM documentation](https://jestjs.io/docs/ecmascript-modules#differences-between-esm-and-commonjs).
74+
75+
```json5
76+
{
77+
'jest/prefer-importing-jest-globals': ['error', { types: ['jest'] }],
78+
}
79+
```
80+
4581
## Further Reading
4682

4783
- [Documentation](https://jestjs.io/docs/api)

src/rules/__tests__/prefer-importing-jest-globals.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,25 @@ ruleTester.run('prefer-importing-jest-globals', rule, {
2222
`,
2323
parserOptions: { sourceType: 'module' },
2424
},
25+
{
26+
code: dedent`
27+
test('should pass', () => {
28+
expect(true).toBeDefined();
29+
});
30+
`,
31+
options: [{ types: ['jest'] }],
32+
parserOptions: { sourceType: 'module' },
33+
},
34+
{
35+
code: dedent`
36+
const { it } = require('@jest/globals');
37+
it('should pass', () => {
38+
expect(true).toBeDefined();
39+
});
40+
`,
41+
options: [{ types: ['test'] }],
42+
parserOptions: { sourceType: 'module' },
43+
},
2544
{
2645
code: dedent`
2746
// with require
@@ -85,6 +104,33 @@ ruleTester.run('prefer-importing-jest-globals', rule, {
85104
},
86105
],
87106
},
107+
{
108+
code: dedent`
109+
jest.useFakeTimers();
110+
describe("suite", () => {
111+
test("foo");
112+
expect(true).toBeDefined();
113+
})
114+
`,
115+
output: dedent`
116+
import { jest } from '@jest/globals';
117+
jest.useFakeTimers();
118+
describe("suite", () => {
119+
test("foo");
120+
expect(true).toBeDefined();
121+
})
122+
`,
123+
options: [{ types: ['jest'] }],
124+
parserOptions: { sourceType: 'module' },
125+
errors: [
126+
{
127+
endColumn: 5,
128+
column: 1,
129+
line: 1,
130+
messageId: 'preferImportingJestGlobal',
131+
},
132+
],
133+
},
88134
{
89135
code: dedent`
90136
import React from 'react';

src/rules/prefer-importing-jest-globals.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { AST_NODE_TYPES, type TSESTree } from '@typescript-eslint/utils';
22
import {
3+
type JestFnType,
34
createRule,
5+
exhaustiveStringTuple,
46
getAccessorValue,
57
getSourceCode,
68
isIdentifier,
@@ -20,6 +22,15 @@ const createFixerImports = (
2022
: `const { ${allImportsFormatted} } = require('@jest/globals');`;
2123
};
2224

25+
const allJestFnTypes = exhaustiveStringTuple<JestFnType>()(
26+
'hook',
27+
'describe',
28+
'test',
29+
'expect',
30+
'jest',
31+
'unknown',
32+
);
33+
2334
export default createRule({
2435
name: __filename,
2536
meta: {
@@ -31,10 +42,29 @@ export default createRule({
3142
},
3243
fixable: 'code',
3344
type: 'problem',
34-
schema: [],
45+
schema: [
46+
{
47+
type: 'object',
48+
properties: {
49+
types: {
50+
type: 'array',
51+
items: {
52+
type: 'string',
53+
enum: allJestFnTypes,
54+
},
55+
},
56+
},
57+
additionalProperties: false,
58+
},
59+
],
3560
},
36-
defaultOptions: [],
61+
defaultOptions: [
62+
{
63+
types: allJestFnTypes as JestFnType[],
64+
},
65+
],
3766
create(context) {
67+
const { types = allJestFnTypes } = context.options[0] || {};
3868
const importedFunctionsWithSource: Record<string, string> = {};
3969
const functionsToImport = new Set<string>();
4070
let reportingNode: TSESTree.Node;
@@ -55,7 +85,10 @@ export default createRule({
5585
return;
5686
}
5787

58-
if (jestFnCall.head.type !== 'import') {
88+
if (
89+
jestFnCall.head.type !== 'import' &&
90+
types.includes(jestFnCall.type)
91+
) {
5992
functionsToImport.add(jestFnCall.name);
6093
reportingNode ||= jestFnCall.head.node;
6194
}

src/rules/utils/misc.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,3 +266,17 @@ export const getDeclaredVariables = (
266266
context.getDeclaredVariables(node)
267267
);
268268
};
269+
270+
type AtLeastOne<T> = [T, ...T[]];
271+
export const exhaustiveTuple =
272+
<T>() =>
273+
<L extends AtLeastOne<T>>(
274+
...x: L extends any
275+
? Exclude<T, L[number]> extends never
276+
? L
277+
: Array<Exclude<T, L[number]>>
278+
: never
279+
) =>
280+
x;
281+
export const exhaustiveStringTuple = <T extends string>() =>
282+
exhaustiveTuple<T>();

0 commit comments

Comments
 (0)