fix(codemode): parse canonical index keys (#37851)
This commit is contained in:
@@ -197,8 +197,8 @@ ultimate source of truth.
|
|||||||
- [x] `length`, numeric indexing, index assignment, spread, and `for...of`.
|
- [x] `length`, numeric indexing, index assignment, spread, and `for...of`.
|
||||||
- [x] The `thisArg` argument of `Array.from` is accepted and ignored, like JS arrows.
|
- [x] The `thisArg` argument of `Array.from` is accepted and ignored, like JS arrows.
|
||||||
- [ ] `Array.prototype.toSpliced`.
|
- [ ] `Array.prototype.toSpliced`.
|
||||||
- [ ] Canonical array/string index parsing: a key such as `"01"` must remain an ordinary property key rather than
|
- [x] Canonical array/string index parsing: keys such as `"01"` remain non-index properties rather than aliasing index
|
||||||
aliasing index `1`.
|
`1`; arbitrary array-property assignment remains unsupported.
|
||||||
- [x] `Array.prototype.sort` preserves trailing holes, while `toSorted` densifies holes into `undefined` elements,
|
- [x] `Array.prototype.sort` preserves trailing holes, while `toSorted` densifies holes into `undefined` elements,
|
||||||
like JavaScript.
|
like JavaScript.
|
||||||
|
|
||||||
|
|||||||
@@ -96,6 +96,15 @@ const globalStaticMembers: Partial<Record<GlobalNamespaceName, Set<string>>> = {
|
|||||||
URL: urlStatics,
|
URL: urlStatics,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MAX_ARRAY_LENGTH = 4_294_967_295
|
||||||
|
|
||||||
|
const parseArrayIndex = (key: string | number): number | undefined => {
|
||||||
|
const property = String(key)
|
||||||
|
if (!/^(0|[1-9]\d*)$/.test(property)) return undefined
|
||||||
|
const index = Number(property)
|
||||||
|
return index < MAX_ARRAY_LENGTH ? index : undefined
|
||||||
|
}
|
||||||
|
|
||||||
const calleeDescription = (callee: AstNode): string => {
|
const calleeDescription = (callee: AstNode): string => {
|
||||||
if (callee.type === "Identifier") return getString(callee, "name")
|
if (callee.type === "Identifier") return getString(callee, "name")
|
||||||
if (callee.type === "MemberExpression") {
|
if (callee.type === "MemberExpression") {
|
||||||
@@ -1916,8 +1925,8 @@ export class Interpreter<R> {
|
|||||||
|
|
||||||
if (typeof objectValue === "string") {
|
if (typeof objectValue === "string") {
|
||||||
if (key === "length") return new ComputedValue(objectValue.length)
|
if (key === "length") return new ComputedValue(objectValue.length)
|
||||||
if (typeof key === "number") return new ComputedValue(objectValue[key])
|
const index = parseArrayIndex(key)
|
||||||
if (typeof key === "string" && /^\d+$/.test(key)) return new ComputedValue(objectValue[Number(key)])
|
if (index !== undefined) return new ComputedValue(objectValue[index])
|
||||||
if (typeof key === "string" && stringMethods.has(key)) return new IntrinsicReference(objectValue, key)
|
if (typeof key === "string" && stringMethods.has(key)) return new IntrinsicReference(objectValue, key)
|
||||||
return new ComputedValue(undefined)
|
return new ComputedValue(undefined)
|
||||||
}
|
}
|
||||||
@@ -2011,18 +2020,14 @@ export class Interpreter<R> {
|
|||||||
|
|
||||||
if (Array.isArray(objectValue)) {
|
if (Array.isArray(objectValue)) {
|
||||||
if (operation === "delete") return { target: objectValue, key }
|
if (operation === "delete") return { target: objectValue, key }
|
||||||
if (
|
const index = parseArrayIndex(key)
|
||||||
key !== "length" &&
|
if (key !== "length" && !(typeof key === "string" && arrayMethods.has(key)) && index === undefined) {
|
||||||
!(typeof key === "string" && arrayMethods.has(key)) &&
|
|
||||||
typeof key !== "number" &&
|
|
||||||
!/^\d+$/.test(key)
|
|
||||||
) {
|
|
||||||
if (typeof key === "string" && Object.hasOwn(objectValue, key)) {
|
if (typeof key === "string" && Object.hasOwn(objectValue, key)) {
|
||||||
return new ComputedValue((objectValue as Record<string, unknown> & Array<unknown>)[key])
|
return new ComputedValue((objectValue as Record<string, unknown> & Array<unknown>)[key])
|
||||||
}
|
}
|
||||||
return new ComputedValue(undefined)
|
return new ComputedValue(undefined)
|
||||||
}
|
}
|
||||||
return { target: objectValue, key }
|
return { target: objectValue, key: index ?? key }
|
||||||
}
|
}
|
||||||
|
|
||||||
return { target: objectValue as SafeObject, key }
|
return { target: objectValue as SafeObject, key }
|
||||||
@@ -2043,10 +2048,9 @@ export class Interpreter<R> {
|
|||||||
)
|
)
|
||||||
return reference
|
return reference
|
||||||
if (Array.isArray(reference.target)) {
|
if (Array.isArray(reference.target)) {
|
||||||
if (typeof reference.key === "string" && arrayMethods.has(reference.key)) {
|
if (reference.key === "length") return reference.target.length
|
||||||
return new IntrinsicReference(reference.target, reference.key)
|
if (typeof reference.key === "string") return new IntrinsicReference(reference.target, reference.key)
|
||||||
}
|
return reference.target[reference.key]
|
||||||
return reference.key === "length" ? reference.target.length : reference.target[Number(reference.key)]
|
|
||||||
}
|
}
|
||||||
if (reference.target instanceof CodeModeURL) {
|
if (reference.target instanceof CodeModeURL) {
|
||||||
return (reference.target.url as unknown as Record<string, unknown>)[String(reference.key)]
|
return (reference.target.url as unknown as Record<string, unknown>)[String(reference.key)]
|
||||||
@@ -2109,7 +2113,7 @@ export class Interpreter<R> {
|
|||||||
throw new InterpreterRuntimeError("Array methods cannot be assigned in CodeMode.", node)
|
throw new InterpreterRuntimeError("Array methods cannot be assigned in CodeMode.", node)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const key = Array.isArray(reference.target) ? Number(reference.key) : String(reference.key)
|
const key = Array.isArray(reference.target) ? reference.key : String(reference.key)
|
||||||
const current =
|
const current =
|
||||||
reference.target instanceof CodeModeURL
|
reference.target instanceof CodeModeURL
|
||||||
? (reference.target.url as unknown as Record<string, unknown>)[key]
|
? (reference.target.url as unknown as Record<string, unknown>)[key]
|
||||||
@@ -2123,16 +2127,15 @@ export class Interpreter<R> {
|
|||||||
private assignToReference(reference: MemberReference, key: number | string, next: unknown, node: AstNode): void {
|
private assignToReference(reference: MemberReference, key: number | string, next: unknown, node: AstNode): void {
|
||||||
if (Array.isArray(reference.target)) {
|
if (Array.isArray(reference.target)) {
|
||||||
const target = reference.target
|
const target = reference.target
|
||||||
const index = key as number
|
if (typeof key !== "number" || parseArrayIndex(key) === undefined) {
|
||||||
if (!Number.isInteger(index) || index < 0) {
|
|
||||||
throw new InterpreterRuntimeError(
|
throw new InterpreterRuntimeError(
|
||||||
"Array assignment index must be a non-negative integer.",
|
"Array assignment index must be a valid array index.",
|
||||||
node,
|
node,
|
||||||
"InvalidDataValue",
|
"InvalidDataValue",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
rejectCircularInsertion(target, next, "Array assignment result", node)
|
rejectCircularInsertion(target, next, "Array assignment result", node)
|
||||||
target[index] = next
|
target[key] = next
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (reference.target instanceof CodeModeURL) {
|
if (reference.target instanceof CodeModeURL) {
|
||||||
|
|||||||
@@ -42,6 +42,15 @@ describe("H2: string property access reads as undefined (not a throw)", () => {
|
|||||||
test("unknown property on a number is undefined", async () => {
|
test("unknown property on a number is undefined", async () => {
|
||||||
expect(await value(`return (5).foo ?? "n"`)).toBe("n")
|
expect(await value(`return (5).foo ?? "n"`)).toBe("n")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("only canonical string index keys access characters", async () => {
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
const text = "abc"
|
||||||
|
return [text[1], text["1"], text["01"], text["1.0"], text[-0], text["-0"]]
|
||||||
|
`),
|
||||||
|
).toEqual(["b", "b", null, null, "a", null])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("H3: array property access reads as undefined (not a throw)", () => {
|
describe("H3: array property access reads as undefined (not a throw)", () => {
|
||||||
@@ -62,6 +71,38 @@ describe("H3: array property access reads as undefined (not a throw)", () => {
|
|||||||
expect(await value(`return [1,2,3][9] === undefined`)).toBe(true)
|
expect(await value(`return [1,2,3][9] === undefined`)).toBe(true)
|
||||||
expect(await value(`return [1,2,3][9]`)).toBeNull()
|
expect(await value(`return [1,2,3][9]`)).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("only canonical array index keys access elements", async () => {
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
const values = ["a", "b"]
|
||||||
|
return [values[1], values["1"], values["01"], values["1.0"], values[-0], values["-0"]]
|
||||||
|
`),
|
||||||
|
).toEqual(["b", "b", null, null, "a", null])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("noncanonical keys cannot mutate or delete an aliased element", async () => {
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
const values = ["a", "b"]
|
||||||
|
let writes = 0
|
||||||
|
try { values["01"] = ++writes } catch {}
|
||||||
|
const removed = delete values["01"]
|
||||||
|
return [writes, removed, values]
|
||||||
|
`),
|
||||||
|
).toEqual([0, true, ["a", "b"]])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("the maximum array length is not accepted as an array index", async () => {
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
const values = []
|
||||||
|
let writes = 0
|
||||||
|
try { values["4294967295"] = ++writes } catch {}
|
||||||
|
return [writes, values.length]
|
||||||
|
`),
|
||||||
|
).toEqual([0, 0])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("H6: object spread of null/undefined is a no-op", () => {
|
describe("H6: object spread of null/undefined is a no-op", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user