Repository: biomejs/biome (language: rust)

Issue:
💅 Biome throw useExhaustiveDependencies error when useRef type is defined using type assertion
### Environment information

```bash
CLI:
  Version:                      1.5.2
  Color support:                true

Platform:
  CPU Architecture:             x86_64
  OS:                           linux

Environment:
  BIOME_LOG_DIR:                unset
  NO_COLOR:                     unset
  TERM:                         "xterm-256color"
  JS_RUNTIME_VERSION:           "v20.9.0"
  JS_RUNTIME_NAME:              "node"
  NODE_PACKAGE_MANAGER:         "pnpm/8.10.2"

Biome Configuration:
  Status:                       Loaded successfully
  Formatter disabled:           false
  Linter disabled:              false
  Organize imports disabled:    false
  VCS disabled:                 true

Workspace:
  Open Documents:               0
```


### Rule name

useExhaustiveDependencies

### Playground link

https://codesandbox.io/p/devbox/biome-shnrcy?file=%2Fsrc%2FApp.tsx

### Expected result

If useRef type is define using type assertion and when ref is used in any react hook, exhaustive deps breaks. 

Works fine
```
import { useEffect, useRef } from "react";

const App = () => {
  const ref = useRef<HTMLDivElement>();

  useEffect(() => {
    console.log(ref.current);
  }, []);

  return <div>Hello World</div>;
};

export default App;
```

Throw error `This hook does not specify all of its dependencies.`
```
import { type MutableRefObject, useEffect, useRef } from "react";

const App = () => {
  const ref = useRef() as MutableRefObject<HTMLDivElement>;

  useEffect(() => {
    console.log(ref.current);
  }, []);

  return <div>Hello World</div>;
};

export default App;
```

### Code of Conduct

- [X] I agree to follow Biome's Code of Conduct


Discussion / hints:
I guess it's an edge case that was not handled correctly from https://github.com/biomejs/biome/pull/996. I'll look into it after fixing https://github.com/biomejs/biome/issues/1637.
I'm not sure how many edge cases we should handle. Currently having these in mind:

- TS `as` operator (`useRef(foo) as MutableRefObject<Foo>`) (`eslint-plugin-react-hooks` only handles this)
- TS `satisfies` operator (`useRef(foo) satisfies MutableRefObject<Foo>`)
- TS non-null assertion operator (`useRef(foo)!`)

There are many other kind of edge cases, like....

- Array destructure (`const [a, b] = [useRef('a'), useRef('b')]`)
- Comma operator (`const a = (console.log('a'), useRef('a'))`)
- IIFE (`const a = (() => useRef('a'))()`, can be much more complex)
- let reassignment (`let a = useRef('a'); a = Math.random()`)
- ...

It's somewhat obvious that it is unrealistic to handle all of those edge cases. Therefore I believe we should scope which edge cases to handle. Personally, I think all of TS operator cases and array destructure should be handled, while other cases are too specific/niche to work on.
I think so too. We should strive to cover the most used patterns and call it a day.

We should take this opportunity and update the documentation explaining the expectations of this rule
Speaking of IIFE, there're also cases where one can use IIFE to return a function that can be used in `useCallback` or `useEffect`. This is a [common functional pattern in JS](https://stackoverflow.com/a/15717241) to hoist and memoize *static* variables. The `useExhaustiveDependencies` rule doesn't catch it currently. It would be great to handle this kind of edge cases.

https://biomejs.dev/playground/?code=aQBtAHAAbwByAHQAIAB7ACAAdQBzAGUARQBmAGYAZQBjAHQALAAgAHUAcwBlAFMAdABhAHQAZQAgAH0AIABmAHIAbwBtACAAIgByAGUAYQBjAHQAIgA7AAoACgBlAHgAcABvAHIAdAAgAGYAdQBuAGMAdABpAG8AbgAgAEYAKAApACAAewAKAAoAIAAgAGMAbwBuAHMAdAAgAFsAYQAsACAAcwBlAHQAQQBdACAAPQAgAHUAcwBlAFMAdABhAHQAZQAoADEAKQA7AAoAIAAgAAoAIAAgAHUAcwBlAEUAZgBmAGUAYwB0ACgAKAApACAAPQA%2BACAAYwBvAG4AcwBvAGwAZQAuAGwAbwBnACgAYQApACwAIABbAF0AKQA7AAoACgAgACAAdQBzAGUARQBmAGYAZQBjAHQAKAAoACgAKQAgAD0APgAgACgAKQAgAD0APgAgAGMAbwBuAHMAbwBsAGUALgBsAG8AZwAoAGEAKQApACgAKQAsACAAWwBdACkAOwAKAH0A
I ran into this yesterday, just found this issue unresolved so I figured I'd take a look at how `eslint-plugin-react-hooks` handles it. I use both in my project, and if I incorrectly include `ref.current` in my deps array as Biome wants me to, ESLint does catch it as unnecessary.

The way they handle this actually seems pretty clever and as far as I can tell cleanly solves at least a good majority of the edge cases you listed. They simply check if the value was returned from a `useRef` call: https://github.com/facebook/react/blob/163365a07872337e04826c4f501565d43dbd2fd4/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js#L251

I also found this on my way there, a lot of good discussion on this topic from when they developed their own exhaustive deps rule: https://github.com/facebook/react/issues/14920

