1 | /**
|
2 | * @fileoverview Create configurations for a rule
|
3 | * @author Ian VanSchooten
|
4 | */
|
5 |
|
6 | ;
|
7 |
|
8 | //------------------------------------------------------------------------------
|
9 | // Requirements
|
10 | //------------------------------------------------------------------------------
|
11 |
|
12 | const 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 | */
|
23 | function 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 | */
|
40 | function 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 | */
|
76 | function 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 | */
|
138 | function 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 | */
|
175 | class 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 | */
|
266 | function 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 | */
|
295 | function 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 |
|
314 | module.exports = {
|
315 | generateConfigsFromSchema,
|
316 | createCoreRuleConfigs
|
317 | };
|