• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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};
355