Repository: eslint/eslint (js)

Issue:
"no-mixed-operators" always outputs errors in pairs of two
**Tell us about your environment**

* **ESLint Version:** 3.15.0
* **Node Version:** 7.5.0
* **npm Version:** 4.1.2

**What parser (default, Babel-ESLint, etc.) are you using?** default

**Please show your full configuration:**

```
    "no-mixed-operators": ["error", {
        "groups": [
            ["+", "-", "*", "/", "%", "**"],
            ["==", "!=", "===", "!==", ">", ">=", "<", "<="],
            ["&&", "||"],
            ["in", "instanceof"]
        ],
        "allowSamePrecedence": true
    }],
```

**What did you do? Please include the actual source code causing the issue.**

```js
var eLen = nBytes * 8 - mLen - 1
```

**What did you expect to happen?**

One error about mixed operators.

**What actually happened? Please include the actual, raw output from ESLint.**

Two errors about mixed operators, one for each operator:

```
  index.js:3:21: Unexpected mix of '*' and '-'. (no-mixed-operators)
  index.js:3:25: Unexpected mix of '*' and '-'. (no-mixed-operators)
```

Is this expected behavior? It seems there is no case where this rule emits a single error, since by definition "mixing" operators requires 2 operators. It would be more intuitive to me if a single error was emitted.

