Skip to content

Commit 016530e

Browse files
committed
[compiler] Bail out and log calls that likely have side effects
ghstack-source-id: 13d4cc0 Pull Request resolved: #30572
1 parent 13034bb commit 016530e

File tree

2 files changed

+186
-5
lines changed

2 files changed

+186
-5
lines changed

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

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

296296
export type FunctionEffect =
297+
| {
298+
kind: 'ImmutableFunctionCall';
299+
loc: SourceLocation;
300+
lvalue: IdentifierId;
301+
callee: IdentifierId;
302+
global: boolean;
303+
}
297304
| {
298305
kind: 'GlobalMutation';
299306
error: CompilerErrorDetailOptions;

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

Lines changed: 179 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
isMutableEffect,
3232
isObjectType,
3333
isRefValueType,
34+
isSetStateType,
3435
isUseRefType,
3536
} from '../HIR/HIR';
3637
import {FunctionSignature} from '../HIR/ObjectShape';
@@ -251,16 +252,133 @@ export default function inferReferenceEffects(
251252
loc: eff.loc,
252253
});
253254
}
255+
case 'ImmutableFunctionCall': {
256+
// Handled below
257+
break;
258+
}
254259
default:
255260
assertExhaustive(
256261
eff,
257262
`Unexpected function effect kind \`${(eff as any).kind}\``,
258263
);
259264
}
260265
});
261-
} else {
262-
fn.effects = functionEffects;
266+
267+
if (functionEffects.length > 0) {
268+
const error = new CompilerError();
269+
let usedIdentifiers = new Set<IdentifierId>();
270+
let names = new Map<IdentifierId, string | undefined>();
271+
272+
function visitFunction(fn: HIRFunction): void {
273+
for (const [, block] of fn.body.blocks) {
274+
for (const instr of block.instructions) {
275+
switch (instr.value.kind) {
276+
case 'FunctionExpression':
277+
case 'ObjectMethod':
278+
names.set(
279+
instr.lvalue.identifier.id,
280+
instr.value.loweredFunc.func.id ?? '(anonymous function)',
281+
);
282+
visitFunction(instr.value.loweredFunc.func);
283+
break;
284+
case 'LoadGlobal':
285+
names.set(instr.lvalue.identifier.id, instr.value.binding.name);
286+
break;
287+
case 'LoadLocal':
288+
case 'LoadContext':
289+
names.set(
290+
instr.lvalue.identifier.id,
291+
instr.value.place.identifier.name?.value ??
292+
names.get(instr.value.place.identifier.id),
293+
);
294+
break;
295+
case 'StoreContext':
296+
case 'StoreLocal':
297+
names.set(
298+
instr.lvalue.identifier.id,
299+
instr.value.value.identifier.name?.value ??
300+
names.get(instr.value.value.identifier.id),
301+
);
302+
names.set(
303+
instr.value.lvalue.place.identifier.id,
304+
instr.value.value.identifier.name?.value ??
305+
names.get(instr.value.value.identifier.id),
306+
);
307+
break;
308+
case 'PropertyLoad':
309+
names.set(
310+
instr.lvalue.identifier.id,
311+
`${instr.value.object.identifier.name?.value ?? names.get(instr.value.object.identifier.id) ?? '(unknown)'}.${instr.value.property}`,
312+
);
313+
break;
314+
case 'ComputedLoad':
315+
names.set(
316+
instr.lvalue.identifier.id,
317+
`${instr.value.object.identifier.name?.value ?? names.get(instr.value.object.identifier.id) ?? '(unknown)'}[...]`,
318+
);
319+
break;
320+
case 'Destructure': {
321+
const destructuredName =
322+
instr.value.value.identifier.name?.value ??
323+
names.get(instr.value.value.identifier.id);
324+
const destructuredMsg = destructuredName
325+
? `(destructured from \`${destructuredName}\`)`
326+
: '(destructured)';
327+
Array.from(
328+
eachPatternOperand(instr.value.lvalue.pattern),
329+
).forEach(place =>
330+
names.set(
331+
place.identifier.id,
332+
`${place.identifier.name?.value ?? 'value'} ${destructuredMsg}`,
333+
),
334+
);
335+
}
336+
}
337+
Array.from(eachInstructionOperand(instr)).forEach(operand =>
338+
usedIdentifiers.add(operand.identifier.id),
339+
);
340+
}
341+
for (const phi of block.phis) {
342+
Array.from(phi.operands.values()).forEach(operand =>
343+
usedIdentifiers.add(operand.id),
344+
);
345+
}
346+
Array.from(eachTerminalOperand(block.terminal)).forEach(operand =>
347+
usedIdentifiers.add(operand.identifier.id),
348+
);
349+
}
350+
}
351+
visitFunction(fn);
352+
353+
const allowedNames = new Set(['invariant', 'recoverableViolation']);
354+
355+
for (const effect of functionEffects) {
356+
CompilerError.invariant(effect.kind === 'ImmutableFunctionCall', {
357+
reason:
358+
'All effects other than ImmutableFunctionCall should have been handled earlier',
359+
loc: null,
360+
});
361+
if (
362+
!usedIdentifiers.has(effect.lvalue) &&
363+
(!effect.global ||
364+
!names.has(effect.callee) ||
365+
!allowedNames.has(names.get(effect.callee)!))
366+
) {
367+
const name = names.get(effect.callee) ?? '(unknown)';
368+
error.push({
369+
reason: `Function \'${name}\' is called with arguments that React Compiler expects to be immutable and its return value is ignored. This call is likely to perform unsafe side effects, which violates the rules of React.`,
370+
loc: effect.loc,
371+
severity: ErrorSeverity.InvalidReact,
372+
});
373+
}
374+
}
375+
376+
if (error.hasErrors()) {
377+
throw error;
378+
}
379+
}
263380
}
381+
fn.effects = functionEffects;
264382
}
265383

