Repository: eslint/eslint
Pull request #8162 (2017-02-27 01:37:11)

Docs: improve documentation for RuleTester cases
**What is the purpose of this pull request? (put an "X" next to item)**

[x] Documentation update

**What changes did you make? (Give an overview)**

This lists the available properties for RuleTester cases rather than relying on a usage example.

**Is there anything you'd like reviewers to focus on?**

Let me know if there's anything unclear/incorrect in the updated documentation.


mention-bot: @not-an-aardvark, thanks for your PR! By analyzing the history of the files in this pull request, we identified @nzakas, @lo1tuma and @ilyavolodin to be potential reviewers.
eslintbot: LGTM

Patch:
diff --git a/conf/eslint-recommended.js b/conf/eslint-recommended.js
index 98a8cd26c..63c2fc770 100755
--- a/conf/eslint-recommended.js
+++ b/conf/eslint-recommended.js
@@ -84,6 +84,7 @@ module.exports = {
         "no-case-declarations": "error",
         "no-catch-shadow": "off",
         "no-class-assign": "error",
+        "no-compare-neg-zero": "off",
         "no-cond-assign": "error",
         "no-confusing-arrow": "off",
         "no-console": "error",
@@ -209,6 +210,7 @@ module.exports = {
         "no-warning-comments": "off",
         "no-whitespace-before-property": "off",
         "no-with": "off",
+        "nonblock-statement-body-position": "off",
         "object-curly-newline": "off",
         "object-curly-spacing": ["off", "never"],
         "object-property-newline": "off",
diff --git a/docs/developer-guide/working-with-plugins.md b/docs/developer-guide/working-with-plugins.md
index 29153ac5b..1f173646e 100644
--- a/docs/developer-guide/working-with-plugins.md
+++ b/docs/developer-guide/working-with-plugins.md
@@ -148,14 +148,37 @@ ruleTester.run("custom-plugin-rule", rule, {
 });
 ```
 
-As shown above, the message passed to `RuleTester` can be either a string or a regular expression.
-
-The `RuleTester` constructor optionally accepts an object argument, which can be used to specify defaults for your test cases. For example, if all of your test cases use ES2015, you can set it as a default:
+The `RuleTester` constructor accepts an optional object argument, which can be used to specify defaults for your test cases. For example, if all of your test cases use ES2015, you can set it as a default:
 
 ```js
 const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2015 } });
 ```
 
+The `RuleTester#run()` method is used to run the tests. It should be passed the following arguments:
+
+* The name of the rule (string)
+* The rule object itself (see ["working with rules"](./working-with-rules))
+* An object containing `valid` and `invalid` properties, each of which is an array containing test cases.
+
+A test case is an object with the following properties:
+
+* `code` (string, required): The source code that the rule should be run on
+* `options` (array, optional): The options passed to the rule. The rule severity should not be included in this list.
+* `filename` (string, optional): The filename for the given case (useful for rules that make assertions about filenames)
+
+In addition to the properties above, invalid test cases can also have the following properties:
+
+* `errors` (number or array, required): Asserts some properties of the errors that the rule is expected to produce when run on this code. If this is a number, asserts the number of errors produced. Otherwise, this should be a list of objects, each containing information about a single reported error. The following properties can be used for an error (all are optional):
+    * `message` (string/regexp): The message for the error
+    * `type` (string): The type of the reported AST node
+    * `line` (number): The 1-based line number of the reported location
+    * `column` (number): The 0-based column number of the reported location
+    * `endLine` (number): The 1-based line number of the end of the reported location
+    * `endColumn` (number): The 0-based column number of the end of the reported location
+* `output` (string, optional): Asserts the output that will be produced when using this rule for a single pass of autofixing (e.g. with the `--fix` command line flag).
+
+Any additional properties of a test case will be passed directly to the linter as config options. For example, a test case can have a `parserOptions` property to configure parser behavior.
+
 #### Customizing RuleTester
 
 To create tests for each valid and invalid case, `RuleTester` internally uses `describe` and `it` methods from the Mocha test framework when it is available. If you use another test framework, you can override `RuleTester.describe` and `RuleTester.it` to make `RuleTester` compatible with it and have proper individual tests and feedback.
diff --git a/docs/rules/no-compare-neg-zero.md b/docs/rules/no-compare-neg-zero.md
new file mode 100644
index 000000000..64afadfe8
--- /dev/null
+++ b/docs/rules/no-compare-neg-zero.md
@@ -0,0 +1,32 @@
+# disallow comparing against -0 (no-compare-neg-zero)
+
+## Rule Details
+
+The rule should warn against code that tries to compare against -0, since that will not work as intended. That is, code like x === -0 will pass for both +0 and -0. The author probably intended Object.is(x, -0).
+
+Examples of **incorrect** code for this rule:
+
+```js
+
+if (x === -0) {
+    // doSomething()...
+}
+```
+
+Examples of **correct** code for this rule:
+
+```js
+
+if (x === 0) {
+    // doSomething()...
+}
+```
+
+```js
+
+if (Object.is(x, -0)) {
+    // doSomething()...
+}
+```
+
+
diff --git a/docs/rules/nonblock-statement-body-position.md b/docs/rules/nonblock-statement-body-position.md
new file mode 100644
index 000000000..0a05d2945
--- /dev/null
+++ b/docs/rules/nonblock-statement-body-position.md
@@ -0,0 +1,158 @@
+# enforce the location of single-line statements (nonblock-statement-body-position)
+
+When writing `if`, `else`, `while`, `do-while`, and `for` statements, the body can be a single statement instead of a block. It can be useful to enforce a consistent location for these single statements.
+
+For example, some developers avoid writing code like this:
+
+```js
+if (foo)
+  bar();
+```
+
+If another developer attempts to add `baz();` to the `if` statement, they might mistakenly change the code to
+
+```js
+if (foo)
+  bar();
+  baz(); // this line is not in the `if` statement!
+```
+
+To avoid this issue, one might require all single-line `if` statements to appear directly after the conditional, without a linebreak:
+
+```js
+if (foo) bar();
+```
+
+## Rule Details
+
+This rule aims to enforce a consistent location for single-line statements.
+
+Note that this rule does not enforce the usage of single-line statements in general. If you would like to disallow single-line statements, use the [`curly`](/docs/rules/curly.md) rule instead.
+
+### Options
+
+This rule accepts a string option:
+
+* `"beside"` (default) disallows a newline before a single-line statement.
+* `"below"` requires a newline before a single-line statement.
+* `"any"` does not enforce the position of a single-line statement.
+
+Additionally, the rule accepts an optional object option with an `"overrides"` key. This can be used to specify a location for particular statements that override the default. For example:
+
+* `"beside", { "overrides": { "while": "below" } }` requires all single-line statements to appear on the same line as their parent, unless the parent is a `while` statement, in which case the single-line statement must not be on the same line.
+* `"below", { "overrides": { "do": "any" } }` disallows all single-line statements from appearing on the same line as their parent, unless the parent is a `do-while` statement, in which case the position of the single-line statement is not enforced.
+
+Examples of **incorrect** code for this rule with the default `"beside"` option:
+
+```js
+/* eslint nonblock-statement-body-position: ["error", "beside"] */
+
+if (foo)
+  bar();
+else
+  baz();
+
+while (foo)
+  bar();
+
+for (let i = 1; i < foo; i++)
+  bar();
+
+do
+  bar();
+while (foo)
+
+```
+
+Examples of **correct** code for this rule with the default `"beside"` option:
+
+```js
+/* eslint nonblock-statement-body-position: ["error", "beside"] */
+
+if (foo) bar();
+else baz();
+
+while (foo) bar();
+
+for (let i = 1; i < foo; i++) bar();
+
+do bar(); while (foo)
+
+if (foo) { // block statements are always allowed with this rule
+  bar();
+} else {
+  baz();
+}
+```
+
+Examples of **incorrect** code for this rule with the `"below"` option:
+
+```js
+/* eslint nonblock-statement-body-position: ["error", "below"] */
+
+if (foo) bar();
+else baz();
+
+while (foo) bar();
+
+for (let i = 1; i < foo; i++) bar();
+
+do bar(); while (foo)
+```
+
+Examples of **correct** code for this rule with the `"below"` option:
+
+```js
+/* eslint nonblock-statement-body-position: ["error", "below"] */
+
+if (foo)
+  bar();
+else
+  baz();
+
+while (foo)
+  bar();
+
+for (let i = 1; i < foo; i++)
+  bar();
+
+do
+  bar();
+while (foo)
+
+if (foo) {
+  // Although the second `if` statement is on the same line as the `else`, this is a very common
+  // pattern, so it's not checked by this rule.
+} else if (bar) {
+}
+```
+
+Examples of **incorrect** code for this rule with the `"beside", { "overrides": { "while": "below" } }` rule:
+
+```js
+/* eslint nonblock-statement-body-position: ["error", "beside", { "overrides": { "while": "below" } }] */
+
+if (foo)
+  bar();
+
+while (foo) bar();
+```
+
+Examples of **correct** code for this rule with the `"beside", { "overrides": { "while": "below" } }` rule:
+
+```js
+/* eslint nonblock-statement-body-position: ["error", "beside", { "overrides": { "while": "below" } }] */
+
+if (foo) bar();
+
+while (foo)
+  bar();
+```
+
+## When Not To Use It
+
+If you're not concerned about consistent locations of single-line statements, you should not turn on this rule. You can also disable this rule if you're using the `"all"` option for the [`curly`](/docs/rules/curly.md) rule, because this will disallow single-line statements entirely.
+
+## Further Reading
+
+* JSCS: [requireNewlineBeforeSingleStatementsInIf](http://jscs.info/rule/requireNewlineBeforeSingleStatementsInIf)
diff --git a/lib/rules/no-compare-neg-zero.js b/lib/rules/no-compare-neg-zero.js
new file mode 100644
index 000000000..d93ade5d3
--- /dev/null
+++ b/lib/rules/no-compare-neg-zero.js
@@ -0,0 +1,53 @@
+/**
+ * @fileoverview The rule should warn against code that tries to compare against -0.
+ * @author Aladdin-ADD <hh_2013@foxmail.com>
+ */
+"use strict";
+
+//------------------------------------------------------------------------------
+// Rule Definition
+//------------------------------------------------------------------------------
+
+module.exports = {
+    meta: {
+        docs: {
+            description: "disallow comparing against -0",
+            category: "Possible Errors",
+            recommended: false
+        },
+        fixable: null,
+        schema: []
+    },
+
+    create(context) {
+
+        //--------------------------------------------------------------------------
+        // Helpers
+        //--------------------------------------------------------------------------
+
+        /**
+         * Checks a given node is -0
+         *
+         * @param {ASTNode} node - A node to check.
+         * @returns {boolean} `true` if the node is -0.
+         */
+        function isNegZero(node) {
+            return node.type === "UnaryExpression" && node.operator === "-" && node.argument.type === "Literal" && node.argument.value === 0;
+        }
+        const OPERATORS_TO_CHECK = new Set([">", ">=", "<", "<=", "==", "===", "!=", "!=="]);
+
+        return {
+            BinaryExpression(node) {
+                if (OPERATORS_TO_CHECK.has(node.operator)) {
+                    if (isNegZero(node.left) || isNegZero(node.right)) {
+                        context.report({
+                            node,
+                            message: "Do not use the '{{operator}}' operator to compare against -0.",
+                            data: { operator: node.operator }
+                        });
+                    }
+                }
+            }
+        };
+    }
+};
diff --git a/lib/rules/no-else-return.js b/lib/rules/no-else-return.js
index 6d82ef3f3..6ba0b2426 100644
--- a/lib/rules/no-else-return.js
+++ b/lib/rules/no-else-return.js
@@ -5,6 +5,12 @@
 
 "use strict";
 
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const FixTracker = require("../util/fix-tracker");
+
 //------------------------------------------------------------------------------
 // Rule Definition
 //------------------------------------------------------------------------------
@@ -86,7 +92,13 @@ module.exports = {
                     } else {
                         fixedSource = source;
                     }
-                    return fixer.replaceTextRange([elseToken.start, node.end], fixedSource);
+
+                    // Extend the replacement range to include the entire
+                    // function to avoid conflicting with no-useless-return.
+                    // https://github.com/eslint/eslint/issues/8026
+                    return new FixTracker(fixer, sourceCode)
+                        .retainEnclosingFunction(node)
+                        .replaceTextRange([elseToken.start, node.end], fixedSource);
                 }
             });
         }
diff --git a/lib/rules/no-extra-semi.js b/lib/rules/no-extra-semi.js
index f51f16ee6..2865cdc99 100644
--- a/lib/rules/no-extra-semi.js
+++ b/lib/rules/no-extra-semi.js
@@ -5,6 +5,12 @@
 
 "use strict";
 
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const FixTracker = require("../util/fix-tracker");
+
 //------------------------------------------------------------------------------
 // Rule Definition
 //------------------------------------------------------------------------------
@@ -34,7 +40,13 @@ module.exports = {
                 node: nodeOrToken,
                 message: "Unnecessary semicolon.",
                 fix(fixer) {
-                    return fixer.remove(nodeOrToken);
+
+                    // Expand the replacement range to include the surrounding
+                    // tokens to avoid conflicting with semi.
+                    // https://github.com/eslint/eslint/issues/7928
+                    return new FixTracker(fixer, context.getSourceCode())
+                        .retainSurroundingTokens(nodeOrToken)
+                        .remove(nodeOrToken);
                 }
             });
         }
diff --git a/lib/rules/no-use-before-define.js b/lib/rules/no-use-before-define.js
index 4bb52030b..1a779b86f 100644
--- a/lib/rules/no-use-before-define.js
+++ b/lib/rules/no-use-before-define.js
@@ -166,7 +166,7 @@ module.exports = {
         const options = parseOptions(context.options[0]);
 
         /**
-         * Determines whether a given use-before-define case should be reportedaccording to the options.
+         * Determines whether a given use-before-define case should be reported according to the options.
          * @param {escope.Variable} variable The variable that gets used before being defined
          * @param {escope.Reference} reference The reference to the variable
          * @returns {boolean} `true` if the usage should be reported
diff --git a/lib/rules/no-useless-return.js b/lib/rules/no-useless-return.js
index 0278bba82..29f644cc4 100644
--- a/lib/rules/no-useless-return.js
+++ b/lib/rules/no-useless-return.js
@@ -8,7 +8,8 @@
 // Requirements
 //------------------------------------------------------------------------------
 
-const astUtils = require("../ast-utils");
+const astUtils = require("../ast-utils"),
+    FixTracker = require("../util/fix-tracker");
 
 //------------------------------------------------------------------------------
 // Helpers
@@ -219,7 +220,17 @@ module.exports = {
                         loc: node.loc,
                         message: "Unnecessary return statement.",
                         fix(fixer) {
-                            return isRemovable(node) ? fixer.remove(node) : null;
+                            if (isRemovable(node)) {
+
+                                // Extend the replacement range to include the
+                                // entire function to avoid conflicting with
+                                // no-else-return.
+                                // https://github.com/eslint/eslint/issues/8026
+                                return new FixTracker(fixer, context.getSourceCode())
+                                    .retainEnclosingFunction(node)
+                                    .remove(node);
+                            }
+                            return null;
                         }
                     });
                 }
diff --git a/lib/rules/nonblock-statement-body-position.js b/lib/rules/nonblock-statement-body-position.js
new file mode 100644
index 000000000..d6ee126da
--- /dev/null
+++ b/lib/rules/nonblock-statement-body-position.js
@@ -0,0 +1,114 @@
+/**
+ * @fileoverview enforce the location of single-line statements
+ * @author Teddy Katz
+ */
+"use strict";
+
+//------------------------------------------------------------------------------
+// Rule Definition
+//------------------------------------------------------------------------------
+
+const POSITION_SCHEMA = { enum: ["beside", "below", "any"] };
+
+module.exports = {
+    meta: {
+        docs: {
+            description: "enforce the location of single-line statements",
+            category: "Stylistic Issues",
+            recommended: false
+        },
+        fixable: "whitespace",
+        schema: [
+            POSITION_SCHEMA,
+            {
+                properties: {
+                    overrides: {
+                        properties: {
+                            if: POSITION_SCHEMA,
+                            else: POSITION_SCHEMA,
+                            while: POSITION_SCHEMA,
+                            do: POSITION_SCHEMA,
+                            for: POSITION_SCHEMA
+                        },
+                        additionalProperties: false
+                    }
+                },
+                additionalProperties: false
+            }
+        ]
+    },
+
+    create(context) {
+        const sourceCode = context.getSourceCode();
+
+        //----------------------------------------------------------------------
+        // Helpers
+        //----------------------------------------------------------------------
+
+        /**
+         * Gets the applicable preference for a particular keyword
+         * @param {string} keywordName The name of a keyword, e.g. 'if'
+         * @returns {string} The applicable option for the keyword, e.g. 'beside'
+         */
+        function getOption(keywordName) {
+            return context.options[1] && context.options[1].overrides && context.options[1].overrides[keywordName] ||
+                context.options[0] ||
+                "beside";
+        }
+
+        /**
+         * Validates the location of a single-line statement
+         * @param {ASTNode} node The single-line statement
+         * @param {string} keywordName The applicable keyword name for the single-line statement
+         * @returns {void}
+         */
+        function validateStatement(node, keywordName) {
+            const option = getOption(keywordName);
+
+            if (node.type === "BlockStatement" || option === "any") {
+                return;
+            }
+
+            const tokenBefore = sourceCode.getTokenBefore(node);
+
+            if (tokenBefore.loc.end.line === node.loc.start.line && option === "below") {
+                context.report({
+                    node,
+                    message: "Expected a linebreak before this statement.",
+                    fix: fixer => fixer.insertTextBefore(node, "\n")
+                });
+            } else if (tokenBefore.loc.end.line !== node.loc.end.line && option === "beside") {
+                context.report({
+                    node,
+                    message: "Expected no linebreak before this statement.",
+                    fix(fixer) {
+                        if (sourceCode.getText().slice(tokenBefore.range[1], node.range[0]).trim()) {
+                            return null;
+                        }
+                        return fixer.replaceTextRange([tokenBefore.range[1], node.range[0]], " ");
+                    }
+                });
+            }
+        }
+
+        //----------------------------------------------------------------------
+        // Public
+        //----------------------------------------------------------------------
+
+        return {
+            IfStatement(node) {
+                validateStatement(node.consequent, "if");
+
+                // Check the `else` node, but don't check 'else if' statements.
+                if (node.alternate && node.alternate.type !== "IfStatement") {
+                    validateStatement(node.alternate, "else");
+                }
+            },
+            WhileStatement: node => validateStatement(node.body, "while"),
+            DoWhileStatement: node => validateStatement(node.body, "do"),
+            ForStatement: node => validateStatement(node.body, "for"),
+            ForInStatement: node => validateStatement(node.body, "for"),
+            ForOfStatement: node => validateStatement(node.body, "for")
+        };
+    }
+};
diff --git a/lib/rules/padded-blocks.js b/lib/rules/padded-blocks.js
index 27657360e..36036aec4 100644
--- a/lib/rules/padded-blocks.js
+++ b/lib/rules/padded-blocks.js
@@ -90,23 +90,32 @@ module.exports = {
             return node.type === "Line" || node.type === "Block";
         }
 
+        /**
+         * Checks if there is padding between two tokens
+         * @param {Token} first The first token
+         * @param {Token} second The second token
+         * @returns {boolean} True if there is at least a line between the tokens
+         */
+        function isPaddingBetweenTokens(first, second) {
+            return second.loc.start.line - first.loc.end.line >= 2;
+        }
+
+
         /**
          * Checks if the given token has a blank line after it.
          * @param {Token} token The token to check.
          * @returns {boolean} Whether or not the token is followed by a blank line.
          */
-        function isTokenTopPadded(token) {
-            const tokenStartLine = token.loc.start.line,
-                expectedFirstLine = tokenStartLine + 2;
-            let first = token;
+        function getFirstBlockToken(token) {
+            let prev = token,
+                first = token;
 
             do {
+                prev = first;
                 first = sourceCode.getTokenAfter(first, { includeComments: true });
-            } while (isComment(first) && first.loc.start.line === tokenStartLine);
-
-            const firstLine = first.loc.start.line;
+            } while (isComment(first) && first.loc.start.line === prev.loc.end.line);
 
-            return expectedFirstLine <= firstLine;
+            return first;
         }
 
         /**
@@ -114,18 +123,16 @@ module.exports = {
          * @param {Token} token The token to check
          * @returns {boolean} Whether or not the token is preceeded by a blank line
          */
-        function isTokenBottomPadded(token) {
-            const blockEnd = token.loc.end.line,
-                expectedLastLine = blockEnd - 2;
-            let last = token;
+        function getLastBlockToken(token) {
+            let last = token,
+                next = token;
 
             do {
+                next = last;
                 last = sourceCode.getTokenBefore(last, { includeComments: true });
-            } while (isComment(last) && last.loc.end.line === blockEnd);
+            } while (isComment(last) && last.loc.end.line === next.loc.start.line);
 
-            const lastLine = last.loc.end.line;
-
-            return lastLine <= expectedLastLine;
+            return last;
         }
 
         /**
@@ -155,17 +162,21 @@ module.exports = {
          */
         function checkPadding(node) {
             const openBrace = getOpenBrace(node),
+                firstBlockToken = getFirstBlockToken(openBrace),
+                tokenBeforeFirst = sourceCode.getTokenBefore(firstBlockToken, { includeComments: true }),
                 closeBrace = sourceCode.getLastToken(node),
-                blockHasTopPadding = isTokenTopPadded(openBrace),
-                blockHasBottomPadding = isTokenBottomPadded(closeBrace);
+                lastBlockToken = getLastBlockToken(closeBrace),
+                tokenAfterLast = sourceCode.getTokenAfter(lastBlockToken, { includeComments: true }),
+                blockHasTopPadding = isPaddingBetweenTokens(tokenBeforeFirst, firstBlockToken),
+                blockHasBottomPadding = isPaddingBetweenTokens(lastBlockToken, tokenAfterLast);
 
             if (requirePaddingFor(node)) {
                 if (!blockHasTopPadding) {
                     context.report({
                         node,
-                        loc: { line: openBrace.loc.start.line, column: openBrace.loc.start.column },
+                        loc: { line: tokenBeforeFirst.loc.start.line, column: tokenBeforeFirst.loc.start.column },
                         fix(fixer) {
-                            return fixer.insertTextAfter(openBrace, "\n");
+                            return fixer.insertTextAfter(tokenBeforeFirst, "\n");
                         },
                         message: ALWAYS_MESSAGE
                     });
@@ -173,39 +184,34 @@ module.exports = {
                 if (!blockHasBottomPadding) {
                     context.report({
                         node,
-                        loc: { line: closeBrace.loc.end.line, column: closeBrace.loc.end.column - 1 },
+                        loc: { line: tokenAfterLast.loc.end.line, column: tokenAfterLast.loc.end.column - 1 },
                         fix(fixer) {
-                            return fixer.insertTextBefore(closeBrace, "\n");
+                            return fixer.insertTextBefore(tokenAfterLast, "\n");
                         },
                         message: ALWAYS_MESSAGE
                     });
                 }
             } else {
                 if (blockHasTopPadding) {
-                    const nextToken = sourceCode.getTokenAfter(openBrace, { includeComments: true });
 
                     context.report({
                         node,
-                        loc: { line: openBrace.loc.start.line, column: openBrace.loc.start.column },
+                        loc: { line: tokenBeforeFirst.loc.start.line, column: tokenBeforeFirst.loc.start.column },
                         fix(fixer) {
-
-                            // FIXME: The start of this range is sometimes larger than the end.
-                            // https://github.com/eslint/eslint/issues/8116
-                            return fixer.replaceTextRange([openBrace.end, nextToken.start - nextToken.loc.start.column], "\n");
+                            return fixer.replaceTextRange([tokenBeforeFirst.end, firstBlockToken.start - firstBlockToken.loc.start.column], "\n");
                         },
                         message: NEVER_MESSAGE
                     });
                 }
 
                 if (blockHasBottomPadding) {
-                    const previousToken = sourceCode.getTokenBefore(closeBrace, { includeComments: true });
 
                     context.report({
                         node,
-                        loc: { line: closeBrace.loc.end.line, column: closeBrace.loc.end.column - 1 },
+                        loc: { line: tokenAfterLast.loc.end.line, column: tokenAfterLast.loc.end.column - 1 },
                         message: NEVER_MESSAGE,
                         fix(fixer) {
-                            return fixer.replaceTextRange([previousToken.end, closeBrace.start - closeBrace.loc.start.column], "\n");
+                            return fixer.replaceTextRange([lastBlockToken.end, tokenAfterLast.start - tokenAfterLast.loc.start.column], "\n");
                         }
                     });
                 }
diff --git a/lib/rules/semi.js b/lib/rules/semi.js
index ee37ab018..8fd43f768 100644
--- a/lib/rules/semi.js
+++ b/lib/rules/semi.js
@@ -4,6 +4,12 @@
  */
 "use strict";
 
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const FixTracker = require("../util/fix-tracker");
+
 //------------------------------------------------------------------------------
 // Rule Definition
 //------------------------------------------------------------------------------
@@ -85,7 +91,13 @@ module.exports = {
                 message = "Extra semicolon.";
                 loc = loc.start;
                 fix = function(fixer) {
-                    return fixer.remove(lastToken);
+
+                    // Expand the replacement range to include the surrounding
+                    // tokens to avoid conflicting with no-extra-semi.
+                    // https://github.com/eslint/eslint/issues/7928
+                    return new FixTracker(fixer, sourceCode)
+                        .retainSurroundingTokens(lastToken)
+                        .remove(lastToken);
                 };
             }
 
diff --git a/lib/util/fix-tracker.js b/lib/util/fix-tracker.js
new file mode 100644
index 000000000..189072e1a
--- /dev/null
+++ b/lib/util/fix-tracker.js
@@ -0,0 +1,121 @@
+/**
+ * @fileoverview Helper class to aid in constructing fix commands.
+ * @author Alan Pierce
+ */
+"use strict";
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const astUtils = require("../ast-utils");
+
+//------------------------------------------------------------------------------
+// Public Interface
+//------------------------------------------------------------------------------
+
+/**
+ * A helper class to combine fix options into a fix command. Currently, it
+ * exposes some "retain" methods that extend the range of the text being
+ * replaced so that other fixes won't touch that region in the same pass.
+ */
+class FixTracker {
+
+    /**
+     * Create a new FixTracker.
+     *
+     * @param {ruleFixer} fixer A ruleFixer instance.
+     * @param {SourceCode} sourceCode A SourceCode object for the current code.
+     */
+    constructor(fixer, sourceCode) {
+        this.fixer = fixer;
+        this.sourceCode = sourceCode;
+        this.retainedRange = null;
+    }
+
+    /**
+     * Mark the given range as "retained", meaning that other fixes may not
+     * may not modify this region in the same pass.
+     *
+     * @param {int[]} range The range to retain.
+     * @returns {FixTracker} The same RuleFixer, for chained calls.
+     */
+    retainRange(range) {
+        this.retainedRange = range;
+        return this;
+    }
+
+    /**
+     * Given a node, find the function containing it (or the entire program) and
+     * mark it as retained, meaning that other fixes may not modify it in this
+     * pass. This is useful for avoiding conflicts in fixes that modify control
+     * flow.
+     *
+     * @param {ASTNode} node The node to use as a starting point.
+     * @returns {FixTracker} The same RuleFixer, for chained calls.
+     */
+    retainEnclosingFunction(node) {
+        const functionNode = astUtils.getUpperFunction(node);
+
+        return this.retainRange(
+            functionNode ? functionNode.range : this.sourceCode.ast.range);
+    }
+
+    /**
+     * Given a node or token, find the token before and afterward, and mark that
+     * range as retained, meaning that other fixes may not modify it in this
+     * pass. This is useful for avoiding conflicts in fixes that make a small
+     * change to the code where the AST should not be changed.
+     *
+     * @param {ASTNode|Token} nodeOrToken The node or token to use as a starting
+     *      point. The token to the left and right are use in the range.
+     * @returns {FixTracker} The same RuleFixer, for chained calls.
+     */
+    retainSurroundingTokens(nodeOrToken) {
+        const tokenBefore = this.sourceCode.getTokenBefore(nodeOrToken) || nodeOrToken;
+        const tokenAfter = this.sourceCode.getTokenAfter(nodeOrToken) || nodeOrToken;
+
+        return this.retainRange([tokenBefore.range[0], tokenAfter.range[1]]);
+    }
+
+    /**
+     * Create a fix command that replaces the given range with the given text,
+     * accounting for any retained ranges.
+     *
+     * @param {int[]} range The range to remove in the fix.
+     * @param {string} text The text to insert in place of the range.
+     * @returns {Object} The fix command.
+     */
+    replaceTextRange(range, text) {
+        let actualRange;
+
+        if (this.retainedRange) {
+            actualRange = [
+                Math.min(this.retainedRange[0], range[0]),
+                Math.max(this.retainedRange[1], range[1])
+            ];
+        } else {
+            actualRange = range;
+        }
+
+        return this.fixer.replaceTextRange(
+            actualRange,
+            this.sourceCode.text.slice(actualRange[0], range[0]) +
+                text +
+                this.sourceCode.text.slice(range[1], actualRange[1])
+        );
+    }
+
+    /**
+     * Create a fix command that removes the given node or token, accounting for
+     * any retained ranges.
+     *
+     * @param {ASTNode|Token} nodeOrToken The node or token to remove.
+     * @returns {Object} The fix command.
+     */
+    remove(nodeOrToken) {
+        return this.replaceTextRange(nodeOrToken.range, "");
+    }
+}
+
+module.exports = FixTracker;
diff --git a/package.json b/package.json
index 11d4948d4..ae8ae4b46 100644
--- a/package.json
+++ b/package.json
@@ -78,14 +78,13 @@
     "browserify": "^12.0.1",
     "chai": "^3.5.0",
     "cheerio": "^0.19.0",
-    "coveralls": "2.11.4",
+    "coveralls": "^2.11.16",
     "dateformat": "^1.0.8",
     "ejs": "^2.3.3",
     "eslint-plugin-node": "^4.1.0",
     "eslint-release": "^0.10.0",
     "esprima": "^2.4.1",
     "esprima-fb": "^15001.1001.0-dev-harmony-fb",
-    "gh-got": "^2.2.0",
     "istanbul": "^0.4.0",
     "jsdoc": "^3.3.0-beta1",
     "karma": "^0.13.22",
@@ -94,7 +93,6 @@
     "karma-mocha-reporter": "^2.0.3",
     "karma-phantomjs-launcher": "^1.0.0",
     "leche": "^2.1.1",
-    "linefix": "^0.1.1",
     "load-perf": "^0.2.0",
     "markdownlint": "^0.3.1",
     "mocha": "^2.4.5",


Test patch:
diff --git a/tests/fixtures/autofix/return-conflicting-fixes.expected.js b/tests/fixtures/autofix/return-conflicting-fixes.expected.js
new file mode 100644
index 000000000..0d1a5b4dd
--- /dev/null
+++ b/tests/fixtures/autofix/return-conflicting-fixes.expected.js
@@ -0,0 +1,10 @@
+/* eslint no-else-return: "error" */
+/* eslint no-useless-return: "error" */
+/* eslint no-trailing-spaces: "error" */
+function f() {
+    if (true) {
+
+    } else {
+        return 1;
+    }
+}
diff --git a/tests/fixtures/autofix/return-conflicting-fixes.js b/tests/fixtures/autofix/return-conflicting-fixes.js
new file mode 100644
index 000000000..486bf2ca9
--- /dev/null
+++ b/tests/fixtures/autofix/return-conflicting-fixes.js
@@ -0,0 +1,10 @@
+/* eslint no-else-return: "error" */
+/* eslint no-useless-return: "error" */
+/* eslint no-trailing-spaces: "error" */
+function f() {
+    if (true) {
+        return;
+    } else {
+        return 1;
+    }
+}
diff --git a/tests/fixtures/autofix/semicolon-conflicting-fixes.expected.js b/tests/fixtures/autofix/semicolon-conflicting-fixes.expected.js
new file mode 100644
index 000000000..9c051be95
--- /dev/null
+++ b/tests/fixtures/autofix/semicolon-conflicting-fixes.expected.js
@@ -0,0 +1,4 @@
+/* eslint semi: ["error", "never"] */
+/* eslint no-extra-semi: "error" */
+var foo = function(){}
+;[1].map(foo)
diff --git a/tests/fixtures/autofix/semicolon-conflicting-fixes.js b/tests/fixtures/autofix/semicolon-conflicting-fixes.js
new file mode 100644
index 000000000..1783291e5
--- /dev/null
+++ b/tests/fixtures/autofix/semicolon-conflicting-fixes.js
@@ -0,0 +1,4 @@
+/* eslint semi: ["error", "never"] */
+/* eslint no-extra-semi: "error" */
+var foo = function(){};
+;[1].map(foo)
diff --git a/tests/lib/cli-engine.js b/tests/lib/cli-engine.js
index dc1fecc72..062b03669 100644
--- a/tests/lib/cli-engine.js
+++ b/tests/lib/cli-engine.js
@@ -246,6 +246,34 @@ describe("CLIEngine", () => {
             });
         });
 
+        it("correctly autofixes semicolon-conflicting-fixes", () => {
+            engine = new CLIEngine({
+                cwd: path.join(fixtureDir, ".."),
+                useEslintrc: false,
+                fix: true
+            });
+            const inputPath = getFixturePath("autofix/semicolon-conflicting-fixes.js");
+            const outputPath = getFixturePath("autofix/semicolon-conflicting-fixes.expected.js");
+            const report = engine.executeOnFiles([inputPath]);
+            const expectedOutput = fs.readFileSync(outputPath, "utf8");
+
+            assert.strictEqual(report.results[0].output, expectedOutput);
+        });
+
+        it("correctly autofixes return-conflicting-fixes", () => {
+            engine = new CLIEngine({
+                cwd: path.join(fixtureDir, ".."),
+                useEslintrc: false,
+                fix: true
+            });
+            const inputPath = getFixturePath("autofix/return-conflicting-fixes.js");
+            const outputPath = getFixturePath("autofix/return-conflicting-fixes.expected.js");
+            const report = engine.executeOnFiles([inputPath]);
+            const expectedOutput = fs.readFileSync(outputPath, "utf8");
+
+            assert.strictEqual(report.results[0].output, expectedOutput);
+        });
+
         it("should return a message and omit fixed text when in fix mode and fixes aren't done", () => {
 
             engine = new CLIEngine({
diff --git a/tests/lib/rules/no-compare-neg-zero.js b/tests/lib/rules/no-compare-neg-zero.js
new file mode 100644
index 000000000..5f1b67f19
--- /dev/null
+++ b/tests/lib/rules/no-compare-neg-zero.js
@@ -0,0 +1,141 @@
+/**
+ * @fileoverview The rule should warn against code that tries to compare against -0.
+ * @author Aladdin-ADD<hh_2013@foxmail.com>
+ */
+"use strict";
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const rule = require("../../../lib/rules/no-compare-neg-zero");
+
+const RuleTester = require("../../../lib/testers/rule-tester");
+
+
+//------------------------------------------------------------------------------
+// Tests
+//------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester();
+
+ruleTester.run("no-compare-neg-zero", rule, {
+
+    valid: [
+        { code: "x === 0" },
+        { code: "0 === x" },
+        { code: "x == 0" },
+        { code: "0 == x" },
+        { code: "x === '0'" },
+        { code: "'0' === x" },
+        { code: "x == '0'" },
+        { code: "'0' == x" },
+        { code: "x === '-0'" },
+        { code: "'-0' === x" },
+        { code: "x == '-0'" },
+        { code: "'-0' == x" },
+        { code: "x === -1" },
+        { code: "-1 === x" },
+        { code: "x < 0" },
+        { code: "0 < x" },
+        { code: "x <= 0" },
+        { code: "x <= 0" },
+        { code: "0 <= x" },
+        { code: "x > 0" },
+        { code: "0 > x" },
+        { code: "x >= 0" },
+        { code: "0 >= x" },
+        { code: "x != 0" },
+        { code: "0 != x" },
+        { code: "x !== 0" },
+        { code: "0 !== x" },
+        { code: "Object.is(x, -0)" }
+    ],
+
+    invalid: [
+        {
+            code: "x === -0",
+            errors: [{
+                message: "Do not use the '===' operator to compare against -0.",
+                type: "BinaryExpression"
+            }]
+        },
+        {
+            code: "-0 === x",
+            errors: [{
+                message: "Do not use the '===' operator to compare against -0.",
+                type: "BinaryExpression"
+            }]
+        },
+        {
+            code: "x == -0",
+            errors: [{
+                message: "Do not use the '==' operator to compare against -0.",
+                type: "BinaryExpression"
+            }]
+        },
+        {
+            code: "-0 == x",
+            errors: [{
+                message: "Do not use the '==' operator to compare against -0.",
+                type: "BinaryExpression"
+            }]
+        },
+        {
+            code: "x > -0",
+            errors: [{
+                message: "Do not use the '>' operator to compare against -0.",
+                type: "BinaryExpression"
+            }]
+        },
+        {
+            code: "-0 > x",
+            errors: [{
+                message: "Do not use the '>' operator to compare against -0.",
+                type: "BinaryExpression"
+            }]
+        },
+        {
+            code: "x >= -0",
+            errors: [{
+                message: "Do not use the '>=' operator to compare against -0.",
+                type: "BinaryExpression"
+            }]
+        },
+        {
+            code: "-0 >= x",
+            errors: [{
+                message: "Do not use the '>=' operator to compare against -0.",
+                type: "BinaryExpression"
+            }]
+        },
+        {
+            code: "x < -0",
+            errors: [{
+                message: "Do not use the '<' operator to compare against -0.",
+                type: "BinaryExpression"
+            }]
+        },
+        {
+            code: "-0 < x",
+            errors: [{
+                message: "Do not use the '<' operator to compare against -0.",
+                type: "BinaryExpression"
+            }]
+        },
+        {
+            code: "x <= -0",
+            errors: [{
+                message: "Do not use the '<=' operator to compare against -0.",
+                type: "BinaryExpression"
+            }]
+        },
+        {
+            code: "-0 <= x",
+            errors: [{
+                message: "Do not use the '<=' operator to compare against -0.",
+                type: "BinaryExpression"
+            }]
+        }
+    ]
+});
diff --git a/tests/lib/rules/no-else-return.js b/tests/lib/rules/no-else-return.js
index 321be6bff..e5540c21a 100644
--- a/tests/lib/rules/no-else-return.js
+++ b/tests/lib/rules/no-else-return.js
@@ -46,7 +46,7 @@ ruleTester.run("no-else-return", rule, {
             errors: [{ message: "Unnecessary 'else' after 'return'.", type: "ReturnStatement" }] },
         {
             code: "function foo4() { if (true) { if (false) return x; else return y; } else { return z; } }",
-            output: "function foo4() { if (true) { if (false) return x; return y; }  return z;  }",
+            output: "function foo4() { if (true) { if (false) return x; return y; } else { return z; } }",  // Other case is fixed in the second pass.
             errors: [{ message: "Unnecessary 'else' after 'return'.", type: "ReturnStatement" }, { message: "Unnecessary 'else' after 'return'.", type: "BlockStatement" }]
         },
         {
@@ -61,7 +61,7 @@ ruleTester.run("no-else-return", rule, {
         },
         {
             code: "function foo7() { if (true) { if (false) { if (true) return x; else return y; } return w; } else { return z; } }",
-            output: "function foo7() { if (true) { if (false) { if (true) return x; return y; } return w; }  return z;  }",
+            output: "function foo7() { if (true) { if (false) { if (true) return x; return y; } return w; } else { return z; } }",  // Other case is fixed in the second pass.
             errors: [
                 { message: "Unnecessary 'else' after 'return'.", type: "ReturnStatement" },
                 { message: "Unnecessary 'else' after 'return'.", type: "BlockStatement" }
@@ -69,7 +69,7 @@ ruleTester.run("no-else-return", rule, {
         },
         {
             code: "function foo8() { if (true) { if (false) { if (true) return x; else return y; } else { w = x; } } else { return z; } }",
-            output: "function foo8() { if (true) { if (false) { if (true) return x; return y; }  w = x;  } else { return z; } }",
+            output: "function foo8() { if (true) { if (false) { if (true) return x; return y; } else { w = x; } } else { return z; } }",  // Other case is fixed in the second pass.
             errors: [
                 { message: "Unnecessary 'else' after 'return'.", type: "ReturnStatement" },
                 { message: "Unnecessary 'else' after 'return'.", type: "BlockStatement" }
diff --git a/tests/lib/rules/no-useless-return.js b/tests/lib/rules/no-useless-return.js
index df626e809..355506aea 100644
--- a/tests/lib/rules/no-useless-return.js
+++ b/tests/lib/rules/no-useless-return.js
@@ -209,9 +209,9 @@ ruleTester.run("no-useless-return", rule, {
                 if (foo) {
                   
                 }
-                
+                return;
               }
-            `,
+            `,  // Other case is fixed in the second pass.
             errors: [
                 { message: "Unnecessary return statement.", type: "ReturnStatement" },
                 { message: "Unnecessary return statement.", type: "ReturnStatement" }
@@ -416,7 +416,7 @@ ruleTester.run("no-useless-return", rule, {
         },
         {
             code: "function foo() { return; return; }",
-            output: "function foo() {   }",
+            output: "function foo() {  return; }",  // Other case is fixed in the second pass.
             errors: [
                 { message: "Unnecessary return statement.", type: "ReturnStatement" },
                 { message: "Unnecessary return statement.", type: "ReturnStatement" }
diff --git a/tests/lib/rules/nonblock-statement-body-position.js b/tests/lib/rules/nonblock-statement-body-position.js
new file mode 100644
index 000000000..37a3b2756
--- /dev/null
+++ b/tests/lib/rules/nonblock-statement-body-position.js
@@ -0,0 +1,336 @@
+/**
+ * @fileoverview enforce the location of single-line statements
+ * @author Teddy Katz
+ */
+"use strict";
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const rule = require("../../../lib/rules/nonblock-statement-body-position");
+const RuleTester = require("../../../lib/testers/rule-tester");
+
+const EXPECTED_LINEBREAK = { message: "Expected a linebreak before this statement." };
+const UNEXPECTED_LINEBREAK = { message: "Expected no linebreak before this statement." };
+
+//------------------------------------------------------------------------------
+// Tests
+//------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } });
+
+ruleTester.run("nonblock-statement-body-position", rule, {
+
+    valid: [
+
+        // 'beside' option
+        "if (foo) bar;",
+        "while (foo) bar;",
+        "do foo; while (bar)",
+        "for (;foo;) bar;",
+        "for (foo in bar) baz;",
+        "for (foo of bar) baz;",
+        "if (foo) bar; else baz;",
+        {
+            code: "if (foo) bar();",
+            options: ["beside"]
+        },
+        {
+            code: "while (foo) bar();",
+            options: ["beside"]
+        },
+        {
+            code: "do bar(); while (foo)",
+            options: ["beside"]
+        },
+        {
+            code: "for (;foo;) bar();",
+            options: ["beside"]
+        },
+
+        // 'below' option
+        {
+            code: `
+                if (foo)
+                    bar();
+            `,
+            options: ["below"]
+        },
+        {
+            code: `
+                while (foo)
+                    bar();
+            `,
+            options: ["below"]
+        },
+        {
+            code: `
+                do
+                    bar();
+                while (foo)
+            `,
+            options: ["below"]
+        },
+        {
+            code: `
+                for (;foo;)
+                    bar();
+            `,
+            options: ["below"]
+        },
+        {
+            code: `
+                for (foo in bar)
+                    bar();
+            `,
+            options: ["below"]
+        },
+        {
+            code: `
+                for (foo of bar)
+                    bar();
+            `,
+            options: ["below"]
+        },
+        {
+            code: `
+                if (foo)
+                    bar();
+                else
+                    baz();
+            `,
+            options: ["below"]
+        },
+
+        // 'any' option
+        {
+            code: "if (foo) bar();",
+            options: ["any"]
+        },
+        {
+            code: `
+                if (foo)
+                    bar();
+            `,
+            options: ["any"]
+        },
+
+        // 'overrides' option
+        {
+            code: "if (foo) bar();",
+            options: ["beside", { overrides: { while: "below" } }]
+        },
+        {
+            code: `
+                while (foo)
+                    bar();
+            `,
+            options: ["beside", { overrides: { while: "below" } }]
+        },
+        {
+            code: `
+                while (foo)
+                    bar();
+            `,
+            options: ["beside", { overrides: { while: "any" } }]
+        },
+        {
+            code: "while (foo) bar();",
+            options: ["beside", { overrides: { while: "any" } }]
+        },
+        {
+            code: "while (foo) bar();",
+            options: ["any", { overrides: { while: "beside" } }]
+        },
+        {
+            code: " ",
+            options: ["any", { overrides: { if: "any", else: "any", for: "any", while: "any", do: "any" } }]
+        },
+
+        // ignore 'else if'
+        `
+            if (foo) {
+            } else if (bar) {
+            }
+        `,
+        {
+            code: `
+                if (foo) {
+                } else if (bar) {
+                }
+            `,
+            options: ["below"]
+        },
+        `
+            if (foo) {
+            } else
+              if (bar) {
+              }
+        `,
+        {
+            code: `
+                if (foo) {
+                } else
+                  if (bar) {
+                  }
+            `,
+            options: ["beside"]
+        }
+    ],
+
+    invalid: [
+
+        // 'beside' option
+        {
+            code: `
+                if (foo)
+                    bar();
+            `,
+            output: `
+                if (foo) bar();
+            `,
+            errors: [UNEXPECTED_LINEBREAK]
+        },
+        {
+            code: `
+                while (foo)
+                    bar();
+            `,
+            output: `
+                while (foo) bar();
+            `,
+            errors: [UNEXPECTED_LINEBREAK]
+        },
+        {
+            code: `
+                do
+                    bar();
+                while (foo)
+            `,
+            output: `
+                do bar();
+                while (foo)
+            `,
+            errors: [UNEXPECTED_LINEBREAK]
+        },
+        {
+            code: `
+                for (;foo;)
+                    bar();
+            `,
+            output: `
+                for (;foo;) bar();
+            `,
+            errors: [UNEXPECTED_LINEBREAK]
+        },
+        {
+            code: `
+                for (foo in bar)
+                    baz();
+            `,
+            output: `
+                for (foo in bar) baz();
+            `,
+            errors: [UNEXPECTED_LINEBREAK]
+        },
+        {
+            code: `
+                for (foo of bar)
+                    baz();
+            `,
+            output: `
+                for (foo of bar) baz();
+            `,
+            errors: [UNEXPECTED_LINEBREAK]
+        },
+        {
+            code: `
+                if (foo)
+                    bar();
+                else
+                    baz();
+            `,
+            output: `
+                if (foo) bar();
+                else baz();
+            `,
+            errors: [UNEXPECTED_LINEBREAK, UNEXPECTED_LINEBREAK]
+        },
+
+        // 'below' option
+        {
+            code: "if (foo) bar();",
+            output: "if (foo) \nbar();",
+            options: ["below"],
+            errors: [EXPECTED_LINEBREAK]
+        },
+        {
+            code: "while (foo) bar();",
+            output: "while (foo) \nbar();",
+            options: ["below"],
+            errors: [EXPECTED_LINEBREAK]
+        },
+        {
+            code: "do bar(); while (foo)",
+            output: "do \nbar(); while (foo)",
+            options: ["below"],
+            errors: [EXPECTED_LINEBREAK]
+        },
+        {
+            code: "for (;foo;) bar();",
+            output: "for (;foo;) \nbar();",
+            options: ["below"],
+            errors: [EXPECTED_LINEBREAK]
+        },
+        {
+            code: "for (foo in bar) baz();",
+            output: "for (foo in bar) \nbaz();",
+            options: ["below"],
+            errors: [EXPECTED_LINEBREAK]
+        },
+        {
+            code: "for (foo of bar) baz();",
+            output: "for (foo of bar) \nbaz();",
+            options: ["below"],
+            errors: [EXPECTED_LINEBREAK]
+        },
+        {
+            code: `
+                if (foo) bar();
+                else baz();
+            `,
+            output: `
+                if (foo) \nbar();
+                else \nbaz();
+            `,
+            options: ["below"],
+            errors: [EXPECTED_LINEBREAK, EXPECTED_LINEBREAK]
+        },
+
+        // overrides
+        {
+            code: "if (foo) bar();",
+            output: "if (foo) \nbar();",
+            options: ["below", { overrides: { while: "beside" } }],
+            errors: [EXPECTED_LINEBREAK]
+        },
+        {
+            code: `
+                while (foo)
+                    bar();
+            `,
+            output: `
+                while (foo) bar();
+            `,
+            options: ["below", { overrides: { while: "beside" } }],
+            errors: [UNEXPECTED_LINEBREAK]
+        },
+        {
+            code: "do bar(); while (foo)",
+            output: "do \nbar(); while (foo)",
+            options: ["any", { overrides: { do: "below" } }],
+            errors: [EXPECTED_LINEBREAK]
+        }
+    ]
+});
diff --git a/tests/lib/rules/padded-blocks.js b/tests/lib/rules/padded-blocks.js
index 5281172a2..c9208183b 100644
--- a/tests/lib/rules/padded-blocks.js
+++ b/tests/lib/rules/padded-blocks.js
@@ -30,6 +30,11 @@ ruleTester.run("padded-blocks", rule, {
         { code: "{\n\na()\n//comment\n\n}" },
         { code: "{\n\na = 1\n\n}" },
         { code: "{//comment\n\na();\n\n}" },
+        { code: "{ /* comment */\n\na();\n\n}" },
+        { code: "{ /* comment \n */\n\na();\n\n}" },
+        { code: "{ /* comment \n */ /* another comment \n */\n\na();\n\n}" },
+        { code: "{ /* comment \n */ /* another comment \n */\n\na();\n\n/* comment \n */ /* another comment \n */}" },
+
         { code: "{\n\na();\n\n/* comment */ }" },
         { code: "{\n\na();\n\n/* comment */ }", options: ["always"] },
         { code: "{\n\na();\n\n/* comment */ }", options: [{ blocks: "always" }] },
@@ -85,6 +90,17 @@ ruleTester.run("padded-blocks", rule, {
                 }
             ]
         },
+        {
+            code: "{ //comment\na();\n\n}",
+            output: "{ //comment\n\na();\n\n}",
+            errors: [
+                {
+                    message: ALWAYS_MESSAGE,
+                    line: 1,
+                    column: 3
+                }
+            ]
+        },
         {
             code: "{\n\na();\n//comment\n}",
             output: "{\n\na();\n//comment\n\n}",
@@ -466,9 +482,39 @@ ruleTester.run("padded-blocks", rule, {
         },
         {
             code: "function foo() { // a\n\n  b;\n}",
-            output: "function foo() { // a\n\n  b;\n}",
+            output: "function foo() { // a\n  b;\n}",
+            options: ["never"],
+            errors: [NEVER_MESSAGE]
+        },
+        {
+            code: "function foo() { /* a\n */\n\n  bar;\n}",
+            output: "function foo() { /* a\n */\n  bar;\n}",
             options: ["never"],
             errors: [NEVER_MESSAGE]
+        },
+        {
+            code: "function foo() {\n\n  bar;\n/* a\n */}",
+            output: "function foo() {\n\n  bar;\n\n/* a\n */}",
+            options: ["always"],
+            errors: [ALWAYS_MESSAGE]
+        },
+        {
+            code: "function foo() { /* a\n */\n/* b\n */\n  bar;\n}",
+            output: "function foo() { /* a\n */\n\n/* b\n */\n  bar;\n\n}",
+            options: ["always"],
+            errors: [ALWAYS_MESSAGE, ALWAYS_MESSAGE]
+        },
+        {
+            code: "function foo() { /* a\n */ /* b\n */\n  bar;\n}",
+            output: "function foo() { /* a\n */ /* b\n */\n\n  bar;\n\n}",
+            options: ["always"],
+            errors: [ALWAYS_MESSAGE, ALWAYS_MESSAGE]
+        },
+        {
+            code: "function foo() { /* a\n */ /* b\n */\n  bar;\n/* c\n *//* d\n */}",
+            output: "function foo() { /* a\n */ /* b\n */\n\n  bar;\n\n/* c\n *//* d\n */}",
+            options: ["always"],
+            errors: [ALWAYS_MESSAGE, ALWAYS_MESSAGE]
         }
     ]
 });
diff --git a/tests/lib/util/fix-tracker.js b/tests/lib/util/fix-tracker.js
new file mode 100644
index 000000000..bceb0de22
--- /dev/null
+++ b/tests/lib/util/fix-tracker.js
@@ -0,0 +1,147 @@
+/**
+ * @fileoverview Tests for FixTracker.
+ * @author Alan Pierce
+ */
+"use strict";
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const assert = require("chai").assert,
+    espree = require("espree"),
+    FixTracker = require("../../../lib/util/fix-tracker"),
+    ruleFixer = require("../../../lib/util/rule-fixer"),
+    SourceCode = require("../../../lib/util/source-code"),
+    Traverser = require("../../../lib/util/traverser");
+
+//------------------------------------------------------------------------------
+// Helpers
+//------------------------------------------------------------------------------
+
+const DEFAULT_CONFIG = {
+    ecmaVersion: 6,
+    comment: true,
+    tokens: true,
+    range: true,
+    loc: true
+};
+
+/**
+ * Create a SourceCode instance from the given code. Also add parent pointers in
+ * the AST so that parent traversals will work.
+ *
+ * @param {string} text The text of the code.
+ * @returns {SourceCode} The SourceCode.
+ */
+function createSourceCode(text) {
+    const ast = espree.parse(text, DEFAULT_CONFIG);
+
+    new Traverser().traverse(ast, {
+        enter(node, parent) {
+            node.parent = parent;
+        }
+    });
+
+    return new SourceCode(text, ast);
+}
+
+//------------------------------------------------------------------------------
+// Tests
+//------------------------------------------------------------------------------
+
+describe("FixTracker", () => {
+    describe("replaceTextRange", () => {
+        it("should expand to include an explicitly retained range", () => {
+            const sourceCode = createSourceCode("var foo = +bar;");
+            const result = new FixTracker(ruleFixer, sourceCode)
+                .retainRange([4, 14])
+                .replaceTextRange([10, 11], "-");
+
+            assert.deepEqual(result, {
+                range: [4, 14],
+                text: "foo = -bar"
+            });
+        });
+
+        it("ignores a retained range that's smaller than the replaced range", () => {
+            const sourceCode = createSourceCode("abcdefghij");
+            const result = new FixTracker(ruleFixer, sourceCode)
+                .retainRange([5, 7])
+                .replaceTextRange([4, 8], "123");
+
+            assert.deepEqual(result, {
+                range: [4, 8],
+                text: "123"
+            });
+        });
+
+        it("allows an unspecified retained range", () => {
+            const sourceCode = createSourceCode("abcdefghij");
+            const result = new FixTracker(ruleFixer, sourceCode)
+                .replaceTextRange([4, 8], "123");
+
+            assert.deepEqual(result, {
+                range: [4, 8],
+                text: "123"
+            });
+        });
+    });
+
+    describe("remove", () => {
+        it("should expand to include an explicitly retained range", () => {
+            const sourceCode = createSourceCode("a = b + +c");
+            const result = new FixTracker(ruleFixer, sourceCode)
+                .retainRange([4, 10])
+                .remove(sourceCode.ast.tokens[4]);
+
+            assert.deepEqual(result, {
+                range: [4, 10],
+                text: "b + c"
+            });
+        });
+    });
+
+    describe("retainEnclosingFunction", () => {
+        it("handles a normal enclosing function", () => {
+            const sourceCode = createSourceCode("f = function() { return x; }");
+            const xNode = sourceCode.ast.body[0].expression.right.body.body[0].argument;
+            const result = new FixTracker(ruleFixer, sourceCode)
+                .retainEnclosingFunction(xNode)
+                .replaceTextRange(xNode.range, "y");
+
+            assert.deepEqual(result, {
+                range: [4, 28],
+                text: "function() { return y; }"
+            });
+        });
+
+        it("handles the case when there is no enclosing function", () => {
+            const sourceCode = createSourceCode("const a = b;");
+            const bNode = sourceCode.ast.body[0].declarations[0].init;
+            const result = new FixTracker(ruleFixer, sourceCode)
+                .retainEnclosingFunction(bNode)
+                .replaceTextRange(bNode.range, "c");
+
+            assert.deepEqual(result, {
+                range: [0, 12],
+                text: "const a = c;"
+            });
+        });
+    });
+
+    describe("retainSurroungingTokens", () => {
+        it("handles a change to a binary operator", () => {
+            const sourceCode = createSourceCode("const i = j + k;");
+            const plusToken = sourceCode.ast.tokens[4];
+            const result = new FixTracker(ruleFixer, sourceCode)
+                .retainSurroundingTokens(plusToken)
+                .replaceTextRange(plusToken.range, "*");
+
+            assert.deepEqual(result, {
+                range: [10, 15],
+                text: "j * k"
+            });
+        });
+    });
+});