55
66"use strict" ;
77
8+ //------------------------------------------------------------------------------
9+ // Requirements
10+ //------------------------------------------------------------------------------
11+
12+ const astUtils = require ( "./utils/ast-utils" ) ;
13+
814//------------------------------------------------------------------------------
915// Rule Definition
1016//------------------------------------------------------------------------------
1319module . 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