Skip to content
This repository was archived by the owner on Aug 7, 2023. It is now read-only.

Commit 132754c

Browse files
Change our caching strategy for ESLint instances…
…which fixes bugs and allows us not to care about `process.chdir`.
1 parent 44ee654 commit 132754c

File tree

1 file changed

+59
-59
lines changed

1 file changed

+59
-59
lines changed

lib/worker.js

Lines changed: 59 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,17 @@
55

66
require('util').inspect.defaultOptions.depth = null;
77

8-
const { join, normalize, sep } = require('path');
8+
const { join, dirname } = require('path');
99
const { createRequire } = require('module');
1010
const compareVersions = require('compare-versions');
1111
const ndjson = require('ndjson');
1212

13-
const PATHS_CACHE = {};
14-
const ESLINT_CACHE = {};
15-
1613
const MINIMUM_ESLINT_VERSION = '7.0.0';
1714

15+
const PATHS_CACHE = new Map();
16+
const ESLINT_CACHE = new Map();
17+
18+
1819
class IncompatibleVersionError extends Error {
1920
constructor (version) {
2021
// eslint-disable-next-line max-len
@@ -56,39 +57,28 @@ function log (message) {
5657
emit({ log: message });
5758
}
5859

59-
function getPathRoot (filePath, eslintPath) {
60-
log(`getPathRoot ${filePath} // ${eslintPath}`);
61-
filePath = normalize(filePath).split(sep);
62-
eslintPath = normalize(eslintPath).split(sep);
63-
64-
for (let index in filePath) {
65-
if (eslintPath[index] !== filePath[index]) {
66-
let ret = filePath.slice(0, index).join(sep);
67-
return ret + sep;
68-
}
69-
}
70-
71-
throw new Error('linter-eslint-node: Cannot determine root');
72-
}
73-
74-
function buildCommonConstructorOptions (config) {
60+
function buildCommonConstructorOptions (config, cwd) {
7561
let {
7662
advanced: { disableEslintIgnore },
7763
autofix: { rulesToDisableWhileFixing }
7864
} = config;
7965

8066
return {
67+
cwd,
8168
ignore: !disableEslintIgnore,
69+
// `fix` can be a function, so we'll use it to ignore any rules that the
70+
// user has told us to ignore. This isn't a "common" option, but it's easy
71+
// to overwrite with `fix: false` for the lint-only instance.
8272
fix: ({ ruleId }) => !rulesToDisableWhileFixing.includes(ruleId)
8373
};
8474
}
8575

8676
function clearESLintCache () {
87-
for (let key in PATHS_CACHE) {
88-
delete PATHS_CACHE[key];
77+
for (let key of PATHS_CACHE) {
78+
PATHS_CACHE.delete(key);
8979
}
90-
for (let key in ESLINT_CACHE) {
91-
delete ESLINT_CACHE[key];
80+
for (let key of ESLINT_CACHE) {
81+
ESLINT_CACHE.delete(key);
9282
}
9383
}
9484

@@ -100,52 +90,72 @@ function resolveESLint (filePath) {
10090
}
10191
}
10292

103-
function getESLint (filePath, projectPath, config, { legacyPackagePresent }) {
104-
if (!PATHS_CACHE[filePath]) {
105-
PATHS_CACHE[filePath] = resolveESLint(filePath);
93+
let builtInEslintPath;
94+
function resolveBuiltInESLint () {
95+
if (!builtInEslintPath) {
96+
builtInEslintPath = createRequire(__dirname).resolve('eslint');
10697
}
98+
return builtInEslintPath;
99+
}
107100

108-
let eslintPath = PATHS_CACHE[filePath];
101+
function getESLint (filePath, config, { legacyPackagePresent }) {
102+
let resolveDir = dirname(filePath);
103+
if (!PATHS_CACHE.has(resolveDir)) {
104+
PATHS_CACHE.set(resolveDir, resolveESLint(filePath));
105+
}
109106

110-
if (!ESLINT_CACHE[eslintPath]) {
107+
let eslintPath = PATHS_CACHE.get(resolveDir);
108+
109+
// We need to manage an arbitrary number of different ESLint versions without
110+
// having them clash. On top of that, a declaration of `ESLint` accepts a
111+
// `cwd` option on instantiation and infers its configuration based on the
112+
// `.eslintrc`s and `.eslintignore`s that it sees from that particular
113+
// directory.
114+
//
115+
// Thus an `ESLint` instance is good for the directory it was created in and
116+
// nothing more. If two files are siblings in a project's file tree, linting
117+
// the first one will create an instance that can be reused when we lint the
118+
// second, but can't be reused with any file in the parent directory or in
119+
// any child directories.
120+
//
121+
// TODO: Add an option to disable this method of caching. Could be useful as
122+
// a diagnostic tool and as a workaround when users report bugs.
123+
if (!ESLINT_CACHE.has(resolveDir)) {
111124
const eslintRootPath = eslintPath.replace(/eslint([/\\]).*?$/, 'eslint$1');
112125
const packageMeta = require(join(eslintRootPath, 'package.json'));
113126

114-
let { ESLint } = createRequire(eslintPath)('eslint');
115-
let commonOptions = buildCommonConstructorOptions(config);
127+
const { ESLint } = createRequire(eslintPath)('eslint');
128+
let commonOptions = buildCommonConstructorOptions(config, resolveDir);
129+
130+
const eslintLint = new ESLint({ ...commonOptions, fix: false });
131+
const eslintFix = new ESLint({ ...commonOptions });
116132

117-
// `fix` is a predicate in `commonOptions` and represents the "yes,
118-
// except..." outcome. For the linter version, we overwrite it with `fix:
119-
// false`.
120-
ESLINT_CACHE[eslintPath] = {
133+
ESLINT_CACHE.set(resolveDir, {
121134
ESLint,
122-
eslintLint: new ESLint({ ...commonOptions, fix: false }),
123-
eslintFix: new ESLint({ ...commonOptions }),
124-
workingDir: getPathRoot(filePath, eslintPath),
135+
eslintLint,
136+
eslintFix,
125137
eslintPath: eslintRootPath,
126138
eslintVersion: packageMeta.version,
127-
isBuiltIn: eslintPath === PATHS_CACHE[__dirname]
128-
};
139+
isBuiltIn: eslintPath === resolveBuiltInESLint()
140+
});
129141
}
130142

131-
let cache = ESLINT_CACHE[eslintPath];
143+
let cached = ESLINT_CACHE.get(resolveDir);
132144

133-
if (compareVersions(cache.eslintVersion, MINIMUM_ESLINT_VERSION) < 1) {
145+
if (compareVersions(cached.eslintVersion, MINIMUM_ESLINT_VERSION) < 1) {
134146
// Unsupported version.
135-
throw new IncompatibleVersionError(cache.eslintVersion);
136-
} else if ((compareVersions(cache.eslintVersion, '8.0.0') < 1) && legacyPackagePresent) {
147+
throw new IncompatibleVersionError(cached.eslintVersion);
148+
} else if ((compareVersions(cached.eslintVersion, '8.0.0') < 1) && legacyPackagePresent) {
137149
// We're dealing with version 7 of ESLint. The legacy `linter-eslint`
138150
// package is present and capable of linting with this version, so we
139151
// should halt instead of trying to lint everything twice.
140-
throw new VersionOverlapError(cache.eslintVersion);
152+
throw new VersionOverlapError(cached.eslintVersion);
141153
}
142154

143-
return ESLINT_CACHE[eslintPath];
155+
return cached;
144156
}
145157

146158
async function lint (eslint, workingDir, filePath, fileContent) {
147-
process.chdir(workingDir);
148-
149159
if (typeof fileContent === 'string') {
150160
return eslint.lintText(fileContent, { filePath });
151161
} else {
@@ -235,7 +245,6 @@ async function processMessage (bundle) {
235245
isModified,
236246
key,
237247
legacyPackagePresent,
238-
projectPath,
239248
type
240249
} = bundle;
241250

@@ -258,18 +267,9 @@ async function processMessage (bundle) {
258267
return;
259268
}
260269

261-
// if (!projectPath) {
262-
// emit({
263-
// key,
264-
// error: `Must provide projectPath`,
265-
// type: 'no-project'
266-
// });
267-
// return;
268-
// }
269-
270270
let eslint;
271271
try {
272-
eslint = getESLint(filePath, projectPath, config, { legacyPackagePresent });
272+
eslint = getESLint(filePath, config, { legacyPackagePresent });
273273
} catch (err) {
274274
if (err instanceof IncompatibleVersionError) {
275275
emit({

0 commit comments

Comments
 (0)