diff --git a/lib/config-validator.js b/lib/config-validator.js new file mode 100644 index 00000000000..cece433c7cc --- /dev/null +++ b/lib/config-validator.js @@ -0,0 +1,110 @@ +/** + * @fileoverview Validates configs. + * @author Brandon Mills + * @copyright 2015 Brandon Mills + */ + +"use strict"; + +var rules = require("./rules"), + schemaValidator = require("is-my-json-valid"); + +var validators = { + rules: Object.create(null) +}; + +/** + * Gets a complete options schema for a rule. + * @param {string} id The rule's unique name. + * @returns {object} JSON Schema for the rule's options. + */ +function getRuleOptionsSchema(id) { + var rule = rules.get(id), + schema = rule && rule.schema; + + if (!schema) { + return { + "type": "array", + "items": [ + { + "enum": [0, 1, 2] + } + ], + "minItems": 1 + }; + } + + // Given a tuple of schemas, insert warning level at the beginning + if (Array.isArray(schema)) { + return { + "type": "array", + "items": [ + { + "enum": [0, 1, 2] + } + ].concat(schema), + "minItems": 1, + "maxItems": schema.length + 1 + }; + } + + // Given a full schema, leave it alone + return schema; +} + +/** + * Validates a rule's options against its schema. + * @param {string} id The rule's unique name. + * @param {array|number} options The given options for the rule. + * @param {string} source The name of the configuration source. + * @returns {void} + */ +function validateRuleOptions(id, options, source) { + var validateRule = validators.rules[id], + message; + + if (!validateRule) { + validateRule = schemaValidator(getRuleOptionsSchema(id), { verbose: true }); + validators.rules[id] = validateRule; + } + + if (typeof options === "number") { + options = [options]; + } + + validateRule(options); + + if (validateRule.errors) { + message = [ + source, ":\n", + "\tConfiguration for rule \"", id, "\" is invalid:\n" + ]; + validateRule.errors.forEach(function (error) { + message.push( + "\tValue \"", error.value, "\" ", error.message, ".\n" + ); + }); + + throw new Error(message.join("")); + } +} + +/** + * Validates an entire config object. + * @param {object} config The config object to validate. + * @param {string} source The location to report with any errors. + * @returns {void} + */ +function validate(config, source) { + if (typeof config.rules === "object") { + Object.keys(config.rules).forEach(function (id) { + validateRuleOptions(id, config.rules[id], source); + }); + } +} + +module.exports = { + getRuleOptionsSchema: getRuleOptionsSchema, + validate: validate, + validateRuleOptions: validateRuleOptions +}; diff --git a/package.json b/package.json index 57999837434..ecc546fda5f 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "estraverse-fb": "^1.3.1", "globals": "^6.1.0", "inquirer": "^0.8.2", + "is-my-json-valid": "^2.10.0", "js-yaml": "^3.2.5", "minimatch": "^2.0.1", "mkdirp": "^0.5.0", diff --git a/tests/lib/config-validator.js b/tests/lib/config-validator.js new file mode 100644 index 00000000000..83f91ad5501 --- /dev/null +++ b/tests/lib/config-validator.js @@ -0,0 +1,135 @@ +/** + * @fileoverview Tests for config validator. + * @author Brandon Mills + * @copyright 2015 Brandon Mills + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +var assert = require("chai").assert, + eslint = require("../../lib/eslint"), + validator = require("../../lib/config-validator"); + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +function mockRule(context) { + return { + "Program": function(node) { + context.report(node, "Expected a validation error."); + } + }; +} + +mockRule.schema = [ + { + "enum": ["first", "second"] + } +]; + +describe("Validator", function() { + + beforeEach(function() { + eslint.defineRule("mock-rule", mockRule); + }); + + describe("validate", function() { + + it("should do nothing with an empty config", function() { + var fn = validator.validate.bind(null, {}, "tests"); + + assert.doesNotThrow(fn); + }); + + it("should do nothing with an empty rules object", function() { + var fn = validator.validate.bind(null, { rules: {} }, "tests"); + + assert.doesNotThrow(fn); + }); + + it("should do nothing with a valid config", function() { + var fn = validator.validate.bind(null, { rules: { "mock-rule": [2, "second"] } }, "tests"); + + assert.doesNotThrow(fn); + }); + + it("should catch invalid rule options", function() { + var fn = validator.validate.bind(null, { rules: { "mock-rule": [3, "third"] } }, "tests"); + + assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tValue \"3\" must be an enum value.\n\tValue \"third\" must be an enum value.\n"); + }); + + }); + + describe("getRuleOptionsSchema", function() { + + it("should return a default schema for a missing rule", function() { + assert.deepEqual(validator.getRuleOptionsSchema("non-existent-rule"), { + "type": "array", + "items": [ + { + "enum": [0, 1, 2] + } + ], + "minItems": 1 + }); + }); + + it("should add warning level validation to provided schemas", function() { + assert.deepEqual(validator.getRuleOptionsSchema("mock-rule"), { + "type": "array", + "items": [ + { + "enum": [0, 1, 2] + }, + { + "enum": ["first", "second"] + } + ], + "minItems": 1, + "maxItems": 2 + }); + }); + + }); + + describe("validateRuleOptions", function() { + + it("should throw for incorrect warning level", function() { + var fn = validator.validateRuleOptions.bind(null, "mock-rule", 3, "tests"); + + assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tValue \"3\" must be an enum value.\n"); + }); + + it("should only check warning level for nonexistent rules", function() { + var fn = validator.validateRuleOptions.bind(null, "non-existent-rule", [3, "foobar"], "tests"); + + assert.throws(fn, "tests:\n\tConfiguration for rule \"non-existent-rule\" is invalid:\n\tValue \"3\" must be an enum value.\n"); + }); + + it("should only check warning level for plugin rules", function() { + var fn = validator.validateRuleOptions.bind(null, "plugin/rule", 3, "tests"); + + assert.throws(fn, "tests:\n\tConfiguration for rule \"plugin/rule\" is invalid:\n\tValue \"3\" must be an enum value.\n"); + }); + + it("should throw for incorrect configuration values", function() { + var fn = validator.validateRuleOptions.bind(null, "mock-rule", [2, "frist"], "tests"); + + assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tValue \"frist\" must be an enum value.\n"); + }); + + it("should throw for too many configuration values", function() { + var fn = validator.validateRuleOptions.bind(null, "mock-rule", [2, "first", "second"], "tests"); + + assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tValue \"2,first,second\" has more items than allowed.\n"); + }); + + }); + +});