remove timer and simon

This commit is contained in:
Cody Loyd
2017-12-15 12:56:14 -06:00
parent 14cc7d40bd
commit 834d78b8b2
2928 changed files with 303453 additions and 313 deletions
+359
View File
@@ -0,0 +1,359 @@
/**
* @fileoverview Used for creating a suggested configuration based on project code.
* @author Ian VanSchooten
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const lodash = require("lodash"),
Linter = require("../linter"),
configRule = require("./config-rule"),
ConfigOps = require("./config-ops"),
recConfig = require("../../conf/eslint-recommended");
const debug = require("debug")("eslint:autoconfig");
const linter = new Linter();
//------------------------------------------------------------------------------
// Data
//------------------------------------------------------------------------------
const MAX_CONFIG_COMBINATIONS = 17, // 16 combinations + 1 for severity only
RECOMMENDED_CONFIG_NAME = "eslint:recommended";
//------------------------------------------------------------------------------
// Private
//------------------------------------------------------------------------------
/**
* Information about a rule configuration, in the context of a Registry.
*
* @typedef {Object} registryItem
* @param {ruleConfig} config A valid configuration for the rule
* @param {number} specificity The number of elements in the ruleConfig array
* @param {number} errorCount The number of errors encountered when linting with the config
*/
/**
* This callback is used to measure execution status in a progress bar
* @callback progressCallback
* @param {number} The total number of times the callback will be called.
*/
/**
* Create registryItems for rules
* @param {rulesConfig} rulesConfig Hash of rule names and arrays of ruleConfig items
* @returns {Object} registryItems for each rule in provided rulesConfig
*/
function makeRegistryItems(rulesConfig) {
return Object.keys(rulesConfig).reduce((accumulator, ruleId) => {
accumulator[ruleId] = rulesConfig[ruleId].map(config => ({
config,
specificity: config.length || 1,
errorCount: void 0
}));
return accumulator;
}, {});
}
/**
* Creates an object in which to store rule configs and error counts
*
* Unless a rulesConfig is provided at construction, the registry will not contain
* any rules, only methods. This will be useful for building up registries manually.
*
* Registry class
*/
class Registry {
/**
* @param {rulesConfig} [rulesConfig] Hash of rule names and arrays of possible configurations
*/
constructor(rulesConfig) {
this.rules = (rulesConfig) ? makeRegistryItems(rulesConfig) : {};
}
/**
* Populate the registry with core rule configs.
*
* It will set the registry's `rule` property to an object having rule names
* as keys and an array of registryItems as values.
*
* @returns {void}
*/
populateFromCoreRules() {
const rulesConfig = configRule.createCoreRuleConfigs();
this.rules = makeRegistryItems(rulesConfig);
}
/**
* Creates sets of rule configurations which can be used for linting
* and initializes registry errors to zero for those configurations (side effect).
*
* This combines as many rules together as possible, such that the first sets
* in the array will have the highest number of rules configured, and later sets
* will have fewer and fewer, as not all rules have the same number of possible
* configurations.
*
* The length of the returned array will be <= MAX_CONFIG_COMBINATIONS.
*
* @param {Object} registry The autoconfig registry
* @returns {Object[]} "rules" configurations to use for linting
*/
buildRuleSets() {
let idx = 0;
const ruleIds = Object.keys(this.rules),
ruleSets = [];
/**
* Add a rule configuration from the registry to the ruleSets
*
* This is broken out into its own function so that it doesn't need to be
* created inside of the while loop.
*
* @param {string} rule The ruleId to add.
* @returns {void}
*/
const addRuleToRuleSet = function(rule) {
/*
* This check ensures that there is a rule configuration and that
* it has fewer than the max combinations allowed.
* If it has too many configs, we will only use the most basic of
* the possible configurations.
*/
const hasFewCombos = (this.rules[rule].length <= MAX_CONFIG_COMBINATIONS);
if (this.rules[rule][idx] && (hasFewCombos || this.rules[rule][idx].specificity <= 2)) {
/*
* If the rule has too many possible combinations, only take
* simple ones, avoiding objects.
*/
if (!hasFewCombos && typeof this.rules[rule][idx].config[1] === "object") {
return;
}
ruleSets[idx] = ruleSets[idx] || {};
ruleSets[idx][rule] = this.rules[rule][idx].config;
/*
* Initialize errorCount to zero, since this is a config which
* will be linted.
*/
this.rules[rule][idx].errorCount = 0;
}
}.bind(this);
while (ruleSets.length === idx) {
ruleIds.forEach(addRuleToRuleSet);
idx += 1;
}
return ruleSets;
}
/**
* Remove all items from the registry with a non-zero number of errors
*
* Note: this also removes rule configurations which were not linted
* (meaning, they have an undefined errorCount).
*
* @returns {void}
*/
stripFailingConfigs() {
const ruleIds = Object.keys(this.rules),
newRegistry = new Registry();
newRegistry.rules = Object.assign({}, this.rules);
ruleIds.forEach(ruleId => {
const errorFreeItems = newRegistry.rules[ruleId].filter(registryItem => (registryItem.errorCount === 0));
if (errorFreeItems.length > 0) {
newRegistry.rules[ruleId] = errorFreeItems;
} else {
delete newRegistry.rules[ruleId];
}
});
return newRegistry;
}
/**
* Removes rule configurations which were not included in a ruleSet
*
* @returns {void}
*/
stripExtraConfigs() {
const ruleIds = Object.keys(this.rules),
newRegistry = new Registry();
newRegistry.rules = Object.assign({}, this.rules);
ruleIds.forEach(ruleId => {
newRegistry.rules[ruleId] = newRegistry.rules[ruleId].filter(registryItem => (typeof registryItem.errorCount !== "undefined"));
});
return newRegistry;
}
/**
* Creates a registry of rules which had no error-free configs.
* The new registry is intended to be analyzed to determine whether its rules
* should be disabled or set to warning.
*
* @returns {Registry} A registry of failing rules.
*/
getFailingRulesRegistry() {
const ruleIds = Object.keys(this.rules),
failingRegistry = new Registry();
ruleIds.forEach(ruleId => {
const failingConfigs = this.rules[ruleId].filter(registryItem => (registryItem.errorCount > 0));
if (failingConfigs && failingConfigs.length === this.rules[ruleId].length) {
failingRegistry.rules[ruleId] = failingConfigs;
}
});
return failingRegistry;
}
/**
* Create an eslint config for any rules which only have one configuration
* in the registry.
*
* @returns {Object} An eslint config with rules section populated
*/
createConfig() {
const ruleIds = Object.keys(this.rules),
config = { rules: {} };
ruleIds.forEach(ruleId => {
if (this.rules[ruleId].length === 1) {
config.rules[ruleId] = this.rules[ruleId][0].config;
}
});
return config;
}
/**
* Return a cloned registry containing only configs with a desired specificity
*
* @param {number} specificity Only keep configs with this specificity
* @returns {Registry} A registry of rules
*/
filterBySpecificity(specificity) {
const ruleIds = Object.keys(this.rules),
newRegistry = new Registry();
newRegistry.rules = Object.assign({}, this.rules);
ruleIds.forEach(ruleId => {
newRegistry.rules[ruleId] = this.rules[ruleId].filter(registryItem => (registryItem.specificity === specificity));
});
return newRegistry;
}
/**
* Lint SourceCodes against all configurations in the registry, and record results
*
* @param {Object[]} sourceCodes SourceCode objects for each filename
* @param {Object} config ESLint config object
* @param {progressCallback} [cb] Optional callback for reporting execution status
* @returns {Registry} New registry with errorCount populated
*/
lintSourceCode(sourceCodes, config, cb) {
let lintedRegistry = new Registry();
lintedRegistry.rules = Object.assign({}, this.rules);
const ruleSets = lintedRegistry.buildRuleSets();
lintedRegistry = lintedRegistry.stripExtraConfigs();
debug("Linting with all possible rule combinations");
const filenames = Object.keys(sourceCodes);
const totalFilesLinting = filenames.length * ruleSets.length;
filenames.forEach(filename => {
debug(`Linting file: ${filename}`);
let ruleSetIdx = 0;
ruleSets.forEach(ruleSet => {
const lintConfig = Object.assign({}, config, { rules: ruleSet });
const lintResults = linter.verify(sourceCodes[filename], lintConfig);
lintResults.forEach(result => {
/*
* It is possible that the error is from a configuration comment
* in a linted file, in which case there may not be a config
* set in this ruleSetIdx.
* (https://github.com/eslint/eslint/issues/5992)
* (https://github.com/eslint/eslint/issues/7860)
*/
if (
lintedRegistry.rules[result.ruleId] &&
lintedRegistry.rules[result.ruleId][ruleSetIdx]
) {
lintedRegistry.rules[result.ruleId][ruleSetIdx].errorCount += 1;
}
});
ruleSetIdx += 1;
if (cb) {
cb(totalFilesLinting); // eslint-disable-line callback-return
}
});
// Deallocate for GC
sourceCodes[filename] = null;
});
return lintedRegistry;
}
}
/**
* Extract rule configuration into eslint:recommended where possible.
*
* This will return a new config with `"extends": "eslint:recommended"` and
* only the rules which have configurations different from the recommended config.
*
* @param {Object} config config object
* @returns {Object} config object using `"extends": "eslint:recommended"`
*/
function extendFromRecommended(config) {
const newConfig = Object.assign({}, config);
ConfigOps.normalizeToStrings(newConfig);
const recRules = Object.keys(recConfig.rules).filter(ruleId => ConfigOps.isErrorSeverity(recConfig.rules[ruleId]));
recRules.forEach(ruleId => {
if (lodash.isEqual(recConfig.rules[ruleId], newConfig.rules[ruleId])) {
delete newConfig.rules[ruleId];
}
});
newConfig.extends = RECOMMENDED_CONFIG_NAME;
return newConfig;
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = {
Registry,
extendFromRecommended
};
+130
View File
@@ -0,0 +1,130 @@
/**
* @fileoverview Responsible for caching config files
* @author Sylvan Mably
*/
"use strict";
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Get a string hash for a config vector
* @param {Array<Object>} vector config vector to hash
* @returns {string} hash of the vector values
* @private
*/
function hash(vector) {
return JSON.stringify(vector);
}
//------------------------------------------------------------------------------
// API
//------------------------------------------------------------------------------
/**
* Configuration caching class
*/
module.exports = class ConfigCache {
constructor() {
this.configFullNameCache = new Map();
this.localHierarchyCache = new Map();
this.mergedVectorCache = new Map();
this.mergedCache = new Map();
}
/**
* Gets a config object from the cache for the specified config file path.
* @param {string} configFullName the name of the configuration as used in the eslint config(e.g. 'plugin:node/recommended'),
* or the absolute path to a config file. This should uniquely identify a config.
* @returns {Object|null} config object, if found in the cache, otherwise null
* @private
*/
getConfig(configFullName) {
return this.configFullNameCache.get(configFullName);
}
/**
* Sets a config object in the cache for the specified config file path.
* @param {string} configFullName the name of the configuration as used in the eslint config(e.g. 'plugin:node/recommended'),
* or the absolute path to a config file. This should uniquely identify a config.
* @param {Object} config the config object to add to the cache
* @returns {void}
* @private
*/
setConfig(configFullName, config) {
this.configFullNameCache.set(configFullName, config);
}
/**
* Gets a list of hierarchy-local config objects that apply to the specified directory.
* @param {string} directory the path to the directory
* @returns {Object[]|null} a list of config objects, if found in the cache, otherwise null
* @private
*/
getHierarchyLocalConfigs(directory) {
return this.localHierarchyCache.get(directory);
}
/**
* For each of the supplied parent directories, sets the list of config objects for that directory to the
* appropriate subset of the supplied parent config objects.
* @param {string[]} parentDirectories a list of parent directories to add to the config cache
* @param {Object[]} parentConfigs a list of config objects that apply to the lowest directory in parentDirectories
* @returns {void}
* @private
*/
setHierarchyLocalConfigs(parentDirectories, parentConfigs) {
parentDirectories.forEach((localConfigDirectory, i) => {
const directoryParentConfigs = parentConfigs.slice(0, parentConfigs.length - i);
this.localHierarchyCache.set(localConfigDirectory, directoryParentConfigs);
});
}
/**
* Gets a merged config object corresponding to the supplied vector.
* @param {Array<Object>} vector the vector to find a merged config for
* @returns {Object|null} a merged config object, if found in the cache, otherwise null
* @private
*/
getMergedVectorConfig(vector) {
return this.mergedVectorCache.get(hash(vector));
}
/**
* Sets a merged config object in the cache for the supplied vector.
* @param {Array<Object>} vector the vector to save a merged config for
* @param {Object} config the merged config object to add to the cache
* @returns {void}
* @private
*/
setMergedVectorConfig(vector, config) {
this.mergedVectorCache.set(hash(vector), config);
}
/**
* Gets a merged config object corresponding to the supplied vector, including configuration options from outside
* the vector.
* @param {Array<Object>} vector the vector to find a merged config for
* @returns {Object|null} a merged config object, if found in the cache, otherwise null
* @private
*/
getMergedConfig(vector) {
return this.mergedCache.get(hash(vector));
}
/**
* Sets a merged config object in the cache for the supplied vector, including configuration options from outside
* the vector.
* @param {Array<Object>} vector the vector to save a merged config for
* @param {Object} config the merged config object to add to the cache
* @returns {void}
* @private
*/
setMergedConfig(vector, config) {
this.mergedCache.set(hash(vector), config);
}
};
+595
View File
@@ -0,0 +1,595 @@
/**
* @fileoverview Helper to locate and load configuration files.
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const fs = require("fs"),
path = require("path"),
ConfigOps = require("./config-ops"),
validator = require("./config-validator"),
ModuleResolver = require("../util/module-resolver"),
naming = require("../util/naming"),
pathIsInside = require("path-is-inside"),
stripComments = require("strip-json-comments"),
stringify = require("json-stable-stringify-without-jsonify"),
requireUncached = require("require-uncached");
const debug = require("debug")("eslint:config-file");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Determines sort order for object keys for json-stable-stringify
*
* see: https://github.com/samn/json-stable-stringify#cmp
*
* @param {Object} a The first comparison object ({key: akey, value: avalue})
* @param {Object} b The second comparison object ({key: bkey, value: bvalue})
* @returns {number} 1 or -1, used in stringify cmp method
*/
function sortByKey(a, b) {
return a.key > b.key ? 1 : -1;
}
//------------------------------------------------------------------------------
// Private
//------------------------------------------------------------------------------
const CONFIG_FILES = [
".eslintrc.js",
".eslintrc.yaml",
".eslintrc.yml",
".eslintrc.json",
".eslintrc",
"package.json"
];
const resolver = new ModuleResolver();
/**
* Convenience wrapper for synchronously reading file contents.
* @param {string} filePath The filename to read.
* @returns {string} The file contents, with the BOM removed.
* @private
*/
function readFile(filePath) {
return fs.readFileSync(filePath, "utf8").replace(/^\ufeff/, "");
}
/**
* Determines if a given string represents a filepath or not using the same
* conventions as require(), meaning that the first character must be nonalphanumeric
* and not the @ sign which is used for scoped packages to be considered a file path.
* @param {string} filePath The string to check.
* @returns {boolean} True if it's a filepath, false if not.
* @private
*/
function isFilePath(filePath) {
return path.isAbsolute(filePath) || !/\w|@/.test(filePath.charAt(0));
}
/**
* Loads a YAML configuration from a file.
* @param {string} filePath The filename to load.
* @returns {Object} The configuration object from the file.
* @throws {Error} If the file cannot be read.
* @private
*/
function loadYAMLConfigFile(filePath) {
debug(`Loading YAML config file: ${filePath}`);
// lazy load YAML to improve performance when not used
const yaml = require("js-yaml");
try {
// empty YAML file can be null, so always use
return yaml.safeLoad(readFile(filePath)) || {};
} catch (e) {
debug(`Error reading YAML file: ${filePath}`);
e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;
throw e;
}
}
/**
* Loads a JSON configuration from a file.
* @param {string} filePath The filename to load.
* @returns {Object} The configuration object from the file.
* @throws {Error} If the file cannot be read.
* @private
*/
function loadJSONConfigFile(filePath) {
debug(`Loading JSON config file: ${filePath}`);
try {
return JSON.parse(stripComments(readFile(filePath)));
} catch (e) {
debug(`Error reading JSON file: ${filePath}`);
e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;
throw e;
}
}
/**
* Loads a legacy (.eslintrc) configuration from a file.
* @param {string} filePath The filename to load.
* @returns {Object} The configuration object from the file.
* @throws {Error} If the file cannot be read.
* @private
*/
function loadLegacyConfigFile(filePath) {
debug(`Loading config file: ${filePath}`);
// lazy load YAML to improve performance when not used
const yaml = require("js-yaml");
try {
return yaml.safeLoad(stripComments(readFile(filePath))) || /* istanbul ignore next */ {};
} catch (e) {
debug(`Error reading YAML file: ${filePath}`);
e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;
throw e;
}
}
/**
* Loads a JavaScript configuration from a file.
* @param {string} filePath The filename to load.
* @returns {Object} The configuration object from the file.
* @throws {Error} If the file cannot be read.
* @private
*/
function loadJSConfigFile(filePath) {
debug(`Loading JS config file: ${filePath}`);
try {
return requireUncached(filePath);
} catch (e) {
debug(`Error reading JavaScript file: ${filePath}`);
e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;
throw e;
}
}
/**
* Loads a configuration from a package.json file.
* @param {string} filePath The filename to load.
* @returns {Object} The configuration object from the file.
* @throws {Error} If the file cannot be read.
* @private
*/
function loadPackageJSONConfigFile(filePath) {
debug(`Loading package.json config file: ${filePath}`);
try {
return loadJSONConfigFile(filePath).eslintConfig || null;
} catch (e) {
debug(`Error reading package.json file: ${filePath}`);
e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;
throw e;
}
}
/**
* Creates an error to notify about a missing config to extend from.
* @param {string} configName The name of the missing config.
* @returns {Error} The error object to throw
* @private
*/
function configMissingError(configName) {
const error = new Error(`Failed to load config "${configName}" to extend from.`);
error.messageTemplate = "extend-config-missing";
error.messageData = {
configName
};
return error;
}
/**
* Loads a configuration file regardless of the source. Inspects the file path
* to determine the correctly way to load the config file.
* @param {Object} file The path to the configuration.
* @returns {Object} The configuration information.
* @private
*/
function loadConfigFile(file) {
const filePath = file.filePath;
let config;
switch (path.extname(filePath)) {
case ".js":
config = loadJSConfigFile(filePath);
if (file.configName) {
config = config.configs[file.configName];
if (!config) {
throw configMissingError(file.configFullName);
}
}
break;
case ".json":
if (path.basename(filePath) === "package.json") {
config = loadPackageJSONConfigFile(filePath);
if (config === null) {
return null;
}
} else {
config = loadJSONConfigFile(filePath);
}
break;
case ".yaml":
case ".yml":
config = loadYAMLConfigFile(filePath);
break;
default:
config = loadLegacyConfigFile(filePath);
}
return ConfigOps.merge(ConfigOps.createEmptyConfig(), config);
}
/**
* Writes a configuration file in JSON format.
* @param {Object} config The configuration object to write.
* @param {string} filePath The filename to write to.
* @returns {void}
* @private
*/
function writeJSONConfigFile(config, filePath) {
debug(`Writing JSON config file: ${filePath}`);
const content = stringify(config, { cmp: sortByKey, space: 4 });
fs.writeFileSync(filePath, content, "utf8");
}
/**
* Writes a configuration file in YAML format.
* @param {Object} config The configuration object to write.
* @param {string} filePath The filename to write to.
* @returns {void}
* @private
*/
function writeYAMLConfigFile(config, filePath) {
debug(`Writing YAML config file: ${filePath}`);
// lazy load YAML to improve performance when not used
const yaml = require("js-yaml");
const content = yaml.safeDump(config, { sortKeys: true });
fs.writeFileSync(filePath, content, "utf8");
}
/**
* Writes a configuration file in JavaScript format.
* @param {Object} config The configuration object to write.
* @param {string} filePath The filename to write to.
* @returns {void}
* @private
*/
function writeJSConfigFile(config, filePath) {
debug(`Writing JS config file: ${filePath}`);
const content = `module.exports = ${stringify(config, { cmp: sortByKey, space: 4 })};`;
fs.writeFileSync(filePath, content, "utf8");
}
/**
* Writes a configuration file.
* @param {Object} config The configuration object to write.
* @param {string} filePath The filename to write to.
* @returns {void}
* @throws {Error} When an unknown file type is specified.
* @private
*/
function write(config, filePath) {
switch (path.extname(filePath)) {
case ".js":
writeJSConfigFile(config, filePath);
break;
case ".json":
writeJSONConfigFile(config, filePath);
break;
case ".yaml":
case ".yml":
writeYAMLConfigFile(config, filePath);
break;
default:
throw new Error("Can't write to unknown file type.");
}
}
/**
* Determines the base directory for node packages referenced in a config file.
* This does not include node_modules in the path so it can be used for all
* references relative to a config file.
* @param {string} configFilePath The config file referencing the file.
* @returns {string} The base directory for the file path.
* @private
*/
function getBaseDir(configFilePath) {
// calculates the path of the project including ESLint as dependency
const projectPath = path.resolve(__dirname, "../../../");
if (configFilePath && pathIsInside(configFilePath, projectPath)) {
// be careful of https://github.com/substack/node-resolve/issues/78
return path.join(path.resolve(configFilePath));
}
/*
* default to ESLint project path since it's unlikely that plugins will be
* in this directory
*/
return path.join(projectPath);
}
/**
* Determines the lookup path, including node_modules, for package
* references relative to a config file.
* @param {string} configFilePath The config file referencing the file.
* @returns {string} The lookup path for the file path.
* @private
*/
function getLookupPath(configFilePath) {
const basedir = getBaseDir(configFilePath);
return path.join(basedir, "node_modules");
}
/**
* Resolves a eslint core config path
* @param {string} name The eslint config name.
* @returns {string} The resolved path of the config.
* @private
*/
function getEslintCoreConfigPath(name) {
if (name === "eslint:recommended") {
/*
* Add an explicit substitution for eslint:recommended to
* conf/eslint-recommended.js.
*/
return path.resolve(__dirname, "../../conf/eslint-recommended.js");
}
if (name === "eslint:all") {
/*
* Add an explicit substitution for eslint:all to conf/eslint-all.js
*/
return path.resolve(__dirname, "../../conf/eslint-all.js");
}
throw configMissingError(name);
}
/**
* Applies values from the "extends" field in a configuration file.
* @param {Object} config The configuration information.
* @param {Config} configContext Plugin context for the config instance
* @param {string} filePath The file path from which the configuration information
* was loaded.
* @param {string} [relativeTo] The path to resolve relative to.
* @returns {Object} A new configuration object with all of the "extends" fields
* loaded and merged.
* @private
*/
function applyExtends(config, configContext, filePath, relativeTo) {
let configExtends = config.extends;
// normalize into an array for easier handling
if (!Array.isArray(config.extends)) {
configExtends = [config.extends];
}
// Make the last element in an array take the highest precedence
config = configExtends.reduceRight((previousValue, parentPath) => {
try {
if (parentPath.startsWith("eslint:")) {
parentPath = getEslintCoreConfigPath(parentPath);
} else if (isFilePath(parentPath)) {
/*
* If the `extends` path is relative, use the directory of the current configuration
* file as the reference point. Otherwise, use as-is.
*/
parentPath = (path.isAbsolute(parentPath)
? parentPath
: path.join(relativeTo || path.dirname(filePath), parentPath)
);
}
debug(`Loading ${parentPath}`);
// eslint-disable-next-line no-use-before-define
return ConfigOps.merge(load(parentPath, configContext, relativeTo), previousValue);
} catch (e) {
/*
* If the file referenced by `extends` failed to load, add the path
* to the configuration file that referenced it to the error
* message so the user is able to see where it was referenced from,
* then re-throw.
*/
e.message += `\nReferenced from: ${filePath}`;
throw e;
}
}, config);
return config;
}
/**
* Resolves a configuration file path into the fully-formed path, whether filename
* or package name.
* @param {string} filePath The filepath to resolve.
* @param {string} [relativeTo] The path to resolve relative to.
* @returns {Object} An object containing 3 properties:
* - 'filePath' (required) the resolved path that can be used directly to load the configuration.
* - 'configName' the name of the configuration inside the plugin.
* - 'configFullName' (required) the name of the configuration as used in the eslint config(e.g. 'plugin:node/recommended'),
* or the absolute path to a config file. This should uniquely identify a config.
* @private
*/
function resolve(filePath, relativeTo) {
if (isFilePath(filePath)) {
const fullPath = path.resolve(relativeTo || "", filePath);
return { filePath: fullPath, configFullName: fullPath };
}
let normalizedPackageName;
if (filePath.startsWith("plugin:")) {
const configFullName = filePath;
const pluginName = filePath.slice(7, filePath.lastIndexOf("/"));
const configName = filePath.slice(filePath.lastIndexOf("/") + 1);
normalizedPackageName = naming.normalizePackageName(pluginName, "eslint-plugin");
debug(`Attempting to resolve ${normalizedPackageName}`);
filePath = resolver.resolve(normalizedPackageName, getLookupPath(relativeTo));
return { filePath, configName, configFullName };
}
normalizedPackageName = naming.normalizePackageName(filePath, "eslint-config");
debug(`Attempting to resolve ${normalizedPackageName}`);
filePath = resolver.resolve(normalizedPackageName, getLookupPath(relativeTo));
return { filePath, configFullName: filePath };
}
/**
* Loads a configuration file from the given file path.
* @param {Object} resolvedPath The value from calling resolve() on a filename or package name.
* @param {Config} configContext Plugins context
* @returns {Object} The configuration information.
*/
function loadFromDisk(resolvedPath, configContext) {
const dirname = path.dirname(resolvedPath.filePath),
lookupPath = getLookupPath(dirname);
let config = loadConfigFile(resolvedPath);
if (config) {
// ensure plugins are properly loaded first
if (config.plugins) {
configContext.plugins.loadAll(config.plugins);
}
// include full path of parser if present
if (config.parser) {
if (isFilePath(config.parser)) {
config.parser = path.resolve(dirname || "", config.parser);
} else {
config.parser = resolver.resolve(config.parser, lookupPath);
}
}
const ruleMap = configContext.linterContext.getRules();
// validate the configuration before continuing
validator.validate(config, resolvedPath.configFullName, ruleMap.get.bind(ruleMap), configContext.linterContext.environments);
/*
* If an `extends` property is defined, it represents a configuration file to use as
* a "parent". Load the referenced file and merge the configuration recursively.
*/
if (config.extends) {
config = applyExtends(config, configContext, resolvedPath.filePath, dirname);
}
}
return config;
}
/**
* Loads a config object, applying extends if present.
* @param {Object} configObject a config object to load
* @param {Config} configContext Context for the config instance
* @returns {Object} the config object with extends applied if present, or the passed config if not
* @private
*/
function loadObject(configObject, configContext) {
return configObject.extends ? applyExtends(configObject, configContext, "") : configObject;
}
/**
* Loads a config object from the config cache based on its filename, falling back to the disk if the file is not yet
* cached.
* @param {string} filePath the path to the config file
* @param {Config} configContext Context for the config instance
* @param {string} [relativeTo] The path to resolve relative to.
* @returns {Object} the parsed config object (empty object if there was a parse error)
* @private
*/
function load(filePath, configContext, relativeTo) {
const resolvedPath = resolve(filePath, relativeTo);
const cachedConfig = configContext.configCache.getConfig(resolvedPath.configFullName);
if (cachedConfig) {
return cachedConfig;
}
const config = loadFromDisk(resolvedPath, configContext);
if (config) {
config.filePath = resolvedPath.filePath;
config.baseDirectory = path.dirname(resolvedPath.filePath);
configContext.configCache.setConfig(resolvedPath.configFullName, config);
}
return config;
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = {
getBaseDir,
getLookupPath,
load,
loadObject,
resolve,
write,
applyExtends,
CONFIG_FILES,
/**
* Retrieves the configuration filename for a given directory. It loops over all
* of the valid configuration filenames in order to find the first one that exists.
* @param {string} directory The directory to check for a config file.
* @returns {?string} The filename of the configuration file for the directory
* or null if there is no configuration file in the directory.
*/
getFilenameForDirectory(directory) {
for (let i = 0, len = CONFIG_FILES.length; i < len; i++) {
const filename = path.join(directory, CONFIG_FILES[i]);
if (fs.existsSync(filename) && fs.statSync(filename).isFile()) {
return filename;
}
}
return null;
}
};
+605
View File
@@ -0,0 +1,605 @@
/**
* @fileoverview Config initialization wizard.
* @author Ilya Volodin
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const util = require("util"),
inquirer = require("inquirer"),
ProgressBar = require("progress"),
semver = require("semver"),
autoconfig = require("./autoconfig.js"),
ConfigFile = require("./config-file"),
ConfigOps = require("./config-ops"),
getSourceCodeOfFiles = require("../util/source-code-util").getSourceCodeOfFiles,
ModuleResolver = require("../util/module-resolver"),
npmUtil = require("../util/npm-util"),
recConfig = require("../../conf/eslint-recommended"),
log = require("../logging");
const debug = require("debug")("eslint:config-initializer");
//------------------------------------------------------------------------------
// Private
//------------------------------------------------------------------------------
/* istanbul ignore next: hard to test fs function */
/**
* Create .eslintrc file in the current working directory
* @param {Object} config object that contains user's answers
* @param {string} format The file format to write to.
* @returns {void}
*/
function writeFile(config, format) {
// default is .js
let extname = ".js";
if (format === "YAML") {
extname = ".yml";
} else if (format === "JSON") {
extname = ".json";
}
const installedESLint = config.installedESLint;
delete config.installedESLint;
ConfigFile.write(config, `./.eslintrc${extname}`);
log.info(`Successfully created .eslintrc${extname} file in ${process.cwd()}`);
if (installedESLint) {
log.info("ESLint was installed locally. We recommend using this local copy instead of your globally-installed copy.");
}
}
/**
* Get the peer dependencies of the given module.
* This adds the gotten value to cache at the first time, then reuses it.
* In a process, this function is called twice, but `npmUtil.fetchPeerDependencies` needs to access network which is relatively slow.
* @param {string} moduleName The module name to get.
* @returns {Object} The peer dependencies of the given module.
* This object is the object of `peerDependencies` field of `package.json`.
* Returns null if npm was not found.
*/
function getPeerDependencies(moduleName) {
let result = getPeerDependencies.cache.get(moduleName);
if (!result) {
log.info(`Checking peerDependencies of ${moduleName}`);
result = npmUtil.fetchPeerDependencies(moduleName);
getPeerDependencies.cache.set(moduleName, result);
}
return result;
}
getPeerDependencies.cache = new Map();
/**
* Synchronously install necessary plugins, configs, parsers, etc. based on the config
* @param {Object} config config object
* @param {boolean} [installESLint=true] If `false` is given, it does not install eslint.
* @returns {void}
*/
function installModules(config, installESLint) {
const modules = {};
// Create a list of modules which should be installed based on config
if (config.plugins) {
for (const plugin of config.plugins) {
modules[`eslint-plugin-${plugin}`] = "latest";
}
}
if (config.extends && config.extends.indexOf("eslint:") === -1) {
const moduleName = `eslint-config-${config.extends}`;
modules[moduleName] = "latest";
Object.assign(
modules,
getPeerDependencies(`${moduleName}@latest`)
);
}
// If no modules, do nothing.
if (Object.keys(modules).length === 0) {
return;
}
if (installESLint === false) {
delete modules.eslint;
} else {
const installStatus = npmUtil.checkDevDeps(["eslint"]);
// Mark to show messages if it's new installation of eslint.
if (installStatus.eslint === false) {
log.info("Local ESLint installation not found.");
modules.eslint = modules.eslint || "latest";
config.installedESLint = true;
}
}
// Install packages
const modulesToInstall = Object.keys(modules).map(name => `${name}@${modules[name]}`);
log.info(`Installing ${modulesToInstall.join(", ")}`);
npmUtil.installSyncSaveDev(modulesToInstall);
}
/**
* Set the `rules` of a config by examining a user's source code
*
* Note: This clones the config object and returns a new config to avoid mutating
* the original config parameter.
*
* @param {Object} answers answers received from inquirer
* @param {Object} config config object
* @returns {Object} config object with configured rules
*/
function configureRules(answers, config) {
const BAR_TOTAL = 20,
BAR_SOURCE_CODE_TOTAL = 4,
newConfig = Object.assign({}, config),
disabledConfigs = {};
let sourceCodes,
registry;
// Set up a progress bar, as this process can take a long time
const bar = new ProgressBar("Determining Config: :percent [:bar] :elapseds elapsed, eta :etas ", {
width: 30,
total: BAR_TOTAL
});
bar.tick(0); // Shows the progress bar
// Get the SourceCode of all chosen files
const patterns = answers.patterns.split(/[\s]+/);
try {
sourceCodes = getSourceCodeOfFiles(patterns, { baseConfig: newConfig, useEslintrc: false }, total => {
bar.tick((BAR_SOURCE_CODE_TOTAL / total));
});
} catch (e) {
log.info("\n");
throw e;
}
const fileQty = Object.keys(sourceCodes).length;
if (fileQty === 0) {
log.info("\n");
throw new Error("Automatic Configuration failed. No files were able to be parsed.");
}
// Create a registry of rule configs
registry = new autoconfig.Registry();
registry.populateFromCoreRules();
// Lint all files with each rule config in the registry
registry = registry.lintSourceCode(sourceCodes, newConfig, total => {
bar.tick((BAR_TOTAL - BAR_SOURCE_CODE_TOTAL) / total); // Subtract out ticks used at beginning
});
debug(`\nRegistry: ${util.inspect(registry.rules, { depth: null })}`);
// Create a list of recommended rules, because we don't want to disable them
const recRules = Object.keys(recConfig.rules).filter(ruleId => ConfigOps.isErrorSeverity(recConfig.rules[ruleId]));
// Find and disable rules which had no error-free configuration
const failingRegistry = registry.getFailingRulesRegistry();
Object.keys(failingRegistry.rules).forEach(ruleId => {
// If the rule is recommended, set it to error, otherwise disable it
disabledConfigs[ruleId] = (recRules.indexOf(ruleId) !== -1) ? 2 : 0;
});
// Now that we know which rules to disable, strip out configs with errors
registry = registry.stripFailingConfigs();
/*
* If there is only one config that results in no errors for a rule, we should use it.
* createConfig will only add rules that have one configuration in the registry.
*/
const singleConfigs = registry.createConfig().rules;
/*
* The "sweet spot" for number of options in a config seems to be two (severity plus one option).
* Very often, a third option (usually an object) is available to address
* edge cases, exceptions, or unique situations. We will prefer to use a config with
* specificity of two.
*/
const specTwoConfigs = registry.filterBySpecificity(2).createConfig().rules;
// Maybe a specific combination using all three options works
const specThreeConfigs = registry.filterBySpecificity(3).createConfig().rules;
// If all else fails, try to use the default (severity only)
const defaultConfigs = registry.filterBySpecificity(1).createConfig().rules;
// Combine configs in reverse priority order (later take precedence)
newConfig.rules = Object.assign({}, disabledConfigs, defaultConfigs, specThreeConfigs, specTwoConfigs, singleConfigs);
// Make sure progress bar has finished (floating point rounding)
bar.update(BAR_TOTAL);
// Log out some stats to let the user know what happened
const finalRuleIds = Object.keys(newConfig.rules);
const totalRules = finalRuleIds.length;
const enabledRules = finalRuleIds.filter(ruleId => (newConfig.rules[ruleId] !== 0)).length;
const resultMessage = [
`\nEnabled ${enabledRules} out of ${totalRules}`,
`rules based on ${fileQty}`,
`file${(fileQty === 1) ? "." : "s."}`
].join(" ");
log.info(resultMessage);
ConfigOps.normalizeToStrings(newConfig);
return newConfig;
}
/**
* process user's answers and create config object
* @param {Object} answers answers received from inquirer
* @returns {Object} config object
*/
function processAnswers(answers) {
let config = { rules: {}, env: {} };
if (answers.es6) {
config.env.es6 = true;
if (answers.modules) {
config.parserOptions = config.parserOptions || {};
config.parserOptions.sourceType = "module";
}
}
if (answers.commonjs) {
config.env.commonjs = true;
}
answers.env.forEach(env => {
config.env[env] = true;
});
if (answers.jsx) {
config.parserOptions = config.parserOptions || {};
config.parserOptions.ecmaFeatures = config.parserOptions.ecmaFeatures || {};
config.parserOptions.ecmaFeatures.jsx = true;
if (answers.react) {
config.plugins = ["react"];
config.parserOptions.ecmaFeatures.experimentalObjectRestSpread = true;
}
}
if (answers.source === "prompt") {
config.extends = "eslint:recommended";
config.rules.indent = ["error", answers.indent];
config.rules.quotes = ["error", answers.quotes];
config.rules["linebreak-style"] = ["error", answers.linebreak];
config.rules.semi = ["error", answers.semi ? "always" : "never"];
}
installModules(config);
if (answers.source === "auto") {
config = configureRules(answers, config);
config = autoconfig.extendFromRecommended(config);
}
ConfigOps.normalizeToStrings(config);
return config;
}
/**
* process user's style guide of choice and return an appropriate config object.
* @param {string} guide name of the chosen style guide
* @param {boolean} [installESLint=true] If `false` is given, it does not install eslint.
* @returns {Object} config object
*/
function getConfigForStyleGuide(guide, installESLint) {
const guides = {
google: { extends: "google" },
airbnb: { extends: "airbnb" },
"airbnb-base": { extends: "airbnb-base" },
standard: { extends: "standard" }
};
if (!guides[guide]) {
throw new Error("You referenced an unsupported guide.");
}
installModules(guides[guide], installESLint);
return guides[guide];
}
/**
* Get the version of the local ESLint.
* @returns {string|null} The version. If the local ESLint was not found, returns null.
*/
function getLocalESLintVersion() {
try {
const resolver = new ModuleResolver();
const eslintPath = resolver.resolve("eslint", process.cwd());
const eslint = require(eslintPath);
return eslint.linter.version || null;
} catch (_err) {
return null;
}
}
/**
* Get the shareable config name of the chosen style guide.
* @param {Object} answers The answers object.
* @returns {string} The shareable config name.
*/
function getStyleGuideName(answers) {
if (answers.styleguide === "airbnb" && !answers.airbnbReact) {
return "airbnb-base";
}
return answers.styleguide;
}
/**
* Check whether the local ESLint version conflicts with the required version of the chosen shareable config.
* @param {Object} answers The answers object.
* @returns {boolean} `true` if the local ESLint is found then it conflicts with the required version of the chosen shareable config.
*/
function hasESLintVersionConflict(answers) {
// Get the local ESLint version.
const localESLintVersion = getLocalESLintVersion();
if (!localESLintVersion) {
return false;
}
// Get the required range of ESLint version.
const configName = getStyleGuideName(answers);
const moduleName = `eslint-config-${configName}@latest`;
const peerDependencies = getPeerDependencies(moduleName) || {};
const requiredESLintVersionRange = peerDependencies.eslint;
if (!requiredESLintVersionRange) {
return false;
}
answers.localESLintVersion = localESLintVersion;
answers.requiredESLintVersionRange = requiredESLintVersionRange;
// Check the version.
if (semver.satisfies(localESLintVersion, requiredESLintVersionRange)) {
answers.installESLint = false;
return false;
}
return true;
}
/* istanbul ignore next: no need to test inquirer*/
/**
* Ask use a few questions on command prompt
* @returns {Promise} The promise with the result of the prompt
*/
function promptUser() {
return inquirer.prompt([
{
type: "list",
name: "source",
message: "How would you like to configure ESLint?",
default: "prompt",
choices: [
{ name: "Answer questions about your style", value: "prompt" },
{ name: "Use a popular style guide", value: "guide" },
{ name: "Inspect your JavaScript file(s)", value: "auto" }
]
},
{
type: "list",
name: "styleguide",
message: "Which style guide do you want to follow?",
choices: [{ name: "Google", value: "google" }, { name: "Airbnb", value: "airbnb" }, { name: "Standard", value: "standard" }],
when(answers) {
answers.packageJsonExists = npmUtil.checkPackageJson();
return answers.source === "guide" && answers.packageJsonExists;
}
},
{
type: "confirm",
name: "airbnbReact",
message: "Do you use React?",
default: false,
when(answers) {
return answers.styleguide === "airbnb";
}
},
{
type: "input",
name: "patterns",
message: "Which file(s), path(s), or glob(s) should be examined?",
when(answers) {
return (answers.source === "auto");
},
validate(input) {
if (input.trim().length === 0 && input.trim() !== ",") {
return "You must tell us what code to examine. Try again.";
}
return true;
}
},
{
type: "list",
name: "format",
message: "What format do you want your config file to be in?",
default: "JavaScript",
choices: ["JavaScript", "YAML", "JSON"],
when(answers) {
return ((answers.source === "guide" && answers.packageJsonExists) || answers.source === "auto");
}
},
{
type: "confirm",
name: "installESLint",
message(answers) {
const verb = semver.ltr(answers.localESLintVersion, answers.requiredESLintVersionRange)
? "upgrade"
: "downgrade";
return `The style guide "${answers.styleguide}" requires eslint@${answers.requiredESLintVersionRange}. You are currently using eslint@${answers.localESLintVersion}.\n Do you want to ${verb}?`;
},
default: true,
when(answers) {
return answers.source === "guide" && answers.packageJsonExists && hasESLintVersionConflict(answers);
}
}
]).then(earlyAnswers => {
// early exit if you are using a style guide
if (earlyAnswers.source === "guide") {
if (!earlyAnswers.packageJsonExists) {
log.info("A package.json is necessary to install plugins such as style guides. Run `npm init` to create a package.json file and try again.");
return void 0;
}
if (earlyAnswers.installESLint === false && !semver.satisfies(earlyAnswers.localESLintVersion, earlyAnswers.requiredESLintVersionRange)) {
log.info(`Note: it might not work since ESLint's version is mismatched with the ${earlyAnswers.styleguide} config.`);
}
if (earlyAnswers.styleguide === "airbnb" && !earlyAnswers.airbnbReact) {
earlyAnswers.styleguide = "airbnb-base";
}
const config = getConfigForStyleGuide(earlyAnswers.styleguide, earlyAnswers.installESLint);
writeFile(config, earlyAnswers.format);
return void 0;
}
// continue with the questions otherwise...
return inquirer.prompt([
{
type: "confirm",
name: "es6",
message: "Are you using ECMAScript 6 features?",
default: false
},
{
type: "confirm",
name: "modules",
message: "Are you using ES6 modules?",
default: false,
when(answers) {
return answers.es6 === true;
}
},
{
type: "checkbox",
name: "env",
message: "Where will your code run?",
default: ["browser"],
choices: [{ name: "Browser", value: "browser" }, { name: "Node", value: "node" }]
},
{
type: "confirm",
name: "commonjs",
message: "Do you use CommonJS?",
default: false,
when(answers) {
return answers.env.some(env => env === "browser");
}
},
{
type: "confirm",
name: "jsx",
message: "Do you use JSX?",
default: false
},
{
type: "confirm",
name: "react",
message: "Do you use React?",
default: false,
when(answers) {
return answers.jsx;
}
}
]).then(secondAnswers => {
// early exit if you are using automatic style generation
if (earlyAnswers.source === "auto") {
const combinedAnswers = Object.assign({}, earlyAnswers, secondAnswers);
const config = processAnswers(combinedAnswers);
installModules(config);
writeFile(config, earlyAnswers.format);
return void 0;
}
// continue with the style questions otherwise...
return inquirer.prompt([
{
type: "list",
name: "indent",
message: "What style of indentation do you use?",
default: "tab",
choices: [{ name: "Tabs", value: "tab" }, { name: "Spaces", value: 4 }]
},
{
type: "list",
name: "quotes",
message: "What quotes do you use for strings?",
default: "double",
choices: [{ name: "Double", value: "double" }, { name: "Single", value: "single" }]
},
{
type: "list",
name: "linebreak",
message: "What line endings do you use?",
default: "unix",
choices: [{ name: "Unix", value: "unix" }, { name: "Windows", value: "windows" }]
},
{
type: "confirm",
name: "semi",
message: "Do you require semicolons?",
default: true
},
{
type: "list",
name: "format",
message: "What format do you want your config file to be in?",
default: "JavaScript",
choices: ["JavaScript", "YAML", "JSON"]
}
]).then(answers => {
const totalAnswers = Object.assign({}, earlyAnswers, secondAnswers, answers);
const config = processAnswers(totalAnswers);
installModules(config);
writeFile(config, answers.format);
});
});
});
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
const init = {
getConfigForStyleGuide,
hasESLintVersionConflict,
processAnswers,
/* istanbul ignore next */initializeConfig() {
return promptUser();
}
};
module.exports = init;
+383
View File
@@ -0,0 +1,383 @@
/**
* @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));
}
};
+322
View File
@@ -0,0 +1,322 @@
/**
* @fileoverview Create configurations for a rule
* @author Ian VanSchooten
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const Rules = require("../rules"),
loadRules = require("../load-rules");
const rules = new Rules();
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Wrap all of the elements of an array into arrays.
* @param {*[]} xs Any array.
* @returns {Array[]} An array of arrays.
*/
function explodeArray(xs) {
return xs.reduce((accumulator, x) => {
accumulator.push([x]);
return accumulator;
}, []);
}
/**
* Mix two arrays such that each element of the second array is concatenated
* onto each element of the first array.
*
* For example:
* combineArrays([a, [b, c]], [x, y]); // -> [[a, x], [a, y], [b, c, x], [b, c, y]]
*
* @param {array} arr1 The first array to combine.
* @param {array} arr2 The second array to combine.
* @returns {array} A mixture of the elements of the first and second arrays.
*/
function combineArrays(arr1, arr2) {
const res = [];
if (arr1.length === 0) {
return explodeArray(arr2);
}
if (arr2.length === 0) {
return explodeArray(arr1);
}
arr1.forEach(x1 => {
arr2.forEach(x2 => {
res.push([].concat(x1, x2));
});
});
return res;
}
/**
* Group together valid rule configurations based on object properties
*
* e.g.:
* groupByProperty([
* {before: true},
* {before: false},
* {after: true},
* {after: false}
* ]);
*
* will return:
* [
* [{before: true}, {before: false}],
* [{after: true}, {after: false}]
* ]
*
* @param {Object[]} objects Array of objects, each with one property/value pair
* @returns {Array[]} Array of arrays of objects grouped by property
*/
function groupByProperty(objects) {
const groupedObj = objects.reduce((accumulator, obj) => {
const prop = Object.keys(obj)[0];
accumulator[prop] = accumulator[prop] ? accumulator[prop].concat(obj) : [obj];
return accumulator;
}, {});
return Object.keys(groupedObj).map(prop => groupedObj[prop]);
}
//------------------------------------------------------------------------------
// Private
//------------------------------------------------------------------------------
/**
* Configuration settings for a rule.
*
* A configuration can be a single number (severity), or an array where the first
* element in the array is the severity, and is the only required element.
* Configs may also have one or more additional elements to specify rule
* configuration or options.
*
* @typedef {array|number} ruleConfig
* @param {number} 0 The rule's severity (0, 1, 2).
*/
/**
* Object whose keys are rule names and values are arrays of valid ruleConfig items
* which should be linted against the target source code to determine error counts.
* (a ruleConfigSet.ruleConfigs).
*
* e.g. rulesConfig = {
* "comma-dangle": [2, [2, "always"], [2, "always-multiline"], [2, "never"]],
* "no-console": [2]
* }
* @typedef rulesConfig
*/
/**
* Create valid rule configurations by combining two arrays,
* with each array containing multiple objects each with a
* single property/value pair and matching properties.
*
* e.g.:
* combinePropertyObjects(
* [{before: true}, {before: false}],
* [{after: true}, {after: false}]
* );
*
* will return:
* [
* {before: true, after: true},
* {before: true, after: false},
* {before: false, after: true},
* {before: false, after: false}
* ]
*
* @param {Object[]} objArr1 Single key/value objects, all with the same key
* @param {Object[]} objArr2 Single key/value objects, all with another key
* @returns {Object[]} Combined objects for each combination of input properties and values
*/
function combinePropertyObjects(objArr1, objArr2) {
const res = [];
if (objArr1.length === 0) {
return objArr2;
}
if (objArr2.length === 0) {
return objArr1;
}
objArr1.forEach(obj1 => {
objArr2.forEach(obj2 => {
const combinedObj = {};
const obj1Props = Object.keys(obj1);
const obj2Props = Object.keys(obj2);
obj1Props.forEach(prop1 => {
combinedObj[prop1] = obj1[prop1];
});
obj2Props.forEach(prop2 => {
combinedObj[prop2] = obj2[prop2];
});
res.push(combinedObj);
});
});
return res;
}
/**
* Creates a new instance of a rule configuration set
*
* A rule configuration set is an array of configurations that are valid for a
* given rule. For example, the configuration set for the "semi" rule could be:
*
* ruleConfigSet.ruleConfigs // -> [[2], [2, "always"], [2, "never"]]
*
* Rule configuration set class
*/
class RuleConfigSet {
/**
* @param {ruleConfig[]} configs Valid rule configurations
*/
constructor(configs) {
/**
* Stored valid rule configurations for this instance
* @type {array}
*/
this.ruleConfigs = configs || [];
}
/**
* Add a severity level to the front of all configs in the instance.
* This should only be called after all configs have been added to the instance.
*
* @param {number} [severity=2] The level of severity for the rule (0, 1, 2)
* @returns {void}
*/
addErrorSeverity(severity) {
severity = severity || 2;
this.ruleConfigs = this.ruleConfigs.map(config => {
config.unshift(severity);
return config;
});
// Add a single config at the beginning consisting of only the severity
this.ruleConfigs.unshift(severity);
}
/**
* Add rule configs from an array of strings (schema enums)
* @param {string[]} enums Array of valid rule options (e.g. ["always", "never"])
* @returns {void}
*/
addEnums(enums) {
this.ruleConfigs = this.ruleConfigs.concat(combineArrays(this.ruleConfigs, enums));
}
/**
* Add rule configurations from a schema object
* @param {Object} obj Schema item with type === "object"
* @returns {boolean} true if at least one schema for the object could be generated, false otherwise
*/
addObject(obj) {
const objectConfigSet = {
objectConfigs: [],
add(property, values) {
for (let idx = 0; idx < values.length; idx++) {
const optionObj = {};
optionObj[property] = values[idx];
this.objectConfigs.push(optionObj);
}
},
combine() {
this.objectConfigs = groupByProperty(this.objectConfigs).reduce((accumulator, objArr) => combinePropertyObjects(accumulator, objArr), []);
}
};
/*
* The object schema could have multiple independent properties.
* If any contain enums or booleans, they can be added and then combined
*/
Object.keys(obj.properties).forEach(prop => {
if (obj.properties[prop].enum) {
objectConfigSet.add(prop, obj.properties[prop].enum);
}
if (obj.properties[prop].type && obj.properties[prop].type === "boolean") {
objectConfigSet.add(prop, [true, false]);
}
});
objectConfigSet.combine();
if (objectConfigSet.objectConfigs.length > 0) {
this.ruleConfigs = this.ruleConfigs.concat(combineArrays(this.ruleConfigs, objectConfigSet.objectConfigs));
return true;
}
return false;
}
}
/**
* Generate valid rule configurations based on a schema object
* @param {Object} schema A rule's schema object
* @returns {array[]} Valid rule configurations
*/
function generateConfigsFromSchema(schema) {
const configSet = new RuleConfigSet();
if (Array.isArray(schema)) {
for (const opt of schema) {
if (opt.enum) {
configSet.addEnums(opt.enum);
} else if (opt.type && opt.type === "object") {
if (!configSet.addObject(opt)) {
break;
}
// TODO (IanVS): support oneOf
} else {
// If we don't know how to fill in this option, don't fill in any of the following options.
break;
}
}
}
configSet.addErrorSeverity();
return configSet.ruleConfigs;
}
/**
* Generate possible rule configurations for all of the core rules
* @returns {rulesConfig} Hash of rule names and arrays of possible configurations
*/
function createCoreRuleConfigs() {
const ruleList = loadRules();
return Object.keys(ruleList).reduce((accumulator, id) => {
const rule = rules.get(id);
const schema = (typeof rule === "function") ? rule.schema : rule.meta.schema;
accumulator[id] = generateConfigsFromSchema(schema);
return accumulator;
}, {});
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = {
generateConfigsFromSchema,
createCoreRuleConfigs
};
+244
View File
@@ -0,0 +1,244 @@
/**
* @fileoverview Validates configs.
* @author Brandon Mills
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const ajv = require("../util/ajv"),
lodash = require("lodash"),
configSchema = require("../../conf/config-schema.js"),
util = require("util");
const ruleValidators = new WeakMap();
//------------------------------------------------------------------------------
// Private
//------------------------------------------------------------------------------
let validateSchema;
/**
* Gets a complete options schema for a rule.
* @param {{create: Function, schema: (Array|null)}} rule A new-style rule object
* @returns {Object} JSON Schema for the rule's options.
*/
function getRuleOptionsSchema(rule) {
const schema = rule.schema || rule.meta && rule.meta.schema;
// Given a tuple of schemas, insert warning level at the beginning
if (Array.isArray(schema)) {
if (schema.length) {
return {
type: "array",
items: schema,
minItems: 0,
maxItems: schema.length
};
}
return {
type: "array",
minItems: 0,
maxItems: 0
};
}
// Given a full schema, leave it alone
return schema || null;
}
/**
* Validates a rule's severity and returns the severity value. Throws an error if the severity is invalid.
* @param {options} options The given options for the rule.
* @returns {number|string} The rule's severity value
*/
function validateRuleSeverity(options) {
const severity = Array.isArray(options) ? options[0] : options;
if (severity !== 0 && severity !== 1 && severity !== 2 && !(typeof severity === "string" && /^(?:off|warn|error)$/i.test(severity))) {
throw new Error(`\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '${util.inspect(severity).replace(/'/g, "\"").replace(/\n/g, "")}').\n`);
}
return severity;
}
/**
* Validates the non-severity options passed to a rule, based on its schema.
* @param {{create: Function}} rule The rule to validate
* @param {array} localOptions The options for the rule, excluding severity
* @returns {void}
*/
function validateRuleSchema(rule, localOptions) {
if (!ruleValidators.has(rule)) {
const schema = getRuleOptionsSchema(rule);
if (schema) {
ruleValidators.set(rule, ajv.compile(schema));
}
}
const validateRule = ruleValidators.get(rule);
if (validateRule) {
validateRule(localOptions);
if (validateRule.errors) {
throw new Error(validateRule.errors.map(
error => `\tValue ${JSON.stringify(error.data)} ${error.message}.\n`
).join(""));
}
}
}
/**
* Validates a rule's options against its schema.
* @param {{create: Function}|null} rule The rule that the config is being validated for
* @param {string} ruleId The rule's unique name.
* @param {array|number} options The given options for the rule.
* @param {string} source The name of the configuration source to report in any errors.
* @returns {void}
*/
function validateRuleOptions(rule, ruleId, options, source) {
if (!rule) {
return;
}
try {
const severity = validateRuleSeverity(options);
if (severity !== 0 && !(typeof severity === "string" && severity.toLowerCase() === "off")) {
validateRuleSchema(rule, Array.isArray(options) ? options.slice(1) : []);
}
} catch (err) {
throw new Error(`${source}:\n\tConfiguration for rule "${ruleId}" is invalid:\n${err.message}`);
}
}
/**
* Validates an environment object
* @param {Object} environment The environment config object to validate.
* @param {string} source The name of the configuration source to report in any errors.
* @param {Environments} envContext Env context
* @returns {void}
*/
function validateEnvironment(environment, source, envContext) {
// not having an environment is ok
if (!environment) {
return;
}
Object.keys(environment).forEach(env => {
if (!envContext.get(env)) {
const message = `${source}:\n\tEnvironment key "${env}" is unknown\n`;
throw new Error(message);
}
});
}
/**
* Validates a rules config object
* @param {Object} rulesConfig The rules config object to validate.
* @param {string} source The name of the configuration source to report in any errors.
* @param {function(string): {create: Function}} ruleMapper A mapper function from strings to loaded rules
* @returns {void}
*/
function validateRules(rulesConfig, source, ruleMapper) {
if (!rulesConfig) {
return;
}
Object.keys(rulesConfig).forEach(id => {
validateRuleOptions(ruleMapper(id), id, rulesConfig[id], source);
});
}
/**
* Formats an array of schema validation errors.
* @param {Array} errors An array of error messages to format.
* @returns {string} Formatted error message
*/
function formatErrors(errors) {
return errors.map(error => {
if (error.keyword === "additionalProperties") {
const formattedPropertyPath = error.dataPath.length ? `${error.dataPath.slice(1)}.${error.params.additionalProperty}` : error.params.additionalProperty;
return `Unexpected top-level property "${formattedPropertyPath}"`;
}
if (error.keyword === "type") {
const formattedField = error.dataPath.slice(1);
const formattedExpectedType = Array.isArray(error.schema) ? error.schema.join("/") : error.schema;
const formattedValue = JSON.stringify(error.data);
return `Property "${formattedField}" is the wrong type (expected ${formattedExpectedType} but got \`${formattedValue}\`)`;
}
const field = error.dataPath[0] === "." ? error.dataPath.slice(1) : error.dataPath;
return `"${field}" ${error.message}. Value: ${JSON.stringify(error.data)}`;
}).map(message => `\t- ${message}.\n`).join("");
}
/**
* Emits a deprecation warning containing a given filepath. A new deprecation warning is emitted
* for each unique file path, but repeated invocations with the same file path have no effect.
* No warnings are emitted if the `--no-deprecation` or `--no-warnings` Node runtime flags are active.
* @param {string} source The name of the configuration source to report the warning for.
* @returns {void}
*/
const emitEcmaFeaturesWarning = lodash.memoize(source => {
/*
* util.deprecate seems to be the only way to emit a warning in Node 4.x while respecting the --no-warnings flag.
* (In Node 6+, process.emitWarning could be used instead.)
*/
util.deprecate(
() => {},
`[eslint] The 'ecmaFeatures' config file property is deprecated, and has no effect. (found in ${source})`
)();
});
/**
* Validates the top level properties of the config object.
* @param {Object} config The config object to validate.
* @param {string} source The name of the configuration source to report in any errors.
* @returns {void}
*/
function validateConfigSchema(config, source) {
validateSchema = validateSchema || ajv.compile(configSchema);
if (!validateSchema(config)) {
throw new Error(`ESLint configuration in ${source} is invalid:\n${formatErrors(validateSchema.errors)}`);
}
if (Object.prototype.hasOwnProperty.call(config, "ecmaFeatures")) {
emitEcmaFeaturesWarning(source);
}
}
/**
* Validates an entire config object.
* @param {Object} config The config object to validate.
* @param {string} source The name of the configuration source to report in any errors.
* @param {function(string): {create: Function}} ruleMapper A mapper function from rule IDs to defined rules
* @param {Environments} envContext The env context
* @returns {void}
*/
function validate(config, source, ruleMapper, envContext) {
validateConfigSchema(config, source);
validateRules(config.rules, source, ruleMapper);
validateEnvironment(config.env, source, envContext);
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = {
getRuleOptionsSchema,
validate,
validateRuleOptions
};
+84
View File
@@ -0,0 +1,84 @@
/**
* @fileoverview Environments manager
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const envs = require("../../conf/environments");
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
class Environments {
/**
* create env context
*/
constructor() {
this._environments = new Map();
this.load();
}
/**
* Loads the default environments.
* @returns {void}
* @private
*/
load() {
Object.keys(envs).forEach(envName => {
this._environments.set(envName, envs[envName]);
});
}
/**
* Gets the environment with the given name.
* @param {string} name The name of the environment to retrieve.
* @returns {Object?} The environment object or null if not found.
*/
get(name) {
return this._environments.get(name) || null;
}
/**
* Gets all the environment present
* @returns {Object} The environment object for each env name
*/
getAll() {
return Array.from(this._environments).reduce((coll, env) => {
coll[env[0]] = env[1];
return coll;
}, {});
}
/**
* Defines an environment.
* @param {string} name The name of the environment.
* @param {Object} env The environment settings.
* @returns {void}
*/
define(name, env) {
this._environments.set(name, env);
}
/**
* Imports all environments from a plugin.
* @param {Object} plugin The plugin object.
* @param {string} pluginName The name of the plugin.
* @returns {void}
*/
importPlugin(plugin, pluginName) {
if (plugin.environments) {
Object.keys(plugin.environments).forEach(envName => {
this.define(`${pluginName}/${envName}`, plugin.environments[envName]);
});
}
}
}
module.exports = Environments;
+150
View File
@@ -0,0 +1,150 @@
/**
* @fileoverview Plugins manager
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const debug = require("debug")("eslint:plugins");
const naming = require("../util/naming");
//------------------------------------------------------------------------------
// Private
//------------------------------------------------------------------------------
const PLUGIN_NAME_PREFIX = "eslint-plugin-";
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/**
* Plugin class
*/
class Plugins {
/**
* Creates the plugins context
* @param {Environments} envContext - env context
* @param {Rules} rulesContext - rules context
*/
constructor(envContext, rulesContext) {
this._plugins = Object.create(null);
this._environments = envContext;
this._rules = rulesContext;
}
/**
* Defines a plugin with a given name rather than loading from disk.
* @param {string} pluginName The name of the plugin to load.
* @param {Object} plugin The plugin object.
* @returns {void}
*/
define(pluginName, plugin) {
const pluginNamespace = naming.getNamespaceFromTerm(pluginName),
pluginNameWithoutNamespace = naming.removeNamespaceFromTerm(pluginName),
pluginNameWithoutPrefix = naming.removePrefixFromTerm(PLUGIN_NAME_PREFIX, pluginNameWithoutNamespace),
shortName = pluginNamespace + pluginNameWithoutPrefix;
// load up environments and rules
this._plugins[shortName] = plugin;
this._environments.importPlugin(plugin, shortName);
this._rules.importPlugin(plugin, shortName);
}
/**
* Gets a plugin with the given name.
* @param {string} pluginName The name of the plugin to retrieve.
* @returns {Object} The plugin or null if not loaded.
*/
get(pluginName) {
return this._plugins[pluginName] || null;
}
/**
* Returns all plugins that are loaded.
* @returns {Object} The plugins cache.
*/
getAll() {
return this._plugins;
}
/**
* Loads a plugin with the given name.
* @param {string} pluginName The name of the plugin to load.
* @returns {void}
* @throws {Error} If the plugin cannot be loaded.
*/
load(pluginName) {
const pluginNamespace = naming.getNamespaceFromTerm(pluginName),
pluginNameWithoutNamespace = naming.removeNamespaceFromTerm(pluginName),
pluginNameWithoutPrefix = naming.removePrefixFromTerm(PLUGIN_NAME_PREFIX, pluginNameWithoutNamespace),
shortName = pluginNamespace + pluginNameWithoutPrefix,
longName = pluginNamespace + PLUGIN_NAME_PREFIX + pluginNameWithoutPrefix;
let plugin = null;
if (pluginName.match(/\s+/)) {
const whitespaceError = new Error(`Whitespace found in plugin name '${pluginName}'`);
whitespaceError.messageTemplate = "whitespace-found";
whitespaceError.messageData = {
pluginName: longName
};
throw whitespaceError;
}
if (!this._plugins[shortName]) {
try {
plugin = require(longName);
} catch (pluginLoadErr) {
try {
// Check whether the plugin exists
require.resolve(longName);
} catch (missingPluginErr) {
// If the plugin can't be resolved, display the missing plugin error (usually a config or install error)
debug(`Failed to load plugin ${longName}.`);
missingPluginErr.message = `Failed to load plugin ${pluginName}: ${missingPluginErr.message}`;
missingPluginErr.messageTemplate = "plugin-missing";
missingPluginErr.messageData = {
pluginName: longName
};
throw missingPluginErr;
}
// Otherwise, the plugin exists and is throwing on module load for some reason, so print the stack trace.
throw pluginLoadErr;
}
this.define(pluginName, plugin);
}
}
/**
* Loads all plugins from an array.
* @param {string[]} pluginNames An array of plugins names.
* @returns {void}
* @throws {Error} If a plugin cannot be loaded.
* @throws {Error} If "plugins" in config is not an array
*/
loadAll(pluginNames) {
// if "plugins" in config is not an array, throw an error so user can fix their config.
if (!Array.isArray(pluginNames)) {
const pluginNotArrayMessage = "ESLint configuration error: \"plugins\" value must be an array";
debug(`${pluginNotArrayMessage}: ${JSON.stringify(pluginNames)}`);
throw new Error(pluginNotArrayMessage);
}
// load each plugin by name
pluginNames.forEach(this.load, this);
}
}
module.exports = Plugins;