Repository: eslint/eslint
Pull request #8888 (2017-07-06 16:04:49)

Fix: Throw error if plugins from config is not an array.
<!--
    ESLint adheres to the [JS Foundation Code of Conduct](https://js.foundation/community/code-of-conduct).
-->

**What is the purpose of this pull request? (put an "X" next to item)**

[ ] Documentation update
[ ] Bug fix ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/bug-report.md))
[ ] New rule ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/rule-proposal.md))
[ ] Changes an existing rule ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/rule-change-proposal.md))
[ ] Add autofixing to a rule
[ ] Add a CLI option
[ ] Add something to the core
[X] Other, please explain:

I'm throwing an Error if the plugins set doesn't match expected input type.  This isn't a bug necessarily, but it's something I've ran into multiple times on multiple projects where I initially tried to pass in a string (with one plugin name) instead of an array.

<!--
    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)**

I'm throwing an Error with a detailed error message if the user has a config with `plugins` as anything other than an array (which is expected).

**Is there anything you'd like reviewers to focus on?**

I have both a `debug(/* ... */)` statement and throwing an error.  I'm not sure if that's the expected convention, but I tried to do similar to what's happening in other parts of the same `plugins.js` file.  Build passes and tests pass.

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


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


Can you please update the pull request to address these?

