@@ -30,6 +30,7 @@ import {
3030 isMutableEffect ,
3131 isObjectType ,
3232 isRefValueType ,
33+ isSetStateType ,
3334 isUseRefType ,
3435} from '../HIR/HIR' ;
3536import { 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
@@ -409,11 +527,12 @@ class InferenceState {
409527 for ( const effect of value . loweredFunc . func . effects ) {
410528 if (
411529 effect . kind === 'GlobalMutation' ||
412- effect . kind === 'ReactMutation'
530+ effect . kind === 'ReactMutation' ||
531+ effect . kind === 'ImmutableFunctionCall'
413532 ) {
414533 // Known effects are always propagated upwards
415534 functionEffects . push ( effect ) ;
416- } else {
535+ } else if ( effect . kind === 'ContextMutation' ) {
417536 /**
418537 * Contextual effects need to be replayed against the current inference
419538 * state, which may know more about the value to which the effect applied.
@@ -444,6 +563,8 @@ class InferenceState {
444563 } // else case 2, local mutable value so this effect was fine
445564 }
446565 }
566+ } else {
567+ assertExhaustive ( effect , `Unexpected function effect kind` ) ;
447568 }
448569 }
449570 }
@@ -1151,7 +1272,9 @@ function inferBlock(
11511272 ) ;
11521273 functionEffects . push (
11531274 ...propEffects . filter (
1154- propEffect => propEffect . kind !== 'GlobalMutation' ,
1275+ propEffect =>
1276+ propEffect . kind !== 'GlobalMutation' &&
1277+ propEffect . kind !== 'ImmutableFunctionCall' ,
11551278 ) ,
11561279 ) ;
11571280 }
@@ -1357,7 +1480,10 @@ function inferBlock(
13571480 functionEffects . push (
13581481 ...argumentEffects . filter (
13591482 argEffect =>
1360- ! isUseEffect || i !== 0 || argEffect . kind !== 'GlobalMutation' ,
1483+ ! isUseEffect ||
1484+ i !== 0 ||
1485+ ( argEffect . kind !== 'GlobalMutation' &&
1486+ argEffect . kind !== 'ImmutableFunctionCall' ) ,
13611487 ) ,
13621488 ) ;
13631489 hasCaptureArgument ||= place . effect === Effect . Capture ;
@@ -1379,6 +1505,32 @@ function inferBlock(
13791505 }
13801506 hasCaptureArgument ||= instrValue . callee . effect === Effect . Capture ;
13811507
1508+ if (
1509+ ! isSetStateType ( instrValue . callee . identifier ) &&
1510+ instrValue . callee . effect === Effect . Read &&
1511+ signature ?. hookKind == null
1512+ ) {
1513+ const allRead = instrValue . args . every ( arg => {
1514+ switch ( arg . kind ) {
1515+ case 'Identifier' :
1516+ return arg . effect === Effect . Read ;
1517+ case 'Spread' :
1518+ return arg . place . effect === Effect . Read ;
1519+ default :
1520+ assertExhaustive ( arg , 'Unexpected arg kind' ) ;
1521+ }
1522+ } ) ;
1523+ if ( allRead ) {
1524+ functionEffects . push ( {
1525+ kind : 'ImmutableFunctionCall' ,
1526+ lvalue : instr . lvalue . identifier . id ,
1527+ callee : instrValue . callee . identifier . id ,
1528+ loc : instrValue . loc ,
1529+ global : state . kind ( instrValue . callee ) . kind === ValueKind . Global ,
1530+ } ) ;
1531+ }
1532+ }
1533+
13821534 state . initialize ( instrValue , returnValueKind ) ;
13831535 state . define ( instr . lvalue , instrValue ) ;
13841536 instr . lvalue . effect = hasCaptureArgument
@@ -1486,7 +1638,10 @@ function inferBlock(
14861638 functionEffects . push (
14871639 ...argumentEffects . filter (
14881640 argEffect =>
1489- ! isUseEffect || i !== 0 || argEffect . kind !== 'GlobalMutation' ,
1641+ ! isUseEffect ||
1642+ i !== 0 ||
1643+ ( argEffect . kind !== 'GlobalMutation' &&
1644+ argEffect . kind !== 'ImmutableFunctionCall' ) ,
14901645 ) ,
14911646 ) ;
14921647 hasCaptureArgument ||= place . effect === Effect . Capture ;
@@ -1508,6 +1663,33 @@ function inferBlock(
15081663 }
15091664 hasCaptureArgument ||= instrValue . receiver . effect === Effect . Capture ;
15101665
1666+ if (
1667+ ! isSetStateType ( instrValue . property . identifier ) &&
1668+ instrValue . receiver . effect === Effect . Read &&
1669+ instrValue . property . effect === Effect . Read &&
1670+ signature ?. hookKind == null
1671+ ) {
1672+ const allRead = instrValue . args . every ( arg => {
1673+ switch ( arg . kind ) {
1674+ case 'Identifier' :
1675+ return arg . effect === Effect . Read ;
1676+ case 'Spread' :
1677+ return arg . place . effect === Effect . Read ;
1678+ default :
1679+ assertExhaustive ( arg , 'Unexpected arg kind' ) ;
1680+ }
1681+ } ) ;
1682+ if ( allRead ) {
1683+ functionEffects . push ( {
1684+ kind : 'ImmutableFunctionCall' ,
1685+ lvalue : instr . lvalue . identifier . id ,
1686+ callee : instrValue . property . identifier . id ,
1687+ loc : instrValue . loc ,
1688+ global : state . kind ( instrValue . property ) . kind === ValueKind . Global ,
1689+ } ) ;
1690+ }
1691+ }
1692+
15111693 state . initialize ( instrValue , returnValueKind ) ;
15121694 state . define ( instr . lvalue , instrValue ) ;
15131695 instr . lvalue . effect = hasCaptureArgument
0 commit comments