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