Skip to content

fix(lint): handle forwardRef callbacks in useHookAtTopLevel#9648

Closed
raashish1601 wants to merge 3 commits intobiomejs:mainfrom
raashish1601:fix-9195-forwardref-hook-detection
Closed

fix(lint): handle forwardRef callbacks in useHookAtTopLevel#9648
raashish1601 wants to merge 3 commits intobiomejs:mainfrom
raashish1601:fix-9195-forwardref-hook-detection

Conversation

@raashish1601
Copy link
Copy Markdown

@raashish1601 raashish1601 commented Mar 29, 2026

Summary

  • treat functions passed by identifier into forwardRef or React.forwardRef as component render callbacks for useHookAtTopLevel
  • add a regression spec covering named forwardRef callbacks in both forms

Closes #9195.

Testing

  • rustfmt crates/biome_js_analyze/src/lint/correctness/use_hook_at_top_level.rs
  • cargo test -p biome_js_analyze --test spec_tests useHookAtTopLevel (fails in this worker checkout because the machine runs out of disk space during dependency compilation)

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 29, 2026

🦋 Changeset detected

Latest commit: bd86ae1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 13 packages
Name Type
@biomejs/biome Patch
@biomejs/cli-win32-x64 Patch
@biomejs/cli-win32-arm64 Patch
@biomejs/cli-darwin-x64 Patch
@biomejs/cli-darwin-arm64 Patch
@biomejs/cli-linux-x64 Patch
@biomejs/cli-linux-arm64 Patch
@biomejs/cli-linux-x64-musl Patch
@biomejs/cli-linux-arm64-musl Patch
@biomejs/wasm-web Patch
@biomejs/wasm-bundler Patch
@biomejs/wasm-nodejs Patch
@biomejs/backend-jsonrpc Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions bot added A-Linter Area: linter L-JavaScript Language: JavaScript and super languages labels Mar 29, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 29, 2026

Warning

Rate limit exceeded

@raashish1601 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 5 minutes and 33 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 5 minutes and 33 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 771c04a8-ebf6-4d66-8839-19077a6021e2

📥 Commits

Reviewing files that changed from the base of the PR and between 4df61ad and bd86ae1.

📒 Files selected for processing (1)
  • .changeset/fix-use-hook-at-top-level-forward-ref.md

Walkthrough

The lint rule use_hook_at_top_level now classifies React components/hooks using semantic information: AnyJsFunctionOrMethod::is_react_component_or_hook accepts a &SemanticModel and detects render functions created via forwardRef by examining binding references and React.forwardRef calls. Nested-function exemption and multiple call sites in UseHookAtTopLevel::run were updated to use the model-aware predicate. New tests validate functions wrapped with forwardRef are treated as components and do not trigger hook-at-top-level diagnostics.

Possibly related PRs

