|
|
/** * @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>
*/ "use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const os = require("os"); const path = require("path"); const { validateConfigArray } = require("../shared/config-validator"); const { emitDeprecationWarning } = require("../shared/deprecation-warnings"); const { ConfigArrayFactory } = require("./config-array-factory"); const { ConfigArray, ConfigDependency, IgnorePattern } = require("./config-array"); const loadRules = require("./load-rules"); const debug = require("debug")("eslint: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 {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. */
/** * @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. */
/** @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 }) { 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 } = {}) { const configArrayFactory = new ConfigArrayFactory({ additionalPluginPool, cwd, resolvePluginsRelativeTo });
internalSlotsMap.set(this, { baseConfigArray: createBaseConfigArray({ baseConfigData, configArrayFactory, cwd, rulePaths }), baseConfigData, cliConfigArray: createCLIConfigArray({ cliConfigData, configArrayFactory, cwd, ignorePath, specificConfigPath }), cliConfigData, configArrayFactory, configCache: new Map(), cwd, finalizeCache: new WeakMap(), ignorePath, rulePaths, specificConfigPath, useEslintrc }); }
/** * 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 } = 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.
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
//------------------------------------------------------------------------------
module.exports = { CascadingConfigArrayFactory };
|