accessor-pairs.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. /**
  2. * @fileoverview Rule to flag wrapping non-iife in parens
  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 `Identifier` node which was named a given name.
  74. * @param {ASTNode} node A node to check.
  75. * @param {string} name An expected name of the node.
  76. * @returns {boolean} `true` if the node is an `Identifier` node which was named as expected.
  77. */
  78. function isIdentifier(node, name) {
  79. return node.type === "Identifier" && node.name === name;
  80. }
  81. /**
  82. * Checks whether or not a given node is an argument of a specified method call.
  83. * @param {ASTNode} node A node to check.
  84. * @param {number} index An expected index of the node in arguments.
  85. * @param {string} object An expected name of the object of the method.
  86. * @param {string} property An expected name of the method.
  87. * @returns {boolean} `true` if the node is an argument of the specified method call.
  88. */
  89. function isArgumentOfMethodCall(node, index, object, property) {
  90. const parent = node.parent;
  91. return (
  92. parent.type === "CallExpression" &&
  93. parent.callee.type === "MemberExpression" &&
  94. parent.callee.computed === false &&
  95. isIdentifier(parent.callee.object, object) &&
  96. isIdentifier(parent.callee.property, property) &&
  97. parent.arguments[index] === node
  98. );
  99. }
  100. /**
  101. * Checks whether or not a given node is a property descriptor.
  102. * @param {ASTNode} node A node to check.
  103. * @returns {boolean} `true` if the node is a property descriptor.
  104. */
  105. function isPropertyDescriptor(node) {
  106. // Object.defineProperty(obj, "foo", {set: ...})
  107. if (isArgumentOfMethodCall(node, 2, "Object", "defineProperty") ||
  108. isArgumentOfMethodCall(node, 2, "Reflect", "defineProperty")
  109. ) {
  110. return true;
  111. }
  112. /*
  113. * Object.defineProperties(obj, {foo: {set: ...}})
  114. * Object.create(proto, {foo: {set: ...}})
  115. */
  116. const grandparent = node.parent.parent;
  117. return grandparent.type === "ObjectExpression" && (
  118. isArgumentOfMethodCall(grandparent, 1, "Object", "create") ||
  119. isArgumentOfMethodCall(grandparent, 1, "Object", "defineProperties")
  120. );
  121. }
  122. //------------------------------------------------------------------------------
  123. // Rule Definition
  124. //------------------------------------------------------------------------------
  125. module.exports = {
  126. meta: {
  127. type: "suggestion",
  128. docs: {
  129. description: "enforce getter and setter pairs in objects and classes",
  130. category: "Best Practices",
  131. recommended: false,
  132. url: "https://eslint.org/docs/rules/accessor-pairs"
  133. },
  134. schema: [{
  135. type: "object",
  136. properties: {
  137. getWithoutSet: {
  138. type: "boolean",
  139. default: false
  140. },
  141. setWithoutGet: {
  142. type: "boolean",
  143. default: true
  144. },
  145. enforceForClassMembers: {
  146. type: "boolean",
  147. default: false
  148. }
  149. },
  150. additionalProperties: false
  151. }],
  152. messages: {
  153. missingGetterInPropertyDescriptor: "Getter is not present in property descriptor.",
  154. missingSetterInPropertyDescriptor: "Setter is not present in property descriptor.",
  155. missingGetterInObjectLiteral: "Getter is not present for {{ name }}.",
  156. missingSetterInObjectLiteral: "Setter is not present for {{ name }}.",
  157. missingGetterInClass: "Getter is not present for class {{ name }}.",
  158. missingSetterInClass: "Setter is not present for class {{ name }}."
  159. }
  160. },
  161. create(context) {
  162. const config = context.options[0] || {};
  163. const checkGetWithoutSet = config.getWithoutSet === true;
  164. const checkSetWithoutGet = config.setWithoutGet !== false;
  165. const enforceForClassMembers = config.enforceForClassMembers === true;
  166. const sourceCode = context.getSourceCode();
  167. /**
  168. * Reports the given node.
  169. * @param {ASTNode} node The node to report.
  170. * @param {string} messageKind "missingGetter" or "missingSetter".
  171. * @returns {void}
  172. * @private
  173. */
  174. function report(node, messageKind) {
  175. if (node.type === "Property") {
  176. context.report({
  177. node,
  178. messageId: `${messageKind}InObjectLiteral`,
  179. loc: astUtils.getFunctionHeadLoc(node.value, sourceCode),
  180. data: { name: astUtils.getFunctionNameWithKind(node.value) }
  181. });
  182. } else if (node.type === "MethodDefinition") {
  183. context.report({
  184. node,
  185. messageId: `${messageKind}InClass`,
  186. loc: astUtils.getFunctionHeadLoc(node.value, sourceCode),
  187. data: { name: astUtils.getFunctionNameWithKind(node.value) }
  188. });
  189. } else {
  190. context.report({
  191. node,
  192. messageId: `${messageKind}InPropertyDescriptor`
  193. });
  194. }
  195. }
  196. /**
  197. * Reports each of the nodes in the given list using the same messageId.
  198. * @param {ASTNode[]} nodes Nodes to report.
  199. * @param {string} messageKind "missingGetter" or "missingSetter".
  200. * @returns {void}
  201. * @private
  202. */
  203. function reportList(nodes, messageKind) {
  204. for (const node of nodes) {
  205. report(node, messageKind);
  206. }
  207. }
  208. /**
  209. * Creates a new `AccessorData` object for the given getter or setter node.
  210. * @param {ASTNode} node A getter or setter node.
  211. * @returns {AccessorData} New `AccessorData` object that contains the given node.
  212. * @private
  213. */
  214. function createAccessorData(node) {
  215. const name = astUtils.getStaticPropertyName(node);
  216. const key = (name !== null) ? name : sourceCode.getTokens(node.key);
  217. return {
  218. key,
  219. getters: node.kind === "get" ? [node] : [],
  220. setters: node.kind === "set" ? [node] : []
  221. };
  222. }
  223. /**
  224. * Merges the given `AccessorData` object into the given accessors list.
  225. * @param {AccessorData[]} accessors The list to merge into.
  226. * @param {AccessorData} accessorData The object to merge.
  227. * @returns {AccessorData[]} The same instance with the merged object.
  228. * @private
  229. */
  230. function mergeAccessorData(accessors, accessorData) {
  231. const equalKeyElement = accessors.find(a => areEqualKeys(a.key, accessorData.key));
  232. if (equalKeyElement) {
  233. equalKeyElement.getters.push(...accessorData.getters);
  234. equalKeyElement.setters.push(...accessorData.setters);
  235. } else {
  236. accessors.push(accessorData);
  237. }
  238. return accessors;
  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 = nodes
  248. .filter(isAccessorKind)
  249. .map(createAccessorData)
  250. .reduce(mergeAccessorData, []);
  251. for (const { getters, setters } of accessors) {
  252. if (checkSetWithoutGet && setters.length && !getters.length) {
  253. reportList(setters, "missingGetter");
  254. }
  255. if (checkGetWithoutSet && getters.length && !setters.length) {
  256. reportList(getters, "missingSetter");
  257. }
  258. }
  259. }
  260. /**
  261. * Checks accessor pairs in an object literal.
  262. * @param {ASTNode} node `ObjectExpression` node to check.
  263. * @returns {void}
  264. * @private
  265. */
  266. function checkObjectLiteral(node) {
  267. checkList(node.properties.filter(p => p.type === "Property"));
  268. }
  269. /**
  270. * Checks accessor pairs in a property descriptor.
  271. * @param {ASTNode} node Property descriptor `ObjectExpression` node to check.
  272. * @returns {void}
  273. * @private
  274. */
  275. function checkPropertyDescriptor(node) {
  276. const namesToCheck = node.properties
  277. .filter(p => p.type === "Property" && p.kind === "init" && !p.computed)
  278. .map(({ key }) => key.name);
  279. const hasGetter = namesToCheck.includes("get");
  280. const hasSetter = namesToCheck.includes("set");
  281. if (checkSetWithoutGet && hasSetter && !hasGetter) {
  282. report(node, "missingGetter");
  283. }
  284. if (checkGetWithoutSet && hasGetter && !hasSetter) {
  285. report(node, "missingSetter");
  286. }
  287. }
  288. /**
  289. * Checks the given object expression as an object literal and as a possible property descriptor.
  290. * @param {ASTNode} node `ObjectExpression` node to check.
  291. * @returns {void}
  292. * @private
  293. */
  294. function checkObjectExpression(node) {
  295. checkObjectLiteral(node);
  296. if (isPropertyDescriptor(node)) {
  297. checkPropertyDescriptor(node);
  298. }
  299. }
  300. /**
  301. * Checks the given class body.
  302. * @param {ASTNode} node `ClassBody` node to check.
  303. * @returns {void}
  304. * @private
  305. */
  306. function checkClassBody(node) {
  307. const methodDefinitions = node.body.filter(m => m.type === "MethodDefinition");
  308. checkList(methodDefinitions.filter(m => m.static));
  309. checkList(methodDefinitions.filter(m => !m.static));
  310. }
  311. const listeners = {};
  312. if (checkSetWithoutGet || checkGetWithoutSet) {
  313. listeners.ObjectExpression = checkObjectExpression;
  314. if (enforceForClassMembers) {
  315. listeners.ClassBody = checkClassBody;
  316. }
  317. }
  318. return listeners;
  319. }
  320. };