| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519 | /** * @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 ConfigValidator = require("./shared/config-validator");const { emitDeprecationWarning } = require("./shared/deprecation-warnings");const { ConfigArrayFactory } = require("./config-array-factory");const { ConfigArray, ConfigDependency, IgnorePattern } = require("./config-array");const debug = require("debug")("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 {string} eslintRecommendedPath The path to the definitions 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 {string} eslintRecommendedPath The path to the definitions 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,        eslintAllPath    } = {}) {        const configArrayFactory = new ConfigArrayFactory({            additionalPluginPool,            cwd,            resolvePluginsRelativeTo,            builtInRules,            resolver,            eslintRecommendedPath,            eslintAllPath        });        internalSlotsMap.set(this, {            baseConfigArray: createBaseConfigArray({                baseConfigData,                configArrayFactory,                cwd,                rulePaths,                loadRules,                resolver            }),            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//------------------------------------------------------------------------------module.exports = { CascadingConfigArrayFactory };
 |