diff --git a/packages/codemode/interpreter-support.md b/packages/codemode/interpreter-support.md index 7cc781ed1bc..65d637e8db9 100644 --- a/packages/codemode/interpreter-support.md +++ b/packages/codemode/interpreter-support.md @@ -31,8 +31,8 @@ ultimate source of truth. - [x] `null`, `undefined`, booleans, finite and non-finite numbers, and strings. - [x] Array literals, including holes and spread from arrays, strings, Maps, Sets, URLSearchParams, custom synchronous iterators, and synchronous generators. -- [x] Object literals with shorthand, computed string/number keys, and spread from plain data objects; `null` and - `undefined` are no-ops, while arrays are rejected. +- [x] Object literals with shorthand, computed string/number keys, and spread following ToObject: data objects and + arrays copy own enumerable keys, strings copy index keys, and other values contribute nothing. - [x] Template literals with interpolation. - [x] Regular-expression literals. - [x] `NaN` and `Infinity` globals. @@ -70,7 +70,7 @@ ultimate source of truth. - [x] `for`, `while`, and `do...while`. - [x] `for...of` over arrays, strings, Maps, Sets, URLSearchParams, custom synchronous iterators, and confined synchronous generators. Abrupt completion invokes the iterator's optional `return()`. -- [x] `for...in` over own keys of plain objects, arrays, and tool references. +- [x] `for...in` over own keys of plain objects, arrays, strings, and tool references; other values iterate nothing. - [x] Unlabeled `break` and `continue`. - [x] `try`, `catch`, optional catch bindings, and `finally`. - [x] `throw` with arbitrary values. @@ -210,7 +210,9 @@ ultimate source of truth. primitive wrapper objects (`Object(1)`) are rejected explicitly. - [x] Computed property names and object spread. - [x] `Object.keys`, `Object.values`, `Object.entries`, `Object.hasOwn`, `Object.assign`, and `Object.fromEntries`, with - synchronous iterator support for `fromEntries`. + synchronous iterator support for `fromEntries`. Sources follow ToObject: strings enumerate by index, other + primitives and wrappers contribute nothing, and `null`/`undefined` throw. `Object.assign` accepts array + targets for index keys only; a primitive target is a `TypeError` rather than a boxed object. - [x] `Object.keys` over arrays and tool references. - [x] Object identity is preserved by in-CodeMode Object helpers. - [x] `__proto__`, `constructor`, and `prototype` are ordinary own data keys. `x.constructor` without an own key resolves diff --git a/packages/codemode/src/interpreter/references.ts b/packages/codemode/src/interpreter/references.ts index 5bdb4c88e96..318e5a70656 100644 --- a/packages/codemode/src/interpreter/references.ts +++ b/packages/codemode/src/interpreter/references.ts @@ -108,3 +108,12 @@ export const typeofValue = (value: unknown): string => { if (value instanceof ToolReference) return value.path.length > 0 ? "function" : "object" return typeof value } + +const MAX_ARRAY_LENGTH = 4_294_967_295 + +export const parseArrayIndex = (key: string | number): number | undefined => { + const property = String(key) + if (!/^(0|[1-9]\d*)$/.test(property)) return undefined + const index = Number(property) + return index < MAX_ARRAY_LENGTH ? index : undefined +} diff --git a/packages/codemode/src/interpreter/runtime.ts b/packages/codemode/src/interpreter/runtime.ts index 4a1e1dbb865..ea77fcc55dd 100644 --- a/packages/codemode/src/interpreter/runtime.ts +++ b/packages/codemode/src/interpreter/runtime.ts @@ -75,6 +75,7 @@ import { containsOpaqueReference, describeValue, isRuntimeReference, + parseArrayIndex, rejectCircularInsertion, typeofValue, } from "./references.js" @@ -85,18 +86,10 @@ import { numberMethods } from "../stdlib/number.js" import { constructRegExp, regexpMethods, regexpProperties } from "../stdlib/regexp.js" import { stringMethods } from "../stdlib/string.js" import { uriArgument, urlMethods, urlProperties, urlSearchParamsMethods, urlWritableProperties } from "../stdlib/url.js" +import { enumerableSource } from "../stdlib/object.js" import { coerceToNumber, coerceToString, compoundOperators, errorBrandName } from "../stdlib/value.js" import { Values } from "../values.js" -const MAX_ARRAY_LENGTH = 4_294_967_295 - -const parseArrayIndex = (key: string | number): number | undefined => { - const property = String(key) - if (!/^(0|[1-9]\d*)$/.test(property)) return undefined - const index = Number(property) - return index < MAX_ARRAY_LENGTH ? index : undefined -} - const calleeDescription = (callee: Expression | Super | undefined): string => { if (callee?.type === "Identifier") return callee.name if (callee?.type === "MemberExpression") { @@ -852,17 +845,11 @@ class Frame { throw new InterpreterRuntimeError(`${context} must be a function.`, node).as("TypeError") } - private enumerableKeys(value: unknown): Array | undefined { - if (value instanceof ToolReference) { - return [...this.runtime.toolKeys(value.path)] - } - if (Array.isArray(value)) { - return Object.keys(value) - } - if (value !== null && typeof value === "object" && !isRuntimeReference(value)) { - return Object.keys(value) - } - return undefined + // for...in over null/undefined iterates nothing, like JS. + private enumerableKeys(value: unknown, node: AstNode): Array { + if (value instanceof ToolReference) return [...this.runtime.toolKeys(value.path)] + if (value === null || value === undefined) return [] + return Object.keys(enumerableSource("for...in", value, node)) } private evaluateForInStatement( @@ -878,13 +865,7 @@ class Frame { if (declared?.lexical) self.predeclarePattern(declared.pattern, declared.mutable, left) const right = yield* self.evaluateExpression(node.right) - const keys = self.enumerableKeys(right) - if (keys === undefined) { - throw new InterpreterRuntimeError( - "for...in requires a plain object, array, or tools reference. Use for...of for arrays/strings/Maps/Sets, or Object.keys(value) for a key list.", - node, - ) - } + const keys = self.enumerableKeys(right, node.right) if (left.type !== "Identifier" && left.type !== "VariableDeclaration") { throw new InterpreterRuntimeError("Unsupported for...in binding.", left) @@ -1916,16 +1897,10 @@ class Frame { for (const property of node.properties) { if (property.type === "SpreadElement") { const spread = yield* self.evaluateExpression(property.argument) - if (spread === null || spread === undefined || Values.isValue(spread)) continue - if (typeof spread !== "object" || Array.isArray(spread) || isRuntimeReference(spread)) { - throw new InterpreterRuntimeError( - `Object spread requires a data object, received ${describeValue(spread)}.`, - property, - "InvalidDataValue", - ) - } - for (const [key, value] of Object.entries(spread)) objectValue[key] = value - copyIteratorSymbols(spread, objectValue) + if (spread === null || spread === undefined) continue + const from = enumerableSource("Object spread", spread, property) + for (const [key, value] of Object.entries(from)) objectValue[key] = value + if (typeof from === "object") copyIteratorSymbols(from, objectValue) continue } diff --git a/packages/codemode/src/stdlib/object.ts b/packages/codemode/src/stdlib/object.ts index 44da0c3be48..47fd029dfb0 100644 --- a/packages/codemode/src/stdlib/object.ts +++ b/packages/codemode/src/stdlib/object.ts @@ -5,6 +5,8 @@ import { type AstNode, AsyncIteratorSymbol, InterpreterRuntimeError, IteratorSym import { containsOpaqueReference, describeValue, + isRuntimeReference, + parseArrayIndex, rejectCircularInsertion, typeofValue, } from "../interpreter/references.js" @@ -14,28 +16,53 @@ import { Values } from "../values.js" import { groupBy } from "./collections.js" import { coerceToString } from "./value.js" -const requireObject = (name: string, input: unknown, node: AstNode): Record => { - if (Array.isArray(input)) return input as unknown as Record - if (Values.isValue(input)) return {} - const prototype = input === null || typeof input !== "object" ? undefined : Object.getPrototypeOf(input) - if (prototype !== null && prototype !== Object.prototype) { +// ToObject for enumeration. Strings return themselves: the host's Object.keys/entries/hasOwn index a +// primitive string directly. Numbers, booleans, wrappers, and functions have no own enumerable keys. +export const enumerableSource = (label: string, value: unknown, node: AstNode): Record => { + if (value === null || value === undefined) { + throw new InterpreterRuntimeError(`${label} cannot convert ${describeValue(value)} to an object.`, node).as( + "TypeError", + ) + } + if (value instanceof Values.Promise) { throw new InterpreterRuntimeError( - `Object.${name} expects a data object or array, received ${describeValue(input)}.`, + `${label} received an un-awaited Promise; await it before inspecting the result.`, node, "InvalidDataValue", ) } - return input as Record + if (value instanceof ToolReference) { + throw new InterpreterRuntimeError( + `${label} cannot read tool references: they are not plain data. Use Object.keys(tools) for names, or search({ query }) for signatures.`, + node, + "InvalidDataValue", + ) + } + if (typeof value === "string") return value as unknown as Record + if (typeof value !== "object" || Values.isValue(value) || isRuntimeReference(value)) return {} + return value as Record } export const objectAssign = (args: Array, node: AstNode): unknown => { const target = args[0] - if (target === null || typeof target !== "object" || Array.isArray(target) || Values.isValue(target)) { - throw new InterpreterRuntimeError("Object.assign expects a data object target.", node) + // JS would box a primitive target; wrappers and primitives cannot hold fields here. + if (target === null || typeof target !== "object" || Values.isValue(target) || isRuntimeReference(target)) { + throw new InterpreterRuntimeError( + `Object.assign expects a data object or array target, received ${describeValue(target)}.`, + node, + ).as("TypeError") } const out = target as Record const seen = new Set() const guardedSet = (key: PropertyKey, item: unknown): void => { + // Arrays hold only indexed elements, as with direct assignment; Reflect.set would otherwise + // reach Array's length and Object.prototype's __proto__ setter. + if (Array.isArray(out) && (typeof key === "symbol" || parseArrayIndex(key) === undefined)) { + throw new InterpreterRuntimeError( + `Object.assign cannot assign '${String(key)}' to an array: only array indexes may be assigned.`, + node, + ).as("TypeError") + } rejectCircularInsertion(out, item, "Object.assign result", node, seen) if (!Reflect.set(out, key, item)) throw new InterpreterRuntimeError(`Object.assign could not assign property '${String(key)}'.`, node).as( @@ -43,18 +70,15 @@ export const objectAssign = (args: Array, node: AstNode): unknown => { ) } for (const source of args.slice(1)) { - if (source === null || source === undefined || Values.isValue(source)) continue - if (typeof source !== "object" || Array.isArray(source)) { - throw new InterpreterRuntimeError("Object.assign expects data objects.", node) + if (source === null || source === undefined) continue + const from = enumerableSource("Object.assign(...)", source, node) + if (typeof from !== "object") { + for (const [key, item] of Object.entries(from)) guardedSet(key, item) + continue } - for (const key of Reflect.ownKeys(source)) { - if (typeof key === "string") { - if (Object.prototype.propertyIsEnumerable.call(source, key)) guardedSet(key, Reflect.get(source, key)) - continue - } - if (key !== AsyncIteratorSymbol && key !== IteratorSymbol) continue - if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue - guardedSet(key, Reflect.get(source, key)) + for (const key of Reflect.ownKeys(from)) { + if (typeof key === "symbol" && key !== AsyncIteratorSymbol && key !== IteratorSymbol) continue + if (Object.prototype.propertyIsEnumerable.call(from, key)) guardedSet(key, Reflect.get(from, key)) } } return out @@ -121,10 +145,7 @@ const rejectTools = (name: string, args: Array, node: AstNode): void => } const objectStatic = (name: string, impl: (args: Array, node: AstNode) => unknown) => - sync(`Object.${name}`, (args, node) => { - rejectTools(name, args, node) - return impl(args, node) - }) + sync(`Object.${name}`, impl) // Object constructs identically with or without new, like JS. Only `keys` copies its result into the // program; `values`, `entries`, `assign`, and `fromEntries` hand back the program's own values. @@ -139,17 +160,19 @@ export const objectGlobal = (runner: Runner, toolKeys: (path: ReadonlyArra toProgram( args[0] instanceof ToolReference ? [...toolKeys(args[0].path)] - : Object.keys(requireObject("keys", args[0], node)), + : Object.keys(enumerableSource("Object.keys(...)", args[0], node)), "Object.keys result", ), ), - values: objectStatic("values", (args, node) => Object.values(requireObject("values", args[0], node))), + values: objectStatic("values", (args, node) => + Object.values(enumerableSource("Object.values(...)", args[0], node)), + ), entries: objectStatic("entries", (args, node) => - Object.entries(requireObject("entries", args[0], node)).map(([key, item]) => [key, item]), + Object.entries(enumerableSource("Object.entries(...)", args[0], node)).map(([key, item]) => [key, item]), ), hasOwn: objectStatic("hasOwn", (args, node) => Object.hasOwn( - requireObject("hasOwn", args[0], node), + enumerableSource("Object.hasOwn(...)", args[0], node), args[1] === AsyncIteratorSymbol || args[1] === IteratorSymbol ? args[1] : String(args[1]), ), ), diff --git a/packages/codemode/test/enumeration.test.ts b/packages/codemode/test/enumeration.test.ts index 3636bbef280..4e92b2d197b 100644 --- a/packages/codemode/test/enumeration.test.ts +++ b/packages/codemode/test/enumeration.test.ts @@ -85,18 +85,16 @@ describe("Object.keys over arrays", () => { expect(await value(`return Object.keys({ a: 1, b: 2 })`)).toEqual(["a", "b"]) }) - test("non-object inputs name what was received", async () => { - expect((await error(`return Object.keys("nope")`)).message).toContain( - "Object.keys expects a data object or array, received a string.", - ) - expect((await error(`return Object.entries(42)`)).message).toContain("received a number.") - expect((await error(`return Object.values(null)`)).message).toContain("received null.") + test("non-object inputs follow ToObject, and nullish inputs name what was received", async () => { + expect( + await value(`return [Object.keys("ab"), Object.entries(42), Object.keys(() => 1), Object.keys(true)]`), + ).toEqual([["0", "1"], [], [], []]) + expect(await value(`try { Object.values(null) } catch (e) { return [e.name, e.message] }`)).toEqual([ + "TypeError", + "Object.values(...) cannot convert null to an object.", + ]) expect((await error(`return Object.keys(tools.github.list_issues({ value: "x" }))`)).message).toContain( - "received an un-awaited Promise.", - ) - expect((await error(`return Object.entries(() => 1)`)).message).toContain("received a function.") - expect((await error(`return { ...[1] }`)).message).toContain( - "Object spread requires a data object, received an array.", + "received an un-awaited Promise", ) expect((await error(`const { a } = new Map(); return a`)).message).toContain("received a Map.") expect((await error(`return Array.from(7)`)).message).toContain("received a number.") @@ -161,11 +159,21 @@ describe("for...in", () => { ).toEqual(["github.list_issues", "github.get_issue", "memory.search", "playwright.navigate"]) }) - test("unsupported values fail with a hint at for...of and Object.keys", async () => { - for (const expression of [`"text"`, "new Map([[1, 2]])", "new Set([1])", "42", "null"]) { - const failure = await error(`for (const key in ${expression}) {}; return "no"`) - expect(failure.message).toContain("for...in requires a plain object, array, or tools reference") - expect(failure.message).toContain("Use for...of for arrays/strings/Maps/Sets, or Object.keys(value)") - } + test("non-object values enumerate like JS: strings by index, everything else nothing", async () => { + expect( + await value(` + const out = [] + for (const key in "ab") out.push(key) + for (const key in 42) out.push(key) + for (const key in null) out.push(key) + for (const key in undefined) out.push(key) + for (const key in new Map([[1, 2]])) out.push(key) + for (const key in Math) out.push(key) + return out + `), + ).toEqual(["0", "1"]) + expect((await error(`for (const key in tools.github.list_issues({ value: "x" })) {}`)).message).toContain( + "un-awaited Promise", + ) }) }) diff --git a/packages/codemode/test/object-coercion-test262.test.ts b/packages/codemode/test/object-coercion-test262.test.ts new file mode 100644 index 00000000000..e8713904c39 --- /dev/null +++ b/packages/codemode/test/object-coercion-test262.test.ts @@ -0,0 +1,217 @@ +/** + * Portions adapted from Test262 at revision 250f204f23a9249ff204be2baec29600faae7b75: + * - test/built-ins/Object/keys/15.2.3.14-1-1.js + * - test/built-ins/Object/keys/15.2.3.14-1-2.js + * - test/built-ins/Object/keys/15.2.3.14-1-3.js + * - test/built-ins/Object/keys/15.2.3.14-1-4.js + * - test/built-ins/Object/keys/15.2.3.14-1-5.js + * - test/built-ins/Object/entries/primitive-strings.js + * - test/built-ins/Object/entries/primitive-numbers.js + * - test/built-ins/Object/entries/primitive-booleans.js + * - test/built-ins/Object/values/primitive-strings.js + * - test/built-ins/Object/values/primitive-numbers.js + * - test/built-ins/Object/values/primitive-booleans.js + * - test/built-ins/Object/hasOwn/toobject_null.js + * - test/built-ins/Object/hasOwn/toobject_undefined.js + * - test/built-ins/Object/hasOwn/hasown_nonexistent.js + * - test/built-ins/Object/assign/Source-String.js + * - test/built-ins/Object/assign/Source-Null-Undefined.js + * - test/built-ins/Object/assign/target-Array.js + * - test/built-ins/Object/assign/Target-Null.js + * - test/built-ins/Object/assign/Target-Undefined.js + * - test/built-ins/Object/assign/Target-Object.js + * - test/built-ins/Object/assign/Override.js + * - test/built-ins/Object/assign/ObjectOverride-sameproperty.js + * + * Copyright (c) 2012 Ecma International. All rights reserved. + * Copyright (C) 2015 Jordan Harband. All rights reserved. + * Copyright 2015 Microsoft Corporation. All rights reserved. + * Copyright 2021 Jamie Kyle. All rights reserved. + * Test262 portions are governed by the BSD license in LICENSE.test262. + * + * Boxed-primitive cases (`Object.assign("a")`, `Object.assign(1, …)`) are omitted: CodeMode has no + * wrapper objects, so a primitive target is a TypeError rather than a boxed result. `Override.js` + * checks `Object.keys(result).length` instead of `Object.getOwnPropertyNames`. `target-Array.js` + * omits its named-key (`-0`, `1.5`, `4294967295`), `length`, and Proxy assertions: arrays here hold + * only indexed elements, so those keys are a TypeError (pinned below) rather than array properties. + */ +import { describe, expect, test } from "bun:test" +import { Effect } from "effect" +import { CodeMode } from "../src/index.js" + +const value = async (code: string) => { + const result = await Effect.runPromise(CodeMode.execute({ code, tools: {} })) + if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`) + return result.value +} + +const throwsTypeError = (expression: string) => + value(`try { ${expression}; return "no throw" } catch (error) { return error.name }`) + +describe("Object.keys Test262 parity", () => { + test("test/built-ins/Object/keys/15.2.3.14-1-{1,2,3}.js: primitives are coerced", async () => { + expect(await value(`return [Object.keys(0), Object.keys(true), Object.keys("abc")]`)).toEqual([ + [], + [], + ["0", "1", "2"], + ]) + }) + + test("test/built-ins/Object/keys/15.2.3.14-1-{4,5}.js: null and undefined throw TypeError", async () => { + expect(await throwsTypeError(`Object.keys(null)`)).toBe("TypeError") + expect(await throwsTypeError(`Object.keys(undefined)`)).toBe("TypeError") + }) +}) + +describe("Object.entries and Object.values Test262 parity", () => { + test("test/built-ins/Object/entries/primitive-strings.js", async () => { + expect( + await value(` + const result = Object.entries('abc') + return [Array.isArray(result), result.length, result[0][0], result[0][1], result[1][0], result[1][1], result[2][0], result[2][1]] + `), + ).toEqual([true, 3, "0", "a", "1", "b", "2", "c"]) + }) + + test("test/built-ins/Object/entries/primitive-numbers.js", async () => { + expect( + await value(` + return [0, -0, Infinity, -Infinity, NaN, Math.PI].map((number) => Object.entries(number).length) + `), + ).toEqual([0, 0, 0, 0, 0, 0]) + }) + + test("test/built-ins/Object/entries/primitive-booleans.js", async () => { + expect( + await value(` + const trueResult = Object.entries(true) + const falseResult = Object.entries(false) + return [Array.isArray(trueResult), trueResult.length, Array.isArray(falseResult), falseResult.length] + `), + ).toEqual([true, 0, true, 0]) + }) + + test("test/built-ins/Object/values/primitive-strings.js", async () => { + expect( + await value(` + const result = Object.values('abc') + return [Array.isArray(result), result.length, result[0], result[1], result[2]] + `), + ).toEqual([true, 3, "a", "b", "c"]) + }) + + test("test/built-ins/Object/values/primitive-numbers.js", async () => { + expect( + await value(` + return [0, -0, Infinity, -Infinity, NaN, Math.PI].map((number) => Object.values(number).length) + `), + ).toEqual([0, 0, 0, 0, 0, 0]) + }) + + test("test/built-ins/Object/values/primitive-booleans.js", async () => { + expect( + await value(` + const trueResult = Object.values(true) + const falseResult = Object.values(false) + return [Array.isArray(trueResult), trueResult.length, Array.isArray(falseResult), falseResult.length] + `), + ).toEqual([true, 0, true, 0]) + }) +}) + +describe("Object.hasOwn Test262 parity", () => { + test("test/built-ins/Object/hasOwn/toobject_{null,undefined}.js", async () => { + expect(await throwsTypeError(`Object.hasOwn(null, 'foo')`)).toBe("TypeError") + expect(await throwsTypeError(`Object.hasOwn(undefined, 'foo')`)).toBe("TypeError") + }) + + test("test/built-ins/Object/hasOwn/hasown_nonexistent.js", async () => { + expect(await value(`const o = {}; return Object.hasOwn(o, "foo")`)).toBe(false) + }) +}) + +describe("Object.assign Test262 parity", () => { + test("test/built-ins/Object/assign/Source-String.js", async () => { + expect( + await value(` + const target = new Object() + const result = Object.assign(target, "123") + return [result[0], result[1], result[2]] + `), + ).toEqual(["1", "2", "3"]) + }) + + test("test/built-ins/Object/assign/Source-Null-Undefined.js", async () => { + expect( + await value(` + const target = new Object() + const result = Object.assign(target, undefined, null) + return result === target + `), + ).toBe(true) + }) + + test("test/built-ins/Object/assign/target-Array.js", async () => { + expect( + await value(` + const target = [7, 8, 9] + let result = Object.assign(target, [1]) + const first = [result === target, [...result]] + const sparseArraySource = [] + sparseArraySource[2] = 3 + result = Object.assign(target, sparseArraySource) + const second = [result === target, [...result]] + result = Object.assign(target, { 4: 0 }) + return [...first, ...second, result === target, result.length, result[3] === undefined, result[4]] + `), + ).toEqual([true, [1, 8, 9], true, [1, 8, 3], true, 5, true, 0]) + }) + + test("array targets accept only array indexes (deviation from target-Array.js)", async () => { + expect( + await value(` + const target = [7] + const out = [] + for (const source of [{ length: 0 }, { x: 1 }, { "1.5": 1 }, { "-0": 1 }, { ["__proto__"]: null }]) { + try { Object.assign(target, source) } catch (error) { out.push(error.name) } + } + return [out, [...target], target.length, Object.keys(target)] + `), + ).toEqual([["TypeError", "TypeError", "TypeError", "TypeError", "TypeError"], [7], 1, ["0"]]) + }) + + test("test/built-ins/Object/assign/Target-{Null,Undefined}.js", async () => { + expect(await throwsTypeError(`Object.assign(null, { a: 1 })`)).toBe("TypeError") + expect(await throwsTypeError(`Object.assign(undefined, { a: 1 })`)).toBe("TypeError") + }) + + test("test/built-ins/Object/assign/Target-Object.js", async () => { + expect( + await value(` + const target = { foo: 1 } + const result = Object.assign(target, { a: 2 }) + return [result.foo, result.a] + `), + ).toEqual([1, 2]) + }) + + test("test/built-ins/Object/assign/Override.js", async () => { + expect( + await value(` + const target = { a: 1 } + const result = Object.assign(target, "1a2c3", { a: "c" }, undefined, { b: 6 }, null, 125, { a: 5 }) + return [Object.keys(result).length, result.a, result[0], result[1], result[2], result[3], result[4], result.b] + `), + ).toEqual([7, 5, "1", "a", "2", "c", "3", 6]) + }) + + test("test/built-ins/Object/assign/ObjectOverride-sameproperty.js", async () => { + expect( + await value(` + const target = { a: 1 } + const result = Object.assign(target, { a: 2 }, { a: "c" }) + return result.a + `), + ).toBe("c") + }) +}) diff --git a/packages/codemode/test/parity.test.ts b/packages/codemode/test/parity.test.ts index 9fc13debe04..4257f1e89e1 100644 --- a/packages/codemode/test/parity.test.ts +++ b/packages/codemode/test/parity.test.ts @@ -118,9 +118,12 @@ describe("H6: object spread of null/undefined is a no-op", () => { expect(await value(`const o = { a: 1 }; return { ...o, b: 2 }`)).toEqual({ a: 1, b: 2 }) }) - test("spreading an array into an object still errors", async () => { - const err = await error(`return { ...[1,2], a: 1 }`) - expect(err.kind).toBe("InvalidDataValue") + test("spreading an array or string into an object copies index keys, like JS", async () => { + expect(await value(`return { ...[1,2], a: 1 }`)).toEqual({ 0: 1, 1: 2, a: 1 }) + expect(await value(`return { ..."ab", ...5, ...true, ...(() => 1), ...new Map([[1, 2]]) }`)).toEqual({ + 0: "a", + 1: "b", + }) }) }) diff --git a/packages/codemode/test/stdlib.test.ts b/packages/codemode/test/stdlib.test.ts index f6af2b8a1cf..86f631e6129 100644 --- a/packages/codemode/test/stdlib.test.ts +++ b/packages/codemode/test/stdlib.test.ts @@ -1130,7 +1130,7 @@ describe("CodeMode values at intra-CodeMode checkpoints", () => { const diagnostic = await error(`return Object.keys(Promise.resolve({ a: 1 }))`) expect(diagnostic.kind).toBe("InvalidDataValue") expect(diagnostic.message).toContain("await") - expect((await error(`return Object.keys(Math)`)).kind).toBe("InvalidDataValue") + expect(await value(`return Object.keys(Math)`)).toEqual([]) }) test("Object.assign keeps Maps usable", async () => {