Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New: Exit on fatal error (fixes #13711) #14730

Merged
merged 10 commits into from Jul 30, 2021
Merged
5 changes: 5 additions & 0 deletions docs/user-guide/command-line-interface.md
Expand Up @@ -81,6 +81,7 @@ Miscellaneous:
--init Run config initialization wizard - default: false
--env-info Output execution environment information - default: false
--no-error-on-unmatched-pattern Prevent errors when pattern is unmatched - default: false
--exit-on-fatal-error Exit with exit code 2 in case of fatal error. - default: false
mdjermanovic marked this conversation as resolved.
Show resolved Hide resolved
A-Katopodis marked this conversation as resolved.
Show resolved Hide resolved
A-Katopodis marked this conversation as resolved.
Show resolved Hide resolved
--debug Output debugging information
-h, --help Show help
-v, --version Output the version number
Expand Down Expand Up @@ -467,6 +468,10 @@ This option outputs information about the execution environment, including the v

This option prevents errors when a quoted glob pattern or `--ext` is unmatched. This will not prevent errors when your shell can't match a glob.

#### `--exit-on-fatal-error`

This option makes ESLint to exit with exit code 2 in case of a fatal error. It allows you to detect if any file was not parsed correctly compared to having rule violations.
A-Katopodis marked this conversation as resolved.
Show resolved Hide resolved

#### `--debug`

This option outputs debugging information to the console. This information is useful when you're seeing a problem and having a hard time pinpointing it. The ESLint team may ask for this debugging information to help solve bugs.
Expand Down
6 changes: 6 additions & 0 deletions lib/cli-engine/cli-engine.js
Expand Up @@ -156,6 +156,9 @@ function calculateStatsPerFile(messages) {
return messages.reduce((stat, message) => {
if (message.fatal || message.severity === 2) {
stat.errorCount++;
if (message.fatal) {
stat.fatalErrorCount++;
}
if (message.fix) {
stat.fixableErrorCount++;
}
Expand All @@ -168,6 +171,7 @@ function calculateStatsPerFile(messages) {
return stat;
}, {
errorCount: 0,
fatalErrorCount: 0,
A-Katopodis marked this conversation as resolved.
Show resolved Hide resolved
warningCount: 0,
fixableErrorCount: 0,
fixableWarningCount: 0
Expand All @@ -183,12 +187,14 @@ function calculateStatsPerFile(messages) {
function calculateStatsPerRun(results) {
return results.reduce((stat, result) => {
stat.errorCount += result.errorCount;
stat.fatalErrorCount += result.fatalErrorCount;
stat.warningCount += result.warningCount;
stat.fixableErrorCount += result.fixableErrorCount;
stat.fixableWarningCount += result.fixableWarningCount;
return stat;
}, {
errorCount: 0,
fatalErrorCount: 0,
warningCount: 0,
fixableErrorCount: 0,
fixableWarningCount: 0
Expand Down
13 changes: 11 additions & 2 deletions lib/cli.js
Expand Up @@ -131,14 +131,16 @@ function translateOptions({
*/
function countErrors(results) {
let errorCount = 0;
let fatalErrorCount = 0;
let warningCount = 0;

for (const result of results) {
errorCount += result.errorCount;
fatalErrorCount += result.fatalErrorCount;
warningCount += result.warningCount;
}

return { errorCount, warningCount };
return { errorCount, fatalErrorCount, warningCount };
}

/**
Expand Down Expand Up @@ -314,9 +316,12 @@ const cli = {
if (await printResults(engine, resultsToPrint, options.format, options.outputFile)) {

// Errors and warnings from the original unfiltered results should determine the exit code
const { errorCount, warningCount } = countErrors(results);
const { errorCount, fatalErrorCount, warningCount } = countErrors(results);

const tooManyWarnings =
options.maxWarnings >= 0 && warningCount > options.maxWarnings;
const fatalErrorExists =
A-Katopodis marked this conversation as resolved.
Show resolved Hide resolved
options.exitOnFatalError && fatalErrorCount > 0;

if (!errorCount && tooManyWarnings) {
log.error(
Expand All @@ -325,6 +330,10 @@ const cli = {
);
}

if (fatalErrorExists) {
A-Katopodis marked this conversation as resolved.
Show resolved Hide resolved
return 2;
}

return (errorCount || tooManyWarnings) ? 1 : 0;
}

Expand Down
1 change: 1 addition & 0 deletions lib/eslint/eslint.js
Expand Up @@ -59,6 +59,7 @@ const { version } = require("../../package.json");
* @property {string} [resolvePluginsRelativeTo] The folder where plugins should be resolved from, defaulting to the CWD.
* @property {string[]} [rulePaths] An array of directories to load custom rules from.
* @property {boolean} [useEslintrc] False disables looking for .eslintrc.* files.
* @property {number} exitOnFatalError Number of fatal errors for the result.
A-Katopodis marked this conversation as resolved.
Show resolved Hide resolved
*/

/**
Expand Down
6 changes: 6 additions & 0 deletions lib/options.js
Expand Up @@ -204,6 +204,12 @@ module.exports = optionator({
default: "-1",
description: "Number of warnings to trigger nonzero exit code"
},
{
option: "exit-on-fatal-error",
type: "Boolean",
default: "false",
description: "Trigger exit code 2 on any fatal errors."
},
A-Katopodis marked this conversation as resolved.
Show resolved Hide resolved
{
heading: "Output"
},
Expand Down
9 changes: 9 additions & 0 deletions tests/bin/eslint.js
Expand Up @@ -89,6 +89,7 @@ describe("bin/eslint.js", () => {
filePath: "<text>",
messages: [],
errorCount: 0,
fatalErrorCount: 0,
warningCount: 0,
fixableErrorCount: 0,
fixableWarningCount: 0,
Expand Down Expand Up @@ -117,6 +118,14 @@ describe("bin/eslint.js", () => {
return assertExitCode(child, 1);
});

it("has exit code 2 if a syntax error is thrown when exit-on-fatal-error is true", () => {
const child = runESLint(["--stdin", "--no-eslintrc", "--exit-on-fatal-error"]);

child.stdin.write("This is not valid JS syntax.\n");
child.stdin.end();
return assertExitCode(child, 2);
});

it("has exit code 1 if a linting error occurs", () => {
const child = runESLint(["--stdin", "--no-eslintrc", "--rule", "semi:2"]);

Expand Down
1 change: 1 addition & 0 deletions tests/fixtures/exit-on-fatal-error/fatal-error.js
@@ -0,0 +1 @@
var foo 1
1 change: 1 addition & 0 deletions tests/fixtures/exit-on-fatal-error/no-fatal-error.js
@@ -0,0 +1 @@
var foo = 1;
29 changes: 25 additions & 4 deletions tests/lib/cli-engine/cli-engine.js
Expand Up @@ -144,6 +144,7 @@ describe("CLIEngine", () => {
assert.strictEqual(report.results.length, 1);
assert.strictEqual(report.errorCount, 5);
assert.strictEqual(report.warningCount, 0);
assert.strictEqual(report.fatalErrorCount, 0);
assert.strictEqual(report.fixableErrorCount, 3);
assert.strictEqual(report.fixableWarningCount, 0);
assert.strictEqual(report.results[0].messages.length, 5);
Expand Down Expand Up @@ -310,13 +311,15 @@ describe("CLIEngine", () => {
messages: [],
errorCount: 0,
warningCount: 0,
fatalErrorCount: 0,
fixableErrorCount: 0,
fixableWarningCount: 0,
output: "var bar = foo;"
}
],
errorCount: 0,
warningCount: 0,
fatalErrorCount: 0,
fixableErrorCount: 0,
fixableWarningCount: 0,
usedDeprecatedRules: []
Expand Down Expand Up @@ -519,13 +522,15 @@ describe("CLIEngine", () => {
],
errorCount: 1,
warningCount: 0,
fatalErrorCount: 0,
fixableErrorCount: 0,
fixableWarningCount: 0,
source: "var bar = foo"
}
],
errorCount: 1,
warningCount: 0,
fatalErrorCount: 0,
fixableErrorCount: 0,
fixableWarningCount: 0,
usedDeprecatedRules: []
Expand Down Expand Up @@ -562,13 +567,15 @@ describe("CLIEngine", () => {
],
errorCount: 1,
warningCount: 0,
fatalErrorCount: 1,
fixableErrorCount: 0,
fixableWarningCount: 0,
output: "var bar = foothis is a syntax error."
}
],
errorCount: 1,
warningCount: 0,
fatalErrorCount: 1,
fixableErrorCount: 0,
fixableWarningCount: 0,
usedDeprecatedRules: []
Expand Down Expand Up @@ -604,13 +611,15 @@ describe("CLIEngine", () => {
],
errorCount: 1,
warningCount: 0,
fatalErrorCount: 1,
fixableErrorCount: 0,
fixableWarningCount: 0,
source: "var bar ="
}
],
errorCount: 1,
warningCount: 0,
fatalErrorCount: 1,
fixableErrorCount: 0,
fixableWarningCount: 0,
usedDeprecatedRules: []
Expand Down Expand Up @@ -692,13 +701,15 @@ describe("CLIEngine", () => {
],
errorCount: 1,
warningCount: 0,
fatalErrorCount: 1,
fixableErrorCount: 0,
fixableWarningCount: 0,
source: "var bar = foothis is a syntax error.\n return bar;"
}
],
errorCount: 1,
warningCount: 0,
fatalErrorCount: 1,
fixableErrorCount: 0,
fixableWarningCount: 0,
usedDeprecatedRules: []
Expand Down Expand Up @@ -1684,6 +1695,7 @@ describe("CLIEngine", () => {
messages: [],
errorCount: 0,
warningCount: 0,
fatalErrorCount: 0,
fixableErrorCount: 0,
fixableWarningCount: 0,
output: "true ? \"yes\" : \"no\";\n"
Expand All @@ -1693,6 +1705,7 @@ describe("CLIEngine", () => {
messages: [],
errorCount: 0,
warningCount: 0,
fatalErrorCount: 0,
fixableErrorCount: 0,
fixableWarningCount: 0
},
Expand All @@ -1713,6 +1726,7 @@ describe("CLIEngine", () => {
],
errorCount: 1,
warningCount: 0,
fatalErrorCount: 0,
fixableErrorCount: 0,
fixableWarningCount: 0,
output: "var msg = \"hi\";\nif (msg == \"hi\") {\n\n}\n"
Expand All @@ -1734,6 +1748,7 @@ describe("CLIEngine", () => {
],
errorCount: 1,
warningCount: 0,
fatalErrorCount: 0,
fixableErrorCount: 0,
fixableWarningCount: 0,
output: "var msg = \"hi\" + foo;\n"
Expand Down Expand Up @@ -5077,13 +5092,15 @@ describe("CLIEngine", () => {
],
errorCount: 1,
warningCount: 0,
fatalErrorCount: 0,
fixableErrorCount: 0,
fixableWarningCount: 0,
source: "/* eslint-disable */"
}
],
errorCount: 1,
warningCount: 0,
fatalErrorCount: 0,
fixableErrorCount: 0,
fixableWarningCount: 0,
usedDeprecatedRules: []
Expand Down Expand Up @@ -6155,7 +6172,8 @@ describe("CLIEngine", () => {
}
],
source: "a == b",
warningCount: 0
warningCount: 0,
fatalErrorCount: 0
}
]);
});
Expand All @@ -6177,7 +6195,8 @@ describe("CLIEngine", () => {
fixableErrorCount: 0,
fixableWarningCount: 0,
messages: [],
warningCount: 0
warningCount: 0,
fatalErrorCount: 0
}
]);
});
Expand Down Expand Up @@ -6223,7 +6242,8 @@ describe("CLIEngine", () => {
fixableErrorCount: 0,
fixableWarningCount: 0,
messages: [],
warningCount: 0
warningCount: 0,
fatalErrorCount: 0
}
]);
});
Expand Down Expand Up @@ -6258,7 +6278,8 @@ describe("CLIEngine", () => {
}
],
source: "a == b",
warningCount: 0
warningCount: 0,
fatalErrorCount: 0
}
]);
});
Expand Down
23 changes: 23 additions & 0 deletions tests/lib/cli.js
Expand Up @@ -803,6 +803,29 @@ describe("cli", () => {
});
});

