Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 379571a

Browse files
authored
feat: add suggestions for no-unused-private-class-members (#20773)
1 parent 97c8c33 commit 379571a

2 files changed

Lines changed: 1167 additions & 35 deletions

File tree

lib/rules/no-unused-private-class-members.js

Lines changed: 202 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55

66
"use strict";
77

8+
//------------------------------------------------------------------------------
9+
// Requirements
10+
//------------------------------------------------------------------------------
11+
12+
const astUtils = require("./utils/ast-utils");
13+
814
//------------------------------------------------------------------------------
915
// Rule Definition
1016
//------------------------------------------------------------------------------
@@ -13,6 +19,7 @@
1319
module.exports = {
1420
meta: {
1521
type: "problem",
22+
hasSuggestions: true,
1623

1724
docs: {
1825
description: "Disallow unused private class members",
@@ -25,12 +32,169 @@ module.exports = {
2532
messages: {
2633
unusedPrivateClassMember:
2734
"'{{classMemberName}}' is defined but never used.",
35+
removeUnusedPrivateClassMember:
36+
"Remove unused private class member '{{classMemberName}}'.",
2837
},
2938
},
3039

3140
create(context) {
41+
const sourceCode = context.sourceCode;
3242
const trackedClasses = [];
3343

44+
/**
45+
* Gets the start index of the line that contains a given token or node.
46+
* @param {ASTNode|Token|Comment} nodeOrToken The token or node to check
47+
* @returns {number} The line start index
48+
*/
49+
function getLineStartIndex(nodeOrToken) {
50+
return nodeOrToken.range[0] - nodeOrToken.loc.start.column;
51+
}
52+
53+
/**
54+
* Checks whether a token or node starts on its own line, preceded only by whitespace.
55+
* @param {ASTNode|Token|Comment} nodeOrToken The token or node to check
56+
* @returns {boolean} Whether the token or node starts on its own line
57+
*/
58+
function startsOnOwnLine(nodeOrToken) {
59+
return (
60+
sourceCode.getTokenBefore(nodeOrToken, {
61+
includeComments: true,
62+
}).loc.end.line !== nodeOrToken.loc.start.line
63+
);
64+
}
65+
66+
/**
67+
* Gets leading comments that are directly attached to a class member.
68+
* @param {ASTNode} classMemberNode The class member node
69+
* @returns {Comment[]} Leading comments to remove with the member
70+
*/
71+
function getLeadingComments(classMemberNode) {
72+
const commentsBefore =
73+
sourceCode.getCommentsBefore(classMemberNode);
74+
const lastNonLeadingCommentIndex = commentsBefore.findLastIndex(
75+
(comment, index, self) => {
76+
const next =
77+
index < self.length - 1
78+
? self[index + 1]
79+
: classMemberNode;
80+
81+
return (
82+
!startsOnOwnLine(comment) ||
83+
next.loc.start.line - comment.loc.end.line > 1
84+
);
85+
},
86+
);
87+
88+
return commentsBefore.slice(lastNonLeadingCommentIndex + 1);
89+
}
90+
91+
/**
92+
* Checks whether a class member shares its line with another token.
93+
* @param {ASTNode} classMemberNode The class member node
94+
* @returns {boolean} Whether the member shares its line with another token
95+
*/
96+
function sharesLineWithAnotherToken(classMemberNode) {
97+
const previousToken = sourceCode.getTokenBefore(classMemberNode);
98+
const nextToken = sourceCode.getTokenAfter(classMemberNode);
99+
100+
return (
101+
previousToken.loc.end.line === classMemberNode.loc.start.line ||
102+
nextToken.loc.start.line === classMemberNode.loc.end.line
103+
);
104+
}
105+
106+
/**
107+
* Gets trailing comments that are directly attached to a class member.
108+
* Same-line trailing comments are preserved when another token shares
109+
* the line, because the comment might describe the remaining code rather
110+
* than the unused member alone.
111+
* @param {ASTNode} classMemberNode The class member node
112+
* @returns {Comment[]} Trailing comments to remove with the member
113+
*/
114+
function getTrailingComments(classMemberNode) {
115+
if (sharesLineWithAnotherToken(classMemberNode)) {
116+
return [];
117+
}
118+
119+
return sourceCode
120+
.getCommentsAfter(classMemberNode)
121+
.filter(
122+
comment =>
123+
comment.loc.start.line === classMemberNode.loc.end.line,
124+
);
125+
}
126+
127+
/**
128+
* Gets the token after which a semicolon should be inserted when removing a class member.
129+
* @param {ASTNode} classMemberNode The member that would be removed
130+
* @returns {Token|null} The token after which a semicolon should be inserted, or null if no semicolon is needed
131+
*/
132+
function getSemicolonInsertionToken(classMemberNode) {
133+
const nextToken = sourceCode.getTokenAfter(classMemberNode);
134+
135+
if (
136+
nextToken.type === "Punctuator" &&
137+
(nextToken.value === "[" || nextToken.value === "*") &&
138+
astUtils.needsPrecedingSemicolon(sourceCode, classMemberNode)
139+
) {
140+
return sourceCode.getTokenBefore(classMemberNode);
141+
}
142+
143+
return null;
144+
}
145+
146+
/**
147+
* Gets the replacement range for removing an unused class member.
148+
* @param {ASTNode} classMemberNode The member that would be removed
149+
* @returns {number[]} The text range to remove
150+
*/
151+
function getMemberRemovalRange(classMemberNode) {
152+
const leadingComments = getLeadingComments(classMemberNode);
153+
const trailingComments = getTrailingComments(classMemberNode);
154+
const shouldRemoveLeadingComments =
155+
leadingComments.length > 0 &&
156+
!sharesLineWithAnotherToken(classMemberNode);
157+
const lastItemToRemove =
158+
trailingComments.length > 0
159+
? trailingComments.at(-1)
160+
: classMemberNode;
161+
162+
const previousToken = sourceCode.getTokenBefore(classMemberNode);
163+
const nextToken = sourceCode.getTokenAfter(lastItemToRemove, {
164+
includeComments: true,
165+
});
166+
const nextTokenStartsOnNewLine =
167+
nextToken.loc.start.line > lastItemToRemove.loc.end.line;
168+
const shouldRemoveOwnLine =
169+
!shouldRemoveLeadingComments &&
170+
startsOnOwnLine(classMemberNode) &&
171+
nextTokenStartsOnNewLine;
172+
let start = classMemberNode.range[0];
173+
let end = lastItemToRemove.range[1];
174+
175+
if (shouldRemoveLeadingComments) {
176+
start = nextTokenStartsOnNewLine
177+
? getLineStartIndex(leadingComments[0])
178+
: leadingComments[0].range[0];
179+
end = nextTokenStartsOnNewLine
180+
? getLineStartIndex(nextToken)
181+
: nextToken.range[0];
182+
} else if (shouldRemoveOwnLine) {
183+
start = getLineStartIndex(classMemberNode);
184+
end = getLineStartIndex(nextToken);
185+
} else if (
186+
previousToken.loc.end.line === classMemberNode.loc.start.line
187+
) {
188+
start = previousToken.range[1];
189+
} else if (
190+
nextToken.loc.start.line === lastItemToRemove.loc.end.line
191+
) {
192+
end = nextToken.range[0];
193+
}
194+
195+
return [start, end];
196+
}
197+
34198
/**
35199
* Check whether the current node is in a write only assignment.
36200
* @param {ASTNode} privateIdentifierNode Node referring to a private identifier
@@ -86,6 +250,7 @@ module.exports = {
86250
if (bodyMember.key.type === "PrivateIdentifier") {
87251
privateMembers.set(bodyMember.key.name, {
88252
declaredNode: bodyMember,
253+
hasReference: false,
89254
isAccessor:
90255
bodyMember.type === "MethodDefinition" &&
91256
(bodyMember.kind === "set" ||
@@ -128,6 +293,8 @@ module.exports = {
128293
return;
129294
}
130295

296+
memberDefinition.hasReference = true;
297+
131298
/*
132299
* Any usage of an accessor is considered a read, as the getter/setter can have
133300
* side-effects in its definition.
@@ -199,18 +366,52 @@ module.exports = {
199366

200367
for (const [
201368
classMemberName,
202-
{ declaredNode, isUsed },
369+
{ declaredNode, hasReference, isUsed },
203370
] of unusedPrivateMembers.entries()) {
204371
if (isUsed) {
205372
continue;
206373
}
374+
207375
context.report({
208376
node: declaredNode,
209377
loc: declaredNode.key.loc,
210378
messageId: "unusedPrivateClassMember",
211379
data: {
212380
classMemberName: `#${classMemberName}`,
213381
},
382+
suggest: [
383+
{
384+
messageId: "removeUnusedPrivateClassMember",
385+
data: {
386+
classMemberName: `#${classMemberName}`,
387+
},
388+
*fix(fixer) {
389+
if (hasReference) {
390+
return;
391+
}
392+
393+
const removalRange =
394+
getMemberRemovalRange(declaredNode);
395+
const semicolonInsertionToken =
396+
getSemicolonInsertionToken(
397+
declaredNode,
398+
);
399+
const removalFix = fixer.replaceTextRange(
400+
removalRange,
401+
"",
402+
);
403+
404+
yield removalFix;
405+
406+
if (semicolonInsertionToken) {
407+
yield fixer.insertTextAfter(
408+
semicolonInsertionToken,
409+
";",
410+
);
411+
}
412+
},
413+
},
414+
],
214415
});
215416
}
216417
},

0 commit comments

Comments
 (0)