Repository: eslint/eslint (js)

Issue:
[4.0.0] indent + multiline expression not working as expected
<!--
This template is for bug reports. If you are reporting a bug, please continue on. If you are here for another reason, please see below:

1. To propose a new rule: http://eslint.org/docs/developer-guide/contributing/new-rules
2. To request a change: http://eslint.org/docs/developer-guide/contributing/changes
3. 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.0.0
* **Node Version:** 8.1.0
* **npm Version:** 5.0.3

**What parser (default, Babel-ESLint, etc.) are you using?** default

**Please show your full configuration:**

<!-- Paste your configuration below: -->
```
rules:
  indent: [2, 2, {ArrayExpression: first,
                  CallExpression: {arguments: first},
                  FunctionDeclaration: {parameters: first},
                  FunctionExpression: {parameters: first},
                  MemberExpression: off,
                  ObjectExpression: first,
                  SwitchCase: 1}]
```

**What did you do? Please include the actual source code causing the issue.**

<!-- Paste the source code below: -->
```js
var foo = bar(function() {
  doSomething();
}).baz(function() {
  doSomethingElse();
});
```

**What did you expect to happen?** No errors.

**What actually happened? Please include the actual, raw output from ESLint.**

```console
$ eslint foo.js

/Users/trott/test/foo.js
  4:1  error  Expected indentation of 4 spaces but found 2  indent
  5:1  error  Expected indentation of 2 spaces but found 0  indent

✖ 2 problems (2 errors, 0 warnings)
  2 errors, 0 warnings potentially fixable with the `--fix` option.

$
```

I'm not sure if this has the same root cause as https://github.com/eslint/eslint/issues/8717 or if it is distinct...

Pull request description:
Update: fix MemberExpression indentation with "off" option (fixes #8721)
**What is the purpose of this pull request? (put an "X" next to item)**

[x] Bug fix (https://github.com/eslint/eslint/issues/8721)

<!--
    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)**

This fixes a bug where token dependencies were configured incorrectly when the "off" MemberExpression option was set, resulting in incorrect behavior for chained expressions.

**Is there anything you'd like reviewers to focus on?**

Nothing in particular

Patch:
diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md
index 03a1dd794..f48670896 100644
--- a/docs/developer-guide/nodejs-api.md
+++ b/docs/developer-guide/nodejs-api.md
@@ -147,6 +147,37 @@ console.log(code.text);     // "var foo = bar;"
 
 In this way, you can retrieve the text and AST used for the last run of `linter.verify()`.
 
+### verifyAndFix()
+
+This method is similar to verify except that it also runs autofixing logic, similar to the `--fix` flag on the command line. The result object will contain the autofixed code, along with any remaining linting messages for the code that were not autofixed.
+
+```js
+var Linter = require("eslint").Linter;
+var linter = new Linter();
+
+var messages = linter.verifyAndFix("var foo", {
+    rules: {
+        semi: 2
+    }
+}, { filename: "foo.js" });
+```
+
+Output object from this method:
+
+```js
+{
+    fixed: true,
+    text: "var foo;",
+    messages: []
+}
+```
+
+The information available is:
+
+* `fixed` - True, if the code was fixed.
+* `text` - Fixed code text (might be the same as input if no fixes were applied).
+* `messages` - Collection of all messages for the given code (It has the same information as explained above under `verify` block).
+
 ## linter
 
 The `eslint.linter` object (deprecated) is an instance of the `Linter` class as defined [above](#Linter). `eslint.linter` exists for backwards compatibility, but we do not recommend using it because any mutations to it are shared among every module that uses `eslint`. Instead, please create your own instance of `eslint.Linter`.
diff --git a/lib/cli-engine.js b/lib/cli-engine.js
index d0abc0a55..f53c2e76a 100644
--- a/lib/cli-engine.js
+++ b/lib/cli-engine.js
@@ -23,11 +23,9 @@ const fs = require("fs"),
     Config = require("./config"),
     fileEntryCache = require("file-entry-cache"),
     globUtil = require("./util/glob-util"),
-    SourceCodeFixer = require("./util/source-code-fixer"),
     validator = require("./config/config-validator"),
     stringify = require("json-stable-stringify"),
     hash = require("./util/hash"),
-
     pkg = require("../package.json");
 
 const debug = require("debug")("eslint:cli-engine");
@@ -132,80 +130,6 @@ function calculateStatsPerRun(results) {
     });
 }
 
