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