Jelajahi Sumber

feat(codemode): support destructuring assignment (#35600)

Kit Langton 1 bulan lalu
induk
melakukan
d603eed5a1

+ 83 - 0
packages/codemode/src/interpreter/runtime.ts

@@ -1403,6 +1403,84 @@ class Interpreter<R> {
     })
   }
 
+  private assignPattern(pattern: AstNode, value: unknown, node: AstNode): Effect.Effect<void, unknown, R> {
+    const self = this
+    return Effect.gen(function* () {
+      if (pattern.type === "Identifier") {
+        self.setIdentifierValue(getString(pattern, "name"), value, pattern)
+        return
+      }
+
+      if (pattern.type === "MemberExpression") {
+        yield* self.writeMember(pattern, value)
+        return
+      }
+
+      if (pattern.type === "AssignmentPattern") {
+        const resolved = value === undefined ? yield* self.evaluateExpression(getNode(pattern, "right")) : value
+        yield* self.assignPattern(getNode(pattern, "left"), resolved, node)
+        return
+      }
+
+      if (pattern.type === "ObjectPattern") {
+        if (value === null || typeof value !== "object" || Array.isArray(value) || isRuntimeReference(value)) {
+          throw new InterpreterRuntimeError(
+            "Object destructuring requires a data object value.",
+            pattern,
+            "InvalidDataValue",
+          )
+        }
+
+        const source = value as SafeObject
+        const consumed = new Set<string>()
+        for (const propertyValue of getArray(pattern, "properties")) {
+          const property = asNode(propertyValue, "properties")
+          if (property.type === "RestElement") {
+            const rest: SafeObject = Object.create(null) as SafeObject
+            for (const [key, item] of Object.entries(source)) {
+              if (!consumed.has(key) && !isBlockedMember(key)) rest[key] = item
+            }
+            yield* self.assignPattern(getNode(property, "argument"), rest, property)
+            continue
+          }
+          if (
+            property.type !== "Property" ||
+            getBoolean(property, "computed") ||
+            getString(property, "kind") !== "init"
+          ) {
+            throw new InterpreterRuntimeError("Only named object destructuring properties are supported.", property)
+          }
+          const keyNode = getNode(property, "key")
+          const key = keyNode.type === "Identifier" ? getString(keyNode, "name") : String(keyNode.value)
+          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
+      }
+
+      if (pattern.type === "ArrayPattern") {
+        if (!Array.isArray(value)) {
+          throw new InterpreterRuntimeError("Array destructuring requires an array value.", pattern)
+        }
+        for (const [index, item] of getArray(pattern, "elements").entries()) {
+          if (item === null) continue
+          const element = asNode(item, `elements[${index}]`)
+          if (element.type === "RestElement") {
+            yield* self.assignPattern(getNode(element, "argument"), value.slice(index), element)
+            break
+          }
+          yield* self.assignPattern(element, value[index], pattern)
+        }
+        return
+      }
+
+      throw new InterpreterRuntimeError(`Unsupported assignment pattern '${pattern.type}'.`, node)
+    })
+  }
+
   private evaluateExpression(node: AstNode): Effect.Effect<unknown, unknown, R> {
     switch (node.type) {
       case "Literal": {
@@ -1804,6 +1882,11 @@ class Interpreter<R> {
       if (operator === "??=" || operator === "||=" || operator === "&&=") {
         return yield* self.evaluateLogicalAssignment(node, left, operator)
       }
+      if (operator === "=" && (left.type === "ObjectPattern" || left.type === "ArrayPattern")) {
+        const rightValue = yield* self.evaluateExpression(getNode(node, "right"))
+        yield* self.assignPattern(left, rightValue, node)
+        return rightValue
+      }
       if (left.type === "Identifier") {
         const name = getString(left, "name")
         if (operator !== "=") {

+ 35 - 0
packages/codemode/test/parity.test.ts

@@ -463,3 +463,38 @@ describe("H5: builtin coercion functions work as array callbacks", () => {
     expect(err.message).toContain("callback")
   })
 })
+
+describe("destructuring assignment", () => {
+  test("assigns object and array patterns to existing bindings", async () => {
+    expect(
+      await value(`
+        let a = 0
+        let b = 0
+        ;({ a } = { a: 2 })
+        ;[a, b] = [3, 4]
+        return [a, b]
+      `),
+    ).toEqual([3, 4])
+  })
+
+  test("supports defaults, nesting, rest, and member targets", async () => {
+    expect(
+      await value(`
+        let first = 0
+        let fallback = 0
+        let rest = {}
+        const target = {}
+        ;[first, fallback = 2, ...target.tail] = [1]
+        ;({ nested: { value: target.value }, kept: target.kept = 3, ...rest } = {
+          nested: { value: 4 },
+          extra: 5,
+        })
+        return { first, fallback, target, rest }
+      `),
+    ).toEqual({ first: 1, fallback: 2, target: { tail: [], value: 4, kept: 3 }, rest: { extra: 5 } })
+  })
+
+  test("returns the assigned value", async () => {
+    expect(await value(`let a = 0; const result = ([a] = [7]); return [a, result]`)).toEqual([7, [7]])
+  })
+})