/** | |
* @fileoverview `CascadingConfigArrayFactory` class. | |
* | |
* `CascadingConfigArrayFactory` class has a responsibility: | |
* | |
* 1. Handles cascading of config files. | |
* | |
* It provides two methods: | |
* | |
* - `getConfigArrayForFile(filePath)` | |
* Get the corresponded configuration of a given file. This method doesn't | |
* throw even if the given file didn't exist. | |
* - `clearCache()` | |
* Clear the internal cache. You have to call this method when | |
* `additionalPluginPool` was updated if `baseConfig` or `cliConfig` depends | |
* on the additional plugins. (`CLIEngine#addPlugin()` method calls this.) | |
* | |
* @author Toru Nagashima <https://github.com/mysticatea> | |
*/ | |
//------------------------------------------------------------------------------ | |
// Requirements | |
//------------------------------------------------------------------------------ | |
import debugOrig from "debug"; | |
import os from "os"; | |
import path from "path"; | |
import { ConfigArrayFactory } from "./config-array-factory.js"; | |
import { | |
ConfigArray, | |
ConfigDependency, | |
IgnorePattern | |
} from "./config-array/index.js"; | |
import ConfigValidator from "./shared/config-validator.js"; | |
import { emitDeprecationWarning } from "./shared/deprecation-warnings.js"; | |
const debug = debugOrig("eslintrc:cascading-config-array-factory"); | |
//------------------------------------------------------------------------------ | |
// Helpers | |
//------------------------------------------------------------------------------ | |
// Define types for VSCode IntelliSense. | |
/** @typedef {import("./shared/types").ConfigData} ConfigData */ | |
/** @typedef {import("./shared/types").Parser} Parser */ | |
/** @typedef {import("./shared/types").Plugin} Plugin */ | |
/** @typedef {import("./shared/types").Rule} Rule */ | |
/** @typedef {ReturnType<ConfigArrayFactory["create"]>} ConfigArray */ | |
/** | |
* @typedef {Object} CascadingConfigArrayFactoryOptions | |
* @property {Map<string,Plugin>} [additionalPluginPool] The map for additional plugins. | |
* @property {ConfigData} [baseConfig] The config by `baseConfig` option. | |
* @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. | |
* @property {string} [cwd] The base directory to start lookup. | |
* @property {string} [ignorePath] The path to the alternative file of `.eslintignore`. | |
* @property {string[]} [rulePaths] The value of `--rulesdir` option. | |
* @property {string} [specificConfigPath] The value of `--config` option. | |
* @property {boolean} [useEslintrc] if `false` then it doesn't load config files. | |
* @property {Function} loadRules The function to use to load rules. | |
* @property {Map<string,Rule>} builtInRules The rules that are built in to ESLint. | |
* @property {Object} [resolver=ModuleResolver] The module resolver object. | |
* @property {string} eslintAllPath The path to the definitions for eslint:all. | |
* @property {Function} getEslintAllConfig Returns the config data for eslint:all. | |
* @property {string} eslintRecommendedPath The path to the definitions for eslint:recommended. | |
* @property {Function} getEslintRecommendedConfig Returns the config data for eslint:recommended. | |
*/ | |
/** | |
* @typedef {Object} CascadingConfigArrayFactoryInternalSlots | |
* @property {ConfigArray} baseConfigArray The config array of `baseConfig` option. | |
* @property {ConfigData} baseConfigData The config data of `baseConfig` option. This is used to reset `baseConfigArray`. | |
* @property {ConfigArray} cliConfigArray The config array of CLI options. | |
* @property {ConfigData} cliConfigData The config data of CLI options. This is used to reset `cliConfigArray`. | |
* @property {ConfigArrayFactory} configArrayFactory The factory for config arrays. | |
* @property {Map<string, ConfigArray>} configCache The cache from directory paths to config arrays. | |
* @property {string} cwd The base directory to start lookup. | |
* @property {WeakMap<ConfigArray, ConfigArray>} finalizeCache The cache from config arrays to finalized config arrays. | |
* @property {string} [ignorePath] The path to the alternative file of `.eslintignore`. | |
* @property {string[]|null} rulePaths The value of `--rulesdir` option. This is used to reset `baseConfigArray`. | |
* @property {string|null} specificConfigPath The value of `--config` option. This is used to reset `cliConfigArray`. | |
* @property {boolean} useEslintrc if `false` then it doesn't load config files. | |
* @property {Function} loadRules The function to use to load rules. | |
* @property {Map<string,Rule>} builtInRules The rules that are built in to ESLint. | |
* @property {Object} [resolver=ModuleResolver] The module resolver object. | |
* @property {string} eslintAllPath The path to the definitions for eslint:all. | |
* @property {Function} getEslintAllConfig Returns the config data for eslint:all. | |
* @property {string} eslintRecommendedPath The path to the definitions for eslint:recommended. | |
* @property {Function} getEslintRecommendedConfig Returns the config data for eslint:recommended. | |
*/ | |
/** @type {WeakMap<CascadingConfigArrayFactory, CascadingConfigArrayFactoryInternalSlots>} */ | |
const internalSlotsMap = new WeakMap(); | |
/** | |
* Create the config array from `baseConfig` and `rulePaths`. | |
* @param {CascadingConfigArrayFactoryInternalSlots} slots The slots. | |
* @returns {ConfigArray} The config array of the base configs. | |
*/ | |
function createBaseConfigArray({ | |
configArrayFactory, | |
baseConfigData, | |
rulePaths, | |
cwd, | |
loadRules | |
}) { | |
const baseConfigArray = configArrayFactory.create( | |
baseConfigData, | |
{ name: "BaseConfig" } | |
); | |
/* | |
* Create the config array element for the default ignore patterns. | |
* This element has `ignorePattern` property that ignores the default | |
* patterns in the current working directory. | |
*/ | |
baseConfigArray.unshift(configArrayFactory.create( | |
{ ignorePatterns: IgnorePattern.DefaultPatterns }, | |
{ name: "DefaultIgnorePattern" } | |
)[0]); | |
/* | |
* Load rules `--rulesdir` option as a pseudo plugin. | |
* Use a pseudo plugin to define rules of `--rulesdir`, so we can validate | |
* the rule's options with only information in the config array. | |
*/ | |
if (rulePaths && rulePaths.length > 0) { | |
baseConfigArray.push({ | |
type: "config", | |
name: "--rulesdir", | |
filePath: "", | |
plugins: { | |
"": new ConfigDependency({ | |
definition: { | |
rules: rulePaths.reduce( | |
(map, rulesPath) => Object.assign( | |
map, | |
loadRules(rulesPath, cwd) | |
), | |
{} | |
) | |
}, | |
filePath: "", | |
id: "", | |
importerName: "--rulesdir", | |
importerPath: "" | |
}) | |
} | |
}); | |
} | |
return baseConfigArray; | |
} | |
/** | |
* Create the config array from CLI options. | |
* @param {CascadingConfigArrayFactoryInternalSlots} slots The slots. | |
* @returns {ConfigArray} The config array of the base configs. | |
*/ | |
function createCLIConfigArray({ | |
cliConfigData, | |
configArrayFactory, | |
cwd, | |
ignorePath, | |
specificConfigPath | |
}) { | |
const cliConfigArray = configArrayFactory.create( | |
cliConfigData, | |
{ name: "CLIOptions" } | |
); | |
cliConfigArray.unshift( | |
...(ignorePath | |
? configArrayFactory.loadESLintIgnore(ignorePath) | |
: configArrayFactory.loadDefaultESLintIgnore()) | |
); | |
if (specificConfigPath) { | |
cliConfigArray.unshift( | |
...configArrayFactory.loadFile( | |
specificConfigPath, | |
{ name: "--config", basePath: cwd } | |
) | |
); | |
} | |
return cliConfigArray; | |
} | |
/** | |
* The error type when there are files matched by a glob, but all of them have been ignored. | |
*/ | |
class ConfigurationNotFoundError extends Error { | |
// eslint-disable-next-line jsdoc/require-description | |
/** | |
* @param {string} directoryPath The directory path. | |
*/ | |
constructor(directoryPath) { | |
super(`No ESLint configuration found in ${directoryPath}.`); | |
this.messageTemplate = "no-config-found"; | |
this.messageData = { directoryPath }; | |
} | |
} | |
/** | |
* This class provides the functionality that enumerates every file which is | |
* matched by given glob patterns and that configuration. | |
*/ | |
class CascadingConfigArrayFactory { | |
/** | |
* Initialize this enumerator. | |
* @param {CascadingConfigArrayFactoryOptions} options The options. | |
*/ | |
constructor({ | |
additionalPluginPool = new Map(), | |
baseConfig: baseConfigData = null, | |
cliConfig: cliConfigData = null, | |
cwd = process.cwd(), | |
ignorePath, | |
resolvePluginsRelativeTo, | |
rulePaths = [], | |
specificConfigPath = null, | |
useEslintrc = true, | |
builtInRules = new Map(), | |
loadRules, | |
resolver, | |
eslintRecommendedPath, | |
getEslintRecommendedConfig, | |
eslintAllPath, | |
getEslintAllConfig | |
} = {}) { | |
const configArrayFactory = new ConfigArrayFactory({ | |
additionalPluginPool, | |
cwd, | |
resolvePluginsRelativeTo, | |
builtInRules, | |
resolver, | |
eslintRecommendedPath, | |
getEslintRecommendedConfig, | |
eslintAllPath, | |
getEslintAllConfig | |
}); | |
internalSlotsMap.set(this, { | |
baseConfigArray: createBaseConfigArray({ | |
baseConfigData, | |
configArrayFactory, | |
cwd, | |
rulePaths, | |
loadRules | |
}), | |
baseConfigData, | |
cliConfigArray: createCLIConfigArray({ | |
cliConfigData, | |
configArrayFactory, | |
cwd, | |
ignorePath, | |
specificConfigPath | |
}), | |
cliConfigData, | |
configArrayFactory, | |
configCache: new Map(), | |
cwd, | |
finalizeCache: new WeakMap(), | |
ignorePath, | |
rulePaths, | |
specificConfigPath, | |
useEslintrc, | |
builtInRules, | |
loadRules | |
}); | |
} | |
/** | |
* The path to the current working directory. | |
* This is used by tests. | |
* @type {string} | |
*/ | |
get cwd() { | |
const { cwd } = internalSlotsMap.get(this); | |
return cwd; | |
} | |
/** | |
* Get the config array of a given file. | |
* If `filePath` was not given, it returns the config which contains only | |
* `baseConfigData` and `cliConfigData`. | |
* @param {string} [filePath] The file path to a file. | |
* @param {Object} [options] The options. | |
* @param {boolean} [options.ignoreNotFoundError] If `true` then it doesn't throw `ConfigurationNotFoundError`. | |
* @returns {ConfigArray} The config array of the file. | |
*/ | |
getConfigArrayForFile(filePath, { ignoreNotFoundError = false } = {}) { | |
const { | |
baseConfigArray, | |
cliConfigArray, | |
cwd | |
} = internalSlotsMap.get(this); | |
if (!filePath) { | |
return new ConfigArray(...baseConfigArray, ...cliConfigArray); | |
} | |
const directoryPath = path.dirname(path.resolve(cwd, filePath)); | |
debug(`Load config files for ${directoryPath}.`); | |
return this._finalizeConfigArray( | |
this._loadConfigInAncestors(directoryPath), | |
directoryPath, | |
ignoreNotFoundError | |
); | |
} | |
/** | |
* Set the config data to override all configs. | |
* Require to call `clearCache()` method after this method is called. | |
* @param {ConfigData} configData The config data to override all configs. | |
* @returns {void} | |
*/ | |
setOverrideConfig(configData) { | |
const slots = internalSlotsMap.get(this); | |
slots.cliConfigData = configData; | |
} | |
/** | |
* Clear config cache. | |
* @returns {void} | |
*/ | |
clearCache() { | |
const slots = internalSlotsMap.get(this); | |
slots.baseConfigArray = createBaseConfigArray(slots); | |
slots.cliConfigArray = createCLIConfigArray(slots); | |
slots.configCache.clear(); | |
} | |
/** | |
* Load and normalize config files from the ancestor directories. | |
* @param {string} directoryPath The path to a leaf directory. | |
* @param {boolean} configsExistInSubdirs `true` if configurations exist in subdirectories. | |
* @returns {ConfigArray} The loaded config. | |
* @private | |
*/ | |
_loadConfigInAncestors(directoryPath, configsExistInSubdirs = false) { | |
const { | |
baseConfigArray, | |
configArrayFactory, | |
configCache, | |
cwd, | |
useEslintrc | |
} = internalSlotsMap.get(this); | |
if (!useEslintrc) { | |
return baseConfigArray; | |
} | |
let configArray = configCache.get(directoryPath); | |
// Hit cache. | |
if (configArray) { | |
debug(`Cache hit: ${directoryPath}.`); | |
return configArray; | |
} | |
debug(`No cache found: ${directoryPath}.`); | |
const homePath = os.homedir(); | |
// Consider this is root. | |
if (directoryPath === homePath && cwd !== homePath) { | |
debug("Stop traversing because of considered root."); | |
if (configsExistInSubdirs) { | |
const filePath = ConfigArrayFactory.getPathToConfigFileInDirectory(directoryPath); | |
if (filePath) { | |
emitDeprecationWarning( | |
filePath, | |
"ESLINT_PERSONAL_CONFIG_SUPPRESS" | |
); | |
} | |
} | |
return this._cacheConfig(directoryPath, baseConfigArray); | |
} | |
// Load the config on this directory. | |
try { | |
configArray = configArrayFactory.loadInDirectory(directoryPath); | |
} catch (error) { | |
/* istanbul ignore next */ | |
if (error.code === "EACCES") { | |
debug("Stop traversing because of 'EACCES' error."); | |
return this._cacheConfig(directoryPath, baseConfigArray); | |
} | |
throw error; | |
} | |
if (configArray.length > 0 && configArray.isRoot()) { | |
debug("Stop traversing because of 'root:true'."); | |
configArray.unshift(...baseConfigArray); | |
return this._cacheConfig(directoryPath, configArray); | |
} | |
// Load from the ancestors and merge it. | |
const parentPath = path.dirname(directoryPath); | |
const parentConfigArray = parentPath && parentPath !== directoryPath | |
? this._loadConfigInAncestors( | |
parentPath, | |
configsExistInSubdirs || configArray.length > 0 | |
) | |
: baseConfigArray; | |
if (configArray.length > 0) { | |
configArray.unshift(...parentConfigArray); | |
} else { | |
configArray = parentConfigArray; | |
} | |
// Cache and return. | |
return this._cacheConfig(directoryPath, configArray); | |
} | |
/** | |
* Freeze and cache a given config. | |
* @param {string} directoryPath The path to a directory as a cache key. | |
* @param {ConfigArray} configArray The config array as a cache value. | |
* @returns {ConfigArray} The `configArray` (frozen). | |
*/ | |
_cacheConfig(directoryPath, configArray) { | |
const { configCache } = internalSlotsMap.get(this); | |
Object.freeze(configArray); | |
configCache.set(directoryPath, configArray); | |
return configArray; | |
} | |
/** | |
* Finalize a given config array. | |
* Concatenate `--config` and other CLI options. | |
* @param {ConfigArray} configArray The parent config array. | |
* @param {string} directoryPath The path to the leaf directory to find config files. | |
* @param {boolean} ignoreNotFoundError If `true` then it doesn't throw `ConfigurationNotFoundError`. | |
* @returns {ConfigArray} The loaded config. | |
* @private | |
*/ | |
_finalizeConfigArray(configArray, directoryPath, ignoreNotFoundError) { | |
const { | |
cliConfigArray, | |
configArrayFactory, | |
finalizeCache, | |
useEslintrc, | |
builtInRules | |
} = internalSlotsMap.get(this); | |
let finalConfigArray = finalizeCache.get(configArray); | |
if (!finalConfigArray) { | |
finalConfigArray = configArray; | |
// Load the personal config if there are no regular config files. | |
if ( | |
useEslintrc && | |
configArray.every(c => !c.filePath) && | |
cliConfigArray.every(c => !c.filePath) // `--config` option can be a file. | |
) { | |
const homePath = os.homedir(); | |
debug("Loading the config file of the home directory:", homePath); | |
const personalConfigArray = configArrayFactory.loadInDirectory( | |
homePath, | |
{ name: "PersonalConfig" } | |
); | |
if ( | |
personalConfigArray.length > 0 && | |
!directoryPath.startsWith(homePath) | |
) { | |
const lastElement = | |
personalConfigArray[personalConfigArray.length - 1]; | |
emitDeprecationWarning( | |
lastElement.filePath, | |
"ESLINT_PERSONAL_CONFIG_LOAD" | |
); | |
} | |
finalConfigArray = finalConfigArray.concat(personalConfigArray); | |
} | |
// Apply CLI options. | |
if (cliConfigArray.length > 0) { | |
finalConfigArray = finalConfigArray.concat(cliConfigArray); | |
} | |
// Validate rule settings and environments. | |
const validator = new ConfigValidator({ | |
builtInRules | |
}); | |
validator.validateConfigArray(finalConfigArray); | |
// Cache it. | |
Object.freeze(finalConfigArray); | |
finalizeCache.set(configArray, finalConfigArray); | |
debug( | |
"Configuration was determined: %o on %s", | |
finalConfigArray, | |
directoryPath | |
); | |
} | |
// At least one element (the default ignore patterns) exists. | |
if (!ignoreNotFoundError && useEslintrc && finalConfigArray.length <= 1) { | |
throw new ConfigurationNotFoundError(directoryPath); | |
} | |
return finalConfigArray; | |
} | |
} | |
//------------------------------------------------------------------------------ | |
// Public Interface | |
//------------------------------------------------------------------------------ | |
export { CascadingConfigArrayFactory }; | |