Repository: eslint/eslint
Pull request #9364 (2017-09-27 21:27:34)

Chore: avoid handling rules instances in config-validator
<!--
    ESLint adheres to the [JS Foundation Code of Conduct](https://js.foundation/community/code-of-conduct).
-->

**What is the purpose of this pull request? (put an "X" next to item)**

[ ] Documentation update
[ ] Bug fix ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/bug-report.md))
[ ] New rule ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/rule-proposal.md))
[ ] Changes an existing rule ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/rule-change-proposal.md))
[ ] Add autofixing to a rule
[ ] Add a CLI option
[ ] Add something to the core
[x] Other, please explain:

<!--
    If the item you've checked above has a template, please paste the template questions below and answer them. (If this pull request is addressing an issue, you can just paste a link to the issue here instead.)
-->

<!--
    Please ensure your pull request is ready:

    - Read the pull request guide (https://eslint.org/docs/developer-guide/contributing/pull-requests)
    - Include tests for this change
    - Update documentation for this change (if appropriate)
-->

<!--
    The following is required for all pull requests:
-->

**What changes did you make? (Give an overview)**

This is a modified version of #9277, which was merged but then reverted later for performance reasons. The performance issues from #9177 have been fixed (`Linter` no longer creates a new map of rules whenever it lints text).

The goal of this change is to progress towards being able to remove the `rules` property from `Linter` instances. Unfortunately, this isn't possible yet, since there are a few other modules that rely on reading the list of defined rules.

**Is there anything you'd like reviewers to focus on?**

Nothing in particular


eslintbot: LGTM
not-an-aardvark: The goal of this change is to progress towards being able to remove the `rules` property from Linter instances. This property is not supposed to be part of the public `Linter` API, but there are other parts of core using it, so we can't remove it yet.
ilyavolodin: Just to clarify, when you are talking about `rules` property you mean `linter.rules`, right? `linter.getRules()` will still be available?
not-an-aardvark: Yes, that's correct.
ilyavolodin: @not-an-aardvark Could you rebase please?

Patch:
diff --git a/Makefile.js b/Makefile.js
index 544ee8fc6..02363aa66 100644
--- a/Makefile.js
+++ b/Makefile.js
@@ -20,7 +20,6 @@ const lodash = require("lodash"),
     fs = require("fs"),
     glob = require("glob"),
     markdownlint = require("markdownlint"),
-    nodeCLI = require("shelljs-nodecli"),
     os = require("os"),
     path = require("path"),
     semver = require("semver"),
@@ -103,7 +102,7 @@ function getTestFilePatterns() {
             initialValue.push(`"tests/lib/${currentValues}/**/*.js"`);
         }
         return initialValue;
-    }, ["tests/lib/rules/**/*.js", "tests/lib/*.js", "tests/bin/**/*.js", "tests/tools/**/*.js"]).join(" ");
+    }, ["\"tests/lib/rules/**/*.js\"", "\"tests/lib/*.js\"", "\"tests/bin/**/*.js\"", "\"tests/tools/**/*.js\""]).join(" ");
 }
 
 /**
@@ -468,6 +467,15 @@ function getFormatterResults() {
     }, { formatterResults: {} });
 }
 
+/**
+ * Gets a path to an executable in node_modules/.bin
+ * @param {string} command The executable name
+ * @returns {string} The executable path
+ */
+function getBinFile(command) {
+    return path.join("node_modules", ".bin", command);
+}
+
 //------------------------------------------------------------------------------
 // Tasks
 //------------------------------------------------------------------------------
