123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374 |
- /**
- * @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<string,boolean>} 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) {
- return module.exports.getRuleSeverity(ruleConfig) === 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<Object>} 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));
- }
- };
|