Suggested reviewers

  • ematipico
  • dyc3
  • mdevils
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarises the main change: handling forwardRef callbacks in the useHookAtTopLevel lint rule.
Description check ✅ Passed The description is directly related to the changeset, covering the fix for treating functions passed into forwardRef as component render callbacks and adding test coverage.
Linked Issues check ✅ Passed The PR addresses issue #9195 by implementing detection of forwardRef render functions and treating them as component callbacks, satisfying the core requirements for parameter recognition and conservative heuristics.
Out of Scope Changes check ✅ Passed All changes are focused on the useHookAtTopLevel lint rule and its test fixtures, directly related to the linked issue requirements without introducing unrelated modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/biome_js_analyze/src/lint/correctness/use_hook_at_top_level.rs`:
- Line 141: The code is using a Result pattern for function.binding() but that
method returns an Option; change the pattern from `let Ok(binding) =
function.binding()` to `let Some(binding) = function.binding()` and adjust the
corresponding else branch handling if needed in the function (the match around
the `function.binding()` call in use_hook_at_top_level.rs); ensure any
error/unreachable logic remains correct for the None case rather than treating
it like a Result::Err.
- Around line 162-177: The current code misuses SyntaxNode::parent generics and
AnyJsCallArgument APIs: replace the generic parent::<AnyJsCallArgument>() call
with a plain parent() followed by casting the returned SyntaxNode to
AnyJsCallArgument (e.g., via AnyJsCallArgument::cast or pattern match) to get an
argument value, and instead of calling as_any_js_expression() (which doesn't
exist on AnyJsCallArgument) extract the inner expression by pattern-matching the
AnyJsCallArgument variants or using the provided accessor (e.g., match on
AnyJsCallArgument::Expr or call a method like AnyJsCallArgument::expression())
so you obtain argument_expression whose omit_parentheses().syntax() can be
compared to expression.syntax(); keep the existing ancestor search using
JsCallExpression::cast unchanged.
- Around line 179-188: The compiler can't infer callee's concrete type here; add
an explicit type annotation on the let binding so the returned value from
call_expression.callee() has a concrete type before calling
callee.omit_parentheses() and is_react_call_api. Change the binding to the form
`let Ok(callee): Result<TheConcreteCalleeType, _> = call_expression.callee()
else { return false; };` (or `let Ok(callee): TheConcreteCalleeType = ...` if
the function returns the type directly), using the actual return type of
call_expression.callee() as defined in its signature, then keep the subsequent
call to callee.omit_parentheses() and is_react_call_api unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a996b240-0e80-4926-8c9d-c8f3804cabf4

📥 Commits

Reviewing files that changed from the base of the PR and between c17e08e and 1f5afd2.

⛔ Files ignored due to path filters (1)
  • crates/biome_js_analyze/tests/specs/correctness/useHookAtTopLevel/valid.js.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (2)
  • crates/biome_js_analyze/src/lint/correctness/use_hook_at_top_level.rs
  • crates/biome_js_analyze/tests/specs/correctness/useHookAtTopLevel/valid.js

let Self::AnyJsFunction(function) = self else {
return false;
};
let Ok(binding) = function.binding() else {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

function.binding() returns Option, not Result.

The pipeline failure confirms this: use let Some(binding) = ... instead of let Ok(binding) = ....

🔧 Proposed fix
-        let Ok(binding) = function.binding() else {
+        let Some(binding) = function.binding() else {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let Ok(binding) = function.binding() else {
let Some(binding) = function.binding() else {
🧰 Tools
🪛 GitHub Actions: autofix.ci

[error] 141-141: Rust compile error E0308: mismatched types. function.binding() has type Option<AnyJsBinding>, but code pattern expects Result via let Ok(binding) = ... else. Expected Option<AnyJsBinding>, found Result<_, _>.

🪛 GitHub Actions: Lint rule docs

[error] 141-141: Rust compile error E0308 (mismatched types): function.binding() has type Option<AnyJsBinding> but code uses let Ok(binding) = ... else expecting a Result. Expected Option<AnyJsBinding>, found Result<_, _>.

🪛 GitHub Actions: Pull request Node.js

[error] 141-141: Rust compiler error E0308: mismatched types in use_hook_at_top_level.rs. let Ok(binding) = function.binding() else { ... } expects Option<AnyJsBinding> but found Result<_, _>.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_js_analyze/src/lint/correctness/use_hook_at_top_level.rs` at
line 141, The code is using a Result pattern for function.binding() but that
method returns an Option; change the pattern from `let Ok(binding) =
function.binding()` to `let Some(binding) = function.binding()` and adjust the
corresponding else branch handling if needed in the function (the match around
the `function.binding()` call in use_hook_at_top_level.rs); ensure any
error/unreachable logic remains correct for the None case rather than treating
it like a Result::Err.

Comment on lines +162 to +177
let Some(argument) = expression.syntax().parent::<AnyJsCallArgument>() else {
return false;
};
let Some(argument_expression) = argument.as_any_js_expression() else {
return false;
};
if argument_expression.omit_parentheses().syntax() != expression.syntax() {
return false;
}

