grouped-accessor-pairs.js 7.0 KB

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