-/**
- * Performs multiple autofix passes over the text until as many fixes as possible
- * have been applied.
- * @param {string} text The source text to apply fixes to.
- * @param {Object} config The ESLint config object to use.
- * @param {Object} options The ESLint options object to use.
- * @param {string} options.filename The filename from which the text was read.
- * @param {boolean} options.allowInlineConfig Flag indicating if inline comments
- *      should be allowed.
- * @param {Linter} linter Linter context
- * @returns {Object} The result of the fix operation as returned from the
- *      SourceCodeFixer.
- * @private
- */
-function multipassFix(text, config, options, linter) {
-    const MAX_PASSES = 10;
-    let messages = [],
-        fixedResult,
-        fixed = false,
-        passNumber = 0;
-
-    /**
-     * This loop continues until one of the following is true:
-     *
-     * 1. No more fixes have been applied.
-     * 2. Ten passes have been made.
-     *
-     * That means anytime a fix is successfully applied, there will be another pass.
-     * Essentially, guaranteeing a minimum of two passes.
-     */
-    do {
-        passNumber++;
-
-        debug(`Linting code for ${options.filename} (pass ${passNumber})`);
-        messages = linter.verify(text, config, options);
-
-        debug(`Generating fixed text for ${options.filename} (pass ${passNumber})`);
-        fixedResult = SourceCodeFixer.applyFixes(linter.getSourceCode(), messages);
-
-        // stop if there are any syntax errors.
-        // 'fixedResult.output' is a empty string.
-        if (messages.length === 1 && messages[0].fatal) {
-            break;
-        }
-
-        // keep track if any fixes were ever applied - important for return value
-        fixed = fixed || fixedResult.fixed;
-
-        // update to use the fixed output instead of the original text
-        text = fixedResult.output;
-
-    } while (
-        fixedResult.fixed &&
-        passNumber < MAX_PASSES
-    );
-
-
-    /*
-     * If the last result had fixes, we need to lint again to be sure we have
-     * the most up-to-date information.
-     */
-    if (fixedResult.fixed) {
-        fixedResult.messages = linter.verify(text, config, options);
-    }
-
-
-    // ensure the last result properly reflects if fixes were done
-    fixedResult.fixed = fixed;
-    fixedResult.output = text;
-
-    return fixedResult;
-
-}
-
 /**
  * Processes an source code using ESLint.
  * @param {string} text The source code to check.
@@ -269,10 +193,10 @@ function processText(text, configHelper, filename, fix, allowInlineConfig, linte
     } else {
 
         if (fix) {
-            fixedResult = multipassFix(text, config, {
+            fixedResult = linter.verifyAndFix(text, config, {
                 filename,
                 allowInlineConfig
-            }, linter);
+            });
             messages = fixedResult.messages;
         } else {
             messages = linter.verify(text, config, {
diff --git a/lib/linter.js b/lib/linter.js
index 25af05223..0b0d06a86 100755
--- a/lib/linter.js
+++ b/lib/linter.js
@@ -27,9 +27,11 @@ const assert = require("assert"),
     Rules = require("./rules"),
     timing = require("./timing"),
     astUtils = require("./ast-utils"),
+    pkg = require("../package.json"),
+    SourceCodeFixer = require("./util/source-code-fixer");
 
-    pkg = require("../package.json");
-
+const debug = require("debug")("eslint:linter");
+const MAX_AUTOFIX_PASSES = 10;
 
 //------------------------------------------------------------------------------
 // Typedefs
@@ -1185,6 +1187,74 @@ class Linter extends EventEmitter {
     getDeclaredVariables(node) {
         return (this.scopeManager && this.scopeManager.getDeclaredVariables(node)) || [];
     }
+
+    /**
+     * Performs multiple autofix passes over the text until as many fixes as possible
+     * have been applied.
+     * @param {string} text The source text to apply fixes to.
+     * @param {Object} config The ESLint config object to use.
+     * @param {Object} options The ESLint options object to use.
+     * @param {string} options.filename The filename from which the text was read.
+     * @param {boolean} options.allowInlineConfig Flag indicating if inline comments
+     *      should be allowed.
+     * @returns {Object} The result of the fix operation as returned from the
+     *      SourceCodeFixer.
+     */
+    verifyAndFix(text, config, options) {
+        let messages = [],
+            fixedResult,
+            fixed = false,
+            passNumber = 0;
+
+        /**
+         * This loop continues until one of the following is true:
+         *
+         * 1. No more fixes have been applied.
+         * 2. Ten passes have been made.
+         *
+         * That means anytime a fix is successfully applied, there will be another pass.
+         * Essentially, guaranteeing a minimum of two passes.
+         */
+        do {
+            passNumber++;
+
+            debug(`Linting code for ${options.filename} (pass ${passNumber})`);
+            messages = this.verify(text, config, options);
+
+            debug(`Generating fixed text for ${options.filename} (pass ${passNumber})`);
+            fixedResult = SourceCodeFixer.applyFixes(this.getSourceCode(), messages);
+
+            // stop if there are any syntax errors.
+            // 'fixedResult.output' is a empty string.
+            if (messages.length === 1 && messages[0].fatal) {
+                break;
+            }
+
+            // keep track if any fixes were ever applied - important for return value
+            fixed = fixed || fixedResult.fixed;
+
+            // update to use the fixed output instead of the original text
+            text = fixedResult.output;
+
+        } while (
+            fixedResult.fixed &&
+            passNumber < MAX_AUTOFIX_PASSES
+        );
+
+        /*
+         * If the last result had fixes, we need to lint again to be sure we have
+         * the most up-to-date information.
+         */
+        if (fixedResult.fixed) {
+            fixedResult.messages = this.verify(text, config, options);
+        }
+
+        // ensure the last result properly reflects if fixes were done
+        fixedResult.fixed = fixed;
+        fixedResult.output = text;
+
+        return fixedResult;
+    }
 }
 
 // methods that exist on SourceCode object