let Some(call_expression) = argument
.syntax()
.ancestors()
.find_map(JsCallExpression::cast)
else {
return false;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Several API misuses causing compilation failures.

The parent() method on SyntaxNode doesn't accept generic arguments. You need to call parent() and then cast separately.

Additionally, once you fix the parent call, argument will be AnyJsCallArgument, which doesn't have as_any_js_expression(). You likely want to extract the inner expression via pattern matching or a dedicated method.

🔧 Proposed fix
-            let Some(argument) = expression.syntax().parent::<AnyJsCallArgument>() else {
-                return false;
-            };
-            let Some(argument_expression) = argument.as_any_js_expression() else {
-                return false;
-            };
-            if argument_expression.omit_parentheses().syntax() != expression.syntax() {
-                return false;
-            }
-
-            let Some(call_expression) = argument
-                .syntax()
-                .ancestors()
-                .find_map(JsCallExpression::cast)
+            let Some(argument) = expression
+                .syntax()
+                .parent()
+                .and_then(AnyJsCallArgument::cast)
             else {
                 return false;
             };
+            let AnyJsCallArgument::AnyJsExpression(argument_expression) = &argument else {
+                return false;
+            };
+            if argument_expression.omit_parentheses().syntax() != expression.syntax() {
+                return false;
+            }
+
+            let Some(call_expression) = argument.syntax().ancestors().find_map(JsCallExpression::cast) else {
+                return false;
+            };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let Some(argument) = expression.syntax().parent::<AnyJsCallArgument>() else {
return false;
};
let Some(argument_expression) = argument.as_any_js_expression() else {
return false;
};
if argument_expression.omit_parentheses().syntax() != expression.syntax() {
return false;
}
let Some(call_expression) = argument
.syntax()
.ancestors()
.find_map(JsCallExpression::cast)
else {
return false;
let Some(argument) = expression
.syntax()
.parent()
.and_then(AnyJsCallArgument::cast)
else {
return false;
};
let AnyJsCallArgument::AnyJsExpression(argument_expression) = &argument else {
return false;
};
if argument_expression.omit_parentheses().syntax() != expression.syntax() {
return false;
}
let Some(call_expression) = argument.syntax().ancestors().find_map(JsCallExpression::cast) else {
return false;
};
🧰 Tools
🪛 GitHub Actions: autofix.ci

[error] 162-162: Rust compile error E0107: method takes 0 generic arguments but 1 was supplied. Call expression.syntax().parent::<AnyJsCallArgument>(); parent() is defined as pub fn parent(&self) -> Option<Self> (no generics).


[error] 165-165: Rust compile error E0599: no method named as_any_js_expression found for SyntaxNode<JsLanguage> at argument.as_any_js_expression().


[error] 168-168: Rust compile error E0282: type annotations needed. Cannot infer type at argument_expression.omit_parentheses().syntax() != expression.syntax().


[error] 173-173: Rust compile error E0599: no method named syntax found for SyntaxNode<JsLanguage> at .syntax() on argument.

🪛 GitHub Actions: Lint rule docs

[error] 162-162: Rust compile error E0107: expression.syntax().parent::<AnyJsCallArgument>() supplies 1 generic argument, but method parent takes 0 generic arguments.


[error] 165-165: Rust compile error E0599: no method named as_any_js_expression found for SyntaxNode<JsLanguage>.


[error] 168-168: Rust compile error E0282: type annotations needed at argument_expression.omit_parentheses().syntax() != expression.syntax() (cannot infer type).


[error] 172-173: Rust compile error E0599: no method named syntax found for SyntaxNode<JsLanguage> at .syntax() call in let Some(call_expression) = argument .syntax() ...

🪛 GitHub Actions: Pull request Node.js

[error] 162-162: Rust compiler error E0107: method takes 0 generic arguments but 1 was supplied. expression.syntax().parent::<AnyJsCallArgument>() should be called without generics; parent() is pub fn parent(&self) -> Option<Self>.


[error] 165-165: Rust compiler error E0599: no method named as_any_js_expression found for SyntaxNode<JsLanguage> in the current scope at .as_any_js_expression().


[error] 168-168: Rust compiler error E0282: type annotations needed. Cannot infer type for argument_expression usage at if argument_expression.omit_parentheses().syntax() != expression.syntax() { ... }.


[error] 172-173: Rust compiler error E0599: no method named syntax found for struct SyntaxNode<JsLanguage> at .syntax() on call_expression.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_js_analyze/src/lint/correctness/use_hook_at_top_level.rs` around
lines 162 - 177, The current code misuses SyntaxNode::parent generics and
AnyJsCallArgument APIs: replace the generic parent::<AnyJsCallArgument>() call
with a plain parent() followed by casting the returned SyntaxNode to
AnyJsCallArgument (e.g., via AnyJsCallArgument::cast or pattern match) to get an
argument value, and instead of calling as_any_js_expression() (which doesn't
exist on AnyJsCallArgument) extract the inner expression by pattern-matching the
AnyJsCallArgument variants or using the provided accessor (e.g., match on
AnyJsCallArgument::Expr or call a method like AnyJsCallArgument::expression())
so you obtain argument_expression whose omit_parentheses().syntax() can be
compared to expression.syntax(); keep the existing ancestor search using
JsCallExpression::cast unchanged.

Comment on lines +179 to +188
let Ok(callee) = call_expression.callee() else {
return false;
};

is_react_call_api(
&callee.omit_parentheses(),
model,
ReactLibrary::React,
"forwardRef",
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Type annotation needed for callee.

Once the earlier errors are fixed, the compiler should be able to infer the type here. However, if it still struggles, you can add an explicit type annotation.

🔧 Optional explicit annotation
-            let Ok(callee) = call_expression.callee() else {
+            let Ok(callee): Result<AnyJsExpression, _> = call_expression.callee() else {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let Ok(callee) = call_expression.callee() else {
return false;
};
is_react_call_api(
&callee.omit_parentheses(),
model,
ReactLibrary::React,
"forwardRef",
)
let Ok(callee): Result<AnyJsExpression, _> = call_expression.callee() else {
return false;
};
is_react_call_api(
&callee.omit_parentheses(),
model,
ReactLibrary::React,
"forwardRef",
)
🧰 Tools
🪛 GitHub Actions: autofix.ci

[error] 179-179: Rust compile error E0282: type annotations needed. Cannot infer type at let Ok(callee) = call_expression.callee() else { ... }.


[error] 184-184: Rust compile error E0282: type annotations needed. Cannot infer type at &callee.omit_parentheses(),.

🪛 GitHub Actions: Lint rule docs

[error] 179-179: Rust compile error E0282: type annotations needed at let Ok(callee) = call_expression.callee() else { ... } (cannot infer type).


[error] 184-184: Rust compile error E0282: type annotations needed at &callee.omit_parentheses() (cannot infer type).

🪛 GitHub Actions: Pull request Node.js

[error] 179-179: Rust compiler error E0282: type annotations needed. Cannot infer type for let Ok(callee) = call_expression.callee() else { ... }.


[error] 184-184: Rust compiler error E0282: type annotations needed. Cannot infer type at &callee.omit_parentheses().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_js_analyze/src/lint/correctness/use_hook_at_top_level.rs` around
lines 179 - 188, The compiler can't infer callee's concrete type here; add an
explicit type annotation on the let binding so the returned value from
call_expression.callee() has a concrete type before calling
callee.omit_parentheses() and is_react_call_api. Change the binding to the form
`let Ok(callee): Result<TheConcreteCalleeType, _> = call_expression.callee()
else { return false; };` (or `let Ok(callee): TheConcreteCalleeType = ...` if
the function returns the type directly), using the actual return type of
call_expression.callee() as defined in its signature, then keep the subsequent
call to callee.omit_parentheses() and is_react_call_api unchanged.

@Conaclos Conaclos added the M-Likely Agent This was likely an automated PR without a human in the loop label Mar 29, 2026
@ematipico ematipico closed this Mar 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Linter Area: linter L-JavaScript Language: JavaScript and super languages M-Likely Agent This was likely an automated PR without a human in the loop

Projects

None yet

Development

Successfully merging this pull request may close these issues.

💅 useHookAtTopLevel false positive when used with forwardRef

3 participants