Repository: eslint/eslint (js)

Issue:
Change in behavior of 'indent' rule in 4.18.0
<!--
    ESLint adheres to the [JS Foundation Code of Conduct](https://js.foundation/community/code-of-conduct).

    This template is for bug reports. If you are here for another reason, please see below:

    1. To propose a new rule: https://eslint.org/docs/developer-guide/contributing/new-rules
    2. To request a rule change: https://eslint.org/docs/developer-guide/contributing/rule-changes
    3. To request a change that is not a bug fix, rule change, or new rule: https://eslint.org/docs/developer-guide/contributing/changes
    4. If you have any questions, please stop by our chatroom: https://gitter.im/eslint/eslint

    Note that leaving sections blank will make it difficult for us to troubleshoot and we may have to close the issue.
-->

**Tell us about your environment**

* **ESLint Version:** 4.18.0
* **Node Version:** 8.9.4
* **npm Version:** 5.5.1

**What parser (default, Babel-ESLint, etc.) are you using?**
default with `ecmaVersion: 8`

**Please show your full configuration:**

<details>
<summary>Configuration</summary>

<!-- Paste your configuration below: -->
```yaml
extends:
  - eslint:recommended
parserOptions:
  ecmaVersion: 8
rules:
  indent:
    - error
    - tab
```

</details>

**What did you do? Please include the actual source code causing the issue, as well as the command that you used to run ESLint.**

<!-- Paste the source code below: -->

This was valid in 4.17.0:

```js
function baz2() {
	return true &&
		![].some(g => {
			return true;
		});
}
```

but 4.18.0 wants it formatted like:

```js
function baz2() {
	return true &&
		![].some(g => {
		return true;
	});
}
```

This only happens if there's a `!` in front of the `[].some` bit.

<!-- Paste the command you used to run ESLint: -->
```bash
eslint file.js
```

**What did you expect to happen?**
Same behavior as 4.17.0, per above.

**What actually happened? Please include the actual, raw output from ESLint.**
```
  4:1   error  Expected indentation of 2 tabs but found 3  indent
  5:1   error  Expected indentation of 1 tab but found 2   indent
```

Pull request description:
Update: give a node at least the indentation of its parent (fixes #9995)
<!--
    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] Bug fix (https://github.com/eslint/eslint/issues/9995)

<!--
    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)**

This updates the `indent` rule to ensure that the tokens in a non-ignored node are always indented at least as much as the first token of the node. This fixes an issue where some tokens would not have enough indentation if their node's listener did not explicitly give them an offset.

**Is there anything you'd like reviewers to focus on?**

Nothing in particular

Patch:
diff --git a/README.md b/README.md
index c333edd83..1f42b791c 100644
--- a/README.md
+++ b/README.md
@@ -227,9 +227,9 @@ In all cases, make sure your plugins' peerDependencies have been installed as we
 
 Yes, ESLint natively supports parsing JSX syntax (this must be enabled in [configuration](https://eslint.org/docs/user-guide/configuring)). Please note that supporting JSX syntax *is not* the same as supporting React. React applies specific semantics to JSX syntax that ESLint doesn't recognize. We recommend using [eslint-plugin-react](https://www.npmjs.com/package/eslint-plugin-react) if you are using React and want React semantics.
 
-### What about ECMAScript 6 support?
+### What ECMAScript versions does ESLint support?
 
-ESLint has full support for ECMAScript 6. By default, this support is off. You can enable ECMAScript 6 syntax and global variables through [configuration](https://eslint.org/docs/user-guide/configuring).
+ESLint has full support for ECMAScript 3, 5 (default), 2015, 2016, 2017, and 2018. You can set your desired ECMAScript syntax (and other settings, like global variables or your target environments) through [configuration](https://eslint.org/docs/user-guide/configuring).
 
 ### What about experimental features?
 
diff --git a/lib/rules/indent.js b/lib/rules/indent.js
index acc524630..a08b4d7e5 100644
--- a/lib/rules/indent.js
+++ b/lib/rules/indent.js
@@ -779,6 +779,19 @@ module.exports = {
             return (statement.type === "ExpressionStatement" || statement.type === "VariableDeclaration") && statement.parent.type === "Program";
         }
 
+        /**
+         * Counts the number of linebreaks that follow the last non-whitespace character in a string
+         * @param {string} string The string to check
+         * @returns {number} The number of JavaScript linebreaks that follow the last non-whitespace character,
+         * or the total number of linebreaks if the string is all whitespace.
+         */
+        function countTrailingLinebreaks(string) {
+            const trailingWhitespace = string.match(/\s*$/)[0];
+            const linebreakMatches = trailingWhitespace.match(astUtils.createGlobalLinebreakMatcher());
+
+            return linebreakMatches === null ? 0 : linebreakMatches.length;
+        }
+
         /**
          * Check indentation for lists of elements (arrays, objects, function params)
          * @param {ASTNode[]} elements List of elements that should be offset
@@ -836,8 +849,12 @@ module.exports = {
                 } else {
                     const previousElement = elements[index - 1];
                     const firstTokenOfPreviousElement = previousElement && getFirstToken(previousElement);
+                    const previousElementLastToken = previousElement && sourceCode.getLastToken(previousElement);
 
-                    if (previousElement && sourceCode.getLastToken(previousElement).loc.end.line > startToken.loc.end.line) {
+                    if (
+                        previousElement &&
+                        previousElementLastToken.loc.end.line - countTrailingLinebreaks(previousElementLastToken.value) > startToken.loc.end.line
+                    ) {
                         offsets.setDesiredOffsets(element.range, firstTokenOfPreviousElement, 0);
                     }
                 }
@@ -980,6 +997,8 @@ module.exports = {
             return !node || node.loc.start.line === token.loc.start.line;
         }
 
+        const ignoredNodeFirstTokens = new Set();
+
         const baseOffsetListeners = {
             "ArrayExpression, ArrayPattern"(node) {
                 const openingBracket = sourceCode.getFirstToken(node);
@@ -1010,15 +1029,6 @@ module.exports = {
                     addElementListIndent(node.params, openingParen, closingParen, options.FunctionExpression.parameters);
                 }
                 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.setDesiredOffset(arrowToken, sourceCode.getFirstToken(node), 0);
             },
 
             AssignmentExpression(node) {
@@ -1128,9 +1138,6 @@ module.exports = {
                          */
                         offsets.setDesiredOffset(firstAlternateToken, firstToken, 1);
                     }
-
-                    offsets.setDesiredOffsets([questionMarkToken.range[1], colonToken.range[0]], firstConsequentToken, 0);
-                    offsets.setDesiredOffsets([colonToken.range[1], node.range[1]], firstAlternateToken, 0);
                 }
             },
 
@@ -1272,20 +1279,9 @@ module.exports = {
             SwitchStatement(node) {
                 const openingCurly = sourceCode.getTokenAfter(node.discriminant, astUtils.isOpeningBraceToken);
                 const closingCurly = sourceCode.getLastToken(node);
-                const caseKeywords = node.cases.map(switchCase => sourceCode.getFirstToken(switchCase));
 
                 offsets.setDesiredOffsets([openingCurly.range[1], closingCurly.range[0]], openingCurly, options.SwitchCase);
 
-                node.cases.forEach((switchCase, index) => {
-                    const caseKeyword = caseKeywords[index];
-
-                    if (!(switchCase.consequent.length === 1 && switchCase.consequent[0].type === "BlockStatement")) {
-                        const tokenAfterCurrentCase = index === node.cases.length - 1 ? closingCurly : caseKeywords[index + 1];
-
-                        offsets.setDesiredOffsets([caseKeyword.range[1], tokenAfterCurrentCase.range[0]], caseKeyword, 1);
-                    }
-                });
-
                 if (node.cases.length) {
                     sourceCode.getTokensBetween(
                         node.cases[node.cases.length - 1],
@@ -1295,6 +1291,15 @@ module.exports = {
                 }
             },
 
+            SwitchCase(node) {
+                if (!(node.consequent.length === 1 && node.consequent[0].type === "BlockStatement")) {
+                    const caseKeyword = sourceCode.getFirstToken(node);
+                    const tokenAfterCurrentCase = sourceCode.getTokenAfter(node);
+
+                    offsets.setDesiredOffsets([caseKeyword.range[1], tokenAfterCurrentCase.range[0]], caseKeyword, 1);
+                }
+            },
+
             TemplateLiteral(node) {
                 node.expressions.forEach((expression, index) => {
                     const previousQuasi = node.quasis[index];
@@ -1385,7 +1390,6 @@ module.exports = {
                 const firstToken = sourceCode.getFirstToken(node);
 
                 offsets.setDesiredOffsets(node.name.range, firstToken, 1);
-                offsets.setDesiredOffset(sourceCode.getLastToken(node), firstToken, 0);
             },
 
             JSXExpressionContainer(node) {
@@ -1397,7 +1401,15 @@ module.exports = {
                     openingCurly,
                     1
                 );
-                offsets.setDesiredOffset(closingCurly, openingCurly, 0);
+            },
+
+            "*"(node) {
+                const firstToken = sourceCode.getFirstToken(node);
+
+                // Ensure that the children of every node are indented at least as much as the first token.
+                if (firstToken && !ignoredNodeFirstTokens.has(firstToken)) {
+                    offsets.setDesiredOffsets(node.range, firstToken, 0);
+                }
             }
         };
 
@@ -1406,7 +1418,8 @@ module.exports = {
         /*
          * To ignore the indentation of a node:
          * 1. Don't call the node's listener when entering it (if it has a listener)
-         * 2. Call `ignoreNode` on the node sometime after exiting it and before validating offsets.
+         * 2. Don't set any offsets against the first token of the node.
+         * 3. Call `ignoreNode` on the node sometime after exiting it and before validating offsets.
          */
         const offsetListeners = lodash.mapValues(
             baseOffsetListeners,
@@ -1434,7 +1447,16 @@ module.exports = {
 
         // For each ignored node selector, set up a listener to collect it into the `ignoredNodes` set.
         const ignoredNodes = new Set();
-        const addToIgnoredNodes = ignoredNodes.add.bind(ignoredNodes);
+
+        /**
+         * Ignores a node
+         * @param {ASTNode} node The node to ignore
+         * @returns {void}
+         */
+        function addToIgnoredNodes(node) {
+            ignoredNodes.add(node);
+            ignoredNodeFirstTokens.add(sourceCode.getFirstToken(node));
+        }
 
         const ignoredNodeListeners = options.ignoredNodes.reduce(
             (listeners, ignoredSelector) => Object.assign(listeners, { [ignoredSelector]: addToIgnoredNodes }),
@@ -1457,7 +1479,7 @@ module.exports = {
 
                     // If a node's type is nonstandard, we can't tell how its children should be offset, so ignore it.
                     if (!KNOWN_NODES.has(node.type)) {
-                        ignoredNodes.add(node);
+                        addToIgnoredNodes(node);
                     }
                 },
                 "Program:exit"() {


Test patch:
diff --git a/tests/lib/rules/indent.js b/tests/lib/rules/indent.js
index 3a6bc0c99..f8914a685 100644
--- a/tests/lib/rules/indent.js
+++ b/tests/lib/rules/indent.js
@@ -2302,6 +2302,17 @@ ruleTester.run("indent", rule, {
             `,
             options: [4]
         },
+        unIndent`
+            foo &&
+                !bar(
+                )
+        `,
+        unIndent`
+            foo &&
+                ![].map(() => {
+                    bar();
+                })
+        `,
         {
             code: unIndent`
                 foo =
@@ -4607,6 +4618,16 @@ ruleTester.run("indent", rule, {
                     );
                 }
             `,
+        unIndent`
+            <div>foo
+                <div>bar</div>
+            </div>
+        `,
+        unIndent`
+            <small>Foo bar&nbsp;
+                <a>baz qux</a>.
+            </small>
+        `,
         {
             code: unIndent`
                 a(b
@@ -7539,6 +7560,34 @@ ruleTester.run("indent", rule, {
             options: [4],
             errors: expectedErrors([2, 4, 8, "Identifier"])
         },
+        {
+            code: unIndent`
+                foo &&
+                    !bar(
+                )
+            `,
+            output: unIndent`
+                foo &&
+                    !bar(
+                    )
+            `,
+            errors: expectedErrors([3, 4, 0, "Punctuator"])
+        },
+        {
+            code: unIndent`
+                foo &&
+                    ![].map(() => {
+                    bar();
+                })
+            `,
+            output: unIndent`
+                foo &&
+                    ![].map(() => {
+                        bar();
+                    })
+            `,
+            errors: expectedErrors([[3, 8, 4, "Identifier"], [4, 4, 0, "Punctuator"]])
+        },
         {
             code: unIndent`
                 [
@@ -9136,6 +9185,32 @@ ruleTester.run("indent", rule, {
             `,
             errors: expectedErrors([3, 8, 6, "Block"])
         },
+        {
+            code: unIndent`
+                <div>foo
+                <div>bar</div>
+                </div>
+            `,
+            output: unIndent`
+                <div>foo
+                    <div>bar</div>
+                </div>
+            `,
+            errors: expectedErrors([2, 4, 0, "Punctuator"])
+        },
+        {
+            code: unIndent`
+                <small>Foo bar&nbsp;
+                <a>baz qux</a>.
+                </small>
+            `,
+            output: unIndent`
+                <small>Foo bar&nbsp;
+                    <a>baz qux</a>.
+                </small>
+            `,
+            errors: expectedErrors([2, 4, 0, "Punctuator"])
+        },
         {
             code: unIndent`
                 ({