/** * @fileoverview Rule to flag unnecessary bind calls * @author Bence Dányi */ '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, }; }, };