225 lines
		
	
	
		
			5.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			225 lines
		
	
	
		
			5.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
/**
 | 
						|
 * @fileoverview Rule to flag unnecessary bind calls
 | 
						|
 * @author Bence Dányi <bence@danyi.me>
 | 
						|
 */
 | 
						|
"use strict";
 | 
						|
 | 
						|
//------------------------------------------------------------------------------
 | 
						|
// Requirements
 | 
						|
//------------------------------------------------------------------------------
 | 
						|
 | 
						|
const astUtils = require("./utils/ast-utils");
 | 
						|
 | 
						|
//------------------------------------------------------------------------------
 | 
						|
// Helpers
 | 
						|
//------------------------------------------------------------------------------
 | 
						|
 | 
						|
const SIDE_EFFECT_FREE_NODE_TYPES = new Set([
 | 
						|
	"Literal",
 | 
						|
	"Identifier",
 | 
						|
	"ThisExpression",
 | 
						|
	"FunctionExpression",
 | 
						|
]);
 | 
						|
 | 
						|
//------------------------------------------------------------------------------
 | 
						|
// Rule Definition
 | 
						|
//------------------------------------------------------------------------------
 | 
						|
 | 
						|
/** @type {import('../types').Rule.RuleModule} */
 | 
						|
module.exports = {
 | 
						|
	meta: {
 | 
						|
		type: "suggestion",
 | 
						|
 | 
						|
		docs: {
 | 
						|
			description: "Disallow unnecessary calls to `.bind()`",
 | 
						|
			recommended: false,
 | 
						|
			url: "https://eslint.org/docs/latest/rules/no-extra-bind",
 | 
						|
		},
 | 
						|
 | 
						|
		schema: [],
 | 
						|
		fixable: "code",
 | 
						|
 | 
						|
		messages: {
 | 
						|
			unexpected: "The function binding is unnecessary.",
 | 
						|
		},
 | 
						|
	},
 | 
						|
 | 
						|
	create(context) {
 | 
						|
		const sourceCode = context.sourceCode;
 | 
						|
		let scopeInfo = null;
 | 
						|
 | 
						|
		/**
 | 
						|
		 * Checks if a node is free of side effects.
 | 
						|
		 *
 | 
						|
		 * This check is stricter than it needs to be, in order to keep the implementation simple.
 | 
						|
		 * @param {ASTNode} node A node to check.
 | 
						|
		 * @returns {boolean} True if the node is known to be side-effect free, false otherwise.
 | 
						|
		 */
 | 
						|
		function isSideEffectFree(node) {
 | 
						|
			return SIDE_EFFECT_FREE_NODE_TYPES.has(node.type);
 | 
						|
		}
 | 
						|
 | 
						|
		/**
 | 
						|
		 * Reports a given function node.
 | 
						|
		 * @param {ASTNode} node A node to report. This is a FunctionExpression or
 | 
						|
		 *      an ArrowFunctionExpression.
 | 
						|
		 * @returns {void}
 | 
						|
		 */
 | 
						|
		function report(node) {
 | 
						|
			const memberNode = node.parent;
 | 
						|
			const callNode =
 | 
						|
				memberNode.parent.type === "ChainExpression"
 | 
						|
					? memberNode.parent.parent
 | 
						|
					: memberNode.parent;
 | 
						|
 | 
						|
			context.report({
 | 
						|
				node: callNode,
 | 
						|
				messageId: "unexpected",
 | 
						|
				loc: memberNode.property.loc,
 | 
						|
 | 
						|
				fix(fixer) {
 | 
						|
					if (!isSideEffectFree(callNode.arguments[0])) {
 | 
						|
						return null;
 | 
						|
					}
 | 
						|
 | 
						|
					/*
 | 
						|
					 * The list of the first/last token pair of a removal range.
 | 
						|
					 * This is two parts because closing parentheses may exist between the method name and arguments.
 | 
						|
					 * E.g. `(function(){}.bind ) (obj)`
 | 
						|
					 *                    ^^^^^   ^^^^^ < removal ranges
 | 
						|
					 * E.g. `(function(){}?.['bind'] ) ?.(obj)`
 | 
						|
					 *                    ^^^^^^^^^^   ^^^^^^^ < removal ranges
 | 
						|
					 */
 | 
						|
					const tokenPairs = [
 | 
						|
						[
 | 
						|
							// `.`, `?.`, or `[` token.
 | 
						|
							sourceCode.getTokenAfter(
 | 
						|
								memberNode.object,
 | 
						|
								astUtils.isNotClosingParenToken,
 | 
						|
							),
 | 
						|
 | 
						|
							// property name or `]` token.
 | 
						|
							sourceCode.getLastToken(memberNode),
 | 
						|
						],
 | 
						|
						[
 | 
						|
							// `?.` or `(` token of arguments.
 | 
						|
							sourceCode.getTokenAfter(
 | 
						|
								memberNode,
 | 
						|
								astUtils.isNotClosingParenToken,
 | 
						|
							),
 | 
						|
 | 
						|
							// `)` token of arguments.
 | 
						|
							sourceCode.getLastToken(callNode),
 | 
						|
						],
 | 
						|
					];
 | 
						|
					const firstTokenToRemove = tokenPairs[0][0];
 | 
						|
					const lastTokenToRemove = tokenPairs[1][1];
 | 
						|
 | 
						|
					if (
 | 
						|
						sourceCode.commentsExistBetween(
 | 
						|
							firstTokenToRemove,
 | 
						|
							lastTokenToRemove,
 | 
						|
						)
 | 
						|
					) {
 | 
						|
						return null;
 | 
						|
					}
 | 
						|
 | 
						|
					return tokenPairs.map(([start, end]) =>
 | 
						|
						fixer.removeRange([start.range[0], end.range[1]]),
 | 
						|
					);
 | 
						|
				},
 | 
						|
			});
 | 
						|
		}
 | 
						|
 | 
						|
		/**
 | 
						|
		 * Checks whether or not a given function node is the callee of `.bind()`
 | 
						|
		 * method.
 | 
						|
		 *
 | 
						|
		 * e.g. `(function() {}.bind(foo))`
 | 
						|
		 * @param {ASTNode} node A node to report. This is a FunctionExpression or
 | 
						|
		 *      an ArrowFunctionExpression.
 | 
						|
		 * @returns {boolean} `true` if the node is the callee of `.bind()` method.
 | 
						|
		 */
 | 
						|
		function isCalleeOfBindMethod(node) {
 | 
						|
			if (!astUtils.isSpecificMemberAccess(node.parent, null, "bind")) {
 | 
						|
				return false;
 | 
						|
			}
 | 
						|
 | 
						|
			// The node of `*.bind` member access.
 | 
						|
			const bindNode =
 | 
						|
				node.parent.parent.type === "ChainExpression"
 | 
						|
					? node.parent.parent
 | 
						|
					: node.parent;
 | 
						|
 | 
						|
			return (
 | 
						|
				bindNode.parent.type === "CallExpression" &&
 | 
						|
				bindNode.parent.callee === bindNode &&
 | 
						|
				bindNode.parent.arguments.length === 1 &&
 | 
						|
				bindNode.parent.arguments[0].type !== "SpreadElement"
 | 
						|
			);
 | 
						|
		}
 | 
						|
 | 
						|
		/**
 | 
						|
		 * Adds a scope information object to the stack.
 | 
						|
		 * @param {ASTNode} node A node to add. This node is a FunctionExpression
 | 
						|
		 *      or a FunctionDeclaration node.
 | 
						|
		 * @returns {void}
 | 
						|
		 */
 | 
						|
		function enterFunction(node) {
 | 
						|
			scopeInfo = {
 | 
						|
				isBound: isCalleeOfBindMethod(node),
 | 
						|
				thisFound: false,
 | 
						|
				upper: scopeInfo,
 | 
						|
			};
 | 
						|
		}
 | 
						|
 | 
						|
		/**
 | 
						|
		 * Removes the scope information object from the top of the stack.
 | 
						|
		 * At the same time, this reports the function node if the function has
 | 
						|
		 * `.bind()` and the `this` keywords found.
 | 
						|
		 * @param {ASTNode} node A node to remove. This node is a
 | 
						|
		 *      FunctionExpression or a FunctionDeclaration node.
 | 
						|
		 * @returns {void}
 | 
						|
		 */
 | 
						|
		function exitFunction(node) {
 | 
						|
			if (scopeInfo.isBound && !scopeInfo.thisFound) {
 | 
						|
				report(node);
 | 
						|
			}
 | 
						|
 | 
						|
			scopeInfo = scopeInfo.upper;
 | 
						|
		}
 | 
						|
 | 
						|
		/**
 | 
						|
		 * Reports a given arrow function if the function is callee of `.bind()`
 | 
						|
		 * method.
 | 
						|
		 * @param {ASTNode} node A node to report. This node is an
 | 
						|
		 *      ArrowFunctionExpression.
 | 
						|
		 * @returns {void}
 | 
						|
		 */
 | 
						|
		function exitArrowFunction(node) {
 | 
						|
			if (isCalleeOfBindMethod(node)) {
 | 
						|
				report(node);
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		/**
 | 
						|
		 * Set the mark as the `this` keyword was found in this scope.
 | 
						|
		 * @returns {void}
 | 
						|
		 */
 | 
						|
		function markAsThisFound() {
 | 
						|
			if (scopeInfo) {
 | 
						|
				scopeInfo.thisFound = true;
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		return {
 | 
						|
			"ArrowFunctionExpression:exit": exitArrowFunction,
 | 
						|
			FunctionDeclaration: enterFunction,
 | 
						|
			"FunctionDeclaration:exit": exitFunction,
 | 
						|
			FunctionExpression: enterFunction,
 | 
						|
			"FunctionExpression:exit": exitFunction,
 | 
						|
			ThisExpression: markAsThisFound,
 | 
						|
		};
 | 
						|
	},
 | 
						|
};
 |