accessor-pairs.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  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. /** @type {import('../shared/types').Rule} */
  114. module.exports = {
  115. meta: {
  116. type: "suggestion",
  117. defaultOptions: [{
  118. enforceForClassMembers: true,
  119. getWithoutSet: false,
  120. setWithoutGet: true
  121. }],
  122. docs: {
  123. description: "Enforce getter and setter pairs in objects and classes",
  124. recommended: false,
  125. url: "https://eslint.org/docs/latest/rules/accessor-pairs"
  126. },
  127. schema: [{
  128. type: "object",
  129. properties: {
  130. getWithoutSet: {
  131. type: "boolean"
  132. },
  133. setWithoutGet: {
  134. type: "boolean"
  135. },
  136. enforceForClassMembers: {
  137. type: "boolean"
  138. }
  139. },
  140. additionalProperties: false
  141. }],
  142. messages: {
  143. missingGetterInPropertyDescriptor: "Getter is not present in property descriptor.",
  144. missingSetterInPropertyDescriptor: "Setter is not present in property descriptor.",
  145. missingGetterInObjectLiteral: "Getter is not present for {{ name }}.",
  146. missingSetterInObjectLiteral: "Setter is not present for {{ name }}.",
  147. missingGetterInClass: "Getter is not present for class {{ name }}.",
  148. missingSetterInClass: "Setter is not present for class {{ name }}."
  149. }
  150. },
  151. create(context) {
  152. const [{
  153. getWithoutSet: checkGetWithoutSet,
  154. setWithoutGet: checkSetWithoutGet,
  155. enforceForClassMembers
  156. }] = context.options;
  157. const sourceCode = context.sourceCode;
  158. /**
  159. * Reports the given node.
  160. * @param {ASTNode} node The node to report.
  161. * @param {string} messageKind "missingGetter" or "missingSetter".
  162. * @returns {void}
  163. * @private
  164. */
  165. function report(node, messageKind) {
  166. if (node.type === "Property") {
  167. context.report({
  168. node,
  169. messageId: `${messageKind}InObjectLiteral`,
  170. loc: astUtils.getFunctionHeadLoc(node.value, sourceCode),
  171. data: { name: astUtils.getFunctionNameWithKind(node.value) }
  172. });
  173. } else if (node.type === "MethodDefinition") {
  174. context.report({
  175. node,
  176. messageId: `${messageKind}InClass`,
  177. loc: astUtils.getFunctionHeadLoc(node.value, sourceCode),
  178. data: { name: astUtils.getFunctionNameWithKind(node.value) }
  179. });
  180. } else {
  181. context.report({
  182. node,
  183. messageId: `${messageKind}InPropertyDescriptor`
  184. });
  185. }
  186. }
  187. /**
  188. * Reports each of the nodes in the given list using the same messageId.
  189. * @param {ASTNode[]} nodes Nodes to report.
  190. * @param {string} messageKind "missingGetter" or "missingSetter".
  191. * @returns {void}
  192. * @private
  193. */
  194. function reportList(nodes, messageKind) {
  195. for (const node of nodes) {
  196. report(node, messageKind);
  197. }
  198. }
  199. /**
  200. * Checks accessor pairs in the given list of nodes.
  201. * @param {ASTNode[]} nodes The list to check.
  202. * @returns {void}
  203. * @private
  204. */
  205. function checkList(nodes) {
  206. const accessors = [];
  207. let found = false;
  208. for (let i = 0; i < nodes.length; i++) {
  209. const node = nodes[i];
  210. if (isAccessorKind(node)) {
  211. // Creates a new `AccessorData` object for the given getter or setter node.
  212. const name = astUtils.getStaticPropertyName(node);
  213. const key = (name !== null) ? name : sourceCode.getTokens(node.key);
  214. // Merges the given `AccessorData` object into the given accessors list.
  215. for (let j = 0; j < accessors.length; j++) {
  216. const accessor = accessors[j];
  217. if (areEqualKeys(accessor.key, key)) {
  218. accessor.getters.push(...node.kind === "get" ? [node] : []);
  219. accessor.setters.push(...node.kind === "set" ? [node] : []);
  220. found = true;
  221. break;
  222. }
  223. }
  224. if (!found) {
  225. accessors.push({
  226. key,
  227. getters: node.kind === "get" ? [node] : [],
  228. setters: node.kind === "set" ? [node] : []
  229. });
  230. }
  231. found = false;
  232. }
  233. }
  234. for (const { getters, setters } of accessors) {
  235. if (checkSetWithoutGet && setters.length && !getters.length) {
  236. reportList(setters, "missingGetter");
  237. }
  238. if (checkGetWithoutSet && getters.length && !setters.length) {
  239. reportList(getters, "missingSetter");
  240. }
  241. }
  242. }
  243. /**
  244. * Checks accessor pairs in an object literal.
  245. * @param {ASTNode} node `ObjectExpression` node to check.
  246. * @returns {void}
  247. * @private
  248. */
  249. function checkObjectLiteral(node) {
  250. checkList(node.properties.filter(p => p.type === "Property"));
  251. }
  252. /**
  253. * Checks accessor pairs in a property descriptor.
  254. * @param {ASTNode} node Property descriptor `ObjectExpression` node to check.
  255. * @returns {void}
  256. * @private
  257. */
  258. function checkPropertyDescriptor(node) {
  259. const namesToCheck = new Set(node.properties
  260. .filter(p => p.type === "Property" && p.kind === "init" && !p.computed)
  261. .map(({ key }) => key.name));
  262. const hasGetter = namesToCheck.has("get");
  263. const hasSetter = namesToCheck.has("set");
  264. if (checkSetWithoutGet && hasSetter && !hasGetter) {
  265. report(node, "missingGetter");
  266. }
  267. if (checkGetWithoutSet && hasGetter && !hasSetter) {
  268. report(node, "missingSetter");
  269. }
  270. }
  271. /**
  272. * Checks the given object expression as an object literal and as a possible property descriptor.
  273. * @param {ASTNode} node `ObjectExpression` node to check.
  274. * @returns {void}
  275. * @private
  276. */
  277. function checkObjectExpression(node) {
  278. checkObjectLiteral(node);
  279. if (isPropertyDescriptor(node)) {
  280. checkPropertyDescriptor(node);
  281. }
  282. }
  283. /**
  284. * Checks the given class body.
  285. * @param {ASTNode} node `ClassBody` node to check.
  286. * @returns {void}
  287. * @private
  288. */
  289. function checkClassBody(node) {
  290. const methodDefinitions = node.body.filter(m => m.type === "MethodDefinition");
  291. checkList(methodDefinitions.filter(m => m.static));
  292. checkList(methodDefinitions.filter(m => !m.static));
  293. }
  294. const listeners = {};
  295. if (checkSetWithoutGet || checkGetWithoutSet) {
  296. listeners.ObjectExpression = checkObjectExpression;
  297. if (enforceForClassMembers) {
  298. listeners.ClassBody = checkClassBody;
  299. }
  300. }
  301. return listeners;
  302. }
  303. };