Pull request:
Docs: clarify no-mixed-operators (fixes #8051)
Update docs for `no-mixed-operators` to clarify that a pair of errors will be triggered for each pair of mixed consecutive operators used. Fixes #8051.

<!--
    ESLint adheres to the [JS Foundation Code of Conduct](https://js.foundation/community/code-of-conduct).
-->

**What is the purpose of this pull request? (put an "X" next to item)**

[x] 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
[ ] Other, please explain:

<!--
    If the item you've checked above has a template, please paste the template questions below and answer them. (If this pull request is addressing an issue, you can just paste a link to the issue here instead.)
-->

<!--
    Please ensure your pull request is ready:

    - Read the pull request guide (https://eslint.org/docs/developer-guide/contributing/pull-requests)
    - Include tests for this change
    - Update documentation for this change (if appropriate)
-->

<!--
    The following is required for all pull requests:
-->

**What changes did you make? (Give an overview)**
Just an update to the documentation for the `no-mixed-operators` rule.

**Is there anything you'd like reviewers to focus on?**




Patch:
diff --git a/docs/rules/block-spacing.md b/docs/rules/block-spacing.md
index 2e37fe54b..aef7c333c 100644
--- a/docs/rules/block-spacing.md
+++ b/docs/rules/block-spacing.md
@@ -1,8 +1,8 @@
-# Disallow or enforce spaces inside of single line blocks (block-spacing)
+# Disallow or enforce spaces inside of blocks after opening block and before closing block (block-spacing)
 
 ## Rule Details
 
-This rule enforces consistent spacing inside single-line blocks.
+This rule enforces consistent spacing inside an open block token and the next token on the same line. This rule also enforces consistent spacing inside a close block token and previous token on the same line.
 
 ## Options
 
@@ -20,6 +20,9 @@ Examples of **incorrect** code for this rule with the default `"always"` option:
 
 function foo() {return true;}
 if (foo) { bar = 0;}
+function baz() {let i = 0;
+    return i;
+}
 ```
 
 Examples of **correct** code for this rule with the default `"always"` option:
diff --git a/docs/rules/no-mixed-operators.md b/docs/rules/no-mixed-operators.md
index b6f6fb6e8..1746c5ed6 100644
--- a/docs/rules/no-mixed-operators.md
+++ b/docs/rules/no-mixed-operators.md
@@ -9,6 +9,21 @@ var foo = (a && b) || c || d;  /*GOOD*/
 var foo = a && (b || c || d);  /*GOOD*/
 ```
 
+**Note:**
+It is expected for this rule to emit one error for each mixed operator in a pair. As a result, for each two consecutive mixed operators used, a distinct error will be displayed, pointing to where the specific operator that breaks the rule is used:
+
+```js
+var foo = a && b || c || d;
+```
+
+will generate
+
+```sh
+1:13  Unexpected mix of '&&' and '||'. (no-mixed-operators)
+1:18  Unexpected mix of '&&' and '||'. (no-mixed-operators)
+```
+
+
 ## Rule Details
 
 This rule checks `BinaryExpression` and `LogicalExpression`.
diff --git a/lib/rules/block-spacing.js b/lib/rules/block-spacing.js
index f18381a31..b3ea8405e 100644
--- a/lib/rules/block-spacing.js
+++ b/lib/rules/block-spacing.js
@@ -14,7 +14,7 @@ const util = require("../ast-utils");
 module.exports = {
     meta: {
         docs: {
-            description: "enforce consistent spacing inside single-line blocks",
+            description: "disallow or enforce spaces inside of blocks after opening block and before closing block",
             category: "Stylistic Issues",
             recommended: false
         },
diff --git a/lib/rules/indent.js b/lib/rules/indent.js
index 0f6468a7e..d33ed1ab9 100644
--- a/lib/rules/indent.js
+++ b/lib/rules/indent.js
@@ -235,10 +235,12 @@ class OffsetStorage {
     /**
      * @param {TokenInfo} tokenInfo a TokenInfo instance
      * @param {number} indentSize The desired size of each indentation level
+     * @param {string} indentType The indentation character
      */
-    constructor(tokenInfo, indentSize) {
+    constructor(tokenInfo, indentSize, indentType) {
         this._tokenInfo = tokenInfo;
         this._indentSize = indentSize;
+        this._indentType = indentType;
 
         this._tree = new BinarySearchTree();
         this._tree.insert(0, { offset: 0, from: null, force: false });
@@ -408,7 +410,7 @@ class OffsetStorage {
     /**
     * Gets the desired indent of a token
     * @param {Token} token The token
-    * @returns {number} The desired indent of the token
+    * @returns {string} The desired indent of the token
     */
     getDesiredIndent(token) {
         if (!this._desiredIndentCache.has(token)) {
@@ -417,7 +419,10 @@ class OffsetStorage {
 
                 // If the token is ignored, use the actual indent of the token as the desired indent.
                 // This ensures that no errors are reported for this token.
-                this._desiredIndentCache.set(token, this._tokenInfo.getTokenIndent(token).length / this._indentSize);
+                this._desiredIndentCache.set(
+                    token,
+                    this._tokenInfo.getTokenIndent(token)
+                );
             } else if (this._lockedFirstTokens.has(token)) {
                 const firstToken = this._lockedFirstTokens.get(token);
 
@@ -428,7 +433,7 @@ class OffsetStorage {
                     this.getDesiredIndent(this._tokenInfo.getFirstTokenOfLine(firstToken)) +
 
                         // (space between the start of the first element's line and the first element)
-                        (firstToken.loc.start.column - this._tokenInfo.getFirstTokenOfLine(firstToken).loc.start.column) / this._indentSize
+                        this._indentType.repeat(firstToken.loc.start.column - this._tokenInfo.getFirstTokenOfLine(firstToken).loc.start.column)
                 );
             } else {
                 const offsetInfo = this._getOffsetDescriptor(token);
@@ -436,9 +441,12 @@ class OffsetStorage {
                     offsetInfo.from &&
                     offsetInfo.from.loc.start.line === token.loc.start.line &&
                     !offsetInfo.force
-                ) ? 0 : offsetInfo.offset;
+                ) ? 0 : offsetInfo.offset * this._indentSize;
 
-                this._desiredIndentCache.set(token, offset + (offsetInfo.from ? this.getDesiredIndent(offsetInfo.from) : 0));
+                this._desiredIndentCache.set(
+                    token,
+                    (offsetInfo.from ? this.getDesiredIndent(offsetInfo.from) : "") + this._indentType.repeat(offset)
+                );
             }
         }
         return this._desiredIndentCache.get(token);
@@ -655,7 +663,7 @@ module.exports = {
 
         const sourceCode = context.getSourceCode();
         const tokenInfo = new TokenInfo(sourceCode);
-        const offsets = new OffsetStorage(tokenInfo, indentSize);
+        const offsets = new OffsetStorage(tokenInfo, indentSize, indentType === "space" ? " " : "\t");
         const parameterParens = new WeakSet();
 
         /**
@@ -688,27 +696,24 @@ module.exports = {
         /**
          * Reports a given indent violation
          * @param {Token} token Token violating the indent rule
-         * @param {int} neededIndentLevel Expected indentation level
-         * @param {int} gottenSpaces Actual number of indentation spaces for the token
-         * @param {int} gottenTabs Actual number of indentation tabs for the token
+         * @param {string} neededIndent Expected indentation string
          * @returns {void}
          */
-        function report(token, neededIndentLevel) {
+        function report(token, neededIndent) {
             const actualIndent = Array.from(tokenInfo.getTokenIndent(token));
             const numSpaces = actualIndent.filter(char => char === " ").length;
             const numTabs = actualIndent.filter(char => char === "\t").length;
-            const neededChars = neededIndentLevel * indentSize;
 
             context.report({
                 node: token,
-                message: createErrorMessage(neededChars, numSpaces, numTabs),
+                message: createErrorMessage(neededIndent.length, numSpaces, numTabs),
                 loc: {
                     start: { line: token.loc.start.line, column: 0 },
                     end: { line: token.loc.start.line, column: token.loc.start.column }
                 },
                 fix(fixer) {
                     const range = [token.range[0] - token.loc.start.column, token.range[0]];
-                    const newText = (indentType === "space" ? " " : "\t").repeat(neededChars);
+                    const newText = neededIndent;
 
                     return fixer.replaceTextRange(range, newText);
                 }
@@ -718,14 +723,13 @@ module.exports = {
         /**
          * Checks if a token's indentation is correct
          * @param {Token} token Token to examine
-         * @param {int} desiredIndentLevel needed indent level
+         * @param {string} desiredIndent Desired indentation of the string
          * @returns {boolean} `true` if the token's indentation is correct
          */
-        function validateTokenIndent(token, desiredIndentLevel) {
+        function validateTokenIndent(token, desiredIndent) {
             const indentation = tokenInfo.getTokenIndent(token);
-            const expectedChar = indentType === "space" ? " " : "\t";
 
-            return indentation === expectedChar.repeat(desiredIndentLevel * indentSize) ||
+            return indentation === desiredIndent ||
 
                 // To avoid conflicts with no-mixed-spaces-and-tabs, don't report mixed spaces and tabs.
                 indentation.includes(" ") && indentation.includes("\t");


Test patch:
diff --git a/tests/lib/rules/indent.js b/tests/lib/rules/indent.js
index 006d7f1dd..0a7718492 100644
--- a/tests/lib/rules/indent.js
+++ b/tests/lib/rules/indent.js
@@ -4744,6 +4744,32 @@ ruleTester.run("indent", rule, {
                 </foo>
             `,
             options: [4, { ignoredNodes: ["JSXOpeningElement"] }]
+        },
+        {
+            code: unIndent`
+                {
+                \tvar x = 1,
+                \t    y = 2;
+                }
+            `,
+            options: ["tab"]
+        },
+        {
+            code: unIndent`
+                var x = 1,
+                    y = 2;
+                var z;
+            `,
+            options: ["tab", { ignoredNodes: ["VariableDeclarator"] }]
+        },
+        {
+            code: unIndent`
+                [
+                    foo(),
+                    bar
+                ]
+            `,
+            options: ["tab", { ArrayExpression: "first", ignoredNodes: ["CallExpression"] }]
         }
     ],