@@ -10,9 +10,9 @@ use biome_console::markup;
1010use biome_diagnostics:: Severity ;
1111use biome_js_semantic:: ReferencesExtensions ;
1212use biome_js_syntax:: {
13- AnyJsClassMember , AnyJsClassMemberName , AnyJsFormalParameter , AnyJsName ,
14- JsAssignmentExpression , JsClassDeclaration , JsSyntaxKind , JsSyntaxNode ,
15- TsAccessibilityModifier , TsPropertyParameter ,
13+ AnyJsClassMember , AnyJsClassMemberName , AnyJsComputedMember , AnyJsExpression ,
14+ AnyJsFormalParameter , AnyJsName , JsAssignmentExpression , JsClassDeclaration , JsSyntaxKind ,
15+ JsSyntaxNode , TsAccessibilityModifier , TsPropertyParameter ,
1616} ;
1717use biome_rowan:: {
1818 AstNode , AstNodeList , AstSeparatedList , BatchMutationExt , SyntaxNodeOptionExt , TextRange ,
@@ -65,6 +65,21 @@ declare_lint_rule! {
6565 /// }
6666 /// ```
6767 ///
68+ /// ## Caveats
69+ ///
70+ /// The rule currently considers that all TypeScript private members are used if it encounters a computed access.
71+ /// In the following example `member` is not reported. It is considered as used.
72+ ///
73+ /// ```ts
74+ /// class TsBioo {
75+ /// private member: number;
76+ ///
77+ /// set_with_name(name: string, value: number) {
78+ /// this[name] = value;
79+ /// }
80+ /// }
81+ /// ```
82+ ///
6883 pub NoUnusedPrivateClassMembers {
6984 version: "1.3.3" ,
7085 name: "noUnusedPrivateClassMembers" ,
@@ -234,37 +249,55 @@ fn traverse_members_usage(
234249 syntax : & JsSyntaxNode ,
235250 mut private_members : FxHashSet < AnyMember > ,
236251) -> Vec < AnyMember > {
237- let iter = syntax. preorder ( ) ;
238-
239- for event in iter {
240- match event {
241- biome_rowan:: WalkEvent :: Enter ( node) => {
242- if let Some ( js_name) = AnyJsName :: cast ( node) {
243- private_members. retain ( |private_member| {
244- let member_being_used =
245- private_member. match_js_name ( & js_name) == Some ( true ) ;
246-
247- if !member_being_used {
248- return true ;
249- }
252+ // `true` is at least one member is a TypeScript private member like `private member`.
253+ // The other private members are sharp members `#member`.
254+ let mut ts_private_count = private_members
255+ . iter ( )
256+ . filter ( |member| !member. is_private_sharp ( ) )
257+ . count ( ) ;
250258
251- let is_write_only =
252- is_write_only ( & js_name) == Some ( true ) && !private_member. is_accessor ( ) ;
253- let is_in_update_expression = is_in_update_expression ( & js_name) ;
259+ for node in syntax. descendants ( ) {
260+ match AnyJsName :: try_cast ( node) {
261+ Ok ( js_name) => {
262+ private_members. retain ( |private_member| {
263+ let member_being_used = private_member. match_js_name ( & js_name) == Some ( true ) ;
254264
255- if is_in_update_expression || is_write_only {
256- return true ;
257- }
265+ if !member_being_used {
266+ return true ;
267+ }
258268
259- false
260- } ) ;
269+ let is_write_only =
270+ is_write_only ( & js_name) == Some ( true ) && !private_member. is_accessor ( ) ;
271+ let is_in_update_expression = is_in_update_expression ( & js_name) ;
261272
262- if private_members . is_empty ( ) {
263- break ;
273+ if is_in_update_expression || is_write_only {
274+ return true ;
264275 }
276+
277+ if !private_member. is_private_sharp ( ) {
278+ ts_private_count -= 1 ;
279+ }
280+
281+ false
282+ } ) ;
283+
284+ if private_members. is_empty ( ) {
285+ break ;
286+ }
287+ }
288+ Err ( node) => {
289+ if ts_private_count != 0
290+ && let Some ( computed_member) = AnyJsComputedMember :: cast ( node)
291+ && matches ! (
292+ computed_member. object( ) ,
293+ Ok ( AnyJsExpression :: JsThisExpression ( _) )
294+ )
295+ {
296+ // We consider that all TypeScript private members are used in expressions like `this[something]`.
297+ private_members. retain ( |private_member| private_member. is_private_sharp ( ) ) ;
298+ ts_private_count = 0 ;
265299 }
266300 }
267- biome_rowan:: WalkEvent :: Leave ( _) => { }
268301 }
269302 }
270303
@@ -409,6 +442,18 @@ impl AnyMember {
409442 )
410443 }
411444
445+ /// Returns `true` if it is a private property starting with `#`.
446+ fn is_private_sharp ( & self ) -> bool {
447+ if let Self :: AnyJsClassMember ( member) = self {
448+ matches ! (
449+ member. name( ) ,
450+ Ok ( Some ( AnyJsClassMemberName :: JsPrivateClassMemberName ( _) ) )
451+ )
452+ } else {
453+ false
454+ }
455+ }
456+
412457 fn is_private ( & self ) -> Option < bool > {
413458 match self {
414459 Self :: AnyJsClassMember ( member) => {
0 commit comments