Repository: eslint/eslint (js)

Issue:
Report format: log number of fixable problems
I'm using eslint v3.7.1.

One of my favourite eslint features is the ability to fix a lot of problems automatically. When my team considers adding a new eslint rule, we take into account how many times the rule is broken and whether those new problems can be fixed automatically. So sometimes we want to know how many problems can be automatically fixed without actually running eslint with the `--fix` option.

The current report ends with something like this:

```
✖ 103 problems (103 errors, 12 warnings)
```

It would be great if the number of fixable problems was also included. Maybe like this:

```
✖ 103 problems (103 errors, 12 warnings).
88 errors, 2 warnings can be fixed by running eslint with the --fix option.
```


Pull request:
Breaking: log number of fixable problems (fixes #7364)
**What is the purpose of this pull request? (put an "X" next to item)**

[ ] Documentation update
[ ] Bug fix ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/bug-report.md))
[ ] New rule ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/rule-proposal.md))
[ ] Changes an existing rule ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/rule-change-proposal.md))
[ ] Add autofixing to a rule
[ ] Add a CLI option
[X] Add something to the core
[ ] Other, please explain:

<!--
    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)**
Add number of fixable problems to `cli-engine` and consume it in stylish formatter. This fixes #7364 

**Is there anything you'd like reviewers to focus on?**
No.



Patch:
diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md
index 60148ce4e..c3cceeebd 100644
--- a/docs/developer-guide/nodejs-api.md
+++ b/docs/developer-guide/nodejs-api.md
@@ -233,11 +233,15 @@ The return value is an object containing the results of the linting operation. H
             }],
             errorCount: 1,
             warningCount: 0,
+            fixableErrorCount: 1,
+            fixableWarningCount: 0,
             source: "\"use strict\"\n"
         }
     ],
     errorCount: 1,
-    warningCount: 0
+    warningCount: 0,
+    fixableErrorCount: 1,
+    fixableWarningCount: 0
 }
 ```
 
@@ -286,13 +290,17 @@ var report = cli.executeOnFiles(["myfile.js", "lib/"]);
                     source: "var foo = function bar() {};"
                 }
             ],
-            errorCount: 1,
+            errorCount: 2,
             warningCount: 0,
+            fixableErrorCount: 1,
+            fixableWarningCount: 0,
             output: "\"use strict\";\nvar foo = function bar() {};\nfoo();\n"
         }
     ],
-    errorCount: 1,
-    warningCount: 0
+    errorCount: 2,
+    warningCount: 0,
+    fixableErrorCount: 1,
+    fixableWarningCount: 0,
 }
 ```
 
@@ -316,11 +324,15 @@ If the operation ends with a parsing error, you will get a single message for th
             ],
             errorCount: 1,
             warningCount: 0,
+            fixableErrorCount: 0,
+            fixableWarningCount: 0,
             source: "fucntion foo() {}"
         }
     ],
     errorCount: 1,
