From 43407d482edb0a16e92b38970341f547bf34908a Mon Sep 17 00:00:00 2001 From: Dmitrii Troitskii Date: Thu, 12 Mar 2026 10:34:26 +0000 Subject: [PATCH 1/2] [compiler] Fix gating export misplaced on _unoptimized variant with TypeScript overload signatures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a function has TypeScript overload signatures (TSDeclareFunction nodes), the overload identifiers were treated as runtime references by Babel's isReferencedIdentifier(). This caused getFunctionReferencedBeforeDeclarationAtTopLevel to incorrectly mark the implementation as 'referenced before declaration', triggering the insertAdditionalFunctionDeclaration path in insertGatedFunctionDeclaration. In that path, the original function is renamed to _unoptimized in-place, keeping any parent ExportNamedDeclaration wrapper — so the export ended up on the wrong (unoptimized) function. The new dispatcher function was inserted without export, making the exported name wrong at runtime. Fix: skip TSDeclareFunction nodes in the top-level reference traversal, so overload signatures are not treated as runtime references, and the standard (non-referencedBeforeDeclaration) gating path is used instead, which correctly replaces the entire export with `export const useSession = gating() ? ... : ...`. Fixes #35991 Test plan: added compiler fixture gating-ts-overload-export.tsx --- .../src/Entrypoint/Program.ts | 7 ++ .../gating-ts-overload-export.expect.md | 86 +++++++++++++++++++ .../gating/gating-ts-overload-export.tsx | 26 ++++++ 3 files changed, 119 insertions(+) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/gating-ts-overload-export.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/gating-ts-overload-export.tsx diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts index 2880e9283c..0a9e219731 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts @@ -1258,6 +1258,13 @@ function getFunctionReferencedBeforeDeclarationAtTopLevel( TSTypeAliasDeclaration(path) { path.skip(); }, + TSDeclareFunction(path) { + // Skip TypeScript overload signatures (function declarations without a body) + // to avoid false positives in reference-before-declaration detection. + // TSDeclareFunction.id is treated as isReferencedIdentifier() by Babel, + // but these are not actual runtime references. + path.skip(); + }, Identifier(id) { const fn = fnNames.get(id.node.name); // We're not tracking this identifier. diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/gating-ts-overload-export.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/gating-ts-overload-export.expect.md new file mode 100644 index 0000000000..2396bb0fa1 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/gating-ts-overload-export.expect.md @@ -0,0 +1,86 @@ + +## Input + +```javascript +// @gating @compilationMode:"annotation" +import {useStore} from 'shared-runtime'; + +interface Session { + user: string; +} + +// Overload signatures +export function useSession(): Session | null; +export function useSession(selector: (session: Session) => T): T | null; +// Implementation +export function useSession( + selector?: (session: Session) => T, +): Session | T | null { + 'use forget'; + return useStore((s: {session: Session | null}) => { + const session = s.session; + if (!session) return null; + return selector ? selector(session) : session; + }); +} + +export const FIXTURE_ENTRYPOINT = { + fn: eval('useSession'), + params: [[]], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag"; // @gating @compilationMode:"annotation" +import { useStore } from "shared-runtime"; + +interface Session { + user: string; +} + +// Overload signatures +export function useSession(): Session | null; +export function useSession(selector: (session: Session) => T): T | null; +// Implementation +export const useSession = isForgetEnabled_Fixtures() + ? function useSession(selector) { + "use forget"; + const $ = _c(2); + let t0; + if ($[0] !== selector) { + t0 = (s) => { + const session = s.session; + if (!session) { + return null; + } + return selector ? selector(session) : session; + }; + $[0] = selector; + $[1] = t0; + } else { + t0 = $[1]; + } + return useStore(t0); + } + : function useSession(selector?: (session: Session) => T) { + "use forget"; + return useStore((s: { session: Session | null }) => { + const session = s.session; + if (!session) return null; + return selector ? selector(session) : session; + }); + }; + +export const FIXTURE_ENTRYPOINT = { + fn: eval("useSession"), + params: [[]], +}; + +``` + +### Eval output +(kind: exception) (0 , _sharedRuntime.useStore) is not a function \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/gating-ts-overload-export.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/gating-ts-overload-export.tsx new file mode 100644 index 0000000000..2464e049c3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/gating-ts-overload-export.tsx @@ -0,0 +1,26 @@ +// @gating @compilationMode:"annotation" +import {useStore} from 'shared-runtime'; + +interface Session { + user: string; +} + +// Overload signatures +export function useSession(): Session | null; +export function useSession(selector: (session: Session) => T): T | null; +// Implementation +export function useSession( + selector?: (session: Session) => T, +): Session | T | null { + 'use forget'; + return useStore((s: {session: Session | null}) => { + const session = s.session; + if (!session) return null; + return selector ? selector(session) : session; + }); +} + +export const FIXTURE_ENTRYPOINT = { + fn: eval('useSession'), + params: [[]], +}; From af8e13971f459f91e9b1d9da0f6949cc41a1d380 Mon Sep 17 00:00:00 2001 From: Dmitrii Troitskii Date: Thu, 12 Mar 2026 16:00:50 +0000 Subject: [PATCH 2/2] fix: convert consecutive line comments to block comment in Program.ts --- .../src/Entrypoint/Program.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts index 0a9e219731..9dc34d62ba 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts @@ -1259,10 +1259,12 @@ function getFunctionReferencedBeforeDeclarationAtTopLevel( path.skip(); }, TSDeclareFunction(path) { - // Skip TypeScript overload signatures (function declarations without a body) - // to avoid false positives in reference-before-declaration detection. - // TSDeclareFunction.id is treated as isReferencedIdentifier() by Babel, - // but these are not actual runtime references. + /* + * Skip TypeScript overload signatures (function declarations without a body) + * to avoid false positives in reference-before-declaration detection. + * TSDeclareFunction.id is treated as isReferencedIdentifier() by Babel, + * but these are not actual runtime references. + */ path.skip(); }, Identifier(id) {