Skip to content

Commit b906112

Browse files
mdevilsdyc3ematipico
authored
fix(lint): useHookAtTopLevel should not report issues unrelated to the nested hook call (#7406)
Co-authored-by: dyc3 <[email protected]> Co-authored-by: ematipico <[email protected]>
1 parent 00e1a6b commit b906112

File tree

8 files changed

+109
-39
lines changed

8 files changed

+109
-39
lines changed

.changeset/poor-horses-smash.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Fixed an issue (#6393) where the [useHookAtTopLevel](https://biomejs.dev/linter/rules/use-hook-at-top-level/) rule reported excessive diagnostics for nested hook calls.
6+
7+
The rule now reports only the offending top-level call site, not sub-hooks of composite hooks.
8+
9+
```js
10+
// Before: reported twice (useFoo and useBar).
11+
function useFoo() { return useBar(); }
12+
function Component() {
13+
if (cond) useFoo();
14+
}
15+
// After: reported once at the call to useFoo().
16+
```

crates/biome_js_analyze/src/lint/correctness/use_hook_at_top_level.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,7 @@ impl Queryable for FunctionCall {
422422
#[derive(Debug)]
423423
pub struct CallPath {
424424
call: JsCallExpression,
425+
is_enclosed_in_component_or_hook: bool,
425426
path: Vec<TextRange>,
426427
}
427428

@@ -449,10 +450,16 @@ impl Rule for UseHookAtTopLevel {
449450
let root = CallPath {
450451
call: call.clone(),
451452
path: vec![],
453+
is_enclosed_in_component_or_hook: false,
452454
};
453455
let mut calls = vec![root];
454456

455-
while let Some(CallPath { call, mut path }) = calls.pop() {
457+
while let Some(CallPath {
458+
call,
459+
mut path,
460+
is_enclosed_in_component_or_hook,
461+
}) = calls.pop()
462+
{
456463
let range = call.syntax().text_range_with_trivia();
457464

458465
if path.contains(&range) {
@@ -485,17 +492,27 @@ impl Rule for UseHookAtTopLevel {
485492
});
486493
}
487494

495+
let enclosed = is_enclosed_in_component_or_hook
496+
|| enclosing_function.is_react_component_or_hook();
497+
488498
if let AnyJsFunctionOrMethod::AnyJsFunction(function) = enclosing_function
489499
&& let Some(calls_iter) = function.all_calls(model)
490500
{
491501
for call in calls_iter {
492502
calls.push(CallPath {
493503
call: call.tree(),
494504
path: path.clone(),
505+
is_enclosed_in_component_or_hook: enclosed,
495506
});
496507
}
497508
}
498509
} else {
510+
// Avoid duplicate diagnostics if this path already passed through
511+
// a component/hook. We still keep previously enqueued paths to
512+
// allow recursion detection elsewhere.
513+
if is_enclosed_in_component_or_hook {
514+
continue;
515+
}
499516
return Some(Suggestion {
500517
hook_name_range: get_hook_name_range()?,
501518
path,

crates/biome_js_analyze/tests/specs/correctness/useHookAtTopLevel/invalid.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,13 @@ export default function Component5() {
6868
}
6969
};
7070

71-
const Component6 = () => {
71+
const useHook6 = () => {
7272
useEffect();
7373
};
7474

7575
const Component7 = () => {
7676
if (a == 1) {
77-
Component6();
77+
useHook6();
7878
}
7979
};
8080

@@ -189,4 +189,4 @@ function useRecursiveHookA() {
189189

190190
function useRecursiveHookB() {
191191
useRecursiveHookA();
192-
}
192+
}

crates/biome_js_analyze/tests/specs/correctness/useHookAtTopLevel/invalid.js.snap

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
---
22
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
assertion_line: 152
34
expression: invalid.js
45
---
56
# Input
@@ -74,13 +75,13 @@ export default function Component5() {
7475
}
7576
};
7677
77-
const Component6 = () => {
78+
const useHook6 = () => {
7879
useEffect();
7980
};
8081
8182
const Component7 = () => {
8283
if (a == 1) {
83-
Component6();
84+
useHook6();
8485
}
8586
};
8687
@@ -196,6 +197,7 @@ function useRecursiveHookA() {
196197
function useRecursiveHookB() {
197198
useRecursiveHookA();
198199
}
200+
199201
```
200202
201203
# Diagnostics
@@ -463,23 +465,14 @@ invalid.js:67:9 lint/correctness/useHookAtTopLevel ━━━━━━━━━
463465
```
464466

465467
```
466-
invalid.js:72:5 lint/correctness/useHookAtTopLevel ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
468+
invalid.js:77:9 lint/correctness/useHookAtTopLevel ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
467469
468-
× This hook is being called indirectly and conditionally, but all hooks must be called in the exact same order in every component render.
469-
470-
71 │ const Component6 = () => {
471-
> 72useEffect();
472-
^^^^^^^^^
473-
73};
474-
74 │
475-
476-
i This is the call path until the hook.
470+
× This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
477471
478472
75 │ const Component7 = () => {
479-
> 76if (a == 1) {
480-
481-
> 77 │ Component6();
482-
│ ^^^^^^^^^^^^
473+
76if (a == 1) {
474+
> 77 │ useHook6();
475+
│ ^^^^^^^^
483476
78 │ }
484477
79};
485478
@@ -881,6 +874,7 @@ invalid.js:187:5 lint/correctness/useHookAtTopLevel ━━━━━━━━━
881874
> 191useRecursiveHookA();
882875
^^^^^^^^^^^^^^^^^^^
883876
192}
877+
193 │
884878
885879
i For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
886880
@@ -900,6 +894,7 @@ invalid.js:191:5 lint/correctness/useHookAtTopLevel ━━━━━━━━━
900894
> 191useRecursiveHookA();
901895
^^^^^^^^^^^^^^^^^
902896
192}
897+
193 │
903898
904899
i This is the call path until the hook.
905900
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
const Component1 = () => {
1+
const useHook = () => {
22
useEffect() as [];
33
};
44

55
const Component2 = () => {
66
if (a == 1) {
7-
Component1();
7+
useHook();
88
}
99
};

crates/biome_js_analyze/tests/specs/correctness/useHookAtTopLevel/invalid.ts.snap

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,32 @@
11
---
22
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
assertion_line: 152
34
expression: invalid.ts
4-
snapshot_kind: text
55
---
66
# Input
77
```ts
8-
const Component1 = () => {
8+
const useHook = () => {
99
useEffect() as [];
1010
};
1111
1212
const Component2 = () => {
1313
if (a == 1) {
14-
Component1();
14+
useHook();
1515
}
1616
};
1717
1818
```
1919

2020
# Diagnostics
2121
```
22-
invalid.ts:2:3 lint/correctness/useHookAtTopLevel ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
22+
invalid.ts:7:7 lint/correctness/useHookAtTopLevel ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2323
24-
× This hook is being called indirectly and conditionally, but all hooks must be called in the exact same order in every component render.
25-
26-
1 │ const Component1 = () => {
27-
> 2useEffect() as [];
28-
^^^^^^^^^
29-
3};
30-
4 │
31-
32-
i This is the call path until the hook.
24+
× This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
3325
3426
5 │ const Component2 = () => {
35-
> 6if (a == 1) {
36-
37-
> 7 │ Component1();
38-
│ ^^^^^^^^^^^^
27+
6if (a == 1) {
28+
> 7 │ useHook();
29+
│ ^^^^^^^
3930
8 │ }
4031
9};
4132
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
function useFoo() {
2+
return useBar();
3+
}
4+
5+
function Component() {
6+
if (condition) {
7+
// This call should be reported just once.
8+
// See https://github.com/biomejs/biome/issues/6393
9+
return useFoo();
10+
}
11+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
assertion_line: 152
4+
expression: invalidCompositeHook.js
5+
---
6+
# Input
7+
```js
8+
function useFoo() {
9+
return useBar();
10+
}
11+
12+
function Component() {
13+
if (condition) {
14+
// This call should be reported just once.
15+
// See https://github.com/biomejs/biome/issues/6393
16+
return useFoo();
17+
}
18+
}
19+
20+
```
21+
22+
# Diagnostics
23+
```
24+
invalidCompositeHook.js:9:16 lint/correctness/useHookAtTopLevel ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
25+
26+
× This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
27+
28+
7 │ // This call should be reported just once.
29+
8 │ // See https://github.com/biomejs/biome/issues/6393
30+
> 9 │ return useFoo();
31+
│ ^^^^^^
32+
10 │ }
33+
11 │ }
34+
35+
i For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
36+
37+
i See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
38+
39+
40+
```

0 commit comments

Comments
 (0)