feat(codemode): expand destructuring support (#37342)
This commit is contained in:
@@ -54,9 +54,9 @@ ultimate source of truth.
|
|||||||
loop headers, so reads before initialization and self- or cross-referential initializers observe the JavaScript
|
loop headers, so reads before initialization and self- or cross-referential initializers observe the JavaScript
|
||||||
temporal dead zone.
|
temporal dead zone.
|
||||||
- [ ] Hoist function declarations accepted directly in switch cases.
|
- [ ] Hoist function declarations accepted directly in switch cases.
|
||||||
- [ ] Computed object destructuring keys such as `const { [field]: value } = record`.
|
- [x] Computed object destructuring keys such as `const { [field]: value } = record`.
|
||||||
- [ ] Object destructuring from arrays, such as `const { length } = values`.
|
- [x] Object destructuring from arrays, such as `const { length } = values`.
|
||||||
- [ ] Array destructuring from supported non-array iterables: strings, Maps, Sets, and URLSearchParams.
|
- [x] Array destructuring from supported non-array iterables: strings, Maps, Sets, and URLSearchParams.
|
||||||
|
|
||||||
## Statements and control flow
|
## Statements and control flow
|
||||||
|
|
||||||
|
|||||||
@@ -777,9 +777,9 @@ export class Interpreter<R> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (pattern.type === "ObjectPattern") {
|
if (pattern.type === "ObjectPattern") {
|
||||||
if (value === null || typeof value !== "object" || Array.isArray(value) || isRuntimeReference(value)) {
|
if (value === null || typeof value !== "object" || isRuntimeReference(value)) {
|
||||||
throw new InterpreterRuntimeError(
|
throw new InterpreterRuntimeError(
|
||||||
"Object destructuring requires a data object value.",
|
"Object destructuring requires a data object or array value.",
|
||||||
pattern,
|
pattern,
|
||||||
"InvalidDataValue",
|
"InvalidDataValue",
|
||||||
)
|
)
|
||||||
@@ -798,38 +798,35 @@ export class Interpreter<R> {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
const key = yield* self.destructuringPropertyKey(property)
|
||||||
property.type !== "Property" ||
|
if (isBlockedMember(String(key))) {
|
||||||
getBoolean(property, "computed") ||
|
throw new InterpreterRuntimeError(`Property '${String(key)}' is not available in CodeMode.`, property)
|
||||||
getString(property, "kind") !== "init"
|
|
||||||
) {
|
|
||||||
throw new InterpreterRuntimeError("Only named object destructuring properties are supported.", property)
|
|
||||||
}
|
}
|
||||||
|
consumed.add(String(key))
|
||||||
const keyNode = getNode(property, "key")
|
yield* self.declarePattern(
|
||||||
const key = keyNode.type === "Identifier" ? getString(keyNode, "name") : String(keyNode.value)
|
getNode(property, "value"),
|
||||||
if (isBlockedMember(key)) {
|
self.destructuringPropertyValue(value as SafeObject | Array<unknown>, key),
|
||||||
throw new InterpreterRuntimeError(`Property '${key}' is not available in CodeMode.`, keyNode)
|
mutable,
|
||||||
}
|
property,
|
||||||
consumed.add(key)
|
)
|
||||||
yield* self.declarePattern(getNode(property, "value"), (value as SafeObject)[key], mutable, property)
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pattern.type === "ArrayPattern") {
|
if (pattern.type === "ArrayPattern") {
|
||||||
if (!Array.isArray(value)) {
|
const items = spreadItems(value)
|
||||||
throw new InterpreterRuntimeError("Array destructuring requires an array value.", pattern)
|
if (items === undefined) {
|
||||||
|
throw new InterpreterRuntimeError("Array destructuring requires a supported iterable value.", pattern)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [index, item] of getArray(pattern, "elements").entries()) {
|
for (const [index, item] of getArray(pattern, "elements").entries()) {
|
||||||
if (item === null) continue
|
if (item === null) continue
|
||||||
const element = asNode(item, `elements[${index}]`)
|
const element = asNode(item, `elements[${index}]`)
|
||||||
if (element.type === "RestElement") {
|
if (element.type === "RestElement") {
|
||||||
yield* self.declarePattern(getNode(element, "argument"), value.slice(index), mutable, element)
|
yield* self.declarePattern(getNode(element, "argument"), items.slice(index), mutable, element)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
yield* self.declarePattern(element, value[index], mutable, pattern)
|
yield* self.declarePattern(element, items[index], mutable, pattern)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -858,15 +855,15 @@ export class Interpreter<R> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (pattern.type === "ObjectPattern") {
|
if (pattern.type === "ObjectPattern") {
|
||||||
if (value === null || typeof value !== "object" || Array.isArray(value) || isRuntimeReference(value)) {
|
if (value === null || typeof value !== "object" || isRuntimeReference(value)) {
|
||||||
throw new InterpreterRuntimeError(
|
throw new InterpreterRuntimeError(
|
||||||
"Object destructuring requires a data object value.",
|
"Object destructuring requires a data object or array value.",
|
||||||
pattern,
|
pattern,
|
||||||
"InvalidDataValue",
|
"InvalidDataValue",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const source = value as SafeObject
|
const source = value as SafeObject | Array<unknown>
|
||||||
const consumed = new Set<string>()
|
const consumed = new Set<string>()
|
||||||
for (const propertyValue of getArray(pattern, "properties")) {
|
for (const propertyValue of getArray(pattern, "properties")) {
|
||||||
const property = asNode(propertyValue, "properties")
|
const property = asNode(propertyValue, "properties")
|
||||||
@@ -878,36 +875,29 @@ export class Interpreter<R> {
|
|||||||
yield* self.assignPattern(getNode(property, "argument"), rest, property)
|
yield* self.assignPattern(getNode(property, "argument"), rest, property)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (
|
const key = yield* self.destructuringPropertyKey(property)
|
||||||
property.type !== "Property" ||
|
if (isBlockedMember(String(key))) {
|
||||||
getBoolean(property, "computed") ||
|
throw new InterpreterRuntimeError(`Property '${String(key)}' is not available in CodeMode.`, property)
|
||||||
getString(property, "kind") !== "init"
|
|
||||||
) {
|
|
||||||
throw new InterpreterRuntimeError("Only named object destructuring properties are supported.", property)
|
|
||||||
}
|
}
|
||||||
const keyNode = getNode(property, "key")
|
consumed.add(String(key))
|
||||||
const key = keyNode.type === "Identifier" ? getString(keyNode, "name") : String(keyNode.value)
|
yield* self.assignPattern(getNode(property, "value"), self.destructuringPropertyValue(source, key), property)
|
||||||
if (isBlockedMember(key)) {
|
|
||||||
throw new InterpreterRuntimeError(`Property '${key}' is not available in CodeMode.`, keyNode)
|
|
||||||
}
|
|
||||||
consumed.add(key)
|
|
||||||
yield* self.assignPattern(getNode(property, "value"), source[key], property)
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pattern.type === "ArrayPattern") {
|
if (pattern.type === "ArrayPattern") {
|
||||||
if (!Array.isArray(value)) {
|
const items = spreadItems(value)
|
||||||
throw new InterpreterRuntimeError("Array destructuring requires an array value.", pattern)
|
if (items === undefined) {
|
||||||
|
throw new InterpreterRuntimeError("Array destructuring requires a supported iterable value.", pattern)
|
||||||
}
|
}
|
||||||
for (const [index, item] of getArray(pattern, "elements").entries()) {
|
for (const [index, item] of getArray(pattern, "elements").entries()) {
|
||||||
if (item === null) continue
|
if (item === null) continue
|
||||||
const element = asNode(item, `elements[${index}]`)
|
const element = asNode(item, `elements[${index}]`)
|
||||||
if (element.type === "RestElement") {
|
if (element.type === "RestElement") {
|
||||||
yield* self.assignPattern(getNode(element, "argument"), value.slice(index), element)
|
yield* self.assignPattern(getNode(element, "argument"), items.slice(index), element)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
yield* self.assignPattern(element, value[index], pattern)
|
yield* self.assignPattern(element, items[index], pattern)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -916,6 +906,26 @@ export class Interpreter<R> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private destructuringPropertyKey(property: AstNode): Effect.Effect<string | number, unknown, R> {
|
||||||
|
if (property.type !== "Property" || getString(property, "kind") !== "init") {
|
||||||
|
throw new InterpreterRuntimeError("Unsupported object destructuring property.", property)
|
||||||
|
}
|
||||||
|
const keyNode = getNode(property, "key")
|
||||||
|
if (getBoolean(property, "computed")) {
|
||||||
|
return Effect.map(this.evaluateExpression(keyNode), (value) => this.toPropertyKey(value, keyNode))
|
||||||
|
}
|
||||||
|
return Effect.succeed(keyNode.type === "Identifier" ? getString(keyNode, "name") : String(keyNode.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
private destructuringPropertyValue(source: SafeObject | Array<unknown>, key: string | number): unknown {
|
||||||
|
if (!Array.isArray(source)) return source[String(key)]
|
||||||
|
if (key === "length") return source.length
|
||||||
|
if (typeof key === "number") return source[key]
|
||||||
|
if (Object.hasOwn(source, key)) return (source as Record<string, unknown> & Array<unknown>)[key]
|
||||||
|
if (arrayMethods.has(key)) return new IntrinsicReference(source, key)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
private evaluateExpression(node: AstNode): Effect.Effect<unknown, unknown, R> {
|
private evaluateExpression(node: AstNode): Effect.Effect<unknown, unknown, R> {
|
||||||
switch (node.type) {
|
switch (node.type) {
|
||||||
case "Literal": {
|
case "Literal": {
|
||||||
|
|||||||
@@ -548,4 +548,71 @@ describe("destructuring assignment", () => {
|
|||||||
test("returns the assigned value", async () => {
|
test("returns the assigned value", async () => {
|
||||||
expect(await value(`let a = 0; const result = ([a] = [7]); return [a, result]`)).toEqual([7, [7]])
|
expect(await value(`let a = 0; const result = ([a] = [7]); return [a, result]`)).toEqual([7, [7]])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("supports computed object keys and evaluates them once", async () => {
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
let calls = 0
|
||||||
|
const field = () => { calls++; return "name" }
|
||||||
|
const { [field()]: name, ...rest } = { name: "Ada", role: "engineer" }
|
||||||
|
return { calls, name, rest }
|
||||||
|
`),
|
||||||
|
).toEqual({ calls: 1, name: "Ada", rest: { role: "engineer" } })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("supports object patterns over arrays", async () => {
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
const { 0: first, length, slice, ...rest } = ["a", "b", "c"]
|
||||||
|
return { first, length, sliced: slice(1), rest }
|
||||||
|
`),
|
||||||
|
).toEqual({ first: "a", length: 3, sliced: ["b", "c"], rest: { 1: "b", 2: "c" } })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("preserves exact computed property names on arrays", async () => {
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
const { ["01"]: item, ...rest } = [10, 20]
|
||||||
|
return { missing: item === undefined, rest }
|
||||||
|
`),
|
||||||
|
).toEqual({ missing: true, rest: { 0: 10, 1: 20 } })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("supports array patterns over strings, Maps, Sets, and URLSearchParams", async () => {
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
const [letter, ...letters] = "A😀B"
|
||||||
|
const [[mapKey, mapValue]] = new Map([["key", 1]])
|
||||||
|
const [setFirst, setSecond] = new Set([2, 3])
|
||||||
|
const [[queryKey, queryValue]] = new URLSearchParams("q=test&page=2")
|
||||||
|
return { letter, letters, mapKey, mapValue, setFirst, setSecond, queryKey, queryValue }
|
||||||
|
`),
|
||||||
|
).toEqual({
|
||||||
|
letter: "A",
|
||||||
|
letters: ["😀", "B"],
|
||||||
|
mapKey: "key",
|
||||||
|
mapValue: 1,
|
||||||
|
setFirst: 2,
|
||||||
|
setSecond: 3,
|
||||||
|
queryKey: "q",
|
||||||
|
queryValue: "test",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("supports iterable patterns in assignment and parameters", async () => {
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
let first
|
||||||
|
let rest
|
||||||
|
;[first, ...rest] = new Set([1, 2, 3])
|
||||||
|
const read = ([[key, value]]) => key + value
|
||||||
|
return { first, rest, entry: read(new Map([["a", 4]])) }
|
||||||
|
`),
|
||||||
|
).toEqual({ first: 1, rest: [2, 3], entry: "a4" })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects computed keys that are not confined property keys", async () => {
|
||||||
|
const err = await error(`const key = {}; const { [key]: value } = {}`)
|
||||||
|
expect(err.message).toContain("Property key must be a string or number")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user