[d565449] | 1 | /**
|
---|
| 2 | * @fileoverview Utility for caching lint results.
|
---|
| 3 | * @author Kevin Partington
|
---|
| 4 | */
|
---|
| 5 | "use strict";
|
---|
| 6 |
|
---|
| 7 | //-----------------------------------------------------------------------------
|
---|
| 8 | // Requirements
|
---|
| 9 | //-----------------------------------------------------------------------------
|
---|
| 10 |
|
---|
| 11 | const assert = require("assert");
|
---|
| 12 | const fs = require("fs");
|
---|
| 13 | const fileEntryCache = require("file-entry-cache");
|
---|
| 14 | const stringify = require("json-stable-stringify-without-jsonify");
|
---|
| 15 | const pkg = require("../../package.json");
|
---|
| 16 | const hash = require("./hash");
|
---|
| 17 |
|
---|
| 18 | const debug = require("debug")("eslint:lint-result-cache");
|
---|
| 19 |
|
---|
| 20 | //-----------------------------------------------------------------------------
|
---|
| 21 | // Helpers
|
---|
| 22 | //-----------------------------------------------------------------------------
|
---|
| 23 |
|
---|
| 24 | const configHashCache = new WeakMap();
|
---|
| 25 | const nodeVersion = process && process.version;
|
---|
| 26 |
|
---|
| 27 | const validCacheStrategies = ["metadata", "content"];
|
---|
| 28 | const invalidCacheStrategyErrorMessage = `Cache strategy must be one of: ${validCacheStrategies
|
---|
| 29 | .map(strategy => `"${strategy}"`)
|
---|
| 30 | .join(", ")}`;
|
---|
| 31 |
|
---|
| 32 | /**
|
---|
| 33 | * Tests whether a provided cacheStrategy is valid
|
---|
| 34 | * @param {string} cacheStrategy The cache strategy to use
|
---|
| 35 | * @returns {boolean} true if `cacheStrategy` is one of `validCacheStrategies`; false otherwise
|
---|
| 36 | */
|
---|
| 37 | function isValidCacheStrategy(cacheStrategy) {
|
---|
| 38 | return (
|
---|
| 39 | validCacheStrategies.includes(cacheStrategy)
|
---|
| 40 | );
|
---|
| 41 | }
|
---|
| 42 |
|
---|
| 43 | /**
|
---|
| 44 | * Calculates the hash of the config
|
---|
| 45 | * @param {ConfigArray} config The config.
|
---|
| 46 | * @returns {string} The hash of the config
|
---|
| 47 | */
|
---|
| 48 | function hashOfConfigFor(config) {
|
---|
| 49 | if (!configHashCache.has(config)) {
|
---|
| 50 | configHashCache.set(config, hash(`${pkg.version}_${nodeVersion}_${stringify(config)}`));
|
---|
| 51 | }
|
---|
| 52 |
|
---|
| 53 | return configHashCache.get(config);
|
---|
| 54 | }
|
---|
| 55 |
|
---|
| 56 | //-----------------------------------------------------------------------------
|
---|
| 57 | // Public Interface
|
---|
| 58 | //-----------------------------------------------------------------------------
|
---|
| 59 |
|
---|
| 60 | /**
|
---|
| 61 | * Lint result cache. This wraps around the file-entry-cache module,
|
---|
| 62 | * transparently removing properties that are difficult or expensive to
|
---|
| 63 | * serialize and adding them back in on retrieval.
|
---|
| 64 | */
|
---|
| 65 | class LintResultCache {
|
---|
| 66 |
|
---|
| 67 | /**
|
---|
| 68 | * Creates a new LintResultCache instance.
|
---|
| 69 | * @param {string} cacheFileLocation The cache file location.
|
---|
| 70 | * @param {"metadata" | "content"} cacheStrategy The cache strategy to use.
|
---|
| 71 | */
|
---|
| 72 | constructor(cacheFileLocation, cacheStrategy) {
|
---|
| 73 | assert(cacheFileLocation, "Cache file location is required");
|
---|
| 74 | assert(cacheStrategy, "Cache strategy is required");
|
---|
| 75 | assert(
|
---|
| 76 | isValidCacheStrategy(cacheStrategy),
|
---|
| 77 | invalidCacheStrategyErrorMessage
|
---|
| 78 | );
|
---|
| 79 |
|
---|
| 80 | debug(`Caching results to ${cacheFileLocation}`);
|
---|
| 81 |
|
---|
| 82 | const useChecksum = cacheStrategy === "content";
|
---|
| 83 |
|
---|
| 84 | debug(
|
---|
| 85 | `Using "${cacheStrategy}" strategy to detect changes`
|
---|
| 86 | );
|
---|
| 87 |
|
---|
| 88 | this.fileEntryCache = fileEntryCache.create(
|
---|
| 89 | cacheFileLocation,
|
---|
| 90 | void 0,
|
---|
| 91 | useChecksum
|
---|
| 92 | );
|
---|
| 93 | this.cacheFileLocation = cacheFileLocation;
|
---|
| 94 | }
|
---|
| 95 |
|
---|
| 96 | /**
|
---|
| 97 | * Retrieve cached lint results for a given file path, if present in the
|
---|
| 98 | * cache. If the file is present and has not been changed, rebuild any
|
---|
| 99 | * missing result information.
|
---|
| 100 | * @param {string} filePath The file for which to retrieve lint results.
|
---|
| 101 | * @param {ConfigArray} config The config of the file.
|
---|
| 102 | * @returns {Object|null} The rebuilt lint results, or null if the file is
|
---|
| 103 | * changed or not in the filesystem.
|
---|
| 104 | */
|
---|
| 105 | getCachedLintResults(filePath, config) {
|
---|
| 106 |
|
---|
| 107 | /*
|
---|
| 108 | * Cached lint results are valid if and only if:
|
---|
| 109 | * 1. The file is present in the filesystem
|
---|
| 110 | * 2. The file has not changed since the time it was previously linted
|
---|
| 111 | * 3. The ESLint configuration has not changed since the time the file
|
---|
| 112 | * was previously linted
|
---|
| 113 | * If any of these are not true, we will not reuse the lint results.
|
---|
| 114 | */
|
---|
| 115 | const fileDescriptor = this.fileEntryCache.getFileDescriptor(filePath);
|
---|
| 116 | const hashOfConfig = hashOfConfigFor(config);
|
---|
| 117 | const changed =
|
---|
| 118 | fileDescriptor.changed ||
|
---|
| 119 | fileDescriptor.meta.hashOfConfig !== hashOfConfig;
|
---|
| 120 |
|
---|
| 121 | if (fileDescriptor.notFound) {
|
---|
| 122 | debug(`File not found on the file system: ${filePath}`);
|
---|
| 123 | return null;
|
---|
| 124 | }
|
---|
| 125 |
|
---|
| 126 | if (changed) {
|
---|
| 127 | debug(`Cache entry not found or no longer valid: ${filePath}`);
|
---|
| 128 | return null;
|
---|
| 129 | }
|
---|
| 130 |
|
---|
| 131 | const cachedResults = fileDescriptor.meta.results;
|
---|
| 132 |
|
---|
| 133 | // Just in case, not sure if this can ever happen.
|
---|
| 134 | if (!cachedResults) {
|
---|
| 135 | return cachedResults;
|
---|
| 136 | }
|
---|
| 137 |
|
---|
| 138 | /*
|
---|
| 139 | * Shallow clone the object to ensure that any properties added or modified afterwards
|
---|
| 140 | * will not be accidentally stored in the cache file when `reconcile()` is called.
|
---|
| 141 | * https://github.com/eslint/eslint/issues/13507
|
---|
| 142 | * All intentional changes to the cache file must be done through `setCachedLintResults()`.
|
---|
| 143 | */
|
---|
| 144 | const results = { ...cachedResults };
|
---|
| 145 |
|
---|
| 146 | // If source is present but null, need to reread the file from the filesystem.
|
---|
| 147 | if (results.source === null) {
|
---|
| 148 | debug(`Rereading cached result source from filesystem: ${filePath}`);
|
---|
| 149 | results.source = fs.readFileSync(filePath, "utf-8");
|
---|
| 150 | }
|
---|
| 151 |
|
---|
| 152 | return results;
|
---|
| 153 | }
|
---|
| 154 |
|
---|
| 155 | /**
|
---|
| 156 | * Set the cached lint results for a given file path, after removing any
|
---|
| 157 | * information that will be both unnecessary and difficult to serialize.
|
---|
| 158 | * Avoids caching results with an "output" property (meaning fixes were
|
---|
| 159 | * applied), to prevent potentially incorrect results if fixes are not
|
---|
| 160 | * written to disk.
|
---|
| 161 | * @param {string} filePath The file for which to set lint results.
|
---|
| 162 | * @param {ConfigArray} config The config of the file.
|
---|
| 163 | * @param {Object} result The lint result to be set for the file.
|
---|
| 164 | * @returns {void}
|
---|
| 165 | */
|
---|
| 166 | setCachedLintResults(filePath, config, result) {
|
---|
| 167 | if (result && Object.prototype.hasOwnProperty.call(result, "output")) {
|
---|
| 168 | return;
|
---|
| 169 | }
|
---|
| 170 |
|
---|
| 171 | const fileDescriptor = this.fileEntryCache.getFileDescriptor(filePath);
|
---|
| 172 |
|
---|
| 173 | if (fileDescriptor && !fileDescriptor.notFound) {
|
---|
| 174 | debug(`Updating cached result: ${filePath}`);
|
---|
| 175 |
|
---|
| 176 | // Serialize the result, except that we want to remove the file source if present.
|
---|
| 177 | const resultToSerialize = Object.assign({}, result);
|
---|
| 178 |
|
---|
| 179 | /*
|
---|
| 180 | * Set result.source to null.
|
---|
| 181 | * In `getCachedLintResults`, if source is explicitly null, we will
|
---|
| 182 | * read the file from the filesystem to set the value again.
|
---|
| 183 | */
|
---|
| 184 | if (Object.prototype.hasOwnProperty.call(resultToSerialize, "source")) {
|
---|
| 185 | resultToSerialize.source = null;
|
---|
| 186 | }
|
---|
| 187 |
|
---|
| 188 | fileDescriptor.meta.results = resultToSerialize;
|
---|
| 189 | fileDescriptor.meta.hashOfConfig = hashOfConfigFor(config);
|
---|
| 190 | }
|
---|
| 191 | }
|
---|
| 192 |
|
---|
| 193 | /**
|
---|
| 194 | * Persists the in-memory cache to disk.
|
---|
| 195 | * @returns {void}
|
---|
| 196 | */
|
---|
| 197 | reconcile() {
|
---|
| 198 | debug(`Persisting cached results: ${this.cacheFileLocation}`);
|
---|
| 199 | this.fileEntryCache.reconcile();
|
---|
| 200 | }
|
---|
| 201 | }
|
---|
| 202 |
|
---|
| 203 | module.exports = LintResultCache;
|
---|