mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-01 05:21:39 +00:00
fix(codemode): visit inherited enumerable keys in for...in (#59933)
This commit is contained in:
parent
c1b5f1b416
commit
ecc075a043
4 changed files with 70 additions and 7 deletions
|
|
@ -104,7 +104,9 @@ the ultimate source of truth. Upstream test262 files run verbatim from `test/tes
|
|||
synchronous iterators, and confined synchronous generators. Abrupt completion invokes the iterator's optional `return()`.
|
||||
- [x] `for...in` over own keys of plain objects, arrays, strings, and tool references. `null`, `undefined`, and other
|
||||
non-objects iterate nothing. An un-awaited promise throws rather than iterating.
|
||||
- [ ] `for...in` over inherited enumerable keys (`Object.create(proto)`), and skipping keys deleted during the loop.
|
||||
- [x] `for...in` over inherited enumerable keys (`Object.create(proto)`): own keys first, then each prototype's, skipping
|
||||
keys a nearer level already has, even as a non-enumerable property.
|
||||
- [ ] `for...in` skipping keys deleted during the loop.
|
||||
- [x] Unlabeled `break` and `continue`.
|
||||
- [x] `try`, `catch`, optional catch bindings, and `finally`.
|
||||
- [x] `throw` with arbitrary values.
|
||||
|
|
@ -338,8 +340,8 @@ reject }` object.
|
|||
(`Object.getPrototypeOf("a") === String.prototype`); `null`, `undefined`, and the two confined symbols throw a
|
||||
`TypeError`.
|
||||
- [x] `Object.create(proto)` with an object or `null` prototype; any other prototype is a `TypeError`
|
||||
(`Object prototype may only be an Object or null`). Inherited reads, `in`, `hasOwnProperty`, and own-only
|
||||
`Object.keys` follow the chain as in JS, but `for...in` still enumerates own keys only. A second `properties`
|
||||
(`Object prototype may only be an Object or null`). Inherited reads, `in`, `hasOwnProperty`, own-only
|
||||
`Object.keys`, and `for...in` follow the chain as in JS. A second `properties`
|
||||
argument other than `undefined` throws a `TypeError`: property descriptors are not supported (there is no
|
||||
`Object.defineProperty` either).
|
||||
- [x] `Object.freeze`, `Object.seal`, and `Object.preventExtensions`, with `isFrozen`, `isSealed`, and `isExtensible`.
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ import {
|
|||
IteratorObj,
|
||||
type Cursor,
|
||||
has,
|
||||
hasOwn,
|
||||
hidden,
|
||||
hasPrototype,
|
||||
keys,
|
||||
|
|
@ -1097,11 +1098,23 @@ class Frame<R> {
|
|||
throw typeError(`${context} must be a function.`, node)
|
||||
}
|
||||
|
||||
// for...in over null/undefined iterates nothing, like JS.
|
||||
// for...in over null/undefined iterates nothing, like JS. After the own keys come each prototype's enumerable
|
||||
// string keys, skipping any key a nearer level has, even as a non-enumerable property.
|
||||
private enumerableKeys(value: Value, node: AstNode): Array<string> {
|
||||
if (value instanceof ToolReference) return [...this.ctx.tools.keys(value.path)]
|
||||
if (value === null || value === undefined) return []
|
||||
return keys(enumerableSource(this.ctx, "for...in", value, node))
|
||||
const source = enumerableSource(this.ctx, "for...in", value, node)
|
||||
const chain = [source]
|
||||
// A string's characters stand in an array, but its prototype is String.prototype.
|
||||
let proto = primitivePrototype(this.ctx.builtins, value) ?? source.proto
|
||||
while (proto !== null) {
|
||||
chain.push(proto)
|
||||
proto = proto.proto
|
||||
}
|
||||
return chain.flatMap((target, depth) => {
|
||||
const nearer = chain.slice(0, depth)
|
||||
return keys(target).filter((key) => !nearer.some((level) => hasOwn(level, key)))
|
||||
})
|
||||
}
|
||||
|
||||
private *evaluateForInStatement(node: ForInStatement, labels?: ReadonlySet<string>): Eval<StatementResult> {
|
||||
|
|
|
|||
|
|
@ -176,4 +176,52 @@ describe("for...in", () => {
|
|||
"un-awaited Promise",
|
||||
)
|
||||
})
|
||||
|
||||
const forIn = (target: string, setup = "") =>
|
||||
value(`${setup}; const out = []; for (const key in ${target}) out.push(key); return out`)
|
||||
|
||||
test("visits inherited enumerable keys after own keys", async () => {
|
||||
expect(await forIn("o", "const p = { inh: 1 }; const o = Object.create(p); o.own = 2")).toEqual(["own", "inh"])
|
||||
})
|
||||
|
||||
test("walks every level, skipping keys a nearer level already has", async () => {
|
||||
expect(
|
||||
await forIn(
|
||||
"o",
|
||||
`const top = { a: 1, top: 1 }
|
||||
const mid = Object.create(top); mid.a = 2; mid.mid = 2
|
||||
const o = Object.create(mid); o.own = 3; o.mid = 3`,
|
||||
),
|
||||
).toEqual(["own", "mid", "a", "top"])
|
||||
})
|
||||
|
||||
test("orders integer-like keys first on each level", async () => {
|
||||
expect(
|
||||
await forIn("o", `const p = { b: 1, 10: 1, 2: 1 }; const o = Object.create(p); o.z = 1; o[5] = 1; o[1] = 1`),
|
||||
).toEqual(["1", "5", "z", "2", "10", "b"])
|
||||
})
|
||||
|
||||
test("a non-enumerable nearer property shadows an enumerable inherited one", async () => {
|
||||
// An array's own length and Array.prototype.push are non-enumerable.
|
||||
const setup = "Object.prototype.length = 1; Object.prototype.push = 2; Object.prototype.extra = 3"
|
||||
expect(await forIn("[5]", setup)).toEqual(["0", "extra"])
|
||||
expect(await forIn("Object.create([5])", setup)).toEqual(["0", "extra"])
|
||||
})
|
||||
|
||||
test("a null prototype ends the walk", async () => {
|
||||
expect(await forIn("o", "const o = Object.create(null); o.a = 1")).toEqual(["a"])
|
||||
expect(await forIn("Object.create(null)")).toEqual([])
|
||||
})
|
||||
|
||||
test("arrays and strings only visit their indexes, since built-in methods are non-enumerable", async () => {
|
||||
expect(await forIn("[7, 8]")).toEqual(["0", "1"])
|
||||
expect(await forIn(`"ab"`)).toEqual(["0", "1"])
|
||||
})
|
||||
|
||||
test("visits enumerable properties a program adds to a built-in prototype", async () => {
|
||||
expect(await forIn("[7, 8]", "Array.prototype.extra = 1")).toEqual(["0", "1", "extra"])
|
||||
expect(await forIn(`"ab"`, "Array.prototype.extra = 1")).toEqual(["0", "1"])
|
||||
expect(await forIn(`"ab"`, "String.prototype.extra = 1")).toEqual(["0", "1", "extra"])
|
||||
expect(await forIn("5", "Number.prototype.extra = 1")).toEqual(["extra"])
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1382,7 +1382,7 @@ describe("Object.getPrototypeOf and Object.create", () => {
|
|||
expect((await error(`Object.getPrototypeOf(Symbol.iterator)`)).message).toContain("cannot convert a symbol")
|
||||
})
|
||||
|
||||
test("Object.create links the prototype: inherited reads, in, and own-only keys", async () => {
|
||||
test("Object.create links the prototype: inherited reads, in, own-only keys, and inherited for...in", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const p = { greet(name) { return "hi " + name }, a: 1 }
|
||||
|
|
@ -1392,7 +1392,7 @@ describe("Object.getPrototypeOf and Object.create", () => {
|
|||
for (const key in c) seen.push(key)
|
||||
return ["greet" in c, Object.keys(c), c.hasOwnProperty("a"), c.a, c.greet(c.name), Object.getPrototypeOf(c) === p, Object.getPrototypeOf(Object.create(null)), seen]
|
||||
`),
|
||||
).toEqual([true, ["name"], false, 1, "hi x", true, null, ["name"]])
|
||||
).toEqual([true, ["name"], false, 1, "hi x", true, null, ["name", "greet", "a"]])
|
||||
})
|
||||
|
||||
test("Object.create rejects non-object prototypes and property descriptors", async () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue