Skip to content

Commit 5cd3d27

Browse files
matanshavitautofix-ci[bot]ematipico
authored
refactor(lint): refactor NoParametersOnlyUsedInRecursion (#7970)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Emanuele Stoppa <[email protected]>
1 parent 0170dcb commit 5cd3d27

File tree

1 file changed

+72
-105
lines changed

1 file changed

+72
-105
lines changed

crates/biome_js_analyze/src/lint/nursery/no_parameters_only_used_in_recursion.rs

Lines changed: 72 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ use biome_analyze::{
44
};
55
use biome_console::markup;
66
use biome_diagnostics::Severity;
7-
use biome_js_semantic::ReferencesExtensions;
7+
use biome_js_semantic::{Reference, ReferencesExtensions};
88
use biome_js_syntax::{
99
AnyJsExpression, JsAssignmentExpression, JsCallExpression, JsIdentifierBinding,
10-
JsVariableDeclarator, binding_ext::AnyJsParameterParentFunction,
10+
JsVariableDeclarator, binding_ext::AnyJsParameterParentFunction, function_ext::AnyFunctionLike,
1111
};
1212
use 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.
369348
fn 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.
387369
fn 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.
482447
fn 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

520485
fn 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.
614581
fn 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

Comments
 (0)