2 * @fileoverview Rule to enforce getter and setter pairs in objects and classes.
3 * @author Gyandeep Singh
8 //------------------------------------------------------------------------------
10 //------------------------------------------------------------------------------
12 const astUtils = require("./utils/ast-utils");
14 //------------------------------------------------------------------------------
16 //------------------------------------------------------------------------------
19 * Property name if it can be computed statically, otherwise the list of the tokens of the key node.
20 * @typedef {string|Token[]} Key
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.
31 //------------------------------------------------------------------------------
33 //------------------------------------------------------------------------------
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.
42 function areEqualTokenLists(left, right) {
43 if (left.length !== right.length) {
47 for (let i = 0; i < left.length; i++) {
48 const leftToken = left[i],
49 rightToken = right[i];
51 if (leftToken.type !== rightToken.type || leftToken.value !== rightToken.value) {
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.
65 function areEqualKeys(left, right) {
66 if (typeof left === "string" && typeof right === "string") {
68 // Statically computed names.
69 return left === right;
71 if (Array.isArray(left) && Array.isArray(right)) {
74 return areEqualTokenLists(left, right);
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.
85 function isAccessorKind(node) {
86 return node.kind === "get" || node.kind === "set";
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.
97 function isArgumentOfMethodCall(node, index, object, property) {
98 const parent = node.parent;
101 parent.type === "CallExpression" &&
102 astUtils.isSpecificMemberAccess(parent.callee, object, property) &&
103 parent.arguments[index] === node
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.
112 function isPropertyDescriptor(node) {
114 // Object.defineProperty(obj, "foo", {set: ...})
115 if (isArgumentOfMethodCall(node, 2, "Object", "defineProperty") ||
116 isArgumentOfMethodCall(node, 2, "Reflect", "defineProperty")
122 * Object.defineProperties(obj, {foo: {set: ...}})
123 * Object.create(proto, {foo: {set: ...}})
125 const grandparent = node.parent.parent;
127 return grandparent.type === "ObjectExpression" && (
128 isArgumentOfMethodCall(grandparent, 1, "Object", "create") ||
129 isArgumentOfMethodCall(grandparent, 1, "Object", "defineProperties")
133 //------------------------------------------------------------------------------
135 //------------------------------------------------------------------------------
142 description: "enforce getter and setter pairs in objects and classes",
143 category: "Best Practices",
145 url: "https://eslint.org/docs/rules/accessor-pairs"
159 enforceForClassMembers: {
164 additionalProperties: false
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 }}."
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();
184 * Reports the given node.
185 * @param {ASTNode} node The node to report.
186 * @param {string} messageKind "missingGetter" or "missingSetter".
190 function report(node, messageKind) {
191 if (node.type === "Property") {
194 messageId: `${messageKind}InObjectLiteral`,
195 loc: astUtils.getFunctionHeadLoc(node.value, sourceCode),
196 data: { name: astUtils.getFunctionNameWithKind(node.value) }
198 } else if (node.type === "MethodDefinition") {
201 messageId: `${messageKind}InClass`,
202 loc: astUtils.getFunctionHeadLoc(node.value, sourceCode),
203 data: { name: astUtils.getFunctionNameWithKind(node.value) }
208 messageId: `${messageKind}InPropertyDescriptor`
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".
220 function reportList(nodes, messageKind) {
221 for (const node of nodes) {
222 report(node, messageKind);
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.
232 function createAccessorData(node) {
233 const name = astUtils.getStaticPropertyName(node);
234 const key = (name !== null) ? name : sourceCode.getTokens(node.key);
238 getters: node.kind === "get" ? [node] : [],
239 setters: node.kind === "set" ? [node] : []
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.
250 function mergeAccessorData(accessors, accessorData) {
251 const equalKeyElement = accessors.find(a => areEqualKeys(a.key, accessorData.key));
253 if (equalKeyElement) {
254 equalKeyElement.getters.push(...accessorData.getters);
255 equalKeyElement.setters.push(...accessorData.setters);
257 accessors.push(accessorData);
264 * Checks accessor pairs in the given list of nodes.
265 * @param {ASTNode[]} nodes The list to check.
269 function checkList(nodes) {
270 const accessors = nodes
271 .filter(isAccessorKind)
272 .map(createAccessorData)
273 .reduce(mergeAccessorData, []);
275 for (const { getters, setters } of accessors) {
276 if (checkSetWithoutGet && setters.length && !getters.length) {
277 reportList(setters, "missingGetter");
279 if (checkGetWithoutSet && getters.length && !setters.length) {
280 reportList(getters, "missingSetter");
286 * Checks accessor pairs in an object literal.
287 * @param {ASTNode} node `ObjectExpression` node to check.
291 function checkObjectLiteral(node) {
292 checkList(node.properties.filter(p => p.type === "Property"));
296 * Checks accessor pairs in a property descriptor.
297 * @param {ASTNode} node Property descriptor `ObjectExpression` node to check.
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);
306 const hasGetter = namesToCheck.includes("get");
307 const hasSetter = namesToCheck.includes("set");
309 if (checkSetWithoutGet && hasSetter && !hasGetter) {
310 report(node, "missingGetter");
312 if (checkGetWithoutSet && hasGetter && !hasSetter) {
313 report(node, "missingSetter");
318 * Checks the given object expression as an object literal and as a possible property descriptor.
319 * @param {ASTNode} node `ObjectExpression` node to check.
323 function checkObjectExpression(node) {
324 checkObjectLiteral(node);
325 if (isPropertyDescriptor(node)) {
326 checkPropertyDescriptor(node);
331 * Checks the given class body.
332 * @param {ASTNode} node `ClassBody` node to check.
336 function checkClassBody(node) {
337 const methodDefinitions = node.body.filter(m => m.type === "MethodDefinition");
339 checkList(methodDefinitions.filter(m => m.static));
340 checkList(methodDefinitions.filter(m => !m.static));
343 const listeners = {};
345 if (checkSetWithoutGet || checkGetWithoutSet) {
346 listeners.ObjectExpression = checkObjectExpression;
347 if (enforceForClassMembers) {
348 listeners.ClassBody = checkClassBody;