/
githubmr
/
facebook-react
Обзор
Документация
Войти
/
githubmr
/
facebook-react
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
main
compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts
739 строк
23 KB
lauren
[compiler] Filter out disabled errors from being reported (#34409)
06 сен 2025, 20:07
Не верифицирован
06 сен 2025, 20:07
7899252
Код
Авторство
О чём код?
/** * 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 {CompilerError, CompilerErrorDetailOptions, SourceLocation} from '..'; import { ArrayExpression, CallExpression, Effect, Environment, FunctionExpression, GeneratedSource, HIRFunction, Identifier, IdentifierId, Instruction, InstructionId, InstructionKind, InstructionValue, isUseEffectHookType, LoadLocal, makeInstructionId, NonLocalImportSpecifier, Place, promoteTemporary, } from '../HIR'; import {createTemporaryPlace, markInstructionIds} from '../HIR/HIRBuilder'; import {getOrInsertWith} from '../Utils/utils'; import { BuiltInFireFunctionId, BuiltInFireId, DefaultNonmutatingHook, } from '../HIR/ObjectShape'; import {eachInstructionOperand} from '../HIR/visitors'; import {printSourceLocationLine} from '../HIR/PrintHIR'; import {USE_FIRE_FUNCTION_NAME} from '../HIR/Environment'; import {ErrorCategory} from '../CompilerError'; /* * TODO(jmbrown): * - traverse object methods * - method calls * - React.useEffect calls */ const CANNOT_COMPILE_FIRE = 'Cannot compile `fire`'; export function transformFire(fn: HIRFunction): void { const context = new Context(fn.env); replaceFireFunctions(fn, context); if (!context.hasErrors()) { ensureNoMoreFireUses(fn, context); } context.throwIfErrorsFound(); } function replaceFireFunctions(fn: HIRFunction, context: Context): void { let importedUseFire: NonLocalImportSpecifier | null = null; let hasRewrite = false; for (const [, block] of fn.body.blocks) { const rewriteInstrs = new Map<InstructionId, Array<Instruction>>(); const deleteInstrs = new Set<InstructionId>(); for (const instr of block.instructions) { const {value, lvalue} = instr; if ( value.kind === 'CallExpression' && isUseEffectHookType(value.callee.identifier) && value.args.length > 0 && value.args[0].kind === 'Identifier' ) { const lambda = context.getFunctionExpression( value.args[0].identifier.id, ); if (lambda != null) { const capturedCallees = visitFunctionExpressionAndPropagateFireDependencies( lambda, context, true, ); // Add useFire calls for all fire calls in found in the lambda const newInstrs = []; for (const [ fireCalleePlace, fireCalleeInfo, ] of capturedCallees.entries()) { if (!context.hasCalleeWithInsertedFire(fireCalleePlace)) { context.addCalleeWithInsertedFire(fireCalleePlace); importedUseFire ??= fn.env.programContext.addImportSpecifier({ source: fn.env.programContext.reactRuntimeModule, importSpecifierName: USE_FIRE_FUNCTION_NAME, }); const loadUseFireInstr = makeLoadUseFireInstruction( fn.env, importedUseFire, ); const loadFireCalleeInstr = makeLoadFireCalleeInstruction( fn.env, fireCalleeInfo.capturedCalleeIdentifier, ); const callUseFireInstr = makeCallUseFireInstruction( fn.env, loadUseFireInstr.lvalue, loadFireCalleeInstr.lvalue, ); const storeUseFireInstr = makeStoreUseFireInstruction( fn.env, callUseFireInstr.lvalue, fireCalleeInfo.fireFunctionBinding, ); newInstrs.push( loadUseFireInstr, loadFireCalleeInstr, callUseFireInstr, storeUseFireInstr, ); // We insert all of these instructions before the useEffect is loaded const loadUseEffectInstrId = context.getLoadGlobalInstrId( value.callee.identifier.id, ); if (loadUseEffectInstrId == null) { context.pushError({ loc: value.loc, description: null, category: ErrorCategory.Invariant, reason: '[InsertFire] No LoadGlobal found for useEffect call', suggestions: null, }); continue; } rewriteInstrs.set(loadUseEffectInstrId, newInstrs); } } ensureNoRemainingCalleeCaptures( lambda.loweredFunc.func, context, capturedCallees, ); if ( value.args.length > 1 && value.args[1] != null && value.args[1].kind === 'Identifier' ) { const depArray = value.args[1]; const depArrayExpression = context.getArrayExpression( depArray.identifier.id, ); if (depArrayExpression != null) { for (const dependency of depArrayExpression.elements) { if (dependency.kind === 'Identifier') { const loadOfDependency = context.getLoadLocalInstr( dependency.identifier.id, ); if (loadOfDependency != null) { const replacedDepArrayItem = capturedCallees.get( loadOfDependency.place.identifier.id, ); if (replacedDepArrayItem != null) { loadOfDependency.place = replacedDepArrayItem.fireFunctionBinding; } } } } } else { context.pushError({ loc: value.args[1].loc, description: 'You must use an array literal for an effect dependency array when that effect uses `fire()`', category: ErrorCategory.Fire, reason: CANNOT_COMPILE_FIRE, suggestions: null, }); } } else if (value.args.length > 1 && value.args[1].kind === 'Spread') { context.pushError({ loc: value.args[1].place.loc, description: 'You must use an array literal for an effect dependency array when that effect uses `fire()`', category: ErrorCategory.Fire, reason: CANNOT_COMPILE_FIRE, suggestions: null, }); } } } else if ( value.kind === 'CallExpression' && value.callee.identifier.type.kind === 'Function' && value.callee.identifier.type.shapeId === BuiltInFireId && context.inUseEffectLambda() ) { /* * We found a fire(callExpr()) call. We remove the `fire()` call and replace the callExpr() * with a freshly generated fire function binding. We'll insert the useFire call before the * useEffect call, which happens in the CallExpression (useEffect) case above. */ /* * We only allow fire to be called with a CallExpression: `fire(f())` * TODO: add support for method calls: `fire(this.method())` */ if (value.args.length === 1 && value.args[0].kind === 'Identifier') { const callExpr = context.getCallExpression( value.args[0].identifier.id, ); if (callExpr != null) { const calleeId = callExpr.callee.identifier.id; const loadLocal = context.getLoadLocalInstr(calleeId); if (loadLocal == null) { context.pushError({ loc: value.loc, description: null, category: ErrorCategory.Invariant, reason: '[InsertFire] No loadLocal found for fire call argument', suggestions: null, }); continue; } const fireFunctionBinding = context.getOrGenerateFireFunctionBinding( loadLocal.place, value.loc, ); loadLocal.place = {...fireFunctionBinding}; // Delete the fire call expression deleteInstrs.add(instr.id); } else { context.pushError({ loc: value.loc, description: '`fire()` can only receive a function call such as `fire(fn(a,b)). Method calls and other expressions are not allowed', category: ErrorCategory.Fire, reason: CANNOT_COMPILE_FIRE, suggestions: null, }); } } else { let description: string = 'fire() can only take in a single call expression as an argument'; if (value.args.length === 0) { description += ' but received none'; } else if (value.args.length > 1) { description += ' but received multiple arguments'; } else if (value.args[0].kind === 'Spread') { description += ' but received a spread argument'; } context.pushError({ loc: value.loc, description, category: ErrorCategory.Fire, reason: CANNOT_COMPILE_FIRE, suggestions: null, }); } } else if (value.kind === 'CallExpression') { context.addCallExpression(lvalue.identifier.id, value); } else if ( value.kind === 'FunctionExpression' && context.inUseEffectLambda() ) { visitFunctionExpressionAndPropagateFireDependencies( value, context, false, ); } else if (value.kind === 'FunctionExpression') { context.addFunctionExpression(lvalue.identifier.id, value); } else if (value.kind === 'LoadLocal') { context.addLoadLocalInstr(lvalue.identifier.id, value); } else if ( value.kind === 'LoadGlobal' && value.binding.kind === 'ImportSpecifier' && value.binding.module === 'react' && value.binding.imported === 'fire' && context.inUseEffectLambda() ) { deleteInstrs.add(instr.id); } else if (value.kind === 'LoadGlobal') { context.addLoadGlobalInstrId(lvalue.identifier.id, instr.id); } else if (value.kind === 'ArrayExpression') { context.addArrayExpression(lvalue.identifier.id, value); } } block.instructions = rewriteInstructions(rewriteInstrs, block.instructions); block.instructions = deleteInstructions(deleteInstrs, block.instructions); if (rewriteInstrs.size > 0 || deleteInstrs.size > 0) { hasRewrite = true; fn.env.hasFireRewrite = true; } } if (hasRewrite) { markInstructionIds(fn.body); } } /** * Traverses a function expression to find fire calls fire(foo()) and replaces them with * fireFoo(). * * When a function captures a fire call we need to update its context to reflect the newly created * fire function bindings and update the LoadLocals referenced by the function's dependencies. * * @param isUseEffect is necessary so we can keep track of when we should additionally insert * useFire hooks calls. */ function visitFunctionExpressionAndPropagateFireDependencies( fnExpr: FunctionExpression, context: Context, enteringUseEffect: boolean, ): FireCalleesToFireFunctionBinding { let withScope = enteringUseEffect ? context.withUseEffectLambdaScope.bind(context) : context.withFunctionScope.bind(context); const calleesCapturedByFnExpression = withScope(() => replaceFireFunctions(fnExpr.loweredFunc.func, context), ); // For each replaced callee, update the context of the function expression to track it for ( let contextIdx = 0; contextIdx < fnExpr.loweredFunc.func.context.length; contextIdx++ ) { const contextItem = fnExpr.loweredFunc.func.context[contextIdx]; const replacedCallee = calleesCapturedByFnExpression.get( contextItem.identifier.id, ); if (replacedCallee != null) { fnExpr.loweredFunc.func.context[contextIdx] = { ...replacedCallee.fireFunctionBinding, }; } } context.mergeCalleesFromInnerScope(calleesCapturedByFnExpression); return calleesCapturedByFnExpression; } /* * eachInstructionOperand is not sufficient for our cases because: * 1. fire is a global, which will not appear * 2. The HIR may be malformed, so can't rely on function deps and must * traverse the whole function. */ function* eachReachablePlace(fn: HIRFunction): Iterable<Place> { for (const [, block] of fn.body.blocks) { for (const instr of block.instructions) { if ( instr.value.kind === 'FunctionExpression' || instr.value.kind === 'ObjectMethod' ) { yield* eachReachablePlace(instr.value.loweredFunc.func); } else { yield* eachInstructionOperand(instr); } } } } function ensureNoRemainingCalleeCaptures( fn: HIRFunction, context: Context, capturedCallees: FireCalleesToFireFunctionBinding, ): void { for (const place of eachReachablePlace(fn)) { const calleeInfo = capturedCallees.get(place.identifier.id); if (calleeInfo != null) { const calleeName = calleeInfo.capturedCalleeIdentifier.name?.kind === 'named' ? calleeInfo.capturedCalleeIdentifier.name.value : '<unknown>'; context.pushError({ loc: place.loc, description: `All uses of ${calleeName} must be either used with a fire() call in \ this effect or not used with a fire() call at all. ${calleeName} was used with fire() on line \ ${printSourceLocationLine(calleeInfo.fireLoc)} in this effect`, category: ErrorCategory.Fire, reason: CANNOT_COMPILE_FIRE, suggestions: null, }); } } } function ensureNoMoreFireUses(fn: HIRFunction, context: Context): void { for (const place of eachReachablePlace(fn)) { if ( place.identifier.type.kind === 'Function' && place.identifier.type.shapeId === BuiltInFireId ) { context.pushError({ loc: place.identifier.loc, description: 'Cannot use `fire` outside of a useEffect function', category: ErrorCategory.Fire, reason: CANNOT_COMPILE_FIRE, suggestions: null, }); } } } function makeLoadUseFireInstruction( env: Environment, importedLoadUseFire: NonLocalImportSpecifier, ): Instruction { const useFirePlace = createTemporaryPlace(env, GeneratedSource); useFirePlace.effect = Effect.Read; useFirePlace.identifier.type = DefaultNonmutatingHook; const instrValue: InstructionValue = { kind: 'LoadGlobal', binding: {...importedLoadUseFire}, loc: GeneratedSource, }; return { id: makeInstructionId(0), value: instrValue, lvalue: {...useFirePlace}, loc: GeneratedSource, effects: null, }; } function makeLoadFireCalleeInstruction( env: Environment, fireCalleeIdentifier: Identifier, ): Instruction { const loadedFireCallee = createTemporaryPlace(env, GeneratedSource); const fireCallee: Place = { kind: 'Identifier', identifier: fireCalleeIdentifier, reactive: false, effect: Effect.Unknown, loc: fireCalleeIdentifier.loc, }; return { id: makeInstructionId(0), value: { kind: 'LoadLocal', loc: GeneratedSource, place: {...fireCallee}, }, lvalue: {...loadedFireCallee}, loc: GeneratedSource, effects: null, }; } function makeCallUseFireInstruction( env: Environment, useFirePlace: Place, argPlace: Place, ): Instruction { const useFireCallResultPlace = createTemporaryPlace(env, GeneratedSource); useFireCallResultPlace.effect = Effect.Read; const useFireCall: CallExpression = { kind: 'CallExpression', callee: {...useFirePlace}, args: [argPlace], loc: GeneratedSource, }; return { id: makeInstructionId(0), value: useFireCall, lvalue: {...useFireCallResultPlace}, loc: GeneratedSource, effects: null, }; } function makeStoreUseFireInstruction( env: Environment, useFireCallResultPlace: Place, fireFunctionBindingPlace: Place, ): Instruction { promoteTemporary(fireFunctionBindingPlace.identifier); const fireFunctionBindingLValuePlace = createTemporaryPlace( env, GeneratedSource, ); return { id: makeInstructionId(0), value: { kind: 'StoreLocal', lvalue: { kind: InstructionKind.Const, place: {...fireFunctionBindingPlace}, }, value: {...useFireCallResultPlace}, type: null, loc: GeneratedSource, }, lvalue: fireFunctionBindingLValuePlace, loc: GeneratedSource, effects: null, }; } type FireCalleesToFireFunctionBinding = Map< IdentifierId, { fireFunctionBinding: Place; capturedCalleeIdentifier: Identifier; fireLoc: SourceLocation; } >; class Context { #env: Environment; #errors: CompilerError = new CompilerError(); /* * Used to look up the call expression passed to a `fire(callExpr())`. Gives back * the `callExpr()`. */ #callExpressions = new Map<IdentifierId, CallExpression>(); /* * We keep track of function expressions so that we can traverse them when * we encounter a lambda passed to a useEffect call */ #functionExpressions = new Map<IdentifierId, FunctionExpression>(); /* * Mapping from lvalue ids to the LoadLocal for it. Allows us to replace dependency LoadLocals. */ #loadLocals = new Map<IdentifierId, LoadLocal>(); /* * Maps all of the fire callees found in a component/hook to the generated fire function places * we create for them. Allows us to reuse already-inserted useFire results */ #fireCalleesToFireFunctions: Map<IdentifierId, Place> = new Map(); /* * The callees for which we have already created fire bindings. Used to skip inserting a new * useFire call for a fire callee if one has already been created. */ #calleesWithInsertedFire = new Set<IdentifierId>(); /* * A mapping from fire callees to the created fire function bindings that are reachable from this * scope. * * We additionally keep track of the captured callee identifier so that we can properly reference * it in the place where we LoadLocal the callee as an argument to useFire. */ #capturedCalleeIdentifierIds: FireCalleesToFireFunctionBinding = new Map(); /* * We only transform fire calls if we're syntactically within a useEffect lambda (for now) */ #inUseEffectLambda = false; /* * Mapping from useEffect callee identifier ids to the instruction id of the * load global instruction for the useEffect call. We use this to insert the * useFire calls before the useEffect call */ #loadGlobalInstructionIds = new Map<IdentifierId, InstructionId>(); constructor(env: Environment) { this.#env = env; } /* * We keep track of array expressions so we can rewrite dependency arrays passed to useEffect * to use the fire functions */ #arrayExpressions = new Map<IdentifierId, ArrayExpression>(); pushError(error: CompilerErrorDetailOptions): void { this.#errors.push(error); } withFunctionScope(fn: () => void): FireCalleesToFireFunctionBinding { fn(); return this.#capturedCalleeIdentifierIds; } withUseEffectLambdaScope(fn: () => void): FireCalleesToFireFunctionBinding { const capturedCalleeIdentifierIds = this.#capturedCalleeIdentifierIds; const inUseEffectLambda = this.#inUseEffectLambda; this.#capturedCalleeIdentifierIds = new Map(); this.#inUseEffectLambda = true; const resultCapturedCalleeIdentifierIds = this.withFunctionScope(fn); this.#capturedCalleeIdentifierIds = capturedCalleeIdentifierIds; this.#inUseEffectLambda = inUseEffectLambda; return resultCapturedCalleeIdentifierIds; } addCallExpression(id: IdentifierId, callExpr: CallExpression): void { this.#callExpressions.set(id, callExpr); } getCallExpression(id: IdentifierId): CallExpression | undefined { return this.#callExpressions.get(id); } addLoadLocalInstr(id: IdentifierId, loadLocal: LoadLocal): void { this.#loadLocals.set(id, loadLocal); } getLoadLocalInstr(id: IdentifierId): LoadLocal | undefined { return this.#loadLocals.get(id); } getOrGenerateFireFunctionBinding( callee: Place, fireLoc: SourceLocation, ): Place { const fireFunctionBinding = getOrInsertWith( this.#fireCalleesToFireFunctions, callee.identifier.id, () => createTemporaryPlace(this.#env, GeneratedSource), ); fireFunctionBinding.identifier.type = { kind: 'Function', shapeId: BuiltInFireFunctionId, return: {kind: 'Poly'}, isConstructor: false, }; this.#capturedCalleeIdentifierIds.set(callee.identifier.id, { fireFunctionBinding, capturedCalleeIdentifier: callee.identifier, fireLoc, }); return fireFunctionBinding; } mergeCalleesFromInnerScope( innerCallees: FireCalleesToFireFunctionBinding, ): void { for (const [id, calleeInfo] of innerCallees.entries()) { this.#capturedCalleeIdentifierIds.set(id, calleeInfo); } } addCalleeWithInsertedFire(id: IdentifierId): void { this.#calleesWithInsertedFire.add(id); } hasCalleeWithInsertedFire(id: IdentifierId): boolean { return this.#calleesWithInsertedFire.has(id); } inUseEffectLambda(): boolean { return this.#inUseEffectLambda; } addFunctionExpression(id: IdentifierId, fn: FunctionExpression): void { this.#functionExpressions.set(id, fn); } getFunctionExpression(id: IdentifierId): FunctionExpression | undefined { return this.#functionExpressions.get(id); } addLoadGlobalInstrId(id: IdentifierId, instrId: InstructionId): void { this.#loadGlobalInstructionIds.set(id, instrId); } getLoadGlobalInstrId(id: IdentifierId): InstructionId | undefined { return this.#loadGlobalInstructionIds.get(id); } addArrayExpression(id: IdentifierId, array: ArrayExpression): void { this.#arrayExpressions.set(id, array); } getArrayExpression(id: IdentifierId): ArrayExpression | undefined { return this.#arrayExpressions.get(id); } hasErrors(): boolean { return this.#errors.hasAnyErrors(); } throwIfErrorsFound(): void { if (this.hasErrors()) throw this.#errors; } } function deleteInstructions( deleteInstrs: Set<InstructionId>, instructions: Array<Instruction>, ): Array<Instruction> { if (deleteInstrs.size > 0) { const newInstrs = instructions.filter(instr => !deleteInstrs.has(instr.id)); return newInstrs; } return instructions; } function rewriteInstructions( rewriteInstrs: Map<InstructionId, Array<Instruction>>, instructions: Array<Instruction>, ): Array<Instruction> { if (rewriteInstrs.size > 0) { const newInstrs = []; for (const instr of instructions) { const newInstrsAtId = rewriteInstrs.get(instr.id); if (newInstrsAtId != null) { newInstrs.push(...newInstrsAtId, instr); } else { newInstrs.push(instr); } } return newInstrs; } return instructions; }