Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/react_compiler/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1995,7 +1995,12 @@
E::Unary {
op: OpCode::UnDelete,
value: property_access_expr(obj, property, loc, None),
flags: E::UnaryFlags::empty(),
// The parser only sets this flag for `delete <ident|dot|index>`;
// lowering only creates PropertyDelete from `delete <EDot>`, so
// the source form always had it set. Without it the printer
// wraps the operand as `delete (0, obj.prop)`, which evaluates
// the property to a value and returns true without deleting.
flags: E::UnaryFlags::WAS_ORIGINALLY_DELETE_OF_IDENTIFIER_OR_PROPERTY_ACCESS,

Check failure on line 2003 in src/react_compiler/codegen.rs

View check run for this annotation

Claude / Claude Code Review

Sibling bug class unfixed: typeof <unbound> also loses UnaryFlags in react_compiler codegen

The immediate sibling of this fix is still broken: `InstructionValue::UnaryExpression` at codegen.rs:1855-1867 emits `flags: E::UnaryFlags::empty()`, so `typeof <unbound-ident>` round-trips without `WAS_ORIGINALLY_TYPEOF_IDENTIFIER` and the printer guard on the line directly above the one this PR cites (js_printer/lib.rs:4003) rewrites it to `typeof (0, window)` — which throws `ReferenceError` instead of returning `"undefined"`, breaking the canonical `typeof window !== "undefined"` SSR check in

Check warning on line 2003 in src/react_compiler/codegen.rs

View check run for this annotation

Claude / Claude Code Review

Comment premise is false; unconditional flag regresses delete-of-folded-conditional

The comment's premise — "the source form always had it set" — is not quite true: the react compiler runs on the *visited* body, and the visitor folds `delete (true ? o.a : o.b)` → `delete <EDot(o.a)>` with `flags: empty()` before `lower_unary` sees it (the `e_if` fold at `visit_expr.rs:1503` is gated only on `dead_code_elimination`, default true). With this change that input now prints `delete o.a` (actually deletes) instead of the spec-correct no-op `delete (0, o.a)`. The trigger is implausible
Comment thread
robobun marked this conversation as resolved.
Outdated
Comment thread
robobun marked this conversation as resolved.
Outdated
},
loc,
))
Expand Down Expand Up @@ -2055,7 +2060,9 @@
},
loc,
),
flags: E::UnaryFlags::empty(),
// See PropertyDelete above; lowering only creates
// ComputedDelete from `delete <EIndex>`.
Comment thread
robobun marked this conversation as resolved.
Outdated
flags: E::UnaryFlags::WAS_ORIGINALLY_DELETE_OF_IDENTIFIER_OR_PROPERTY_ACCESS,
},
loc,
))
Expand Down
40 changes: 40 additions & 0 deletions test/bundler/transpiler/react-compiler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,46 @@ describe("bundler", () => {
},
});

// Regression: codegen.rs PropertyDelete/ComputedDelete emitted `E::Unary` with
// `UnaryFlags::empty()`. The parser sets `WAS_ORIGINALLY_DELETE_OF_IDENTIFIER_OR_PROPERTY_ACCESS`
// for `delete <dot|index>`; the printer re-wraps any `delete <dot|index>`
// lacking that flag as `delete (0, obj.prop)`, which evaluates the property to
// a value and returns `true` without deleting anything.
itBundled("react-compiler/PropertyDeletePreservesReferenceSemantics", {
files: {
"/entry.jsx": /* jsx */ `
import { useMemo } from "react";
export function useThing(a, b) {
return useMemo(() => {
const x = { a, b, c: 3 };
delete x.b;
const key = "c";
delete x[key];
return x;
}, [a, b]);
}
console.log(JSON.stringify(useThing(1, 2)));
`,
"/node_modules/react/index.js": `exports.useMemo = (f) => f();`,
"/node_modules/react/compiler-runtime.js": `exports.c = n => new Array(n).fill(Symbol.for("react.memo_cache_sentinel"));`,
"/node_modules/react/package.json": `{"name":"react","main":"./index.js"}`,
},
reactCompiler: true,
target: "browser",
backend: "cli",
run: { stdout: '{"a":1}' },
onAfterBundle(api) {
const out = api.readFile("/out.js");
// The hook must be compiled (sanity: codegen, not a bailout, is on trial).
// With react bundled the `_c` import is renamed, so assert on the
// compiler-runtime body being linked in instead.
expect(out).toContain("react.memo_cache_sentinel");
// `delete (0, x.b)` / `delete (0, x[...])` evaluates to a value, not a
// Reference — must not appear for either the dot or index form.
expect(out).not.toMatch(/delete\s*\(\s*0\s*,/);
},
});

itBundled("react-compiler/NonComponentUntouched", {
files: {
"/entry.jsx": /* jsx */ `
Expand Down
Loading