1 | /**
|
2 | * @fileoverview Rule to disallow returning values from setters
|
3 | * @author Milos Djermanovic
|
4 | */
|
5 |
|
6 | ;
|
7 |
|
8 | //------------------------------------------------------------------------------
|
9 | // Requirements
|
10 | //------------------------------------------------------------------------------
|
11 |
|
12 | const astUtils = require("./utils/ast-utils");
|
13 | const { findVariable } = require("eslint-utils");
|
14 |
|
15 | //------------------------------------------------------------------------------
|
16 | // Helpers
|
17 | //------------------------------------------------------------------------------
|
18 |
|
19 | /**
|
20 | * Determines whether the given identifier node is a reference to a global variable.
|
21 | * @param {ASTNode} node `Identifier` node to check.
|
22 | * @param {Scope} scope Scope to which the node belongs.
|
23 | * @returns {boolean} True if the identifier is a reference to a global variable.
|
24 | */
|
25 | function isGlobalReference(node, scope) {
|
26 | const variable = findVariable(scope, node);
|
27 |
|
28 | return variable !== null && variable.scope.type === "global" && variable.defs.length === 0;
|
29 | }
|
30 |
|
31 | /**
|
32 | * Determines whether the given node is an argument of the specified global method call, at the given `index` position.
|
33 | * E.g., for given `index === 1`, this function checks for `objectName.methodName(foo, node)`, where objectName is a global variable.
|
34 | * @param {ASTNode} node The node to check.
|
35 | * @param {Scope} scope Scope to which the node belongs.
|
36 | * @param {string} objectName Name of the global object.
|
37 | * @param {string} methodName Name of the method.
|
38 | * @param {number} index The given position.
|
39 | * @returns {boolean} `true` if the node is argument at the given position.
|
40 | */
|
41 | function isArgumentOfGlobalMethodCall(node, scope, objectName, methodName, index) {
|
42 | const callNode = node.parent;
|
43 |
|
44 | return callNode.type === "CallExpression" &&
|
45 | callNode.arguments[index] === node &&
|
46 | astUtils.isSpecificMemberAccess(callNode.callee, objectName, methodName) &&
|
47 | isGlobalReference(astUtils.skipChainExpression(callNode.callee).object, scope);
|
48 | }
|
49 |
|
50 | /**
|
51 | * Determines whether the given node is used as a property descriptor.
|
52 | * @param {ASTNode} node The node to check.
|
53 | * @param {Scope} scope Scope to which the node belongs.
|
54 | * @returns {boolean} `true` if the node is a property descriptor.
|
55 | */
|
56 | function isPropertyDescriptor(node, scope) {
|
57 | if (
|
58 | isArgumentOfGlobalMethodCall(node, scope, "Object", "defineProperty", 2) ||
|
59 | isArgumentOfGlobalMethodCall(node, scope, "Reflect", "defineProperty", 2)
|
60 | ) {
|
61 | return true;
|
62 | }
|
63 |
|
64 | const parent = node.parent;
|
65 |
|
66 | if (
|
67 | parent.type === "Property" &&
|
68 | parent.value === node
|
69 | ) {
|
70 | const grandparent = parent.parent;
|
71 |
|
72 | if (
|
73 | grandparent.type === "ObjectExpression" &&
|
74 | (
|
75 | isArgumentOfGlobalMethodCall(grandparent, scope, "Object", "create", 1) ||
|
76 | isArgumentOfGlobalMethodCall(grandparent, scope, "Object", "defineProperties", 1)
|
77 | )
|
78 | ) {
|
79 | return true;
|
80 | }
|
81 | }
|
82 |
|
83 | return false;
|
84 | }
|
85 |
|
86 | /**
|
87 | * Determines whether the given function node is used as a setter function.
|
88 | * @param {ASTNode} node The node to check.
|
89 | * @param {Scope} scope Scope to which the node belongs.
|
90 | * @returns {boolean} `true` if the node is a setter.
|
91 | */
|
92 | function isSetter(node, scope) {
|
93 | const parent = node.parent;
|
94 |
|
95 | if (
|
96 | parent.kind === "set" &&
|
97 | parent.value === node
|
98 | ) {
|
99 |
|
100 | // Setter in an object literal or in a class
|
101 | return true;
|
102 | }
|
103 |
|
104 | if (
|
105 | parent.type === "Property" &&
|
106 | parent.value === node &&
|
107 | astUtils.getStaticPropertyName(parent) === "set" &&
|
108 | parent.parent.type === "ObjectExpression" &&
|
109 | isPropertyDescriptor(parent.parent, scope)
|
110 | ) {
|
111 |
|
112 | // Setter in a property descriptor
|
113 | return true;
|
114 | }
|
115 |
|
116 | return false;
|
117 | }
|
118 |
|
119 | /**
|
120 | * Finds function's outer scope.
|
121 | * @param {Scope} scope Function's own scope.
|
122 | * @returns {Scope} Function's outer scope.
|
123 | */
|
124 | function getOuterScope(scope) {
|
125 | const upper = scope.upper;
|
126 |
|
127 | if (upper.type === "function-expression-name") {
|
128 | return upper.upper;
|
129 | }
|
130 |
|
131 | return upper;
|
132 | }
|
133 |
|
134 | //------------------------------------------------------------------------------
|
135 | // Rule Definition
|
136 | //------------------------------------------------------------------------------
|
137 |
|
138 | module.exports = {
|
139 | meta: {
|
140 | type: "problem",
|
141 |
|
142 | docs: {
|
143 | description: "disallow returning values from setters",
|
144 | category: "Possible Errors",
|
145 | recommended: true,
|
146 | url: "https://eslint.org/docs/rules/no-setter-return"
|
147 | },
|
148 |
|
149 | schema: [],
|
150 |
|
151 | messages: {
|
152 | returnsValue: "Setter cannot return a value."
|
153 | }
|
154 | },
|
155 |
|
156 | create(context) {
|
157 | let funcInfo = null;
|
158 |
|
159 | /**
|
160 | * Creates and pushes to the stack a function info object for the given function node.
|
161 | * @param {ASTNode} node The function node.
|
162 | * @returns {void}
|
163 | */
|
164 | function enterFunction(node) {
|
165 | const outerScope = getOuterScope(context.getScope());
|
166 |
|
167 | funcInfo = {
|
168 | upper: funcInfo,
|
169 | isSetter: isSetter(node, outerScope)
|
170 | };
|
171 | }
|
172 |
|
173 | /**
|
174 | * Pops the current function info object from the stack.
|
175 | * @returns {void}
|
176 | */
|
177 | function exitFunction() {
|
178 | funcInfo = funcInfo.upper;
|
179 | }
|
180 |
|
181 | /**
|
182 | * Reports the given node.
|
183 | * @param {ASTNode} node Node to report.
|
184 | * @returns {void}
|
185 | */
|
186 | function report(node) {
|
187 | context.report({ node, messageId: "returnsValue" });
|
188 | }
|
189 |
|
190 | return {
|
191 |
|
192 | /*
|
193 | * Function declarations cannot be setters, but we still have to track them in the `funcInfo` stack to avoid
|
194 | * false positives, because a ReturnStatement node can belong to a function declaration inside a setter.
|
195 | *
|
196 | * Note: A previously declared function can be referenced and actually used as a setter in a property descriptor,
|
197 | * but that's out of scope for this rule.
|
198 | */
|
199 | FunctionDeclaration: enterFunction,
|
200 | FunctionExpression: enterFunction,
|
201 | ArrowFunctionExpression(node) {
|
202 | enterFunction(node);
|
203 |
|
204 | if (funcInfo.isSetter && node.expression) {
|
205 |
|
206 | // { set: foo => bar } property descriptor. Report implicit return 'bar' as the equivalent for a return statement.
|
207 | report(node.body);
|
208 | }
|
209 | },
|
210 |
|
211 | "FunctionDeclaration:exit": exitFunction,
|
212 | "FunctionExpression:exit": exitFunction,
|
213 | "ArrowFunctionExpression:exit": exitFunction,
|
214 |
|
215 | ReturnStatement(node) {
|
216 |
|
217 | // Global returns (e.g., at the top level of a Node module) don't have `funcInfo`.
|
218 | if (funcInfo && funcInfo.isSetter && node.argument) {
|
219 | report(node);
|
220 | }
|
221 | }
|
222 | };
|
223 | }
|
224 | };
|