Skip to content

Commit 9d6b4b1

Browse files
committed
[compiler] Bail out and log calls that likely have side effects
ghstack-source-id: 70b37bb Pull Request resolved: #30572
1 parent 06d0b89 commit 9d6b4b1

File tree

2 files changed

+72
-3
lines changed

2 files changed

+72
-3
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,11 @@ export type HIRFunction = {
294294
};
295295

296296
export type FunctionEffect =
297+
| {
298+
kind: 'GlobalFunctionCall',
299+
error: CompilerErrorDetailOptions;
300+
lvalue: IdentifierId | null;
301+
}
297302
| {
298303
kind: 'GlobalMutation';
299304
error: CompilerErrorDetailOptions;

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

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ export default function inferReferenceEffects(
238238
}
239239

240240
if (!options.isFunctionExpression) {
241+
let usedIdentifiers: null | Set<IdentifierId> = null;
241242
functionEffects.forEach(eff => {
242243
switch (eff.kind) {
243244
case 'ReactMutation':
@@ -251,6 +252,28 @@ export default function inferReferenceEffects(
251252
loc: eff.loc,
252253
});
253254
}
255+
case "GlobalFunctionCall": {
256+
if (eff.lvalue == null) {
257+
functionEffects.push(eff);
258+
} else {
259+
if (usedIdentifiers === null) {
260+
usedIdentifiers = new Set();
261+
for (const [,block] of fn.body.blocks) {
262+
for (const instr of block.instructions) {
263+
Array.from(eachInstructionOperand(instr)).forEach(operand => usedIdentifiers?.add(operand.identifier.id));
264+
}
265+
for (const phi of block.phis) {
266+
Array.from(phi.operands.values()).forEach(operand => usedIdentifiers?.add(operand.id));
267+
}
268+
Array.from(eachTerminalOperand(block.terminal)).forEach(operand => usedIdentifiers?.add(operand.identifier.id));
269+
}
270+
}
271+
if (!usedIdentifiers.has(eff.lvalue)) {
272+
CompilerError.throw(eff.error);
273+
}
274+
}
275+
break;
276+
}
254277
default:
255278
assertExhaustive(
256279
eff,
@@ -406,14 +429,35 @@ class InferenceState {
406429
value.kind === 'ObjectMethod') &&
407430
value.loweredFunc.func.effects != null
408431
) {
432+
let usedIdentifiers: null | Set<IdentifierId> = null;
409433
for (const effect of value.loweredFunc.func.effects) {
410434
if (
411435
effect.kind === 'GlobalMutation' ||
412436
effect.kind === 'ReactMutation'
413437
) {
414438
// Known effects are always propagated upwards
415439
functionEffects.push(effect);
416-
} else {
440+
} else if (effect.kind === "GlobalFunctionCall") {
441+
if (effect.lvalue == null) {
442+
functionEffects.push(effect);
443+
} else {
444+
if (usedIdentifiers === null) {
445+
usedIdentifiers = new Set();
446+
for (const [,block] of value.loweredFunc.func.body.blocks) {
447+
for (const instr of block.instructions) {
448+
Array.from(eachInstructionOperand(instr)).forEach(operand => usedIdentifiers?.add(operand.identifier.id));
449+
}
450+
for (const phi of block.phis) {
451+
Array.from(phi.operands.values()).forEach(operand => usedIdentifiers?.add(operand.id));
452+
}
453+
Array.from(eachTerminalOperand(block.terminal)).forEach(operand => usedIdentifiers?.add(operand.identifier.id));
454+
}
455+
}
456+
if (!usedIdentifiers.has(effect.lvalue)) {
457+
functionEffects.push({ ...effect, lvalue: null });
458+
}
459+
}
460+
} else if (effect.kind === 'ContextMutation') {
417461
/**
418462
* Contextual effects need to be replayed against the current inference
419463
* state, which may know more about the value to which the effect applied.
@@ -444,6 +488,8 @@ class InferenceState {
444488
} // else case 2, local mutable value so this effect was fine
445489
}
446490
}
491+
} else {
492+
assertExhaustive(effect, `Unexpected function effect kind`);
447493
}
448494
}
449495
}
@@ -1357,7 +1403,7 @@ function inferBlock(
13571403
functionEffects.push(
13581404
...argumentEffects.filter(
13591405
argEffect =>
1360-
!isUseEffect || i !== 0 || argEffect.kind !== 'GlobalMutation',
1406+
!isUseEffect || i !== 0 || (argEffect.kind !== 'GlobalMutation' && argEffect.kind !== 'GlobalFunctionCall'),
13611407
),
13621408
);
13631409
hasCaptureArgument ||= place.effect === Effect.Capture;
@@ -1379,6 +1425,24 @@ function inferBlock(
13791425
}
13801426
hasCaptureArgument ||= instrValue.callee.effect === Effect.Capture;
13811427

1428+
const calleeValue = state.kind(instrValue.callee);
1429+
if (calleeValue.kind === ValueKind.Global && signature?.hookKind == null) {
1430+
const allRead = instrValue.args.every(arg => {
1431+
switch (arg.kind) {
1432+
case "Identifier":
1433+
return arg.effect === Effect.Read;
1434+
case "Spread":
1435+
return arg.place.effect === Effect.Read;
1436+
default:
1437+
assertExhaustive(arg, 'Unexpected arg kind');
1438+
}
1439+
});
1440+
if (allRead) {
1441+
functionEffects.push({kind: "GlobalFunctionCall", lvalue: instr.lvalue.identifier.id, error: {reason: "External function is called with arguments that React Compiler expects to be immutable and return value is ignored. This call is likely to perform unsafe side effects, which violates the rules of React.", loc: instrValue.loc, severity: ErrorSeverity.InvalidReact}});
1442+
}
1443+
}
1444+
1445+
13821446
state.initialize(instrValue, returnValueKind);
13831447
state.define(instr.lvalue, instrValue);
13841448
instr.lvalue.effect = hasCaptureArgument
@@ -1486,7 +1550,7 @@ function inferBlock(
14861550
functionEffects.push(
14871551
...argumentEffects.filter(
14881552
argEffect =>
1489-
!isUseEffect || i !== 0 || argEffect.kind !== 'GlobalMutation',
1553+
!isUseEffect || i !== 0 || (argEffect.kind !== 'GlobalMutation' && argEffect.kind !== 'GlobalFunctionCall'),
14901554
),
14911555
);
14921556
hasCaptureArgument ||= place.effect === Effect.Capture;

0 commit comments

Comments
 (0)