/** * @fileoverview Config file operations. This file must be usable in the browser, * so no Node-specific code can be here. * @author Nicholas C. Zakas */ "use strict"; //------------------------------------------------------------------------------ // Requirements //------------------------------------------------------------------------------ const minimatch = require("minimatch"), path = require("path"); const debug = require("debug")("eslint:config-ops"); //------------------------------------------------------------------------------ // Private //------------------------------------------------------------------------------ const RULE_SEVERITY_STRINGS = ["off", "warn", "error"], RULE_SEVERITY = RULE_SEVERITY_STRINGS.reduce((map, value, index) => { map[value] = index; return map; }, {}), VALID_SEVERITIES = [0, 1, 2, "off", "warn", "error"]; //------------------------------------------------------------------------------ // Public Interface //------------------------------------------------------------------------------ module.exports = { /** * Creates an empty configuration object suitable for merging as a base. * @returns {Object} A configuration object. */ createEmptyConfig() { return { globals: {}, env: {}, rules: {}, parserOptions: {} }; }, /** * Creates an environment config based on the specified environments. * @param {Object} env The environment settings. * @param {Environments} envContext The environment context. * @returns {Object} A configuration object with the appropriate rules and globals * set. */ createEnvironmentConfig(env, envContext) { const envConfig = this.createEmptyConfig(); if (env) { envConfig.env = env; Object.keys(env).filter(name => env[name]).forEach(name => { const environment = envContext.get(name); if (environment) { debug(`Creating config for environment ${name}`); if (environment.globals) { Object.assign(envConfig.globals, environment.globals); } if (environment.parserOptions) { Object.assign(envConfig.parserOptions, environment.parserOptions); } } }); } return envConfig; }, /** * Given a config with environment settings, applies the globals and * ecmaFeatures to the configuration and returns the result. * @param {Object} config The configuration information. * @param {Environments} envContent env context. * @returns {Object} The updated configuration information. */ applyEnvironments(config, envContent) { if (config.env && typeof config.env === "object") { debug("Apply environment settings to config"); return this.merge(this.createEnvironmentConfig(config.env, envContent), config); } return config; }, /** * Merges two config objects. This will not only add missing keys, but will also modify values to match. * @param {Object} target config object * @param {Object} src config object. Overrides in this config object will take priority over base. * @param {boolean} [combine] Whether to combine arrays or not * @param {boolean} [isRule] Whether its a rule * @returns {Object} merged config object. */ merge: function deepmerge(target, src, combine, isRule) { /* * The MIT License (MIT) * * Copyright (c) 2012 Nicholas Fisher * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /* * This code is taken from deepmerge repo * (https://github.com/KyleAMathews/deepmerge) * and modified to meet our needs. */ const array = Array.isArray(src) || Array.isArray(target); let dst = array && [] || {}; if (array) { const resolvedTarget = target || []; // src could be a string, so check for array if (isRule && Array.isArray(src) && src.length > 1) { dst = dst.concat(src); } else { dst = dst.concat(resolvedTarget); } const resolvedSrc = typeof src === "object" ? src : [src]; Object.keys(resolvedSrc).forEach((_, i) => { const e = resolvedSrc[i]; if (typeof dst[i] === "undefined") { dst[i] = e; } else if (typeof e === "object") { if (isRule) { dst[i] = e; } else { dst[i] = deepmerge(resolvedTarget[i], e, combine, isRule); } } else { if (!combine) { dst[i] = e; } else { if (dst.indexOf(e) === -1) { dst.push(e); } } } }); } else { if (target && typeof target === "object") { Object.keys(target).forEach(key => { dst[key] = target[key]; }); } Object.keys(src).forEach(key => { if (key === "overrides") { dst[key] = (target[key] || []).concat(src[key] || []); } else if (Array.isArray(src[key]) || Array.isArray(target[key])) { dst[key] = deepmerge(target[key], src[key], key === "plugins" || key === "extends", isRule); } else if (typeof src[key] !== "object" || !src[key] || key === "exported" || key === "astGlobals") { dst[key] = src[key]; } else { dst[key] = deepmerge(target[key] || {}, src[key], combine, key === "rules"); } }); } return dst; }, /** * Normalizes the severity value of a rule's configuration to a number * @param {(number|string|[number, ...*]|[string, ...*])} ruleConfig A rule's configuration value, generally * received from the user. A valid config value is either 0, 1, 2, the string "off" (treated the same as 0), * the string "warn" (treated the same as 1), the string "error" (treated the same as 2), or an array * whose first element is one of the above values. Strings are matched case-insensitively. * @returns {(0|1|2)} The numeric severity value if the config value was valid, otherwise 0. */ getRuleSeverity(ruleConfig) { const severityValue = Array.isArray(ruleConfig) ? ruleConfig[0] : ruleConfig; if (severityValue === 0 || severityValue === 1 || severityValue === 2) { return severityValue; } if (typeof severityValue === "string") { return RULE_SEVERITY[severityValue.toLowerCase()] || 0; } return 0; }, /** * Converts old-style severity settings (0, 1, 2) into new-style * severity settings (off, warn, error) for all rules. Assumption is that severity * values have already been validated as correct. * @param {Object} config The config object to normalize. * @returns {void} */ normalizeToStrings(config) { if (config.rules) { Object.keys(config.rules).forEach(ruleId => { const ruleConfig = config.rules[ruleId]; if (typeof ruleConfig === "number") { config.rules[ruleId] = RULE_SEVERITY_STRINGS[ruleConfig] || RULE_SEVERITY_STRINGS[0]; } else if (Array.isArray(ruleConfig) && typeof ruleConfig[0] === "number") { ruleConfig[0] = RULE_SEVERITY_STRINGS[ruleConfig[0]] || RULE_SEVERITY_STRINGS[0]; } }); } }, /** * Determines if the severity for the given rule configuration represents an error. * @param {int|string|Array} ruleConfig The configuration for an individual rule. * @returns {boolean} True if the rule represents an error, false if not. */ isErrorSeverity(ruleConfig) { let severity = Array.isArray(ruleConfig) ? ruleConfig[0] : ruleConfig; if (typeof severity === "string") { severity = RULE_SEVERITY[severity.toLowerCase()] || 0; } return (typeof severity === "number" && severity === 2); }, /** * Checks whether a given config has valid severity or not. * @param {number|string|Array} ruleConfig - The configuration for an individual rule. * @returns {boolean} `true` if the configuration has valid severity. */ isValidSeverity(ruleConfig) { let severity = Array.isArray(ruleConfig) ? ruleConfig[0] : ruleConfig; if (typeof severity === "string") { severity = severity.toLowerCase(); } return VALID_SEVERITIES.indexOf(severity) !== -1; }, /** * Checks whether every rule of a given config has valid severity or not. * @param {Object} config - The configuration for rules. * @returns {boolean} `true` if the configuration has valid severity. */ isEverySeverityValid(config) { return Object.keys(config).every(ruleId => this.isValidSeverity(config[ruleId])); }, /** * Merges all configurations in a given config vector. A vector is an array of objects, each containing a config * file path and a list of subconfig indices that match the current file path. All config data is assumed to be * cached. * @param {Array} vector list of config files and their subconfig indices that match the current file path * @param {Object} configCache the config cache * @returns {Object} config object */ getConfigFromVector(vector, configCache) { const cachedConfig = configCache.getMergedVectorConfig(vector); if (cachedConfig) { return cachedConfig; } debug("Using config from partial cache"); const subvector = Array.from(vector); let nearestCacheIndex = subvector.length - 1, partialCachedConfig; while (nearestCacheIndex >= 0) { partialCachedConfig = configCache.getMergedVectorConfig(subvector); if (partialCachedConfig) { break; } subvector.pop(); nearestCacheIndex--; } if (!partialCachedConfig) { partialCachedConfig = {}; } let finalConfig = partialCachedConfig; // Start from entry immediately following nearest cached config (first uncached entry) for (let i = nearestCacheIndex + 1; i < vector.length; i++) { finalConfig = this.mergeVectorEntry(finalConfig, vector[i], configCache); configCache.setMergedVectorConfig(vector.slice(0, i + 1), finalConfig); } return finalConfig; }, /** * Merges the config options from a single vector entry into the supplied config. * @param {Object} config the base config to merge the vector entry's options into * @param {Object} vectorEntry a single entry from a vector, consisting of a config file path and an array of * matching override indices * @param {Object} configCache the config cache * @returns {Object} merged config object */ mergeVectorEntry(config, vectorEntry, configCache) { const vectorEntryConfig = Object.assign({}, configCache.getConfig(vectorEntry.filePath)); let mergedConfig = Object.assign({}, config), overrides; if (vectorEntryConfig.overrides) { overrides = vectorEntryConfig.overrides.filter( (override, overrideIndex) => vectorEntry.matchingOverrides.indexOf(overrideIndex) !== -1 ); } else { overrides = []; } mergedConfig = this.merge(mergedConfig, vectorEntryConfig); delete mergedConfig.overrides; mergedConfig = overrides.reduce((lastConfig, override) => this.merge(lastConfig, override), mergedConfig); if (mergedConfig.filePath) { delete mergedConfig.filePath; delete mergedConfig.baseDirectory; } else if (mergedConfig.files) { delete mergedConfig.files; } return mergedConfig; }, /** * Checks that the specified file path matches all of the supplied glob patterns. * @param {string} filePath The file path to test patterns against * @param {string|string[]} patterns One or more glob patterns, of which at least one should match the file path * @param {string|string[]} [excludedPatterns] One or more glob patterns, of which none should match the file path * @returns {boolean} True if all the supplied patterns match the file path, false otherwise */ pathMatchesGlobs(filePath, patterns, excludedPatterns) { const patternList = [].concat(patterns); const excludedPatternList = [].concat(excludedPatterns || []); patternList.concat(excludedPatternList).forEach(pattern => { if (path.isAbsolute(pattern) || pattern.includes("..")) { throw new Error(`Invalid override pattern (expected relative path not containing '..'): ${pattern}`); } }); const opts = { matchBase: true }; return patternList.some(pattern => minimatch(filePath, pattern, opts)) && !excludedPatternList.some(excludedPattern => minimatch(filePath, excludedPattern, opts)); } };