]>
Commit | Line | Data |
---|---|---|
eb39fafa DC |
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 | ||
5422a9cc | 12 | const equal = require("fast-deep-equal"), |
eb39fafa | 13 | recConfig = require("../../conf/eslint-recommended"), |
6f036462 | 14 | ConfigOps = require("@eslint/eslintrc/lib/shared/config-ops"), |
eb39fafa DC |
15 | { Linter } = require("../linter"), |
16 | configRule = require("./config-rule"); | |
17 | ||
18 | const debug = require("debug")("eslint:autoconfig"); | |
19 | const linter = new Linter(); | |
20 | ||
21 | //------------------------------------------------------------------------------ | |
22 | // Data | |
23 | //------------------------------------------------------------------------------ | |
24 | ||
25 | const 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 | */ | |
51 | function 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 | */ | |
70 | class 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() { | |
5422a9cc | 88 | const rulesConfig = configRule.createCoreRuleConfigs(/* noDeprecated = */ true); |
eb39fafa DC |
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) { | |
56c4a2cb | 304 | cb(totalFilesLinting); // eslint-disable-line node/callback-return |
eb39fafa DC |
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 | * | |
56c4a2cb | 319 | * This will return a new config with `["extends": [ ..., "eslint:recommended"]` and |
eb39fafa DC |
320 | * only the rules which have configurations different from the recommended config. |
321 | * @param {Object} config config object | |
56c4a2cb | 322 | * @returns {Object} config object using `"extends": ["eslint:recommended"]` |
eb39fafa DC |
323 | */ |
324 | function 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 => { | |
5422a9cc | 332 | if (equal(recConfig.rules[ruleId], newConfig.rules[ruleId])) { |
eb39fafa DC |
333 | delete newConfig.rules[ruleId]; |
334 | } | |
335 | }); | |
56c4a2cb | 336 | newConfig.extends.unshift(RECOMMENDED_CONFIG_NAME); |
eb39fafa DC |
337 | return newConfig; |
338 | } | |
339 | ||
340 | ||
341 | //------------------------------------------------------------------------------ | |
342 | // Public Interface | |
343 | //------------------------------------------------------------------------------ | |
344 | ||
345 | module.exports = { | |
346 | Registry, | |
347 | extendFromRecommended | |
348 | }; |