@@ -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