no-console.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. /**
  2. * @fileoverview Rule to flag use of console object
  3. * @author Nicholas C. Zakas
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const astUtils = require("./utils/ast-utils");
  10. //------------------------------------------------------------------------------
  11. // Rule Definition
  12. //------------------------------------------------------------------------------
  13. /** @type {import('../types').Rule.RuleModule} */
  14. module.exports = {
  15. meta: {
  16. type: "suggestion",
  17. defaultOptions: [{}],
  18. docs: {
  19. description: "Disallow the use of `console`",
  20. recommended: false,
  21. url: "https://eslint.org/docs/latest/rules/no-console",
  22. },
  23. schema: [
  24. {
  25. type: "object",
  26. properties: {
  27. allow: {
  28. type: "array",
  29. items: {
  30. type: "string",
  31. },
  32. minItems: 1,
  33. uniqueItems: true,
  34. },
  35. },
  36. additionalProperties: false,
  37. },
  38. ],
  39. hasSuggestions: true,
  40. messages: {
  41. unexpected: "Unexpected console statement.",
  42. limited:
  43. "Unexpected console statement. Only these console methods are allowed: {{ allowed }}.",
  44. removeConsole: "Remove the console.{{ propertyName }}().",
  45. removeMethodCall: "Remove the console method call.",
  46. },
  47. },
  48. create(context) {
  49. const [{ allow: allowed = [] }] = context.options;
  50. const sourceCode = context.sourceCode;
  51. /**
  52. * Checks whether the given reference is 'console' or not.
  53. * @param {eslint-scope.Reference} reference The reference to check.
  54. * @returns {boolean} `true` if the reference is 'console'.
  55. */
  56. function isConsole(reference) {
  57. const id = reference.identifier;
  58. return id && id.name === "console";
  59. }
  60. /**
  61. * Checks whether the property name of the given MemberExpression node
  62. * is allowed by options or not.
  63. * @param {ASTNode} node The MemberExpression node to check.
  64. * @returns {boolean} `true` if the property name of the node is allowed.
  65. */
  66. function isAllowed(node) {
  67. const propertyName = astUtils.getStaticPropertyName(node);
  68. return propertyName && allowed.includes(propertyName);
  69. }
  70. /**
  71. * Checks whether the given reference is a member access which is not
  72. * allowed by options or not.
  73. * @param {eslint-scope.Reference} reference The reference to check.
  74. * @returns {boolean} `true` if the reference is a member access which
  75. * is not allowed by options.
  76. */
  77. function isMemberAccessExceptAllowed(reference) {
  78. const node = reference.identifier;
  79. const parent = node.parent;
  80. return (
  81. parent.type === "MemberExpression" &&
  82. parent.object === node &&
  83. !isAllowed(parent)
  84. );
  85. }
  86. /**
  87. * Checks if removing the ExpressionStatement node will cause ASI to
  88. * break.
  89. * eg.
  90. * foo()
  91. * console.log();
  92. * [1, 2, 3].forEach(a => doSomething(a))
  93. *
  94. * Removing the console.log(); statement should leave two statements, but
  95. * here the two statements will become one because [ causes continuation after
  96. * foo().
  97. * @param {ASTNode} node The ExpressionStatement node to check.
  98. * @returns {boolean} `true` if ASI will break after removing the ExpressionStatement
  99. * node.
  100. */
  101. function maybeAsiHazard(node) {
  102. const SAFE_TOKENS_BEFORE = /^[:;{]$/u; // One of :;{
  103. const UNSAFE_CHARS_AFTER = /^[-[(/+`]/u; // One of [(/+-`
  104. const tokenBefore = sourceCode.getTokenBefore(node);
  105. const tokenAfter = sourceCode.getTokenAfter(node);
  106. return (
  107. Boolean(tokenAfter) &&
  108. UNSAFE_CHARS_AFTER.test(tokenAfter.value) &&
  109. tokenAfter.value !== "++" &&
  110. tokenAfter.value !== "--" &&
  111. Boolean(tokenBefore) &&
  112. !SAFE_TOKENS_BEFORE.test(tokenBefore.value)
  113. );
  114. }
  115. /**
  116. * Checks if the MemberExpression node's parent.parent.parent is a
  117. * Program, BlockStatement, StaticBlock, or SwitchCase node. This check
  118. * is necessary to avoid providing a suggestion that might cause a syntax error.
  119. *
  120. * eg. if (a) console.log(b), removing console.log() here will lead to a
  121. * syntax error.
  122. * if (a) { console.log(b) }, removing console.log() here is acceptable.
  123. *
  124. * Additionally, it checks if the callee of the CallExpression node is
  125. * the node itself.
  126. *
  127. * eg. foo(console.log), cannot provide a suggestion here.
  128. * @param {ASTNode} node The MemberExpression node to check.
  129. * @returns {boolean} `true` if a suggestion can be provided for a node.
  130. */
  131. function canProvideSuggestions(node) {
  132. return (
  133. node.parent.type === "CallExpression" &&
  134. node.parent.callee === node &&
  135. node.parent.parent.type === "ExpressionStatement" &&
  136. astUtils.STATEMENT_LIST_PARENTS.has(
  137. node.parent.parent.parent.type,
  138. ) &&
  139. !maybeAsiHazard(node.parent.parent)
  140. );
  141. }
  142. /**
  143. * Reports the given reference as a violation.
  144. * @param {eslint-scope.Reference} reference The reference to report.
  145. * @returns {void}
  146. */
  147. function report(reference) {
  148. const node = reference.identifier.parent;
  149. const suggest = [];
  150. if (canProvideSuggestions(node)) {
  151. const suggestion = {
  152. fix(fixer) {
  153. return fixer.remove(node.parent.parent);
  154. },
  155. };
  156. if (node.computed) {
  157. suggestion.messageId = "removeMethodCall";
  158. } else {
  159. suggestion.messageId = "removeConsole";
  160. suggestion.data = { propertyName: node.property.name };
  161. }
  162. suggest.push(suggestion);
  163. }
  164. context.report({
  165. node,
  166. loc: node.loc,
  167. messageId: allowed.length ? "limited" : "unexpected",
  168. data: { allowed: allowed.join(", ") },
  169. suggest,
  170. });
  171. }
  172. return {
  173. "Program:exit"(node) {
  174. const scope = sourceCode.getScope(node);
  175. const consoleVar = astUtils.getVariableByName(scope, "console");
  176. const shadowed = consoleVar && consoleVar.defs.length > 0;
  177. /*
  178. * 'scope.through' includes all references to undefined
  179. * variables. If the variable 'console' is not defined, it uses
  180. * 'scope.through'.
  181. */
  182. const references = consoleVar
  183. ? consoleVar.references
  184. : scope.through.filter(isConsole);
  185. if (!shadowed) {
  186. references
  187. .filter(isMemberAccessExceptAllowed)
  188. .forEach(report);
  189. }
  190. },
  191. };
  192. },
  193. };