@@ -560,21 +568,20 @@ target.test = function() {
     let errors = 0,
         lastReturn;
 
-    // exec(ISTANBUL + " cover " + MOCHA + "-- -c " + TEST_FILES);
-    lastReturn = nodeCLI.exec("istanbul", "cover", MOCHA, `-- -R progress -t ${MOCHA_TIMEOUT}`, "-c", TEST_FILES);
+    lastReturn = exec(`${getBinFile("istanbul")} cover ${MOCHA} -- -R progress -t ${MOCHA_TIMEOUT} -c ${TEST_FILES}`);
     if (lastReturn.code !== 0) {
         errors++;
     }
 
-    // exec(ISTANBUL + "check-coverage --statement 99 --branch 98 --function 99 --lines 99");
-    lastReturn = nodeCLI.exec("istanbul", "check-coverage", "--statement 99 --branch 98 --function 99 --lines 99");
+    lastReturn = exec(`${getBinFile("istanbul")} check-coverage --statement 99 --branch 98 --function 99 --lines 99`);
+
     if (lastReturn.code !== 0) {
         errors++;
     }
 
     target.browserify();
 
-    lastReturn = nodeCLI.exec("karma", "start karma.conf.js");
+    lastReturn = exec(`${getBinFile("karma")} start karma.conf.js`);
     if (lastReturn.code !== 0) {
         errors++;
     }
@@ -588,7 +595,7 @@ target.test = function() {
 
 target.docs = function() {
     echo("Generating documentation");
-    nodeCLI.exec("jsdoc", "-d jsdoc lib");
+    exec(`${getBinFile("jsdoc")} -d jsdoc lib`);
     echo("Documentation has been output to /jsdoc");
 };
 
@@ -799,10 +806,10 @@ target.browserify = function() {
     generateRulesIndex(TEMP_DIR);
 
     // 5. browserify the temp directory
-    nodeCLI.exec("browserify", "-x espree", `${TEMP_DIR}linter.js`, "-o", `${BUILD_DIR}eslint.js`, "-s eslint", "--global-transform [ babelify --presets [ es2015 ] ]");
+    exec(`${getBinFile("browserify")} -x espree ${TEMP_DIR}linter.js -o ${BUILD_DIR}eslint.js -s eslint --global-transform [ babelify --presets [ es2015 ] ]`);
 
     // 6. Browserify espree
-    nodeCLI.exec("browserify", "-r espree", "-o", `${TEMP_DIR}espree.js`);
+    exec(`${getBinFile("browserify")} -r espree -o ${TEMP_DIR}espree.js`);
 
     // 7. Concatenate Babel polyfill, Espree, and ESLint files together
     cat("./node_modules/babel-polyfill/dist/polyfill.js", `${TEMP_DIR}espree.js`, `${BUILD_DIR}eslint.js`).to(`${BUILD_DIR}eslint.js`);
diff --git a/appveyor.yml b/appveyor.yml
index e424a345b..966263e29 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -7,7 +7,7 @@ version: "{build}"
 # What combinations to test
 environment:
   matrix:
-    - nodejs_version: 4
+    - nodejs_version: 8
 
 branches:
   only:
diff --git a/docs/rules/lines-around-comment.md b/docs/rules/lines-around-comment.md
index ae8ef4de7..61af59698 100644
--- a/docs/rules/lines-around-comment.md
+++ b/docs/rules/lines-around-comment.md
@@ -21,6 +21,8 @@ This rule has an object option:
 * `"allowObjectEnd": true` allows comments to appear at the end of object literals
 * `"allowArrayStart": true` allows comments to appear at the start of array literals
 * `"allowArrayEnd": true` allows comments to appear at the end of array literals
+* `"allowClassStart": true` allows comments to appear at the start of classes
+* `"allowClassEnd": true` allows comments to appear at the end of classes
 * `"applyDefaultIgnorePatterns"` enables or disables the default comment patterns to be ignored by the rule
 * `"ignorePattern"` custom patterns to be ignored by the rule
 
diff --git a/docs/rules/lines-between-class-members.md b/docs/rules/lines-between-class-members.md
index c17b7a1aa..5196b6ad5 100644
--- a/docs/rules/lines-between-class-members.md
+++ b/docs/rules/lines-between-class-members.md
@@ -102,6 +102,6 @@ If you don't want to enforce empty lines between class members, you can disable
 ## Related Rules
 
 * [padded-blocks](padded-blocks.md)
-* [padding-line-between-statement](padding-line-between-statement.md)
+* [padding-line-between-statements](padding-line-between-statements.md)
 * [requirePaddingNewLinesAfterBlocks](http://jscs.info/rule/requirePaddingNewLinesAfterBlocks)
 * [disallowPaddingNewLinesAfterBlocks](http://jscs.info/rule/disallowPaddingNewLinesAfterBlocks)
diff --git a/lib/cli-engine.js b/lib/cli-engine.js
index 39169dc42..55450fd63 100644
--- a/lib/cli-engine.js
+++ b/lib/cli-engine.js
@@ -409,9 +409,13 @@ class CLIEngine {
             });
         }
 
-        Object.keys(this.options.rules || {}).forEach(name => {
-            validator.validateRuleOptions(name, this.options.rules[name], "CLI", this.linter.rules);
-        });
+        if (this.options.rules && Object.keys(this.options.rules).length) {
+            const loadedRules = this.linter.getRules();
+
+            Object.keys(this.options.rules).forEach(name => {
+                validator.validateRuleOptions(loadedRules.get(name), name, this.options.rules[name], "CLI");
+            });
+        }
 
         this.config = new Config(this.options, this.linter);
     }
diff --git a/lib/config/config-file.js b/lib/config/config-file.js
index 8cefd437d..c5ff073cf 100644
--- a/lib/config/config-file.js
+++ b/lib/config/config-file.js
@@ -501,8 +501,10 @@ function loadFromDisk(resolvedPath, configContext) {
             }
         }
 
+        const ruleMap = configContext.linterContext.getRules();
+
         // validate the configuration before continuing
-        validator.validate(config, resolvedPath.configFullName, configContext.linterContext.rules, configContext.linterContext.environments);
+        validator.validate(config, resolvedPath.configFullName, ruleMap.get.bind(ruleMap), configContext.linterContext.environments);
 
         /*
          * If an `extends` property is defined, it represents a configuration file to use as
diff --git a/lib/config/config-validator.js b/lib/config/config-validator.js
index f2307dc63..3d98b5104 100644
--- a/lib/config/config-validator.js
+++ b/lib/config/config-validator.js
@@ -14,9 +14,7 @@ const ajv = require("../util/ajv"),
     configSchema = require("../../conf/config-schema.js"),
     util = require("util");
 
-const validators = {
-    rules: Object.create(null)
-};
+const ruleValidators = new WeakMap();
 
 //------------------------------------------------------------------------------
 // Private
@@ -25,13 +23,11 @@ let validateSchema;
 
 /**
  * Gets a complete options schema for a rule.
- * @param {string} id The rule's unique name.
- * @param {Rules} rulesContext Rule context
+ * @param {{create: Function, schema: (Array|null)}} rule A new-style rule object
  * @returns {Object} JSON Schema for the rule's options.
  */
-function getRuleOptionsSchema(id, rulesContext) {
-    const rule = rulesContext.get(id),
-        schema = rule && rule.schema || rule && rule.meta && rule.meta.schema;
+function getRuleOptionsSchema(rule) {
+    const schema = rule.schema || rule.meta && rule.meta.schema;
 
     // Given a tuple of schemas, insert warning level at the beginning
     if (Array.isArray(schema)) {
@@ -72,19 +68,20 @@ function validateRuleSeverity(options) {
 
 /**
  * Validates the non-severity options passed to a rule, based on its schema.
- * @param {string} id The rule's unique name
+ * @param {{create: Function}} rule The rule to validate
  * @param {array} localOptions The options for the rule, excluding severity
- * @param {Rules} rulesContext Rule context
  * @returns {void}
  */
-function validateRuleSchema(id, localOptions, rulesContext) {
-    const schema = getRuleOptionsSchema(id, rulesContext);
+function validateRuleSchema(rule, localOptions) {
+    if (!ruleValidators.has(rule)) {
+        const schema = getRuleOptionsSchema(rule);
 
-    if (!validators.rules[id] && schema) {
-        validators.rules[id] = ajv.compile(schema);
+        if (schema) {
+            ruleValidators.set(rule, ajv.compile(schema));
+        }
     }
 
-    const validateRule = validators.rules[id];
+    const validateRule = ruleValidators.get(rule);
 
     if (validateRule) {
         validateRule(localOptions);
@@ -98,21 +95,24 @@ function validateRuleSchema(id, localOptions, rulesContext) {
 
 /**
  * Validates a rule's options against its schema.
- * @param {string} id The rule's unique name.
+ * @param {{create: Function}|null} rule The rule that the config is being validated for
+ * @param {string} ruleId The rule's unique name.
  * @param {array|number} options The given options for the rule.
  * @param {string} source The name of the configuration source to report in any errors.
- * @param {Rules} rulesContext Rule context
  * @returns {void}
  */
-function validateRuleOptions(id, options, source, rulesContext) {
+function validateRuleOptions(rule, ruleId, options, source) {
+    if (!rule) {
+        return;
+    }
     try {
         const severity = validateRuleSeverity(options);
 
         if (severity !== 0 && !(typeof severity === "string" && severity.toLowerCase() === "off")) {
-            validateRuleSchema(id, Array.isArray(options) ? options.slice(1) : [], rulesContext);
+            validateRuleSchema(rule, Array.isArray(options) ? options.slice(1) : []);
         }
     } catch (err) {
-        throw new Error(`${source}:\n\tConfiguration for rule "${id}" is invalid:\n${err.message}`);
+        throw new Error(`${source}:\n\tConfiguration for rule "${ruleId}" is invalid:\n${err.message}`);
     }
 }
 
@@ -143,16 +143,16 @@ function validateEnvironment(environment, source, envContext) {
  * Validates a rules config object
  * @param {Object} rulesConfig The rules config object to validate.
  * @param {string} source The name of the configuration source to report in any errors.
- * @param {Rules} rulesContext Rule context
+ * @param {function(string): {create: Function}} ruleMapper A mapper function from strings to loaded rules
  * @returns {void}
  */
-function validateRules(rulesConfig, source, rulesContext) {
+function validateRules(rulesConfig, source, ruleMapper) {
     if (!rulesConfig) {
         return;
     }
 
     Object.keys(rulesConfig).forEach(id => {
-        validateRuleOptions(id, rulesConfig[id], source, rulesContext);
+        validateRuleOptions(ruleMapper(id), id, rulesConfig[id], source);
     });
 }
 
@@ -223,13 +223,13 @@ function validateConfigSchema(config, source) {
  * Validates an entire config object.
  * @param {Object} config The config object to validate.
  * @param {string} source The name of the configuration source to report in any errors.
- * @param {Rules} rulesContext The rules context
+ * @param {function(string): {create: Function}} ruleMapper A mapper function from rule IDs to defined rules
  * @param {Environments} envContext The env context
  * @returns {void}
  */
-function validate(config, source, rulesContext, envContext) {
+function validate(config, source, ruleMapper, envContext) {
     validateConfigSchema(config, source);
-    validateRules(config.rules, source, rulesContext);
+    validateRules(config.rules, source, ruleMapper);
     validateEnvironment(config.env, source, envContext);
 }
 
diff --git a/lib/linter.js b/lib/linter.js
index 128798424..35ecf7c75 100755
--- a/lib/linter.js
+++ b/lib/linter.js
@@ -278,7 +278,7 @@ function createDisableDirectives(type, loc, value) {
  * @param {string} filename The file being checked.
  * @param {ASTNode} ast The top node of the AST.
  * @param {Object} config The existing configuration data.
- * @param {Linter} linterContext Linter context object
+ * @param {function(string): {create: Function}} ruleMapper A map from rule IDs to defined rules
  * @returns {{
  *      config: Object,
  *      problems: Problem[],
@@ -291,7 +291,7 @@ function createDisableDirectives(type, loc, value) {
  * }} Modified config object, along with any problems encountered
  * while parsing config comments
  */
-function modifyConfigsFromComments(filename, ast, config, linterContext) {
+function modifyConfigsFromComments(filename, ast, config, ruleMapper) {
 
     const commentConfig = {
         exported: {},
@@ -337,7 +337,7 @@ function modifyConfigsFromComments(filename, ast, config, linterContext) {
                             Object.keys(parseResult.config).forEach(name => {
                                 const ruleValue = parseResult.config[name];
 
-                                validator.validateRuleOptions(name, ruleValue, `${filename} line ${comment.loc.start.line}`, linterContext.rules);
+                                validator.validateRuleOptions(ruleMapper(name), name, ruleValue, `${filename} line ${comment.loc.start.line}`);
                                 commentRules[name] = ruleValue;
                             });
                         } else {
@@ -806,7 +806,7 @@ module.exports = class Linter {
 
         // parse global comments and modify config
         if (allowInlineConfig !== false) {
-            const modifyConfigResult = modifyConfigsFromComments(filename, sourceCode.ast, config, this);
+            const modifyConfigResult = modifyConfigsFromComments(filename, sourceCode.ast, config, ruleId => this.rules.get(ruleId));
 
             config = modifyConfigResult.config;
             modifyConfigResult.problems.forEach(problem => problems.push(problem));
diff --git a/lib/util/safe-emitter.js b/lib/util/safe-emitter.js
index 242cbe429..2fa373cb9 100644
--- a/lib/util/safe-emitter.js
+++ b/lib/util/safe-emitter.js
@@ -10,7 +10,7 @@
 //------------------------------------------------------------------------------
 
 /**
- * An object describing an AST selector
+ * An event emitter
  * @typedef {Object} SafeEmitter
  * @property {function(eventName: string, listenerFunc: Function): void} on Adds a listener for a given event name
  * @property {function(eventName: string, arg1?: any, arg2?: any, arg3?: any)} emit Emits an event with a given name.
diff --git a/package.json b/package.json
index da71cf6d0..2dee47190 100644
--- a/package.json
+++ b/package.json
@@ -84,11 +84,11 @@
     "coveralls": "^2.13.1",
     "dateformat": "^2.0.0",
     "ejs": "^2.5.6",
-    "eslint-rule-composer": "^0.1.0",
     "eslint-plugin-eslint-plugin": "^1.2.0",
     "eslint-plugin-node": "^5.1.0",
     "eslint-plugin-rulesdir": "^0.1.0",
     "eslint-release": "^0.10.1",
+    "eslint-rule-composer": "^0.1.0",
     "eslump": "1.6.0",
     "esprima": "^4.0.0",
     "esprima-fb": "^15001.1001.0-dev-harmony-fb",
@@ -108,7 +108,6 @@
     "phantomjs-prebuilt": "^2.1.14",
     "proxyquire": "^1.8.0",
     "shelljs": "^0.7.7",
-    "shelljs-nodecli": "~0.1.1",
     "sinon": "^3.2.1",
     "temp": "^0.8.3",
     "through": "^2.3.8"
diff --git a/packages/eslint-config-eslint/LICENSE b/packages/eslint-config-eslint/LICENSE
index d41bdf795..777939e8f 100644
--- a/packages/eslint-config-eslint/LICENSE
+++ b/packages/eslint-config-eslint/LICENSE
@@ -1,5 +1,5 @@
 ESLint
-Copyright jQuery Foundation and other contributors, https://jquery.org/
+Copyright JS Foundation and other contributors, https://js.foundation
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal


Test patch:
diff --git a/lib/testers/rule-tester.js b/lib/testers/rule-tester.js
index 9861998ff..aae66301d 100644
--- a/lib/testers/rule-tester.js
+++ b/lib/testers/rule-tester.js
@@ -271,6 +271,23 @@ class RuleTester {
             ].concat(scenarioErrors).join("\n"));
         }
 
+
+        linter.defineRule(ruleName, Object.assign({}, rule, {
+
+            // Create a wrapper rule that freezes the `context` properties.
+            create(context) {
+                freezeDeeply(context.options);
+                freezeDeeply(context.settings);
+                freezeDeeply(context.parserOptions);
+
+                return (typeof rule === "function" ? rule : rule.create)(context);
+            }
+        }));
+
+        linter.defineRules(this.rules);
+
+        const ruleMap = linter.getRules();
+
         /**
          * Run the rule for the given item
          * @param {string|Object} item Item to run the rule against
@@ -313,20 +330,22 @@ class RuleTester {
                 config.rules[ruleName] = 1;
             }
 
-            linter.defineRule(ruleName, Object.assign({}, rule, {
+            const schema = validator.getRuleOptionsSchema(rule);
 
-                // Create a wrapper rule that freezes the `context` properties.
-                create(context) {
-                    freezeDeeply(context.options);
-                    freezeDeeply(context.settings);
-                    freezeDeeply(context.parserOptions);
-
-                    return (typeof rule === "function" ? rule : rule.create)(context);
+            /*
+             * Setup AST getters.
+             * The goal is to check whether or not AST was modified when
+             * running the rule under test.
+             */
+            linter.defineRule("rule-tester/validate-ast", () => ({
+                Program(node) {
+                    beforeAST = cloneDeeplyExcludesParent(node);
+                },
+                "Program:exit"(node) {
+                    afterAST = node;
                 }
             }));
 
-            const schema = validator.getRuleOptionsSchema(ruleName, linter.rules);
-
             if (schema) {
                 ajv.validateSchema(schema);
 
@@ -341,21 +360,7 @@ class RuleTester {
                 }
             }
 
-            validator.validate(config, "rule-tester", linter.rules, new Environments());
-
-            /*
-             * Setup AST getters.
-             * The goal is to check whether or not AST was modified when
-             * running the rule under test.
-             */
-            linter.defineRule("rule-tester/validate-ast", () => ({
-                Program(node) {
-                    beforeAST = cloneDeeplyExcludesParent(node);
-                },
-                "Program:exit"(node) {
-                    afterAST = node;
-                }
-            }));
+            validator.validate(config, "rule-tester", ruleMap.get.bind(ruleMap), new Environments());
 
             return {
                 messages: linter.verify(code, config, filename, true),
@@ -532,7 +537,6 @@ class RuleTester {
             RuleTester.describe("valid", () => {
                 test.valid.forEach(valid => {
                     RuleTester.it(typeof valid === "object" ? valid.code : valid, () => {
-                        linter.defineRules(this.rules);
                         testValidTemplate(valid);
                     });
                 });
@@ -541,7 +545,6 @@ class RuleTester {
             RuleTester.describe("invalid", () => {
                 test.invalid.forEach(invalid => {
                     RuleTester.it(invalid.code, () => {
-                        linter.defineRules(this.rules);
                         testInvalidTemplate(invalid);
                     });
                 });
diff --git a/tests/lib/config/config-validator.js b/tests/lib/config/config-validator.js
index 5f3d65278..5d6cf8cc0 100644
--- a/tests/lib/config/config-validator.js
+++ b/tests/lib/config/config-validator.js
@@ -90,6 +90,15 @@ const mockRequiredOptionsRule = {
 
 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);
+    }
+
     beforeEach(() => {
         linter.defineRule("mock-rule", mockRule);
         linter.defineRule("mock-required-options-rule", mockRequiredOptionsRule);
@@ -98,7 +107,7 @@ describe("Validator", () => {
     describe("validate", () => {
 
         it("should do nothing with an empty config", () => {
-            const fn = validator.validate.bind(null, {}, "tests", linter.rules, linter.environments);
+            const fn = validator.validate.bind(null, {}, "tests", ruleMapper, linter.environments);
 
             assert.doesNotThrow(fn);
         });
@@ -118,7 +127,7 @@ describe("Validator", () => {
                     rules: {}
                 },
                 "tests",
-                linter.rules,
+                ruleMapper,
                 linter.environments
             );
 
@@ -132,7 +141,7 @@ describe("Validator", () => {
                     foo: true
                 },
                 "tests",
-                linter.rules,
+                ruleMapper,
                 linter.environments
             );
 
@@ -141,13 +150,13 @@ describe("Validator", () => {
 
         describe("root", () => {
             it("should throw with a string value", () => {
-                const fn = validator.validate.bind(null, { root: "true" }, null, linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { root: "true" }, null, ruleMapper, linter.environments);
 
                 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, linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { root: 0 }, null, ruleMapper, linter.environments);
 
                 assert.throws(fn, "Property \"root\" is the wrong type (expected boolean but got `0`).");
             });
@@ -155,13 +164,13 @@ describe("Validator", () => {
 
         describe("globals", () => {
             it("should throw with a string value", () => {
-                const fn = validator.validate.bind(null, { globals: "jQuery" }, null, linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { globals: "jQuery" }, null, ruleMapper, linter.environments);
 
                 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, linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { globals: ["jQuery"] }, null, ruleMapper, linter.environments);
 
                 assert.throws(fn, "Property \"globals\" is the wrong type (expected object but got `[\"jQuery\"]`).");
             });
@@ -169,7 +178,7 @@ describe("Validator", () => {
 
         describe("parser", () => {
             it("should not throw with a null value", () => {
-                const fn = validator.validate.bind(null, { parser: null }, null, linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { parser: null }, null, ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
@@ -178,31 +187,31 @@ describe("Validator", () => {
         describe("env", () => {
 
             it("should throw with an array environment", () => {
-                const fn = validator.validate.bind(null, { env: [] }, null, linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { env: [] }, null, ruleMapper, linter.environments);
 
                 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, linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { env: 1 }, null, ruleMapper, linter.environments);
 
                 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, linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { env: { browser: true, invalid: true } }, null, ruleMapper, linter.environments);
 
                 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, linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { env: { browser: true, invalid: false } }, null, ruleMapper, linter.environments);
 
                 assert.throws(fn, "Environment key \"invalid\" is unknown\n");
             });
 
             it("should do nothing with an undefined environment", () => {
-                const fn = validator.validate.bind(null, {}, null, linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, {}, null, ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
@@ -211,13 +220,13 @@ describe("Validator", () => {
 
         describe("plugins", () => {
             it("should not throw with an empty array", () => {
-                const fn = validator.validate.bind(null, { plugins: [] }, null, linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { plugins: [] }, null, ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
 
             it("should throw with a string", () => {
-                const fn = validator.validate.bind(null, { plugins: "react" }, null, linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { plugins: "react" }, null, ruleMapper, linter.environments);
 
                 assert.throws(fn, "Property \"plugins\" is the wrong type (expected array but got `\"react\"`).");
             });
@@ -225,13 +234,13 @@ describe("Validator", () => {
 
         describe("settings", () => {
             it("should not throw with an empty object", () => {
-                const fn = validator.validate.bind(null, { settings: {} }, null, linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { settings: {} }, null, ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
 
             it("should throw with an array", () => {
-                const fn = validator.validate.bind(null, { settings: ["foo"] }, null, linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { settings: ["foo"] }, null, ruleMapper, linter.environments);
 
                 assert.throws(fn, "Property \"settings\" is the wrong type (expected object but got `[\"foo\"]`).");
             });
@@ -239,19 +248,19 @@ describe("Validator", () => {
 
         describe("extends", () => {
             it("should not throw with an empty array", () => {
-                const fn = validator.validate.bind(null, { extends: [] }, null, linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { extends: [] }, null, ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
 
             it("should not throw with a string", () => {
-                const fn = validator.validate.bind(null, { extends: "react" }, null, linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { extends: "react" }, null, ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
 
             it("should throw with an object", () => {
-                const fn = validator.validate.bind(null, { extends: {} }, null, linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { extends: {} }, null, ruleMapper, linter.environments);
 
                 assert.throws(fn, "Property \"extends\" is the wrong type (expected string/array but got `{}`).");
             });
@@ -259,13 +268,13 @@ describe("Validator", () => {
 
         describe("parserOptions", () => {
             it("should not throw with an empty object", () => {
-                const fn = validator.validate.bind(null, { parserOptions: {} }, null, linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { parserOptions: {} }, null, ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
 
             it("should throw with an array", () => {
-                const fn = validator.validate.bind(null, { parserOptions: ["foo"] }, null, linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { parserOptions: ["foo"] }, null, ruleMapper, linter.environments);
 
                 assert.throws(fn, "Property \"parserOptions\" is the wrong type (expected object but got `[\"foo\"]`).");
             });
@@ -274,67 +283,67 @@ describe("Validator", () => {
         describe("rules", () => {
 
             it("should do nothing with an empty rules object", () => {
-                const fn = validator.validate.bind(null, { rules: {} }, "tests", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { rules: {} }, "tests", ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
 
             it("should do nothing with a valid config with rules", () => {
-                const fn = validator.validate.bind(null, { rules: { "mock-rule": [2, "second"] } }, "tests", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { rules: { "mock-rule": [2, "second"] } }, "tests", ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
 
             it("should do nothing with a valid config when severity is off", () => {
-                const fn = validator.validate.bind(null, { rules: { "mock-rule": ["off", "second"] } }, "tests", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { rules: { "mock-rule": ["off", "second"] } }, "tests", ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
 
             it("should do nothing with an invalid config when severity is off", () => {
-                const fn = validator.validate.bind(null, { rules: { "mock-required-options-rule": "off" } }, "tests", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { rules: { "mock-required-options-rule": "off" } }, "tests", ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
 
             it("should do nothing with an invalid config when severity is an array with 'off'", () => {
-                const fn = validator.validate.bind(null, { rules: { "mock-required-options-rule": ["off"] } }, "tests", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { rules: { "mock-required-options-rule": ["off"] } }, "tests", ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
 
             it("should do nothing with a valid config when severity is warn", () => {
-                const fn = validator.validate.bind(null, { rules: { "mock-rule": ["warn", "second"] } }, "tests", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { rules: { "mock-rule": ["warn", "second"] } }, "tests", ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
 
             it("should do nothing with a valid config when severity is error", () => {
-                const fn = validator.validate.bind(null, { rules: { "mock-rule": ["error", "second"] } }, "tests", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { rules: { "mock-rule": ["error", "second"] } }, "tests", ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
 
             it("should do nothing with a valid config when severity is Off", () => {
-                const fn = validator.validate.bind(null, { rules: { "mock-rule": ["Off", "second"] } }, "tests", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { rules: { "mock-rule": ["Off", "second"] } }, "tests", ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
 
             it("should do nothing with a valid config when severity is Warn", () => {
-                const fn = validator.validate.bind(null, { rules: { "mock-rule": ["Warn", "second"] } }, "tests", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { rules: { "mock-rule": ["Warn", "second"] } }, "tests", ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
 
             it("should do nothing with a valid config when severity is Error", () => {
-                const fn = validator.validate.bind(null, { rules: { "mock-rule": ["Error", "second"] } }, "tests", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { rules: { "mock-rule": ["Error", "second"] } }, "tests", ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
 
             it("should catch invalid rule options", () => {
-                const fn = validator.validate.bind(null, { rules: { "mock-rule": [3, "third"] } }, "tests", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { rules: { "mock-rule": [3, "third"] } }, "tests", ruleMapper, linter.environments);
 
                 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");
             });
@@ -342,7 +351,7 @@ describe("Validator", () => {
             it("should allow for rules with no options", () => {
                 linter.defineRule("mock-no-options-rule", mockNoOptionsRule);
 
-                const fn = validator.validate.bind(null, { rules: { "mock-no-options-rule": 2 } }, "tests", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { rules: { "mock-no-options-rule": 2 } }, "tests", ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
@@ -350,7 +359,7 @@ describe("Validator", () => {
             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", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { rules: { "mock-no-options-rule": [2, "extra"] } }, "tests", ruleMapper, linter.environments);
 
                 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");
             });
@@ -358,43 +367,43 @@ describe("Validator", () => {
 
         describe("overrides", () => {
             it("should not throw with an empty overrides array", () => {
-                const fn = validator.validate.bind(null, { overrides: [] }, "tests", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { overrides: [] }, "tests", ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
 
             it("should not throw with a valid overrides array", () => {
-                const fn = validator.validate.bind(null, { overrides: [{ files: "*", rules: {} }] }, "tests", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { overrides: [{ files: "*", rules: {} }] }, "tests", ruleMapper, linter.environments);
 
                 assert.doesNotThrow(fn);
             });
 
             it("should throw if override does not specify files", () => {
-                const fn = validator.validate.bind(null, { overrides: [{ rules: {} }] }, "tests", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { overrides: [{ rules: {} }] }, "tests", ruleMapper, linter.environments);
 
                 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", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { overrides: [{ files: [] }] }, "tests", ruleMapper, linter.environments);
 
                 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 less than 1 items. Value: [].\n\t- \"overrides[0].files\" should match exactly one schema in oneOf. Value: [].\n");
             });
 
             it("should throw if override has nested overrides", () => {
-                const fn = validator.validate.bind(null, { overrides: [{ files: "*", overrides: [{ files: "*", rules: {} }] }] }, "tests", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { overrides: [{ files: "*", overrides: [{ files: "*", rules: {} }] }] }, "tests", ruleMapper, linter.environments);
 
                 assert.throws(fn, "ESLint configuration in tests is invalid:\n\t- Unexpected top-level property \"overrides[0].overrides\".\n");
             });
 
             it("should throw if override extends", () => {
-                const fn = validator.validate.bind(null, { overrides: [{ files: "*", extends: "eslint-recommended" }] }, "tests", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { overrides: [{ files: "*", extends: "eslint-recommended" }] }, "tests", ruleMapper, linter.environments);
 
                 assert.throws(fn, "ESLint configuration in tests is invalid:\n\t- Unexpected top-level property \"overrides[0].extends\".\n");
             });
 
             it("should throw if override tries to set root", () => {
-                const fn = validator.validate.bind(null, { overrides: [{ files: "*", root: "true" }] }, "tests", linter.rules, linter.environments);
+                const fn = validator.validate.bind(null, { overrides: [{ files: "*", root: "true" }] }, "tests", ruleMapper, linter.environments);
 
                 assert.throws(fn, "ESLint configuration in tests is invalid:\n\t- Unexpected top-level property \"overrides[0].root\".\n");
             });
@@ -405,12 +414,12 @@ describe("Validator", () => {
     describe("getRuleOptionsSchema", () => {
 
         it("should return null for a missing rule", () => {
-            assert.strictEqual(validator.getRuleOptionsSchema("non-existent-rule", linter.rules), null);
+            assert.strictEqual(validator.getRuleOptionsSchema(linter.rules.get("non-existent-rule")), null);
         });
 
         it("should not modify object schema", () => {
             linter.defineRule("mock-object-rule", mockObjectRule);
-            assert.deepStrictEqual(validator.getRuleOptionsSchema("mock-object-rule", linter.rules), {
+            assert.deepStrictEqual(validator.getRuleOptionsSchema(linter.rules.get("mock-object-rule")), {
                 enum: ["first", "second"]
             });
         });
@@ -420,43 +429,43 @@ describe("Validator", () => {
     describe("validateRuleOptions", () => {
 
         it("should throw for incorrect warning level number", () => {
-            const fn = validator.validateRuleOptions.bind(null, "mock-rule", 3, "tests", linter.rules);
+            const fn = validator.validateRuleOptions.bind(null, linter.rules.get("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, "mock-rule", "booya", "tests", linter.rules);
+            const fn = validator.validateRuleOptions.bind(null, linter.rules.get("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, "mock-rule", [["error"]], "tests", linter.rules);
+            const fn = validator.validateRuleOptions.bind(null, linter.rules.get("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, "non-existent-rule", [3, "foobar"], "tests", linter.rules);
+            const fn = validator.validateRuleOptions.bind(null, linter.rules.get("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, "plugin/rule", 3, "tests", linter.rules);
+            const fn = validator.validateRuleOptions.bind(null, linter.rules.get("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, "mock-rule", [2, "frist"], "tests", linter.rules);
+            const fn = validator.validateRuleOptions.bind(null, linter.rules.get("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, "mock-rule", [2, "first", "second"], "tests", linter.rules);
+            const fn = validator.validateRuleOptions.bind(null, linter.rules.get("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");
         });