eslint
Version:
An AST-based pattern checker for JavaScript.
251 lines (212 loc) • 8.41 kB
JavaScript
/**
* @fileoverview Utility for caching lint results.
* @author Kevin Partington
*/
;
//-----------------------------------------------------------------------------
// Requirements
//-----------------------------------------------------------------------------
const fs = require("node:fs");
const path = require("node:path");
const fileEntryCache = require("file-entry-cache");
const stringify = require("json-stable-stringify-without-jsonify");
const pkg = require("../../package.json");
const assert = require("../shared/assert");
const hash = require("./hash");
const debug = require("debug")("eslint:lint-result-cache");
//------------------------------------------------------------------------------
// Typedefs
//------------------------------------------------------------------------------
/** @typedef {import("../types").Linter.Config} Config */
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
const configHashCache = new WeakMap();
const nodeVersion = process && process.version;
const validCacheStrategies = ["metadata", "content"];
const invalidCacheStrategyErrorMessage = `Cache strategy must be one of: ${validCacheStrategies
.map(strategy => `"${strategy}"`)
.join(", ")}`;
/**
* Tests whether a provided cacheStrategy is valid
* @param {string} cacheStrategy The cache strategy to use
* @returns {boolean} true if `cacheStrategy` is one of `validCacheStrategies`; false otherwise
*/
function isValidCacheStrategy(cacheStrategy) {
return validCacheStrategies.includes(cacheStrategy);
}
/**
* Calculates the hash of the config
* @param {Config} config The config.
* @returns {string} The hash of the config
*/
function hashOfConfigFor(config) {
if (!configHashCache.has(config)) {
configHashCache.set(
config,
hash(`${pkg.version}_${nodeVersion}_${stringify(config)}`),
);
}
return configHashCache.get(config);
}
//-----------------------------------------------------------------------------
// Public Interface
//-----------------------------------------------------------------------------
/**
* Lint result cache. This wraps around the file-entry-cache module,
* transparently removing properties that are difficult or expensive to
* serialize and adding them back in on retrieval.
*/
class LintResultCache {
/**
* Creates a new LintResultCache instance.
* @param {string} cacheFileLocation The cache file location.
* @param {"metadata" | "content"} cacheStrategy The cache strategy to use.
*/
constructor(cacheFileLocation, cacheStrategy) {
assert(cacheFileLocation, "Cache file location is required");
assert(cacheStrategy, "Cache strategy is required");
assert(
isValidCacheStrategy(cacheStrategy),
invalidCacheStrategyErrorMessage,
);
debug("Caching results to %s", cacheFileLocation);
const useCheckSum = cacheStrategy === "content";
debug('Using "%s" strategy to detect changes', cacheStrategy);
/*
* useModifiedTime: If `true` (default), use mtime and size to determine if the file has changed.
* It corresponds to the "metadata" cache strategy.
* useCheckSum: If `true`, use hash of the content to determine if the file has changed.
* It corresponds to the "content" cache strategy.
*
* For the "content" cache strategy, it is important to set useModifiedTime to `false`.
* Otherwise, file-entry-cache would use _both_ checks to determine if the file has changed,
* which would defeat the purpose of this cache strategy (use cases where the modification time
* of files changes even if their contents have not, e.g., after `git clone`).
*/
this.fileEntryCache = fileEntryCache.create(
path.basename(cacheFileLocation),
path.dirname(cacheFileLocation),
{
useModifiedTime: !useCheckSum,
useCheckSum,
},
);
this.cacheFileLocation = cacheFileLocation;
}
/**
* Retrieve cached lint results for a given file path, if present in the
* cache. If the file is present and has not been changed, rebuild any
* missing result information.
* @param {string} filePath The file for which to retrieve lint results.
* @param {Config} config The config of the file.
* @returns {Object|null} The rebuilt lint results, or null if the file is
* changed or not in the filesystem.
*/
getCachedLintResults(filePath, config) {
const cachedResults = this.getValidCachedLintResults(filePath, config);
if (!cachedResults) {
return cachedResults;
}
/*
* Shallow clone the object to ensure that any properties added or modified afterwards
* will not be accidentally stored in the cache file when `reconcile()` is called.
* https://github.com/eslint/eslint/issues/13507
* All intentional changes to the cache file must be done through `setCachedLintResults()`.
*/
const results = { ...cachedResults };
// If source is present but null, need to reread the file from the filesystem.
if (results.source === null) {
debug(
"Rereading cached result source from filesystem: %s",
filePath,
);
results.source = fs.readFileSync(filePath, "utf-8");
}
return results;
}
/**
* Retrieve cached lint results for a given file path, if present in the
* cache and still valid.
* @param {string} filePath The file for which to retrieve lint results.
* @param {Config} config The config of the file.
* @returns {Object|null} The cached lint results if present in the cache
* and still valid; null otherwise.
*/
getValidCachedLintResults(filePath, config) {
/*
* Cached lint results are valid if and only if:
* 1. The file is present in the filesystem
* 2. The file has not changed since the time it was previously linted
* 3. The ESLint configuration has not changed since the time the file
* was previously linted
* If any of these are not true, we will not reuse the lint results.
*/
const fileDescriptor = this.fileEntryCache.getFileDescriptor(filePath);
if (fileDescriptor.notFound) {
debug("File not found on the file system: %s", filePath);
return null;
}
if (!fileDescriptor.changed && !fileDescriptor.meta.data) {
debug("Legacy cache entry found: %s", filePath);
return null;
}
const hashOfConfig = hashOfConfigFor(config);
const changed =
fileDescriptor.changed ||
fileDescriptor.meta.data.hashOfConfig !== hashOfConfig;
if (changed) {
debug("Cache entry not found or no longer valid: %s", filePath);
return null;
}
return fileDescriptor.meta.data.results;
}
/**
* Set the cached lint results for a given file path, after removing any
* information that will be both unnecessary and difficult to serialize.
* Avoids caching results with an "output" property (meaning fixes were
* applied), to prevent potentially incorrect results if fixes are not
* written to disk.
* @param {string} filePath The file for which to set lint results.
* @param {Config} config The config of the file.
* @param {Object} result The lint result to be set for the file.
* @returns {void}
*/
setCachedLintResults(filePath, config, result) {
if (result && Object.hasOwn(result, "output")) {
return;
}
const fileDescriptor = this.fileEntryCache.getFileDescriptor(filePath);
if (fileDescriptor && !fileDescriptor.notFound) {
debug("Updating cached result: %s", filePath);
// Serialize the result, except that we want to remove the file source if present.
const resultToSerialize = Object.assign({}, result);
/*
* Set result.source to null.
* In `getCachedLintResults`, if source is explicitly null, we will
* read the file from the filesystem to set the value again.
*/
if (Object.hasOwn(resultToSerialize, "source")) {
resultToSerialize.source = null;
}
// remove legacy properties set by a previous version of ESLint, if present
if (fileDescriptor.meta.hashOfConfig) {
delete fileDescriptor.meta.results;
delete fileDescriptor.meta.hashOfConfig;
}
fileDescriptor.meta.data = {
results: resultToSerialize,
hashOfConfig: hashOfConfigFor(config),
};
}
}
/**
* Persists the in-memory cache to disk.
* @returns {void}
*/
reconcile() {
debug("Persisting cached results: %s", this.cacheFileLocation);
this.fileEntryCache.reconcile();
}
}
module.exports = LintResultCache;