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