Skip to content

Commit

Permalink
feat: prefer importing jest globals for specific types
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
tomquist committed Apr 26, 2024
1 parent 20c8703 commit 38eb74a
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 3 deletions.
36 changes: 36 additions & 0 deletions docs/rules/prefer-importing-jest-globals.md
Expand Up @@ -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)
46 changes: 46 additions & 0 deletions src/rules/__tests__/prefer-importing-jest-globals.test.ts
Expand Up @@ -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
Expand Down Expand Up @@ -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';
Expand Down
39 changes: 36 additions & 3 deletions 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,
Expand All @@ -20,6 +22,15 @@ const createFixerImports = (
: `const { ${allImportsFormatted} } = require('@jest/globals');`;
};

const allJestFnTypes = exhaustiveStringTuple<JestFnType>()(
'hook',
'describe',
'test',
'expect',
'jest',
'unknown',
);

export default createRule({
name: __filename,
meta: {
Expand All @@ -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<string, string> = {};
const functionsToImport = new Set<string>();
let reportingNode: TSESTree.Node;
Expand All @@ -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;
}
Expand Down
14 changes: 14 additions & 0 deletions src/rules/utils/misc.ts
Expand Up @@ -266,3 +266,17 @@ export const getDeclaredVariables = (
context.getDeclaredVariables(node)
);
};

type AtLeastOne<T> = [T, ...T[]];
export const exhaustiveTuple =
<T>() =>
<L extends AtLeastOne<T>>(
...x: L extends any
? Exclude<T, L[number]> extends never
? L
: Array<Exclude<T, L[number]>>
: never
) =>
x;
export const exhaustiveStringTuple = <T extends string>() =>
exhaustiveTuple<T>();

0 comments on commit 38eb74a

Please sign in to comment.