diff options
Diffstat (limited to 'tools/eslint/lib/config.js')
-rw-r--r-- | tools/eslint/lib/config.js | 440 |
1 files changed, 232 insertions, 208 deletions
diff --git a/tools/eslint/lib/config.js b/tools/eslint/lib/config.js index 5407134d6f..c69d120ef7 100644 --- a/tools/eslint/lib/config.js +++ b/tools/eslint/lib/config.js @@ -13,6 +13,7 @@ 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"); @@ -24,149 +25,22 @@ const debug = require("debug")("eslint:config"); //------------------------------------------------------------------------------ const PERSONAL_CONFIG_DIR = os.homedir() || null; +const SUBCONFIG_SEP = ":"; //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ /** - * Check if item is an javascript object - * @param {*} item object to check for - * @returns {boolean} True if its an object - * @private - */ -function isObject(item) { - return typeof item === "object" && !Array.isArray(item) && item !== null; -} - -/** - * Load and parse a JSON config object from a file. - * @param {string|Object} configToLoad the path to the JSON config file or the config object itself. - * @param {Config} configContext config instance object - * @returns {Object} the parsed config object (empty object if there was a parse error) - * @private - */ -function loadConfig(configToLoad, configContext) { - let config = {}, - filePath = ""; - - if (configToLoad) { - - if (isObject(configToLoad)) { - config = configToLoad; - - if (config.extends) { - config = ConfigFile.applyExtends(config, configContext, filePath); - } - } else { - filePath = configToLoad; - config = ConfigFile.load(filePath, configContext); - } - - } - return config; -} - -/** - * Get personal config object from ~/.eslintrc. - * @param {Config} configContext Plugin context for the config instance - * @returns {Object} the personal config object (null if there is no personal config) - * @private - */ -function getPersonalConfig(configContext) { - let config; - - if (PERSONAL_CONFIG_DIR) { - - const filename = ConfigFile.getFilenameForDirectory(PERSONAL_CONFIG_DIR); - - if (filename) { - debug("Using personal config"); - config = loadConfig(filename, configContext); - } - } - - return config || null; -} - -/** - * Determine if rules were explicitly passed in as options. + * 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; } -/** - * Get a local config object. - * @param {Config} thisConfig A Config object. - * @param {string} directory The directory to start looking in for a local config file. - * @returns {Object} The local config object, or an empty object if there is no local config. - */ -function getLocalConfig(thisConfig, directory) { - - const projectConfigPath = ConfigFile.getFilenameForDirectory(thisConfig.options.cwd); - const localConfigFiles = thisConfig.findLocalConfigFiles(directory); - let found, - config = {}; - - for (const localConfigFile of localConfigFiles) { - - // 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 (path.dirname(localConfigFile) === PERSONAL_CONFIG_DIR && localConfigFile !== projectConfigPath) { - continue; - } - - debug(`Loading ${localConfigFile}`); - const localConfig = loadConfig(localConfigFile, thisConfig); - - // Don't consider a local config file found if the config is null - if (!localConfig) { - continue; - } - - found = true; - debug(`Using ${localConfigFile}`); - config = ConfigOps.merge(localConfig, config); - - // Check for root flag - if (localConfig.root === true) { - break; - } - } - - if (!found && !thisConfig.useSpecificConfig) { - - /* - * - Is there a personal config in the user's home directory? If so, - * merge that with the passed-in config. - * - Otherwise, if no rules were manually passed in, throw and error. - * - Note: This function is not called if useEslintrc is false. - */ - const personalConfig = getPersonalConfig(thisConfig); - - if (personalConfig) { - config = ConfigOps.merge(config, personalConfig); - } else if (!hasRules(thisConfig.options) && !thisConfig.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; - } - } - - return config; -} - //------------------------------------------------------------------------------ // API //------------------------------------------------------------------------------ @@ -177,7 +51,6 @@ function getLocalConfig(thisConfig, directory) { class Config { /** - * Config options * @param {Object} options Options to be passed in * @param {Linter} linterContext Linter instance object */ @@ -187,18 +60,26 @@ class Config { this.linterContext = linterContext; this.plugins = new Plugins(linterContext.environments, linterContext.rules); + this.options = options; this.ignore = options.ignore; this.ignorePath = options.ignorePath; - this.cache = {}; this.parser = options.parser; this.parserOptions = options.parserOptions || {}; - this.baseConfig = options.baseConfig ? loadConfig(options.baseConfig, this) : { rules: {} }; + this.baseConfig = options.baseConfig + ? ConfigOps.merge({}, ConfigFile.loadObject(options.baseConfig, this)) + : { rules: {} }; + this.baseConfig.filePath = ""; + this.baseConfig.baseDirectory = this.options.cwd; + + this.configCache = new ConfigCache(); + 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; + envs[name] = true; return envs; }, {}); @@ -209,132 +90,275 @@ class Config { * If user declares "foo", convert to "foo:false". */ this.globals = (options.globals || []).reduce((globals, def) => { - const parts = def.split(":"); + const parts = def.split(SUBCONFIG_SEP); globals[parts[0]] = (parts.length > 1 && parts[1] === "true"); return globals; }, {}); - this.options = options; - this.loadConfigFile(options.configFile); + 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 from the configuration file - * @param {string} configFile - patch to the config file - * @returns {undefined} - */ - loadConfigFile(configFile) { - if (configFile) { - debug(`Using command line config ${configFile}`); - if (isResolvable(configFile) || isResolvable(`eslint-config-${configFile}`) || configFile.charAt(0) === "@") { - this.useSpecificConfig = loadConfig(configFile, this); - } else { - this.useSpecificConfig = loadConfig(path.resolve(this.options.cwd, configFile), this); + * 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) === "@"; + + if (!isNamedConfig) { + config = path.resolve(this.options.cwd, config); } + + this.specificConfig = ConfigFile.load(config, this); } } /** - * Build a config object merging the base config (conf/eslint-recommended), - * the environments config (conf/environments.js) and eventually the user - * config. - * @param {string} filePath a file in whose directory we start looking for a local config - * @returns {Object} config object + * Gets the personal config object from user's home directory. + * @returns {Object} the personal config object (null if there is no personal config) + * @private */ - getConfig(filePath) { - const directory = filePath ? path.dirname(filePath) : this.options.cwd; - let config, - userConfig; + getPersonalConfig() { + if (typeof this.personalConfig === "undefined") { + let config; - debug(`Constructing config for ${filePath ? filePath : "text"}`); + if (PERSONAL_CONFIG_DIR) { + const filename = ConfigFile.getFilenameForDirectory(PERSONAL_CONFIG_DIR); - config = this.cache[directory]; - if (config) { - debug("Using config from cache"); - return config; + if (filename) { + debug("Using personal config"); + config = ConfigFile.load(filename, this); + } + } + this.personalConfig = config || null; } - // Step 1: Determine user-specified config from .eslintrc.* and package.json files + 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"); - userConfig = getLocalConfig(this, directory); + configs = configs.concat(this.getLocalConfigHierarchy(directory)); } else { debug("Not using .eslintrc or package.json files"); - userConfig = {}; } - // Step 2: Create a copy of the baseConfig - config = ConfigOps.merge({}, this.baseConfig); + // Step 3: Merge in command line config file + if (this.specificConfig) { + debug("Using command line config file"); + configs.push(this.specificConfig); + } - // Step 3: Merge in the user-specified configuration from .eslintrc and package.json - config = ConfigOps.merge(config, userConfig); + return configs; + } - // Step 4: Merge in command line config file - if (this.useSpecificConfig) { - debug("Merging command line config file"); + /** + * 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 = []; - config = ConfigOps.merge(config, this.useSpecificConfig); - } + for (const localConfigFile of localConfigFiles) { + const localConfigDirectory = path.dirname(localConfigFile); + const localConfigHierarchyCache = this.configCache.getHierarchyLocalConfigs(localConfigDirectory); - // Step 5: Merge in command line environments - debug("Merging command line environment settings"); - config = ConfigOps.merge(config, { env: this.env }); + if (localConfigHierarchyCache) { + const localConfigHierarchy = localConfigHierarchyCache.concat(configs.reverse()); - // Step 6: Merge in command line rules - if (this.options.rules) { - debug("Merging command line rules"); - config = ConfigOps.merge(config, { rules: this.options.rules }); - } + 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; + } - // Step 7: Merge in command line globals - config = ConfigOps.merge(config, { globals: this.globals }); + debug(`Loading ${localConfigFile}`); + const localConfig = ConfigFile.load(localConfigFile, this); - // Only override parser 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 - }); - } + // Ignore empty config files + if (!localConfig) { + continue; + } - if (this.parserOptions) { - config = ConfigOps.merge(config, { - parserOptions: this.parserOptions - }); - } + debug(`Using ${localConfigFile}`); + configs.push(localConfig); + searched.push(localConfigDirectory); - // Step 8: Merge in command line plugins - if (this.options.plugins) { - debug("Merging command line plugins"); - this.plugins.loadAll(this.options.plugins); - config = ConfigOps.merge(config, { plugins: this.options.plugins }); + // Stop traversing if a config is found with the root flag set + if (localConfig.root) { + break; + } } - // Step 9: Apply environments to the config if present - if (config.env) { - config = ConfigOps.applyEnvironments(config, this.linterContext.environments); + 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; + } } - this.cache[directory] = config; + // Set the caches for the parent directories + this.configCache.setHierarchyLocalConfigs(searched, configs.reverse()); - return config; + return configs; } /** - * Find local config files from directory and parent directories. + * 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 if present + if (config.env) { + config = ConfigOps.applyEnvironments(config, this.linterContext.environments); + } + + this.configCache.setMergedConfig(vector, config); + + return config; + } } module.exports = Config; |