Files
oxc-project__oxc/crates/oxc_react_compiler/tests/snapshots/gating-with-function-outlining.snap
Boshen 78bf14f4f8 fix(react_compiler): insert outlined functions as siblings at any nesting depth (#25548)
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)
2026-08-12 14:31:36 +00:00

29 lines
689 B
Plaintext

---
source: crates/oxc_react_compiler/tests/snapshot.rs
input_file: crates/oxc_react_compiler/fixtures/gating-with-function-outlining.js
---
import { c as _c } from "react/compiler-runtime";
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag";
// @gating
import { useEffect } from "react";
const Component = isForgetEnabled_Fixtures() ? function Component() {
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 Component() {
useEffect(() => {
console.log("mounted");
}, []);
return null;
};
function _temp() {
console.log("mounted");
}