UNPKG

11.1 kBJavaScriptView Raw
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
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 `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 */
95function 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 */
107function 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 */
125function 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
150module.exports = {
151 meta: {
152 type: "suggestion",
153
154 docs: {
155 description: "enforce getter and setter pairs in objects",
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 },
173 additionalProperties: false
174 }],
175
176 messages: {
177 missingGetterInPropertyDescriptor: "Getter is not present in property descriptor.",
178 missingSetterInPropertyDescriptor: "Setter is not present in property descriptor.",
179 missingGetterInObjectLiteral: "Getter is not present for {{ name }}.",
180 missingSetterInObjectLiteral: "Setter is not present for {{ name }}."
181 }
182 },
183 create(context) {
184 const config = context.options[0] || {};
185 const checkGetWithoutSet = config.getWithoutSet === true;
186 const checkSetWithoutGet = config.setWithoutGet !== false;
187 const sourceCode = context.getSourceCode();
188
189 /**
190 * Reports the given node.
191 * @param {ASTNode} node The node to report.
192 * @param {string} messageKind "missingGetter" or "missingSetter".
193 * @returns {void}
194 * @private
195 */
196 function report(node, messageKind) {
197 if (node.type === "Property") {
198 context.report({
199 node,
200 messageId: `${messageKind}InObjectLiteral`,
201 loc: astUtils.getFunctionHeadLoc(node.value, sourceCode),
202 data: { name: astUtils.getFunctionNameWithKind(node.value) }
203 });
204 } else {
205 context.report({
206 node,
207 messageId: `${messageKind}InPropertyDescriptor`
208 });
209 }
210 }
211
212 /**
213 * Reports each of the nodes in the given list using the same messageId.
214 * @param {ASTNode[]} nodes Nodes to report.
215 * @param {string} messageKind "missingGetter" or "missingSetter".
216 * @returns {void}
217 * @private
218 */
219 function reportList(nodes, messageKind) {
220 for (const node of nodes) {
221 report(node, messageKind);
222 }
223 }
224
225 /**
226 * Creates a new `AccessorData` object for the given getter or setter node.
227 * @param {ASTNode} node A getter or setter node.
228 * @returns {AccessorData} New `AccessorData` object that contains the given node.
229 * @private
230 */
231 function createAccessorData(node) {
232 const name = astUtils.getStaticPropertyName(node);
233 const key = (name !== null) ? name : sourceCode.getTokens(node.key);
234
235 return {
236 key,
237 getters: node.kind === "get" ? [node] : [],
238 setters: node.kind === "set" ? [node] : []
239 };
240 }
241
242 /**
243 * Merges the given `AccessorData` object into the given accessors list.
244 * @param {AccessorData[]} accessors The list to merge into.
245 * @param {AccessorData} accessorData The object to merge.
246 * @returns {AccessorData[]} The same instance with the merged object.
247 * @private
248 */
249 function mergeAccessorData(accessors, accessorData) {
250 const equalKeyElement = accessors.find(a => areEqualKeys(a.key, accessorData.key));
251
252 if (equalKeyElement) {
253 equalKeyElement.getters.push(...accessorData.getters);
254 equalKeyElement.setters.push(...accessorData.setters);
255 } else {
256 accessors.push(accessorData);
257 }
258
259 return accessors;
260 }
261
262 /**
263 * Checks accessor pairs in the given list of nodes.
264 * @param {ASTNode[]} nodes The list to check.
265 * @returns {void}
266 * @private
267 */
268 function checkList(nodes) {
269 const accessors = nodes
270 .filter(isAccessorKind)
271 .map(createAccessorData)
272 .reduce(mergeAccessorData, []);
273
274 for (const { getters, setters } of accessors) {
275 if (checkSetWithoutGet && setters.length && !getters.length) {
276 reportList(setters, "missingGetter");
277 }
278 if (checkGetWithoutSet && getters.length && !setters.length) {
279 reportList(getters, "missingSetter");
280 }
281 }
282 }
283
284 /**
285 * Checks accessor pairs in an object literal.
286 * @param {ASTNode} node `ObjectExpression` node to check.
287 * @returns {void}
288 * @private
289 */
290 function checkObjectLiteral(node) {
291 checkList(node.properties.filter(p => p.type === "Property"));
292 }
293
294 /**
295 * Checks accessor pairs in a property descriptor.
296 * @param {ASTNode} node Property descriptor `ObjectExpression` node to check.
297 * @returns {void}
298 * @private
299 */
300 function checkPropertyDescriptor(node) {
301 const namesToCheck = node.properties
302 .filter(p => p.type === "Property" && p.kind === "init" && !p.computed)
303 .map(({ key }) => key.name);
304
305 const hasGetter = namesToCheck.includes("get");
306 const hasSetter = namesToCheck.includes("set");
307
308 if (checkSetWithoutGet && hasSetter && !hasGetter) {
309 report(node, "missingGetter");
310 }
311 if (checkGetWithoutSet && hasGetter && !hasSetter) {
312 report(node, "missingSetter");
313 }
314 }
315
316 return {
317 ObjectExpression(node) {
318 if (checkSetWithoutGet || checkGetWithoutSet) {
319 checkObjectLiteral(node);
320 if (isPropertyDescriptor(node)) {
321 checkPropertyDescriptor(node);
322 }
323 }
324 }
325 };
326 }
327};