| /** |
| * @fileoverview Used for creating a suggested configuration based on project code. |
| * @author Ian VanSchooten |
| */ |
| |
| "use strict"; |
| |
| //------------------------------------------------------------------------------ |
| // Requirements |
| //------------------------------------------------------------------------------ |
| |
| const lodash = require("lodash"), |
| recConfig = require("../../conf/eslint-recommended"), |
| ConfigOps = require("../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 { |
| |
| /** |
| * @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 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 |
| }; |