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