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 ConfigValidator = require("./shared/config-validator");
29 const { emitDeprecationWarning } = require("./shared/deprecation-warnings");
30 const { ConfigArrayFactory } = require("./config-array-factory");
31 const { ConfigArray, ConfigDependency, IgnorePattern } = require("./config-array");
32 const debug = require("debug")("eslintrc: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 {import("./shared/types").Rule} Rule */
43 /** @typedef {ReturnType<ConfigArrayFactory["create"]>} ConfigArray */
46 * @typedef {Object} CascadingConfigArrayFactoryOptions
47 * @property {Map<string,Plugin>} [additionalPluginPool] The map for additional plugins.
48 * @property {ConfigData} [baseConfig] The config by `baseConfig` option.
49 * @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.
50 * @property {string} [cwd] The base directory to start lookup.
51 * @property {string} [ignorePath] The path to the alternative file of `.eslintignore`.
52 * @property {string[]} [rulePaths] The value of `--rulesdir` option.
53 * @property {string} [specificConfigPath] The value of `--config` option.
54 * @property {boolean} [useEslintrc] if `false` then it doesn't load config files.
55 * @property {Function} loadRules The function to use to load rules.
56 * @property {Map<string,Rule>} builtInRules The rules that are built in to ESLint.
57 * @property {Object} [resolver=ModuleResolver] The module resolver object.
58 * @property {string} eslintAllPath The path to the definitions for eslint:all.
59 * @property {string} eslintRecommendedPath The path to the definitions for eslint:recommended.
63 * @typedef {Object} CascadingConfigArrayFactoryInternalSlots
64 * @property {ConfigArray} baseConfigArray The config array of `baseConfig` option.
65 * @property {ConfigData} baseConfigData The config data of `baseConfig` option. This is used to reset `baseConfigArray`.
66 * @property {ConfigArray} cliConfigArray The config array of CLI options.
67 * @property {ConfigData} cliConfigData The config data of CLI options. This is used to reset `cliConfigArray`.
68 * @property {ConfigArrayFactory} configArrayFactory The factory for config arrays.
69 * @property {Map<string, ConfigArray>} configCache The cache from directory paths to config arrays.
70 * @property {string} cwd The base directory to start lookup.
71 * @property {WeakMap<ConfigArray, ConfigArray>} finalizeCache The cache from config arrays to finalized config arrays.
72 * @property {string} [ignorePath] The path to the alternative file of `.eslintignore`.
73 * @property {string[]|null} rulePaths The value of `--rulesdir` option. This is used to reset `baseConfigArray`.
74 * @property {string|null} specificConfigPath The value of `--config` option. This is used to reset `cliConfigArray`.
75 * @property {boolean} useEslintrc if `false` then it doesn't load config files.
76 * @property {Function} loadRules The function to use to load rules.
77 * @property {Map<string,Rule>} builtInRules The rules that are built in to ESLint.
78 * @property {Object} [resolver=ModuleResolver] The module resolver object.
79 * @property {string} eslintAllPath The path to the definitions for eslint:all.
80 * @property {string} eslintRecommendedPath The path to the definitions for eslint:recommended.
83 /** @type {WeakMap<CascadingConfigArrayFactory, CascadingConfigArrayFactoryInternalSlots>} */
84 const internalSlotsMap = new WeakMap();
87 * Create the config array from `baseConfig` and `rulePaths`.
88 * @param {CascadingConfigArrayFactoryInternalSlots} slots The slots.
89 * @returns {ConfigArray} The config array of the base configs.
91 function createBaseConfigArray({
98 const baseConfigArray = configArrayFactory.create(
100 { name: "BaseConfig" }
104 * Create the config array element for the default ignore patterns.
105 * This element has `ignorePattern` property that ignores the default
106 * patterns in the current working directory.
108 baseConfigArray.unshift(configArrayFactory.create(
109 { ignorePatterns: IgnorePattern.DefaultPatterns },
110 { name: "DefaultIgnorePattern" }
114 * Load rules `--rulesdir` option as a pseudo plugin.
115 * Use a pseudo plugin to define rules of `--rulesdir`, so we can validate
116 * the rule's options with only information in the config array.
118 if (rulePaths && rulePaths.length > 0) {
119 baseConfigArray.push({
124 "": new ConfigDependency({
126 rules: rulePaths.reduce(
127 (map, rulesPath) => Object.assign(
129 loadRules(rulesPath, cwd)
136 importerName: "--rulesdir",
143 return baseConfigArray;
147 * Create the config array from CLI options.
148 * @param {CascadingConfigArrayFactoryInternalSlots} slots The slots.
149 * @returns {ConfigArray} The config array of the base configs.
151 function createCLIConfigArray({
158 const cliConfigArray = configArrayFactory.create(
160 { name: "CLIOptions" }
163 cliConfigArray.unshift(
165 ? configArrayFactory.loadESLintIgnore(ignorePath)
166 : configArrayFactory.loadDefaultESLintIgnore())
169 if (specificConfigPath) {
170 cliConfigArray.unshift(
171 ...configArrayFactory.loadFile(
173 { name: "--config", basePath: cwd }
178 return cliConfigArray;
182 * The error type when there are files matched by a glob, but all of them have been ignored.
184 class ConfigurationNotFoundError extends Error {
186 // eslint-disable-next-line jsdoc/require-description
188 * @param {string} directoryPath The directory path.
190 constructor(directoryPath) {
191 super(`No ESLint configuration found in ${directoryPath}.`);
192 this.messageTemplate = "no-config-found";
193 this.messageData = { directoryPath };
198 * This class provides the functionality that enumerates every file which is
199 * matched by given glob patterns and that configuration.
201 class CascadingConfigArrayFactory {
204 * Initialize this enumerator.
205 * @param {CascadingConfigArrayFactoryOptions} options The options.
208 additionalPluginPool = new Map(),
209 baseConfig: baseConfigData = null,
210 cliConfig: cliConfigData = null,
213 resolvePluginsRelativeTo,
215 specificConfigPath = null,
217 builtInRules = new Map(),
220 eslintRecommendedPath,
223 const configArrayFactory = new ConfigArrayFactory({
224 additionalPluginPool,
226 resolvePluginsRelativeTo,
229 eslintRecommendedPath,
233 internalSlotsMap.set(this, {
234 baseConfigArray: createBaseConfigArray({
243 cliConfigArray: createCLIConfigArray({
252 configCache: new Map(),
254 finalizeCache: new WeakMap(),
265 * The path to the current working directory.
266 * This is used by tests.
270 const { cwd } = internalSlotsMap.get(this);
276 * Get the config array of a given file.
277 * If `filePath` was not given, it returns the config which contains only
278 * `baseConfigData` and `cliConfigData`.
279 * @param {string} [filePath] The file path to a file.
280 * @param {Object} [options] The options.
281 * @param {boolean} [options.ignoreNotFoundError] If `true` then it doesn't throw `ConfigurationNotFoundError`.
282 * @returns {ConfigArray} The config array of the file.
284 getConfigArrayForFile(filePath, { ignoreNotFoundError = false } = {}) {
289 } = internalSlotsMap.get(this);
292 return new ConfigArray(...baseConfigArray, ...cliConfigArray);
295 const directoryPath = path.dirname(path.resolve(cwd, filePath));
297 debug(`Load config files for ${directoryPath}.`);
299 return this._finalizeConfigArray(
300 this._loadConfigInAncestors(directoryPath),
307 * Set the config data to override all configs.
308 * Require to call `clearCache()` method after this method is called.
309 * @param {ConfigData} configData The config data to override all configs.
312 setOverrideConfig(configData) {
313 const slots = internalSlotsMap.get(this);
315 slots.cliConfigData = configData;
319 * Clear config cache.
323 const slots = internalSlotsMap.get(this);
325 slots.baseConfigArray = createBaseConfigArray(slots);
326 slots.cliConfigArray = createCLIConfigArray(slots);
327 slots.configCache.clear();
331 * Load and normalize config files from the ancestor directories.
332 * @param {string} directoryPath The path to a leaf directory.
333 * @param {boolean} configsExistInSubdirs `true` if configurations exist in subdirectories.
334 * @returns {ConfigArray} The loaded config.
337 _loadConfigInAncestors(directoryPath, configsExistInSubdirs = false) {
344 } = internalSlotsMap.get(this);
347 return baseConfigArray;
350 let configArray = configCache.get(directoryPath);
354 debug(`Cache hit: ${directoryPath}.`);
357 debug(`No cache found: ${directoryPath}.`);
359 const homePath = os.homedir();
361 // Consider this is root.
362 if (directoryPath === homePath && cwd !== homePath) {
363 debug("Stop traversing because of considered root.");
364 if (configsExistInSubdirs) {
365 const filePath = ConfigArrayFactory.getPathToConfigFileInDirectory(directoryPath);
368 emitDeprecationWarning(
370 "ESLINT_PERSONAL_CONFIG_SUPPRESS"
374 return this._cacheConfig(directoryPath, baseConfigArray);
377 // Load the config on this directory.
379 configArray = configArrayFactory.loadInDirectory(directoryPath);
381 /* istanbul ignore next */
382 if (error.code === "EACCES") {
383 debug("Stop traversing because of 'EACCES' error.");
384 return this._cacheConfig(directoryPath, baseConfigArray);
389 if (configArray.length > 0 && configArray.isRoot()) {
390 debug("Stop traversing because of 'root:true'.");
391 configArray.unshift(...baseConfigArray);
392 return this._cacheConfig(directoryPath, configArray);
395 // Load from the ancestors and merge it.
396 const parentPath = path.dirname(directoryPath);
397 const parentConfigArray = parentPath && parentPath !== directoryPath
398 ? this._loadConfigInAncestors(
400 configsExistInSubdirs || configArray.length > 0
404 if (configArray.length > 0) {
405 configArray.unshift(...parentConfigArray);
407 configArray = parentConfigArray;
411 return this._cacheConfig(directoryPath, configArray);
415 * Freeze and cache a given config.
416 * @param {string} directoryPath The path to a directory as a cache key.
417 * @param {ConfigArray} configArray The config array as a cache value.
418 * @returns {ConfigArray} The `configArray` (frozen).
420 _cacheConfig(directoryPath, configArray) {
421 const { configCache } = internalSlotsMap.get(this);
423 Object.freeze(configArray);
424 configCache.set(directoryPath, configArray);
430 * Finalize a given config array.
431 * Concatenate `--config` and other CLI options.
432 * @param {ConfigArray} configArray The parent config array.
433 * @param {string} directoryPath The path to the leaf directory to find config files.
434 * @param {boolean} ignoreNotFoundError If `true` then it doesn't throw `ConfigurationNotFoundError`.
435 * @returns {ConfigArray} The loaded config.
438 _finalizeConfigArray(configArray, directoryPath, ignoreNotFoundError) {
445 } = internalSlotsMap.get(this);
447 let finalConfigArray = finalizeCache.get(configArray);
449 if (!finalConfigArray) {
450 finalConfigArray = configArray;
452 // Load the personal config if there are no regular config files.
455 configArray.every(c => !c.filePath) &&
456 cliConfigArray.every(c => !c.filePath) // `--config` option can be a file.
458 const homePath = os.homedir();
460 debug("Loading the config file of the home directory:", homePath);
462 const personalConfigArray = configArrayFactory.loadInDirectory(
464 { name: "PersonalConfig" }
468 personalConfigArray.length > 0 &&
469 !directoryPath.startsWith(homePath)
472 personalConfigArray[personalConfigArray.length - 1];
474 emitDeprecationWarning(
475 lastElement.filePath,
476 "ESLINT_PERSONAL_CONFIG_LOAD"
480 finalConfigArray = finalConfigArray.concat(personalConfigArray);
483 // Apply CLI options.
484 if (cliConfigArray.length > 0) {
485 finalConfigArray = finalConfigArray.concat(cliConfigArray);
488 // Validate rule settings and environments.
489 const validator = new ConfigValidator({
493 validator.validateConfigArray(finalConfigArray);
496 Object.freeze(finalConfigArray);
497 finalizeCache.set(configArray, finalConfigArray);
500 "Configuration was determined: %o on %s",
506 // At least one element (the default ignore patterns) exists.
507 if (!ignoreNotFoundError && useEslintrc && finalConfigArray.length <= 1) {
508 throw new ConfigurationNotFoundError(directoryPath);
511 return finalConfigArray;
515 //------------------------------------------------------------------------------
517 //------------------------------------------------------------------------------
519 module.exports = { CascadingConfigArrayFactory };