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("./util/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);
-
- 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.unshift(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.unshift(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);
-
- 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;
|