2 * @fileoverview Rule to flag declared but unused private class members
3 * @author Tim van der Lippe
8 //------------------------------------------------------------------------------
10 //------------------------------------------------------------------------------
17 description
: "disallow unused private class members",
19 url
: "https://eslint.org/docs/rules/no-unused-private-class-members"
25 unusedPrivateClassMember
: "'{{classMemberName}}' is defined but never used."
30 const trackedClasses
= [];
33 * Check whether the current node is in a write only assignment.
34 * @param {ASTNode} privateIdentifierNode Node referring to a private identifier
35 * @returns {boolean} Whether the node is in a write only assignment
38 function isWriteOnlyAssignment(privateIdentifierNode
) {
39 const parentStatement
= privateIdentifierNode
.parent
.parent
;
40 const isAssignmentExpression
= parentStatement
.type
=== "AssignmentExpression";
42 if (!isAssignmentExpression
&&
43 parentStatement
.type
!== "ForInStatement" &&
44 parentStatement
.type
!== "ForOfStatement" &&
45 parentStatement
.type
!== "AssignmentPattern") {
49 // It is a write-only usage, since we still allow usages on the right for reads
50 if (parentStatement
.left
!== privateIdentifierNode
.parent
) {
54 // For any other operator (such as '+=') we still consider it a read operation
55 if (isAssignmentExpression
&& parentStatement
.operator
!== "=") {
58 * However, if the read operation is "discarded" in an empty statement, then
59 * we consider it write only.
61 return parentStatement
.parent
.type
=== "ExpressionStatement";
67 //--------------------------------------------------------------------------
69 //--------------------------------------------------------------------------
73 // Collect all declared members up front and assume they are all unused
74 ClassBody(classBodyNode
) {
75 const privateMembers
= new Map();
77 trackedClasses
.unshift(privateMembers
);
78 for (const bodyMember
of classBodyNode
.body
) {
79 if (bodyMember
.type
=== "PropertyDefinition" || bodyMember
.type
=== "MethodDefinition") {
80 if (bodyMember
.key
.type
=== "PrivateIdentifier") {
81 privateMembers
.set(bodyMember
.key
.name
, {
82 declaredNode
: bodyMember
,
83 isAccessor
: bodyMember
.type
=== "MethodDefinition" &&
84 (bodyMember
.kind
=== "set" || bodyMember
.kind
=== "get")
92 * Process all usages of the private identifier and remove a member from
93 * `declaredAndUnusedPrivateMembers` if we deem it used.
95 PrivateIdentifier(privateIdentifierNode
) {
96 const classBody
= trackedClasses
.find(classProperties
=> classProperties
.has(privateIdentifierNode
.name
));
98 // Can't happen, as it is a parser to have a missing class body, but let's code defensively here.
103 // In case any other usage was already detected, we can short circuit the logic here.
104 const memberDefinition
= classBody
.get(privateIdentifierNode
.name
);
106 if (memberDefinition
.isUsed
) {
110 // The definition of the class member itself
111 if (privateIdentifierNode
.parent
.type
=== "PropertyDefinition" ||
112 privateIdentifierNode
.parent
.type
=== "MethodDefinition") {
117 * Any usage of an accessor is considered a read, as the getter/setter can have
118 * side-effects in its definition.
120 if (memberDefinition
.isAccessor
) {
121 memberDefinition
.isUsed
= true;
125 // Any assignments to this member, except for assignments that also read
126 if (isWriteOnlyAssignment(privateIdentifierNode
)) {
130 const wrappingExpressionType
= privateIdentifierNode
.parent
.parent
.type
;
131 const parentOfWrappingExpressionType
= privateIdentifierNode
.parent
.parent
.parent
.type
;
133 // A statement which only increments (`this.#x++;`)
134 if (wrappingExpressionType
=== "UpdateExpression" &&
135 parentOfWrappingExpressionType
=== "ExpressionStatement") {
140 * ({ x: this.#usedInDestructuring } = bar);
142 * But should treat the following as a read:
143 * ({ [this.#x]: a } = foo);
145 if (wrappingExpressionType
=== "Property" &&
146 parentOfWrappingExpressionType
=== "ObjectPattern" &&
147 privateIdentifierNode
.parent
.parent
.value
=== privateIdentifierNode
.parent
) {
151 // [...this.#unusedInRestPattern] = bar;
152 if (wrappingExpressionType
=== "RestElement") {
156 // [this.#unusedInAssignmentPattern] = bar;
157 if (wrappingExpressionType
=== "ArrayPattern") {
162 * We can't delete the memberDefinition, as we need to keep track of which member we are marking as used.
163 * In the case of nested classes, we only mark the first member we encounter as used. If you were to delete
164 * the member, then any subsequent usage could incorrectly mark the member of an encapsulating parent class
165 * as used, which is incorrect.
167 memberDefinition
.isUsed
= true;
171 * Post-process the class members and report any remaining members.
172 * Since private members can only be accessed in the current class context,
173 * we can safely assume that all usages are within the current class body.
176 const unusedPrivateMembers
= trackedClasses
.shift();
178 for (const [classMemberName
, { declaredNode
, isUsed
}] of unusedPrivateMembers
.entries()) {
184 loc
: declaredNode
.key
.loc
,
185 messageId
: "unusedPrivateClassMember",
187 classMemberName
: `#${classMemberName}`