/* * STOP!!! DO NOT MODIFY. * * This file is part of the ongoing work to move the eslintrc-style config * system into the @eslint/eslintrc package. This file needs to remain * unchanged in order for this work to proceed. * * If you think you need to change this file, please contact @nzakas first. * * Thanks in advance for your cooperation. */ /** * @fileoverview Tests for config validator. * @author Brandon Mills */ "use strict"; //------------------------------------------------------------------------------ // Requirements //------------------------------------------------------------------------------ const assert = require("chai").assert, { Linter } = require("../../../lib/linter"), validator = require("../../../lib/shared/config-validator"), Rules = require("../../../lib/linter/rules"); //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ const linter = new Linter(); const mockRule = { meta: { schema: [{ enum: ["first", "second"] }] }, create(context) { return { Program(node) { context.report(node, "Expected a validation error."); } }; } }; const mockObjectRule = { meta: { schema: { enum: ["first", "second"] } }, create(context) { return { Program(node) { context.report(node, "Expected a validation error."); } }; } }; const mockNoOptionsRule = { meta: { schema: [] }, create(context) { return { Program(node) { context.report(node, "Expected a validation error."); } }; } }; const mockRequiredOptionsRule = { meta: { schema: { type: "array", minItems: 1 } }, create(context) { return { Program(node) { context.report(node, "Expected a validation error."); } }; } }; //------------------------------------------------------------------------------ // Tests //------------------------------------------------------------------------------ describe("Validator", () => { /** * Gets a loaded rule given a rule ID * @param {string} ruleId The ID of the rule * @returns {{create: Function}} The loaded rule */ function ruleMapper(ruleId) { return linter.getRules().get(ruleId) || new Rules().get(ruleId); } beforeEach(() => { linter.defineRule("mock-rule", mockRule); linter.defineRule("mock-required-options-rule", mockRequiredOptionsRule); }); describe("validate", () => { it("should do nothing with an empty config", () => { validator.validate({}, "tests", ruleMapper); }); it("should do nothing with a valid eslint config", () => { validator.validate( { $schema: "http://json.schemastore.org/eslintrc", root: true, globals: { globalFoo: "readonly" }, parser: "parserFoo", env: { browser: true }, plugins: ["pluginFoo", "pluginBar"], settings: { foo: "bar" }, extends: ["configFoo", "configBar"], parserOptions: { foo: "bar" }, rules: {} }, "tests", ruleMapper ); }); it("should throw with an unknown property", () => { const fn = validator.validate.bind( null, { foo: true }, "tests", ruleMapper ); assert.throws(fn, "Unexpected top-level property \"foo\"."); }); describe("root", () => { it("should throw with a string value", () => { const fn = validator.validate.bind(null, { root: "true" }, null, ruleMapper); assert.throws(fn, "Property \"root\" is the wrong type (expected boolean but got `\"true\"`)."); }); it("should throw with a numeric value", () => { const fn = validator.validate.bind(null, { root: 0 }, null, ruleMapper); assert.throws(fn, "Property \"root\" is the wrong type (expected boolean but got `0`)."); }); }); describe("globals", () => { it("should throw with a string value", () => { const fn = validator.validate.bind(null, { globals: "jQuery" }, null, ruleMapper); assert.throws(fn, "Property \"globals\" is the wrong type (expected object but got `\"jQuery\"`)."); }); it("should throw with an array value", () => { const fn = validator.validate.bind(null, { globals: ["jQuery"] }, null, ruleMapper); assert.throws(fn, "Property \"globals\" is the wrong type (expected object but got `[\"jQuery\"]`)."); }); }); describe("parser", () => { it("should not throw with a null value", () => { validator.validate({ parser: null }, null, ruleMapper); }); }); describe("env", () => { it("should throw with an array environment", () => { const fn = validator.validate.bind(null, { env: [] }, null, ruleMapper); assert.throws(fn, "Property \"env\" is the wrong type (expected object but got `[]`)."); }); it("should throw with a primitive environment", () => { const fn = validator.validate.bind(null, { env: 1 }, null, ruleMapper); assert.throws(fn, "Property \"env\" is the wrong type (expected object but got `1`)."); }); it("should catch invalid environments", () => { const fn = validator.validate.bind(null, { env: { browser: true, invalid: true } }, null, ruleMapper); assert.throws(fn, "Environment key \"invalid\" is unknown\n"); }); it("should catch disabled invalid environments", () => { const fn = validator.validate.bind(null, { env: { browser: true, invalid: false } }, null, ruleMapper); assert.throws(fn, "Environment key \"invalid\" is unknown\n"); }); it("should do nothing with an undefined environment", () => { validator.validate({}, null, ruleMapper); }); }); describe("plugins", () => { it("should not throw with an empty array", () => { validator.validate({ plugins: [] }, null, ruleMapper); }); it("should throw with a string", () => { const fn = validator.validate.bind(null, { plugins: "react" }, null, ruleMapper); assert.throws(fn, "Property \"plugins\" is the wrong type (expected array but got `\"react\"`)."); }); }); describe("settings", () => { it("should not throw with an empty object", () => { validator.validate({ settings: {} }, null, ruleMapper); }); it("should throw with an array", () => { const fn = validator.validate.bind(null, { settings: ["foo"] }, null, ruleMapper); assert.throws(fn, "Property \"settings\" is the wrong type (expected object but got `[\"foo\"]`)."); }); }); describe("extends", () => { it("should not throw with an empty array", () => { validator.validate({ extends: [] }, null, ruleMapper); }); it("should not throw with a string", () => { validator.validate({ extends: "react" }, null, ruleMapper); }); it("should throw with an object", () => { const fn = validator.validate.bind(null, { extends: {} }, null, ruleMapper); assert.throws(fn, "ESLint configuration in null is invalid:\n\t- Property \"extends\" is the wrong type (expected string but got `{}`).\n\t- Property \"extends\" is the wrong type (expected array but got `{}`).\n\t- \"extends\" should match exactly one schema in oneOf. Value: {}."); }); }); describe("parserOptions", () => { it("should not throw with an empty object", () => { validator.validate({ parserOptions: {} }, null, ruleMapper); }); it("should throw with an array", () => { const fn = validator.validate.bind(null, { parserOptions: ["foo"] }, null, ruleMapper); assert.throws(fn, "Property \"parserOptions\" is the wrong type (expected object but got `[\"foo\"]`)."); }); }); describe("rules", () => { it("should do nothing with an empty rules object", () => { validator.validate({ rules: {} }, "tests", ruleMapper); }); it("should do nothing with a valid config with rules", () => { validator.validate({ rules: { "mock-rule": [2, "second"] } }, "tests", ruleMapper); }); it("should do nothing with a valid config when severity is off", () => { validator.validate({ rules: { "mock-rule": ["off", "second"] } }, "tests", ruleMapper); }); it("should do nothing with an invalid config when severity is off", () => { validator.validate({ rules: { "mock-required-options-rule": "off" } }, "tests", ruleMapper); }); it("should do nothing with an invalid config when severity is an array with 'off'", () => { validator.validate({ rules: { "mock-required-options-rule": ["off"] } }, "tests", ruleMapper); }); it("should do nothing with a valid config when severity is warn", () => { validator.validate({ rules: { "mock-rule": ["warn", "second"] } }, "tests", ruleMapper); }); it("should do nothing with a valid config when severity is error", () => { validator.validate({ rules: { "mock-rule": ["error", "second"] } }, "tests", ruleMapper); }); it("should do nothing with a valid config when severity is Off", () => { validator.validate({ rules: { "mock-rule": ["Off", "second"] } }, "tests", ruleMapper); }); it("should do nothing with a valid config when severity is Warn", () => { validator.validate({ rules: { "mock-rule": ["Warn", "second"] } }, "tests", ruleMapper); }); it("should do nothing with a valid config when severity is Error", () => { validator.validate({ rules: { "mock-rule": ["Error", "second"] } }, "tests", ruleMapper); }); it("should catch invalid rule options", () => { const fn = validator.validate.bind(null, { rules: { "mock-rule": [3, "third"] } }, "tests", ruleMapper); assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '3').\n"); }); it("should allow for rules with no options", () => { linter.defineRule("mock-no-options-rule", mockNoOptionsRule); validator.validate({ rules: { "mock-no-options-rule": 2 } }, "tests", ruleMapper); }); it("should not allow options for rules with no options", () => { linter.defineRule("mock-no-options-rule", mockNoOptionsRule); const fn = validator.validate.bind(null, { rules: { "mock-no-options-rule": [2, "extra"] } }, "tests", ruleMapper); assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-no-options-rule\" is invalid:\n\tValue [\"extra\"] should NOT have more than 0 items.\n"); }); }); describe("globals", () => { it("should disallow globals set to invalid values", () => { assert.throws( () => validator.validate({ globals: { foo: "AAAAA" } }, "tests", ruleMapper), "ESLint configuration of global 'foo' in tests is invalid:\n'AAAAA' is not a valid configuration for a global (use 'readonly', 'writable', or 'off')" ); }); }); describe("overrides", () => { it("should not throw with an empty overrides array", () => { validator.validate({ overrides: [] }, "tests", ruleMapper); }); it("should not throw with a valid overrides array", () => { validator.validate({ overrides: [{ files: "*", rules: {} }] }, "tests", ruleMapper); }); it("should throw if override does not specify files", () => { const fn = validator.validate.bind(null, { overrides: [{ rules: {} }] }, "tests", ruleMapper); assert.throws(fn, "ESLint configuration in tests is invalid:\n\t- \"overrides[0]\" should have required property 'files'. Value: {\"rules\":{}}.\n"); }); it("should throw if override has an empty files array", () => { const fn = validator.validate.bind(null, { overrides: [{ files: [] }] }, "tests", ruleMapper); assert.throws(fn, "ESLint configuration in tests is invalid:\n\t- Property \"overrides[0].files\" is the wrong type (expected string but got `[]`).\n\t- \"overrides[0].files\" should NOT have fewer than 1 items. Value: [].\n\t- \"overrides[0].files\" should match exactly one schema in oneOf. Value: [].\n"); }); it("should not throw if override has nested overrides", () => { validator.validate({ overrides: [{ files: "*", overrides: [{ files: "*", rules: {} }] }] }, "tests", ruleMapper); }); it("should not throw if override extends", () => { validator.validate({ overrides: [{ files: "*", extends: "eslint-recommended" }] }, "tests", ruleMapper); }); it("should throw if override tries to set root", () => { const fn = validator.validate.bind(null, { overrides: [{ files: "*", root: "true" }] }, "tests", ruleMapper); assert.throws(fn, "ESLint configuration in tests is invalid:\n\t- Unexpected top-level property \"overrides[0].root\".\n"); }); describe("env", () => { it("should catch invalid environments", () => { const fn = validator.validate.bind(null, { overrides: [{ files: "*", env: { browser: true, invalid: true } }] }, null, ruleMapper); assert.throws(fn, "Environment key \"invalid\" is unknown\n"); }); it("should catch disabled invalid environments", () => { const fn = validator.validate.bind(null, { overrides: [{ files: "*", env: { browser: true, invalid: false } }] }, null, ruleMapper); assert.throws(fn, "Environment key \"invalid\" is unknown\n"); }); }); describe("rules", () => { it("should catch invalid rule options", () => { const fn = validator.validate.bind(null, { overrides: [{ files: "*", rules: { "mock-rule": [3, "third"] } }] }, "tests", ruleMapper); assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '3').\n"); }); it("should not allow options for rules with no options", () => { linter.defineRule("mock-no-options-rule", mockNoOptionsRule); const fn = validator.validate.bind(null, { overrides: [{ files: "*", rules: { "mock-no-options-rule": [2, "extra"] } }] }, "tests", ruleMapper); assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-no-options-rule\" is invalid:\n\tValue [\"extra\"] should NOT have more than 0 items.\n"); }); }); }); }); describe("getRuleOptionsSchema", () => { it("should return null for a missing rule", () => { assert.strictEqual(validator.getRuleOptionsSchema(ruleMapper("non-existent-rule")), null); }); it("should not modify object schema", () => { linter.defineRule("mock-object-rule", mockObjectRule); assert.deepStrictEqual(validator.getRuleOptionsSchema(ruleMapper("mock-object-rule")), { enum: ["first", "second"] }); }); }); describe("validateRuleOptions", () => { it("should throw for incorrect warning level number", () => { const fn = validator.validateRuleOptions.bind(null, ruleMapper("mock-rule"), "mock-rule", 3, "tests"); assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '3').\n"); }); it("should throw for incorrect warning level string", () => { const fn = validator.validateRuleOptions.bind(null, ruleMapper("mock-rule"), "mock-rule", "booya", "tests"); assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '\"booya\"').\n"); }); it("should throw for invalid-type warning level", () => { const fn = validator.validateRuleOptions.bind(null, ruleMapper("mock-rule"), "mock-rule", [["error"]], "tests"); assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '[ \"error\" ]').\n"); }); it("should only check warning level for nonexistent rules", () => { const fn = validator.validateRuleOptions.bind(null, ruleMapper("non-existent-rule"), "non-existent-rule", [3, "foobar"], "tests"); assert.throws(fn, "tests:\n\tConfiguration for rule \"non-existent-rule\" is invalid:\n\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '3').\n"); }); it("should only check warning level for plugin rules", () => { const fn = validator.validateRuleOptions.bind(null, ruleMapper("plugin/rule"), "plugin/rule", 3, "tests"); assert.throws(fn, "tests:\n\tConfiguration for rule \"plugin/rule\" is invalid:\n\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '3').\n"); }); it("should throw for incorrect configuration values", () => { const fn = validator.validateRuleOptions.bind(null, ruleMapper("mock-rule"), "mock-rule", [2, "frist"], "tests"); assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tValue \"frist\" should be equal to one of the allowed values.\n"); }); it("should throw for too many configuration values", () => { const fn = validator.validateRuleOptions.bind(null, ruleMapper("mock-rule"), "mock-rule", [2, "first", "second"], "tests"); assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tValue [\"first\",\"second\"] should NOT have more than 1 items.\n"); }); }); });