describe("when given the exit-on-fatal-error flag", () => {
it("should not change exit code if no fatal errors are reported", async () => {
const filePath = getFixturePath("exit-on-fatal-error/no-fatal-error.js");
A-Katopodis marked this conversation as resolved.
Show resolved Hide resolved
const exitCode = await cli.execute(`--no-ignore --exit-on-fatal-error ${filePath}`);

assert.strictEqual(exitCode, 0);
});

A-Katopodis marked this conversation as resolved.
Show resolved Hide resolved
it("should exit with exit code 2 fatal error is found", async () => {
A-Katopodis marked this conversation as resolved.
Show resolved Hide resolved
const filePath = getFixturePath("exit-on-fatal-error", "fatal-error.js");
const exitCode = await cli.execute(`--no-ignore --exit-on-fatal-error ${filePath}`);

assert.strictEqual(exitCode, 2);
});

it("should exit with exit code 2 fatal error is found in any file", async () => {
A-Katopodis marked this conversation as resolved.
Show resolved Hide resolved
const filePath = getFixturePath("exit-on-fatal-error");
const exitCode = await cli.execute(`--no-ignore --exit-on-fatal-error ${filePath}`);

assert.strictEqual(exitCode, 2);
});
});

describe("when passed --no-inline-config", () => {
let localCLI;

Expand Down