diff --git a/lib/rules/indent.js b/lib/rules/indent.js
index 82268975d..fb143806c 100644
--- a/lib/rules/indent.js
+++ b/lib/rules/indent.js
@@ -1114,28 +1114,29 @@ module.exports = {
                 const tokenBeforeObject = sourceCode.getTokenBefore(node.object, token => astUtils.isNotOpeningParenToken(token) || parameterParens.has(token));
                 const firstObjectToken = tokenBeforeObject ? sourceCode.getTokenAfter(tokenBeforeObject) : sourceCode.ast.tokens[0];
                 const lastObjectToken = sourceCode.getTokenBefore(firstNonObjectToken);
+                const firstPropertyToken = node.computed ? firstNonObjectToken : secondNonObjectToken;
 
                 if (node.computed) {
 
                     // For computed MemberExpressions, match the closing bracket with the opening bracket.
                     offsets.matchIndentOf(firstNonObjectToken, sourceCode.getLastToken(node));
+                    offsets.setDesiredOffsets(getTokensAndComments(node.property), firstNonObjectToken, 1);
                 }
 
-                if (typeof options.MemberExpression === "number") {
-                    const firstPropertyToken = node.computed ? firstNonObjectToken : secondNonObjectToken;
+                /*
+                 * If the object ends on the same line that the property starts, match against the last token
+                 * of the object, to ensure that the MemberExpression is not indented.
+                 *
+                 * Otherwise, match against the first token of the object, e.g.
+                 * foo
+                 *   .bar
+                 *   .baz // <-- offset by 1 from `foo`
+                 */
+                const offsetBase = lastObjectToken.loc.end.line === firstPropertyToken.loc.start.line
+                    ? lastObjectToken
+                    : firstObjectToken;
 
-                    /*
-                     * If the object ends on the same line that the property starts, match against the last token
-                     * of the object, to ensure that the MemberExpression is not indented.
-                     *
-                     * Otherwise, match against the first token of the object, e.g.
-                     * foo
-                     *   .bar
-                     *   .baz // <-- offset by 1 from `foo`
-                     */
-                    const offsetBase = lastObjectToken.loc.end.line === firstPropertyToken.loc.start.line
-                        ? lastObjectToken
-                        : firstObjectToken;
+                if (typeof options.MemberExpression === "number") {
 
                     // Match the dot (for non-computed properties) or the opening bracket (for computed properties) against the object.
                     offsets.setDesiredOffset(firstNonObjectToken, offsetBase, options.MemberExpression);
@@ -1150,6 +1151,9 @@ module.exports = {
                     // If the MemberExpression option is off, ignore the dot and the first token of the property.
                     offsets.ignoreToken(firstNonObjectToken);
                     offsets.ignoreToken(secondNonObjectToken);
+
+                    // To ignore the property indentation, ensure that the property tokens depend on the ignored tokens.
+                    offsets.matchIndentOf(offsetBase, firstNonObjectToken);
                     offsets.matchIndentOf(firstNonObjectToken, secondNonObjectToken);
                 }
             },


Test patch:
diff --git a/tests/lib/linter.js b/tests/lib/linter.js
index ed5fb2238..9c47fafe5 100644
--- a/tests/lib/linter.js
+++ b/tests/lib/linter.js
@@ -70,4 +70,18 @@ describe("Linter", () => {
             });
         });
     });
+
+    describe("verifyAndFix", () => {
+        it("Fixes the code", () => {
+            const linter = new Linter();
+            const messages = linter.verifyAndFix("var a", {
+                rules: {
+                    semi: 2
+                }
+            }, { filename: "test.js" });
+
+            assert.equal(messages.output, "var a;", "Fixes were applied correctly");
+            assert.isTrue(messages.fixed);
+        });
+    });
 });
diff --git a/tests/lib/rules/indent.js b/tests/lib/rules/indent.js
index 335e8882f..86e38a8ea 100644
--- a/tests/lib/rules/indent.js
+++ b/tests/lib/rules/indent.js
@@ -3376,6 +3376,23 @@ ruleTester.run("indent", rule, {
             `,
             options: [4, { MemberExpression: "off" }]
         },
+        {
+            code: unIndent`
+                  foo = bar(
+                  ).baz(
+                  )
+            `,
+            options: [4, { MemberExpression: "off" }]
+        },
+        {
+            code: unIndent`
+                foo[
+                    bar ? baz :
+                    qux
+                ]
+            `,
+            options: [4, { flatTernaryExpressions: true }]
+        },
         {
             code: unIndent`
                   foo