Skip to content

Commit 8f4ce72

Browse files
authored
[commit] Improve error for hoisting violations (#33514)
The previous error for hoisting violations pointed only to the variable declaration, but didn't show where the value was accessed before that declaration. We now track where each hoisted variable is first accessed and report two errors, one for the reference and one for the declaration. When we improve our diagnostic infra to support reporting errors at multiple locations we can merge these into a single conceptual error. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33514). * #33571 * #33558 * #33547 * #33543 * #33533 * #33532 * #33530 * #33526 * #33522 * #33518 * __->__ #33514 * #33573
1 parent 7ce2a63 commit 8f4ce72

File tree

3 files changed

+86
-42
lines changed

3 files changed

+86
-42
lines changed

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

Lines changed: 70 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
import {
3939
eachInstructionValueLValue,
4040
eachInstructionValueOperand,
41+
eachTerminalOperand,
4142
eachTerminalSuccessor,
4243
} from '../HIR/visitors';
4344
import {Ok, Result} from '../Utils/Result';
@@ -221,8 +222,19 @@ export function inferMutationAliasingEffects(
221222
return Ok(undefined);
222223
}
223224

224-
function findHoistedContextDeclarations(fn: HIRFunction): Set<DeclarationId> {
225-
const hoisted = new Set<DeclarationId>();
225+
function findHoistedContextDeclarations(
226+
fn: HIRFunction,
227+
): Map<DeclarationId, Place | null> {
228+
const hoisted = new Map<DeclarationId, Place | null>();
229+
function visit(place: Place): void {
230+
if (
231+
hoisted.has(place.identifier.declarationId) &&
232+
hoisted.get(place.identifier.declarationId) == null
233+
) {
234+
// If this is the first load of the value, store the location
235+
hoisted.set(place.identifier.declarationId, place);
236+
}
237+
}
226238
for (const block of fn.body.blocks.values()) {
227239
for (const instr of block.instructions) {
228240
if (instr.value.kind === 'DeclareContext') {
@@ -232,10 +244,17 @@ function findHoistedContextDeclarations(fn: HIRFunction): Set<DeclarationId> {
232244
kind == InstructionKind.HoistedFunction ||
233245
kind == InstructionKind.HoistedLet
234246
) {
235-
hoisted.add(instr.value.lvalue.place.identifier.declarationId);
247+
hoisted.set(instr.value.lvalue.place.identifier.declarationId, null);
248+
}
249+
} else {
250+
for (const operand of eachInstructionValueOperand(instr.value)) {
251+
visit(operand);
236252
}
237253
}
238254
}
255+
for (const operand of eachTerminalOperand(block.terminal)) {
256+
visit(operand);
257+
}
239258
}
240259
return hoisted;
241260
}
@@ -248,12 +267,12 @@ class Context {
248267
catchHandlers: Map<BlockId, Place> = new Map();
249268
isFuctionExpression: boolean;
250269
fn: HIRFunction;
251-
hoistedContextDeclarations: Set<DeclarationId>;
270+
hoistedContextDeclarations: Map<DeclarationId, Place | null>;
252271

253272
constructor(
254273
isFunctionExpression: boolean,
255274
fn: HIRFunction,
256-
hoistedContextDeclarations: Set<DeclarationId>,
275+
hoistedContextDeclarations: Map<DeclarationId, Place | null>,
257276
) {
258277
this.isFuctionExpression = isFunctionExpression;
259278
this.fn = fn;
@@ -901,48 +920,69 @@ function applyEffect(
901920
console.log(prettyFormat(state.debugAbstractValue(value)));
902921
}
903922

904-
let reason: string;
905-
let description: string | null = null;
906-
907923
if (
908924
mutationKind === 'mutate-frozen' &&
909925
context.hoistedContextDeclarations.has(
910926
effect.value.identifier.declarationId,
911927
)
912928
) {
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 (
929+
const description =
915930
effect.value.identifier.name !== null &&
916931
effect.value.identifier.name.kind === 'named'
917-
) {
918-
description = `Move the declaration of \`${effect.value.identifier.name.value}\` to before it is first referenced`;
932+
? `Variable \`${effect.value.identifier.name.value}\` is accessed before it is declared`
933+
: null;
934+
const hoistedAccess = context.hoistedContextDeclarations.get(
935+
effect.value.identifier.declarationId,
936+
);
937+
if (hoistedAccess != null && hoistedAccess.loc != effect.value.loc) {
938+
effects.push({
939+
kind: 'MutateFrozen',
940+
place: effect.value,
941+
error: {
942+
severity: ErrorSeverity.InvalidReact,
943+
reason: `This variable is accessed before it is declared, which may prevent it from updating as the assigned value changes over time`,
944+
description,
945+
loc: hoistedAccess.loc,
946+
suggestions: null,
947+
},
948+
});
919949
}
950+
951+
effects.push({
952+
kind: 'MutateFrozen',
953+
place: effect.value,
954+
error: {
955+
severity: ErrorSeverity.InvalidReact,
956+
reason: `This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time`,
957+
description,
958+
loc: effect.value.loc,
959+
suggestions: null,
960+
},
961+
});
920962
} else {
921-
reason = getWriteErrorReason({
963+
const reason = getWriteErrorReason({
922964
kind: value.kind,
923965
reason: value.reason,
924966
context: new Set(),
925967
});
926-
if (
968+
const description =
927969
effect.value.identifier.name !== null &&
928970
effect.value.identifier.name.kind === 'named'
929-
) {
930-
description = `Found mutation of \`${effect.value.identifier.name.value}\``;
931-
}
971+
? `Found mutation of \`${effect.value.identifier.name.value}\``
972+
: null;
973+
effects.push({
974+
kind:
975+
value.kind === ValueKind.Frozen ? 'MutateFrozen' : 'MutateGlobal',
976+
place: effect.value,
977+
error: {
978+
severity: ErrorSeverity.InvalidReact,
979+
reason,
980+
description,
981+
loc: effect.value.loc,
982+
suggestions: null,
983+
},
984+
});
932985
}
933-
934-
effects.push({
935-
kind:
936-
value.kind === ValueKind.Frozen ? 'MutateFrozen' : 'MutateGlobal',
937-
place: effect.value,
938-
error: {
939-
severity: ErrorSeverity.InvalidReact,
940-
reason,
941-
description,
942-
loc: effect.value.loc,
943-
suggestions: null,
944-
},
945-
});
946986
}
947987
break;
948988
}

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,15 @@ export const FIXTURE_ENTRYPOINT = {
3838
## Error
3939

4040
```
41-
19 | useEffect(() => setState(2), []);
41+
17 | * $2 = Function context=setState
42+
18 | */
43+
> 19 | useEffect(() => setState(2), []);
44+
| ^^^^^^^^ InvalidReact: This variable is accessed before it is declared, which may prevent it from updating as the assigned value changes over time. Variable `setState` is accessed before it is declared (19:19)
45+
46+
InvalidReact: This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time. Variable `setState` is accessed before it is declared (21:21)
4247
20 |
43-
> 21 | const [state, setState] = useState(0);
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)
48+
21 | const [state, setState] = useState(0);
4549
22 | return <Stringify state={state} />;
46-
23 | }
47-
24 |
4850
```
4951
5052

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-referencing-frozen-hoisted-storecontext-const.expect.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,15 @@ function Component({content, refetch}) {
3131
## Error
3232

3333
```
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 | }
34+
9 | // TDZ violation!
35+
10 | const onRefetch = useCallback(() => {
36+
> 11 | refetch(data);
37+
| ^^^^ InvalidReact: This variable is accessed before it is declared, which may prevent it from updating as the assigned value changes over time. Variable `data` is accessed before it is declared (11:11)
38+
39+
InvalidReact: This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time. Variable `data` is accessed before it is declared (19:19)
40+
12 | }, [refetch]);
41+
13 |
42+
14 | // The context variable gets frozen here since it's passed to a hook
4143
```
4244
4345

0 commit comments

Comments
 (0)