Skip to content

Commit 6940488

Browse files
authoredSep 29, 2021
fix(no-deprecated-functions): remove process.cwd from resolve paths (#889)
1 parent ffc9392 commit 6940488

7 files changed

+347
-180
lines changed
 

‎README.md

+27-6
Original file line numberDiff line numberDiff line change
@@ -59,23 +59,43 @@ doing:
5959
This is included in all configs shared by this plugin, so can be omitted if
6060
extending them.
6161

62-
The behaviour of some rules (specifically `no-deprecated-functions`) change
63-
depending on the version of `jest` being used.
62+
### Jest `version` setting
6463

65-
This setting is detected automatically based off the version of the `jest`
66-
package installed in `node_modules`, but it can also be provided explicitly if
67-
desired:
64+
The behaviour of some rules (specifically [`no-deprecated-functions`][]) change
65+
depending on the version of Jest being used.
66+
67+
By default, this plugin will attempt to determine to locate Jest using
68+
`require.resolve`, meaning it will start looking in the closest `node_modules`
69+
folder to the file being linted and work its way up.
70+
71+
Since we cache the automatically determined version, if you're linting
72+
sub-folders that have different versions of Jest, you may find that the wrong
73+
version of Jest is considered when linting. You can work around this by
74+
providing the Jest version explicitly in nested ESLint configs:
6875

6976
```json
7077
{
7178
"settings": {
7279
"jest": {
73-
"version": 26
80+
"version": 27
7481
}
7582
}
7683
}
7784
```
7885

86+
To avoid hard-coding a number, you can also fetch it from the installed version
87+
of Jest if you use a JavaScript config file such as `.eslintrc.js`:
88+
89+
```js
90+
module.exports = {
91+
settings: {
92+
jest: {
93+
version: require('jest/package.json').version,
94+
},
95+
},
96+
};
97+
```
98+
7999
## Shareable configurations
80100

81101
### Recommended
@@ -226,3 +246,4 @@ https://github.com/istanbuljs/eslint-plugin-istanbul
226246
[suggest]: https://img.shields.io/badge/-suggest-yellow.svg
227247
[fixable]: https://img.shields.io/badge/-fixable-green.svg
228248
[style]: https://img.shields.io/badge/-style-blue.svg
249+
[`no-deprecated-functions`]: docs/rules/no-deprecated-functions.md

‎docs/rules/no-deprecated-functions.md

+5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ either been renamed for clarity, or replaced with more powerful APIs.
66
While typically these deprecated functions are kept in the codebase for a number
77
of majors, eventually they are removed completely.
88

9+
This rule requires knowing which version of Jest you're using - see
10+
[this section of the readme](../../README.md#jest-version-setting) for details
11+
on how that is obtained automatically and how you can explicitly provide a
12+
version if needed.
13+
914
## Rule details
1015

1116
This rule warns about calls to deprecated functions, and provides details on

‎src/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const importDefault = (moduleName: string) =>
2626
interopRequireDefault(require(moduleName)).default;
2727

2828
const rulesDir = join(__dirname, 'rules');
29-
const excludedFiles = ['__tests__', 'utils'];
29+
const excludedFiles = ['__tests__', 'detectJestVersion', 'utils'];
3030

3131
const rules = readdirSync(rulesDir)
3232
.map(rule => parse(rule).name)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
import { spawnSync } from 'child_process';
2+
import * as fs from 'fs';
3+
import * as os from 'os';
4+
import * as path from 'path';
5+
import { JSONSchemaForNPMPackageJsonFiles } from '@schemastore/package';
6+
import { create } from 'ts-node';
7+
import { detectJestVersion } from '../detectJestVersion';
8+
9+
const compileFnCode = (pathToFn: string) => {
10+
const fnContents = fs.readFileSync(pathToFn, 'utf-8');
11+
12+
return create({
13+
transpileOnly: true,
14+
compilerOptions: { sourceMap: false },
15+
}).compile(fnContents, pathToFn);
16+
};
17+
const compiledFn = compileFnCode(require.resolve('../detectJestVersion.ts'));
18+
const relativePathToFn = 'eslint-plugin-jest/lib/rules/detectJestVersion.js';
19+
20+
const runNodeScript = (cwd: string, script: string) => {
21+
return spawnSync('node', ['-e', script.split('\n').join(' ')], {
22+
cwd,
23+
encoding: 'utf-8',
24+
});
25+
};
26+
27+
const runDetectJestVersion = (cwd: string) => {
28+
return runNodeScript(
29+
cwd,
30+
`
31+
try {
32+
console.log(require('${relativePathToFn}').detectJestVersion());
33+
} catch (error) {
34+
console.error(error.message);
35+
}
36+
`,
37+
);
38+
};
39+
40+
/**
41+
* Makes a new temp directory, prefixed with `eslint-plugin-jest-`
42+
*
43+
* @return {Promise<string>}
44+
*/
45+
const makeTempDir = () =>
46+
fs.mkdtempSync(path.join(os.tmpdir(), 'eslint-plugin-jest-'));
47+
48+
interface ProjectStructure {
49+
[key: `${string}/package.json`]: JSONSchemaForNPMPackageJsonFiles;
50+
[key: `${string}/${typeof relativePathToFn}`]: string;
51+
[key: `${string}/`]: null;
52+
'package.json'?: JSONSchemaForNPMPackageJsonFiles;
53+
}
54+
55+
const setupFakeProject = (structure: ProjectStructure): string => {
56+
const tempDir = makeTempDir();
57+
58+
for (const [filePath, contents] of Object.entries(structure)) {
59+
if (contents === null) {
60+
fs.mkdirSync(path.join(tempDir, filePath), { recursive: true });
61+
62+
continue;
63+
}
64+
65+
const folderPath = path.dirname(filePath);
66+
67+
// make the directory (recursively)
68+
fs.mkdirSync(path.join(tempDir, folderPath), { recursive: true });
69+
70+
const finalContents =
71+
typeof contents === 'string' ? contents : JSON.stringify(contents);
72+
73+
fs.writeFileSync(path.join(tempDir, filePath), finalContents);
74+
}
75+
76+
return tempDir;
77+
};
78+
79+
describe('detectJestVersion', () => {
80+
describe('basic tests', () => {
81+
const packageJsonFactory = jest.fn<JSONSchemaForNPMPackageJsonFiles, []>();
82+
83+
beforeEach(() => {
84+
jest.resetModules();
85+
jest.doMock(require.resolve('jest/package.json'), packageJsonFactory);
86+
});
87+
88+
describe('when the package.json is missing the version property', () => {
89+
it('throws an error', () => {
90+
packageJsonFactory.mockReturnValue({});
91+
92+
expect(() => detectJestVersion()).toThrow(
93+
/Unable to detect Jest version/iu,
94+
);
95+
});
96+
});
97+
98+
it('caches versions', () => {
99+
packageJsonFactory.mockReturnValue({ version: '1.2.3' });
100+
101+
const version = detectJestVersion();
102+
103+
jest.resetModules();
104+
105+
expect(detectJestVersion).not.toThrow();
106+
expect(detectJestVersion()).toBe(version);
107+
});
108+
});
109+
110+
describe('when in a simple project', () => {
111+
it('finds the correct version', () => {
112+
const projectDir = setupFakeProject({
113+
'package.json': { name: 'simple-project' },
114+
[`node_modules/${relativePathToFn}` as const]: compiledFn,
115+
'node_modules/jest/package.json': {
116+
name: 'jest',
117+
version: '21.0.0',
118+
},
119+
});
120+
121+
const { stdout, stderr } = runDetectJestVersion(projectDir);
122+
123+
expect(stdout.trim()).toBe('21');
124+
expect(stderr.trim()).toBe('');
125+
});
126+
});
127+
128+
describe('when in a hoisted mono-repo', () => {
129+
it('finds the correct version', () => {
130+
const projectDir = setupFakeProject({
131+
'package.json': { name: 'mono-repo' },
132+
[`node_modules/${relativePathToFn}` as const]: compiledFn,
133+
'node_modules/jest/package.json': {
134+
name: 'jest',
135+
version: '19.0.0',
136+
},
137+
'packages/a/package.json': { name: 'package-a' },
138+
'packages/b/package.json': { name: 'package-b' },
139+
});
140+
141+
const { stdout, stderr } = runDetectJestVersion(projectDir);
142+
143+
expect(stdout.trim()).toBe('19');
144+
expect(stderr.trim()).toBe('');
145+
});
146+
});
147+
148+
describe('when in a subproject', () => {
149+
it('finds the correct versions', () => {
150+
const projectDir = setupFakeProject({
151+
'backend/package.json': { name: 'package-a' },
152+
[`backend/node_modules/${relativePathToFn}` as const]: compiledFn,
153+
'backend/node_modules/jest/package.json': {
154+
name: 'jest',
155+
version: '24.0.0',
156+
},
157+
'frontend/package.json': { name: 'package-b' },
158+
[`frontend/node_modules/${relativePathToFn}` as const]: compiledFn,
159+
'frontend/node_modules/jest/package.json': {
160+
name: 'jest',
161+
version: '15.0.0',
162+
},
163+
});
164+
165+
const { stdout: stdoutBackend, stderr: stderrBackend } =
166+
runDetectJestVersion(path.join(projectDir, 'backend'));
167+
168+
expect(stdoutBackend.trim()).toBe('24');
169+
expect(stderrBackend.trim()).toBe('');
170+
171+
const { stdout: stdoutFrontend, stderr: stderrFrontend } =
172+
runDetectJestVersion(path.join(projectDir, 'frontend'));
173+
174+
expect(stdoutFrontend.trim()).toBe('15');
175+
expect(stderrFrontend.trim()).toBe('');
176+
});
177+
});
178+
179+
describe('when jest is not installed', () => {
180+
it('throws an error', () => {
181+
const projectDir = setupFakeProject({
182+
'package.json': { name: 'no-jest' },
183+
[`node_modules/${relativePathToFn}` as const]: compiledFn,
184+
'node_modules/pack/package.json': { name: 'pack' },
185+
});
186+
187+
const { stdout, stderr } = runDetectJestVersion(projectDir);
188+
189+
expect(stdout.trim()).toBe('');
190+
expect(stderr.trim()).toContain('Unable to detect Jest version');
191+
});
192+
});
193+
194+
describe('when jest is changed on disk', () => {
195+
it('uses the cached version', () => {
196+
const projectDir = setupFakeProject({
197+
'package.json': { name: 'no-jest' },
198+
[`node_modules/${relativePathToFn}` as const]: compiledFn,
199+
'node_modules/jest/package.json': { name: 'jest', version: '26.0.0' },
200+
});
201+
202+
const { stdout, stderr } = runNodeScript(
203+
projectDir,
204+
`
205+
const { detectJestVersion } = require('${relativePathToFn}');
206+
const fs = require('fs');
207+
208+
console.log(detectJestVersion());
209+
fs.writeFileSync(
210+
'node_modules/jest/package.json',
211+
JSON.stringify({
212+
name: 'jest',
213+
version: '25.0.0',
214+
}),
215+
);
216+
console.log(detectJestVersion());
217+
`,
218+
);
219+
220+
const [firstCall, secondCall] = stdout.split('\n');
221+
222+
expect(firstCall).toBe('26');
223+
expect(secondCall).toBe('26');
224+
expect(stderr.trim()).toBe('');
225+
});
226+
});
227+
});
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,17 @@
1-
import * as fs from 'fs';
2-
import * as os from 'os';
3-
import * as path from 'path';
4-
import { JSONSchemaForNPMPackageJsonFiles } from '@schemastore/package';
51
import { TSESLint } from '@typescript-eslint/experimental-utils';
6-
import rule, {
7-
JestVersion,
8-
_clearCachedJestVersion,
9-
} from '../no-deprecated-functions';
2+
import { JestVersion, detectJestVersion } from '../detectJestVersion';
3+
import rule from '../no-deprecated-functions';
104

11-
const ruleTester = new TSESLint.RuleTester();
12-
13-
// pin the original cwd so that we can restore it after each test
14-
const projectDir = process.cwd();
15-
16-
afterEach(() => process.chdir(projectDir));
17-
18-
/**
19-
* Makes a new temp directory, prefixed with `eslint-plugin-jest-`
20-
*
21-
* @return {Promise<string>}
22-
*/
23-
const makeTempDir = async () =>
24-
fs.mkdtempSync(path.join(os.tmpdir(), 'eslint-plugin-jest-'));
25-
26-
/**
27-
* Sets up a fake project with a `package.json` file located in
28-
* `node_modules/jest` whose version is set to the given `jestVersion`.
29-
*
30-
* @param {JestVersion} jestVersion
31-
*
32-
* @return {Promise<string>}
33-
*/
34-
const setupFakeProjectDirectory = async (
35-
jestVersion: JestVersion,
36-
): Promise<string> => {
37-
const jestPackageJson: JSONSchemaForNPMPackageJsonFiles = {
38-
name: 'jest',
39-
version: `${jestVersion}.0.0`,
40-
};
5+
jest.mock('../detectJestVersion');
416

42-
const tempDir = await makeTempDir();
43-
const jestPackagePath = path.join(tempDir, 'node_modules', 'jest');
7+
const detectJestVersionMock = detectJestVersion as jest.MockedFunction<
8+
typeof detectJestVersion
9+
>;
4410

45-
// todo: remove in node@10 & replace with { recursive: true }
46-
fs.mkdirSync(path.join(tempDir, 'node_modules'));
47-
48-
fs.mkdirSync(jestPackagePath);
49-
await fs.writeFileSync(
50-
path.join(jestPackagePath, 'package.json'),
51-
JSON.stringify(jestPackageJson),
52-
);
53-
54-
return tempDir;
55-
};
11+
const ruleTester = new TSESLint.RuleTester();
5612

5713
const generateValidCases = (
58-
jestVersion: JestVersion | undefined,
14+
jestVersion: JestVersion | string | undefined,
5915
functionCall: string,
6016
): Array<TSESLint.ValidTestCase<never>> => {
6117
const [name, func] = functionCall.split('.');
@@ -70,7 +26,7 @@ const generateValidCases = (
7026
};
7127

7228
const generateInvalidCases = (
73-
jestVersion: JestVersion | undefined,
29+
jestVersion: JestVersion | string | undefined,
7430
deprecation: string,
7531
replacement: string,
7632
): Array<TSESLint.InvalidTestCase<'deprecatedFunction', never>> => {
@@ -97,45 +53,18 @@ const generateInvalidCases = (
9753
];
9854
};
9955

100-
describe('the jest version cache', () => {
101-
beforeEach(async () => process.chdir(await setupFakeProjectDirectory(17)));
102-
103-
// change the jest version *after* each test case
104-
afterEach(async () => {
105-
const jestPackageJson: JSONSchemaForNPMPackageJsonFiles = {
106-
name: 'jest',
107-
version: '24.0.0',
108-
};
109-
110-
const tempDir = process.cwd();
111-
112-
await fs.writeFileSync(
113-
path.join(tempDir, 'node_modules', 'jest', 'package.json'),
114-
JSON.stringify(jestPackageJson),
115-
);
116-
});
117-
118-
ruleTester.run('no-deprecated-functions', rule, {
119-
valid: [
120-
'require("fs")', // this will cause jest version to be read & cached
121-
'jest.requireActual()', // deprecated after jest 17
122-
],
123-
invalid: [],
124-
});
125-
});
126-
12756
// contains the cache-clearing beforeEach so we can test the cache too
12857
describe('the rule', () => {
129-
beforeEach(() => _clearCachedJestVersion());
130-
13158
// a few sanity checks before doing our massive loop
13259
ruleTester.run('no-deprecated-functions', rule, {
13360
valid: [
134-
'jest',
135-
'require("fs")',
61+
{ settings: { jest: { version: 14 } }, code: 'jest' },
62+
{ settings: { jest: { version: 14 } }, code: 'require("fs")' },
13663
...generateValidCases(14, 'jest.resetModuleRegistry'),
13764
...generateValidCases(17, 'require.requireActual'),
13865
...generateValidCases(25, 'jest.genMockFromModule'),
66+
...generateValidCases('25.1.1', 'jest.genMockFromModule'),
67+
...generateValidCases('17.2', 'require.requireActual'),
13968
],
14069
invalid: [
14170
...generateInvalidCases(
@@ -149,15 +78,20 @@ describe('the rule', () => {
14978
'jest.genMockFromModule',
15079
'jest.createMockFromModule',
15180
),
81+
...generateInvalidCases(
82+
'26.0.0-next.11',
83+
'jest.genMockFromModule',
84+
'jest.createMockFromModule',
85+
),
15286
],
15387
});
15488

15589
describe.each<JestVersion>([
15690
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
15791
])('when using jest version %i', jestVersion => {
158-
beforeEach(async () =>
159-
process.chdir(await setupFakeProjectDirectory(jestVersion)),
160-
);
92+
beforeEach(async () => {
93+
detectJestVersionMock.mockReturnValue(jestVersion);
94+
});
16195

16296
const allowedFunctions: string[] = [];
16397
const deprecations = (
@@ -210,50 +144,23 @@ describe('the rule', () => {
210144
});
211145
});
212146

213-
describe('when no jest version is provided', () => {
214-
describe('when the jest package.json is missing the version property', () => {
215-
beforeEach(async () => {
216-
const tempDir = await setupFakeProjectDirectory(1);
217-
218-
await fs.writeFileSync(
219-
path.join(tempDir, 'node_modules', 'jest', 'package.json'),
220-
JSON.stringify({}),
221-
);
222-
223-
process.chdir(tempDir);
224-
});
225-
226-
it('requires the version to be set explicitly', () => {
227-
expect(() => {
228-
const linter = new TSESLint.Linter();
229-
230-
linter.defineRule('no-deprecated-functions', rule);
231-
232-
linter.verify('jest.resetModuleRegistry()', {
233-
rules: { 'no-deprecated-functions': 'error' },
234-
});
235-
}).toThrow(
236-
'Unable to detect Jest version - please ensure jest package is installed, or otherwise set version explicitly',
237-
);
147+
describe('when there is an error in detecting the jest version', () => {
148+
beforeEach(() => {
149+
detectJestVersionMock.mockImplementation(() => {
150+
throw new Error('oh noes!');
238151
});
239152
});
240153

241-
describe('when the jest package.json is not found', () => {
242-
beforeEach(async () => process.chdir(await makeTempDir()));
154+
it('bubbles the error up', () => {
155+
expect(() => {
156+
const linter = new TSESLint.Linter();
243157

244-
it('requires the version to be set explicitly', () => {
245-
expect(() => {
246-
const linter = new TSESLint.Linter();
158+
linter.defineRule('no-deprecated-functions', rule);
247159

248-
linter.defineRule('no-deprecated-functions', rule);
249-
250-
linter.verify('jest.resetModuleRegistry()', {
251-
rules: { 'no-deprecated-functions': 'error' },
252-
});
253-
}).toThrow(
254-
'Unable to detect Jest version - please ensure jest package is installed, or otherwise set version explicitly',
255-
);
256-
});
160+
linter.verify('jest.resetModuleRegistry()', {
161+
rules: { 'no-deprecated-functions': 'error' },
162+
});
163+
}).toThrow('oh noes!');
257164
});
258165
});
259166
});

‎src/rules/detectJestVersion.ts

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { JSONSchemaForNPMPackageJsonFiles } from '@schemastore/package';
2+
3+
export type JestVersion =
4+
| 14
5+
| 15
6+
| 16
7+
| 17
8+
| 18
9+
| 19
10+
| 20
11+
| 21
12+
| 22
13+
| 23
14+
| 24
15+
| 25
16+
| 26
17+
| 27
18+
| number;
19+
20+
let cachedJestVersion: JestVersion | null = null;
21+
22+
export const detectJestVersion = (): JestVersion => {
23+
if (cachedJestVersion) {
24+
return cachedJestVersion;
25+
}
26+
27+
try {
28+
const jestPath = require.resolve('jest/package.json');
29+
30+
const jestPackageJson =
31+
// eslint-disable-next-line @typescript-eslint/no-require-imports
32+
require(jestPath) as JSONSchemaForNPMPackageJsonFiles;
33+
34+
if (jestPackageJson.version) {
35+
const [majorVersion] = jestPackageJson.version.split('.');
36+
37+
return (cachedJestVersion = parseInt(majorVersion, 10));
38+
}
39+
} catch {}
40+
41+
throw new Error(
42+
'Unable to detect Jest version - please ensure jest package is installed, or otherwise set version explicitly',
43+
);
44+
};

‎src/rules/no-deprecated-functions.ts

+10-47
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,26 @@
1-
import { JSONSchemaForNPMPackageJsonFiles } from '@schemastore/package';
21
import {
32
AST_NODE_TYPES,
43
TSESTree,
54
} from '@typescript-eslint/experimental-utils';
5+
import { JestVersion, detectJestVersion } from './detectJestVersion';
66
import { createRule, getNodeName } from './utils';
77

88
interface ContextSettings {
99
jest?: EslintPluginJestSettings;
1010
}
1111

12-
export type JestVersion =
13-
| 14
14-
| 15
15-
| 16
16-
| 17
17-
| 18
18-
| 19
19-
| 20
20-
| 21
21-
| 22
22-
| 23
23-
| 24
24-
| 25
25-
| 26
26-
| 27
27-
| number;
28-
2912
interface EslintPluginJestSettings {
30-
version: JestVersion;
13+
version: JestVersion | string;
3114
}
3215

33-
let cachedJestVersion: JestVersion | null = null;
34-
35-
/** @internal */
36-
export const _clearCachedJestVersion = () => (cachedJestVersion = null);
37-
38-
const detectJestVersion = (): JestVersion => {
39-
if (cachedJestVersion) {
40-
return cachedJestVersion;
16+
const parseJestVersion = (rawVersion: number | string): JestVersion => {
17+
if (typeof rawVersion === 'number') {
18+
return rawVersion;
4119
}
4220

43-
try {
44-
const jestPath = require.resolve('jest/package.json', {
45-
paths: [process.cwd()],
46-
});
47-
48-
const jestPackageJson =
49-
// eslint-disable-next-line @typescript-eslint/no-require-imports
50-
require(jestPath) as JSONSchemaForNPMPackageJsonFiles;
51-
52-
if (jestPackageJson.version) {
53-
const [majorVersion] = jestPackageJson.version.split('.');
54-
55-
return (cachedJestVersion = parseInt(majorVersion, 10));
56-
}
57-
} catch {}
21+
const [majorVersion] = rawVersion.split('.');
5822

59-
throw new Error(
60-
'Unable to detect Jest version - please ensure jest package is installed, or otherwise set version explicitly',
61-
);
23+
return parseInt(majorVersion, 10);
6224
};
6325

6426
export default createRule({
@@ -79,9 +41,10 @@ export default createRule({
7941
},
8042
defaultOptions: [],
8143
create(context) {
82-
const jestVersion =
44+
const jestVersion = parseJestVersion(
8345
(context.settings as ContextSettings)?.jest?.version ||
84-
detectJestVersion();
46+
detectJestVersion(),
47+
);
8548

8649
const deprecations: Record<string, string> = {
8750
...(jestVersion >= 15 && {

0 commit comments

Comments
 (0)
Please sign in to comment.