Skip to content

Comments

fix(linter): react rules of hooks false positive#9807

Closed
hulin32 wants to merge 4 commits intooxc-project:mainfrom
hulin32:fix/9795-react-rules-of-hooks-false-positive
Closed

fix(linter): react rules of hooks false positive#9807
hulin32 wants to merge 4 commits intooxc-project:mainfrom
hulin32:fix/9795-react-rules-of-hooks-false-positive

Conversation

@hulin32
Copy link

@hulin32 hulin32 commented Mar 16, 2025

Try to fix #9795 react-hooks/rules-of-hooks false positive

@graphite-app
Copy link
Contributor

graphite-app bot commented Mar 16, 2025

How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:

  • 0-merge - adds this PR to the back of the merge queue
  • hotfix - for urgent hot fixes, skip the queue and merge this PR next

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

@github-actions github-actions bot added the A-linter Area - Linter label Mar 16, 2025
@hulin32 hulin32 changed the title Fix/9795 react rules of hooks false positive fix(linter): react rules of hooks false positive Mar 16, 2025
@github-actions github-actions bot added the C-bug Category - Bug label Mar 16, 2025
@camc314 camc314 self-assigned this Mar 16, 2025
@codspeed-hq
Copy link

codspeed-hq bot commented Mar 16, 2025

CodSpeed Performance Report

Merging #9807 will not alter performance

Comparing hulin32:fix/9795-react-rules-of-hooks-false-positive (93213fd) with main (fef680a)

Summary

✅ 33 untouched benchmarks

@Boshen Boshen requested a review from camc314 March 17, 2025 04:11
Comment on lines +268 to +274

// Check if the hook call is part of a condition expression
// If the hook is used in a condition (like `if (useHook())`) rather than being conditionally called,
// then it's valid and we should return false
if is_hook_in_condition_expression(nodes, to) {
return false;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

hmm while this solution works, i'm not convinced it's the ideal solution?

Ideally, i think we would change the cfg to represent that the test of an if statement is always represented as visited.

We would look to change the code inside visit_if_statement to something like:

iff --git a/crates/oxc_semantic/src/builder.rs b/crates/oxc_semantic/src/builder.rs
index 394307c1c..8c99f9391 100644
--- a/crates/oxc_semantic/src/builder.rs
+++ b/crates/oxc_semantic/src/builder.rs
@@ -1183,21 +1183,17 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> {
 
             cfg.add_edge(before_if_stmt_graph_ix, start_of_condition_graph_ix, EdgeType::Normal);
 
-            cfg.add_edge(after_consequent_stmt_graph_ix, after_if_graph_ix, EdgeType::Normal);
-
             cfg.add_edge(after_test_graph_ix, before_consequent_stmt_graph_ix, EdgeType::Jump);
 
+            cfg.add_edge(after_consequent_stmt_graph_ix, after_if_graph_ix, EdgeType::Normal);
+
             if let Some((start_of_alternate_stmt_graph_ix, after_alternate_stmt_graph_ix)) =
                 else_graph_ix
             {
-                cfg.add_edge(
-                    before_if_stmt_graph_ix,
-                    start_of_alternate_stmt_graph_ix,
-                    EdgeType::Normal,
-                );
+                cfg.add_edge(after_test_graph_ix, start_of_alternate_stmt_graph_ix, EdgeType::Jump);
                 cfg.add_edge(after_alternate_stmt_graph_ix, after_if_graph_ix, EdgeType::Normal);
             } else {
-                cfg.add_edge(before_if_stmt_graph_ix, after_if_graph_ix, EdgeType::Normal);
+                cfg.add_edge(after_test_graph_ix, after_if_graph_ix, EdgeType::Jump);
             }
         });
         /* cfg */

Copy link
Author

Choose a reason for hiding this comment

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

This is much much more elegant 👍, I will close my MR, thx for sharing

Copy link
Author

Choose a reason for hiding this comment

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

Any possible to give a bit more context why this can fix the issue?
I will look into the codes, still trying to understand the whole process in OXC.
Thanks for sharing again

Copy link
Contributor

@camc314 camc314 Mar 17, 2025

Choose a reason for hiding this comment

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

yeah sure. firstly thanks for trying to fix it, we appreciate the contributions!

so the control flow graph logic of this codebase is (imo, i'm sure others may disagree 🙂) some of the most complex logical code.

From my understanding, the problem was that the "test" of the IfStatement, was being marked as conditionally visited (which is incorrect as the test is always evaluated).

by changing that logic in oxc_semantic, we changed the CFG, to represent that the test expression is always visited.

i'll try and put up a PR tomorrow, with a more detailed explanation on why those exact changes fix this issue

(on mobile atm)

thanks!

@hulin32 hulin32 closed this Mar 17, 2025
graphite-app bot pushed a commit that referenced this pull request Mar 19, 2025
This PR fixes the construction of the cfg within `oxc_semantic`, by marking the `test` of `IfStatement` as unconditionally visited.

Given the following:
```ts
if (foo) {
  bar();
} else {
  baz();
}
```

Would produce the following graph:

```mermaid
graph TD
    0["bb0"]
    1["bb1 IfStatement"]
    2["bb2 Condition(IdentifierReference(foo))"]
    3["bb3 BlockStatement ExpressionStatement"]
    4["bb4 BlockStatement ExpressionStatement"]
    5["bb5"]

    1 -- "Error(Implicit)" --> 0
    2 -- "Error(Implicit)" --> 0
    3 -- "Error(Implicit)" --> 0
    4 -- "Error(Implicit)" --> 0
    5 -- "Error(Implicit)" --> 0
    1 -- "Normal" --> 2
    1 -- "Normal" --> 4
    3 -- "Normal" --> 5
    2 -- "Jump" --> 3
    4 -- "Normal" --> 5
```

After this change, it produces the following graph:

```mermaid
graph TD
    0["bb0"]
    1["bb1 IfStatement"]
    2["bb2 Condition(IdentifierReference(foo))"]
    3["bb3 BlockStatement ExpressionStatement"]
    4["bb4 BlockStatement ExpressionStatement"]
    5["bb5"]

    1 -- "Error(Implicit)" --> 0
    2 -- "Error(Implicit)" --> 0
    3 -- "Error(Implicit)" --> 0
    4 -- "Error(Implicit)" --> 0
    5 -- "Error(Implicit)" --> 0
    1 -- "Normal" --> 2
    3 -- "Normal" --> 5
    2 -- "Jump" --> 3
    2 -- "Jump" --> 4
    4 -- "Normal" --> 5
```

Rather than jumping from the if statment (`bb1\nIfStatement`) directly to the consequent (`bb4 BlockStatement ExpressionStatement`), it now unconditionally visits the `test` of the if statement. This can be seen in the after graph as the jump to `bb4 BlockStatement ExpressionStatement"` comes **from** `bb2 Condition(IdentifierReference(foo))` rather than from `bb1 IfStatement`.

As a result of this change, `rules_of_hooks`, does not have false positives when a hook is in a position such as:
```ts
if (useMe) {
```
As the cfg would previously think that `useMe` was called conditionally when in fact it was not

fixes #9795
related #9807
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-linter Area - Linter C-bug Category - Bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

linter: react-hooks/rules-of-hooks false positive

2 participants