UNPKG

12.2 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 * @returns {Object[]} "rules" configurations to use for linting
106 */
107 buildRuleSets() {
108 let idx = 0;
109 const ruleIds = Object.keys(this.rules),
110 ruleSets = [];
111
112 /**
113 * Add a rule configuration from the registry to the ruleSets
114 *
115 * This is broken out into its own function so that it doesn't need to be
116 * created inside of the while loop.
117 *
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 *
166 * @returns {void}
167 */
168 stripFailingConfigs() {
169 const ruleIds = Object.keys(this.rules),
170 newRegistry = new Registry();
171
172 newRegistry.rules = Object.assign({}, this.rules);
173 ruleIds.forEach(ruleId => {
174 const errorFreeItems = newRegistry.rules[ruleId].filter(registryItem => (registryItem.errorCount === 0));
175
176 if (errorFreeItems.length > 0) {
177 newRegistry.rules[ruleId] = errorFreeItems;
178 } else {
179 delete newRegistry.rules[ruleId];
180 }
181 });
182
183 return newRegistry;
184 }
185
186 /**
187 * Removes rule configurations which were not included in a ruleSet
188 *
189 * @returns {void}
190 */
191 stripExtraConfigs() {
192 const ruleIds = Object.keys(this.rules),
193 newRegistry = new Registry();
194
195 newRegistry.rules = Object.assign({}, this.rules);
196 ruleIds.forEach(ruleId => {
197 newRegistry.rules[ruleId] = newRegistry.rules[ruleId].filter(registryItem => (typeof registryItem.errorCount !== "undefined"));
198 });
199
200 return newRegistry;
201 }
202
203 /**
204 * Creates a registry of rules which had no error-free configs.
205 * The new registry is intended to be analyzed to determine whether its rules
206 * should be disabled or set to warning.
207 *
208 * @returns {Registry} A registry of failing rules.
209 */
210 getFailingRulesRegistry() {
211 const ruleIds = Object.keys(this.rules),
212 failingRegistry = new Registry();
213
214 ruleIds.forEach(ruleId => {
215 const failingConfigs = this.rules[ruleId].filter(registryItem => (registryItem.errorCount > 0));
216
217 if (failingConfigs && failingConfigs.length === this.rules[ruleId].length) {
218 failingRegistry.rules[ruleId] = failingConfigs;
219 }
220 });
221
222 return failingRegistry;
223 }
224
225 /**
226 * Create an eslint config for any rules which only have one configuration
227 * in the registry.
228 *
229 * @returns {Object} An eslint config with rules section populated
230 */
231 createConfig() {
232 const ruleIds = Object.keys(this.rules),
233 config = { rules: {} };
234
235 ruleIds.forEach(ruleId => {
236 if (this.rules[ruleId].length === 1) {
237 config.rules[ruleId] = this.rules[ruleId][0].config;
238 }
239 });
240
241 return config;
242 }
243
244 /**
245 * Return a cloned registry containing only configs with a desired specificity
246 *
247 * @param {number} specificity Only keep configs with this specificity
248 * @returns {Registry} A registry of rules
249 */
250 filterBySpecificity(specificity) {
251 const ruleIds = Object.keys(this.rules),
252 newRegistry = new Registry();
253
254 newRegistry.rules = Object.assign({}, this.rules);
255 ruleIds.forEach(ruleId => {
256 newRegistry.rules[ruleId] = this.rules[ruleId].filter(registryItem => (registryItem.specificity === specificity));
257 });
258
259 return newRegistry;
260 }
261
262 /**
263 * Lint SourceCodes against all configurations in the registry, and record results
264 *
265 * @param {Object[]} sourceCodes SourceCode objects for each filename
266 * @param {Object} config ESLint config object
267 * @param {progressCallback} [cb] Optional callback for reporting execution status
268 * @returns {Registry} New registry with errorCount populated
269 */
270 lintSourceCode(sourceCodes, config, cb) {
271 let lintedRegistry = new Registry();
272
273 lintedRegistry.rules = Object.assign({}, this.rules);
274
275 const ruleSets = lintedRegistry.buildRuleSets();
276
277 lintedRegistry = lintedRegistry.stripExtraConfigs();
278
279 debug("Linting with all possible rule combinations");
280
281 const filenames = Object.keys(sourceCodes);
282 const totalFilesLinting = filenames.length * ruleSets.length;
283
284 filenames.forEach(filename => {
285 debug(`Linting file: ${filename}`);
286
287 let ruleSetIdx = 0;
288
289 ruleSets.forEach(ruleSet => {
290 const lintConfig = Object.assign({}, config, { rules: ruleSet });
291 const lintResults = linter.verify(sourceCodes[filename], lintConfig);
292
293 lintResults.forEach(result => {
294
295 /*
296 * It is possible that the error is from a configuration comment
297 * in a linted file, in which case there may not be a config
298 * set in this ruleSetIdx.
299 * (https://github.com/eslint/eslint/issues/5992)
300 * (https://github.com/eslint/eslint/issues/7860)
301 */
302 if (
303 lintedRegistry.rules[result.ruleId] &&
304 lintedRegistry.rules[result.ruleId][ruleSetIdx]
305 ) {
306 lintedRegistry.rules[result.ruleId][ruleSetIdx].errorCount += 1;
307 }
308 });
309
310 ruleSetIdx += 1;
311
312 if (cb) {
313 cb(totalFilesLinting); // eslint-disable-line callback-return
314 }
315 });
316
317 // Deallocate for GC
318 sourceCodes[filename] = null;
319 });
320
321 return lintedRegistry;
322 }
323}
324
325/**
326 * Extract rule configuration into eslint:recommended where possible.
327 *
328 * This will return a new config with `"extends": "eslint:recommended"` and
329 * only the rules which have configurations different from the recommended config.
330 *
331 * @param {Object} config config object
332 * @returns {Object} config object using `"extends": "eslint:recommended"`
333 */
334function extendFromRecommended(config) {
335 const newConfig = Object.assign({}, config);
336
337 ConfigOps.normalizeToStrings(newConfig);
338
339 const recRules = Object.keys(recConfig.rules).filter(ruleId => ConfigOps.isErrorSeverity(recConfig.rules[ruleId]));
340
341 recRules.forEach(ruleId => {
342 if (lodash.isEqual(recConfig.rules[ruleId], newConfig.rules[ruleId])) {
343 delete newConfig.rules[ruleId];
344 }
345 });
346 newConfig.extends = RECOMMENDED_CONFIG_NAME;
347 return newConfig;
348}
349
350
351//------------------------------------------------------------------------------
352// Public Interface
353//------------------------------------------------------------------------------
354
355module.exports = {
356 Registry,
357 extendFromRecommended
358};