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