/** * @fileoverview Rule to disallow returning values from setters * @author Milos Djermanovic */ "use strict"; //------------------------------------------------------------------------------ // Requirements //------------------------------------------------------------------------------ const astUtils = require("./utils/ast-utils"); const { findVariable } = require("eslint-utils"); //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ /** * Determines whether the given identifier node is a reference to a global variable. * @param {ASTNode} node `Identifier` node to check. * @param {Scope} scope Scope to which the node belongs. * @returns {boolean} True if the identifier is a reference to a global variable. */ function isGlobalReference(node, scope) { const variable = findVariable(scope, node); return variable !== null && variable.scope.type === "global" && variable.defs.length === 0; } /** * Determines whether the given node is an argument of the specified global method call, at the given `index` position. * E.g., for given `index === 1`, this function checks for `objectName.methodName(foo, node)`, where objectName is a global variable. * @param {ASTNode} node The node to check. * @param {Scope} scope Scope to which the node belongs. * @param {string} objectName Name of the global object. * @param {string} methodName Name of the method. * @param {number} index The given position. * @returns {boolean} `true` if the node is argument at the given position. */ function isArgumentOfGlobalMethodCall(node, scope, objectName, methodName, index) { const callNode = node.parent; return callNode.type === "CallExpression" && callNode.arguments[index] === node && astUtils.isSpecificMemberAccess(callNode.callee, objectName, methodName) && isGlobalReference(astUtils.skipChainExpression(callNode.callee).object, scope); } /** * Determines whether the given node is used as a property descriptor. * @param {ASTNode} node The node to check. * @param {Scope} scope Scope to which the node belongs. * @returns {boolean} `true` if the node is a property descriptor. */ function isPropertyDescriptor(node, scope) { if ( isArgumentOfGlobalMethodCall(node, scope, "Object", "defineProperty", 2) || isArgumentOfGlobalMethodCall(node, scope, "Reflect", "defineProperty", 2) ) { return true; } const parent = node.parent; if ( parent.type === "Property" && parent.value === node ) { const grandparent = parent.parent; if ( grandparent.type === "ObjectExpression" && ( isArgumentOfGlobalMethodCall(grandparent, scope, "Object", "create", 1) || isArgumentOfGlobalMethodCall(grandparent, scope, "Object", "defineProperties", 1) ) ) { return true; } } return false; } /** * Determines whether the given function node is used as a setter function. * @param {ASTNode} node The node to check. * @param {Scope} scope Scope to which the node belongs. * @returns {boolean} `true` if the node is a setter. */ function isSetter(node, scope) { const parent = node.parent; if ( (parent.type === "Property" || parent.type === "MethodDefinition") && parent.kind === "set" && parent.value === node ) { // Setter in an object literal or in a class return true; } if ( parent.type === "Property" && parent.value === node && astUtils.getStaticPropertyName(parent) === "set" && parent.parent.type === "ObjectExpression" && isPropertyDescriptor(parent.parent, scope) ) { // Setter in a property descriptor return true; } return false; } /** * Finds function's outer scope. * @param {Scope} scope Function's own scope. * @returns {Scope} Function's outer scope. */ function getOuterScope(scope) { const upper = scope.upper; if (upper.type === "function-expression-name") { return upper.upper; } return upper; } //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ /** @type {import('../shared/types').Rule} */ module.exports = { meta: { type: "problem", docs: { description: "disallow returning values from setters", recommended: true, url: "https://eslint.org/docs/rules/no-setter-return" }, schema: [], messages: { returnsValue: "Setter cannot return a value." } }, create(context) { let funcInfo = null; /** * Creates and pushes to the stack a function info object for the given function node. * @param {ASTNode} node The function node. * @returns {void} */ function enterFunction(node) { const outerScope = getOuterScope(context.getScope()); funcInfo = { upper: funcInfo, isSetter: isSetter(node, outerScope) }; } /** * Pops the current function info object from the stack. * @returns {void} */ function exitFunction() { funcInfo = funcInfo.upper; } /** * Reports the given node. * @param {ASTNode} node Node to report. * @returns {void} */ function report(node) { context.report({ node, messageId: "returnsValue" }); } return { /* * Function declarations cannot be setters, but we still have to track them in the `funcInfo` stack to avoid * false positives, because a ReturnStatement node can belong to a function declaration inside a setter. * * Note: A previously declared function can be referenced and actually used as a setter in a property descriptor, * but that's out of scope for this rule. */ FunctionDeclaration: enterFunction, FunctionExpression: enterFunction, ArrowFunctionExpression(node) { enterFunction(node); if (funcInfo.isSetter && node.expression) { // { set: foo => bar } property descriptor. Report implicit return 'bar' as the equivalent for a return statement. report(node.body); } }, "FunctionDeclaration:exit": exitFunction, "FunctionExpression:exit": exitFunction, "ArrowFunctionExpression:exit": exitFunction, ReturnStatement(node) { // Global returns (e.g., at the top level of a Node module) don't have `funcInfo`. if (funcInfo && funcInfo.isSetter && node.argument) { report(node); } } }; } };