From b06d5d04e69f29dd5face29b16609739d611ad81 Mon Sep 17 00:00:00 2001 From: Dmitrii Troitskii Date: Thu, 23 Apr 2026 10:40:49 +0000 Subject: [PATCH] [compiler] fix: use null instead of GeneratedSource Symbol for synthesized Babel AST node loc When codegenPlace() produced an identifier for a Place whose loc was GeneratedSource (the internal Symbol sentinel for "no source location"), it directly assigned that Symbol to identifier.loc via: identifier.loc = place.loc as any; Babel's Node.loc contract requires SourceLocation | null. A Symbol value violates the contract and causes v8.serialize / jest-worker IPC failures with "Symbol() could not be cloned" when the compiled AST is passed across worker boundaries. All other assignment sites in CodegenReactiveFunction.ts already guard against GeneratedSource (via the withLoc() helper or explicit checks), but this one site was unguarded. Fix: replace the raw assignment with a guarded form that uses null for synthesized nodes: identifier.loc = place.loc !== GeneratedSource ? (place.loc as t.SourceLocation) : null; Adds a compiler fixture that exercises the triggering pattern (useMemo + early return + destructuring hoisted across memo scopes). Fixes #36327 --- .../ReactiveScopes/CodegenReactiveFunction.ts | 7 +- ...rated-source-symbol-in-babel-loc.expect.md | 180 ++++++++++++++++++ ...ug-generated-source-symbol-in-babel-loc.js | 27 +++ 3 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-generated-source-symbol-in-babel-loc.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-generated-source-symbol-in-babel-loc.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts index 486773d5eb91..b85746965d2f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts @@ -2420,7 +2420,12 @@ function codegenPlace(cx: Context, place: Place): t.Expression | t.JSXText { loc: place.loc, }); const identifier = convertIdentifier(place.identifier); - identifier.loc = place.loc as any; + /* + * Guard against GeneratedSource (a Symbol) leaking into Babel AST nodes. + * Babel requires Node.loc to be SourceLocation | null, so synthesized nodes + * without real source positions must use null, not the internal sentinel. + */ + identifier.loc = place.loc !== GeneratedSource ? (place.loc as t.SourceLocation) : null; return identifier; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-generated-source-symbol-in-babel-loc.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-generated-source-symbol-in-babel-loc.expect.md new file mode 100644 index 000000000000..d2a3d3a946cb --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-generated-source-symbol-in-babel-loc.expect.md @@ -0,0 +1,180 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import {useMemo} from 'react'; + +function visualFor(state, getLabels) { + return {label: getLabels(state), tint: 'red', glyph: () => null}; +} + +// Repro: synthesized temporaries for destructured bindings from cross-scope +// hoisting would have their Babel AST identifier node's .loc set to the +// internal GeneratedSource Symbol sentinel instead of null, causing +// v8.serialize / jest-worker IPC failures. +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 ( + + ); +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; + +function visualFor(state, getLabels) { + const $ = _c(5); + let t0; + if ($[0] !== getLabels || $[1] !== state) { + t0 = getLabels(state); + $[0] = getLabels; + $[1] = state; + $[2] = t0; + } else { + t0 = $[2]; + } + let t1; + if ($[3] !== t0) { + t1 = { label: t0, tint: "red", glyph: _temp }; + $[3] = t0; + $[4] = t1; + } else { + t1 = $[4]; + } + return t1; +} + +// Repro: synthesized temporaries for destructured bindings from cross-scope +// hoisting would have their Babel AST identifier node's .loc set to the +// internal GeneratedSource Symbol sentinel instead of null, causing +// v8.serialize / jest-worker IPC failures. +function _temp() { + return null; +} +export function Example(t0) { + const $ = _c(27); + const { state, getLabels, colors, onTap } = t0; + let t1; + if ($[0] !== state) { + t1 = { state }; + $[0] = state; + $[1] = t1; + } else { + t1 = $[1]; + } + const session = t1; + if (session.state === "off") { + return null; + } + let t2; + if ($[2] !== onTap || $[3] !== session.state) { + t2 = () => onTap?.(session.state); + $[2] = onTap; + $[3] = session.state; + $[4] = t2; + } else { + t2 = $[4]; + } + const handleTap = t2; + let label; + let t3; + let t4; + let t5; + let t6; + if ( + $[5] !== colors.fg || + $[6] !== getLabels || + $[7] !== handleTap || + $[8] !== session.state + ) { + const { label: t7, tint, glyph } = visualFor(session.state, getLabels); + label = t7; + t4 = label; + t5 = handleTap; + t6 = { background: tint }; + t3 = session.state === "listening" ? ... : glyph(colors.fg); + $[5] = colors.fg; + $[6] = getLabels; + $[7] = handleTap; + $[8] = session.state; + $[9] = label; + $[10] = t3; + $[11] = t4; + $[12] = t5; + $[13] = t6; + } else { + label = $[9]; + t3 = $[10]; + t4 = $[11]; + t5 = $[12]; + t6 = $[13]; + } + let t7; + if ($[14] !== colors.fg) { + t7 = { color: colors.fg }; + $[14] = colors.fg; + $[15] = t7; + } else { + t7 = $[15]; + } + let t8; + if ($[16] !== label || $[17] !== t7) { + t8 = {label}; + $[16] = label; + $[17] = t7; + $[18] = t8; + } else { + t8 = $[18]; + } + let t9; + if ($[19] !== t3 || $[20] !== t8) { + t9 = ( + + {t3} + {t8} + + ); + $[19] = t3; + $[20] = t8; + $[21] = t9; + } else { + t9 = $[21]; + } + let t10; + if ($[22] !== t4 || $[23] !== t5 || $[24] !== t6 || $[25] !== t9) { + t10 = ( + + ); + $[22] = t4; + $[23] = t5; + $[24] = t6; + $[25] = t9; + $[26] = t10; + } else { + t10 = $[26]; + } + return t10; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-generated-source-symbol-in-babel-loc.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-generated-source-symbol-in-babel-loc.js new file mode 100644 index 000000000000..4b979d8bb766 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-generated-source-symbol-in-babel-loc.js @@ -0,0 +1,27 @@ +// @validatePreserveExistingMemoizationGuarantees +import {useMemo} from 'react'; + +function visualFor(state, getLabels) { + return {label: getLabels(state), tint: 'red', glyph: () => null}; +} + +// Repro: synthesized temporaries for destructured bindings from cross-scope +// hoisting would have their Babel AST identifier node's .loc set to the +// internal GeneratedSource Symbol sentinel instead of null, causing +// v8.serialize / jest-worker IPC failures. +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 ( + + ); +}