mirror of
https://github.com/10h30/odin-javascript-exercises.git
synced 2026-07-13 11:46:03 +09:00
remove timer and simon
This commit is contained in:
+359
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
Reference in New Issue
Block a user