lint-result-cache.js 6.69 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
/**
 * @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.indexOf(cacheStrategy) !== -1
    );
}

/**
 * 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;
        }

        // If source is present but null, need to reread the file from the filesystem.
        if (
            fileDescriptor.meta.results &&
            fileDescriptor.meta.results.source === null
        ) {
            debug(`Rereading cached result source from filesystem: ${filePath}`);
            fileDescriptor.meta.results.source = fs.readFileSync(filePath, "utf-8");
        }

        return fileDescriptor.meta.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;