diff --git a/src/react_compiler/hir/cfg_utils.rs b/src/react_compiler/hir/cfg_utils.rs
index ae7306280630..99127968cd59 100644
--- a/src/react_compiler/hir/cfg_utils.rs
+++ b/src/react_compiler/hir/cfg_utils.rs
@@ -221,12 +221,18 @@ pub fn remove_unnecessary_try_catch(hir: &mut HIR) {
}
// Clean up fallthrough predecessor info
- if let Some(fallthrough) = hir.blocks.get_mut(&fallthrough_id) {
+ if let Some(fallthrough) = hir.blocks.get(&fallthrough_id) {
if fallthrough.preds.len() == 1 && fallthrough.preds.contains(&handler_id) {
// The handler was the only predecessor: remove the fallthrough block
hir.blocks.shift_remove(&fallthrough_id);
} else {
- fallthrough.preds.shift_remove(&handler_id);
+ // Upstream removes only `handler_id`, but a longer handler ends in another block.
+ let is_gone = |pred: &&BlockId| !hir.blocks.contains_key(*pred);
+ let gone: Blocks = fallthrough.preds.iter().filter(is_gone).copied().collect();
+ let fallthrough = hir.blocks.get_mut(&fallthrough_id).unwrap();
+ for pred in &gone {
+ fallthrough.preds.shift_remove(pred);
+ }
}
}
}
diff --git a/test/bundler/transpiler/react-compiler.test.ts b/test/bundler/transpiler/react-compiler.test.ts
index 3eaa8fe94a1d..1e9156776a96 100644
--- a/test/bundler/transpiler/react-compiler.test.ts
+++ b/test/bundler/transpiler/react-compiler.test.ts
@@ -1931,6 +1931,191 @@ describe("bundler", () => {
},
});
+ // PruneMaybeThrows removes a catch handler when nothing in the try body can
+ // throw: here the body is a constant, or a check that `process.env.NODE_ENV`
+ // folds away. The `try` terminal then becomes a goto, and the block after the
+ // try statement survives only if it is merged into the try block. A catch
+ // body of more than one block (`if`, `?:`, `??`, `?.`) left a stale
+ // predecessor that blocked the merge: `function IfInCatch(props) { ; }`. In a
+ // loop body or an outer try body only the rest of that body was lost.
+ // babel-plugin-react-compiler 1.0.0 prints the same.
+ //
+ // The fake `c` records the size of every memo cache, so the second element of
+ // each pair is the `_c(n)` of the function, or `[]` for one that the compiler
+ // leaves as written. `SingleBlockCatch` and `CanThrow` are the controls.
+ // `AssignedInBranchingCatch` is skipped (its phi has an operand of a removed
+ // block). `DeadTryInDoWhile` loses the test of its do-while with the handler
+ // and is skipped too: a merge into that `break` would move `p.f()` into the
+ // `if`.
+ itBundled("react-compiler/TryBodyThatCannotThrowWithBranchingCatchKeepsTheRestOfTheFunction", {
+ files: {
+ "/entry.jsx": /* jsx */ `
+ import { sizes } from "react/compiler-runtime";
+
+ const validate = () => {};
+
+ function DevOnlyCheck(props) {
+ try {
+ const isDev = process.env.NODE_ENV !== "production";
+ if (isDev) validate(props);
+ } catch (e) {
+ props.onError?.(e);
+ }
+ return
{props.a}
;
+ }
+ function IfInCatch(props) {
+ try {
+ const limit = 3;
+ } catch (e) {
+ if (props.debug) console.warn(e);
+ }
+ return {props.a}
;
+ }
+ function TernaryInCatch(props) {
+ try {
+ const limit = 3;
+ } catch {
+ console.log(props.c ? 1 : 2);
+ }
+ return {props.a}
;
+ }
+ function NullishInCatch(p) {
+ try {
+ const limit = 3;
+ } catch {
+ const u0 = p.o ?? 1;
+ }
+ return {p.a}
;
+ }
+ function useOptionalInCatch(p) {
+ "use memo";
+ try {
+ const limit = 3;
+ } catch {
+ console.log(p.o?.v);
+ }
+ return [p.a];
+ }
+ function RestOfLoopBody(p) {
+ const out = [];
+ for (const it of p.list) {
+ try {
+ const limit = 3;
+ } catch {
+ if (p.c) console.log(1);
+ }
+ out.push(it);
+ }
+ return {out}
;
+ }
+ function RestOfOuterTryBody(p) {
+ try {
+ try {
+ const limit = 3;
+ } catch {
+ if (p.c) console.log(1);
+ }
+ p.f();
+ } catch {}
+ return {p.a}
;
+ }
+ function SingleBlockCatch(props) {
+ try {
+ const limit = 3;
+ } catch (e) {
+ console.warn(e);
+ }
+ return {props.a}
;
+ }
+ function CanThrow(props) {
+ try {
+ props.load();
+ } catch (e) {
+ if (props.debug) console.warn(e);
+ }
+ return {props.a}
;
+ }
+ function AssignedInBranchingCatch(props) {
+ let ok = true;
+ try {
+ const limit = 3;
+ } catch {
+ if (props.strict) ok = false;
+ }
+ return {ok ? "on" : "off"}
;
+ }
+ function DeadTryInDoWhile(p) {
+ try {
+ do {
+ if (p.a) break;
+ try {
+ const limit = 3;
+ return null;
+ } catch {}
+ break;
+ } while (p.c);
+ p.f();
+ } catch {}
+ return {p.z}
;
+ }
+
+ function render(fn, props) {
+ const before = sizes().length;
+ const result = fn(props);
+ return [result?.props?.children ?? result ?? "undefined", sizes().slice(before)];
+ }
+ let fCalls = 0;
+ const f = () => fCalls++;
+ console.log(JSON.stringify({
+ DevOnlyCheck: render(DevOnlyCheck, { a: 1 }),
+ IfInCatch: render(IfInCatch, { a: 2 }),
+ TernaryInCatch: render(TernaryInCatch, { a: 3 }),
+ NullishInCatch: render(NullishInCatch, { a: 4 }),
+ useOptionalInCatch: render(useOptionalInCatch, { a: 5 }),
+ RestOfLoopBody: render(RestOfLoopBody, { list: [1, 2] }),
+ RestOfOuterTryBody: render(RestOfOuterTryBody, { a: 6, f }),
+ SingleBlockCatch: render(SingleBlockCatch, { a: 7 }),
+ CanThrow: render(CanThrow, { a: 8, load: f }),
+ AssignedInBranchingCatch: render(AssignedInBranchingCatch, { strict: true }),
+ DeadTryInDoWhile: render(DeadTryInDoWhile, { a: true, z: 9, f }),
+ fCalls,
+ }));
+ `,
+ "/node_modules/react/package.json": `{"name":"react","main":"./index.js"}`,
+ "/node_modules/react/index.js": `exports.createElement = () => null;`,
+ "/node_modules/react/jsx-runtime.js": `exports.jsx = exports.jsxs = (type, props) => ({ type, props });`,
+ "/node_modules/react/jsx-dev-runtime.js": `exports.jsxDEV = (type, props) => ({ type, props });`,
+ "/node_modules/react/compiler-runtime.js": /* js */ `
+ const sizes = [];
+ exports.c = size => {
+ sizes.push(size);
+ return new Array(size).fill(Symbol.for("react.memo_cache_sentinel"));
+ };
+ exports.sizes = () => sizes;
+ `,
+ },
+ reactCompiler: true,
+ target: "browser",
+ backend: "cli",
+ define: { "process.env.NODE_ENV": '"production"' },
+ run: {
+ stdout: JSON.stringify({
+ DevOnlyCheck: [1, [2]],
+ IfInCatch: [2, [2]],
+ TernaryInCatch: [3, [2]],
+ NullishInCatch: [4, [2]],
+ useOptionalInCatch: [[5], [2]],
+ RestOfLoopBody: [[1, 2], [2]],
+ RestOfOuterTryBody: [6, [2]],
+ SingleBlockCatch: [7, [2]],
+ CanThrow: [8, [2]],
+ AssignedInBranchingCatch: ["on", []],
+ DeadTryInDoWhile: [9, []],
+ fCalls: 3,
+ }),
+ },
+ });
+
// A 0-arg call to an unknown import is non-reactive in InferReactivePlaces
// (no operand is reactive, callee isn't a hook), so its scope's deps prune
// to empty and it becomes a sentinel-only block. Babel does the same; this