Skip to content

fix(react_compiler): keep imports referenced only by a local re-export#23176

Merged
graphite-app[bot] merged 1 commit into
mainfrom
fix-react-compiler-dangling-reexport
Jun 9, 2026
Merged

fix(react_compiler): keep imports referenced only by a local re-export#23176
graphite-app[bot] merged 1 commit into
mainfrom
fix-react-compiler-dangling-reexport

Conversation

@Boshen

@Boshen Boshen commented Jun 9, 2026

Copy link
Copy Markdown
Member

Problem

When the React Compiler compiles a module, a named import that is only re-exported via a local export { … } clause was dropped from the output, leaving a dangling export { Foo } that references a removed binding — i.e. invalid code.

Minimal repro:

import { Foo } from "./foo";
export { Foo };
export function C() { const x = useState(0); return <div>{x}</div>; }

Before this change, the compiled output emits export { Foo } with no import { Foo }. With the React Compiler disabled the import is kept, so the bug is specific to the compiler's AST round-trip.

Root cause

oxc's parser rewrites a local export's specifier local from IdentifierName to IdentifierReference when there is no source (parse_export_named_specifiers), and semantic analysis only records a reference to the imported binding for an IdentifierReference (binder.rs). The React Compiler's reverse AST conversion (convert_ast_reverse) always rebuilt local as a plain IdentifierName, so no reference was created, and the downstream TypeScript import elision then removed the "unused" import — while leaving the export clause behind.

Fix

Mirror the parser: in convert_export_named_declaration, when the export has no source, build each specifier's local as an IdentifierReference. Re-exports (export { x } from "…") keep an IdentifierName, since there local names an export of the other module.

Verification

  • Added regression test local_reexport_keeps_its_import_binding; cargo test -p oxc_react_compiler passes (17/17), napi/transform passes (62/62).
  • Found by differential-testing the React Compiler against babel-plugin-react-compiler across the ecosystem-ci repos. Re-running the 90 affected files with the fix: the invalid-output bucket drops to 0 and 84/90 now match Babel exactly (the rest are pre-existing, benign output-equivalent differences).

Note

The upstream oxc-react-compiler repo carries an identical copy of this conversion code (crates/react_compiler_oxc/src/convert_ast_reverse.rs); the same change should be mirrored there to survive the next vendor sync.

🤖 Generated with Claude Code

@codspeed-hq

codspeed-hq Bot commented Jun 9, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 57 untouched benchmarks
⏩ 14 skipped benchmarks1


Comparing fix-react-compiler-dangling-reexport (2db6e10) with main (d7038de)

Open in CodSpeed

Footnotes

  1. 14 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@Boshen Boshen added the 0-merge Merge with Graphite Merge Queue label Jun 9, 2026

Boshen commented Jun 9, 2026

Copy link
Copy Markdown
Member Author

Merge activity

#23176)

## Problem

When the React Compiler compiles a module, a named import that is only re-exported via a local `export { … }` clause was dropped from the output, leaving a dangling `export { Foo }` that references a removed binding — i.e. **invalid code**.

Minimal repro:

```tsx
import { Foo } from "./foo";
export { Foo };
export function C() { const x = useState(0); return <div>{x}</div>; }
```

Before this change, the compiled output emits `export { Foo }` with **no** `import { Foo }`. With the React Compiler disabled the import is kept, so the bug is specific to the compiler's AST round-trip.

## Root cause

oxc's parser rewrites a *local* export's specifier `local` from `IdentifierName` to `IdentifierReference` when there is no `source` (`parse_export_named_specifiers`), and semantic analysis only records a reference to the imported binding for an `IdentifierReference` (`binder.rs`). The React Compiler's reverse AST conversion (`convert_ast_reverse`) always rebuilt `local` as a plain `IdentifierName`, so no reference was created, and the downstream TypeScript import elision then removed the "unused" import — while leaving the export clause behind.

## Fix

