384 lines
15 KiB
JavaScript
384 lines
15 KiB
JavaScript
/**
|
|
* @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 && [] || {};
|
|
|
|
combine = !!combine;
|
|
isRule = !!isRule;
|
|
if (array) {
|
|
target = 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(target);
|
|
}
|
|
if (typeof src !== "object" && !Array.isArray(src)) {
|
|
src = [src];
|
|
}
|
|
Object.keys(src).forEach((e, i) => {
|
|
e = src[i];
|
|
if (typeof dst[i] === "undefined") {
|
|
dst[i] = e;
|
|
} else if (typeof e === "object") {
|
|
if (isRule) {
|
|
dst[i] = e;
|
|
} else {
|
|
dst[i] = deepmerge(target[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<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));
|
|
}
|
|
};
|