]> git.proxmox.com Git - pve-eslint.git/blob - eslint/lib/rules/accessor-pairs.js
first commit
[pve-eslint.git] / eslint / lib / rules / accessor-pairs.js
1 /**
2 * @fileoverview Rule to flag wrapping non-iife in parens
3 * @author Gyandeep Singh
4 */
5
6 "use strict";
7
8 //------------------------------------------------------------------------------
9 // Requirements
10 //------------------------------------------------------------------------------
11
12 const astUtils = require("./utils/ast-utils");
13
14 //------------------------------------------------------------------------------
15 // Typedefs
16 //------------------------------------------------------------------------------
17
18 /**
19 * Property name if it can be computed statically, otherwise the list of the tokens of the key node.
20 * @typedef {string|Token[]} Key
21 */
22
23 /**
24 * Accessor nodes with the same key.
25 * @typedef {Object} AccessorData
26 * @property {Key} key Accessor's key
27 * @property {ASTNode[]} getters List of getter nodes.
28 * @property {ASTNode[]} setters List of setter nodes.
29 */
30
31 //------------------------------------------------------------------------------
32 // Helpers
33 //------------------------------------------------------------------------------
34
35 /**
36 * Checks whether or not the given lists represent the equal tokens in the same order.
37 * Tokens are compared by their properties, not by instance.
38 * @param {Token[]} left First list of tokens.
39 * @param {Token[]} right Second list of tokens.
40 * @returns {boolean} `true` if the lists have same tokens.
41 */
42 function areEqualTokenLists(left, right) {
43 if (left.length !== right.length) {
44 return false;
45 }
46
47 for (let i = 0; i < left.length; i++) {
48 const leftToken = left[i],
49 rightToken = right[i];
50
51 if (leftToken.type !== rightToken.type || leftToken.value !== rightToken.value) {
52 return false;
53 }
54 }
55
56 return true;
57 }
58
59 /**
60 * Checks whether or not the given keys are equal.
61 * @param {Key} left First key.
62 * @param {Key} right Second key.
63 * @returns {boolean} `true` if the keys are equal.
64 */
65 function areEqualKeys(left, right) {
66 if (typeof left === "string" && typeof right === "string") {
67
68 // Statically computed names.
69 return left === right;
70 }
71 if (Array.isArray(left) && Array.isArray(right)) {
72
73 // Token lists.
74 return areEqualTokenLists(left, right);
75 }
76
77 return false;
78 }
79
80 /**
81 * Checks whether or not a given node is of an accessor kind ('get' or 'set').
82 * @param {ASTNode} node A node to check.
83 * @returns {boolean} `true` if the node is of an accessor kind.
84 */
85 function isAccessorKind(node) {
86 return node.kind === "get" || node.kind === "set";
87 }
88
89 /**
90 * Checks whether or not a given node is an `Identifier` node which was named a given name.
91 * @param {ASTNode} node A node to check.
92 * @param {string} name An expected name of the node.
93 * @returns {boolean} `true` if the node is an `Identifier` node which was named as expected.
94 */
95 function isIdentifier(node, name) {
96 return node.type === "Identifier" && node.name === name;
97 }
98
99 /**
100 * Checks whether or not a given node is an argument of a specified method call.
101 * @param {ASTNode} node A node to check.
102 * @param {number} index An expected index of the node in arguments.
103 * @param {string} object An expected name of the object of the method.
104 * @param {string} property An expected name of the method.
105 * @returns {boolean} `true` if the node is an argument of the specified method call.
106 */
107 function isArgumentOfMethodCall(node, index, object, property) {
108 const parent = node.parent;
109
110 return (
111 parent.type === "CallExpression" &&
112 parent.callee.type === "MemberExpression" &&
113 parent.callee.computed === false &&
114 isIdentifier(parent.callee.object, object) &&
115 isIdentifier(parent.callee.property, property) &&
116 parent.arguments[index] === node
117 );
118 }
119
120 /**
121 * Checks whether or not a given node is a property descriptor.
122 * @param {ASTNode} node A node to check.
123 * @returns {boolean} `true` if the node is a property descriptor.
124 */
125 function isPropertyDescriptor(node) {
126
127 // Object.defineProperty(obj, "foo", {set: ...})
128 if (isArgumentOfMethodCall(node, 2, "Object", "defineProperty") ||
129 isArgumentOfMethodCall(node, 2, "Reflect", "defineProperty")
130 ) {
131 return true;
132 }
133
134 /*
135 * Object.defineProperties(obj, {foo: {set: ...}})
136 * Object.create(proto, {foo: {set: ...}})
137 */
138 const grandparent = node.parent.parent;
139
140 return grandparent.type === "ObjectExpression" && (
141 isArgumentOfMethodCall(grandparent, 1, "Object", "create") ||
142 isArgumentOfMethodCall(grandparent, 1, "Object", "defineProperties")
143 );
144 }
145
146 //------------------------------------------------------------------------------
147 // Rule Definition
148 //------------------------------------------------------------------------------
149
150 module.exports = {
151 meta: {
152 type: "suggestion",
153
154 docs: {
155 description: "enforce getter and setter pairs in objects and classes",
156 category: "Best Practices",
157 recommended: false,
158 url: "https://eslint.org/docs/rules/accessor-pairs"
159 },
160
161 schema: [{
162 type: "object",
163 properties: {
164 getWithoutSet: {
165 type: "boolean",
166 default: false
167 },
168 setWithoutGet: {
169 type: "boolean",
170 default: true
171 },
172 enforceForClassMembers: {
173 type: "boolean",
174 default: true
175 }
176 },
177 additionalProperties: false
178 }],
179
180 messages: {
181 missingGetterInPropertyDescriptor: "Getter is not present in property descriptor.",
182 missingSetterInPropertyDescriptor: "Setter is not present in property descriptor.",
183 missingGetterInObjectLiteral: "Getter is not present for {{ name }}.",
184 missingSetterInObjectLiteral: "Setter is not present for {{ name }}.",
185 missingGetterInClass: "Getter is not present for class {{ name }}.",
186 missingSetterInClass: "Setter is not present for class {{ name }}."
187 }
188 },
189 create(context) {
190 const config = context.options[0] || {};
191 const checkGetWithoutSet = config.getWithoutSet === true;
192 const checkSetWithoutGet = config.setWithoutGet !== false;
193 const enforceForClassMembers = config.enforceForClassMembers !== false;
194 const sourceCode = context.getSourceCode();
195
196 /**
197 * Reports the given node.
198 * @param {ASTNode} node The node to report.
199 * @param {string} messageKind "missingGetter" or "missingSetter".
200 * @returns {void}
201 * @private
202 */
203 function report(node, messageKind) {
204 if (node.type === "Property") {
205 context.report({
206 node,
207 messageId: `${messageKind}InObjectLiteral`,
208 loc: astUtils.getFunctionHeadLoc(node.value, sourceCode),
209 data: { name: astUtils.getFunctionNameWithKind(node.value) }
210 });
211 } else if (node.type === "MethodDefinition") {
212 context.report({
213 node,
214 messageId: `${messageKind}InClass`,
215 loc: astUtils.getFunctionHeadLoc(node.value, sourceCode),
216 data: { name: astUtils.getFunctionNameWithKind(node.value) }
217 });
218 } else {
219 context.report({
220 node,
221 messageId: `${messageKind}InPropertyDescriptor`
222 });
223 }
224 }
225
226 /**
227 * Reports each of the nodes in the given list using the same messageId.
228 * @param {ASTNode[]} nodes Nodes to report.
229 * @param {string} messageKind "missingGetter" or "missingSetter".
230 * @returns {void}
231 * @private
232 */
233 function reportList(nodes, messageKind) {
234 for (const node of nodes) {
235 report(node, messageKind);
236 }
237 }
238
239 /**
240 * Creates a new `AccessorData` object for the given getter or setter node.
241 * @param {ASTNode} node A getter or setter node.
242 * @returns {AccessorData} New `AccessorData` object that contains the given node.
243 * @private
244 */
245 function createAccessorData(node) {
246 const name = astUtils.getStaticPropertyName(node);
247 const key = (name !== null) ? name : sourceCode.getTokens(node.key);
248
249 return {
250 key,
251 getters: node.kind === "get" ? [node] : [],
252 setters: node.kind === "set" ? [node] : []
253 };
254 }
255
256 /**
257 * Merges the given `AccessorData` object into the given accessors list.
258 * @param {AccessorData[]} accessors The list to merge into.
259 * @param {AccessorData} accessorData The object to merge.
260 * @returns {AccessorData[]} The same instance with the merged object.
261 * @private
262 */
263 function mergeAccessorData(accessors, accessorData) {
264 const equalKeyElement = accessors.find(a => areEqualKeys(a.key, accessorData.key));
265
266 if (equalKeyElement) {
267 equalKeyElement.getters.push(...accessorData.getters);
268 equalKeyElement.setters.push(...accessorData.setters);
269 } else {
270 accessors.push(accessorData);
271 }
272
273 return accessors;
274 }
275
276 /**
277 * Checks accessor pairs in the given list of nodes.
278 * @param {ASTNode[]} nodes The list to check.
279 * @returns {void}
280 * @private
281 */
282 function checkList(nodes) {
283 const accessors = nodes
284 .filter(isAccessorKind)
285 .map(createAccessorData)
286 .reduce(mergeAccessorData, []);
287
288 for (const { getters, setters } of accessors) {
289 if (checkSetWithoutGet && setters.length && !getters.length) {
290 reportList(setters, "missingGetter");
291 }
292 if (checkGetWithoutSet && getters.length && !setters.length) {
293 reportList(getters, "missingSetter");
294 }
295 }
296 }
297
298 /**
299 * Checks accessor pairs in an object literal.
300 * @param {ASTNode} node `ObjectExpression` node to check.
301 * @returns {void}
302 * @private
303 */
304 function checkObjectLiteral(node) {
305 checkList(node.properties.filter(p => p.type === "Property"));
306 }
307
308 /**
309 * Checks accessor pairs in a property descriptor.
310 * @param {ASTNode} node Property descriptor `ObjectExpression` node to check.
311 * @returns {void}
312 * @private
313 */
314 function checkPropertyDescriptor(node) {
315 const namesToCheck = node.properties
316 .filter(p => p.type === "Property" && p.kind === "init" && !p.computed)
317 .map(({ key }) => key.name);
318
319 const hasGetter = namesToCheck.includes("get");
320 const hasSetter = namesToCheck.includes("set");
321
322 if (checkSetWithoutGet && hasSetter && !hasGetter) {
323 report(node, "missingGetter");
324 }
325 if (checkGetWithoutSet && hasGetter && !hasSetter) {
326 report(node, "missingSetter");
327 }
328 }
329
330 /**
331 * Checks the given object expression as an object literal and as a possible property descriptor.
332 * @param {ASTNode} node `ObjectExpression` node to check.
333 * @returns {void}
334 * @private
335 */
336 function checkObjectExpression(node) {
337 checkObjectLiteral(node);
338 if (isPropertyDescriptor(node)) {
339 checkPropertyDescriptor(node);
340 }
341 }
342
343 /**
344 * Checks the given class body.
345 * @param {ASTNode} node `ClassBody` node to check.
346 * @returns {void}
347 * @private
348 */
349 function checkClassBody(node) {
350 const methodDefinitions = node.body.filter(m => m.type === "MethodDefinition");
351
352 checkList(methodDefinitions.filter(m => m.static));
353 checkList(methodDefinitions.filter(m => !m.static));
354 }
355
356 const listeners = {};
357
358 if (checkSetWithoutGet || checkGetWithoutSet) {
359 listeners.ObjectExpression = checkObjectExpression;
360 if (enforceForClassMembers) {
361 listeners.ClassBody = checkClassBody;
362 }
363 }
364
365 return listeners;
366 }
367 };