Skip to content

[Compiler Bug]: AST contract violation: babel-plugin-react-compiler writes Symbol() into Node.loc, breaks downstream serialization (jest-worker / v8.serialize / IPC) #36327

@ambar

Description

@ambar

What kind of issue is this?

  • React Compiler core (the JS output is incorrect, or your app works incorrectly after optimization)
  • babel-plugin-react-compiler (build issue installing or using the Babel plugin)
  • eslint-plugin-react-hooks (build issue installing or using the eslint plugin)
  • react-compiler-healthcheck (build issue installing or using the healthcheck script)

Link to repro

https://github.com/ambar/react-compiler-symbol-loc-repro

Repro steps

babel-plugin-react-compiler uses an internal sentinel const GeneratedSource = Symbol() for "no source location" and writes it into the loc field of synthesized AST nodes. Babel defines Node.loc: SourceLocation | null — a Symbol value violates that contract. The sentinel leaks from the compiler output to any downstream AST consumer.
Minimal standalone repro (no React, no Metro, no bundler — just Babel + Node IPC):

git clone <repro url>          # or paste the 5 files inline (see below)
cd react-compiler-symbol-loc-repro
npm install
node repro.js

Expected output (abridged):

─── Part 1: AST contract violation ─────────────────────────
Found 1 AST node(s) with `loc: Symbol()` in the babel-plugin-react-compiler output.
  path=ast.program.body[5].body.body[15].consequent.body[1].expression.right
  type=Identifier  name=_t  loc=Symbol()

─── Part 2: V8 structured-clone fails on this AST ──────────
v8.serialize(ast) threw: Symbol() could not be cloned.

─── Part 3: child_process.send reproduces the original error ─
parent process.send threw synchronously: Symbol() could not be cloned.

The synthesized _t Identifier appears on the RHS of an AssignmentExpression (label = _t) that React Compiler generates when it rewrites the memoization scope for:

export function Example({state, getLabels, colors, onTap}) {
  const session = useMemo(() => ({state}), [state]);
  if (session.state === 'off') return null;

  const handleTap = () => onTap?.(session.state);
  const {label, tint, glyph} = visualFor(session.state, getLabels);

  return (
    <button aria-label={label} onClick={handleTap} style={{background: tint}}>
      <span>
        {session.state === 'listening' ? <em></em> : glyph(colors.fg)}
        <span style={{color: colors.fg}}>{label}</span>
      </span>
    </button>
  );
}

Required conditions (I narrowed it down):

  1. At least one hook call at the top so the component is actually compiled (otherwise the compiler bails out).
  2. An early return null after the hooks.
  3. const {label, ...} = someFn(...) destructure from a function call.
  4. The destructured names referenced from 2+ independent JSX memo scopes, forcing the compiler to hoist them into a shared reactive cache slot instead of keeping them scope-local.

The compiler then produces (abridged):

if ($[N] !== ...) {
  var _visualFor = visualFor(...),
      _t = _visualFor.label,      // _t is synthesized; no source location
      tint = _visualFor.tint,
      glyph = _visualFor.glyph;
  label = _t;                     // <-- _t Identifier has loc: Symbol()
  ...
}

How often does this bug happen?

Every time

What version of React are you using?

19.1.0

What version of React Compiler are you using?

babel-plugin-react-compiler

Metadata

Metadata

Assignees

No one assigned

    Labels

    Status: UnconfirmedA potential issue that we haven't yet confirmed as a bugType: Bug

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions