Add detection of dynamic hooks

ghstack-source-id: 3acfa4fde5dfd65353fa407378057ca937ee3599
Pull Request resolved: https://github.com/facebook/react-forget/pull/2901
This commit is contained in:
Joe Savona
2024-04-24 15:15:34 -07:00
parent 89f2f7fcef
commit 2d569a3353
18 changed files with 164 additions and 26 deletions
@@ -122,8 +122,7 @@ export function validateHooksUsage(fn: HIRFunction): void {
place.loc,
new CompilerErrorDetail({
description: null,
reason:
"Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)",
reason,
loc: place.loc,
severity: ErrorSeverity.InvalidReact,
suggestions: null,
@@ -140,7 +139,24 @@ export function validateHooksUsage(fn: HIRFunction): void {
new CompilerErrorDetail({
description: null,
reason:
"Hooks may not be referenced as normal values, they must be called. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)",
"Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values",
loc: place.loc,
severity: ErrorSeverity.InvalidReact,
suggestions: null,
})
);
}
}
function recordDynamicHookUsageError(place: Place): void {
const previousError =
typeof place.loc !== "symbol" ? errorsByPlace.get(place.loc) : undefined;
if (previousError === undefined) {
recordError(
place.loc,
new CompilerErrorDetail({
description: null,
reason:
"Hooks must be the same function on every render, but this value may change over time to a different function. See https://react.dev/reference/rules/react-calls-components-and-hooks#dont-dynamically-use-hooks",
loc: place.loc,
severity: ErrorSeverity.InvalidReact,
suggestions: null,
@@ -224,7 +240,10 @@ export function validateHooksUsage(fn: HIRFunction): void {
case "StoreLocal":
case "StoreContext": {
visitPlace(instr.value.value);
const kind = getKindForPlace(instr.value.value);
const kind = joinKinds(
getKindForPlace(instr.value.value),
getKindForPlace(instr.value.lvalue.place)
);
setKind(instr.value.lvalue.place, kind);
setKind(instr.lvalue, kind);
break;
@@ -298,10 +317,11 @@ export function validateHooksUsage(fn: HIRFunction): void {
calleeKind === Kind.KnownHook || calleeKind === Kind.PotentialHook;
if (isHookCallee && !unconditionalBlocks.has(block.id)) {
recordConditionalHookError(instr.value.callee);
} else if (calleeKind === Kind.PotentialHook) {
recordDynamicHookUsageError(instr.value.callee);
}
/**
* We intentionally skip the callee because known/potential hooks
* are always allowed to be called.
* We intentionally skip the callee because it's validated above
*/
for (const operand of eachInstructionOperand(instr)) {
if (operand === instr.value.callee) {
@@ -317,10 +337,11 @@ export function validateHooksUsage(fn: HIRFunction): void {
calleeKind === Kind.KnownHook || calleeKind === Kind.PotentialHook;
if (isHookCallee && !unconditionalBlocks.has(block.id)) {
recordConditionalHookError(instr.value.property);
} else if (calleeKind === Kind.PotentialHook) {
recordDynamicHookUsageError(instr.value.property);
}
/*
* We intentionally skip the callee because known/potential hooks
* are always allowed to be called as methods (`React.useState()`).
* We intentionally skip the property because it's validated above
*/
for (const operand of eachInstructionOperand(instr)) {
if (operand === instr.value.property) {
@@ -26,9 +26,9 @@ export const FIXTURE_ENTRYPOINT = {
5 |
6 | function Foo() {
> 7 | let bar = useFoo.useBar;
| ^^^^^^^^^^^^^ InvalidReact: Hooks may not be referenced as normal values, they must be called. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (7:7)
| ^^^^^^^^^^^^^ InvalidReact: Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values (7:7)
InvalidReact: Hooks may not be referenced as normal values, they must be called. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (8:8)
InvalidReact: Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values (8:8)
8 | return bar();
9 | }
10 |
@@ -16,7 +16,7 @@ function Component(props) {
```
1 | function Component(props) {
> 2 | const x = useState;
| ^^^^^^^^ InvalidReact: Hooks may not be referenced as normal values, they must be called. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (2:2)
| ^^^^^^^^ InvalidReact: Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values (2:2)
3 | const state = x(null);
4 | return state[0];
5 | }
@@ -14,7 +14,7 @@ function Component(props) {
```
1 | function Component(props) {
> 2 | return foo(useFoo);
| ^^^^^^ InvalidReact: Hooks may not be referenced as normal values, they must be called. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (2:2)
| ^^^^^^ InvalidReact: Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values (2:2)
3 | }
4 |
```
@@ -14,7 +14,7 @@ function Component(props) {
```
1 | function Component(props) {
> 2 | return <Child foo={useFoo} />;
| ^^^^^^ InvalidReact: Hooks may not be referenced as normal values, they must be called. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (2:2)
| ^^^^^^ InvalidReact: Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values (2:2)
3 | }
4 |
```
@@ -15,13 +15,13 @@ function Component(props) {
```
1 | function Component(props) {
> 2 | const x = props.cond ? useA : useB;
| ^^^^ InvalidReact: Hooks may not be referenced as normal values, they must be called. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (2:2)
| ^^^^ InvalidReact: Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values (2:2)
InvalidReact: Hooks may not be referenced as normal values, they must be called. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (2:2)
InvalidReact: Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values (2:2)
InvalidReact: Hooks may not be referenced as normal values, they must be called. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (2:2)
InvalidReact: Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values (2:2)
InvalidReact: Hooks may not be referenced as normal values, they must be called. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (3:3)
InvalidReact: Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values (3:3)
3 | return x();
4 | }
5 |
@@ -15,9 +15,9 @@ function Component() {
```
1 | function Component() {
> 2 | const x = Foo.useFoo;
| ^^^^^^^^^^ InvalidReact: Hooks may not be referenced as normal values, they must be called. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (2:2)
| ^^^^^^^^^^ InvalidReact: Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values (2:2)
InvalidReact: Hooks may not be referenced as normal values, they must be called. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (3:3)
InvalidReact: Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values (3:3)
3 | return x();
4 | }
5 |
@@ -21,11 +21,11 @@ function Component(props) {
1 | function Component(props) {
2 | // This is a violation of using a hook as a normal value rule:
> 3 | const getUser = props.cond ? useGetUser : emptyFunction;
| ^^^^^^^^^^ InvalidReact: Hooks may not be referenced as normal values, they must be called. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (3:3)
| ^^^^^^^^^^ InvalidReact: Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values (3:3)
InvalidReact: Hooks may not be referenced as normal values, they must be called. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (3:3)
InvalidReact: Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values (3:3)
InvalidReact: Hooks may not be referenced as normal values, they must be called. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (8:8)
InvalidReact: Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values (8:8)
4 |
5 | // Ideally we would report a "conditional hook call" error here.
6 | // It's an unconditional call, but the value may or may not be a hook.
@@ -0,0 +1,25 @@
## Input
```javascript
function Component() {
const someFunction = useContext(FooContext);
const useOhItsNamedLikeAHookNow = someFunction;
useOhItsNamedLikeAHookNow();
}
```
## Error
```
2 | const someFunction = useContext(FooContext);
3 | const useOhItsNamedLikeAHookNow = someFunction;
> 4 | useOhItsNamedLikeAHookNow();
| ^^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: Hooks must be the same function on every render, but this value may change over time to a different function. See https://react.dev/reference/rules/react-calls-components-and-hooks#dont-dynamically-use-hooks (4:4)
5 | }
6 |
```
@@ -0,0 +1,5 @@
function Component() {
const someFunction = useContext(FooContext);
const useOhItsNamedLikeAHookNow = someFunction;
useOhItsNamedLikeAHookNow();
}
@@ -15,7 +15,7 @@ function Component(props) {
```
1 | function Component(props) {
> 2 | const x = props.cond ? (useFoo ? 1 : 2) : 3;
| ^^^^^^ InvalidReact: Hooks may not be referenced as normal values, they must be called. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (2:2)
| ^^^^^^ InvalidReact: Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values (2:2)
3 | return x;
4 | }
5 |
@@ -0,0 +1,22 @@
## Input
```javascript
function Component({ useFoo }) {
useFoo();
}
```
## Error
```
1 | function Component({ useFoo }) {
> 2 | useFoo();
| ^^^^^^ InvalidReact: Hooks must be the same function on every render, but this value may change over time to a different function. See https://react.dev/reference/rules/react-calls-components-and-hooks#dont-dynamically-use-hooks (2:2)
3 | }
4 |
```
@@ -0,0 +1,3 @@
function Component({ useFoo }) {
useFoo();
}
@@ -0,0 +1,26 @@
## Input
```javascript
function useFoo({ data }) {
const useMedia = useVideoPlayer();
const foo = useMedia();
return foo;
}
```
## Error
```
1 | function useFoo({ data }) {
2 | const useMedia = useVideoPlayer();
> 3 | const foo = useMedia();
| ^^^^^^^^ InvalidReact: Hooks must be the same function on every render, but this value may change over time to a different function. See https://react.dev/reference/rules/react-calls-components-and-hooks#dont-dynamically-use-hooks (3:3)
4 | return foo;
5 | }
6 |
```
@@ -0,0 +1,5 @@
function useFoo({ data }) {
const useMedia = useVideoPlayer();
const foo = useMedia();
return foo;
}
@@ -0,0 +1,26 @@
## Input
```javascript
function useFoo({ data }) {
const player = useVideoPlayer();
const foo = player.useMedia();
return foo;
}
```
## Error
```
1 | function useFoo({ data }) {
2 | const player = useVideoPlayer();
> 3 | const foo = player.useMedia();
| ^^^^^^^^^^^^^^^ InvalidReact: Hooks must be the same function on every render, but this value may change over time to a different function. See https://react.dev/reference/rules/react-calls-components-and-hooks#dont-dynamically-use-hooks (3:3)
4 | return foo;
5 | }
6 |
```
@@ -0,0 +1,5 @@
function useFoo({ data }) {
const player = useVideoPlayer();
const foo = player.useMedia();
return foo;
}
@@ -17,11 +17,11 @@ function Component(props) {
1 | function Component(props) {
2 | let y;
> 3 | props.cond ? (y = useFoo) : null;
| ^^^^^^ InvalidReact: Hooks may not be referenced as normal values, they must be called. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (3:3)
| ^^^^^^ InvalidReact: Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values (3:3)
InvalidReact: Hooks may not be referenced as normal values, they must be called. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (3:3)
InvalidReact: Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values (3:3)
InvalidReact: Hooks may not be referenced as normal values, they must be called. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (4:4)
InvalidReact: Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values (4:4)
4 | return y();
5 | }
6 |