no-unused-private-class-members.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. /**
  2. * @fileoverview Rule to flag declared but unused private class members
  3. * @author Tim van der Lippe
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Rule Definition
  8. //------------------------------------------------------------------------------
  9. /** @type {import('../types').Rule.RuleModule} */
  10. module.exports = {
  11. meta: {
  12. type: "problem",
  13. docs: {
  14. description: "Disallow unused private class members",
  15. recommended: true,
  16. url: "https://eslint.org/docs/latest/rules/no-unused-private-class-members",
  17. },
  18. schema: [],
  19. messages: {
  20. unusedPrivateClassMember:
  21. "'{{classMemberName}}' is defined but never used.",
  22. },
  23. },
  24. create(context) {
  25. const trackedClasses = [];
  26. /**
  27. * Check whether the current node is in a write only assignment.
  28. * @param {ASTNode} privateIdentifierNode Node referring to a private identifier
  29. * @returns {boolean} Whether the node is in a write only assignment
  30. * @private
  31. */
  32. function isWriteOnlyAssignment(privateIdentifierNode) {
  33. const parentStatement = privateIdentifierNode.parent.parent;
  34. const isAssignmentExpression =
  35. parentStatement.type === "AssignmentExpression";
  36. if (
  37. !isAssignmentExpression &&
  38. parentStatement.type !== "ForInStatement" &&
  39. parentStatement.type !== "ForOfStatement" &&
  40. parentStatement.type !== "AssignmentPattern"
  41. ) {
  42. return false;
  43. }
  44. // It is a write-only usage, since we still allow usages on the right for reads
  45. if (parentStatement.left !== privateIdentifierNode.parent) {
  46. return false;
  47. }
  48. // For any other operator (such as '+=') we still consider it a read operation
  49. if (isAssignmentExpression && parentStatement.operator !== "=") {
  50. /*
  51. * However, if the read operation is "discarded" in an empty statement, then
  52. * we consider it write only.
  53. */
  54. return parentStatement.parent.type === "ExpressionStatement";
  55. }
  56. return true;
  57. }
  58. //--------------------------------------------------------------------------
  59. // Public
  60. //--------------------------------------------------------------------------
  61. return {
  62. // Collect all declared members up front and assume they are all unused
  63. ClassBody(classBodyNode) {
  64. const privateMembers = new Map();
  65. trackedClasses.unshift(privateMembers);
  66. for (const bodyMember of classBodyNode.body) {
  67. if (
  68. bodyMember.type === "PropertyDefinition" ||
  69. bodyMember.type === "MethodDefinition"
  70. ) {
  71. if (bodyMember.key.type === "PrivateIdentifier") {
  72. privateMembers.set(bodyMember.key.name, {
  73. declaredNode: bodyMember,
  74. isAccessor:
  75. bodyMember.type === "MethodDefinition" &&
  76. (bodyMember.kind === "set" ||
  77. bodyMember.kind === "get"),
  78. });
  79. }
  80. }
  81. }
  82. },
  83. /*
  84. * Process all usages of the private identifier and remove a member from
  85. * `declaredAndUnusedPrivateMembers` if we deem it used.
  86. */
  87. PrivateIdentifier(privateIdentifierNode) {
  88. const classBody = trackedClasses.find(classProperties =>
  89. classProperties.has(privateIdentifierNode.name),
  90. );
  91. // Can't happen, as it is a parser to have a missing class body, but let's code defensively here.
  92. if (!classBody) {
  93. return;
  94. }
  95. // In case any other usage was already detected, we can short circuit the logic here.
  96. const memberDefinition = classBody.get(
  97. privateIdentifierNode.name,
  98. );
  99. if (memberDefinition.isUsed) {
  100. return;
  101. }
  102. // The definition of the class member itself
  103. if (
  104. privateIdentifierNode.parent.type ===
  105. "PropertyDefinition" ||
  106. privateIdentifierNode.parent.type === "MethodDefinition"
  107. ) {
  108. return;
  109. }
  110. /*
  111. * Any usage of an accessor is considered a read, as the getter/setter can have
  112. * side-effects in its definition.
  113. */
  114. if (memberDefinition.isAccessor) {
  115. memberDefinition.isUsed = true;
  116. return;
  117. }
  118. // Any assignments to this member, except for assignments that also read
  119. if (isWriteOnlyAssignment(privateIdentifierNode)) {
  120. return;
  121. }
  122. const wrappingExpressionType =
  123. privateIdentifierNode.parent.parent.type;
  124. const parentOfWrappingExpressionType =
  125. privateIdentifierNode.parent.parent.parent.type;
  126. // A statement which only increments (`this.#x++;`)
  127. if (
  128. wrappingExpressionType === "UpdateExpression" &&
  129. parentOfWrappingExpressionType === "ExpressionStatement"
  130. ) {
  131. return;
  132. }
  133. /*
  134. * ({ x: this.#usedInDestructuring } = bar);
  135. *
  136. * But should treat the following as a read:
  137. * ({ [this.#x]: a } = foo);
  138. */
  139. if (
  140. wrappingExpressionType === "Property" &&
  141. parentOfWrappingExpressionType === "ObjectPattern" &&
  142. privateIdentifierNode.parent.parent.value ===
  143. privateIdentifierNode.parent
  144. ) {
  145. return;
  146. }
  147. // [...this.#unusedInRestPattern] = bar;
  148. if (wrappingExpressionType === "RestElement") {
  149. return;
  150. }
  151. // [this.#unusedInAssignmentPattern] = bar;
  152. if (wrappingExpressionType === "ArrayPattern") {
  153. return;
  154. }
  155. /*
  156. * We can't delete the memberDefinition, as we need to keep track of which member we are marking as used.
  157. * In the case of nested classes, we only mark the first member we encounter as used. If you were to delete
  158. * the member, then any subsequent usage could incorrectly mark the member of an encapsulating parent class
  159. * as used, which is incorrect.
  160. */
  161. memberDefinition.isUsed = true;
  162. },
  163. /*
  164. * Post-process the class members and report any remaining members.
  165. * Since private members can only be accessed in the current class context,
  166. * we can safely assume that all usages are within the current class body.
  167. */
  168. "ClassBody:exit"() {
  169. const unusedPrivateMembers = trackedClasses.shift();
  170. for (const [
  171. classMemberName,
  172. { declaredNode, isUsed },
  173. ] of unusedPrivateMembers.entries()) {
  174. if (isUsed) {
  175. continue;
  176. }
  177. context.report({
  178. node: declaredNode,
  179. loc: declaredNode.key.loc,
  180. messageId: "unusedPrivateClassMember",
  181. data: {
  182. classMemberName: `#${classMemberName}`,
  183. },
  184. });
  185. }
  186. },
  187. };
  188. },
  189. };