diff --git a/crates/biome_js_analyze/src/lint/correctness/use_exhaustive_dependencies.rs b/crates/biome_js_analyze/src/lint/correctness/use_exhaustive_dependencies.rs index 0d48534bc368..b5c5575c9a00 100644 --- a/crates/biome_js_analyze/src/lint/correctness/use_exhaustive_dependencies.rs +++ b/crates/biome_js_analyze/src/lint/correctness/use_exhaustive_dependencies.rs @@ -651,6 +651,10 @@ fn is_stable_binding( return true; } + let Some(decl) = binding.declaration() else { + return false; + }; + // Any declarations outside the component function are considered stable if binding .range() @@ -660,10 +664,6 @@ fn is_stable_binding( return true; } - let Some(decl) = binding.declaration() else { - return false; - }; - match decl.parent_binding_pattern_declaration().unwrap_or(decl) { // These declarations are always stable AnyJsBindingDeclaration::JsClassDeclaration(_) @@ -849,14 +849,28 @@ fn is_stable_expression( && let Some(binding) = model.binding(&name) { let binding = &binding.tree(); - if let Some(declaration_node) = - &binding.declaration().map(|decl| decl.syntax().clone()) - && identifier + // Check for self-reference (e.g., using a variable in its own initializer) + // but NOT for arrow function parameters, since they are meant to be used + // inside the function body. + if let Some(decl) = binding.declaration() { + let declaration_node = decl.syntax().clone(); + let is_ancestor = identifier .syntax() .ancestors() - .any(|ancestor| declaration_node == &ancestor) - { - return true; + .any(|ancestor| declaration_node == ancestor); + // Only treat as self-reference if the declaration is NOT an arrow function + // or formal parameter. For arrow function parameters like `props =>`, + // the identifier `props` used in the body should NOT be considered stable. + if is_ancestor + && !matches!( + decl, + AnyJsBindingDeclaration::JsArrowFunctionExpression(_) + | AnyJsBindingDeclaration::JsFormalParameter(_) + | AnyJsBindingDeclaration::JsRestParameter(_) + ) + { + return true; + } } is_stable_binding( @@ -868,6 +882,9 @@ fn is_stable_expression( depth + 1, ) } else { + // If we can't find the binding (e.g., an undefined global or external reference), + // assume it's stable. Unknown references are typically globals or externals that + // don't change between renders. true } } diff --git a/crates/biome_js_analyze/tests/specs/correctness/useExhaustiveDependencies/issue8883.tsx b/crates/biome_js_analyze/tests/specs/correctness/useExhaustiveDependencies/issue8883.tsx new file mode 100644 index 000000000000..ef5fc417c5b3 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/useExhaustiveDependencies/issue8883.tsx @@ -0,0 +1,12 @@ +import { useEffect } from 'react'; + +// Test: Arrow function with parameter without parentheses, using props directly +const TestDirect = props => { + useEffect(() => console.log(props.msg), [props.msg]); +}; + +// Test: Arrow function with parameter without parentheses, destructuring in body +const TestDestructure = props => { + const { msg } = props; + useEffect(() => console.log(msg), [msg]); +}; diff --git a/crates/biome_js_analyze/tests/specs/correctness/useExhaustiveDependencies/issue8883.tsx.snap b/crates/biome_js_analyze/tests/specs/correctness/useExhaustiveDependencies/issue8883.tsx.snap new file mode 100644 index 000000000000..92e1181916b3 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/useExhaustiveDependencies/issue8883.tsx.snap @@ -0,0 +1,21 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 152 +expression: issue8883.tsx +--- +# Input +```tsx +import { useEffect } from 'react'; + +// Test: Arrow function with parameter without parentheses, using props directly +const TestDirect = props => { + useEffect(() => console.log(props.msg), [props.msg]); +}; + +// Test: Arrow function with parameter without parentheses, destructuring in body +const TestDestructure = props => { + const { msg } = props; + useEffect(() => console.log(msg), [msg]); +}; + +```