Skip to content

Commit 2b97607

Browse files
authored
feat: Implement caching for FlatESLint (#16190)
Refs #13481
1 parent fd5d3d3 commit 2b97607

5 files changed

Lines changed: 168 additions & 32 deletions

File tree

lib/config/flat-config-array.js

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -139,31 +139,72 @@ class FlatConfigArray extends ConfigArray {
139139
[ConfigArraySymbol.finalizeConfig](config) {
140140

141141
const { plugins, languageOptions, processor } = config;
142+
let parserName, processorName;
143+
let invalidParser = false,
144+
invalidProcessor = false;
142145

143146
// Check parser value
144-
if (languageOptions && languageOptions.parser && typeof languageOptions.parser === "string") {
145-
const { pluginName, objectName: parserName } = splitPluginIdentifier(languageOptions.parser);
147+
if (languageOptions && languageOptions.parser) {
148+
if (typeof languageOptions.parser === "string") {
149+
const { pluginName, objectName: localParserName } = splitPluginIdentifier(languageOptions.parser);
146150

147-
if (!plugins || !plugins[pluginName] || !plugins[pluginName].parsers || !plugins[pluginName].parsers[parserName]) {
148-
throw new TypeError(`Key "parser": Could not find "${parserName}" in plugin "${pluginName}".`);
149-
}
151+
parserName = languageOptions.parser;
152+
153+
if (!plugins || !plugins[pluginName] || !plugins[pluginName].parsers || !plugins[pluginName].parsers[localParserName]) {
154+
throw new TypeError(`Key "parser": Could not find "${localParserName}" in plugin "${pluginName}".`);
155+
}
150156

151-
languageOptions.parser = plugins[pluginName].parsers[parserName];
157+
languageOptions.parser = plugins[pluginName].parsers[localParserName];
158+
} else {
159+
invalidParser = true;
160+
}
152161
}
153162

154163
// Check processor value
155-
if (processor && typeof processor === "string") {
156-
const { pluginName, objectName: processorName } = splitPluginIdentifier(processor);
164+
if (processor) {
165+
if (typeof processor === "string") {
166+
const { pluginName, objectName: localProcessorName } = splitPluginIdentifier(processor);
157167

158-
if (!plugins || !plugins[pluginName] || !plugins[pluginName].processors || !plugins[pluginName].processors[processorName]) {
159-
throw new TypeError(`Key "processor": Could not find "${processorName}" in plugin "${pluginName}".`);
160-
}
168+
processorName = processor;
169+
170+
if (!plugins || !plugins[pluginName] || !plugins[pluginName].processors || !plugins[pluginName].processors[localProcessorName]) {
171+
throw new TypeError(`Key "processor": Could not find "${localProcessorName}" in plugin "${pluginName}".`);
172+
}
161173

162-
config.processor = plugins[pluginName].processors[processorName];
174+
config.processor = plugins[pluginName].processors[localProcessorName];
175+
} else {
176+
invalidProcessor = true;
177+
}
163178
}
164179

165180
ruleValidator.validate(config);
166181

182+
// apply special logic for serialization into JSON
183+
/* eslint-disable object-shorthand -- shorthand would change "this" value */
184+
Object.defineProperty(config, "toJSON", {
185+
value: function() {
186+
187+
if (invalidParser) {
188+
throw new Error("Caching is not supported when parser is an object.");
189+
}
190+
191+
if (invalidProcessor) {
192+
throw new Error("Caching is not supported when processor is an object.");
193+
}
194+
195+
return {
196+
...this,
197+
plugins: Object.keys(plugins),
198+
languageOptions: {
199+
...languageOptions,
200+
parser: parserName
201+
},
202+
processor: processorName
203+
};
204+
}
205+
});
206+
/* eslint-enable object-shorthand -- ok to enable now */
207+
167208
return config;
168209
}
169210
/* eslint-enable class-methods-use-this -- Desired as instance method */

lib/eslint/eslint-helpers.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -436,9 +436,6 @@ function processOptions({
436436
if (typeof cache !== "boolean") {
437437
errors.push("'cache' must be a boolean.");
438438
}
439-
if (cache) {
440-
errors.push("'cache' option is not yet supported.");
441-
}
442439
if (!isNonEmptyString(cacheLocation)) {
443440
errors.push("'cacheLocation' must be a non-empty string.");
444441
}

lib/eslint/flat-eslint.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const {
3030
const {
3131
fileExists,
3232
findFiles,
33+
getCacheFile,
3334

3435
isNonEmptyString,
3536
isArrayOfNonEmptyString,
@@ -41,6 +42,7 @@ const {
4142
} = require("./eslint-helpers");
4243
const { pathToFileURL } = require("url");
4344
const { FlatConfigArray } = require("../config/flat-config-array");
45+
const LintResultCache = require("../cli-engine/lint-result-cache");
4446

4547
/*
4648
* This is necessary to allow overwriting writeFile for testing purposes.
@@ -606,9 +608,20 @@ class FlatESLint {
606608
configType: "flat"
607609
});
608610

611+
const cacheFilePath = getCacheFile(
612+
processedOptions.cacheLocation,
613+
processedOptions.cwd
614+
);
615+
616+
const lintResultCache = processedOptions.cache
617+
? new LintResultCache(cacheFilePath, processedOptions.cacheStrategy)
618+
: null;
619+
609620
privateMembers.set(this, {
610621
options: processedOptions,
611622
linter,
623+
cacheFilePath,
624+
lintResultCache,
612625
defaultConfigs,
613626
defaultIgnores: () => false,
614627
configs: null
@@ -782,6 +795,8 @@ class FlatESLint {
782795

783796
// Delete cache file; should this be done here?
784797
if (!cache && cacheFilePath) {
798+
debug(`Deleting cache file at ${cacheFilePath}`);
799+
785800
try {
786801
await fs.unlink(cacheFilePath);
787802
} catch (error) {

tests/lib/config/flat-config-array.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const { FlatConfigArray } = require("../../../lib/config/flat-config-array");
1313
const assert = require("chai").assert;
1414
const allConfig = require("../../../conf/eslint-all");
1515
const recommendedConfig = require("../../../conf/eslint-recommended");
16+
const stringify = require("json-stable-stringify-without-jsonify");
1617

1718
//-----------------------------------------------------------------------------
1819
// Helpers
@@ -182,6 +183,78 @@ describe("FlatConfigArray", () => {
182183
assert.notStrictEqual(base[0].languageOptions.parserOptions, config.languageOptions.parserOptions, "parserOptions should be new object");
183184
});
184185

186+
describe("Serialization of configs", () => {
187+
it("should convert config into normalized JSON object", () => {
188+
189+
const configs = new FlatConfigArray([{
190+
plugins: {
191+
a: {},
192+
b: {}
193+
}
194+
}]);
195+
196+
configs.normalizeSync();
197+
198+
const config = configs.getConfig("foo.js");
199+
const expected = {
200+
plugins: ["@", "a", "b"],
201+
languageOptions: {
202+
ecmaVersion: "latest",
203+
sourceType: "module",
204+
parser: "@/espree",
205+
parserOptions: {}
206+
},
207+
processor: void 0
208+
};
209+
const actual = config.toJSON();
210+
211+
assert.deepStrictEqual(actual, expected);
212+
213+
assert.strictEqual(stringify(actual), stringify(expected));
214+
});
215+
216+
it("should throw an error when config with parser object is normalized", () => {
217+
218+
const configs = new FlatConfigArray([{
219+
languageOptions: {
220+
parser: {
221+
parse() { /* empty */ }
222+
}
223+
}
224+
}]);
225+
226+
configs.normalizeSync();
227+
228+
const config = configs.getConfig("foo.js");
229+
230+
assert.throws(() => {
231+
config.toJSON();
232+
}, /Caching is not supported/u);
233+
234+
});
235+
236+
it("should throw an error when config with processor object is normalized", () => {
237+
238+
const configs = new FlatConfigArray([{
239+
processor: {
240+
preprocess() { /* empty */ },
241+
postprocess() { /* empty */ }
242+
}
243+
}]);
244+
245+
configs.normalizeSync();
246+
247+
const config = configs.getConfig("foo.js");
248+
249+
assert.throws(() => {
250+
config.toJSON();
251+
}, /Caching is not supported/u);
252+
253+
});
254+
255+
256+
});
257+
185258
describe("Special configs", () => {
186259
it("eslint:recommended is replaced with an actual config", async () => {
187260
const configs = new FlatConfigArray(["eslint:recommended"]);

tests/lib/eslint/flat-eslint.js

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1396,7 +1396,7 @@ describe("FlatESLint", () => {
13961396
});
13971397

13981398
// Cannot be run properly until cache is implemented
1399-
xit("should run autofix even if files are cached without autofix results", async () => {
1399+
it("should run autofix even if files are cached without autofix results", async () => {
14001400
const baseOptions = {
14011401
cwd: path.join(fixtureDir, ".."),
14021402
overrideConfigFile: true,
@@ -1470,7 +1470,7 @@ describe("FlatESLint", () => {
14701470
});
14711471
});
14721472

1473-
xdescribe("cache", () => {
1473+
describe("cache", () => {
14741474

14751475
/**
14761476
* helper method to delete a file without caring about exceptions
@@ -1609,11 +1609,15 @@ describe("FlatESLint", () => {
16091609
assert(shell.test("-f", path.resolve(cwd, ".eslintcache")), "the cache for eslint was created at provided cwd");
16101610
});
16111611

1612-
it("should invalidate the cache if the configuration changed between executions", async () => {
1613-
assert(!shell.test("-f", path.resolve(".eslintcache")), "the cache for eslint does not exist");
1612+
it("should invalidate the cache if the overrideConfig changed between executions", async () => {
1613+
const cwd = getFixturePath("cache/src");
1614+
const cacheLocation = path.resolve(cwd, ".eslintcache");
1615+
1616+
assert(!shell.test("-f", cacheLocation), "the cache for eslint does not exist");
16141617

16151618
eslint = new FlatESLint({
16161619
overrideConfigFile: true,
1620+
cwd,
16171621

16181622
// specifying cache true the cache will be created
16191623
cache: true,
@@ -1627,24 +1631,26 @@ describe("FlatESLint", () => {
16271631
ignore: false
16281632
});
16291633

1630-
let spy = sinon.spy(fs, "readFileSync");
1634+
let spy = sinon.spy(fs.promises, "readFile");
16311635

1632-
let file = getFixturePath("cache/src", "test-file.js");
1636+
let file = path.join(cwd, "test-file.js");
16331637

16341638
file = fs.realpathSync(file);
16351639
const results = await eslint.lintFiles([file]);
16361640

16371641
for (const { errorCount, warningCount } of results) {
16381642
assert.strictEqual(errorCount + warningCount, 0, "the file passed without errors or warnings");
16391643
}
1640-
assert.strictEqual(spy.getCall(0).args[0], file, "the module read the file because is considered changed");
1641-
assert(shell.test("-f", path.resolve(".eslintcache")), "the cache for eslint was created");
1644+
1645+
assert(spy.calledWith(file), "ESLint should have read the file because it's considered changed");
1646+
assert(shell.test("-f", cacheLocation), "the cache for eslint should still exist");
16421647

16431648
// destroy the spy
16441649
sinon.restore();
16451650

16461651
eslint = new FlatESLint({
16471652
overrideConfigFile: true,
1653+
cwd,
16481654

16491655
// specifying cache true the cache will be created
16501656
cache: true,
@@ -1659,20 +1665,23 @@ describe("FlatESLint", () => {
16591665
});
16601666

16611667
// create a new spy
1662-
spy = sinon.spy(fs, "readFileSync");
1668+
spy = sinon.spy(fs.promises, "readFile");
16631669

16641670
const [cachedResult] = await eslint.lintFiles([file]);
16651671

1666-
assert.strictEqual(spy.getCall(0).args[0], file, "the module read the file because is considered changed because the config changed");
1667-
assert.strictEqual(cachedResult.errorCount, 1, "since configuration changed the cache was not used an one error was reported");
1668-
assert(shell.test("-f", path.resolve(".eslintcache")), "the cache for eslint was created");
1672+
assert(spy.calledWith(file), "ESLint should have read the file again because is considered changed because the config changed");
1673+
assert.strictEqual(cachedResult.errorCount, 1, "since configuration changed the cache was not used and one error was reported");
1674+
assert(shell.test("-f", cacheLocation), "The cache for ESLint should still exist (2)");
16691675
});
16701676

16711677
it("should remember the files from a previous run and do not operate on them if not changed", async () => {
1672-
assert(!shell.test("-f", path.resolve(".eslintcache")), "the cache for eslint does not exist");
1678+
1679+
const cwd = getFixturePath("cache/src");
1680+
const cacheLocation = path.resolve(cwd, ".eslintcache");
16731681

16741682
eslint = new FlatESLint({
16751683
overrideConfigFile: true,
1684+
cwd,
16761685

16771686
// specifying cache true the cache will be created
16781687
cache: true,
@@ -1686,22 +1695,23 @@ describe("FlatESLint", () => {
16861695
ignore: false
16871696
});
16881697

1689-
let spy = sinon.spy(fs, "readFileSync");
1698+
let spy = sinon.spy(fs.promises, "readFile");
16901699

16911700
let file = getFixturePath("cache/src", "test-file.js");
16921701

16931702
file = fs.realpathSync(file);
16941703

16951704
const result = await eslint.lintFiles([file]);
16961705

1697-
assert.strictEqual(spy.getCall(0).args[0], file, "the module read the file because is considered changed");
1698-
assert(shell.test("-f", path.resolve(".eslintcache")), "the cache for eslint was created");
1706+
assert(spy.calledWith(file), "the module read the file because is considered changed");
1707+
assert(shell.test("-f", cacheLocation), "the cache for eslint was created");
16991708

17001709
// destroy the spy
17011710
sinon.restore();
17021711

17031712
eslint = new FlatESLint({
17041713
overrideConfigFile: true,
1714+
cwd,
17051715

17061716
// specifying cache true the cache will be created
17071717
cache: true,
@@ -1716,7 +1726,7 @@ describe("FlatESLint", () => {
17161726
});
17171727

17181728
// create a new spy
1719-
spy = sinon.spy(fs, "readFileSync");
1729+
spy = sinon.spy(fs.promises, "readFile");
17201730

17211731
const cachedResult = await eslint.lintFiles([file]);
17221732

0 commit comments

Comments
 (0)