1 | /**
|
2 | * @fileoverview Utility for caching lint results.
|
3 | * @author Kevin Partington
|
4 | */
|
5 | ;
|
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.indexOf(cacheStrategy) !== -1
|
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 | // If source is present but null, need to reread the file from the filesystem.
|
132 | if (
|
133 | fileDescriptor.meta.results &&
|
134 | fileDescriptor.meta.results.source === null
|
135 | ) {
|
136 | debug(`Rereading cached result source from filesystem: ${filePath}`);
|
137 | fileDescriptor.meta.results.source = fs.readFileSync(filePath, "utf-8");
|
138 | }
|
139 |
|
140 | return fileDescriptor.meta.results;
|
141 | }
|
142 |
|
143 | /**
|
144 | * Set the cached lint results for a given file path, after removing any
|
145 | * information that will be both unnecessary and difficult to serialize.
|
146 | * Avoids caching results with an "output" property (meaning fixes were
|
147 | * applied), to prevent potentially incorrect results if fixes are not
|
148 | * written to disk.
|
149 | * @param {string} filePath The file for which to set lint results.
|
150 | * @param {ConfigArray} config The config of the file.
|
151 | * @param {Object} result The lint result to be set for the file.
|
152 | * @returns {void}
|
153 | */
|
154 | setCachedLintResults(filePath, config, result) {
|
155 | if (result && Object.prototype.hasOwnProperty.call(result, "output")) {
|
156 | return;
|
157 | }
|
158 |
|
159 | const fileDescriptor = this.fileEntryCache.getFileDescriptor(filePath);
|
160 |
|
161 | if (fileDescriptor && !fileDescriptor.notFound) {
|
162 | debug(`Updating cached result: ${filePath}`);
|
163 |
|
164 | // Serialize the result, except that we want to remove the file source if present.
|
165 | const resultToSerialize = Object.assign({}, result);
|
166 |
|
167 | /*
|
168 | * Set result.source to null.
|
169 | * In `getCachedLintResults`, if source is explicitly null, we will
|
170 | * read the file from the filesystem to set the value again.
|
171 | */
|
172 | if (Object.prototype.hasOwnProperty.call(resultToSerialize, "source")) {
|
173 | resultToSerialize.source = null;
|
174 | }
|
175 |
|
176 | fileDescriptor.meta.results = resultToSerialize;
|
177 | fileDescriptor.meta.hashOfConfig = hashOfConfigFor(config);
|
178 | }
|
179 | }
|
180 |
|
181 | /**
|
182 | * Persists the in-memory cache to disk.
|
183 | * @returns {void}
|
184 | */
|
185 | reconcile() {
|
186 | debug(`Persisting cached results: ${this.cacheFileLocation}`);
|
187 | this.fileEntryCache.reconcile();
|
188 | }
|
189 | }
|
190 |
|
191 | module.exports = LintResultCache;
|