/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import {CompilerDiagnostic, CompilerError, Effect, ErrorSeverity} from '..'; import { HIRFunction, IdentifierId, isRefOrRefLikeMutableType, Place, } from '../HIR'; import { eachInstructionValueOperand, eachTerminalOperand, } from '../HIR/visitors'; import {AliasingEffect} from '../Inference/AliasingEffects'; import {Result} from '../Utils/Result'; /** * Validates that functions with known mutations (ie due to types) cannot be passed * where a frozen value is expected. Example: * * ``` * function Component() { * const cache = new Map(); * const onClick = () => { * cache.set(...); * } * useHook(onClick); // ERROR: cannot pass a mutable value * return // ERROR: cannot pass a mutable value * } * ``` * * Because `onClick` function mutates `cache` when called, `onClick` is equivalent to a mutable * variables. But unlike other mutables values like an array, the receiver of the function has * no way to avoid mutation — for example, a function can receive an array and choose not to mutate * it, but there's no way to know that a function is mutable and avoid calling it. * * This pass detects functions with *known* mutations (Store or Mutate, not ConditionallyMutate) * that are passed where a frozen value is expected and rejects them. */ export function validateNoFreezingKnownMutableFunctions( fn: HIRFunction, ): Result { const errors = new CompilerError(); const contextMutationEffects: Map< IdentifierId, Extract > = new Map(); function visitOperand(operand: Place): void { if (operand.effect === Effect.Freeze) { const effect = contextMutationEffects.get(operand.identifier.id); if (effect != null) { const place = effect.value; const variable = place != null && place.identifier.name != null && place.identifier.name.kind === 'named' ? `\`${place.identifier.name.value}\`` : 'a local variable'; errors.pushDiagnostic( CompilerDiagnostic.create({ severity: ErrorSeverity.InvalidReact, category: 'Cannot modify local variables after render completes', description: `This argument is a function which may reassign or mutate ${variable} after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.`, }) .withDetail({ kind: 'error', loc: operand.loc, message: `This function may (indirectly) reassign or modify ${variable} after render`, }) .withDetail({ kind: 'error', loc: effect.value.loc, message: `This modifies ${variable}`, }), ); } } } for (const block of fn.body.blocks.values()) { for (const instr of block.instructions) { const {lvalue, value} = instr; switch (value.kind) { case 'LoadLocal': { const effect = contextMutationEffects.get(value.place.identifier.id); if (effect != null) { contextMutationEffects.set(lvalue.identifier.id, effect); } break; } case 'StoreLocal': { const effect = contextMutationEffects.get(value.value.identifier.id); if (effect != null) { contextMutationEffects.set(lvalue.identifier.id, effect); contextMutationEffects.set( value.lvalue.place.identifier.id, effect, ); } break; } case 'FunctionExpression': { if (value.loweredFunc.func.aliasingEffects != null) { const context = new Set( value.loweredFunc.func.context.map(p => p.identifier.id), ); effects: for (const effect of value.loweredFunc.func .aliasingEffects) { switch (effect.kind) { case 'Mutate': case 'MutateTransitive': { const knownMutation = contextMutationEffects.get( effect.value.identifier.id, ); if (knownMutation != null) { contextMutationEffects.set( lvalue.identifier.id, knownMutation, ); } else if ( context.has(effect.value.identifier.id) && !isRefOrRefLikeMutableType(effect.value.identifier.type) ) { contextMutationEffects.set(lvalue.identifier.id, effect); break effects; } break; } case 'MutateConditionally': case 'MutateTransitiveConditionally': { const knownMutation = contextMutationEffects.get( effect.value.identifier.id, ); if (knownMutation != null) { contextMutationEffects.set( lvalue.identifier.id, knownMutation, ); } break; } } } } break; } default: { for (const operand of eachInstructionValueOperand(value)) { visitOperand(operand); } } } } for (const operand of eachTerminalOperand(block.terminal)) { visitOperand(operand); } } return errors.asResult(); }