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 | * @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 | */
|
335 | function 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 |
|
356 | module.exports = {
|
357 | Registry,
|
358 | extendFromRecommended
|
359 | };
|