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.

322 lines
9.4 KiB

/**
* @fileoverview Create configurations for a rule
* @author Ian VanSchooten
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const rules = require("../rules"),
loadRules = require("../load-rules");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Wrap all of the elements of an array into arrays.
* @param {*[]} xs Any array.
* @returns {Array[]} An array of arrays.
*/
function explodeArray(xs) {
return xs.reduce((accumulator, x) => {
accumulator.push([x]);
return accumulator;
}, []);
}
/**
* Mix two arrays such that each element of the second array is concatenated
* onto each element of the first array.
*
* For example:
* combineArrays([a, [b, c]], [x, y]); // -> [[a, x], [a, y], [b, c, x], [b, c, y]]
*
* @param {array} arr1 The first array to combine.
* @param {array} arr2 The second array to combine.
* @returns {array} A mixture of the elements of the first and second arrays.
*/
function combineArrays(arr1, arr2) {
const res = [];
if (arr1.length === 0) {
return explodeArray(arr2);
}
if (arr2.length === 0) {
return explodeArray(arr1);
}
arr1.forEach(x1 => {
arr2.forEach(x2 => {
res.push([].concat(x1, x2));
});
});
return res;
}
/**
* Group together valid rule configurations based on object properties
*
* e.g.:
* groupByProperty([
* {before: true},
* {before: false},
* {after: true},
* {after: false}
* ]);
*
* will return:
* [
* [{before: true}, {before: false}],
* [{after: true}, {after: false}]
* ]
*
* @param {Object[]} objects Array of objects, each with one property/value pair
* @returns {Array[]} Array of arrays of objects grouped by property
*/
function groupByProperty(objects) {
const groupedObj = objects.reduce((accumulator, obj) => {
const prop = Object.keys(obj)[0];
accumulator[prop] = accumulator[prop] ? accumulator[prop].concat(obj) : [obj];
return accumulator;
}, {});
return Object.keys(groupedObj).map(prop => groupedObj[prop]);
}
//------------------------------------------------------------------------------
// Private
//------------------------------------------------------------------------------
/**
* Configuration settings for a rule.
*
* A configuration can be a single number (severity), or an array where the first
* element in the array is the severity, and is the only required element.
* Configs may also have one or more additional elements to specify rule
* configuration or options.
*
* @typedef {array|number} ruleConfig
* @param {number} 0 The rule's severity (0, 1, 2).
*/
/**
* Object whose keys are rule names and values are arrays of valid ruleConfig items
* which should be linted against the target source code to determine error counts.
* (a ruleConfigSet.ruleConfigs).
*
* e.g. rulesConfig = {
* "comma-dangle": [2, [2, "always"], [2, "always-multiline"], [2, "never"]],
* "no-console": [2]
* }
* @typedef rulesConfig
*/
/**
* Create valid rule configurations by combining two arrays,
* with each array containing multiple objects each with a
* single property/value pair and matching properties.
*
* e.g.:
* combinePropertyObjects(
* [{before: true}, {before: false}],
* [{after: true}, {after: false}]
* );
*
* will return:
* [
* {before: true, after: true},
* {before: true, after: false},
* {before: false, after: true},
* {before: false, after: false}
* ]
*
* @param {Object[]} objArr1 Single key/value objects, all with the same key
* @param {Object[]} objArr2 Single key/value objects, all with another key
* @returns {Object[]} Combined objects for each combination of input properties and values
*/
function combinePropertyObjects(objArr1, objArr2) {
const res = [];
if (objArr1.length === 0) {
return objArr2;
}
if (objArr2.length === 0) {
return objArr1;
}
objArr1.forEach(obj1 => {
objArr2.forEach(obj2 => {
const combinedObj = {};
const obj1Props = Object.keys(obj1);
const obj2Props = Object.keys(obj2);
obj1Props.forEach(prop1 => {
combinedObj[prop1] = obj1[prop1];
});
obj2Props.forEach(prop2 => {
combinedObj[prop2] = obj2[prop2];
});
res.push(combinedObj);
});
});
return res;
}
/**
* Creates a new instance of a rule configuration set
*
* A rule configuration set is an array of configurations that are valid for a
* given rule. For example, the configuration set for the "semi" rule could be:
*
* ruleConfigSet.ruleConfigs // -> [[2], [2, "always"], [2, "never"]]
*
* Rule configuration set class
*/
class RuleConfigSet {
/**
* @param {ruleConfig[]} configs Valid rule configurations
*/
constructor(configs) {
/**
* Stored valid rule configurations for this instance
* @type {array}
*/
this.ruleConfigs = configs || [];
}
/**
* Add a severity level to the front of all configs in the instance.
* This should only be called after all configs have been added to the instance.
*
* @param {number} [severity=2] The level of severity for the rule (0, 1, 2)
* @returns {void}
*/
addErrorSeverity(severity) {
severity = severity || 2;
this.ruleConfigs = this.ruleConfigs.map(config => {
config.unshift(severity);
return config;
});
// Add a single config at the beginning consisting of only the severity
this.ruleConfigs.unshift(severity);
}
/**
* Add rule configs from an array of strings (schema enums)
* @param {string[]} enums Array of valid rule options (e.g. ["always", "never"])
* @returns {void}
*/
addEnums(enums) {
this.ruleConfigs = this.ruleConfigs.concat(combineArrays(this.ruleConfigs, enums));
}
/**
* Add rule configurations from a schema object
* @param {Object} obj Schema item with type === "object"
* @returns {boolean} true if at least one schema for the object could be generated, false otherwise
*/
addObject(obj) {
const objectConfigSet = {
objectConfigs: [],
add(property, values) {
for (let idx = 0; idx < values.length; idx++) {
const optionObj = {};
optionObj[property] = values[idx];
this.objectConfigs.push(optionObj);
}
},
combine() {
this.objectConfigs = groupByProperty(this.objectConfigs).reduce((accumulator, objArr) => combinePropertyObjects(accumulator, objArr), []);
}
};
/*
* The object schema could have multiple independent properties.
* If any contain enums or booleans, they can be added and then combined
*/
Object.keys(obj.properties).forEach(prop => {
if (obj.properties[prop].enum) {
objectConfigSet.add(prop, obj.properties[prop].enum);
}
if (obj.properties[prop].type && obj.properties[prop].type === "boolean") {
objectConfigSet.add(prop, [true, false]);
}
});
objectConfigSet.combine();
if (objectConfigSet.objectConfigs.length > 0) {
this.ruleConfigs = this.ruleConfigs.concat(combineArrays(this.ruleConfigs, objectConfigSet.objectConfigs));
return true;
}
return false;
}
}
/**
* Generate valid rule configurations based on a schema object
* @param {Object} schema A rule's schema object
* @returns {array[]} Valid rule configurations
*/
function generateConfigsFromSchema(schema) {
const configSet = new RuleConfigSet();
if (Array.isArray(schema)) {
for (const opt of schema) {
if (opt.enum) {
configSet.addEnums(opt.enum);
} else if (opt.type && opt.type === "object") {
if (!configSet.addObject(opt)) {
break;
}
// TODO (IanVS): support oneOf
} else {
// If we don't know how to fill in this option, don't fill in any of the following options.
break;
}
}
}
configSet.addErrorSeverity();
return configSet.ruleConfigs;
}
/**
* Generate possible rule configurations for all of the core rules
* @returns {rulesConfig} Hash of rule names and arrays of possible configurations
*/
function createCoreRuleConfigs() {
const ruleList = loadRules();
return Object.keys(ruleList).reduce((accumulator, id) => {
const rule = rules.get(id);
const schema = (typeof rule === "function") ? rule.schema : rule.meta.schema;
accumulator[id] = generateConfigsFromSchema(schema);
return accumulator;
}, {});
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = {
generateConfigsFromSchema,
createCoreRuleConfigs
};