fix(codemode): destructure object patterns from primitives and convert Date components through ToPrimitive (#51239)

This commit is contained in:
Aiden Cline
2026-09-24 19:32:19 -05:00
committed by GitHub
parent e8b3e19e85
commit e23d89c9a9
6 changed files with 156 additions and 64 deletions
+10 -9
View File
@@ -85,8 +85,9 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [x] Array binding and assignment destructuring from strings, Maps, Sets, URLSearchParams, custom synchronous
iterators, and synchronous generators, including stepwise elisions/rest and `IteratorClose` on early completion
or binding/default failure.
- [ ] Object destructuring from primitives follows ToObject (`const { length } = "abc"`, `const {} = 1`); non-object
sources are rejected.
- [x] Object destructuring from primitives follows ToObject: `const { length } = "abc"` is `3`, `const { toFixed } = 1`
finds the built-in, `const {} = 1` is a no-op, and a rest element copies a string's indexes (`{ 1: "y", 2: "z" }`).
Only `null` and `undefined` sources throw (`Cannot destructure null as it is null.`).
- [x] Destructuring reads through the prototype chain like member access: `const { constructor } = error` and
`const { slice } = values` find the inherited built-in.
- [x] Any assignment target as a `for...in` head, like `for...of`: `for (x.y in obj)`, `for (a[i++] in obj)`, and
@@ -240,9 +241,9 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
throws: `{ valueOf() { return 7 } } * 2` is `14`, `` `${{ toString() { return "x" } }}` `` is `"x"`, and
`[1, 2]` with `arr.toString = () => "x"` makes `arr + ""` `"x"`. Dates keep their `Symbol.toPrimitive`
behavior (`date + 1` concatenates, `date - date` subtracts).
- [ ] ToPrimitive elsewhere: multi-argument `Date` construction, `Date.UTC`, `Error.prototype.toString` on an object
`message`, and numeric built-in arguments outside `Math` (`at`, `indexOf` start, `toFixed` digits) still use the
built-in form (`NaN`, `"[object Object]"`) and ignore own methods.
- [ ] ToPrimitive elsewhere: `Error.prototype.toString` on an object `message` and numeric built-in arguments outside
`Math` and `Date` (`at`, `indexOf` start, `toFixed` digits) still use the built-in form (`NaN`,
`"[object Object]"`) and ignore own methods.
- [x] Property keys follow ToPropertyKey: `x[null]`, `x[true]`, and objects (via their built-in string form) become
string keys.
@@ -460,15 +461,15 @@ reject }` object.
- [x] `getTimezoneOffset`, arithmetic, relational comparison, and `instanceof Date`.
- [x] Date values serialize to ISO strings; invalid dates serialize to `null`.
- [x] Local and UTC Date setters, including native argument coercion, mutation, rollover, invalid-Date recovery, and
`TimeClip` behavior.
`TimeClip` behavior. On an invalid Date every setter but `setTime` and `set(UTC)FullYear` answers `NaN` without
writing, so a time set inside an argument's `valueOf` survives.
- [x] `Date.prototype.toUTCString` and its `toGMTString` alias.
- [x] `toDateString` and `toTimeString` in the host's local timezone.
- [x] `toLocaleString`, `toLocaleDateString`, and `toLocaleTimeString` always format as `en-US` in UTC
(`"1/1/1970, 12:00:00 AM"`) so output does not depend on the host.
- [x] Native one-argument Date coercion for supported values, including booleans, null, arrays, and plain objects.
- [x] Date setters and one-argument construction coerce object arguments through their own `valueOf`/`toString` and
surface their throws.
- [ ] Multi-argument construction and `Date.UTC` coerce object arguments the same way (see ToPrimitive above).
- [x] Date setters, construction, and `Date.UTC` coerce object arguments through their own `valueOf`/`toString` in
argument order and surface their throws; only the first seven components are converted.
- [x] Native Date loose-equality and default primitive-coercion semantics, using CodeMode's deterministic ISO string
representation for the string primitive.
- [x] Native `RangeError` branding for invalid `toISOString()` calls.
@@ -1136,18 +1136,15 @@ class Frame<R> {
}
if (pattern.type === "ObjectPattern") {
if (!(value instanceof Obj)) {
throw typeError(
`Object destructuring requires a data object or array value, received ${describeValue(value)}.`,
pattern,
)
if (value === null || value === undefined) {
throw typeError(`Cannot destructure ${describeValue(value)} as it is ${value}.`, pattern)
}
const consumed = new Set<PropertyKey>()
for (const property of pattern.properties) {
if (property.type === "RestElement") {
const rest = new Obj(self.ctx.builtins.Object)
assign(rest, value, consumed)
assign(rest, enumerableSource(self.ctx, "Object destructuring", value, pattern), consumed)
yield* self.declarePattern(property.argument, rest, mutable, property, initialize)
continue
}
@@ -1156,7 +1153,7 @@ class Frame<R> {
consumed.add(typeof key === "symbol" ? key : String(key))
yield* self.declarePattern(
property.value,
self.readProperty(value, key, property),
self.destructuredProperty(value, key, property),
mutable,
property,
initialize,
@@ -1195,24 +1192,21 @@ class Frame<R> {
}
if (pattern.type === "ObjectPattern") {
if (!(value instanceof Obj)) {
throw invalidData(
`Object destructuring requires a data object or array value, received ${describeValue(value)}.`,
pattern,
)
if (value === null || value === undefined) {
throw typeError(`Cannot destructure ${describeValue(value)} as it is ${value}.`, pattern)
}
const consumed = new Set<PropertyKey>()
for (const property of pattern.properties) {
if (property.type === "RestElement") {
const rest = new Obj(self.ctx.builtins.Object)
assign(rest, value, consumed)
assign(rest, enumerableSource(self.ctx, "Object destructuring", value, pattern), consumed)
yield* self.assignPattern(property.argument, rest, property)
continue
}
const key = yield* self.destructuringPropertyKey(property)
consumed.add(typeof key === "symbol" ? key : String(key))
yield* self.assignPattern(property.value, self.readProperty(value, key, property), property)
yield* self.assignPattern(property.value, self.destructuredProperty(value, key, property), property)
}
return
}
@@ -2189,32 +2183,48 @@ class Frame<R> {
: propertyNode.type === "Identifier"
? propertyNode.name
: self.toPropertyKey(yield* self.evaluateExpression(propertyNode))
if (objectValue instanceof ToolReference) {
if (typeof key !== "string") {
throw typeError("Tool paths must use string property names.", propertyNode)
}
return new ToolReference([...objectValue.path, key])
}
if (objectValue instanceof Obj) return { target: objectValue, key, receiver: objectValue }
// Strings own length and indexes; every other primitive property reads through the wrapper prototype.
if (typeof objectValue === "string") {
if (key === "length") return { value: objectValue.length }
const index = typeof key === "symbol" ? undefined : parseArrayIndex(key)
if (index !== undefined) return { value: objectValue[index] }
}
const proto = primitivePrototype(self.ctx.builtins, objectValue)
if (proto !== undefined) return { target: proto, key, receiver: objectValue }
if (objectValue === null || objectValue === undefined) {
throw typeError(`Cannot read properties of ${objectValue} (reading '${String(key)}').`, objectNode)
}
throw typeError("Cannot access a property on a non-object value.", objectNode)
return self.resolveProperty(objectValue, key, objectNode, propertyNode)
})
}
private resolveProperty(
objectValue: Value,
key: PropertyKey,
objectNode: AstNode,
propertyNode: AstNode,
): MemberReference | ToolReference | { value: Value } {
if (objectValue instanceof ToolReference) {
if (typeof key !== "string") {
throw typeError("Tool paths must use string property names.", propertyNode)
}
return new ToolReference([...objectValue.path, key])
}
if (objectValue instanceof Obj) return { target: objectValue, key, receiver: objectValue }
// Strings own length and indexes; every other primitive property reads through the wrapper prototype.
if (typeof objectValue === "string") {
if (key === "length") return { value: objectValue.length }
const index = typeof key === "symbol" ? undefined : parseArrayIndex(key)
if (index !== undefined) return { value: objectValue[index] }
}
const proto = primitivePrototype(this.ctx.builtins, objectValue)
if (proto !== undefined) return { target: proto, key, receiver: objectValue }
if (objectValue === null || objectValue === undefined) {
throw typeError(`Cannot read properties of ${objectValue} (reading '${String(key)}').`, objectNode)
}
throw typeError("Cannot access a property on a non-object value.", objectNode)
}
// One destructured property, read the way a member expression would read it (primitives use their prototype).
private destructuredProperty(source: Value, key: PropertyKey, node: AstNode): Value {
const reference = this.resolveProperty(source, key, node, node)
if (reference instanceof ToolReference) return reference
if ("value" in reference) return reference.value
return this.readProperty(reference.target, reference.key, node, reference.receiver)
}
private readReference(reference: MemberReference, node: MemberExpression): Value {
// Reject unknown promise properties so a missing await cannot hide.
if (reference.target instanceof PromiseObj && !has(reference.target, reference.key)) {
+16 -3
View File
@@ -16,8 +16,11 @@ const constructDate = <R>(ctx: Interpreter<R>, args: Array<Value>, proto: Obj) =
: new DateObj(proto, new Date(coerceToNumber(value)).getTime()),
)
}
const parts = args.map((arg) => coerceToNumber(arg))
return Effect.succeed(new DateObj(proto, new Date(...(parts as [number, number])).getTime()))
// The spec converts at most seven components, in order, so extra arguments never run program code.
return Effect.map(
Effect.forEach(args.slice(0, 7), (arg) => toPrimitiveNumber(ctx, arg), { concurrency: 1 }),
(parts) => new DateObj(proto, new Date(...(parts as [number, number])).getTime()),
)
}
type Getter = keyof {
@@ -79,7 +82,15 @@ export const dateGlobal = <R>(ctx: Interpreter<R>) => {
methods(builtins, date, [
["now", 0, () => Date.now()],
["parse", 1, (_, args) => Date.parse(coerceToString(args[0]))],
["UTC", 7, (_, args) => Date.UTC(...(args.map((arg) => coerceToNumber(arg)) as Parameters<typeof Date.UTC>))],
[
"UTC",
7,
(_, args) =>
Effect.map(
Effect.forEach(args.slice(0, 7), (arg) => toPrimitiveNumber(ctx, arg), { concurrency: 1 }),
(parts) => Date.UTC(...(parts as Parameters<typeof Date.UTC>)),
),
],
])
const self = (thisValue: Value, name: string) => receiver(DateObj, thisValue, `Date.prototype.${name}`)
@@ -125,6 +136,8 @@ export const dateGlobal = <R>(ctx: Interpreter<R>) => {
concurrency: 1,
}),
(values) => {
// Every setter but setTime and setFullYear leaves an invalid Date untouched and answers NaN.
if (Number.isNaN(hosted.getTime()) && name !== "setTime" && !name.endsWith("FullYear")) return NaN
target.time = hosted[name](...(values as [number, number, number, number]))
return target.time
},
+49
View File
@@ -1688,3 +1688,52 @@ describe("ToPrimitive: operators and conversions honor program valueOf and toStr
expect(failure.message).toContain("Cannot convert object to primitive value")
})
})
describe("object destructuring from primitives", () => {
test("reads through the primitive's prototype like member access", async () => {
expect(
await value(`
const { length, 0: first, toUpperCase } = "abc"
const { toFixed } = 1.5
const {} = true
const { 0: a, ...rest } = "xyz"
const { ...none } = 42
let n
;({ length: n } = "hello")
return [length, first, toUpperCase.call("q"), toFixed.call(2.345, 1), a, rest, none, n]
`),
).toEqual([3, "a", "Q", "2.3", "x", { 1: "y", 2: "z" }, {}, 5])
})
test("only null and undefined sources throw", async () => {
expect((await error(`const { a } = null`)).message).toContain("Cannot destructure null as it is null")
expect((await error(`const {} = undefined`)).message).toContain("Cannot destructure undefined")
expect((await error(`let a; ({ a } = undefined)`)).message).toContain("Cannot destructure undefined")
})
})
describe("Date components convert through ToPrimitive", () => {
test("construction and Date.UTC ask each of the first seven arguments in order", async () => {
expect(
await value(`
const seen = []
const part = (n) => ({ valueOf() { seen.push(n); return n } })
const time = new Date(part(2024), part(1), part(2), part(3), part(4), part(5), part(6), part(99)).getTime()
const utc = Date.UTC(2024, { valueOf() { return 0 } }, 15)
return [seen, time === new Date(2024, 1, 2, 3, 4, 5, 6).getTime(), utc === Date.UTC(2024, 0, 15)]
`),
).toEqual([[2024, 1, 2, 3, 4, 5, 6], true, true])
expect((await error(`new Date(2024, { valueOf() { throw new RangeError("boom") } })`)).message).toContain("boom")
})
test("setters on an invalid Date answer NaN without overwriting a time set during coercion", async () => {
expect(
await value(`
const d = new Date(NaN)
const result = d.setDate({ valueOf() { d.setTime(0); return 1 } })
const y = new Date(NaN)
return [Number.isNaN(result), d.getTime(), y.setFullYear(2020) === Date.UTC(2020, 0, 1) - y.getTimezoneOffset() * 60000]
`),
).toEqual([true, 0, true])
})
})
@@ -2,6 +2,7 @@
"revision": "250f204f23a9249ff204be2baec29600faae7b75",
"directories": [
"built-ins/Array/prototype",
"built-ins/Date",
"built-ins/Function/prototype/apply",
"built-ins/Function/prototype/bind",
"built-ins/Function/prototype/call",
@@ -17,6 +18,7 @@
"built-ins/parseFloat",
"built-ins/parseInt",
"language/arguments-object",
"language/expressions/assignment",
"language/expressions/does-not-equals",
"language/expressions/equals",
"language/expressions/exponentiation",
+32 -15
View File
@@ -925,16 +925,6 @@ language/statements/async-generator/dstr/dflt-ary-ptrn-elem-id-iter-val-array-pr
language/statements/async-generator/return-undefined-implicit-and-explicit.js # Actual ["tick 1", "tick 2", "g1 ret", "g2 ret", "g3 ret", "g4 ret"] and expected ["tick 1", "g1 ret"
language/statements/const/dstr/ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/const/dstr/ary-ptrn-elem-id-iter-val-array-prototype.js # Expected SameValue(«3», «42») to be true
language/statements/for-await-of/async-func-decl-dstr-obj-empty-bool.js # TypeError: Object destructuring requires a data object or array value, received a boolean.
language/statements/for-await-of/async-func-decl-dstr-obj-empty-num.js # TypeError: Object destructuring requires a data object or array value, received a number.
language/statements/for-await-of/async-func-decl-dstr-obj-empty-string.js # TypeError: Object destructuring requires a data object or array value, received a string.
language/statements/for-await-of/async-func-decl-dstr-obj-rest-number.js # TypeError: Object destructuring requires a data object or array value, received a number.
language/statements/for-await-of/async-func-decl-dstr-obj-rest-str-val.js # TypeError: Object destructuring requires a data object or array value, received a string.
language/statements/for-await-of/async-gen-decl-dstr-obj-empty-bool.js # TypeError: Object destructuring requires a data object or array value, received a boolean.
language/statements/for-await-of/async-gen-decl-dstr-obj-empty-num.js # TypeError: Object destructuring requires a data object or array value, received a number.
language/statements/for-await-of/async-gen-decl-dstr-obj-empty-string.js # TypeError: Object destructuring requires a data object or array value, received a string.
language/statements/for-await-of/async-gen-decl-dstr-obj-rest-number.js # TypeError: Object destructuring requires a data object or array value, received a number.
language/statements/for-await-of/async-gen-decl-dstr-obj-rest-str-val.js # TypeError: Object destructuring requires a data object or array value, received a string.
language/statements/for-in/S12.6.4_A7_T2.js # for...in visits keys deleted during the loop
language/statements/for-in/order-enumerable-shadowed.js # Object.create property descriptors are not supported; assign the fields after creating the object.
language/statements/for-in/S12.6.4_A6.1.js # TypeError: FACTORY cannot be constructed: user-defined constructors and classes are not supported. C
@@ -959,11 +949,6 @@ language/statements/for-of/dstr/const-ary-init-iter-get-err-array-prototype.js
language/statements/for-of/dstr/const-ary-ptrn-elem-id-iter-val-array-prototype.js # Expected SameValue(«3», «42») to be true
language/statements/for-of/dstr/let-ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/for-of/dstr/let-ary-ptrn-elem-id-iter-val-array-prototype.js # Expected SameValue(«3», «42») to be true
language/statements/for-of/dstr/obj-empty-bool.js # Object destructuring requires a data object or array value, received a boolean.
language/statements/for-of/dstr/obj-empty-num.js # Object destructuring requires a data object or array value, received a number.
language/statements/for-of/dstr/obj-empty-string.js # Object destructuring requires a data object or array value, received a string.
language/statements/for-of/dstr/obj-rest-number.js # Object destructuring requires a data object or array value, received a number.
language/statements/for-of/dstr/obj-rest-str-val.js # Object destructuring requires a data object or array value, received a string.
language/statements/for-of/dstr/var-ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/for-of/dstr/var-ary-ptrn-elem-id-iter-val-array-prototype.js # Expected SameValue(«3», «42») to be true
language/statements/for-of/throw-from-catch.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
@@ -1097,3 +1082,35 @@ built-ins/Array/prototype/some/15.4.4.17-7-c-i-26.js # TypeError: Array.prototy
built-ins/Array/prototype/some/15.4.4.17-7-c-i-27.js # TypeError: Array.prototype.some called on incompatible receiver an Arguments.
built-ins/parseFloat/S15.1.2.3_A7.5.js # TypeError: Cannot read properties of undefined (reading '…').
built-ins/parseInt/S15.1.2.2_A9.5.js # TypeError: Cannot read properties of undefined (reading '…').
built-ins/Date/S15.9.3.1_A4_T0.js # `new Date(new PoisonedValueOf(1))` throws a Test262Error exception Expected a Test262Error but got a
built-ins/Date/S15.9.3.1_A4_T1.js # `new Date(new PoisonedValueOf(1), new PoisonedValueOf(2))` throws a Test262Error exception Expected
built-ins/Date/S15.9.3.1_A4_T2.js # `new Date(new PoisonedValueOf(1), new PoisonedValueOf(2), new PoisonedValueOf(3))` throws a Test262E
built-ins/Date/S15.9.3.1_A4_T3.js # `new Date(new PoisonedValueOf(1), new PoisonedValueOf(2), new PoisonedValueOf(3), new PoisonedValueO
built-ins/Date/S15.9.3.1_A4_T4.js # `new Date(new PoisonedValueOf(1), new PoisonedValueOf(2), new PoisonedValueOf(3), new PoisonedValueO
built-ins/Date/S15.9.3.1_A4_T5.js # `new Date(new PoisonedValueOf(1), new PoisonedValueOf(2), new PoisonedValueOf(3), new PoisonedValueO
built-ins/Date/S15.9.3.1_A4_T6.js # `new Date(new PoisonedValueOf(1), new PoisonedValueOf(2), new PoisonedValueOf(3), new PoisonedValueO
built-ins/Date/prototype/toJSON/invoke-result.js # TypeError: Date.prototype.toJSON called on incompatible receiver a data object.
built-ins/Date/prototype/toJSON/to-primitive-value-of.js # TypeError: Date.prototype.toJSON called on incompatible receiver a data object.
built-ins/Date/prototype/toString/format.js # Expected SameValue(«null», «null») to be false
built-ins/Date/prototype/toString/negative-year.js # Date.prototype.toString serializes year -1 to "-0001" Expected SameValue(«undefined», «"-0001"») to
language/expressions/assignment/dstr/array-elem-iter-rtrn-close-err.js # TypeError: Iterator next must be a function.
language/expressions/assignment/dstr/array-elem-iter-thrw-close-err.js # Expected SameValue(«1», «0») to be true
language/expressions/assignment/dstr/array-elem-iter-thrw-close.js # Expected SameValue(«1», «0») to be true
language/expressions/assignment/dstr/array-elem-trlg-iter-list-thrw-close-err.js # Expected SameValue(«1», «0») to be true
language/expressions/assignment/dstr/array-elem-trlg-iter-list-thrw-close.js # Expected SameValue(«1», «0») to be true
language/expressions/assignment/dstr/array-elem-trlg-iter-rest-rtrn-close-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/expressions/assignment/dstr/array-elem-trlg-iter-rest-rtrn-close-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/expressions/assignment/dstr/array-elem-trlg-iter-rest-rtrn-close.js # Expected SameValue(«11», «1») to be true
language/expressions/assignment/dstr/array-elem-trlg-iter-rest-thrw-close-err.js # Expected SameValue(«11», «1») to be true
language/expressions/assignment/dstr/array-elem-trlg-iter-rest-thrw-close.js # Expected SameValue(«11», «1») to be true
language/expressions/assignment/dstr/array-rest-iter-rtrn-close-err.js # TypeError: Iterator next must be a function.
language/expressions/assignment/dstr/array-rest-iter-rtrn-close-null.js # TypeError: Iterator next must be a function.
language/expressions/assignment/dstr/array-rest-iter-rtrn-close.js # Test262Error
language/expressions/assignment/dstr/array-rest-iter-thrw-close-err.js # Expected SameValue(«11», «0») to be true
language/expressions/assignment/dstr/array-rest-iter-thrw-close.js # Expected SameValue(«11», «0») to be true
language/expressions/assignment/dstr/array-rest-lref-err.js # Expected SameValue(«1», «0») to be true
language/expressions/assignment/target-member-computed-reference-null.js # Expected a DummyError but got a TypeError
language/expressions/assignment/target-member-computed-reference-undefined.js # Expected a DummyError but got a TypeError
language/expressions/assignment/target-member-computed-reference.js # Expected a DummyError but got a TypeError
language/expressions/assignment/target-member-identifier-reference-null.js # Expected SameValue(«0», «1») to be true
language/expressions/assignment/target-member-identifier-reference-undefined.js # Expected SameValue(«0», «1») to be true