Repository: eslint/eslint
Pull request #8756 (2017-06-17 17:36:26)

Fix: fix indent errors on multiline destructure
**What is the purpose of this pull request? (put an "X" next to item)**

[X] Bug fix ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/bug-report.md))

**Tell us about your environment**

* **ESLint Version:4.0.0**
* **Node Version:6.9.5**
* **npm Version:3.10.10**

**What parser (default, Babel-ESLint, etc.) are you using?**
default
**Please show your full configuration:**

**What did you do? Please include the actual source code causing the issue.**
I have a fix for errors that were being reported on multiline destructure as reported in #8729 
I Added test cases for a bug where errors were reported on indentation on multiline destructure


**Is there anything you'd like reviewers to focus on?**
Not sure if my approach is correct. Any feedback is welcome.



eslintbot: Thanks for the pull request, @VictorHom! 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
eslintbot: LGTM
eslintbot: LGTM
eslintbot: LGTM

Patch:
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
index e36b18286..8675e4491 100644
--- a/.github/ISSUE_TEMPLATE.md
+++ b/.github/ISSUE_TEMPLATE.md
@@ -1,11 +1,13 @@
 <!--
-This template is for bug reports. If you are reporting a bug, please continue on. If you are here for another reason, please see below:
+    ESLint adheres to the [JS Foundation Code of Conduct](https://js.foundation/community/code-of-conduct).
 
-1. To propose a new rule: http://eslint.org/docs/developer-guide/contributing/new-rules
-2. To request a change: http://eslint.org/docs/developer-guide/contributing/changes
-3. If you have any questions, please stop by our chatroom: https://gitter.im/eslint/eslint
+    This template is for bug reports. If you are reporting a bug, please continue on. If you are here for another reason, please see below:
 
-Note that leaving sections blank will make it difficult for us to troubleshoot and we may have to close the issue.
+    1. To propose a new rule: http://eslint.org/docs/developer-guide/contributing/new-rules
+    2. To request a change: http://eslint.org/docs/developer-guide/contributing/changes
+    3. If you have any questions, please stop by our chatroom: https://gitter.im/eslint/eslint
+
+    Note that leaving sections blank will make it difficult for us to troubleshoot and we may have to close the issue.
 -->
 
 **Tell us about your environment**
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 3b0da9eff..6b7012737 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,3 +1,7 @@
+<!--
+    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
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 000000000..2fc80ddcd
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,1 @@
+This project adheres to the [JS Foundation Code of Conduct](https://js.foundation/community/code-of-conduct).
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 2f288ce80..ffe6b574c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,6 +2,10 @@
 
 Please be sure to read the contribution guidelines before making or requesting a change.
 
+## Code of Conduct
+
+This project adheres to the [JS Foundation Code of Conduct](https://js.foundation/community/code-of-conduct). We kindly request that you read over our code of conduct before contributing.
+
 ## Filing Issues
 
 Before filing an issue, please be sure to read the guidelines for what you're reporting:
diff --git a/README.md b/README.md
index 7fb4fa343..7304f1b70 100644
--- a/README.md
+++ b/README.md
@@ -137,6 +137,10 @@ These folks keep the project moving and are resources for help.
 
 We have scheduled releases every two weeks on Friday or Saturday.
 
+## Code of Conduct
+
+ESLint adheres to the [JS Foundation Code of Conduct](https://js.foundation/community/code-of-conduct).
+
 ## Filing Issues
 
 Before filing an issue, please be sure to read the guidelines for what you're reporting:
diff --git a/conf/default-config-options.js b/conf/default-config-options.js
index 63d28c48b..96fe25ce6 100644
--- a/conf/default-config-options.js
+++ b/conf/default-config-options.js
@@ -25,9 +25,5 @@ module.exports = deepFreeze({
     rules: {},
     settings: {},
     parser: "espree",
-    parserOptions: {
-        ecmaVersion: 5,
-        sourceType: "script",
-        ecmaFeatures: {}
-    }
+    parserOptions: {}
 });
diff --git a/docs/developer-guide/contributing/README.md b/docs/developer-guide/contributing/README.md
index dcd275187..384e5b9e4 100644
--- a/docs/developer-guide/contributing/README.md
+++ b/docs/developer-guide/contributing/README.md
@@ -4,6 +4,10 @@ One of the great things about open source projects is that anyone can contribute
 
 This guide is intended for anyone who wants to contribute to an ESLint project. Please read it carefully as it answers a lot of the questions many newcomers have when first working with our projects.
 
+## Read the [Code of Conduct](https://js.foundation/community/code-of-conduct)
+
+ESLint welcomes contributions from everyone and adheres to the [JS Foundation Code of Conduct](https://js.foundation/community/code-of-conduct). We kindly request that you read over our code of conduct before contributing.
+
 ## [Signing the CLA](https://contribute.jquery.org/cla)
 
 In order to submit code or documentation to an ESLint project, please electronically sign the [Contributor License Agreement](https://contribute.jquery.org/cla). The CLA is you giving us permission to use your contribution.
diff --git a/docs/developer-guide/working-with-rules.md b/docs/developer-guide/working-with-rules.md
index 26c978f7c..67b5e7952 100644
--- a/docs/developer-guide/working-with-rules.md
+++ b/docs/developer-guide/working-with-rules.md
@@ -198,6 +198,15 @@ The `fixer` object has the following methods:
 * `replaceText(nodeOrToken, text)` - replaces the text in the given node or token
 * `replaceTextRange(range, text)` - replaces the text in the given range
 
+The above methods return a `fixing` object.
+The `fix()` function can return the following values:
+
+* A `fixing` object.
+* An array which includes `fixing` objects.
+* An iterable object which enumerates `fixing` objects. Especially, the `fix()` function can be a generator.
+
+If you make a `fix()` function which returns multiple `fixing` objects, those `fixing` objects must not be overlapped.
+
 Best practices for fixes:
 
 1. Avoid any fixes that could change the runtime behavior of code and cause it to stop working.
@@ -286,6 +295,7 @@ Once you have an instance of `SourceCode`, you can use the methods on it to work
 * `getNodeByRangeIndex(index)` - returns the deepest node in the AST containing the given source index.
 * `getLocFromIndex(index)` - returns an object with `line` and `column` properties, corresponding to the location of the given source index. `line` is 1-based and `column` is 0-based.
 * `getIndexFromLoc(loc)` - returns the index of a given location in the source code, where `loc` is an object with a 1-based `line` key and a 0-based `column` key.
+* `commentsExistBetween(nodeOrToken1, nodeOrToken2)` - returns `true` if comments exist between two nodes.
 
 > `skipOptions` is an object which has 3 properties; `skip`, `includeComments`, and `filter`. Default is `{skip: 0, includeComments: false, filter: null}`.
 > - `skip` is a positive integer, the number of skipping tokens. If `filter` option is given at the same time, it doesn't count filtered tokens as skipped.
diff --git a/lib/cli-engine.js b/lib/cli-engine.js
index f53c2e76a..ebca4cb81 100644
--- a/lib/cli-engine.js
+++ b/lib/cli-engine.js
@@ -318,7 +318,7 @@ function getCacheFile(cacheFile, cwd) {
     cacheFile = path.normalize(cacheFile);
 
     const resolvedCacheFile = path.resolve(cwd, cacheFile);
-    const looksLikeADirectory = cacheFile[cacheFile.length - 1 ] === path.sep;
+    const looksLikeADirectory = cacheFile[cacheFile.length - 1] === path.sep;
 
     /**
      * return the name for the cache file in case the provided parameter is a directory
diff --git a/lib/code-path-analysis/code-path-state.js b/lib/code-path-analysis/code-path-state.js
index a5adb554f..7c8abb207 100644
--- a/lib/code-path-analysis/code-path-state.js
+++ b/lib/code-path-analysis/code-path-state.js
@@ -240,7 +240,7 @@ class CodePathState {
         this.breakContext = null;
 
         this.currentSegments = [];
-        this.initialSegment = this.forkContext.head[ 0 ];
+        this.initialSegment = this.forkContext.head[0];
 
         // returnedSegments and thrownSegments push elements into finalSegments also.
         const final = this.finalSegments = [];
diff --git a/lib/config.js b/lib/config.js
index 5407134d6..5c04ee4e1 100644
--- a/lib/config.js
+++ b/lib/config.js
@@ -198,7 +198,7 @@ class Config {
         this.useEslintrc = (options.useEslintrc !== false);
 
         this.env = (options.envs || []).reduce((envs, name) => {
-            envs[ name ] = true;
+            envs[name] = true;
             return envs;
         }, {});
 
diff --git a/lib/linter.js b/lib/linter.js
index 0b0d06a86..d2f1f4657 100755
--- a/lib/linter.js
+++ b/lib/linter.js
@@ -455,8 +455,8 @@ function normalizeEcmaVersion(ecmaVersion, isModule) {
  */
 function prepareConfig(config, envContext) {
     config.globals = config.globals || {};
-    const copiedRules = Object.assign({}, defaultConfig.rules);
-    let parserOptions = Object.assign({}, defaultConfig.parserOptions);
+    const copiedRules = {};
+    let parserOptions = {};
 
     if (typeof config.rules === "object") {
         Object.keys(config.rules).forEach(k => {
diff --git a/lib/rule-context.js b/lib/rule-context.js
index 99221666a..66987b867 100644
--- a/lib/rule-context.js
+++ b/lib/rule-context.js
@@ -8,6 +8,7 @@
 // Requirements
 //------------------------------------------------------------------------------
 
+const assert = require("assert");
 const ruleFixer = require("./util/rule-fixer");
 
 //------------------------------------------------------------------------------
@@ -60,6 +61,75 @@ const PASSTHROUGHS = [
 // Rule 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 eslint object.
@@ -120,12 +190,7 @@ class RuleContext {
         // check to see if it's a new style call
         if (arguments.length === 1) {
             const descriptor = nodeOrDescriptor;
-            let fix = null;
-
-            // if there's a fix specified, get it
-            if (typeof descriptor.fix === "function") {
-                fix = descriptor.fix(ruleFixer);
-            }
+            const fix = getFix(descriptor, this.getSourceCode());
 
             this.eslint.report(
                 this.id,
diff --git a/lib/rules/arrow-body-style.js b/lib/rules/arrow-body-style.js
index f2f14245b..1630b8937 100644
--- a/lib/rules/arrow-body-style.js
+++ b/lib/rules/arrow-body-style.js
@@ -65,6 +65,29 @@ module.exports = {
         const requireReturnForObjectLiteral = options[1] && options[1].requireReturnForObjectLiteral;
         const sourceCode = context.getSourceCode();
 
+        /**
+         * Checks whether the given node has ASI problem or not.
+         * @param {Token} token The token to check.
+         * @returns {boolean} `true` if it changes semantics if `;` or `}` followed by the token are removed.
+         */
+        function hasASIProblem(token) {
+            return token && token.type === "Punctuator" && /^[([/`+-]/.test(token.value);
+        }
+
+        /**
+         * Gets the closing parenthesis which is the pair of the given opening parenthesis.
+         * @param {Token} token The opening parenthesis token to get.
+         * @returns {Token} The found closing parenthesis token.
+         */
+        function findClosingParen(token) {
+            let node = sourceCode.getNodeByRangeIndex(token.range[1]);
+
+            while (!astUtils.isParenthesised(sourceCode, node)) {
+                node = node.parent;
+            }
+            return sourceCode.getTokenAfter(node);
+        }
+
         /**
          * Determines whether a arrow function body needs braces
          * @param {ASTNode} node The arrow function node.
@@ -91,47 +114,55 @@ module.exports = {
                         loc: arrowBody.loc.start,
                         message: "Unexpected block statement surrounding arrow body.",
                         fix(fixer) {
-                            if (blockBody.length !== 1 || blockBody[0].type !== "ReturnStatement" || !blockBody[0].argument) {
-                                return null;
+                            const fixes = [];
+
+                            if (blockBody.length !== 1 ||
+                                blockBody[0].type !== "ReturnStatement" ||
+                                !blockBody[0].argument ||
+                                hasASIProblem(sourceCode.getTokenAfter(arrowBody))
+                            ) {
+                                return fixes;
                             }
 
-                            const sourceText = sourceCode.getText();
-                            const returnKeyword = sourceCode.getFirstToken(blockBody[0]);
-                            const firstValueToken = sourceCode.getTokenAfter(returnKeyword);
-                            let lastValueToken = sourceCode.getLastToken(blockBody[0]);
-
-                            if (astUtils.isSemicolonToken(lastValueToken)) {
-
-                                /* The last token of the returned value is the last token of the ReturnExpression (if
-                                 * the ReturnExpression has no semicolon), or the second-to-last token (if the ReturnExpression
-                                 * has a semicolon).
-                                 */
-                                lastValueToken = sourceCode.getTokenBefore(lastValueToken);
+                            const openingBrace = sourceCode.getFirstToken(arrowBody);
+                            const closingBrace = sourceCode.getLastToken(arrowBody);
+                            const firstValueToken = sourceCode.getFirstToken(blockBody[0], 1);
+                            const lastValueToken = sourceCode.getLastToken(blockBody[0]);
+                            const commentsExist =
+                                sourceCode.commentsExistBetween(openingBrace, firstValueToken) ||
+                                sourceCode.commentsExistBetween(lastValueToken, closingBrace);
+
+                            // Remove tokens around the return value.
+                            // If comments don't exist, remove extra spaces as well.
+                            if (commentsExist) {
+                                fixes.push(
+                                    fixer.remove(openingBrace),
+                                    fixer.remove(closingBrace),
+                                    fixer.remove(sourceCode.getTokenAfter(openingBrace)) // return keyword
+                                );
+                            } else {
+                                fixes.push(
+                                    fixer.removeRange([openingBrace.range[0], firstValueToken.range[0]]),
+                                    fixer.removeRange([lastValueToken.range[1], closingBrace.range[1]])
+                                );
                             }
 
-                            const tokenAfterArrowBody = sourceCode.getTokenAfter(arrowBody);
-
-                            if (tokenAfterArrowBody && tokenAfterArrowBody.type === "Punctuator" && /^[([/`+-]/.test(tokenAfterArrowBody.value)) {
+                            // If the first token of the reutrn value is `{`,
+                            // enclose the return value by parentheses to avoid syntax error.
+                            if (astUtils.isOpeningBraceToken(firstValueToken)) {
+                                fixes.push(
+                                    fixer.insertTextBefore(firstValueToken, "("),
+                                    fixer.insertTextAfter(lastValueToken, ")")
+                                );
+                            }
 
-                                // Don't do a fix if the next token would cause ASI issues when preceded by the returned value.
-                                return null;
+                            // If the last token of the return statement is semicolon, remove it.
+                            // Non-block arrow body is an expression, not a statement.
+                            if (astUtils.isSemicolonToken(lastValueToken)) {
+                                fixes.push(fixer.remove(lastValueToken));
                             }
 
-                            const textBeforeReturn = sourceText.slice(arrowBody.range[0] + 1, returnKeyword.range[0]);
-                            const textBetweenReturnAndValue = sourceText.slice(returnKeyword.range[1], firstValueToken.range[0]);
-                            const rawReturnValueText = sourceText.slice(firstValueToken.range[0], lastValueToken.range[1]);
-                            const returnValueText = astUtils.isOpeningBraceToken(firstValueToken) ? `(${rawReturnValueText})` : rawReturnValueText;
-                            const textAfterValue = sourceText.slice(lastValueToken.range[1], blockBody[0].range[1] - 1);
-                            const textAfterReturnStatement = sourceText.slice(blockBody[0].range[1], arrowBody.range[1] - 1);
-
-                            /*
-                             * For fixes that only contain spaces around the return value, remove the extra spaces.
-                             * This avoids ugly fixes that end up with extra spaces after the arrow, e.g. `() =>   0 ;`
-                             */
-                            return fixer.replaceText(
-                                arrowBody,
-                                (textBeforeReturn + textBetweenReturnAndValue).replace(/^\s*$/, "") + returnValueText + (textAfterValue + textAfterReturnStatement).replace(/^\s*$/, "")
-                            );
+                            return fixes;
                         }
                     });
                 }
@@ -142,13 +173,29 @@ module.exports = {
                         loc: arrowBody.loc.start,
                         message: "Expected block statement surrounding arrow body.",
                         fix(fixer) {
-                            const lastTokenBeforeBody = sourceCode.getLastTokenBetween(sourceCode.getFirstToken(node), arrowBody, astUtils.isNotOpeningParenToken);
-                            const firstBodyToken = sourceCode.getTokenAfter(lastTokenBeforeBody);
-
-                            return fixer.replaceTextRange(
-                                [firstBodyToken.range[0], node.range[1]],
-                                `{return ${sourceCode.getText().slice(firstBodyToken.range[0], node.range[1])}}`
+                            const fixes = [];
+                            const arrowToken = sourceCode.getTokenBefore(arrowBody, astUtils.isArrowToken);
+                            const firstBodyToken = sourceCode.getTokenAfter(arrowToken);
+                            const lastBodyToken = sourceCode.getLastToken(node);
+                            const isParenthesisedObjectLiteral =
+                                astUtils.isOpeningParenToken(firstBodyToken) &&
+                                astUtils.isOpeningBraceToken(sourceCode.getTokenAfter(firstBodyToken));
+
+                            // Wrap the value by a block and a return statement.
+                            fixes.push(
+                                fixer.insertTextBefore(firstBodyToken, "{return "),
+                                fixer.insertTextAfter(lastBodyToken, "}")
                             );
+
+                            // If the value is object literal, remove parentheses which were forced by syntax.
+                            if (isParenthesisedObjectLiteral) {
+                                fixes.push(
+                                    fixer.remove(firstBodyToken),
+                                    fixer.remove(findClosingParen(firstBodyToken))
+                                );
+                            }
+
+                            return fixes;
                         }
                     });
                 }
@@ -156,7 +203,7 @@ module.exports = {
         }
 
         return {
-            ArrowFunctionExpression: validate
+            "ArrowFunctionExpression:exit": validate
         };
     }
 };
diff --git a/lib/rules/indent.js b/lib/rules/indent.js
index 5a8feee8f..52d08ff3d 100644
--- a/lib/rules/indent.js
+++ b/lib/rules/indent.js
@@ -658,7 +658,6 @@ module.exports = {
                 while (astUtils.isOpeningParenToken(token) && token !== startToken) {
                     token = sourceCode.getTokenBefore(token);
                 }
-
                 return sourceCode.getTokenAfter(token);
             }
 
@@ -675,7 +674,6 @@ module.exports = {
             if (offset === "first" && elements.length && !elements[0]) {
                 return;
             }
-
             elements.forEach((element, index) => {
                 if (offset === "off") {
                     offsets.ignoreToken(getFirstToken(element));
@@ -1242,6 +1240,7 @@ module.exports = {
                     offsets.ignoreToken(equalOperator);
                     offsets.ignoreToken(tokenAfterOperator);
                     offsets.matchIndentOf(equalOperator, tokenAfterOperator);
+                    offsets.matchIndentOf(sourceCode.getFirstToken(node), equalOperator);
                 }
             },
 
diff --git a/lib/token-store/index.js b/lib/token-store/index.js
index 86510bcb7..1446b9ff0 100644
--- a/lib/token-store/index.js
+++ b/lib/token-store/index.js
@@ -12,6 +12,7 @@ const assert = require("assert");
 const cursors = require("./cursors");
 const ForwardTokenCursor = require("./forward-token-cursor");
 const PaddedTokenCursor = require("./padded-token-cursor");
+const utils = require("./utils");
 const astUtils = require("../ast-utils");
 
 //------------------------------------------------------------------------------
@@ -560,6 +561,26 @@ module.exports = class TokenStore {
         ).getAllTokens();
     }
 
+    //--------------------------------------------------------------------------
+    // Others.
+    //--------------------------------------------------------------------------
+
+    /**
+     * Checks whether any comments exist or not between the given 2 nodes.
+     *
+     * @param {ASTNode} left - The node to check.
+     * @param {ASTNode} right - The node to check.
+     * @returns {boolean} `true` if one or more comments exist.
+     */
+    commentsExistBetween(left, right) {
+        const index = utils.search(this[COMMENTS], left.range[1]);
+
+        return (
+            index < this[COMMENTS].length &&
+            this[COMMENTS][index].range[1] <= right.range[0]
+        );
+    }
+
     /**
      * Gets all comment tokens directly before the given node or token.
      * @param {ASTNode|token} nodeOrToken The AST node or token to check for adjacent comment tokens.
diff --git a/packages/eslint-config-eslint/default.yml b/packages/eslint-config-eslint/default.yml
index 9180f7525..634fd4624 100644
--- a/packages/eslint-config-eslint/default.yml
+++ b/packages/eslint-config-eslint/default.yml
@@ -16,6 +16,7 @@ rules:
     comma-dangle: "error"
     comma-spacing: "error"
     comma-style: ["error", "last"]
+    computed-property-spacing: "error"
     consistent-return: "error"
     curly: ["error", "all"]
     default-case: "error"


Test patch:
diff --git a/lib/testers/rule-tester.js b/lib/testers/rule-tester.js
index d66cd175a..9d747e96a 100644
--- a/lib/testers/rule-tester.js
+++ b/lib/testers/rule-tester.js
@@ -468,9 +468,11 @@ class RuleTester {
                     )
                 );
 
+                const hasMessageOfThisRule = messages.some(m => m.ruleId === ruleName);
+
                 for (let i = 0, l = item.errors.length; i < l; i++) {
-                    assert.ok(!("fatal" in messages[i]), `A fatal parsing error occurred: ${messages[i].message}`);
-                    assert.equal(messages[i].ruleId, ruleName, "Error rule name should be the same as the name of the rule being tested");
+                    assert(!messages[i].fatal, `A fatal parsing error occurred: ${messages[i].message}`);
+                    assert(hasMessageOfThisRule, "Error rule name should be the same as the name of the rule being tested");
 
                     if (typeof item.errors[i] === "string" || item.errors[i] instanceof RegExp) {
 
diff --git a/tests/fixtures/parsers/throws-with-options.js b/tests/fixtures/parsers/throws-with-options.js
new file mode 100644
index 000000000..95857751a
--- /dev/null
+++ b/tests/fixtures/parsers/throws-with-options.js
@@ -0,0 +1,10 @@
+"use strict";
+
+const espree = require("espree");
+
+exports.parse = (sourceText, options) => {
+    if (options.ecmaVersion) {
+        throw new Error("Expected no parserOptions to be used");
+    }
+    return espree.parse(sourceText, options);
+};
diff --git a/tests/lib/linter.js b/tests/lib/linter.js
index 01eec7bba..4acc46b3f 100644
--- a/tests/lib/linter.js
+++ b/tests/lib/linter.js
@@ -3905,6 +3905,13 @@ describe("eslint", () => {
                 assert.equal(messages[0].message, errorPrefix + require(parser).expectedError);
             });
 
+            it("should not pass any default parserOptions to the parser", () => {
+                const parser = path.join(parserFixtures, "throws-with-options.js");
+
+                const messages = linter.verify(";", { parser }, "filename");
+
+                assert.strictEqual(messages.length, 0);
+            });
         });
     }
 
diff --git a/tests/lib/rule-context.js b/tests/lib/rule-context.js
index c51e5560c..274c3f3d8 100644
--- a/tests/lib/rule-context.js
+++ b/tests/lib/rule-context.js
@@ -83,6 +83,9 @@ describe("RuleContext", () => {
                 mockESLint.expects("report")
                     .once()
                     .withArgs("fake-rule", 2, node, location, message, messageOpts, fixerObj);
+                mockESLint.expects("getSourceCode")
+                    .once()
+                    .returns(null);
 
                 ruleContext.report({
                     node,
@@ -95,6 +98,171 @@ describe("RuleContext", () => {
                 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 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();
+            });
+
         });
     });
 
diff --git a/tests/lib/rules/arrow-body-style.js b/tests/lib/rules/arrow-body-style.js
index 5b2204a47..46baf9a53 100644
--- a/tests/lib/rules/arrow-body-style.js
+++ b/tests/lib/rules/arrow-body-style.js
@@ -56,7 +56,7 @@ ruleTester.run("arrow-body-style", rule, {
         },
         {
             code: "var foo = () => ({});",
-            output: "var foo = () => {return ({})};",
+            output: "var foo = () => {return {}};",
             options: ["always"],
             errors: [
                 { line: 1, column: 18, type: "ArrowFunctionExpression", message: "Expected block statement surrounding arrow body." }
@@ -160,7 +160,7 @@ ruleTester.run("arrow-body-style", rule, {
         },
         {
             code: "var foo = () => ({});",
-            output: "var foo = () => {return ({})};",
+            output: "var foo = () => {return {}};",
             options: ["as-needed", { requireReturnForObjectLiteral: true }],
             errors: [
                 { line: 1, column: 18, type: "ArrowFunctionExpression", message: "Expected block statement surrounding arrow body." }
@@ -168,7 +168,7 @@ ruleTester.run("arrow-body-style", rule, {
         },
         {
             code: "var foo = () => ({ bar: 0 });",
-            output: "var foo = () => {return ({ bar: 0 })};",
+            output: "var foo = () => {return { bar: 0 }};",
             options: ["as-needed", { requireReturnForObjectLiteral: true }],
             errors: [
                 { line: 1, column: 18, type: "ArrowFunctionExpression", message: "Expected block statement surrounding arrow body." }
@@ -290,6 +290,18 @@ ruleTester.run("arrow-body-style", rule, {
             errors: [
                 { line: 2, column: 31, type: "ArrowFunctionExpression", message: "Unexpected block statement surrounding arrow body." }
             ]
+        },
+        {
+            code: "var foo = () => ({foo: 1}).foo();",
+            output: "var foo = () => {return {foo: 1}.foo()};",
+            options: ["always"],
+            errors: ["Expected block statement surrounding arrow body."]
+        },
+        {
+            code: "var foo = () => ({foo: 1}.foo());",
+            output: "var foo = () => {return {foo: 1}.foo()};",
+            options: ["always"],
+            errors: ["Expected block statement surrounding arrow body."]
         }
     ]
 });
diff --git a/tests/lib/rules/indent.js b/tests/lib/rules/indent.js
index fd2619ef9..12b42d712 100644
--- a/tests/lib/rules/indent.js
+++ b/tests/lib/rules/indent.js
@@ -2026,6 +2026,58 @@ ruleTester.run("indent", rule, {
             `,
             options: [2]
         },
+        {
+            code: unIndent`
+                const {
+                  a
+                }
+                =
+                {
+                  a: 1
+                }
+            `,
+            options: [2]
+        },
+        {
+            code: unIndent`
+                const {
+                  a
+                } = {
+                  a: 1
+                }
+            `,
+            options: [2]
+        },
+        {
+            code: unIndent`
+                const
+                  {
+                    a
+                  } = {
+                    a: 1
+                  };
+            `,
+            options: [2]
+        },
+        {
+            code: unIndent`
+                const
+                  foo = {
+                    bar: 1
+                  }
+            `,
+            options: [2]
+        },
+        {
+            code: unIndent`
+                const [
+                  a
+                ] = [
+                  1
+                ]
+            `,
+            options: [2]
+        },
         {
 
             // https://github.com/eslint/eslint/issues/7233
@@ -6834,6 +6886,24 @@ ruleTester.run("indent", rule, {
             options: [2],
             errors: expectedErrors([[2, 2, 0, "Identifier"], [4, 2, 4, "Identifier"], [5, 2, 6, "Identifier"], [6, 0, 2, "Punctuator"]])
         },
+        {
+            code: unIndent`
+                const {
+                  a
+                } = {
+                    a: 1
+                  }
+            `,
+            output: unIndent`
+                const {
+                  a
+                } = {
+                  a: 1
+                }
+            `,
+            options: [2],
+            errors: expectedErrors([[4, 2, 4, "Identifier"], [5, 0, 2, "Punctuator"]])
+        },
         {
             code: unIndent`
                 var foo = [
diff --git a/tests/lib/rules/semi.js b/tests/lib/rules/semi.js
index b4653014e..2c272b6eb 100644
--- a/tests/lib/rules/semi.js
+++ b/tests/lib/rules/semi.js
@@ -169,6 +169,25 @@ ruleTester.run("semi", rule, {
         { code: "export default (foo) => foo.bar();", output: "export default (foo) => foo.bar()", options: ["never"], parserOptions: { sourceType: "module" }, errors: [{ message: "Extra semicolon.", type: "ExportDefaultDeclaration" }] },
         { code: "export default foo = 42;", output: "export default foo = 42", options: ["never"], parserOptions: { sourceType: "module" }, errors: [{ message: "Extra semicolon.", type: "ExportDefaultDeclaration" }] },
         { code: "export default foo += 42;", output: "export default foo += 42", options: ["never"], parserOptions: { sourceType: "module" }, errors: [{ message: "Extra semicolon.", type: "ExportDefaultDeclaration" }] },
-        { code: "a;\n++b", output: "a\n++b", options: ["never"], errors: [{ message: "Extra semicolon." }] }
+        { code: "a;\n++b", output: "a\n++b", options: ["never"], errors: [{ message: "Extra semicolon." }] },
+
+        // https://github.com/eslint/eslint/issues/7928
+        {
+            options: ["never"],
+            code: [
+                "/*eslint no-extra-semi: error */",
+                "foo();",
+                ";[0,1,2].forEach(bar)"
+            ].join("\n"),
+            errors: [
+                "Extra semicolon.",
+                "Unnecessary semicolon."
+            ],
+            output: [
+                "/*eslint no-extra-semi: error */",
+                "foo()",
+                ";[0,1,2].forEach(bar)"
+            ].join("\n")
+        }
     ]
 });
diff --git a/tests/lib/token-store.js b/tests/lib/token-store.js
index 21c0c10c5..ddde3c5f8 100644
--- a/tests/lib/token-store.js
+++ b/tests/lib/token-store.js
@@ -1273,6 +1273,18 @@ describe("TokenStore", () => {
         });
     });
 
+    describe("when calling commentsExistBetween", () => {
+
+        it("should retrieve false if comments don't exist", () => {
+            assert.isFalse(store.commentsExistBetween(AST.tokens[0], AST.tokens[1]));
+        });
+
+        it("should retrieve true if comments exist", () => {
+            assert.isTrue(store.commentsExistBetween(AST.tokens[1], AST.tokens[2]));
+        });
+
+    });
+
     describe("getCommentsBefore", () => {
         it("should retrieve comments before a node", () => {
             assert.equal(