diff --git a/crates/oxc_linter/src/rules/react/rules_of_hooks.rs b/crates/oxc_linter/src/rules/react/rules_of_hooks.rs index 1f74ff95f0633..8041150d8cb28 100644 --- a/crates/oxc_linter/src/rules/react/rules_of_hooks.rs +++ b/crates/oxc_linter/src/rules/react/rules_of_hooks.rs @@ -444,6 +444,62 @@ fn test() { useHook(); } ", + + + // Valid because hooks can be used in condition expressions beginning with a ternary condition + " + function ComponentWithConditionalHook() { + if (useHook() ? good() : bad()) { + check(); + } + } + ", + + // Valid because hooks can be used in condition expressions + " + function Component() { + if (!useHasPermission()) { + return null; + } + return ; + } + ", + // Valid because hooks can be used in ternary condition expressions + " + function Component() { + return useHasPermission() ? : null; + } + ", + // Valid because hooks can be used in logical expressions (left side) + " + function Component() { + return useHasPermission() && ; + } + ", + // Valid because hooks can be used with negation in condition expressions + " + function Component() { + if (!useHasPermission()) { + return null; + } + return ; + } + ", + // Valid because hooks can be used in complex condition expressions + " + function Component() { + if (useHasPermission() && isAdmin()) { + return ; + } + return ; + } + ", + // Valid because hooks can be used in nested condition expressions + " + function Component() { + return (useHasPermission() && isAdmin()) ? : ; + } + ", // Valid because components can use hooks. " function createComponentWithHook() { @@ -968,6 +1024,38 @@ fn test() { } } ", + // Invalid because hooks are used in the right side of logical expressions + " + function useHook() { + a && useHook1(); + b && useHook2(); + } + ", + // Invalid because hooks are used conditionally after a condition + " + function Component() { + if (condition) { + // This is invalid because the hook is called conditionally + useHook(); + } + } + ", + // Invalid because hooks are used in the right side of ternary expressions + " + function Component() { + condition ? useHook() : null; + } + ", + " + function Component() { + if (Math.random()) { + return null; + } else if (!useHasPermission()) { + return + } + return ; + } + ", // Invalid because hooks can only be called inside of a component. // errors: [ // topLevelError('Hook.useState'), diff --git a/crates/oxc_linter/src/snapshots/react_rules_of_hooks.snap b/crates/oxc_linter/src/snapshots/react_rules_of_hooks.snap index de971c9841845..015062a5f6117 100644 --- a/crates/oxc_linter/src/snapshots/react_rules_of_hooks.snap +++ b/crates/oxc_linter/src/snapshots/react_rules_of_hooks.snap @@ -9,6 +9,46 @@ source: crates/oxc_linter/src/tester.rs 5 │ } ╰──── + ⚠ eslint-plugin-react-hooks(rules-of-hooks): React Hook "useHook1" is called conditionally. React Hooks must be called in the exact same order in every component render. + ╭─[rules_of_hooks.tsx:3:22] + 2 │ function useHook() { + 3 │ a && useHook1(); + · ────────── + 4 │ b && useHook2(); + ╰──── + + ⚠ eslint-plugin-react-hooks(rules-of-hooks): React Hook "useHook2" is called conditionally. React Hooks must be called in the exact same order in every component render. + ╭─[rules_of_hooks.tsx:4:22] + 3 │ a && useHook1(); + 4 │ b && useHook2(); + · ────────── + 5 │ } + ╰──── + + ⚠ eslint-plugin-react-hooks(rules-of-hooks): React Hook "useHook" is called conditionally. React Hooks must be called in the exact same order in every component render. + ╭─[rules_of_hooks.tsx:5:21] + 4 │ // This is invalid because the hook is called conditionally + 5 │ useHook(); + · ───────── + 6 │ } + ╰──── + + ⚠ eslint-plugin-react-hooks(rules-of-hooks): React Hook "useHook" is called conditionally. React Hooks must be called in the exact same order in every component render. + ╭─[rules_of_hooks.tsx:3:29] + 2 │ function Component() { + 3 │ condition ? useHook() : null; + · ───────── + 4 │ } + ╰──── + + ⚠ eslint-plugin-react-hooks(rules-of-hooks): React Hook "useHasPermission" is called conditionally. React Hooks must be called in the exact same order in every component render. + ╭─[rules_of_hooks.tsx:5:23] + 4 │ return null; + 5 │ } else if (!useHasPermission()) { + · ────────────────── + 6 │ return + ╰──── + ⚠ eslint-plugin-react-hooks(rules-of-hooks): React Hook "useState" cannot be called at the top level. React Hooks must be called in a React function component or a custom React Hook function. ╭─[rules_of_hooks.tsx:2:13] 1 │ diff --git a/crates/oxc_semantic/src/builder.rs b/crates/oxc_semantic/src/builder.rs index bb9ec3db8fc14..8e8f60cd59168 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 */ diff --git a/crates/oxc_semantic/tests/integration/snapshots/if_else.snap b/crates/oxc_semantic/tests/integration/snapshots/if_else.snap index e2a961ede9ce5..6d2504ae6fc4e 100644 --- a/crates/oxc_semantic/tests/integration/snapshots/if_else.snap +++ b/crates/oxc_semantic/tests/integration/snapshots/if_else.snap @@ -103,9 +103,9 @@ unreachable" shape = box] 9 -> 10 [ label="Unreachable", style="dotted"] 11 -> 2 [ label="Error(Implicit)", style=dashed, color=red] 3 -> 4 [ label="Normal"] - 8 -> 11 [ label="Normal", style="dotted"] 6 -> 7 [ label="Jump", color=green] - 3 -> 9 [ label="Normal"] + 8 -> 11 [ label="Normal", style="dotted"] + 6 -> 9 [ label="Jump", color=green] 10 -> 11 [ label="Normal", style="dotted"] 12 -> 2 [ label="Error(Implicit)", style=dashed, color=red] 11 -> 12 [ label="Unreachable", style="dotted"] diff --git a/crates/oxc_semantic/tests/integration/snapshots/if_stmt_in_for_in.snap b/crates/oxc_semantic/tests/integration/snapshots/if_stmt_in_for_in.snap index 08c6daa1cd22e..50e9298673fba 100644 --- a/crates/oxc_semantic/tests/integration/snapshots/if_stmt_in_for_in.snap +++ b/crates/oxc_semantic/tests/integration/snapshots/if_stmt_in_for_in.snap @@ -139,14 +139,14 @@ return" shape = box] 12 -> 13 [ label="Unreachable", style="dotted"] 14 -> 2 [ label="Error(Implicit)", color=red, style=dashed] 10 -> 11 [ label="Normal"] - 13 -> 14 [ label="Normal", style="dotted"] 11 -> 12 [ label="Jump", color=green] - 10 -> 14 [ label="Normal"] + 13 -> 14 [ label="Normal", style="dotted"] + 11 -> 14 [ label="Jump", color=green] 15 -> 2 [ label="Error(Implicit)", color=red, style=dashed] 6 -> 7 [ label="Normal"] - 9 -> 15 [ label="Normal", style="dotted"] 7 -> 8 [ label="Jump", color=green] - 6 -> 10 [ label="Normal"] + 9 -> 15 [ label="Normal", style="dotted"] + 7 -> 10 [ label="Jump", color=green] 14 -> 15 [ label="Normal"] 16 -> 2 [ label="Error(Implicit)", color=red, style=dashed] 3 -> 4 [ label="Normal"] diff --git a/crates/oxc_semantic/tests/integration/snapshots/labeled_block_break.snap b/crates/oxc_semantic/tests/integration/snapshots/labeled_block_break.snap index a2232bd5551de..1ec5323500ad7 100644 --- a/crates/oxc_semantic/tests/integration/snapshots/labeled_block_break.snap +++ b/crates/oxc_semantic/tests/integration/snapshots/labeled_block_break.snap @@ -83,9 +83,9 @@ unreachable" shape = box] 6 -> 7 [ label="Unreachable", style="dotted"] 8 -> 0 [ label="Error(Implicit)", color=red, style=dashed] 4 -> 5 [ label="Normal"] - 7 -> 8 [ label="Normal", style="dotted"] 5 -> 6 [ label="Jump", color=green] - 4 -> 8 [ label="Normal"] + 7 -> 8 [ label="Normal", style="dotted"] + 5 -> 8 [ label="Jump", color=green] 9 -> 0 [ label="Error(Implicit)", color=red, style=dashed] 8 -> 9 [ label="Normal"] 6 -> 9 [ label="Jump", color=green] diff --git a/crates/oxc_semantic/tests/integration/snapshots/logical_expressions_short_circuit.snap b/crates/oxc_semantic/tests/integration/snapshots/logical_expressions_short_circuit.snap index 0995d38c64adc..76ac964c991d0 100644 --- a/crates/oxc_semantic/tests/integration/snapshots/logical_expressions_short_circuit.snap +++ b/crates/oxc_semantic/tests/integration/snapshots/logical_expressions_short_circuit.snap @@ -110,9 +110,9 @@ ExpressionStatement" shape = box] 5 -> 0 [ label="Error(Implicit)", color=red, style=dashed] 6 -> 0 [ label="Error(Implicit)", color=red, style=dashed] 1 -> 2 [ label="Normal"] - 5 -> 6 [ label="Normal"] 4 -> 5 [ label="Jump", color=green] - 1 -> 6 [ label="Normal"] + 5 -> 6 [ label="Normal"] + 4 -> 6 [ label="Jump", color=green] 7 -> 0 [ label="Error(Implicit)", color=red, style=dashed] 8 -> 0 [ label="Error(Implicit)", color=red, style=dashed] 9 -> 0 [ label="Error(Implicit)", color=red, style=dashed] @@ -122,9 +122,9 @@ ExpressionStatement" shape = box] 10 -> 0 [ label="Error(Implicit)", color=red, style=dashed] 11 -> 0 [ label="Error(Implicit)", color=red, style=dashed] 6 -> 7 [ label="Normal"] - 10 -> 11 [ label="Normal"] 9 -> 10 [ label="Jump", color=green] - 6 -> 11 [ label="Normal"] + 10 -> 11 [ label="Normal"] + 9 -> 11 [ label="Jump", color=green] 12 -> 0 [ label="Error(Implicit)", color=red, style=dashed] 13 -> 0 [ label="Error(Implicit)", color=red, style=dashed] 14 -> 0 [ label="Error(Implicit)", color=red, style=dashed] @@ -134,7 +134,7 @@ ExpressionStatement" shape = box] 15 -> 0 [ label="Error(Implicit)", color=red, style=dashed] 16 -> 0 [ label="Error(Implicit)", color=red, style=dashed] 11 -> 12 [ label="Normal"] - 15 -> 16 [ label="Normal"] 14 -> 15 [ label="Jump", color=green] - 11 -> 16 [ label="Normal"] + 15 -> 16 [ label="Normal"] + 14 -> 16 [ label="Jump", color=green] }