accessor-pairs.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  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 (
  42. leftToken.type !== rightToken.type ||
  43. leftToken.value !== rightToken.value
  44. ) {
  45. return false;
  46. }
  47. }
  48. return true;
  49. }
  50. /**
  51. * Checks whether or not the given keys are equal.
  52. * @param {Key} left First key.
  53. * @param {Key} right Second key.
  54. * @returns {boolean} `true` if the keys are equal.
  55. */
  56. function areEqualKeys(left, right) {
  57. if (typeof left === "string" && typeof right === "string") {
  58. // Statically computed names.
  59. return left === right;
  60. }
  61. if (Array.isArray(left) && Array.isArray(right)) {
  62. // Token lists.
  63. return areEqualTokenLists(left, right);
  64. }
  65. return false;
  66. }
  67. /**
  68. * Checks whether or not a given node is of an accessor kind ('get' or 'set').
  69. * @param {ASTNode} node A node to check.
  70. * @returns {boolean} `true` if the node is of an accessor kind.
  71. */
  72. function isAccessorKind(node) {
  73. return node.kind === "get" || node.kind === "set";
  74. }
  75. /**
  76. * Checks whether or not a given node is an argument of a specified method call.
  77. * @param {ASTNode} node A node to check.
  78. * @param {number} index An expected index of the node in arguments.
  79. * @param {string} object An expected name of the object of the method.
  80. * @param {string} property An expected name of the method.
  81. * @returns {boolean} `true` if the node is an argument of the specified method call.
  82. */
  83. function isArgumentOfMethodCall(node, index, object, property) {
  84. const parent = node.parent;
  85. return (
  86. parent.type === "CallExpression" &&
  87. astUtils.isSpecificMemberAccess(parent.callee, object, property) &&
  88. parent.arguments[index] === node
  89. );
  90. }
  91. /**
  92. * Checks whether or not a given node is a property descriptor.
  93. * @param {ASTNode} node A node to check.
  94. * @returns {boolean} `true` if the node is a property descriptor.
  95. */
  96. function isPropertyDescriptor(node) {
  97. // Object.defineProperty(obj, "foo", {set: ...})
  98. if (
  99. isArgumentOfMethodCall(node, 2, "Object", "defineProperty") ||
  100. isArgumentOfMethodCall(node, 2, "Reflect", "defineProperty")
  101. ) {
  102. return true;
  103. }
  104. /*
  105. * Object.defineProperties(obj, {foo: {set: ...}})
  106. * Object.create(proto, {foo: {set: ...}})
  107. */
  108. const grandparent = node.parent.parent;
  109. return (
  110. grandparent.type === "ObjectExpression" &&
  111. (isArgumentOfMethodCall(grandparent, 1, "Object", "create") ||
  112. isArgumentOfMethodCall(
  113. grandparent,
  114. 1,
  115. "Object",
  116. "defineProperties",
  117. ))
  118. );
  119. }
  120. //------------------------------------------------------------------------------
  121. // Rule Definition
  122. //------------------------------------------------------------------------------
  123. /** @type {import('../types').Rule.RuleModule} */
  124. module.exports = {
  125. meta: {
  126. type: "suggestion",
  127. defaultOptions: [
  128. {
  129. enforceForTSTypes: false,
  130. enforceForClassMembers: true,
  131. getWithoutSet: false,
  132. setWithoutGet: true,
  133. },
  134. ],
  135. docs: {
  136. description:
  137. "Enforce getter and setter pairs in objects and classes",
  138. recommended: false,
  139. url: "https://eslint.org/docs/latest/rules/accessor-pairs",
  140. },
  141. schema: [
  142. {
  143. type: "object",
  144. properties: {
  145. getWithoutSet: {
  146. type: "boolean",
  147. },
  148. setWithoutGet: {
  149. type: "boolean",
  150. },
  151. enforceForClassMembers: {
  152. type: "boolean",
  153. },
  154. enforceForTSTypes: {
  155. type: "boolean",
  156. },
  157. },
  158. additionalProperties: false,
  159. },
  160. ],
  161. messages: {
  162. missingGetterInPropertyDescriptor:
  163. "Getter is not present in property descriptor.",
  164. missingSetterInPropertyDescriptor:
  165. "Setter is not present in property descriptor.",
  166. missingGetterInObjectLiteral:
  167. "Getter is not present for {{ name }}.",
  168. missingSetterInObjectLiteral:
  169. "Setter is not present for {{ name }}.",
  170. missingGetterInClass: "Getter is not present for class {{ name }}.",
  171. missingSetterInClass: "Setter is not present for class {{ name }}.",
  172. missingGetterInType: "Getter is not present for type {{ name }}.",
  173. missingSetterInType: "Setter is not present for type {{ name }}.",
  174. },
  175. },
  176. create(context) {
  177. const [
  178. {
  179. getWithoutSet: checkGetWithoutSet,
  180. setWithoutGet: checkSetWithoutGet,
  181. enforceForClassMembers,
  182. enforceForTSTypes,
  183. },
  184. ] = context.options;
  185. const sourceCode = context.sourceCode;
  186. /**
  187. * Reports the given node.
  188. * @param {ASTNode} node The node to report.
  189. * @param {string} messageKind "missingGetter" or "missingSetter".
  190. * @returns {void}
  191. * @private
  192. */
  193. function report(node, messageKind) {
  194. if (node.type === "Property") {
  195. context.report({
  196. node,
  197. messageId: `${messageKind}InObjectLiteral`,
  198. loc: astUtils.getFunctionHeadLoc(node.value, sourceCode),
  199. data: {
  200. name: astUtils.getFunctionNameWithKind(node.value),
  201. },
  202. });
  203. } else if (node.type === "MethodDefinition") {
  204. context.report({
  205. node,
  206. messageId: `${messageKind}InClass`,
  207. loc: astUtils.getFunctionHeadLoc(node.value, sourceCode),
  208. data: {
  209. name: astUtils.getFunctionNameWithKind(node.value),
  210. },
  211. });
  212. } else if (node.type === "TSMethodSignature") {
  213. context.report({
  214. node,
  215. messageId: `${messageKind}InType`,
  216. loc: astUtils.getFunctionHeadLoc(node, sourceCode),
  217. data: {
  218. name: astUtils.getFunctionNameWithKind(node),
  219. },
  220. });
  221. } else {
  222. context.report({
  223. node,
  224. messageId: `${messageKind}InPropertyDescriptor`,
  225. });
  226. }
  227. }
  228. /**
  229. * Reports each of the nodes in the given list using the same messageId.
  230. * @param {ASTNode[]} nodes Nodes to report.
  231. * @param {string} messageKind "missingGetter" or "missingSetter".
  232. * @returns {void}
  233. * @private
  234. */
  235. function reportList(nodes, messageKind) {
  236. for (const node of nodes) {
  237. report(node, messageKind);
  238. }
  239. }
  240. /**
  241. * Checks accessor pairs in the given list of nodes.
  242. * @param {ASTNode[]} nodes The list to check.
  243. * @returns {void}
  244. * @private
  245. */
  246. function checkList(nodes) {
  247. const accessors = [];
  248. let found = false;
  249. for (let i = 0; i < nodes.length; i++) {
  250. const node = nodes[i];
  251. if (isAccessorKind(node)) {
  252. // Creates a new `AccessorData` object for the given getter or setter node.
  253. const name = astUtils.getStaticPropertyName(node);
  254. const key =
  255. name !== null ? name : sourceCode.getTokens(node.key);
  256. // Merges the given `AccessorData` object into the given accessors list.
  257. for (let j = 0; j < accessors.length; j++) {
  258. const accessor = accessors[j];
  259. if (areEqualKeys(accessor.key, key)) {
  260. accessor.getters.push(
  261. ...(node.kind === "get" ? [node] : []),
  262. );
  263. accessor.setters.push(
  264. ...(node.kind === "set" ? [node] : []),
  265. );
  266. found = true;
  267. break;
  268. }
  269. }
  270. if (!found) {
  271. accessors.push({
  272. key,
  273. getters: node.kind === "get" ? [node] : [],
  274. setters: node.kind === "set" ? [node] : [],
  275. });
  276. }
  277. found = false;
  278. }
  279. }
  280. for (const { getters, setters } of accessors) {
  281. if (checkSetWithoutGet && setters.length && !getters.length) {
  282. reportList(setters, "missingGetter");
  283. }
  284. if (checkGetWithoutSet && getters.length && !setters.length) {
  285. reportList(getters, "missingSetter");
  286. }
  287. }
  288. }
  289. /**
  290. * Checks accessor pairs in an object literal.
  291. * @param {ASTNode} node `ObjectExpression` node to check.
  292. * @returns {void}
  293. * @private
  294. */
  295. function checkObjectLiteral(node) {
  296. checkList(node.properties.filter(p => p.type === "Property"));
  297. }
  298. /**
  299. * Checks accessor pairs in a property descriptor.
  300. * @param {ASTNode} node Property descriptor `ObjectExpression` node to check.
  301. * @returns {void}
  302. * @private
  303. */
  304. function checkPropertyDescriptor(node) {
  305. const namesToCheck = new Set(
  306. node.properties
  307. .filter(
  308. p =>
  309. p.type === "Property" &&
  310. p.kind === "init" &&
  311. !p.computed,
  312. )
  313. .map(({ key }) => key.name),
  314. );
  315. const hasGetter = namesToCheck.has("get");
  316. const hasSetter = namesToCheck.has("set");
  317. if (checkSetWithoutGet && hasSetter && !hasGetter) {
  318. report(node, "missingGetter");
  319. }
  320. if (checkGetWithoutSet && hasGetter && !hasSetter) {
  321. report(node, "missingSetter");
  322. }
  323. }
  324. /**
  325. * Checks the given object expression as an object literal and as a possible property descriptor.
  326. * @param {ASTNode} node `ObjectExpression` node to check.
  327. * @returns {void}
  328. * @private
  329. */
  330. function checkObjectExpression(node) {
  331. checkObjectLiteral(node);
  332. if (isPropertyDescriptor(node)) {
  333. checkPropertyDescriptor(node);
  334. }
  335. }
  336. /**
  337. * Checks the given class body.
  338. * @param {ASTNode} node `ClassBody` node to check.
  339. * @returns {void}
  340. * @private
  341. */
  342. function checkClassBody(node) {
  343. const methodDefinitions = node.body.filter(
  344. m => m.type === "MethodDefinition",
  345. );
  346. checkList(methodDefinitions.filter(m => m.static));
  347. checkList(methodDefinitions.filter(m => !m.static));
  348. }
  349. /**
  350. * Checks the given type.
  351. * @param {ASTNode} node `TSTypeLiteral` or `TSInterfaceBody` node to check.
  352. * @returns {void}
  353. * @private
  354. */
  355. function checkType(node) {
  356. const members =
  357. node.type === "TSTypeLiteral" ? node.members : node.body;
  358. const methodDefinitions = members.filter(
  359. m => m.type === "TSMethodSignature",
  360. );
  361. checkList(methodDefinitions);
  362. }
  363. const listeners = {};
  364. if (checkSetWithoutGet || checkGetWithoutSet) {
  365. listeners.ObjectExpression = checkObjectExpression;
  366. if (enforceForClassMembers) {
  367. listeners.ClassBody = checkClassBody;
  368. }
  369. if (enforceForTSTypes) {
  370. listeners["TSTypeLiteral, TSInterfaceBody"] = checkType;
  371. }
  372. }
  373. return listeners;
  374. },
  375. };