• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * STOP!!! DO NOT MODIFY.
3 *
4 * This file is part of the ongoing work to move the eslintrc-style config
5 * system into the @eslint/eslintrc package. This file needs to remain
6 * unchanged in order for this work to proceed.
7 *
8 * If you think you need to change this file, please contact @nzakas first.
9 *
10 * Thanks in advance for your cooperation.
11 */
12
13/**
14 * @fileoverview `CascadingConfigArrayFactory` class.
15 *
16 * `CascadingConfigArrayFactory` class has a responsibility:
17 *
18 * 1. Handles cascading of config files.
19 *
20 * It provides two methods:
21 *
22 * - `getConfigArrayForFile(filePath)`
23 *     Get the corresponded configuration of a given file. This method doesn't
24 *     throw even if the given file didn't exist.
25 * - `clearCache()`
26 *     Clear the internal cache. You have to call this method when
27 *     `additionalPluginPool` was updated if `baseConfig` or `cliConfig` depends
28 *     on the additional plugins. (`CLIEngine#addPlugin()` method calls this.)
29 *
30 * @author Toru Nagashima <https://github.com/mysticatea>
31 */
32"use strict";
33
34//------------------------------------------------------------------------------
35// Requirements
36//------------------------------------------------------------------------------
37
38const os = require("os");
39const path = require("path");
40const { validateConfigArray } = require("../shared/config-validator");
41const { emitDeprecationWarning } = require("../shared/deprecation-warnings");
42const { ConfigArrayFactory } = require("./config-array-factory");
43const { ConfigArray, ConfigDependency, IgnorePattern } = require("./config-array");
44const loadRules = require("./load-rules");
45const debug = require("debug")("eslint:cascading-config-array-factory");
46
47//------------------------------------------------------------------------------
48// Helpers
49//------------------------------------------------------------------------------
50
51// Define types for VSCode IntelliSense.
52/** @typedef {import("../shared/types").ConfigData} ConfigData */
53/** @typedef {import("../shared/types").Parser} Parser */
54/** @typedef {import("../shared/types").Plugin} Plugin */
55/** @typedef {ReturnType<ConfigArrayFactory["create"]>} ConfigArray */
56
57/**
58 * @typedef {Object} CascadingConfigArrayFactoryOptions
59 * @property {Map<string,Plugin>} [additionalPluginPool] The map for additional plugins.
60 * @property {ConfigData} [baseConfig] The config by `baseConfig` option.
61 * @property {ConfigData} [cliConfig] The config by CLI options (`--env`, `--global`, `--ignore-pattern`, `--parser`, `--parser-options`, `--plugin`, and `--rule`). CLI options overwrite the setting in config files.
62 * @property {string} [cwd] The base directory to start lookup.
63 * @property {string} [ignorePath] The path to the alternative file of `.eslintignore`.
64 * @property {string[]} [rulePaths] The value of `--rulesdir` option.
65 * @property {string} [specificConfigPath] The value of `--config` option.
66 * @property {boolean} [useEslintrc] if `false` then it doesn't load config files.
67 */
68
69/**
70 * @typedef {Object} CascadingConfigArrayFactoryInternalSlots
71 * @property {ConfigArray} baseConfigArray The config array of `baseConfig` option.
72 * @property {ConfigData} baseConfigData The config data of `baseConfig` option. This is used to reset `baseConfigArray`.
73 * @property {ConfigArray} cliConfigArray The config array of CLI options.
74 * @property {ConfigData} cliConfigData The config data of CLI options. This is used to reset `cliConfigArray`.
75 * @property {ConfigArrayFactory} configArrayFactory The factory for config arrays.
76 * @property {Map<string, ConfigArray>} configCache The cache from directory paths to config arrays.
77 * @property {string} cwd The base directory to start lookup.
78 * @property {WeakMap<ConfigArray, ConfigArray>} finalizeCache The cache from config arrays to finalized config arrays.
79 * @property {string} [ignorePath] The path to the alternative file of `.eslintignore`.
80 * @property {string[]|null} rulePaths The value of `--rulesdir` option. This is used to reset `baseConfigArray`.
81 * @property {string|null} specificConfigPath The value of `--config` option. This is used to reset `cliConfigArray`.
82 * @property {boolean} useEslintrc if `false` then it doesn't load config files.
83 */
84
85/** @type {WeakMap<CascadingConfigArrayFactory, CascadingConfigArrayFactoryInternalSlots>} */
86const internalSlotsMap = new WeakMap();
87
88/**
89 * Create the config array from `baseConfig` and `rulePaths`.
90 * @param {CascadingConfigArrayFactoryInternalSlots} slots The slots.
91 * @returns {ConfigArray} The config array of the base configs.
92 */
93function createBaseConfigArray({
94    configArrayFactory,
95    baseConfigData,
96    rulePaths,
97    cwd
98}) {
99    const baseConfigArray = configArrayFactory.create(
100        baseConfigData,
101        { name: "BaseConfig" }
102    );
103
104    /*
105     * Create the config array element for the default ignore patterns.
106     * This element has `ignorePattern` property that ignores the default
107     * patterns in the current working directory.
108     */
109    baseConfigArray.unshift(configArrayFactory.create(
110        { ignorePatterns: IgnorePattern.DefaultPatterns },
111        { name: "DefaultIgnorePattern" }
112    )[0]);
113
114    /*
115     * Load rules `--rulesdir` option as a pseudo plugin.
116     * Use a pseudo plugin to define rules of `--rulesdir`, so we can validate
117     * the rule's options with only information in the config array.
118     */
119    if (rulePaths && rulePaths.length > 0) {
120        baseConfigArray.push({
121            type: "config",
122            name: "--rulesdir",
123            filePath: "",
124            plugins: {
125                "": new ConfigDependency({
126                    definition: {
127                        rules: rulePaths.reduce(
128                            (map, rulesPath) => Object.assign(
129                                map,
130                                loadRules(rulesPath, cwd)
131                            ),
132                            {}
133                        )
134                    },
135                    filePath: "",
136                    id: "",
137                    importerName: "--rulesdir",
138                    importerPath: ""
139                })
140            }
141        });
142    }
143
144    return baseConfigArray;
145}
146
147/**
148 * Create the config array from CLI options.
149 * @param {CascadingConfigArrayFactoryInternalSlots} slots The slots.
150 * @returns {ConfigArray} The config array of the base configs.
151 */
152function createCLIConfigArray({
153    cliConfigData,
154    configArrayFactory,
155    cwd,
156    ignorePath,
157    specificConfigPath
158}) {
159    const cliConfigArray = configArrayFactory.create(
160        cliConfigData,
161        { name: "CLIOptions" }
162    );
163
164    cliConfigArray.unshift(
165        ...(ignorePath
166            ? configArrayFactory.loadESLintIgnore(ignorePath)
167            : configArrayFactory.loadDefaultESLintIgnore())
168    );
169
170    if (specificConfigPath) {
171        cliConfigArray.unshift(
172            ...configArrayFactory.loadFile(
173                specificConfigPath,
174                { name: "--config", basePath: cwd }
175            )
176        );
177    }
178
179    return cliConfigArray;
180}
181
182/**
183 * The error type when there are files matched by a glob, but all of them have been ignored.
184 */
185class ConfigurationNotFoundError extends Error {
186
187    // eslint-disable-next-line jsdoc/require-description
188    /**
189     * @param {string} directoryPath The directory path.
190     */
191    constructor(directoryPath) {
192        super(`No ESLint configuration found in ${directoryPath}.`);
193        this.messageTemplate = "no-config-found";
194        this.messageData = { directoryPath };
195    }
196}
197
198/**
199 * This class provides the functionality that enumerates every file which is
200 * matched by given glob patterns and that configuration.
201 */
202class CascadingConfigArrayFactory {
203
204    /**
205     * Initialize this enumerator.
206     * @param {CascadingConfigArrayFactoryOptions} options The options.
207     */
208    constructor({
209        additionalPluginPool = new Map(),
210        baseConfig: baseConfigData = null,
211        cliConfig: cliConfigData = null,
212        cwd = process.cwd(),
213        ignorePath,
214        resolvePluginsRelativeTo,
215        rulePaths = [],
216        specificConfigPath = null,
217        useEslintrc = true
218    } = {}) {
219        const configArrayFactory = new ConfigArrayFactory({
220            additionalPluginPool,
221            cwd,
222            resolvePluginsRelativeTo
223        });
224
225        internalSlotsMap.set(this, {
226            baseConfigArray: createBaseConfigArray({
227                baseConfigData,
228                configArrayFactory,
229                cwd,
230                rulePaths
231            }),
232            baseConfigData,
233            cliConfigArray: createCLIConfigArray({
234                cliConfigData,
235                configArrayFactory,
236                cwd,
237                ignorePath,
238                specificConfigPath
239            }),
240            cliConfigData,
241            configArrayFactory,
242            configCache: new Map(),
243            cwd,
244            finalizeCache: new WeakMap(),
245            ignorePath,
246            rulePaths,
247            specificConfigPath,
248            useEslintrc
249        });
250    }
251
252    /**
253     * The path to the current working directory.
254     * This is used by tests.
255     * @type {string}
256     */
257    get cwd() {
258        const { cwd } = internalSlotsMap.get(this);
259
260        return cwd;
261    }
262
263    /**
264     * Get the config array of a given file.
265     * If `filePath` was not given, it returns the config which contains only
266     * `baseConfigData` and `cliConfigData`.
267     * @param {string} [filePath] The file path to a file.
268     * @param {Object} [options] The options.
269     * @param {boolean} [options.ignoreNotFoundError] If `true` then it doesn't throw `ConfigurationNotFoundError`.
270     * @returns {ConfigArray} The config array of the file.
271     */
272    getConfigArrayForFile(filePath, { ignoreNotFoundError = false } = {}) {
273        const {
274            baseConfigArray,
275            cliConfigArray,
276            cwd
277        } = internalSlotsMap.get(this);
278
279        if (!filePath) {
280            return new ConfigArray(...baseConfigArray, ...cliConfigArray);
281        }
282
283        const directoryPath = path.dirname(path.resolve(cwd, filePath));
284
285        debug(`Load config files for ${directoryPath}.`);
286
287        return this._finalizeConfigArray(
288            this._loadConfigInAncestors(directoryPath),
289            directoryPath,
290            ignoreNotFoundError
291        );
292    }
293
294    /**
295     * Set the config data to override all configs.
296     * Require to call `clearCache()` method after this method is called.
297     * @param {ConfigData} configData The config data to override all configs.
298     * @returns {void}
299     */
300    setOverrideConfig(configData) {
301        const slots = internalSlotsMap.get(this);
302
303        slots.cliConfigData = configData;
304    }
305
306    /**
307     * Clear config cache.
308     * @returns {void}
309     */
310    clearCache() {
311        const slots = internalSlotsMap.get(this);
312
313        slots.baseConfigArray = createBaseConfigArray(slots);
314        slots.cliConfigArray = createCLIConfigArray(slots);
315        slots.configCache.clear();
316    }
317
318    /**
319     * Load and normalize config files from the ancestor directories.
320     * @param {string} directoryPath The path to a leaf directory.
321     * @param {boolean} configsExistInSubdirs `true` if configurations exist in subdirectories.
322     * @returns {ConfigArray} The loaded config.
323     * @private
324     */
325    _loadConfigInAncestors(directoryPath, configsExistInSubdirs = false) {
326        const {
327            baseConfigArray,
328            configArrayFactory,
329            configCache,
330            cwd,
331            useEslintrc
332        } = internalSlotsMap.get(this);
333
334        if (!useEslintrc) {
335            return baseConfigArray;
336        }
337
338        let configArray = configCache.get(directoryPath);
339
340        // Hit cache.
341        if (configArray) {
342            debug(`Cache hit: ${directoryPath}.`);
343            return configArray;
344        }
345        debug(`No cache found: ${directoryPath}.`);
346
347        const homePath = os.homedir();
348
349        // Consider this is root.
350        if (directoryPath === homePath && cwd !== homePath) {
351            debug("Stop traversing because of considered root.");
352            if (configsExistInSubdirs) {
353                const filePath = ConfigArrayFactory.getPathToConfigFileInDirectory(directoryPath);
354
355                if (filePath) {
356                    emitDeprecationWarning(
357                        filePath,
358                        "ESLINT_PERSONAL_CONFIG_SUPPRESS"
359                    );
360                }
361            }
362            return this._cacheConfig(directoryPath, baseConfigArray);
363        }
364
365        // Load the config on this directory.
366        try {
367            configArray = configArrayFactory.loadInDirectory(directoryPath);
368        } catch (error) {
369            /* istanbul ignore next */
370            if (error.code === "EACCES") {
371                debug("Stop traversing because of 'EACCES' error.");
372                return this._cacheConfig(directoryPath, baseConfigArray);
373            }
374            throw error;
375        }
376
377        if (configArray.length > 0 && configArray.isRoot()) {
378            debug("Stop traversing because of 'root:true'.");
379            configArray.unshift(...baseConfigArray);
380            return this._cacheConfig(directoryPath, configArray);
381        }
382
383        // Load from the ancestors and merge it.
384        const parentPath = path.dirname(directoryPath);
385        const parentConfigArray = parentPath && parentPath !== directoryPath
386            ? this._loadConfigInAncestors(
387                parentPath,
388                configsExistInSubdirs || configArray.length > 0
389            )
390            : baseConfigArray;
391
392        if (configArray.length > 0) {
393            configArray.unshift(...parentConfigArray);
394        } else {
395            configArray = parentConfigArray;
396        }
397
398        // Cache and return.
399        return this._cacheConfig(directoryPath, configArray);
400    }
401
402    /**
403     * Freeze and cache a given config.
404     * @param {string} directoryPath The path to a directory as a cache key.
405     * @param {ConfigArray} configArray The config array as a cache value.
406     * @returns {ConfigArray} The `configArray` (frozen).
407     */
408    _cacheConfig(directoryPath, configArray) {
409        const { configCache } = internalSlotsMap.get(this);
410
411        Object.freeze(configArray);
412        configCache.set(directoryPath, configArray);
413
414        return configArray;
415    }
416
417    /**
418     * Finalize a given config array.
419     * Concatenate `--config` and other CLI options.
420     * @param {ConfigArray} configArray The parent config array.
421     * @param {string} directoryPath The path to the leaf directory to find config files.
422     * @param {boolean} ignoreNotFoundError If `true` then it doesn't throw `ConfigurationNotFoundError`.
423     * @returns {ConfigArray} The loaded config.
424     * @private
425     */
426    _finalizeConfigArray(configArray, directoryPath, ignoreNotFoundError) {
427        const {
428            cliConfigArray,
429            configArrayFactory,
430            finalizeCache,
431            useEslintrc
432        } = internalSlotsMap.get(this);
433
434        let finalConfigArray = finalizeCache.get(configArray);
435
436        if (!finalConfigArray) {
437            finalConfigArray = configArray;
438
439            // Load the personal config if there are no regular config files.
440            if (
441                useEslintrc &&
442                configArray.every(c => !c.filePath) &&
443                cliConfigArray.every(c => !c.filePath) // `--config` option can be a file.
444            ) {
445                const homePath = os.homedir();
446
447                debug("Loading the config file of the home directory:", homePath);
448
449                const personalConfigArray = configArrayFactory.loadInDirectory(
450                    homePath,
451                    { name: "PersonalConfig" }
452                );
453
454                if (
455                    personalConfigArray.length > 0 &&
456                    !directoryPath.startsWith(homePath)
457                ) {
458                    const lastElement =
459                        personalConfigArray[personalConfigArray.length - 1];
460
461                    emitDeprecationWarning(
462                        lastElement.filePath,
463                        "ESLINT_PERSONAL_CONFIG_LOAD"
464                    );
465                }
466
467                finalConfigArray = finalConfigArray.concat(personalConfigArray);
468            }
469
470            // Apply CLI options.
471            if (cliConfigArray.length > 0) {
472                finalConfigArray = finalConfigArray.concat(cliConfigArray);
473            }
474
475            // Validate rule settings and environments.
476            validateConfigArray(finalConfigArray);
477
478            // Cache it.
479            Object.freeze(finalConfigArray);
480            finalizeCache.set(configArray, finalConfigArray);
481
482            debug(
483                "Configuration was determined: %o on %s",
484                finalConfigArray,
485                directoryPath
486            );
487        }
488
489        // At least one element (the default ignore patterns) exists.
490        if (!ignoreNotFoundError && useEslintrc && finalConfigArray.length <= 1) {
491            throw new ConfigurationNotFoundError(directoryPath);
492        }
493
494        return finalConfigArray;
495    }
496}
497
498//------------------------------------------------------------------------------
499// Public Interface
500//------------------------------------------------------------------------------
501
502module.exports = { CascadingConfigArrayFactory };
503