Skip to content

Commit 05f20b8

Browse files
authored
feat(no-deprecated-functions): support jest version setting (#564)
* feat(no-deprecated-functions): support jest `version` setting * chore(no-deprecated-functions): cache jest version
1 parent 0d9dce0 commit 05f20b8

6 files changed

+360
-51
lines changed

README.md

+17
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,23 @@ You can also whitelist the environment variables provided by Jest by doing:
5656
}
5757
```
5858

59+
The behaviour of some rules (specifically `no-deprecated-functions`) change
60+
depending on the version of `jest` being used.
61+
62+
This setting is detected automatically based off the version of the `jest`
63+
package installed in `node_modules`, but it can also be provided explicitly if
64+
desired:
65+
66+
```json
67+
{
68+
"settings": {
69+
"jest": {
70+
"version": 26
71+
}
72+
}
73+
}
74+
```
75+
5976
## Shareable configurations
6077

6178
### Recommended

docs/rules/no-deprecated-functions.md

+11-11
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,20 @@ of majors, eventually they are removed completely.
99
## Rule details
1010

1111
This rule warns about calls to deprecated functions, and provides details on
12-
what to replace them with.
12+
what to replace them with, based on the version of Jest that is installed.
1313

1414
This rule can also autofix a number of these deprecations for you.
1515

16+
### `jest.resetModuleRegistry`
17+
18+
This function was renamed to `resetModules` in Jest 15, and is scheduled for
19+
removal in Jest 27.
20+
21+
### `jest.addMatchers`
22+
23+
This function was replaced with `expect.extend` in Jest 17, and is scheduled for
24+
removal in Jest 27.
25+
1626
### `require.requireActual` & `require.requireMock`
1727

1828
These functions were replaced in Jest 21 and removed in Jest 26.
@@ -25,16 +35,6 @@ for type checkers to handle, and their use via `require` deprecated. Finally,
2535
the release of Jest 26 saw them removed from the `require` function all
2636
together.
2737

28-
### `jest.addMatchers`
29-
30-
This function was replaced with `expect.extend` in Jest 17, and is scheduled for
31-
removal in Jest 27.
32-
33-
### `jest.resetModuleRegistry`
34-
35-
This function was renamed to `resetModules` in Jest 15, and is scheduled for
36-
removal in Jest 27.
37-
3838
### `jest.runTimersToTime`
3939

4040
This function was renamed to `advanceTimersByTime` in Jest 22, and is scheduled

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"@babel/preset-typescript": "^7.3.3",
4747
"@commitlint/cli": "^8.2.0",
4848
"@commitlint/config-conventional": "^8.2.0",
49+
"@schemastore/package": "^0.0.5",
4950
"@semantic-release/changelog": "^3.0.5",
5051
"@semantic-release/git": "^7.0.17",
5152
"@types/eslint": "^6.1.3",
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,265 @@
1+
import * as fs from 'fs';
2+
import * as os from 'os';
3+
import * as path from 'path';
4+
import { JSONSchemaForNPMPackageJsonFiles } from '@schemastore/package';
15
import { TSESLint } from '@typescript-eslint/experimental-utils';
2-
import rule from '../no-deprecated-functions';
6+
import rule, {
7+
JestVersion,
8+
_clearCachedJestVersion,
9+
} from '../no-deprecated-functions';
310

411
const ruleTester = new TSESLint.RuleTester();
512

6-
[
7-
['require.requireMock', 'jest.requireMock'],
8-
['require.requireActual', 'jest.requireActual'],
9-
['jest.addMatchers', 'expect.extend'],
10-
['jest.resetModuleRegistry', 'jest.resetModules'],
11-
['jest.runTimersToTime', 'jest.advanceTimersByTime'],
12-
['jest.genMockFromModule', 'jest.createMockFromModule'],
13-
].forEach(([deprecation, replacement]) => {
13+
/**
14+
* Makes a new temp directory, prefixed with `eslint-plugin-jest-`
15+
*
16+
* @return {Promise<string>}
17+
*/
18+
const makeTempDir = async () =>
19+
fs.mkdtempSync(path.join(os.tmpdir(), 'eslint-plugin-jest-'));
20+
21+
/**
22+
* Sets up a fake project with a `package.json` file located in
23+
* `node_modules/jest` whose version is set to the given `jestVersion`.
24+
*
25+
* @param {JestVersion} jestVersion
26+
*
27+
* @return {Promise<string>}
28+
*/
29+
const setupFakeProjectDirectory = async (
30+
jestVersion: JestVersion,
31+
): Promise<string> => {
32+
const jestPackageJson: JSONSchemaForNPMPackageJsonFiles = {
33+
name: 'jest',
34+
version: `${jestVersion}.0.0`,
35+
};
36+
37+
const tempDir = await makeTempDir();
38+
const jestPackagePath = path.join(tempDir, 'node_modules', 'jest');
39+
40+
// todo: remove in node@10 & replace with { recursive: true }
41+
fs.mkdirSync(path.join(tempDir, 'node_modules'));
42+
43+
fs.mkdirSync(jestPackagePath);
44+
await fs.writeFileSync(
45+
path.join(jestPackagePath, 'package.json'),
46+
JSON.stringify(jestPackageJson),
47+
);
48+
49+
return tempDir;
50+
};
51+
52+
const generateValidCases = (
53+
jestVersion: JestVersion | undefined,
54+
functionCall: string,
55+
): Array<TSESLint.ValidTestCase<never>> => {
56+
const [name, func] = functionCall.split('.');
57+
const settings = { jest: { version: jestVersion } } as const;
58+
59+
return [
60+
{ settings, code: `${functionCall}()` },
61+
{ settings, code: `${functionCall}` },
62+
{ settings, code: `${name}['${func}']()` },
63+
{ settings, code: `${name}['${func}']` },
64+
];
65+
};
66+
67+
const generateInvalidCases = (
68+
jestVersion: JestVersion | undefined,
69+
deprecation: string,
70+
replacement: string,
71+
): Array<TSESLint.InvalidTestCase<'deprecatedFunction', never>> => {
1472
const [deprecatedName, deprecatedFunc] = deprecation.split('.');
1573
const [replacementName, replacementFunc] = replacement.split('.');
74+
const settings = { jest: { version: jestVersion } };
75+
const errors: [TSESLint.TestCaseError<'deprecatedFunction'>] = [
76+
{ messageId: 'deprecatedFunction', data: { deprecation, replacement } },
77+
];
78+
79+
return [
80+
{
81+
code: `${deprecation}()`,
82+
output: `${replacement}()`,
83+
settings,
84+
errors,
85+
},
86+
{
87+
code: `${deprecatedName}['${deprecatedFunc}']()`,
88+
output: `${replacementName}['${replacementFunc}']()`,
89+
settings,
90+
errors,
91+
},
92+
];
93+
};
94+
95+
describe('the jest version cache', () => {
96+
beforeEach(async () => process.chdir(await setupFakeProjectDirectory(17)));
97+
98+
// change the jest version *after* each test case
99+
afterEach(async () => {
100+
const jestPackageJson: JSONSchemaForNPMPackageJsonFiles = {
101+
name: 'jest',
102+
version: '24.0.0',
103+
};
104+
105+
const tempDir = process.cwd();
106+
107+
await fs.writeFileSync(
108+
path.join(tempDir, 'node_modules', 'jest', 'package.json'),
109+
JSON.stringify(jestPackageJson),
110+
);
111+
});
112+
113+
ruleTester.run('no-deprecated-functions', rule, {
114+
valid: [
115+
'require("fs")', // this will cause jest version to be read & cached
116+
'jest.requireActual()', // deprecated after jest 17
117+
],
118+
invalid: [],
119+
});
120+
});
16121

17-
ruleTester.run(`${deprecation} -> ${replacement}`, rule, {
122+
// contains the cache-clearing beforeEach so we can test the cache too
123+
describe('the rule', () => {
124+
beforeEach(() => _clearCachedJestVersion());
125+
126+
// a few sanity checks before doing our massive loop
127+
ruleTester.run('no-deprecated-functions', rule, {
18128
valid: [
19129
'jest',
20130
'require("fs")',
21-
`${replacement}()`,
22-
replacement,
23-
`${replacementName}['${replacementFunc}']()`,
24-
`${replacementName}['${replacementFunc}']`,
131+
...generateValidCases(14, 'jest.resetModuleRegistry'),
132+
...generateValidCases(17, 'require.requireActual'),
133+
...generateValidCases(25, 'jest.genMockFromModule'),
25134
],
26135
invalid: [
27-
{
28-
code: `${deprecation}()`,
29-
output: `${replacement}()`,
30-
errors: [
31-
{
32-
messageId: 'deprecatedFunction',
33-
data: { deprecation, replacement },
34-
},
35-
],
36-
},
37-
{
38-
code: `${deprecatedName}['${deprecatedFunc}']()`,
39-
output: `${replacementName}['${replacementFunc}']()`,
40-
errors: [
41-
{
42-
messageId: 'deprecatedFunction',
43-
data: { deprecation, replacement },
44-
},
45-
],
46-
},
136+
...generateInvalidCases(
137+
21,
138+
'jest.resetModuleRegistry',
139+
'jest.resetModules',
140+
),
141+
...generateInvalidCases(24, 'jest.addMatchers', 'expect.extend'),
142+
...generateInvalidCases(
143+
26,
144+
'jest.genMockFromModule',
145+
'jest.createMockFromModule',
146+
),
47147
],
48148
});
149+
150+
describe.each<JestVersion>([
151+
14,
152+
15,
153+
16,
154+
17,
155+
18,
156+
19,
157+
20,
158+
21,
159+
22,
160+
23,
161+
24,
162+
25,
163+
26,
164+
27,
165+
])('when using jest version %i', jestVersion => {
166+
beforeEach(async () =>
167+
process.chdir(await setupFakeProjectDirectory(jestVersion)),
168+
);
169+
170+
const allowedFunctions: string[] = [];
171+
const deprecations = ([
172+
[15, 'jest.resetModuleRegistry', 'jest.resetModules'],
173+
[17, 'jest.addMatchers', 'expect.extend'],
174+
[21, 'require.requireMock', 'jest.requireMock'],
175+
[21, 'require.requireActual', 'jest.requireActual'],
176+
[22, 'jest.runTimersToTime', 'jest.advanceTimersByTime'],
177+
[26, 'jest.genMockFromModule', 'jest.createMockFromModule'],
178+
] as const).filter(deprecation => {
179+
if (deprecation[0] > jestVersion) {
180+
allowedFunctions.push(deprecation[1]);
181+
182+
return false;
183+
}
184+
185+
return true;
186+
});
187+
188+
ruleTester.run('explict jest version', rule, {
189+
valid: [
190+
'jest',
191+
'require("fs")',
192+
...allowedFunctions
193+
.map(func => generateValidCases(jestVersion, func))
194+
.reduce((acc, arr) => acc.concat(arr), []),
195+
],
196+
invalid: deprecations
197+
.map(([, deprecation, replacement]) =>
198+
generateInvalidCases(jestVersion, deprecation, replacement),
199+
)
200+
.reduce((acc, arr) => acc.concat(arr), []),
201+
});
202+
203+
ruleTester.run('detected jest version', rule, {
204+
valid: [
205+
'jest',
206+
'require("fs")',
207+
...allowedFunctions
208+
.map(func => generateValidCases(undefined, func))
209+
.reduce((acc, arr) => acc.concat(arr), []),
210+
],
211+
invalid: deprecations
212+
.map(([, deprecation, replacement]) =>
213+
generateInvalidCases(undefined, deprecation, replacement),
214+
)
215+
.reduce((acc, arr) => acc.concat(arr), []),
216+
});
217+
});
218+
219+
describe('when no jest version is provided', () => {
220+
describe('when the jest package.json is missing the version property', () => {
221+
beforeEach(async () => {
222+
const tempDir = await setupFakeProjectDirectory(1);
223+
224+
await fs.writeFileSync(
225+
path.join(tempDir, 'node_modules', 'jest', 'package.json'),
226+
JSON.stringify({}),
227+
);
228+
229+
process.chdir(tempDir);
230+
});
231+
232+
it('requires the version to be set explicitly', () => {
233+
expect(() => {
234+
const linter = new TSESLint.Linter();
235+
236+
linter.defineRule('no-deprecated-functions', rule);
237+
238+
linter.verify('jest.resetModuleRegistry()', {
239+
rules: { 'no-deprecated-functions': 'error' },
240+
});
241+
}).toThrow(
242+
'Unable to detect Jest version - please ensure jest package is installed, or otherwise set version explicitly',
243+
);
244+
});
245+
});
246+
247+
describe('when the jest package.json is not found', () => {
248+
beforeEach(async () => process.chdir(await makeTempDir()));
249+
250+
it('requires the version to be set explicitly', () => {
251+
expect(() => {
252+
const linter = new TSESLint.Linter();
253+
254+
linter.defineRule('no-deprecated-functions', rule);
255+
256+
linter.verify('jest.resetModuleRegistry()', {
257+
rules: { 'no-deprecated-functions': 'error' },
258+
});
259+
}).toThrow(
260+
'Unable to detect Jest version - please ensure jest package is installed, or otherwise set version explicitly',
261+
);
262+
});
263+
});
264+
});
49265
});

0 commit comments

Comments
 (0)