208 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			208 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /**
 | |
|  * @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);
 | |
|                 }
 | |
|             }
 | |
|         };
 | |
|     }
 | |
| };
 |