UNPKG

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