• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/**
2 * @fileoverview Create configurations for a rule
3 * @author Ian VanSchooten
4 */
5
6"use strict";
7
8//------------------------------------------------------------------------------
9// Requirements
10//------------------------------------------------------------------------------
11
12const builtInRules = require("../rules");
13
14//------------------------------------------------------------------------------
15// Helpers
16//------------------------------------------------------------------------------
17
18/**
19 * Wrap all of the elements of an array into arrays.
20 * @param   {*[]}     xs Any array.
21 * @returns {Array[]}    An array of arrays.
22 */
23function explodeArray(xs) {
24    return xs.reduce((accumulator, x) => {
25        accumulator.push([x]);
26        return accumulator;
27    }, []);
28}
29
30/**
31 * Mix two arrays such that each element of the second array is concatenated
32 * onto each element of the first array.
33 *
34 * For example:
35 * combineArrays([a, [b, c]], [x, y]); // -> [[a, x], [a, y], [b, c, x], [b, c, y]]
36 * @param   {Array} arr1 The first array to combine.
37 * @param   {Array} arr2 The second array to combine.
38 * @returns {Array}      A mixture of the elements of the first and second arrays.
39 */
40function combineArrays(arr1, arr2) {
41    const res = [];
42
43    if (arr1.length === 0) {
44        return explodeArray(arr2);
45    }
46    if (arr2.length === 0) {
47        return explodeArray(arr1);
48    }
49    arr1.forEach(x1 => {
50        arr2.forEach(x2 => {
51            res.push([].concat(x1, x2));
52        });
53    });
54    return res;
55}
56
57/**
58 * Group together valid rule configurations based on object properties
59 *
60 * e.g.:
61 * groupByProperty([
62 *     {before: true},
63 *     {before: false},
64 *     {after: true},
65 *     {after: false}
66 * ]);
67 *
68 * will return:
69 * [
70 *     [{before: true}, {before: false}],
71 *     [{after: true}, {after: false}]
72 * ]
73 * @param   {Object[]} objects Array of objects, each with one property/value pair
74 * @returns {Array[]}          Array of arrays of objects grouped by property
75 */
76function groupByProperty(objects) {
77    const groupedObj = objects.reduce((accumulator, obj) => {
78        const prop = Object.keys(obj)[0];
79
80        accumulator[prop] = accumulator[prop] ? accumulator[prop].concat(obj) : [obj];
81        return accumulator;
82    }, {});
83
84    return Object.keys(groupedObj).map(prop => groupedObj[prop]);
85}
86
87
88//------------------------------------------------------------------------------
89// Private
90//------------------------------------------------------------------------------
91
92/**
93 * Configuration settings for a rule.
94 *
95 * A configuration can be a single number (severity), or an array where the first
96 * element in the array is the severity, and is the only required element.
97 * Configs may also have one or more additional elements to specify rule
98 * configuration or options.
99 * @typedef {Array|number} ruleConfig
100 * @param {number}  0  The rule's severity (0, 1, 2).
101 */
102
103/**
104 * Object whose keys are rule names and values are arrays of valid ruleConfig items
105 * which should be linted against the target source code to determine error counts.
106 * (a ruleConfigSet.ruleConfigs).
107 *
108 * e.g. rulesConfig = {
109 *     "comma-dangle": [2, [2, "always"], [2, "always-multiline"], [2, "never"]],
110 *     "no-console": [2]
111 * }
112 * @typedef rulesConfig
113 */
114
115
116/**
117 * Create valid rule configurations by combining two arrays,
118 * with each array containing multiple objects each with a
119 * single property/value pair and matching properties.
120 *
121 * e.g.:
122 * combinePropertyObjects(
123 *     [{before: true}, {before: false}],
124 *     [{after: true}, {after: false}]
125 * );
126 *
127 * will return:
128 * [
129 *     {before: true, after: true},
130 *     {before: true, after: false},
131 *     {before: false, after: true},
132 *     {before: false, after: false}
133 * ]
134 * @param   {Object[]} objArr1 Single key/value objects, all with the same key
135 * @param   {Object[]} objArr2 Single key/value objects, all with another key
136 * @returns {Object[]}         Combined objects for each combination of input properties and values
137 */
138function combinePropertyObjects(objArr1, objArr2) {
139    const res = [];
140
141    if (objArr1.length === 0) {
142        return objArr2;
143    }
144    if (objArr2.length === 0) {
145        return objArr1;
146    }
147    objArr1.forEach(obj1 => {
148        objArr2.forEach(obj2 => {
149            const combinedObj = {};
150            const obj1Props = Object.keys(obj1);
151            const obj2Props = Object.keys(obj2);
152
153            obj1Props.forEach(prop1 => {
154                combinedObj[prop1] = obj1[prop1];
155            });
156            obj2Props.forEach(prop2 => {
157                combinedObj[prop2] = obj2[prop2];
158            });
159            res.push(combinedObj);
160        });
161    });
162    return res;
163}
164
165/**
166 * Creates a new instance of a rule configuration set
167 *
168 * A rule configuration set is an array of configurations that are valid for a
169 * given rule.  For example, the configuration set for the "semi" rule could be:
170 *
171 * ruleConfigSet.ruleConfigs // -> [[2], [2, "always"], [2, "never"]]
172 *
173 * Rule configuration set class
174 */
175class RuleConfigSet {
176
177    // eslint-disable-next-line jsdoc/require-description
178    /**
179     * @param {ruleConfig[]} configs Valid rule configurations
180     */
181    constructor(configs) {
182
183        /**
184         * Stored valid rule configurations for this instance
185         * @type {Array}
186         */
187        this.ruleConfigs = configs || [];
188    }
189
190    /**
191     * Add a severity level to the front of all configs in the instance.
192     * This should only be called after all configs have been added to the instance.
193     * @returns {void}
194     */
195    addErrorSeverity() {
196        const severity = 2;
197
198        this.ruleConfigs = this.ruleConfigs.map(config => {
199            config.unshift(severity);
200            return config;
201        });
202
203        // Add a single config at the beginning consisting of only the severity
204        this.ruleConfigs.unshift(severity);
205    }
206
207    /**
208     * Add rule configs from an array of strings (schema enums)
209     * @param  {string[]} enums Array of valid rule options (e.g. ["always", "never"])
210     * @returns {void}
211     */
212    addEnums(enums) {
213        this.ruleConfigs = this.ruleConfigs.concat(combineArrays(this.ruleConfigs, enums));
214    }
215
216    /**
217     * Add rule configurations from a schema object
218     * @param  {Object} obj Schema item with type === "object"
219     * @returns {boolean} true if at least one schema for the object could be generated, false otherwise
220     */
221    addObject(obj) {
222        const objectConfigSet = {
223            objectConfigs: [],
224            add(property, values) {
225                for (let idx = 0; idx < values.length; idx++) {
226                    const optionObj = {};
227
228                    optionObj[property] = values[idx];
229                    this.objectConfigs.push(optionObj);
230                }
231            },
232
233            combine() {
234                this.objectConfigs = groupByProperty(this.objectConfigs).reduce((accumulator, objArr) => combinePropertyObjects(accumulator, objArr), []);
235            }
236        };
237
238        /*
239         * The object schema could have multiple independent properties.
240         * If any contain enums or booleans, they can be added and then combined
241         */
242        Object.keys(obj.properties).forEach(prop => {
243            if (obj.properties[prop].enum) {
244                objectConfigSet.add(prop, obj.properties[prop].enum);
245            }
246            if (obj.properties[prop].type && obj.properties[prop].type === "boolean") {
247                objectConfigSet.add(prop, [true, false]);
248            }
249        });
250        objectConfigSet.combine();
251
252        if (objectConfigSet.objectConfigs.length > 0) {
253            this.ruleConfigs = this.ruleConfigs.concat(combineArrays(this.ruleConfigs, objectConfigSet.objectConfigs));
254            return true;
255        }
256
257        return false;
258    }
259}
260
261/**
262 * Generate valid rule configurations based on a schema object
263 * @param   {Object} schema  A rule's schema object
264 * @returns {Array[]}        Valid rule configurations
265 */
266function generateConfigsFromSchema(schema) {
267    const configSet = new RuleConfigSet();
268
269    if (Array.isArray(schema)) {
270        for (const opt of schema) {
271            if (opt.enum) {
272                configSet.addEnums(opt.enum);
273            } else if (opt.type && opt.type === "object") {
274                if (!configSet.addObject(opt)) {
275                    break;
276                }
277
278            // TODO (IanVS): support oneOf
279            } else {
280
281                // If we don't know how to fill in this option, don't fill in any of the following options.
282                break;
283            }
284        }
285    }
286    configSet.addErrorSeverity();
287    return configSet.ruleConfigs;
288}
289
290/**
291 * Generate possible rule configurations for all of the core rules
292 * @param {boolean} noDeprecated Indicates whether ignores deprecated rules or not.
293 * @returns {rulesConfig} Hash of rule names and arrays of possible configurations
294 */
295function createCoreRuleConfigs(noDeprecated = false) {
296    return Array.from(builtInRules).reduce((accumulator, [id, rule]) => {
297        const schema = (typeof rule === "function") ? rule.schema : rule.meta.schema;
298        const isDeprecated = (typeof rule === "function") ? rule.deprecated : rule.meta.deprecated;
299
300        if (noDeprecated && isDeprecated) {
301            return accumulator;
302        }
303
304        accumulator[id] = generateConfigsFromSchema(schema);
305        return accumulator;
306    }, {});
307}
308
309
310//------------------------------------------------------------------------------
311// Public Interface
312//------------------------------------------------------------------------------
313
314module.exports = {
315    generateConfigsFromSchema,
316    createCoreRuleConfigs
317};
318