1/** 2 * @fileoverview Used for creating a suggested configuration based on project code. 3 * @author Ian VanSchooten 4 */ 5 6"use strict"; 7 8//------------------------------------------------------------------------------ 9// Requirements 10//------------------------------------------------------------------------------ 11 12const lodash = require("lodash"), 13 recConfig = require("../../conf/eslint-recommended"), 14 ConfigOps = require("@eslint/eslintrc/lib/shared/config-ops"), 15 { Linter } = require("../linter"), 16 configRule = require("./config-rule"); 17 18const debug = require("debug")("eslint:autoconfig"); 19const linter = new Linter(); 20 21//------------------------------------------------------------------------------ 22// Data 23//------------------------------------------------------------------------------ 24 25const MAX_CONFIG_COMBINATIONS = 17, // 16 combinations + 1 for severity only 26 RECOMMENDED_CONFIG_NAME = "eslint:recommended"; 27 28//------------------------------------------------------------------------------ 29// Private 30//------------------------------------------------------------------------------ 31 32/** 33 * Information about a rule configuration, in the context of a Registry. 34 * @typedef {Object} registryItem 35 * @param {ruleConfig} config A valid configuration for the rule 36 * @param {number} specificity The number of elements in the ruleConfig array 37 * @param {number} errorCount The number of errors encountered when linting with the config 38 */ 39 40/** 41 * This callback is used to measure execution status in a progress bar 42 * @callback progressCallback 43 * @param {number} The total number of times the callback will be called. 44 */ 45 46/** 47 * Create registryItems for rules 48 * @param {rulesConfig} rulesConfig Hash of rule names and arrays of ruleConfig items 49 * @returns {Object} registryItems for each rule in provided rulesConfig 50 */ 51function makeRegistryItems(rulesConfig) { 52 return Object.keys(rulesConfig).reduce((accumulator, ruleId) => { 53 accumulator[ruleId] = rulesConfig[ruleId].map(config => ({ 54 config, 55 specificity: config.length || 1, 56 errorCount: void 0 57 })); 58 return accumulator; 59 }, {}); 60} 61 62/** 63 * Creates an object in which to store rule configs and error counts 64 * 65 * Unless a rulesConfig is provided at construction, the registry will not contain 66 * any rules, only methods. This will be useful for building up registries manually. 67 * 68 * Registry class 69 */ 70class Registry { 71 72 // eslint-disable-next-line jsdoc/require-description 73 /** 74 * @param {rulesConfig} [rulesConfig] Hash of rule names and arrays of possible configurations 75 */ 76 constructor(rulesConfig) { 77 this.rules = (rulesConfig) ? makeRegistryItems(rulesConfig) : {}; 78 } 79 80 /** 81 * Populate the registry with core rule configs. 82 * 83 * It will set the registry's `rule` property to an object having rule names 84 * as keys and an array of registryItems as values. 85 * @returns {void} 86 */ 87 populateFromCoreRules() { 88 const rulesConfig = configRule.createCoreRuleConfigs(); 89 90 this.rules = makeRegistryItems(rulesConfig); 91 } 92 93 /** 94 * Creates sets of rule configurations which can be used for linting 95 * and initializes registry errors to zero for those configurations (side effect). 96 * 97 * This combines as many rules together as possible, such that the first sets 98 * in the array will have the highest number of rules configured, and later sets 99 * will have fewer and fewer, as not all rules have the same number of possible 100 * configurations. 101 * 102 * The length of the returned array will be <= MAX_CONFIG_COMBINATIONS. 103 * @returns {Object[]} "rules" configurations to use for linting 104 */ 105 buildRuleSets() { 106 let idx = 0; 107 const ruleIds = Object.keys(this.rules), 108 ruleSets = []; 109 110 /** 111 * Add a rule configuration from the registry to the ruleSets 112 * 113 * This is broken out into its own function so that it doesn't need to be 114 * created inside of the while loop. 115 * @param {string} rule The ruleId to add. 116 * @returns {void} 117 */ 118 const addRuleToRuleSet = function(rule) { 119 120 /* 121 * This check ensures that there is a rule configuration and that 122 * it has fewer than the max combinations allowed. 123 * If it has too many configs, we will only use the most basic of 124 * the possible configurations. 125 */ 126 const hasFewCombos = (this.rules[rule].length <= MAX_CONFIG_COMBINATIONS); 127 128 if (this.rules[rule][idx] && (hasFewCombos || this.rules[rule][idx].specificity <= 2)) { 129 130 /* 131 * If the rule has too many possible combinations, only take 132 * simple ones, avoiding objects. 133 */ 134 if (!hasFewCombos && typeof this.rules[rule][idx].config[1] === "object") { 135 return; 136 } 137 138 ruleSets[idx] = ruleSets[idx] || {}; 139 ruleSets[idx][rule] = this.rules[rule][idx].config; 140 141 /* 142 * Initialize errorCount to zero, since this is a config which 143 * will be linted. 144 */ 145 this.rules[rule][idx].errorCount = 0; 146 } 147 }.bind(this); 148 149 while (ruleSets.length === idx) { 150 ruleIds.forEach(addRuleToRuleSet); 151 idx += 1; 152 } 153 154 return ruleSets; 155 } 156 157 /** 158 * Remove all items from the registry with a non-zero number of errors 159 * 160 * Note: this also removes rule configurations which were not linted 161 * (meaning, they have an undefined errorCount). 162 * @returns {void} 163 */ 164 stripFailingConfigs() { 165 const ruleIds = Object.keys(this.rules), 166 newRegistry = new Registry(); 167 168 newRegistry.rules = Object.assign({}, this.rules); 169 ruleIds.forEach(ruleId => { 170 const errorFreeItems = newRegistry.rules[ruleId].filter(registryItem => (registryItem.errorCount === 0)); 171 172 if (errorFreeItems.length > 0) { 173 newRegistry.rules[ruleId] = errorFreeItems; 174 } else { 175 delete newRegistry.rules[ruleId]; 176 } 177 }); 178 179 return newRegistry; 180 } 181 182 /** 183 * Removes rule configurations which were not included in a ruleSet 184 * @returns {void} 185 */ 186 stripExtraConfigs() { 187 const ruleIds = Object.keys(this.rules), 188 newRegistry = new Registry(); 189 190 newRegistry.rules = Object.assign({}, this.rules); 191 ruleIds.forEach(ruleId => { 192 newRegistry.rules[ruleId] = newRegistry.rules[ruleId].filter(registryItem => (typeof registryItem.errorCount !== "undefined")); 193 }); 194 195 return newRegistry; 196 } 197 198 /** 199 * Creates a registry of rules which had no error-free configs. 200 * The new registry is intended to be analyzed to determine whether its rules 201 * should be disabled or set to warning. 202 * @returns {Registry} A registry of failing rules. 203 */ 204 getFailingRulesRegistry() { 205 const ruleIds = Object.keys(this.rules), 206 failingRegistry = new Registry(); 207 208 ruleIds.forEach(ruleId => { 209 const failingConfigs = this.rules[ruleId].filter(registryItem => (registryItem.errorCount > 0)); 210 211 if (failingConfigs && failingConfigs.length === this.rules[ruleId].length) { 212 failingRegistry.rules[ruleId] = failingConfigs; 213 } 214 }); 215 216 return failingRegistry; 217 } 218 219 /** 220 * Create an eslint config for any rules which only have one configuration 221 * in the registry. 222 * @returns {Object} An eslint config with rules section populated 223 */ 224 createConfig() { 225 const ruleIds = Object.keys(this.rules), 226 config = { rules: {} }; 227 228 ruleIds.forEach(ruleId => { 229 if (this.rules[ruleId].length === 1) { 230 config.rules[ruleId] = this.rules[ruleId][0].config; 231 } 232 }); 233 234 return config; 235 } 236 237 /** 238 * Return a cloned registry containing only configs with a desired specificity 239 * @param {number} specificity Only keep configs with this specificity 240 * @returns {Registry} A registry of rules 241 */ 242 filterBySpecificity(specificity) { 243 const ruleIds = Object.keys(this.rules), 244 newRegistry = new Registry(); 245 246 newRegistry.rules = Object.assign({}, this.rules); 247 ruleIds.forEach(ruleId => { 248 newRegistry.rules[ruleId] = this.rules[ruleId].filter(registryItem => (registryItem.specificity === specificity)); 249 }); 250 251 return newRegistry; 252 } 253 254 /** 255 * Lint SourceCodes against all configurations in the registry, and record results 256 * @param {Object[]} sourceCodes SourceCode objects for each filename 257 * @param {Object} config ESLint config object 258 * @param {progressCallback} [cb] Optional callback for reporting execution status 259 * @returns {Registry} New registry with errorCount populated 260 */ 261 lintSourceCode(sourceCodes, config, cb) { 262 let lintedRegistry = new Registry(); 263 264 lintedRegistry.rules = Object.assign({}, this.rules); 265 266 const ruleSets = lintedRegistry.buildRuleSets(); 267 268 lintedRegistry = lintedRegistry.stripExtraConfigs(); 269 270 debug("Linting with all possible rule combinations"); 271 272 const filenames = Object.keys(sourceCodes); 273 const totalFilesLinting = filenames.length * ruleSets.length; 274 275 filenames.forEach(filename => { 276 debug(`Linting file: ${filename}`); 277 278 let ruleSetIdx = 0; 279 280 ruleSets.forEach(ruleSet => { 281 const lintConfig = Object.assign({}, config, { rules: ruleSet }); 282 const lintResults = linter.verify(sourceCodes[filename], lintConfig); 283 284 lintResults.forEach(result => { 285 286 /* 287 * It is possible that the error is from a configuration comment 288 * in a linted file, in which case there may not be a config 289 * set in this ruleSetIdx. 290 * (https://github.com/eslint/eslint/issues/5992) 291 * (https://github.com/eslint/eslint/issues/7860) 292 */ 293 if ( 294 lintedRegistry.rules[result.ruleId] && 295 lintedRegistry.rules[result.ruleId][ruleSetIdx] 296 ) { 297 lintedRegistry.rules[result.ruleId][ruleSetIdx].errorCount += 1; 298 } 299 }); 300 301 ruleSetIdx += 1; 302 303 if (cb) { 304 cb(totalFilesLinting); // eslint-disable-line node/callback-return 305 } 306 }); 307 308 // Deallocate for GC 309 sourceCodes[filename] = null; 310 }); 311 312 return lintedRegistry; 313 } 314} 315 316/** 317 * Extract rule configuration into eslint:recommended where possible. 318 * 319 * This will return a new config with `["extends": [ ..., "eslint:recommended"]` and 320 * only the rules which have configurations different from the recommended config. 321 * @param {Object} config config object 322 * @returns {Object} config object using `"extends": ["eslint:recommended"]` 323 */ 324function extendFromRecommended(config) { 325 const newConfig = Object.assign({}, config); 326 327 ConfigOps.normalizeToStrings(newConfig); 328 329 const recRules = Object.keys(recConfig.rules).filter(ruleId => ConfigOps.isErrorSeverity(recConfig.rules[ruleId])); 330 331 recRules.forEach(ruleId => { 332 if (lodash.isEqual(recConfig.rules[ruleId], newConfig.rules[ruleId])) { 333 delete newConfig.rules[ruleId]; 334 } 335 }); 336 newConfig.extends.unshift(RECOMMENDED_CONFIG_NAME); 337 return newConfig; 338} 339 340 341//------------------------------------------------------------------------------ 342// Public Interface 343//------------------------------------------------------------------------------ 344 345module.exports = { 346 Registry, 347 extendFromRecommended 348}; 349