Repository: eslint/eslint
Pull request #9173 (2017-08-28 22:36:47)

Chore: avoid using private Linter APIs in astUtils tests (refs #9161)
<!--
    ESLint adheres to the [JS Foundation Code of Conduct](https://js.foundation/community/code-of-conduct).
-->

**What is the purpose of this pull request? (put an "X" next to item)**

[x] Other, please explain:

<!--
    If the item you've checked above has a template, please paste the template questions below and answer them. (If this pull request is addressing an issue, you can just paste a link to the issue here instead.)
-->

<!--
    Please ensure your pull request is ready:

    - Read the pull request guide (http://eslint.org/docs/developer-guide/contributing/pull-requests)
    - Include tests for this change
    - Update documentation for this change (if appropriate)
-->

<!--
    The following is required for all pull requests:
-->

**What changes did you make? (Give an overview)**

This updates the tests for `ast-utils` to use the public `Linter#defineRule` API rather than the private `Linter#on` API.

**Is there anything you'd like reviewers to focus on?**

Nothing in particular


eslintbot: LGTM

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/lib/linter.js b/lib/linter.js
index 02be483a2..39630d045 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"),
@@ -685,6 +685,44 @@ function parse(text, config, filePath, messages) {
     }
 }
 
+// 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
 //------------------------------------------------------------------------------
@@ -832,46 +870,82 @@ class Linter extends EventEmitter {
             // 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: parseResult && parseResult.services || {},
+                        settings: config.settings
+                    }
+                )
+            );
+
             // enable appropriate rules
-            Object.keys(config.rules).filter(key => getRuleSeverity(config.rules[key]) > 0).forEach(key => {
+            Object.keys(config.rules).filter(ruleId => getRuleSeverity(config.rules[ruleId]) > 0).forEach(ruleId => {
                 let ruleCreator;
 
-                ruleCreator = this.rules.get(key);
+                ruleCreator = this.rules.get(ruleId);
 
                 if (!ruleCreator) {
-                    const replacementMsg = getRuleReplacementMessage(key);
+                    const replacementMsg = getRuleReplacementMessage(ruleId);
 
                     if (replacementMsg) {
                         ruleCreator = createStubRule(replacementMsg);
                     } else {
-                        ruleCreator = createStubRule(`Definition for rule '${key}' was not found`);
+                        ruleCreator = createStubRule(`Definition for rule '${ruleId}' was not found`);
                     }
-                    this.rules.define(key, ruleCreator);
+                    this.rules.define(ruleId, ruleCreator);
                 }
 
-                const severity = getRuleSeverity(config.rules[key]);
-                const options = getRuleOptions(config.rules[key]);
+                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);
+                                    }
+                                }
+                            ])
+                        }
+                    )
+                );
 
                 try {
-                    const ruleContext = new RuleContext(
-                        key, this, severity, options,
-                        config.settings, config.parserOptions, config.parser,
-                        ruleCreator.meta,
-                        (parseResult && parseResult.services ? parseResult.services : {})
-                    );
-
-                    const rule = ruleCreator.create ? ruleCreator.create(ruleContext)
+                    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])
+                            ? timing.time(ruleId, rule[selector])
                             : rule[selector]
                         );
                     });
                 } catch (ex) {
-                    ex.message = `Error while loading rule '${key}': ${ex.message}`;
+                    ex.message = `Error while loading rule '${ruleId}': ${ex.message}`;
                     throw ex;
                 }
             });
