| /** | |
|  * @fileoverview Rule to flag use of console object | |
|  * @author Nicholas C. Zakas | |
|  */ | |
| 
 | |
| "use strict"; | |
| 
 | |
| //------------------------------------------------------------------------------ | |
| // Requirements | |
| //------------------------------------------------------------------------------ | |
|  | |
| const astUtils = require("./utils/ast-utils"); | |
| 
 | |
| //------------------------------------------------------------------------------ | |
| // Rule Definition | |
| //------------------------------------------------------------------------------ | |
|  | |
| /** @type {import('../shared/types').Rule} */ | |
| module.exports = { | |
|     meta: { | |
|         type: "suggestion", | |
| 
 | |
|         docs: { | |
|             description: "Disallow the use of `console`", | |
|             recommended: false, | |
|             url: "https://eslint.org/docs/latest/rules/no-console" | |
|         }, | |
| 
 | |
|         schema: [ | |
|             { | |
|                 type: "object", | |
|                 properties: { | |
|                     allow: { | |
|                         type: "array", | |
|                         items: { | |
|                             type: "string" | |
|                         }, | |
|                         minItems: 1, | |
|                         uniqueItems: true | |
|                     } | |
|                 }, | |
|                 additionalProperties: false | |
|             } | |
|         ], | |
| 
 | |
|         hasSuggestions: true, | |
| 
 | |
|         messages: { | |
|             unexpected: "Unexpected console statement.", | |
|             removeConsole: "Remove the console.{{ propertyName }}()." | |
|         } | |
|     }, | |
| 
 | |
|     create(context) { | |
|         const options = context.options[0] || {}; | |
|         const allowed = options.allow || []; | |
|         const sourceCode = context.sourceCode; | |
| 
 | |
|         /** | |
|          * Checks whether the given reference is 'console' or not. | |
|          * @param {eslint-scope.Reference} reference The reference to check. | |
|          * @returns {boolean} `true` if the reference is 'console'. | |
|          */ | |
|         function isConsole(reference) { | |
|             const id = reference.identifier; | |
| 
 | |
|             return id && id.name === "console"; | |
|         } | |
| 
 | |
|         /** | |
|          * Checks whether the property name of the given MemberExpression node | |
|          * is allowed by options or not. | |
|          * @param {ASTNode} node The MemberExpression node to check. | |
|          * @returns {boolean} `true` if the property name of the node is allowed. | |
|          */ | |
|         function isAllowed(node) { | |
|             const propertyName = astUtils.getStaticPropertyName(node); | |
| 
 | |
|             return propertyName && allowed.includes(propertyName); | |
|         } | |
| 
 | |
|         /** | |
|          * Checks whether the given reference is a member access which is not | |
|          * allowed by options or not. | |
|          * @param {eslint-scope.Reference} reference The reference to check. | |
|          * @returns {boolean} `true` if the reference is a member access which | |
|          *      is not allowed by options. | |
|          */ | |
|         function isMemberAccessExceptAllowed(reference) { | |
|             const node = reference.identifier; | |
|             const parent = node.parent; | |
| 
 | |
|             return ( | |
|                 parent.type === "MemberExpression" && | |
|                 parent.object === node && | |
|                 !isAllowed(parent) | |
|             ); | |
|         } | |
| 
 | |
|         /** | |
|          * 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. | |
|          * @returns {void} | |
|          */ | |
|         function report(reference) { | |
|             const node = reference.identifier.parent; | |
| 
 | |
|             const propertyName = astUtils.getStaticPropertyName(node); | |
| 
 | |
|             context.report({ | |
|                 node, | |
|                 loc: node.loc, | |
|                 messageId: "unexpected", | |
|                 suggest: canProvideSuggestions(node) | |
|                     ? [{ | |
|                         messageId: "removeConsole", | |
|                         data: { propertyName }, | |
|                         fix(fixer) { | |
|                             return fixer.remove(node.parent.parent); | |
|                         } | |
|                     }] | |
|                     : [] | |
|             }); | |
|         } | |
| 
 | |
|         return { | |
|             "Program:exit"(node) { | |
|                 const scope = sourceCode.getScope(node); | |
|                 const consoleVar = astUtils.getVariableByName(scope, "console"); | |
|                 const shadowed = consoleVar && consoleVar.defs.length > 0; | |
| 
 | |
|                 /* | |
|                  * 'scope.through' includes all references to undefined | |
|                  * variables. If the variable 'console' is not defined, it uses | |
|                  * 'scope.through'. | |
|                  */ | |
|                 const references = consoleVar | |
|                     ? consoleVar.references | |
|                     : scope.through.filter(isConsole); | |
| 
 | |
|                 if (!shadowed) { | |
|                     references | |
|                         .filter(isMemberAccessExceptAllowed) | |
|                         .forEach(report); | |
|                 } | |
|             } | |
|         }; | |
|     } | |
| };
 |