Repository: eslint/eslint (js)

Issue:
Autofixes in processors
I'd like to reopen discussion about support for autofixing in processors. Now that autofixing is stabilized and we have more rules that can be fixed automatically, it feels like it would be helpful to support it for processors. This is continuation of the discussion from #5121
My proposal is to create an additional function called `autoFix` in the processor definition. It will take two arguments, an array of strings with fixed text and filename. It's job would be to combine passed in fixed text and original file text and return complete text of the modified file. ESLint will, at that point, write that file to disk.
I'll open WIP PR with the changes. My only concern is using autofix in processors through API. Since API just gets back a list of messages, and not fixed text, location of those messages will be off. I'm not sure what would be a good approach to fix this issue, we can either strip autofix information from the messages, or require yet another function from the processor to fix location information for messages (or maybe require that `postprocess` fix not only message locations, but also autofix locations as well?).

Pull request description:
Update: allow autofixing when using processors (fixes #7510)
<!--
    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] Add something to the core

<!--
    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 implements the proposal from https://github.com/eslint/eslint/issues/7510#issuecomment-275947091 to allow autofixes to be applied when using processors.

The processor API basically remains the same. The only changes are:

* Processors can now export a `supportsAutofix: true` property, which opts a processor into autofixing.
* If a processor supports autofixing, the `postprocess` method is expected to also transform autofix ranges as necessary when it transforms the locations of reported problems. Afterwards, the transformed fixes are applied to the original, unprocessed text.

Multipass autofixing works by calling `preprocess` and `postprocess` for each pass.

(One alternative approach would be to run multipass autofixing on the processed text, and only return the fixed text to the postprocessor afterwards. As discussed in https://github.com/eslint/eslint/issues/7510, though, that would make it difficult for postprocessors because they would need to figure out how to split the fixed text into code blocks again, which would require some sort of imprecise diffing against the original text.)

Summary of changes:

* `linter.verify` and `linter.verifyAndFix`now accept optional `preprocess` and `postprocess` arguments for splitting up the text into code blocks and combining the messages from each code block. By default, the text is "preprocessed" into a single code block containing the code, and the problems are "postprocessed" by flattening the array. (This is the same as not applying a processor at all.)
* Applying processors is now is the responsibility of `Linter` rather than `CLIEngine`. (However, determining which processors need to be applied for a given file is still the responsibility of `CLIEngine`.)

TODO:

- [x] Add more tests
- [x] Add documentation
- [x] Get feedback on this API from people who have implemented processors (cc @BenoitZugmeyer, @btmills, others?)
- [x] Approve this API in a TSC meeting

**Is there anything you'd like reviewers to focus on?**

* Does the API seem reasonable?
* Does the API provide enough information to processors to allow them map autofix ranges correctly?
* ~~Is this a breaking change? (It seems like if a processor currently doesn't apply a transformation to fix ranges, then the original fix ranges will get used, which might be wildly incorrect and result in broken code.) One way to avoid this could be to have processors opt-in to allowing autofixes.~~
* Does it make sense to allow `postprocess` without `preprocess`? The default `postprocess` (flattening the array of problems) will almost always do the wrong thing if multiple code blocks are being linted, since some of the locations will probably be invalid. It might be better to just throw a validation error.
* This effectively allows plugins to remove or add any message to the user's reported problems list, with no opt-in. Are we sure we're okay with that? (Actually, that was already possible with processors before this change, but processors haven't gotten a lot of use because they prevent autofixing. Now that they don't have downsides, I can imagine that plugins could use them just for the purpose of modifying the problems list.)

Patch:
diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md
index 797bee468..96c3e0754 100644
--- a/docs/developer-guide/nodejs-api.md
+++ b/docs/developer-guide/nodejs-api.md
@@ -67,6 +67,8 @@ The most important method on `Linter` is `verify()`, which initiates linting of
     * **Note**: If you want to lint text and have your configuration be read and processed, use CLIEngine's [`executeOnFiles`](#executeonfiles) or [`executeOnText`](#executeontext) instead.
 * `options` - (optional) Additional options for this run.
     * `filename` - (optional) the filename to associate with the source code.
+    * `preprocess` - (optional) A function that accepts a string containing source text, and returns an array of strings containing blocks of code to lint. Also see: [Processors in Plugins](/docs/developer-guide/working-with-plugins#processors-in-plugins)
+    * `postprocess` - (optional) A function that accepts an array of problem lists (one list of problems for each block of code from `preprocess`), and returns a one-dimensional array of problems containing problems for the original, unprocessed text. Also see: [Processors in Plugins](/docs/developer-guide/working-with-plugins#processors-in-plugins)
     * `allowInlineConfig` - (optional) set to `false` to disable inline comments from changing eslint rules.
 
 If the third argument is a string, it is interpreted as the `filename`.
diff --git a/docs/developer-guide/working-with-plugins.md b/docs/developer-guide/working-with-plugins.md
index 59f4161d4..5cf63b7ee 100644
--- a/docs/developer-guide/working-with-plugins.md
+++ b/docs/developer-guide/working-with-plugins.md
@@ -72,15 +72,45 @@ processors: {
 
             // you need to return a one-dimensional array of the messages you want to keep
             return [Message];
-        }
+        },
+
+        supportsAutofix: true // (optional, defaults to false)
     }
 }
 ```
 
 The `preprocess` method takes the file contents and filename as arguments, and returns an array of strings to lint. The strings will be linted separately but still be registered to the filename. It's up to the plugin to decide if it needs to return just one part, or multiple pieces. For example in the case of processing `.html` files, you might want to return just one item in the array by combining all scripts, but for `.md` file where each JavaScript block might be independent, you can return multiple items.
 
-The `postprocess` method takes a two-dimensional array of arrays of lint messages and the filename. Each item in the input
-array corresponds to the part that was returned from the `preprocess` method. The `postprocess` method must adjust the location of all errors and aggregate them into a single flat array and return it.
+The `postprocess` method takes a two-dimensional array of arrays of lint messages and the filename. Each item in the input array corresponds to the part that was returned from the `preprocess` method. The `postprocess` method must adjust the locations of all errors to correspond to locations in the original, unprocessed code, and aggregate them into a single flat array and return it.
+
+Reported problems have the following location information:
+
+```typescript
+{
+    line: number,
+    column: number,
+
+    endLine?: number,
+    endColumn?: number
+}
+```
+
+By default, ESLint will not perform autofixes when a processor is used, even when the `--fix` flag is enabled on the command line. To allow ESLint to autofix code when using your processor, you should take the following additional steps:
+
+1. Update the `postprocess` method to additionally transform the `fix` property of reported problems. All autofixable problems will have a `fix` property, which is an object with the following schema:
+
+    ```js
+    {
+        range: [number, number],
+        text: string
+    }
+    ```
+
+    The `range` property contains two indexes in the code, referring to the start and end location of a contiguous section of text that will be replaced. The `text` property refers to the text that will replace the given range.
+
+    In the initial list of problems, the `fix` property will refer refer to a fix in the processed JavaScript. The `postprocess` method should transform the object to refer to a fix in the original, unprocessed file.
+
+2. Add a `supportsAutofix: true` property to the processor.
 
 You can have both rules and processors in a single plugin. You can also have multiple processors in one plugin.
 To support multiple extensions, add each one to the `processors` element and point them to the same object.
diff --git a/docs/rules/id-length.md b/docs/rules/id-length.md
index b63a88b08..a28407fec 100644
--- a/docs/rules/id-length.md
+++ b/docs/rules/id-length.md
@@ -192,7 +192,7 @@ data["y"] = 3;  // excused because of calculated property access
 Examples of **incorrect** code for this rule with the `{ "max": 10 }` option:
 
 ```js
-/*eslint id-length: ["error", { "max": "10" }]*/
+/*eslint id-length: ["error", { "max": 10 }]*/
 /*eslint-env es6*/
 
 var reallyLongVarName = 5;
@@ -210,7 +210,7 @@ try {
 Examples of **correct** code for this rule with the `{ "max": 10 }` option:
 
 ```js
-/*eslint id-length: ["error", { "max": "10" }]*/
+/*eslint id-length: ["error", { "max": 10 }]*/
 /*eslint-env es6*/
 
 var varName = 5;
diff --git a/docs/rules/no-restricted-globals.md b/docs/rules/no-restricted-globals.md
index c9df9d339..e3d612412 100644
--- a/docs/rules/no-restricted-globals.md
+++ b/docs/rules/no-restricted-globals.md
@@ -77,7 +77,7 @@ Examples of **incorrect** code for a sample `"event"` global variable name, alon
 
 ```js
 /*global event*/
-/* eslint no-restricted-globals: [{ name: "error", message: "Use local parameter instead." }] */
+/* eslint no-restricted-globals: ["error", { name: "error", message: "Use local parameter instead." }] */
 
 function onClick() {
     console.log(event);    // Unexpected global variable 'event'. Use local parameter instead.
diff --git a/docs/user-guide/command-line-interface.md b/docs/user-guide/command-line-interface.md
index 8e8b6206e..fb0152967 100644
--- a/docs/user-guide/command-line-interface.md
+++ b/docs/user-guide/command-line-interface.md
@@ -359,7 +359,7 @@ The resulting configuration file will be created in the current directory.
 This option instructs ESLint to try to fix as many issues as possible. The fixes are made to the actual files themselves and only the remaining unfixed issues are output. Not all problems are fixable using this option, and the option does not work in these situations:
 
 1. This option throws an error when code is piped to ESLint.
-1. This option has no effect on code that uses processors.
+1. This option has no effect on code that uses a processor, unless the processor opts into allowing autofixes.
 
 #### `--debug`
 
diff --git a/lib/cli-engine.js b/lib/cli-engine.js
index 8ac336c7a..054048038 100644
--- a/lib/cli-engine.js
+++ b/lib/cli-engine.js
@@ -143,10 +143,8 @@ function calculateStatsPerRun(results) {
  */
 function processText(text, configHelper, filename, fix, allowInlineConfig, linter) {
     let filePath,
-        messages,
         fileExtension,
-        processor,
-        fixedResult;
+        processor;
 
     if (filename) {
         filePath = path.resolve(filename);
@@ -170,51 +168,28 @@ function processText(text, configHelper, filename, fix, allowInlineConfig, linte
         }
     }
 
-    if (processor) {
-        debug("Using processor");
-        const parsedBlocks = processor.preprocess(text, filename);
-        const unprocessedMessages = [];
+    const autofixingEnabled = typeof fix !== "undefined" && (!processor || processor.supportsAutofix);
 
-        parsedBlocks.forEach(block => {
-            unprocessedMessages.push(linter.verify(block, config, {
-                filename,
-                allowInlineConfig
-            }));
-        });
-
-        // TODO(nzakas): Figure out how fixes might work for processors
-
-        messages = processor.postprocess(unprocessedMessages, filename);
-
-    } else {
-
-        if (fix) {
-            fixedResult = linter.verifyAndFix(text, config, {
-                filename,
-                allowInlineConfig,
-                fix
-            });
-            messages = fixedResult.messages;
-        } else {
-            messages = linter.verify(text, config, {
-                filename,
-                allowInlineConfig
-            });
-        }
-    }
+    const fixedResult = linter.verifyAndFix(text, config, {
+        filename,
+        allowInlineConfig,
+        fix: !!autofixingEnabled && fix,
+        preprocess: processor && (rawText => processor.preprocess(rawText, filename)),
+        postprocess: processor && (problemLists => processor.postprocess(problemLists, filename))
+    });
 
-    const stats = calculateStatsPerFile(messages);
+    const stats = calculateStatsPerFile(fixedResult.messages);
 
     const result = {
         filePath: filename,
-        messages,
+        messages: fixedResult.messages,
         errorCount: stats.errorCount,
         warningCount: stats.warningCount,
         fixableErrorCount: stats.fixableErrorCount,
         fixableWarningCount: stats.fixableWarningCount
     };
 
-    if (fixedResult && fixedResult.fixed) {
+    if (fixedResult.fixed) {
         result.output = fixedResult.output;
     }
 
diff --git a/lib/linter.js b/lib/linter.js
index 1e9a75028..dedd7e1f3 100755
--- a/lib/linter.js
+++ b/lib/linter.js
@@ -12,6 +12,7 @@
 const EventEmitter = require("events").EventEmitter,
     eslintScope = require("eslint-scope"),
     levn = require("levn"),
+    lodash = require("lodash"),
     blankScriptAST = require("../conf/blank-script.json"),
     defaultConfig = require("../conf/default-config-options.js"),
     CodePathAnalyzer = require("./code-path-analysis/code-path-analyzer"),
@@ -721,7 +722,7 @@ module.exports = class Linter {
      */
 
     /**
-     * Verifies the text against the rules specified by the second argument.
+     * Same as linter.verify, except without support for processors.
      * @param {string|SourceCode} textOrSourceCode The text to parse or a SourceCode object.
      * @param {ESLintConfig} config An ESLintConfig instance to configure everything.
      * @param {(string|Object)} [filenameOrOptions] The optional filename of the file being checked.
@@ -731,7 +732,7 @@ module.exports = class Linter {
      *      Useful if you want to validate JS without comments overriding rules.
      * @returns {Object[]} The results as an array of messages or null if no messages.
      */
-    verify(textOrSourceCode, config, filenameOrOptions) {
+    _verifyWithoutProcessors(textOrSourceCode, config, filenameOrOptions) {
         let text,
             parserServices,
             allowInlineConfig,
@@ -965,6 +966,35 @@ module.exports = class Linter {
         });
     }
 
+    /**
+     * Verifies the text against the rules specified by the second argument.
+     * @param {string|SourceCode} textOrSourceCode The text to parse or a SourceCode object.
+     * @param {ESLintConfig} config An ESLintConfig instance to configure everything.
+     * @param {(string|Object)} [filenameOrOptions] The optional filename of the file being checked.
+     *      If this is not set, the filename will default to '<input>' in the rule context. If
+     *      an object, then it has "filename", "saveState", and "allowInlineConfig" properties.
+     * @param {boolean} [saveState] Indicates if the state from the last run should be saved.
+     *      Mostly useful for testing purposes.
+     * @param {boolean} [filenameOrOptions.allowInlineConfig] Allow/disallow inline comments' ability to change config once it is set. Defaults to true if not supplied.
+     *      Useful if you want to validate JS without comments overriding rules.
+     * @param {function(string): string[]} [filenameOrOptions.preprocess] preprocessor for source text. If provided,
+     *      this should accept a string of source text, and return an array of code blocks to lint.
+     * @param {function(Array<Object[]>): Object[]} [filenameOrOptions.postprocess] postprocessor for report messages. If provided,
+     *      this should accept an array of the message lists for each code block returned from the preprocessor,
+     *      apply a mapping to the messages as appropriate, and return a one-dimensional array of messages
+     * @returns {Object[]} The results as an array of messages or null if no messages.
+     */
+    verify(textOrSourceCode, config, filenameOrOptions) {
+        const preprocess = filenameOrOptions && filenameOrOptions.preprocess || (rawText => [rawText]);
+        const postprocess = filenameOrOptions && filenameOrOptions.postprocess || lodash.flatten;
+
+        return postprocess(
+            preprocess(textOrSourceCode).map(
+                textBlock => this._verifyWithoutProcessors(textBlock, config, filenameOrOptions)
+            )
+        );
+    }
+
     /**
      * Gets the SourceCode object representing the parsed source.
      * @returns {SourceCode} The SourceCode object.
@@ -1012,6 +1042,11 @@ module.exports = class Linter {
      * @param {boolean} options.allowInlineConfig Flag indicating if inline comments
      *      should be allowed.
      * @param {boolean|Function} options.fix Determines whether fixes should be applied
+     * @param {Function} options.preprocess preprocessor for source text. If provided, this should
+     *      accept a string of source text, and return an array of code blocks to lint.
+     * @param {Function} options.postprocess postprocessor for report messages. If provided,
+     *      this should accept an array of the message lists for each code block returned from the preprocessor,
+     *      apply a mapping to the messages as appropriate, and return a one-dimensional array of messages
      * @returns {Object} The result of the fix operation as returned from the
      *      SourceCodeFixer.
      */
diff --git a/lib/rules/indent.js b/lib/rules/indent.js
index 07aa1e830..0f6468a7e 100644
--- a/lib/rules/indent.js
+++ b/lib/rules/indent.js
@@ -950,12 +950,12 @@ module.exports = {
         }
 
         /**
-         * Check whether the given token is the first token of a statement.
+         * Check whether the given token is on the first line of a statement.
          * @param {Token} token The token to check.
          * @param {ASTNode} leafNode The expression node that the token belongs directly.
-         * @returns {boolean} `true` if the token is the first token of a statement.
+         * @returns {boolean} `true` if the token is on the first line of a statement.
          */
-        function isFirstTokenOfStatement(token, leafNode) {
+        function isOnFirstLineOfStatement(token, leafNode) {
             let node = leafNode;
 
             while (node.parent && !node.parent.type.endsWith("Statement") && !node.parent.type.endsWith("Declaration")) {
@@ -963,7 +963,7 @@ module.exports = {
             }
             node = node.parent;
 
-            return !node || node.range[0] === token.range[0];
+            return !node || node.loc.start.line === token.loc.start.line;
         }
 
         const baseOffsetListeners = {
@@ -1076,7 +1076,7 @@ module.exports = {
                 //     /*else*/ qiz ;
                 if (!options.flatTernaryExpressions ||
                     !astUtils.isTokenOnSameLine(node.test, node.consequent) ||
-                    isFirstTokenOfStatement(firstToken, node)
+                    isOnFirstLineOfStatement(firstToken, node)
                 ) {
                     const questionMarkToken = sourceCode.getFirstTokenBetween(node.test, node.consequent, token => token.type === "Punctuator" && token.value === "?");
                     const colonToken = sourceCode.getFirstTokenBetween(node.consequent, node.alternate, token => token.type === "Punctuator" && token.value === ":");
diff --git a/lib/rules/newline-per-chained-call.js b/lib/rules/newline-per-chained-call.js
index 613429d13..356c4baf3 100644
--- a/lib/rules/newline-per-chained-call.js
+++ b/lib/rules/newline-per-chained-call.js
@@ -19,7 +19,7 @@ module.exports = {
             category: "Stylistic Issues",
             recommended: false
         },
-
+        fixable: "whitespace",
         schema: [{
             type: "object",
             properties: {
@@ -40,6 +40,18 @@ module.exports = {
 
         const sourceCode = context.getSourceCode();
 
+        /**
+         * Get the prefix of a given MemberExpression node.
+         * If the MemberExpression node is a computed value it returns a
+         * left bracket. If not it returns a period.
+         *
+         * @param  {ASTNode} node - A MemberExpression node to get
+         * @returns {string} The prefix of the node.
+         */
+        function getPrefix(node) {
+            return node.computed ? "[" : ".";
+        }
+
         /**
          * Gets the property text of a given MemberExpression node.
          * If the text is multiline, this returns only the first line.
@@ -48,7 +60,7 @@ module.exports = {
          * @returns {string} The property text of the node.
          */
         function getPropertyText(node) {
-            const prefix = node.computed ? "[" : ".";
+            const prefix = getPrefix(node);
             const lines = sourceCode.getText(node.property).split(astUtils.LINEBREAK_MATCHER);
             const suffix = node.computed && lines.length === 1 ? "]" : "";
 
@@ -70,13 +82,18 @@ module.exports = {
                     parent = parent.callee.object;
                 }
 
-                if (depth > ignoreChainWithDepth && callee.property.loc.start.line === callee.object.loc.end.line) {
+                if (depth > ignoreChainWithDepth && astUtils.isTokenOnSameLine(callee.object, callee.property)) {
                     context.report({
                         node: callee.property,
                         loc: callee.property.loc.start,
                         message: "Expected line break before `{{callee}}`.",
                         data: {
                             callee: getPropertyText(callee)
+                        },
+                        fix(fixer) {
+                            const firstTokenAfterObject = sourceCode.getTokenAfter(callee.object, astUtils.isNotClosingParenToken);
+
+                            return fixer.insertTextBefore(firstTokenAfterObject, "\n");
                         }
                     });
                 }
diff --git a/lib/rules/no-undef-init.js b/lib/rules/no-undef-init.js
index 9df40e9ce..7e58f55a6 100644
--- a/lib/rules/no-undef-init.js
+++ b/lib/rules/no-undef-init.js
@@ -43,6 +43,10 @@ module.exports = {
                         message: "It's not necessary to initialize '{{name}}' to undefined.",
                         data: { name },
                         fix(fixer) {
+                            if (node.parent.kind === "var") {
+                                return null;
+                            }
+
                             if (node.id.type === "ArrayPattern" || node.id.type === "ObjectPattern") {
 
                                 // Don't fix destructuring assignment to `undefined`.
diff --git a/package.json b/package.json
index 898ce38ad..0fed8f35f 100644
--- a/package.json
+++ b/package.json
@@ -40,7 +40,7 @@
     "chalk": "^2.1.0",
     "concat-stream": "^1.6.0",
     "cross-spawn": "^5.1.0",
-    "debug": "^2.6.8",
+    "debug": "^3.0.1",
     "doctrine": "^2.0.0",
     "eslint-scope": "^3.7.1",
     "espree": "^3.5.0",
@@ -64,7 +64,7 @@
     "natural-compare": "^1.4.0",
     "optionator": "^0.8.2",
     "path-is-inside": "^1.0.2",
-    "pluralize": "^4.0.0",
+    "pluralize": "^7.0.0",
     "progress": "^2.0.0",
     "require-uncached": "^1.0.3",
     "semver": "^5.3.0",
@@ -89,7 +89,7 @@
     "eslint-plugin-node": "^5.1.0",
     "eslint-release": "^0.10.1",
     "eslump": "1.6.0",
-    "esprima": "^3.1.3",
+    "esprima": "^4.0.0",
     "esprima-fb": "^15001.1001.0-dev-harmony-fb",
     "istanbul": "^0.4.5",
     "jsdoc": "^3.4.3",
@@ -100,7 +100,7 @@
     "karma-phantomjs-launcher": "^1.0.4",
     "leche": "^2.1.2",
     "load-perf": "^0.2.0",
-    "markdownlint": "^0.5.0",
+    "markdownlint": "^0.6.1",
     "mocha": "^3.4.2",
     "mock-fs": "^4.3.0",
     "npm-license": "^0.3.3",
@@ -108,7 +108,7 @@
     "proxyquire": "^1.8.0",
     "shelljs": "^0.7.7",
     "shelljs-nodecli": "~0.1.1",
-    "sinon": "^2.3.2",
+    "sinon": "^3.2.1",
     "temp": "^0.8.3",
     "through": "^2.3.8"
   },


Test patch:
diff --git a/tests/lib/cli-engine.js b/tests/lib/cli-engine.js
index ea564ca00..b862062c8 100644
--- a/tests/lib/cli-engine.js
+++ b/tests/lib/cli-engine.js
@@ -2478,6 +2478,94 @@ describe("CLIEngine", () => {
                 assert.equal(report.results[0].messages[0].message, "'b' is defined but never used.");
                 assert.equal(report.results[0].messages[0].ruleId, "post-processed");
             });
+
+            describe("autofixing with processors", () => {
+                const HTML_PROCESSOR = Object.freeze({
+                    preprocess(text) {
+                        return [text.replace(/^<script>/, "").replace(/<\/script>$/, "")];
+                    },
+                    postprocess(problemLists) {
+                        return problemLists[0].map(problem => {
+                            if (problem.fix) {
+                                const updatedFix = Object.assign({}, problem.fix, {
+                                    range: problem.fix.range.map(index => index + "<script>".length)
+                                });
+
+                                return Object.assign({}, problem, { fix: updatedFix });
+                            }
+                            return problem;
+                        });
+                    }
+                });
+
+
+                it("should run in autofix mode when using a processor that supports autofixing", () => {
+                    engine = new CLIEngine({
+                        useEslintrc: false,
+                        plugins: ["test-processor"],
+                        rules: {
+                            semi: 2
+                        },
+                        extensions: ["js", "txt"],
+                        ignore: false,
+                        fix: true
+                    });
+
+                    engine.addPlugin("test-processor", {
+                        processors: {
+                            ".html": Object.assign({ supportsAutofix: true }, HTML_PROCESSOR)
+                        }
+                    });
+
+                    const report = engine.executeOnText("<script>foo</script>", "foo.html");
+
+                    assert.strictEqual(report.results[0].messages.length, 0);
+                    assert.strictEqual(report.results[0].output, "<script>foo;</script>");
+                });
+
+                it("should not run in autofix mode when using a processor that does not support autofixing", () => {
+                    engine = new CLIEngine({
+                        useEslintrc: false,
+                        plugins: ["test-processor"],
+                        rules: {
+                            semi: 2
+                        },
+                        extensions: ["js", "txt"],
+                        ignore: false,
+                        fix: true
+                    });
+
+                    engine.addPlugin("test-processor", { processors: { ".html": HTML_PROCESSOR } });
+
+                    const report = engine.executeOnText("<script>foo</script>", "foo.html");
+
+                    assert.strictEqual(report.results[0].messages.length, 1);
+                    assert.isFalse(Object.prototype.hasOwnProperty.call(report.results[0], "output"));
+                });
+
+                it("should not run in autofix mode when `fix: true` is not provided, even if the processor supports autofixing", () => {
+                    engine = new CLIEngine({
+                        useEslintrc: false,
+                        plugins: ["test-processor"],
+                        rules: {
+                            semi: 2
+                        },
+                        extensions: ["js", "txt"],
+                        ignore: false
+                    });
+
+                    engine.addPlugin("test-processor", {
+                        processors: {
+                            ".html": Object.assign({ supportsAutofix: true }, HTML_PROCESSOR)
+                        }
+                    });
+
+                    const report = engine.executeOnText("<script>foo</script>", "foo.html");
+
+                    assert.strictEqual(report.results[0].messages.length, 1);
+                    assert.isFalse(Object.prototype.hasOwnProperty.call(report.results[0], "output"));
+                });
+            });
         });
     });
 
diff --git a/tests/lib/linter.js b/tests/lib/linter.js
index 5b65fb06c..bd91c4ff8 100644
--- a/tests/lib/linter.js
+++ b/tests/lib/linter.js
@@ -3590,6 +3590,136 @@ describe("Linter", () => {
         });
     });
 
+    describe("processors", () => {
+        beforeEach(() => {
+
+            // A rule that always reports the AST with a message equal to the source text
+            linter.defineRule("report-original-text", context => ({
+                Program(ast) {
+                    context.report({ node: ast, message: context.getSourceCode().text });
+                }
+            }));
+        });
+
+        describe("preprocessors", () => {
+            it("should apply a preprocessor to the code, and lint each code sample separately", () => {
+                const code = "foo bar baz";
+                const problems = linter.verify(
+                    code,
+                    { rules: { "report-original-text": "error" } },
+                    {
+
+                        // Apply a preprocessor that splits the source text into spaces and lints each word individually
+                        preprocess(input) {
+                            assert.strictEqual(input, code);
+                            assert.strictEqual(arguments.length, 1);
+                            return input.split(" ");
+                        }
+                    }
+                );
+
+                assert.strictEqual(problems.length, 3);
+                assert.deepEqual(problems.map(problem => problem.message), ["foo", "bar", "baz"]);
+            });
+        });
+
+        describe("postprocessors", () => {
+            it("should apply a postprocessor to the reported messages", () => {
+                const code = "foo bar baz";
+
+                const problems = linter.verify(
+                    code,
+                    { rules: { "report-original-text": "error" } },
+                    {
+                        preprocess: input => input.split(" "),
+
+                        /*
+                         * Apply a postprocessor that updates the locations of the reported problems
+                         * to make sure they correspond to the locations in the original text.
+                         */
+                        postprocess(problemLists) {
+                            assert.strictEqual(problemLists.length, 3);
+                            assert.strictEqual(arguments.length, 1);
+
+                            problemLists.forEach(problemList => assert.strictEqual(problemList.length, 1));
+                            return problemLists.reduce(
+                                (combinedList, problemList, index) =>
+                                    combinedList.concat(
+                                        problemList.map(
+                                            problem =>
+                                                Object.assign(
+                                                    {},
+                                                    problem,
+                                                    {
+                                                        message: problem.message.toUpperCase(),
+                                                        column: problem.column + index * 4
+                                                    }
+                                                )
+                                        )
+                                    ),
+                                []
+                            );
+                        }
+                    }
+                );
+
+                assert.strictEqual(problems.length, 3);
+                assert.deepEqual(problems.map(problem => problem.message), ["FOO", "BAR", "BAZ"]);
+                assert.deepEqual(problems.map(problem => problem.column), [1, 5, 9]);
+            });
+
+            it("should use postprocessed problem ranges when applying autofixes", () => {
+                const code = "foo bar baz";
+
+                linter.defineRule("capitalize-identifiers", context => ({
+                    Identifier(node) {
+                        if (node.name !== node.name.toUpperCase()) {
+                            context.report({
+                                node,
+                                message: "Capitalize this identifier",
+                                fix: fixer => fixer.replaceText(node, node.name.toUpperCase())
+                            });
+                        }
+                    }
+                }));
+
+                const fixResult = linter.verifyAndFix(
+                    code,
+                    { rules: { "capitalize-identifiers": "error" } },
+                    {
+
+                        /*
+                         * Apply a postprocessor that updates the locations of autofixes
+                         * to make sure they correspond to locations in the original text.
+                         */
+                        preprocess: input => input.split(" "),
+                        postprocess(problemLists) {
+                            return problemLists.reduce(
+                                (combinedProblems, problemList, blockIndex) =>
+                                    combinedProblems.concat(
+                                        problemList.map(problem =>
+                                            Object.assign(problem, {
+                                                fix: {
+                                                    text: problem.fix.text,
+                                                    range: problem.fix.range.map(
+                                                        rangeIndex => rangeIndex + blockIndex * 4
+                                                    )
+                                                }
+                                            }))
+                                    ),
+                                []
+                            );
+                        }
+                    }
+                );
+
+                assert.strictEqual(fixResult.fixed, true);
+                assert.strictEqual(fixResult.messages.length, 0);
+                assert.strictEqual(fixResult.output, "FOO BAR BAZ");
+            });
+        });
+    });
+
     describe("verifyAndFix", () => {
         it("Fixes the code", () => {
             const messages = linter.verifyAndFix("var a", {
diff --git a/tests/lib/rules/indent.js b/tests/lib/rules/indent.js
index 1339c16f6..006d7f1dd 100644
--- a/tests/lib/rules/indent.js
+++ b/tests/lib/rules/indent.js
@@ -3591,6 +3591,31 @@ ruleTester.run("indent", rule, {
             `,
             options: [4, { flatTernaryExpressions: true }]
         },
+        {
+            code: unIndent`
+                function foo() {
+                    return foo ? bar :
+                        baz
+                }
+            `,
+            options: [4, { flatTernaryExpressions: true }]
+        },
+        {
+            code: unIndent`
+                throw foo ? bar :
+                    baz
+            `,
+            options: [4, { flatTernaryExpressions: true }]
+        },
+        {
+            code: unIndent`
+                foo(
+                    bar
+                ) ? baz :
+                    qux
+            `,
+            options: [4, { flatTernaryExpressions: true }]
+        },
         unIndent`
                 foo
                     [
diff --git a/tests/lib/rules/newline-per-chained-call.js b/tests/lib/rules/newline-per-chained-call.js
index 232aafed6..ada18a70b 100644
--- a/tests/lib/rules/newline-per-chained-call.js
+++ b/tests/lib/rules/newline-per-chained-call.js
@@ -28,6 +28,7 @@ ruleTester.run("newline-per-chained-call", rule, {
     }],
     invalid: [{
         code: "_\n.chain({}).map(foo).filter(bar).value();",
+        output: "_\n.chain({}).map(foo)\n.filter(bar)\n.value();",
         errors: [{
             message: "Expected line break before `.filter`."
         }, {
@@ -35,26 +36,31 @@ ruleTester.run("newline-per-chained-call", rule, {
         }]
     }, {
         code: "_\n.chain({})\n.map(foo)\n.filter(bar).value();",
+        output: "_\n.chain({})\n.map(foo)\n.filter(bar)\n.value();",
         errors: [{
             message: "Expected line break before `.value`."
         }]
     }, {
         code: "a().b().c().e.d()",
+        output: "a().b()\n.c().e.d()",
         errors: [{
             message: "Expected line break before `.c`."
         }]
     }, {
         code: "a.b.c().e().d()",
+        output: "a.b.c().e()\n.d()",
         errors: [{
             message: "Expected line break before `.d`."
         }]
     }, {
         code: "_.chain({}).map(a).value(); ",
+        output: "_.chain({}).map(a)\n.value(); ",
         errors: [{
             message: "Expected line break before `.value`."
         }]
     }, {
         code: "var a = m1.m2();\n var b = m1.m2().m3().m4().m5();",
+        output: "var a = m1.m2();\n var b = m1.m2().m3()\n.m4()\n.m5();",
         errors: [{
             message: "Expected line break before `.m4`."
         }, {
@@ -62,11 +68,13 @@ ruleTester.run("newline-per-chained-call", rule, {
         }]
     }, {
         code: "var a = m1.m2();\n var b = m1.m2().m3()\n.m4().m5();",
+        output: "var a = m1.m2();\n var b = m1.m2().m3()\n.m4()\n.m5();",
         errors: [{
             message: "Expected line break before `.m5`."
         }]
     }, {
         code: "var a = m1().m2\n.m3().m4().m5().m6().m7();",
+        output: "var a = m1().m2\n.m3().m4().m5()\n.m6()\n.m7();",
         options: [{
             ignoreChainWithDepth: 3
         }],
@@ -105,6 +113,37 @@ ruleTester.run("newline-per-chained-call", rule, {
             "    // Do something with error.",
             "}).end();"
         ].join("\n"),
+        output: [
+            "http.request({",
+            "    // Param",
+            "    // Param",
+            "    // Param",
+            "}).on('response', function(response) {",
+            "    // Do something with response.",
+            "    // Do something with response.",
+            "    // Do something with response.",
+            "    // Do something with response.",
+            "    // Do something with response.",
+            "    // Do something with response.",
+            "    // Do something with response.",
+            "    // Do something with response.",
+            "    // Do something with response.",
+            "    // Do something with response.",
+            "})",
+            ".on('error', function(error) {",
+            "    // Do something with error.",
+            "    // Do something with error.",
+            "    // Do something with error.",
+            "    // Do something with error.",
+            "    // Do something with error.",
+            "    // Do something with error.",
+            "    // Do something with error.",
+            "    // Do something with error.",
+            "    // Do something with error.",
+            "    // Do something with error.",
+            "})",
+            ".end();"
+        ].join("\n"),
         errors: [{
             message: "Expected line break before `.on`."
         }, {
@@ -116,6 +155,13 @@ ruleTester.run("newline-per-chained-call", rule, {
             "    'method3' :",
             "    'method4']()"
         ].join("\n"),
+        output: [
+            "anObject.method1().method2()",
+            "['method' + n]()",
+            "[aCondition ?",
+            "    'method3' :",
+            "    'method4']()"
+        ].join("\n"),
         errors: [{
             message: "Expected line break before `['method' + n]`."
         }, {
@@ -123,8 +169,28 @@ ruleTester.run("newline-per-chained-call", rule, {
         }]
     }, {
         code: "foo.bar()['foo' + \u2029 + 'bar']()",
+        output: "foo.bar()\n['foo' + \u2029 + 'bar']()",
         options: [{ ignoreChainWithDepth: 1 }],
         errors: [{ message: "Expected line break before `['foo' + `." }]
+    }, {
+        code: "foo.bar()[(biz)]()",
+        output: "foo.bar()\n[(biz)]()",
+        options: [{ ignoreChainWithDepth: 1 }],
+        errors: [{ message: "Expected line break before `[biz]`." }]
+    }, {
+        code: "(foo).bar().biz()",
+        output: "(foo).bar()\n.biz()",
+        options: [{ ignoreChainWithDepth: 1 }],
+        errors: [{ message: "Expected line break before `.biz`." }]
+    }, {
+        code: "foo.bar(). /* comment */ biz()",
+        output: "foo.bar()\n. /* comment */ biz()",
+        options: [{ ignoreChainWithDepth: 1 }],
+        errors: [{ message: "Expected line break before `.biz`." }]
+    }, {
+        code: "foo.bar() /* comment */ .biz()",
+        output: "foo.bar() /* comment */ \n.biz()",
+        options: [{ ignoreChainWithDepth: 1 }],
+        errors: [{ message: "Expected line break before `.biz`." }]
     }]
-
 });
diff --git a/tests/lib/rules/no-undef-init.js b/tests/lib/rules/no-undef-init.js
index 2d44e1656..dfd45e008 100644
--- a/tests/lib/rules/no-undef-init.js
+++ b/tests/lib/rules/no-undef-init.js
@@ -27,17 +27,17 @@ ruleTester.run("no-undef-init", rule, {
     invalid: [
         {
             code: "var a = undefined;",
-            output: "var a;",
+            output: null,
             errors: [{ message: "It's not necessary to initialize 'a' to undefined.", type: "VariableDeclarator" }]
         },
         {
             code: "var a = undefined, b = 1;",
-            output: "var a, b = 1;",
+            output: null,
             errors: [{ message: "It's not necessary to initialize 'a' to undefined.", type: "VariableDeclarator" }]
         },
         {
             code: "var a = 1, b = undefined, c = 5;",
-            output: "var a = 1, b, c = 5;",
+            output: null,
             errors: [{ message: "It's not necessary to initialize 'b' to undefined.", type: "VariableDeclarator" }]
         },
         {
@@ -51,6 +51,47 @@ ruleTester.run("no-undef-init", rule, {
             output: null,
             parserOptions: { ecmaVersion: 6 },
             errors: [{ message: "It's not necessary to initialize '{a}' to undefined.", type: "VariableDeclarator" }]
+        },
+        {
+            code: "for(var i in [1,2,3]){var a = undefined; for(var j in [1,2,3]){}}",
+            output: null,
+            errors: [{ message: "It's not necessary to initialize 'a' to undefined.", type: "VariableDeclarator" }]
+        },
+        {
+            code: "let a = undefined;",
+            output: "let a;",
+            parserOptions: { ecmaVersion: 6 },
+            errors: [{ message: "It's not necessary to initialize 'a' to undefined.", type: "VariableDeclarator" }]
+        },
+        {
+            code: "let a = undefined, b = 1;",
+            output: "let a, b = 1;",
+            parserOptions: { ecmaVersion: 6 },
+            errors: [{ message: "It's not necessary to initialize 'a' to undefined.", type: "VariableDeclarator" }]
+        },
+        {
+            code: "let a = 1, b = undefined, c = 5;",
+            output: "let a = 1, b, c = 5;",
+            parserOptions: { ecmaVersion: 6 },
+            errors: [{ message: "It's not necessary to initialize 'b' to undefined.", type: "VariableDeclarator" }]
+        },
+        {
+            code: "let [a] = undefined;",
+            output: null,
+            parserOptions: { ecmaVersion: 6 },
+            errors: [{ message: "It's not necessary to initialize '[a]' to undefined.", type: "VariableDeclarator" }]
+        },
+        {
+            code: "let {a} = undefined;",
+            output: null,
+            parserOptions: { ecmaVersion: 6 },
+            errors: [{ message: "It's not necessary to initialize '{a}' to undefined.", type: "VariableDeclarator" }]
+        },
+        {
+            code: "for(var i in [1,2,3]){let a = undefined; for(var j in [1,2,3]){}}",
+            output: "for(var i in [1,2,3]){let a; for(var j in [1,2,3]){}}",
+            parserOptions: { ecmaVersion: 6 },
+            errors: [{ message: "It's not necessary to initialize 'a' to undefined.", type: "VariableDeclarator" }]
         }
     ]
 });