Skip to content

Commit e3b5830

Browse files
committed
[commit] Better error message for invalid hoisting
We're already tracking which variables are hoisted context variables, so if we see a mutation of a frozen value we can emit a custom error message to help users identify the problem.
1 parent b40fd24 commit e3b5830

File tree

4 files changed

+97
-11
lines changed

4 files changed

+97
-11
lines changed

compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -901,23 +901,44 @@ function applyEffect(
901901
console.log(prettyFormat(state.debugAbstractValue(value)));
902902
}
903903

904-
const reason = getWriteErrorReason({
905-
kind: value.kind,
906-
reason: value.reason,
907-
context: new Set(),
908-
});
904+
let reason: string;
905+
let description: string | null = null;
906+
907+
if (
908+
mutationKind === 'mutate-frozen' &&
909+
context.hoistedContextDeclarations.has(
910+
effect.value.identifier.declarationId,
911+
)
912+
) {
913+
reason = `This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time`;
914+
if (
915+
effect.value.identifier.name !== null &&
916+
effect.value.identifier.name.kind === 'named'
917+
) {
918+
description = `Move the declaration of \`${effect.value.identifier.name.value}\` to before it is first referenced`;
919+
}
920+
} else {
921+
reason = getWriteErrorReason({
922+
kind: value.kind,
923+
reason: value.reason,
924+
context: new Set(),
925+
});
926+
if (
927+
effect.value.identifier.name !== null &&
928+
effect.value.identifier.name.kind === 'named'
929+
) {
930+
description = `Found mutation of \`${effect.value.identifier.name.value}\``;
931+
}
932+
}
933+
909934
effects.push({
910935
kind:
911936
value.kind === ValueKind.Frozen ? 'MutateFrozen' : 'MutateGlobal',
912937
place: effect.value,
913938
error: {
914939
severity: ErrorSeverity.InvalidReact,
915940
reason,
916-
description:
917-
effect.value.identifier.name !== null &&
918-
effect.value.identifier.name.kind === 'named'
919-
? `Found mutation of \`${effect.value.identifier.name.value}\``
920-
: null,
941+
description,
921942
loc: effect.value.loc,
922943
suggestions: null,
923944
},

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-hoisting-setstate.expect.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export const FIXTURE_ENTRYPOINT = {
4141
19 | useEffect(() => setState(2), []);
4242
20 |
4343
> 21 | const [state, setState] = useState(0);
44-
| ^^^^^^^^ InvalidReact: Updating a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the mutation before calling useEffect(). Found mutation of `setState` (21:21)
44+
| ^^^^^^^^ InvalidReact: This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time. Move the declaration of `setState` to before it is first referenced (21:21)
4545
22 | return <Stringify state={state} />;
4646
23 | }
4747
24 |
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
2+
## Input
3+
4+
```javascript
5+
//@flow @validatePreserveExistingMemoizationGuarantees @enableNewMutationAliasingModel
6+
7+
import {useCallback} from 'react';
8+
import {useIdentity} from 'shared-runtime';
9+
10+
function Component({content, refetch}) {
11+
// This callback function accesses a hoisted const as a dependency,
12+
// but it cannot reference it as a dependency since that would be a
13+
// TDZ violation!
14+
const onRefetch = useCallback(() => {
15+
refetch(data);
16+
}, [refetch]);
17+
18+
// The context variable gets frozen here since it's passed to a hook
19+
const onSubmit = useIdentity(onRefetch);
20+
21+
// This has to error: onRefetch needs to memoize with `content` as a
22+
// dependency, but the dependency comes later
23+
const {data = null} = content;
24+
25+
return <Foo data={data} onSubmit={onSubmit} />;
26+
}
27+
28+
```
29+
30+
31+
## Error
32+
33+
```
34+
17 | // This has to error: onRefetch needs to memoize with `content` as a
35+
18 | // dependency, but the dependency comes later
36+
> 19 | const {data = null} = content;
37+
| ^^^^^^^^^^^ InvalidReact: This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time. Move the declaration of `data` to before it is first referenced (19:19)
38+
20 |
39+
21 | return <Foo data={data} onSubmit={onSubmit} />;
40+
22 | }
41+
```
42+
43+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//@flow @validatePreserveExistingMemoizationGuarantees @enableNewMutationAliasingModel
2+
3+
import {useCallback} from 'react';
4+
import {useIdentity} from 'shared-runtime';
5+
6+
function Component({content, refetch}) {
7+
// This callback function accesses a hoisted const as a dependency,
8+
// but it cannot reference it as a dependency since that would be a
9+
// TDZ violation!
10+
const onRefetch = useCallback(() => {
11+
refetch(data);
12+
}, [refetch]);
13+
14+
// The context variable gets frozen here since it's passed to a hook
15+
const onSubmit = useIdentity(onRefetch);
16+
17+
// This has to error: onRefetch needs to memoize with `content` as a
18+
// dependency, but the dependency comes later
19+
const {data = null} = content;
20+
21+
return <Foo data={data} onSubmit={onSubmit} />;
22+
}

0 commit comments

Comments
 (0)