| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364 | /** * @fileoverview Responsible for loading config files * @author Seth McLaughlin */"use strict";//------------------------------------------------------------------------------// Requirements//------------------------------------------------------------------------------const path = require("path"),    os = require("os"),    ConfigOps = require("./config/config-ops"),    ConfigFile = require("./config/config-file"),    ConfigCache = require("./config/config-cache"),    Plugins = require("./config/plugins"),    FileFinder = require("./file-finder"),    isResolvable = require("is-resolvable");const debug = require("debug")("eslint:config");//------------------------------------------------------------------------------// Constants//------------------------------------------------------------------------------const PERSONAL_CONFIG_DIR = os.homedir();const SUBCONFIG_SEP = ":";//------------------------------------------------------------------------------// Helpers//------------------------------------------------------------------------------/** * Determines if any rules were explicitly passed in as options. * @param {Object} options The options used to create our configuration. * @returns {boolean} True if rules were passed in as options, false otherwise. * @private */function hasRules(options) {    return options.rules && Object.keys(options.rules).length > 0;}//------------------------------------------------------------------------------// API//------------------------------------------------------------------------------/** * Configuration class */class Config {    /**     * @param {Object} providedOptions Options to be passed in     * @param {Linter} linterContext Linter instance object     */    constructor(providedOptions, linterContext) {        const options = providedOptions || {};        this.linterContext = linterContext;        this.plugins = new Plugins(linterContext.environments, linterContext.rules);        this.options = options;        this.ignore = options.ignore;        this.ignorePath = options.ignorePath;        this.parser = options.parser;        this.parserOptions = options.parserOptions || {};        this.configCache = new ConfigCache();        this.baseConfig = options.baseConfig            ? ConfigOps.merge({}, ConfigFile.loadObject(options.baseConfig, this))            : { rules: {} };        this.baseConfig.filePath = "";        this.baseConfig.baseDirectory = this.options.cwd;        this.configCache.setConfig(this.baseConfig.filePath, this.baseConfig);        this.configCache.setMergedVectorConfig(this.baseConfig.filePath, this.baseConfig);        this.useEslintrc = (options.useEslintrc !== false);        this.env = (options.envs || []).reduce((envs, name) => {            envs[name] = true;            return envs;        }, {});        /*         * Handle declared globals.         * For global variable foo, handle "foo:false" and "foo:true" to set         * whether global is writable.         * If user declares "foo", convert to "foo:false".         */        this.globals = (options.globals || []).reduce((globals, def) => {            const parts = def.split(SUBCONFIG_SEP);            globals[parts[0]] = (parts.length > 1 && parts[1] === "true");            return globals;        }, {});        this.loadSpecificConfig(options.configFile);        // Empty values in configs don't merge properly        const cliConfigOptions = {            env: this.env,            rules: this.options.rules,            globals: this.globals,            parserOptions: this.parserOptions,            plugins: this.options.plugins        };        this.cliConfig = {};        Object.keys(cliConfigOptions).forEach(configKey => {            const value = cliConfigOptions[configKey];            if (value) {                this.cliConfig[configKey] = value;            }        });    }    /**     * Loads the config options from a config specified on the command line.     * @param {string} [config] A shareable named config or path to a config file.     * @returns {void}     */    loadSpecificConfig(config) {        if (config) {            debug(`Using command line config ${config}`);            const isNamedConfig =                isResolvable(config) ||                isResolvable(`eslint-config-${config}`) ||                config.charAt(0) === "@";            this.specificConfig = ConfigFile.load(                isNamedConfig ? config : path.resolve(this.options.cwd, config),                this            );        }    }    /**     * Gets the personal config object from user's home directory.     * @returns {Object} the personal config object (null if there is no personal config)     * @private     */    getPersonalConfig() {        if (typeof this.personalConfig === "undefined") {            let config;            const filename = ConfigFile.getFilenameForDirectory(PERSONAL_CONFIG_DIR);            if (filename) {                debug("Using personal config");                config = ConfigFile.load(filename, this);            }            this.personalConfig = config || null;        }        return this.personalConfig;    }    /**     * Builds a hierarchy of config objects, including the base config, all local configs from the directory tree,     * and a config file specified on the command line, if applicable.     * @param {string} directory a file in whose directory we start looking for a local config     * @returns {Object[]} The config objects, in ascending order of precedence     * @private     */    getConfigHierarchy(directory) {        debug(`Constructing config file hierarchy for ${directory}`);        // Step 1: Always include baseConfig        let configs = [this.baseConfig];        // Step 2: Add user-specified config from .eslintrc.* and package.json files        if (this.useEslintrc) {            debug("Using .eslintrc and package.json files");            configs = configs.concat(this.getLocalConfigHierarchy(directory));        } else {            debug("Not using .eslintrc or package.json files");        }        // Step 3: Merge in command line config file        if (this.specificConfig) {            debug("Using command line config file");            configs.push(this.specificConfig);        }        return configs;    }    /**     * Gets a list of config objects extracted from local config files that apply to the current directory, in     * descending order, beginning with the config that is highest in the directory tree.     * @param {string} directory The directory to start looking in for local config files.     * @returns {Object[]} The shallow local config objects, in ascending order of precedence (closest to the current     * directory at the end), or an empty array if there are no local configs.     * @private     */    getLocalConfigHierarchy(directory) {        const localConfigFiles = this.findLocalConfigFiles(directory),            projectConfigPath = ConfigFile.getFilenameForDirectory(this.options.cwd),            searched = [],            configs = [];        for (const localConfigFile of localConfigFiles) {            const localConfigDirectory = path.dirname(localConfigFile);            const localConfigHierarchyCache = this.configCache.getHierarchyLocalConfigs(localConfigDirectory);            if (localConfigHierarchyCache) {                const localConfigHierarchy = localConfigHierarchyCache.concat(configs.reverse());                this.configCache.setHierarchyLocalConfigs(searched, localConfigHierarchy);                return localConfigHierarchy;            }            /*             * Don't consider the personal config file in the home directory,             * except if the home directory is the same as the current working directory             */            if (localConfigDirectory === PERSONAL_CONFIG_DIR && localConfigFile !== projectConfigPath) {                continue;            }            debug(`Loading ${localConfigFile}`);            const localConfig = ConfigFile.load(localConfigFile, this);            // Ignore empty config files            if (!localConfig) {                continue;            }            debug(`Using ${localConfigFile}`);            configs.push(localConfig);            searched.push(localConfigDirectory);            // Stop traversing if a config is found with the root flag set            if (localConfig.root) {                break;            }        }        if (!configs.length && !this.specificConfig) {            // Fall back on the personal config from ~/.eslintrc            debug("Using personal config file");            const personalConfig = this.getPersonalConfig();            if (personalConfig) {                configs.push(personalConfig);            } else if (!hasRules(this.options) && !this.options.baseConfig) {                // No config file, no manual configuration, and no rules, so error.                const noConfigError = new Error("No ESLint configuration found.");                noConfigError.messageTemplate = "no-config-found";                noConfigError.messageData = {                    directory,                    filesExamined: localConfigFiles                };                throw noConfigError;            }        }        // Set the caches for the parent directories        this.configCache.setHierarchyLocalConfigs(searched, configs.reverse());        return configs;    }    /**     * Gets the vector of applicable configs and subconfigs from the hierarchy for a given file. A vector is an array of     * entries, each of which in an object specifying a config file path and an array of override indices corresponding     * to entries in the config file's overrides section whose glob patterns match the specified file path; e.g., the     * vector entry { configFile: '/home/john/app/.eslintrc', matchingOverrides: [0, 2] } would indicate that the main     * project .eslintrc file and its first and third override blocks apply to the current file.     * @param {string} filePath The file path for which to build the hierarchy and config vector.     * @returns {Array<Object>} config vector applicable to the specified path     * @private     */    getConfigVector(filePath) {        const directory = filePath ? path.dirname(filePath) : this.options.cwd;        return this.getConfigHierarchy(directory).map(config => {            const vectorEntry = {                filePath: config.filePath,                matchingOverrides: []            };            if (config.overrides) {                const relativePath = path.relative(config.baseDirectory, filePath || directory);                config.overrides.forEach((override, i) => {                    if (ConfigOps.pathMatchesGlobs(relativePath, override.files, override.excludedFiles)) {                        vectorEntry.matchingOverrides.push(i);                    }                });            }            return vectorEntry;        });    }    /**     * Finds local config files from the specified directory and its parent directories.     * @param {string} directory The directory to start searching from.     * @returns {GeneratorFunction} The paths of local config files found.     */    findLocalConfigFiles(directory) {        if (!this.localConfigFinder) {            this.localConfigFinder = new FileFinder(ConfigFile.CONFIG_FILES, this.options.cwd);        }        return this.localConfigFinder.findAllInDirectoryAndParents(directory);    }    /**     * Builds the authoritative config object for the specified file path by merging the hierarchy of config objects     * that apply to the current file, including the base config (conf/eslint-recommended), the user's personal config     * from their homedir, all local configs from the directory tree, any specific config file passed on the command     * line, any configuration overrides set directly on the command line, and finally the environment configs     * (conf/environments).     * @param {string} filePath a file in whose directory we start looking for a local config     * @returns {Object} config object     */    getConfig(filePath) {        const vector = this.getConfigVector(filePath);        let config = this.configCache.getMergedConfig(vector);        if (config) {            debug("Using config from cache");            return config;        }        // Step 1: Merge in the filesystem configurations (base, local, and personal)        config = ConfigOps.getConfigFromVector(vector, this.configCache);        // Step 2: Merge in command line configurations        config = ConfigOps.merge(config, this.cliConfig);        if (this.cliConfig.plugins) {            this.plugins.loadAll(this.cliConfig.plugins);        }        /*         * Step 3: Override parser only if it is passed explicitly through the command line         * or if it's not defined yet (because the final object will at least have the parser key)         */        if (this.parser || !config.parser) {            config = ConfigOps.merge(config, { parser: this.parser });        }        // Step 4: Apply environments to the config        config = ConfigOps.applyEnvironments(config, this.linterContext.environments);        this.configCache.setMergedConfig(vector, config);        return config;    }}module.exports = Config;
 |