UNPKG

12.2 kBJavaScriptView Raw
1/**
2 * @fileoverview Rule to enforce getter and setter pairs in objects and classes.
3 * @author Gyandeep Singh
4 */
5
6"use strict";
7
8//------------------------------------------------------------------------------
9// Requirements
10//------------------------------------------------------------------------------
11
12const 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 */
42function 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 */
65function 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 */
85function isAccessorKind(node) {
86 return node.kind === "get" || node.kind === "set";
87}
88
89/**
90 * Checks whether or not a given node is an argument of a specified method call.
91 * @param {ASTNode} node A node to check.
92 * @param {number} index An expected index of the node in arguments.
93 * @param {string} object An expected name of the object of the method.
94 * @param {string} property An expected name of the method.
95 * @returns {boolean} `true` if the node is an argument of the specified method call.
96 */
97function isArgumentOfMethodCall(node, index, object, property) {
98 const parent = node.parent;
99
100 return (
101 parent.type === "CallExpression" &&
102 astUtils.isSpecificMemberAccess(parent.callee, object, property) &&
103 parent.arguments[index] === node
104 );
105}
106
107/**
108 * Checks whether or not a given node is a property descriptor.
109 * @param {ASTNode} node A node to check.
110 * @returns {boolean} `true` if the node is a property descriptor.
111 */
112function isPropertyDescriptor(node) {
113
114 // Object.defineProperty(obj, "foo", {set: ...})
115 if (isArgumentOfMethodCall(node, 2, "Object", "defineProperty") ||
116 isArgumentOfMethodCall(node, 2, "Reflect", "defineProperty")
117 ) {
118 return true;
119 }
120
121 /*
122 * Object.defineProperties(obj, {foo: {set: ...}})
123 * Object.create(proto, {foo: {set: ...}})
124 */
125 const grandparent = node.parent.parent;
126
127 return grandparent.type === "ObjectExpression" && (
128 isArgumentOfMethodCall(grandparent, 1, "Object", "create") ||
129 isArgumentOfMethodCall(grandparent, 1, "Object", "defineProperties")
130 );
131}
132
133//------------------------------------------------------------------------------
134// Rule Definition
135//------------------------------------------------------------------------------
136
137module.exports = {
138 meta: {
139 type: "suggestion",
140
141 docs: {
142 description: "enforce getter and setter pairs in objects and classes",
143 category: "Best Practices",
144 recommended: false,
145 url: "https://eslint.org/docs/rules/accessor-pairs"
146 },
147
148 schema: [{
149 type: "object",
150 properties: {
151 getWithoutSet: {
152 type: "boolean",
153 default: false
154 },
155 setWithoutGet: {
156 type: "boolean",
157 default: true
158 },
159 enforceForClassMembers: {
160 type: "boolean",
161 default: true
162 }
163 },
164 additionalProperties: false
165 }],
166
167 messages: {
168 missingGetterInPropertyDescriptor: "Getter is not present in property descriptor.",
169 missingSetterInPropertyDescriptor: "Setter is not present in property descriptor.",
170 missingGetterInObjectLiteral: "Getter is not present for {{ name }}.",
171 missingSetterInObjectLiteral: "Setter is not present for {{ name }}.",
172 missingGetterInClass: "Getter is not present for class {{ name }}.",
173 missingSetterInClass: "Setter is not present for class {{ name }}."
174 }
175 },
176 create(context) {
177 const config = context.options[0] || {};
178 const checkGetWithoutSet = config.getWithoutSet === true;
179 const checkSetWithoutGet = config.setWithoutGet !== false;
180 const enforceForClassMembers = config.enforceForClassMembers !== false;
181 const sourceCode = context.getSourceCode();
182
183 /**
184 * Reports the given node.
185 * @param {ASTNode} node The node to report.
186 * @param {string} messageKind "missingGetter" or "missingSetter".
187 * @returns {void}
188 * @private
189 */
190 function report(node, messageKind) {
191 if (node.type === "Property") {
192 context.report({
193 node,
194 messageId: `${messageKind}InObjectLiteral`,
195 loc: astUtils.getFunctionHeadLoc(node.value, sourceCode),
196 data: { name: astUtils.getFunctionNameWithKind(node.value) }
197 });
198 } else if (node.type === "MethodDefinition") {
199 context.report({
200 node,
201 messageId: `${messageKind}InClass`,
202 loc: astUtils.getFunctionHeadLoc(node.value, sourceCode),
203 data: { name: astUtils.getFunctionNameWithKind(node.value) }
204 });
205 } else {
206 context.report({
207 node,
208 messageId: `${messageKind}InPropertyDescriptor`
209 });
210 }
211 }
212
213 /**
214 * Reports each of the nodes in the given list using the same messageId.
215 * @param {ASTNode[]} nodes Nodes to report.
216 * @param {string} messageKind "missingGetter" or "missingSetter".
217 * @returns {void}
218 * @private
219 */
220 function reportList(nodes, messageKind) {
221 for (const node of nodes) {
222 report(node, messageKind);
223 }
224 }
225
226 /**
227 * Creates a new `AccessorData` object for the given getter or setter node.
228 * @param {ASTNode} node A getter or setter node.
229 * @returns {AccessorData} New `AccessorData` object that contains the given node.
230 * @private
231 */
232 function createAccessorData(node) {
233 const name = astUtils.getStaticPropertyName(node);
234 const key = (name !== null) ? name : sourceCode.getTokens(node.key);
235
236 return {
237 key,
238 getters: node.kind === "get" ? [node] : [],
239 setters: node.kind === "set" ? [node] : []
240 };
241 }
242
243 /**
244 * Merges the given `AccessorData` object into the given accessors list.
245 * @param {AccessorData[]} accessors The list to merge into.
246 * @param {AccessorData} accessorData The object to merge.
247 * @returns {AccessorData[]} The same instance with the merged object.
248 * @private
249 */
250 function mergeAccessorData(accessors, accessorData) {
251 const equalKeyElement = accessors.find(a => areEqualKeys(a.key, accessorData.key));
252
253 if (equalKeyElement) {
254 equalKeyElement.getters.push(...accessorData.getters);
255 equalKeyElement.setters.push(...accessorData.setters);
256 } else {
257 accessors.push(accessorData);
258 }
259
260 return accessors;
261 }
262
263 /**
264 * Checks accessor pairs in the given list of nodes.
265 * @param {ASTNode[]} nodes The list to check.
266 * @returns {void}
267 * @private
268 */
269 function checkList(nodes) {
270 const accessors = nodes
271 .filter(isAccessorKind)
272 .map(createAccessorData)
273 .reduce(mergeAccessorData, []);
274
275 for (const { getters, setters } of accessors) {
276 if (checkSetWithoutGet && setters.length && !getters.length) {
277 reportList(setters, "missingGetter");
278 }
279 if (checkGetWithoutSet && getters.length && !setters.length) {
280 reportList(getters, "missingSetter");
281 }
282 }
283 }
284
285 /**
286 * Checks accessor pairs in an object literal.
287 * @param {ASTNode} node `ObjectExpression` node to check.
288 * @returns {void}
289 * @private
290 */
291 function checkObjectLiteral(node) {
292 checkList(node.properties.filter(p => p.type === "Property"));
293 }
294
295 /**
296 * Checks accessor pairs in a property descriptor.
297 * @param {ASTNode} node Property descriptor `ObjectExpression` node to check.
298 * @returns {void}
299 * @private
300 */
301 function checkPropertyDescriptor(node) {
302 const namesToCheck = node.properties
303 .filter(p => p.type === "Property" && p.kind === "init" && !p.computed)
304 .map(({ key }) => key.name);
305
306 const hasGetter = namesToCheck.includes("get");
307 const hasSetter = namesToCheck.includes("set");
308
309 if (checkSetWithoutGet && hasSetter && !hasGetter) {
310 report(node, "missingGetter");
311 }
312 if (checkGetWithoutSet && hasGetter && !hasSetter) {
313 report(node, "missingSetter");
314 }
315 }
316
317 /**
318 * Checks the given object expression as an object literal and as a possible property descriptor.
319 * @param {ASTNode} node `ObjectExpression` node to check.
320 * @returns {void}
321 * @private
322 */
323 function checkObjectExpression(node) {
324 checkObjectLiteral(node);
325 if (isPropertyDescriptor(node)) {
326 checkPropertyDescriptor(node);
327 }
328 }
329
330 /**
331 * Checks the given class body.
332 * @param {ASTNode} node `ClassBody` node to check.
333 * @returns {void}
334 * @private
335 */
336 function checkClassBody(node) {
337 const methodDefinitions = node.body.filter(m => m.type === "MethodDefinition");
338
339 checkList(methodDefinitions.filter(m => m.static));
340 checkList(methodDefinitions.filter(m => !m.static));
341 }
342
343 const listeners = {};
344
345 if (checkSetWithoutGet || checkGetWithoutSet) {
346 listeners.ObjectExpression = checkObjectExpression;
347 if (enforceForClassMembers) {
348 listeners.ClassBody = checkClassBody;
349 }
350 }
351
352 return listeners;
353 }
354};