• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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    recConfig = require("../../conf/eslint-recommended"),
14    ConfigOps = require("@eslint/eslintrc/lib/shared/config-ops"),
15    { Linter } = require("../linter"),
16    configRule = require("./config-rule");
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 * @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 */
51function 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 */
70class 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 node/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 */
324function 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.unshift(RECOMMENDED_CONFIG_NAME);
337    return newConfig;
338}
339
340
341//------------------------------------------------------------------------------
342// Public Interface
343//------------------------------------------------------------------------------
344
345module.exports = {
346    Registry,
347    extendFromRecommended
348};
349