@@ -4,10 +4,10 @@ use biome_analyze::{
44} ;
55use biome_console:: markup;
66use biome_diagnostics:: Severity ;
7- use biome_js_semantic:: ReferencesExtensions ;
7+ use biome_js_semantic:: { Reference , ReferencesExtensions } ;
88use biome_js_syntax:: {
99 AnyJsExpression , JsAssignmentExpression , JsCallExpression , JsIdentifierBinding ,
10- JsVariableDeclarator , binding_ext:: AnyJsParameterParentFunction ,
10+ JsVariableDeclarator , binding_ext:: AnyJsParameterParentFunction , function_ext :: AnyFunctionLike ,
1111} ;
1212use biome_rowan:: { AstNode , BatchMutationExt , TokenText } ;
1313
@@ -17,8 +17,6 @@ declare_lint_rule! {
1717 /// A parameter that is only passed to recursive calls is effectively unused
1818 /// and can be removed or replaced with a constant, simplifying the function.
1919 ///
20- /// This rule is inspired by Rust Clippy's `only_used_in_recursion` lint.
21- ///
2220 /// ## Examples
2321 ///
2422 /// ### Invalid
@@ -124,32 +122,26 @@ impl Rule for NoParametersOnlyUsedInRecursion {
124122 }
125123
126124 // Get function name for recursion detection
127- let function_name = get_function_name ( & parent_function) ;
125+ let function_name = get_function_name ( & parent_function) ? ;
128126
129127 // Get function binding for semantic comparison
130128 let parent_function_binding = get_function_binding ( & parent_function, model) ;
131129
132- // Get all references to this parameter
133- let all_refs: Vec < _ > = binding. all_references ( model) . collect ( ) ;
134-
135- // If no references, let noUnusedFunctionParameters handle it
136- if all_refs. is_empty ( ) {
137- return None ;
138- }
139-
140130 // Classify references
141131 let mut refs_in_recursion = 0 ;
142132 let mut refs_elsewhere = 0 ;
143133
144- for reference in all_refs {
134+ for reference in binding . all_references ( model ) {
145135 if is_reference_in_recursive_call (
146136 & reference,
147- function_name. as_ref ( ) ,
137+ & function_name,
148138 & parent_function,
149139 name_text,
150140 model,
151141 parent_function_binding. as_ref ( ) ,
152- ) {
142+ )
143+ . unwrap_or_default ( )
144+ {
153145 refs_in_recursion += 1 ;
154146 } else {
155147 refs_elsewhere += 1 ;
@@ -293,7 +285,7 @@ fn get_function_binding(
293285 return model. binding ( js_id_assignment) ;
294286 }
295287
296- if is_function_like ( & ancestor) {
288+ if AnyFunctionLike :: can_cast ( ancestor. kind ( ) ) {
297289 break ;
298290 }
299291 }
@@ -343,29 +335,16 @@ fn get_arrow_function_name(
343335
344336 // Stop searching if we hit a function boundary
345337 // (prevents extracting wrong name from outer scope)
346- if is_function_like ( & ancestor) {
338+ if AnyFunctionLike :: can_cast ( ancestor. kind ( ) ) {
347339 break ;
348340 }
349341 }
350342
351343 None
352344}
353345
354- /// Checks if a syntax node is a function-like boundary
355- /// (we should stop searching for names beyond these)
356- fn is_function_like ( node : & biome_rowan:: SyntaxNode < biome_js_syntax:: JsLanguage > ) -> bool {
357- use biome_js_syntax:: JsSyntaxKind :: * ;
358- matches ! (
359- node. kind( ) ,
360- JS_FUNCTION_DECLARATION
361- | JS_FUNCTION_EXPRESSION
362- | JS_ARROW_FUNCTION_EXPRESSION
363- | JS_METHOD_CLASS_MEMBER
364- | JS_METHOD_OBJECT_MEMBER
365- | JS_CONSTRUCTOR_CLASS_MEMBER
366- )
367- }
368-
346+ /// Returns true if the function is a TypeScript signature without an implementation body.
347+ /// Matches interface method signatures, call signatures, function types, and declared functions.
369348fn is_function_signature ( parent_function : & AnyJsParameterParentFunction ) -> bool {
370349 matches ! (
371350 parent_function,
@@ -384,109 +363,95 @@ fn is_function_signature(parent_function: &AnyJsParameterParentFunction) -> bool
384363 )
385364}
386365
366+ /// Checks if a call expression is a recursive call to the current function.
367+ /// Handles direct calls (`foo()`), method calls (`this.foo()`), and computed members (`this["foo"]()`).
368+ /// Uses a conservative approach to avoid false positives.
387369fn is_recursive_call (
388370 call : & JsCallExpression ,
389371 function_name : Option < & TokenText > ,
390372 model : & biome_js_semantic:: SemanticModel ,
391373 parent_function_binding : Option < & biome_js_semantic:: Binding > ,
392- ) -> bool {
393- let Ok ( callee) = call. callee ( ) else {
394- return false ;
395- } ;
396-
397- let Some ( name) = function_name else {
398- return false ;
399- } ;
374+ ) -> Option < bool > {
375+ let callee = call. callee ( ) . ok ( ) ?;
400376
401377 let expr = callee. omit_parentheses ( ) ;
402378
403379 // Simple identifier: foo()
404380 if let Some ( ref_id) = expr. as_js_reference_identifier ( ) {
405- let name_matches = ref_id. name ( ) . ok ( ) . is_some_and ( |n| n. text ( ) == name. text ( ) ) ;
381+ let name = ref_id. value_token ( ) . ok ( ) ?;
382+ let name_matches = name. token_text_trimmed ( ) == * function_name?;
406383 if !name_matches {
407- return false ;
384+ return Some ( false ) ;
408385 }
409386
410387 let called_binding = model. binding ( & ref_id) ;
411388
412389 match ( parent_function_binding, called_binding) {
413390 // Both have bindings - compare them directly
414391 ( Some ( parent_binding) , Some ( called_binding) ) => {
415- return called_binding == * parent_binding;
392+ return Some ( called_binding == * parent_binding) ;
416393 }
417394 // Parent has no binding (e.g. in the case of a method),
418395 // but call resolves to a binding
419396 ( None , Some ( _) ) => {
420- return false ;
397+ return Some ( false ) ;
421398 }
422399 // Parent has binding but call doesn't resolve
423400 ( Some ( _) , None ) => {
424- return false ;
401+ return Some ( false ) ;
425402 }
426403 // Neither has a binding. Fall back to name comparison
427404 ( None , None ) => {
428- return name_matches;
405+ return Some ( name_matches) ;
429406 }
430407 }
431408 }
432409
433410 // Member expression: this.foo() or this?.foo()
434411 if let Some ( member) = expr. as_js_static_member_expression ( ) {
435412 // Check if object is 'this' (for method calls)
436- let is_this_call = member
437- . object ( )
438- . ok ( )
439- . is_some_and ( |obj| obj. as_js_this_expression ( ) . is_some ( ) ) ;
440-
441- if !is_this_call {
442- return false ;
413+ let object = member. object ( ) . ok ( ) ?;
414+ if object. as_js_this_expression ( ) . is_none ( ) {
415+ return Some ( false ) ;
443416 }
444417
445418 // Check if member name matches function name
446- let member_name_matches = member. member ( ) . ok ( ) . is_some_and ( |m| {
447- m. as_js_name ( )
448- . and_then ( |n| n. value_token ( ) . ok ( ) )
449- . is_some_and ( |t| t. text_trimmed ( ) == name. text ( ) )
450- } ) ;
451-
452- return member_name_matches;
419+ let member_node = member. member ( ) . ok ( ) ?;
420+ let name = member_node. as_js_name ( ) ?;
421+ let token = name. value_token ( ) . ok ( ) ?;
422+ return Some ( token. token_text_trimmed ( ) == * function_name?) ;
453423 }
454424
455425 // Computed member expression: this["foo"]() or this?.["foo"]()
456426 if let Some ( computed) = expr. as_js_computed_member_expression ( ) {
457427 // Check if object is 'this' (for method calls)
458- let is_this_call = computed
459- . object ( )
460- . ok ( )
461- . is_some_and ( |obj| obj. as_js_this_expression ( ) . is_some ( ) ) ;
462-
463- if !is_this_call {
464- return false ;
428+ let object = computed. object ( ) . ok ( ) ?;
429+ if object. as_js_this_expression ( ) . is_none ( ) {
430+ return Some ( false ) ;
465431 }
466432
467433 // Conservative approach: only handle string literal members
468- if let Ok ( member_expr) = computed. member ( )
469- && let Some ( lit) = member_expr. as_any_js_literal_expression ( )
470- && let Some ( string_lit) = lit. as_js_string_literal_expression ( )
471- && let Ok ( text) = string_lit. inner_string_text ( )
472- {
473- return text. text ( ) == name. text ( ) ;
474- }
475-
476- return false ;
434+ let member_expr = computed. member ( ) . ok ( ) ?;
435+ let lit = member_expr. as_any_js_literal_expression ( ) ?;
436+ let string_lit = lit. as_js_string_literal_expression ( ) ?;
437+ let text = string_lit. inner_string_text ( ) . ok ( ) ?;
438+ return Some ( text == * function_name?) ;
477439 }
478440
479- false
441+ Some ( false )
480442}
481443
444+ /// Checks if a parameter reference occurs within a recursive call expression.
445+ /// Walks up the syntax tree from the reference to find a recursive call that uses the parameter,
446+ /// stopping at the function boundary.
482447fn is_reference_in_recursive_call (
483- reference : & biome_js_semantic :: Reference ,
484- function_name : Option < & TokenText > ,
448+ reference : & Reference ,
449+ function_name : & TokenText ,
485450 parent_function : & AnyJsParameterParentFunction ,
486451 param_name : & str ,
487452 model : & biome_js_semantic:: SemanticModel ,
488453 parent_function_binding : Option < & biome_js_semantic:: Binding > ,
489- ) -> bool {
454+ ) -> Option < bool > {
490455 let ref_node = reference. syntax ( ) ;
491456
492457 // Walk up the tree to find if we're inside a call expression
@@ -495,14 +460,14 @@ fn is_reference_in_recursive_call(
495460 // Check if this is a call expression
496461 if let Some ( call_expr) = JsCallExpression :: cast_ref ( & node) {
497462 // Check if this call is recursive AND uses our parameter
498- if is_recursive_call_with_param_usage (
463+ if let Some ( true ) = is_recursive_call_with_param_usage (
499464 & call_expr,
500465 function_name,
501466 param_name,
502467 model,
503468 parent_function_binding,
504469 ) {
505- return true ;
470+ return Some ( true ) ;
506471 }
507472 }
508473
@@ -514,7 +479,7 @@ fn is_reference_in_recursive_call(
514479 current = node. parent ( ) ;
515480 }
516481
517- false
482+ Some ( false )
518483}
519484
520485fn is_function_boundary (
@@ -530,18 +495,18 @@ fn is_function_boundary(
530495///
531496/// Uses an iterative approach with a worklist to avoid stack overflow
532497/// on deeply nested expressions.
533- fn traces_to_parameter ( expr : & AnyJsExpression , param_name : & str ) -> bool {
498+ fn traces_to_parameter ( expr : & AnyJsExpression , param_name : & str ) -> Option < bool > {
534499 // Worklist of expressions to examine
535500 let mut to_check = vec ! [ expr. clone( ) ] ;
536501
537502 while let Some ( current_expr) = to_check. pop ( ) {
538503 // Omit parentheses
539504 let current_expr = current_expr. omit_parentheses ( ) ;
540505
541- // Direct parameter reference - found it!
542506 if let Some ( ref_id) = current_expr. as_js_reference_identifier ( ) {
543- if ref_id. name ( ) . ok ( ) . is_some_and ( |n| n. text ( ) == param_name) {
544- return true ;
507+ if ref_id. name ( ) . ok ( ) ?. text ( ) == param_name {
508+ // Found direct parameter reference
509+ return Some ( true ) ;
545510 }
546511 continue ;
547512 }
@@ -588,48 +553,50 @@ fn traces_to_parameter(expr: &AnyJsExpression, param_name: &str) -> bool {
588553 // Unary operations: -a, !flag
589554 // Add argument to worklist
590555 if let Some ( unary_expr) = current_expr. as_js_unary_expression ( ) {
591- if let Ok ( arg ) = unary_expr. argument ( ) {
592- to_check. push ( arg ) ;
556+ if let Ok ( argument ) = unary_expr. argument ( ) {
557+ to_check. push ( argument ) ;
593558 }
594559 continue ;
595560 }
596561
597562 // Static member access: obj.field
598563 // Add object to worklist
599564 if let Some ( member_expr) = current_expr. as_js_static_member_expression ( )
600- && let Ok ( obj ) = member_expr. object ( )
565+ && let Ok ( object ) = member_expr. object ( )
601566 {
602- to_check. push ( obj ) ;
567+ to_check. push ( object ) ;
603568 }
604569
605570 // Any other expression - not safe to trace
606571 // Just continue to next item in worklist
607572 }
608573
609574 // Didn't find the parameter anywhere
610- false
575+ Some ( false )
611576}
612577
613- /// Enhanced version that checks if any argument traces to parameters
578+ /// Checks if a recursive call uses a specific parameter in its arguments.
579+ /// Examines each argument to see if it traces back to the parameter through transformations
580+ /// like arithmetic operations, unary operations, or member access.
614581fn is_recursive_call_with_param_usage (
615582 call : & JsCallExpression ,
616- function_name : Option < & TokenText > ,
583+ function_name : & TokenText ,
617584 param_name : & str ,
618585 model : & biome_js_semantic:: SemanticModel ,
619586 parent_function_binding : Option < & biome_js_semantic:: Binding > ,
620- ) -> bool {
587+ ) -> Option < bool > {
621588 // First check if this is a recursive call at all
622- if !is_recursive_call ( call, function_name, model, parent_function_binding) {
623- return false ;
589+ if !is_recursive_call ( call, Some ( function_name) , model, parent_function_binding) ? {
590+ return Some ( false ) ;
624591 }
625592
626593 // Check if any argument uses the parameter
627- let Ok ( arguments) = call. arguments ( ) else {
628- return false ;
629- } ;
594+ let arguments = call. arguments ( ) . ok ( ) ?;
630595
631596 for arg in arguments. args ( ) {
632- let Ok ( arg_node) = arg else { continue } ;
597+ let Some ( arg_node) = arg. ok ( ) else {
598+ continue ;
599+ } ;
633600
634601 // Skip spread arguments (conservative)
635602 if arg_node. as_js_spread ( ) . is_some ( ) {
@@ -638,11 +605,11 @@ fn is_recursive_call_with_param_usage(
638605
639606 // Check if argument expression uses the parameter
640607 if let Some ( expr) = arg_node. as_any_js_expression ( )
641- && traces_to_parameter ( expr, param_name)
608+ && traces_to_parameter ( expr, param_name) ?
642609 {
643- return true ;
610+ return Some ( true ) ;
644611 }
645612 }
646613
647- false
614+ Some ( false )
648615}
0 commit comments