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