From 4b1ec7ed949ac909982ec0a42604f6b574c6ff58 Mon Sep 17 00:00:00 2001 From: Brandon Mills Date: Wed, 13 May 2015 11:00:31 -0400 Subject: [PATCH] New: Add config validator (refs #2179) --- lib/config-validator.js | 99 +++++++++++++++++++++++++++++++++++ package.json | 1 + tests/lib/config-validator.js | 78 +++++++++++++++++++++++++++ 3 files changed, 178 insertions(+) create mode 100644 lib/config-validator.js create mode 100644 tests/lib/config-validator.js diff --git a/lib/config-validator.js b/lib/config-validator.js new file mode 100644 index 00000000000..db0185836de --- /dev/null +++ b/lib/config-validator.js @@ -0,0 +1,99 @@ +/** + * @fileoverview Validates configs. + * @author Brandon Mills + * @copyright 2015 Brandon Mills + */ + +"use strict"; + +var rules = require("./rules"), + schemaValidator = require("is-my-json-valid"); + +var optionsValidators = Object.create(null); + +/** + * Converts a rule's exported, abbreviated schema into a full schema. + * @param {object} options Exported schema from a rule. + * @returns {object} Full schema ready for validation. + */ +function makeRuleOptionsSchema(options) { + + // If no schema, only validate warning level, and permit anything after + if (!options) { + return { + "type": "array", + "items": [ + { + "enum": [0, 1, 2] + } + ], + "minItems": 1 + }; + } + + // Given a tuple of schemas, insert warning level at the beginning + if (Array.isArray(options)) { + return { + "type": "array", + "items": [ + { + "enum": [0, 1, 2] + } + ].concat(options), + "minItems": 1, + "maxItems": options.length + 1 + }; + } + + // Given a full schema, leave it alone + return options; +} + +/** + * Gets an options schema for a rule. + * @param {string} id The rule's unique name. + * @returns {object} vJSON Schema for the rule's options. + */ +function getRuleOptionsSchema(id) { + var rule = rules.get(id); + return makeRuleOptionsSchema(rule && rule.schema); +} + +/** + * Validates a rule's options against its schema. + * @param {string} id The rule's unique name. + * @param {object} options The given options for the rule. + * @param {string} source The name of the configuration source. + * @returns {void} + */ +module.exports.validateRuleOptions = function (id, options, source) { + var validate = optionsValidators[id], + message; + + if (!validate) { + validate = schemaValidator(getRuleOptionsSchema(id), { verbose: true }); + optionsValidators[id] = validate; + } + + if (typeof options === "number") { + options = [options]; + } + + validate(options); + + if (validate.errors) { + message = [ + source, ":\n", + "\tConfiguration for rule \"", id, "\" is invalid:\n" + ]; + validate.errors.forEach(function (error) { + message.push( + "\tValue \"", error.value, "\" ", error.message, ".\n" + ); + }); + + throw new Error(message.join("")); + } +}; + +module.exports.getRuleOptionsSchema = optionsValidators; diff --git a/package.json b/package.json index cd4f7fe2888..ec530670b87 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..1eccec7f5bc --- /dev/null +++ b/tests/lib/config-validator.js @@ -0,0 +1,78 @@ +/** + * @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": ["single", "double", "backtick"] + }, + { + "enum": ["avoid-escape"] + } +]; + +describe("Validator", function() { + + describe("validateRuleOptions", function() { + + beforeEach(function() { + eslint.defineRule("mock-rule", mockRule); + }); + + 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, "doulbe", "avoidEscape"], "tests"); + + assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tValue \"doulbe\" must be an enum value.\n\tValue \"avoidEscape\" must be an enum value.\n"); + }); + + it("should throw for too many configuration values", function() { + var fn = validator.validateRuleOptions.bind(null, "mock-rule", [2, "single", "avoid-escape", "extra"], "tests"); + + assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tValue \"2,single,avoid-escape,extra\" has more items than allowed.\n"); + }); + + }); + +});