summaryrefslogtreecommitdiff
path: root/tools/eslint/lib/config.js
diff options
context:
space:
mode:
Diffstat (limited to 'tools/eslint/lib/config.js')
-rw-r--r--tools/eslint/lib/config.js440
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;