diff options
Diffstat (limited to 'tools/node_modules/eslint/lib/config.js')
-rw-r--r-- | tools/node_modules/eslint/lib/config.js | 365 |
1 files changed, 365 insertions, 0 deletions
diff --git a/tools/node_modules/eslint/lib/config.js b/tools/node_modules/eslint/lib/config.js new file mode 100644 index 0000000000..b66b9f41e0 --- /dev/null +++ b/tools/node_modules/eslint/lib/config.js @@ -0,0 +1,365 @@ +/** + * @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} options Options to be passed in + * @param {Linter} linterContext Linter instance object + */ + constructor(options, linterContext) { + options = options || {}; + + 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) === "@"; + + if (!isNamedConfig) { + config = path.resolve(this.options.cwd, config); + } + + this.specificConfig = ConfigFile.load(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; |