(More information can be found in our [pull request guide](http://eslint.org/docs/developer-guide/contributing/pull-requests).)
eslintbot: LGTM
calvinf: Updated commit message to match guidelines.

Patch:
diff --git a/.gitignore b/.gitignore
index 400e6646a..eae2b21d1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@ build/
 npm-debug.log
 .DS_Store
 tmp/
+debug/
 .idea
 jsdoc/
 versions.json
diff --git a/.travis.yml b/.travis.yml
index 6b75ef05e..497ae06ca 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -6,9 +6,15 @@ node_js:
     - "7"
     - "8"
 sudo: false
-script: "npm test && npm run docs"
+branches:
+    only:
+    - master
+
+# Run npm test always
+script:
+    - "npm test"
 after_success:
-    - npm run coveralls
+    - 'if [ "$node_js" = "8" ]; then npm run coveralls; fi'
 addons:
     code_climate:
         repo_token: 1945f7420d920a59f1ff8bf8d1a7b60ccd9e2838a692f73a5a74accd8df30146
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5e017bec8..3866a56b3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,42 @@
+v4.2.0 - July 8, 2017
+
+* e0f0101 Update: fix indentation of nested function parameters (fixes #8892) (#8900) (Teddy Katz)
+* 9f95a3e Chore: remove unused helper method from `indent` (#8901) (Teddy Katz)
+* 11ffe6b Fix: no-regex-spaces rule incorrectly fixes quantified spaces (#8773) (Keri Warr)
+* 975dacf Update: fix indentation of EmptyStatements (fixes #8882) (#8885) (Teddy Katz)
+* 88ed041 Build: Turnoff CI branch build (fixes #8804) (#8873) (Gyandeep Singh)
+* 72f22eb Chore: replace is-my-json-valid with Ajv (#8852) (Gajus Kuizinas)
+* 7c8de92 Docs: Clarified PR guidelines in maintainer guide (#8876) (Kevin Partington)
+* d1fc408 Docs: Update CLA link in Contributing docs (#8883) (Calvin Freitas)
+* 931a9f1 Fix: indent false positive with multi-line await expression (#8837) (薛定谔的猫)
+* 3767cda Update: add no-sync option to allow at root level (fixes #7985) (#8859) (Victor Hom)
+* 1ce553d Docs: Fix wording of minProperties in object-curly-newline (fixes #8874) (#8878) (solmsted)
+* f00854e Fix: --quiet no longer fixes warnings (fixes #8675) (#8858) (Kevin Partington)
+* b678535 Chore: Add collapsible block for config in ISSUE_TEMPLATE (#8872) (Gyandeep Singh)
+* 1f5bfc2 Update: Add always-multiline option to multiline-ternary (fixes #8770) (#8841) (Nathan Woltman)
+* 22116f2 Fix: correct comma-dangle JSON schema (#8864) (Evgeny Poberezkin)
+* 676af9e Update: fix indentation of JSXExpressionContainer contents (fixes #8832) (#8850) (Teddy Katz)
+* 330dd58 Chore: fix title of linter test suite (#8861) (Teddy Katz)
+* 60099ed Chore: enable for-direction rule on ESLint codebase (#8853) (薛定谔的猫)
+* e0d1a84 Chore: upgrade eslint-plugin-eslint-plugin & eslint-plugin-node (#8856) (薛定谔的猫)
+* 0780d86 Chore: remove identical tests (#8851) (Teddy Katz)
+* 5c3ac8e Fix: arrow-parens fixer gets tripped up with trailing comma in args (#8838) (薛定谔的猫)
+* c4f2e29 Build: fix race condition in demo (#8827) (Teddy Katz)
+* c693be5 New: Allow passing a function as `fix` option (fixes #8039) (#8730) (Ian VanSchooten)
+* 8796d55 Docs: add missing item to 4.0 migration guide table of contents (#8835) (薛定谔的猫)
+* 742998c doc md update: false -> `false` (#8825) (Erik Vold)
+* ce969f9 Docs: add guidelines for patch release communication (fixes #7277) (#8823) (Teddy Katz)
+* 5c83c99 Docs: Clarify arrow function parens in no-extra-parens (fixes #8741) (#8822) (Kevin Partington)
+* 84d921d Docs: Added note about Node/CJS scoping to no-redeclare (fixes #8814) (#8820) (Kevin Partington)
+* 85c9327 Update: fix parenthesized CallExpression indentation (fixes #8790) (#8802) (Teddy Katz)
+* be8d354 Update: simplify variable declarator indent handling (fixes #8785) (#8801) (Teddy Katz)
+* 9417818 Fix: no-debugger autofixer produced invalid syntax (#8806) (Teddy Katz)
+* 8698a92 New: getter-return rule (fixes #8449) (#8460) (薛定谔的猫)
+* eac06f2 Fix: no-extra-parens false positives for variables called "let" (#8808) (Teddy Katz)
+* 616587f Fix: dot-notation autofix produces syntax errors for object called "let" (#8807) (Teddy Katz)
+* a53ef7e Fix: don't require a third argument in linter.verifyAndFix (fixes #8805) (#8809) (Teddy Katz)
+* 5ad8b70 Docs: add minor formatting improvement to paragraph about parsers (#8816) (Teddy Katz)
+
 v4.1.1 - June 25, 2017
 
 * f307aa0 Fix: ensure configs from a plugin are cached separately (fixes #8792) (#8798) (Teddy Katz)
diff --git a/Makefile.js b/Makefile.js
index 0d6567f92..3e04619d1 100644
--- a/Makefile.js
+++ b/Makefile.js
@@ -51,6 +51,7 @@ const OPEN_SOURCE_LICENSES = [
 const NODE = "node ", // intentional extra space
     NODE_MODULES = "./node_modules/",
     TEMP_DIR = "./tmp/",
+    DEBUG_DIR = "./debug/",
     BUILD_DIR = "./build/",
     DOCS_DIR = "../eslint.github.io/docs",
     SITE_DIR = "../eslint.github.io/",
@@ -62,7 +63,7 @@ const NODE = "node ", // intentional extra space
 
     // Files
     MAKEFILE = "./Makefile.js",
-    JS_FILES = "\"lib/**/*.js\" \"conf/**/*.js\" \"bin/**/*.js\"",
+    JS_FILES = "\"lib/**/*.js\" \"conf/**/*.js\" \"bin/**/*.js\" \"tools/**/*.js\"",
     JSON_FILES = find("conf/").filter(fileType("json")),
     MARKDOWN_FILES_ARRAY = find("docs/").concat(ls(".")).filter(fileType("md")),
     TEST_FILES = getTestFilePatterns(),
@@ -86,16 +87,12 @@ const NODE = "node ", // intentional extra space
  * @private
  */
 function getTestFilePatterns() {
-    const testLibPath = "tests/lib/",
-        testTemplatesPath = "tests/templates/",
-        testBinPath = "tests/bin/";
-
-    return ls(testLibPath).filter(pathToCheck => test("-d", testLibPath + pathToCheck)).reduce((initialValue, currentValues) => {
+    return ls("tests/lib/").filter(pathToCheck => test("-d", `tests/lib/${pathToCheck}`)).reduce((initialValue, currentValues) => {
         if (currentValues !== "rules") {
-            initialValue.push(`"${testLibPath + currentValues}/**/*.js"`);
+            initialValue.push(`"tests/lib/${currentValues}/**/*.js"`);
         }
         return initialValue;
-    }, [`"${testLibPath}rules/**/*.js"`, `"${testLibPath}*.js"`, `"${testTemplatesPath}*.js"`, `"${testBinPath}**/*.js"`]).join(" ");
+    }, ["tests/lib/rules/**/*.js", "tests/lib/*.js", "tests/templates/*.js", "tests/bin/**/*.js", "tests/tools/*.js"]).join(" ");
 }
 
 /**
@@ -543,6 +540,42 @@ target.lint = function() {
     }
 };
 
+target.fuzz = function() {
+    const fuzzerRunner = require("./tools/fuzzer-runner");
+    const fuzzResults = fuzzerRunner.run({ amount: process.env.CI ? 1000 : 300 });
+
+    if (fuzzResults.length) {
+        echo(`The fuzzer reported ${fuzzResults.length} error${fuzzResults.length === 1 ? "" : "s"}.`);
+
+        const formattedResults = JSON.stringify({ results: fuzzResults }, null, 4);
+
+        if (process.env.CI) {
+            echo("More details can be found below.");
+            echo(formattedResults);
+        } else {
+            if (!test("-d", DEBUG_DIR)) {
+                mkdir(DEBUG_DIR);
+            }
+
+            let fuzzLogPath;
+            let fileSuffix = 0;
+
+            // To avoid overwriting any existing fuzzer log files, append a numeric suffix to the end of the filename.
+            do {
+                fuzzLogPath = path.join(DEBUG_DIR, `fuzzer-log-${fileSuffix}.json`);
+                fileSuffix++;
+            } while (test("-f", fuzzLogPath));
+
+            formattedResults.to(fuzzLogPath);
+
+            // TODO: (not-an-aardvark) Create a better way to isolate and test individual fuzzer errors from the log file
+            echo(`More details can be found in ${fuzzLogPath}.`);
+        }
+
+        exit(1);
+    }
+};
+
 target.test = function() {
     target.lint();
     target.checkRuleFiles();
diff --git a/appveyor.yml b/appveyor.yml
index e0e77131a..e424a345b 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -9,6 +9,10 @@ environment:
   matrix:
     - nodejs_version: 4
 
+branches:
+  only:
+    - master
+
 install:
   # Get the latest stable version of Node.js
   - ps: Install-Product node $env:nodejs_version
diff --git a/conf/json-schema-schema.json b/conf/json-schema-schema.json
deleted file mode 100644
index 85eb502a6..000000000
--- a/conf/json-schema-schema.json
+++ /dev/null
@@ -1,150 +0,0 @@
-{
-    "id": "http://json-schema.org/draft-04/schema#",
-    "$schema": "http://json-schema.org/draft-04/schema#",
-    "description": "Core schema meta-schema",
-    "definitions": {
-        "schemaArray": {
-            "type": "array",
-            "minItems": 1,
-            "items": { "$ref": "#" }
-        },
-        "positiveInteger": {
-            "type": "integer",
-            "minimum": 0
-        },
-        "positiveIntegerDefault0": {
-            "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ]
-        },
-        "simpleTypes": {
-            "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ]
-        },
-        "stringArray": {
-            "type": "array",
-            "items": { "type": "string" },
-            "minItems": 1,
-            "uniqueItems": true
-        }
-    },
-    "type": "object",
-    "properties": {
-        "id": {
-            "type": "string",
-            "format": "uri"
-        },
-        "$schema": {
-            "type": "string",
-            "format": "uri"
-        },
-        "title": {
-            "type": "string"
-        },
-        "description": {
-            "type": "string"
-        },
-        "default": {},
-        "multipleOf": {
-            "type": "number",
-            "minimum": 0,
-            "exclusiveMinimum": true
-        },
-        "maximum": {
-            "type": "number"
-        },
-        "exclusiveMaximum": {
-            "type": "boolean",
-            "default": false
-        },
-        "minimum": {
-            "type": "number"
-        },
-        "exclusiveMinimum": {
-            "type": "boolean",
-            "default": false
-        },
-        "maxLength": { "$ref": "#/definitions/positiveInteger" },
-        "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" },
-        "pattern": {
-            "type": "string",
-            "format": "regex"
-        },
-        "additionalItems": {
-            "anyOf": [
-                { "type": "boolean" },
-                { "$ref": "#" }
-            ],
-            "default": {}
-        },
-        "items": {
-            "anyOf": [
-                { "$ref": "#" },
-                { "$ref": "#/definitions/schemaArray" }
-            ],
-            "default": {}
-        },
-        "maxItems": { "$ref": "#/definitions/positiveInteger" },
-        "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" },
-        "uniqueItems": {
-            "type": "boolean",
-            "default": false
-        },
-        "maxProperties": { "$ref": "#/definitions/positiveInteger" },
-        "minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" },
-        "required": { "$ref": "#/definitions/stringArray" },
-        "additionalProperties": {
-            "anyOf": [
-                { "type": "boolean" },
-                { "$ref": "#" }
-            ],
-            "default": {}
-        },
-        "definitions": {
-            "type": "object",
-            "additionalProperties": { "$ref": "#" },
-            "default": {}
-        },
-        "properties": {
-            "type": "object",
-            "additionalProperties": { "$ref": "#" },
-            "default": {}
-        },
-        "patternProperties": {
-            "type": "object",
-            "additionalProperties": { "$ref": "#" },
-            "default": {}
-        },
-        "dependencies": {
-            "type": "object",
-            "additionalProperties": {
-                "anyOf": [
-                    { "$ref": "#" },
-                    { "$ref": "#/definitions/stringArray" }
-                ]
-            }
-        },
-        "enum": {
-            "type": "array",
-            "minItems": 1,
-            "uniqueItems": true
-        },
-        "type": {
-            "anyOf": [
-                { "$ref": "#/definitions/simpleTypes" },
-                {
-                    "type": "array",
-                    "items": { "$ref": "#/definitions/simpleTypes" },
-                    "minItems": 1,
-                    "uniqueItems": true
-                }
-            ]
-        },
-        "allOf": { "$ref": "#/definitions/schemaArray" },
-        "anyOf": { "$ref": "#/definitions/schemaArray" },
-        "oneOf": { "$ref": "#/definitions/schemaArray" },
-        "not": { "$ref": "#" }
-    },
-    "dependencies": {
-        "exclusiveMaximum": [ "maximum" ],
-        "exclusiveMinimum": [ "minimum" ]
-    },
-    "default": {}
-}
diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md
index b4e77e184..431aaf049 100644
--- a/docs/developer-guide/nodejs-api.md
+++ b/docs/developer-guide/nodejs-api.md
@@ -167,7 +167,7 @@ Output object from this method:
 ```js
 {
     fixed: true,
-    text: "var foo;",
+    output: "var foo;",
     messages: []
 }
 ```
@@ -175,7 +175,7 @@ Output object from this method:
 The information available is:
 
 * `fixed` - True, if the code was fixed.
-* `text` - Fixed code text (might be the same as input if no fixes were applied).
+* `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
diff --git a/docs/maintainer-guide/pullrequests.md b/docs/maintainer-guide/pullrequests.md
index fffeb75b5..3b5618389 100644
--- a/docs/maintainer-guide/pullrequests.md
+++ b/docs/maintainer-guide/pullrequests.md
@@ -11,14 +11,14 @@ Anyone, both team members and the public, may leave comments on pull requests.
 When a pull request is opened, the bot will check the following:
 
 1. Has the submitter signed a CLA?
-1. Is the commit message summary in the correct format? Double-check that the tag ("Fix:", "New:", etc.) is correct based on the issue. Documentation-only pull requests do not require an issue.
-1. Does the commit summary reference an issue?
+1. Is the commit message summary in the correct format?
 1. Is the commit summary too long?
 
 The bot will add a comment specifying the problems that it finds. You do not need to look at the pull request any further until those problems have been addressed (there's no need to comment on the pull request to ask the submitter to do what the bot asked - that's why we have the bot!).
 
 Once the bot checks have been satisfied, you check the following:
 
+1. Double-check that the commit message tag ("Fix:", "New:", etc.) is correct based on the issue (or, if no issue is referenced, based on the stated problem).
 1. Does the code follow our conventions (including header comments, JSDoc comments, etc.)? If not, please leave that feedback and reference the conventions document.
 1. For code changes:
     * Are there tests that verify the change? If not, please ask for them.
@@ -38,6 +38,7 @@ Committers may merge a pull request if it is a non-breaking change and is:
 1. A bug fix (for either rules or core)
 1. A dependency upgrade
 1. Related to the build system
+1. A chore
 
 In addition, committers may merge any non-breaking pull request if it has been approved by at least one TSC member.
 
diff --git a/docs/rules/multiline-ternary.md b/docs/rules/multiline-ternary.md
index 4faaad79c..0cb94643a 100644
--- a/docs/rules/multiline-ternary.md
+++ b/docs/rules/multiline-ternary.md
@@ -68,7 +68,7 @@ foo > bar ?
 Examples of **incorrect** code for this rule with the `"always-multiline"` option:
 
 ```js
-/*eslint multiline-ternary: ["error", "always"]*/
+/*eslint multiline-ternary: ["error", "always-multiline"]*/
 
 foo > bar ? value1 :
     value2;
@@ -83,7 +83,7 @@ foo > bar &&
 Examples of **correct** code for this rule with the `"always-multiline"` option:
 
 ```js
-/*eslint multiline-ternary: ["error", "always"]*/
+/*eslint multiline-ternary: ["error", "always-multiline"]*/
 
 foo > bar ? value1 : value2;
 
diff --git a/docs/rules/no-sync.md b/docs/rules/no-sync.md
index b55ee424c..50ab515ee 100644
--- a/docs/rules/no-sync.md
+++ b/docs/rules/no-sync.md
@@ -6,17 +6,23 @@ In Node.js, most I/O is done through asynchronous methods. However, there are of
 
 This rule is aimed at preventing synchronous methods from being called in Node.js. It looks specifically for the method suffix "`Sync`" (as is the convention with Node.js operations).
 
-Examples of **incorrect** code for this rule:
+## Options
+
+This rule has an optional object option `{ allowAtRootLevel: <boolean> }`, which determines whether synchronous methods should be allowed at the top level of a file, outside of any functions. This option defaults to `false`.
+
+Examples of **incorrect** code for this rule with the default `{ allowAtRootLevel: false }` option:
 
 ```js
 /*eslint no-sync: "error"*/
 
 fs.existsSync(somePath);
 
-var contents = fs.readFileSync(somePath).toString();
+function foo() {
+  var contents = fs.readFileSync(somePath).toString();
+}
 ```
 
-Examples of **correct** code for this rule:
+Examples of **correct** code for this rule with the default `{ allowAtRootLevel: false }` option:
 
 ```js
 /*eslint no-sync: "error"*/
@@ -28,6 +34,26 @@ async(function() {
 });
 ```
 
+Examples of **incorrect** code for this rule with the `{ allowAtRootLevel: true }` option
+
+```js
+/*eslint no-sync: ["error", { allowAtRootLevel: true }]*/
+
+function foo() {
+  var contents = fs.readFileSync(somePath).toString();
+}
+
+var bar = baz => fs.readFileSync(qux);
+```
+
+Examples of **correct** code for this rule with the `{ allowAtRootLevel: true }` option
+
+```js
+/*eslint no-sync: ["error", { allowAtRootLevel: true }]*/
+
+fs.readFileSync(somePath).toString();
+```
+
 ## When Not To Use It
 
-If you want to allow synchronous operations in your script.
+If you want to allow synchronous operations in your script, do not enable this rule.
diff --git a/lib/config/config-validator.js b/lib/config/config-validator.js
index 8754485f4..fb5a96564 100644
--- a/lib/config/config-validator.js
+++ b/lib/config/config-validator.js
@@ -9,7 +9,7 @@
 // Requirements
 //------------------------------------------------------------------------------
 
-const schemaValidator = require("is-my-json-valid"),
+const ajv = require("../util/ajv"),
     configSchema = require("../../conf/config-schema.js"),
     util = require("util");
 
@@ -20,6 +20,7 @@ const validators = {
 //------------------------------------------------------------------------------
 // Private
 //------------------------------------------------------------------------------
+let validateSchema;
 
 /**
  * Gets a complete options schema for a rule.
@@ -79,7 +80,7 @@ function validateRuleSchema(id, localOptions, rulesContext) {
     const schema = getRuleOptionsSchema(id, rulesContext);
 
     if (!validators.rules[id] && schema) {
-        validators.rules[id] = schemaValidator(schema, { verbose: true });
+        validators.rules[id] = ajv.compile(schema);
     }
 
     const validateRule = validators.rules[id];
@@ -87,7 +88,7 @@ function validateRuleSchema(id, localOptions, rulesContext) {
     if (validateRule) {
         validateRule(localOptions);
         if (validateRule.errors) {
-            throw new Error(validateRule.errors.map(error => `\tValue "${error.value}" ${error.message}.\n`).join(""));
+            throw new Error(validateRule.errors.map(error => `\tValue "${error.data}" ${error.message}.\n`).join(""));
         }
     }
 }
@@ -158,19 +159,23 @@ function validateRules(rulesConfig, source, rulesContext) {
  * @returns {string} Formatted error message
  */
 function formatErrors(errors) {
-
     return errors.map(error => {
-        if (error.message === "has additional properties") {
-            return `Unexpected top-level property "${error.value.replace(/^data\./, "")}"`;
+        if (error.keyword === "additionalProperties") {
+            const formattedPropertyPath = error.dataPath.length ? `${error.dataPath.slice(1)}.${error.params.additionalProperty}` : error.params.additionalProperty;
+
+            return `Unexpected top-level property "${formattedPropertyPath}"`;
         }
-        if (error.message === "is the wrong type") {
-            const formattedField = error.field.replace(/^data\./, "");
-            const formattedExpectedType = typeof error.type === "string" ? error.type : error.type.join("/");
-            const formattedValue = JSON.stringify(error.value);
+        if (error.keyword === "type") {
+            const formattedField = error.dataPath.slice(1);
+            const formattedExpectedType = Array.isArray(error.schema) ? error.schema.join("/") : error.schema;
+            const formattedValue = JSON.stringify(error.data);
 
             return `Property "${formattedField}" is the wrong type (expected ${formattedExpectedType} but got \`${formattedValue}\`)`;
         }
-        return `"${error.field.replace(/^(data\.)/, "")}" ${error.message}. Value: ${JSON.stringify(error.value)}`;
+
+        const field = error.dataPath[0] === "." ? error.dataPath.slice(1) : error.dataPath;
+
+        return `"${field}" ${error.message}. Value: ${JSON.stringify(error.data)}`;
     }).map(message => `\t- ${message}.\n`).join("");
 }
 
@@ -181,10 +186,10 @@ function formatErrors(errors) {
  * @returns {void}
  */
 function validateConfigSchema(config, source) {
-    const validator = schemaValidator(configSchema, { verbose: true });
+    validateSchema = validateSchema || ajv.compile(configSchema);
 
-    if (!validator(config)) {
-        throw new Error(`${source}:\n\tESLint configuration is invalid:\n${formatErrors(validator.errors)}`);
+    if (!validateSchema(config)) {
+        throw new Error(`${source}:\n\tESLint configuration is invalid:\n${formatErrors(validateSchema.errors)}`);
     }
 }
 
diff --git a/lib/config/plugins.js b/lib/config/plugins.js
index 11852df5c..adfd8a1bb 100644
--- a/lib/config/plugins.js
+++ b/lib/config/plugins.js
@@ -156,8 +156,20 @@ class Plugins {
      * @param {string[]} pluginNames An array of plugins names.
      * @returns {void}
      * @throws {Error} If a plugin cannot be loaded.
+     * @throws {Error} If "plugins" in config is not an array
      */
     loadAll(pluginNames) {
+
+        // if "plugins" in config is not an array, throw an error so user can fix their config.
+        if (!Array.isArray(pluginNames)) {
+            const pluginNotArrayMessage = "ESLint configuration error: \"plugins\" value must be an array";
+
+            debug(`${pluginNotArrayMessage}: ${JSON.stringify(pluginNames)}`);
+
+            throw new Error(pluginNotArrayMessage);
+        }
+
+        // load each plugin by name
         pluginNames.forEach(this.load, this);
     }
 }
diff --git a/lib/rules/indent.js b/lib/rules/indent.js
index c8195c7d7..727ecfaae 100644
--- a/lib/rules/indent.js
+++ b/lib/rules/indent.js
@@ -386,18 +386,6 @@ class OffsetStorage {
     getFirstDependency(token) {
         return this.desiredOffsets[token.range[0]].from;
     }
-
-    /**
-     * Increases the offset for a token from its parent by the given amount
-     * @param {Token} token The token whose offset should be increased
-     * @param {number} amount The number of indent levels that the offset should increase by
-     * @returns {void}
-     */
-    increaseOffset(token, amount) {
-        const currentOffsetInfo = this.desiredOffsets[token.range[0]];
-
-        this.desiredOffsets[token.range[0]] = { offset: currentOffsetInfo.offset + amount, from: currentOffsetInfo.from };
-    }
 }
 
 const ELEMENT_LIST_SCHEMA = {
@@ -750,7 +738,7 @@ module.exports = {
                     const firstTokenOfPreviousElement = previousElement && getFirstToken(previousElement);
 
                     if (previousElement && sourceCode.getLastToken(previousElement).loc.start.line > startToken.loc.end.line) {
-                        offsets.matchIndentOf(firstTokenOfPreviousElement, getFirstToken(element));
+                        offsets.setDesiredOffsets(getTokensAndComments(element), firstTokenOfPreviousElement, 0);
                     }
                 }
             });
@@ -825,7 +813,7 @@ module.exports = {
                  */
                 const lastToken = sourceCode.getLastToken(node);
 
-                if (astUtils.isSemicolonToken(lastToken)) {
+                if (node.type !== "EmptyStatement" && astUtils.isSemicolonToken(lastToken)) {
                     offsets.matchIndentOf(lastParentToken, lastToken);
                 }
             }
diff --git a/lib/rules/no-regex-spaces.js b/lib/rules/no-regex-spaces.js
index 05ac86e87..09b689e8e 100644
--- a/lib/rules/no-regex-spaces.js
+++ b/lib/rules/no-regex-spaces.js
@@ -37,11 +37,11 @@ module.exports = {
          * @private
          */
         function checkRegex(node, value, valueStart) {
-            const multipleSpacesRegex = /( {2,})+?/,
+            const multipleSpacesRegex = /( {2,})( [+*{?]|[^+*{?]|$)/,
                 regexResults = multipleSpacesRegex.exec(value);
 
             if (regexResults !== null) {
-                const count = regexResults[0].length;
+                const count = regexResults[1].length;
 
                 context.report({
                     node,
diff --git a/lib/util/ajv.js b/lib/util/ajv.js
new file mode 100644
index 000000000..f9e8b9853
--- /dev/null
+++ b/lib/util/ajv.js
@@ -0,0 +1,29 @@
+/**
+ * @fileoverview The instance of Ajv validator.
+ * @author Evgeny Poberezkin
+ */
+"use strict";
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const Ajv = require("ajv"),
+    metaSchema = require("ajv/lib/refs/json-schema-draft-04.json");
+
+//------------------------------------------------------------------------------
+// Public Interface
+//------------------------------------------------------------------------------
+
+const ajv = new Ajv({
+    meta: false,
+    validateSchema: false,
+    missingRefs: "ignore",
+    verbose: true
+});
+
+ajv.addMetaSchema(metaSchema);
+// eslint-disable-next-line no-underscore-dangle
+ajv._opts.defaultMeta = metaSchema.id;
+
+module.exports = ajv;
diff --git a/package.json b/package.json
index 1f459dea8..fdd950684 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "eslint",
-  "version": "4.1.1",
+  "version": "4.2.0",
   "author": "Nicholas C. Zakas <nicholas+npm@nczconsulting.com>",
   "description": "An AST-based pattern checker for JavaScript.",
   "bin": {
@@ -10,6 +10,7 @@
   "scripts": {
     "test": "node Makefile.js test",
     "lint": "node Makefile.js lint",
+    "fuzz": "node Makefile.js fuzz",
     "release": "node Makefile.js release",
     "ci-release": "node Makefile.js ciRelease",
     "alpharelease": "node Makefile.js prerelease -- alpha",
@@ -34,6 +35,7 @@
   "homepage": "http://eslint.org",
   "bugs": "https://github.com/eslint/eslint/issues/",
   "dependencies": {
+    "ajv": "^5.2.0",
     "babel-code-frame": "^6.22.0",
     "chalk": "^1.1.3",
     "concat-stream": "^1.6.0",
@@ -50,7 +52,6 @@
     "ignore": "^3.3.3",
     "imurmurhash": "^0.1.4",
     "inquirer": "^3.0.6",
-    "is-my-json-valid": "^2.16.0",
     "is-resolvable": "^1.0.0",
     "js-yaml": "^3.8.4",
     "json-stable-stringify": "^1.0.1",
@@ -83,6 +84,7 @@
     "eslint-plugin-eslint-plugin": "^0.7.4",
     "eslint-plugin-node": "^5.1.0",
     "eslint-release": "^0.10.1",
+    "eslump": "1.6.0",
     "esprima": "^3.1.3",
     "esprima-fb": "^15001.1001.0-dev-harmony-fb",
     "istanbul": "^0.4.5",
diff --git a/tools/eslint-fuzzer.js b/tools/eslint-fuzzer.js
new file mode 100644
index 000000000..3653673f3
--- /dev/null
+++ b/tools/eslint-fuzzer.js
@@ -0,0 +1,139 @@
+/**
+ * @fileoverview A fuzzer that runs eslint on randomly-generated code samples to detect bugs
+ * @author Teddy Katz
+ */
+
+"use strict";
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const assert = require("assert");
+const lodash = require("lodash");
+const eslump = require("eslump");
+const SourceCodeFixer = require("../lib/util/source-code-fixer");
+const ruleConfigs = require("../lib/config/config-rule").createCoreRuleConfigs();
+
+//------------------------------------------------------------------------------
+// Public API
+//------------------------------------------------------------------------------
+
+/**
+ * Generates random JS code, runs ESLint on it, and returns a list of detected crashes or autofix bugs
+ * @param {Object} options Config options for fuzzing
+ * @param {number} options.count The number of fuzz iterations.
+ * @param {Object} options.linter The linter object to test with.
+ * @param {function(Object): string} [options.codeGenerator=eslump.generateRandomJS] A function to use to generate random
+ * code. Accepts an object argument with a `sourceType` key, indicating the source type of the generated code. The object
+ * might also be passed other keys.
+ * @param {boolean} [options.checkAutofixes=true] `true` if the fuzzer should check for autofix bugs. The fuzzer runs
+ * roughly 4 times slower with autofix checking enabled.
+ * @param {function()} [options.progressCallback] A function that gets called once for each code sample
+ * @returns {Object[]} A list of problems found. Each problem has the following properties:
+ * type (string): The type of problem. This is either "crash" (a rule crashes) or "autofix" (an autofix produces a syntax error)
+ * text (string): The text that ESLint should be run on to reproduce the problem
+ * config (object): The config object that should be used to reproduce the problem. The fuzzer will try to return a minimal
+ *     config (that only has one rule enabled), but this isn't always possible.
+ * error (*) The problem that occurred. For crashes, this will be the error stack. For autofix bugs, this will be
+ *     the parsing error object that was thrown when parsing the autofixed code.
+ */
+function fuzz(options) {
+    assert.strictEqual(typeof options, "object", "An options object must be provided");
+    assert.strictEqual(typeof options.count, "number", "The number of iterations (options.count) must be provided");
+    assert.strictEqual(typeof options.linter, "object", "An linter object (options.linter) must be provided");
+
+    const linter = options.linter;
+    const codeGenerator = options.codeGenerator || (genOptions => eslump.generateRandomJS(Object.assign({ comments: true, whitespace: true }, genOptions)));
+    const checkAutofixes = options.checkAutofixes !== false;
+    const progressCallback = options.progressCallback || (() => {});
+
+    /**
+     * Tries to isolate the smallest config that reproduces a problem
+     * @param {string} text The source text to lint
+     * @param {Object} config A config object that causes a crash or autofix error
+     * @returns {Object} A config object with only one rule enabled that produces the same crash or autofix error, if possible.
+     * Otherwise, the same as `config`
+     */
+    function isolateBadConfig(text, config) {
+        for (const ruleId of Object.keys(config.rules)) {
+            const reducedConfig = Object.assign({}, config, { rules: { [ruleId]: config.rules[ruleId] } });
+            let fixResult;
+
+            try {
+                fixResult = linter.verifyAndFix(text, reducedConfig, {});
+            } catch (err) {
+                return reducedConfig;
+            }
+
+            if (fixResult.messages.length === 1 && fixResult.messages[0].fatal) {
+                return reducedConfig;
+            }
+        }
+        return config;
+    }
+
+    /**
+     * Runs multipass autofix one pass at a time to find the last good source text before a fatal error occurs
+     * @param {string} originalText Syntactically valid source code that results in a syntax error or crash when autofixing with `config`
+     * @param {Object} config The config to lint with
+     * @returns {string} A possibly-modified version of originalText that results in the same syntax error or crash after only one pass
+     */
+    function isolateBadAutofixPass(originalText, config) {
+        let lastGoodText = originalText;
+        let currentText = originalText;
+
+        do {
+            let messages;
+
+            try {
+                messages = linter.verify(currentText, config);
+            } catch (err) {
+                return lastGoodText;
+            }
+
+            if (messages.length === 1 && messages[0].fatal) {
+                return lastGoodText;
+            }
+
+            lastGoodText = currentText;
+            currentText = SourceCodeFixer.applyFixes(linter.getSourceCode(), messages).output;
+        } while (lastGoodText !== currentText);
+
+        return lastGoodText;
+    }
+
+    const problems = [];
+
+    for (let i = 0; i < options.count; progressCallback(), i++) {
+        const sourceType = lodash.sample(["script", "module"]);
+        const text = codeGenerator({ sourceType });
+        const config = {
+            rules: lodash.mapValues(ruleConfigs, lodash.sample),
+            parserOptions: { sourceType, ecmaVersion: 2017 }
+        };
+
+        let autofixResult;
+
+        try {
+            if (checkAutofixes) {
+                autofixResult = linter.verifyAndFix(text, config, {});
+            } else {
+                linter.verify(text, config);
+            }
+        } catch (err) {
+            problems.push({ type: "crash", text, config: isolateBadConfig(text, config), error: err.stack });
+            continue;
+        }
+
+        if (checkAutofixes && autofixResult.fixed && autofixResult.messages.length === 1 && autofixResult.messages[0].fatal) {
+            const lastGoodText = isolateBadAutofixPass(text, config);
+
+            problems.push({ type: "autofix", text: lastGoodText, config: isolateBadConfig(lastGoodText, config), error: autofixResult.messages[0] });
+        }
+    }
+
+    return problems;
+}
+
+module.exports = fuzz;
diff --git a/tools/fuzzer-runner.js b/tools/fuzzer-runner.js
new file mode 100644
index 000000000..120f48c28
--- /dev/null
+++ b/tools/fuzzer-runner.js
@@ -0,0 +1,82 @@
+/**
+ * @fileoverview An opinionated wrapper around eslint-fuzzer
+ * @author Teddy Katz
+ */
+
+"use strict";
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const ProgressBar = require("progress");
+const fuzz = require("./eslint-fuzzer");
+const eslint = require("..");
+const linter = new eslint.Linter();
+
+//------------------------------------------------------------------------------
+// Helpers
+//------------------------------------------------------------------------------
+
+// An estimate of how many times faster it is to do a crash-only fuzzer run versus an autofixing run.
+const ESTIMATED_CRASH_AUTOFIX_PERFORMANCE_RATIO = 4;
+
+// The number of crash-only tests to run for each autofix test. Right now, this is mostly arbitrary.
+const CRASH_AUTOFIX_TEST_COUNT_RATIO = 3;
+
+//------------------------------------------------------------------------------
+// Public API
+//------------------------------------------------------------------------------
+
+/**
+ * Runs the fuzzer and outputs a progress bar
+ * @param {Object} [options] Options for the fuzzer
+ * @param {number} [options.amount=300] A positive integer indicating how much testing to do. Larger values result in a higher
+ * chance of finding bugs, but cause the testing to take longer (linear increase). With the default value, the fuzzer
+ * takes about 15 seconds to run.
+ * @returns {Object[]} A list of objects, where each object represents a problem detected by the fuzzer. The objects have the same
+ * schema as objects returned from eslint-fuzzer.
+ */
+function run(options) {
+    const amount = options && options.amount || 300;
+
+    const crashTestCount = amount * CRASH_AUTOFIX_TEST_COUNT_RATIO;
+    const autofixTestCount = amount;
+
+    /*
+     * To keep the progress bar moving at a roughly constant speed, apply a different weight for finishing
+     * a crash-only fuzzer run versus an autofix fuzzer run.
+     */
+    const progressBar = new ProgressBar(
+        "Fuzzing rules [:bar] :percent, :elapseds elapsed, eta :etas",
+        { width: 30, total: crashTestCount + autofixTestCount * ESTIMATED_CRASH_AUTOFIX_PERFORMANCE_RATIO }
+    );
+
+    // Start displaying the progress bar.
+    progressBar.tick(0);
+
+    const crashTestResults = fuzz({
+        linter,
+        count: crashTestCount,
+        checkAutofixes: false,
+        progressCallback: () => {
+            progressBar.tick(1);
+            progressBar.render();
+        }
+    });
+
+    const autofixTestResults = fuzz({
+        linter,
+        count: autofixTestCount,
+        checkAutofixes: true,
+        progressCallback: () => {
+            progressBar.tick(ESTIMATED_CRASH_AUTOFIX_PERFORMANCE_RATIO);
+            progressBar.render();
+        }
+    });
+
+    return crashTestResults.concat(autofixTestResults);
+
+}
+
+module.exports = { run };


Test patch:
diff --git a/lib/testers/rule-tester.js b/lib/testers/rule-tester.js
index 9d747e96a..fc6781fdc 100644
--- a/lib/testers/rule-tester.js
+++ b/lib/testers/rule-tester.js
@@ -44,10 +44,9 @@ const lodash = require("lodash"),
     assert = require("assert"),
     util = require("util"),
     validator = require("../config/config-validator"),
-    validate = require("is-my-json-valid"),
+    ajv = require("../util/ajv"),
     Linter = require("../linter"),
     Environments = require("../config/environments"),
-    metaSchema = require("../../conf/json-schema-schema.json"),
     SourceCodeFixer = require("../util/source-code-fixer");
 
 //------------------------------------------------------------------------------
@@ -73,8 +72,6 @@ const RuleTesterParameters = [
     "output"
 ];
 
-const validateSchema = validate(metaSchema, { verbose: true });
-
 const hasOwnProperty = Function.call.bind(Object.hasOwnProperty);
 
 /**
@@ -318,12 +315,16 @@ class RuleTester {
             const schema = validator.getRuleOptionsSchema(ruleName, linter.rules);
 
             if (schema) {
-                validateSchema(schema);
+                ajv.validateSchema(schema);
+
+                if (ajv.errors) {
+                    const errors = ajv.errors.map(error => {
+                        const field = error.dataPath[0] === "." ? error.dataPath.slice(1) : error.dataPath;
+
+                        return `\t${field}: ${error.message}`;
+                    }).join("\n");
 
-                if (validateSchema.errors) {
-                    throw new Error([
-                        `Schema for rule ${ruleName} is invalid:`
-                    ].concat(validateSchema.errors.map(error => `\t${error.field}: ${error.message}`)).join("\n"));
+                    throw new Error([`Schema for rule ${ruleName} is invalid:`, errors]);
                 }
             }
 
diff --git a/tests/conf/config-schema.js b/tests/conf/config-schema.js
new file mode 100644
index 000000000..3fdb4bb03
--- /dev/null
+++ b/tests/conf/config-schema.js
@@ -0,0 +1,26 @@
+/**
+ * @fileoverview Tests for config-schema.
+ * @author Evgeny Poberezkin
+ */
+
+"use strict";
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const ajv = require("../../lib/util/ajv"),
+    configSchema = require("../../conf/config-schema.js"),
+    assert = require("assert");
+
+//------------------------------------------------------------------------------
+// Tests
+//------------------------------------------------------------------------------
+
+describe("config-schema", () => {
+    it("should be valid against meta-schema", () => {
+        const valid = ajv.validateSchema(configSchema);
+
+        assert.strictEqual(valid, true);
+    });
+});
diff --git a/tests/lib/config/config-validator.js b/tests/lib/config/config-validator.js
index d220fdb93..5e42823b2 100644
--- a/tests/lib/config/config-validator.js
+++ b/tests/lib/config/config-validator.js
@@ -350,7 +350,7 @@ describe("Validator", () => {
 
                 const fn = validator.validate.bind(null, { rules: { "mock-no-options-rule": [2, "extra"] } }, "tests", linter.rules, linter.environments);
 
-                assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-no-options-rule\" is invalid:\n\tValue \"extra\" has more items than allowed.\n");
+                assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-no-options-rule\" is invalid:\n\tValue \"extra\" should NOT have more than 0 items.\n");
             });
         });
 
@@ -370,31 +370,31 @@ describe("Validator", () => {
             it("should throw if override does not specify files", () => {
                 const fn = validator.validate.bind(null, { overrides: [{ rules: {} }] }, "tests", linter.rules, linter.environments);
 
-                assert.throws(fn, "tests:\n\tESLint configuration is invalid:\n\t- \"overrides.0.files\" is required. Value: {\"rules\":{}}.\n");
+                assert.throws(fn, "tests:\n\tESLint configuration is invalid:\n\t- \"overrides[0]\" should have required property 'files'. Value: {\"rules\":{}}.\n");
             });
 
             it("should throw if override has an empty files array", () => {
                 const fn = validator.validate.bind(null, { overrides: [{ files: [] }] }, "tests", linter.rules, linter.environments);
 
-                assert.throws(fn, "tests:\n\tESLint configuration is invalid:\n\t- \"overrides.0.files\" no (or more than one) schemas match. Value: [].\n");
+                assert.throws(fn, "tests:\n\tESLint configuration is invalid:\n\t- Property \"overrides[0].files\" is the wrong type (expected string but got `[]`).\n\t- \"overrides[0].files\" should NOT have less than 1 items. Value: [].\n\t- \"overrides[0].files\" should match exactly one schema in oneOf. Value: [].\n");
             });
 
             it("should throw if override has nested overrides", () => {
                 const fn = validator.validate.bind(null, { overrides: [{ files: "*", overrides: [{ files: "*", rules: {} }] }] }, "tests", linter.rules, linter.environments);
 
-                assert.throws(fn, "tests:\n\tESLint configuration is invalid:\n\t- Unexpected top-level property \"overrides[j].overrides\".\n");
+                assert.throws(fn, "tests:\n\tESLint configuration is invalid:\n\t- Unexpected top-level property \"overrides[0].overrides\".\n");
             });
 
             it("should throw if override extends", () => {
                 const fn = validator.validate.bind(null, { overrides: [{ files: "*", extends: "eslint-recommended" }] }, "tests", linter.rules, linter.environments);
 
-                assert.throws(fn, "tests:\n\tESLint configuration is invalid:\n\t- Unexpected top-level property \"overrides[j].extends\".\n");
+                assert.throws(fn, "tests:\n\tESLint configuration is invalid:\n\t- Unexpected top-level property \"overrides[0].extends\".\n");
             });
 
             it("should throw if override tries to set root", () => {
                 const fn = validator.validate.bind(null, { overrides: [{ files: "*", root: "true" }] }, "tests", linter.rules, linter.environments);
 
-                assert.throws(fn, "tests:\n\tESLint configuration is invalid:\n\t- Unexpected top-level property \"overrides[j].root\".\n");
+                assert.throws(fn, "tests:\n\tESLint configuration is invalid:\n\t- Unexpected top-level property \"overrides[0].root\".\n");
             });
         });
 
@@ -450,13 +450,13 @@ describe("Validator", () => {
         it("should throw for incorrect configuration values", () => {
             const fn = validator.validateRuleOptions.bind(null, "mock-rule", [2, "frist"], "tests", linter.rules);
 
-            assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tValue \"frist\" must be an enum value.\n");
+            assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tValue \"frist\" should be equal to one of the allowed values.\n");
         });
 
         it("should throw for too many configuration values", () => {
             const fn = validator.validateRuleOptions.bind(null, "mock-rule", [2, "first", "second"], "tests", linter.rules);
 
-            assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tValue \"first,second\" has more items than allowed.\n");
+            assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tValue \"first,second\" should NOT have more than 1 items.\n");
         });
 
     });
diff --git a/tests/lib/config/plugins.js b/tests/lib/config/plugins.js
index 00b53af72..4b165ea4e 100644
--- a/tests/lib/config/plugins.js
+++ b/tests/lib/config/plugins.js
@@ -236,6 +236,10 @@ describe("Plugins", () => {
             assert.equal(rules.get("example2/bar"), plugin2.rules.bar);
         });
 
+        it("should throw an error if plugins is not an array", () => {
+            assert.throws(() => StubbedPlugins.loadAll("example1"), "\"plugins\" value must be an array");
+        });
+
     });
 
     describe("removePrefix()", () => {
diff --git a/tests/lib/rules/indent.js b/tests/lib/rules/indent.js
index 17e5b5357..0001c76fd 100644
--- a/tests/lib/rules/indent.js
+++ b/tests/lib/rules/indent.js
@@ -1470,6 +1470,27 @@ ruleTester.run("indent", rule, {
             `,
             options: [4, { VariableDeclarator: 0, SwitchCase: 1 }]
         },
+        {
+            code: unIndent`
+                [[
+                ], function(
+                    foo
+                ) {}
+                ]
+            `
+        },
+        {
+            code: unIndent`
+                define([
+                    'foo'
+                ], function(
+                    bar
+                ) {
+                    baz;
+                }
+                )
+            `
+        },
         {
             code: unIndent`
                 const func = function (opts) {
@@ -3343,6 +3364,12 @@ ruleTester.run("indent", rule, {
                 ; [1, 2, 3].map(baz)
             `
         },
+        {
+            code: unIndent`
+                if (foo)
+                    ;
+            `
+        },
         {
             code: "x => {}"
         },
@@ -5582,6 +5609,46 @@ ruleTester.run("indent", rule, {
                 [5, 0, 2, "Punctuator"]
             ])
         },
+        {
+            code: unIndent`
+                [[
+                ], function(
+                        foo
+                    ) {}
+                ]
+            `,
+            output: unIndent`
+                [[
+                ], function(
+                    foo
+                ) {}
+                ]
+            `,
+            errors: expectedErrors([[3, 4, 8, "Identifier"], [4, 0, 4, "Punctuator"]])
+        },
+        {
+            code: unIndent`
+                define([
+                    'foo'
+                ], function(
+                        bar
+                    ) {
+                    baz;
+                }
+                )
+            `,
+            output: unIndent`
+                define([
+                    'foo'
+                ], function(
+                    bar
+                ) {
+                    baz;
+                }
+                )
+            `,
+            errors: expectedErrors([[4, 4, 8, "Identifier"], [5, 0, 4, "Punctuator"]])
+        },
         {
             code: unIndent`
                 while (1 < 2)
@@ -8028,6 +8095,17 @@ ruleTester.run("indent", rule, {
             `,
             errors: expectedErrors([3, 0, 4, "Punctuator"])
         },
+        {
+            code: unIndent`
+                if (foo)
+                ;
+            `,
+            output: unIndent`
+                if (foo)
+                    ;
+            `,
+            errors: expectedErrors([2, 4, 0, "Punctuator"])
+        },
         {
             code: unIndent`
                 import {foo}
diff --git a/tests/lib/rules/no-regex-spaces.js b/tests/lib/rules/no-regex-spaces.js
index 1ed316f8a..72e1fd4e2 100644
--- a/tests/lib/rules/no-regex-spaces.js
+++ b/tests/lib/rules/no-regex-spaces.js
@@ -23,7 +23,8 @@ ruleTester.run("no-regex-spaces", rule, {
         "var foo = RegExp('bar\t\t\tbaz');",
         "var foo = new RegExp('bar\t\t\tbaz');",
         "var RegExp = function() {}; var foo = new RegExp('bar   baz');",
-        "var RegExp = function() {}; var foo = RegExp('bar   baz');"
+        "var RegExp = function() {}; var foo = RegExp('bar   baz');",
+        "var foo = /  +/;"
     ],
 
     invalid: [
@@ -69,6 +70,26 @@ ruleTester.run("no-regex-spaces", rule, {
                     type: "CallExpression"
                 }
             ]
+        },
+        {
+            code: "var foo = /bar    ?baz/;",
+            output: "var foo = /bar {3} ?baz/;",
+            errors: [
+                {
+                    message: "Spaces are hard to count. Use {3}.",
+                    type: "Literal"
+                }
+            ]
+        },
+        {
+            code: "var foo = new RegExp('bar    ');",
+            output: "var foo = new RegExp('bar {4}');",
+            errors: [
+                {
+                    message: "Spaces are hard to count. Use {4}.",
+                    type: "NewExpression"
+                }
+            ]
         }
     ]
 });
diff --git a/tests/lib/testers/rule-tester.js b/tests/lib/testers/rule-tester.js
index b2ca45e9e..86bf40cde 100644
--- a/tests/lib/testers/rule-tester.js
+++ b/tests/lib/testers/rule-tester.js
@@ -619,7 +619,7 @@ describe("RuleTester", () => {
                     { code: "var answer = 6 * 7;", options: ["bar"], errors: [{ message: "Expected nothing." }] }
                 ]
             });
-        }, /Schema for rule .* is invalid/);
+        }, "Schema for rule no-invalid-schema is invalid:,\titems: should be object\n\titems[0].enum: should NOT have less than 1 items\n\titems: should match some schema in anyOf");
 
     });
 
@@ -635,7 +635,7 @@ describe("RuleTester", () => {
                     { code: "var answer = 6 * 7;", options: ["bar"], errors: [{ message: "Expected foo." }] }
                 ]
             });
-        }, /Value "bar" must be an enum value./);
+        }, /Value "bar" should be equal to one of the allowed values./);
 
     });
 
diff --git a/tests/tools/eslint-fuzzer.js b/tests/tools/eslint-fuzzer.js
new file mode 100644
index 000000000..8ea25e0f5
--- /dev/null
+++ b/tests/tools/eslint-fuzzer.js
@@ -0,0 +1,258 @@
+"use strict";
+
+const assert = require("chai").assert;
+const eslint = require("../..");
+const espree = require("espree");
+const sinon = require("sinon");
+const configRule = require("../../lib/config/config-rule");
+
+describe("eslint-fuzzer", function() {
+    let fakeRule, fuzz;
+
+    /*
+     * These tests take awhile because isolating which rule caused an error requires running eslint up to hundreds of
+     * times, one rule at a time.
+     */
+    this.timeout(15000); // eslint-disable-line no-invalid-this
+
+    const linter = new eslint.Linter();
+    const coreRules = linter.getRules();
+    const fixableRuleNames = Array.from(coreRules)
+        .filter(rulePair => rulePair[1].meta && rulePair[1].meta.fixable)
+        .map(rulePair => rulePair[0]);
+    const CRASH_BUG = new TypeError("error thrown from a rule");
+
+    // A comment to disable all core fixable rules
+    const disableFixableRulesComment = `// eslint-disable-line ${fixableRuleNames.join(",")}`;
+
+    before(() => {
+        const realCoreRuleConfigs = configRule.createCoreRuleConfigs();
+
+        // Make sure the config generator generates a config for "test-fuzzer-rule"
+        sinon.stub(configRule, "createCoreRuleConfigs").returns(Object.assign(realCoreRuleConfigs, { "test-fuzzer-rule": [2] }));
+
+        // Create a closure around `fakeRule` so that tests can reassign it and have the changes take effect.
+        linter.defineRule("test-fuzzer-rule", Object.assign(context => fakeRule(context), { meta: { fixable: "code" } }));
+
+        fuzz = require("../../tools/eslint-fuzzer");
+    });
+
+    after(() => {
+        linter.reset();
+        configRule.createCoreRuleConfigs.restore();
+    });
+
+    describe("when running in crash-only mode", () => {
+        describe("when a rule crashes on the given input", () => {
+            it("should report the crash with a minimal config", () => {
+                fakeRule = () => ({
+                    Program() {
+                        throw CRASH_BUG;
+                    }
+                });
+
+                const results = fuzz({ count: 1, codeGenerator: () => "foo", checkAutofixes: false, linter });
+
+                assert.strictEqual(results.length, 1);
+                assert.strictEqual(results[0].type, "crash");
+                assert.strictEqual(results[0].text, "foo");
+                assert.deepEqual(results[0].config.rules, { "test-fuzzer-rule": 2 });
+                assert.strictEqual(results[0].error, CRASH_BUG.stack);
+            });
+        });
+
+        describe("when no rules crash", () => {
+            it("should return an empty array", () => {
+                fakeRule = () => ({});
+
+                assert.deepEqual(fuzz({ count: 1, codeGenerator: () => "foo", checkAutofixes: false, linter }), []);
+            });
+        });
+    });
+
+    describe("when running in crash-and-autofix mode", () => {
+        const INVALID_SYNTAX = "this is not valid javascript syntax";
+        let expectedSyntaxError;
+
+        try {
+            espree.parse(INVALID_SYNTAX);
+        } catch (err) {
+            expectedSyntaxError = err;
+        }
+
+        describe("when a rule crashes on the given input", () => {
+            it("should report the crash with a minimal config", () => {
+                fakeRule = () => ({
+                    Program() {
+                        throw CRASH_BUG;
+                    }
+                });
+
+                const results = fuzz({ count: 1, codeGenerator: () => "foo", checkAutofixes: false, linter });
+
+                assert.strictEqual(results.length, 1);
+                assert.strictEqual(results[0].type, "crash");
+                assert.strictEqual(results[0].text, "foo");
+                assert.deepEqual(results[0].config.rules, { "test-fuzzer-rule": 2 });
+                assert.strictEqual(results[0].error, CRASH_BUG.stack);
+            });
+        });
+
+        describe("when a rule's autofix produces valid syntax", () => {
+            it("does not report any errors", () => {
+
+                // Replaces programs that start with "foo" with "bar"
+                fakeRule = context => ({
+                    Program(node) {
+                        if (context.getSourceCode().text.startsWith("foo")) {
+                            context.report({
+                                node,
+                                message: "no foos allowed",
+                                fix: fixer => fixer.replaceText(node, `bar ${disableFixableRulesComment}`)
+                            });
+                        }
+                    }
+                });
+
+                const results = fuzz({
+                    count: 1,
+
+                    /*
+                     * To ensure that no other rules produce a different autofix and mess up the test, add a big disable
+                     * comment for all core fixable rules.
+                     */
+                    codeGenerator: () => `foo ${disableFixableRulesComment}`,
+                    checkAutofixes: true,
+                    linter
+                });
+
+                assert.deepEqual(results, []);
+            });
+        });
+
+        describe("when a rule's autofix produces invalid syntax on the first pass", () => {
+            it("reports an autofix error with a minimal config", () => {
+
+                // Replaces programs that start with "foo" with invalid syntax
+                fakeRule = context => ({
+                    Program(node) {
+                        const sourceCode = context.getSourceCode();
+
+                        if (sourceCode.text.startsWith("foo")) {
+                            context.report({
+                                node,
+                                message: "no foos allowed",
+                                fix: fixer => fixer.replaceTextRange([0, sourceCode.text.length], INVALID_SYNTAX)
+                            });
+                        }
+                    }
+                });
+
+                const results = fuzz({
+                    count: 1,
+                    codeGenerator: () => `foo ${disableFixableRulesComment}`,
+                    checkAutofixes: true,
+                    linter
+                });
+
+                assert.strictEqual(results.length, 1);
+                assert.strictEqual(results[0].type, "autofix");
+                assert.strictEqual(results[0].text, `foo ${disableFixableRulesComment}`);
+                assert.deepEqual(results[0].config.rules, { "test-fuzzer-rule": 2 });
+                assert.deepEqual(results[0].error, {
+                    ruleId: null,
+                    fatal: true,
+                    severity: 2,
+                    source: INVALID_SYNTAX,
+                    message: `Parsing error: ${expectedSyntaxError.message}`,
+                    line: expectedSyntaxError.lineNumber,
+                    column: expectedSyntaxError.column
+                });
+            });
+        });
+
+        describe("when a rule's autofix produces invalid syntax on the second pass", () => {
+            it("reports an autofix error with a minimal config and the text from the second pass", () => {
+                const intermediateCode = `bar ${disableFixableRulesComment}`;
+
+                // Replaces programs that start with "foo" with invalid syntax
+                fakeRule = context => ({
+                    Program(node) {
+                        const sourceCode = context.getSourceCode();
+
+                        if (sourceCode.text.startsWith("foo") || sourceCode.text.startsWith("bar")) {
+                            context.report({
+                                node,
+                                message: "no foos allowed",
+                                fix(fixer) {
+                                    return fixer.replaceTextRange(
+                                        [0, sourceCode.text.length],
+                                        sourceCode.text === intermediateCode ? INVALID_SYNTAX : intermediateCode
+                                    );
+                                }
+                            });
+                        }
+                    }
+                });
+
+                const results = fuzz({
+                    count: 1,
+                    codeGenerator: () => `foo ${disableFixableRulesComment}`,
+                    checkAutofixes: true,
+                    linter
+                });
+
+                assert.strictEqual(results.length, 1);
+                assert.strictEqual(results[0].type, "autofix");
+                assert.strictEqual(results[0].text, intermediateCode);
+                assert.deepEqual(results[0].config.rules, { "test-fuzzer-rule": 2 });
+                assert.deepEqual(results[0].error, {
+                    ruleId: null,
+                    fatal: true,
+                    severity: 2,
+                    source: INVALID_SYNTAX,
+                    message: `Parsing error: ${expectedSyntaxError.message}`,
+                    line: expectedSyntaxError.lineNumber,
+                    column: expectedSyntaxError.column
+                });
+            });
+        });
+
+        describe("when a rule crashes on the second autofix pass", () => {
+            it("reports a crash error with a minimal config", () => {
+
+                // Replaces programs that start with "foo" with invalid syntax
+                fakeRule = context => ({
+                    Program(node) {
+                        const sourceCode = context.getSourceCode();
+
+                        if (sourceCode.text.startsWith("foo")) {
+                            context.report({
+                                node,
+                                message: "no foos allowed",
+                                fix: fixer => fixer.replaceText(node, "bar")
+                            });
+                        } else if (sourceCode.text.startsWith("bar")) {
+                            throw CRASH_BUG;
+                        }
+                    }
+                });
+
+                const results = fuzz({
+                    count: 1,
+                    codeGenerator: () => `foo ${disableFixableRulesComment}`,
+                    checkAutofixes: true,
+                    linter
+                });
+
+                assert.strictEqual(results.length, 1);
+                assert.strictEqual(results[0].type, "crash");
+
+                // TODO: (not-an-aardvark) It might be more useful to output the intermediate code here.
+                assert.strictEqual(results[0].text, `foo ${disableFixableRulesComment}`);
+                assert.deepEqual(results[0].config.rules, { "test-fuzzer-rule": 2 });
+                assert.strictEqual(results[0].error, CRASH_BUG.stack);
+            });
+        });
+    });
+});