Repository: eslint/eslint
Pull request #9276 (2017-09-10 01:41:09)

Chore: avoid monkeypatching Linter instances in RuleTester
<!--
    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)**

This updates `RuleTester` to avoid monkeypatching its `Linter` instance, and use rule composition instead in order to freeze the context properties.

**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/Makefile.js b/Makefile.js
index 7e22731d2..ded744d63 100644
--- a/Makefile.js
+++ b/Makefile.js
@@ -792,6 +792,7 @@ target.gensite = function(prereleaseVersion) {
     // 13. Update demos, but only for non-prereleases
     if (!prereleaseVersion) {
         echo("> Updating the demos (Step 13)");
+        target.browserify();
         cp("-f", "build/eslint.js", `${SITE_DIR}js/app/eslint.js`);
     } else {
         echo("> Skipped updating the demos (Step 13)");
diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md
index 622d8612b..797bee468 100644
--- a/docs/developer-guide/nodejs-api.md
+++ b/docs/developer-guide/nodejs-api.md
@@ -60,16 +60,16 @@ var linter = new Linter();
 
 ### Linter#verify
 
-The most important method on `Linter` is `verify()`, which initiates linting of the given text. This method accepts four arguments:
+The most important method on `Linter` is `verify()`, which initiates linting of the given text. This method accepts three arguments:
 
 * `code` - the source code to lint (a string or instance of `SourceCode`).
 * `config` - a configuration object that has been processed and normalized by CLIEngine using eslintrc files and/or other configuration arguments.
     * **Note**: If you want to lint text and have your configuration be read and processed, use CLIEngine's [`executeOnFiles`](#executeonfiles) or [`executeOnText`](#executeontext) instead.
-* `optionsOrFilename` - (optional) Additional options for this run or a string representing the filename to associate with the code being linted.
+* `options` - (optional) Additional options for this run.
     * `filename` - (optional) the filename to associate with the source code.
-    * `saveState` - (optional) see below. This will override any value passed as the fourth argument if an options object is used here instead of the filename.
     * `allowInlineConfig` - (optional) set to `false` to disable inline comments from changing eslint rules.
-* `saveState` - (optional) set to true to maintain the internal state of `linter` after linting (mostly used for testing purposes)
+
+If the third argument is a string, it is interpreted as the `filename`.
 
 You can call `verify()` like this:
 
@@ -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/ast-utils.js b/lib/ast-utils.js
index e65284ff7..47cc71990 100644
--- a/lib/ast-utils.js
+++ b/lib/ast-utils.js
@@ -626,6 +626,9 @@ module.exports = {
                 //     // setup...
                 //     return function foo() { ... };
                 //   })();
+                //   obj.foo = (() =>
+                //     function foo() { ... }
+                //   )();
                 case "ReturnStatement": {
                     const func = getUpperFunction(parent);
 
@@ -635,6 +638,12 @@ module.exports = {
                     node = func.parent;
                     break;
                 }
+                case "ArrowFunctionExpression":
+                    if (node !== parent.body || !isCallee(parent)) {
+                        return true;
+                    }
+                    node = parent.parent;
+                    break;
 
                 // e.g.
                 //   var obj = { foo() { ... } };
diff --git a/lib/cli-engine.js b/lib/cli-engine.js
index 6bcab1889..8ac336c7a 100644
--- a/lib/cli-engine.js
+++ b/lib/cli-engine.js
@@ -142,10 +142,6 @@ function calculateStatsPerRun(results) {
  * @private
  */
 function processText(text, configHelper, filename, fix, allowInlineConfig, linter) {
-
-    // clear all existing settings for a new file
-    linter.reset();
-
     let filePath,
         messages,
         fileExtension,
@@ -423,9 +419,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..1e9a75028 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 {
@@ -715,14 +709,6 @@ module.exports = class Linter {
         this.environments = new Environments();
     }
 
-    /**
-     * Resets the internal state of the object.
-     * @returns {void}
-     */
-    reset() {
-        this.sourceCode = null;
-    }
-
     /**
      * Configuration object for the `verify` API. A JS representation of the eslintrc files.
      * @typedef {Object} ESLintConfig
@@ -741,40 +727,32 @@ module.exports = class Linter {
      * @param {(string|Object)} [filenameOrOptions] The optional filename of the file being checked.
      *      If this is not set, the filename will default to '<input>' in the rule context. If
      *      an object, then it has "filename", "saveState", and "allowInlineConfig" properties.
-     * @param {boolean} [saveState] Indicates if the state from the last run should be saved.
-     *      Mostly useful for testing purposes.
      * @param {boolean} [filenameOrOptions.allowInlineConfig] Allow/disallow inline comments' ability to change config once it is set. Defaults to true if not supplied.
      *      Useful if you want to validate JS without comments overriding rules.
      * @returns {Object[]} The results as an array of messages or null if no messages.
      */
-    verify(textOrSourceCode, config, filenameOrOptions, saveState) {
+    verify(textOrSourceCode, config, filenameOrOptions) {
         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") {
             providedFilename = filenameOrOptions.filename;
             allowInlineConfig = filenameOrOptions.allowInlineConfig;
-            saveState = filenameOrOptions.saveState;
         } else {
             providedFilename = filenameOrOptions;
         }
 
         const filename = typeof providedFilename === "string" ? providedFilename : "<input>";
 
-        if (!saveState) {
-            this.reset();
+        if (typeof textOrSourceCode === "string") {
+            this.sourceCode = null;
+            text = textOrSourceCode;
+        } else {
+            this.sourceCode = textOrSourceCode;
+            text = this.sourceCode.text;
         }
 
         // search and apply "eslint-env *".
@@ -824,7 +802,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..98297262b 100644
--- a/lib/testers/rule-tester.js
+++ b/lib/testers/rule-tester.js
@@ -307,9 +307,19 @@ class RuleTester {
                 config.rules[ruleName] = 1;
             }
 
-            linter.defineRule(ruleName, rule);
+            linter.defineRule(ruleName, Object.assign({}, rule, {
 
-            const schema = validator.getRuleOptionsSchema(ruleName, linter.rules);
+                // 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);
+                }
+            }));
+
+            const schema = validator.getRuleOptionsSchema(rule);
 
             if (schema) {
                 ajv.validateSchema(schema);
@@ -325,15 +335,13 @@ class RuleTester {
                 }
             }
 
-            validator.validate(config, "rule-tester", linter.rules, new Environments());
+            validator.validate(config, "rule-tester", linter.getRules(), new Environments());
 
             /*
              * Setup AST getters.
              * The goal is to check whether or not AST was modified when
              * running the rule under test.
              */
-            linter.reset();
-
             linter.defineRule("rule-tester/validate-ast", () => ({
                 Program(node) {
                     beforeAST = cloneDeeplyExcludesParent(node);
@@ -343,35 +351,11 @@ class RuleTester {
                 }
             }));
 
-            // Freezes rule-context properties.
-            const originalGet = linter.rules.get;
-
-            try {
-                linter.rules.get = function(ruleId) {
-                    const originalRule = originalGet.call(linter.rules, ruleId);
-
-                    return {
-                        meta: originalRule.meta,
-                        create(context) {
-                            Object.freeze(context);
-                            freezeDeeply(context.options);
-                            freezeDeeply(context.settings);
-                            freezeDeeply(context.parserOptions);
-
-                            return originalRule.create(context);
-                        }
-                    };
-
-                };
-
-                return {
-                    messages: linter.verify(code, config, filename, true),
-                    beforeAST,
-                    afterAST: cloneDeeplyExcludesParent(afterAST)
-                };
-            } finally {
-                linter.rules.get = originalGet;
-            }
+            return {
+                messages: linter.verify(code, config, filename, true),
+                beforeAST,
+                afterAST: cloneDeeplyExcludesParent(afterAST)
+            };
         }
 
         /**
diff --git a/tests/lib/ast-utils.js b/tests/lib/ast-utils.js
index 085bd35c0..fa092fa10 100644
--- a/tests/lib/ast-utils.js
+++ b/tests/lib/ast-utils.js
@@ -56,8 +56,6 @@ describe("ast-utils", () => {
                 `Expected ${func.toString()} to be called at least once but it was not called`
             );
         });
-
-        linter.reset();
     });
 
     describe("isTokenOnSameLine", () => {
diff --git a/tests/lib/code-path-analysis/code-path-analyzer.js b/tests/lib/code-path-analysis/code-path-analyzer.js
index ee603ef47..82384b157 100644
--- a/tests/lib/code-path-analysis/code-path-analyzer.js
+++ b/tests/lib/code-path-analysis/code-path-analyzer.js
@@ -54,11 +54,6 @@ function getExpectedDotArrows(source) {
 //------------------------------------------------------------------------------
 
 describe("CodePathAnalyzer", () => {
-
-    afterEach(() => {
-        linter.reset();
-    });
-
     EventGeneratorTester.testEventGeneratorInterface(
         new CodePathAnalyzer(new NodeEventGenerator(new EventEmitter()))
     );
diff --git a/tests/lib/code-path-analysis/code-path.js b/tests/lib/code-path-analysis/code-path.js
index 6bbd1ec48..be0b35f5c 100644
--- a/tests/lib/code-path-analysis/code-path.js
+++ b/tests/lib/code-path-analysis/code-path.js
@@ -26,7 +26,6 @@ const linter = new Linter();
 function parseCodePaths(code) {
     const retv = [];
 
-    linter.reset();
     linter.defineRule("test", () => ({
         onCodePathStart(codePath) {
             retv.push(codePath);
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..5b65fb06c 100644
--- a/tests/lib/linter.js
+++ b/tests/lib/linter.js
@@ -171,7 +171,6 @@ describe("Linter", () => {
         const code = TEST_CODE;
 
         it("should retrieve SourceCode object after reset", () => {
-            linter.reset();
             linter.verify(code, {}, filename, true);
 
             const sourceCode = linter.getSourceCode();
@@ -182,7 +181,6 @@ describe("Linter", () => {
         });
 
         it("should retrieve SourceCode object without reset", () => {
-            linter.reset();
             linter.verify(code, {}, filename);
 
             const sourceCode = linter.getSourceCode();
@@ -860,7 +858,6 @@ describe("Linter", () => {
         const code = "test-rule";
 
         it("should pass settings to all rules", () => {
-            linter.reset();
             linter.defineRule(code, context => ({
                 Literal(node) {
                     context.report(node, context.settings.info);
@@ -878,7 +875,6 @@ describe("Linter", () => {
         });
 
         it("should not have any settings if they were not passed in", () => {
-            linter.reset();
             linter.defineRule(code, context => ({
                 Literal(node) {
                     if (Object.getOwnPropertyNames(context.settings).length !== 0) {
@@ -908,7 +904,6 @@ describe("Linter", () => {
                 }
             };
 
-            linter.reset();
             linter.defineRule("test-rule", sandbox.mock().withArgs(
                 sinon.match({ parserOptions })
             ).returns({}));
@@ -922,7 +917,6 @@ describe("Linter", () => {
 
             const parserOptions = {};
 
-            linter.reset();
             linter.defineRule("test-rule", sandbox.mock().withArgs(
                 sinon.match({ parserOptions })
             ).returns({}));
@@ -942,7 +936,6 @@ describe("Linter", () => {
 
                 const alternateParser = "esprima-fb";
 
-                linter.reset();
                 linter.defineRule("test-rule", sandbox.mock().withArgs(
                     sinon.match({ parserPath: alternateParser })
                 ).returns({}));
@@ -955,9 +948,6 @@ describe("Linter", () => {
             it("should use parseForESLint() in custom parser when custom parser is specified", () => {
 
                 const alternateParser = path.resolve(__dirname, "../fixtures/parsers/enhanced-parser.js");
-
-                linter.reset();
-
                 const config = { rules: {}, parser: alternateParser };
                 const messages = linter.verify("0", config, filename);
 
@@ -968,7 +958,6 @@ describe("Linter", () => {
 
                 const alternateParser = path.resolve(__dirname, "../fixtures/parsers/enhanced-parser.js");
 
-                linter.reset();
                 linter.defineRule("test-service-rule", context => ({
                     Literal(node) {
                         context.report({
@@ -1007,7 +996,6 @@ describe("Linter", () => {
                 config = { rules: {} };
 
             config.rules[rule] = 1;
-            linter.reset();
 
             const messages = linter.verify(code, config, filename, true);
 
@@ -1020,7 +1008,6 @@ describe("Linter", () => {
                 config = { rules: {} };
 
             config.rules[rule] = "warn";
-            linter.reset();
 
             const messages = linter.verify(code, config, filename, true);
 
@@ -1034,7 +1021,6 @@ describe("Linter", () => {
                 config = { rules: {} };
 
             config.rules[rule] = [1];
-            linter.reset();
 
             const messages = linter.verify(code, config, filename, true);
 
@@ -1047,7 +1033,6 @@ describe("Linter", () => {
                 config = { rules: {} };
 
             config.rules[rule] = ["warn"];
-            linter.reset();
 
             const messages = linter.verify(code, config, filename, true);
 
@@ -1061,7 +1046,6 @@ describe("Linter", () => {
                 config = { rules: {} };
 
             config.rules[rule] = "1";
-            linter.reset();
 
             const messages = linter.verify(code, config, filename, true);
 
@@ -1070,8 +1054,6 @@ describe("Linter", () => {
 
         it("should process empty config", () => {
             const config = {};
-
-            linter.reset();
             const messages = linter.verify(code, config, filename, true);
 
             assert.equal(messages.length, 0);
@@ -1415,7 +1397,6 @@ describe("Linter", () => {
         const code = "new-rule";
 
         it("can add a rule dynamically", () => {
-            linter.reset();
             linter.defineRule(code, context => ({
                 Literal(node) {
                     context.report(node, "message");
@@ -1438,7 +1419,6 @@ describe("Linter", () => {
         const code = ["new-rule-0", "new-rule-1"];
 
         it("can add multiple rules dynamically", () => {
-            linter.reset();
             const config = { rules: {} };
             const newRules = {};
 
@@ -1470,7 +1450,6 @@ describe("Linter", () => {
         const code = "filename-rule";
 
         it("has access to the filename", () => {
-            linter.reset();
             linter.defineRule(code, context => ({
                 Literal(node) {
                     context.report(node, context.getFilename());
@@ -1487,7 +1466,6 @@ describe("Linter", () => {
         });
 
         it("defaults filename to '<input>'", () => {
-            linter.reset();
             linter.defineRule(code, context => ({
                 Literal(node) {
                     context.report(node, context.getFilename());
@@ -1522,8 +1500,6 @@ describe("Linter", () => {
             const config = { rules: { strict: 2 } };
             const codeA = "/*eslint strict: 0*/ function bar() { return 2; }";
             const codeB = "function foo() { return 1; }";
-
-            linter.reset();
             let messages = linter.verify(codeA, config, filename, false);
 
             assert.equal(messages.length, 0);
@@ -1536,8 +1512,6 @@ describe("Linter", () => {
             const config = { rules: { quotes: [2, "double"] } };
             const codeA = "/*eslint quotes: 0*/ function bar() { return '2'; }";
             const codeB = "function foo() { return '1'; }";
-
-            linter.reset();
             let messages = linter.verify(codeA, config, filename, false);
 
             assert.equal(messages.length, 0);
@@ -1550,8 +1524,6 @@ describe("Linter", () => {
             const config = { rules: { quotes: [2, "double"] } };
             const codeA = "/*eslint quotes: [0, \"single\"]*/ function bar() { return '2'; }";
             const codeB = "function foo() { return '1'; }";
-
-            linter.reset();
             let messages = linter.verify(codeA, config, filename, false);
 
             assert.equal(messages.length, 0);
@@ -1564,8 +1536,6 @@ describe("Linter", () => {
             const config = { rules: { "no-unused-vars": [2, { vars: "all" }] } };
             const codeA = "/*eslint no-unused-vars: [0, {\"vars\": \"local\"}]*/ var a = 44;";
             const codeB = "var b = 55;";
-
-            linter.reset();
             let messages = linter.verify(codeA, config, filename, false);
 
             assert.equal(messages.length, 0);
@@ -1691,8 +1661,6 @@ describe("Linter", () => {
             const config = { rules: { "test-plugin/test-rule": 2 } };
             const codeA = "/*eslint test-plugin/test-rule: 0*/ var a = \"trigger violation\";";
             const codeB = "var a = \"trigger violation\";";
-
-            linter.reset();
             let messages = linter.verify(codeA, config, filename, false);
 
             assert.equal(messages.length, 0);
@@ -2907,16 +2875,6 @@ describe("Linter", () => {
             });
         });
 
-        describe("saveState", () => {
-            it("should save the state when saveState is passed as an option", () => {
-
-                const spy = sinon.spy(linter, "reset");
-
-                linter.verify("foo;", {}, { saveState: true });
-                assert.equal(spy.callCount, 0);
-            });
-        });
-
         it("should report warnings in order by line and column when called", () => {
 
             const code = "foo()\n    alert('test')";
@@ -3202,6 +3160,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",
diff --git a/tests/lib/rules/no-invalid-this.js b/tests/lib/rules/no-invalid-this.js
index d975a385a..a87a9aa44 100644
--- a/tests/lib/rules/no-invalid-this.js
+++ b/tests/lib/rules/no-invalid-this.js
@@ -308,6 +308,26 @@ const patterns = [
         valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES],
         invalid: []
     },
+    {
+        code: "obj.foo = (() => function() { console.log(this); z(x => console.log(x, this)); })();",
+        parserOptions: { ecmaVersion: 6 },
+        valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES],
+        invalid: []
+    },
+    {
+        code: "obj.foo = (function() { return () => { console.log(this); z(x => console.log(x, this)); }; })();",
+        parserOptions: { ecmaVersion: 6 },
+        valid: [NORMAL],
+        invalid: [USE_STRICT, IMPLIED_STRICT, MODULES],
+        errors
+    },
+    {
+        code: "obj.foo = (() => () => { console.log(this); z(x => console.log(x, this)); })();",
+        parserOptions: { ecmaVersion: 6 },
+        valid: [NORMAL],
+        invalid: [USE_STRICT, IMPLIED_STRICT, MODULES],
+        errors
+    },
 
     // Class Instance Methods.
     {
diff --git a/tests/lib/util/source-code.js b/tests/lib/util/source-code.js
index 746d78568..ac0cfa79d 100644
--- a/tests/lib/util/source-code.js
+++ b/tests/lib/util/source-code.js
@@ -216,10 +216,6 @@ describe("SourceCode", () => {
 
         const sandbox = sinon.sandbox.create();
 
-        beforeEach(() => {
-            linter.reset();
-        });
-
         afterEach(() => {
             sandbox.verifyAndRestore();
         });
@@ -905,7 +901,6 @@ describe("SourceCode", () => {
 
 
         beforeEach(() => {
-            linter.reset();
             unusedAssertionFuncs = new Set();
         });
 
diff --git a/tests/tools/eslint-fuzzer.js b/tests/tools/eslint-fuzzer.js
index 8ea25e0f5..26047308b 100644
--- a/tests/tools/eslint-fuzzer.js
+++ b/tests/tools/eslint-fuzzer.js
@@ -38,7 +38,6 @@ describe("eslint-fuzzer", function() {
     });
 
     after(() => {
-        linter.reset();
         configRule.createCoreRuleConfigs.restore();
     });