266384
// Maintains a mapping of top-level variables to the kind of value they hold
@@ -433,11 +551,12 @@ class InferenceState {
433551
for (const effect of dependentEffects) {
434552
if (
435553
effect.kind === 'GlobalMutation' ||
436-
effect.kind === 'ReactMutation'
554+
effect.kind === 'ReactMutation' ||
555+
effect.kind === 'ImmutableFunctionCall'
437556
) {
438557
// Known effects are always propagated upwards
439558
functionEffects.push(effect);
440-
} else {
559+
} else if (effect.kind === 'ContextMutation') {
441560
/**
442561
* Contextual effects need to be replayed against the current inference
443562
* state, which may know more about the value to which the effect applied.
@@ -1416,6 +1535,32 @@ function inferBlock(
14161535
}
14171536
hasCaptureArgument ||= instrValue.callee.effect === Effect.Capture;
14181537

1538+
if (
1539+
!isSetStateType(instrValue.callee.identifier) &&
1540+
instrValue.callee.effect === Effect.Read &&
1541+
signature?.hookKind == null
1542+
) {
1543+
const allRead = instrValue.args.every(arg => {
1544+
switch (arg.kind) {
1545+
case 'Identifier':
1546+
return arg.effect === Effect.Read;
1547+
case 'Spread':
1548+
return arg.place.effect === Effect.Read;
1549+
default:
1550+
assertExhaustive(arg, 'Unexpected arg kind');
1551+
}
1552+
});
1553+
if (allRead) {
1554+
functionEffects.push({
1555+
kind: 'ImmutableFunctionCall',
1556+
lvalue: instr.lvalue.identifier.id,
1557+
callee: instrValue.callee.identifier.id,
1558+
loc: instrValue.loc,
1559+
global: state.kind(instrValue.callee).kind === ValueKind.Global,
1560+
});
1561+
}
1562+
}
1563+
14191564
state.initialize(instrValue, returnValueKind);
14201565
state.define(instr.lvalue, instrValue);
14211566
instr.lvalue.effect = hasCaptureArgument
@@ -1544,6 +1689,33 @@ function inferBlock(
15441689
}
15451690
hasCaptureArgument ||= instrValue.receiver.effect === Effect.Capture;
15461691

1692+
if (
1693+
!isSetStateType(instrValue.property.identifier) &&
1694+
instrValue.receiver.effect === Effect.Read &&
1695+
instrValue.property.effect === Effect.Read &&
1696+
signature?.hookKind == null
1697+
) {
1698+
const allRead = instrValue.args.every(arg => {
1699+
switch (arg.kind) {
1700+
case 'Identifier':
1701+
return arg.effect === Effect.Read;
1702+
case 'Spread':
1703+
return arg.place.effect === Effect.Read;
1704+
default:
1705+
assertExhaustive(arg, 'Unexpected arg kind');
1706+
}
1707+
});
1708+
if (allRead) {
1709+
functionEffects.push({
1710+
kind: 'ImmutableFunctionCall',
1711+
lvalue: instr.lvalue.identifier.id,
1712+
callee: instrValue.property.identifier.id,
1713+
loc: instrValue.loc,
1714+
global: state.kind(instrValue.property).kind === ValueKind.Global,
1715+
});
1716+
}
1717+
}
1718+
15471719
state.initialize(instrValue, returnValueKind);
15481720
state.define(instr.lvalue, instrValue);
15491721
instr.lvalue.effect = hasCaptureArgument
@@ -2173,7 +2345,9 @@ function areArgumentsImmutableAndNonMutating(
21732345
}
21742346

21752347
function isEffectSafeOutsideRender(effect: FunctionEffect): boolean {
2176-
return effect.kind === 'GlobalMutation';
2348+
return (
2349+
effect.kind === 'GlobalMutation' || effect.kind === 'ImmutableFunctionCall'
2350+
);
21772351
}
21782352

21792353
function getWriteErrorReason(abstractValue: AbstractValue): string {

0 commit comments

Comments
 (0)