Skip to content

[compiler] Fix gating export misplaced on _unoptimized variant with TypeScript overload signatures#696

Open
everettbu wants to merge 2 commits intomainfrom
fix-35991
Open

[compiler] Fix gating export misplaced on _unoptimized variant with TypeScript overload signatures#696
everettbu wants to merge 2 commits intomainfrom
fix-35991

Conversation

@everettbu
Copy link
Copy Markdown

Mirror of facebook/react#36020
Original author: sleitor


Summary

When a function has TypeScript overload signatures (TSDeclareFunction nodes), the overload identifier names 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 <fn>_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, breaking the exported binding at runtime.

Before:

// Wrong: export lands on unoptimized variant
export function useSession_unoptimized<T>(...) { ... }
// Wrong: dispatcher has no export
function useSession(arg0) { if (gate()) ... }

After:

// Correct: export is on the dispatcher
export const useSession = isForgetEnabled_Fixtures()
  ? function useSession(selector) { /* optimized */ }
  : function useSession(selector) { /* original */ };

Fix

Skip TSDeclareFunction nodes in the top-level reference traversal (the same way TSTypeAliasDeclaration is already skipped). Overload signatures are not runtime references and should not influence the reference-before-declaration detection.

Fixes #35991

Test Plan

Added compiler fixture gating-ts-overload-export.tsx covering the exported function with TypeScript overload signatures + gating + annotation mode.

…ypeScript overload signatures

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 <fn>_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
@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 12, 2026

Greptile Summary

This PR fixes a bug where TypeScript overload signatures (TSDeclareFunction nodes) were incorrectly treated as runtime references by getFunctionReferencedBeforeDeclarationAtTopLevel, causing the gating logic to take the insertAdditionalFunctionDeclaration code path and misplace the export keyword onto the _unoptimized variant instead of the new dispatcher. The one-line fix — adding a TSDeclareFunction visitor that calls path.skip(), mirroring the existing TSTypeAliasDeclaration skip — is correct and minimal.

Key changes:

  • Program.ts: Skip TSDeclareFunction nodes during top-level reference traversal so that overload signature identifiers are never counted as runtime references-before-declaration.
  • gating-ts-overload-export.tsx / .expect.md: New golden-file test covering an exported hook with TypeScript overload signatures under gating + annotation mode.

Notable limitation in the generated output: The compiler currently preserves the TSDeclareFunction overload signatures in the output unchanged. Because the implementation is now emitted as export const useSession = ... rather than a function declaration, the overload signatures are orphaned, producing invalid TypeScript (TS2391). This is not a runtime regression (type-stripping tools strip the signatures before execution) but could surface as a type-checking error in downstream tsc passes over the transformed file. Removing or rewriting the preceding TSDeclareFunction siblings when converting an implementation to a const would be a natural follow-up to this fix.

Confidence Score: 4/5

  • Safe to merge — the core fix is correct and well-scoped; the remaining TypeScript-validity concern in the output is a pre-existing limitation, not a new runtime regression.
  • The Program.ts change is a minimal, correct addition consistent with the existing pattern for skipping type-only nodes. The test fixture confirms the export is now placed correctly. The only open issue is that the compiled output leaves orphaned TSDeclareFunction overload signatures before the const declaration, which is technically invalid TypeScript but does not affect runtime behavior. This is a pre-existing structural limitation of buildFunctionExpression in Gating.ts and is separate from the bug this PR fixes.
  • The golden-file gating-ts-overload-export.expect.md warrants attention for the orphaned overload signatures issue; Gating.ts (not modified in this PR) would need changes to fully resolve that follow-up.

Important Files Changed

Filename Overview
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts Adds a TSDeclareFunction visitor that calls path.skip() inside getFunctionReferencedBeforeDeclarationAtTopLevel, consistent with the existing TSTypeAliasDeclaration skip. This correctly prevents TypeScript overload signatures (which carry no runtime identity) from being misclassified as references-before-declaration and triggering the insertAdditionalFunctionDeclaration path. The fix is minimal, well-commented, and correct.
compiler/packages/babel-plugin-react-compiler/src/tests/fixtures/compiler/gating/gating-ts-overload-export.expect.md New golden-file fixture confirming the export lands correctly on the gated const useSession dispatcher. However, the output preserves the two TSDeclareFunction overload signatures directly before export const useSession = ..., which is invalid TypeScript (TS2391 — overloads must be immediately followed by a function declaration implementation). Additionally, the unoptimized branch at line 69 uses T without a <T> type parameter (pre-existing buildFunctionExpression limitation).
compiler/packages/babel-plugin-react-compiler/src/tests/fixtures/compiler/gating/gating-ts-overload-export.tsx New test input fixture covering the bug scenario: an exported TypeScript function with two overload signatures, gating enabled, and compilationMode:"annotation". Well-structured and representative of the real-world pattern.

Fix All in Claude Code Fix All in Codex

Prompt To Fix All With AI
This is a comment left during a code review.
Path: compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/gating-ts-overload-export.expect.md
Line: 46-49

Comment:
**Orphaned overload signatures produce invalid TypeScript output**

The compiled output retains the two `TSDeclareFunction` overload signatures (lines 46–47) but follows them with `export const useSession = ...` (line 49). TypeScript requires overload signatures to be **immediately followed by a function declaration implementation**; a `const` variable declaration does not satisfy this requirement and will produce:

> TS2391: Function implementation is missing or not immediately following the declaration.

