diff --git a/bin/eslint.js b/bin/eslint.js index 7094ac77bc4..5c7972cc086 100755 --- a/bin/eslint.js +++ b/bin/eslint.js @@ -92,6 +92,14 @@ function getErrorMessage(error) { return util.format("%o", error); } +/** + * Tracks error messages that are shown to the user so we only ever show the + * same message once. + * @type {Set} + */ + +const displayedErrors = new Set(); + /** * Catch and report unexpected error. * @param {any} error The thrown error object. @@ -101,14 +109,17 @@ function onFatalError(error) { process.exitCode = 2; const { version } = require("../package.json"); - const message = getErrorMessage(error); - - console.error(` + const message = ` Oops! Something went wrong! :( ESLint: ${version} -${message}`); +${getErrorMessage(error)}`; + + if (!displayedErrors.has(message)) { + console.error(message); + displayedErrors.add(message); + } } //------------------------------------------------------------------------------ diff --git a/tests/bin/eslint.js b/tests/bin/eslint.js index b09496202b0..dca8955d038 100644 --- a/tests/bin/eslint.js +++ b/tests/bin/eslint.js @@ -387,6 +387,36 @@ describe("bin/eslint.js", () => { return Promise.all([exitCodeAssertion, outputAssertion]); }); + // https://github.com/eslint/eslint/issues/17560 + describe("does not print duplicate errors in the event of a crash", () => { + + it("when there is an invalid config read from a config file", () => { + const config = path.join(__dirname, "../fixtures/bin/eslint.config-invalid.js"); + const child = runESLint(["--config", config, "conf", "tools"]); + const exitCodeAssertion = assertExitCode(child, 2); + const outputAssertion = getOutput(child).then(output => { + + // The error text should appear exactly once in stderr + assert.strictEqual(output.stderr.match(/A config object is using the "globals" key/gu).length, 1); + }); + + return Promise.all([exitCodeAssertion, outputAssertion]); + }); + + it("when there is an error in the next tick", () => { + const config = path.join(__dirname, "../fixtures/bin/eslint.config-tick-throws.js"); + const child = runESLint(["--config", config, "Makefile.js"]); + const exitCodeAssertion = assertExitCode(child, 2); + const outputAssertion = getOutput(child).then(output => { + + // The error text should appear exactly once in stderr + assert.strictEqual(output.stderr.match(/test_error_stack/gu).length, 1); + }); + + return Promise.all([exitCodeAssertion, outputAssertion]); + }); + }); + it("prints the error message pointing to line of code", () => { const invalidConfig = path.join(__dirname, "../fixtures/bin/eslint.config.js"); const child = runESLint(["--no-ignore", "-c", invalidConfig]); diff --git a/tests/fixtures/bin/eslint.config-invalid.js b/tests/fixtures/bin/eslint.config-invalid.js new file mode 100644 index 00000000000..6f68e178419 --- /dev/null +++ b/tests/fixtures/bin/eslint.config-invalid.js @@ -0,0 +1,3 @@ +module.exports = [{ + globals: {} +}]; diff --git a/tests/fixtures/bin/eslint.config-tick-throws.js b/tests/fixtures/bin/eslint.config-tick-throws.js new file mode 100644 index 00000000000..c72f86a840f --- /dev/null +++ b/tests/fixtures/bin/eslint.config-tick-throws.js @@ -0,0 +1,24 @@ +function throwError() { + const error = new Error(); + error.stack = "test_error_stack"; + throw error; +} + +module.exports = [{ + plugins: { + foo: { + rules: { + bar: { + create() { + process.nextTick(throwError); + process.nextTick(throwError); + return {}; + } + } + } + } + }, + rules: { + "foo/bar": "error" + } +}];