Skip to content

Commit

Permalink
feat: prefer importing jest globals [new rule]
Browse files Browse the repository at this point in the history
- Fix #1101

Issue: #1101
  • Loading branch information
MadeinFrance committed Jan 22, 2024
1 parent 505258c commit 45af6d2
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | | | | | |
Expand Down
47 changes: 47 additions & 0 deletions docs/rules/prefer-importing-jest-globals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Prefer importing Jest globals (`prefer-importing-jest-globals`)

🔧 This rule is automatically fixable by the
[`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).

<!-- end auto-generated rule header -->

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)
1 change: 1 addition & 0 deletions src/__tests__/__snapshots__/rules.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/rules.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
64 changes: 64 additions & 0 deletions src/rules/__tests__/prefer-importing-jest-globals.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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' },
},
{
code: dedent`
import { it as itChecks } from '@jest/globals';
itChecks("foo");
`,
parserOptions: { sourceType: 'module' },
},
{
code: dedent`
const { test } = require('@jest/globals');
test("foo");
`,
parserOptions: { sourceType: 'module' },
},
],
invalid: [
{
code: dedent`
describe("suite", () => {
test("foo");
expect(true).toBeDefined();
})
`,
output: dedent`
import { describe, test, expect } from '@jest/globals';
describe("suite", () => {
test("foo");
expect(true).toBeDefined();
})
`,
parserOptions: { sourceType: 'module' },
errors: [
{ endColumn: 3, column: 1, messageId: 'preferImportingJestGlobal' },
],
},
],
});
69 changes: 69 additions & 0 deletions src/rules/prefer-importing-jest-globals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
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: `Import the following Jest functions from '@jest/globals': {{ jestFunctions }}`,
},
fixable: 'code',
type: 'problem',
schema: [],
},
defaultOptions: [],
create(context) {
const jestGlobalFunctions = Object.keys(globalsJson);
const importedJestFunctions: string[] = [];
const usedJestFunctions = new Set<string>();

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'() {
const jestFunctionsToImport = Array.from(usedJestFunctions).filter(
jestFunction => !importedJestFunctions.includes(jestFunction),
);

if (jestFunctionsToImport.length > 0) {
const node = context.getSourceCode().ast;
const jestFunctionsToImportFormatted =
jestFunctionsToImport.join(', ');

context.report({
node,
messageId: 'preferImportingJestGlobal',
data: { jestFunctions: jestFunctionsToImportFormatted },
fix(fixer) {
return fixer.insertTextBefore(
node,
`import { ${jestFunctionsToImportFormatted} } from '@jest/globals';\n`,
);
},
});
}
},
};
},
});

0 comments on commit 45af6d2

Please sign in to comment.