Skip to content

Commit 43407d4

Browse files
committed
[compiler] Fix gating export misplaced on _unoptimized variant with TypeScript 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
1 parent 7b5b561 commit 43407d4

3 files changed

Lines changed: 119 additions & 0 deletions

File tree

compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1258,6 +1258,13 @@ function getFunctionReferencedBeforeDeclarationAtTopLevel(
12581258
TSTypeAliasDeclaration(path) {
12591259
path.skip();
12601260
},
1261+
TSDeclareFunction(path) {
1262+
// Skip TypeScript overload signatures (function declarations without a body)
1263+
// to avoid false positives in reference-before-declaration detection.
1264+
// TSDeclareFunction.id is treated as isReferencedIdentifier() by Babel,
1265+
// but these are not actual runtime references.
1266+
path.skip();
1267+
},
12611268
Identifier(id) {
12621269
const fn = fnNames.get(id.node.name);
12631270
// We're not tracking this identifier.
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @gating @compilationMode:"annotation"
6+
import {useStore} from 'shared-runtime';
7+
8+
interface Session {
9+
user: string;
10+
}
11+
12+
// Overload signatures
13+
export function useSession(): Session | null;
14+
export function useSession<T>(selector: (session: Session) => T): T | null;
15+
// Implementation
16+
export function useSession<T>(
17+
selector?: (session: Session) => T,
18+
): Session | T | null {
19+
'use forget';
20+
return useStore((s: {session: Session | null}) => {
21+
const session = s.session;
22+
if (!session) return null;
23+
return selector ? selector(session) : session;
24+
});
25+
}
26+
27+
export const FIXTURE_ENTRYPOINT = {
28+
fn: eval('useSession'),
29+
params: [[]],
30+
};
31+
32+
```
33+
34+
## Code
35+
36+
```javascript
37+
import { c as _c } from "react/compiler-runtime";
38+
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag"; // @gating @compilationMode:"annotation"
39+
import { useStore } from "shared-runtime";
40+
41+
interface Session {
42+
user: string;
43+
}
44+
45+
// Overload signatures
46+
export function useSession(): Session | null;
47+
export function useSession<T>(selector: (session: Session) => T): T | null;
48+
// Implementation
49+
export const useSession = isForgetEnabled_Fixtures()
50+
? function useSession(selector) {
51+
"use forget";
52+
const $ = _c(2);
53+
let t0;
54+
if ($[0] !== selector) {
55+
t0 = (s) => {
56+
const session = s.session;
57+
if (!session) {
58+
return null;
59+
}
60+
return selector ? selector(session) : session;
61+
};
62+
$[0] = selector;
63+
$[1] = t0;
64+
} else {
65+
t0 = $[1];
66+
}
67+
return useStore(t0);
68+
}
69+
: function useSession(selector?: (session: Session) => T) {
70+
"use forget";
71+
return useStore((s: { session: Session | null }) => {
72+
const session = s.session;
73+
if (!session) return null;
74+
return selector ? selector(session) : session;
75+
});
76+
};
77+
78+
export const FIXTURE_ENTRYPOINT = {
79+
fn: eval("useSession"),
80+
params: [[]],
81+
};
82+
83+
```
84+
85+
### Eval output
86+
(kind: exception) (0 , _sharedRuntime.useStore) is not a function
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// @gating @compilationMode:"annotation"
2+
import {useStore} from 'shared-runtime';
3+
4+
interface Session {
5+
user: string;
6+
}
7+
8+
// Overload signatures
9+
export function useSession(): Session | null;
10+
export function useSession<T>(selector: (session: Session) => T): T | null;
11+
// Implementation
12+
export function useSession<T>(
13+
selector?: (session: Session) => T,
14+
): Session | T | null {
15+
'use forget';
16+
return useStore((s: {session: Session | null}) => {
17+
const session = s.session;
18+
if (!session) return null;
19+
return selector ? selector(session) : session;
20+
});
21+
}
22+
23+
export const FIXTURE_ENTRYPOINT = {
24+
fn: eval('useSession'),
25+
params: [[]],
26+
};

0 commit comments

Comments
 (0)