feat(codemode): support custom async iterators (#38141)
This commit is contained in:
parent
dbfbb13ccc
commit
6f4b9504e5
7 changed files with 636 additions and 51 deletions
|
|
@ -1,10 +1,15 @@
|
|||
/*
|
||||
* Portions adapted from Test262 at revision 250f204f23a9249ff204be2baec29600faae7b75:
|
||||
* - test/language/statements/for-await-of/ticks-with-sync-iter-resolved-promise-and-constructor-lookup.js
|
||||
* - test/language/statements/for-await-of/ticks-with-async-iter-resolved-promise-and-constructor-lookup.js
|
||||
* - test/language/statements/for-await-of/async-func-dstr-let-ary-ptrn-elem-id-iter-val.js
|
||||
* - test/language/statements/for-await-of/async-func-decl-dstr-array-rest-after-element.js
|
||||
* - test/language/statements/for-await-of/iterator-close-non-throw-get-method-is-null.js
|
||||
* - test/language/statements/for-await-of/iterator-close-non-throw-get-method-non-callable.js
|
||||
* - test/language/statements/for-await-of/iterator-close-throw-get-method-non-callable.js
|
||||
*
|
||||
* Copyright (C) 2019 André Bargull. All rights reserved.
|
||||
* Copyright (C) 2020 Alexey Shvayka. All rights reserved.
|
||||
* Test262 portions are governed by the BSD license in LICENSE.test262.
|
||||
*/
|
||||
import { describe, expect, test } from "bun:test"
|
||||
|
|
@ -130,10 +135,400 @@ describe("Test262 for-await-of adaptations", () => {
|
|||
).toEqual([1, 3])
|
||||
})
|
||||
|
||||
test("keeps custom iterator objects outside the supported subset", async () => {
|
||||
test("drives a custom async iterator sequentially", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
let index = 0
|
||||
const iterator = {
|
||||
[Symbol.asyncIterator]: () => iterator,
|
||||
async next() {
|
||||
index += 1
|
||||
if (index > 3) return { done: true }
|
||||
return { done: false, value: index }
|
||||
},
|
||||
}
|
||||
const values = []
|
||||
for await (const item of iterator) values.push(item)
|
||||
return values
|
||||
`),
|
||||
).toEqual([1, 2, 3])
|
||||
})
|
||||
|
||||
test("leaves async iterator values under the iterator's control", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
let done = false
|
||||
const iterator = {
|
||||
[Symbol.asyncIterator]: () => iterator,
|
||||
next: async () => done ? { done: true } : (done = true, { done: false, value: Promise.resolve(1) }),
|
||||
}
|
||||
for await (const item of iterator) return [item instanceof Promise, await item]
|
||||
`),
|
||||
).toEqual([true, 1])
|
||||
})
|
||||
|
||||
test("awaits synchronous results from an async iterator before the body", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const events = ["pre"]
|
||||
const ticks = Promise.resolve()
|
||||
.then(() => events.push("tick 1"))
|
||||
.then(() => events.push("tick 2"))
|
||||
let done = false
|
||||
const iterator = {
|
||||
[Symbol.asyncIterator]: () => iterator,
|
||||
next: () => done ? { done: true } : (done = true, { done: false, value: Promise.resolve(1) }),
|
||||
}
|
||||
for await (const item of iterator) events.push(item instanceof Promise ? "loop" : "adopted")
|
||||
events.push("post")
|
||||
await ticks
|
||||
return events
|
||||
`),
|
||||
).toEqual(["pre", "tick 1", "loop", "tick 2", "post"])
|
||||
})
|
||||
|
||||
test("falls back to a custom synchronous iterator", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
let index = 0
|
||||
const iterator = {
|
||||
[Symbol.iterator]: () => iterator,
|
||||
next() {
|
||||
index += 1
|
||||
return index > 2 ? { done: true } : { done: false, value: Promise.resolve(index) }
|
||||
},
|
||||
}
|
||||
const values = []
|
||||
for await (const item of iterator) values.push(item)
|
||||
return values
|
||||
`),
|
||||
).toEqual([1, 2])
|
||||
})
|
||||
|
||||
test("adopts terminal and close values from synchronous iterators", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const terminal = {
|
||||
[Symbol.iterator]: () => terminal,
|
||||
next: () => ({ done: true, value: Promise.reject("terminal") }),
|
||||
}
|
||||
let terminalError
|
||||
try {
|
||||
for await (const item of terminal) {}
|
||||
} catch (error) {
|
||||
terminalError = error
|
||||
}
|
||||
|
||||
const closing = {
|
||||
[Symbol.iterator]: () => closing,
|
||||
next: () => ({ done: false, value: 1 }),
|
||||
return: () => ({ done: true, value: Promise.reject("close") }),
|
||||
}
|
||||
let closeError
|
||||
try {
|
||||
for await (const item of closing) break
|
||||
} catch (error) {
|
||||
closeError = error
|
||||
}
|
||||
return [terminalError, closeError]
|
||||
`),
|
||||
).toEqual(["terminal", "close"])
|
||||
})
|
||||
|
||||
test("captures the next method when acquiring the iterator", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
let count = 0
|
||||
const next = () => {
|
||||
count += 1
|
||||
if (count === 1) iterator.next = () => ({ done: true })
|
||||
return count > 2 ? { done: true } : { done: false, value: count }
|
||||
}
|
||||
const iterator = { [Symbol.iterator]: () => iterator, next }
|
||||
const values = []
|
||||
for await (const item of iterator) values.push(item)
|
||||
return values
|
||||
`),
|
||||
).toEqual([1, 2])
|
||||
})
|
||||
|
||||
test("captures synchronous iterator result fields before suspending", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
let count = 0
|
||||
const result = { done: false, value: 1 }
|
||||
const iterator = {
|
||||
[Symbol.iterator]: () => iterator,
|
||||
next() {
|
||||
count += 1
|
||||
if (count > 1) return { done: true }
|
||||
Promise.resolve().then(() => {
|
||||
result.done = true
|
||||
result.value = 2
|
||||
})
|
||||
return result
|
||||
},
|
||||
}
|
||||
const values = []
|
||||
for await (const item of iterator) values.push(item)
|
||||
return values
|
||||
`),
|
||||
).toEqual([1])
|
||||
})
|
||||
|
||||
test("preserves the async close turn for a sync iterator without return", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const events = []
|
||||
const iterator = {
|
||||
[Symbol.iterator]: () => iterator,
|
||||
next: () => ({ done: false, value: 1 }),
|
||||
}
|
||||
for await (const item of iterator) {
|
||||
Promise.resolve().then(() => events.push("reaction"))
|
||||
break
|
||||
}
|
||||
events.push("after")
|
||||
return events
|
||||
`),
|
||||
).toEqual(["reaction", "after"])
|
||||
})
|
||||
|
||||
test("defers synchronous iterator protocol errors", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const events = []
|
||||
const throwing = {
|
||||
[Symbol.iterator]: () => throwing,
|
||||
next() {
|
||||
Promise.resolve().then(() => events.push("next reaction"))
|
||||
throw "next"
|
||||
},
|
||||
}
|
||||
try {
|
||||
for await (const item of throwing) {}
|
||||
} catch (error) {
|
||||
events.push("next catch")
|
||||
}
|
||||
|
||||
const malformed = {
|
||||
[Symbol.iterator]: () => malformed,
|
||||
next() {
|
||||
Promise.resolve().then(() => events.push("result reaction"))
|
||||
return 1
|
||||
},
|
||||
}
|
||||
try {
|
||||
for await (const item of malformed) {}
|
||||
} catch (error) {
|
||||
events.push("result catch")
|
||||
}
|
||||
|
||||
const closing = {
|
||||
[Symbol.iterator]: () => closing,
|
||||
next: () => ({ done: false, value: 1 }),
|
||||
return() {
|
||||
Promise.resolve().then(() => events.push("return reaction"))
|
||||
throw "return"
|
||||
},
|
||||
}
|
||||
try {
|
||||
for await (const item of closing) break
|
||||
} catch (error) {
|
||||
events.push("return catch")
|
||||
}
|
||||
return events
|
||||
`),
|
||||
).toEqual(["next reaction", "next catch", "result reaction", "result catch", "return reaction", "return catch"])
|
||||
})
|
||||
|
||||
test("prefers Symbol.asyncIterator over Symbol.iterator", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
let done = false
|
||||
const asyncIterator = {
|
||||
next: async () => done ? { done: true } : (done = true, { done: false, value: "async" }),
|
||||
}
|
||||
const syncIterator = { next: () => ({ done: true }) }
|
||||
const iterable = {
|
||||
[Symbol.asyncIterator]: () => asyncIterator,
|
||||
[Symbol.iterator]: () => syncIterator,
|
||||
}
|
||||
const values = []
|
||||
for await (const item of iterable) values.push(item)
|
||||
return values
|
||||
`),
|
||||
).toEqual(["async"])
|
||||
})
|
||||
|
||||
test("closes a custom iterator on abrupt loop completion", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
let closed = 0
|
||||
const iterator = {
|
||||
[Symbol.asyncIterator]: () => iterator,
|
||||
next: async () => ({ done: false, value: 1 }),
|
||||
return: async () => (closed += 1, { done: true }),
|
||||
}
|
||||
for await (const item of iterator) break
|
||||
try {
|
||||
for await (const item of iterator) throw "stop"
|
||||
} catch (error) {}
|
||||
return closed
|
||||
`),
|
||||
).toBe(2)
|
||||
})
|
||||
|
||||
test("treats a null iterator return method as absent", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
let count = 0
|
||||
const iterator = {
|
||||
[Symbol.asyncIterator]: () => iterator,
|
||||
next: async () => ({ done: false, value: 1 }),
|
||||
return: null,
|
||||
}
|
||||
for await (const item of iterator) {
|
||||
count += 1
|
||||
break
|
||||
}
|
||||
return count
|
||||
`),
|
||||
).toBe(1)
|
||||
})
|
||||
|
||||
test("a non-callable return method replaces break with TypeError", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const iterator = {
|
||||
[Symbol.asyncIterator]: () => iterator,
|
||||
next: async () => ({ done: false, value: 1 }),
|
||||
return: true,
|
||||
}
|
||||
try {
|
||||
for await (const item of iterator) break
|
||||
} catch (error) {
|
||||
return error.name
|
||||
}
|
||||
return "missed"
|
||||
`),
|
||||
).toBe("TypeError")
|
||||
})
|
||||
|
||||
test("a body throw wins over a non-callable return method", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const iterator = {
|
||||
[Symbol.asyncIterator]: () => iterator,
|
||||
next: async () => ({ done: false, value: 1 }),
|
||||
return: true,
|
||||
}
|
||||
try {
|
||||
for await (const item of iterator) throw "body"
|
||||
} catch (error) {
|
||||
return error
|
||||
}
|
||||
return "missed"
|
||||
`),
|
||||
).toBe("body")
|
||||
})
|
||||
|
||||
test("rejects a primitive iterator return result", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const iterator = {
|
||||
[Symbol.asyncIterator]: () => iterator,
|
||||
next: async () => ({ done: false, value: 1 }),
|
||||
return: async () => null,
|
||||
}
|
||||
try {
|
||||
for await (const item of iterator) break
|
||||
} catch (error) {
|
||||
return error.name
|
||||
}
|
||||
return "missed"
|
||||
`),
|
||||
).toBe("TypeError")
|
||||
})
|
||||
|
||||
test("propagates iterator acquisition failures", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const iterator = {
|
||||
[Symbol.asyncIterator]: () => { throw "acquire" },
|
||||
}
|
||||
try {
|
||||
for await (const item of iterator) {}
|
||||
} catch (error) {
|
||||
return error
|
||||
}
|
||||
return "missed"
|
||||
`),
|
||||
).toBe("acquire")
|
||||
})
|
||||
|
||||
test("rejects malformed iterator acquisition methods and results", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const names = []
|
||||
const invalid = [
|
||||
{ [Symbol.asyncIterator]: true },
|
||||
{ [Symbol.asyncIterator]: () => 1 },
|
||||
{ [Symbol.asyncIterator]: () => ({ next: true }) },
|
||||
]
|
||||
for (const iterator of invalid) {
|
||||
try {
|
||||
for await (const item of iterator) {}
|
||||
} catch (error) {
|
||||
names.push(error.name)
|
||||
}
|
||||
}
|
||||
return names
|
||||
`),
|
||||
).toEqual(["TypeError", "TypeError", "TypeError"])
|
||||
})
|
||||
|
||||
test("preserves iterator protocol keys through object copies", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
let done = false
|
||||
const iterable = {
|
||||
plain: true,
|
||||
[Symbol.asyncIterator]: () => iterable,
|
||||
next: async () => done ? { done: true } : (done = true, { done: false, value: 1 }),
|
||||
}
|
||||
const spread = { ...iterable }
|
||||
const { plain, ...rest } = iterable
|
||||
const assigned = Object.assign({}, iterable)
|
||||
const values = []
|
||||
for await (const item of spread) values.push(item)
|
||||
return [
|
||||
values,
|
||||
Object.hasOwn(spread, Symbol.asyncIterator),
|
||||
Object.hasOwn(rest, Symbol.asyncIterator),
|
||||
Object.hasOwn(assigned, Symbol.asyncIterator),
|
||||
]
|
||||
`),
|
||||
).toEqual([[1], true, true, true])
|
||||
})
|
||||
|
||||
test("rejects malformed iterator protocol results", async () => {
|
||||
const result = await execute(`
|
||||
const iterator = {
|
||||
[Symbol.asyncIterator]: () => iterator,
|
||||
next: async () => 1,
|
||||
}
|
||||
for await (const item of iterator) {}
|
||||
`)
|
||||
expect(result.ok).toBe(false)
|
||||
if (result.ok) return
|
||||
expect(result.error.message).toContain("Iterator next() result must be an object")
|
||||
})
|
||||
|
||||
test("rejects objects without an iterator protocol method", async () => {
|
||||
const result = await execute(`for await (const item of { values: [1, 2] }) {}`)
|
||||
expect(result.ok).toBe(false)
|
||||
if (result.ok) return
|
||||
expect(result.error.message).toContain("for await...of requires an array, string, Map, Set, or URLSearchParams")
|
||||
expect(result.error.message).toContain("or custom iterator value")
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -717,6 +717,18 @@ describe("destructuring assignment", () => {
|
|||
).toEqual({ first: 1, rest: [2, 3], entry: "a4" })
|
||||
})
|
||||
|
||||
test("excludes computed numeric keys from object rest", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const { [0]: declared, ...declarationRest } = { 0: "a", 1: "b" }
|
||||
let assigned
|
||||
let assignmentRest
|
||||
;({ [0]: assigned, ...assignmentRest } = { 0: "c", 1: "d" })
|
||||
return { declared, declarationRest, assigned, assignmentRest }
|
||||
`),
|
||||
).toEqual({ declared: "a", declarationRest: { 1: "b" }, assigned: "c", assignmentRest: { 1: "d" } })
|
||||
})
|
||||
|
||||
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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue