Repository: eslint/eslint
Pull request #8161 (2017-02-26 23:14:31)

Chore: upgrade `coveralls` to ^2.11.16
**What is the purpose of this pull request? (put an "X" next to item)**

[ ] Documentation update
[ ] Bug fix ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/bug-report.md))
[ ] New rule ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/rule-proposal.md))
[ ] Changes an existing rule ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/rule-change-proposal.md))
[ ] Add autofixing to a rule
[ ] Add a CLI option
[ ] Add something to the core
[X] Other, please explain:

Upgrade coveralls to get rid of warning on install about using a deprecated package. Also added `^` to version.

<!--
    If the item you've checked above has a template, please paste the template questions below and answer them. (If this pull request is addressing an issue, you can just paste a link to the issue here instead.)
-->

<!--
    Please ensure your pull request is ready:

    - Read the pull request guide (http://eslint.org/docs/developer-guide/contributing/pull-requests)
    - Include tests for this change
    - Update documentation for this change (if appropriate)
-->

<!--
    The following is required for all pull requests:
-->

**Is there anything you'd like reviewers to focus on?**
I used "Chore" tag instead of "Upgrade" since this is a dev dependency, so it seemed more appropriate.



mention-bot: @alberto, thanks for your PR! By analyzing the history of the files in this pull request, we identified @nzakas, @mysticatea and @gyandeeps to be potential reviewers.
eslintbot: LGTM
not-an-aardvark: > I used "Chore" tag instead of "Upgrade" since this is a dev dependency, so it seemed more appropriate.

Oops, I just saw this. Never mind what I said in the review.

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/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"
+            });
+        });
+    });
+});