From c8cb1031dfd12c54bfa3203cfe5327a2581e83e8 Mon Sep 17 00:00:00 2001 From: Cold Fry Date: Thu, 16 Apr 2026 12:35:52 +0000 Subject: [PATCH 1/2] fix Object.assign crashing when source is a non-table value `{ ...(cond && obj) }` short-circuits to `false` when `cond` is falsy. JS treats this as a no-op (Object.assign coerces primitives to wrapper objects with no own enumerable properties), but `__TS__ObjectAssign` was iterating every source unconditionally, so `pairs(false)` errored at runtime. Skip non-table sources, matching the spec for null/undefined/primitive args. --- src/lualib/ObjectAssign.ts | 1 + test/unit/builtins/object.spec.ts | 10 ++++++++++ test/unit/spread.spec.ts | 9 +++++++++ 3 files changed, 20 insertions(+) diff --git a/src/lualib/ObjectAssign.ts b/src/lualib/ObjectAssign.ts index a7f691433..f2e9529a8 100644 --- a/src/lualib/ObjectAssign.ts +++ b/src/lualib/ObjectAssign.ts @@ -2,6 +2,7 @@ export function __TS__ObjectAssign(this: void, target: T, ...sources: T[]): T { for (const i of $range(1, sources.length)) { const source = sources[i - 1]; + if (type(source) !== "table") continue; for (const key in source) { target[key] = source[key]; } diff --git a/test/unit/builtins/object.spec.ts b/test/unit/builtins/object.spec.ts index dfed73502..192f17d9f 100644 --- a/test/unit/builtins/object.spec.ts +++ b/test/unit/builtins/object.spec.ts @@ -10,6 +10,16 @@ test.each([ util.testExpression`Object.assign(${util.formatCode(initial)}, ${argsString})`.expectToMatchJsResult(); }); +test.each([ + "Object.assign({}, false)", + "Object.assign({}, null)", + "Object.assign({}, undefined)", + "Object.assign({}, null, undefined)", + "Object.assign({ a: 1 }, false, { b: 2 })", +])("Object.assign skips non-object sources (%p)", expression => { + util.testExpression(expression).expectToMatchJsResult(); +}); + test.each([{}, { abc: 3 }, { abc: 3, def: "xyz" }])("Object.entries (%p)", obj => { const testBuilder = util.testExpressionTemplate`Object.entries(${obj})`; // Need custom matcher because order is not guaranteed in neither JS nor Lua diff --git a/test/unit/spread.spec.ts b/test/unit/spread.spec.ts index 53898aae5..79262c185 100644 --- a/test/unit/spread.spec.ts +++ b/test/unit/spread.spec.ts @@ -119,6 +119,15 @@ describe("in object literal", () => { util.testExpression(expression).expectToMatchJsResult(); }); + test.each([ + "{ ...((false && { a: 1 }) as any) }", + "{ ...((true && { a: 1 }) as any) }", + "{ a: 1, ...((false && { b: 2 }) as any) }", + "{ ...(null as any), ...(undefined as any) }", + ])("of short-circuited operand (%p)", expression => { + util.testExpression(expression).expectToMatchJsResult(); + }); + test("of object reference", () => { util.testFunction` const object = { x: 0, y: 1 }; From 782f831946cf72104140a6dbe493e1b1825fdd97 Mon Sep 17 00:00:00 2001 From: Cold Fry Date: Thu, 16 Apr 2026 12:38:20 +0000 Subject: [PATCH 2/2] refactor __TS__ObjectAssign to emit cleaner Lua Invert the type guard so it nests the spread loop instead of using `continue`, which lowered to a `repeat ... until true` + `break` dance. Same behavior, the bundled helper now reads like the hand-written original. --- src/lualib/ObjectAssign.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/lualib/ObjectAssign.ts b/src/lualib/ObjectAssign.ts index f2e9529a8..7d0a3a9b1 100644 --- a/src/lualib/ObjectAssign.ts +++ b/src/lualib/ObjectAssign.ts @@ -2,9 +2,10 @@ export function __TS__ObjectAssign(this: void, target: T, ...sources: T[]): T { for (const i of $range(1, sources.length)) { const source = sources[i - 1]; - if (type(source) !== "table") continue; - for (const key in source) { - target[key] = source[key]; + if (type(source) === "table") { + for (const key in source) { + target[key] = source[key]; + } } }