Skip to content

Commit 45af6d2

Browse files
committed
feat: prefer importing jest globals [new rule]
- Fix jest-community#1101 Issue: jest-community#1101
1 parent 505258c commit 45af6d2

6 files changed

+183
-1
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ set to warn in.\
248248
| [prefer-expect-resolves](docs/rules/prefer-expect-resolves.md) | Prefer `await expect(...).resolves` over `expect(await ...)` syntax | | | 🔧 | | |
249249
| [prefer-hooks-in-order](docs/rules/prefer-hooks-in-order.md) | Prefer having hooks in a consistent order | | | | | |
250250
| [prefer-hooks-on-top](docs/rules/prefer-hooks-on-top.md) | Suggest having hooks before any test cases | | | | | |
251+
| [prefer-importing-jest-globals](docs/rules/prefer-importing-jest-globals.md) | Prefer importing Jest globals | | | 🔧 | | |
251252
| [prefer-lowercase-title](docs/rules/prefer-lowercase-title.md) | Enforce lowercase test names | | | 🔧 | | |
252253
| [prefer-mock-promise-shorthand](docs/rules/prefer-mock-promise-shorthand.md) | Prefer mock resolved/rejected shorthands for promises | | | 🔧 | | |
253254
| [prefer-snapshot-hint](docs/rules/prefer-snapshot-hint.md) | Prefer including a hint with external snapshots | | | | | |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Prefer importing Jest globals (`prefer-importing-jest-globals`)
2+
3+
🔧 This rule is automatically fixable by the
4+
[`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
5+
6+
<!-- end auto-generated rule header -->
7+
8+
This rule aims to enforce explicit imports from `@jest/globals`.
9+
10+
1. This is useful for ensuring that the Jest APIs are imported the same way in
11+
the codebase.
12+
2. When you can't modify Jest's
13+
[`injectGlobals`](https://jestjs.io/docs/configuration#injectglobals-boolean)
14+
configuration property, this rule can help to ensure that the Jest globals
15+
are imported explicitly and facilitate a migration to `@jest/globals`.
16+
17+
## Rule details
18+
19+
Examples of **incorrect** code for this rule
20+
21+
```js
22+
/* eslint jest/prefer-importing-jest-globals: "error" */
23+
24+
describe('foo', () => {
25+
it('accepts this input', () => {
26+
// ...
27+
});
28+
});
29+
```
30+
31+
Examples of **correct** code for this rule
32+
33+
```js
34+
/* eslint jest/prefer-importing-jest-globals: "error" */
35+
36+
import { describe, it } from '@jest/globals';
37+
38+
describe('foo', () => {
39+
it('accepts this input', () => {
40+
// ...
41+
});
42+
});
43+
```
44+
45+
## Further Reading
46+
47+
- [Documentation](https://jestjs.io/docs/api)

src/__tests__/__snapshots__/rules.test.ts.snap

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ exports[`rules should export configs that refer to actual rules 1`] = `
4545
"jest/prefer-expect-resolves": "error",
4646
"jest/prefer-hooks-in-order": "error",
4747
"jest/prefer-hooks-on-top": "error",
48+
"jest/prefer-importing-jest-globals": "error",
4849
"jest/prefer-lowercase-title": "error",
4950
"jest/prefer-mock-promise-shorthand": "error",
5051
"jest/prefer-snapshot-hint": "error",

src/__tests__/rules.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { existsSync } from 'fs';
22
import { resolve } from 'path';
33
import plugin from '../';
44

5-
const numberOfRules = 53;
5+
const numberOfRules = 54;
66
const ruleNames = Object.keys(plugin.rules);
77
const deprecatedRules = Object.entries(plugin.rules)
88
.filter(([, rule]) => rule.meta.deprecated)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { TSESLint } from '@typescript-eslint/utils';
2+
import dedent from 'dedent';
3+
import rule from '../prefer-importing-jest-globals';
4+
import { espreeParser } from './test-utils';
5+
6+
const ruleTester = new TSESLint.RuleTester({
7+
parser: espreeParser,
8+
parserOptions: {
9+
ecmaVersion: 2015,
10+
sourceType: 'module',
11+
},
12+
});
13+
14+
ruleTester.run('prefer-importing-jest-globals', rule, {
15+
valid: [
16+
{
17+
code: dedent`
18+
import { test, expect } from '@jest/globals';
19+
20+
test('should pass', () => {
21+
expect(true).toBeDefined();
22+
});
23+
`,
24+
parserOptions: { sourceType: 'module' },
25+
},
26+
{
27+
code: dedent`
28+
import { it as itChecks } from '@jest/globals';
29+
30+
itChecks("foo");
31+
`,
32+
parserOptions: { sourceType: 'module' },
33+
},
34+
{
35+
code: dedent`
36+
const { test } = require('@jest/globals');
37+
38+
test("foo");
39+
`,
40+
parserOptions: { sourceType: 'module' },
41+
},
42+
],
43+
invalid: [
44+
{
45+
code: dedent`
46+
describe("suite", () => {
47+
test("foo");
48+
expect(true).toBeDefined();
49+
})
50+
`,
51+
output: dedent`
52+
import { describe, test, expect } from '@jest/globals';
53+
describe("suite", () => {
54+
test("foo");
55+
expect(true).toBeDefined();
56+
})
57+
`,
58+
parserOptions: { sourceType: 'module' },
59+
errors: [
60+
{ endColumn: 3, column: 1, messageId: 'preferImportingJestGlobal' },
61+
],
62+
},
63+
],
64+
});
+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import globalsJson from '../globals.json';
2+
import { createRule, parseJestFnCall } from './utils';
3+
4+
export default createRule({
5+
name: __filename,
6+
meta: {
7+
docs: {
8+
category: 'Best Practices',
9+
description: 'Prefer importing Jest globals',
10+
recommended: false,
11+
},
12+
messages: {
13+
preferImportingJestGlobal: `Import the following Jest functions from '@jest/globals': {{ jestFunctions }}`,
14+
},
15+
fixable: 'code',
16+
type: 'problem',
17+
schema: [],
18+
},
19+
defaultOptions: [],
20+
create(context) {
21+
const jestGlobalFunctions = Object.keys(globalsJson);
22+
const importedJestFunctions: string[] = [];
23+
const usedJestFunctions = new Set<string>();
24+
25+
return {
26+
CallExpression(node) {
27+
const jestFnCall = parseJestFnCall(node, context);
28+
29+
if (!jestFnCall) {
30+
return;
31+
}
32+
if (
33+
jestFnCall.head.type === 'import' &&
34+
jestGlobalFunctions.includes(jestFnCall.name)
35+
) {
36+
importedJestFunctions.push(jestFnCall.name);
37+
}
38+
39+
/* istanbul ignore else */
40+
if (jestGlobalFunctions.includes(jestFnCall.name)) {
41+
usedJestFunctions.add(jestFnCall.name);
42+
}
43+
},
44+
'Program:exit'() {
45+
const jestFunctionsToImport = Array.from(usedJestFunctions).filter(
46+
jestFunction => !importedJestFunctions.includes(jestFunction),
47+
);
48+
49+
if (jestFunctionsToImport.length > 0) {
50+
const node = context.getSourceCode().ast;
51+
const jestFunctionsToImportFormatted =
52+
jestFunctionsToImport.join(', ');
53+
54+
context.report({
55+
node,
56+
messageId: 'preferImportingJestGlobal',
57+
data: { jestFunctions: jestFunctionsToImportFormatted },
58+
fix(fixer) {
59+
return fixer.insertTextBefore(
60+
node,
61+
`import { ${jestFunctionsToImportFormatted} } from '@jest/globals';\n`,
62+
);
63+
},
64+
});
65+
}
66+
},
67+
};
68+
},
69+
});

0 commit comments

Comments
 (0)