accessor-pairs.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. /**
  2. * @fileoverview Rule to enforce getter and setter pairs in objects and classes.
  3. * @author Gyandeep Singh
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const astUtils = require("./utils/ast-utils");
  10. //------------------------------------------------------------------------------
  11. // Typedefs
  12. //------------------------------------------------------------------------------
  13. /**
  14. * Property name if it can be computed statically, otherwise the list of the tokens of the key node.
  15. * @typedef {string|Token[]} Key
  16. */
  17. /**
  18. * Accessor nodes with the same key.
  19. * @typedef {Object} AccessorData
  20. * @property {Key} key Accessor's key
  21. * @property {ASTNode[]} getters List of getter nodes.
  22. * @property {ASTNode[]} setters List of setter nodes.
  23. */
  24. //------------------------------------------------------------------------------
  25. // Helpers
  26. //------------------------------------------------------------------------------
  27. /**
  28. * Checks whether or not the given lists represent the equal tokens in the same order.
  29. * Tokens are compared by their properties, not by instance.
  30. * @param {Token[]} left First list of tokens.
  31. * @param {Token[]} right Second list of tokens.
  32. * @returns {boolean} `true` if the lists have same tokens.
  33. */
  34. function areEqualTokenLists(left, right) {
  35. if (left.length !== right.length) {
  36. return false;
  37. }
  38. for (let i = 0; i < left.length; i++) {
  39. const leftToken = left[i],
  40. rightToken = right[i];
  41. if (leftToken.type !== rightToken.type || leftToken.value !== rightToken.value) {
  42. return false;
  43. }
  44. }
  45. return true;
  46. }
  47. /**
  48. * Checks whether or not the given keys are equal.
  49. * @param {Key} left First key.
  50. * @param {Key} right Second key.
  51. * @returns {boolean} `true` if the keys are equal.
  52. */
  53. function areEqualKeys(left, right) {
  54. if (typeof left === "string" && typeof right === "string") {
  55. // Statically computed names.
  56. return left === right;
  57. }
  58. if (Array.isArray(left) && Array.isArray(right)) {
  59. // Token lists.
  60. return areEqualTokenLists(left, right);
  61. }
  62. return false;
  63. }
  64. /**
  65. * Checks whether or not a given node is of an accessor kind ('get' or 'set').
  66. * @param {ASTNode} node A node to check.
  67. * @returns {boolean} `true` if the node is of an accessor kind.
  68. */
  69. function isAccessorKind(node) {
  70. return node.kind === "get" || node.kind === "set";
  71. }
  72. /**
  73. * Checks whether or not a given node is an argument of a specified method call.
  74. * @param {ASTNode} node A node to check.
  75. * @param {number} index An expected index of the node in arguments.
  76. * @param {string} object An expected name of the object of the method.
  77. * @param {string} property An expected name of the method.
  78. * @returns {boolean} `true` if the node is an argument of the specified method call.
  79. */
  80. function isArgumentOfMethodCall(node, index, object, property) {
  81. const parent = node.parent;
  82. return (
  83. parent.type === "CallExpression" &&
  84. astUtils.isSpecificMemberAccess(parent.callee, object, property) &&
  85. parent.arguments[index] === node
  86. );
  87. }
  88. /**
  89. * Checks whether or not a given node is a property descriptor.
  90. * @param {ASTNode} node A node to check.
  91. * @returns {boolean} `true` if the node is a property descriptor.
  92. */
  93. function isPropertyDescriptor(node) {
  94. // Object.defineProperty(obj, "foo", {set: ...})
  95. if (isArgumentOfMethodCall(node, 2, "Object", "defineProperty") ||
  96. isArgumentOfMethodCall(node, 2, "Reflect", "defineProperty")
  97. ) {
  98. return true;
  99. }
  100. /*
  101. * Object.defineProperties(obj, {foo: {set: ...}})
  102. * Object.create(proto, {foo: {set: ...}})
  103. */
  104. const grandparent = node.parent.parent;
  105. return grandparent.type === "ObjectExpression" && (
  106. isArgumentOfMethodCall(grandparent, 1, "Object", "create") ||
  107. isArgumentOfMethodCall(grandparent, 1, "Object", "defineProperties")
  108. );
  109. }
  110. //------------------------------------------------------------------------------
  111. // Rule Definition
  112. //------------------------------------------------------------------------------
  113. module.exports = {
  114. meta: {
  115. type: "suggestion",
  116. docs: {
  117. description: "enforce getter and setter pairs in objects and classes",
  118. category: "Best Practices",
  119. recommended: false,
  120. url: "https://eslint.org/docs/rules/accessor-pairs"
  121. },
  122. schema: [{
  123. type: "object",
  124. properties: {
  125. getWithoutSet: {
  126. type: "boolean",
  127. default: false
  128. },
  129. setWithoutGet: {
  130. type: "boolean",
  131. default: true
  132. },
  133. enforceForClassMembers: {
  134. type: "boolean",
  135. default: true
  136. }
  137. },
  138. additionalProperties: false
  139. }],
  140. messages: {
  141. missingGetterInPropertyDescriptor: "Getter is not present in property descriptor.",
  142. missingSetterInPropertyDescriptor: "Setter is not present in property descriptor.",
  143. missingGetterInObjectLiteral: "Getter is not present for {{ name }}.",
  144. missingSetterInObjectLiteral: "Setter is not present for {{ name }}.",
  145. missingGetterInClass: "Getter is not present for class {{ name }}.",
  146. missingSetterInClass: "Setter is not present for class {{ name }}."
  147. }
  148. },
  149. create(context) {
  150. const config = context.options[0] || {};
  151. const checkGetWithoutSet = config.getWithoutSet === true;
  152. const checkSetWithoutGet = config.setWithoutGet !== false;
  153. const enforceForClassMembers = config.enforceForClassMembers !== false;
  154. const sourceCode = context.getSourceCode();
  155. /**
  156. * Reports the given node.
  157. * @param {ASTNode} node The node to report.
  158. * @param {string} messageKind "missingGetter" or "missingSetter".
  159. * @returns {void}
  160. * @private
  161. */
  162. function report(node, messageKind) {
  163. if (node.type === "Property") {
  164. context.report({
  165. node,
  166. messageId: `${messageKind}InObjectLiteral`,
  167. loc: astUtils.getFunctionHeadLoc(node.value, sourceCode),
  168. data: { name: astUtils.getFunctionNameWithKind(node.value) }
  169. });
  170. } else if (node.type === "MethodDefinition") {
  171. context.report({
  172. node,
  173. messageId: `${messageKind}InClass`,
  174. loc: astUtils.getFunctionHeadLoc(node.value, sourceCode),
  175. data: { name: astUtils.getFunctionNameWithKind(node.value) }
  176. });
  177. } else {
  178. context.report({
  179. node,
  180. messageId: `${messageKind}InPropertyDescriptor`
  181. });
  182. }
  183. }
  184. /**
  185. * Reports each of the nodes in the given list using the same messageId.
  186. * @param {ASTNode[]} nodes Nodes to report.
  187. * @param {string} messageKind "missingGetter" or "missingSetter".
  188. * @returns {void}
  189. * @private
  190. */
  191. function reportList(nodes, messageKind) {
  192. for (const node of nodes) {
  193. report(node, messageKind);
  194. }
  195. }
  196. /**
  197. * Creates a new `AccessorData` object for the given getter or setter node.
  198. * @param {ASTNode} node A getter or setter node.
  199. * @returns {AccessorData} New `AccessorData` object that contains the given node.
  200. * @private
  201. */
  202. function createAccessorData(node) {
  203. const name = astUtils.getStaticPropertyName(node);
  204. const key = (name !== null) ? name : sourceCode.getTokens(node.key);
  205. return {
  206. key,
  207. getters: node.kind === "get" ? [node] : [],
  208. setters: node.kind === "set" ? [node] : []
  209. };
  210. }
  211. /**
  212. * Merges the given `AccessorData` object into the given accessors list.
  213. * @param {AccessorData[]} accessors The list to merge into.
  214. * @param {AccessorData} accessorData The object to merge.
  215. * @returns {AccessorData[]} The same instance with the merged object.
  216. * @private
  217. */
  218. function mergeAccessorData(accessors, accessorData) {
  219. const equalKeyElement = accessors.find(a => areEqualKeys(a.key, accessorData.key));
  220. if (equalKeyElement) {
  221. equalKeyElement.getters.push(...accessorData.getters);
  222. equalKeyElement.setters.push(...accessorData.setters);
  223. } else {
  224. accessors.push(accessorData);
  225. }
  226. return accessors;
  227. }
  228. /**
  229. * Checks accessor pairs in the given list of nodes.
  230. * @param {ASTNode[]} nodes The list to check.
  231. * @returns {void}
  232. * @private
  233. */
  234. function checkList(nodes) {
  235. const accessors = nodes
  236. .filter(isAccessorKind)
  237. .map(createAccessorData)
  238. .reduce(mergeAccessorData, []);
  239. for (const { getters, setters } of accessors) {
  240. if (checkSetWithoutGet && setters.length && !getters.length) {
  241. reportList(setters, "missingGetter");
  242. }
  243. if (checkGetWithoutSet && getters.length && !setters.length) {
  244. reportList(getters, "missingSetter");
  245. }
  246. }
  247. }
  248. /**
  249. * Checks accessor pairs in an object literal.
  250. * @param {ASTNode} node `ObjectExpression` node to check.
  251. * @returns {void}
  252. * @private
  253. */
  254. function checkObjectLiteral(node) {
  255. checkList(node.properties.filter(p => p.type === "Property"));
  256. }
  257. /**
  258. * Checks accessor pairs in a property descriptor.
  259. * @param {ASTNode} node Property descriptor `ObjectExpression` node to check.
  260. * @returns {void}
  261. * @private
  262. */
  263. function checkPropertyDescriptor(node) {
  264. const namesToCheck = node.properties
  265. .filter(p => p.type === "Property" && p.kind === "init" && !p.computed)
  266. .map(({ key }) => key.name);
  267. const hasGetter = namesToCheck.includes("get");
  268. const hasSetter = namesToCheck.includes("set");
  269. if (checkSetWithoutGet && hasSetter && !hasGetter) {
  270. report(node, "missingGetter");
  271. }
  272. if (checkGetWithoutSet && hasGetter && !hasSetter) {
  273. report(node, "missingSetter");
  274. }
  275. }
  276. /**
  277. * Checks the given object expression as an object literal and as a possible property descriptor.
  278. * @param {ASTNode} node `ObjectExpression` node to check.
  279. * @returns {void}
  280. * @private
  281. */
  282. function checkObjectExpression(node) {
  283. checkObjectLiteral(node);
  284. if (isPropertyDescriptor(node)) {
  285. checkPropertyDescriptor(node);
  286. }
  287. }
  288. /**
  289. * Checks the given class body.
  290. * @param {ASTNode} node `ClassBody` node to check.
  291. * @returns {void}
  292. * @private
  293. */
  294. function checkClassBody(node) {
  295. const methodDefinitions = node.body.filter(m => m.type === "MethodDefinition");
  296. checkList(methodDefinitions.filter(m => m.static));
  297. checkList(methodDefinitions.filter(m => !m.static));
  298. }
  299. const listeners = {};
  300. if (checkSetWithoutGet || checkGetWithoutSet) {
  301. listeners.ObjectExpression = checkObjectExpression;
  302. if (enforceForClassMembers) {
  303. listeners.ClassBody = checkClassBody;
  304. }
  305. }
  306. return listeners;
  307. }
  308. };