@@ -933,88 +1007,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.
@@ -1217,7 +1209,7 @@ class Linter extends EventEmitter {
             messages = this.verify(text, config, options);
 
             debug(`Generating fixed text for ${debugTextDescription} (pass ${passNumber})`);
-            fixedResult = SourceCodeFixer.applyFixes(this.getSourceCode(), messages, shouldFix);
+            fixedResult = SourceCodeFixer.applyFixes(text, messages, shouldFix);
 
             // stop if there are any syntax errors.
             // 'fixedResult.output' is a empty string.
@@ -1252,33 +1244,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/indent.js b/lib/rules/indent.js
index 8872836f4..07aa1e830 100644
--- a/lib/rules/indent.js
+++ b/lib/rules/indent.js
@@ -802,10 +802,19 @@ module.exports = {
                 return;
             }
             elements.forEach((element, index) => {
+                if (!element) {
+
+                    // Skip holes in arrays
+                    return;
+                }
                 if (offset === "off") {
+
+                    // Ignore the first token of every element if the "off" option is used
                     offsets.ignoreToken(getFirstToken(element));
                 }
-                if (index === 0 || !element) {
+
+                // Offset the following elements correctly relative to the first element
+                if (index === 0) {
                     return;
                 }
                 if (offset === "first" && tokenInfo.isFirstTokenOfLine(getFirstToken(element))) {
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/source-code-fixer.js b/lib/util/source-code-fixer.js
index 1b6270a1e..ebb7e3e02 100644
--- a/lib/util/source-code-fixer.js
+++ b/lib/util/source-code-fixer.js
@@ -53,37 +53,28 @@ function SourceCodeFixer() {
 /**
  * Applies the fixes specified by the messages to the given text. Tries to be
  * smart about the fixes and won't apply fixes over the same area in the text.
- * @param {SourceCode} sourceCode The source code to apply the changes to.
+ * @param {string} sourceText The text to apply the changes to.
  * @param {Message[]} messages The array of messages reported by ESLint.
  * @param {boolean|Function} [shouldFix=true] Determines whether each message should be fixed
  * @returns {Object} An object containing the fixed text and any unfixed messages.
  */
-SourceCodeFixer.applyFixes = function(sourceCode, messages, shouldFix) {
+SourceCodeFixer.applyFixes = function(sourceText, messages, shouldFix) {
     debug("Applying fixes");
 
-    if (!sourceCode) {
-        debug("No source code to fix");
-        return {
-            fixed: false,
-            messages,
-            output: ""
-        };
-    }
-
     if (shouldFix === false) {
         debug("shouldFix parameter was false, not attempting fixes");
         return {
             fixed: false,
             messages,
-            output: sourceCode.text
+            output: sourceText
         };
     }
 
     // clone the array
     const remainingMessages = [],
         fixes = [],
-        bom = (sourceCode.hasBOM ? BOM : ""),
-        text = sourceCode.text;
+        bom = sourceText.startsWith(BOM) ? BOM : "",
+        text = bom ? sourceText.slice(1) : sourceText;
     let lastPos = Number.NEGATIVE_INFINITY,
         output = bom;
 
diff --git a/tools/eslint-fuzzer.js b/tools/eslint-fuzzer.js
index 3653673f3..ea84e994b 100644
--- a/tools/eslint-fuzzer.js
+++ b/tools/eslint-fuzzer.js
@@ -97,7 +97,7 @@ function fuzz(options) {
             }
 
             lastGoodText = currentText;
-            currentText = SourceCodeFixer.applyFixes(linter.getSourceCode(), messages).output;
+            currentText = SourceCodeFixer.applyFixes(currentText, messages).output;
         } while (lastGoodText !== currentText);
 
         return lastGoodText;


Test patch:
diff --git a/lib/testers/rule-tester.js b/lib/testers/rule-tester.js
index acd3bee7c..5b58e5452 100644
--- a/lib/testers/rule-tester.js
+++ b/lib/testers/rule-tester.js
@@ -159,7 +159,8 @@ class RuleTester {
 
             // we have to clone because merge uses the first argument for recipient
             lodash.cloneDeep(defaultConfig),
-            testerConfig
+            testerConfig,
+            { rules: { "rule-tester/validate-ast": "error" } }
         );
 
         /**
@@ -333,13 +334,14 @@ class RuleTester {
              */
             linter.reset();
 
-            linter.on("Program", node => {
-                beforeAST = cloneDeeplyExcludesParent(node);
-            });
-
-            linter.on("Program:exit", node => {
-                afterAST = node;
-            });
+            linter.defineRule("rule-tester/validate-ast", () => ({
+                Program(node) {
+                    beforeAST = cloneDeeplyExcludesParent(node);
+                },
+                "Program:exit"(node) {
+                    afterAST = node;
+                }
+            }));
 
             // Freezes rule-context properties.
             const originalGet = linter.rules.get;
@@ -519,7 +521,7 @@ class RuleTester {
                         "Expected no autofixes to be suggested"
                     );
                 } else {
-                    const fixResult = SourceCodeFixer.applyFixes(linter.getSourceCode(), messages);
+                    const fixResult = SourceCodeFixer.applyFixes(item.code, messages);
 
                     assert.equal(fixResult.output, item.output, "Output is incorrect.");
                 }
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/linter.js b/tests/lib/linter.js
index 91c5638fa..c175d3310 100644
--- a/tests/lib/linter.js
+++ b/tests/lib/linter.js
@@ -88,16 +88,17 @@ describe("Linter", () => {
         const code = TEST_CODE;
 
         it("an error should be thrown when an error occurs inside of an event handler", () => {
-            const config = { rules: {} };
+            const config = { rules: { checker: "error" } };
 
-            linter.reset();
-            linter.on("Program", () => {
-                throw new Error("Intentional error.");
-            });
+            linter.defineRule("checker", () => ({
+                Program() {
+                    throw new Error("Intentional error.");
+                }
+            }));
 
             assert.throws(() => {
                 linter.verify(code, config, filename, true);
-            }, Error);
+            }, "Intentional error.");
         });
     });
 
@@ -193,44 +194,28 @@ describe("Linter", () => {
 
         it("should retrieve all text when used without parameters", () => {
 
-            /**
-             * Callback handler
-             * @returns {void}
-             */
-            function handler() {
-                const source = linter.getSource();
-
-                assert.equal(source, TEST_CODE);
-            }
+            const config = { rules: { checker: "error" } },
+                spy = sandbox.spy(() => {
+                    const source = linter.getSource();
 
-            const config = { rules: {} },
-                spy = sandbox.spy(handler);
+                    assert.equal(source, TEST_CODE);
+                });
 
-            linter.reset();
-            linter.on("Program", spy);
+            linter.defineRule("checker", () => ({ Program: spy }));
 
             linter.verify(code, config, filename, true);
             assert(spy.calledOnce);
         });
 
         it("should retrieve all text for root node", () => {
-
-            /**
-             * Callback handler
-             * @param {ASTNode} node node to examine
-             * @returns {void}
-             */
-            function handler(node) {
+            const config = { rules: { checker: "error" } };
+            const spy = sandbox.spy(node => {
                 const source = linter.getSource(node);
 
                 assert.equal(source, TEST_CODE);
-            }
-
-            const config = { rules: {} },
-                spy = sandbox.spy(handler);
+            });
 
-            linter.reset();
-            linter.on("Program", spy);
+            linter.defineRule("checker", () => ({ Program: spy }));
 
             linter.verify(code, config, filename, true);
             assert(spy.calledOnce);
@@ -249,66 +234,70 @@ describe("Linter", () => {
                 assert.equal(source, TEST_CODE);
             }
 
-            const config = { rules: {} },
+            const config = { rules: { checker: "error" } },
                 spy = sandbox.spy(handler);
 
-            linter.reset();
-            linter.on("Program", spy);
+            linter.defineRule("checker", () => ({ Program: spy }));
 
             linter.verify(code, config, filename, true);
             assert(spy.calledOnce);
         });
 
         it("should retrieve all text for binary expression", () => {
-            const config = { rules: {} };
-
-            linter.reset();
-            linter.on("BinaryExpression", node => {
+            const spy = sandbox.spy(node => {
                 const source = linter.getSource(node);
 
                 assert.equal(source, "6 * 7");
             });
 
+            const config = { rules: { checker: "error" } };
+
+            linter.defineRule("checker", () => ({ BinaryExpression: spy }));
             linter.verify(code, config, filename, true);
+            assert(spy.calledOnce);
         });
 
         it("should retrieve all text plus two characters before for binary expression", () => {
-            const config = { rules: {} };
+            const config = { rules: { checker: "error" } };
 
-            linter.reset();
-            linter.on("BinaryExpression", node => {
+            const spy = sandbox.spy(node => {
                 const source = linter.getSource(node, 2);
 
                 assert.equal(source, "= 6 * 7");
             });
 
+            linter.defineRule("checker", () => ({ BinaryExpression: spy }));
+
             linter.verify(code, config, filename, true);
+            assert(spy.calledOnce);
         });
 
         it("should retrieve all text plus one character after for binary expression", () => {
-            const config = { rules: {} };
-
-            linter.reset();
-            linter.on("BinaryExpression", node => {
+            const config = { rules: { checker: "error" } };
+            const spy = sandbox.spy(node => {
                 const source = linter.getSource(node, 0, 1);
 
-                assert.equal(source, "6 * 7;");
+                assert.strictEqual(source, "6 * 7;");
             });
 
+            linter.defineRule("checker", () => ({ BinaryExpression: spy }));
+
             linter.verify(code, config, filename, true);
+            assert(spy.calledOnce);
         });
 
         it("should retrieve all text plus two characters before and one character after for binary expression", () => {
-            const config = { rules: {} };
-
-            linter.reset();
-            linter.on("BinaryExpression", node => {
+            const config = { rules: { checker: "error" } };
+            const spy = sandbox.spy(node => {
                 const source = linter.getSource(node, 2, 1);
 
                 assert.equal(source, "= 6 * 7;");
             });
 
+            linter.defineRule("checker", () => ({ BinaryExpression: spy }));
+
             linter.verify(code, config, filename, true);
+            assert(spy.calledOnce);
         });
 
     });
@@ -318,29 +307,31 @@ describe("Linter", () => {
 
         it("should retrieve all ancestors when used", () => {
 
-            const config = { rules: {} };
-
-            linter.reset();
-            linter.on("BinaryExpression", () => {
+            const config = { rules: { checker: "error" } };
+            const spy = sandbox.spy(() => {
                 const ancestors = linter.getAncestors();
 
                 assert.equal(ancestors.length, 3);
             });
 
+            linter.defineRule("checker", () => ({ BinaryExpression: spy }));
+
             linter.verify(code, config, filename, true);
+            assert(spy.calledOnce);
         });
 
         it("should retrieve empty ancestors for root node", () => {
-            const config = { rules: {} };
-
-            linter.reset();
-            linter.on("Program", () => {
+            const config = { rules: { checker: "error" } };
+            const spy = sandbox.spy(() => {
                 const ancestors = linter.getAncestors();
 
                 assert.equal(ancestors.length, 0);
             });
 
-            linter.verify(code, config, filename, true);
+            linter.defineRule("checker", () => ({ Program: spy }));
+
+            linter.verify(code, config);
+            assert(spy.calledOnce);
         });
     });
 
@@ -348,63 +339,65 @@ describe("Linter", () => {
         const code = TEST_CODE;
 
         it("should retrieve a node starting at the given index", () => {
-            const config = { rules: {} };
-
-            linter.reset();
-            linter.on("Program", () => {
+            const config = { rules: { checker: "error" } };
+            const spy = sandbox.spy(() => {
                 const node = linter.getNodeByRangeIndex(4);
 
                 assert.equal(node.type, "Identifier");
             });
 
-            linter.verify(code, config, filename, true);
+            linter.defineRule("checker", () => ({ Program: spy }));
+
+            linter.verify(code, config);
+            assert(spy.calledOnce);
         });
 
         it("should retrieve a node containing the given index", () => {
-            const config = { rules: {} };
-
-            linter.reset();
-            linter.on("Program", () => {
+            const config = { rules: { checker: "error" } };
+            const spy = sandbox.spy(() => {
                 const node = linter.getNodeByRangeIndex(6);
 
                 assert.equal(node.type, "Identifier");
             });
 
-            linter.verify(code, config, filename, true);
+            linter.defineRule("checker", () => ({ Program: spy }));
+
+            linter.verify(code, config);
+            assert(spy.calledOnce);
         });
 
         it("should retrieve a node that is exactly the given index", () => {
-            const config = { rules: {} };
-
-            linter.reset();
-            linter.on("Program", () => {
+            const config = { rules: { checker: "error" } };
+            const spy = sandbox.spy(() => {
                 const node = linter.getNodeByRangeIndex(13);
 
                 assert.equal(node.type, "Literal");
                 assert.equal(node.value, 6);
             });
 
-            linter.verify(code, config, filename, true);
+            linter.defineRule("checker", () => ({ Program: spy }));
+
+            linter.verify(code, config);
+            assert(spy.calledOnce);
         });
 
         it("should retrieve a node ending with the given index", () => {
-            const config = { rules: {} };
-
-            linter.reset();
-            linter.on("Program", () => {
+            const config = { rules: { checker: "error" } };
+            const spy = sandbox.spy(() => {
                 const node = linter.getNodeByRangeIndex(9);
 
                 assert.equal(node.type, "Identifier");
             });
 
-            linter.verify(code, config, filename, true);
+            linter.defineRule("checker", () => ({ Program: spy }));
+
+            linter.verify(code, config);
+            assert(spy.calledOnce);
         });
 
         it("should retrieve the deepest node containing the given index", () => {
-            const config = { rules: {} };
-
-            linter.reset();
-            linter.on("Program", () => {
+            const config = { rules: { checker: "error" } };
+            const spy = sandbox.spy(() => {
                 let node = linter.getNodeByRangeIndex(14);
 
                 assert.equal(node.type, "BinaryExpression");
@@ -412,14 +405,14 @@ describe("Linter", () => {
                 assert.equal(node.type, "VariableDeclaration");
             });
 
-            linter.verify(code, config, filename, true);
+            linter.defineRule("checker", () => ({ Program: spy }));
+            linter.verify(code, config);
+            assert(spy.calledOnce);
         });
 
         it("should return null if the index is outside the range of any node", () => {
-            const config = { rules: {} };
-
-            linter.reset();
-            linter.on("Program", () => {
+            const config = { rules: { checker: "error" } };
+            const spy = sandbox.spy(() => {
                 let node = linter.getNodeByRangeIndex(-1);
 
                 assert.isNull(node);
@@ -427,28 +420,29 @@ describe("Linter", () => {
                 assert.isNull(node);
             });
 
-            linter.verify(code, config, filename, true);
+            linter.defineRule("checker", () => ({ Program: spy }));
+            linter.verify(code, config);
+            assert(spy.calledOnce);
         });
 
         it("should attach the node's parent", () => {
-            const config = { rules: {} };
-
-            linter.reset();
-            linter.on("Program", () => {
+            const config = { rules: { checker: "error" } };
+            const spy = sandbox.spy(() => {
                 const node = linter.getNodeByRangeIndex(14);
 
                 assert.property(node, "parent");
                 assert.equal(node.parent.type, "VariableDeclarator");
             });
 
-            linter.verify(code, config, filename, true);
+            linter.defineRule("checker", () => ({ Program: spy }));
+
+            linter.verify(code, config);
+            assert(spy.calledOnce);
         });
 
         it("should not modify the node when attaching the parent", () => {
-            const config = { rules: {} };
-
-            linter.reset();
-            linter.on("Program", () => {
+            const config = { rules: { checker: "error" } };
+            const spy = sandbox.spy(() => {
                 let node = linter.getNodeByRangeIndex(10);
 
                 assert.equal(node.type, "VariableDeclarator");
@@ -459,7 +453,10 @@ describe("Linter", () => {
                 assert.notProperty(node.parent, "parent");
             });
 
-            linter.verify(code, config, filename, true);
+            linter.defineRule("checker", () => ({ Program: spy }));
+
+            linter.verify(code, config);
+            assert(spy.calledOnce);
         });
 
     });
@@ -469,676 +466,275 @@ describe("Linter", () => {
         const code = "function foo() { q: for(;;) { break q; } } function bar () { var q = t; } var baz = (() => { return 1; });";
 
         it("should retrieve the global scope correctly from a Program", () => {
-            const config = { rules: {} };
-
-            linter.reset();
-            linter.on("Program", () => {
+            const config = { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6 } };
+            const spy = sandbox.spy(() => {
                 const scope = linter.getScope();
 
                 assert.equal(scope.type, "global");
             });
 
-            linter.verify(code, config, filename, true);
+            linter.defineRule("checker", () => ({ Program: spy }));
+
+            linter.verify(code, config);
+            assert(spy.calledOnce);
         });
 
         it("should retrieve the function scope correctly from a FunctionDeclaration", () => {
-            const config = { rules: {} };
-
-            linter.reset();
-            linter.on("FunctionDeclaration", () => {
+            const config = { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6 } };
+            const spy = sandbox.spy(() => {
                 const scope = linter.getScope();
 
                 assert.equal(scope.type, "function");
             });
 
-            linter.verify(code, config, filename, true);
+            linter.defineRule("checker", () => ({ FunctionDeclaration: spy }));
+
+            linter.verify(code, config);
+            assert(spy.calledTwice);
         });
 
         it("should retrieve the function scope correctly from a LabeledStatement", () => {
-            const config = { rules: {} };
-
-            linter.reset();
-            linter.on("LabeledStatement", () => {
+            const config = { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6 } };
+            const spy = sandbox.spy(() => {
                 const scope = linter.getScope();
 
                 assert.equal(scope.type, "function");
                 assert.equal(scope.block.id.name, "foo");
             });
 
-            linter.verify(code, config, filename, true);
+            linter.defineRule("checker", () => ({ LabeledStatement: spy }));
+
+            linter.verify(code, config);
+            assert(spy.calledOnce);
         });
 
         it("should retrieve the function scope correctly from within an ArrowFunctionExpression", () => {
-            const config = { rules: {}, ecmaFeatures: { arrowFunctions: true } };
-
-            linter.reset();
-            linter.on("ReturnStatement", () => {
+            const config = { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6 } };
+            const spy = sandbox.spy(() => {
                 const scope = linter.getScope();
 
                 assert.equal(scope.type, "function");
                 assert.equal(scope.block.type, "ArrowFunctionExpression");
             });
 
-            linter.verify(code, config, filename, true);
+            linter.defineRule("checker", () => ({ ReturnStatement: spy }));
+
+            linter.verify(code, config);
+            assert(spy.calledOnce);
         });
 
         it("should retrieve the function scope correctly from within an SwitchStatement", () => {
-            const config = { rules: {}, parserOptions: { ecmaVersion: 6 } };
-
-            linter.reset();
-            linter.on("SwitchStatement", () => {
+            const config = { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6 } };
+            const spy = sandbox.spy(() => {
                 const scope = linter.getScope();
 
                 assert.equal(scope.type, "switch");
                 assert.equal(scope.block.type, "SwitchStatement");
             });
 
-            linter.verify("switch(foo){ case 'a': var b = 'foo'; }", config, filename, true);
+            linter.defineRule("checker", () => ({ SwitchStatement: spy }));
+
+            linter.verify("switch(foo){ case 'a': var b = 'foo'; }", config);
+            assert(spy.calledOnce);
         });
 
         it("should retrieve the function scope correctly from within a BlockStatement", () => {
-            const config = { rules: {}, parserOptions: { ecmaVersion: 6 } };
-
-            linter.reset();
-            linter.on("BlockStatement", () => {
+            const config = { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6 } };
+            const spy = sandbox.spy(() => {
                 const scope = linter.getScope();
 
                 assert.equal(scope.type, "block");
                 assert.equal(scope.block.type, "BlockStatement");
             });
 
-            linter.verify("var x; {let y = 1}", config, filename, true);
+            linter.defineRule("checker", () => ({ BlockStatement: spy }));
+
+            linter.verify("var x; {let y = 1}", config);
+            assert(spy.calledOnce);
         });
 
         it("should retrieve the function scope correctly from within a nested block statement", () => {
-            const config = { rules: {}, parserOptions: { ecmaVersion: 6 } };
-
-            linter.reset();
-            linter.on("BlockStatement", () => {
+            const config = { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6 } };
+            const spy = sandbox.spy(() => {
                 const scope = linter.getScope();
 
                 assert.equal(scope.type, "block");
                 assert.equal(scope.block.type, "BlockStatement");
             });
 
-            linter.verify("if (true) { let x = 1 }", config, filename, true);
+            linter.defineRule("checker", () => ({ BlockStatement: spy }));
+            linter.verify("if (true) { let x = 1 }", config);
+            assert(spy.calledOnce);
         });
 
         it("should retrieve the function scope correctly from within a FunctionDeclaration", () => {
-            const config = { rules: {}, parserOptions: { ecmaVersion: 6 } };
-
-            linter.reset();
-            linter.on("FunctionDeclaration", () => {
+            const config = { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6 } };
+            const spy = sandbox.spy(() => {
                 const scope = linter.getScope();
 
-                assert.equal(scope.type, "function");
-                assert.equal(scope.block.type, "FunctionDeclaration");
-            });
-
-            linter.verify("function foo() {}", config, filename, true);
-        });
-
-        it("should retrieve the function scope correctly from within a FunctionExpression", () => {
-            const config = { rules: {}, parserOptions: { ecmaVersion: 6 } };
-
-            linter.reset();
-            linter.on("FunctionExpression", () => {
-                const scope = linter.getScope();
-
-                assert.equal(scope.type, "function");
-                assert.equal(scope.block.type, "FunctionExpression");
-            });
-
-            linter.verify("(function foo() {})();", config, filename, true);
-        });
-
-        it("should retrieve the catch scope correctly from within a CatchClause", () => {
-            const config = { rules: {}, parserOptions: { ecmaVersion: 6 } };
-
-            linter.reset();
-            linter.on("CatchClause", () => {
-                const scope = linter.getScope();
-
-                assert.equal(scope.type, "catch");
-                assert.equal(scope.block.type, "CatchClause");
-            });
-
-            linter.verify("try {} catch (err) {}", config, filename, true);
-        });
-
-        it("should retrieve module scope correctly from an ES6 module", () => {
-            const config = { rules: {}, parserOptions: { sourceType: "module" } };
-
-            linter.reset();
-            linter.on("AssignmentExpression", () => {
-                const scope = linter.getScope();
-
-                assert.equal(scope.type, "module");
-            });
-
-            linter.verify("var foo = {}; foo.bar = 1;", config, filename, true);
-        });
-
-        it("should retrieve function scope correctly when globalReturn is true", () => {
-            const config = { rules: {}, parserOptions: { ecmaFeatures: { globalReturn: true } } };
-
-            linter.reset();
-            linter.on("AssignmentExpression", () => {
-                const scope = linter.getScope();
-
-                assert.equal(scope.type, "function");
-            });
-
-            linter.verify("var foo = {}; foo.bar = 1;", config, filename, true);
-        });
-    });
-
-    describe("marking variables as used", () => {
-        it("should mark variables in current scope as used", () => {
-            const code = "var a = 1, b = 2;";
-
-            linter.reset();
-            linter.on("Program:exit", () => {
-                assert.isTrue(linter.markVariableAsUsed("a"));
-
-                const scope = linter.getScope();
-
-                assert.isTrue(getVariable(scope, "a").eslintUsed);
-                assert.notOk(getVariable(scope, "b").eslintUsed);
-            });
-
-            linter.verify(code, {}, filename, true);
-        });
-        it("should mark variables in function args as used", () => {
-            const code = "function abc(a, b) { return 1; }";
-
-            linter.reset();
-            linter.on("ReturnStatement", () => {
-                assert.isTrue(linter.markVariableAsUsed("a"));
-
-                const scope = linter.getScope();
-
-                assert.isTrue(getVariable(scope, "a").eslintUsed);
-                assert.notOk(getVariable(scope, "b").eslintUsed);
-            });
-
-            linter.verify(code, {}, filename, true);
-        });
-        it("should mark variables in higher scopes as used", () => {
-            const code = "var a, b; function abc() { return 1; }";
-
-            linter.reset();
-            linter.on("ReturnStatement", () => {
-                assert.isTrue(linter.markVariableAsUsed("a"));
-            });
-            linter.on("Program:exit", () => {
-                const scope = linter.getScope();
-
-                assert.isTrue(getVariable(scope, "a").eslintUsed);
-                assert.notOk(getVariable(scope, "b").eslintUsed);
-            });
-
-            linter.verify(code, {}, filename, true);
-        });
-
-        it("should mark variables in Node.js environment as used", () => {
-            const code = "var a = 1, b = 2;";
-
-            linter.reset();
-            linter.on("Program:exit", () => {
-                const globalScope = linter.getScope(),
-                    childScope = globalScope.childScopes[0];
-
-                assert.isTrue(linter.markVariableAsUsed("a"));
-
-                assert.isTrue(getVariable(childScope, "a").eslintUsed);
-                assert.isUndefined(getVariable(childScope, "b").eslintUsed);
-            });
-
-            linter.verify(code, { env: { node: true } }, filename, true);
-        });
-
-        it("should mark variables in modules as used", () => {
-            const code = "var a = 1, b = 2;";
-
-            linter.reset();
-            linter.on("Program:exit", () => {
-                const globalScope = linter.getScope(),
-                    childScope = globalScope.childScopes[0];
-
-                assert.isTrue(linter.markVariableAsUsed("a"));
-
-                assert.isTrue(getVariable(childScope, "a").eslintUsed);
-                assert.isUndefined(getVariable(childScope, "b").eslintUsed);
-            });
-
-            linter.verify(code, { parserOptions: { sourceType: "module" } }, filename, true);
-        });
-
-        it("should return false if the given variable is not found", () => {
-            const code = "var a = 1, b = 2;";
-
-            linter.reset();
-            linter.on("Program:exit", () => {
-                assert.isFalse(linter.markVariableAsUsed("c"));
-            });
-
-            linter.verify(code, {}, filename, true);
-        });
-    });
-
-    describe("report()", () => {
-
-        let config;
-
-        beforeEach(() => {
-            config = { rules: {} };
-        });
-
-        it("should correctly parse a message when being passed all options", () => {
-            linter.on("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.on("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.on("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.on("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.on("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.on("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.on("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.on("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.on("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.on("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.on("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.reset();
-            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.reset();
-            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.reset();
-            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.reset();
-            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.reset();
-            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(scope.type, "function");
+                assert.equal(scope.block.type, "FunctionDeclaration");
+            });
 
-            assert.equal(messages[0].message, "message {{parameter}}");
+            linter.defineRule("checker", () => ({ FunctionDeclaration: spy }));
+            linter.verify("function foo() {}", config);
+            assert(spy.calledOnce);
         });
 
-        it("should ignore template parameter with no specified value with warn severity", () => {
-            linter.reset();
-            linter.defineRule("test-rule", context => ({
-                Literal(node) {
-                    context.report(node, "message {{parameter}}", {});
-                }
-            }));
-
-            config.rules["test-rule"] = "warn";
+        it("should retrieve the function scope correctly from within a FunctionExpression", () => {
+            const config = { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6 } };
+            const spy = sandbox.spy(() => {
+                const scope = linter.getScope();
 
-            const messages = linter.verify("0", config);
+                assert.equal(scope.type, "function");
+                assert.equal(scope.block.type, "FunctionExpression");
+            });
 
-            assert.equal(messages[0].severity, 1);
-            assert.equal(messages[0].message, "message {{parameter}}");
+            linter.defineRule("checker", () => ({ FunctionExpression: spy }));
+            linter.verify("(function foo() {})();", config);
+            assert(spy.calledOnce);
         });
 
-        it("should handle leading whitespace in template parameter", () => {
-            linter.reset();
-            linter.defineRule("test-rule", context => ({
-                Literal(node) {
-                    context.report(node, "message {{ parameter}}", {
-                        parameter: "yay!"
-                    });
-                }
-            }));
-
-            config.rules["test-rule"] = 1;
+        it("should retrieve the catch scope correctly from within a CatchClause", () => {
+            const config = { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6 } };
+            const spy = sandbox.spy(() => {
+                const scope = linter.getScope();
 
-            const messages = linter.verify("0", config);
+                assert.equal(scope.type, "catch");
+                assert.equal(scope.block.type, "CatchClause");
+            });
 
-            assert.equal(messages[0].message, "message yay!");
+            linter.defineRule("checker", () => ({ CatchClause: spy }));
+            linter.verify("try {} catch (err) {}", config);
+            assert(spy.calledOnce);
         });
 
-        it("should handle trailing whitespace in template parameter", () => {
-            linter.reset();
-            linter.defineRule("test-rule", context => ({
-                Literal(node) {
-                    context.report(node, "message {{parameter }}", {
-                        parameter: "yay!"
-                    });
-                }
-            }));
+        it("should retrieve module scope correctly from an ES6 module", () => {
+            const config = { rules: { checker: "error" }, parserOptions: { sourceType: "module" } };
+            const spy = sandbox.spy(() => {
+                const scope = linter.getScope();
 
-            config.rules["test-rule"] = 1;
+                assert.equal(scope.type, "module");
+            });
 
-            const messages = linter.verify("0", config);
+            linter.defineRule("checker", () => ({ AssignmentExpression: spy }));
 
-            assert.equal(messages[0].message, "message yay!");
+            linter.verify("var foo = {}; foo.bar = 1;", config);
+            assert(spy.calledOnce);
         });
 
-        it("should still allow inner whitespace as well as leading/trailing", () => {
-            linter.reset();
-            linter.defineRule("test-rule", context => ({
-                Literal(node) {
-                    context.report(node, "message {{ parameter name }}", {
-                        "parameter name": "yay!"
-                    });
-                }
-            }));
+        it("should retrieve function scope correctly when globalReturn is true", () => {
+            const config = { rules: { checker: "error" }, parserOptions: { ecmaFeatures: { globalReturn: true } } };
+            const spy = sandbox.spy(() => {
+                const scope = linter.getScope();
 
-            config.rules["test-rule"] = 1;
+                assert.equal(scope.type, "function");
+            });
 
-            const messages = linter.verify("0", config);
+            linter.defineRule("checker", () => ({ AssignmentExpression: spy }));
 
-            assert.equal(messages[0].message, "message yay!");
+            linter.verify("var foo = {}; foo.bar = 1;", config);
+            assert(spy.calledOnce);
         });
+    });
 
-        it("should still allow non-identifier characters as well as leading/trailing whitespace", () => {
-            linter.reset();
-            linter.defineRule("test-rule", context => ({
-                Literal(node) {
-                    context.report(node, "message {{ parameter-name }}", {
-                        "parameter-name": "yay!"
-                    });
-                }
-            }));
+    describe("marking variables as used", () => {
+        it("should mark variables in current scope as used", () => {
+            const code = "var a = 1, b = 2;";
+            const spy = sandbox.spy(() => {
+                assert.isTrue(linter.markVariableAsUsed("a"));
 
-            config.rules["test-rule"] = 1;
+                const scope = linter.getScope();
 
-            const messages = linter.verify("0", config);
+                assert.isTrue(getVariable(scope, "a").eslintUsed);
+                assert.notOk(getVariable(scope, "b").eslintUsed);
+            });
 
-            assert.equal(messages[0].message, "message yay!");
+            linter.defineRule("checker", () => ({ "Program:exit": spy }));
+            linter.verify(code, { rules: { checker: "error" } });
+            assert(spy.calledOnce);
         });
+        it("should mark variables in function args as used", () => {
+            const code = "function abc(a, b) { return 1; }";
+            const spy = sandbox.spy(() => {
+                assert.isTrue(linter.markVariableAsUsed("a"));
 
-        it("should include a fix passed as the last argument when location is passed", () => {
-            linter.on("Program", node => {
-                linter.report("test-rule", 2, node, { line: 42, column: 23 }, "my message {{1}}{{0}}", ["!", "testing"], { range: [1, 1], text: "" });
+                const scope = linter.getScope();
+
+                assert.isTrue(getVariable(scope, "a").eslintUsed);
+                assert.notOk(getVariable(scope, "b").eslintUsed);
             });
 
-            const messages = linter.verify("0", config, "", true);
+            linter.defineRule("checker", () => ({ ReturnStatement: spy }));
 
-            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.on("Program", node => {
-                linter.report(
-                    "test-rule",
-                    2,
-                    node,
-                    "test"
-                );
+            linter.verify(code, { rules: { checker: "error" } });
+            assert(spy.calledOnce);
+        });
+        it("should mark variables in higher scopes as used", () => {
+            const code = "var a, b; function abc() { return 1; }";
+            const returnSpy = sandbox.spy(() => {
+                assert.isTrue(linter.markVariableAsUsed("a"));
             });
+            const exitSpy = sandbox.spy(() => {
+                const scope = linter.getScope();
 
-            const sourceText = "foo + bar;";
+                assert.isTrue(getVariable(scope, "a").eslintUsed);
+                assert.notOk(getVariable(scope, "b").eslintUsed);
+            });
 
-            const messages = linter.verify(sourceText, config, "", true);
+            linter.defineRule("checker", () => ({ ReturnStatement: returnSpy, "Program:exit": exitSpy }));
 
-            assert.strictEqual(messages[0].endLine, 1);
-            assert.strictEqual(messages[0].endColumn, sourceText.length + 1); // (1-based column)
+            linter.verify(code, { rules: { checker: "error" } });
+            assert(returnSpy.calledOnce);
+            assert(exitSpy.calledOnce);
         });
 
-        it("should have 'endLine' and 'endColumn' when 'loc' property has 'end' property.", () => {
-            linter.on("Program", node => {
-                linter.report(
-                    "test-rule",
-                    2,
-                    node,
-                    node.loc,
-                    "test"
-                );
+        it("should mark variables in Node.js environment as used", () => {
+            const code = "var a = 1, b = 2;";
+            const spy = sandbox.spy(() => {
+                const globalScope = linter.getScope(),
+                    childScope = globalScope.childScopes[0];
+
+                assert.isTrue(linter.markVariableAsUsed("a"));
+
+                assert.isTrue(getVariable(childScope, "a").eslintUsed);
+                assert.isUndefined(getVariable(childScope, "b").eslintUsed);
             });
 
-            const messages = linter.verify("0", config, "", true);
+            linter.defineRule("checker", () => ({ "Program:exit": spy }));
 
-            assert.strictEqual(messages[0].endLine, 1);
-            assert.strictEqual(messages[0].endColumn, 2);
+            linter.verify(code, { rules: { checker: "error" }, env: { node: true } });
+            assert(spy.calledOnce);
         });
 
-        it("should not have 'endLine' and 'endColumn' when 'loc' property does not have 'end' property.", () => {
-            linter.on("Program", node => {
-                linter.report(
-                    "test-rule",
-                    2,
-                    node,
-                    node.loc.start,
-                    "test"
-                );
-            });
+        it("should mark variables in modules as used", () => {
+            const code = "var a = 1, b = 2;";
+            const spy = sandbox.spy(() => {
+                const globalScope = linter.getScope(),
+                    childScope = globalScope.childScopes[0];
+
+                assert.isTrue(linter.markVariableAsUsed("a"));
 
-            const messages = linter.verify("0", config, "", true);
+                assert.isTrue(getVariable(childScope, "a").eslintUsed);
+                assert.isUndefined(getVariable(childScope, "b").eslintUsed);
+            });
 
-            assert.strictEqual(messages[0].endLine, void 0);
-            assert.strictEqual(messages[0].endColumn, void 0);
+            linter.defineRule("checker", () => ({ "Program:exit": spy }));
+            linter.verify(code, { rules: { checker: "error" }, parserOptions: { sourceType: "module" } }, filename, true);
+            assert(spy.calledOnce);
         });
 
-        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" } });
+        it("should return false if the given variable is not found", () => {
+            const code = "var a = 1, b = 2;";
+            const spy = sandbox.spy(() => {
+                assert.isFalse(linter.markVariableAsUsed("c"));
+            });
+
+            linter.defineRule("checker", () => ({ "Program:exit": spy }));
 
-            assert.strictEqual(messages.length, 1);
-            assert.strictEqual(messages[0].endLine, 1);
-            assert.strictEqual(messages[0].endColumn, 4);
+            linter.verify(code, { rules: { checker: "error" } });
+            assert(spy.calledOnce);
         });
     });
 
@@ -1146,7 +742,7 @@ describe("Linter", () => {
         const code = TEST_CODE;
 
         it("events for each node type should fire", () => {
-            const config = { rules: {} };
+            const config = { rules: { checker: "error" } };
 
             // spies for various AST node types
             const spyLiteral = sinon.spy(),
@@ -1155,12 +751,13 @@ describe("Linter", () => {
                 spyIdentifier = sinon.spy(),
                 spyBinaryExpression = sinon.spy();
 
-            linter.reset();
-            linter.on("Literal", spyLiteral);
-            linter.on("VariableDeclarator", spyVariableDeclarator);
-            linter.on("VariableDeclaration", spyVariableDeclaration);
-            linter.on("Identifier", spyIdentifier);
-            linter.on("BinaryExpression", spyBinaryExpression);
+            linter.defineRule("checker", () => ({
+                Literal: spyLiteral,
+                VariableDeclarator: spyVariableDeclarator,
+                VariableDeclaration: spyVariableDeclaration,
+                Identifier: spyIdentifier,
+                BinaryExpression: spyBinaryExpression
+            }));
 
             const messages = linter.verify(code, config, filename, true);
 
@@ -1402,35 +999,6 @@ describe("Linter", () => {
     describe("after calling reset()", () => {
         const code = TEST_CODE;
 
-        it("previously registered event handlers should not be called", () => {
-
-            const config = { rules: {} };
-
-            // spies for various AST node types
-            const spyLiteral = sinon.spy(),
-                spyVariableDeclarator = sinon.spy(),
-                spyVariableDeclaration = sinon.spy(),
-                spyIdentifier = sinon.spy(),
-                spyBinaryExpression = sinon.spy();
-
-            linter.reset();
-            linter.on("Literal", spyLiteral);
-            linter.on("VariableDeclarator", spyVariableDeclarator);
-            linter.on("VariableDeclaration", spyVariableDeclaration);
-            linter.on("Identifier", spyIdentifier);
-            linter.on("BinaryExpression", spyBinaryExpression);
-            linter.reset();
-
-            const messages = linter.verify(code, config, filename, true);
-
-            assert.equal(messages.length, 0);
-            sinon.assert.notCalled(spyVariableDeclaration);
-            sinon.assert.notCalled(spyVariableDeclarator);
-            sinon.assert.notCalled(spyIdentifier);
-            sinon.assert.notCalled(spyLiteral);
-            sinon.assert.notCalled(spyBinaryExpression);
-        });
-
         it("text should not be available", () => {
             const config = { rules: {} };
 
@@ -1460,10 +1028,8 @@ describe("Linter", () => {
         const code = "/*global a b:true c:false*/ function foo() {} /*globals d:true*/";
 
         it("variables should be available in global scope", () => {
-            const config = { rules: {} };
-
-            linter.reset();
-            linter.on("Program", () => {
+            const config = { rules: { checker: "error" } };
+            const spy = sandbox.spy(() => {
                 const scope = linter.getScope();
                 const a = getVariable(scope, "a"),
                     b = getVariable(scope, "b"),
@@ -1480,7 +1046,9 @@ describe("Linter", () => {
                 assert.equal(d.writeable, true);
             });
 
-            linter.verify(code, config, filename, true);
+            linter.defineRule("checker", () => ({ Program: spy }));
+            linter.verify(code, config);
+            assert(spy.calledOnce);
         });
     });
 
@@ -1488,10 +1056,8 @@ describe("Linter", () => {
         const code = "/* global  a b  : true   c:  false*/";
 
         it("variables should be available in global scope", () => {
-            const config = { rules: {} };
-
-            linter.reset();
-            linter.on("Program", () => {
+            const config = { rules: { checker: "error" } };
+            const spy = sandbox.spy(() => {
                 const scope = linter.getScope(),
                     a = getVariable(scope, "a"),
                     b = getVariable(scope, "b"),
@@ -1505,17 +1071,17 @@ describe("Linter", () => {
                 assert.equal(c.writeable, false);
             });
 
-            linter.verify(code, config, filename, true);
+            linter.defineRule("checker", () => ({ Program: spy }));
+            linter.verify(code, config);
+            assert(spy.calledOnce);
         });
     });
 
     describe("when evaluating code containing /*eslint-env */ block", () => {
         it("variables should be available in global scope", () => {
             const code = "/*eslint-env node*/ function f() {} /*eslint-env browser, foo*/";
-            const config = { rules: {} };
-
-            linter.reset();
-            linter.on("Program", () => {
+            const config = { rules: { checker: "error" } };
+            const spy = sandbox.spy(() => {
                 const scope = linter.getScope(),
                     exports = getVariable(scope, "exports"),
                     window = getVariable(scope, "window");
@@ -1523,7 +1089,10 @@ describe("Linter", () => {
                 assert.equal(exports.writeable, true);
                 assert.equal(window.writeable, false);
             });
-            linter.verify(code, config, filename, true);
+
+            linter.defineRule("checker", () => ({ Program: spy }));
+            linter.verify(code, config);
+            assert(spy.calledOnce);
         });
     });
 
@@ -1531,10 +1100,8 @@ describe("Linter", () => {
         const code = "/* eslint-env ,, node  , no-browser ,,  */";
 
         it("variables should be available in global scope", () => {
-            const config = { rules: {} };
-
-            linter.reset();
-            linter.on("Program", () => {
+            const config = { rules: { checker: "error" } };
+            const spy = sandbox.spy(() => {
                 const scope = linter.getScope(),
                     exports = getVariable(scope, "exports"),
                     window = getVariable(scope, "window");
@@ -1542,7 +1109,10 @@ describe("Linter", () => {
                 assert.equal(exports.writeable, true);
                 assert.equal(window, null);
             });
-            linter.verify(code, config, filename, true);
+
+            linter.defineRule("checker", () => ({ Program: spy }));
+            linter.verify(code, config);
+            assert(spy.calledOnce);
         });
     });
 
@@ -1552,78 +1122,83 @@ describe("Linter", () => {
             const code = "/* exported horse */";
             const config = { rules: {} };
 
-            linter.reset();
             linter.verify(code, config, filename, true);
         });
 
         it("variables should be exported", () => {
             const code = "/* exported horse */\n\nvar horse = 'circus'";
-            const config = { rules: {} };
-
-            linter.reset();
-            linter.on("Program", () => {
+            const config = { rules: { checker: "error" } };
+            const spy = sandbox.spy(() => {
                 const scope = linter.getScope(),
                     horse = getVariable(scope, "horse");
 
                 assert.equal(horse.eslintUsed, true);
             });
-            linter.verify(code, config, filename, true);
+
+            linter.defineRule("checker", () => ({ Program: spy }));
+
+            linter.verify(code, config);
+            assert(spy.calledOnce);
         });
 
         it("undefined variables should not be exported", () => {
             const code = "/* exported horse */\n\nhorse = 'circus'";
-            const config = { rules: {} };
-
-            linter.reset();
-            linter.on("Program", () => {
+            const config = { rules: { checker: "error" } };
+            const spy = sandbox.spy(() => {
                 const scope = linter.getScope(),
                     horse = getVariable(scope, "horse");
 
                 assert.equal(horse, null);
             });
-            linter.verify(code, config, filename, true);
+
+            linter.defineRule("checker", () => ({ Program: spy }));
+            linter.verify(code, config);
+            assert(spy.calledOnce);
         });
 
         it("variables should be exported in strict mode", () => {
             const code = "/* exported horse */\n'use strict';\nvar horse = 'circus'";
-            const config = { rules: {} };
-
-            linter.reset();
-            linter.on("Program", () => {
+            const config = { rules: { checker: "error" } };
+            const spy = sandbox.spy(() => {
                 const scope = linter.getScope(),
                     horse = getVariable(scope, "horse");
 
                 assert.equal(horse.eslintUsed, true);
             });
-            linter.verify(code, config, filename, true);
+
+            linter.defineRule("checker", () => ({ Program: spy }));
+            linter.verify(code, config);
+            assert(spy.calledOnce);
         });
 
         it("variables should not be exported in the es6 module environment", () => {
             const code = "/* exported horse */\nvar horse = 'circus'";
-            const config = { rules: {}, parserOptions: { sourceType: "module" } };
-
-            linter.reset();
-            linter.on("Program", () => {
+            const config = { rules: { checker: "error" }, parserOptions: { sourceType: "module" } };
+            const spy = sandbox.spy(() => {
                 const scope = linter.getScope(),
                     horse = getVariable(scope, "horse");
 
                 assert.equal(horse, null); // there is no global scope at all
             });
-            linter.verify(code, config, filename, true);
+
+            linter.defineRule("checker", () => ({ Program: spy }));
+            linter.verify(code, config);
+            assert(spy.calledOnce);
         });
 
         it("variables should not be exported when in the node environment", () => {
             const code = "/* exported horse */\nvar horse = 'circus'";
-            const config = { rules: {}, env: { node: true } };
-
-            linter.reset();
-            linter.on("Program", () => {
+            const config = { rules: { checker: "error" }, env: { node: true } };
+            const spy = sandbox.spy(() => {
                 const scope = linter.getScope(),
                     horse = getVariable(scope, "horse");
 
                 assert.equal(horse, null); // there is no global scope at all
             });
-            linter.verify(code, config, filename, true);
+
+            linter.defineRule("checker", () => ({ Program: spy }));
+            linter.verify(code, config);
+            assert(spy.calledOnce);
         });
     });
 
@@ -1631,16 +1206,16 @@ describe("Linter", () => {
         const code = "//global a \n function f() {}";
 
         it("should not introduce a global variable", () => {
-
-            const config = { rules: {} };
-
-            linter.reset();
-            linter.on("Program", () => {
+            const config = { rules: { checker: "error" } };
+            const spy = sandbox.spy(() => {
                 const scope = linter.getScope();
 
                 assert.equal(getVariable(scope, "a"), null);
             });
-            linter.verify(code, config, filename, true);
+
+            linter.defineRule("checker", () => ({ Program: spy }));
+            linter.verify(code, config);
+            assert(spy.calledOnce);
         });
     });
 
@@ -1648,10 +1223,8 @@ describe("Linter", () => {
         const code = "/**/  /*a*/  /*b:true*/  /*foo c:false*/";
 
         it("should not introduce a global variable", () => {
-            const config = { rules: {} };
-
-            linter.reset();
-            linter.on("Program", () => {
+            const config = { rules: { checker: "error" } };
+            const spy = sandbox.spy(() => {
                 const scope = linter.getScope();
 
                 assert.equal(getVariable(scope, "a"), null);
@@ -1659,53 +1232,59 @@ describe("Linter", () => {
                 assert.equal(getVariable(scope, "foo"), null);
                 assert.equal(getVariable(scope, "c"), null);
             });
-            linter.verify(code, config, filename, true);
+
+            linter.defineRule("checker", () => ({ Program: spy }));
+            linter.verify(code, config);
+            assert(spy.calledOnce);
         });
     });
 
     describe("when evaluating any code", () => {
-        const code = "";
+        const code = "x";
 
         it("builtin global variables should be available in the global scope", () => {
-            const config = { rules: {} };
-
-            linter.reset();
-            linter.on("Program", () => {
+            const config = { rules: { checker: "error" } };
+            const spy = sandbox.spy(() => {
                 const scope = linter.getScope();
 
                 assert.notEqual(getVariable(scope, "Object"), null);
                 assert.notEqual(getVariable(scope, "Array"), null);
                 assert.notEqual(getVariable(scope, "undefined"), null);
             });
-            linter.verify(code, config, filename, true);
+
+            linter.defineRule("checker", () => ({ Program: spy }));
+            linter.verify(code, config);
+            assert(spy.calledOnce);
         });
 
         it("ES6 global variables should not be available by default", () => {
-            const config = { rules: {} };
-
-            linter.reset();
-            linter.on("Program", () => {
+            const config = { rules: { checker: "error" } };
+            const spy = sandbox.spy(() => {
                 const scope = linter.getScope();
 
                 assert.equal(getVariable(scope, "Promise"), null);
                 assert.equal(getVariable(scope, "Symbol"), null);
                 assert.equal(getVariable(scope, "WeakMap"), null);
             });
-            linter.verify(code, config, filename, true);
+
+            linter.defineRule("checker", () => ({ Program: spy }));
+            linter.verify(code, config);
+            assert(spy.calledOnce);
         });
 
         it("ES6 global variables should be available in the es6 environment", () => {
-            const config = { rules: {} };
-
-            linter.reset();
-            linter.on("Program", () => {
+            const config = { rules: { checker: "error" }, env: { es6: true } };
+            const spy = sandbox.spy(() => {
                 const scope = linter.getScope();
 
                 assert.notEqual(getVariable(scope, "Promise"), null);
                 assert.notEqual(getVariable(scope, "Symbol"), null);
                 assert.notEqual(getVariable(scope, "WeakMap"), null);
             });
-            linter.verify(code, config, filename, true);
+
+            linter.defineRule("checker", () => ({ Program: spy }));
+            linter.verify(code, config);
+            assert(spy.calledOnce);
         });
     });
 
@@ -2631,17 +2210,17 @@ describe("Linter", () => {
         });
 
         it("should have a comment with the shebang in it", () => {
-            const config = { rules: { "no-extra-semi": 1 } };
-
-            linter.reset();
-
-            linter.on("Program", () => {
+            const config = { rules: { checker: "error" } };
+            const spy = sandbox.spy(() => {
                 const comments = linter.getAllComments();
 
                 assert.equal(comments.length, 1);
                 assert.equal(comments[0].type, "Shebang");
             });
-            linter.verify(code, config, "foo.js", true);
+
+            linter.defineRule("checker", () => ({ Program: spy }));
+            linter.verify(code, config);
+            assert(spy.calledOnce);
         });
     });
 
@@ -2725,7 +2304,7 @@ describe("Linter", () => {
                     column: 1,
                     severity: 1,
                     source: "var answer = 6 * 7;",
-                    nodeType: void 0
+                    nodeType: null
                 }
             );
         });
@@ -3065,17 +2644,15 @@ describe("Linter", () => {
 
     describe("when evaluating code with hashbang", () => {
         it("should comment hashbang without breaking offset", () => {
-
             const code = "#!/usr/bin/env node\n'123';";
-
-            const config = { rules: {} };
-
-            linter.reset();
-            linter.on("ExpressionStatement", node => {
+            const config = { rules: { checker: "error" } };
+            const spy = sandbox.spy(node => {
                 assert.equal(linter.getSource(node), "'123';");
             });
 
-            linter.verify(code, config, filename, true);
+            linter.defineRule("checker", () => ({ ExpressionStatement: spy }));
+            linter.verify(code, config);
+            assert(spy.calledOnce);
         });
     });
 
@@ -3427,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", () => {
@@ -3858,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/indent.js b/tests/lib/rules/indent.js
index 7ea3c1890..1339c16f6 100644
--- a/tests/lib/rules/indent.js
+++ b/tests/lib/rules/indent.js
@@ -3222,6 +3222,10 @@ ruleTester.run("indent", rule, {
             code: "[,]",
             options: [2, { ArrayExpression: "first" }]
         },
+        {
+            code: "[,]",
+            options: [2, { ArrayExpression: "off" }]
+        },
         {
             code: unIndent`
                 [
diff --git a/tests/lib/util/source-code-fixer.js b/tests/lib/util/source-code-fixer.js
index 1ce5adc25..758baf178 100644
--- a/tests/lib/util/source-code-fixer.js
+++ b/tests/lib/util/source-code-fixer.js
@@ -9,17 +9,14 @@
 //------------------------------------------------------------------------------
 
 const assert = require("chai").assert,
-    espree = require("espree"),
     sinon = require("sinon"),
-    SourceCode = require("../../../lib/util/source-code"),
     SourceCodeFixer = require("../../../lib/util/source-code-fixer");
 
 //------------------------------------------------------------------------------
 // Helpers
 //------------------------------------------------------------------------------
 
-const TEST_CODE = "var answer = 6 * 7;",
-    TEST_AST = espree.parse(TEST_CODE, { loc: true, range: true, tokens: true, comment: true });
+const TEST_CODE = "var answer = 6 * 7;";
 const INSERT_AT_END = {
         message: "End",
         fix: {
@@ -150,34 +147,16 @@ describe("SourceCodeFixer", () => {
     });
 
     describe("applyFixes() with no BOM", () => {
-
-        let sourceCode;
-
-        beforeEach(() => {
-            sourceCode = new SourceCode(TEST_CODE, TEST_AST);
-        });
-
-        it("Should have empty output if sourceCode is not provided", () => {
-            const result = SourceCodeFixer.applyFixes(null, [INSERT_AT_END]);
-
-            assert.equal(result.output.length, 0);
-        });
-
         describe("shouldFix parameter", () => {
-
-            beforeEach(() => {
-                sourceCode = new SourceCode(TEST_CODE, TEST_AST);
-            });
-
             it("Should not perform any fixes if 'shouldFix' is false", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [INSERT_AT_END], false);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE, [INSERT_AT_END], false);
 
                 assert.isFalse(result.fixed);
-                assert.equal(result.output, sourceCode.text);
+                assert.equal(result.output, TEST_CODE);
             });
 
             it("Should perform fixes if 'shouldFix' is not provided", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [INSERT_AT_END]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE, [INSERT_AT_END]);
 
                 assert.isTrue(result.fixed);
             });
@@ -185,34 +164,34 @@ describe("SourceCodeFixer", () => {
             it("should call a function provided as 'shouldFix' for each message", () => {
                 const shouldFixSpy = sinon.spy();
 
-                SourceCodeFixer.applyFixes(sourceCode, [INSERT_IN_MIDDLE, INSERT_AT_START, INSERT_AT_END], shouldFixSpy);
+                SourceCodeFixer.applyFixes(TEST_CODE, [INSERT_IN_MIDDLE, INSERT_AT_START, INSERT_AT_END], shouldFixSpy);
                 assert.isTrue(shouldFixSpy.calledThrice);
             });
 
             it("should provide a message object as an argument to 'shouldFix'", () => {
                 const shouldFixSpy = sinon.spy();
 
-                SourceCodeFixer.applyFixes(sourceCode, [INSERT_AT_START], shouldFixSpy);
+                SourceCodeFixer.applyFixes(TEST_CODE, [INSERT_AT_START], shouldFixSpy);
                 assert.equal(shouldFixSpy.firstCall.args[0], INSERT_AT_START);
             });
 
             it("should not perform fixes if 'shouldFix' function returns false", () => {
                 const shouldFixSpy = sinon.spy(() => false);
-                const result = SourceCodeFixer.applyFixes(sourceCode, [INSERT_AT_START], shouldFixSpy);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE, [INSERT_AT_START], shouldFixSpy);
 
                 assert.isFalse(result.fixed);
             });
 
             it("should return original text as output if 'shouldFix' function prevents all fixes", () => {
                 const shouldFixSpy = sinon.spy(() => false);
-                const result = SourceCodeFixer.applyFixes(sourceCode, [INSERT_AT_START], shouldFixSpy);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE, [INSERT_AT_START], shouldFixSpy);
 
                 assert.equal(result.output, TEST_CODE);
             });
 
             it("should only apply fixes for which the 'shouldFix' function returns true", () => {
                 const shouldFixSpy = sinon.spy(problem => problem.message === "foo");
-                const result = SourceCodeFixer.applyFixes(sourceCode, [INSERT_AT_START, REPLACE_ID], shouldFixSpy);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE, [INSERT_AT_START, REPLACE_ID], shouldFixSpy);
 
                 assert.equal(result.output, "var foo = 6 * 7;");
             });
@@ -220,7 +199,7 @@ describe("SourceCodeFixer", () => {
             it("is called without access to internal eslint state", () => {
                 const shouldFixSpy = sinon.spy();
 
-                SourceCodeFixer.applyFixes(sourceCode, [INSERT_AT_START], shouldFixSpy);
+                SourceCodeFixer.applyFixes(TEST_CODE, [INSERT_AT_START], shouldFixSpy);
 
                 assert.isUndefined(shouldFixSpy.thisValues[0]);
             });
@@ -229,28 +208,28 @@ describe("SourceCodeFixer", () => {
         describe("Text Insertion", () => {
 
             it("should insert text at the end of the code", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [INSERT_AT_END]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE, [INSERT_AT_END]);
 
                 assert.equal(result.output, TEST_CODE + INSERT_AT_END.fix.text);
                 assert.equal(result.messages.length, 0);
             });
 
             it("should insert text at the beginning of the code", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [INSERT_AT_START]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE, [INSERT_AT_START]);
 
                 assert.equal(result.output, INSERT_AT_START.fix.text + TEST_CODE);
                 assert.equal(result.messages.length, 0);
             });
 
             it("should insert text in the middle of the code", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [INSERT_IN_MIDDLE]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE, [INSERT_IN_MIDDLE]);
 
                 assert.equal(result.output, TEST_CODE.replace("6 *", `${INSERT_IN_MIDDLE.fix.text}6 *`));
                 assert.equal(result.messages.length, 0);
             });
 
             it("should insert text at the beginning, middle, and end of the code", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [INSERT_IN_MIDDLE, INSERT_AT_START, INSERT_AT_END]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE, [INSERT_IN_MIDDLE, INSERT_AT_START, INSERT_AT_END]);
 
                 assert.equal(result.output, INSERT_AT_START.fix.text + TEST_CODE.replace("6 *", `${INSERT_IN_MIDDLE.fix.text}6 *`) + INSERT_AT_END.fix.text);
                 assert.equal(result.messages.length, 0);
@@ -258,7 +237,7 @@ describe("SourceCodeFixer", () => {
 
 
             it("should ignore reversed ranges", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [REVERSED_RANGE]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE, [REVERSED_RANGE]);
 
                 assert.equal(result.output, TEST_CODE);
             });
@@ -269,7 +248,7 @@ describe("SourceCodeFixer", () => {
         describe("Text Replacement", () => {
 
             it("should replace text at the end of the code", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [REPLACE_VAR]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE, [REPLACE_VAR]);
 
                 assert.equal(result.messages.length, 0);
                 assert.equal(result.output, TEST_CODE.replace("var", "let"));
@@ -277,7 +256,7 @@ describe("SourceCodeFixer", () => {
             });
 
             it("should replace text at the beginning of the code", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [REPLACE_ID]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE, [REPLACE_ID]);
 
                 assert.equal(result.messages.length, 0);
                 assert.equal(result.output, TEST_CODE.replace("answer", "foo"));
@@ -285,7 +264,7 @@ describe("SourceCodeFixer", () => {
             });
 
             it("should replace text in the middle of the code", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [REPLACE_NUM]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE, [REPLACE_NUM]);
 
                 assert.equal(result.messages.length, 0);
                 assert.equal(result.output, TEST_CODE.replace("6", "5"));
@@ -293,7 +272,7 @@ describe("SourceCodeFixer", () => {
             });
 
             it("should replace text at the beginning and end of the code", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [REPLACE_ID, REPLACE_VAR, REPLACE_NUM]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE, [REPLACE_ID, REPLACE_VAR, REPLACE_NUM]);
 
                 assert.equal(result.messages.length, 0);
                 assert.equal(result.output, "let foo = 5 * 7;");
@@ -305,7 +284,7 @@ describe("SourceCodeFixer", () => {
         describe("Text Removal", () => {
 
             it("should remove text at the start of the code", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [REMOVE_START]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE, [REMOVE_START]);
 
                 assert.equal(result.messages.length, 0);
                 assert.equal(result.output, TEST_CODE.replace("var ", ""));
@@ -313,7 +292,7 @@ describe("SourceCodeFixer", () => {
             });
 
             it("should remove text in the middle of the code", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [REMOVE_MIDDLE]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE, [REMOVE_MIDDLE]);
 
                 assert.equal(result.messages.length, 0);
                 assert.equal(result.output, TEST_CODE.replace("answer", "a"));
@@ -321,7 +300,7 @@ describe("SourceCodeFixer", () => {
             });
 
             it("should remove text towards the end of the code", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [REMOVE_END]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE, [REMOVE_END]);
 
                 assert.equal(result.messages.length, 0);
                 assert.equal(result.output, TEST_CODE.replace(" * 7", ""));
@@ -329,7 +308,7 @@ describe("SourceCodeFixer", () => {
             });
 
             it("should remove text at the beginning, middle, and end of the code", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [REMOVE_END, REMOVE_START, REMOVE_MIDDLE]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE, [REMOVE_END, REMOVE_START, REMOVE_MIDDLE]);
 
                 assert.equal(result.messages.length, 0);
                 assert.equal(result.output, "a = 6;");
@@ -340,7 +319,7 @@ describe("SourceCodeFixer", () => {
         describe("Combination", () => {
 
             it("should replace text at the beginning, remove text in the middle, and insert text at the end", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [INSERT_AT_END, REMOVE_END, REPLACE_VAR]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE, [INSERT_AT_END, REMOVE_END, REPLACE_VAR]);
 
                 assert.equal(result.output, "let answer = 6;// end");
                 assert.isTrue(result.fixed);
@@ -348,7 +327,7 @@ describe("SourceCodeFixer", () => {
             });
 
             it("should only apply one fix when ranges overlap", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [REMOVE_MIDDLE, REPLACE_ID]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE, [REMOVE_MIDDLE, REPLACE_ID]);
 
                 assert.equal(result.output, TEST_CODE.replace("answer", "foo"));
                 assert.equal(result.messages.length, 1);
@@ -357,7 +336,7 @@ describe("SourceCodeFixer", () => {
             });
 
             it("should apply one fix when the end of one range is the same as the start of a previous range overlap", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [REMOVE_START, REPLACE_ID]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE, [REMOVE_START, REPLACE_ID]);
 
                 assert.equal(result.output, TEST_CODE.replace("var ", ""));
                 assert.equal(result.messages.length, 1);
@@ -366,7 +345,7 @@ describe("SourceCodeFixer", () => {
             });
 
             it("should only apply one fix when ranges overlap and one message has no fix", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [REMOVE_MIDDLE, REPLACE_ID, NO_FIX]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE, [REMOVE_MIDDLE, REPLACE_ID, NO_FIX]);
 
                 assert.equal(result.output, TEST_CODE.replace("answer", "foo"));
                 assert.equal(result.messages.length, 2);
@@ -376,8 +355,8 @@ describe("SourceCodeFixer", () => {
             });
 
             it("should apply the same fix when ranges overlap regardless of order", () => {
-                const result1 = SourceCodeFixer.applyFixes(sourceCode, [REMOVE_MIDDLE, REPLACE_ID]);
-                const result2 = SourceCodeFixer.applyFixes(sourceCode, [REPLACE_ID, REMOVE_MIDDLE]);
+                const result1 = SourceCodeFixer.applyFixes(TEST_CODE, [REMOVE_MIDDLE, REPLACE_ID]);
+                const result2 = SourceCodeFixer.applyFixes(TEST_CODE, [REPLACE_ID, REMOVE_MIDDLE]);
 
                 assert.equal(result1.output, result2.output);
             });
@@ -386,7 +365,7 @@ describe("SourceCodeFixer", () => {
         describe("No Fixes", () => {
 
             it("should only apply one fix when ranges overlap and one message has no fix", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [NO_FIX]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE, [NO_FIX]);
 
                 assert.equal(result.output, TEST_CODE);
                 assert.equal(result.messages.length, 1);
@@ -395,7 +374,7 @@ describe("SourceCodeFixer", () => {
             });
 
             it("should sort the no fix messages correctly", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [REPLACE_ID, NO_FIX2, NO_FIX1]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE, [REPLACE_ID, NO_FIX2, NO_FIX1]);
 
                 assert.equal(result.output, TEST_CODE.replace("answer", "foo"));
                 assert.equal(result.messages.length, 2);
@@ -409,7 +388,7 @@ describe("SourceCodeFixer", () => {
         describe("BOM manipulations", () => {
 
             it("should insert BOM with an insertion of '\uFEFF' at 0", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [INSERT_BOM]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE, [INSERT_BOM]);
 
                 assert.equal(result.output, `\uFEFF${TEST_CODE}`);
                 assert.isTrue(result.fixed);
@@ -417,7 +396,7 @@ describe("SourceCodeFixer", () => {
             });
 
             it("should insert BOM with an insertion of '\uFEFFfoobar' at 0", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [INSERT_BOM_WITH_TEXT]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE, [INSERT_BOM_WITH_TEXT]);
 
                 assert.equal(result.output, `\uFEFF// start\n${TEST_CODE}`);
                 assert.isTrue(result.fixed);
@@ -425,7 +404,7 @@ describe("SourceCodeFixer", () => {
             });
 
             it("should remove BOM with a negative range", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [REMOVE_BOM]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE, [REMOVE_BOM]);
 
                 assert.equal(result.output, TEST_CODE);
                 assert.isTrue(result.fixed);
@@ -433,7 +412,7 @@ describe("SourceCodeFixer", () => {
             });
 
             it("should replace BOM with a negative range and 'foobar'", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [REPLACE_BOM_WITH_TEXT]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE, [REPLACE_BOM_WITH_TEXT]);
 
                 assert.equal(result.output, `// start\n${TEST_CODE}`);
                 assert.isTrue(result.fixed);
@@ -448,37 +427,33 @@ describe("SourceCodeFixer", () => {
     // Just `result.output` has BOM.
     describe("applyFixes() with BOM", () => {
 
-        let sourceCode;
-
-        beforeEach(() => {
-            sourceCode = new SourceCode(`\uFEFF${TEST_CODE}`, TEST_AST);
-        });
+        const TEST_CODE_WITH_BOM = `\uFEFF${TEST_CODE}`;
 
         describe("Text Insertion", () => {
 
             it("should insert text at the end of the code", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [INSERT_AT_END]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [INSERT_AT_END]);
 
                 assert.equal(result.output, `\uFEFF${TEST_CODE}${INSERT_AT_END.fix.text}`);
                 assert.equal(result.messages.length, 0);
             });
 
             it("should insert text at the beginning of the code", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [INSERT_AT_START]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [INSERT_AT_START]);
 
                 assert.equal(result.output, `\uFEFF${INSERT_AT_START.fix.text}${TEST_CODE}`);
                 assert.equal(result.messages.length, 0);
             });
 
             it("should insert text in the middle of the code", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [INSERT_IN_MIDDLE]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [INSERT_IN_MIDDLE]);
 
                 assert.equal(result.output, `\uFEFF${TEST_CODE.replace("6 *", `${INSERT_IN_MIDDLE.fix.text}6 *`)}`);
                 assert.equal(result.messages.length, 0);
             });
 
             it("should insert text at the beginning, middle, and end of the code", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [INSERT_IN_MIDDLE, INSERT_AT_START, INSERT_AT_END]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [INSERT_IN_MIDDLE, INSERT_AT_START, INSERT_AT_END]);
                 const insertInMiddle = TEST_CODE.replace("6 *", `${INSERT_IN_MIDDLE.fix.text}6 *`);
 
                 assert.equal(result.output, `\uFEFF${INSERT_AT_START.fix.text}${insertInMiddle}${INSERT_AT_END.fix.text}`);
@@ -486,7 +461,7 @@ describe("SourceCodeFixer", () => {
             });
 
             it("should ignore reversed ranges", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [REVERSED_RANGE]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [REVERSED_RANGE]);
 
                 assert.equal(result.output, `\uFEFF${TEST_CODE}`);
             });
@@ -496,7 +471,7 @@ describe("SourceCodeFixer", () => {
         describe("Text Replacement", () => {
 
             it("should replace text at the end of the code", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [REPLACE_VAR]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [REPLACE_VAR]);
 
                 assert.equal(result.messages.length, 0);
                 assert.equal(result.output, `\uFEFF${TEST_CODE.replace("var", "let")}`);
@@ -504,7 +479,7 @@ describe("SourceCodeFixer", () => {
             });
 
             it("should replace text at the beginning of the code", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [REPLACE_ID]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [REPLACE_ID]);
 
                 assert.equal(result.messages.length, 0);
                 assert.equal(result.output, `\uFEFF${TEST_CODE.replace("answer", "foo")}`);
@@ -512,7 +487,7 @@ describe("SourceCodeFixer", () => {
             });
 
             it("should replace text in the middle of the code", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [REPLACE_NUM]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [REPLACE_NUM]);
 
                 assert.equal(result.messages.length, 0);
                 assert.equal(result.output, `\uFEFF${TEST_CODE.replace("6", "5")}`);
@@ -520,7 +495,7 @@ describe("SourceCodeFixer", () => {
             });
 
             it("should replace text at the beginning and end of the code", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [REPLACE_ID, REPLACE_VAR, REPLACE_NUM]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [REPLACE_ID, REPLACE_VAR, REPLACE_NUM]);
 
                 assert.equal(result.messages.length, 0);
                 assert.equal(result.output, "\uFEFFlet foo = 5 * 7;");
@@ -532,7 +507,7 @@ describe("SourceCodeFixer", () => {
         describe("Text Removal", () => {
 
             it("should remove text at the start of the code", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [REMOVE_START]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [REMOVE_START]);
 
                 assert.equal(result.messages.length, 0);
                 assert.equal(result.output, `\uFEFF${TEST_CODE.replace("var ", "")}`);
@@ -540,7 +515,7 @@ describe("SourceCodeFixer", () => {
             });
 
             it("should remove text in the middle of the code", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [REMOVE_MIDDLE]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [REMOVE_MIDDLE]);
 
                 assert.equal(result.messages.length, 0);
                 assert.equal(result.output, `\uFEFF${TEST_CODE.replace("answer", "a")}`);
@@ -548,7 +523,7 @@ describe("SourceCodeFixer", () => {
             });
 
             it("should remove text towards the end of the code", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [REMOVE_END]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [REMOVE_END]);
 
                 assert.equal(result.messages.length, 0);
                 assert.equal(result.output, `\uFEFF${TEST_CODE.replace(" * 7", "")}`);
@@ -556,7 +531,7 @@ describe("SourceCodeFixer", () => {
             });
 
             it("should remove text at the beginning, middle, and end of the code", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [REMOVE_END, REMOVE_START, REMOVE_MIDDLE]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [REMOVE_END, REMOVE_START, REMOVE_MIDDLE]);
 
                 assert.equal(result.messages.length, 0);
                 assert.equal(result.output, "\uFEFFa = 6;");
@@ -567,7 +542,7 @@ describe("SourceCodeFixer", () => {
         describe("Combination", () => {
 
             it("should replace text at the beginning, remove text in the middle, and insert text at the end", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [INSERT_AT_END, REMOVE_END, REPLACE_VAR]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [INSERT_AT_END, REMOVE_END, REPLACE_VAR]);
 
                 assert.equal(result.output, "\uFEFFlet answer = 6;// end");
                 assert.isTrue(result.fixed);
@@ -575,7 +550,7 @@ describe("SourceCodeFixer", () => {
             });
 
             it("should only apply one fix when ranges overlap", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [REMOVE_MIDDLE, REPLACE_ID]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [REMOVE_MIDDLE, REPLACE_ID]);
 
                 assert.equal(result.output, `\uFEFF${TEST_CODE.replace("answer", "foo")}`);
                 assert.equal(result.messages.length, 1);
@@ -584,7 +559,7 @@ describe("SourceCodeFixer", () => {
             });
 
             it("should apply one fix when the end of one range is the same as the start of a previous range overlap", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [REMOVE_START, REPLACE_ID]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [REMOVE_START, REPLACE_ID]);
 
                 assert.equal(result.output, `\uFEFF${TEST_CODE.replace("var ", "")}`);
                 assert.equal(result.messages.length, 1);
@@ -593,7 +568,7 @@ describe("SourceCodeFixer", () => {
             });
 
             it("should only apply one fix when ranges overlap and one message has no fix", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [REMOVE_MIDDLE, REPLACE_ID, NO_FIX]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [REMOVE_MIDDLE, REPLACE_ID, NO_FIX]);
 
                 assert.equal(result.output, `\uFEFF${TEST_CODE.replace("answer", "foo")}`);
                 assert.equal(result.messages.length, 2);
@@ -603,8 +578,8 @@ describe("SourceCodeFixer", () => {
             });
 
             it("should apply the same fix when ranges overlap regardless of order", () => {
-                const result1 = SourceCodeFixer.applyFixes(sourceCode, [REMOVE_MIDDLE, REPLACE_ID]);
-                const result2 = SourceCodeFixer.applyFixes(sourceCode, [REPLACE_ID, REMOVE_MIDDLE]);
+                const result1 = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [REMOVE_MIDDLE, REPLACE_ID]);
+                const result2 = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [REPLACE_ID, REMOVE_MIDDLE]);
 
                 assert.equal(result1.output, result2.output);
             });
@@ -614,7 +589,7 @@ describe("SourceCodeFixer", () => {
         describe("No Fixes", () => {
 
             it("should only apply one fix when ranges overlap and one message has no fix", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [NO_FIX]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [NO_FIX]);
 
                 assert.equal(result.output, `\uFEFF${TEST_CODE}`);
                 assert.equal(result.messages.length, 1);
@@ -627,7 +602,7 @@ describe("SourceCodeFixer", () => {
         describe("BOM manipulations", () => {
 
             it("should insert BOM with an insertion of '\uFEFF' at 0", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [INSERT_BOM]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [INSERT_BOM]);
 
                 assert.equal(result.output, `\uFEFF${TEST_CODE}`);
                 assert.isTrue(result.fixed);
@@ -635,7 +610,7 @@ describe("SourceCodeFixer", () => {
             });
 
             it("should insert BOM with an insertion of '\uFEFFfoobar' at 0", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [INSERT_BOM_WITH_TEXT]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [INSERT_BOM_WITH_TEXT]);
 
                 assert.equal(result.output, `\uFEFF// start\n${TEST_CODE}`);
                 assert.isTrue(result.fixed);
@@ -643,7 +618,7 @@ describe("SourceCodeFixer", () => {
             });
 
             it("should remove BOM with a negative range", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [REMOVE_BOM]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [REMOVE_BOM]);
 
                 assert.equal(result.output, TEST_CODE);
                 assert.isTrue(result.fixed);
@@ -651,7 +626,7 @@ describe("SourceCodeFixer", () => {
             });
 
             it("should replace BOM with a negative range and 'foobar'", () => {
-                const result = SourceCodeFixer.applyFixes(sourceCode, [REPLACE_BOM_WITH_TEXT]);
+                const result = SourceCodeFixer.applyFixes(TEST_CODE_WITH_BOM, [REPLACE_BOM_WITH_TEXT]);
 
                 assert.equal(result.output, `// start\n${TEST_CODE}`);
                 assert.isTrue(result.fixed);
diff --git a/tests/lib/util/source-code.js b/tests/lib/util/source-code.js
index c3506ef1e..642d62700 100644
--- a/tests/lib/util/source-code.js
+++ b/tests/lib/util/source-code.js
@@ -213,8 +213,7 @@ describe("SourceCode", () => {
 
     describe("getJSDocComment()", () => {
 
-        const sandbox = sinon.sandbox.create(),
-            filename = "foo.js";
+        const sandbox = sinon.sandbox.create();
 
         beforeEach(() => {
             linter.reset();
@@ -246,8 +245,8 @@ describe("SourceCode", () => {
 
             const spy = sandbox.spy(assertJSDoc);
 
-            linter.on("FunctionExpression", spy);
-            linter.verify(code, { rules: {} }, filename, true);
+            linter.defineRule("checker", () => ({ FunctionExpression: spy }));
+            linter.verify(code, { rules: { checker: "error" } });
             assert.isTrue(spy.calledOnce, "Event handler should be called.");
 
         });
@@ -274,8 +273,8 @@ describe("SourceCode", () => {
 
             const spy = sandbox.spy(assertJSDoc);
 
-            linter.on("FunctionExpression", spy);
-            linter.verify(code, { rules: {} }, filename, true);
+            linter.defineRule("checker", () => ({ FunctionExpression: spy }));
+            linter.verify(code, { rules: { checker: "error" } });
             assert.isTrue(spy.calledOnce, "Event handler should be called.");
 
         });
@@ -304,8 +303,8 @@ describe("SourceCode", () => {
 
             const spy = sandbox.spy(assertJSDoc);
 
-            linter.on("FunctionExpression", spy);
-            linter.verify(code, { rules: {} }, filename, true);
+            linter.defineRule("checker", () => ({ FunctionExpression: spy }));
+            linter.verify(code, { rules: { checker: "error" } });
             assert.isTrue(spy.calledTwice, "Event handler should be called twice.");
 
         });
@@ -336,8 +335,8 @@ describe("SourceCode", () => {
 
             const spy = sandbox.spy(assertJSDoc);
 
-            linter.on("FunctionExpression", spy);
-            linter.verify(code, { rules: {} }, filename, true);
+            linter.defineRule("checker", () => ({ FunctionExpression: spy }));
+            linter.verify(code, { rules: { checker: "error" } });
             assert.isTrue(spy.calledOnce, "Event handler should be called.");
         });
 
@@ -364,8 +363,8 @@ describe("SourceCode", () => {
 
             const spy = sandbox.spy(assertJSDoc);
 
-            linter.on("FunctionDeclaration", spy);
-            linter.verify(code, { rules: {} }, filename, true);
+            linter.defineRule("checker", () => ({ FunctionDeclaration: spy }));
+            linter.verify(code, { rules: { checker: "error" } });
             assert.isTrue(spy.calledOnce, "Event handler should be called.");
 
         });
@@ -393,8 +392,8 @@ describe("SourceCode", () => {
 
             const spy = sandbox.spy(assertJSDoc);
 
-            linter.on("FunctionDeclaration", spy);
-            linter.verify(code, { parserOptions: { sourceType: "module" }, rules: {} }, filename, true);
+            linter.defineRule("checker", () => ({ FunctionDeclaration: spy }));
+            linter.verify(code, { rules: { checker: "error" }, parserOptions: { sourceType: "module" } });
             assert.isTrue(spy.calledOnce, "Event handler should be called.");
 
         });
@@ -424,8 +423,8 @@ describe("SourceCode", () => {
 
             const spy = sandbox.spy(assertJSDoc);
 
-            linter.on("FunctionDeclaration", spy);
-            linter.verify(code, { rules: {} }, filename, true);
+            linter.defineRule("checker", () => ({ FunctionDeclaration: spy }));
+            linter.verify(code, { rules: { checker: "error" } });
             assert.isTrue(spy.calledOnce, "Event handler should be called.");
 
         });
@@ -455,8 +454,8 @@ describe("SourceCode", () => {
 
             const spy = sandbox.spy(assertJSDoc);
 
-            linter.on("FunctionDeclaration", spy);
-            linter.verify(code, { rules: {} }, filename, true);
+            linter.defineRule("checker", () => ({ FunctionDeclaration: spy }));
+            linter.verify(code, { rules: { checker: "error" } });
             assert.isTrue(spy.calledOnce, "Event handler should be called.");
 
         });
@@ -485,8 +484,8 @@ describe("SourceCode", () => {
 
             const spy = sandbox.spy(assertJSDoc);
 
-            linter.on("FunctionDeclaration", spy);
-            linter.verify(code, { rules: {} }, filename, true);
+            linter.defineRule("checker", () => ({ FunctionDeclaration: spy }));
+            linter.verify(code, { rules: { checker: "error" } });
             assert.isTrue(spy.calledOnce, "Event handler should be called.");
 
         });
@@ -517,8 +516,8 @@ describe("SourceCode", () => {
 
             const spy = sandbox.spy(assertJSDoc);
 
-            linter.on("FunctionDeclaration", spy);
-            linter.verify(code, { rules: {} }, filename, true);
+            linter.defineRule("checker", () => ({ FunctionDeclaration: spy }));
+            linter.verify(code, { rules: { checker: "error" } });
             assert.isTrue(spy.calledOnce, "Event handler should be called.");
         });
 
@@ -548,8 +547,8 @@ describe("SourceCode", () => {
 
             const spy = sandbox.spy(assertJSDoc);
 
-            linter.on("FunctionExpression", spy);
-            linter.verify(code, { rules: {} }, filename, true);
+            linter.defineRule("checker", () => ({ FunctionExpression: spy }));
+            linter.verify(code, { rules: { checker: "error" } });
             assert.isTrue(spy.calledOnce, "Event handler should be called.");
         });
 
@@ -579,8 +578,8 @@ describe("SourceCode", () => {
 
             const spy = sandbox.spy(assertJSDoc);
 
-            linter.on("ArrowFunctionExpression", spy);
-            linter.verify(code, { parserOptions: { ecmaVersion: 6 }, rules: {} }, filename, true);
+            linter.defineRule("checker", () => ({ ArrowFunctionExpression: spy }));
+            linter.verify(code, { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6 } });
             assert.isTrue(spy.calledOnce, "Event handler should be called.");
         });
 
@@ -608,8 +607,8 @@ describe("SourceCode", () => {
 
             const spy = sandbox.spy(assertJSDoc);
 
-            linter.on("FunctionExpression", spy);
-            linter.verify(code, { rules: {} }, filename, true);
+            linter.defineRule("checker", () => ({ FunctionExpression: spy }));
+            linter.verify(code, { rules: { checker: "error" } });
             assert.isTrue(spy.calledOnce, "Event handler should be called.");
         });
 
@@ -641,8 +640,8 @@ describe("SourceCode", () => {
 
             const spy = sandbox.spy(assertJSDoc);
 
-            linter.on("FunctionExpression", spy);
-            linter.verify(code, { rules: {} }, filename, true);
+            linter.defineRule("checker", () => ({ FunctionExpression: spy }));
+            linter.verify(code, { rules: { checker: "error" } });
             assert.isTrue(spy.calledTwice, "Event handler should be called.");
         });
 
@@ -673,8 +672,8 @@ describe("SourceCode", () => {
 
             const spy = sandbox.spy(assertJSDoc);
 
-            linter.on("FunctionExpression", spy);
-            linter.verify(code, { rules: {} }, filename, true);
+            linter.defineRule("checker", () => ({ FunctionExpression: spy }));
+            linter.verify(code, { rules: { checker: "error" } });
             assert.isTrue(spy.calledTwice, "Event handler should be called.");
         });
 
@@ -703,8 +702,8 @@ describe("SourceCode", () => {
 
             const spy = sandbox.spy(assertJSDoc);
 
-            linter.on("FunctionExpression", spy);
-            linter.verify(code, { rules: {} }, filename, true);
+            linter.defineRule("checker", () => ({ FunctionExpression: spy }));
+            linter.verify(code, { rules: { checker: "error" } });
             assert.isTrue(spy.calledOnce, "Event handler should be called.");
         });
 
@@ -741,8 +740,8 @@ describe("SourceCode", () => {
 
             const spy = sandbox.spy(assertJSDoc);
 
-            linter.on("FunctionExpression", spy);
-            linter.verify(code, { rules: {} }, filename, true);
+            linter.defineRule("checker", () => ({ FunctionExpression: spy }));
+            linter.verify(code, { rules: { checker: "error" } });
             assert.isTrue(spy.calledTwice, "Event handler should be called.");
         });
 
@@ -770,8 +769,8 @@ describe("SourceCode", () => {
 
             const spy = sandbox.spy(assertJSDoc);
 
-            linter.on("ClassExpression", spy);
-            linter.verify(code, { rules: {}, parserOptions: { ecmaVersion: 6 } }, filename, true);
+            linter.defineRule("checker", () => ({ ClassExpression: spy }));
+            linter.verify(code, { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6 } });
             assert.isTrue(spy.calledOnce, "Event handler should be called.");
         });
 
@@ -799,8 +798,8 @@ describe("SourceCode", () => {
 
             const spy = sandbox.spy(assertJSDoc);
 
-            linter.on("ClassDeclaration", spy);
-            linter.verify(code, { rules: {}, parserOptions: { ecmaVersion: 6 } }, filename, true);
+            linter.defineRule("checker", () => ({ ClassDeclaration: spy }));
+            linter.verify(code, { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6 } });
             assert.isTrue(spy.calledOnce, "Event handler should be called.");
         });
 
@@ -828,8 +827,8 @@ describe("SourceCode", () => {
 
             const spy = sandbox.spy(assertJSDoc);
 
-            linter.on("FunctionExpression", spy);
-            linter.verify(code, { rules: {}, parserOptions: { ecmaVersion: 6 } }, filename, true);
+            linter.defineRule("checker", () => ({ FunctionExpression: spy }));
+            linter.verify(code, { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6 } });
             assert.isTrue(spy.calledOnce, "Event handler should be called.");
         });
 
@@ -861,8 +860,8 @@ describe("SourceCode", () => {
 
             const spy = sandbox.spy(assertJSDoc);
 
-            linter.on("FunctionExpression", spy);
-            linter.verify(code, { rules: {}, parserOptions: { ecmaVersion: 6 } }, filename, true);
+            linter.defineRule("checker", () => ({ FunctionExpression: spy }));
+            linter.verify(code, { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6 } });
             assert.isTrue(spy.calledOnce, "Event handler should be called.");
         });
 
@@ -892,15 +891,22 @@ describe("SourceCode", () => {
 
             const spy = sandbox.spy(assertJSDoc);
 
-            linter.on("FunctionDeclaration", spy);
-            linter.verify(code, { rules: {}, parserOptions: { ecmaVersion: 6 } }, filename, true);
+            linter.defineRule("checker", () => ({ FunctionDeclaration: spy }));
+            linter.verify(code, { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6 } });
             assert.isTrue(spy.calledOnce, "Event handler should be called.");
         });
 
     });
 
     describe("getComments()", () => {
-        const config = { rules: {} };
+        const config = { rules: { checker: "error" }, parserOptions: { sourceType: "module" } };
+        let unusedAssertionFuncs;
+
+
+        beforeEach(() => {
+            linter.reset();
+            unusedAssertionFuncs = new Set();
+        });
 
         /**
          * Check comment count
@@ -910,17 +916,30 @@ describe("SourceCode", () => {
          * @private
          */
         function assertCommentCount(leading, trailing) {
-            return function(node) {
+
+            /**
+             * Asserts the comment count for a node
+             * @param {ASTNode} node the node being traversed
+             * @returns {void}
+             */
+            function assertionFunc(node) {
+                unusedAssertionFuncs.delete(assertionFunc);
                 const sourceCode = linter.getSourceCode();
                 const comments = sourceCode.getComments(node);
 
                 assert.equal(comments.leading.length, leading);
                 assert.equal(comments.trailing.length, trailing);
-            };
+            }
+            unusedAssertionFuncs.add(assertionFunc);
+            return assertionFunc;
         }
 
-        beforeEach(() => {
-            linter.reset();
+        afterEach(() => {
+            assert.strictEqual(
+                unusedAssertionFuncs.size,
+                0,
+                "An assertion function was created with assertCommentCount, but the function was not called."
+            );
         });
 
         it("should return comments around nodes", () => {
@@ -930,13 +949,15 @@ describe("SourceCode", () => {
                 "/* Trailing comment for VariableDeclaration */"
             ].join("\n");
 
-            linter.on("Program", assertCommentCount(0, 0));
-            linter.on("VariableDeclaration", assertCommentCount(1, 1));
-            linter.on("VariableDeclarator", assertCommentCount(0, 0));
-            linter.on("Identifier", assertCommentCount(0, 0));
-            linter.on("Literal", assertCommentCount(0, 0));
+            linter.defineRule("checker", () => ({
+                Program: assertCommentCount(0, 0),
+                VariableDeclaration: assertCommentCount(1, 1),
+                VariableDeclarator: assertCommentCount(0, 0),
+                Identifier: assertCommentCount(0, 0),
+                Literal: assertCommentCount(0, 0)
+            }));
 
-            linter.verify(code, config, "", true);
+            assert.isEmpty(linter.verify(code, config));
         });
 
         it("should return trailing comments inside a block", () => {
@@ -947,13 +968,15 @@ describe("SourceCode", () => {
                 "}"
             ].join("\n");
 
-            linter.on("Program", assertCommentCount(0, 0));
-            linter.on("BlockStatement", assertCommentCount(0, 0));
-            linter.on("ExpressionStatement", assertCommentCount(0, 1));
-            linter.on("CallExpression", assertCommentCount(0, 0));
-            linter.on("Identifier", assertCommentCount(0, 0));
+            linter.defineRule("checker", () => ({
+                Program: assertCommentCount(0, 0),
+                BlockStatement: assertCommentCount(0, 0),
+                ExpressionStatement: assertCommentCount(0, 1),
+                CallExpression: assertCommentCount(0, 0),
+                Identifier: assertCommentCount(0, 0)
+            }));
 
-            linter.verify(code, config, "", true);
+            assert.isEmpty(linter.verify(code, config));
         });
 
         it("should return comments within a conditional", () => {
@@ -962,12 +985,14 @@ describe("SourceCode", () => {
                 "if (/* Leading comment for Identifier */ a) {}"
             ].join("\n");
 
-            linter.on("Program", assertCommentCount(0, 0));
-            linter.on("IfStatement", assertCommentCount(1, 0));
-            linter.on("Identifier", assertCommentCount(1, 0));
-            linter.on("BlockStatement", assertCommentCount(0, 0));
+            linter.defineRule("checker", () => ({
+                Program: assertCommentCount(0, 0),
+                IfStatement: assertCommentCount(1, 0),
+                Identifier: assertCommentCount(1, 0),
+                BlockStatement: assertCommentCount(0, 0)
+            }));
 
-            linter.verify(code, config, "", true);
+            assert.isEmpty(linter.verify(code, config));
         });
 
         it("should not return comments within a previous node", () => {
@@ -980,15 +1005,17 @@ describe("SourceCode", () => {
                 "}"
             ].join("\n");
 
-            linter.on("Program", assertCommentCount(0, 0));
-            linter.on("Identifier", assertCommentCount(0, 0));
-            linter.on("BlockStatement", assertCommentCount(0, 0));
-            linter.on("VariableDeclaration", assertCommentCount(0, 0));
-            linter.on("VariableDeclarator", assertCommentCount(0, 0));
-            linter.on("ObjectExpression", assertCommentCount(0, 1));
-            linter.on("ReturnStatement", assertCommentCount(0, 0));
+            linter.defineRule("checker", () => ({
+                Program: assertCommentCount(0, 0),
+                Identifier: assertCommentCount(0, 0),
+                BlockStatement: assertCommentCount(0, 0),
+                VariableDeclaration: assertCommentCount(0, 0),
+                VariableDeclarator: assertCommentCount(0, 0),
+                ObjectExpression: assertCommentCount(0, 1),
+                ReturnStatement: assertCommentCount(0, 0)
+            }));
 
-            linter.verify(code, config, "", true);
+            assert.isEmpty(linter.verify(code, config));
         });
 
         it("should return comments only for children of parent node", () => {
@@ -1000,15 +1027,17 @@ describe("SourceCode", () => {
                 "var baz;"
             ].join("\n");
 
-            linter.on("Program", assertCommentCount(0, 0));
-            linter.on("VariableDeclaration", assertCommentCount(0, 0));
-            linter.on("VariableDeclerator", assertCommentCount(0, 0));
-            linter.on("Identifier", assertCommentCount(0, 0));
-            linter.on("ObjectExpression", assertCommentCount(0, 0));
-            linter.on("Property", assertCommentCount(0, 1));
-            linter.on("Literal", assertCommentCount(0, 0));
+            linter.defineRule("checker", () => ({
+                Program: assertCommentCount(0, 0),
+                VariableDeclaration: assertCommentCount(0, 0),
+                VariableDeclarator: assertCommentCount(0, 0),
+                Identifier: assertCommentCount(0, 0),
+                ObjectExpression: assertCommentCount(0, 0),
+                Property: assertCommentCount(0, 1),
+                Literal: assertCommentCount(0, 0)
+            }));
 
-            linter.verify(code, config, "", true);
+            assert.isEmpty(linter.verify(code, config));
         });
 
         it("should return comments for an export default anonymous class", () => {
@@ -1025,16 +1054,18 @@ describe("SourceCode", () => {
                 "}"
             ].join("\n");
 
-            linter.on("Program", assertCommentCount(0, 0));
-            linter.on("ExportDefaultDeclaration", assertCommentCount(1, 0));
-            linter.on("ClassDeclaration", assertCommentCount(0, 0));
-            linter.on("ClassBody", assertCommentCount(0, 0));
-            linter.on("MethodDefinition", assertCommentCount(1, 0));
-            linter.on("Identifier", assertCommentCount(0, 0));
-            linter.on("FunctionExpression", assertCommentCount(0, 0));
-            linter.on("BlockStatement", assertCommentCount(0, 0));
+            linter.defineRule("checker", () => ({
+                Program: assertCommentCount(0, 0),
+                ExportDefaultDeclaration: assertCommentCount(1, 0),
+                ClassDeclaration: assertCommentCount(0, 0),
+                ClassBody: assertCommentCount(0, 0),
+                MethodDefinition: assertCommentCount(1, 0),
+                Identifier: assertCommentCount(0, 0),
+                FunctionExpression: assertCommentCount(0, 0),
+                BlockStatement: assertCommentCount(0, 0)
+            }));
 
-            linter.verify(code, config, "", true);
+            assert.isEmpty(linter.verify(code, config));
         });
 
         it("should return leading comments", () => {
@@ -1046,19 +1077,21 @@ describe("SourceCode", () => {
             ].join("\n");
             let varDeclCount = 0;
 
-            linter.on("Program", assertCommentCount(0, 0));
-            linter.on("VariableDeclaration", node => {
-                if (varDeclCount === 0) {
-                    assertCommentCount(1, 1)(node);
-                } else {
-                    assertCommentCount(1, 0)(node);
-                }
-                varDeclCount++;
-            });
-            linter.on("VariableDeclarator", assertCommentCount(0, 0));
-            linter.on("Identifier", assertCommentCount(0, 0));
+            linter.defineRule("checker", () => ({
+                Program: assertCommentCount(0, 0),
+                VariableDeclaration(node) {
+                    if (varDeclCount === 0) {
+                        assertCommentCount(1, 1)(node);
+                    } else {
+                        assertCommentCount(1, 0)(node);
+                    }
+                    varDeclCount++;
+                },
+                VariableDeclarator: assertCommentCount(0, 0),
+                Identifier: assertCommentCount(0, 0)
+            }));
 
-            linter.verify(code, config, "", true);
+            assert.isEmpty(linter.verify(code, config));
         });
 
         it("should return shebang comments", () => {
@@ -1070,26 +1103,28 @@ describe("SourceCode", () => {
             ].join("\n");
             let varDeclCount = 0;
 
-            linter.on("Program", assertCommentCount(0, 0));
-            linter.on("VariableDeclaration", node => {
-                if (varDeclCount === 0) {
-                    assertCommentCount(1, 1)(node);
-                } else {
-                    assertCommentCount(1, 0)(node);
-                }
-                varDeclCount++;
-            });
-            linter.on("VariableDeclarator", assertCommentCount(0, 0));
-            linter.on("Identifier", assertCommentCount(0, 0));
+            linter.defineRule("checker", () => ({
+                Program: assertCommentCount(0, 0),
+                VariableDeclaration(node) {
+                    if (varDeclCount === 0) {
+                        assertCommentCount(1, 1)(node);
+                    } else {
+                        assertCommentCount(1, 0)(node);
+                    }
+                    varDeclCount++;
+                },
+                Identifier: assertCommentCount(0, 0)
+            }));
 
-            linter.verify(code, config, "", true);
+            assert.isEmpty(linter.verify(code, config));
         });
 
         it("should include shebang comment when program only contains shebang", () => {
             const code = "#!/usr/bin/env node";
 
-            linter.on("Program", assertCommentCount(1, 0));
-            linter.verify(code, config, "", true);
+            linter.defineRule("checker", () => ({ Program: assertCommentCount(1, 0) }));
+
+            assert.isEmpty(linter.verify(code, config));
         });
 
         it("should return mixture of line and block comments", () => {
@@ -1099,13 +1134,15 @@ describe("SourceCode", () => {
                 "// Trailing comment for VariableDeclaration"
             ].join("\n");
 
-            linter.on("Program", assertCommentCount(0, 0));
-            linter.on("VariableDeclaration", assertCommentCount(1, 1));
-            linter.on("VariableDeclarator", assertCommentCount(0, 0));
-            linter.on("Identifier", assertCommentCount(0, 1));
-            linter.on("Literal", assertCommentCount(0, 0));
+            linter.defineRule("checker", () => ({
+                Program: assertCommentCount(0, 0),
+                VariableDeclaration: assertCommentCount(1, 1),
+                VariableDeclarator: assertCommentCount(0, 0),
+                Identifier: assertCommentCount(0, 1),
+                Literal: assertCommentCount(0, 0)
+            }));
 
-            linter.verify(code, config, "", true);
+            assert.isEmpty(linter.verify(code, config));
         });
 
         it("should return comments surrounding a call expression", () => {
@@ -1117,14 +1154,16 @@ describe("SourceCode", () => {
                 "}"
             ].join("\n");
 
-            linter.on("Program", assertCommentCount(0, 0));
-            linter.on("FunctionDeclaration", assertCommentCount(0, 0));
-            linter.on("Identifier", assertCommentCount(0, 0));
-            linter.on("BlockStatement", assertCommentCount(0, 0));
-            linter.on("ExpressionStatement", assertCommentCount(1, 1));
-            linter.on("CallExpression", assertCommentCount(0, 0));
+            linter.defineRule("checker", () => ({
+                Program: assertCommentCount(0, 0),
+                FunctionDeclaration: assertCommentCount(0, 0),
+                Identifier: assertCommentCount(0, 0),
+                BlockStatement: assertCommentCount(0, 0),
+                ExpressionStatement: assertCommentCount(1, 1),
+                CallExpression: assertCommentCount(0, 0)
+            }));
 
-            linter.verify(code, config, "", true);
+            assert.isEmpty(linter.verify(code, config));
         });
 
         it("should return comments surrounding a debugger statement", () => {
@@ -1136,13 +1175,15 @@ describe("SourceCode", () => {
                 "}"
             ].join("\n");
 
-            linter.on("Program", assertCommentCount(0, 0));
-            linter.on("FunctionDeclaration", assertCommentCount(0, 0));
-            linter.on("Identifier", assertCommentCount(0, 0));
-            linter.on("BlockStatement", assertCommentCount(0, 0));
-            linter.on("DebuggerStatement", assertCommentCount(1, 1));
+            linter.defineRule("checker", () => ({
+                Program: assertCommentCount(0, 0),
+                FunctionDeclaration: assertCommentCount(0, 0),
+                Identifier: assertCommentCount(0, 0),
+                BlockStatement: assertCommentCount(0, 0),
+                DebuggerStatement: assertCommentCount(1, 1)
+            }));
 
-            linter.verify(code, config, "", true);
+            assert.isEmpty(linter.verify(code, config));
         });
 
         it("should return comments surrounding a return statement", () => {
@@ -1154,13 +1195,15 @@ describe("SourceCode", () => {
                 "}"
             ].join("\n");
 
-            linter.on("Program", assertCommentCount(0, 0));
-            linter.on("FunctionDeclaration", assertCommentCount(0, 0));
-            linter.on("Identifier", assertCommentCount(0, 0));
-            linter.on("BlockStatement", assertCommentCount(0, 0));
-            linter.on("ReturnStatement", assertCommentCount(1, 1));
+            linter.defineRule("checker", () => ({
+                Program: assertCommentCount(0, 0),
+                FunctionDeclaration: assertCommentCount(0, 0),
+                Identifier: assertCommentCount(0, 0),
+                BlockStatement: assertCommentCount(0, 0),
+                ReturnStatement: assertCommentCount(1, 1)
+            }));
 
-            linter.verify(code, config, "", true);
+            assert.isEmpty(linter.verify(code, config));
         });
 
         it("should return comments surrounding a throw statement", () => {
@@ -1172,13 +1215,15 @@ describe("SourceCode", () => {
                 "}"
             ].join("\n");
 
-            linter.on("Program", assertCommentCount(0, 0));
-            linter.on("FunctionDeclaration", assertCommentCount(0, 0));
-            linter.on("Identifier", assertCommentCount(0, 0));
-            linter.on("BlockStatement", assertCommentCount(0, 0));
-            linter.on("ThrowStatement", assertCommentCount(1, 1));
+            linter.defineRule("checker", () => ({
+                Program: assertCommentCount(0, 0),
+                FunctionDeclaration: assertCommentCount(0, 0),
+                Identifier: assertCommentCount(0, 0),
+                BlockStatement: assertCommentCount(0, 0),
+                ThrowStatement: assertCommentCount(1, 1)
+            }));
 
-            linter.verify(code, config, "", true);
+            assert.isEmpty(linter.verify(code, config));
         });
 
         it("should return comments surrounding a while loop", () => {
@@ -1191,16 +1236,18 @@ describe("SourceCode", () => {
                 "}"
             ].join("\n");
 
-            linter.on("Program", assertCommentCount(0, 0));
-            linter.on("FunctionDeclaration", assertCommentCount(0, 0));
-            linter.on("Identifier", assertCommentCount(0, 0));
-            linter.on("BlockStatement", assertCommentCount(0, 0));
-            linter.on("WhileStatement", assertCommentCount(1, 1));
-            linter.on("Literal", assertCommentCount(0, 0));
-            linter.on("VariableDeclaration", assertCommentCount(1, 0));
-            linter.on("VariableDeclarator", assertCommentCount(0, 0));
+            linter.defineRule("checker", () => ({
+                Program: assertCommentCount(0, 0),
+                FunctionDeclaration: assertCommentCount(0, 0),
+                Identifier: assertCommentCount(0, 0),
+                BlockStatement: assertCommentCount(0, 0),
+                WhileStatement: assertCommentCount(1, 1),
+                Literal: assertCommentCount(0, 0),
+                VariableDeclaration: assertCommentCount(1, 0),
+                VariableDeclarator: assertCommentCount(0, 0)
+            }));
 
-            linter.verify(code, config, "", true);
+            assert.isEmpty(linter.verify(code, config));
         });
 
         it("should return switch case fallthrough comments in functions", () => {
@@ -1217,24 +1264,26 @@ describe("SourceCode", () => {
             ].join("\n");
             let switchCaseCount = 0;
 
-            linter.on("Program", assertCommentCount(0, 0));
-            linter.on("FunctionDeclaration", assertCommentCount(0, 0));
-            linter.on("Identifier", assertCommentCount(0, 0));
-            linter.on("BlockStatement", assertCommentCount(0, 0));
-            linter.on("SwitchStatement", assertCommentCount(0, 0));
-            linter.on("SwitchCase", node => {
-                if (switchCaseCount === 0) {
-                    assertCommentCount(1, 1)(node);
-                } else {
-                    assertCommentCount(1, 0)(node);
-                }
-                switchCaseCount++;
-            });
-            linter.on("Literal", assertCommentCount(0, 0));
-            linter.on("ExpressionStatement", assertCommentCount(0, 0));
-            linter.on("CallExpression", assertCommentCount(0, 0));
-
-            linter.verify(code, config, "", true);
+            linter.defineRule("checker", () => ({
+                Program: assertCommentCount(0, 0),
+                FunctionDeclaration: assertCommentCount(0, 0),
+                Identifier: assertCommentCount(0, 0),
+                BlockStatement: assertCommentCount(0, 0),
+                SwitchStatement: assertCommentCount(0, 0),
+                SwitchCase: node => {
+                    if (switchCaseCount === 0) {
+                        assertCommentCount(1, 1)(node);
+                    } else {
+                        assertCommentCount(1, 0)(node);
+                    }
+                    switchCaseCount++;
+                },
+                Literal: assertCommentCount(0, 0),
+                ExpressionStatement: assertCommentCount(0, 0),
+                CallExpression: assertCommentCount(0, 0)
+            }));
+
+            assert.isEmpty(linter.verify(code, config));
         });
 
         it("should return switch case fallthrough comments", () => {
@@ -1249,21 +1298,23 @@ describe("SourceCode", () => {
             ].join("\n");
             let switchCaseCount = 0;
 
-            linter.on("Program", assertCommentCount(0, 0));
-            linter.on("SwitchStatement", assertCommentCount(0, 0));
-            linter.on("SwitchCase", node => {
-                if (switchCaseCount === 0) {
-                    assertCommentCount(1, 1)(node);
-                } else {
-                    assertCommentCount(1, 0)(node);
-                }
-                switchCaseCount++;
-            });
-            linter.on("Literal", assertCommentCount(0, 0));
-            linter.on("ExpressionStatement", assertCommentCount(0, 0));
-            linter.on("CallExpression", assertCommentCount(0, 0));
-
-            linter.verify(code, config, "", true);
+            linter.defineRule("checker", () => ({
+                Program: assertCommentCount(0, 0),
+                SwitchStatement: assertCommentCount(0, 0),
+                SwitchCase: node => {
+                    if (switchCaseCount === 0) {
+                        assertCommentCount(1, 1)(node);
+                    } else {
+                        assertCommentCount(1, 0)(node);
+                    }
+                    switchCaseCount++;
+                },
+                Literal: assertCommentCount(0, 0),
+                ExpressionStatement: assertCommentCount(0, 0),
+                CallExpression: assertCommentCount(0, 0)
+            }));
+
+            assert.isEmpty(linter.verify(code, config));
         });
 
         it("should return switch case no-default comments in functions", () => {
@@ -1280,23 +1331,25 @@ describe("SourceCode", () => {
             ].join("\n");
             let breakStatementCount = 0;
 
-            linter.on("Program", assertCommentCount(0, 0));
-            linter.on("FunctionDeclaration", assertCommentCount(0, 0));
-            linter.on("Identifier", assertCommentCount(0, 0));
-            linter.on("BlockStatement", assertCommentCount(0, 0));
-            linter.on("SwitchStatement", assertCommentCount(0, 0));
-            linter.on("SwitchCase", node => {
-                if (breakStatementCount === 0) {
-                    assertCommentCount(0, 0)(node);
-                } else {
-                    assertCommentCount(0, 1)(node);
-                }
-                breakStatementCount++;
-            });
-            linter.on("BreakStatement", assertCommentCount(0, 0));
-            linter.on("Literal", assertCommentCount(0, 0));
-
-            linter.verify(code, config, "", true);
+            linter.defineRule("checker", () => ({
+                Program: assertCommentCount(0, 0),
+                FunctionDeclaration: assertCommentCount(0, 0),
+                Identifier: assertCommentCount(0, 0),
+                BlockStatement: assertCommentCount(0, 0),
+                SwitchStatement: assertCommentCount(0, 0),
+                SwitchCase: node => {
+                    if (breakStatementCount === 0) {
+                        assertCommentCount(0, 0)(node);
+                    } else {
+                        assertCommentCount(0, 1)(node);
+                    }
+                    breakStatementCount++;
+                },
+                BreakStatement: assertCommentCount(0, 0),
+                Literal: assertCommentCount(0, 0)
+            }));
+
+            assert.isEmpty(linter.verify(code, config));
         });
 
         it("should return switch case no-default comments", () => {
@@ -1308,14 +1361,16 @@ describe("SourceCode", () => {
                 "}"
             ].join("\n");
 
-            linter.on("Program", assertCommentCount(0, 0));
-            linter.on("SwitchStatement", assertCommentCount(0, 0));
-            linter.on("Identifier", assertCommentCount(0, 0));
-            linter.on("SwitchCase", assertCommentCount(0, 1));
-            linter.on("BreakStatement", assertCommentCount(0, 0));
-            linter.on("Literal", assertCommentCount(0, 0));
+            linter.defineRule("checker", () => ({
+                Program: assertCommentCount(0, 0),
+                SwitchStatement: assertCommentCount(0, 0),
+                Identifier: assertCommentCount(0, 0),
+                SwitchCase: assertCommentCount(0, 1),
+                BreakStatement: assertCommentCount(0, 0),
+                Literal: assertCommentCount(0, 0)
+            }));
 
-            linter.verify(code, config, "", true);
+            assert.isEmpty(linter.verify(code, config));
         });
 
         it("should return switch case no-default comments in nested functions", () => {
@@ -1332,22 +1387,24 @@ describe("SourceCode", () => {
                 "};"
             ].join("\n");
 
-            linter.on("Program", assertCommentCount(0, 0));
-            linter.on("ExpressionStatement", assertCommentCount(0, 0));
-            linter.on("AssignmentExpression", assertCommentCount(0, 0));
-            linter.on("MemberExpression", assertCommentCount(0, 0));
-            linter.on("Identifier", assertCommentCount(0, 0));
-            linter.on("FunctionExpression", assertCommentCount(0, 0));
-            linter.on("BlockStatement", assertCommentCount(0, 0));
-            linter.on("FunctionDeclaration", assertCommentCount(0, 0));
-            linter.on("SwitchStatement", assertCommentCount(0, 0));
-            linter.on("SwitchCase", assertCommentCount(0, 1));
-            linter.on("ReturnStatement", assertCommentCount(0, 0));
-            linter.on("CallExpression", assertCommentCount(0, 0));
-            linter.on("BinaryExpression", assertCommentCount(0, 0));
-            linter.on("Literal", assertCommentCount(0, 0));
-
-            linter.verify(code, config, "", true);
+            linter.defineRule("checker", () => ({
+                Program: assertCommentCount(0, 0),
+                ExpressionStatement: assertCommentCount(0, 0),
+                AssignmentExpression: assertCommentCount(0, 0),
+                MemberExpression: assertCommentCount(0, 0),
+                Identifier: assertCommentCount(0, 0),
+                FunctionExpression: assertCommentCount(0, 0),
+                BlockStatement: assertCommentCount(0, 0),
+                FunctionDeclaration: assertCommentCount(0, 0),
+                SwitchStatement: assertCommentCount(0, 0),
+                SwitchCase: assertCommentCount(0, 1),
+                ReturnStatement: assertCommentCount(0, 0),
+                CallExpression: assertCommentCount(0, 0),
+                BinaryExpression: assertCommentCount(0, 0),
+                Literal: assertCommentCount(0, 0)
+            }));
+
+            assert.isEmpty(linter.verify(code, config));
         });
 
         it("should return leading comments if the code only contains comments", () => {
@@ -1356,9 +1413,9 @@ describe("SourceCode", () => {
                 "/*another comment*/"
             ].join("\n");
 
-            linter.on("Program", assertCommentCount(2, 0));
+            linter.defineRule("checker", () => ({ Program: assertCommentCount(2, 0) }));
 
-            linter.verify(code, config, "", true);
+            assert.isEmpty(linter.verify(code, config));
         });
 
         it("should return trailing comments if a block statement only contains comments", () => {
@@ -1369,10 +1426,12 @@ describe("SourceCode", () => {
                 "}"
             ].join("\n");
 
-            linter.on("Program", assertCommentCount(0, 0));
-            linter.on("BlockStatement", assertCommentCount(0, 2));
+            linter.defineRule("checker", () => ({
+                Program: assertCommentCount(0, 0),
+                BlockStatement: assertCommentCount(0, 2)
+            }));
 
-            linter.verify(code, config, "", true);
+            assert.isEmpty(linter.verify(code, config));
         });
 
         it("should return trailing comments if a class body only contains comments", () => {
@@ -1383,11 +1442,13 @@ describe("SourceCode", () => {
                 "}"
             ].join("\n");
 
-            linter.on("Program", assertCommentCount(0, 0));
-            linter.on("ClassDeclaration", assertCommentCount(0, 0));
-            linter.on("ClassBody", assertCommentCount(0, 2));
+            linter.defineRule("checker", () => ({
+                Program: assertCommentCount(0, 0),
+                ClassDeclaration: assertCommentCount(0, 0),
+                ClassBody: assertCommentCount(0, 2)
+            }));
 
-            linter.verify(code, config, "", true);
+            assert.isEmpty(linter.verify(code, config));
         });
 
         it("should return trailing comments if an object only contains comments", () => {
@@ -1398,11 +1459,13 @@ describe("SourceCode", () => {
                 "})"
             ].join("\n");
 
-            linter.on("Program", assertCommentCount(0, 0));
-            linter.on("ExpressionStatement", assertCommentCount(0, 0));
-            linter.on("ObjectExpression", assertCommentCount(0, 2));
+            linter.defineRule("checker", () => ({
+                Program: assertCommentCount(0, 0),
+                ExpressionStatement: assertCommentCount(0, 0),
+                ObjectExpression: assertCommentCount(0, 2)
+            }));
 
-            linter.verify(code, config, "", true);
+            assert.isEmpty(linter.verify(code, config));
         });
 
         it("should return trailing comments if an array only contains comments", () => {
@@ -1413,11 +1476,13 @@ describe("SourceCode", () => {
                 "]"
             ].join("\n");
 
-            linter.on("Program", assertCommentCount(0, 0));
-            linter.on("ExpressionStatement", assertCommentCount(0, 0));
-            linter.on("ArrayExpression", assertCommentCount(0, 2));
+            linter.defineRule("checker", () => ({
+                Program: assertCommentCount(0, 0),
+                ExpressionStatement: assertCommentCount(0, 0),
+                ArrayExpression: assertCommentCount(0, 2)
+            }));
 
-            linter.verify(code, config, "", true);
+            assert.isEmpty(linter.verify(code, config));
         });
 
         it("should return trailing comments if a switch statement only contains comments", () => {
@@ -1428,11 +1493,13 @@ describe("SourceCode", () => {
                 "}"
             ].join("\n");
 
-            linter.on("Program", assertCommentCount(0, 0));
-            linter.on("SwitchStatement", assertCommentCount(0, 2));
-            linter.on("Identifier", assertCommentCount(0, 0));
+            linter.defineRule("checker", () => ({
+                Program: assertCommentCount(0, 0),
+                SwitchStatement: assertCommentCount(0, 2),
+                Identifier: assertCommentCount(0, 0)
+            }));
 
-            linter.verify(code, config, "", true);
+            assert.isEmpty(linter.verify(code, config));
         });
 
         it("should return comments for multiple declarations with a single variable", () => {
@@ -1445,21 +1512,23 @@ describe("SourceCode", () => {
             ].join("\n");
             let varDeclCount = 0;
 
-            linter.on("Program", assertCommentCount(0, 0));
-            linter.on("VariableDeclaration", assertCommentCount(1, 2));
-            linter.on("VariableDeclarator", node => {
-                if (varDeclCount === 0) {
-                    assertCommentCount(0, 0)(node);
-                } else if (varDeclCount === 1) {
-                    assertCommentCount(1, 0)(node);
-                } else {
-                    assertCommentCount(1, 0)(node);
-                }
-                varDeclCount++;
-            });
-            linter.on("Identifier", assertCommentCount(0, 0));
-
-            linter.verify(code, config, "", true);
+            linter.defineRule("checker", () => ({
+                Program: assertCommentCount(0, 0),
+                VariableDeclaration: assertCommentCount(1, 2),
+                VariableDeclarator: node => {
+                    if (varDeclCount === 0) {
+                        assertCommentCount(0, 0)(node);
+                    } else if (varDeclCount === 1) {
+                        assertCommentCount(1, 0)(node);
+                    } else {
+                        assertCommentCount(1, 0)(node);
+                    }
+                    varDeclCount++;
+                },
+                Identifier: assertCommentCount(0, 0)
+            }));
+
+            assert.isEmpty(linter.verify(code, config));
         });
 
         it("should return comments when comments exist between var keyword and VariableDeclarator", () => {
@@ -1469,52 +1538,60 @@ describe("SourceCode", () => {
                 "    a;"
             ].join("\n");
 
-            linter.on("Program", assertCommentCount(0, 0));
-            linter.on("VariableDeclaration", assertCommentCount(0, 0));
-            linter.on("VariableDeclarator", assertCommentCount(2, 0));
-            linter.on("Identifier", assertCommentCount(0, 0));
+            linter.defineRule("checker", () => ({
+                Program: assertCommentCount(0, 0),
+                VariableDeclaration: assertCommentCount(0, 0),
+                VariableDeclarator: assertCommentCount(2, 0),
+                Identifier: assertCommentCount(0, 0)
+            }));
 
-            linter.verify(code, config, "", true);
+            assert.isEmpty(linter.verify(code, config));
         });
 
         it("should return attached comments between tokens to the correct nodes for empty function declarations", () => {
             const code = "/* 1 */ function /* 2 */ foo(/* 3 */) /* 4 */ { /* 5 */ } /* 6 */";
 
-            linter.on("Program", assertCommentCount(0, 0));
-            linter.on("FunctionDeclaration", assertCommentCount(1, 1));
-            linter.on("Identifier", assertCommentCount(1, 0));
-            linter.on("BlockStatement", assertCommentCount(1, 1));
+            linter.defineRule("checker", () => ({
+                Program: assertCommentCount(0, 0),
+                FunctionDeclaration: assertCommentCount(1, 1),
+                Identifier: assertCommentCount(1, 0),
+                BlockStatement: assertCommentCount(1, 1)
+            }));
 
-            linter.verify(code, config, "", true);
+            assert.isEmpty(linter.verify(code, config));
         });
 
         it("should return attached comments between tokens to the correct nodes for empty class declarations", () => {
             const code = "/* 1 */ class /* 2 */ Foo /* 3 */ extends /* 4 */ Bar /* 5 */ { /* 6 */ } /* 7 */";
             let idCount = 0;
 
-            linter.on("Program", assertCommentCount(0, 0));
-            linter.on("ClassDeclaration", assertCommentCount(1, 1));
-            linter.on("Identifier", node => {
-                if (idCount === 0) {
-                    assertCommentCount(1, 1)(node);
-                } else {
-                    assertCommentCount(1, 1)(node);
-                }
-                idCount++;
-            });
-            linter.on("ClassBody", assertCommentCount(1, 1));
+            linter.defineRule("checker", () => ({
+                Program: assertCommentCount(0, 0),
+                ClassDeclaration: assertCommentCount(1, 1),
+                Identifier: node => {
+                    if (idCount === 0) {
+                        assertCommentCount(1, 1)(node);
+                    } else {
+                        assertCommentCount(1, 1)(node);
+                    }
+                    idCount++;
+                },
+                ClassBody: assertCommentCount(1, 1)
+            }));
 
-            linter.verify(code, config, "", true);
+            assert.isEmpty(linter.verify(code, config));
         });
 
         it("should return attached comments between tokens to the correct nodes for empty switch statements", () => {
             const code = "/* 1 */ switch /* 2 */ (/* 3 */ foo /* 4 */) /* 5 */ { /* 6 */ } /* 7 */";
 
-            linter.on("Program", assertCommentCount(0, 0));
-            linter.on("SwitchStatement", assertCommentCount(1, 6));
-            linter.on("Identifier", assertCommentCount(1, 1));
+            linter.defineRule("checker", () => ({
+                Program: assertCommentCount(0, 0),
+                SwitchStatement: assertCommentCount(1, 6),
+                Identifier: assertCommentCount(1, 1)
+            }));
 
-            linter.verify(code, config, "", true);
+            assert.isEmpty(linter.verify(code, config));
         });
     });