1/** 2 * @fileoverview Rule to require grouped accessor pairs in object literals and classes 3 * @author Milos Djermanovic 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// Rule Definition 91//------------------------------------------------------------------------------ 92 93module.exports = { 94 meta: { 95 type: "suggestion", 96 97 docs: { 98 description: "require grouped accessor pairs in object literals and classes", 99 category: "Best Practices", 100 recommended: false, 101 url: "https://eslint.org/docs/rules/grouped-accessor-pairs" 102 }, 103 104 schema: [ 105 { 106 enum: ["anyOrder", "getBeforeSet", "setBeforeGet"] 107 } 108 ], 109 110 messages: { 111 notGrouped: "Accessor pair {{ formerName }} and {{ latterName }} should be grouped.", 112 invalidOrder: "Expected {{ latterName }} to be before {{ formerName }}." 113 } 114 }, 115 116 create(context) { 117 const order = context.options[0] || "anyOrder"; 118 const sourceCode = context.getSourceCode(); 119 120 /** 121 * Reports the given accessor pair. 122 * @param {string} messageId messageId to report. 123 * @param {ASTNode} formerNode getter/setter node that is defined before `latterNode`. 124 * @param {ASTNode} latterNode getter/setter node that is defined after `formerNode`. 125 * @returns {void} 126 * @private 127 */ 128 function report(messageId, formerNode, latterNode) { 129 context.report({ 130 node: latterNode, 131 messageId, 132 loc: astUtils.getFunctionHeadLoc(latterNode.value, sourceCode), 133 data: { 134 formerName: astUtils.getFunctionNameWithKind(formerNode.value), 135 latterName: astUtils.getFunctionNameWithKind(latterNode.value) 136 } 137 }); 138 } 139 140 /** 141 * Creates a new `AccessorData` object for the given getter or setter node. 142 * @param {ASTNode} node A getter or setter node. 143 * @returns {AccessorData} New `AccessorData` object that contains the given node. 144 * @private 145 */ 146 function createAccessorData(node) { 147 const name = astUtils.getStaticPropertyName(node); 148 const key = (name !== null) ? name : sourceCode.getTokens(node.key); 149 150 return { 151 key, 152 getters: node.kind === "get" ? [node] : [], 153 setters: node.kind === "set" ? [node] : [] 154 }; 155 } 156 157 /** 158 * Merges the given `AccessorData` object into the given accessors list. 159 * @param {AccessorData[]} accessors The list to merge into. 160 * @param {AccessorData} accessorData The object to merge. 161 * @returns {AccessorData[]} The same instance with the merged object. 162 * @private 163 */ 164 function mergeAccessorData(accessors, accessorData) { 165 const equalKeyElement = accessors.find(a => areEqualKeys(a.key, accessorData.key)); 166 167 if (equalKeyElement) { 168 equalKeyElement.getters.push(...accessorData.getters); 169 equalKeyElement.setters.push(...accessorData.setters); 170 } else { 171 accessors.push(accessorData); 172 } 173 174 return accessors; 175 } 176 177 /** 178 * Checks accessor pairs in the given list of nodes. 179 * @param {ASTNode[]} nodes The list to check. 180 * @param {Function} shouldCheck – Predicate that returns `true` if the node should be checked. 181 * @returns {void} 182 * @private 183 */ 184 function checkList(nodes, shouldCheck) { 185 const accessors = nodes 186 .filter(shouldCheck) 187 .filter(isAccessorKind) 188 .map(createAccessorData) 189 .reduce(mergeAccessorData, []); 190 191 for (const { getters, setters } of accessors) { 192 193 // Don't report accessor properties that have duplicate getters or setters. 194 if (getters.length === 1 && setters.length === 1) { 195 const [getter] = getters, 196 [setter] = setters, 197 getterIndex = nodes.indexOf(getter), 198 setterIndex = nodes.indexOf(setter), 199 formerNode = getterIndex < setterIndex ? getter : setter, 200 latterNode = getterIndex < setterIndex ? setter : getter; 201 202 if (Math.abs(getterIndex - setterIndex) > 1) { 203 report("notGrouped", formerNode, latterNode); 204 } else if ( 205 (order === "getBeforeSet" && getterIndex > setterIndex) || 206 (order === "setBeforeGet" && getterIndex < setterIndex) 207 ) { 208 report("invalidOrder", formerNode, latterNode); 209 } 210 } 211 } 212 } 213 214 return { 215 ObjectExpression(node) { 216 checkList(node.properties, n => n.type === "Property"); 217 }, 218 ClassBody(node) { 219 checkList(node.body, n => n.type === "MethodDefinition" && !n.static); 220 checkList(node.body, n => n.type === "MethodDefinition" && n.static); 221 } 222 }; 223 } 224}; 225