1 | /**
|
2 | * @fileoverview Used for creating a suggested configuration based on project code.
|
3 | * @author Ian VanSchooten
|
4 | */
|
5 |
|
6 | ;
|
7 |
|
8 | //------------------------------------------------------------------------------
|
9 | // Requirements
|
10 | //------------------------------------------------------------------------------
|
11 |
|
12 | const lodash = require("lodash"),
|
13 | Linter = require("../linter"),
|
14 | configRule = require("./config-rule"),
|
15 | ConfigOps = require("./config-ops"),
|
16 | recConfig = require("../../conf/eslint-recommended");
|
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 | *
|
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 | */
|
52 | function 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 | */
|
71 | class 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 | */
|
334 | function 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 |
|
355 | module.exports = {
|
356 | Registry,
|
357 | extendFromRecommended
|
358 | };
|