| /** | |
|  * @fileoverview Utility for caching lint results. | |
|  * @author Kevin Partington | |
|  */ | |
| "use strict"; | |
| 
 | |
| //----------------------------------------------------------------------------- | |
| // Requirements | |
| //----------------------------------------------------------------------------- | |
|  | |
| const assert = require("assert"); | |
| const fs = require("fs"); | |
| const fileEntryCache = require("file-entry-cache"); | |
| const stringify = require("json-stable-stringify-without-jsonify"); | |
| const pkg = require("../../package.json"); | |
| const hash = require("./hash"); | |
| 
 | |
| const debug = require("debug")("eslint:lint-result-cache"); | |
| 
 | |
| //----------------------------------------------------------------------------- | |
| // 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 {ConfigArray} 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 ${cacheFileLocation}`); | |
| 
 | |
|         const useChecksum = cacheStrategy === "content"; | |
| 
 | |
|         debug( | |
|             `Using "${cacheStrategy}" strategy to detect changes` | |
|         ); | |
| 
 | |
|         this.fileEntryCache = fileEntryCache.create( | |
|             cacheFileLocation, | |
|             void 0, | |
|             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 {ConfigArray} 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) { | |
| 
 | |
|         /* | |
|          * 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); | |
|         const hashOfConfig = hashOfConfigFor(config); | |
|         const changed = | |
|             fileDescriptor.changed || | |
|             fileDescriptor.meta.hashOfConfig !== hashOfConfig; | |
| 
 | |
|         if (fileDescriptor.notFound) { | |
|             debug(`File not found on the file system: ${filePath}`); | |
|             return null; | |
|         } | |
| 
 | |
|         if (changed) { | |
|             debug(`Cache entry not found or no longer valid: ${filePath}`); | |
|             return null; | |
|         } | |
| 
 | |
|         const cachedResults = fileDescriptor.meta.results; | |
| 
 | |
|         // Just in case, not sure if this can ever happen. | |
|         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: ${filePath}`); | |
|             results.source = fs.readFileSync(filePath, "utf-8"); | |
|         } | |
| 
 | |
|         return 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 {ConfigArray} 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.prototype.hasOwnProperty.call(result, "output")) { | |
|             return; | |
|         } | |
| 
 | |
|         const fileDescriptor = this.fileEntryCache.getFileDescriptor(filePath); | |
| 
 | |
|         if (fileDescriptor && !fileDescriptor.notFound) { | |
|             debug(`Updating cached result: ${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.prototype.hasOwnProperty.call(resultToSerialize, "source")) { | |
|                 resultToSerialize.source = null; | |
|             } | |
| 
 | |
|             fileDescriptor.meta.results = resultToSerialize; | |
|             fileDescriptor.meta.hashOfConfig = hashOfConfigFor(config); | |
|         } | |
|     } | |
| 
 | |
|     /** | |
|      * Persists the in-memory cache to disk. | |
|      * @returns {void} | |
|      */ | |
|     reconcile() { | |
|         debug(`Persisting cached results: ${this.cacheFileLocation}`); | |
|         this.fileEntryCache.reconcile(); | |
|     } | |
| } | |
| 
 | |
| module.exports = LintResultCache;
 |