2 * @fileoverview `CascadingConfigArrayFactory` class.
4 * `CascadingConfigArrayFactory` class has a responsibility:
6 * 1. Handles cascading of config files.
8 * It provides two methods:
10 * - `getConfigArrayForFile(filePath)`
11 * Get the corresponded configuration of a given file. This method doesn't
12 * throw even if the given file didn't exist.
14 * Clear the internal cache. You have to call this method when
15 * `additionalPluginPool` was updated if `baseConfig` or `cliConfig` depends
16 * on the additional plugins. (`CLIEngine#addPlugin()` method calls this.)
18 * @author Toru Nagashima <https://github.com/mysticatea>
22 //------------------------------------------------------------------------------
24 //------------------------------------------------------------------------------
26 const os = require("os");
27 const path = require("path");
28 const { validateConfigArray } = require("../shared/config-validator");
29 const { ConfigArrayFactory } = require("./config-array-factory");
30 const { ConfigArray, ConfigDependency, IgnorePattern } = require("./config-array");
31 const loadRules = require("./load-rules");
32 const debug = require("debug")("eslint:cascading-config-array-factory");
34 //------------------------------------------------------------------------------
36 //------------------------------------------------------------------------------
38 // Define types for VSCode IntelliSense.
39 /** @typedef {import("../shared/types").ConfigData} ConfigData */
40 /** @typedef {import("../shared/types").Parser} Parser */
41 /** @typedef {import("../shared/types").Plugin} Plugin */
42 /** @typedef {ReturnType<ConfigArrayFactory["create"]>} ConfigArray */
45 * @typedef {Object} CascadingConfigArrayFactoryOptions
46 * @property {Map<string,Plugin>} [additionalPluginPool] The map for additional plugins.
47 * @property {ConfigData} [baseConfig] The config by `baseConfig` option.
48 * @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.
49 * @property {string} [cwd] The base directory to start lookup.
50 * @property {string} [ignorePath] The path to the alternative file of `.eslintignore`.
51 * @property {string[]} [rulePaths] The value of `--rulesdir` option.
52 * @property {string} [specificConfigPath] The value of `--config` option.
53 * @property {boolean} [useEslintrc] if `false` then it doesn't load config files.
57 * @typedef {Object} CascadingConfigArrayFactoryInternalSlots
58 * @property {ConfigArray} baseConfigArray The config array of `baseConfig` option.
59 * @property {ConfigData} baseConfigData The config data of `baseConfig` option. This is used to reset `baseConfigArray`.
60 * @property {ConfigArray} cliConfigArray The config array of CLI options.
61 * @property {ConfigData} cliConfigData The config data of CLI options. This is used to reset `cliConfigArray`.
62 * @property {ConfigArrayFactory} configArrayFactory The factory for config arrays.
63 * @property {Map<string, ConfigArray>} configCache The cache from directory paths to config arrays.
64 * @property {string} cwd The base directory to start lookup.
65 * @property {WeakMap<ConfigArray, ConfigArray>} finalizeCache The cache from config arrays to finalized config arrays.
66 * @property {string} [ignorePath] The path to the alternative file of `.eslintignore`.
67 * @property {string[]|null} rulePaths The value of `--rulesdir` option. This is used to reset `baseConfigArray`.
68 * @property {string|null} specificConfigPath The value of `--config` option. This is used to reset `cliConfigArray`.
69 * @property {boolean} useEslintrc if `false` then it doesn't load config files.
72 /** @type {WeakMap<CascadingConfigArrayFactory, CascadingConfigArrayFactoryInternalSlots>} */
73 const internalSlotsMap = new WeakMap();
76 * Create the config array from `baseConfig` and `rulePaths`.
77 * @param {CascadingConfigArrayFactoryInternalSlots} slots The slots.
78 * @returns {ConfigArray} The config array of the base configs.
80 function createBaseConfigArray({
86 const baseConfigArray = configArrayFactory.create(
88 { name: "BaseConfig" }
92 * Create the config array element for the default ignore patterns.
93 * This element has `ignorePattern` property that ignores the default
94 * patterns in the current working directory.
96 baseConfigArray.unshift(configArrayFactory.create(
97 { ignorePatterns: IgnorePattern.DefaultPatterns },
98 { name: "DefaultIgnorePattern" }
102 * Load rules `--rulesdir` option as a pseudo plugin.
103 * Use a pseudo plugin to define rules of `--rulesdir`, so we can validate
104 * the rule's options with only information in the config array.
106 if (rulePaths && rulePaths.length > 0) {
107 baseConfigArray.push({
111 "": new ConfigDependency({
113 rules: rulePaths.reduce(
114 (map, rulesPath) => Object.assign(
116 loadRules(rulesPath, cwd)
123 importerName: "--rulesdir",
130 return baseConfigArray;
134 * Create the config array from CLI options.
135 * @param {CascadingConfigArrayFactoryInternalSlots} slots The slots.
136 * @returns {ConfigArray} The config array of the base configs.
138 function createCLIConfigArray({
144 const cliConfigArray = configArrayFactory.create(
146 { name: "CLIOptions" }
149 cliConfigArray.unshift(
151 ? configArrayFactory.loadESLintIgnore(ignorePath)
152 : configArrayFactory.loadDefaultESLintIgnore())
155 if (specificConfigPath) {
156 cliConfigArray.unshift(
157 ...configArrayFactory.loadFile(
164 return cliConfigArray;
168 * The error type when there are files matched by a glob, but all of them have been ignored.
170 class ConfigurationNotFoundError extends Error {
172 // eslint-disable-next-line jsdoc/require-description
174 * @param {string} directoryPath The directory path.
176 constructor(directoryPath) {
177 super(`No ESLint configuration found in ${directoryPath}.`);
178 this.messageTemplate = "no-config-found";
179 this.messageData = { directoryPath };
184 * This class provides the functionality that enumerates every file which is
185 * matched by given glob patterns and that configuration.
187 class CascadingConfigArrayFactory {
190 * Initialize this enumerator.
191 * @param {CascadingConfigArrayFactoryOptions} options The options.
194 additionalPluginPool = new Map(),
195 baseConfig: baseConfigData = null,
196 cliConfig: cliConfigData = null,
199 resolvePluginsRelativeTo = cwd,
201 specificConfigPath = null,
204 const configArrayFactory = new ConfigArrayFactory({
205 additionalPluginPool,
207 resolvePluginsRelativeTo
210 internalSlotsMap.set(this, {
211 baseConfigArray: createBaseConfigArray({
218 cliConfigArray: createCLIConfigArray({
226 configCache: new Map(),
228 finalizeCache: new WeakMap(),
237 * The path to the current working directory.
238 * This is used by tests.
242 const { cwd } = internalSlotsMap.get(this);
248 * Get the config array of a given file.
249 * If `filePath` was not given, it returns the config which contains only
250 * `baseConfigData` and `cliConfigData`.
251 * @param {string} [filePath] The file path to a file.
252 * @param {Object} [options] The options.
253 * @param {boolean} [options.ignoreNotFoundError] If `true` then it doesn't throw `ConfigurationNotFoundError`.
254 * @returns {ConfigArray} The config array of the file.
256 getConfigArrayForFile(filePath, { ignoreNotFoundError = false } = {}) {
261 } = internalSlotsMap.get(this);
264 return new ConfigArray(...baseConfigArray, ...cliConfigArray);
267 const directoryPath = path.dirname(path.resolve(cwd, filePath));
269 debug(`Load config files for ${directoryPath}.`);
271 return this._finalizeConfigArray(
272 this._loadConfigInAncestors(directoryPath),
279 * Clear config cache.
283 const slots = internalSlotsMap.get(this);
285 slots.baseConfigArray = createBaseConfigArray(slots);
286 slots.cliConfigArray = createCLIConfigArray(slots);
287 slots.configCache.clear();
291 * Load and normalize config files from the ancestor directories.
292 * @param {string} directoryPath The path to a leaf directory.
293 * @returns {ConfigArray} The loaded config.
296 _loadConfigInAncestors(directoryPath) {
303 } = internalSlotsMap.get(this);
306 return baseConfigArray;
309 let configArray = configCache.get(directoryPath);
313 debug(`Cache hit: ${directoryPath}.`);
316 debug(`No cache found: ${directoryPath}.`);
318 const homePath = os.homedir();
320 // Consider this is root.
321 if (directoryPath === homePath && cwd !== homePath) {
322 debug("Stop traversing because of considered root.");
323 return this._cacheConfig(directoryPath, baseConfigArray);
326 // Load the config on this directory.
328 configArray = configArrayFactory.loadInDirectory(directoryPath);
330 /* istanbul ignore next */
331 if (error.code === "EACCES") {
332 debug("Stop traversing because of 'EACCES' error.");
333 return this._cacheConfig(directoryPath, baseConfigArray);
338 if (configArray.length > 0 && configArray.isRoot()) {
339 debug("Stop traversing because of 'root:true'.");
340 configArray.unshift(...baseConfigArray);
341 return this._cacheConfig(directoryPath, configArray);
344 // Load from the ancestors and merge it.
345 const parentPath = path.dirname(directoryPath);
346 const parentConfigArray = parentPath && parentPath !== directoryPath
347 ? this._loadConfigInAncestors(parentPath)
350 if (configArray.length > 0) {
351 configArray.unshift(...parentConfigArray);
353 configArray = parentConfigArray;
357 return this._cacheConfig(directoryPath, configArray);
361 * Freeze and cache a given config.
362 * @param {string} directoryPath The path to a directory as a cache key.
363 * @param {ConfigArray} configArray The config array as a cache value.
364 * @returns {ConfigArray} The `configArray` (frozen).
366 _cacheConfig(directoryPath, configArray) {
367 const { configCache } = internalSlotsMap.get(this);
369 Object.freeze(configArray);
370 configCache.set(directoryPath, configArray);
376 * Finalize a given config array.
377 * Concatenate `--config` and other CLI options.
378 * @param {ConfigArray} configArray The parent config array.
379 * @param {string} directoryPath The path to the leaf directory to find config files.
380 * @param {boolean} ignoreNotFoundError If `true` then it doesn't throw `ConfigurationNotFoundError`.
381 * @returns {ConfigArray} The loaded config.
384 _finalizeConfigArray(configArray, directoryPath, ignoreNotFoundError) {
390 } = internalSlotsMap.get(this);
392 let finalConfigArray = finalizeCache.get(configArray);
394 if (!finalConfigArray) {
395 finalConfigArray = configArray;
397 // Load the personal config if there are no regular config files.
400 configArray.every(c => !c.filePath) &&
401 cliConfigArray.every(c => !c.filePath) // `--config` option can be a file.
403 debug("Loading the config file of the home directory.");
405 finalConfigArray = configArrayFactory.loadInDirectory(
407 { name: "PersonalConfig", parent: finalConfigArray }
411 // Apply CLI options.
412 if (cliConfigArray.length > 0) {
413 finalConfigArray = finalConfigArray.concat(cliConfigArray);
416 // Validate rule settings and environments.
417 validateConfigArray(finalConfigArray);
420 Object.freeze(finalConfigArray);
421 finalizeCache.set(configArray, finalConfigArray);
424 "Configuration was determined: %o on %s",
430 // At least one element (the default ignore patterns) exists.
431 if (!ignoreNotFoundError && useEslintrc && finalConfigArray.length <= 1) {
432 throw new ConfigurationNotFoundError(directoryPath);
435 return finalConfigArray;
439 //------------------------------------------------------------------------------
441 //------------------------------------------------------------------------------
443 module.exports = { CascadingConfigArrayFactory };