Fix patch:
diff --git a/.changeset/fix_1597_useexhaustivedependencies_now_consider_react_hooks_stable_within_parentheses_or_type_assertions.md b/.changeset/fix_1597_useexhaustivedependencies_now_consider_react_hooks_stable_within_parentheses_or_type_assertions.md
new file mode 100644
index 000000000000..f0ad6fc9841d
--- /dev/null
+++ b/.changeset/fix_1597_useexhaustivedependencies_now_consider_react_hooks_stable_within_parentheses_or_type_assertions.md
@@ -0,0 +1,5 @@
+---
+biome_js_analyze: patch
+---
+
+# Fix #1597, useExhaustiveDependencies now consider React hooks stable within parentheses or type assertions
diff --git a/crates/biome_js_analyze/src/react/hooks.rs b/crates/biome_js_analyze/src/react/hooks.rs
index 8c3cb9031796..1f9c10869c75 100644
--- a/crates/biome_js_analyze/src/react/hooks.rs
+++ b/crates/biome_js_analyze/src/react/hooks.rs
@@ -447,7 +447,7 @@ pub fn is_binding_react_stable(
     let Some(callee) = declarator
         .initializer()
         .and_then(|initializer| initializer.expression().ok())
-        .and_then(|initializer| initializer.as_js_call_expression()?.callee().ok())
+        .and_then(|initializer| unwrap_to_call_expression(initializer)?.callee().ok())
     else {
         return false;
     };
@@ -474,6 +474,32 @@ pub fn is_binding_react_stable(
     })
 }
 
+/// Unwrap the expression to a call expression without changing the result of the expression,
+/// such as type assertions.
+fn unwrap_to_call_expression(mut expression: AnyJsExpression) -> Option<JsCallExpression> {
+    loop {
+        match expression {
+            AnyJsExpression::JsCallExpression(expr) => return Some(expr),
+            AnyJsExpression::JsParenthesizedExpression(expr) => {
+                expression = expr.expression().ok()?;
+            }
+            AnyJsExpression::JsSequenceExpression(expr) => {
+                expression = expr.right().ok()?;
+            }
+            AnyJsExpression::TsAsExpression(expr) => {
+                expression = expr.expression().ok()?;
+            }
+            AnyJsExpression::TsSatisfiesExpression(expr) => {
+                expression = expr.expression().ok()?;
+            }
+            AnyJsExpression::TsNonNullAssertionExpression(expr) => {
+                expression = expr.expression().ok()?;
+            }
+            _ => return None,
+        }
+    }
+}
+
 #[cfg(test)]
 mod test {
     use super::*;


Test patch:
diff --git a/crates/biome_js_analyze/tests/specs/correctness/useExhaustiveDependencies/valid.ts b/crates/biome_js_analyze/tests/specs/correctness/useExhaustiveDependencies/valid.ts
index 329ce4db8a16..ee52cebd7826 100644
--- a/crates/biome_js_analyze/tests/specs/correctness/useExhaustiveDependencies/valid.ts
+++ b/crates/biome_js_analyze/tests/specs/correctness/useExhaustiveDependencies/valid.ts
@@ -1,6 +1,6 @@
 /* should not generate diagnostics */
 
-import { useEffect } from "react";
+import { type MutableRefObject, useEffect, useRef } from "react";
 
 // capturing declarations
 function overloaded(s: string): string;
@@ -46,3 +46,20 @@ export function MyComponent3({ outer }: { outer: string[] }) {
 			console.log(a)
 		}, []);
 }
+
+// useRef's are still considered as stable even if there's any type assertion
+export function MyComponent4() {
+    const parenthesizedRef = (((useRef())));
+    const sequenceRef = (doSomething(), useRef());
+    const nonNullAssertedRef = useRef()!;
+    const castedRef = useRef() as MutableRefObject<HTMLElement>;
+    const satisfiedRef = useRef() satisfies MutableRefObject<HTMLElement>;
+
+    useEffect(() => {
+        console.log(parenthesizedRef.current);
+        console.log(sequenceRef.current);
+        console.log(nonNullAssertedRef.current);
+        console.log(castedRef.current.innerHTML);
+        console.log(satisfiedRef.current.innerHTML);
+    }, []);
+}
diff --git a/crates/biome_js_analyze/tests/specs/correctness/useExhaustiveDependencies/valid.ts.snap b/crates/biome_js_analyze/tests/specs/correctness/useExhaustiveDependencies/valid.ts.snap
index a81f964d7d46..8a09d5a4eb6b 100644
--- a/crates/biome_js_analyze/tests/specs/correctness/useExhaustiveDependencies/valid.ts.snap
+++ b/crates/biome_js_analyze/tests/specs/correctness/useExhaustiveDependencies/valid.ts.snap
@@ -6,7 +6,7 @@ expression: valid.ts
 ```ts
 /* should not generate diagnostics */
 
-import { useEffect } from "react";
+import { type MutableRefObject, useEffect, useRef } from "react";
 
 // capturing declarations
 function overloaded(s: string): string;
@@ -53,6 +53,21 @@ export function MyComponent3({ outer }: { outer: string[] }) {
 		}, []);
 }
 
-```
+// useRef's are still considered as stable even if there's any type assertion
+export function MyComponent4() {
+    const parenthesizedRef = (((useRef())));
+    const sequenceRef = (doSomething(), useRef());
+    const nonNullAssertedRef = useRef()!;
+    const castedRef = useRef() as MutableRefObject<HTMLElement>;
+    const satisfiedRef = useRef() satisfies MutableRefObject<HTMLElement>;
 
+    useEffect(() => {
+        console.log(parenthesizedRef.current);
+        console.log(sequenceRef.current);
+        console.log(nonNullAssertedRef.current);
+        console.log(castedRef.current.innerHTML);
+        console.log(satisfiedRef.current.innerHTML);
+    }, []);
+}
 
+```