Repository: eslint/eslint (js)

Issue:
Allow arbitrary nodes to be ignored in the `indent` rule
# Problem

People have lots of different indentation preferences, and the `indent` rule can't realistically support all of them. With other rules, we generally encourage people to make a plugin if they want to enforce their specific preferences, which implies disabling the core rule and using the rule from the plugin instead.

However, the `indent` rule is big and complicated, and if a user wants to disable it in favor of their plugin, then the plugin needs to reimplement the entire rule. This adds a very large maintenance burden.

# Proposal

**What rule do you want to change?**

`indent`

**Does this change cause the rule to produce more or fewer warnings?**

By default, it produces the same amount.

**How will the change be implemented? (New option, new default behavior, etc.)?**

This change adds an `ignoredNodes` option to the rule, which is an array of AST selectors. Any nodes matching those selectors will be treated the same way as unknown node types -- the indentation of their direct children will be ignored, but the indentation of nested children will still be enforced relative to each other.

**Please provide some example code that this change will affect:**

```js
/* eslint indent: [
    error,
    4,
    {
        ignoredNodes: ["VariableDeclarator[kind='const']"]
    }
] */

const foo = 1,
      bar = 2,
      baz = 3,
      qux = function() {
          return;
      };
```

**What does the rule currently do for this code?**

The rule reports an error because it enforces a style where `bar`, `baz`, and `qux` are indented incorrectly. (We have gotten several feature requests to allow a style like this.)

**What will the rule do after it's changed?**

The rule will ignore the indentation of `bar`, `baz`, and `qux`, but it will still enforce the alignment of the `return` statement with `qux`. This would allow someone to write a plugin that just checks the indentation of `bar`, `baz`, and `qux`, without reimplementing the indentation logic for anything else.

Pull request:
Update: allow arbitrary nodes to be ignored in `indent` (fixes #8594)
<!--
    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] Changes an existing rule (https://github.com/eslint/eslint/issues/8594)

<!--
    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 adds an option to the `indent` rule to allow the indentation checking for an arbitrary node type to be ignored, as described in https://github.com/eslint/eslint/issues/8594. This should make it easier for users to use the `indent` rule even if they think a different indentation should be enforced for a particular node type.

**Is there anything you'd like reviewers to focus on?**

Nothing in particular


Patch:
diff --git a/Makefile.js b/Makefile.js
index 06cbef959..bf175d261 100644
--- a/Makefile.js
+++ b/Makefile.js
@@ -458,7 +458,7 @@ function hasBranch(branchName) {
  */
 function getFormatterResults() {
     const CLIEngine = require("./lib/cli-engine"),
-        chalk = require("chalk");
+        stripAnsi = require("strip-ansi");
 
     const formatterFiles = fs.readdirSync("./lib/formatters/"),
         cli = new CLIEngine({
@@ -489,7 +489,7 @@ function getFormatterResults() {
 
         if (fileExt === ".js") {
             data.formatterResults[name] = {
-                result: chalk.stripColor(cli.getFormatter(name)(rawMessages.results))
+                result: stripAnsi(cli.getFormatter(name)(rawMessages.results))
             };
         }
         return data;
diff --git a/docs/developer-guide/working-with-plugins.md b/docs/developer-guide/working-with-plugins.md
index 725bb065a..04274f21e 100644
--- a/docs/developer-guide/working-with-plugins.md
+++ b/docs/developer-guide/working-with-plugins.md
@@ -49,7 +49,7 @@ Plugin environments can define the following objects:
 
 ### Processors in Plugins
 
-You can also create plugins that would tell ESLint how to process files other than JavaScript. In order to create a processor, object that is exported from your module has to conform to the following interface:
+You can also create plugins that would tell ESLint how to process files other than JavaScript. In order to create a processor, the object that is exported from your module has to conform to the following interface:
 
 ```js
 processors: {
diff --git a/docs/rules/indent.md b/docs/rules/indent.md
index ade61b75e..9b375d864 100644
--- a/docs/rules/indent.md
+++ b/docs/rules/indent.md
@@ -84,6 +84,7 @@ This rule has an object option:
 * `"ObjectExpression"` (default: 1) enforces indentation level for properties in objects. It can be set to the string `"first"`, indicating that all properties in the object should be aligned with the first property. This can also be set to `"off"` to disable checking for object properties.
 * `"ImportDeclaration"` (default: 1) enforces indentation level for import statements. It can be set to the string `"first"`, indicating that all imported members from a module should be aligned with the first member in the list. This can also be set to `"off"` to disable checking for imported module members.
 * `"flatTernaryExpressions": true` (`false` by default) requires no indentation for ternary expressions which are nested in other ternary expressions.
+* `ignoredNodes` accepts an array of [selectors](/docs/developer-guide/selectors). If an AST node is matched by any of the selectors, the indentation of tokens which are direct children of that node will be ignored. This can be used as an escape hatch to relax the rule if you disagree with the indentation that it enforces for a particular syntactic pattern.
 
 Level of indentation denotes the multiple of the indent specified. Example:
 
@@ -607,6 +608,38 @@ var a =
     boop;
 ```
 
+### ignoredNodes
+
+The following configuration ignores the indentation of `ConditionalExpression` ("ternary expression") nodes:
+
+Examples of **correct** code for this rule with the `4, { "ignoredNodes": ["ConditionalExpression"] }` option:
+
+```js
+/*eslint indent: ["error", 4, { "ignoredNodes": ["ConditionalExpression"] }]*/
+
+var a = foo
+      ? bar
+      : baz;
+
+var a = foo
+                ? bar
+: baz;
+```
+
+The following configuration ignores indentation in the body of IIFEs.
+
+Examples of **correct** code for this rule with the `4, { "ignoredNodes": ["CallExpression > FunctionExpression.callee > BlockStatement.body"] }` option:
+
+```js
+/*eslint indent: ["error", 4, { "ignoredNodes": ["CallExpression > FunctionExpression.callee > BlockStatement.body"] }]*/
+
+(function() {
+
+foo();
+bar();
+
+})
+```
 
 ## Compatibility
 
diff --git a/lib/formatters/junit.js b/lib/formatters/junit.js
index c41a2a4dd..069ed5ff8 100644
--- a/lib/formatters/junit.js
+++ b/lib/formatters/junit.js
@@ -39,10 +39,7 @@ module.exports = function(results) {
 
         const messages = result.messages;
 
-        if (messages.length) {
-            output += `<testsuite package="org.eslint" time="0" tests="${messages.length}" errors="${messages.length}" name="${result.filePath}">\n`;
-        }
-
+        output += `<testsuite package="org.eslint" time="0" tests="${messages.length}" errors="${messages.length}" name="${result.filePath}">\n`;
         messages.forEach(message => {
             const type = message.fatal ? "error" : "failure";
 
@@ -57,10 +54,7 @@ module.exports = function(results) {
             output += `</${type}>`;
             output += "</testcase>\n";
         });
-
-        if (messages.length) {
-            output += "</testsuite>\n";
-        }
+        output += "</testsuite>\n";
 
     });
 
diff --git a/lib/formatters/stylish.js b/lib/formatters/stylish.js
index 4ec97a31a..023ebc1dc 100644
--- a/lib/formatters/stylish.js
+++ b/lib/formatters/stylish.js
@@ -5,6 +5,7 @@
 "use strict";
 
 const chalk = require("chalk"),
+    stripAnsi = require("strip-ansi"),
     table = require("text-table");
 
 //------------------------------------------------------------------------------
@@ -71,7 +72,7 @@ module.exports = function(results) {
             {
                 align: ["", "r", "l"],
                 stringLength(str) {
-                    return chalk.stripColor(str).length;
+                    return stripAnsi(str).length;
                 }
             }
         ).split("\n").map(el => el.replace(/(\d+)\s+(\d+)/, (m, p1, p2) => chalk.dim(`${p1}:${p2}`))).join("\n")}\n\n`;
diff --git a/lib/rules/indent.js b/lib/rules/indent.js
index 429567ee5..8872836f4 100644
--- a/lib/rules/indent.js
+++ b/lib/rules/indent.js
@@ -581,6 +581,15 @@ module.exports = {
                     ImportDeclaration: ELEMENT_LIST_SCHEMA,
                     flatTernaryExpressions: {
                         type: "boolean"
+                    },
+                    ignoredNodes: {
+                        type: "array",
+                        items: {
+                            type: "string",
+                            not: {
+                                pattern: ":exit$"
+                            }
+                        }
                     }
                 },
                 additionalProperties: false
@@ -618,7 +627,8 @@ module.exports = {
             ArrayExpression: 1,
             ObjectExpression: 1,
             ImportDeclaration: 1,
-            flatTernaryExpressions: false
+            flatTernaryExpressions: false,
+            ignoredNodes: []
         };
 
         if (context.options.length) {
@@ -914,7 +924,7 @@ module.exports = {
         * @param {ASTNode} node Unknown Node
         * @returns {void}
         */
-        function ignoreUnknownNode(node) {
+        function ignoreNode(node) {
             const unknownNodeTokens = new Set(sourceCode.getTokens(node, { includeComments: true }));
 
             unknownNodeTokens.forEach(token => {
@@ -930,19 +940,6 @@ module.exports = {
             });
         }
 
-        /**
-        * Ignore node if it is unknown
-        * @param {ASTNode} node Node
-        * @returns {void}
-        */
-        function checkForUnknownNode(node) {
-            const isNodeUnknown = !(KNOWN_NODES.has(node.type));
-
-            if (isNodeUnknown) {
-                ignoreUnknownNode(node);
-            }
-        }
-
         /**
          * Check whether the given token is the first token of a statement.
          * @param {Token} token The token to check.
@@ -960,7 +957,7 @@ module.exports = {
             return !node || node.range[0] === token.range[0];
         }
 
-        return {
+        const baseOffsetListeners = {
             "ArrayExpression, ArrayPattern"(node) {
                 const openingBracket = sourceCode.getFirstToken(node);
                 const closingBracket = sourceCode.getTokenAfter(lodash.findLast(node.elements) || openingBracket, astUtils.isClosingBracketToken);
@@ -1178,14 +1175,15 @@ module.exports = {
                 }
             },
 
-            "MemberExpression, JSXMemberExpression"(node) {
-                const firstNonObjectToken = sourceCode.getFirstTokenBetween(node.object, node.property, astUtils.isNotClosingParenToken);
+            "MemberExpression, JSXMemberExpression, MetaProperty"(node) {
+                const object = node.type === "MetaProperty" ? node.meta : node.object;
+                const firstNonObjectToken = sourceCode.getFirstTokenBetween(object, node.property, astUtils.isNotClosingParenToken);
                 const secondNonObjectToken = sourceCode.getTokenAfter(firstNonObjectToken);
 
-                const objectParenCount = sourceCode.getTokensBetween(node.object, node.property, { filter: astUtils.isClosingParenToken }).length;
+                const objectParenCount = sourceCode.getTokensBetween(object, node.property, { filter: astUtils.isClosingParenToken }).length;
                 const firstObjectToken = objectParenCount
-                    ? sourceCode.getTokenBefore(node.object, { skip: objectParenCount - 1 })
-                    : sourceCode.getFirstToken(node.object);
+                    ? sourceCode.getTokenBefore(object, { skip: objectParenCount - 1 })
+                    : sourceCode.getFirstToken(object);
                 const lastObjectToken = sourceCode.getTokenBefore(firstNonObjectToken);
                 const firstPropertyToken = node.computed ? firstNonObjectToken : secondNonObjectToken;
 
@@ -1333,8 +1331,6 @@ module.exports = {
                 }
             },
 
-            "*:exit": checkForUnknownNode,
-
             "JSXAttribute[value]"(node) {
                 const equalsToken = sourceCode.getFirstTokenBetween(node.name, node.value, token => token.type === "Punctuator" && token.value === "=");
 
@@ -1378,62 +1374,130 @@ module.exports = {
                     1
                 );
                 offsets.setDesiredOffset(closingCurly, openingCurly, 0);
-            },
+            }
+        };
 
-            "Program:exit"() {
-                addParensIndent(sourceCode.ast.tokens);
+        const listenerCallQueue = [];
 
-                /*
-                 * Create a Map from (tokenOrComment) => (precedingToken).
-                 * This is necessary because sourceCode.getTokenBefore does not handle a comment as an argument correctly.
-                 */
-                const precedingTokens = sourceCode.ast.comments.reduce((commentMap, comment) => {
-                    const tokenOrCommentBefore = sourceCode.getTokenBefore(comment, { includeComments: true });
+        /*
+         * 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.
+         */
+        const offsetListeners = lodash.mapValues(
+            baseOffsetListeners,
+
+            /*
+             * Offset listener calls are deferred until traversal is finished, and are called as
+             * part of the final `Program:exit` listener. This is necessary because a node might
+             * be matched by multiple selectors.
+             *
+             * Example: Suppose there is an offset listener for `Identifier`, and the user has
+             * specified in configuration that `MemberExpression > Identifier` should be ignored.
+             * Due to selector specificity rules, the `Identifier` listener will get called first. However,
+             * if a given Identifier node is supposed to be ignored, then the `Identifier` offset listener
+             * should not have been called at all. Without doing extra selector matching, we don't know
+             * whether the Identifier matches the `MemberExpression > Identifier` selector until the
+             * `MemberExpression > Identifier` listener is called.
+             *
+             * To avoid this, the `Identifier` listener isn't called until traversal finishes and all
+             * ignored nodes are known.
+             */
+            listener =>
+                node =>
+                    listenerCallQueue.push({ listener, node })
+        );
 
-                    return commentMap.set(comment, commentMap.has(tokenOrCommentBefore) ? commentMap.get(tokenOrCommentBefore) : tokenOrCommentBefore);
-                }, new WeakMap());
+        // 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);
 
-                sourceCode.lines.forEach((line, lineIndex) => {
-                    const lineNumber = lineIndex + 1;
+        const ignoredNodeListeners = options.ignoredNodes.reduce(
+            (listeners, ignoredSelector) => Object.assign(listeners, { [ignoredSelector]: addToIgnoredNodes }),
+            {}
+        );
 
-                    if (!tokenInfo.firstTokensByLineNumber.has(lineNumber)) {
+        /*
+         * Join the listeners, and add a listener to verify that all tokens actually have the correct indentation
+         * at the end.
+         *
+         * Using Object.assign will cause some offset listeners to be overwritten if the same selector also appears
+         * in `ignoredNodeListeners`. This isn't a problem because all of the matching nodes will be ignored,
+         * so those listeners wouldn't be called anyway.
+         */
+        return Object.assign(
+            offsetListeners,
+            ignoredNodeListeners,
+            {
+                "*:exit"(node) {
 
-                        // Don't check indentation on blank lines
-                        return;
+                    // 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);
                     }
+                },
+                "Program:exit"() {
 
-                    const firstTokenOfLine = tokenInfo.firstTokensByLineNumber.get(lineNumber);
+                    // Invoke the queued offset listeners for the nodes that aren't ignored.
+                    listenerCallQueue
+                        .filter(nodeInfo => !ignoredNodes.has(nodeInfo.node))
+                        .forEach(nodeInfo => nodeInfo.listener(nodeInfo.node));
 
-                    if (firstTokenOfLine.loc.start.line !== lineNumber) {
+                    // Update the offsets for ignored nodes to prevent their child tokens from being reported.
+                    ignoredNodes.forEach(ignoreNode);
 
-                        // Don't check the indentation of multi-line tokens (e.g. template literals or block comments) twice.
-                        return;
-                    }
+                    addParensIndent(sourceCode.ast.tokens);
 
-                    // If the token matches the expected expected indentation, don't report it.
-                    if (validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(firstTokenOfLine))) {
-                        return;
-                    }
+                    /*
+                     * Create a Map from (tokenOrComment) => (precedingToken).
+                     * This is necessary because sourceCode.getTokenBefore does not handle a comment as an argument correctly.
+                     */
+                    const precedingTokens = sourceCode.ast.comments.reduce((commentMap, comment) => {
+                        const tokenOrCommentBefore = sourceCode.getTokenBefore(comment, { includeComments: true });
+
+                        return commentMap.set(comment, commentMap.has(tokenOrCommentBefore) ? commentMap.get(tokenOrCommentBefore) : tokenOrCommentBefore);
+                    }, new WeakMap());
 
-                    if (astUtils.isCommentToken(firstTokenOfLine)) {
-                        const tokenBefore = precedingTokens.get(firstTokenOfLine);
-                        const tokenAfter = tokenBefore ? sourceCode.getTokenAfter(tokenBefore) : sourceCode.ast.tokens[0];
+                    sourceCode.lines.forEach((line, lineIndex) => {
+                        const lineNumber = lineIndex + 1;
 
-                        // If a comment matches the expected indentation of the token immediately before or after, don't report it.
-                        if (
-                            tokenBefore && validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(tokenBefore)) ||
-                            tokenAfter && validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(tokenAfter))
-                        ) {
+                        if (!tokenInfo.firstTokensByLineNumber.has(lineNumber)) {
+
+                            // Don't check indentation on blank lines
                             return;
                         }
-                    }
 
-                    // Otherwise, report the token/comment.
-                    report(firstTokenOfLine, offsets.getDesiredIndent(firstTokenOfLine));
-                });
-            }
+                        const firstTokenOfLine = tokenInfo.firstTokensByLineNumber.get(lineNumber);
 
-        };
+                        if (firstTokenOfLine.loc.start.line !== lineNumber) {
+
+                            // Don't check the indentation of multi-line tokens (e.g. template literals or block comments) twice.
+                            return;
+                        }
+
+                        // If the token matches the expected expected indentation, don't report it.
+                        if (validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(firstTokenOfLine))) {
+                            return;
+                        }
+
+                        if (astUtils.isCommentToken(firstTokenOfLine)) {
+                            const tokenBefore = precedingTokens.get(firstTokenOfLine);
+                            const tokenAfter = tokenBefore ? sourceCode.getTokenAfter(tokenBefore) : sourceCode.ast.tokens[0];
 
+                            // If a comment matches the expected indentation of the token immediately before or after, don't report it.
+                            if (
+                                tokenBefore && validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(tokenBefore)) ||
+                                tokenAfter && validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(tokenAfter))
+                            ) {
+                                return;
+                            }
+                        }
+
+                        // Otherwise, report the token/comment.
+                        report(firstTokenOfLine, offsets.getDesiredIndent(firstTokenOfLine));
+                    });
+                }
+            }
+        );
     }
 };
diff --git a/package.json b/package.json
index 541cb8d18..97baf4bfd 100644
--- a/package.json
+++ b/package.json
@@ -37,7 +37,7 @@
   "dependencies": {
     "ajv": "^5.2.0",
     "babel-code-frame": "^6.22.0",
-    "chalk": "^1.1.3",
+    "chalk": "^2.1.0",
     "concat-stream": "^1.6.0",
     "cross-spawn": "^5.1.0",
     "debug": "^2.6.8",
@@ -68,6 +68,7 @@
     "progress": "^2.0.0",
     "require-uncached": "^1.0.3",
     "semver": "^5.3.0",
+    "strip-ansi": "^4.0.0",
     "strip-json-comments": "~2.0.1",
     "table": "^4.0.1",
     "text-table": "~0.2.0"


Test patch:
diff --git a/tests/lib/formatters/codeframe.js b/tests/lib/formatters/codeframe.js
index c059c357b..35dc12adf 100644
--- a/tests/lib/formatters/codeframe.js
+++ b/tests/lib/formatters/codeframe.js
@@ -14,6 +14,7 @@ const sinon = require("sinon");
 const proxyquire = require("proxyquire");
 const chalk = require("chalk");
 const path = require("path");
+const stripAnsi = require("strip-ansi");
 
 // Chalk protects its methods so we need to inherit from it for Sinon to work.
 const chalkStub = Object.create(chalk, {
@@ -86,7 +87,7 @@ describe("formatter:codeframe", () => {
         it("should return a string in the correct format for warnings", () => {
             const result = formatter(code);
 
-            assert.equal(chalk.stripColor(result), [
+            assert.equal(stripAnsi(result), [
                 `warning: Unexpected foo (foo) at ${path.join("lib", "foo.js")}:1:5:`,
                 "> 1 | var foo = 1;",
                 "    |     ^",
@@ -115,7 +116,7 @@ describe("formatter:codeframe", () => {
             it("should return a string in the correct format", () => {
                 const result = formatter(code);
 
-                assert.equal(chalk.stripColor(result), [
+                assert.equal(stripAnsi(result), [
                     `warning: Unexpected foo (foo) at ${path.join("lib", "foo.js")}:1:5:`,
                     "> 1 | var foo = 1;",
                     "    |     ^",
@@ -147,7 +148,7 @@ describe("formatter:codeframe", () => {
         it("should return a string in the correct format for errors", () => {
             const result = formatter(code);
 
-            assert.equal(chalk.stripColor(result), [
+            assert.equal(stripAnsi(result), [
                 `error: Unexpected foo (foo) at ${path.join("lib", "foo.js")}:1:5:`,
                 "> 1 | var foo = 1;",
                 "    |     ^",
@@ -193,7 +194,7 @@ describe("formatter:codeframe", () => {
         it("should return a string with multiple entries", () => {
             const result = formatter(code);
 
-            assert.equal(chalk.stripColor(result), [
+            assert.equal(stripAnsi(result), [
                 "error: Missing semicolon (semi) at foo.js:1:14:",
                 "> 1 | const foo = 1",
                 "    |              ^",
@@ -241,7 +242,7 @@ describe("formatter:codeframe", () => {
         it("should return a string with code preview pointing to the correct location after fixes", () => {
             const result = formatter(code);
 
-            assert.equal(chalk.stripColor(result), [
+            assert.equal(stripAnsi(result), [
                 "error: 'foo' is assigned a value but never used (no-unused-vars) at foo.js:4:11:",
                 "  2 | ",
                 "  3 |     // a comment",
@@ -286,7 +287,7 @@ describe("formatter:codeframe", () => {
         it("should return a string with multiple entries", () => {
             const result = formatter(code);
 
-            assert.equal(chalk.stripColor(result), [
+            assert.equal(stripAnsi(result), [
                 "error: Missing semicolon (semi) at foo.js:1:14:",
                 "> 1 | const foo = 1",
                 "    |              ^",
@@ -322,7 +323,7 @@ describe("formatter:codeframe", () => {
         it("should return a string in the correct format", () => {
             const result = formatter(code);
 
-            assert.equal(chalk.stripColor(result), [
+            assert.equal(stripAnsi(result), [
                 "error: Parsing error: Unexpected token { at foo.js:1:2:",
                 "> 1 | e{}",
                 "    |  ^",
@@ -347,7 +348,7 @@ describe("formatter:codeframe", () => {
         it("should return a string without code preview (codeframe)", () => {
             const result = formatter(code);
 
-            assert.equal(chalk.stripColor(result), "error: Couldn't find foo.js at foo.js\n\n\n1 error found.");
+            assert.equal(stripAnsi(result), "error: Couldn't find foo.js at foo.js\n\n\n1 error found.");
         });
     });
 
@@ -367,13 +368,13 @@ describe("formatter:codeframe", () => {
         it("should return a string without code preview (codeframe)", () => {
             const result = formatter(code);
 
-            assert.equal(chalk.stripColor(result), "error: Unexpected foo (foo) at foo.js\n\n\n1 error found.");
+            assert.equal(stripAnsi(result), "error: Unexpected foo (foo) at foo.js\n\n\n1 error found.");
         });
 
         it("should output filepath but without 'line:column' appended", () => {
             const result = formatter(code);
 
-            assert.equal(chalk.stripColor(result), "error: Unexpected foo (foo) at foo.js\n\n\n1 error found.");
+            assert.equal(stripAnsi(result), "error: Unexpected foo (foo) at foo.js\n\n\n1 error found.");
         });
     });
 
diff --git a/tests/lib/formatters/junit.js b/tests/lib/formatters/junit.js
index 81a5e06c5..cddff54d7 100644
--- a/tests/lib/formatters/junit.js
+++ b/tests/lib/formatters/junit.js
@@ -177,7 +177,7 @@ describe("formatter:junit", () => {
         });
     });
 
-    describe("when passed multiple files with total 1 failure", () => {
+    describe("when passed multiple files should print even if no errors", () => {
         const code = [{
             filePath: "foo.js",
             messages: [{
@@ -192,10 +192,10 @@ describe("formatter:junit", () => {
             messages: []
         }];
 
-        it("should return 1 <testsuite>", () => {
+        it("should return 2 <testsuite>", () => {
             const result = formatter(code);
 
-            assert.equal(result.replace(/\n/g, ""), "<?xml version=\"1.0\" encoding=\"utf-8\"?><testsuites><testsuite package=\"org.eslint\" time=\"0\" tests=\"1\" errors=\"1\" name=\"foo.js\"><testcase time=\"0\" name=\"org.eslint.foo\"><failure message=\"Unexpected foo.\"><![CDATA[line 5, col 10, Warning - Unexpected foo. (foo)]]></failure></testcase></testsuite></testsuites>");
+            assert.equal(result.replace(/\n/g, ""), "<?xml version=\"1.0\" encoding=\"utf-8\"?><testsuites><testsuite package=\"org.eslint\" time=\"0\" tests=\"1\" errors=\"1\" name=\"foo.js\"><testcase time=\"0\" name=\"org.eslint.foo\"><failure message=\"Unexpected foo.\"><![CDATA[line 5, col 10, Warning - Unexpected foo. (foo)]]></failure></testcase></testsuite><testsuite package=\"org.eslint\" time=\"0\" tests=\"0\" errors=\"0\" name=\"bar.js\"></testsuite></testsuites>");
         });
     });
 });
diff --git a/tests/lib/rules/indent.js b/tests/lib/rules/indent.js
index 63aa710b0..7ea3c1890 100644
--- a/tests/lib/rules/indent.js
+++ b/tests/lib/rules/indent.js
@@ -1843,6 +1843,18 @@ ruleTester.run("indent", rule, {
             `,
             options: [4, { MemberExpression: 1 }]
         },
+        unIndent`
+            function foo() {
+                new
+                    .target
+            }
+        `,
+        unIndent`
+            function foo() {
+                new.
+                    target
+            }
+        `,
         {
             code: unIndent`
                 if (foo) {
@@ -4573,6 +4585,136 @@ ruleTester.run("indent", rule, {
                 );
             `,
             options: [2, { CallExpression: { arguments: "off" } }]
+        },
+        {
+            code: unIndent`
+                foo
+                ? bar
+                            : baz
+            `,
+            options: [4, { ignoredNodes: ["ConditionalExpression"] }]
+        },
+        {
+            code: unIndent`
+                class Foo {
+                foo() {
+                    bar();
+                }
+                }
+            `,
+            options: [4, { ignoredNodes: ["ClassBody"] }]
+        },
+        {
+            code: unIndent`
+                class Foo {
+                foo() {
+                bar();
+                }
+                }
+            `,
+            options: [4, { ignoredNodes: ["ClassBody", "BlockStatement"] }]
+        },
+        {
+            code: unIndent`
+                foo({
+                        bar: 1
+                    },
+                    {
+                        baz: 2
+                    },
+                    {
+                        qux: 3
+                })
+            `,
+            options: [4, { ignoredNodes: ["CallExpression > ObjectExpression"] }]
+        },
+        {
+            code: unIndent`
+                foo
+                                            .bar
+            `,
+            options: [4, { ignoredNodes: ["MemberExpression"] }]
+        },
+        {
+            code: unIndent`
+                $(function() {
+
+                foo();
+                bar();
+
+                });
+            `,
+            options: [4, {
+                ignoredNodes: ["Program > ExpressionStatement > CallExpression[callee.name='$'] > FunctionExpression > BlockStatement"]
+            }]
+        },
+        {
+            code: unIndent`
+                <Foo
+                            bar="1" />
+            `,
+            options: [4, { ignoredNodes: ["JSXOpeningElement"] }]
+        },
+        {
+            code: unIndent`
+                (function($) {
+                $(function() {
+                    foo;
+                });
+                }())
+            `,
+            options: [4, { ignoredNodes: ["ExpressionStatement > CallExpression > FunctionExpression.callee > BlockStatement"] }]
+        },
+        {
+            code: unIndent`
+                const value = (
+                    condition ?
+                    valueIfTrue :
+                    valueIfFalse
+                );
+            `,
+            options: [4, { ignoredNodes: ["ConditionalExpression"] }]
+        },
+        {
+            code: unIndent`
+                export default foo(
+                    a,
+                    b, {
+                    c
+                    }
+                )
+            `,
+            options: [4, { ignoredNodes: ["ExportDefaultDeclaration > CallExpression > ObjectExpression"] }],
+            parserOptions: { sourceType: "module" }
+        },
+        {
+            code: unIndent`
+                foobar = baz
+                       ? qux
+                       : boop
+            `,
+            options: [4, { ignoredNodes: ["ConditionalExpression"] }]
+        },
+        {
+            code: unIndent`
+                \`
+                    SELECT
+                        \${
+                            foo
+                        } FROM THE_DATABASE
+                \`
+            `,
+            options: [4, { ignoredNodes: ["TemplateLiteral"] }]
+        },
+        {
+            code: unIndent`
+                <foo
+                    prop='bar'
+                    >
+                    Text
+                </foo>
+            `,
+            options: [4, { ignoredNodes: ["JSXOpeningElement"] }]
         }
     ],
 
@@ -6127,6 +6269,36 @@ ruleTester.run("indent", rule, {
             options: [2, { MemberExpression: 2 }],
             errors: expectedErrors([[2, 4, 2, "Punctuator"], [3, 4, 2, "Punctuator"]])
         },
+        {
+            code: unIndent`
+                function foo() {
+                    new
+                    .target
+                }
+            `,
+            output: unIndent`
+                function foo() {
+                    new
+                        .target
+                }
+            `,
+            errors: expectedErrors([3, 8, 4, "Punctuator"])
+        },
+        {
+            code: unIndent`
+                function foo() {
+                    new.
+                    target
+                }
+            `,
+            output: unIndent`
+                function foo() {
+                    new.
+                        target
+                }
+            `,
+            errors: expectedErrors([3, 8, 4, "Identifier"])
+        },
         {
 
             // Indentation with multiple else statements: https://github.com/eslint/eslint/issues/6956
@@ -8888,6 +9060,74 @@ ruleTester.run("indent", rule, {
             `,
             errors: expectedErrors([3, 0, 4, "Punctuator"]),
             parser: require.resolve("../../fixtures/parsers/babel-eslint7/object-pattern-with-object-annotation")
+        },
+        {
+            code: unIndent`
+                class Foo {
+                foo() {
+                bar();
+                }
+                }
+            `,
+            output: unIndent`
+                class Foo {
+                foo() {
+                    bar();
+                }
+                }
+            `,
+            options: [4, { ignoredNodes: ["ClassBody"] }],
+            errors: expectedErrors([3, 4, 0, "Identifier"])
+        },
+        {
+            code: unIndent`
+                $(function() {
+
+                foo();
+                bar();
+
+                foo(function() {
+                baz();
+                });
+
+                });
+            `,
+            output: unIndent`
+                $(function() {
+
+                foo();
+                bar();
+
+                foo(function() {
+                    baz();
+                });
+
+                });
+            `,
+            options: [4, {
+                ignoredNodes: ["ExpressionStatement > CallExpression[callee.name='$'] > FunctionExpression > BlockStatement"]
+            }],
+            errors: expectedErrors([7, 4, 0, "Identifier"])
+        },
+        {
+            code: unIndent`
+                (function($) {
+                $(function() {
+                foo;
+                });
+                })()
+            `,
+            output: unIndent`
+                (function($) {
+                $(function() {
+                    foo;
+                });
+                })()
+            `,
+            options: [4, {
+                ignoredNodes: ["ExpressionStatement > CallExpression > FunctionExpression.callee > BlockStatement"]
+            }],
+            errors: expectedErrors([3, 4, 0, "Identifier"])
         }
     ]
 });