This means the output file, while correct at runtime (type-stripping tools like `@babel/preset-typescript` or `tsc --noEmit false` will strip the signatures before execution), is technically invalid TypeScript and will fail IDE type-checking or a strict `tsc` pass over the transformed files.

A complete fix would require `insertGatedFunctionDeclaration` (or its call site in the compile loop) to detect any preceding `TSDeclareFunction` siblings and remove or rewrite them when the implementation is converted to a `const`. That said, this is a pre-existing limitation of `buildFunctionExpression` in `Gating.ts` and may be intentionally out of scope for this PR — worth tracking as a follow-up.

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: af8e139

Comment on lines +64 to +68
} else {
t0 = $[1];
}
return useStore(t0);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unoptimized variant is missing <T> type parameter

The unoptimized function expression in the expected output uses T in its parameter type (selector?: (session: Session) => T) without declaring <T> as a type parameter on the function expression itself. This would produce a TypeScript "Cannot find name 'T'" error if a downstream tsc type-check pass runs over this emitted code.

This is a pre-existing limitation of buildFunctionExpression in Gating.ts, which constructs the FunctionExpression node manually without copying the typeParameters field from the source FunctionDeclaration:

const fn: t.FunctionExpression = {
  type: 'FunctionExpression',
  async: node.async,
  generator: node.generator,
  loc: node.loc ?? null,
  id: node.id ?? null,
  params: node.params,  // type annotations referencing T are preserved…
  body: node.body,
  // typeParameters: node.typeParameters  ← missing
};

This PR correctly funnels generic TypeScript functions with overload signatures into the buildFunctionExpression path for the first time, making the gap more visible. Worth tracking as a follow-up: either copy typeParameters in buildFunctionExpression, or strip TypeScript annotations from the unoptimized branch before emitting.

Prompt To Fix With AI
This is a comment left during a code review.
Path: compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/gating-ts-overload-export.expect.md
Line: 64-68

Comment:
**Unoptimized variant is missing `<T>` type parameter**

The unoptimized function expression in the expected output uses `T` in its parameter type (`selector?: (session: Session) => T`) without declaring `<T>` as a type parameter on the function expression itself. This would produce a TypeScript "Cannot find name 'T'" error if a downstream `tsc` type-check pass runs over this emitted code.

This is a pre-existing limitation of `buildFunctionExpression` in `Gating.ts`, which constructs the `FunctionExpression` node manually without copying the `typeParameters` field from the source `FunctionDeclaration`:

```ts
const fn: t.FunctionExpression = {
  type: 'FunctionExpression',
  async: node.async,
  generator: node.generator,
  loc: node.loc ?? null,
  id: node.id ?? null,
  params: node.params,  // type annotations referencing T are preserved…
  body: node.body,
  // typeParameters: node.typeParameters  ← missing
};
```

This PR correctly funnels generic TypeScript functions with overload signatures into the `buildFunctionExpression` path for the first time, making the gap more visible. Worth tracking as a follow-up: either copy `typeParameters` in `buildFunctionExpression`, or strip TypeScript annotations from the unoptimized branch before emitting.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Codex

Comment on lines +46 to +49
export function useSession(): Session | null;
export function useSession<T>(selector: (session: Session) => T): T | null;
// Implementation
export const useSession = isForgetEnabled_Fixtures()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Orphaned overload signatures produce invalid TypeScript output

The compiled output retains the two TSDeclareFunction overload signatures (lines 46–47) but follows them with export const useSession = ... (line 49). TypeScript requires overload signatures to be immediately followed by a function declaration implementation; a const variable declaration does not satisfy this requirement and will produce:

TS2391: Function implementation is missing or not immediately following the declaration.

This means the output file, while correct at runtime (type-stripping tools like @babel/preset-typescript or tsc --noEmit false will strip the signatures before execution), is technically invalid TypeScript and will fail IDE type-checking or a strict tsc pass over the transformed files.

A complete fix would require insertGatedFunctionDeclaration (or its call site in the compile loop) to detect any preceding TSDeclareFunction siblings and remove or rewrite them when the implementation is converted to a const. That said, this is a pre-existing limitation of buildFunctionExpression in Gating.ts and may be intentionally out of scope for this PR — worth tracking as a follow-up.

Prompt To Fix With AI
This is a comment left during a code review.
Path: compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/gating-ts-overload-export.expect.md
Line: 46-49

Comment:
**Orphaned overload signatures produce invalid TypeScript output**

The compiled output retains the two `TSDeclareFunction` overload signatures (lines 46–47) but follows them with `export const useSession = ...` (line 49). TypeScript requires overload signatures to be **immediately followed by a function declaration implementation**; a `const` variable declaration does not satisfy this requirement and will produce:

> TS2391: Function implementation is missing or not immediately following the declaration.

This means the output file, while correct at runtime (type-stripping tools like `@babel/preset-typescript` or `tsc --noEmit false` will strip the signatures before execution), is technically invalid TypeScript and will fail IDE type-checking or a strict `tsc` pass over the transformed files.

A complete fix would require `insertGatedFunctionDeclaration` (or its call site in the compile loop) to detect any preceding `TSDeclareFunction` siblings and remove or rewrite them when the implementation is converted to a `const`. That said, this is a pre-existing limitation of `buildFunctionExpression` in `Gating.ts` and may be intentionally out of scope for this PR — worth tracking as a follow-up.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Codex

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants