]>
Commit | Line | Data |
---|---|---|
eb39fafa DC |
1 | /** |
2 | * @fileoverview Rule to disallow returning values from setters | |
3 | * @author Milos Djermanovic | |
4 | */ | |
5 | ||
6 | "use strict"; | |
7 | ||
8 | //------------------------------------------------------------------------------ | |
9 | // Requirements | |
10 | //------------------------------------------------------------------------------ | |
11 | ||
12 | const astUtils = require("./utils/ast-utils"); | |
f2a92ac6 | 13 | const { findVariable } = require("@eslint-community/eslint-utils"); |
eb39fafa DC |
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) { | |
6f036462 | 42 | const callNode = node.parent; |
eb39fafa | 43 | |
6f036462 TL |
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); | |
eb39fafa DC |
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 ( | |
609c276f | 96 | (parent.type === "Property" || parent.type === "MethodDefinition") && |
eb39fafa DC |
97 | parent.kind === "set" && |
98 | parent.value === node | |
99 | ) { | |
100 | ||
101 | // Setter in an object literal or in a class | |
102 | return true; | |
103 | } | |
104 | ||
105 | if ( | |
106 | parent.type === "Property" && | |
107 | parent.value === node && | |
108 | astUtils.getStaticPropertyName(parent) === "set" && | |
109 | parent.parent.type === "ObjectExpression" && | |
110 | isPropertyDescriptor(parent.parent, scope) | |
111 | ) { | |
112 | ||
113 | // Setter in a property descriptor | |
114 | return true; | |
115 | } | |
116 | ||
117 | return false; | |
118 | } | |
119 | ||
120 | /** | |
121 | * Finds function's outer scope. | |
122 | * @param {Scope} scope Function's own scope. | |
123 | * @returns {Scope} Function's outer scope. | |
124 | */ | |
125 | function getOuterScope(scope) { | |
126 | const upper = scope.upper; | |
127 | ||
128 | if (upper.type === "function-expression-name") { | |
129 | return upper.upper; | |
130 | } | |
131 | ||
132 | return upper; | |
133 | } | |
134 | ||
135 | //------------------------------------------------------------------------------ | |
136 | // Rule Definition | |
137 | //------------------------------------------------------------------------------ | |
138 | ||
34eeec05 | 139 | /** @type {import('../shared/types').Rule} */ |
eb39fafa DC |
140 | module.exports = { |
141 | meta: { | |
142 | type: "problem", | |
143 | ||
144 | docs: { | |
8f9d1d4d | 145 | description: "Disallow returning values from setters", |
eb39fafa | 146 | recommended: true, |
f2a92ac6 | 147 | url: "https://eslint.org/docs/latest/rules/no-setter-return" |
eb39fafa DC |
148 | }, |
149 | ||
150 | schema: [], | |
151 | ||
152 | messages: { | |
153 | returnsValue: "Setter cannot return a value." | |
154 | } | |
155 | }, | |
156 | ||
157 | create(context) { | |
158 | let funcInfo = null; | |
f2a92ac6 | 159 | const sourceCode = context.sourceCode; |
eb39fafa DC |
160 | |
161 | /** | |
162 | * Creates and pushes to the stack a function info object for the given function node. | |
163 | * @param {ASTNode} node The function node. | |
164 | * @returns {void} | |
165 | */ | |
166 | function enterFunction(node) { | |
f2a92ac6 | 167 | const outerScope = getOuterScope(sourceCode.getScope(node)); |
eb39fafa DC |
168 | |
169 | funcInfo = { | |
170 | upper: funcInfo, | |
171 | isSetter: isSetter(node, outerScope) | |
172 | }; | |
173 | } | |
174 | ||
175 | /** | |
176 | * Pops the current function info object from the stack. | |
177 | * @returns {void} | |
178 | */ | |
179 | function exitFunction() { | |
180 | funcInfo = funcInfo.upper; | |
181 | } | |
182 | ||
183 | /** | |
184 | * Reports the given node. | |
185 | * @param {ASTNode} node Node to report. | |
186 | * @returns {void} | |
187 | */ | |
188 | function report(node) { | |
189 | context.report({ node, messageId: "returnsValue" }); | |
190 | } | |
191 | ||
192 | return { | |
193 | ||
194 | /* | |
195 | * Function declarations cannot be setters, but we still have to track them in the `funcInfo` stack to avoid | |
196 | * false positives, because a ReturnStatement node can belong to a function declaration inside a setter. | |
197 | * | |
198 | * Note: A previously declared function can be referenced and actually used as a setter in a property descriptor, | |
199 | * but that's out of scope for this rule. | |
200 | */ | |
201 | FunctionDeclaration: enterFunction, | |
202 | FunctionExpression: enterFunction, | |
203 | ArrowFunctionExpression(node) { | |
204 | enterFunction(node); | |
205 | ||
206 | if (funcInfo.isSetter && node.expression) { | |
207 | ||
208 | // { set: foo => bar } property descriptor. Report implicit return 'bar' as the equivalent for a return statement. | |
209 | report(node.body); | |
210 | } | |
211 | }, | |
212 | ||
213 | "FunctionDeclaration:exit": exitFunction, | |
214 | "FunctionExpression:exit": exitFunction, | |
215 | "ArrowFunctionExpression:exit": exitFunction, | |
216 | ||
217 | ReturnStatement(node) { | |
218 | ||
219 | // Global returns (e.g., at the top level of a Node module) don't have `funcInfo`. | |
220 | if (funcInfo && funcInfo.isSetter && node.argument) { | |
221 | report(node); | |
222 | } | |
223 | } | |
224 | }; | |
225 | } | |
226 | }; |