123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532 |
- /**
- * @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 };
|