533 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			533 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /**
 | |
|  * @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 };
 |