diff --git a/lib/rules/no-console.js b/lib/rules/no-console.js index f257098d38b..d20477c5d9a 100644 --- a/lib/rules/no-console.js +++ b/lib/rules/no-console.js @@ -43,8 +43,11 @@ module.exports = { } ], + hasSuggestions: true, + messages: { - unexpected: "Unexpected console statement." + unexpected: "Unexpected console statement.", + removeConsole: "Remove the console.{{ propertyName }}()." } }, @@ -94,6 +97,64 @@ module.exports = { ); } + /** + * Checks if removing the ExpressionStatement node will cause ASI to + * break. + * eg. + * foo() + * console.log(); + * [1, 2, 3].forEach(a => doSomething(a)) + * + * Removing the console.log(); statement should leave two statements, but + * here the two statements will become one because [ causes continuation after + * foo(). + * @param {ASTNode} node The ExpressionStatement node to check. + * @returns {boolean} `true` if ASI will break after removing the ExpressionStatement + * node. + */ + function maybeAsiHazard(node) { + const SAFE_TOKENS_BEFORE = /^[:;{]$/u; // One of :;{ + const UNSAFE_CHARS_AFTER = /^[-[(/+`]/u; // One of [(/+-` + + const tokenBefore = sourceCode.getTokenBefore(node); + const tokenAfter = sourceCode.getTokenAfter(node); + + return ( + Boolean(tokenAfter) && + UNSAFE_CHARS_AFTER.test(tokenAfter.value) && + tokenAfter.value !== "++" && + tokenAfter.value !== "--" && + Boolean(tokenBefore) && + !SAFE_TOKENS_BEFORE.test(tokenBefore.value) + ); + } + + /** + * Checks if the MemberExpression node's parent.parent.parent is a + * Program, BlockStatement, StaticBlock, or SwitchCase node. This check + * is necessary to avoid providing a suggestion that might cause a syntax error. + * + * eg. if (a) console.log(b), removing console.log() here will lead to a + * syntax error. + * if (a) { console.log(b) }, removing console.log() here is acceptable. + * + * Additionally, it checks if the callee of the CallExpression node is + * the node itself. + * + * eg. foo(console.log), cannot provide a suggestion here. + * @param {ASTNode} node The MemberExpression node to check. + * @returns {boolean} `true` if a suggestion can be provided for a node. + */ + function canProvideSuggestions(node) { + return ( + node.parent.type === "CallExpression" && + node.parent.callee === node && + node.parent.parent.type === "ExpressionStatement" && + astUtils.STATEMENT_LIST_PARENTS.has(node.parent.parent.parent.type) && + !maybeAsiHazard(node.parent.parent) + ); + } + /** * Reports the given reference as a violation. * @param {eslint-scope.Reference} reference The reference to report. @@ -102,10 +163,21 @@ module.exports = { function report(reference) { const node = reference.identifier.parent; + const propertyName = astUtils.getStaticPropertyName(node); + context.report({ node, loc: node.loc, - messageId: "unexpected" + messageId: "unexpected", + suggest: canProvideSuggestions(node) + ? [{ + messageId: "removeConsole", + data: { propertyName }, + fix(fixer) { + return fixer.remove(node.parent.parent); + } + }] + : [] }); } diff --git a/tests/lib/rules/no-console.js b/tests/lib/rules/no-console.js index d1c9176d0ff..d55cf5c2d58 100644 --- a/tests/lib/rules/no-console.js +++ b/tests/lib/rules/no-console.js @@ -40,24 +40,388 @@ ruleTester.run("no-console", rule, { invalid: [ // no options - { code: "console.log(foo)", errors: [{ messageId: "unexpected", type: "MemberExpression" }] }, - { code: "console.error(foo)", errors: [{ messageId: "unexpected", type: "MemberExpression" }] }, - { code: "console.info(foo)", errors: [{ messageId: "unexpected", type: "MemberExpression" }] }, - { code: "console.warn(foo)", errors: [{ messageId: "unexpected", type: "MemberExpression" }] }, + { + code: "if (a) console.warn(foo)", + errors: [{ + messageId: "unexpected", + type: "MemberExpression", + suggestions: null + }] + }, + { + code: "foo(console.log)", + errors: [{ + messageId: "unexpected", + type: "MemberExpression", + suggestions: null + }] + }, + { + code: "console.log(foo)", + errors: [{ + messageId: "unexpected", + type: "MemberExpression", + suggestions: [{ + messageId: "removeConsole", + data: { propertyName: "log" }, + output: "" + }] + }] + }, + { + code: "console.error(foo)", + errors: [{ + messageId: "unexpected", + type: "MemberExpression", + suggestions: [{ + messageId: "removeConsole", + data: { propertyName: "error" }, + output: "" + }] + }] + }, + { + code: "console.info(foo)", + errors: [{ + messageId: "unexpected", + type: "MemberExpression", + suggestions: [{ + messageId: "removeConsole", + data: { propertyName: "info" }, + output: "" + }] + }] + }, + { + code: "console.warn(foo)", + errors: [{ + messageId: "unexpected", + type: "MemberExpression", + suggestions: [{ + messageId: "removeConsole", + data: { propertyName: "warn" }, + output: "" + }] + }] + }, + { + code: "switch (a) { case 1: console.log(foo) }", + errors: [{ + messageId: "unexpected", + type: "MemberExpression", + suggestions: [{ + messageId: "removeConsole", + data: { propertyName: "log" }, + output: "switch (a) { case 1: }" + }] + }] + }, + { + code: "if (a) { console.warn(foo) }", + errors: [{ + messageId: "unexpected", + type: "MemberExpression", + suggestions: [{ + messageId: "removeConsole", + data: { propertyName: "warn" }, + output: "if (a) { }" + }] + }] + }, + { + code: "a();\nconsole.log(foo);\nb();", + errors: [{ + messageId: "unexpected", + type: "MemberExpression", + suggestions: [{ + messageId: "removeConsole", + data: { propertyName: "log" }, + output: "a();\n\nb();" + }] + }] + }, + { + code: "class A { static { console.info(foo) } }", + parserOptions: { ecmaVersion: "latest" }, + errors: [{ + messageId: "unexpected", + type: "MemberExpression", + suggestions: [{ + messageId: "removeConsole", + data: { propertyName: "info" }, + output: "class A { static { } }" + }] + }] + }, + { + code: "a()\nconsole.log(foo);\n[1, 2, 3].forEach(a => doSomething(a))", + parserOptions: { ecmaVersion: "latest" }, + errors: [{ + messageId: "unexpected", + type: "MemberExpression", + suggestions: null + }] + }, + { + code: "a++\nconsole.log();\n/b/", + parserOptions: { ecmaVersion: "latest" }, + errors: [{ + messageId: "unexpected", + type: "MemberExpression", + suggestions: null + }] + }, + { + code: "a();\nconsole.log(foo);\n[1, 2, 3].forEach(a => doSomething(a));", + parserOptions: { ecmaVersion: "latest" }, + errors: [{ + messageId: "unexpected", + type: "MemberExpression", + suggestions: [{ + messageId: "removeConsole", + data: { propertyName: "log" }, + output: "a();\n\n[1, 2, 3].forEach(a => doSomething(a));" + }] + }] + }, // one option - { code: "console.log(foo)", options: [{ allow: ["error"] }], errors: [{ messageId: "unexpected", type: "MemberExpression" }] }, - { code: "console.error(foo)", options: [{ allow: ["warn"] }], errors: [{ messageId: "unexpected", type: "MemberExpression" }] }, - { code: "console.info(foo)", options: [{ allow: ["log"] }], errors: [{ messageId: "unexpected", type: "MemberExpression" }] }, - { code: "console.warn(foo)", options: [{ allow: ["error"] }], errors: [{ messageId: "unexpected", type: "MemberExpression" }] }, + { + code: "if (a) console.info(foo)", + options: [{ allow: ["warn"] }], + errors: [{ + messageId: "unexpected", + type: "MemberExpression", + suggestions: null + }] + }, + { + code: "foo(console.warn)", + options: [{ allow: ["log"] }], + errors: [{ + messageId: "unexpected", + type: "MemberExpression", + suggestions: null + }] + }, + { + code: "console.log(foo)", + options: [{ allow: ["error"] }], + errors: [{ + messageId: "unexpected", + type: "MemberExpression", + suggestions: [{ + messageId: "removeConsole", + data: { propertyName: "log" }, + output: "" + }] + }] + }, + { + code: "console.error(foo)", + options: [{ allow: ["warn"] }], + errors: [{ + messageId: "unexpected", + type: "MemberExpression", + suggestions: [{ + messageId: "removeConsole", + data: { propertyName: "error" }, + output: "" + }] + }] + }, + { + code: "console.info(foo)", + options: [{ allow: ["log"] }], + errors: [{ + messageId: "unexpected", + type: "MemberExpression", + suggestions: [{ + messageId: "removeConsole", + data: { propertyName: "info" }, + output: "" + }] + }] + }, + { + code: "console.warn(foo)", + options: [{ allow: ["error"] }], + errors: [{ + messageId: "unexpected", + type: "MemberExpression", + suggestions: [{ + messageId: "removeConsole", + data: { propertyName: "warn" }, + output: "" + }] + }] + }, + { + code: "switch (a) { case 1: console.log(foo) }", + options: [{ allow: ["error"] }], + errors: [{ + messageId: "unexpected", + type: "MemberExpression", + suggestions: [{ + messageId: "removeConsole", + data: { propertyName: "log" }, + output: "switch (a) { case 1: }" + }] + }] + }, + { + code: "if (a) { console.info(foo) }", + options: [{ allow: ["warn"] }], + errors: [{ + messageId: "unexpected", + type: "MemberExpression", + suggestions: [{ + messageId: "removeConsole", + data: { propertyName: "info" }, + output: "if (a) { }" + }] + }] + }, + { + code: "class A { static { console.error(foo) } }", + options: [{ allow: ["log"] }], + parserOptions: { ecmaVersion: "latest" }, + errors: [{ + messageId: "unexpected", + type: "MemberExpression", + suggestions: [{ + messageId: "removeConsole", + data: { propertyName: "error" }, + output: "class A { static { } }" + }] + }] + }, // multiple options - { code: "console.log(foo)", options: [{ allow: ["warn", "info"] }], errors: [{ messageId: "unexpected", type: "MemberExpression" }] }, - { code: "console.error(foo)", options: [{ allow: ["warn", "info", "log"] }], errors: [{ messageId: "unexpected", type: "MemberExpression" }] }, - { code: "console.info(foo)", options: [{ allow: ["warn", "error", "log"] }], errors: [{ messageId: "unexpected", type: "MemberExpression" }] }, - { code: "console.warn(foo)", options: [{ allow: ["info", "log"] }], errors: [{ messageId: "unexpected", type: "MemberExpression" }] }, + { + code: "if (a) console.log(foo)", + options: [{ allow: ["warn", "error"] }], + errors: [{ + messageId: "unexpected", + type: "MemberExpression", + suggestions: null + }] + }, + { + code: "foo(console.info)", + options: [{ allow: ["warn", "error"] }], + errors: [{ + messageId: "unexpected", + type: "MemberExpression", + suggestions: null + }] + }, + { + code: "console.log(foo)", + options: [{ allow: ["warn", "info"] }], + errors: [{ + messageId: "unexpected", + type: "MemberExpression", + suggestions: [{ + messageId: "removeConsole", + data: { propertyName: "log" }, + output: "" + }] + }] + }, + { + code: "console.error(foo)", + options: [{ allow: ["warn", "info", "log"] }], + errors: [{ + messageId: "unexpected", + type: "MemberExpression", + suggestions: [{ + messageId: "removeConsole", + data: { propertyName: "error" }, + output: "" + }] + }] + }, + { + code: "console.info(foo)", + options: [{ allow: ["warn", "error", "log"] }], + errors: [{ + messageId: "unexpected", + type: "MemberExpression", + suggestions: [{ + messageId: "removeConsole", + data: { propertyName: "info" }, + output: "" + }] + }] + }, + { + code: "console.warn(foo)", + options: [{ allow: ["info", "log"] }], + errors: [{ + messageId: "unexpected", + type: "MemberExpression", + suggestions: [{ + messageId: "removeConsole", + data: { propertyName: "warn" }, + output: "" + }] + }] + }, + { + code: "switch (a) { case 1: console.error(foo) }", + options: [{ allow: ["info", "log"] }], + errors: [{ + messageId: "unexpected", + type: "MemberExpression", + suggestions: [{ + messageId: "removeConsole", + data: { propertyName: "error" }, + output: "switch (a) { case 1: }" + }] + }] + }, + { + code: "if (a) { console.log(foo) }", + options: [{ allow: ["warn", "error"] }], + errors: [{ + messageId: "unexpected", + type: "MemberExpression", + suggestions: [{ + messageId: "removeConsole", + data: { propertyName: "log" }, + output: "if (a) { }" + }] + }] + }, + { + code: "class A { static { console.info(foo) } }", + options: [{ allow: ["log", "error", "warn"] }], + parserOptions: { ecmaVersion: "latest" }, + errors: [{ + messageId: "unexpected", + type: "MemberExpression", + suggestions: [{ + messageId: "removeConsole", + data: { propertyName: "info" }, + output: "class A { static { } }" + }] + }] + }, // In case that implicit global variable of 'console' exists - { code: "console.log(foo)", env: { node: true }, errors: [{ messageId: "unexpected", type: "MemberExpression" }] } + { + code: "console.log(foo)", + env: { node: true }, + errors: [{ + messageId: "unexpected", + type: "MemberExpression", + suggestions: [{ + messageId: "removeConsole", + data: { propertyName: "log" }, + output: "" + }] + }] + } ] });