Spaces:
Runtime error
Runtime error
/** | |
* @fileoverview Used for creating a suggested configuration based on project code. | |
* @author Ian VanSchooten | |
*/ | |
; | |
//------------------------------------------------------------------------------ | |
// Requirements | |
//------------------------------------------------------------------------------ | |
const lodash = require("lodash"), | |
recConfig = require("../../conf/eslint-recommended"), | |
ConfigOps = require("@eslint/eslintrc/lib/shared/config-ops"), | |
{ Linter } = require("../linter"), | |
configRule = require("./config-rule"); | |
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 { | |
// eslint-disable-next-line jsdoc/require-description | |
/** | |
* @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. | |
* @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 node/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.unshift(RECOMMENDED_CONFIG_NAME); | |
return newConfig; | |
} | |
//------------------------------------------------------------------------------ | |
// Public Interface | |
//------------------------------------------------------------------------------ | |
module.exports = { | |
Registry, | |
extendFromRecommended | |
}; | |