Repository: eslint/eslint
Pull request #9176 (2017-08-28 22:41:11)

no-unused-vars: Improve error messages to include the allowed patterns
This is a WIP and is just providing an implementation of tests to ensure the cases are satisfactory before updating the rule.

**What is the purpose of this pull request? (put an "X" next to item)**

[ ] Documentation update
[ ] Bug fix ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/bug-report.md))
[ ] New rule ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/rule-proposal.md))
[x] Changes an existing rule ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/rule-change-proposal.md))
[ ] Add autofixing to a rule
[ ] Add a CLI option
[ ] Add something to the core
[ ] Other, please explain:

**What rule do you want to change?**
no-unused-vars

**Does this change cause the rule to produce more or fewer warnings?**
Neither

**How will the change be implemented? (New option, new default behavior, etc.)?**
Updating the error messages

**Please provide some example code that this change will affect:**
See #9171 

**What changes did you make? (Give an overview)**
Improving the error messages to give a hint as to what the allowed pattern is. 

**Is there anything you'd like reviewers to focus on?**




eslintbot: Thanks for the pull request, @TheSavior! I took a look to make sure it's ready for merging and found some changes are needed:


* The commit summary needs to begin with a tag (such as `Fix:` or `Update:`). Please check out our [guide](http://eslint.org/docs/developer-guide/contributing/pull-requests#step-2-make-your-changes) for how to properly format your commit summary and [update](http://eslint.org/docs/developer-guide/contributing/pull-requests#updating-the-commit-message) it on this pull request.


Can you please update the pull request to address these?

(More information can be found in our [pull request guide](http://eslint.org/docs/developer-guide/contributing/pull-requests).)
jsf-clabot: [![CLA assistant check](https://cla.js.foundation/pull/badge/not_signed)](https://cla.js.foundation/eslint/eslint?pullRequest=9176) <br/>Thank you for your submission, we really appreciate it. Like many open source projects, we ask that you sign our [Contributor License Agreement](https://cla.js.foundation/eslint/eslint?pullRequest=9176) before we can accept your contribution.<br/>
eslintbot: Thanks for the pull request, @TheSavior! I took a look to make sure it's ready for merging and found some changes are needed:


* The commit summary needs to begin with a tag (such as `Fix:` or `Update:`). Please check out our [guide](http://eslint.org/docs/developer-guide/contributing/pull-requests#step-2-make-your-changes) for how to properly format your commit summary and [update](http://eslint.org/docs/developer-guide/contributing/pull-requests#updating-the-commit-message) it on this pull request.


Can you please update the pull request to address these?

(More information can be found in our [pull request guide](http://eslint.org/docs/developer-guide/contributing/pull-requests).)
not-an-aardvark: The report messages look good to me. As a nitpick, I think we generally try to make report messages end in a period, but it's not a big deal either way.
eslintbot: Thanks for the pull request, @TheSavior! I took a look to make sure it's ready for merging and found some changes are needed:


* The commit summary needs to begin with a tag (such as `Fix:` or `Update:`). Please check out our [guide](http://eslint.org/docs/developer-guide/contributing/pull-requests#step-2-make-your-changes) for how to properly format your commit summary and [update](http://eslint.org/docs/developer-guide/contributing/pull-requests#updating-the-commit-message) it on this pull request.


Can you please update the pull request to address these?

(More information can be found in our [pull request guide](http://eslint.org/docs/developer-guide/contributing/pull-requests).)
eslintbot: Thanks for the pull request, @TheSavior! I took a look to make sure it's ready for merging and found some changes are needed:


* The commit summary needs to begin with a tag (such as `Fix:` or `Update:`). Please check out our [guide](http://eslint.org/docs/developer-guide/contributing/pull-requests#step-2-make-your-changes) for how to properly format your commit summary and [update](http://eslint.org/docs/developer-guide/contributing/pull-requests#updating-the-commit-message) it on this pull request.


Can you please update the pull request to address these?

(More information can be found in our [pull request guide](http://eslint.org/docs/developer-guide/contributing/pull-requests).)
eslintbot: Thanks for the pull request, @TheSavior! I took a look to make sure it's ready for merging and found some changes are needed:


* The commit summary must be 72 characters or shorter. Please check out our [guide](http://eslint.org/docs/developer-guide/contributing/pull-requests#step-2-make-your-changes) for how to properly format your commit summary and [update](http://eslint.org/docs/developer-guide/contributing/pull-requests#updating-the-commit-message) it on this pull request.


Can you please update the pull request to address these?

(More information can be found in our [pull request guide](http://eslint.org/docs/developer-guide/contributing/pull-requests).)
eslintbot: Thanks for the pull request, @TheSavior! I took a look to make sure it's ready for merging and found some changes are needed:


* The commit summary must be 72 characters or shorter. Please check out our [guide](http://eslint.org/docs/developer-guide/contributing/pull-requests#step-2-make-your-changes) for how to properly format your commit summary and [update](http://eslint.org/docs/developer-guide/contributing/pull-requests#updating-the-commit-message) it on this pull request.


Can you please update the pull request to address these?

(More information can be found in our [pull request guide](http://eslint.org/docs/developer-guide/contributing/pull-requests).)
eslintbot: LGTM
eslintbot: LGTM
eslintbot: LGTM
TheSavior: Yay! It builds successfully. 

Patch:
diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md
index 431aaf049..622d8612b 100644
--- a/docs/developer-guide/nodejs-api.md
+++ b/docs/developer-guide/nodejs-api.md
@@ -58,6 +58,8 @@ var Linter = require("eslint").Linter;
 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:
 
 * `code` - the source code to lint (a string or instance of `SourceCode`).
@@ -147,7 +149,7 @@ console.log(code.text);     // "var foo = bar;"
 
 In this way, you can retrieve the text and AST used for the last run of `linter.verify()`.
 
-### verifyAndFix()
+### Linter#verifyAndFix()
 
 This method is similar to verify except that it also runs autofixing logic, similar to the `--fix` flag on the command line. The result object will contain the autofixed code, along with any remaining linting messages for the code that were not autofixed.
 
@@ -178,6 +180,76 @@ The information available is:
 * `output` - Fixed code text (might be the same as input if no fixes were applied).
 * `messages` - Collection of all messages for the given code (It has the same information as explained above under `verify` block).
 
+### Linter#defineRule
+
+Each `Linter` instance holds a map of rule names to loaded rule objects. By default, all ESLint core rules are loaded. If you want to use `Linter` with custom rules, you should use the `defineRule` method to register your rules by ID.
+
+```js
+const Linter = require("eslint").Linter;
+const linter = new Linter();
+
+linter.defineRule("my-custom-rule", {
+    // (an ESLint rule)
+
+    create(context) {
+        // ...
+    }
+});
+
+const results = linter.verify("// some source text", { rules: { "my-custom-rule": "error" } });
+```
+
+### Linter#defineRules
+
+This is a convenience method similar to `Linter#defineRule`, except that it allows you to define many rules at once using an object.
+
+```js
+const Linter = require("eslint").Linter;
+const linter = new Linter();
+
+linter.defineRules({
+    "my-custom-rule": { /* an ESLint rule */ create() {} },
+    "another-custom-rule": { /* an ESLint rule */ create() {} }
+});
+
+const results = linter.verify("// some source text", {
+    rules: {
+        "my-custom-rule": "error",
+        "another-custom-rule": "warn"
+    }
+});
+```
+
+### Linter#getRules
+
+This method returns a map of all loaded rules.
+
+```js
+const Linter = require("eslint").Linter;
+const linter = new Linter();
+
+linter.getRules();
+
+/*
+Map {
+  'accessor-pairs' => { meta: { docs: [Object], schema: [Array] }, create: [Function: create] },
+  'array-bracket-newline' => { meta: { docs: [Object], schema: [Array] }, create: [Function: create] },
+  ...
+}
+*/
+```
+
+### Linter#version
+
+Each instance of `Linter` has a `version` property containing the semantic version number of ESLint that the `Linter` instance is from.
+
+```js
+const Linter = require("eslint").Linter;
+const linter = new Linter();
+
+linter.version; // => '4.5.0'
+```
+
 ## linter
 
 The `eslint.linter` object (deprecated) is an instance of the `Linter` class as defined [above](#Linter). `eslint.linter` exists for backwards compatibility, but we do not recommend using it because any mutations to it are shared among every module that uses `eslint`. Instead, please create your own instance of `eslint.Linter`.
@@ -613,6 +685,14 @@ var report = cli.executeOnFiles(["myfile.js", "lib/"]);
 CLIEngine.outputFixes(report);
 ```
 
+### CLIEngine.version
+
+`CLIEngine` has a static `version` property containing the semantic version number of ESLint that it comes from.
+
+```js
+require("eslint").CLIEngine.version; // '4.5.0'
+```
+
 ## 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/rules/generator-star-spacing.md b/docs/rules/generator-star-spacing.md
index 91fa1213b..efa7a5a4e 100644
--- a/docs/rules/generator-star-spacing.md
+++ b/docs/rules/generator-star-spacing.md
@@ -75,6 +75,27 @@ An example of shorthand configuration:
 "generator-star-spacing": ["error", "after"]
 ```
 
+Additionally, this rule allows further configurability via overrides per function type.
+
+* `named` provides overrides for named functions
+* `anonymous` provides overrides for anonymous functions
+* `method` provides overrides for class methods or property function shorthand
+
+An example of a configuration with overrides:
+
+```json
+"generator-star-spacing": ["error", {
+    "before": false,
+    "after": true,
+    "anonymous": "neither",
+    "method": {"before": true, "after": true}
+}]
+```
+
+In the example configuration above, the top level "before" and "after" options define the default behavior of
+the rule, while the "anonymous", "method", and "static" options override the default behavior.
+Overrides can be either an object with "before" and "after", or a shorthand string as above.
+
 ## Examples
 
 ### before
@@ -137,6 +158,46 @@ var anonymous = function*() {};
 var shorthand = { *generator() {} };
 ```
 
+Examples of **incorrect** code for this rule with overrides present:
+
+```js
+/*eslint generator-star-spacing: ["error", {
+    "before": false,
+    "after": true,
+    "anonymous": "neither",
+    "method": {"before": true, "after": true}
+}]*/
+/*eslint-env es6*/
+
+function * generator() {}
+
+var anonymous = function* () {};
+
+var shorthand = { *generator() {} };
+
+class Class { static* method() {} }
+```
+
+Examples of **correct** code for this rule with overrides present:
+
+```js
+/*eslint generator-star-spacing: ["error", {
+    "before": false,
+    "after": true,
+    "anonymous": "neither",
+    "method": {"before": true, "after": true}
+}]*/
+/*eslint-env es6*/
+
+function* generator() {}
+
+var anonymous = function*() {};
+
+var shorthand = { * generator() {} };
+
+class Class { static * method() {} }
+```
+
 ## When Not To Use It
 
 If your project will not be using generators or you are not concerned with spacing consistency, you do not need this rule.
diff --git a/lib/config/config-initializer.js b/lib/config/config-initializer.js
index d344fa0ac..11d441029 100644
--- a/lib/config/config-initializer.js
+++ b/lib/config/config-initializer.js
@@ -65,6 +65,7 @@ function writeFile(config, format) {
  * @param {string} moduleName The module name to get.
  * @returns {Object} The peer dependencies of the given module.
  * This object is the object of `peerDependencies` field of `package.json`.
+ * Returns null if npm was not found.
  */
 function getPeerDependencies(moduleName) {
     let result = getPeerDependencies.cache.get(moduleName);
@@ -356,7 +357,8 @@ function hasESLintVersionConflict(answers) {
     // Get the required range of ESLint version.
     const configName = getStyleGuideName(answers);
     const moduleName = `eslint-config-${configName}@latest`;
-    const requiredESLintVersionRange = getPeerDependencies(moduleName).eslint;
+    const peerDependencies = getPeerDependencies(moduleName) || {};
+    const requiredESLintVersionRange = peerDependencies.eslint;
 
     if (!requiredESLintVersionRange) {
         return false;
diff --git a/lib/linter.js b/lib/linter.js
index 856fc7e75..7b3117d92 100755
--- a/lib/linter.js
+++ b/lib/linter.js
@@ -9,10 +9,10 @@
 // Requirements
 //------------------------------------------------------------------------------
 
-const assert = require("assert"),
-    EventEmitter = require("events").EventEmitter,
+const EventEmitter = require("events").EventEmitter,
     eslintScope = require("eslint-scope"),
     levn = require("levn"),
+    lodash = require("lodash"),
     blankScriptAST = require("../conf/blank-script.json"),
     defaultConfig = require("../conf/default-config-options.js"),
     replacements = require("../conf/replacements.json"),
@@ -23,7 +23,7 @@ const assert = require("assert"),
     NodeEventGenerator = require("./util/node-event-generator"),
     SourceCode = require("./util/source-code"),
     Traverser = require("./util/traverser"),
-    RuleContext = require("./rule-context"),
+    createReportTranslator = require("./report-translator"),
     Rules = require("./rules"),
     timing = require("./timing"),
     astUtils = require("./ast-utils"),
@@ -616,18 +616,16 @@ function getRuleOptions(ruleConfig) {
  * optimization of functions, so it's best to keep the try-catch as isolated
  * as possible
  * @param {string} text The text to parse.
- * @param {Object} config The ESLint configuration object.
+ * @param {Object} providedParserOptions Options to pass to the parser
+ * @param {string} parserName The name of the parser
  * @param {string} filePath The path to the file being parsed.
- * @returns {ASTNode|CustomParseResult} The AST or parse result if successful,
- *      or null if not.
- * @param {Array<Object>} messages Messages array for the linter object
- * @returns {*} parsed text if successful otherwise null
+ * @returns {{success: false, error: Problem}|{success: true,ast: ASTNode, services: Object}}
+ * An object containing the AST and parser services if parsing was successful, or the error if parsing failed
  * @private
  */
-function parse(text, config, filePath, messages) {
+function parse(text, providedParserOptions, parserName, filePath) {
 
-    let parser;
-    const parserOptions = Object.assign({}, config.parserOptions, {
+    const parserOptions = Object.assign({}, providedParserOptions, {
         loc: true,
         range: true,
         raw: true,
@@ -636,20 +634,23 @@ function parse(text, config, filePath, messages) {
         filePath
     });
 
+    let parser;
+
     try {
-        parser = require(config.parser);
+        parser = require(parserName);
     } catch (ex) {
-        messages.push({
-            ruleId: null,
-            fatal: true,
-            severity: 2,
-            source: null,
-            message: ex.message,
-            line: 0,
-            column: 0
-        });
-
-        return null;
+        return {
+            success: false,
+            error: {
+                ruleId: null,
+                fatal: true,
+                severity: 2,
+                source: null,
+                message: ex.message,
+                line: 0,
+                column: 0
+            }
+        };
     }
 
     /*
@@ -660,31 +661,79 @@ function parse(text, config, filePath, messages) {
      */
     try {
         if (typeof parser.parseForESLint === "function") {
-            return parser.parseForESLint(text, parserOptions);
+            const parseResult = parser.parseForESLint(text, parserOptions);
+
+            return {
+                success: true,
+                ast: parseResult.ast,
+                services: parseResult.services || {}
+            };
         }
-        return parser.parse(text, parserOptions);
 
+        return {
+            success: true,
+            ast: parser.parse(text, parserOptions),
+            services: {}
+        };
     } catch (ex) {
 
         // If the message includes a leading line number, strip it:
-        const message = ex.message.replace(/^line \d+:/i, "").trim();
-        const source = (ex.lineNumber) ? SourceCode.splitLines(text)[ex.lineNumber - 1] : null;
-
-        messages.push({
-            ruleId: null,
-            fatal: true,
-            severity: 2,
-            source,
-            message: `Parsing error: ${message}`,
-
-            line: ex.lineNumber,
-            column: ex.column
-        });
+        const message = `Parsing error: ${ex.message.replace(/^line \d+:/i, "").trim()}`;
+        const source = ex.lineNumber ? SourceCode.splitLines(text)[ex.lineNumber - 1] : null;
 
-        return null;
+        return {
+            success: false,
+            error: {
+                ruleId: null,
+                fatal: true,
+                severity: 2,
+                source,
+                message,
+                line: ex.lineNumber,
+                column: ex.column
+            }
+        };
     }
 }
 
+// methods that exist on SourceCode object
+const DEPRECATED_SOURCECODE_PASSTHROUGHS = {
+    getSource: "getText",
+    getSourceLines: "getLines",
+    getAllComments: "getAllComments",
+    getNodeByRangeIndex: "getNodeByRangeIndex",
+    getComments: "getComments",
+    getCommentsBefore: "getCommentsBefore",
+    getCommentsAfter: "getCommentsAfter",
+    getCommentsInside: "getCommentsInside",
+    getJSDocComment: "getJSDocComment",
+    getFirstToken: "getFirstToken",
+    getFirstTokens: "getFirstTokens",
+    getLastToken: "getLastToken",
+    getLastTokens: "getLastTokens",
+    getTokenAfter: "getTokenAfter",
+    getTokenBefore: "getTokenBefore",
+    getTokenByRangeStart: "getTokenByRangeStart",
+    getTokens: "getTokens",
+    getTokensAfter: "getTokensAfter",
+    getTokensBefore: "getTokensBefore",
+    getTokensBetween: "getTokensBetween"
+};
+
+const BASE_TRAVERSAL_CONTEXT = Object.freeze(
+    Object.keys(DEPRECATED_SOURCECODE_PASSTHROUGHS).reduce(
+        (contextInfo, methodName) =>
+            Object.assign(contextInfo, {
+                [methodName]() {
+                    const sourceCode = this.getSourceCode();
+
+                    return sourceCode[DEPRECATED_SOURCECODE_PASSTHROUGHS[methodName]].apply(sourceCode, arguments);
+                }
+            }),
+        {}
+    )
+);
+
 //------------------------------------------------------------------------------
 // Public Interface
 //------------------------------------------------------------------------------
@@ -693,13 +742,11 @@ function parse(text, config, filePath, messages) {
  * Object that is responsible for verifying JavaScript text
  * @name eslint
  */
-class Linter extends EventEmitter {
+class Linter {
 
     constructor() {
-        super();
         this.messages = [];
         this.currentConfig = null;
-        this.currentScopes = null;
         this.scopeManager = null;
         this.currentFilename = null;
         this.traverser = null;
@@ -709,9 +756,6 @@ class Linter extends EventEmitter {
 
         this.rules = new Rules();
         this.environments = new Environments();
-
-        // set unlimited listeners (see https://github.com/eslint/eslint/issues/524)
-        this.setMaxListeners(0);
     }
 
     /**
@@ -719,10 +763,8 @@ class Linter extends EventEmitter {
      * @returns {void}
      */
     reset() {
-        this.removeAllListeners();
         this.messages = [];
         this.currentConfig = null;
-        this.currentScopes = null;
         this.scopeManager = null;
         this.traverser = null;
         this.reportingConfig = [];
@@ -754,11 +796,18 @@ class Linter extends EventEmitter {
      * @returns {Object[]} The results as an array of messages or null if no messages.
      */
     verify(textOrSourceCode, config, filenameOrOptions, saveState) {
-        const text = (typeof textOrSourceCode === "string") ? textOrSourceCode : null;
-        let ast,
-            parseResult,
+        let text,
+            parserServices,
             allowInlineConfig;
 
+        if (typeof textOrSourceCode === "string") {
+            this.sourceCode = null;
+            text = textOrSourceCode;
+        } else {
+            this.sourceCode = textOrSourceCode;
+            text = this.sourceCode.text;
+        }
+
         // evaluate arguments
         if (typeof filenameOrOptions === "object") {
             this.currentFilename = filenameOrOptions.filename;
@@ -773,7 +822,7 @@ class Linter extends EventEmitter {
         }
 
         // search and apply "eslint-env *".
-        const envInFile = findEslintEnv(text || textOrSourceCode.text);
+        const envInFile = findEslintEnv(text);
 
         config = Object.assign({}, config);
 
@@ -788,136 +837,156 @@ class Linter extends EventEmitter {
         // process initial config to make it safe to extend
         config = prepareConfig(config, this.environments);
 
-        // only do this for text
-        if (text !== null) {
+        if (this.sourceCode) {
+            parserServices = {};
+        } else {
 
             // there's no input, just exit here
             if (text.trim().length === 0) {
                 this.sourceCode = new SourceCode(text, blankScriptAST);
-                return this.messages;
+                return [];
             }
 
-            parseResult = parse(
+            const parseResult = parse(
                 stripUnicodeBOM(text).replace(astUtils.SHEBANG_MATCHER, (match, captured) => `//${captured}`),
-                config,
-                this.currentFilename,
-                this.messages
+                config.parserOptions,
+                config.parser,
+                this.currentFilename
             );
 
-            // if this result is from a parseForESLint() method, normalize
-            if (parseResult && parseResult.ast) {
-                ast = parseResult.ast;
-            } else {
-                ast = parseResult;
-                parseResult = null;
+            if (!parseResult.success) {
+                return [parseResult.error];
             }
 
-            if (ast) {
-                this.sourceCode = new SourceCode(text, ast);
-            }
+            parserServices = parseResult.services;
+            this.sourceCode = new SourceCode(text, parseResult.ast);
+        }
 
-        } else {
-            this.sourceCode = textOrSourceCode;
-            ast = this.sourceCode.ast;
+        // parse global comments and modify config
+        if (allowInlineConfig !== false) {
+            config = modifyConfigsFromComments(this.currentFilename, this.sourceCode.ast, config, this);
         }
 
-        // if espree failed to parse the file, there's no sense in setting up rules
-        if (ast) {
+        // ensure that severities are normalized in the config
+        ConfigOps.normalize(config);
 
-            // parse global comments and modify config
-            if (allowInlineConfig !== false) {
-                config = modifyConfigsFromComments(this.currentFilename, ast, config, this);
-            }
+        const emitter = new EventEmitter().setMaxListeners(Infinity);
 
-            // ensure that severities are normalized in the config
-            ConfigOps.normalize(config);
+        /*
+         * Create a frozen object with the ruleContext properties and methods that are shared by all rules.
+         * All rule contexts will inherit from this object. This avoids the performance penalty of copying all the
+         * properties once for each rule.
+         */
+        const sharedTraversalContext = Object.freeze(
+            Object.assign(
+                Object.create(BASE_TRAVERSAL_CONTEXT),
+                {
+                    getAncestors: this.getAncestors.bind(this),
+                    getDeclaredVariables: this.getDeclaredVariables.bind(this),
+                    getFilename: this.getFilename.bind(this),
+                    getScope: this.getScope.bind(this),
+                    getSourceCode: () => this.sourceCode,
+                    markVariableAsUsed: this.markVariableAsUsed.bind(this),
+                    parserOptions: config.parserOptions,
+                    parserPath: config.parser,
+                    parserServices,
+                    settings: config.settings
+                }
+            )
+        );
 
-            // enable appropriate rules
-            Object.keys(config.rules).filter(key => getRuleSeverity(config.rules[key]) > 0).forEach(key => {
-                let ruleCreator;
+        // enable appropriate rules
+        Object.keys(config.rules).filter(ruleId => getRuleSeverity(config.rules[ruleId]) > 0).forEach(ruleId => {
+            let ruleCreator = this.rules.get(ruleId);
 
-                ruleCreator = this.rules.get(key);
+            if (!ruleCreator) {
+                const replacementMsg = getRuleReplacementMessage(ruleId);
 
-                if (!ruleCreator) {
-                    const replacementMsg = getRuleReplacementMessage(key);
+                if (replacementMsg) {
+                    ruleCreator = createStubRule(replacementMsg);
+                } else {
+                    ruleCreator = createStubRule(`Definition for rule '${ruleId}' was not found`);
+                }
+                this.rules.define(ruleId, ruleCreator);
+            }
 
-                    if (replacementMsg) {
-                        ruleCreator = createStubRule(replacementMsg);
-                    } else {
-                        ruleCreator = createStubRule(`Definition for rule '${key}' was not found`);
+            const severity = getRuleSeverity(config.rules[ruleId]);
+            const ruleContext = Object.freeze(
+                Object.assign(
+                    Object.create(sharedTraversalContext),
+                    {
+                        id: ruleId,
+                        options: getRuleOptions(config.rules[ruleId]),
+                        report: lodash.flow([
+                            createReportTranslator({ ruleId, severity, sourceCode: this.sourceCode }),
+                            problem => {
+                                if (problem.fix && ruleCreator.meta && !ruleCreator.meta.fixable) {
+                                    throw new Error("Fixable rules should export a `meta.fixable` property.");
+                                }
+                                if (!isDisabledByReportingConfig(this.reportingConfig, ruleId, problem)) {
+                                    this.messages.push(problem);
+                                }
+                            }
+                        ])
                     }
-                    this.rules.define(key, ruleCreator);
-                }
+                )
+            );
 
-                const severity = getRuleSeverity(config.rules[key]);
-                const options = getRuleOptions(config.rules[key]);
+            try {
+                const rule = ruleCreator.create
+                    ? ruleCreator.create(ruleContext)
+                    : ruleCreator(ruleContext);
 
-                try {
-                    const ruleContext = new RuleContext(
-                        key, this, severity, options,
-                        config.settings, config.parserOptions, config.parser,
-                        ruleCreator.meta,
-                        (parseResult && parseResult.services ? parseResult.services : {})
+                // add all the selectors from the rule as listeners
+                Object.keys(rule).forEach(selector => {
+                    emitter.on(selector, timing.enabled
+                        ? timing.time(ruleId, rule[selector])
+                        : rule[selector]
                     );
+                });
+            } catch (ex) {
+                ex.message = `Error while loading rule '${ruleId}': ${ex.message}`;
+                throw ex;
+            }
+        });
 
-                    const rule = ruleCreator.create ? ruleCreator.create(ruleContext)
-                        : ruleCreator(ruleContext);
-
-                    // add all the selectors from the rule as listeners
-                    Object.keys(rule).forEach(selector => {
-                        this.on(selector, timing.enabled
-                            ? timing.time(key, rule[selector])
-                            : rule[selector]
-                        );
-                    });
-                } catch (ex) {
-                    ex.message = `Error while loading rule '${key}': ${ex.message}`;
-                    throw ex;
-                }
-            });
-
-            // save config so rules can access as necessary
-            this.currentConfig = config;
-            this.traverser = new Traverser();
-
-            const ecmaFeatures = this.currentConfig.parserOptions.ecmaFeatures || {};
-            const ecmaVersion = this.currentConfig.parserOptions.ecmaVersion || 5;
-
-            // gather scope data that may be needed by the rules
-            this.scopeManager = eslintScope.analyze(ast, {
-                ignoreEval: true,
-                nodejsScope: ecmaFeatures.globalReturn,
-                impliedStrict: ecmaFeatures.impliedStrict,
-                ecmaVersion,
-                sourceType: this.currentConfig.parserOptions.sourceType || "script",
-                fallback: Traverser.getKeys
-            });
-
-            this.currentScopes = this.scopeManager.scopes;
-
-            // augment global scope with declared global variables
-            addDeclaredGlobals(ast, this.currentScopes[0], this.currentConfig, this.environments);
+        // save config so rules can access as necessary
+        this.currentConfig = config;
+        this.traverser = new Traverser();
+
+        const ecmaFeatures = this.currentConfig.parserOptions.ecmaFeatures || {};
+        const ecmaVersion = this.currentConfig.parserOptions.ecmaVersion || 5;
+
+        // gather scope data that may be needed by the rules
+        this.scopeManager = eslintScope.analyze(this.sourceCode.ast, {
+            ignoreEval: true,
+            nodejsScope: ecmaFeatures.globalReturn,
+            impliedStrict: ecmaFeatures.impliedStrict,
+            ecmaVersion,
+            sourceType: this.currentConfig.parserOptions.sourceType || "script",
+            fallback: Traverser.getKeys
+        });
 
-            let eventGenerator = new NodeEventGenerator(this);
+        // augment global scope with declared global variables
+        addDeclaredGlobals(this.sourceCode.ast, this.scopeManager.scopes[0], this.currentConfig, this.environments);
 
-            eventGenerator = new CodePathAnalyzer(eventGenerator);
+        const eventGenerator = new CodePathAnalyzer(new NodeEventGenerator(emitter));
 
-            /*
-             * Each node has a type property. Whenever a particular type of
-             * node is found, an event is fired. This allows any listeners to
-             * automatically be informed that this type of node has been found
-             * and react accordingly.
-             */
-            this.traverser.traverse(ast, {
-                enter(node, parent) {
-                    node.parent = parent;
-                    eventGenerator.enterNode(node);
-                },
-                leave(node) {
-                    eventGenerator.leaveNode(node);
-                }
-            });
-        }
+        /*
+         * Each node has a type property. Whenever a particular type of
+         * node is found, an event is fired. This allows any listeners to
+         * automatically be informed that this type of node has been found
+         * and react accordingly.
+         */
+        this.traverser.traverse(this.sourceCode.ast, {
+            enter(node, parent) {
+                node.parent = parent;
+                eventGenerator.enterNode(node);
+            },
+            leave(node) {
+                eventGenerator.leaveNode(node);
+            }
+        });
 
         // sort by line and column
         this.messages.sort((a, b) => {
@@ -933,88 +1002,6 @@ class Linter extends EventEmitter {
         return this.messages;
     }
 
-    /**
-     * Reports a message from one of the rules.
-     * @param {string} ruleId The ID of the rule causing the message.
-     * @param {number} severity The severity level of the rule as configured.
-     * @param {ASTNode} node The AST node that the message relates to.
-     * @param {Object=} location An object containing the error line and column
-     *      numbers. If location is not provided the node's start location will
-     *      be used.
-     * @param {string} message The actual message.
-     * @param {Object} opts Optional template data which produces a formatted message
-     *     with symbols being replaced by this object's values.
-     * @param {Object} fix A fix command description.
-     * @param {Object} meta Metadata of the rule
-     * @returns {void}
-     */
-    report(ruleId, severity, node, location, message, opts, fix, meta) {
-        if (node) {
-            assert.strictEqual(typeof node, "object", "Node must be an object");
-        }
-
-        let endLocation;
-
-        if (typeof location === "string") {
-            assert.ok(node, "Node must be provided when reporting error if location is not provided");
-
-            meta = fix;
-            fix = opts;
-            opts = message;
-            message = location;
-            location = node.loc.start;
-            endLocation = node.loc.end;
-        } else {
-            endLocation = location.end;
-        }
-
-        location = location.start || location;
-
-        if (isDisabledByReportingConfig(this.reportingConfig, ruleId, location)) {
-            return;
-        }
-
-        if (opts) {
-            message = message.replace(/\{\{\s*([^{}]+?)\s*\}\}/g, (fullMatch, term) => {
-                if (term in opts) {
-                    return opts[term];
-                }
-
-                // Preserve old behavior: If parameter name not provided, don't replace it.
-                return fullMatch;
-            });
-        }
-
-        const problem = {
-            ruleId,
-            severity,
-            message,
-            line: location.line,
-            column: location.column + 1, // switch to 1-base instead of 0-base
-            nodeType: node && node.type,
-            source: this.sourceCode.lines[location.line - 1] || ""
-        };
-
-        // Define endLine and endColumn if exists.
-        if (endLocation) {
-            problem.endLine = endLocation.line;
-            problem.endColumn = endLocation.column + 1; // switch to 1-base instead of 0-base
-        }
-
-        // ensure there's range and text properties, otherwise it's not a valid fix
-        if (fix && Array.isArray(fix.range) && (typeof fix.text === "string")) {
-
-            // If rule uses fix, has metadata, but has no metadata.fixable, we should throw
-            if (meta && !meta.fixable) {
-                throw new Error("Fixable rules should export a `meta.fixable` property.");
-            }
-
-            problem.fix = fix;
-        }
-
-        this.messages.push(problem);
-    }
-
     /**
      * Gets the SourceCode object representing the parsed source.
      * @returns {SourceCode} The SourceCode object.
@@ -1072,7 +1059,7 @@ class Linter extends EventEmitter {
 
         }
 
-        return this.currentScopes[0];
+        return this.scopeManager.scopes[0];
     }
 
     /**
@@ -1252,33 +1239,8 @@ class Linter extends EventEmitter {
     }
 }
 
-// methods that exist on SourceCode object
-const externalMethods = {
-    getSource: "getText",
-    getSourceLines: "getLines",
-    getAllComments: "getAllComments",
-    getNodeByRangeIndex: "getNodeByRangeIndex",
-    getComments: "getComments",
-    getCommentsBefore: "getCommentsBefore",
-    getCommentsAfter: "getCommentsAfter",
-    getCommentsInside: "getCommentsInside",
-    getJSDocComment: "getJSDocComment",
-    getFirstToken: "getFirstToken",
-    getFirstTokens: "getFirstTokens",
-    getLastToken: "getLastToken",
-    getLastTokens: "getLastTokens",
-    getTokenAfter: "getTokenAfter",
-    getTokenBefore: "getTokenBefore",
-    getTokenByRangeStart: "getTokenByRangeStart",
-    getTokens: "getTokens",
-    getTokensAfter: "getTokensAfter",
-    getTokensBefore: "getTokensBefore",
-    getTokensBetween: "getTokensBetween"
-};
-
-// copy over methods
-Object.keys(externalMethods).forEach(methodName => {
-    const exMethodName = externalMethods[methodName];
+Object.keys(DEPRECATED_SOURCECODE_PASSTHROUGHS).forEach(methodName => {
+    const exMethodName = DEPRECATED_SOURCECODE_PASSTHROUGHS[methodName];
 
     // Applies the SourceCode methods to the Linter prototype
     Object.defineProperty(Linter.prototype, methodName, {
diff --git a/lib/report-translator.js b/lib/report-translator.js
new file mode 100644
index 000000000..fe59d9a75
--- /dev/null
+++ b/lib/report-translator.js
@@ -0,0 +1,274 @@
+/**
+ * @fileoverview A helper that translates context.report() calls from the rule API into generic problem objects
+ * @author Teddy Katz
+ */
+
+"use strict";
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const assert = require("assert");
+const ruleFixer = require("./util/rule-fixer");
+
+//------------------------------------------------------------------------------
+// Typedefs
+//------------------------------------------------------------------------------
+
+/**
+ * An error message description
+ * @typedef {Object} MessageDescriptor
+ * @property {ASTNode} [node] The reported node
+ * @property {Location} loc The location of the problem.
+ * @property {string} message The problem message.
+ * @property {Object} [data] Optional data to use to fill in placeholders in the
+ *      message.
+ * @property {Function} [fix] The function to call that creates a fix command.
+ */
+
+//------------------------------------------------------------------------------
+// Module Definition
+//------------------------------------------------------------------------------
+
+
+/**
+ * Translates a multi-argument context.report() call into a single object argument call
+ * @param {...*} arguments A list of arguments passed to `context.report`
+ * @returns {MessageDescriptor} A normalized object containing report information
+ */
+function normalizeMultiArgReportCall() {
+
+    // If there is one argument, it is considered to be a new-style call already.
+    if (arguments.length === 1) {
+        return Object.assign({}, arguments[0]);
+    }
+
+    // If the second argument is a string, the arguments are interpreted as [node, message, data, fix].
+    if (typeof arguments[1] === "string") {
+        return {
+            node: arguments[0],
+            message: arguments[1],
+            data: arguments[2],
+            fix: arguments[3]
+        };
+    }
+
+    // Otherwise, the arguments are interpreted as [node, loc, message, data, fix].
+    return {
+        node: arguments[0],
+        loc: arguments[1],
+        message: arguments[2],
+        data: arguments[3],
+        fix: arguments[4]
+    };
+}
+
+/**
+ * Asserts that either a loc or a node was provided, and the node is valid if it was provided.
+ * @param {MessageDescriptor} descriptor A descriptor to validate
+ * @returns {MessageDescriptor} The same descriptor
+ * @throws AssertionError if neither a node nor a loc was provided, or if the node is not an object
+ */
+function assertValidNodeInfo(descriptor) {
+    if (descriptor.node) {
+        assert(typeof descriptor.node === "object", "Node must be an object");
+    } else {
+        assert(descriptor.loc, "Node must be provided when reporting error if location is not provided");
+    }
+
+    return descriptor;
+}
+
+/**
+ * Normalizes a MessageDescriptor to always have a `loc` with `start` and `end` properties
+ * @param {MessageDescriptor} descriptor A descriptor for the report from a rule. This descriptor may be mutated
+ * by this function.
+ * @returns {MessageDescriptor} The updated MessageDescriptor that infers the `start` and `end` properties from
+ * the `node` of the original descriptor, or infers the `start` from the `loc` of the original descriptor.
+ */
+function normalizeReportLoc(descriptor) {
+    if (descriptor.loc) {
+        if (descriptor.loc.start) {
+            return descriptor;
+        }
+        return Object.assign(descriptor, { loc: { start: descriptor.loc, end: null } });
+    }
+
+    return Object.assign(descriptor, { loc: descriptor.node.loc });
+}
+
+/**
+ * Interpolates data placeholders in report messages
+ * @param {MessageDescriptor} descriptor The report message descriptor. This descriptor may be mutated
+ * by this function.
+ * @returns {MessageDescriptor} An new descriptor with a message containing the interpolated data
+ */
+function normalizeMessagePlaceholders(descriptor) {
+    if (!descriptor.data) {
+        return descriptor;
+    }
+    return Object.assign(descriptor, {
+        message: descriptor.message.replace(/\{\{\s*([^{}]+?)\s*\}\}/g, (fullMatch, term) => {
+            if (term in descriptor.data) {
+                return descriptor.data[term];
+            }
+
+            return fullMatch;
+        })
+    });
+}
+
+/**
+ * Compares items in a fixes array by range.
+ * @param {Fix} a The first message.
+ * @param {Fix} b The second message.
+ * @returns {int} -1 if a comes before b, 1 if a comes after b, 0 if equal.
+ * @private
+ */
+function compareFixesByRange(a, b) {
+    return a.range[0] - b.range[0] || a.range[1] - b.range[1];
+}
+
+/**
+ * Merges the given fixes array into one.
+ * @param {Fix[]} fixes The fixes to merge.
+ * @param {SourceCode} sourceCode The source code object to get the text between fixes.
+ * @returns {void}
+ */
+function mergeFixes(fixes, sourceCode) {
+    if (fixes.length === 0) {
+        return null;
+    }
+    if (fixes.length === 1) {
+        return fixes[0];
+    }
+
+    fixes.sort(compareFixesByRange);
+
+    const originalText = sourceCode.text;
+    const start = fixes[0].range[0];
+    const end = fixes[fixes.length - 1].range[1];
+    let text = "";
+    let lastPos = Number.MIN_SAFE_INTEGER;
+
+    for (const fix of fixes) {
+        assert(fix.range[0] >= lastPos, "Fix objects must not be overlapped in a report.");
+
+        if (fix.range[0] >= 0) {
+            text += originalText.slice(Math.max(0, start, lastPos), fix.range[0]);
+        }
+        text += fix.text;
+        lastPos = fix.range[1];
+    }
+    text += originalText.slice(Math.max(0, start, lastPos), end);
+
+    return { range: [start, end], text };
+}
+
+/**
+ * Gets one fix object from the given descriptor.
+ * If the descriptor retrieves multiple fixes, this merges those to one.
+ * @param {MessageDescriptor} descriptor The report descriptor. This descriptor may be mutated
+ * by this function.
+ * @param {SourceCode} sourceCode The source code object to get text between fixes.
+ * @returns {MessageDescriptor} The updated descriptor.
+ */
+function normalizeFixes(descriptor, sourceCode) {
+    if (typeof descriptor.fix !== "function") {
+        return Object.assign(descriptor, { fix: null });
+    }
+
+    // @type {null | Fix | Fix[] | IterableIterator<Fix>}
+    const fix = descriptor.fix(ruleFixer);
+
+    // Merge to one.
+    if (fix && Symbol.iterator in fix) {
+        return Object.assign(descriptor, { fix: mergeFixes(Array.from(fix), sourceCode) });
+    }
+    return Object.assign(descriptor, { fix });
+}
+
+/**
+ * Creates information about the report from a descriptor
+ * @param {MessageDescriptor} descriptor The message descriptor
+ * @param {string} ruleId The rule ID of the problem
+ * @param {(0|1|2)} severity The severity of the problem
+ * @returns {function(...args): {
+ *      ruleId: string,
+ *      severity: (0|1|2),
+ *      message: string,
+ *      line: number,
+ *      column: number,
+ *      endLine: (number|undefined),
+ *      endColumn: (number|undefined),
+ *      nodeType: (string|null),
+ *      source: string,
+ *      fix: ({text: string, range: [number, number]}|null)
+ * }} Information about the report
+ */
+function createProblemFromDescriptor(descriptor, ruleId, severity) {
+    const problem = {
+        ruleId,
+        severity,
+        message: descriptor.message,
+        line: descriptor.loc.start.line,
+        column: descriptor.loc.start.column + 1,
+        nodeType: descriptor.node && descriptor.node.type || null
+    };
+
+    if (descriptor.loc.end) {
+        problem.endLine = descriptor.loc.end.line;
+        problem.endColumn = descriptor.loc.end.column + 1;
+    }
+
+    if (descriptor.fix) {
+        problem.fix = descriptor.fix;
+    }
+
+    return problem;
+}
+
+/**
+ * Returns a function that converts the arguments of a `context.report` call from a rule into a reported
+ * problem for the Node.js API.
+ * @param {{ruleId: string, severity: number, sourceCode: SourceCode}} metadata Metadata for the reported problem
+ * @param {SourceCode} sourceCode The `SourceCode` instance for the text being linted
+ * @returns {function(...args): {
+ *      ruleId: string,
+ *      severity: (0|1|2),
+ *      message: string,
+ *      line: number,
+ *      column: number,
+ *      endLine: (number|undefined),
+ *      endColumn: (number|undefined),
+ *      nodeType: (string|null),
+ *      source: string,
+ *      fix: ({text: string, range: [number, number]}|null)
+ * }}
+ * Information about the report
+ */
+
+module.exports = function createReportTranslator(metadata) {
+
+    /*
+     * `createReportTranslator` gets called once per enabled rule per file. It needs to be very performant.
+     * The report translator itself (i.e. the function that `createReportTranslator` returns) gets
+     * called every time a rule reports a problem, which happens much less frequently (usually, the vast
+     * majority of rules don't report any problems for a given file).
+     */
+    return function() {
+        const descriptor = normalizeMultiArgReportCall.apply(null, arguments);
+
+        assertValidNodeInfo(descriptor);
+        normalizeReportLoc(descriptor);
+        normalizeMessagePlaceholders(descriptor);
+        normalizeFixes(descriptor, metadata.sourceCode);
+
+        const problem = createProblemFromDescriptor(descriptor, metadata.ruleId, metadata.severity);
+
+        problem.source = metadata.sourceCode.lines[problem.line - 1] || "";
+
+        return problem;
+    };
+};
diff --git a/lib/rule-context.js b/lib/rule-context.js
deleted file mode 100644
index 0a2dc188d..000000000
--- a/lib/rule-context.js
+++ /dev/null
@@ -1,241 +0,0 @@
-/**
- * @fileoverview RuleContext utility for rules
- * @author Nicholas C. Zakas
- */
-"use strict";
-
-//------------------------------------------------------------------------------
-// Requirements
-//------------------------------------------------------------------------------
-
-const assert = require("assert");
-const ruleFixer = require("./util/rule-fixer");
-
-//------------------------------------------------------------------------------
-// Constants
-//------------------------------------------------------------------------------
-
-const PASSTHROUGHS = [
-    "getAncestors",
-    "getDeclaredVariables",
-    "getFilename",
-    "getScope",
-    "getSourceCode",
-    "markVariableAsUsed",
-
-    // DEPRECATED
-    "getAllComments",
-    "getComments",
-    "getFirstToken",
-    "getFirstTokens",
-    "getJSDocComment",
-    "getLastToken",
-    "getLastTokens",
-    "getNodeByRangeIndex",
-    "getSource",
-    "getSourceLines",
-    "getTokenAfter",
-    "getTokenBefore",
-    "getTokenByRangeStart",
-    "getTokens",
-    "getTokensAfter",
-    "getTokensBefore",
-    "getTokensBetween"
-];
-
-//------------------------------------------------------------------------------
-// Typedefs
-//------------------------------------------------------------------------------
-
-/**
- * An error message description
- * @typedef {Object} MessageDescriptor
- * @property {string} nodeType The type of node.
- * @property {Location} loc The location of the problem.
- * @property {string} message The problem message.
- * @property {Object} [data] Optional data to use to fill in placeholders in the
- *      message.
- * @property {Function} fix The function to call that creates a fix command.
- */
-
-//------------------------------------------------------------------------------
-// Module Definition
-//------------------------------------------------------------------------------
-
-/**
- * Compares items in a fixes array by range.
- * @param {Fix} a The first message.
- * @param {Fix} b The second message.
- * @returns {int} -1 if a comes before b, 1 if a comes after b, 0 if equal.
- * @private
- */
-function compareFixesByRange(a, b) {
-    return a.range[0] - b.range[0] || a.range[1] - b.range[1];
-}
-
-/**
- * Merges the given fixes array into one.
- * @param {Fix[]} fixes The fixes to merge.
- * @param {SourceCode} sourceCode The source code object to get the text between fixes.
- * @returns {void}
- */
-function mergeFixes(fixes, sourceCode) {
-    if (fixes.length === 0) {
-        return null;
-    }
-    if (fixes.length === 1) {
-        return fixes[0];
-    }
-
-    fixes.sort(compareFixesByRange);
-
-    const originalText = sourceCode.text;
-    const start = fixes[0].range[0];
-    const end = fixes[fixes.length - 1].range[1];
-    let text = "";
-    let lastPos = Number.MIN_SAFE_INTEGER;
-
-    for (const fix of fixes) {
-        assert(fix.range[0] >= lastPos, "Fix objects must not be overlapped in a report.");
-
-        if (fix.range[0] >= 0) {
-            text += originalText.slice(Math.max(0, start, lastPos), fix.range[0]);
-        }
-        text += fix.text;
-        lastPos = fix.range[1];
-    }
-    text += originalText.slice(Math.max(0, start, lastPos), end);
-
-    return { range: [start, end], text };
-}
-
-/**
- * Gets one fix object from the given descriptor.
- * If the descriptor retrieves multiple fixes, this merges those to one.
- * @param {Object} descriptor The report descriptor.
- * @param {SourceCode} sourceCode The source code object to get text between fixes.
- * @returns {Fix} The got fix object.
- */
-function getFix(descriptor, sourceCode) {
-    if (typeof descriptor.fix !== "function") {
-        return null;
-    }
-
-    // @type {null | Fix | Fix[] | IterableIterator<Fix>}
-    const fix = descriptor.fix(ruleFixer);
-
-    // Merge to one.
-    if (fix && Symbol.iterator in fix) {
-        return mergeFixes(Array.from(fix), sourceCode);
-    }
-    return fix;
-}
-
-/**
- * Rule context class
- * Acts as an abstraction layer between rules and the main linter object.
- */
-class RuleContext {
-
-    /**
-     * @param {string} ruleId The ID of the rule using this object.
-     * @param {Linter} linter The linter object.
-     * @param {number} severity The configured severity level of the rule.
-     * @param {Array} options The configuration information to be added to the rule.
-     * @param {Object} settings The configuration settings passed from the config file.
-     * @param {Object} parserOptions The parserOptions settings passed from the config file.
-     * @param {Object} parserPath The parser setting passed from the config file.
-     * @param {Object} meta The metadata of the rule
-     * @param {Object} parserServices The parser services for the rule.
-     */
-    constructor(ruleId, linter, severity, options, settings, parserOptions, parserPath, meta, parserServices) {
-
-        // public.
-        this.id = ruleId;
-        this.options = options;
-        this.settings = settings;
-        this.parserOptions = parserOptions;
-        this.parserPath = parserPath;
-        this.meta = meta;
-
-        // create a separate copy and freeze it (it's not nice to freeze other people's objects)
-        this.parserServices = Object.freeze(Object.assign({}, parserServices));
-
-        // private.
-        this._linter = linter;
-        this._severity = severity;
-
-        Object.freeze(this);
-    }
-
-    /**
-     * Passthrough to Linter#report() that automatically assigns the rule ID and severity.
-     * @param {ASTNode|MessageDescriptor} nodeOrDescriptor The AST node related to the message or a message
-     *      descriptor.
-     * @param {Object=} location The location of the error.
-     * @param {string} message The message to display to the user.
-     * @param {Object} opts Optional template data which produces a formatted message
-     *     with symbols being replaced by this object's values.
-     * @returns {void}
-     */
-    report(nodeOrDescriptor, location, message, opts) {
-
-        // check to see if it's a new style call
-        if (arguments.length === 1) {
-            const descriptor = nodeOrDescriptor;
-            const fix = getFix(descriptor, this.getSourceCode());
-
-            if (descriptor.loc) {
-                this._linter.report(
-                    this.id,
-                    this._severity,
-                    descriptor.node,
-                    descriptor.loc,
-                    descriptor.message,
-                    descriptor.data,
-                    fix,
-                    this.meta
-                );
-            } else {
-                this._linter.report(
-                    this.id,
-                    this._severity,
-                    descriptor.node,
-
-                    /* loc not provided */
-                    descriptor.message,
-                    descriptor.data,
-                    fix,
-                    this.meta
-                );
-            }
-
-        } else {
-
-            // old style call
-            this._linter.report(
-                this.id,
-                this._severity,
-                nodeOrDescriptor,
-                location,
-                message,
-                opts,
-                this.meta
-            );
-        }
-    }
-}
-
-// Copy over passthrough methods.
-PASSTHROUGHS.forEach(name => {
-    Object.defineProperty(RuleContext.prototype, name, {
-        value() {
-            return this._linter[name].apply(this._linter, arguments);
-        },
-        configurable: true,
-        writable: true,
-        enumerable: false
-    });
-});
-
-module.exports = RuleContext;
diff --git a/lib/rules/generator-star-spacing.js b/lib/rules/generator-star-spacing.js
index 9836e4ead..5c6120224 100644
--- a/lib/rules/generator-star-spacing.js
+++ b/lib/rules/generator-star-spacing.js
@@ -9,6 +9,22 @@
 // Rule Definition
 //------------------------------------------------------------------------------
 
+const OVERRIDE_SCHEMA = {
+    oneOf: [
+        {
+            enum: ["before", "after", "both", "neither"]
+        },
+        {
+            type: "object",
+            properties: {
+                before: { type: "boolean" },
+                after: { type: "boolean" }
+            },
+            additionalProperties: false
+        }
+    ]
+};
+
 module.exports = {
     meta: {
         docs: {
@@ -29,7 +45,10 @@ module.exports = {
                         type: "object",
                         properties: {
                             before: { type: "boolean" },
-                            after: { type: "boolean" }
+                            after: { type: "boolean" },
+                            named: OVERRIDE_SCHEMA,
+                            anonymous: OVERRIDE_SCHEMA,
+                            method: OVERRIDE_SCHEMA
                         },
                         additionalProperties: false
                     }
@@ -40,16 +59,39 @@ module.exports = {
 
     create(context) {
 
-        const mode = (function(option) {
-            if (!option || typeof option === "string") {
-                return {
-                    before: { before: true, after: false },
-                    after: { before: false, after: true },
-                    both: { before: true, after: true },
-                    neither: { before: false, after: false }
-                }[option || "before"];
+        const optionDefinitions = {
+            before: { before: true, after: false },
+            after: { before: false, after: true },
+            both: { before: true, after: true },
+            neither: { before: false, after: false }
+        };
+
+        /**
+         * Returns resolved option definitions based on an option and defaults
+         * 
+         * @param {any} option - The option object or string value
+         * @param {Object} defaults - The defaults to use if options are not present
+         * @returns {Object} the resolved object definition
+         */
+        function optionToDefinition(option, defaults) {
+            if (!option) {
+                return defaults;
             }
-            return option;
+
+            return typeof option === "string"
+                ? optionDefinitions[option]
+                : Object.assign({}, defaults, option);
+        }
+
+        const modes = (function(option) {
+            option = option || {};
+            const defaults = optionToDefinition(option, optionDefinitions.before);
+
+            return {
+                named: optionToDefinition(option.named, defaults),
+                anonymous: optionToDefinition(option.anonymous, defaults),
+                method: optionToDefinition(option.method, defaults)
+            };
         }(context.options[0]));
 
         const sourceCode = context.getSourceCode();
@@ -79,6 +121,8 @@ module.exports = {
 
         /**
          * Checks the spacing between two tokens before or after the star token.
+         * 
+         * @param {string} kind Either "named", "anonymous", or "method"
          * @param {string} side Either "before" or "after".
          * @param {Token} leftToken `function` keyword token if side is "before", or
          *     star token if side is "after".
@@ -86,10 +130,10 @@ module.exports = {
          *     token if side is "after".
          * @returns {void}
          */
-        function checkSpacing(side, leftToken, rightToken) {
-            if (!!(rightToken.range[0] - leftToken.range[1]) !== mode[side]) {
+        function checkSpacing(kind, side, leftToken, rightToken) {
+            if (!!(rightToken.range[0] - leftToken.range[1]) !== modes[kind][side]) {
                 const after = leftToken.value === "*";
-                const spaceRequired = mode[side];
+                const spaceRequired = modes[kind][side];
                 const node = after ? leftToken : rightToken;
                 const type = spaceRequired ? "Missing" : "Unexpected";
                 const message = "{{type}} space {{side}} *.";
@@ -117,6 +161,7 @@ module.exports = {
 
         /**
          * Enforces the spacing around the star if node is a generator function.
+         * 
          * @param {ASTNode} node A function expression or declaration node.
          * @returns {void}
          */
@@ -126,17 +171,23 @@ module.exports = {
             }
 
             const starToken = getStarToken(node);
-
-            // Only check before when preceded by `function`|`static` keyword
             const prevToken = sourceCode.getTokenBefore(starToken);
+            const nextToken = sourceCode.getTokenAfter(starToken);
 
-            if (prevToken.value === "function" || prevToken.value === "static") {
-                checkSpacing("before", prevToken, starToken);
+            let kind = "named";
+
+            if (node.parent.type === "MethodDefinition" || (node.parent.type === "Property" && node.parent.method)) {
+                kind = "method";
+            } else if (!node.id) {
+                kind = "anonymous";
             }
 
-            const nextToken = sourceCode.getTokenAfter(starToken);
+            // Only check before when preceded by `function`|`static` keyword
+            if (!(kind === "method" && starToken === sourceCode.getFirstToken(node.parent))) {
+                checkSpacing(kind, "before", prevToken, starToken);
+            }
 
-            checkSpacing("after", starToken, nextToken);
+            checkSpacing(kind, "after", starToken, nextToken);
         }
 
         return {
diff --git a/lib/rules/no-unused-vars.js b/lib/rules/no-unused-vars.js
index 3ed278d54..05940d593 100644
--- a/lib/rules/no-unused-vars.js
+++ b/lib/rules/no-unused-vars.js
@@ -64,8 +64,6 @@ module.exports = {
     create(context) {
         const sourceCode = context.getSourceCode();
 
-        const DEFINED_MESSAGE = "'{{name}}' is defined but never used.";
-        const ASSIGNED_MESSAGE = "'{{name}}' is assigned a value but never used.";
         const REST_PROPERTY_TYPE = /^(?:Experimental)?RestProperty$/;
 
         const config = {
@@ -100,6 +98,49 @@ module.exports = {
             }
         }
 
+        /**
+         * Generate the warning message about the variable being
+         * defined and unused, including the ignore pattern if configured.
+         * @param {Variable} unusedVar - eslint-scope variable object.
+         * @returns {string} The warning message to be used with this unused variable.
+         */
+        function getDefinedMessage(unusedVar) {
+            let type;
+            let pattern;
+
+            if (config.varsIgnorePattern) {
+                type = "vars";
+                pattern = config.varsIgnorePattern.toString();
+            }
+
+            if (unusedVar.defs && unusedVar.defs[0] && unusedVar.defs[0].type) {
+                const defType = unusedVar.defs[0].type;
+
+                if (defType === "CatchClause" && config.caughtErrorsIgnorePattern) {
+                    type = "args";
+                    pattern = config.caughtErrorsIgnorePattern.toString();
+                } else if (defType === "Parameter" && config.argsIgnorePattern) {
+                    type = "args";
+                    pattern = config.argsIgnorePattern.toString();
+                }
+            }
+
+            const additional = type ? ` Allowed unused ${type} must match ${pattern}.` : "";
+
+            return `'{{name}}' is defined but never used.${additional}`;
+        }
+
+        /**
+         * Generate the warning message about the variable being
+         * assigned and unused, including the ignore pattern if configured.
+         * @returns {string} The warning message to be used with this unused variable.
+         */
+        function getAssignedMessage() {
+            const additional = config.varsIgnorePattern ? ` Allowed unused vars must match ${config.varsIgnorePattern.toString()}.` : "";
+
+            return `'{{name}}' is assigned a value but never used.${additional}`;
+        }
+
         //--------------------------------------------------------------------------
         // Helpers
         //--------------------------------------------------------------------------
@@ -586,13 +627,15 @@ module.exports = {
                         context.report({
                             node: programNode,
                             loc: getLocation(unusedVar),
-                            message: DEFINED_MESSAGE,
+                            message: getDefinedMessage(unusedVar),
                             data: unusedVar
                         });
                     } else if (unusedVar.defs.length > 0) {
                         context.report({
                             node: unusedVar.identifiers[0],
-                            message: unusedVar.references.some(ref => ref.isWrite()) ? ASSIGNED_MESSAGE : DEFINED_MESSAGE,
+                            message: unusedVar.references.some(ref => ref.isWrite())
+                                ? getAssignedMessage()
+                                : getDefinedMessage(unusedVar),
                             data: unusedVar
                         });
                     }
diff --git a/lib/rules/padded-blocks.js b/lib/rules/padded-blocks.js
index 64c2c5c72..ad65882ac 100644
--- a/lib/rules/padded-blocks.js
+++ b/lib/rules/padded-blocks.js
@@ -111,7 +111,7 @@ module.exports = {
          * @returns {boolean} Whether or not the token is followed by a blank line.
          */
         function getFirstBlockToken(token) {
-            let prev = token,
+            let prev,
                 first = token;
 
             do {
@@ -129,7 +129,7 @@ module.exports = {
          */
         function getLastBlockToken(token) {
             let last = token,
-                next = token;
+                next;
 
             do {
                 next = last;
diff --git a/lib/rules/prefer-arrow-callback.js b/lib/rules/prefer-arrow-callback.js
index 43a8bb77d..de7c33940 100644
--- a/lib/rules/prefer-arrow-callback.js
+++ b/lib/rules/prefer-arrow-callback.js
@@ -88,7 +88,6 @@ function getCallbackInfo(node) {
                         parent.parent.arguments.length === 1 &&
                         parent.parent.arguments[0].type === "ThisExpression"
                     );
-                    node = parent;
                     parent = parent.parent;
                 } else {
                     return retv;
diff --git a/lib/util/npm-util.js b/lib/util/npm-util.js
index 4f488c012..6c431e039 100644
--- a/lib/util/npm-util.js
+++ b/lib/util/npm-util.js
@@ -53,22 +53,39 @@ function installSyncSaveDev(packages) {
     if (!Array.isArray(packages)) {
         packages = [packages];
     }
-    spawn.sync("npm", ["i", "--save-dev"].concat(packages), { stdio: "inherit" });
+    const npmProcess = spawn.sync("npm", ["i", "--save-dev"].concat(packages),
+        { stdio: "inherit" });
+    const error = npmProcess.error;
+
+    if (error && error.code === "ENOENT") {
+        const pluralS = packages.length > 1 ? "s" : "";
+
+        log.error(`Could not execute npm. Please install the following package${pluralS} with your package manager of choice: ${packages.join(", ")}`);
+    }
 }
 
 /**
  * Fetch `peerDependencies` of the given package by `npm show` command.
  * @param {string} packageName The package name to fetch peerDependencies.
- * @returns {Object} Gotten peerDependencies.
+ * @returns {Object} Gotten peerDependencies. Returns null if npm was not found.
  */
 function fetchPeerDependencies(packageName) {
-    const fetchedText = spawn.sync(
+    const npmProcess = spawn.sync(
         "npm",
         ["show", "--json", packageName, "peerDependencies"],
         { encoding: "utf8" }
-    ).stdout.trim();
+    );
+
+    const error = npmProcess.error;
+
+    if (error && error.code === "ENOENT") {
+        return null;
+    }
+    const fetchedText = npmProcess.stdout.trim();
 
     return JSON.parse(fetchedText || "{}");
+
+
 }
 
 /**


Test patch:
diff --git a/tests/lib/ast-utils.js b/tests/lib/ast-utils.js
index 96127ca2d..f4df03844 100644
--- a/tests/lib/ast-utils.js
+++ b/tests/lib/ast-utils.js
@@ -10,7 +10,6 @@
 //------------------------------------------------------------------------------
 
 const assert = require("chai").assert,
-    sinon = require("sinon"),
     espree = require("espree"),
     astUtils = require("../../lib/ast-utils"),
     Linter = require("../../lib/linter"),
@@ -30,150 +29,86 @@ const ESPREE_CONFIG = {
 const linter = new Linter();
 
 describe("ast-utils", () => {
-    const filename = "filename.js";
-    let sandbox;
+    let callCounts;
 
     beforeEach(() => {
-        sandbox = sinon.sandbox.create();
+        callCounts = new Map();
     });
 
+    /**
+     * Asserts that a given function is called at least once during a test
+     * @param {Function} func The function that must be called at least once
+     * @returns {Function} A wrapper around the same function
+     */
+    function mustCall(func) {
+        callCounts.set(func, 0);
+        return function Wrapper() {
+            callCounts.set(func, callCounts.get(func) + 1);
+
+            return func.apply(this, arguments);
+        };
+    }
+
     afterEach(() => {
+        callCounts.forEach((callCount, func) => {
+            assert(
+                callCount > 0,
+                `Expected ${func.toString()} to be called at least once but it was not called`
+            );
+        });
+
         linter.reset();
-        sandbox.verifyAndRestore();
     });
 
     describe("isTokenOnSameLine", () => {
-        it("should return false if its not on sameline", () => {
-
-            /**
-             * Check the node for tokens
-             * @param {ASTNode} node node to examine
-             * @returns {void}
-             */
-            function checker(node) {
-                assert.isFalse(astUtils.isTokenOnSameLine(linter.getTokenBefore(node), node));
-            }
+        it("should return false if the tokens are not on the same line", () => {
+            linter.defineRule("checker", mustCall(() => ({
+                BlockStatement: mustCall(node => {
+                    assert.isFalse(astUtils.isTokenOnSameLine(linter.getTokenBefore(node), node));
+                })
+            })));
 
-            linter.reset();
-            linter.on("BlockStatement", checker);
-            linter.verify("if(a)\n{}", {}, filename, true);
+            linter.verify("if(a)\n{}", { rules: { checker: "error" } });
         });
 
-        it("should return true if its on sameline", () => {
+        it("should return true if the tokens are on the same line", () => {
 
-            /**
-             * Check the node for tokens
-             * @param {ASTNode} node node to examine
-             * @returns {void}
-             */
-            function checker(node) {
-                assert.isTrue(astUtils.isTokenOnSameLine(linter.getTokenBefore(node), node));
-            }
+            linter.defineRule("checker", mustCall(() => ({
+                BlockStatement: mustCall(node => {
+                    assert.isTrue(astUtils.isTokenOnSameLine(linter.getTokenBefore(node), node));
+                })
+            })));
 
-            linter.reset();
-            linter.on("BlockStatement", checker);
-            linter.verify("if(a){}", {}, filename, true);
+            linter.verify("if(a){}", { rules: { checker: "error" } });
         });
     });
 
     describe("isNullOrUndefined", () => {
-        it("should return true if its null", () => {
-
-            /**
-             * Check the node for tokens
-             * @param {ASTNode} node node to examine
-             * @returns {void}
-             */
-            function checker(node) {
-                assert.isTrue(astUtils.isNullOrUndefined(node.arguments[0]));
-            }
-
-            linter.reset();
-            linter.on("CallExpression", checker);
-            linter.verify("foo.apply(null, a, b);", {}, filename, true);
+        it("should return true if the argument is null", () => {
+            assert.isTrue(astUtils.isNullOrUndefined(espree.parse("null").body[0].expression));
         });
 
-        it("should return true if its undefined", () => {
-
-            /**
-             * Check the node for tokens
-             * @param {ASTNode} node node to examine
-             * @returns {void}
-             */
-            function checker(node) {
-                assert.isTrue(astUtils.isNullOrUndefined(node.arguments[0]));
-            }
-
-            linter.reset();
-            linter.on("CallExpression", checker);
-            linter.verify("foo.apply(undefined, a, b);", {}, filename, true);
+        it("should return true if the argument is undefined", () => {
+            assert.isTrue(astUtils.isNullOrUndefined(espree.parse("undefined").body[0].expression));
         });
 
-        it("should return false if its a number", () => {
-
-            /**
-             * Check the node for tokens
-             * @param {ASTNode} node node to examine
-             * @returns {void}
-             */
-            function checker(node) {
-                assert.isFalse(astUtils.isNullOrUndefined(node.arguments[0]));
-            }
-
-            linter.reset();
-            linter.on("CallExpression", checker);
-            linter.verify("foo.apply(1, a, b);", {}, filename, true);
+        it("should return false if the argument is a number", () => {
+            assert.isFalse(astUtils.isNullOrUndefined(espree.parse("1").body[0].expression));
         });
 
-        it("should return false if its a string", () => {
-
-            /**
-             * Check the node for tokens
-             * @param {ASTNode} node node to examine
-             * @returns {void}
-             */
-            function checker(node) {
-                assert.isFalse(astUtils.isNullOrUndefined(node.arguments[0]));
-            }
-
-            linter.reset();
-            linter.on("CallExpression", checker);
-            linter.verify("foo.apply(`test`, a, b);", {}, filename, true);
+        it("should return false if the argument is a string", () => {
+            assert.isFalse(astUtils.isNullOrUndefined(espree.parse("'test'").body[0].expression));
         });
 
-        it("should return false if its a boolean", () => {
-
-            /**
-             * Check the node for tokens
-             * @param {ASTNode} node node to examine
-             * @returns {void}
-             */
-            function checker(node) {
-                assert.isFalse(astUtils.isNullOrUndefined(node.arguments[0]));
-            }
-
-            linter.reset();
-            linter.on("CallExpression", checker);
-            linter.verify("foo.apply(false, a, b);", {}, filename, true);
+        it("should return false if the argument is a boolean", () => {
+            assert.isFalse(astUtils.isNullOrUndefined(espree.parse("true").body[0].expression));
         });
 
-        it("should return false if its an object", () => {
-
-            /**
-             * Check the node for tokens
-             * @param {ASTNode} node node to examine
-             * @returns {void}
-             */
-            function checker(node) {
-                assert.isFalse(astUtils.isNullOrUndefined(node.arguments[0]));
-            }
-
-            linter.reset();
-            linter.on("CallExpression", checker);
-            linter.verify("foo.apply({}, a, b);", {}, filename, true);
+        it("should return false if the argument is an object", () => {
+            assert.isFalse(astUtils.isNullOrUndefined(espree.parse("({})").body[0].expression));
         });
 
-        it("should return false if it's a unicode regex", () => {
+        it("should return false if the argument is a unicode regex", () => {
             assert.isFalse(astUtils.isNullOrUndefined(espree.parse("/abc/u", { ecmaVersion: 6 }).body[0].expression));
         });
     });
@@ -182,96 +117,66 @@ describe("ast-utils", () => {
 
         // catch
         it("should return true if reference is assigned for catch", () => {
+            linter.defineRule("checker", mustCall(() => ({
+                CatchClause: mustCall(node => {
+                    const variables = linter.getDeclaredVariables(node);
 
-            /**
-             * Check the node for tokens
-             * @param {ASTNode} node node to examine
-             * @returns {void}
-             */
-            function checker(node) {
-                const variables = linter.getDeclaredVariables(node);
-
-                assert.lengthOf(astUtils.getModifyingReferences(variables[0].references), 1);
-            }
+                    assert.lengthOf(astUtils.getModifyingReferences(variables[0].references), 1);
+                })
+            })));
 
-            linter.reset();
-            linter.on("CatchClause", checker);
-            linter.verify("try { } catch (e) { e = 10; }", { rules: {} }, filename, true);
+            linter.verify("try { } catch (e) { e = 10; }", { rules: { checker: "error" } });
         });
 
         // const
         it("should return true if reference is assigned for const", () => {
+            linter.defineRule("checker", mustCall(() => ({
+                VariableDeclaration: mustCall(node => {
+                    const variables = linter.getDeclaredVariables(node);
 
-            /**
-             * Check the node for tokens
-             * @param {ASTNode} node node to examine
-             * @returns {void}
-             */
-            function checker(node) {
-                const variables = linter.getDeclaredVariables(node);
+                    assert.lengthOf(astUtils.getModifyingReferences(variables[0].references), 1);
+                })
+            })));
 
-                assert.lengthOf(astUtils.getModifyingReferences(variables[0].references), 1);
-            }
-
-            linter.reset();
-            linter.on("VariableDeclaration", checker);
-            linter.verify("const a = 1; a = 2;", {}, filename, true);
+            linter.verify("const a = 1; a = 2;", { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6 } });
         });
 
         it("should return false if reference is not assigned for const", () => {
+            linter.defineRule("checker", mustCall(() => ({
+                VariableDeclaration: mustCall(node => {
+                    const variables = linter.getDeclaredVariables(node);
 
-            /**
-             * Check the node for tokens
-             * @param {ASTNode} node node to examine
-             * @returns {void}
-             */
-            function checker(node) {
-                const variables = linter.getDeclaredVariables(node);
-
-                assert.lengthOf(astUtils.getModifyingReferences(variables[0].references), 0);
-            }
+                    assert.lengthOf(astUtils.getModifyingReferences(variables[0].references), 0);
+                })
+            })));
 
-            linter.reset();
-            linter.on("VariableDeclaration", checker);
-            linter.verify("const a = 1; c = 2;", {}, filename, true);
+            linter.verify("const a = 1; c = 2;", { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6 } });
         });
 
         // class
         it("should return true if reference is assigned for class", () => {
+            linter.defineRule("checker", mustCall(() => ({
+                ClassDeclaration: mustCall(node => {
+                    const variables = linter.getDeclaredVariables(node);
 
-            /**
-             * Check the node for tokens
-             * @param {ASTNode} node node to examine
-             * @returns {void}
-             */
-            function checker(node) {
-                const variables = linter.getDeclaredVariables(node);
-
-                assert.lengthOf(astUtils.getModifyingReferences(variables[0].references), 1);
-                assert.lengthOf(astUtils.getModifyingReferences(variables[1].references), 0);
-            }
+                    assert.lengthOf(astUtils.getModifyingReferences(variables[0].references), 1);
+                    assert.lengthOf(astUtils.getModifyingReferences(variables[1].references), 0);
+                })
+            })));
 
-            linter.reset();
-            linter.on("ClassDeclaration", checker);
-            linter.verify("class A { }\n A = 1;", {}, filename, true);
+            linter.verify("class A { }\n A = 1;", { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6 } });
         });
 
         it("should return false if reference is not assigned for class", () => {
+            linter.defineRule("checker", mustCall(() => ({
+                ClassDeclaration: mustCall(node => {
+                    const variables = linter.getDeclaredVariables(node);
 
-            /**
-             * Check the node for tokens
-             * @param {ASTNode} node node to examine
-             * @returns {void}
-             */
-            function checker(node) {
-                const variables = linter.getDeclaredVariables(node);
+                    assert.lengthOf(astUtils.getModifyingReferences(variables[0].references), 0);
+                })
+            })));
 
-                assert.lengthOf(astUtils.getModifyingReferences(variables[0].references), 0);
-            }
-
-            linter.reset();
-            linter.on("ClassDeclaration", checker);
-            linter.verify("class A { } foo(A);", {}, filename, true);
+            linter.verify("class A { } foo(A);", { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6 } });
         });
     });
 
@@ -463,9 +368,12 @@ describe("ast-utils", () => {
         function assertNodeTypeInLoop(code, nodeType, expectedInLoop) {
             const results = [];
 
-            linter.reset();
-            linter.on(nodeType, node => results.push(astUtils.isInLoop(node)));
-            linter.verify(code, { parserOptions: { ecmaVersion: 6 } }, filename, true);
+            linter.defineRule("checker", mustCall(() => ({
+                [nodeType]: mustCall(node => {
+                    results.push(astUtils.isInLoop(node));
+                })
+            })));
+            linter.verify(code, { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6 } });
 
             assert.lengthOf(results, 1);
             assert.equal(results[0], expectedInLoop);
@@ -789,27 +697,16 @@ describe("ast-utils", () => {
 
         Object.keys(expectedResults).forEach(key => {
             it(`should return "${expectedResults[key]}" for "${key}".`, () => {
-                let called = false;
-
-                /**
-                 * Verify.
-                 * @param {ASTNode} node - The function node to verify.
-                 * @returns {void}
-                 */
-                function verify(node) {
-                    assert.strictEqual(
-                        astUtils.getFunctionNameWithKind(node),
-                        expectedResults[key]
-                    );
-                    called = true;
-                }
-
-                linter.on("FunctionDeclaration", verify);
-                linter.on("FunctionExpression", verify);
-                linter.on("ArrowFunctionExpression", verify);
-                linter.verify(key, { parserOptions: { ecmaVersion: 8 } }, "test.js", true);
-
-                assert(called);
+                linter.defineRule("checker", mustCall(() => ({
+                    ":function": mustCall(node => {
+                        assert.strictEqual(
+                            astUtils.getFunctionNameWithKind(node),
+                            expectedResults[key]
+                        );
+                    })
+                })));
+
+                linter.verify(key, { rules: { checker: "error" }, parserOptions: { ecmaVersion: 8 } });
             });
         });
     });
@@ -873,27 +770,16 @@ describe("ast-utils", () => {
             };
 
             it(`should return "${JSON.stringify(expectedLoc)}" for "${key}".`, () => {
-                let called = false;
-
-                /**
-                 * Verify.
-                 * @param {ASTNode} node - The function node to verify.
-                 * @returns {void}
-                 */
-                function verify(node) {
-                    assert.deepEqual(
-                        astUtils.getFunctionHeadLoc(node, linter.getSourceCode()),
-                        expectedLoc
-                    );
-                    called = true;
-                }
-
-                linter.on("FunctionDeclaration", verify);
-                linter.on("FunctionExpression", verify);
-                linter.on("ArrowFunctionExpression", verify);
-                linter.verify(key, { parserOptions: { ecmaVersion: 8 } }, "test.js", true);
-
-                assert(called);
+                linter.defineRule("checker", mustCall(() => ({
+                    ":function": mustCall(node => {
+                        assert.deepEqual(
+                            astUtils.getFunctionHeadLoc(node, linter.getSourceCode()),
+                            expectedLoc
+                        );
+                    })
+                })));
+
+                linter.verify(key, { rules: { checker: "error" }, parserOptions: { ecmaVersion: 8 } }, "test.js", true);
             });
         });
     });
diff --git a/tests/lib/code-path-analysis/code-path-analyzer.js b/tests/lib/code-path-analysis/code-path-analyzer.js
index 3c5d71db2..ee603ef47 100644
--- a/tests/lib/code-path-analysis/code-path-analyzer.js
+++ b/tests/lib/code-path-analysis/code-path-analyzer.js
@@ -14,7 +14,7 @@ const assert = require("assert"),
     fs = require("fs"),
     path = require("path"),
     Linter = require("../../../lib/linter"),
-    EventGeneratorTester = require("../../../lib/testers/event-generator-tester"),
+    EventGeneratorTester = require("../../../tools/internal-testers/event-generator-tester"),
     debug = require("../../../lib/code-path-analysis/debug-helpers"),
     CodePath = require("../../../lib/code-path-analysis/code-path"),
     CodePathAnalyzer = require("../../../lib/code-path-analysis/code-path-analyzer"),
diff --git a/tests/lib/linter.js b/tests/lib/linter.js
index 248ec72d7..c175d3310 100644
--- a/tests/lib/linter.js
+++ b/tests/lib/linter.js
@@ -738,441 +738,6 @@ describe("Linter", () => {
         });
     });
 
-    describe("report()", () => {
-
-        let config;
-
-        beforeEach(() => {
-            config = { rules: { "test-rule": "error" } };
-        });
-
-        it("should correctly parse a message when being passed all options", () => {
-            linter.defineRule("test-rule", () => ({
-                Program(node) {
-                    linter.report("test-rule", 2, node, node.loc.end, "hello {{dynamic}}", { dynamic: node.type });
-                }
-            }));
-
-            const messages = linter.verify("0", config, "", true);
-
-            assert.deepEqual(messages[0], {
-                severity: 2,
-                ruleId: "test-rule",
-                message: "hello Program",
-                nodeType: "Program",
-                line: 1,
-                column: 2,
-                source: "0"
-            });
-        });
-
-        it("should use the report the provided location when given", () => {
-            linter.defineRule("test-rule", () => ({
-                Program(node) {
-                    linter.report("test-rule", 2, node, { line: 42, column: 13 }, "hello world");
-                }
-            }));
-
-            const messages = linter.verify("0", config, "", true);
-
-            assert.deepEqual(messages[0], {
-                severity: 2,
-                ruleId: "test-rule",
-                message: "hello world",
-                nodeType: "Program",
-                line: 42,
-                column: 14,
-                source: ""
-            });
-        });
-
-        it("should not throw an error if node is provided and location is not", () => {
-            linter.defineRule("test-rule", () => ({
-                Program(node) {
-                    linter.report("test-rule", 2, node, "hello world");
-                }
-            }));
-
-            assert.doesNotThrow(() => {
-                linter.verify("0", config, "", true);
-            });
-        });
-
-        it("should not throw an error if location is provided and node is not", () => {
-            linter.defineRule("test-rule", () => ({
-                Program() {
-                    linter.report("test-rule", 2, null, { line: 1, column: 1 }, "hello world");
-                }
-            }));
-
-            assert.doesNotThrow(() => {
-                linter.verify("0", config, "", true);
-            });
-        });
-
-        it("should throw an error if neither node nor location is provided", () => {
-            linter.defineRule("test-rule", () => ({
-                Program() {
-                    linter.report("test-rule", 2, null, "hello world");
-                }
-            }));
-
-            assert.throws(() => {
-                linter.verify("0", config, "", true);
-            }, /Node must be provided when reporting error if location is not provided$/);
-        });
-
-        it("should throw an error if node is not an object", () => {
-            linter.defineRule("test-rule", () => ({
-                Program() {
-                    linter.report("test-rule", 2, "not a node", "hello world");
-                }
-            }));
-
-            assert.throws(() => {
-                linter.verify("0", config, "", true);
-            }, /Node must be an object$/);
-        });
-
-        it("should throw an error if fix is passed but meta has no `fixable` property", () => {
-            const meta = {
-                docs: {},
-                schema: []
-            };
-
-            linter.defineRule("test-rule", () => ({
-                Program(node) {
-                    linter.report("test-rule", 2, node, "hello world", [], { range: [1, 1], text: "" }, meta);
-                }
-            }));
-
-            assert.throws(() => {
-                linter.verify("0", config, "", true);
-            }, /Fixable rules should export a `meta\.fixable` property.$/);
-        });
-
-        it("should not throw an error if fix is passed and no metadata is passed", () => {
-            linter.defineRule("test-rule", () => ({
-                Program(node) {
-                    linter.report("test-rule", 2, node, "hello world", [], { range: [1, 1], text: "" });
-                }
-            }));
-
-            assert.doesNotThrow(() => {
-                linter.verify("0", config, "", true);
-            });
-        });
-
-        it("should correctly parse a message with object keys as numbers", () => {
-            linter.defineRule("test-rule", () => ({
-                Program(node) {
-                    linter.report("test-rule", 2, node, "my message {{name}}{{0}}", { 0: "!", name: "testing" });
-                }
-            }));
-
-            const messages = linter.verify("0", config, "", true);
-
-            assert.deepEqual(messages[0], {
-                severity: 2,
-                ruleId: "test-rule",
-                message: "my message testing!",
-                nodeType: "Program",
-                line: 1,
-                column: 1,
-                endLine: 1,
-                endColumn: 2,
-                source: "0"
-            });
-        });
-
-        it("should correctly parse a message with array", () => {
-            linter.defineRule("test-rule", () => ({
-                Program(node) {
-                    linter.report("test-rule", 2, node, "my message {{1}}{{0}}", ["!", "testing"]);
-                }
-            }));
-
-            const messages = linter.verify("0", config, "", true);
-
-            assert.deepEqual(messages[0], {
-                severity: 2,
-                ruleId: "test-rule",
-                message: "my message testing!",
-                nodeType: "Program",
-                line: 1,
-                column: 1,
-                endLine: 1,
-                endColumn: 2,
-                source: "0"
-            });
-        });
-
-        it("should include a fix passed as the last argument when location is not passed", () => {
-            linter.defineRule("test-rule", () => ({
-                Program(node) {
-                    linter.report("test-rule", 2, node, "my message {{1}}{{0}}", ["!", "testing"], { range: [1, 1], text: "" });
-                }
-            }));
-
-            const messages = linter.verify("0", config, "", true);
-
-            assert.deepEqual(messages[0], {
-                severity: 2,
-                ruleId: "test-rule",
-                message: "my message testing!",
-                nodeType: "Program",
-                line: 1,
-                column: 1,
-                endLine: 1,
-                endColumn: 2,
-                source: "0",
-                fix: { range: [1, 1], text: "" }
-            });
-        });
-
-        it("should allow template parameter with inner whitespace", () => {
-            linter.defineRule("test-rule", context => ({
-                Literal(node) {
-                    context.report(node, "message {{parameter name}}", {
-                        "parameter name": "yay!"
-                    });
-                }
-            }));
-
-            config.rules["test-rule"] = 1;
-
-            const messages = linter.verify("0", config);
-
-            assert.equal(messages[0].message, "message yay!");
-        });
-
-        it("should not crash if no template parameters are passed", () => {
-            linter.defineRule("test-rule", context => ({
-                Literal(node) {
-                    context.report(node, "message {{code}}");
-                }
-            }));
-
-            config.rules["test-rule"] = 1;
-
-            const messages = linter.verify("0", config);
-
-            assert.equal(messages[0].message, "message {{code}}");
-        });
-
-        it("should allow template parameter with non-identifier characters", () => {
-            linter.defineRule("test-rule", context => ({
-                Literal(node) {
-                    context.report(node, "message {{parameter-name}}", {
-                        "parameter-name": "yay!"
-                    });
-                }
-            }));
-
-            config.rules["test-rule"] = 1;
-
-            const messages = linter.verify("0", config);
-
-            assert.equal(messages[0].message, "message yay!");
-        });
-
-        it("should allow template parameter wrapped in braces", () => {
-            linter.defineRule("test-rule", context => ({
-                Literal(node) {
-                    context.report(node, "message {{{param}}}", {
-                        param: "yay!"
-                    });
-                }
-            }));
-
-            config.rules["test-rule"] = 1;
-
-            const messages = linter.verify("0", config);
-
-            assert.equal(messages[0].message, "message {yay!}");
-        });
-
-        it("should ignore template parameter with no specified value", () => {
-            linter.defineRule("test-rule", context => ({
-                Literal(node) {
-                    context.report(node, "message {{parameter}}", {});
-                }
-            }));
-
-            config.rules["test-rule"] = 1;
-
-            const messages = linter.verify("0", config);
-
-            assert.equal(messages[0].message, "message {{parameter}}");
-        });
-
-        it("should ignore template parameter with no specified value with warn severity", () => {
-            linter.defineRule("test-rule", context => ({
-                Literal(node) {
-                    context.report(node, "message {{parameter}}", {});
-                }
-            }));
-
-            config.rules["test-rule"] = "warn";
-
-            const messages = linter.verify("0", config);
-
-            assert.equal(messages[0].severity, 1);
-            assert.equal(messages[0].message, "message {{parameter}}");
-        });
-
-        it("should handle leading whitespace in template parameter", () => {
-            linter.defineRule("test-rule", context => ({
-                Literal(node) {
-                    context.report(node, "message {{ parameter}}", {
-                        parameter: "yay!"
-                    });
-                }
-            }));
-
-            config.rules["test-rule"] = 1;
-
-            const messages = linter.verify("0", config);
-
-            assert.equal(messages[0].message, "message yay!");
-        });
-
-        it("should handle trailing whitespace in template parameter", () => {
-            linter.defineRule("test-rule", context => ({
-                Literal(node) {
-                    context.report(node, "message {{parameter }}", {
-                        parameter: "yay!"
-                    });
-                }
-            }));
-
-            config.rules["test-rule"] = 1;
-
-            const messages = linter.verify("0", config);
-
-            assert.equal(messages[0].message, "message yay!");
-        });
-
-        it("should still allow inner whitespace as well as leading/trailing", () => {
-            linter.defineRule("test-rule", context => ({
-                Literal(node) {
-                    context.report(node, "message {{ parameter name }}", {
-                        "parameter name": "yay!"
-                    });
-                }
-            }));
-
-            config.rules["test-rule"] = 1;
-
-            const messages = linter.verify("0", config);
-
-            assert.equal(messages[0].message, "message yay!");
-        });
-
-        it("should still allow non-identifier characters as well as leading/trailing whitespace", () => {
-            linter.defineRule("test-rule", context => ({
-                Literal(node) {
-                    context.report(node, "message {{ parameter-name }}", {
-                        "parameter-name": "yay!"
-                    });
-                }
-            }));
-
-            config.rules["test-rule"] = 1;
-
-            const messages = linter.verify("0", config);
-
-            assert.equal(messages[0].message, "message yay!");
-        });
-
-        it("should include a fix passed as the last argument when location is passed", () => {
-            linter.defineRule("test-rule", () => ({
-                Program(node) {
-                    linter.report("test-rule", 2, node, { line: 42, column: 23 }, "my message {{1}}{{0}}", ["!", "testing"], { range: [1, 1], text: "" });
-                }
-            }));
-
-            const messages = linter.verify("0", config, "", true);
-
-            assert.deepEqual(messages[0], {
-                severity: 2,
-                ruleId: "test-rule",
-                message: "my message testing!",
-                nodeType: "Program",
-                line: 42,
-                column: 24,
-                source: "",
-                fix: { range: [1, 1], text: "" }
-            });
-        });
-
-        it("should have 'endLine' and 'endColumn' when there is not 'loc' property.", () => {
-            linter.defineRule("test-rule", () => ({
-                Program(node) {
-                    linter.report(
-                        "test-rule",
-                        2,
-                        node,
-                        "test"
-                    );
-                }
-            }));
-
-            const sourceText = "foo + bar;";
-
-            const messages = linter.verify(sourceText, config, "", true);
-
-            assert.strictEqual(messages[0].endLine, 1);
-            assert.strictEqual(messages[0].endColumn, sourceText.length + 1); // (1-based column)
-        });
-
-        it("should have 'endLine' and 'endColumn' when 'loc' property has 'end' property.", () => {
-            linter.defineRule("test-rule", () => ({
-                Program(node) {
-                    linter.report(
-                        "test-rule",
-                        2,
-                        node,
-                        node.loc,
-                        "test"
-                    );
-                }
-            }));
-
-            const messages = linter.verify("0", config, "", true);
-
-            assert.strictEqual(messages[0].endLine, 1);
-            assert.strictEqual(messages[0].endColumn, 2);
-        });
-
-        it("should not have 'endLine' and 'endColumn' when 'loc' property does not have 'end' property.", () => {
-            linter.defineRule("test-rule", () => ({
-                Program(node) {
-                    linter.report(
-                        "test-rule",
-                        2,
-                        node,
-                        node.loc.start,
-                        "test"
-                    );
-                }
-            }));
-
-            const messages = linter.verify("0", config, "", true);
-
-            assert.strictEqual(messages[0].endLine, void 0);
-            assert.strictEqual(messages[0].endColumn, void 0);
-        });
-
-        it("should infer an 'endLine' and 'endColumn' property when using the object-based context.report API", () => {
-            const messages = linter.verify("foo", { rules: { "no-undef": "error" } });
-
-            assert.strictEqual(messages.length, 1);
-            assert.strictEqual(messages[0].endLine, 1);
-            assert.strictEqual(messages[0].endColumn, 4);
-        });
-    });
-
     describe("when evaluating code", () => {
         const code = TEST_CODE;
 
@@ -2739,7 +2304,7 @@ describe("Linter", () => {
                     column: 1,
                     severity: 1,
                     source: "var answer = 6 * 7;",
-                    nodeType: void 0
+                    nodeType: null
                 }
             );
         });
@@ -3439,6 +3004,17 @@ describe("Linter", () => {
                 linter.verify("var foo", config);
             });
         });
+
+        it("should pass 'id' to rule contexts with the rule id", () => {
+            const spy = sandbox.spy(context => {
+                assert.strictEqual(context.id, "foo-bar-baz");
+                return {};
+            });
+
+            linter.defineRule("foo-bar-baz", spy);
+            linter.verify("x", { rules: { "foo-bar-baz": "error" } });
+            assert(spy.calledOnce);
+        });
     });
 
     describe("Variables and references", () => {
@@ -3870,6 +3446,38 @@ describe("Linter", () => {
             assert.strictEqual(fixResult.output, `${" ".repeat(10)}a`);
             assert.strictEqual(fixResult.messages.length, 1);
         });
+
+        it("should throw an error if fix is passed but meta has no `fixable` property", () => {
+            linter.defineRule("test-rule", {
+                meta: {
+                    docs: {},
+                    schema: []
+                },
+                create: context => ({
+                    Program(node) {
+                        context.report(node, "hello world", {}, () => ({ range: [1, 1], text: "" }));
+                    }
+                })
+            });
+
+            assert.throws(() => {
+                linter.verify("0", { rules: { "test-rule": "error" } });
+            }, /Fixable rules should export a `meta\.fixable` property.$/);
+        });
+
+        it("should not throw an error if fix is passed and there is no metadata", () => {
+            linter.defineRule("test-rule", {
+                create: context => ({
+                    Program(node) {
+                        context.report(node, "hello world", {}, () => ({ range: [1, 1], text: "" }));
+                    }
+                })
+            });
+
+            assert.doesNotThrow(() => {
+                linter.verify("0", { rules: { "test-rule": "error" } });
+            });
+        });
     });
 
     describe("Edge cases", () => {
diff --git a/tests/lib/report-translator.js b/tests/lib/report-translator.js
new file mode 100644
index 000000000..3e04658ba
--- /dev/null
+++ b/tests/lib/report-translator.js
@@ -0,0 +1,564 @@
+/**
+ * @fileoverview Tests for createReportTranslator
+ * @author Teddy Katz
+ */
+"use strict";
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const assert = require("chai").assert;
+const SourceCode = require("../../lib/util/source-code");
+const espree = require("espree");
+const createReportTranslator = require("../../lib/report-translator");
+
+//------------------------------------------------------------------------------
+// Tests
+//------------------------------------------------------------------------------
+
+describe("createReportTranslator", () => {
+
+    /**
+     * Creates a SourceCode instance out of JavaScript text
+     * @param {string} text Source text
+     * @returns {SourceCode} A SourceCode instance for that text
+     */
+    function createSourceCode(text) {
+        return new SourceCode(
+            text,
+            espree.parse(
+                text.replace(/^\uFEFF/, ""),
+                {
+                    loc: true,
+                    range: true,
+                    raw: true,
+                    tokens: true,
+                    comment: true
+                }
+            ));
+    }
+
+    let node, location, message, translateReport;
+
+    beforeEach(() => {
+        const sourceCode = createSourceCode("foo\nbar");
+
+        node = sourceCode.ast.body[0];
+        location = sourceCode.ast.body[1].loc.start;
+        message = "foo";
+        translateReport = createReportTranslator({ ruleId: "foo-rule", severity: 2, sourceCode });
+    });
+
+    describe("old-style call with location", () => {
+        it("should extract the location correctly", () => {
+            assert.deepEqual(
+                translateReport(node, location, message, {}),
+                {
+                    ruleId: "foo-rule",
+                    severity: 2,
+                    message: "foo",
+                    line: 2,
+                    column: 1,
+                    nodeType: "ExpressionStatement",
+                    source: "bar"
+                }
+            );
+        });
+    });
+
+    describe("old-style call without location", () => {
+        it("should use the start location and end location of the node", () => {
+            assert.deepEqual(
+                translateReport(node, message, {}),
+                {
+                    ruleId: "foo-rule",
+                    severity: 2,
+                    message: "foo",
+                    line: 1,
+                    column: 1,
+                    endLine: 1,
+                    endColumn: 4,
+                    nodeType: "ExpressionStatement",
+                    source: "foo"
+                }
+            );
+        });
+    });
+
+    describe("new-style call with all options", () => {
+        it("should include the new-style options in the report", () => {
+            const reportDescriptor = {
+                node,
+                loc: location,
+                message,
+                fix: () => ({ range: [1, 2], text: "foo" })
+            };
+
+            assert.deepEqual(
+                translateReport(reportDescriptor),
+                {
+                    ruleId: "foo-rule",
+                    severity: 2,
+                    message: "foo",
+                    line: 2,
+                    column: 1,
+                    nodeType: "ExpressionStatement",
+                    source: "bar",
+                    fix: {
+                        range: [1, 2],
+                        text: "foo"
+                    }
+                }
+            );
+        });
+    });
+    describe("combining autofixes", () => {
+        it("should merge fixes to one if 'fix' function returns an array of fixes.", () => {
+            const reportDescriptor = {
+                node,
+                loc: location,
+                message,
+                fix: () => [{ range: [1, 2], text: "foo" }, { range: [4, 5], text: "bar" }]
+            };
+
+            assert.deepEqual(
+                translateReport(reportDescriptor),
+                {
+                    ruleId: "foo-rule",
+                    severity: 2,
+                    message: "foo",
+                    line: 2,
+                    column: 1,
+                    nodeType: "ExpressionStatement",
+                    source: "bar",
+                    fix: {
+                        range: [1, 5],
+                        text: "fooo\nbar"
+                    }
+                }
+            );
+        });
+
+        it("should merge fixes to one if 'fix' function returns an iterator of fixes.", () => {
+            const reportDescriptor = {
+                node,
+                loc: location,
+                message,
+                *fix() {
+                    yield { range: [1, 2], text: "foo" };
+                    yield { range: [4, 5], text: "bar" };
+                }
+            };
+
+            assert.deepEqual(
+                translateReport(reportDescriptor),
+                {
+                    ruleId: "foo-rule",
+                    severity: 2,
+                    message: "foo",
+                    line: 2,
+                    column: 1,
+                    nodeType: "ExpressionStatement",
+                    source: "bar",
+                    fix: {
+                        range: [1, 5],
+                        text: "fooo\nbar"
+                    }
+                }
+            );
+        });
+
+        it("should pass through fixes if only one is present", () => {
+            const reportDescriptor = {
+                node,
+                loc: location,
+                message,
+                fix: () => [{ range: [1, 2], text: "foo" }]
+            };
+
+            assert.deepEqual(
+                translateReport(reportDescriptor),
+                {
+                    ruleId: "foo-rule",
+                    severity: 2,
+                    message: "foo",
+                    line: 2,
+                    column: 1,
+                    nodeType: "ExpressionStatement",
+                    source: "bar",
+                    fix: {
+                        range: [1, 2],
+                        text: "foo"
+                    }
+                }
+            );
+        });
+
+        it("should handle inserting BOM correctly.", () => {
+            const reportDescriptor = {
+                node,
+                loc: location,
+                message,
+                fix: () => [{ range: [0, 3], text: "\uFEFFfoo" }, { range: [4, 5], text: "x" }]
+            };
+
+            assert.deepEqual(
+                translateReport(reportDescriptor),
+                {
+                    ruleId: "foo-rule",
+                    severity: 2,
+                    message: "foo",
+                    line: 2,
+                    column: 1,
+                    nodeType: "ExpressionStatement",
+                    source: "bar",
+                    fix: {
+                        range: [0, 5],
+                        text: "\uFEFFfoo\nx"
+                    }
+                }
+            );
+        });
+
+
+        it("should handle removing BOM correctly.", () => {
+            const sourceCode = createSourceCode("\uFEFFfoo\nbar");
+
+            node = sourceCode.ast.body[0];
+
+            const reportDescriptor = {
+                node,
+                message,
+                fix: () => [{ range: [-1, 3], text: "foo" }, { range: [4, 5], text: "x" }]
+            };
+
+            assert.deepEqual(
+                createReportTranslator({ ruleId: "foo-rule", severity: 1, sourceCode })(reportDescriptor),
+                {
+                    ruleId: "foo-rule",
+                    severity: 1,
+                    message: "foo",
+                    line: 1,
+                    column: 1,
+                    endLine: 1,
+                    endColumn: 4,
+                    nodeType: "ExpressionStatement",
+                    source: "foo",
+                    fix: {
+                        range: [-1, 5],
+                        text: "foo\nx"
+                    }
+                }
+            );
+        });
+
+        it("should throw an assertion error if ranges are overlapped.", () => {
+            const reportDescriptor = {
+                node,
+                loc: location,
+                message,
+                fix: () => [{ range: [0, 3], text: "\uFEFFfoo" }, { range: [2, 5], text: "x" }]
+            };
+
+            assert.throws(
+                translateReport.bind(null, reportDescriptor),
+                "Fix objects must not be overlapped in a report."
+            );
+        });
+
+        it("should include a fix passed as the last argument when location is passed", () => {
+            assert.deepEqual(
+                translateReport(
+                    node,
+                    { line: 42, column: 23 },
+                    "my message {{1}}{{0}}",
+                    ["!", "testing"],
+                    () => ({ range: [1, 1], text: "" })
+                ),
+                {
+                    ruleId: "foo-rule",
+                    severity: 2,
+                    message: "my message testing!",
+                    line: 42,
+                    column: 24,
+                    nodeType: "ExpressionStatement",
+                    source: "",
+                    fix: {
+                        range: [1, 1],
+                        text: ""
+                    }
+                }
+            );
+        });
+
+    });
+
+    describe("message interpolation", () => {
+        it("should correctly parse a message when being passed all options in an old-style report", () => {
+            assert.deepEqual(
+                translateReport(node, node.loc.end, "hello {{dynamic}}", { dynamic: node.type }),
+                {
+                    severity: 2,
+                    ruleId: "foo-rule",
+                    message: "hello ExpressionStatement",
+                    nodeType: "ExpressionStatement",
+                    line: 1,
+                    column: 4,
+                    source: "foo"
+                }
+            );
+        });
+
+        it("should correctly parse a message when being passed all options in a new-style report", () => {
+            assert.deepEqual(
+                translateReport({ node, loc: node.loc.end, message: "hello {{dynamic}}", data: { dynamic: node.type } }),
+                {
+                    severity: 2,
+                    ruleId: "foo-rule",
+                    message: "hello ExpressionStatement",
+                    nodeType: "ExpressionStatement",
+                    line: 1,
+                    column: 4,
+                    source: "foo"
+                }
+            );
+        });
+
+        it("should correctly parse a message with object keys as numbers", () => {
+            assert.strictEqual(
+                translateReport(node, "my message {{name}}{{0}}", { 0: "!", name: "testing" }).message,
+                "my message testing!"
+            );
+        });
+
+        it("should correctly parse a message with array", () => {
+            assert.strictEqual(
+                translateReport(node, "my message {{1}}{{0}}", ["!", "testing"]).message,
+                "my message testing!"
+            );
+        });
+
+        it("should allow template parameter with inner whitespace", () => {
+            assert.strictEqual(
+                translateReport(node, "message {{parameter name}}", { "parameter name": "yay!" }).message,
+                "message yay!"
+            );
+        });
+
+        it("should allow template parameter with non-identifier characters", () => {
+            assert.strictEqual(
+                translateReport(node, "message {{parameter-name}}", { "parameter-name": "yay!" }).message,
+                "message yay!"
+            );
+        });
+
+        it("should allow template parameter wrapped in braces", () => {
+            assert.strictEqual(
+                translateReport(node, "message {{{param}}}", { param: "yay!" }).message,
+                "message {yay!}"
+            );
+        });
+
+        it("should ignore template parameter with no specified value", () => {
+            assert.strictEqual(
+                translateReport(node, "message {{parameter}}", {}).message,
+                "message {{parameter}}"
+            );
+        });
+
+        it("should handle leading whitespace in template parameter", () => {
+            assert.strictEqual(
+                translateReport({ node, message: "message {{ parameter}}", data: { parameter: "yay!" } }).message,
+                "message yay!"
+            );
+        });
+
+        it("should handle trailing whitespace in template parameter", () => {
+            assert.strictEqual(
+                translateReport({ node, message: "message {{parameter }}", data: { parameter: "yay!" } }).message,
+                "message yay!"
+            );
+        });
+
+        it("should still allow inner whitespace as well as leading/trailing", () => {
+            assert.strictEqual(
+                translateReport(node, "message {{ parameter name }}", { "parameter name": "yay!" }).message,
+                "message yay!"
+            );
+        });
+
+        it("should still allow non-identifier characters as well as leading/trailing whitespace", () => {
+            assert.strictEqual(
+                translateReport(node, "message {{ parameter-name }}", { "parameter-name": "yay!" }).message,
+                "message yay!"
+            );
+        });
+    });
+
+    describe("location inference", () => {
+        it("should use the provided location when given in an old-style call", () => {
+            assert.deepEqual(
+                translateReport(node, { line: 42, column: 13 }, "hello world"),
+                {
+                    severity: 2,
+                    ruleId: "foo-rule",
+                    message: "hello world",
+                    nodeType: "ExpressionStatement",
+                    line: 42,
+                    column: 14,
+                    source: ""
+                }
+            );
+        });
+
+        it("should use the provided location when given in an new-style call", () => {
+            assert.deepEqual(
+                translateReport({ node, loc: { line: 42, column: 13 }, message: "hello world" }),
+                {
+                    severity: 2,
+                    ruleId: "foo-rule",
+                    message: "hello world",
+                    nodeType: "ExpressionStatement",
+                    line: 42,
+                    column: 14,
+                    source: ""
+                }
+            );
+        });
+
+        it("should extract the start and end locations from a node if no location is provided", () => {
+            assert.deepEqual(
+                translateReport(node, "hello world"),
+                {
+                    severity: 2,
+                    ruleId: "foo-rule",
+                    message: "hello world",
+                    nodeType: "ExpressionStatement",
+                    line: 1,
+                    column: 1,
+                    endLine: 1,
+                    endColumn: 4,
+                    source: "foo"
+                }
+            );
+        });
+
+        it("should have 'endLine' and 'endColumn' when 'loc' property has 'end' property.", () => {
+            assert.deepEqual(
+                translateReport({ loc: node.loc, message: "hello world" }),
+                {
+                    severity: 2,
+                    ruleId: "foo-rule",
+                    message: "hello world",
+                    nodeType: null,
+                    line: 1,
+                    column: 1,
+                    endLine: 1,
+                    endColumn: 4,
+                    source: "foo"
+                }
+            );
+        });
+
+        it("should not have 'endLine' and 'endColumn' when 'loc' property does not have 'end' property.", () => {
+            assert.deepEqual(
+                translateReport({ loc: node.loc.start, message: "hello world" }),
+                {
+                    severity: 2,
+                    ruleId: "foo-rule",
+                    message: "hello world",
+                    nodeType: null,
+                    line: 1,
+                    column: 1,
+                    source: "foo"
+                }
+            );
+        });
+
+        it("should infer an 'endLine' and 'endColumn' property when using the object-based context.report API", () => {
+            assert.deepEqual(
+                translateReport({ node, message: "hello world" }),
+                {
+                    severity: 2,
+                    ruleId: "foo-rule",
+                    message: "hello world",
+                    nodeType: "ExpressionStatement",
+                    line: 1,
+                    column: 1,
+                    endLine: 1,
+                    endColumn: 4,
+                    source: "foo"
+                }
+            );
+        });
+    });
+
+    describe("converting old-style calls", () => {
+        it("should include a fix passed as the last argument when location is not passed", () => {
+            assert.deepEqual(
+                translateReport(node, "my message {{1}}{{0}}", ["!", "testing"], () => ({ range: [1, 1], text: "" })),
+                {
+                    severity: 2,
+                    ruleId: "foo-rule",
+                    message: "my message testing!",
+                    nodeType: "ExpressionStatement",
+                    line: 1,
+                    column: 1,
+                    endLine: 1,
+                    endColumn: 4,
+                    source: "foo",
+                    fix: { range: [1, 1], text: "" }
+                }
+            );
+        });
+    });
+
+    describe("validation", () => {
+
+        it("should throw an error if node is not an object", () => {
+            assert.throws(
+                () => translateReport("not a node", "hello world"),
+                "Node must be an object"
+            );
+        });
+
+
+        it("should not throw an error if location is provided and node is not in an old-style call", () => {
+            assert.deepEqual(
+                translateReport(null, { line: 1, column: 1 }, "hello world"),
+                {
+                    severity: 2,
+                    ruleId: "foo-rule",
+                    message: "hello world",
+                    nodeType: null,
+                    line: 1,
+                    column: 2,
+                    source: "foo"
+                }
+            );
+        });
+
+        it("should not throw an error if location is provided and node is not in a new-style call", () => {
+            assert.deepEqual(
+                translateReport({ loc: { line: 1, column: 1 }, message: "hello world" }),
+                {
+                    severity: 2,
+                    ruleId: "foo-rule",
+                    message: "hello world",
+                    nodeType: null,
+                    line: 1,
+                    column: 2,
+                    source: "foo"
+                }
+            );
+        });
+
+        it("should throw an error if neither node nor location is provided", () => {
+            assert.throws(
+                () => translateReport(null, "hello world"),
+                "Node must be provided when reporting error if location is not provided"
+            );
+        });
+    });
+});
diff --git a/tests/lib/rule-context.js b/tests/lib/rule-context.js
deleted file mode 100644
index 4195cb4a1..000000000
--- a/tests/lib/rule-context.js
+++ /dev/null
@@ -1,328 +0,0 @@
-/**
- * @fileoverview Tests for RuleContext object.
- * @author Kevin Partington
- */
-
-"use strict";
-
-//------------------------------------------------------------------------------
-// Requirements
-//------------------------------------------------------------------------------
-
-const sinon = require("sinon"),
-    assert = require("chai").assert,
-    leche = require("leche"),
-    Linter = require("../../lib/linter"),
-    RuleContext = require("../../lib/rule-context");
-
-const realESLint = new Linter();
-
-//------------------------------------------------------------------------------
-// Tests
-//------------------------------------------------------------------------------
-
-describe("RuleContext", () => {
-    const sandbox = sinon.sandbox.create();
-
-    describe("report()", () => {
-        let ruleContext, eslint;
-
-        beforeEach(() => {
-            eslint = leche.fake(realESLint);
-            ruleContext = new RuleContext("fake-rule", eslint, 2, {}, {}, {}, "espree");
-        });
-
-        describe("old-style call with location", () => {
-            it("should call eslint.report() with rule ID and severity prepended", () => {
-                const node = {},
-                    location = {},
-                    message = "Message",
-                    messageOpts = {};
-
-                const mockESLint = sandbox.mock(eslint);
-
-                mockESLint.expects("report")
-                    .once()
-                    .withArgs("fake-rule", 2, node, location, message, messageOpts);
-
-                ruleContext.report(node, location, message, messageOpts);
-
-                mockESLint.verify();
-            });
-        });
-
-        describe("old-style call without location", () => {
-            it("should call eslint.report() with rule ID and severity prepended", () => {
-                const node = {},
-                    message = "Message",
-                    messageOpts = {};
-
-                const mockESLint = sandbox.mock(eslint);
-
-                mockESLint.expects("report")
-                    .once()
-                    .withArgs("fake-rule", 2, node, message, messageOpts);
-
-                ruleContext.report(node, message, messageOpts);
-
-                mockESLint.verify();
-            });
-        });
-
-        describe("new-style call with all options", () => {
-            it("should call eslint.report() with rule ID and severity prepended and all new-style options", () => {
-                const node = {},
-                    location = {},
-                    message = "Message",
-                    messageOpts = {},
-                    fixerObj = {},
-                    fix = sandbox.mock().returns(fixerObj).once();
-
-                const mockESLint = sandbox.mock(eslint);
-
-                mockESLint.expects("report")
-                    .once()
-                    .withArgs("fake-rule", 2, node, location, message, messageOpts, fixerObj);
-                mockESLint.expects("getSourceCode")
-                    .once()
-                    .returns(null);
-
-                ruleContext.report({
-                    node,
-                    loc: location,
-                    message,
-                    data: messageOpts,
-                    fix
-                });
-
-                fix.verify();
-                mockESLint.verify();
-            });
-
-            it("should merge fixes to one if 'fix' function returns an array of fixes.", () => {
-                const mockESLint = sandbox.mock(eslint);
-
-                mockESLint.expects("getSourceCode")
-                    .returns({ text: "var foo = 100;" });
-                mockESLint.expects("report")
-                    .once()
-                    .withArgs(
-                        sinon.match.any,
-                        sinon.match.any,
-                        sinon.match.any,
-                        sinon.match.any,
-                        sinon.match.any,
-                        sinon.match.any,
-                        sinon.match({
-                            range: [4, 13],
-                            text: "bar = 234"
-                        })
-                    );
-
-                ruleContext.report({
-                    node: {},
-                    loc: {},
-                    message: "Message",
-                    fix(fixer) {
-                        return [
-                            fixer.replaceTextRange([10, 13], "234"),
-                            fixer.replaceTextRange([4, 7], "bar")
-                        ];
-                    }
-                });
-
-                mockESLint.verify();
-            });
-
-            it("should merge fixes to one if 'fix' function returns an iterator of fixes.", () => {
-                const mockESLint = sandbox.mock(eslint);
-
-                mockESLint.expects("getSourceCode").returns({ text: "var foo = 100;" });
-                mockESLint.expects("report").once().withArgs(
-                    sinon.match.any,
-                    sinon.match.any,
-                    sinon.match.any,
-                    sinon.match.any,
-                    sinon.match.any,
-                    sinon.match.any,
-                    sinon.match({
-                        range: [4, 13],
-                        text: "bar = 234"
-                    })
-                );
-
-                ruleContext.report({
-                    node: {},
-                    loc: {},
-                    message: "Message",
-                    *fix(fixer) {
-                        yield fixer.replaceTextRange([10, 13], "234");
-                        yield fixer.replaceTextRange([4, 7], "bar");
-                    }
-                });
-
-                mockESLint.verify();
-            });
-
-            it("should pass through fixes if only one is present", () => {
-                const mockESLint = sandbox.mock(eslint);
-
-                mockESLint.expects("getSourceCode").returns({ text: "var foo = 100;" });
-                mockESLint.expects("report").once().withArgs(
-                    sinon.match.any,
-                    sinon.match.any,
-                    sinon.match.any,
-                    sinon.match.any,
-                    sinon.match.any,
-                    sinon.match.any,
-                    sinon.match({
-                        range: [10, 13],
-                        text: "234"
-                    })
-                );
-
-                ruleContext.report({
-                    node: {},
-                    loc: {},
-                    message: "Message",
-                    *fix(fixer) {
-                        yield fixer.replaceTextRange([10, 13], "234");
-                    }
-                });
-
-                mockESLint.verify();
-            });
-
-            it("should handle inserting BOM correctly.", () => {
-                const mockESLint = sandbox.mock(eslint);
-
-                mockESLint.expects("getSourceCode")
-                    .returns({ text: "var foo = 100;" });
-                mockESLint.expects("report")
-                    .once()
-                    .withArgs(
-                        sinon.match.any,
-                        sinon.match.any,
-                        sinon.match.any,
-                        sinon.match.any,
-                        sinon.match.any,
-                        sinon.match.any,
-                        sinon.match({
-                            range: [0, 13],
-                            text: "\uFEFFvar bar = 234"
-                        })
-                    );
-
-                ruleContext.report({
-                    node: {},
-                    loc: {},
-                    message: "Message",
-                    fix(fixer) {
-                        return [
-                            fixer.insertTextBeforeRange([0, 1], "\uFEFF"),
-                            fixer.replaceTextRange([10, 13], "234"),
-                            fixer.replaceTextRange([4, 7], "bar")
-                        ];
-                    }
-                });
-
-                mockESLint.verify();
-            });
-
-
-            it("should handle removing BOM correctly.", () => {
-                const mockESLint = sandbox.mock(eslint);
-
-                mockESLint.expects("getSourceCode")
-                    .returns({ text: "var foo = 100;" });
-                mockESLint.expects("report")
-                    .once()
-                    .withArgs(
-                        sinon.match.any,
-                        sinon.match.any,
-                        sinon.match.any,
-                        sinon.match.any,
-                        sinon.match.any,
-                        sinon.match.any,
-                        sinon.match({
-                            range: [-1, 13],
-                            text: "var bar = 234"
-                        })
-                    );
-
-                ruleContext.report({
-                    node: {},
-                    loc: {},
-                    message: "Message",
-                    fix(fixer) {
-                        return [
-                            fixer.removeRange([-1, 0]),
-                            fixer.replaceTextRange([10, 13], "234"),
-                            fixer.replaceTextRange([4, 7], "bar")
-                        ];
-                    }
-                });
-
-                mockESLint.verify();
-            });
-
-            it("should throw an assertion error if ranges are overlapped.", () => {
-                const mockESLint = sandbox.mock(eslint);
-
-                mockESLint.expects("getSourceCode")
-                    .returns({ text: "var foo = 100;" });
-                mockESLint.expects("report")
-                    .never();
-
-                assert.throws(() => {
-                    ruleContext.report({
-                        node: {},
-                        loc: {},
-                        message: "Message",
-                        fix(fixer) {
-                            return [
-                                fixer.removeRange([-1, 0]),
-                                fixer.removeRange([-1, 0])
-                            ];
-                        }
-                    });
-                }, "Fix objects must not be overlapped in a report.");
-
-                mockESLint.verify();
-            });
-
-        });
-    });
-
-    describe("parserServices", () => {
-
-        it("should pass through parserServices properties to context", () => {
-            const services = {
-                test: {}
-            };
-            const ruleContext = new RuleContext("fake-rule", {}, 2, {}, {}, {}, "espree", {}, services);
-
-            assert.equal(ruleContext.parserServices.test, services.test);
-        });
-
-        it("should copy parserServices properties to a new object", () => {
-            const services = {
-                test: {}
-            };
-            const ruleContext = new RuleContext("fake-rule", {}, 2, {}, {}, {}, "espree", {}, services);
-
-            assert.notEqual(ruleContext.parserServices, services);
-        });
-
-        it("should make context.parserServices a frozen object", () => {
-            const services = {
-                test: {}
-            };
-            const ruleContext = new RuleContext("fake-rule", {}, 2, {}, {}, {}, "espree", {}, services);
-
-            assert.ok(Object.isFrozen(ruleContext.parserServices));
-        });
-
-    });
-
-});
diff --git a/tests/lib/rules/_set-default-parser.js b/tests/lib/rules/_set-default-parser.js
index 633a3ff6b..076640758 100644
--- a/tests/lib/rules/_set-default-parser.js
+++ b/tests/lib/rules/_set-default-parser.js
@@ -16,5 +16,5 @@ const path = require("path");
 const RuleTester = require("../../../lib/testers/rule-tester");
 
 RuleTester.setDefaultConfig({
-    parser: path.resolve(__dirname, "../../../lib/testers/test-parser")
+    parser: path.resolve(__dirname, "../../../tools/internal-testers/test-parser")
 });
diff --git a/tests/lib/rules/generator-star-spacing.js b/tests/lib/rules/generator-star-spacing.js
index 34075485e..5108091ff 100644
--- a/tests/lib/rules/generator-star-spacing.js
+++ b/tests/lib/rules/generator-star-spacing.js
@@ -404,6 +404,56 @@ ruleTester.run("generator-star-spacing", rule, {
             options: [{ before: false, after: false }]
         },
 
+        // full configurability
+        {
+            code: "function * foo(){}",
+            options: [{ before: false, after: false, named: "both" }]
+        },
+        {
+            code: "var foo = function * (){};",
+            options: [{ before: false, after: false, anonymous: "both" }]
+        },
+        {
+            code: "class Foo { * foo(){} }",
+            options: [{ before: false, after: false, method: "both" }]
+        },
+        {
+            code: "var foo = { * foo(){} }",
+            options: [{ before: false, after: false, method: "both" }]
+        },
+        {
+            code: "var foo = { bar: function * () {} }",
+            options: [{ before: false, after: false, anonymous: "both" }]
+        },
+        {
+            code: "class Foo { static * foo(){} }",
+            options: [{ before: false, after: false, method: "both" }]
+        },
+
+        // default to top level "before"
+        {
+            code: "function *foo(){}",
+            options: [{ method: "both" }]
+        },
+
+        // don't apply unrelated override
+        {
+            code: "function*foo(){}",
+            options: [{ before: false, after: false, method: "both" }]
+        },
+
+        // ensure using object-type override works
+        {
+            code: "function * foo(){}",
+            options: [{ before: false, after: false, named: { before: true, after: true } }]
+        },
+
+        // unspecified option uses default
+        {
+            code: "function *foo(){}",
+            options: [{ before: false, after: false, named: { before: true } }]
+        },
+
         // https://github.com/eslint/eslint/issues/7101#issuecomment-246080531
         {
             code: "async function foo() { }",
@@ -1168,6 +1218,124 @@ ruleTester.run("generator-star-spacing", rule, {
                 message: "Unexpected space after *.",
                 type: "Punctuator"
             }]
+        },
+
+        // full configurability
+        {
+            code: "function*foo(){}",
+            output: "function * foo(){}",
+            options: [{ before: false, after: false, named: "both" }],
+            errors: [{
+                message: "Missing space before *.",
+                type: "Punctuator"
+            }, {
+                message: "Missing space after *.",
+                type: "Punctuator"
+            }]
+        },
+        {
+            code: "var foo = function*(){};",
+            output: "var foo = function * (){};",
+            options: [{ before: false, after: false, anonymous: "both" }],
+            errors: [{
+                message: "Missing space before *.",
+                type: "Punctuator"
+            }, {
+                message: "Missing space after *.",
+                type: "Punctuator"
+            }]
+        },
+        {
+            code: "class Foo { *foo(){} }",
+            output: "class Foo { * foo(){} }",
+            options: [{ before: false, after: false, method: "both" }],
+            errors: [{
+                message: "Missing space after *.",
+                type: "Punctuator"
+            }]
+        },
+        {
+            code: "var foo = { *foo(){} }",
+            output: "var foo = { * foo(){} }",
+            options: [{ before: false, after: false, method: "both" }],
+            errors: [{
+                message: "Missing space after *.",
+                type: "Punctuator"
+            }]
+        },
+        {
+            code: "var foo = { bar: function*() {} }",
+            output: "var foo = { bar: function * () {} }",
+            options: [{ before: false, after: false, anonymous: "both" }],
+            errors: [{
+                message: "Missing space before *.",
+                type: "Punctuator"
+            }, {
+                message: "Missing space after *.",
+                type: "Punctuator"
+            }]
+        },
+        {
+            code: "class Foo { static*foo(){} }",
+            output: "class Foo { static * foo(){} }",
+            options: [{ before: false, after: false, method: "both" }],
+            errors: [{
+                message: "Missing space before *.",
+                type: "Punctuator"
+            }, {
+                message: "Missing space after *.",
+                type: "Punctuator"
+            }]
+        },
+
+        // default to top level "before"
+        {
+            code: "function*foo(){}",
+            output: "function *foo(){}",
+            options: [{ method: "both" }],
+            errors: [{
+                message: "Missing space before *.",
+                type: "Punctuator"
+            }]
+        },
+
+        // don't apply unrelated override
+        {
+            code: "function * foo(){}",
+            output: "function*foo(){}",
+            options: [{ before: false, after: false, method: "both" }],
+            errors: [{
+                message: "Unexpected space before *.",
+                type: "Punctuator"
+            }, {
+                message: "Unexpected space after *.",
+                type: "Punctuator"
+            }]
+        },
+
+        // ensure using object-type override works
+        {
+            code: "function*foo(){}",
+            output: "function * foo(){}",
+            options: [{ before: false, after: false, named: { before: true, after: true } }],
+            errors: [{
+                message: "Missing space before *.",
+                type: "Punctuator"
+            }, {
+                message: "Missing space after *.",
+                type: "Punctuator"
+            }]
+        },
+
+        // unspecified option uses default
+        {
+            code: "function*foo(){}",
+            output: "function *foo(){}",
+            options: [{ before: false, after: false, named: { before: true } }],
+            errors: [{
+                message: "Missing space before *.",
+                type: "Punctuator"
+            }]
         }
 
     ]
diff --git a/tests/lib/rules/no-unused-vars.js b/tests/lib/rules/no-unused-vars.js
index 4e2849fe1..b7ccc3844 100644
--- a/tests/lib/rules/no-unused-vars.js
+++ b/tests/lib/rules/no-unused-vars.js
@@ -326,11 +326,37 @@ ruleTester.run("no-unused-vars", rule, {
         { code: "/*exported x*/ var { x, y } = z", parserOptions: { ecmaVersion: 6 }, errors: [assignedError("y")] },
 
         // ignore pattern
-        { code: "var _a; var b;", options: [{ vars: "all", varsIgnorePattern: "^_" }], errors: [{ message: "'b' is defined but never used.", line: 1, column: 13 }] },
-        { code: "var a; function foo() { var _b; var c_; } foo();", options: [{ vars: "local", varsIgnorePattern: "^_" }], errors: [{ message: "'c_' is defined but never used.", line: 1, column: 37 }] },
-        { code: "function foo(a, _b) { } foo();", options: [{ args: "all", argsIgnorePattern: "^_" }], errors: [{ message: "'a' is defined but never used.", line: 1, column: 14 }] },
-        { code: "function foo(a, _b, c) { return a; } foo();", options: [{ args: "after-used", argsIgnorePattern: "^_" }], errors: [{ message: "'c' is defined but never used.", line: 1, column: 21 }] },
-        { code: "var [ firstItemIgnored, secondItem ] = items;", options: [{ vars: "all", varsIgnorePattern: "[iI]gnored" }], parserOptions: { ecmaVersion: 6 }, errors: [{ message: "'secondItem' is assigned a value but never used.", line: 1, column: 25 }] },
+        {
+            code: "var _a; var b;",
+            options: [{ vars: "all", varsIgnorePattern: "^_" }],
+            errors: [{ message: "'b' is defined but never used. Allowed unused vars must match /^_/.", line: 1, column: 13 }]
+        },
+        {
+            code: "var a; function foo() { var _b; var c_; } foo();",
+            options: [{ vars: "local", varsIgnorePattern: "^_" }],
+            errors: [{ message: "'c_' is defined but never used. Allowed unused vars must match /^_/.", line: 1, column: 37 }]
+        },
+        {
+            code: "function foo(a, _b) { } foo();",
+            options: [{ args: "all", argsIgnorePattern: "^_" }],
+            errors: [{ message: "'a' is defined but never used. Allowed unused args must match /^_/.", line: 1, column: 14 }]
+        },
+        {
+            code: "function foo(a, _b, c) { return a; } foo();",
+            options: [{ args: "after-used", argsIgnorePattern: "^_" }],
+            errors: [{ message: "'c' is defined but never used. Allowed unused args must match /^_/.", line: 1, column: 21 }]
+        },
+        {
+            code: "function foo(_a) { } foo();",
+            options: [{ args: "all", argsIgnorePattern: "[iI]gnored" }],
+            errors: [{ message: "'_a' is defined but never used. Allowed unused args must match /[iI]gnored/.", line: 1, column: 14 }]
+        },
+        {
+            code: "var [ firstItemIgnored, secondItem ] = items;",
+            options: [{ vars: "all", varsIgnorePattern: "[iI]gnored" }],
+            parserOptions: { ecmaVersion: 6 },
+            errors: [{ message: "'secondItem' is assigned a value but never used. Allowed unused vars must match /[iI]gnored/.", line: 1, column: 25 }]
+        },
 
         // for-in loops (see #2342)
         { code: "(function(obj) { var name; for ( name in obj ) { i(); return; } })({});", errors: [{ message: "'name' is assigned a value but never used.", line: 1, column: 22 }] },
@@ -491,14 +517,14 @@ ruleTester.run("no-unused-vars", rule, {
         {
             code: "try{}catch(err){};",
             options: [{ caughtErrors: "all", caughtErrorsIgnorePattern: "^ignore" }],
-            errors: [{ message: "'err' is defined but never used." }]
+            errors: [{ message: "'err' is defined but never used. Allowed unused args must match /^ignore/." }]
         },
 
         // multiple try catch with one success
         {
             code: "try{}catch(ignoreErr){}try{}catch(err){};",
             options: [{ caughtErrors: "all", caughtErrorsIgnorePattern: "^ignore" }],
-            errors: [{ message: "'err' is defined but never used." }]
+            errors: [{ message: "'err' is defined but never used. Allowed unused args must match /^ignore/." }]
         },
 
         // multiple try catch both fail
@@ -506,8 +532,8 @@ ruleTester.run("no-unused-vars", rule, {
             code: "try{}catch(error){}try{}catch(err){};",
             options: [{ caughtErrors: "all", caughtErrorsIgnorePattern: "^ignore" }],
             errors: [
-                { message: "'error' is defined but never used." },
-                { message: "'err' is defined but never used." }
+                { message: "'error' is defined but never used. Allowed unused args must match /^ignore/." },
+                { message: "'err' is defined but never used. Allowed unused args must match /^ignore/." }
             ]
         },
 
@@ -578,25 +604,25 @@ ruleTester.run("no-unused-vars", rule, {
         {
             code: "(function(a, b, c) {})",
             options: [{ argsIgnorePattern: "c" }],
-            errors: [{ message: "'b' is defined but never used." }]
+            errors: [{ message: "'b' is defined but never used. Allowed unused args must match /c/." }]
         },
         {
             code: "(function(a, b, {c, d}) {})",
             options: [{ argsIgnorePattern: "[cd]" }],
             parserOptions: { ecmaVersion: 6 },
-            errors: [{ message: "'b' is defined but never used." }]
+            errors: [{ message: "'b' is defined but never used. Allowed unused args must match /[cd]/." }]
         },
         {
             code: "(function(a, b, {c, d}) {})",
             options: [{ argsIgnorePattern: "c" }],
             parserOptions: { ecmaVersion: 6 },
-            errors: [{ message: "'d' is defined but never used." }]
+            errors: [{ message: "'d' is defined but never used. Allowed unused args must match /c/." }]
         },
         {
             code: "(function(a, b, {c, d}) {})",
             options: [{ argsIgnorePattern: "d" }],
             parserOptions: { ecmaVersion: 6 },
-            errors: [{ message: "'c' is defined but never used." }]
+            errors: [{ message: "'c' is defined but never used. Allowed unused args must match /d/." }]
         },
         {
             code: "/*global\rfoo*/",
diff --git a/tests/lib/util/node-event-generator.js b/tests/lib/util/node-event-generator.js
index e297b5e45..b59fabce7 100644
--- a/tests/lib/util/node-event-generator.js
+++ b/tests/lib/util/node-event-generator.js
@@ -13,7 +13,7 @@ const assert = require("assert"),
     sinon = require("sinon"),
     espree = require("espree"),
     estraverse = require("estraverse"),
-    EventGeneratorTester = require("../../../lib/testers/event-generator-tester"),
+    EventGeneratorTester = require("../../../tools/internal-testers/event-generator-tester"),
     NodeEventGenerator = require("../../../lib/util/node-event-generator");
 
 //------------------------------------------------------------------------------
diff --git a/tests/lib/util/npm-util.js b/tests/lib/util/npm-util.js
index 466d4b839..fa9a0d199 100644
--- a/tests/lib/util/npm-util.js
+++ b/tests/lib/util/npm-util.js
@@ -170,7 +170,7 @@ describe("npmUtil", () => {
 
     describe("installSyncSaveDev()", () => {
         it("should invoke npm to install a single desired package", () => {
-            const stub = sandbox.stub(spawn, "sync");
+            const stub = sandbox.stub(spawn, "sync").returns({ stdout: "" });
 
             npmUtil.installSyncSaveDev("desired-package");
             assert(stub.calledOnce);
@@ -180,7 +180,7 @@ describe("npmUtil", () => {
         });
 
         it("should accept an array of packages to install", () => {
-            const stub = sandbox.stub(spawn, "sync");
+            const stub = sandbox.stub(spawn, "sync").returns({ stdout: "" });
 
             npmUtil.installSyncSaveDev(["first-package", "second-package"]);
             assert(stub.calledOnce);
@@ -188,6 +188,18 @@ describe("npmUtil", () => {
             assert.deepEqual(stub.firstCall.args[1], ["i", "--save-dev", "first-package", "second-package"]);
             stub.restore();
         });
+
+        it("should log an error message if npm throws ENOENT error", () => {
+            const logErrorStub = sandbox.stub(log, "error");
+            const npmUtilStub = sandbox.stub(spawn, "sync").returns({ error: { code: "ENOENT" } });
+
+            npmUtil.installSyncSaveDev("some-package");
+
+            assert(logErrorStub.calledOnce);
+
+            logErrorStub.restore();
+            npmUtilStub.restore();
+        });
     });
 
     describe("fetchPeerDependencies()", () => {
@@ -200,5 +212,15 @@ describe("npmUtil", () => {
             assert.deepEqual(stub.firstCall.args[1], ["show", "--json", "desired-package", "peerDependencies"]);
             stub.restore();
         });
+
+        it("should return null if npm throws ENOENT error", () => {
+            const stub = sandbox.stub(spawn, "sync").returns({ error: { code: "ENOENT" } });
+
+            const peerDependencies = npmUtil.fetchPeerDependencies("desired-package");
+
+            assert.isNull(peerDependencies);
+
+            stub.restore();
+        });
     });
 });
diff --git a/lib/testers/event-generator-tester.js b/tools/internal-testers/event-generator-tester.js
similarity index 100%
rename from lib/testers/event-generator-tester.js
rename to tools/internal-testers/event-generator-tester.js
diff --git a/lib/testers/test-parser.js b/tools/internal-testers/test-parser.js
similarity index 95%
rename from lib/testers/test-parser.js
rename to tools/internal-testers/test-parser.js
index e7523f2e6..1ed49a1c5 100644
--- a/lib/testers/test-parser.js
+++ b/tools/internal-testers/test-parser.js
@@ -4,7 +4,7 @@
 "use strict";
 
 const espree = require("espree");
-const Traverser = require("../util/traverser");
+const Traverser = require("../../lib/util/traverser");
 
 /**
  * Define `start`/`end` properties as throwing error.