Skip to content

Commit b067c6f

Browse files
authored
[compiler] Improve error message for mutating hook args/return (#33513)
The previous error message was generic, because the old style function signature didn't support a way to specify a reason alongside a freeze effect. This meant we could only say why a value was frozen for instructions, but not hooks which use function signatures. By defining a new aliasing signature for custom hooks we can specify a reason and provide a better error message. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33513). * #33571 * #33558 * #33547 * #33543 * #33533 * #33532 * #33530 * #33526 * #33522 * #33518 * #33514 * __->__ #33513
1 parent e081cb3 commit b067c6f

File tree

10 files changed

+71
-26
lines changed

10 files changed

+71
-26
lines changed

compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1388,6 +1388,16 @@ export enum ValueReason {
13881388
*/
13891389
JsxCaptured = 'jsx-captured',
13901390

1391+
/**
1392+
* Argument to a hook
1393+
*/
1394+
HookCaptured = 'hook-captured',
1395+
1396+
/**
1397+
* Return value of a hook
1398+
*/
1399+
HookReturn = 'hook-return',
1400+
13911401
/**
13921402
* Passed to an effect
13931403
*/

compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1302,6 +1302,34 @@ export const DefaultNonmutatingHook = addHook(
13021302
calleeEffect: Effect.Read,
13031303
hookKind: 'Custom',
13041304
returnValueKind: ValueKind.Frozen,
1305+
aliasing: {
1306+
receiver: makeIdentifierId(0),
1307+
params: [],
1308+
rest: makeIdentifierId(1),
1309+
returns: makeIdentifierId(2),
1310+
temporaries: [],
1311+
effects: [
1312+
// Freeze the arguments
1313+
{
1314+
kind: 'Freeze',
1315+
value: signatureArgument(1),
1316+
reason: ValueReason.HookCaptured,
1317+
},
1318+
// Returns a frozen value
1319+
{
1320+
kind: 'Create',
1321+
into: signatureArgument(2),
1322+
value: ValueKind.Frozen,
1323+
reason: ValueReason.HookReturn,
1324+
},
1325+
// May alias any arguments into the return
1326+
{
1327+
kind: 'Alias',
1328+
from: signatureArgument(1),
1329+
into: signatureArgument(2),
1330+
},
1331+
],
1332+
},
13051333
},
13061334
'DefaultNonmutatingHook',
13071335
);

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,10 @@ export function getWriteErrorReason(abstractValue: AbstractValue): string {
341341
return "Mutating a value returned from 'useReducer()', which should not be mutated. Use the dispatch function to update instead";
342342
} else if (abstractValue.reason.has(ValueReason.Effect)) {
343343
return 'Updating a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the mutation before calling useEffect()';
344+
} else if (abstractValue.reason.has(ValueReason.HookCaptured)) {
345+
return 'Updating a value previously passed as an argument to a hook is not allowed. Consider moving the mutation before calling the hook';
346+
} else if (abstractValue.reason.has(ValueReason.HookReturn)) {
347+
return 'Updating a value returned from a hook is not allowed. Consider moving the mutation into the hook where the value is constructed';
344348
} else {
345349
return 'This mutates a variable that React considers immutable';
346350
}

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

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1980,28 +1980,17 @@ function computeEffectsForLegacySignature(
19801980
break;
19811981
}
19821982
case Effect.ConditionallyMutateIterator: {
1983-
if (
1984-
isArrayType(place.identifier) ||
1985-
isSetType(place.identifier) ||
1986-
isMapType(place.identifier)
1987-
) {
1988-
effects.push({
1989-
kind: 'Capture',
1990-
from: place,
1991-
into: lvalue,
1992-
});
1993-
} else {
1994-
effects.push({
1995-
kind: 'Capture',
1996-
from: place,
1997-
into: lvalue,
1998-
});
1983+
const mutateIterator = conditionallyMutateIterator(place);
1984+
if (mutateIterator != null) {
1985+
effects.push(mutateIterator);
1986+
// TODO: should we always push to captures?
19991987
captures.push(place);
2000-
effects.push({
2001-
kind: 'MutateTransitiveConditionally',
2002-
value: place,
2003-
});
20041988
}
1989+
effects.push({
1990+
kind: 'Capture',
1991+
from: place,
1992+
into: lvalue,
1993+
});
20051994
break;
20061995
}
20071996
case Effect.Freeze: {
@@ -2191,6 +2180,7 @@ function computeEffectsForSignature(
21912180
return null;
21922181
}
21932182
// Build substitutions
2183+
const mutableSpreads = new Set<IdentifierId>();
21942184
const substitutions: Map<IdentifierId, Array<Place>> = new Map();
21952185
substitutions.set(signature.receiver, [receiver]);
21962186
substitutions.set(signature.returns, [lvalue]);
@@ -2208,6 +2198,13 @@ function computeEffectsForSignature(
22082198
}
22092199
const place = arg.kind === 'Identifier' ? arg : arg.place;
22102200
getOrInsertWith(substitutions, signature.rest, () => []).push(place);
2201+
2202+
if (arg.kind === 'Spread') {
2203+
const mutateIterator = conditionallyMutateIterator(arg.place);
2204+
if (mutateIterator != null) {
2205+
mutableSpreads.add(arg.place.identifier.id);
2206+
}
2207+
}
22112208
} else {
22122209
const param = params[i];
22132210
substitutions.set(param, [arg]);
@@ -2279,6 +2276,12 @@ function computeEffectsForSignature(
22792276
case 'Freeze': {
22802277
const values = substitutions.get(effect.value.identifier.id) ?? [];
22812278
for (const value of values) {
2279+
if (mutableSpreads.has(value.identifier.id)) {
2280+
CompilerError.throwTodo({
2281+
reason: 'Support spread syntax for hook arguments',
2282+
loc: value.loc,
2283+
});
2284+
}
22822285
effects.push({kind: 'Freeze', value, reason: effect.reason});
22832286
}
22842287
break;

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-call-freezes-captured-identifier.expect.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export const FIXTURE_ENTRYPOINT = {
3232
11 | });
3333
12 |
3434
> 13 | x.value += count;
35-
| ^ InvalidReact: This mutates a variable that React considers immutable (13:13)
35+
| ^ InvalidReact: Updating a value previously passed as an argument to a hook is not allowed. Consider moving the mutation before calling the hook (13:13)
3636
14 | return <Stringify x={x} cb={cb} />;
3737
15 | }
3838
16 |

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-call-freezes-captured-memberexpr.expect.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export const FIXTURE_ENTRYPOINT = {
3232
11 | });
3333
12 |
3434
> 13 | x.value += count;
35-
| ^ InvalidReact: This mutates a variable that React considers immutable (13:13)
35+
| ^ InvalidReact: Updating a value previously passed as an argument to a hook is not allowed. Consider moving the mutation before calling the hook (13:13)
3636
14 | return <Stringify x={x} cb={cb} />;
3737
15 | }
3838
16 |

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-non-imported-reanimated-shared-value-writes.expect.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ function SomeComponent() {
2727
9 | return (
2828
10 | <Button
2929
> 11 | onPress={() => (sharedVal.value = Math.random())}
30-
| ^^^^^^^^^ InvalidReact: This mutates a variable that React considers immutable. Found mutation of `sharedVal` (11:11)
30+
| ^^^^^^^^^ InvalidReact: Updating a value returned from a hook is not allowed. Consider moving the mutation into the hook where the value is constructed. Found mutation of `sharedVal` (11:11)
3131
12 | title="Randomize"
3232
13 | />
3333
14 | );

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/no-emit/retry-no-emit.expect.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export const FIXTURE_ENTRYPOINT = {
5252
## Logs
5353

5454
```
55-
{"kind":"CompileError","fnLoc":{"start":{"line":5,"column":0,"index":163},"end":{"line":13,"column":1,"index":357},"filename":"retry-no-emit.ts"},"detail":{"reason":"This mutates a variable that React considers immutable","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":11,"column":2,"index":320},"end":{"line":11,"column":6,"index":324},"filename":"retry-no-emit.ts","identifierName":"arr2"}}}
55+
{"kind":"CompileError","fnLoc":{"start":{"line":5,"column":0,"index":163},"end":{"line":13,"column":1,"index":357},"filename":"retry-no-emit.ts"},"detail":{"reason":"Updating a value previously passed as an argument to a hook is not allowed. Consider moving the mutation before calling the hook","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":11,"column":2,"index":320},"end":{"line":11,"column":6,"index":324},"filename":"retry-no-emit.ts","identifierName":"arr2"}}}
5656
{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":7,"column":2,"index":216},"end":{"line":7,"column":36,"index":250},"filename":"retry-no-emit.ts"},"decorations":[{"start":{"line":7,"column":31,"index":245},"end":{"line":7,"column":34,"index":248},"filename":"retry-no-emit.ts","identifierName":"arr"}]}
5757
{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":10,"column":2,"index":274},"end":{"line":10,"column":44,"index":316},"filename":"retry-no-emit.ts"},"decorations":[{"start":{"line":10,"column":25,"index":297},"end":{"line":10,"column":29,"index":301},"filename":"retry-no-emit.ts","identifierName":"arr2"},{"start":{"line":10,"column":25,"index":297},"end":{"line":10,"column":29,"index":301},"filename":"retry-no-emit.ts","identifierName":"arr2"},{"start":{"line":10,"column":35,"index":307},"end":{"line":10,"column":42,"index":314},"filename":"retry-no-emit.ts","identifierName":"propVal"}]}
5858
{"kind":"CompileSuccess","fnLoc":{"start":{"line":5,"column":0,"index":163},"end":{"line":13,"column":1,"index":357},"filename":"retry-no-emit.ts"},"fnName":"Foo","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0}

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.mutate-frozen-value.expect.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ function Component({a, b}) {
1919
3 | const x = {a};
2020
4 | useFreeze(x);
2121
> 5 | x.y = true;
22-
| ^ InvalidReact: This mutates a variable that React considers immutable (5:5)
22+
| ^ InvalidReact: Updating a value previously passed as an argument to a hook is not allowed. Consider moving the mutation before calling the hook (5:5)
2323
6 | return <div>error</div>;
2424
7 | }
2525
8 |

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/retry-no-emit.expect.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export const FIXTURE_ENTRYPOINT = {
5252
## Logs
5353

5454
```
55-
{"kind":"CompileError","fnLoc":{"start":{"line":5,"column":0,"index":195},"end":{"line":13,"column":1,"index":389},"filename":"retry-no-emit.ts"},"detail":{"reason":"This mutates a variable that React considers immutable","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":11,"column":2,"index":352},"end":{"line":11,"column":6,"index":356},"filename":"retry-no-emit.ts","identifierName":"arr2"}}}
55+
{"kind":"CompileError","fnLoc":{"start":{"line":5,"column":0,"index":195},"end":{"line":13,"column":1,"index":389},"filename":"retry-no-emit.ts"},"detail":{"reason":"Updating a value previously passed as an argument to a hook is not allowed. Consider moving the mutation before calling the hook","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":11,"column":2,"index":352},"end":{"line":11,"column":6,"index":356},"filename":"retry-no-emit.ts","identifierName":"arr2"}}}
5656
{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":7,"column":2,"index":248},"end":{"line":7,"column":36,"index":282},"filename":"retry-no-emit.ts"},"decorations":[{"start":{"line":7,"column":31,"index":277},"end":{"line":7,"column":34,"index":280},"filename":"retry-no-emit.ts","identifierName":"arr"}]}
5757
{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":10,"column":2,"index":306},"end":{"line":10,"column":44,"index":348},"filename":"retry-no-emit.ts"},"decorations":[{"start":{"line":10,"column":25,"index":329},"end":{"line":10,"column":29,"index":333},"filename":"retry-no-emit.ts","identifierName":"arr2"},{"start":{"line":10,"column":25,"index":329},"end":{"line":10,"column":29,"index":333},"filename":"retry-no-emit.ts","identifierName":"arr2"},{"start":{"line":10,"column":35,"index":339},"end":{"line":10,"column":42,"index":346},"filename":"retry-no-emit.ts","identifierName":"propVal"}]}
5858
{"kind":"CompileSuccess","fnLoc":{"start":{"line":5,"column":0,"index":195},"end":{"line":13,"column":1,"index":389},"filename":"retry-no-emit.ts"},"fnName":"Foo","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0}

0 commit comments

Comments
 (0)