@@ -31,6 +31,7 @@ import {
3131 isMutableEffect ,
3232 isObjectType ,
3333 isRefValueType ,
34+ isSetStateType ,
3435 isUseRefType ,
3536} from '../HIR/HIR' ;
3637import { 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
21752347function isEffectSafeOutsideRender ( effect : FunctionEffect ) : boolean {
2176- return effect . kind === 'GlobalMutation' ;
2348+ return (
2349+ effect . kind === 'GlobalMutation' || effect . kind === 'ImmutableFunctionCall'
2350+ ) ;
21772351}
21782352
21792353function getWriteErrorReason ( abstractValue : AbstractValue ) : string {
0 commit comments