mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
fix(codemode): name the received value in data diagnostics and document intentional gaps (#48211)
This commit is contained in:
@@ -214,6 +214,7 @@ ultimate source of truth.
|
||||
- [x] `Object.keys` over arrays and tool references.
|
||||
- [x] Object identity is preserved by in-CodeMode Object helpers.
|
||||
- [x] Prototype traversal and mutation through `__proto__`, `constructor`, and `prototype` are blocked.
|
||||
- [x] Circular references are rejected when created (`o.self = o`, `array.push(array)`), not at serialization as in JS.
|
||||
- [ ] Legal own data fields named `__proto__`, `constructor`, or `prototype` are rejected at JSON/tool boundaries and
|
||||
cannot be created, read, or written in CodeMode; tool path segments with those names remain supported.
|
||||
- [x] `Object.is` for supported data values.
|
||||
@@ -364,7 +365,8 @@ ultimate source of truth.
|
||||
or without `new`.
|
||||
- [x] `AggregateError` with the `(errors, message?)` signature and an own `errors` array, constructed directly or by
|
||||
an all-rejected `Promise.any`; direct construction accepts custom synchronous iterators and generators.
|
||||
- [x] Error `name`/`message`, error inheritance through `instanceof`, and plain-data serialization.
|
||||
- [x] Error `name`/`message`, error inheritance through `instanceof`, and plain-data serialization. Errors have no
|
||||
`stack`; the diagnostic carries the source location instead.
|
||||
- [x] `instanceof` for Date, RegExp, Map, Set, URL, URLSearchParams, Array, Object, Promise, and Error types.
|
||||
- [x] Catchable user throws, runtime failures raised during interpreted evaluation, awaited tool failures, and awaited
|
||||
tool-call-limit failures; parse/compile failures, cooperative timeout, and output bounding remain outside program
|
||||
|
||||
@@ -77,6 +77,23 @@ export const rejectCircularInsertion = (
|
||||
}
|
||||
}
|
||||
|
||||
export const describeValue = (value: unknown): string => {
|
||||
if (value === null) return "null"
|
||||
if (Array.isArray(value)) return "an array"
|
||||
if (value instanceof Values.Promise) return "an un-awaited Promise"
|
||||
if (value instanceof ToolReference) return "a tool reference"
|
||||
if (value instanceof Values.Date) return "a Date"
|
||||
if (value instanceof Values.RegExp) return "a RegExp"
|
||||
if (value instanceof Values.Map) return "a Map"
|
||||
if (value instanceof Values.Set) return "a Set"
|
||||
if (value instanceof Values.URL) return "a URL"
|
||||
if (value instanceof Values.URLSearchParams) return "a URLSearchParams"
|
||||
if (value instanceof CodeModeGenerator) return "a generator"
|
||||
if (isRuntimeReference(value)) return "a function"
|
||||
if (typeof value === "object") return "a data object"
|
||||
return `a ${typeof value}`
|
||||
}
|
||||
|
||||
export const typeofValue = (value: unknown): string => {
|
||||
if (
|
||||
value instanceof HostFunction ||
|
||||
|
||||
@@ -71,7 +71,13 @@ import { HostFunction, HostNamespace } from "./host.js"
|
||||
import { invokeIntrinsic } from "./methods.js"
|
||||
import { preserveConsumerError, type Runner } from "./runner.js"
|
||||
import { invokePromiseInstanceMethod, PromiseRuntime, resolvePromise, resolvePromiseValue } from "./promises.js"
|
||||
import { containsOpaqueReference, isRuntimeReference, rejectCircularInsertion, typeofValue } from "./references.js"
|
||||
import {
|
||||
containsOpaqueReference,
|
||||
describeValue,
|
||||
isRuntimeReference,
|
||||
rejectCircularInsertion,
|
||||
typeofValue,
|
||||
} from "./references.js"
|
||||
import { ScopeStack } from "./scope.js"
|
||||
import { arrayMethods, mapMethods, setMethods } from "../stdlib/collections.js"
|
||||
import { dateMethods } from "../stdlib/date.js"
|
||||
@@ -1025,7 +1031,7 @@ class Frame<R> {
|
||||
if (pattern.type === "ObjectPattern") {
|
||||
if (value === null || typeof value !== "object" || isRuntimeReference(value)) {
|
||||
throw new InterpreterRuntimeError(
|
||||
"Object destructuring requires a data object or array value.",
|
||||
`Object destructuring requires a data object or array value, received ${describeValue(value)}.`,
|
||||
pattern,
|
||||
"InvalidDataValue",
|
||||
)
|
||||
@@ -1091,7 +1097,7 @@ class Frame<R> {
|
||||
if (pattern.type === "ObjectPattern") {
|
||||
if (value === null || typeof value !== "object" || isRuntimeReference(value)) {
|
||||
throw new InterpreterRuntimeError(
|
||||
"Object destructuring requires a data object or array value.",
|
||||
`Object destructuring requires a data object or array value, received ${describeValue(value)}.`,
|
||||
pattern,
|
||||
"InvalidDataValue",
|
||||
)
|
||||
@@ -1896,7 +1902,11 @@ class Frame<R> {
|
||||
const spread = yield* self.evaluateExpression(property.argument)
|
||||
if (spread === null || spread === undefined || Values.isValue(spread)) continue
|
||||
if (typeof spread !== "object" || Array.isArray(spread) || isRuntimeReference(spread)) {
|
||||
throw new InterpreterRuntimeError("Object spread requires a data object.", property, "InvalidDataValue")
|
||||
throw new InterpreterRuntimeError(
|
||||
`Object spread requires a data object, received ${describeValue(spread)}.`,
|
||||
property,
|
||||
"InvalidDataValue",
|
||||
)
|
||||
}
|
||||
for (const [key, value] of Object.entries(spread)) {
|
||||
if (isBlockedMember(key)) throw new InterpreterRuntimeError(`Property '${key}' is not available.`, property)
|
||||
@@ -2126,7 +2136,7 @@ class Frame<R> {
|
||||
|
||||
if (isRuntimeReference(objectValue)) {
|
||||
throw new InterpreterRuntimeError(
|
||||
"Runtime references are opaque and do not expose properties.",
|
||||
`Cannot read properties of ${describeValue(objectValue)}; only data values expose properties.`,
|
||||
objectNode,
|
||||
"InvalidDataValue",
|
||||
)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Effect } from "effect"
|
||||
import { HostFunction, sync, syncCall } from "../interpreter/host.js"
|
||||
import { type AstNode, CodeModeGenerator, InterpreterRuntimeError } from "../interpreter/model.js"
|
||||
import { describeValue } from "../interpreter/references.js"
|
||||
import { applyCollectionCallback, preserveConsumerError, type Runner } from "../interpreter/runner.js"
|
||||
import { Values } from "../values.js"
|
||||
|
||||
const constructArray = (args: Array<unknown>, node: AstNode): Array<unknown> => {
|
||||
if (args.length !== 1) return [...args]
|
||||
@@ -16,13 +16,6 @@ const constructArray = (args: Array<unknown>, node: AstNode): Array<unknown> =>
|
||||
}
|
||||
|
||||
const arrayLikeSource = (source: unknown, node: AstNode): { readonly length: number; readonly source: object } => {
|
||||
if (source instanceof Values.Promise) {
|
||||
throw new InterpreterRuntimeError(
|
||||
"Array.from received an un-awaited Promise; await it before creating the array.",
|
||||
node,
|
||||
"InvalidDataValue",
|
||||
)
|
||||
}
|
||||
if (
|
||||
source !== null &&
|
||||
typeof source === "object" &&
|
||||
@@ -35,7 +28,7 @@ const arrayLikeSource = (source: unknown, node: AstNode): { readonly length: num
|
||||
return { length: normalized, source }
|
||||
}
|
||||
throw new InterpreterRuntimeError(
|
||||
"Array.from expects an array, string, Map, Set, or array-like value.",
|
||||
`Array.from expects an array, string, Map, Set, or array-like value, received ${describeValue(source)}.`,
|
||||
node,
|
||||
"InvalidDataValue",
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Effect } from "effect"
|
||||
import { isBlockedMember, type SafeObject } from "../data.js"
|
||||
import { HostFunction, requiresNew } from "../interpreter/host.js"
|
||||
import { type AstNode, InterpreterRuntimeError, isRecord } from "../interpreter/model.js"
|
||||
import { isRuntimeReference } from "../interpreter/references.js"
|
||||
import { describeValue, isRuntimeReference } from "../interpreter/references.js"
|
||||
import { applyCollectionCallback, preserveConsumerError, type Runner, toPrimitive } from "../interpreter/runner.js"
|
||||
import { Values } from "../values.js"
|
||||
import { coerceToString } from "./value.js"
|
||||
@@ -73,7 +73,11 @@ const coerceGroupByPropertyKey = <R>(
|
||||
): Effect.Effect<string, unknown, R> => {
|
||||
if (value instanceof Values.Promise) return Effect.succeed("[object Promise]")
|
||||
if (!Values.isValue(value) && isRuntimeReference(value)) {
|
||||
throw new InterpreterRuntimeError("Object.groupBy callback must return a data value.", node, "InvalidDataValue")
|
||||
throw new InterpreterRuntimeError(
|
||||
`Object.groupBy callback must return a data value, received ${describeValue(value)}.`,
|
||||
node,
|
||||
"InvalidDataValue",
|
||||
)
|
||||
}
|
||||
return Effect.map(toPrimitive(runner, value, "string", node), coerceToString)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,12 @@ import { Effect } from "effect"
|
||||
import { isBlockedMember, toProgram } from "../data.js"
|
||||
import { HostFunction, sync, syncCall } from "../interpreter/host.js"
|
||||
import { type AstNode, AsyncIteratorSymbol, InterpreterRuntimeError, IteratorSymbol } from "../interpreter/model.js"
|
||||
import { containsOpaqueReference, rejectCircularInsertion, typeofValue } from "../interpreter/references.js"
|
||||
import {
|
||||
containsOpaqueReference,
|
||||
describeValue,
|
||||
rejectCircularInsertion,
|
||||
typeofValue,
|
||||
} from "../interpreter/references.js"
|
||||
import { preserveConsumerError, type Runner } from "../interpreter/runner.js"
|
||||
import { ToolReference } from "../tool-runtime.js"
|
||||
import { Values } from "../values.js"
|
||||
@@ -12,20 +17,14 @@ import { coerceToString } from "./value.js"
|
||||
const requireObject = (name: string, input: unknown, node: AstNode): Record<string, unknown> => {
|
||||
if (Array.isArray(input)) return input as unknown as Record<string, unknown>
|
||||
if (Values.isValue(input)) return {}
|
||||
if (input instanceof Values.Promise) {
|
||||
const prototype = input === null || typeof input !== "object" ? undefined : Object.getPrototypeOf(input)
|
||||
if (prototype !== null && prototype !== Object.prototype) {
|
||||
throw new InterpreterRuntimeError(
|
||||
`Object.${name} received an un-awaited Promise; await it before inspecting the result.`,
|
||||
`Object.${name} expects a data object or array, received ${describeValue(input)}.`,
|
||||
node,
|
||||
"InvalidDataValue",
|
||||
)
|
||||
}
|
||||
if (input === null || typeof input !== "object") {
|
||||
throw new InterpreterRuntimeError(`Object.${name} expects a data object or array.`, node, "InvalidDataValue")
|
||||
}
|
||||
const prototype = Object.getPrototypeOf(input)
|
||||
if (prototype !== null && prototype !== Object.prototype) {
|
||||
throw new InterpreterRuntimeError(`Object.${name} expects a data object or array.`, node, "InvalidDataValue")
|
||||
}
|
||||
return input as Record<string, unknown>
|
||||
}
|
||||
|
||||
|
||||
@@ -85,9 +85,22 @@ describe("Object.keys over arrays", () => {
|
||||
expect(await value(`return Object.keys({ a: 1, b: 2 })`)).toEqual(["a", "b"])
|
||||
})
|
||||
|
||||
test("non-object inputs still fail clearly", async () => {
|
||||
const failure = await error(`return Object.keys("nope")`)
|
||||
expect(failure.message).toContain("Object.keys expects a data object or array")
|
||||
test("non-object inputs name what was received", async () => {
|
||||
expect((await error(`return Object.keys("nope")`)).message).toContain(
|
||||
"Object.keys expects a data object or array, received a string.",
|
||||
)
|
||||
expect((await error(`return Object.entries(42)`)).message).toContain("received a number.")
|
||||
expect((await error(`return Object.values(null)`)).message).toContain("received null.")
|
||||
expect((await error(`return Object.keys(tools.github.list_issues({ value: "x" }))`)).message).toContain(
|
||||
"received an un-awaited Promise.",
|
||||
)
|
||||
expect((await error(`return Object.entries(() => 1)`)).message).toContain("received a function.")
|
||||
expect((await error(`return { ...[1] }`)).message).toContain(
|
||||
"Object spread requires a data object, received an array.",
|
||||
)
|
||||
expect((await error(`const { a } = new Map(); return a`)).message).toContain("received a Map.")
|
||||
expect((await error(`return Array.from(7)`)).message).toContain("received a number.")
|
||||
expect((await error(`return (() => 1).x`)).message).toContain("Cannot read properties of a function")
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user