mirror of
https://github.com/oxc-project/oxc.git
synced 2026-09-14 19:36:11 +08:00
78bf14f4f8
Closes #25536 ## Problem With `enableFunctionOutlining` (default on), the compiler outlines function expressions whose lowered **context** is empty. Context only counts variables captured from scopes inside the function being compiled — a free variable of the whole compiled function (e.g. a binding of an *enclosing* function) is modeled as a plain global load/store in HIR, so a callback referencing one still gets outlined. Correctness then depends entirely on where the outlined declaration is inserted: upstream uses Babel `originalFn.insertAfter(...)`, which splices into the statement list containing the original at **any nesting depth**, preserving the invariant that every name visible to the original is visible at its sibling position. `ox_insert_outlined_after` searched only `program.body`. For a component declared inside another function, the search missed and the fallback appended the outlined declaration at module scope: ```js export function makeProbe() { let mounts = 0; function MountCounter() { useEffect(() => { mounts += 1; }, []); return null; } return { MountCounter, read: () => mounts }; } ``` compiled to ```js export function makeProbe() { /* ... useEffect(_temp, t0) ... */ } function _temp() { mounts = mounts + 1; // ReferenceError: mounts is not defined } ``` with no diagnostic — a silent miscompilation that only fails when the effect fires. A second instance of the same shape: gating ran before outlined insertion. The gating rewrite replaces the declaration statement with a `const`, destroying the scope-id anchor, so gated components' outlined functions always fell to the end-of-program fallback (upstream inserts them during the compile-queue loop, before any replacement, so they land right after the gated `const`). ## Fix - `ox_insert_outlined_after` locates the declaring statement through a new `InsertOutlinedAfter` mode on the shared `OxcVisitor`, scanning each statement list before descending into it. Top-level originals (the common case) cost the same as before — the target is found in the first list scanned with no descent — and the walk prunes itself once insertion happens. Matching by `scope_id` cell stays sound because `ox_replace_function` mutates originals in place and discovery skips compiled bodies, so the anchor statement is always original AST. The top-level append survives only as a fallback for sloppy-mode annex-B positions (`if (c) function F() {}`). - Outlined insertion now runs before `ox_apply_gated_conditional`, restoring upstream's sequencing. Arrow/function-expression originals still append at module scope: upstream does the same (`program.pushContainer('body', ...)`) — including for nested originals, where upstream's own output breaks the same way — and the port reproduces upstream behavior rather than diverging (now documented in the placement comment). Placement in all three added fixtures (nested declaration = the issue repro, nested block, gating + outlining) is byte-identical to `babel-plugin-react-compiler` built from react @ 22e4f993c7 with matching options; the existing 1310 fixture snapshots are unchanged. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
36 lines
890 B
Plaintext
36 lines
890 B
Plaintext
---
|
|
source: crates/oxc_react_compiler/tests/snapshot.rs
|
|
input_file: crates/oxc_react_compiler/fixtures/outlined-function-in-nested-block.js
|
|
---
|
|
import { c as _c } from "react/compiler-runtime";
|
|
// @compilationMode:"infer"
|
|
import { useEffect } from "react";
|
|
// Like outlined-function-in-nested-declaration, but the compiled component sits
|
|
// in a block statement: the outlined callback is inserted into that block,
|
|
// mirroring Babel `insertAfter` on the enclosing statement list.
|
|
export function makeProbe(flag) {
|
|
let mounts = 0;
|
|
if (flag) {
|
|
function MountCounter() {
|
|
const $ = _c(1);
|
|
let t0;
|
|
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
|
t0 = [];
|
|
$[0] = t0;
|
|
} else {
|
|
t0 = $[0];
|
|
}
|
|
useEffect(_temp, t0);
|
|
return null;
|
|
}
|
|
function _temp() {
|
|
mounts = mounts + 1;
|
|
}
|
|
return {
|
|
MountCounter,
|
|
read: () => mounts
|
|
};
|
|
}
|
|
return null;
|
|
}
|