-    warningCount: 0
+    warningCount: 0,
+    fixableErrorCount: 0,
+    fixableWarningCount: 0,
 }
 ```
 
diff --git a/lib/cli-engine.js b/lib/cli-engine.js
index 227305903..6dd85a394 100644
--- a/lib/cli-engine.js
+++ b/lib/cli-engine.js
@@ -73,8 +73,10 @@ const debug = require("debug")("eslint:cli-engine");
  * @typedef {Object} LintResult
  * @property {string} filePath The path to the file that was linted.
  * @property {LintMessage[]} messages All of the messages for the result.
- * @property {number} errorCount Number or errors for the result.
- * @property {number} warningCount Number or warnings for the result.
+ * @property {number} errorCount Number of errors for the result.
+ * @property {number} warningCount Number of warnings for the result.
+ * @property {number} fixableErrorCount Number of fixable errors for the result.
+ * @property {number} fixableWarningCount Number of fixable warnings for the result.
  * @property {string=} [source] The source code of the file that was linted.
  * @property {string=} [output] The source code of the file that was linted, with as many fixes applied as possible.
  */
@@ -93,13 +95,21 @@ function calculateStatsPerFile(messages) {
     return messages.reduce((stat, message) => {
         if (message.fatal || message.severity === 2) {
             stat.errorCount++;
+            if (message.fix) {
+                stat.fixableErrorCount++;
+            }
         } else {
             stat.warningCount++;
+            if (message.fix) {
+                stat.fixableWarningCount++;
+            }
         }
         return stat;
     }, {
         errorCount: 0,
-        warningCount: 0
+        warningCount: 0,
+        fixableErrorCount: 0,
+        fixableWarningCount: 0
     });
 }
 
@@ -113,10 +123,14 @@ function calculateStatsPerRun(results) {
     return results.reduce((stat, result) => {
         stat.errorCount += result.errorCount;
         stat.warningCount += result.warningCount;
+        stat.fixableErrorCount += result.fixableErrorCount;
+        stat.fixableWarningCount += result.fixableWarningCount;
         return stat;
     }, {
         errorCount: 0,
-        warningCount: 0
+        warningCount: 0,
+        fixableErrorCount: 0,
+        fixableWarningCount: 0
     });
 }
 
@@ -274,7 +288,9 @@ function processText(text, configHelper, filename, fix, allowInlineConfig) {
         filePath: filename,
         messages,
         errorCount: stats.errorCount,
-        warningCount: stats.warningCount
+        warningCount: stats.warningCount,
+        fixableErrorCount: stats.fixableErrorCount,
+        fixableWarningCount: stats.fixableWarningCount
     };
 
     if (fixedResult && fixedResult.fixed) {
@@ -339,7 +355,9 @@ function createIgnoreResult(filePath, baseDir) {
             }
         ],
         errorCount: 0,
-        warningCount: 1
+        warningCount: 1,
+        fixableErrorCount: 0,
+        fixableWarningCount: 0
     };
 }
 
@@ -495,7 +513,9 @@ class CLIEngine {
                     Object.assign(result, {
                         messages: filteredMessages,
                         errorCount: filteredMessages.length,
-                        warningCount: 0
+                        warningCount: 0,
+                        fixableErrorCount: result.fixableErrorCount,
+                        fixableWarningCount: 0
                     })
                 );
             }
@@ -681,7 +701,9 @@ class CLIEngine {
         return {
             results,
             errorCount: stats.errorCount,
-            warningCount: stats.warningCount
+            warningCount: stats.warningCount,
+            fixableErrorCount: stats.fixableErrorCount,
+            fixableWarningCount: stats.fixableWarningCount
         };
     }
 
@@ -717,7 +739,9 @@ class CLIEngine {
         return {
             results,
             errorCount: stats.errorCount,
-            warningCount: stats.warningCount
+            warningCount: stats.warningCount,
+            fixableErrorCount: stats.fixableErrorCount,
+            fixableWarningCount: stats.fixableWarningCount
         };
     }
 
diff --git a/lib/formatters/codeframe.js b/lib/formatters/codeframe.js
index 1191ccb8e..ed50ae608 100644
--- a/lib/formatters/codeframe.js
+++ b/lib/formatters/codeframe.js
@@ -74,11 +74,14 @@ function formatMessage(message, parentResult) {
  * Gets the formatted output summary for a given number of errors and warnings.
  * @param   {number} errors   The number of errors.
  * @param   {number} warnings The number of warnings.
+ * @param   {number} fixableErrors The number of fixable errors.
+ * @param   {number} fixableWarnings The number of fixable warnings.
  * @returns {string}          The formatted output summary.
  */
-function formatSummary(errors, warnings) {
+function formatSummary(errors, warnings, fixableErrors, fixableWarnings) {
     const summaryColor = errors > 0 ? "red" : "yellow";
     const summary = [];
+    const fixablesSummary = [];
 
     if (errors > 0) {
         summary.push(`${errors} ${pluralize("error", errors)}`);
@@ -88,7 +91,21 @@ function formatSummary(errors, warnings) {
         summary.push(`${warnings} ${pluralize("warning", warnings)}`);
     }
 
-    return chalk[summaryColor].bold(`${summary.join(" and ")} found.`);
+    if (fixableErrors > 0) {
+        fixablesSummary.push(`${fixableErrors} ${pluralize("error", fixableErrors)}`);
+    }
+
+    if (fixableWarnings > 0) {
+        fixablesSummary.push(`${fixableWarnings} ${pluralize("warning", fixableWarnings)}`);
+    }
+
+    let output = chalk[summaryColor].bold(`${summary.join(" and ")} found.`);
+
+    if (fixableErrors || fixableWarnings) {
+        output += chalk[summaryColor].bold(`\n${fixablesSummary.join(" and ")} potentially fixable with the \`--fix\` option.`);
+    }
+
+    return output;
 }
 
 //------------------------------------------------------------------------------