Mirror the parser: in `convert_export_named_declaration`, when the export has no `source`, build each specifier's `local` as an `IdentifierReference`. Re-exports (`export { x } from "…"`) keep an `IdentifierName`, since there `local` names an export of the other module.

## Verification

- Added regression test `local_reexport_keeps_its_import_binding`; `cargo test -p oxc_react_compiler` passes (17/17), `napi/transform` passes (62/62).
- Found by differential-testing the React Compiler against `babel-plugin-react-compiler` across the ecosystem-ci repos. Re-running the 90 affected files with the fix: the invalid-output bucket drops to **0** and 84/90 now match Babel exactly (the rest are pre-existing, benign output-equivalent differences).

## Note

The upstream `oxc-react-compiler` repo carries an identical copy of this conversion code (`crates/react_compiler_oxc/src/convert_ast_reverse.rs`); the same change should be mirrored there to survive the next vendor sync.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
@graphite-app graphite-app Bot force-pushed the fix-react-compiler-dangling-reexport branch from 2db6e10 to de38a3f Compare June 9, 2026 17:29
@graphite-app graphite-app Bot merged commit de38a3f into main Jun 9, 2026
29 checks passed
@graphite-app graphite-app Bot removed the 0-merge Merge with Graphite Merge Queue label Jun 9, 2026
@graphite-app graphite-app Bot deleted the fix-react-compiler-dangling-reexport branch June 9, 2026 17:34
Boshen added a commit that referenced this pull request Jun 15, 2026
### 💥 BREAKING CHANGES

- 7a24911 codegen: [**BREAKING**] Borrow sourcemaps from codegen
(#23422) (Boshen)
- bb0ed44 transformer: [**BREAKING**] Disable styled-components
transpileTemplateLiterals by default (#23171) (Boshen)

### 🚀 Features

- 1490a0a linter/react: Implement react-compiler rule (#23202) (Boshen)
- 6c0bdf0 transformer/react-refresh: Support `module.property.useHook()`
(#23190) (Dunqing)
- 47991bd semantic: Report TS1228 for invalid type predicates (#23174)
(camc314)
- 1d3af58 parser: Add TS2398 parameter property diagnostic (#23216)
(camc314)
- 44313da semantic: Add `scope_is_descendant_of` api (#22313) (camc314)
- e5050c0 parser: Improve diagnostic for rest initializer (#23205)
(camc314)
- ec266bb transformer: Run React Compiler as a feature-gated transform
pass (#23201) (Boshen)
- e7374fe parser: Report error for `const` modifier on interface type
parameter (#23173) (camc314)
- a7c1c9b parser: Report ambient definite variable assertions (#23165)
(camc314)
- d169fcd parser: Report invalid class definite assertions (#23164)
(camc314)
- 00244d8 parser: Report definite property initializer errors (#23160)
(camc314)

### 🐛 Bug Fixes

- 52d0c31 transformer: Replace ambient dot defines (#23231) (camc314)
- 2c28748 transformer/class: Parent generated constructors to class
scope (#23222) (camc314)
- 8edd234 parser: Report accessor definite assertion on token (#23203)
(camc314)
- de38a3f react_compiler: Keep imports referenced only by a local
re-export (#23176) (Boshen)
- f5721c2 codegen: Preserve parentheses around `intrinsic` type
reference (#23156) (Boshen)
- e89f81d parser: Don't emit TS1477 for parenthesized instantiation
expression (#23147) (Boshen)
- 8a04149 parser: Reject module-referencing imports/exports in a
namespace body (#22829) (Boshen)

### ⚡ Performance

- 2783295 parser: Table-driven operator precedence lookup (#23346)
(Boshen)
- 231d5de parser: Single-match member expression dispatch (#23347)
(Boshen)
- e89729b codegen: Accept one-shot wrap closures (#23265) (camc314)
- a6c11fa parser: Force-inline read_non_decimal to fold per-digit number
dispatch (#23157) (Boshen)
- d74964c parser: Store class definite assertion offset (#23170)
(camc314)
- f0fda4d parser: Shrink-wrap cold diagnostic tails out of hot frames
(#23159) (Boshen)
- a082180 parser: Store definite assertion offset (#23167) (camc314)
- 534f9c6 oxc: Conditionally rebuild semantic in compiler pipeline
(#23153) (Boshen)
- b435c6a parser: Skip checkpoint for `infer T extends U` constraint in
disallow context (#23128) (Boshen)
- 7464dce parser: Peek instead of checkpoint/rewind for `export default`
modifier (#23124) (Boshen)
- 80a9a32 parser: Fast-path single-keyword TS declarations (#23083)
(Boshen)
- da1a6c6 diagnostics: Migrate to allocation-optimized oxc-miette
(#23094) (Boshen)
- b7b08ce parser: Peek once for the static modifier disambiguation
(#23079) (Boshen)
- e7e07a3 parser: Fold unary dispatch into a single match (#23076)
(Boshen)

### 📚 Documentation

- d241add semantic: Add `AGENTS.md` test guidance for agents (#23441)
(camc314)
- 026f1ae parser: Add `AGENTS.md` test guidance for agents (#23440)
(camc314)
- 09755ac transformer: Add `AGENTS.md` test guidance for agents (#23439)
(camc314)
- e6bdfd4 lexer: Correct reference link for `byte_handlers!` (#23313)
(Dunqing)
- 65b6d7a allocator: Fix memory leaks in `Arena` examples (#23257)
(overlookmotel)

Co-authored-by: Boshen <1430279+Boshen@users.noreply.github.com>
camc314 pushed a commit that referenced this pull request Jul 3, 2026
#23176)

## Problem

When the React Compiler compiles a module, a named import that is only re-exported via a local `export { … }` clause was dropped from the output, leaving a dangling `export { Foo }` that references a removed binding — i.e. **invalid code**.

Minimal repro:

```tsx
import { Foo } from "./foo";
export { Foo };
export function C() { const x = useState(0); return <div>{x}</div>; }
```

Before this change, the compiled output emits `export { Foo }` with **no** `import { Foo }`. With the React Compiler disabled the import is kept, so the bug is specific to the compiler's AST round-trip.

## Root cause

oxc's parser rewrites a *local* export's specifier `local` from `IdentifierName` to `IdentifierReference` when there is no `source` (`parse_export_named_specifiers`), and semantic analysis only records a reference to the imported binding for an `IdentifierReference` (`binder.rs`). The React Compiler's reverse AST conversion (`convert_ast_reverse`) always rebuilt `local` as a plain `IdentifierName`, so no reference was created, and the downstream TypeScript import elision then removed the "unused" import — while leaving the export clause behind.

## Fix

Mirror the parser: in `convert_export_named_declaration`, when the export has no `source`, build each specifier's `local` as an `IdentifierReference`. Re-exports (`export { x } from "…"`) keep an `IdentifierName`, since there `local` names an export of the other module.

## Verification

- Added regression test `local_reexport_keeps_its_import_binding`; `cargo test -p oxc_react_compiler` passes (17/17), `napi/transform` passes (62/62).
- Found by differential-testing the React Compiler against `babel-plugin-react-compiler` across the ecosystem-ci repos. Re-running the 90 affected files with the fix: the invalid-output bucket drops to **0** and 84/90 now match Babel exactly (the rest are pre-existing, benign output-equivalent differences).

## Note

The upstream `oxc-react-compiler` repo carries an identical copy of this conversion code (`crates/react_compiler_oxc/src/convert_ast_reverse.rs`); the same change should be mirrored there to survive the next vendor sync.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
camc314 pushed a commit that referenced this pull request Jul 3, 2026
### 💥 BREAKING CHANGES

- 7a24911 codegen: [**BREAKING**] Borrow sourcemaps from codegen
(#23422) (Boshen)
- bb0ed44 transformer: [**BREAKING**] Disable styled-components
transpileTemplateLiterals by default (#23171) (Boshen)

### 🚀 Features

- 1490a0a linter/react: Implement react-compiler rule (#23202) (Boshen)
- 6c0bdf0 transformer/react-refresh: Support `module.property.useHook()`
(#23190) (Dunqing)
- 47991bd semantic: Report TS1228 for invalid type predicates (#23174)
(camc314)
- 1d3af58 parser: Add TS2398 parameter property diagnostic (#23216)
(camc314)
- 44313da semantic: Add `scope_is_descendant_of` api (#22313) (camc314)
- e5050c0 parser: Improve diagnostic for rest initializer (#23205)
(camc314)
- ec266bb transformer: Run React Compiler as a feature-gated transform
pass (#23201) (Boshen)
- e7374fe parser: Report error for `const` modifier on interface type
parameter (#23173) (camc314)
- a7c1c9b parser: Report ambient definite variable assertions (#23165)
(camc314)
- d169fcd parser: Report invalid class definite assertions (#23164)
(camc314)
- 00244d8 parser: Report definite property initializer errors (#23160)
(camc314)

### 🐛 Bug Fixes

- 52d0c31 transformer: Replace ambient dot defines (#23231) (camc314)
- 2c28748 transformer/class: Parent generated constructors to class
scope (#23222) (camc314)
- 8edd234 parser: Report accessor definite assertion on token (#23203)
(camc314)
- de38a3f react_compiler: Keep imports referenced only by a local
re-export (#23176) (Boshen)
- f5721c2 codegen: Preserve parentheses around `intrinsic` type
reference (#23156) (Boshen)
- e89f81d parser: Don't emit TS1477 for parenthesized instantiation
expression (#23147) (Boshen)
- 8a04149 parser: Reject module-referencing imports/exports in a
namespace body (#22829) (Boshen)

### ⚡ Performance

- 2783295 parser: Table-driven operator precedence lookup (#23346)
(Boshen)
- 231d5de parser: Single-match member expression dispatch (#23347)
(Boshen)
- e89729b codegen: Accept one-shot wrap closures (#23265) (camc314)
- a6c11fa parser: Force-inline read_non_decimal to fold per-digit number
dispatch (#23157) (Boshen)
- d74964c parser: Store class definite assertion offset (#23170)
(camc314)
- f0fda4d parser: Shrink-wrap cold diagnostic tails out of hot frames
(#23159) (Boshen)
- a082180 parser: Store definite assertion offset (#23167) (camc314)
- 534f9c6 oxc: Conditionally rebuild semantic in compiler pipeline
(#23153) (Boshen)
- b435c6a parser: Skip checkpoint for `infer T extends U` constraint in
disallow context (#23128) (Boshen)
- 7464dce parser: Peek instead of checkpoint/rewind for `export default`
modifier (#23124) (Boshen)
- 80a9a32 parser: Fast-path single-keyword TS declarations (#23083)
(Boshen)
- da1a6c6 diagnostics: Migrate to allocation-optimized oxc-miette
(#23094) (Boshen)
- b7b08ce parser: Peek once for the static modifier disambiguation
(#23079) (Boshen)
- e7e07a3 parser: Fold unary dispatch into a single match (#23076)
(Boshen)

### 📚 Documentation

- d241add semantic: Add `AGENTS.md` test guidance for agents (#23441)
(camc314)
- 026f1ae parser: Add `AGENTS.md` test guidance for agents (#23440)
(camc314)
- 09755ac transformer: Add `AGENTS.md` test guidance for agents (#23439)
(camc314)
- e6bdfd4 lexer: Correct reference link for `byte_handlers!` (#23313)
(Dunqing)
- 65b6d7a allocator: Fix memory leaks in `Arena` examples (#23257)
(overlookmotel)

Co-authored-by: Boshen <1430279+Boshen@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant