Repository navigation
Conversation
…ext of a nested function The visit pass declares the JSX runtime imports and the runtime helpers on `module_scope.generated` only, as `Kind::Other`. `resolve_identifier` loads them as globals, but `gather_captured_context` captured them as context of each nested function that has JSX. The context identifier took the name of the import, so the compiler renamed a later local of the component with that name (`jsx` to `jsx_0`, `Fragment` to `Fragment_0`). The callee and the `Fragment` tag of `<>` have the same position, and the sort compared positions only, so the order of the two entries came from the iteration order of a std HashMap. The sort key is now the position and the symbol index. `is_module_level_or_global` and `is_context_identifier` have the same checks without `generated`. No generated symbol reaches the first one (the JSX imports are `EImportIdentifier`), and the second one only answers for reassigned locals, so they stay as they are.
|
Warning Review paused — included plan limit reachedKeep your review moving with free on-demand reviews.
On-demand reviews are free for one more day.
Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing. Promotion and pricing detailsOn-demand reviews are free for one more day. After that, they cost $0.25 per reviewed file. Review limit detailsOr wait 6 minutes for your next included review. Limit details: You’ve used all 10 included reviews currently available. Review configuration: ⚙️ Run configurationConfiguration used: Repository: oven-sh/bun/.coderabbit.yaml Review profile: ASSERTIVE Plan: Essentials Run ID: 📒 Files selected for processing (2)
Comment |
|
Updated 2:01 PM PT - Sep 19th, 2026
✅ @robobun, your commit a9a52c77ff953eca80cd7d9aac9be0d011fbb70c passed in 🧪 To try this PR locally: bunx bun-pr 43528That installs a local version of the PR into your bun-43528 --bun |
There was a problem hiding this comment.
LGTM — a small, self-contained fix that brings gather_captured_context in line with the module_scope.generated check resolve_identifier already makes.
What was reviewed:
- Traced every writer of
module_scope.generatedin the parser (declare_generated_symbol, import namespace refs, CJS export shims,Host::new_generated/new_import_item, hoisting): all are module-level, so the new skip cannot drop a real captured local;Host::new_localanddeclare_temp_varappend to the function scope, not the module scope. - The
(pos, inner_index)sort key makes theHashMapcollection order total; no consumer depends on the previous tie order. - The four new
itBundledcases cover Dev/Prod × CJS/ESM runtime stubs; the CJS variants assert both thatCompwas compiled (memo cache read) and the exact unsuffixed local names, and all variants assert runtime stdout. The ESM variants pass with and without the fix, but they guard the bundler's renamer path rather than the compiler, which is a reasonable inclusion.
Extended reasoning...
Overview
The PR touches one function in the React Compiler HIR lowering (/home/claude/bun/src/react_compiler/lowering/build_hir/mod.rs, gather_captured_context) and the existing test file /home/claude/bun/test/bundler/transpiler/react-compiler.test.ts. The code change is two lines of logic: skip symbols that live on module_scope.generated when collecting the captured context of a nested function, and add the symbol index as a tiebreaker to the position sort so HashMap iteration order cannot leak into the result. The test change hoists the fake react fixture into a shared object, derives an ESM variant, and adds four itBundled cases.
Security risks
None. This is compile-time transpiler logic for an opt-in feature (reactCompiler); it does not parse untrusted network input, touch file system paths, or affect runtime security boundaries. The fixture rewrite (exports.X = to export const X =) is test-only.
Level of scrutiny
Moderate. The change is small, but the correctness question is whether module_scope.generated can ever contain a non-module-level binding that a nested function legitimately captures. I checked every append site in src/js_parser/p.rs, fold.rs, and react_compiler_host.rs: the ones that write to module_scope_mut().generated are all module-level constructs (JSX/runtime auto-imports via declare_generated_symbol, import namespace refs, CJS export symbols, Host::new_generated/new_import_item), while Host::new_local, declare_temp_var, generate_temp_ref, and hoisting write to current_scope/the nearest hoisting scope, which is the function scope when inside a function. So the skip is sound and mirrors the identical check already in resolve_identifier (hir_builder.rs:1030). The sort tiebreaker is pure determinism with no output consumer depending on the old tie order.
Other factors
No debug build was present in this checkout, so I did not execute the tests; the assessment rests on reading the diff and the parser/lowering sources. The new tests follow the existing file's conventions (itBundled, run.stdout, onAfterBundle), cover both jsx.development states and both module formats for the runtime stub, and the CJS variants assert exact local names rather than substring presence. The changed paths are not covered by CODEOWNERS, there are no prior review threads, and the bug hunt exited on dry_streak. The two ruled-out candidates (dropped real capture; ESM cases passing both ways) were re-examined and hold: the former is excluded by the writer audit above, and the latter is a variant-matrix inclusion guarding the bundler renamer rather than a vacuous assertion.
|
Status: ready for review. CI is running. How to reproduce on main // entry.jsx
export function Comp({ items }) {
function cb() {
return <>{items}</>;
}
const Fragment = items.length;
return <div onClick={cb} title={Fragment} />;
}bun build --react-compiler --target=browser --external '*' entry.jsx
Test bun bd test test/bundler/transpiler/react-compiler.test.ts -t JsxRuntimeImportIsNotContextOfANestedFunctionThe |
Problem
reactCompiler, a local named like a JSX runtime import gets a new name.const jsx = props.aafter a nested function with JSX prints asjsx_0(alsojsxs_0,jsxDEV_0,Fragment_0).gather_captured_context(src/react_compiler/lowering/build_hir/mod.rs:382). It captures the JSX runtime imports as context of each nested function with JSX. Each context identifier reserves its name.Fragmenttag of<>share a position. The function sorts a stdHashMapby position only, so the order of the two entries differs between processes. No printed output depends on it.Fix
gather_captured_contextskips a symbol onmodule_scope.generated, asresolve_identifierdoes. The output of the 1806 upstream fixtures is unchanged.Loc::EMPTY. Such a function is not compiled, so no test shows it.test/bundler/transpiler/react-compiler.test.ts(four new cases, two fail without the fix). Alsoreact-compiler-fixtures.test.tsandbundler_jsx.test.ts.Background
_0.<>a</>is the calljsxDEV(Fragment, {...}). Both identifiers have the position of the<.module_scope.generatedasKind::Other, not inmodule_scope.members.Notes
module_scope.generatedmakes a module-level symbol:declare_generated_symbol, import namespace and import item refs, CommonJS export symbols, module-level temporaries,Host::new_generated/new_import_item.hoist_symbolsonly appends to thegeneratedlist of a block scope. The compiler only takes module-level functions. So the new check cannot drop a real capture.var _s = $RefreshSig$()is a Todo (Handle var kinds in VariableDeclaration).Fragmenttag (lower_jsx_calldoes not make the callee an operand, and it turns theFragmenttag intoJsxFragment). Dead code elimination of the nested function then removes both context entries, before the passes that build scopes.EliminateRedundantPhifor the whole fixture corpus, two runs of one binary. One fixture differed,multiple-calls-to-hoisted-callback-from-other-callback.js. The context of the arrowbwas(jsxDEV, Fragment, onClick)in one run and(Fragment, jsxDEV, onClick)in the next, and the declaration ids followed.generatedcheck (bothFragment/jsxDEV), 0 with it. With--react-fast-refreshand the check, a component withconst Inner = () => { useA(); useB(); ... }(both hooks from props) logsuseA/useBat position -1.Bun.buildof all fixture inputs with the options ofreact-compiler-fixtures.test.ts. It gives 1585 outputs and 221 expected errors.diff -rof the outputs of the two builds is empty, also withminify: { syntax: true }.$jsx, so a suffix on a local can only be the_0of the compiler. With the ES module stub (or an externalreact) the import prints asjsx, and the local gets its number from the renamer of the bundler (jsx2), like a local of a function that the compiler does not touch.is_module_level_or_global(build_hir/expr.rs) has nogeneratedarm, but the JSX imports and runtime helpers areEImportIdentifier, which returns before it.is_context_identifieronly answers for reassigned locals.resolve_identifierneeds the finerModuleLocal/Globalanswer, so one shared helper does not fit all three.HashMap/HashSetinsrc/react_compiler/loweringfeeds an ordered result: the others are used for lookups, or collected into an unordered set.sort_unstable_by_keyline to the shared index sort. The comparator there needs the same second key.