@@ -98,6 +115,9 @@ function formatSummary(errors, warnings) {
 module.exports = function(results) {
     let errors = 0;
     let warnings = 0;
+    let fixableErrors = 0;
+    let fixableWarnings = 0;
+
     const resultsWithMessages = results.filter(result => result.messages.length > 0);
 
     let output = resultsWithMessages.reduce((resultsOutput, result) => {
@@ -105,12 +125,14 @@ module.exports = function(results) {
 
         errors += result.errorCount;
         warnings += result.warningCount;
+        fixableErrors += result.fixableErrorCount;
+        fixableWarnings += result.fixableWarningCount;
 
         return resultsOutput.concat(messages);
     }, []).join("\n");
 
     output += "\n";
-    output += formatSummary(errors, warnings);
+    output += formatSummary(errors, warnings, fixableErrors, fixableWarnings);
 
     return (errors + warnings) > 0 ? output : "";
 };
diff --git a/lib/formatters/stylish.js b/lib/formatters/stylish.js
index c19b95d6e..4ec97a31a 100644
--- a/lib/formatters/stylish.js
+++ b/lib/formatters/stylish.js
@@ -28,8 +28,10 @@ function pluralize(word, count) {
 module.exports = function(results) {
 
     let output = "\n",
-        errors = 0,
-        warnings = 0,
+        errorCount = 0,
+        warningCount = 0,
+        fixableErrorCount = 0,
+        fixableWarningCount = 0,
         summaryColor = "yellow";
 
     results.forEach(result => {
@@ -39,8 +41,10 @@ module.exports = function(results) {
             return;
         }
 
-        errors += result.errorCount;
-        warnings += result.warningCount;
+        errorCount += result.errorCount;
+        warningCount += result.warningCount;
+        fixableErrorCount += result.fixableErrorCount;
+        fixableWarningCount += result.fixableWarningCount;
 
         output += `${chalk.underline(result.filePath)}\n`;
 
@@ -73,14 +77,22 @@ module.exports = function(results) {
         ).split("\n").map(el => el.replace(/(\d+)\s+(\d+)/, (m, p1, p2) => chalk.dim(`${p1}:${p2}`))).join("\n")}\n\n`;
     });
 
-    const total = errors + warnings;
+    const total = errorCount + warningCount;
 
     if (total > 0) {
         output += chalk[summaryColor].bold([
             "\u2716 ", total, pluralize(" problem", total),
-            " (", errors, pluralize(" error", errors), ", ",
-            warnings, pluralize(" warning", warnings), ")\n"
+            " (", errorCount, pluralize(" error", errorCount), ", ",
+            warningCount, pluralize(" warning", warningCount), ")\n"
         ].join(""));
+
+        if (fixableErrorCount > 0 || fixableWarningCount > 0) {
+            output += chalk[summaryColor].bold([
+                "  ", fixableErrorCount, pluralize(" error", fixableErrorCount), ", ",
+                fixableWarningCount, pluralize(" warning", fixableWarningCount),
+                " potentially fixable with the `--fix` option.\n"
+            ].join(""));
+        }
     }
 
     return total > 0 ? output : "";


Test patch:
diff --git a/tests/fixtures/configurations/single-quotes-error.json b/tests/fixtures/configurations/single-quotes-error.json
new file mode 100644
index 000000000..45e5d69d6
--- /dev/null
+++ b/tests/fixtures/configurations/single-quotes-error.json
@@ -0,0 +1,5 @@
+{
+    "rules": {
+        "quotes": [2, "single"]
+    }
+}
diff --git a/tests/lib/cli-engine.js b/tests/lib/cli-engine.js
index 90a490fb5..b89d17c20 100644
--- a/tests/lib/cli-engine.js
+++ b/tests/lib/cli-engine.js
@@ -103,7 +103,7 @@ describe("CLIEngine", () => {
 
         let engine;
 
-        it("should report 5 message when using local cwd .eslintrc", () => {
+        it("should report the total and per file errors when using local cwd .eslintrc", () => {
 
             engine = new CLIEngine();
 
@@ -112,12 +112,45 @@ describe("CLIEngine", () => {
             assert.equal(report.results.length, 1);
             assert.equal(report.errorCount, 5);
             assert.equal(report.warningCount, 0);
+            assert.equal(report.fixableErrorCount, 3);
+            assert.equal(report.fixableWarningCount, 0);
             assert.equal(report.results[0].messages.length, 5);
             assert.equal(report.results[0].messages[0].ruleId, "strict");
             assert.equal(report.results[0].messages[1].ruleId, "no-var");
             assert.equal(report.results[0].messages[2].ruleId, "no-unused-vars");
             assert.equal(report.results[0].messages[3].ruleId, "quotes");
             assert.equal(report.results[0].messages[4].ruleId, "eol-last");
+            assert.equal(report.results[0].fixableErrorCount, 3);
+            assert.equal(report.results[0].fixableWarningCount, 0);
+        });
+
+        it("should report the toatl and per file warnings when using local cwd .eslintrc", () => {
+
+            engine = new CLIEngine({
+                rules: {
+                    quotes: 1,
+                    "no-var": 1,
+                    "eol-last": 1,
+                    strict: 1,
+                    "no-unused-vars": 1
+                }
+            });
+
+            const report = engine.executeOnText("var foo = 'bar';");
+
+            assert.equal(report.results.length, 1);
+            assert.equal(report.errorCount, 0);
+            assert.equal(report.warningCount, 5);
+            assert.equal(report.fixableErrorCount, 0);
+            assert.equal(report.fixableWarningCount, 3);
+            assert.equal(report.results[0].messages.length, 5);
+            assert.equal(report.results[0].messages[0].ruleId, "strict");
+            assert.equal(report.results[0].messages[1].ruleId, "no-var");
+            assert.equal(report.results[0].messages[2].ruleId, "no-unused-vars");
+            assert.equal(report.results[0].messages[3].ruleId, "quotes");
+            assert.equal(report.results[0].messages[4].ruleId, "eol-last");
+            assert.equal(report.results[0].fixableErrorCount, 0);
+            assert.equal(report.results[0].fixableWarningCount, 3);
         });
 
         it("should report one message when using specific config file", () => {
@@ -133,10 +166,13 @@ describe("CLIEngine", () => {
             assert.equal(report.results.length, 1);
             assert.equal(report.errorCount, 1);
             assert.equal(report.warningCount, 0);
+            assert.equal(report.fixableErrorCount, 1);
+            assert.equal(report.fixableWarningCount, 0);
             assert.equal(report.results[0].messages.length, 1);
             assert.equal(report.results[0].messages[0].ruleId, "quotes");
             assert.isUndefined(report.results[0].messages[0].output);
             assert.equal(report.results[0].errorCount, 1);
+            assert.equal(report.results[0].fixableErrorCount, 1);
             assert.equal(report.results[0].warningCount, 0);
         });
 
@@ -163,12 +199,16 @@ describe("CLIEngine", () => {
             assert.equal(report.results.length, 1);
             assert.equal(report.errorCount, 0);
             assert.equal(report.warningCount, 1);
+            assert.equal(report.fixableErrorCount, 0);
+            assert.equal(report.fixableWarningCount, 0);
             assert.equal(report.results[0].filePath, getFixturePath("passing.js"));
             assert.equal(report.results[0].messages[0].severity, 1);
             assert.equal(report.results[0].messages[0].message, "File ignored because of a matching ignore pattern. Use \"--no-ignore\" to override.");
             assert.isUndefined(report.results[0].messages[0].output);
             assert.equal(report.results[0].errorCount, 0);
             assert.equal(report.results[0].warningCount, 1);
+            assert.equal(report.results[0].fixableErrorCount, 0);
+            assert.equal(report.results[0].fixableWarningCount, 0);
         });
 
         it("should not return a warning when given a filename by --stdin-filename in excluded files list if warnIgnored is false", () => {
@@ -238,11 +278,15 @@ describe("CLIEngine", () => {
                         messages: [],
                         errorCount: 0,
                         warningCount: 0,
+                        fixableErrorCount: 0,
+                        fixableWarningCount: 0,
                         output: "var bar = foo;"
                     }
                 ],
                 errorCount: 0,
-                warningCount: 0
+                warningCount: 0,
+                fixableErrorCount: 0,
+                fixableWarningCount: 0
             });
         });
 
@@ -305,11 +349,15 @@ describe("CLIEngine", () => {
                         ],
                         errorCount: 1,
                         warningCount: 0,
+                        fixableErrorCount: 0,
+                        fixableWarningCount: 0,
                         source: "var bar = foo"
                     }
                 ],
                 errorCount: 1,
-                warningCount: 0
+                warningCount: 0,
+                fixableErrorCount: 0,
+                fixableWarningCount: 0
             });
         });
 
@@ -343,11 +391,15 @@ describe("CLIEngine", () => {
                         ],
                         errorCount: 1,
                         warningCount: 0,
+                        fixableErrorCount: 0,
+                        fixableWarningCount: 0,
                         output: "var bar = foothis is a syntax error."
                     }
                 ],
                 errorCount: 1,
-                warningCount: 0
+                warningCount: 0,
+                fixableErrorCount: 0,
+                fixableWarningCount: 0
             });
         });
 
@@ -381,11 +433,15 @@ describe("CLIEngine", () => {
                         ],
                         errorCount: 1,
                         warningCount: 0,
+                        fixableErrorCount: 0,
+                        fixableWarningCount: 0,
                         source: "var bar ="
                     }
                 ],
                 errorCount: 1,
-                warningCount: 0
+                warningCount: 0,
+                fixableErrorCount: 0,
+                fixableWarningCount: 0
             });
         });
 
@@ -465,11 +521,15 @@ describe("CLIEngine", () => {
                         ],
                         errorCount: 1,
                         warningCount: 0,
+                        fixableErrorCount: 0,
+                        fixableWarningCount: 0,
                         source: "var bar = foothis is a syntax error.\n return bar;"
                     }
                 ],
                 errorCount: 1,
-                warningCount: 0
+                warningCount: 0,
+                fixableErrorCount: 0,
+                fixableWarningCount: 0
             });
         });
 
@@ -636,6 +696,8 @@ describe("CLIEngine", () => {
             assert.equal(report.results.length, 1);
             assert.equal(report.results[0].errorCount, 0);
             assert.equal(report.results[0].warningCount, 1);
+            assert.equal(report.results[0].fixableErrorCount, 0);
+            assert.equal(report.results[0].fixableWarningCount, 0);
             assert.equal(report.results[0].messages[0].message, expectedMsg);
         });
 
@@ -653,6 +715,8 @@ describe("CLIEngine", () => {
             assert.equal(report.results.length, 1);
             assert.equal(report.results[0].errorCount, 1);
             assert.equal(report.results[0].warningCount, 0);
+            assert.equal(report.results[0].fixableErrorCount, 1);
+            assert.equal(report.results[0].fixableWarningCount, 0);
         });
 
         it("should not check default ignored files without --no-ignore flag", () => {
@@ -695,6 +759,8 @@ describe("CLIEngine", () => {
             assert.equal(report.results.length, 1);
             assert.equal(report.results[0].errorCount, 0);
             assert.equal(report.results[0].warningCount, 1);
+            assert.equal(report.results[0].fixableErrorCount, 0);
+            assert.equal(report.results[0].fixableWarningCount, 0);
             assert.equal(report.results[0].messages[0].message, expectedMsg);
         });
 
@@ -714,6 +780,8 @@ describe("CLIEngine", () => {
             assert.equal(report.results.length, 1);
             assert.equal(report.results[0].warningCount, 0);
             assert.equal(report.results[0].errorCount, 1);
+            assert.equal(report.results[0].fixableErrorCount, 1);
+            assert.equal(report.results[0].fixableWarningCount, 0);
             assert.equal(report.results[0].messages[0].ruleId, "quotes");
         });
 
@@ -734,6 +802,8 @@ describe("CLIEngine", () => {
             assert.equal(report.results.length, 1);
             assert.equal(report.results[0].warningCount, 0);
             assert.equal(report.results[0].errorCount, 1);
+            assert.equal(report.results[0].fixableErrorCount, 1);
+            assert.equal(report.results[0].fixableWarningCount, 0);
             assert.equal(report.results[0].messages[0].ruleId, "quotes");
         });
 
@@ -764,13 +834,17 @@ describe("CLIEngine", () => {
             assert.equal(report.results[0].messages.length, 1);
             assert.equal(report.errorCount, 1);
             assert.equal(report.warningCount, 0);
+            assert.equal(report.fixableErrorCount, 1);
+            assert.equal(report.fixableWarningCount, 0);
             assert.equal(report.results[0].messages[0].ruleId, "quotes");
             assert.equal(report.results[0].messages[0].severity, 2);
             assert.equal(report.results[0].errorCount, 1);
             assert.equal(report.results[0].warningCount, 0);
+            assert.equal(report.results[0].fixableErrorCount, 1);
+            assert.equal(report.results[0].fixableWarningCount, 0);
         });
 
-        it("should return two messages when given a config file and a directory of files", () => {
+        it("should return 3 messages when given a config file and a directory of 3 valid files", () => {
 
             engine = new CLIEngine({
                 cwd: path.join(fixtureDir, ".."),
@@ -782,15 +856,51 @@ describe("CLIEngine", () => {
             assert.equal(report.results.length, 3);
             assert.equal(report.errorCount, 0);
             assert.equal(report.warningCount, 0);
+            assert.equal(report.fixableErrorCount, 0);
+            assert.equal(report.fixableWarningCount, 0);
             assert.equal(report.results[0].messages.length, 0);
             assert.equal(report.results[1].messages.length, 0);
             assert.equal(report.results[2].messages.length, 0);
             assert.equal(report.results[0].errorCount, 0);
             assert.equal(report.results[0].warningCount, 0);
+            assert.equal(report.results[0].fixableErrorCount, 0);
+            assert.equal(report.results[0].fixableWarningCount, 0);
             assert.equal(report.results[1].errorCount, 0);
             assert.equal(report.results[1].warningCount, 0);
+            assert.equal(report.results[1].fixableErrorCount, 0);
+            assert.equal(report.results[1].fixableWarningCount, 0);
             assert.equal(report.results[2].errorCount, 0);
             assert.equal(report.results[2].warningCount, 0);
+            assert.equal(report.results[2].fixableErrorCount, 0);
+            assert.equal(report.results[2].fixableWarningCount, 0);
+        });
+
+
+        it("should return the total number of errors when given multiple files", () => {
+
+            engine = new CLIEngine({
+                cwd: path.join(fixtureDir, ".."),
+                configFile: getFixturePath("configurations", "single-quotes-error.json")
+            });
+
+            const report = engine.executeOnFiles([getFixturePath("formatters")]);
+
+            assert.equal(report.errorCount, 6);
+            assert.equal(report.warningCount, 0);
+            assert.equal(report.fixableErrorCount, 6);
+            assert.equal(report.fixableWarningCount, 0);
+            assert.equal(report.results[0].errorCount, 0);
+            assert.equal(report.results[0].warningCount, 0);
+            assert.equal(report.results[0].fixableErrorCount, 0);
+            assert.equal(report.results[0].fixableWarningCount, 0);
+            assert.equal(report.results[1].errorCount, 3);
+            assert.equal(report.results[1].warningCount, 0);
+            assert.equal(report.results[1].fixableErrorCount, 3);
+            assert.equal(report.results[1].fixableWarningCount, 0);
+            assert.equal(report.results[2].errorCount, 3);
+            assert.equal(report.results[2].warningCount, 0);
+            assert.equal(report.results[2].fixableErrorCount, 3);
+            assert.equal(report.results[2].fixableWarningCount, 0);
         });
 
         it("should process when file is given by not specifying extensions", () => {
@@ -924,6 +1034,8 @@ describe("CLIEngine", () => {
             assert.equal(report.results.length, 1);
             assert.equal(report.results[0].errorCount, 0);
             assert.equal(report.results[0].warningCount, 0);
+            assert.equal(report.results[0].fixableErrorCount, 0);
+            assert.equal(report.results[0].fixableWarningCount, 0);
         });
 
         // https://github.com/eslint/eslint/issues/3812
@@ -964,11 +1076,15 @@ describe("CLIEngine", () => {
             assert.equal(report.results.length, 1);
             assert.equal(report.errorCount, 0);
             assert.equal(report.warningCount, 1);
+            assert.equal(report.fixableErrorCount, 0);
+            assert.equal(report.fixableWarningCount, 0);
             assert.equal(report.results[0].filePath, filePath);
             assert.equal(report.results[0].messages[0].severity, 1);
             assert.equal(report.results[0].messages[0].message, "File ignored because of a matching ignore pattern. Use \"--no-ignore\" to override.");
             assert.equal(report.results[0].errorCount, 0);
             assert.equal(report.results[0].warningCount, 1);
+            assert.equal(report.results[0].fixableErrorCount, 0);
+            assert.equal(report.results[0].fixableWarningCount, 0);
         });
 
         it("should return two messages when given a file in excluded files list while ignore is off", () => {
@@ -1246,13 +1362,17 @@ describe("CLIEngine", () => {
                             messages: [],
                             errorCount: 0,
                             warningCount: 0,
+                            fixableErrorCount: 0,
+                            fixableWarningCount: 0,
                             output: "true ? \"yes\" : \"no\";\n"
                         },
                         {
                             filePath: fs.realpathSync(path.resolve(fixtureDir, "fixmode/ok.js")),
                             messages: [],
                             errorCount: 0,
-                            warningCount: 0
+                            warningCount: 0,
+                            fixableErrorCount: 0,
+                            fixableWarningCount: 0
                         },
                         {
                             filePath: fs.realpathSync(path.resolve(fixtureDir, "fixmode/quotes-semi-eqeqeq.js")),
@@ -1269,6 +1389,8 @@ describe("CLIEngine", () => {
                             ],
                             errorCount: 1,
                             warningCount: 0,
+                            fixableErrorCount: 0,
+                            fixableWarningCount: 0,
                             output: "var msg = \"hi\";\nif (msg == \"hi\") {\n\n}\n"
                         },
                         {
@@ -1286,11 +1408,15 @@ describe("CLIEngine", () => {
                             ],
                             errorCount: 1,
                             warningCount: 0,
+                            fixableErrorCount: 0,
+                            fixableWarningCount: 0,
                             output: "var msg = \"hi\" + foo;\n"
                         }
                     ],
                     errorCount: 2,
-                    warningCount: 0
+                    warningCount: 0,
+                    fixableErrorCount: 0,
+                    fixableWarningCount: 0
                 });
             });
 
@@ -2513,6 +2639,8 @@ describe("CLIEngine", () => {
 
             assert.lengthOf(errorResults[0].messages, 5);
             assert.equal(errorResults[0].errorCount, 5);
+            assert.equal(errorResults[0].fixableErrorCount, 3);
+            assert.equal(errorResults[0].fixableWarningCount, 0);
             assert.equal(errorResults[0].messages[0].ruleId, "strict");
             assert.equal(errorResults[0].messages[0].severity, 2);
             assert.equal(errorResults[0].messages[1].ruleId, "no-var");
@@ -2534,6 +2662,7 @@ describe("CLIEngine", () => {
             const errorResults = CLIEngine.getErrorResults(report.results);
 
             assert.equal(errorResults[0].warningCount, 0);
+            assert.equal(errorResults[0].fixableWarningCount, 0);
         });
 
         it("should return 0 error or warning messages even when the file has warnings", () => {
@@ -2549,8 +2678,12 @@ describe("CLIEngine", () => {
             assert.lengthOf(report.results, 1);
             assert.equal(report.errorCount, 0);
             assert.equal(report.warningCount, 1);
+            assert.equal(report.fixableErrorCount, 0);
+            assert.equal(report.fixableWarningCount, 0);
             assert.equal(report.results[0].errorCount, 0);
             assert.equal(report.results[0].warningCount, 1);
+            assert.equal(report.fixableErrorCount, 0);
+            assert.equal(report.fixableWarningCount, 0);
         });
 
         it("should return source code of file in the `source` property", () => {
diff --git a/tests/lib/formatters/codeframe.js b/tests/lib/formatters/codeframe.js
index fffaea2ee..c059c357b 100644
--- a/tests/lib/formatters/codeframe.js
+++ b/tests/lib/formatters/codeframe.js
@@ -78,7 +78,9 @@ describe("formatter:codeframe", () => {
                 ruleId: "foo"
             }],
             errorCount: 0,
-            warningCount: 1
+            warningCount: 1,
+            fixableErrorCount: 0,
+            fixableWarningCount: 0
         }];
 
         it("should return a string in the correct format for warnings", () => {
@@ -104,6 +106,27 @@ describe("formatter:codeframe", () => {
             assert.equal(chalkStub.yellow.bold.callCount, 1);
             assert.equal(chalkStub.red.bold.callCount, 0);
         });
+
+        describe("when the warning is fixable", () => {
+            beforeEach(() => {
+                code[0].fixableWarningCount = 1;
+            });
+
+            it("should return a string in the correct format", () => {
+                const result = formatter(code);
+
+                assert.equal(chalk.stripColor(result), [
+                    `warning: Unexpected foo (foo) at ${path.join("lib", "foo.js")}:1:5:`,
+                    "> 1 | var foo = 1;",
+                    "    |     ^",
+                    "  2 |  var bar = 2;",
+                    "  3 | ",
+                    "\n",
+                    "1 warning found.",
+                    "1 warning potentially fixable with the `--fix` option."
+                ].join("\n"));
+            });
+        });
     });
 
     describe("when passed a single error message", () => {
@@ -353,4 +376,93 @@ describe("formatter:codeframe", () => {
             assert.equal(chalk.stripColor(result), "error: Unexpected foo (foo) at foo.js\n\n\n1 error found.");
         });
     });
+
+
+    describe("fixable problems", () => {
+        it("should not output fixable problems message when no errors or warnings are fixable", () => {
+            const code = [{
+                filePath: "foo.js",
+                errorCount: 1,
+                warningCount: 0,
+                fixableErrorCount: 0,
+                fixableWarningCount: 0,
+                messages: [{
+                    message: "Unexpected foo.",
+                    severity: 2,
+                    line: 5,
+                    column: 10,
+                    ruleId: "foo"
+                }]
+            }];
+
+            const result = formatter(code);
+
+            assert.notInclude(result, "potentially fixable");
+        });
+
+        it("should output the fixable problems message when errors are fixable", () => {
+            const code = [{
+                filePath: "foo.js",
+                errorCount: 1,
+                warningCount: 0,
+                fixableErrorCount: 1,
+                fixableWarningCount: 0,
+                messages: [{
+                    message: "Unexpected foo.",
+                    severity: 2,
+                    line: 5,
+                    column: 10,
+                    ruleId: "foo"
+                }]
+            }];
+
+            const result = formatter(code);
+
+            assert.include(result, "1 error potentially fixable with the `--fix` option.");
+        });
+
+        it("should output fixable problems message when warnings are fixable", () => {
+            const code = [{
+                filePath: "foo.js",
+                errorCount: 0,
+                warningCount: 3,
+                fixableErrorCount: 0,
+                fixableWarningCount: 2,
+                messages: [{
+                    message: "Unexpected foo."
+                }]
+            }];
+
+            const result = formatter(code);
+
+            assert.include(result, "2 warnings potentially fixable with the `--fix` option.");
+        });
+
+        it("should output the total number of fixable errors and warnings", () => {
+            const code = [{
+                filePath: "foo.js",
+                errorCount: 5,
+                warningCount: 3,
+                fixableErrorCount: 5,
+                fixableWarningCount: 2,
+                messages: [{
+                    message: "Unexpected foo."
+                }]
+            }, {
+                filePath: "bar.js",
+                errorCount: 4,
+                warningCount: 2,
+                fixableErrorCount: 4,
+                fixableWarningCount: 1,
+                messages: [{
+                    message: "Unexpected bar."
+                }]
+            }];
+
+            const result = formatter(code);
+
+            assert.include(result, "9 errors and 3 warnings potentially fixable with the `--fix` option.");
+        });
+    });
+
 });
diff --git a/tests/lib/formatters/stylish.js b/tests/lib/formatters/stylish.js
index 958693001..67a653dc3 100644
--- a/tests/lib/formatters/stylish.js
+++ b/tests/lib/formatters/stylish.js
@@ -73,11 +73,13 @@ describe("formatter:stylish", () => {
         });
     });
 
-    describe("when passed a single message", () => {
+    describe("when passed a single error message", () => {
         const code = [{
             filePath: "foo.js",
             errorCount: 1,
             warningCount: 0,
+            fixableErrorCount: 0,
+            fixableWarningCount: 0,
             messages: [{
                 message: "Unexpected foo.",
                 severity: 2,
@@ -87,7 +89,7 @@ describe("formatter:stylish", () => {
             }]
         }];
 
-        it("should return a string in the correct format for errors", () => {
+        it("should return a string in the correct format", () => {
             const result = formatter(code);
 
             assert.equal(result, "\nfoo.js\n  5:10  error  Unexpected foo  foo\n\n\u2716 1 problem (1 error, 0 warnings)\n");
@@ -95,17 +97,59 @@ describe("formatter:stylish", () => {
             assert.equal(chalkStub.red.bold.callCount, 1);
         });
 
-        it("should return a string in the correct format for warnings", () => {
-            code[0].messages[0].severity = 1;
-            code[0].errorCount = 0;
-            code[0].warningCount = 1;
+        describe("when the error is fixable", () => {
+            beforeEach(() => {
+                code[0].fixableErrorCount = 1;
+            });
+
+            it("should return a string in the correct format", () => {
+                const result = formatter(code);
+
+                assert.equal(result, "\nfoo.js\n  5:10  error  Unexpected foo  foo\n\n\u2716 1 problem (1 error, 0 warnings)\n  1 error, 0 warnings potentially fixable with the `--fix` option.\n");
+                assert.equal(chalkStub.yellow.bold.callCount, 0);
+                assert.equal(chalkStub.red.bold.callCount, 2);
+            });
+        });
+    });
 
+    describe("when passed a single warning message", () => {
+        const code = [{
+            filePath: "foo.js",
+            errorCount: 0,
+            warningCount: 1,
+            fixableErrorCount: 0,
+            fixableWarningCount: 0,
+            messages: [{
+                message: "Unexpected foo.",
+                severity: 1,
+                line: 5,
+                column: 10,
+                ruleId: "foo"
+            }]
+        }];
+
+        it("should return a string in the correct format", () => {
             const result = formatter(code);
 
             assert.equal(result, "\nfoo.js\n  5:10  warning  Unexpected foo  foo\n\n\u2716 1 problem (0 errors, 1 warning)\n");
             assert.equal(chalkStub.yellow.bold.callCount, 1);
             assert.equal(chalkStub.red.bold.callCount, 0);
         });
+
+        describe("when the error is fixable", () => {
+            beforeEach(() => {
+                code[0].fixableWarningCount = 1;
+            });
+
+            it("should return a string in the correct format", () => {
+                const result = formatter(code);
+
+                assert.equal(result, "\nfoo.js\n  5:10  warning  Unexpected foo  foo\n\n\u2716 1 problem (0 errors, 1 warning)\n  0 errors, 1 warning potentially fixable with the `--fix` option.\n");
+                assert.equal(chalkStub.yellow.bold.callCount, 2);
+                assert.equal(chalkStub.red.bold.callCount, 0);
+            });
+
+        });
     });
 
     describe("when passed a fatal error message", () => {
@@ -239,4 +283,91 @@ describe("formatter:stylish", () => {
             assert.equal(chalkStub.red.bold.callCount, 1);
         });
     });
+
+    describe("fixable problems", () => {
+        it("should not output fixable problems message when no errors or warnings are fixable", () => {
+            const code = [{
+                filePath: "foo.js",
+                errorCount: 1,
+                warningCount: 0,
+                fixableErrorCount: 0,
+                fixableWarningCount: 0,
+                messages: [{
+                    message: "Unexpected foo.",
+                    severity: 2,
+                    line: 5,
+                    column: 10,
+                    ruleId: "foo"
+                }]
+            }];
+
+            const result = formatter(code);
+
+            assert.notInclude(result, "potentially fixable");
+        });
+
+        it("should output the fixable problems message when errors are fixable", () => {
+            const code = [{
+                filePath: "foo.js",
+                errorCount: 1,
+                warningCount: 0,
+                fixableErrorCount: 1,
+                fixableWarningCount: 0,
+                messages: [{
+                    message: "Unexpected foo.",
+                    severity: 2,
+                    line: 5,
+                    column: 10,
+                    ruleId: "foo"
+                }]
+            }];
+
+            const result = formatter(code);
+
+            assert.include(result, "  1 error, 0 warnings potentially fixable with the `--fix` option.\n");
+        });
+
+        it("should output fixable problems message when warnings are fixable", () => {
+            const code = [{
+                filePath: "foo.js",
+                errorCount: 0,
+                warningCount: 3,
+                fixableErrorCount: 0,
+                fixableWarningCount: 2,
+                messages: [{
+                    message: "Unexpected foo."
+                }]
+            }];
+
+            const result = formatter(code);
+
+            assert.include(result, "  0 errors, 2 warnings potentially fixable with the `--fix` option.\n");
+        });
+
+        it("should output the total number of fixable errors and warnings", () => {
+            const code = [{
+                filePath: "foo.js",
+                errorCount: 5,
+                warningCount: 3,
+                fixableErrorCount: 5,
+                fixableWarningCount: 2,
+                messages: [{
+                    message: "Unexpected foo."
+                }]
+            }, {
+                filePath: "bar.js",
+                errorCount: 4,
+                warningCount: 2,
+                fixableErrorCount: 4,
+                fixableWarningCount: 1,
+                messages: [{
+                    message: "Unexpected bar."
+                }]
+            }];
+
+            const result = formatter(code);
+
+            assert.include(result, "  9 errors, 3 warnings potentially fixable with the `--fix` option.\n");
+        });
+    });
 });