lint-result-cache.js 7.1 KB

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