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