Repository: eslint/eslint
Pull request #9277 (2017-09-10 03:00:10)

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)**

[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 (http://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)**

Handling `Rules` instances outside of `Linter` is undesirable, because this gives any handler the ability to mutate linters at a distance. This commit updates `config-validator` to avoid handling `Rules` instances directly, and instead handle `Map`s that won't affect the underlying `Rules` instance if mutated.

The long-term goal is to make the `rules` property of `Linter` instances private, so that only `Linter` handles `Rules` instances. However, this isn't possible yet because `Plugins` still relies on mutating a `Rules` instance that it's provided as an argument.

**Is there anything you'd like reviewers to focus on?**

Nothing in particular


eslintbot: LGTM

Patch:
diff --git a/.eslintrc.yml b/.eslintrc.yml
index d7b0e414b..8a03a0ed0 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -9,7 +9,7 @@ extends:
     - "plugin:eslint-plugin/recommended"
 rules:
     eslint-plugin/consistent-output: "error"
-    eslint-plugin/no-identical-tests: "error"
+    eslint-plugin/no-deprecated-context-methods: "error"
     eslint-plugin/prefer-output-null: "error"
     eslint-plugin/prefer-placeholders: "error"
     eslint-plugin/report-message-format: ["error", '[^a-z].*\.$']
diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md
index 622d8612b..76aa51e21 100644
--- a/docs/developer-guide/nodejs-api.md
+++ b/docs/developer-guide/nodejs-api.md
@@ -693,6 +693,125 @@ CLIEngine.outputFixes(report);
 require("eslint").CLIEngine.version; // '4.5.0'
 ```
 
+## RuleTester
+
+`eslint.RuleTester` is a utility to write tests for ESLint rules. It is used internally for the bundled rules that come with ESLint, and it can also be used by plugins.
+
+Example usage:
+
+```js
+"use strict";
+
+const rule = require("../../../lib/rules/my-rule");
+const RuleTester = require("eslint").RuleTester;
+
+const ruleTester = new RuleTester();
+
+ruleTester.run("my-rule", rule, {
+    valid: [
+        {
+            code: "var foo = true",
+            options: [{ allowFoo: true }]
+        }
+    ],
+
+    invalid: [
+        {
+            code: "var invalidVariable = true",
+            errors: [{ message: "Unexpected invalid variable." }]
+        },
+        {
+            code: "var invalidVariable = true",
+            errors: [{ message: /^Unexpected.+variable/ }]
+        }
+    ]
+});
+```
+
+The `RuleTester` constructor accepts an optional object argument, which can be used to specify defaults for your test cases. For example, if all of your test cases use ES2015, you can set it as a default:
+
+```js
+const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2015 } });
+```
+
+The `RuleTester#run()` method is used to run the tests. It should be passed the following arguments:
+
+* The name of the rule (string)
+* The rule object itself (see ["working with rules"](./working-with-rules))
+* An object containing `valid` and `invalid` properties, each of which is an array containing test cases.
+
+A test case is an object with the following properties:
+
+* `code` (string, required): The source code that the rule should be run on
+* `options` (array, optional): The options passed to the rule. The rule severity should not be included in this list.
+* `filename` (string, optional): The filename for the given case (useful for rules that make assertions about filenames).
+
+In addition to the properties above, invalid test cases can also have the following properties:
+
+* `errors` (number or array, required): Asserts some properties of the errors that the rule is expected to produce when run on this code. If this is a number, asserts the number of errors produced. Otherwise, this should be a list of objects, each containing information about a single reported error. The following properties can be used for an error (all are optional):
+    * `message` (string/regexp): The message for the error
+    * `type` (string): The type of the reported AST node
+    * `line` (number): The 1-based line number of the reported location
+    * `column` (number): The 0-based column number of the reported location
+    * `endLine` (number): The 1-based line number of the end of the reported location
+    * `endColumn` (number): The 0-based column number of the end of the reported location
+
+    If a string is provided as an error instead of an object, the string is used to assert the `message` of the error.
+* `output` (string, optional): Asserts the output that will be produced when using this rule for a single pass of autofixing (e.g. with the `--fix` command line flag). If this is `null`, asserts that none of the reported problems suggest autofixes.
+
+Any additional properties of a test case will be passed directly to the linter as config options. For example, a test case can have a `parserOptions` property to configure parser behavior:
+
+```js
+{
+    code: "let foo;",
+    parserOptions: { ecmaVersion: 2015 }
+}
+```
+
+If a valid test case only uses the `code` property, it can optionally be provided as a string containing the code, rather than an object with a `code` key.
+
+### Customizing RuleTester
+
+`RuleTester` depends on two functions to run tests: `describe` and `it`. These functions can come from various places:
+
+1. If `RuleTester.describe` and `RuleTester.it` have been set to function values, `RuleTester` will use `RuleTester.describe` and `RuleTester.it` to run tests. You can use this to customize the behavior of `RuleTester` to match a test framework that you're using.
+1. Otherwise, if `describe` and `it` are present as globals, `RuleTester` will use `global.describe` and `global.it` to run tests. This allows `RuleTester` to work when using frameworks like [Mocha](https://mochajs.org/) without any additional configuration.
+1. Otherwise, `RuleTester#run` will simply execute all of the tests in sequence, and will throw an error if one of them fails. This means you can simply execute a test file that calls `RuleTester.run` using `node`, without needing a testing framework.
+
+`RuleTester#run` calls the `describe` function with two arguments: a string describing the rule, and a callback function. The callback calls the `it` function with a string describing the test case, and a test function. The test function will return successfully if the test passes, and throw an error if the test fails. (Note that this is the standard behavior for test suites when using frameworks like [Mocha](https://mochajs.org/); this information is only relevant if you plan to customize `RuleTester.it` and `RuleTester.describe`.)
+
+Example of customizing `RuleTester`:
+
+```js
+"use strict";
+
+const RuleTester = require("eslint").RuleTester;
+const test = require("my-test-runner");
+const myRule = require("../../../lib/rules/my-rule");
+
+RuleTester.describe = function(text, method) {
+    RuleTester.it.title = text;
+    return method.call(this);
+};
+
+RuleTester.it = function(text, method) {
+    test(RuleTester.it.title + ": " + text, method);
+};
+
+// then use RuleTester as documented
+
+const ruleTester = new RuleTester();
+
+ruleTester.run("my-rule", myRule, {
+    valid: [
+        // valid test cases
+    ],
+    invalid: [
+        // invalid test cases
+    ]
+})
+```
+
 ## Deprecated APIs
 
 * `cli` - the `cli` object has been deprecated in favor of `CLIEngine`. As of v1.0.0, `cli` is no longer exported and should not be used by external tools.
diff --git a/docs/developer-guide/working-with-plugins.md b/docs/developer-guide/working-with-plugins.md
index 04274f21e..59f4161d4 100644
--- a/docs/developer-guide/working-with-plugins.md
+++ b/docs/developer-guide/working-with-plugins.md
@@ -119,90 +119,7 @@ The plugin support was introduced in ESLint version `0.8.0`. Ensure the `peerDep
 
 ### Testing
 
-You can test the rules of your plugin [the same way as bundled ESLint rules](working-with-rules.md#rule-unit-tests) using RuleTester.
-
-Example:
-
-```js
-"use strict";
-
-var rule = require("../../../lib/rules/custom-plugin-rule"),
-    RuleTester = require("eslint").RuleTester;
-
-var ruleTester = new RuleTester();
-ruleTester.run("custom-plugin-rule", rule, {
-    valid: [
-        "var validVariable = true",
-    ],
-
-    invalid: [
-        {
-            code: "var invalidVariable = true",
-            errors: [ { message: "Unexpected invalid variable." } ]
-        },
-        {
-            code: "var invalidVariable = true",
-            errors: [ { message: /^Unexpected.+variable/ } ]
-        }
-    ]
-});
-```
-
-The `RuleTester` constructor accepts an optional object argument, which can be used to specify defaults for your test cases. For example, if all of your test cases use ES2015, you can set it as a default:
-
-```js
-const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2015 } });
-```
-
-The `RuleTester#run()` method is used to run the tests. It should be passed the following arguments:
-
-* The name of the rule (string)
-* The rule object itself (see ["working with rules"](./working-with-rules))
-* An object containing `valid` and `invalid` properties, each of which is an array containing test cases.
-
-A test case is an object with the following properties:
-
-* `code` (string, required): The source code that the rule should be run on
-* `options` (array, optional): The options passed to the rule. The rule severity should not be included in this list.
-* `filename` (string, optional): The filename for the given case (useful for rules that make assertions about filenames)
-
-In addition to the properties above, invalid test cases can also have the following properties:
-
-* `errors` (number or array, required): Asserts some properties of the errors that the rule is expected to produce when run on this code. If this is a number, asserts the number of errors produced. Otherwise, this should be a list of objects, each containing information about a single reported error. The following properties can be used for an error (all are optional):
-    * `message` (string/regexp): The message for the error
-    * `type` (string): The type of the reported AST node
-    * `line` (number): The 1-based line number of the reported location
-    * `column` (number): The 0-based column number of the reported location
-    * `endLine` (number): The 1-based line number of the end of the reported location
-    * `endColumn` (number): The 0-based column number of the end of the reported location
-* `output` (string, optional): Asserts the output that will be produced when using this rule for a single pass of autofixing (e.g. with the `--fix` command line flag). If this is `null`, asserts that none of the reported problems suggest autofixes.
-
-Any additional properties of a test case will be passed directly to the linter as config options. For example, a test case can have a `parserOptions` property to configure parser behavior.
-
-#### Customizing RuleTester
-
-To create tests for each valid and invalid case, `RuleTester` internally uses `describe` and `it` methods from the Mocha test framework when it is available. If you use another test framework, you can override `RuleTester.describe` and `RuleTester.it` to make `RuleTester` compatible with it and have proper individual tests and feedback.
-
-Example:
-
-```js
-"use strict";
-
-var RuleTester = require("eslint").RuleTester;
-var test = require("my-test-runner");
-
-RuleTester.describe = function(text, method) {
-    RuleTester.it.title = text;
-    return method.apply(this);
-};
-
-RuleTester.it = function(text, method) {
-    test(RuleTester.it.title + ": " + text, method);
-};
-
-// then use RuleTester as documented
-```
-
+ESLint provides the [`RuleTester`](/docs/developer-guide/nodejs-api#ruletester) utility to make it easy to test the rules of your plugin.
 
 ## Share Plugins
 
diff --git a/docs/developer-guide/working-with-rules.md b/docs/developer-guide/working-with-rules.md
index 5295946f5..66a87b3bd 100644
--- a/docs/developer-guide/working-with-rules.md
+++ b/docs/developer-guide/working-with-rules.md
@@ -427,267 +427,9 @@ You can access that code path objects with five events related to code paths.
 
 ## Rule Unit Tests
 
-Each rule must have a set of unit tests submitted with it to be accepted. The test file is named the same as the source file but lives in `tests/lib/`. For example, if your rule source file is `lib/rules/foo.js` then your test file should be `tests/lib/rules/foo.js`.
+Each bundled rule for ESLint core must have a set of unit tests submitted with it to be accepted. The test file is named the same as the source file but lives in `tests/lib/`. For example, if the rule source file is `lib/rules/foo.js` then the test file should be `tests/lib/rules/foo.js`.
 
-For your rule, be sure to test:
-
-1. All instances that should be flagged as warnings.
-1. At least one pattern that should **not** be flagged as a warning.
-
-The basic pattern for a rule unit test file is:
-
-```js
-/**
- * @fileoverview Tests for no-with rule.
- * @author Nicholas C. Zakas
- */
-
-"use strict";
-
-//------------------------------------------------------------------------------
-// Requirements
-//------------------------------------------------------------------------------
-
-var rule = require("../../../lib/rules/no-with"),
-    RuleTester = require("../../../lib/testers/rule-tester");
-
-//------------------------------------------------------------------------------
-// Tests
-//------------------------------------------------------------------------------
-
-var ruleTester = new RuleTester();
-ruleTester.run("no-with", rule, {
-    valid: [
-        "foo.bar()"
-    ],
-    invalid: [
-        {
-            code: "with(foo) { bar() }",
-            errors: [{ message: "Unexpected use of 'with' statement.", type: "WithStatement"}]
-        }
-    ]
-});
-```
-
-Be sure to replace the value of `"no-with"` with your rule's ID. There are plenty of examples in the `tests/lib/rules/` directory.
-
-### Valid Code
-
-Each valid case can be either a string or an object. The object form is used when you need to specify additional global variables or arguments for the rule. For example, the following defines `window` as a global variable for code that should not trigger the rule being tested:
-
-```js
-valid: [
-    {
-        code: "window.alert()",
-        globals: [ "window" ]
-    }
-]
-```
-
-You can also pass options to the rule (if it accepts them). These arguments are equivalent to how people can configure rules in their `.eslintrc` file. For example:
-
-```js
-valid: [
-    {
-        code: "var msg = 'Hello';",
-        options: [ "single" ]
-    }
-]
-```
-
-The `options` property must be an array of options. This gets passed through to `context.options` in the rule.
-
-### Invalid Code
-
-Each invalid case must be an object containing the code to test and at least one message that is produced by the rule. The `errors` key specifies an array of objects, each containing a message (your rule may trigger multiple messages for the same code). You should also specify the type of AST node you expect to receive back using the `type` key. The AST node should represent the actual spot in the code where there is a problem. For example:
-
-```js
-invalid: [
-    {
-        code: "function doSomething() { var f; if (true) { var build = true; } f = build; }",
-        errors: [
-            { message: "build used outside of binding context.", type: "Identifier" }
-        ]
-    }
-]
-```
-
-In this case, the message is specific to the variable being used and the AST node type is `Identifier`.
-
-You can also check that the rule returns the correct line and column numbers for the message by adding `line` and `column` properties as needed (both are optional, but highly recommend):
-
-```js
-invalid: [
-    {
-        code: "function doSomething() { var f; if (true) { var build = true; } f = build; }",
-        errors: [
-            {
-                message: "build used outside of binding context.",
-                type: "Identifier",
-                line: 1,
-                column: 68
-            }
-        ]
-    }
-]
-```
-
-The test fails if the line or column reported by the rule doesn't match the options specified in the test.
-
-Similar to the valid cases, you can also specify `options` to be passed to the rule:
-
-```js
-invalid: [
-    {
-        code: "function doSomething() { var f; if (true) { var build = true; } f = build; }",
-        options: [ "double" ],
-        errors: [
-            { message: "build used outside of binding context.", type: "Identifier" }
-        ]
-    }
-]
-```
-
-For simpler cases where the only thing that really matters is the error message, you can also specify any `errors` as strings. You can also have some strings and some objects, if you like.
-
-```js
-invalid: [
-    {
-        code: "'single quotes'",
-        options: ["double"],
-        errors: ["Strings must use doublequote."]
-    }
-]
-```
-
-### Specifying Globals
-
-If your rule relies on globals to be specified, you can provide global variable declarations by using the `globals` property. For example:
-
-```js
-valid: [
-    {
-        code: "for (x of a) doSomething();",
-        globals: { window: true }
-    }
-]
-```
-
-The same works on invalid tests:
-
-```js
-invalid: [
-    {
-        code: "'single quotes'",
-        globals: { window: true },
-        errors: ["Strings must use doublequote."]
-    }
-]
-```
-
-### Specifying Settings
-
-If your rule relies on `context.settings` to be specified, you can provide those settings by using the `settings` property. For example:
-
-```js
-valid: [
-    {
-        code: "for (x of a) doSomething();",
-        settings: { message: "hi" }
-    }
-]
-```
-
-The same works on invalid tests:
-
-```js
-invalid: [
-    {
-        code: "'single quotes'",
-        settings: { message: "hi" },
-        errors: ["Strings must use doublequote."]
-    }
-]
-```
-
-You can then access `context.settings.message` inside of the rule.
-
-### Specifying Filename
-
-If your rule relies on `context.getFilename()` to be specified, you can provide the filename by using the `filename` property. For example:
-
-```js
-valid: [
-    {
-        code: "for (x of a) doSomething();",
-        filename: "foo/bar.js"
-    }
-]
-```
-
-The same works on invalid tests:
-
-```js
-invalid: [
-    {
-        code: "'single quotes'",
-        filename: "foo/bar.js",
-        errors: ["Strings must use doublequote."]
-    }
-]
-```
-
-You can then access `context.getFilename()` inside of the rule.
-
-### Specifying Parser and Parser Options
-
-Some tests require that a certain parser configuration must be used. This can be specified in test specifications via the `parser` and `parserOptions` properties. While the following examples show usage in `valid` tests, you can use the same options in `invalid` tests as well.
-
-For example, to set `ecmaVersion` to 6 (in order to use constructs like `for-of`):
-
-```js
-valid: [
-    {
-        code: "for (x of a) doSomething();",
-        parserOptions: { ecmaVersion: 6 }
-    }
-]
-```
-
-If you are working with ES6 modules:
-
-```js
-valid: [
-    {
-        code: "export default function () {};",
-        parserOptions: { ecmaVersion: 6, sourceType: "module" }
-    }
-]
-```
-
-For non-version specific features such as JSX:
-
-```js
-valid: [
-    {
-        code: "var foo = <div>{bar}</div>",
-        parserOptions: { ecmaFeatures: { jsx: true } }
-    }
-]
-```
-
-To use a different parser:
-
-```js
-valid: [
-    {
-        code: "var foo = <div>{bar}</div>",
-        parser: "my-custom-parser"
-    }
-]
-```
-
-The options available and the expected syntax for `parserOptions` is the same as those used in [configuration](../user-guide/configuring#specifying-parser-options).
+ESLint provides the [`RuleTester`](/docs/developer-guide/nodejs-api#ruletester) utility to make it easy to write tests for rules.
 
 ## Performance Testing
 
diff --git a/lib/cli-engine.js b/lib/cli-engine.js
index 6bcab1889..aabcb6fd1 100644
--- a/lib/cli-engine.js
+++ b/lib/cli-engine.js
@@ -423,9 +423,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).filter(ruleId => loadedRules.has(ruleId)).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 87412dd2a..e74fd4640 100644
--- a/lib/config/config-file.js
+++ b/lib/config/config-file.js
@@ -546,7 +546,7 @@ function loadFromDisk(resolvedPath, configContext) {
         }
 
         // validate the configuration before continuing
-        validator.validate(config, resolvedPath.configFullName, configContext.linterContext.rules, configContext.linterContext.environments);
+        validator.validate(config, resolvedPath.configFullName, configContext.linterContext.getRules(), 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 22bc1efbd..c06354e77 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);
@@ -96,21 +93,21 @@ function validateRuleSchema(id, localOptions, rulesContext) {
 
 /**
  * Validates a rule's options against its schema.
- * @param {string} id The rule's unique name.
+ * @param {{create: Function}} 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) {
     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}`);
     }
 }
 
@@ -141,16 +138,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 {Map<string, {create: Function}>} rulesMap A map from strings to loaded rules
  * @returns {void}
  */
-function validateRules(rulesConfig, source, rulesContext) {
+function validateRules(rulesConfig, source, rulesMap) {
     if (!rulesConfig) {
         return;
     }
 
-    Object.keys(rulesConfig).forEach(id => {
-        validateRuleOptions(id, rulesConfig[id], source, rulesContext);
+    Object.keys(rulesConfig).filter(id => rulesMap.has(id)).forEach(id => {
+        validateRuleOptions(rulesMap.get(id), id, rulesConfig[id], source);
     });
 }
 
@@ -221,13 +218,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 {Map<string, {create: Function}>} ruleMap A map from rule IDs to defined rules
  * @param {Environments} envContext The env context
  * @returns {void}
  */
-function validate(config, source, rulesContext, envContext) {
+function validate(config, source, ruleMap, envContext) {
     validateConfigSchema(config, source);
-    validateRules(config.rules, source, rulesContext);
+    validateRules(config.rules, source, ruleMap);
     validateEnvironment(config.env, source, envContext);
 }
 
diff --git a/lib/linter.js b/lib/linter.js
index eedcace78..f230cb200 100755
--- a/lib/linter.js
+++ b/lib/linter.js
@@ -276,7 +276,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 {Map<string, {create: Function}>} ruleMap A map from rule IDs to defined rules
  * @returns {{
  *      config: Object,
  *      problems: Problem[],
@@ -289,9 +289,9 @@ 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, ruleMap) {
 
-    let commentConfig = {
+    const commentConfig = {
         exported: {},
         astGlobals: {},
         rules: {},
@@ -339,7 +339,9 @@ 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);
+                                if (ruleMap.has(name)) {
+                                    validator.validateRuleOptions(ruleMap.get(name), name, ruleValue, `${filename} line ${comment.loc.start.line}`);
+                                }
                                 commentRules[name] = ruleValue;
                             });
                         } else {
@@ -361,14 +363,6 @@ function modifyConfigsFromComments(filename, ast, config, linterContext) {
         }
     });
 
-    // apply environment configs
-    Object.keys(commentConfig.env).forEach(name => {
-        const env = linterContext.environments.get(name);
-
-        if (env) {
-            commentConfig = ConfigOps.merge(commentConfig, env);
-        }
-    });
     Object.assign(commentConfig.rules, commentRules);
 
     return {
@@ -750,17 +744,8 @@ module.exports = class Linter {
     verify(textOrSourceCode, config, filenameOrOptions, saveState) {
         let text,
             parserServices,
-            allowInlineConfig;
-
-        if (typeof textOrSourceCode === "string") {
-            this.sourceCode = null;
-            text = textOrSourceCode;
-        } else {
-            this.sourceCode = textOrSourceCode;
-            text = this.sourceCode.text;
-        }
-
-        let providedFilename;
+            allowInlineConfig,
+            providedFilename;
 
         // evaluate arguments
         if (typeof filenameOrOptions === "object") {
@@ -777,6 +762,14 @@ module.exports = class Linter {
             this.reset();
         }
 
+        if (typeof textOrSourceCode === "string") {
+            this.sourceCode = null;
+            text = textOrSourceCode;
+        } else {
+            this.sourceCode = textOrSourceCode;
+            text = this.sourceCode.text;
+        }
+
         // search and apply "eslint-env *".
         const envInFile = findEslintEnv(text);
 
@@ -824,7 +817,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, this.getRules());
 
             config = modifyConfigResult.config;
             modifyConfigResult.problems.forEach(problem => problems.push(problem));
diff --git a/lib/rules/indent-legacy.js b/lib/rules/indent-legacy.js
index 301217e4a..5534944e6 100644
--- a/lib/rules/indent-legacy.js
+++ b/lib/rules/indent-legacy.js
@@ -1040,7 +1040,7 @@ module.exports = {
 
                 const checkNodes = [node.property];
 
-                const dot = context.getTokenBefore(node.property);
+                const dot = sourceCode.getTokenBefore(node.property);
 
                 if (dot.type === "Punctuator" && dot.value === ".") {
                     checkNodes.push(dot);
diff --git a/lib/rules/no-tabs.js b/lib/rules/no-tabs.js
index 0757a6906..4bab96f38 100644
--- a/lib/rules/no-tabs.js
+++ b/lib/rules/no-tabs.js
@@ -27,7 +27,7 @@ module.exports = {
     create(context) {
         return {
             Program(node) {
-                context.getSourceLines().forEach((line, index) => {
+                context.getSourceCode().getLines().forEach((line, index) => {
                     const match = regex.exec(line);
 
                     if (match) {
diff --git a/package.json b/package.json
index dd79d8d27..898ce38ad 100644
--- a/package.json
+++ b/package.json
@@ -85,7 +85,7 @@
     "coveralls": "^2.13.1",
     "dateformat": "^2.0.0",
     "ejs": "^2.5.6",
-    "eslint-plugin-eslint-plugin": "^1.0.0",
+    "eslint-plugin-eslint-plugin": "^1.2.0",
     "eslint-plugin-node": "^5.1.0",
     "eslint-release": "^0.10.1",
     "eslump": "1.6.0",


Test patch:
diff --git a/lib/testers/rule-tester.js b/lib/testers/rule-tester.js
index c16f51c6d..ce30f2783 100644
--- a/lib/testers/rule-tester.js
+++ b/lib/testers/rule-tester.js
@@ -309,7 +309,7 @@ class RuleTester {
 
             linter.defineRule(ruleName, rule);
 
-            const schema = validator.getRuleOptionsSchema(ruleName, linter.rules);
+            const schema = validator.getRuleOptionsSchema(rule);
 
             if (schema) {
                 ajv.validateSchema(schema);
@@ -325,7 +325,7 @@ class RuleTester {
                 }
             }
 
-            validator.validate(config, "rule-tester", linter.rules, new Environments());
+            validator.validate(config, "rule-tester", linter.getRules(), new Environments());
 
             /*
              * Setup AST getters.
diff --git a/tests/lib/config/config-validator.js b/tests/lib/config/config-validator.js
index c6fec97ae..467035d97 100644
--- a/tests/lib/config/config-validator.js
+++ b/tests/lib/config/config-validator.js
@@ -98,7 +98,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", linter.getRules(), linter.environments);
 
             assert.doesNotThrow(fn);
         });
@@ -118,7 +118,7 @@ describe("Validator", () => {
                     rules: {}
                 },
                 "tests",
-                linter.rules,
+                linter.getRules(),
                 linter.environments
             );
 
@@ -132,7 +132,7 @@ describe("Validator", () => {
                     foo: true
                 },
                 "tests",
-                linter.rules,
+                linter.getRules(),
                 linter.environments
             );
 
@@ -141,13 +141,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, linter.getRules(), 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, linter.getRules(), linter.environments);
 
                 assert.throws(fn, "Property \"root\" is the wrong type (expected boolean but got `0`).");
             });
@@ -155,13 +155,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, linter.getRules(), 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, linter.getRules(), linter.environments);
 
                 assert.throws(fn, "Property \"globals\" is the wrong type (expected object but got `[\"jQuery\"]`).");
             });
@@ -169,7 +169,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, linter.getRules(), linter.environments);
 
                 assert.doesNotThrow(fn);
             });
@@ -178,31 +178,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, linter.getRules(), 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, linter.getRules(), 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, linter.getRules(), 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, linter.getRules(), 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, linter.getRules(), linter.environments);
 
                 assert.doesNotThrow(fn);
             });
@@ -211,13 +211,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, linter.getRules(), 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, linter.getRules(), linter.environments);
 
                 assert.throws(fn, "Property \"plugins\" is the wrong type (expected array but got `\"react\"`).");
             });
@@ -225,13 +225,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, linter.getRules(), 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, linter.getRules(), linter.environments);
 
                 assert.throws(fn, "Property \"settings\" is the wrong type (expected object but got `[\"foo\"]`).");
             });
@@ -239,19 +239,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, linter.getRules(), 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, linter.getRules(), 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, linter.getRules(), linter.environments);
 
                 assert.throws(fn, "Property \"extends\" is the wrong type (expected string/array but got `{}`).");
             });
@@ -259,13 +259,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, linter.getRules(), 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, linter.getRules(), linter.environments);
 
                 assert.throws(fn, "Property \"parserOptions\" is the wrong type (expected object but got `[\"foo\"]`).");
             });
@@ -274,67 +274,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", linter.getRules(), 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", linter.getRules(), 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", linter.getRules(), 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", linter.getRules(), 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", linter.getRules(), 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", linter.getRules(), 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", linter.getRules(), 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", linter.getRules(), 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", linter.getRules(), 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", linter.getRules(), 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", linter.getRules(), 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 +342,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", linter.getRules(), linter.environments);
 
                 assert.doesNotThrow(fn);
             });
@@ -350,7 +350,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", linter.getRules(), 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 +358,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", linter.getRules(), 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", linter.getRules(), 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", linter.getRules(), 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", linter.getRules(), 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", linter.getRules(), 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", linter.getRules(), 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", linter.getRules(), linter.environments);
 
                 assert.throws(fn, "ESLint configuration in tests is invalid:\n\t- Unexpected top-level property \"overrides[0].root\".\n");
             });
@@ -405,12 +405,12 @@ describe("Validator", () => {
     describe("getRuleOptionsSchema", () => {
 
         it("should return null for a missing rule", () => {
-            assert.equal(validator.getRuleOptionsSchema("non-existent-rule", linter.rules), null);
+            assert.equal(validator.getRuleOptionsSchema(linter.rules.get("non-existent-rule")), null);
         });
 
         it("should not modify object schema", () => {
             linter.defineRule("mock-object-rule", mockObjectRule);
-            assert.deepEqual(validator.getRuleOptionsSchema("mock-object-rule", linter.rules), {
+            assert.deepEqual(validator.getRuleOptionsSchema(linter.rules.get("mock-object-rule")), {
                 enum: ["first", "second"]
             });
         });
@@ -420,43 +420,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");
         });
diff --git a/tests/lib/linter.js b/tests/lib/linter.js
index dd8201c8a..2185b39db 100644
--- a/tests/lib/linter.js
+++ b/tests/lib/linter.js
@@ -3202,6 +3202,29 @@ describe("Linter", () => {
             linter.verify(linter.getSourceCode(), { parserOptions: { ecmaVersion: 6, ecmaFeatures: { jsx: true } } });
         });
 
+        it("should reuse the SourceCode object", () => {
+            let ast1 = null,
+                ast2 = null;
+
+            linter.defineRule("save-ast1", () => ({
+                Program(node) {
+                    ast1 = node;
+                }
+            }));
+            linter.defineRule("save-ast2", () => ({
+                Program(node) {
+                    ast2 = node;
+                }
+            }));
+
+            linter.verify("function render() { return <div className='test'>{hello}</div> }", { parserOptions: { ecmaVersion: 6, ecmaFeatures: { jsx: true } }, rules: { "save-ast1": 2 } });
+            linter.verify(linter.getSourceCode(), { parserOptions: { ecmaVersion: 6, ecmaFeatures: { jsx: true } }, rules: { "save-ast2": 2 } });
+
+            assert(ast1 !== null);
+            assert(ast2 !== null);
+            assert(ast1 === ast2);
+        });
+
         it("should allow 'await' as a property name in modules", () => {
             const result = linter.verify(
                 "obj.await",