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