class-methods-use-this.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. /**
  2. * @fileoverview Rule to enforce that all class methods use 'this'.
  3. * @author Patrick Williams
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const astUtils = require("./utils/ast-utils");
  10. //------------------------------------------------------------------------------
  11. // Rule Definition
  12. //------------------------------------------------------------------------------
  13. /** @type {import('../types').Rule.RuleModule} */
  14. module.exports = {
  15. meta: {
  16. dialects: ["javascript", "typescript"],
  17. language: "javascript",
  18. type: "suggestion",
  19. defaultOptions: [
  20. {
  21. enforceForClassFields: true,
  22. exceptMethods: [],
  23. ignoreOverrideMethods: false,
  24. },
  25. ],
  26. docs: {
  27. description: "Enforce that class methods utilize `this`",
  28. recommended: false,
  29. url: "https://eslint.org/docs/latest/rules/class-methods-use-this",
  30. },
  31. schema: [
  32. {
  33. type: "object",
  34. properties: {
  35. exceptMethods: {
  36. type: "array",
  37. items: {
  38. type: "string",
  39. },
  40. },
  41. enforceForClassFields: {
  42. type: "boolean",
  43. },
  44. ignoreOverrideMethods: {
  45. type: "boolean",
  46. },
  47. ignoreClassesWithImplements: {
  48. enum: ["all", "public-fields"],
  49. },
  50. },
  51. additionalProperties: false,
  52. },
  53. ],
  54. messages: {
  55. missingThis: "Expected 'this' to be used by class {{name}}.",
  56. },
  57. },
  58. create(context) {
  59. const [options] = context.options;
  60. const {
  61. enforceForClassFields,
  62. ignoreOverrideMethods,
  63. ignoreClassesWithImplements,
  64. } = options;
  65. const exceptMethods = new Set(options.exceptMethods);
  66. const stack = [];
  67. /**
  68. * Push `this` used flag initialized with `false` onto the stack.
  69. * @returns {void}
  70. */
  71. function pushContext() {
  72. stack.push(false);
  73. }
  74. /**
  75. * Pop `this` used flag from the stack.
  76. * @returns {boolean | undefined} `this` used flag
  77. */
  78. function popContext() {
  79. return stack.pop();
  80. }
  81. /**
  82. * Initializes the current context to false and pushes it onto the stack.
  83. * These booleans represent whether 'this' has been used in the context.
  84. * @returns {void}
  85. * @private
  86. */
  87. function enterFunction() {
  88. pushContext();
  89. }
  90. /**
  91. * Check if the node is an instance method
  92. * @param {ASTNode} node node to check
  93. * @returns {boolean} True if its an instance method
  94. * @private
  95. */
  96. function isInstanceMethod(node) {
  97. switch (node.type) {
  98. case "MethodDefinition":
  99. return !node.static && node.kind !== "constructor";
  100. case "AccessorProperty":
  101. case "PropertyDefinition":
  102. return !node.static && enforceForClassFields;
  103. default:
  104. return false;
  105. }
  106. }
  107. /**
  108. * Check if the node's parent class implements any interfaces
  109. * @param {ASTNode} node node to check
  110. * @returns {boolean} True if parent class implements interfaces
  111. * @private
  112. */
  113. function hasImplements(node) {
  114. const classNode = node.parent.parent;
  115. return (
  116. classNode?.type === "ClassDeclaration" &&
  117. classNode.implements?.length > 0
  118. );
  119. }
  120. /**
  121. * Check if the node is an instance method not excluded by config
  122. * @param {ASTNode} node node to check
  123. * @returns {boolean} True if it is an instance method, and not excluded by config
  124. * @private
  125. */
  126. function isIncludedInstanceMethod(node) {
  127. if (isInstanceMethod(node)) {
  128. if (node.computed) {
  129. return true;
  130. }
  131. if (ignoreOverrideMethods && node.override) {
  132. return false;
  133. }
  134. if (ignoreClassesWithImplements) {
  135. const implementsInterfaces = hasImplements(node);
  136. if (implementsInterfaces) {
  137. if (
  138. ignoreClassesWithImplements === "all" ||
  139. (ignoreClassesWithImplements === "public-fields" &&
  140. node.key.type !== "PrivateIdentifier" &&
  141. (!node.accessibility ||
  142. node.accessibility === "public"))
  143. ) {
  144. return false;
  145. }
  146. }
  147. }
  148. const hashIfNeeded =
  149. node.key.type === "PrivateIdentifier" ? "#" : "";
  150. const name =
  151. node.key.type === "Literal"
  152. ? astUtils.getStaticStringValue(node.key)
  153. : node.key.name || "";
  154. return !exceptMethods.has(hashIfNeeded + name);
  155. }
  156. return false;
  157. }
  158. /**
  159. * Checks if we are leaving a function that is a method, and reports if 'this' has not been used.
  160. * Static methods and the constructor are exempt.
  161. * Then pops the context off the stack.
  162. * @param {ASTNode} node A function node that was entered.
  163. * @returns {void}
  164. * @private
  165. */
  166. function exitFunction(node) {
  167. const methodUsesThis = popContext();
  168. if (isIncludedInstanceMethod(node.parent) && !methodUsesThis) {
  169. context.report({
  170. node,
  171. loc: astUtils.getFunctionHeadLoc(node, context.sourceCode),
  172. messageId: "missingThis",
  173. data: {
  174. name: astUtils.getFunctionNameWithKind(node),
  175. },
  176. });
  177. }
  178. }
  179. /**
  180. * Mark the current context as having used 'this'.
  181. * @returns {void}
  182. * @private
  183. */
  184. function markThisUsed() {
  185. if (stack.length) {
  186. stack[stack.length - 1] = true;
  187. }
  188. }
  189. return {
  190. FunctionDeclaration: enterFunction,
  191. "FunctionDeclaration:exit": exitFunction,
  192. FunctionExpression: enterFunction,
  193. "FunctionExpression:exit": exitFunction,
  194. /*
  195. * Class field value are implicit functions.
  196. */
  197. "AccessorProperty > *.key:exit": pushContext,
  198. "AccessorProperty:exit": popContext,
  199. "PropertyDefinition > *.key:exit": pushContext,
  200. "PropertyDefinition:exit": popContext,
  201. /*
  202. * Class static blocks are implicit functions. They aren't required to use `this`,
  203. * but we have to push context so that it captures any use of `this` in the static block
  204. * separately from enclosing contexts, because static blocks have their own `this` and it
  205. * shouldn't count as used `this` in enclosing contexts.
  206. */
  207. StaticBlock: pushContext,
  208. "StaticBlock:exit": popContext,
  209. ThisExpression: markThisUsed,
  210. Super: markThisUsed,
  211. ...(enforceForClassFields && {
  212. "AccessorProperty > ArrowFunctionExpression.value":
  213. enterFunction,
  214. "AccessorProperty > ArrowFunctionExpression.value:exit":
  215. exitFunction,
  216. "PropertyDefinition > ArrowFunctionExpression.value":
  217. enterFunction,
  218. "PropertyDefinition > ArrowFunctionExpression.value:exit":
  219. exitFunction,
  220. }),
  221. };
  222. },
  223. };