Repository: eslint/eslint (js)

Issue:
Incorrect indented body for nested arrow functions
Trying the new indent rule on my company's code base, ran into this issue:

* **ESLint Version:** 4.0.0-alpha.0
* **Node Version:** 6.9.5
* **npm Version:** 3.10.10

**What parser (default, Babel-ESLint, etc.) are you using?**
default

**Please show your full configuration:**

```yml
---
parserOptions:
  ecmaVersion: 2017
rules:
  indent: [2, 2]
```

**What did you do? Please include the actual source code causing the issue.**

<!-- Paste the source code below: -->
```js
const f = (
  longParams
) => () => {
  return;
}
```

**What did you expect to happen?**
Nothing

**What actually happened? Please include the actual, raw output from ESLint.**
Output:
```
  4:1  error  Expected indentation of 4 spaces but found 2  indent
  5:1  error  Expected indentation of 2 spaces but found 0  indent
```

"Fixed" file:
```js
const f = (
  longParams
) => () => {
    return;
  }
```

Expected no changes.

Pull request description:
Update: handle multiline parents consistently in indent (fixes #8455)
**What is the purpose of this pull request? (put an "X" next to item)**

[x] Bug fix (https://github.com/eslint/eslint/issues/8455)

<!--
    If the item you've checked above has a template, please paste the template questions below and answer them. (If this pull request is addressing an issue, you can just paste a link to the issue here instead.)
-->

<!--
    Please ensure your pull request is ready:

    - Read the pull request guide (http://eslint.org/docs/developer-guide/contributing/pull-requests)
    - Include tests for this change
    - Update documentation for this change (if appropriate)
-->

<!--
    The following is required for all pull requests:
-->

**What changes did you make? (Give an overview)**

Previously, the indent rule was inconsistent about handling cases where a multiline node had a child that followed it on the same line. This commit fixes the rule to handle these cases consistently.

For example, the rule considers the following code to be valid:

```js
// case 1
foo.bar({
    ok: true
}).baz({
    ok: true
});

// case 2
it(['foo'
    'bar'], function() {
    baz();
})
```

However, it previously considered the following cases to be invalid (it expected another two indentation levels for the last two lines):

```js
// case 1
if (
    foo
) baz({
    ok: true
});

// case 2
[
    'foo',
    'bar'].forEach(function() {
    baz();
});
```

In both cases, the node on line 3 starts on the same line as its parent ends on. However, the rule treated them differently because it had a special case for `MemberExpression` nodes. As a result of this change, all four code samples are valid.

**Is there anything you'd like reviewers to focus on?**

Nothing in particular


Patch:
diff --git a/lib/rules/indent.js b/lib/rules/indent.js
index 9774708c3..56ce2e13e 100644
--- a/lib/rules/indent.js
+++ b/lib/rules/indent.js
@@ -667,14 +667,22 @@ module.exports = {
          * Check and decide whether to check for indentation for blockless nodes
          * Scenarios are for or while statements without braces around them
          * @param {ASTNode} node node to examine
-         * @param {ASTNode} parent The parent of the node to examine
          * @returns {void}
          */
-        function addBlocklessNodeIndent(node, parent) {
+        function addBlocklessNodeIndent(node) {
             if (node.type !== "BlockStatement") {
-                const firstParentToken = sourceCode.getFirstToken(parent);
+                const lastParentToken = sourceCode.getTokenBefore(node, astUtils.isNotOpeningParenToken);
 
-                offsets.setDesiredOffsets(getTokensAndComments(node), firstParentToken, 1);
+                let bodyTokens = getTokensAndComments(node);
+
+                while (
+                    astUtils.isOpeningParenToken(sourceCode.getTokenBefore(bodyTokens[0])) &&
+                    astUtils.isClosingParenToken(sourceCode.getTokenAfter(bodyTokens[bodyTokens.length - 1]))
+                ) {
+                    bodyTokens = [sourceCode.getTokenBefore(bodyTokens[0])].concat(bodyTokens).concat(sourceCode.getTokenAfter(bodyTokens[bodyTokens.length - 1]));
+                }
+
+                offsets.setDesiredOffsets(bodyTokens, lastParentToken, 1);
 
                 /*
                  * For blockless nodes with semicolon-first style, don't indent the semicolon.
@@ -685,7 +693,7 @@ module.exports = {
                 const lastToken = sourceCode.getLastToken(node);
 
                 if (astUtils.isSemicolonToken(lastToken)) {
-                    offsets.matchIndentOf(firstParentToken, lastToken);
+                    offsets.matchIndentOf(lastParentToken, lastToken);
                 }
             }
         }
@@ -813,9 +821,16 @@ module.exports = {
 
             ArrowFunctionExpression(node) {
                 addFunctionParamsIndent(node, options.FunctionExpression.parameters);
-                if (node.body.type !== "BlockStatement") {
-                    offsets.setDesiredOffsets(getTokensAndComments(node.body), sourceCode.getFirstToken(node), 1);
+                addBlocklessNodeIndent(node.body);
+
+                let arrowToken;
+
+                if (node.params.length) {
+                    arrowToken = sourceCode.getTokenAfter(node.params[node.params.length - 1], astUtils.isArrowToken);
+                } else {
+                    arrowToken = sourceCode.getFirstToken(node, astUtils.isArrowToken);
                 }
+                offsets.matchIndentOf(sourceCode.getFirstToken(node), arrowToken);
             },
 
             AssignmentExpression(node) {
@@ -846,7 +861,7 @@ module.exports = {
                 }
             },
 
-            DoWhileStatement: node => addBlocklessNodeIndent(node.body, node),
+            DoWhileStatement: node => addBlocklessNodeIndent(node.body),
 
             ExportNamedDeclaration(node) {
                 if (node.declaration === null) {
@@ -865,9 +880,9 @@ module.exports = {
                 }
             },
 
-            ForInStatement: node => addBlocklessNodeIndent(node.body, node),
+            ForInStatement: node => addBlocklessNodeIndent(node.body),
 
-            ForOfStatement: node => addBlocklessNodeIndent(node.body, node),
+            ForOfStatement: node => addBlocklessNodeIndent(node.body),
 
             ForStatement(node) {
                 const forOpeningParen = sourceCode.getFirstToken(node, 1);
@@ -881,7 +896,7 @@ module.exports = {
                 if (node.update) {
                     offsets.setDesiredOffsets(getTokensAndComments(node.update), forOpeningParen, 1);
                 }
-                addBlocklessNodeIndent(node.body, node);
+                addBlocklessNodeIndent(node.body);
             },
 
             FunctionDeclaration(node) {
@@ -893,9 +908,9 @@ module.exports = {
             },
 
             IfStatement(node) {
-                addBlocklessNodeIndent(node.consequent, node);
+                addBlocklessNodeIndent(node.consequent);
                 if (node.alternate && node.alternate.type !== "IfStatement") {
-                    addBlocklessNodeIndent(node.alternate, node);
+                    addBlocklessNodeIndent(node.alternate);
                 }
             },
 
@@ -927,18 +942,15 @@ module.exports = {
                     offsets.matchIndentOf(firstNonObjectToken, sourceCode.getLastToken(node));
                 }
 
-                offsets.setDesiredOffsets(tokensToIndent, tokensToIndent[0], 0);
-
                 if (typeof options.MemberExpression !== "number") {
-                    tokensToIndent.forEach(token => {
-                        offsets.matchIndentOf(tokenInfo.getFirstTokenOfLine(token), token);
-                        offsets.ignoreToken(token);
-                    });
-                } else if (node.object.loc.end.line === node.property.loc.start.line) {
-                    offsets.setDesiredOffsets(tokensToIndent, tokenInfo.getFirstTokenOfLine(sourceCode.getLastToken(node.object)), options.MemberExpression);
-                } else {
-                    offsets.setDesiredOffsets(tokensToIndent, sourceCode.getFirstToken(node.object), options.MemberExpression);
+                    tokensToIndent.forEach(token => offsets.ignoreToken(token));
                 }
+
+                const offsetBase = node.object.loc.end.line === node.property.loc.start.line
+                    ? sourceCode.getLastToken(node.object)
+                    : sourceCode.getFirstToken(node.object);
+
+                offsets.setDesiredOffsets(tokensToIndent, offsetBase, options.MemberExpression);
             },
 
             NewExpression(node) {
@@ -1046,7 +1058,7 @@ module.exports = {
                 }
             },
 
-            WhileStatement: node => addBlocklessNodeIndent(node.body, node),
+            WhileStatement: node => addBlocklessNodeIndent(node.body),
 
             "Program:exit"() {
                 addParensIndent(sourceCode.ast.tokens);


Test patch:
diff --git a/tests/lib/rules/indent.js b/tests/lib/rules/indent.js
index 2f9b93660..7d04e50a3 100644
--- a/tests/lib/rules/indent.js
+++ b/tests/lib/rules/indent.js
@@ -775,8 +775,8 @@ ruleTester.run("indent", rule, {
             code: unIndent`
                 [a, boop,
                     c].forEach((index) => {
-                        index;
-                    });
+                    index;
+                });
             `,
             options: [4],
             parserOptions: { ecmaVersion: 6 }
@@ -785,8 +785,8 @@ ruleTester.run("indent", rule, {
             code: unIndent`
                 [a, b,
                     c].forEach(function(index){
-                        return index;
-                    });
+                    return index;
+                });
             `,
             options: [4],
             parserOptions: { ecmaVersion: 6 }
@@ -3005,10 +3005,10 @@ ruleTester.run("indent", rule, {
         },
         {
             code: unIndent`
-                [ foop,
+                [ foo,
                   bar ].forEach(function() {
-                    baz;
-                  })
+                  baz;
+                })
             `,
             options: [2, { ArrayExpression: "first", MemberExpression: 1 }]
         },
@@ -3066,6 +3066,73 @@ ruleTester.run("indent", rule, {
         {
             code: "import 'foo'",
             parserOptions: { sourceType: "module" }
+        },
+
+        // https://github.com/eslint/eslint/issues/8455
+        {
+            code: unIndent`
+                (
+                    a
+                ) => b => {
+                    c
+                }
+            `,
+            parserOptions: { ecmaVersion: 6 }
+        },
+        {
+            code: unIndent`
+                (
+                    a
+                ) => b => c => d => {
+                    e
+                }
+            `,
+            parserOptions: { ecmaVersion: 6 }
+        },
+        {
+            code: unIndent`
+                (
+                    a
+                ) =>
+                    (
+                        b
+                    ) => {
+                        c
+                    }
+            `,
+            parserOptions: { ecmaVersion: 6 }
+        },
+        {
+            code: unIndent`
+                if (
+                    foo
+                ) bar(
+                    baz
+                );
+            `
+        },
+        {
+            code: unIndent`
+                () =>
+                    ({})
+            `,
+            parserOptions: { ecmaVersion: 6 }
+        },
+        {
+            code: unIndent`
+                () =>
+                    (({}))
+            `,
+            parserOptions: { ecmaVersion: 6 }
+        },
+        {
+            code: unIndent`
+                (
+                    () =>
+                        ({})
+                )
+            `,
+            parserOptions: { ecmaVersion: 6 }
         }
     ],
 
@@ -3749,22 +3816,21 @@ ruleTester.run("indent", rule, {
         {
             code: unIndent`
                 [a, b,
-                c].forEach((index) => {
-                  index;
-                });
+                    c].forEach((index) => {
+                        index;
+                    });
             `,
             output: unIndent`
                 [a, b,
                     c].forEach((index) => {
-                        index;
-                    });
+                    index;
+                });
             `,
             options: [4],
             parserOptions: { ecmaVersion: 6 },
             errors: expectedErrors([
-                [2, 4, 0, "Identifier"],
-                [3, 8, 2, "Identifier"],
-                [4, 4, 0, "Punctuator"]
+                [3, 4, 8, "Identifier"],
+                [4, 0, 4, "Punctuator"]
             ])
         },
         {
@@ -3777,15 +3843,14 @@ ruleTester.run("indent", rule, {
             output: unIndent`
                 [a, b,
                     c].forEach(function(index){
-                        return index;
-                    });
+                    return index;
+                });
             `,
             options: [4],
             parserOptions: { ecmaVersion: 6 },
             errors: expectedErrors([
                 [2, 4, 0, "Identifier"],
-                [3, 8, 2, "Keyword"],
-                [4, 4, 0, "Punctuator"]
+                [3, 4, 2, "Keyword"]
             ])
         },
         {
@@ -6222,19 +6287,19 @@ ruleTester.run("indent", rule, {
         },
         {
             code: unIndent`
-                [ foop,
+                [ foo,
                   bar ].forEach(function() {
-                  baz;
-                })
+                    baz;
+                  })
             `,
             output: unIndent`
-                [ foop,
+                [ foo,
                   bar ].forEach(function() {
-                    baz;
-                  })
+                  baz;
+                })
             `,
             options: [2, { ArrayExpression: "first", MemberExpression: 1 }],
-            errors: expectedErrors([[3, 4, 2, "Identifier"], [4, 2, 0, "Punctuator"]])
+            errors: expectedErrors([[3, 2, 4, "Identifier"], [4, 0, 2, "Punctuator"]])
         },
         {
             code: unIndent`
@@ -6316,6 +6381,59 @@ ruleTester.run("indent", rule, {
             `,
             parserOptions: { sourceType: "module" },
             errors: expectedErrors([2, 4, 0, "Identifier"])
+        },
+        {
+            code: unIndent`
+                (
+                    a
+                ) => b => {
+                        c
+                    }
+            `,
+            output: unIndent`
+                (
+                    a
+                ) => b => {
+                    c
+                }
+            `,
+            parserOptions: { ecmaVersion: 6 },
+            errors: expectedErrors([[4, 4, 8, "Identifier"], [5, 0, 4, "Punctuator"]])
+        },
+        {
+            code: unIndent`
+                (
+                    a
+                ) => b => c => d => {
+                        e
+                    }
+            `,
+            output: unIndent`
+                (
+                    a
+                ) => b => c => d => {
+                    e
+                }
+            `,
+            parserOptions: { ecmaVersion: 6 },
+            errors: expectedErrors([[4, 4, 8, "Identifier"], [5, 0, 4, "Punctuator"]])
+        },
+        {
+            code: unIndent`
+                if (
+                    foo
+                ) bar(
+                        baz
+                    );
+            `,
+            output: unIndent`
+                if (
+                    foo
+                ) bar(
+                    baz
+                );
+            `,
+            errors: expectedErrors([[4, 4, 8, "Identifier"], [5, 0, 4, "Punctuator"]])
         }
     ]
 });