You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

317 lines
9.5 KiB

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