compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneHoistedContexts.ts TYPESCRIPT 171 lines View on github.com → Search inside
1/**2 * Copyright (c) Meta Platforms, Inc. and affiliates.3 *4 * This source code is licensed under the MIT license found in the5 * LICENSE file in the root directory of this source tree.6 */78import {CompilerError} from '..';9import {10  convertHoistedLValueKind,11  IdentifierId,12  InstructionId,13  InstructionKind,14  Place,15  ReactiveFunction,16  ReactiveInstruction,17  ReactiveScopeBlock,18  ReactiveStatement,19} from '../HIR';20import {empty, Stack} from '../Utils/Stack';21import {22  ReactiveFunctionTransform,23  Transformed,24  visitReactiveFunction,25} from './visitors';2627/*28 * Prunes DeclareContexts lowered for HoistedConsts, and transforms any references back to its29 * original instruction kind.30 *31 * Also detects and bails out on context variables which are:32 * - function declarations, which are hoisted by JS engines to the nearest block scope33 * - referenced before they are defined (i.e. having a `DeclareContext HoistedConst`)34 * - declared35 *36 * This is because React Compiler converts a `function foo()` function declaration to37 * 1. a `let foo;` declaration before reactive memo blocks38 * 2. a `foo = function foo() {}` assignment within the block39 *40 * This means references before the assignment are invalid (see fixture41 * error.todo-functiondecl-hoisting)42 */43export function pruneHoistedContexts(fn: ReactiveFunction): void {44  visitReactiveFunction(fn, new Visitor(), {45    activeScopes: empty(),46    uninitialized: new Map(),47  });48}4950type VisitorState = {51  activeScopes: Stack<Set<IdentifierId>>;52  uninitialized: Map<53    IdentifierId,54    | {55        kind: 'unknown-kind';56      }57    | {58        kind: 'func';59        definition: Place | null;60      }61  >;62};6364class Visitor extends ReactiveFunctionTransform<VisitorState> {65  override visitScope(scope: ReactiveScopeBlock, state: VisitorState): void {66    state.activeScopes = state.activeScopes.push(67      new Set(scope.scope.declarations.keys()),68    );69    /**70     * Add declared but not initialized / assigned variables. This may include71     * function declarations that escape the memo block.72     */73    for (const decl of scope.scope.declarations.values()) {74      state.uninitialized.set(decl.identifier.id, {kind: 'unknown-kind'});75    }76    this.traverseScope(scope, state);77    state.activeScopes.pop();78    for (const decl of scope.scope.declarations.values()) {79      state.uninitialized.delete(decl.identifier.id);80    }81  }82  override visitPlace(83    _id: InstructionId,84    place: Place,85    state: VisitorState,86  ): void {87    const maybeHoistedFn = state.uninitialized.get(place.identifier.id);88    if (89      maybeHoistedFn?.kind === 'func' &&90      maybeHoistedFn.definition !== place91    ) {92      CompilerError.throwTodo({93        reason: '[PruneHoistedContexts] Rewrite hoisted function references',94        loc: place.loc,95      });96    }97  }98  override transformInstruction(99    instruction: ReactiveInstruction,100    state: VisitorState,101  ): Transformed<ReactiveStatement> {102    /**103     * Remove hoisted declarations to preserve TDZ104     */105    if (instruction.value.kind === 'DeclareContext') {106      const maybeNonHoisted = convertHoistedLValueKind(107        instruction.value.lvalue.kind,108      );109      if (maybeNonHoisted != null) {110        if (111          maybeNonHoisted === InstructionKind.Function &&112          state.uninitialized.has(instruction.value.lvalue.place.identifier.id)113        ) {114          state.uninitialized.set(115            instruction.value.lvalue.place.identifier.id,116            {117              kind: 'func',118              definition: null,119            },120          );121        }122        return {kind: 'remove'};123      }124    }125    if (126      instruction.value.kind === 'StoreContext' &&127      instruction.value.lvalue.kind !== InstructionKind.Reassign128    ) {129      /**130       * Rewrite StoreContexts let/const that will be pre-declared in131       * codegen to reassignments.132       */133      const lvalueId = instruction.value.lvalue.place.identifier.id;134      const isDeclaredByScope = state.activeScopes.find(scope =>135        scope.has(lvalueId),136      );137      if (isDeclaredByScope) {138        if (139          instruction.value.lvalue.kind === InstructionKind.Let ||140          instruction.value.lvalue.kind === InstructionKind.Const141        ) {142          instruction.value.lvalue.kind = InstructionKind.Reassign;143        } else if (instruction.value.lvalue.kind === InstructionKind.Function) {144          const maybeHoistedFn = state.uninitialized.get(lvalueId);145          if (maybeHoistedFn != null) {146            CompilerError.invariant(maybeHoistedFn.kind === 'func', {147              reason: '[PruneHoistedContexts] Unexpected hoisted function',148              loc: instruction.loc,149            });150            maybeHoistedFn.definition = instruction.value.lvalue.place;151            /**152             * References to hoisted functions are now "safe" as variable assignments153             * have finished.154             */155            state.uninitialized.delete(lvalueId);156          }157        } else {158          CompilerError.throwTodo({159            reason: '[PruneHoistedContexts] Unexpected kind',160            description: `(${instruction.value.lvalue.kind})`,161            loc: instruction.loc,162          });163        }164      }165    }166167    this.visitInstruction(instruction, state);168    return {kind: 'keep'};169  }170}

Findings

✓ No findings reported for this file.

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.