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 { emitDeprecationWarning
} = require("../shared/deprecation-warnings");
30 const { ConfigArrayFactory
} = require("./config-array-factory");
31 const { ConfigArray
, ConfigDependency
, IgnorePattern
} = require("./config-array");
32 const loadRules
= require("./load-rules");
33 const debug
= require("debug")("eslint:cascading-config-array-factory");
35 //------------------------------------------------------------------------------
37 //------------------------------------------------------------------------------
39 // Define types for VSCode IntelliSense.
40 /** @typedef {import("../shared/types").ConfigData} ConfigData */
41 /** @typedef {import("../shared/types").Parser} Parser */
42 /** @typedef {import("../shared/types").Plugin} Plugin */
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.
58 * @typedef {Object} CascadingConfigArrayFactoryInternalSlots
59 * @property {ConfigArray} baseConfigArray The config array of `baseConfig` option.
60 * @property {ConfigData} baseConfigData The config data of `baseConfig` option. This is used to reset `baseConfigArray`.
61 * @property {ConfigArray} cliConfigArray The config array of CLI options.
62 * @property {ConfigData} cliConfigData The config data of CLI options. This is used to reset `cliConfigArray`.
63 * @property {ConfigArrayFactory} configArrayFactory The factory for config arrays.
64 * @property {Map<string, ConfigArray>} configCache The cache from directory paths to config arrays.
65 * @property {string} cwd The base directory to start lookup.
66 * @property {WeakMap<ConfigArray, ConfigArray>} finalizeCache The cache from config arrays to finalized config arrays.
67 * @property {string} [ignorePath] The path to the alternative file of `.eslintignore`.
68 * @property {string[]|null} rulePaths The value of `--rulesdir` option. This is used to reset `baseConfigArray`.
69 * @property {string|null} specificConfigPath The value of `--config` option. This is used to reset `cliConfigArray`.
70 * @property {boolean} useEslintrc if `false` then it doesn't load config files.
73 /** @type {WeakMap<CascadingConfigArrayFactory, CascadingConfigArrayFactoryInternalSlots>} */
74 const internalSlotsMap
= new WeakMap();
77 * Create the config array from `baseConfig` and `rulePaths`.
78 * @param {CascadingConfigArrayFactoryInternalSlots} slots The slots.
79 * @returns {ConfigArray} The config array of the base configs.
81 function createBaseConfigArray({
87 const baseConfigArray
= configArrayFactory
.create(
89 { name
: "BaseConfig" }
93 * Create the config array element for the default ignore patterns.
94 * This element has `ignorePattern` property that ignores the default
95 * patterns in the current working directory.
97 baseConfigArray
.unshift(configArrayFactory
.create(
98 { ignorePatterns
: IgnorePattern
.DefaultPatterns
},
99 { name
: "DefaultIgnorePattern" }
103 * Load rules `--rulesdir` option as a pseudo plugin.
104 * Use a pseudo plugin to define rules of `--rulesdir`, so we can validate
105 * the rule's options with only information in the config array.
107 if (rulePaths
&& rulePaths
.length
> 0) {
108 baseConfigArray
.push({
113 "": new ConfigDependency({
115 rules
: rulePaths
.reduce(
116 (map
, rulesPath
) => Object
.assign(
118 loadRules(rulesPath
, cwd
)
125 importerName
: "--rulesdir",
132 return baseConfigArray
;
136 * Create the config array from CLI options.
137 * @param {CascadingConfigArrayFactoryInternalSlots} slots The slots.
138 * @returns {ConfigArray} The config array of the base configs.
140 function createCLIConfigArray({
147 const cliConfigArray
= configArrayFactory
.create(
149 { name
: "CLIOptions" }
152 cliConfigArray
.unshift(
154 ? configArrayFactory
.loadESLintIgnore(ignorePath
)
155 : configArrayFactory
.loadDefaultESLintIgnore())
158 if (specificConfigPath
) {
159 cliConfigArray
.unshift(
160 ...configArrayFactory
.loadFile(
162 { name
: "--config", basePath
: cwd
}
167 return cliConfigArray
;
171 * The error type when there are files matched by a glob, but all of them have been ignored.
173 class ConfigurationNotFoundError
extends Error
{
175 // eslint-disable-next-line jsdoc/require-description
177 * @param {string} directoryPath The directory path.
179 constructor(directoryPath
) {
180 super(`No ESLint configuration found in ${directoryPath}.`);
181 this.messageTemplate
= "no-config-found";
182 this.messageData
= { directoryPath
};
187 * This class provides the functionality that enumerates every file which is
188 * matched by given glob patterns and that configuration.
190 class CascadingConfigArrayFactory
{
193 * Initialize this enumerator.
194 * @param {CascadingConfigArrayFactoryOptions} options The options.
197 additionalPluginPool
= new Map(),
198 baseConfig
: baseConfigData
= null,
199 cliConfig
: cliConfigData
= null,
202 resolvePluginsRelativeTo
,
204 specificConfigPath
= null,
207 const configArrayFactory
= new ConfigArrayFactory({
208 additionalPluginPool
,
210 resolvePluginsRelativeTo
213 internalSlotsMap
.set(this, {
214 baseConfigArray
: createBaseConfigArray({
221 cliConfigArray
: createCLIConfigArray({
230 configCache
: new Map(),
232 finalizeCache
: new WeakMap(),
241 * The path to the current working directory.
242 * This is used by tests.
246 const { cwd
} = internalSlotsMap
.get(this);
252 * Get the config array of a given file.
253 * If `filePath` was not given, it returns the config which contains only
254 * `baseConfigData` and `cliConfigData`.
255 * @param {string} [filePath] The file path to a file.
256 * @param {Object} [options] The options.
257 * @param {boolean} [options.ignoreNotFoundError] If `true` then it doesn't throw `ConfigurationNotFoundError`.
258 * @returns {ConfigArray} The config array of the file.
260 getConfigArrayForFile(filePath
, { ignoreNotFoundError
= false } = {}) {
265 } = internalSlotsMap
.get(this);
268 return new ConfigArray(...baseConfigArray
, ...cliConfigArray
);
271 const directoryPath
= path
.dirname(path
.resolve(cwd
, filePath
));
273 debug(`Load config files for ${directoryPath}.`);
275 return this._finalizeConfigArray(
276 this._loadConfigInAncestors(directoryPath
),
283 * Set the config data to override all configs.
284 * Require to call `clearCache()` method after this method is called.
285 * @param {ConfigData} configData The config data to override all configs.
288 setOverrideConfig(configData
) {
289 const slots
= internalSlotsMap
.get(this);
291 slots
.cliConfigData
= configData
;
295 * Clear config cache.
299 const slots
= internalSlotsMap
.get(this);
301 slots
.baseConfigArray
= createBaseConfigArray(slots
);
302 slots
.cliConfigArray
= createCLIConfigArray(slots
);
303 slots
.configCache
.clear();
307 * Load and normalize config files from the ancestor directories.
308 * @param {string} directoryPath The path to a leaf directory.
309 * @param {boolean} configsExistInSubdirs `true` if configurations exist in subdirectories.
310 * @returns {ConfigArray} The loaded config.
313 _loadConfigInAncestors(directoryPath
, configsExistInSubdirs
= false) {
320 } = internalSlotsMap
.get(this);
323 return baseConfigArray
;
326 let configArray
= configCache
.get(directoryPath
);
330 debug(`Cache hit: ${directoryPath}.`);
333 debug(`No cache found: ${directoryPath}.`);
335 const homePath
= os
.homedir();
337 // Consider this is root.
338 if (directoryPath
=== homePath
&& cwd
!== homePath
) {
339 debug("Stop traversing because of considered root.");
340 if (configsExistInSubdirs
) {
341 const filePath
= ConfigArrayFactory
.getPathToConfigFileInDirectory(directoryPath
);
344 emitDeprecationWarning(
346 "ESLINT_PERSONAL_CONFIG_SUPPRESS"
350 return this._cacheConfig(directoryPath
, baseConfigArray
);
353 // Load the config on this directory.
355 configArray
= configArrayFactory
.loadInDirectory(directoryPath
);
357 /* istanbul ignore next */
358 if (error
.code
=== "EACCES") {
359 debug("Stop traversing because of 'EACCES' error.");
360 return this._cacheConfig(directoryPath
, baseConfigArray
);
365 if (configArray
.length
> 0 && configArray
.isRoot()) {
366 debug("Stop traversing because of 'root:true'.");
367 configArray
.unshift(...baseConfigArray
);
368 return this._cacheConfig(directoryPath
, configArray
);
371 // Load from the ancestors and merge it.
372 const parentPath
= path
.dirname(directoryPath
);
373 const parentConfigArray
= parentPath
&& parentPath
!== directoryPath
374 ? this._loadConfigInAncestors(
376 configsExistInSubdirs
|| configArray
.length
> 0
380 if (configArray
.length
> 0) {
381 configArray
.unshift(...parentConfigArray
);
383 configArray
= parentConfigArray
;
387 return this._cacheConfig(directoryPath
, configArray
);
391 * Freeze and cache a given config.
392 * @param {string} directoryPath The path to a directory as a cache key.
393 * @param {ConfigArray} configArray The config array as a cache value.
394 * @returns {ConfigArray} The `configArray` (frozen).
396 _cacheConfig(directoryPath
, configArray
) {
397 const { configCache
} = internalSlotsMap
.get(this);
399 Object
.freeze(configArray
);
400 configCache
.set(directoryPath
, configArray
);
406 * Finalize a given config array.
407 * Concatenate `--config` and other CLI options.
408 * @param {ConfigArray} configArray The parent config array.
409 * @param {string} directoryPath The path to the leaf directory to find config files.
410 * @param {boolean} ignoreNotFoundError If `true` then it doesn't throw `ConfigurationNotFoundError`.
411 * @returns {ConfigArray} The loaded config.
414 _finalizeConfigArray(configArray
, directoryPath
, ignoreNotFoundError
) {
420 } = internalSlotsMap
.get(this);
422 let finalConfigArray
= finalizeCache
.get(configArray
);
424 if (!finalConfigArray
) {
425 finalConfigArray
= configArray
;
427 // Load the personal config if there are no regular config files.
430 configArray
.every(c
=> !c
.filePath
) &&
431 cliConfigArray
.every(c
=> !c
.filePath
) // `--config` option can be a file.
433 const homePath
= os
.homedir();
435 debug("Loading the config file of the home directory:", homePath
);
437 const personalConfigArray
= configArrayFactory
.loadInDirectory(
439 { name
: "PersonalConfig" }
443 personalConfigArray
.length
> 0 &&
444 !directoryPath
.startsWith(homePath
)
447 personalConfigArray
[personalConfigArray
.length
- 1];
449 emitDeprecationWarning(
450 lastElement
.filePath
,
451 "ESLINT_PERSONAL_CONFIG_LOAD"
455 finalConfigArray
= finalConfigArray
.concat(personalConfigArray
);
458 // Apply CLI options.
459 if (cliConfigArray
.length
> 0) {
460 finalConfigArray
= finalConfigArray
.concat(cliConfigArray
);
463 // Validate rule settings and environments.
464 validateConfigArray(finalConfigArray
);
467 Object
.freeze(finalConfigArray
);
468 finalizeCache
.set(configArray
, finalConfigArray
);
471 "Configuration was determined: %o on %s",
477 // At least one element (the default ignore patterns) exists.
478 if (!ignoreNotFoundError
&& useEslintrc
&& finalConfigArray
.length
<= 1) {
479 throw new ConfigurationNotFoundError(directoryPath
);
482 return finalConfigArray
;
486 //------------------------------------------------------------------------------
488 //------------------------------------------------------------------------------
490 module
.exports
= { CascadingConfigArrayFactory
};