mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 02:24:10 +08:00
refactor: restrict reflective access (#48860)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
name: typecheck
|
||||
name: check
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
typecheck:
|
||||
check:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -17,5 +17,5 @@ jobs:
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Run typecheck
|
||||
run: bun typecheck
|
||||
- name: Run checks
|
||||
run: bun run check
|
||||
+1
-1
@@ -17,4 +17,4 @@ if (process.versions.bun !== expectedBunVersion) {
|
||||
console.warn(`Warning: Bun version ${process.versions.bun} differs from expected ${expectedBunVersion}`);
|
||||
}
|
||||
'
|
||||
bun typecheck
|
||||
bun run check
|
||||
|
||||
+14
-37
@@ -1,45 +1,22 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/nicolo-ribaudo/oxc-project.github.io/refs/heads/json-schema/src/public/.oxlintrc.schema.json",
|
||||
"options": {
|
||||
"typeAware": true
|
||||
},
|
||||
"categories": {
|
||||
"suspicious": "warn"
|
||||
"correctness": "off",
|
||||
"suspicious": "off",
|
||||
"pedantic": "off",
|
||||
"perf": "off",
|
||||
"style": "off",
|
||||
"restriction": "off",
|
||||
"nursery": "off"
|
||||
},
|
||||
"rules": {
|
||||
"typescript/no-base-to-string": "warn",
|
||||
// Effect uses `function*` with Effect.gen/Effect.fnUntraced that don't always yield
|
||||
"require-yield": "off",
|
||||
// SolidJS uses `let ref: T | undefined` for JSX ref bindings assigned at runtime
|
||||
"no-unassigned-vars": "off",
|
||||
// SolidJS tracks reactive deps by reading properties inside createEffect
|
||||
"no-unused-expressions": "off",
|
||||
// Intentional control char matching (ANSI escapes, null byte sanitization)
|
||||
"no-control-regex": "off",
|
||||
// SST and plugin tools require triple-slash references
|
||||
"triple-slash-reference": "off",
|
||||
|
||||
// Suspicious category: suppress noisy rules
|
||||
// Effect's nested function* closures inherently shadow outer scope
|
||||
"no-shadow": "off",
|
||||
// Namespace-heavy codebase makes this too noisy
|
||||
"unicorn/consistent-function-scoping": "off",
|
||||
// Opinionated — .sort()/.reverse() mutation is fine in this codebase
|
||||
"unicorn/no-array-sort": "off",
|
||||
"unicorn/no-array-reverse": "off",
|
||||
// Not relevant — this isn't a DOM event handler codebase
|
||||
"unicorn/prefer-add-event-listener": "off",
|
||||
// Bundler handles module resolution
|
||||
"unicorn/require-module-specifiers": "off",
|
||||
// postMessage target origin not relevant for this codebase
|
||||
"unicorn/require-post-message-target-origin": "off",
|
||||
// Side-effectful constructors are intentional in some places
|
||||
"no-new": "off",
|
||||
|
||||
// Type-aware: catch unhandled promises
|
||||
"typescript/no-floating-promises": "warn",
|
||||
// Warn when spreading non-plain objects (Headers, class instances, etc.)
|
||||
"typescript/no-misused-spread": "warn"
|
||||
"no-restricted-globals": [
|
||||
"error",
|
||||
{
|
||||
"name": "Reflect",
|
||||
"message": "Use typed property access or direct invocation. Suppress this rule only for genuine reflection."
|
||||
}
|
||||
]
|
||||
},
|
||||
"ignorePatterns": ["**/node_modules", "**/dist", "**/.build", "**/.sst", "**/*.d.ts", "**/sdk.gen.ts"]
|
||||
}
|
||||
|
||||
@@ -170,9 +170,10 @@ const table = sqliteTable("session", {
|
||||
- Test actual implementation, do not duplicate logic into tests
|
||||
- Tests cannot run from repo root (guard: `do-not-run-tests-from-root`); run from package directories such as `packages/core`.
|
||||
|
||||
## Type Checking
|
||||
## Checks
|
||||
|
||||
- Always run `bun typecheck` from package directories (for example, `packages/core`), never `tsc` directly.
|
||||
- Run `bun run check` from the repository root as the canonical full lint and type-check verification.
|
||||
- During focused iteration, run `bun typecheck` from the affected package directory (for example, `packages/core`). Never run `tsc` directly.
|
||||
|
||||
## V2 Session Core
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"test:lint-rules": "ast-grep test -c script/ast-grep/sgconfig.yml",
|
||||
"test:effect-simplification-rules": "ast-grep test -c script/ast-grep/effect-simplifications/sgconfig.yml",
|
||||
"typecheck": "bun turbo typecheck --concurrency=3",
|
||||
"check": "bun run lint && bun run typecheck",
|
||||
"typecheck:profile": "bun script/profile-typecheck.ts",
|
||||
"typecheck:profile:packages": "bun script/profile-typecheck-packages.ts",
|
||||
"upgrade-opentui": "bun run script/upgrade-opentui.ts",
|
||||
|
||||
@@ -325,6 +325,7 @@ describe("provider package entrypoints", () => {
|
||||
test("requires an Anthropic-compatible base URL at runtime", async () => {
|
||||
const AnthropicCompatible = await import("@opencode/ai/providers/anthropic-compatible")
|
||||
expect(() =>
|
||||
// oxlint-disable-next-line no-restricted-globals -- This test intentionally bypasses static required-option checks.
|
||||
Reflect.apply(AnthropicCompatible.model, undefined, ["compatible-model", { apiKey: "fixture" }]),
|
||||
).toThrow(configuration("anthropic-compatible", "Anthropic-compatible providers require a baseURL"))
|
||||
})
|
||||
@@ -333,6 +334,7 @@ describe("provider package entrypoints", () => {
|
||||
const Anthropic = await import("@opencode/ai/providers/anthropic")
|
||||
const AnthropicCompatible = await import("@opencode/ai/providers/anthropic-compatible")
|
||||
expect(() =>
|
||||
// oxlint-disable-next-line no-restricted-globals -- This test intentionally passes a statically invalid option combination.
|
||||
Reflect.apply(AnthropicCompatible.model, undefined, [
|
||||
"compatible-model",
|
||||
{
|
||||
@@ -343,6 +345,7 @@ describe("provider package entrypoints", () => {
|
||||
]),
|
||||
).toThrow(configuration("anthropic-compatible", "Anthropic-compatible apiKey cannot be combined with authToken"))
|
||||
expect(() =>
|
||||
// oxlint-disable-next-line no-restricted-globals -- This test intentionally passes a statically invalid option combination.
|
||||
Reflect.apply(Anthropic.model, undefined, ["claude-sonnet-4-6", { apiKey: "fixture", authToken: "token" }]),
|
||||
).toThrow(configuration("anthropic", "Anthropic apiKey cannot be combined with authToken"))
|
||||
})
|
||||
@@ -490,11 +493,13 @@ describe("provider package entrypoints", () => {
|
||||
const GoogleVertexResponses = await import("@opencode/ai/providers/google-vertex/responses")
|
||||
const Providers = await import("@opencode/ai/providers")
|
||||
expect(() =>
|
||||
// oxlint-disable-next-line no-restricted-globals -- This test intentionally passes a statically invalid option combination.
|
||||
Reflect.apply(GoogleVertex.model, undefined, [
|
||||
"gemini-3.5-flash",
|
||||
{ accessToken: "token", apiKey: "fixture", project: "vertex-project" },
|
||||
]),
|
||||
).toThrow(configuration("google-vertex", "Google Vertex apiKey cannot be combined with accessToken or auth"))
|
||||
// oxlint-disable-next-line no-restricted-globals -- This test intentionally passes a statically invalid option combination.
|
||||
const configured = Reflect.apply(GoogleVertex.configure, undefined, [
|
||||
{ accessToken: "token", auth: {}, project: "vertex-project" },
|
||||
])
|
||||
@@ -502,34 +507,40 @@ describe("provider package entrypoints", () => {
|
||||
configuration("google-vertex", "Google Vertex accessToken cannot be combined with auth"),
|
||||
)
|
||||
expect(() =>
|
||||
// oxlint-disable-next-line no-restricted-globals -- This test intentionally passes an unsupported authentication option.
|
||||
Reflect.apply(GoogleVertexMessages.model, undefined, [
|
||||
"claude-sonnet-4-6",
|
||||
{ apiKey: "fixture", project: "vertex-project" },
|
||||
]),
|
||||
).toThrow(configuration("google-vertex", "Google Vertex Messages does not support API keys"))
|
||||
expect(() =>
|
||||
// oxlint-disable-next-line no-restricted-globals -- This test intentionally passes an unsupported authentication option.
|
||||
Reflect.apply(Providers.GoogleVertexMessages.configure, undefined, [
|
||||
{ apiKey: "fixture", project: "vertex-project" },
|
||||
]),
|
||||
).toThrow(configuration("google-vertex", "Google Vertex Messages does not support API keys"))
|
||||
expect(() =>
|
||||
// oxlint-disable-next-line no-restricted-globals -- This test intentionally passes an unsupported authentication option.
|
||||
Reflect.apply(GoogleVertexChat.model, undefined, [
|
||||
"deepseek-ai/deepseek-v3.2-maas",
|
||||
{ apiKey: "fixture", project: "vertex-project" },
|
||||
]),
|
||||
).toThrow(configuration("google-vertex", "Google Vertex Chat does not support API keys"))
|
||||
expect(() =>
|
||||
// oxlint-disable-next-line no-restricted-globals -- This test intentionally passes an unsupported authentication option.
|
||||
Reflect.apply(Providers.GoogleVertexChat.configure, undefined, [
|
||||
{ apiKey: "fixture", project: "vertex-project" },
|
||||
]),
|
||||
).toThrow(configuration("google-vertex", "Google Vertex Chat does not support API keys"))
|
||||
expect(() =>
|
||||
// oxlint-disable-next-line no-restricted-globals -- This test intentionally passes an unsupported authentication option.
|
||||
Reflect.apply(GoogleVertexResponses.model, undefined, [
|
||||
"xai/grok-4.20-reasoning",
|
||||
{ apiKey: "fixture", project: "vertex-project" },
|
||||
]),
|
||||
).toThrow(configuration("google-vertex", "Google Vertex Responses does not support API keys"))
|
||||
expect(() =>
|
||||
// oxlint-disable-next-line no-restricted-globals -- This test intentionally passes an unsupported authentication option.
|
||||
Reflect.apply(Providers.GoogleVertexResponses.configure, undefined, [
|
||||
{ apiKey: "fixture", project: "vertex-project" },
|
||||
]),
|
||||
|
||||
@@ -120,16 +120,16 @@ describe("LLMClient tools", () => {
|
||||
|
||||
const second = bodies[1]
|
||||
if (!second || typeof second !== "object") throw new Error("Expected second request body")
|
||||
const messages = Reflect.get(second, "messages")
|
||||
const tools = Reflect.get(second, "tools")
|
||||
const messages = "messages" in second ? second.messages : undefined
|
||||
const tools = "tools" in second ? second.tools : undefined
|
||||
|
||||
expect(Reflect.get(second, "max_completion_tokens")).toBe(50)
|
||||
expect(Reflect.get(second, "tool_choice")).toBe("auto")
|
||||
expect("max_completion_tokens" in second ? second.max_completion_tokens : undefined).toBe(50)
|
||||
expect("tool_choice" in second ? second.tool_choice : undefined).toBe("auto")
|
||||
expect(tools).toHaveLength(1)
|
||||
expect(
|
||||
Array.isArray(messages)
|
||||
? messages.map((message) =>
|
||||
message && typeof message === "object" ? Reflect.get(message, "role") : undefined,
|
||||
message && typeof message === "object" && "role" in message ? message.role : undefined,
|
||||
)
|
||||
: undefined,
|
||||
).toEqual(["user", "assistant", "tool"])
|
||||
@@ -398,7 +398,9 @@ describe("LLMClient tools", () => {
|
||||
required: ["temperature", "condition"],
|
||||
additionalProperties: false,
|
||||
})
|
||||
expect(Reflect.get(Reflect.get(typed?.outputSchema ?? {}, "properties") as object, "temperature")).toBeDefined()
|
||||
const properties =
|
||||
typed?.outputSchema && "properties" in typed.outputSchema ? typed.outputSchema.properties : undefined
|
||||
expect(properties && "temperature" in properties ? properties.temperature : undefined).toBeDefined()
|
||||
expect(dynamic?.outputSchema).toEqual(schema)
|
||||
})
|
||||
|
||||
|
||||
@@ -63,20 +63,20 @@ for (const size of ["typical", "large"]) {
|
||||
const source = page.locator(`[data-timeline-part-id="${sourcePart}"] [data-component="markdown"]`)
|
||||
await expect(source).toHaveAttribute("data-markdown-ready", "")
|
||||
await page.locator(`[data-slot="titlebar-tabs"] a[href="${stressSessionHref(fixture.targetID)}"]`).click()
|
||||
await page.waitForFunction(() => Reflect.get(window, "markdownGate").held)
|
||||
await page.waitForFunction(() => window.markdownGate.held)
|
||||
await expect(page.locator(`[data-timeline-part-id="${targetPart}"]`)).toBeAttached()
|
||||
const cdp = await page.context().newCDPSession(page)
|
||||
await cdp.send("Performance.enable")
|
||||
const before = await cdp.send("Performance.getMetrics")
|
||||
await page.evaluate(() => Reflect.get(window, "markdownGate").arm())
|
||||
await page.evaluate(() => window.markdownGate.arm())
|
||||
await page.locator(`[data-slot="titlebar-tabs"] a[href="${stressSessionHref(fixture.sourceID)}"]`).click()
|
||||
await expect(source).toHaveAttribute("data-markdown-ready", "")
|
||||
await expect(source.getByRole("heading", { name: "Current destination" })).toBeVisible()
|
||||
await expect(page.locator(`[data-timeline-part-id="${targetPart}"]`)).toHaveCount(0)
|
||||
await page.waitForFunction(() => Reflect.get(window, "markdownGate").settled > 0)
|
||||
await page.waitForFunction(() => window.markdownGate.settled > 0)
|
||||
const after = await cdp.send("Performance.getMetrics")
|
||||
const stats = await page.evaluate(() => {
|
||||
const value = Reflect.get(window, "markdownGate")
|
||||
const value = window.markdownGate
|
||||
return {
|
||||
admitted: value.admitted,
|
||||
responses: value.responses,
|
||||
|
||||
@@ -4,6 +4,23 @@ import type {
|
||||
MarkdownWorkerResponse,
|
||||
} from "../../../../session-ui/src/components/markdown-worker-protocol"
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
markdownGate: {
|
||||
admitted: number
|
||||
responses: number
|
||||
held: boolean
|
||||
started: number
|
||||
ready: number
|
||||
released: number
|
||||
settled: number
|
||||
sanitizeCalls: number
|
||||
sanitizeChars: number
|
||||
arm: () => void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function installMarkdownGate(
|
||||
page: Page,
|
||||
input: { answer: string; sourcePart: string; targetPart: string; href: string },
|
||||
|
||||
@@ -51,7 +51,7 @@ Terminal.prototype.open = function (element) {
|
||||
context.drawImage = function (...args: unknown[]) {
|
||||
probe.draws++
|
||||
if (hidden) probe.hiddenDraws++
|
||||
Reflect.apply(draw, this, args)
|
||||
draw.apply(this, args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ for (const scenario of ["visible-output", "hidden-output", "full-scrollback-tear
|
||||
window.terminalProbe.draws++
|
||||
if (!this.canvas.checkVisibility()) window.terminalProbe.hiddenDraws++
|
||||
}
|
||||
Reflect.apply(fill, this, args)
|
||||
fill.apply(this, args)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ export async function installTimelineStreamProbe(
|
||||
}
|
||||
if (state.scroll.lastCallFrame === state.scroll.frame) state.scroll.sameFrameCalls += 1
|
||||
state.scroll.lastCallFrame = state.scroll.frame
|
||||
Reflect.apply(scrollTo, this, typeof first === "number" ? [first, second] : [first])
|
||||
scrollTo.apply(this, typeof first === "number" ? [first, second] : [first])
|
||||
}
|
||||
Element.prototype.scrollTo = measuredScrollTo
|
||||
Object.defineProperty(Element.prototype, "scrollTop", {
|
||||
|
||||
@@ -13,8 +13,8 @@ test("loads home and the directory picker without newer browser APIs", async ({
|
||||
})
|
||||
await page.addInitScript((directory) => {
|
||||
// Safari 16.6 has neither API. Remove them before the web entry runs.
|
||||
Reflect.deleteProperty(Map, "groupBy")
|
||||
Reflect.deleteProperty(Promise, "withResolvers")
|
||||
delete (Map as Partial<typeof Map>).groupBy
|
||||
delete (Promise as Partial<typeof Promise>).withResolvers
|
||||
localStorage.setItem(
|
||||
"opencode.global.dat:server",
|
||||
JSON.stringify({
|
||||
|
||||
@@ -59,7 +59,7 @@ describe("checkServerHealth", () => {
|
||||
|
||||
await checkServerHealth(server, fetch).finally(() => {
|
||||
if (timeout) Object.defineProperty(AbortSignal, "timeout", timeout)
|
||||
if (!timeout) Reflect.deleteProperty(AbortSignal, "timeout")
|
||||
if (!timeout) delete (AbortSignal as Partial<typeof AbortSignal>).timeout
|
||||
})
|
||||
|
||||
expect(timeoutMs).toBe(30_000)
|
||||
@@ -100,7 +100,7 @@ describe("checkServerHealth", () => {
|
||||
timeoutMs: 10,
|
||||
}).finally(() => {
|
||||
if (timeout) Object.defineProperty(AbortSignal, "timeout", timeout)
|
||||
if (!timeout) Reflect.deleteProperty(AbortSignal, "timeout")
|
||||
if (!timeout) delete (AbortSignal as Partial<typeof AbortSignal>).timeout
|
||||
})
|
||||
|
||||
expect(aborted).toBe(true)
|
||||
|
||||
@@ -27,7 +27,7 @@ test("detects iOS home-screen apps when the standalone media query does not matc
|
||||
expect(isStandalone()).toBe(true)
|
||||
} finally {
|
||||
if (descriptor) Object.defineProperty(navigator, "standalone", descriptor)
|
||||
if (!descriptor) Reflect.deleteProperty(navigator, "standalone")
|
||||
if (!descriptor) delete (navigator as Navigator & { standalone?: boolean }).standalone
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -219,6 +219,7 @@ test("snapshots materialize only measured rows and restore their current geometr
|
||||
new Proxy(measurements, {
|
||||
get(target, key, receiver) {
|
||||
if (typeof key === "string" && /^\d+$/.test(key)) reads.push(Number(key))
|
||||
// oxlint-disable-next-line no-restricted-globals -- Proxy forwarding requires receiver-aware property access.
|
||||
return Reflect.get(target, key, receiver)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -78,7 +78,7 @@ export async function syncEditedFiles(input: {
|
||||
const files = Array.isArray(input.metadata.files)
|
||||
? input.metadata.files.flatMap((file): string[] => {
|
||||
if (!file || typeof file !== "object") return []
|
||||
const path = Reflect.get(file, "file")
|
||||
const path = "file" in file ? file.file : undefined
|
||||
return typeof path === "string" ? [path] : []
|
||||
})
|
||||
: []
|
||||
|
||||
@@ -761,7 +761,7 @@ function formRequestOptions(location: LocationRef | undefined): [] | [{ headers:
|
||||
}
|
||||
|
||||
function formAlreadySettled(error: unknown) {
|
||||
return !!error && typeof error === "object" && Reflect.get(error, "_tag") === "FormAlreadySettledError"
|
||||
return !!error && typeof error === "object" && "_tag" in error && error._tag === "FormAlreadySettledError"
|
||||
}
|
||||
|
||||
function partID(eventID: string) {
|
||||
|
||||
@@ -102,7 +102,7 @@ async function selectSession(input: {
|
||||
}) {
|
||||
const explicit = input.session
|
||||
? await input.client.session.get({ sessionID: input.session }, ...requestOptions(input.signal)).catch((error) => {
|
||||
if (error && typeof error === "object" && Reflect.get(error, "_tag") === "SessionNotFoundError")
|
||||
if (error && typeof error === "object" && "_tag" in error && error._tag === "SessionNotFoundError")
|
||||
return undefined
|
||||
throw error
|
||||
})
|
||||
|
||||
@@ -27,7 +27,7 @@ test("acp prompt resolves after ordered turn updates", async () => {
|
||||
if (!body || typeof body !== "object") {
|
||||
return new Response(null, { status: 400 })
|
||||
}
|
||||
const id = Reflect.get(body, "id")
|
||||
const id = "id" in body ? body.id : undefined
|
||||
if (typeof id !== "string") return new Response(null, { status: 400 })
|
||||
queueMicrotask(() => {
|
||||
if (!events) return
|
||||
|
||||
@@ -574,7 +574,7 @@ function permissionReplies(fixture: Fixture) {
|
||||
return fixture.requests.flatMap((request): Array<[string, string]> => {
|
||||
const match = /^\/api\/session\/[^/]+\/permission\/([^/]+)\/reply$/.exec(request.path)
|
||||
if (!match?.[1] || !request.body || typeof request.body !== "object") return []
|
||||
const reply = Reflect.get(request.body, "reply")
|
||||
const reply = "reply" in request.body ? request.body.reply : undefined
|
||||
return typeof reply === "string" ? [[decodeURIComponent(match[1]), reply]] : []
|
||||
})
|
||||
}
|
||||
|
||||
@@ -237,7 +237,7 @@ describe("acp service prompt routing and usage", () => {
|
||||
|
||||
function requestID(request: FixtureRequest) {
|
||||
if (!request.body || typeof request.body !== "object") throw new Error(`missing body for ${request.path}`)
|
||||
const id = Reflect.get(request.body, "id")
|
||||
const id = "id" in request.body ? request.body.id : undefined
|
||||
if (typeof id !== "string") throw new Error(`missing prompt id for ${request.path}`)
|
||||
return id
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ export async function withTimeout<Value>(promise: Promise<Value>, message: strin
|
||||
|
||||
function stringField(value: unknown, key: string) {
|
||||
if (!value || typeof value !== "object") return undefined
|
||||
const field = Reflect.get(value, key)
|
||||
const field = (value as Record<string, unknown>)[key]
|
||||
return typeof field === "string" ? field : undefined
|
||||
}
|
||||
|
||||
|
||||
@@ -301,6 +301,7 @@ test("uses migrated keybinds when persistence fails", async () => {
|
||||
const fs = new Proxy(node, {
|
||||
get(target, property, receiver) {
|
||||
if (property === "rename") return () => Effect.die(new Error("read-only config"))
|
||||
// oxlint-disable-next-line no-restricted-globals -- Proxy forwarding requires receiver-aware property access.
|
||||
return Reflect.get(target, property, receiver)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -21,6 +21,7 @@ const writeFileString: FileSystem.FileSystem["writeFileString"] = (target, data,
|
||||
const fs = new Proxy(node, {
|
||||
get(target, property, receiver) {
|
||||
if (property === "writeFileString") return writeFileString
|
||||
// oxlint-disable-next-line no-restricted-globals -- Proxy forwarding requires receiver-aware property access.
|
||||
return Reflect.get(target, property, receiver)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -249,7 +249,7 @@ export const arrayGlobal = <R>(runner: Runner<R>) => {
|
||||
items[index] = item
|
||||
})
|
||||
Array.from({ length: holeCount }, (_, index) => itemCount + index).forEach((index) => {
|
||||
Reflect.deleteProperty(items, index)
|
||||
delete items[index]
|
||||
})
|
||||
return target
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ const metadataBoolean = (metadata: unknown, key: string) => {
|
||||
if (metadata === undefined || metadata === null || typeof metadata !== "object" || Array.isArray(metadata)) {
|
||||
return undefined
|
||||
}
|
||||
const value = Reflect.get(metadata, key)
|
||||
const value = (metadata as Record<string, unknown>)[key]
|
||||
if (typeof value === "boolean") return value
|
||||
if (typeof value !== "string") return undefined
|
||||
const normalized = value.trim().toLowerCase()
|
||||
|
||||
@@ -88,11 +88,7 @@ const layer = Layer.effect(
|
||||
const trigger: Interface["trigger"] = Effect.fnUntraced(function* (domain, name, event) {
|
||||
for (const entry of callbacks.get(key(domain, name)) ?? []) {
|
||||
if (entry.options?.providerID !== undefined && entry.options.providerID !== eventProviderID(event)) continue
|
||||
const result: Effect.Effect<void, Failures[typeof domain][typeof name]> = Reflect.apply(
|
||||
entry.callback,
|
||||
undefined,
|
||||
[event],
|
||||
)
|
||||
const result: Effect.Effect<void, Failures[typeof domain][typeof name]> = entry.callback(event)
|
||||
yield* result
|
||||
}
|
||||
return event
|
||||
|
||||
@@ -177,6 +177,7 @@ export const make = Effect.fn("PluginHost.make")(function* (
|
||||
options: event.options,
|
||||
sdk: event.sdk,
|
||||
}
|
||||
// oxlint-disable-next-line no-restricted-globals -- The generic hook callback remains a union after narrowing by hook name.
|
||||
return Reflect.apply(callback, undefined, [output]).pipe(
|
||||
Effect.tap(() => Effect.sync(() => (event.sdk = output.sdk))),
|
||||
)
|
||||
@@ -190,6 +191,7 @@ export const make = Effect.fn("PluginHost.make")(function* (
|
||||
sdk: event.sdk,
|
||||
language: event.language,
|
||||
}
|
||||
// oxlint-disable-next-line no-restricted-globals -- The generic hook callback remains a union after narrowing by hook name.
|
||||
return Reflect.apply(callback, undefined, [output]).pipe(
|
||||
Effect.tap(() => Effect.sync(() => (event.language = output.language))),
|
||||
)
|
||||
|
||||
@@ -119,7 +119,7 @@ const layer = Layer.effect(
|
||||
)
|
||||
const result = yield* Effect.suspend(() => {
|
||||
// The heterogeneous registry erases handlers after their selected schema validates input.
|
||||
const execution: Effect.Effect<unknown, unknown> = Reflect.apply(handler, undefined, [parsed, callContext])
|
||||
const execution: Effect.Effect<unknown, unknown> = handler(parsed, callContext)
|
||||
return execution
|
||||
}).pipe(
|
||||
Effect.catch((error) => encodeError(method, error)),
|
||||
|
||||
@@ -27,7 +27,7 @@ describe("renderer response headers", () => {
|
||||
test("adds the crash-report document policy only to renderer documents", () => {
|
||||
const document = {}
|
||||
addRendererHeaders(document, { document: true })
|
||||
expect(Reflect.get(document, documentPolicyHeader)).toEqual([jsCallStacksDocumentPolicy])
|
||||
expect(document[documentPolicyHeader]).toEqual([jsCallStacksDocumentPolicy])
|
||||
const asset = {}
|
||||
addRendererHeaders(asset, { document: false })
|
||||
expect(Object.keys(asset)).not.toContain(documentPolicyHeader)
|
||||
|
||||
@@ -7,7 +7,7 @@ export const jsCallStacksDocumentPolicy = "include-js-call-stacks-in-crash-repor
|
||||
// A server's own `Access-Control-Allow-Headers` is kept as-is. Chromium reuses a cached preflight
|
||||
// only when that header names `authorization` explicitly (`*` never covers it), so overwriting the
|
||||
// server's exact list with `*` forced a fresh OPTIONS round trip in front of every API call.
|
||||
export function addRendererHeaders(headers: object, options: { document: boolean }) {
|
||||
export function addRendererHeaders(headers: Record<string, string | string[]>, options: { document: boolean }) {
|
||||
upsertHeader(headers, "Access-Control-Allow-Origin", ["*"])
|
||||
if (!hasHeader(headers, "Access-Control-Allow-Headers")) {
|
||||
upsertHeader(headers, "Access-Control-Allow-Headers", ["*, authorization"])
|
||||
@@ -19,11 +19,11 @@ export function addRendererHeaders(headers: object, options: { document: boolean
|
||||
if (options.document) upsertHeader(headers, documentPolicyHeader, [jsCallStacksDocumentPolicy])
|
||||
}
|
||||
|
||||
export function hasHeader(headers: object, key: string) {
|
||||
export function hasHeader(headers: Record<string, string | string[]>, key: string) {
|
||||
return Object.keys(headers).some((header) => header.toLowerCase() === key.toLowerCase())
|
||||
}
|
||||
|
||||
export function upsertHeader(headers: object, key: string, value: string | string[]) {
|
||||
export function upsertHeader(headers: Record<string, string | string[]>, key: string, value: string | string[]) {
|
||||
const current = Object.keys(headers).find((header) => header.toLowerCase() === key.toLowerCase())
|
||||
Reflect.set(headers, current ?? key, value)
|
||||
headers[current ?? key] = value
|
||||
}
|
||||
|
||||
@@ -374,7 +374,7 @@ const makeRecordingWebSocketConstructor = (
|
||||
return (url, protocols) => {
|
||||
const sequence = nextSequence++
|
||||
const requestedProtocols = normalizeProtocols(protocols)
|
||||
const native = Reflect.apply(upstream, undefined, [url, protocols])
|
||||
const native = upstream(url, protocols)
|
||||
const events: WebSocketEvent[] = []
|
||||
let opened = false
|
||||
let failed = false
|
||||
@@ -439,12 +439,15 @@ const makeRecordingWebSocketConstructor = (
|
||||
get: (target, property) => {
|
||||
if (property === "send")
|
||||
return (data: string | ArrayBufferLike | Blob | ArrayBufferView) => {
|
||||
// oxlint-disable-next-line no-restricted-globals -- The socket implementation's overloaded send signature is erased by the proxy boundary.
|
||||
Reflect.apply(target.send, target, [data])
|
||||
appendEvent("client", data)
|
||||
}
|
||||
// oxlint-disable-next-line no-restricted-globals -- Proxy forwarding requires receiver-aware property access.
|
||||
const value: unknown = Reflect.get(target, property, target)
|
||||
return typeof value === "function" ? value.bind(target) : value
|
||||
},
|
||||
// oxlint-disable-next-line no-restricted-globals -- Proxy forwarding requires receiver-aware property assignment.
|
||||
set: (target, property, value) => Reflect.set(target, property, value, target),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -99,6 +99,7 @@ describe("WebSocket", () => {
|
||||
Effect.gen(function* () {
|
||||
const constructor = yield* Socket.WebSocketConstructor
|
||||
const options = { headers: { authorization: "Bearer fixture" } }
|
||||
// oxlint-disable-next-line no-restricted-globals -- This test intentionally passes runtime constructor options absent from the public socket type.
|
||||
const socket = Reflect.apply(constructor, undefined, ["wss://echo.example.test/options", options])
|
||||
yield* Effect.callback<void>((resume) => {
|
||||
socket.addEventListener("open", () => {
|
||||
|
||||
@@ -1726,6 +1726,7 @@ function streamDataSchema(schema: Extract<HttpApiSchema.StreamSchema, { readonly
|
||||
}
|
||||
|
||||
function streamEncodedDataSchema(schema: Extract<HttpApiSchema.StreamSchema, { readonly _tag: "StreamSse" }>) {
|
||||
// oxlint-disable-next-line no-restricted-globals -- Effect exposes this runtime helper without a public type.
|
||||
const replaceEncoding: unknown = Reflect.get(SchemaAST, "replaceEncoding")
|
||||
if (typeof replaceEncoding !== "function") throw new GenerationError({ reason: "Invalid SSE data schema" })
|
||||
const ast: unknown = replaceEncoding(streamDataAst(schema.events.ast), undefined)
|
||||
|
||||
@@ -1491,6 +1491,7 @@ describe("HttpApiCodegen.generate", () => {
|
||||
const link = JsonNumber.ast.encoding?.[0]
|
||||
if (link === undefined) throw new Error("Expected JSON number encoding")
|
||||
// This helper is present at runtime but omitted from the public declaration surface.
|
||||
// oxlint-disable-next-line no-restricted-globals -- The test verifies an Effect runtime helper without a public type.
|
||||
const replaceEncoding: unknown = Reflect.get(SchemaAST, "replaceEncoding")
|
||||
if (typeof replaceEncoding !== "function") throw new Error("Expected SchemaAST.replaceEncoding")
|
||||
const ast: unknown = replaceEncoding(JsonNumber.ast, [
|
||||
|
||||
@@ -130,6 +130,7 @@ const rpcFromEffect = Effect.fn("Plugin.Rpc.fromEffect")(function* (host: HostRp
|
||||
try: (signal) => {
|
||||
// SAFETY: Promise RPC handlers return Promise values before this adapter erases their concrete types.
|
||||
// oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion
|
||||
// oxlint-disable-next-line no-restricted-globals -- The portable RPC registry intentionally erases each handler's concrete signature.
|
||||
return Reflect.apply(handler, undefined, [
|
||||
input,
|
||||
{
|
||||
|
||||
@@ -127,4 +127,10 @@ render(() => {
|
||||
)
|
||||
}, document.getElementById("root")!)
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
markdownLifetime: Omit<typeof stats, "cacheChars"> & { cacheChars?: number; cacheBytes?: number }
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(window, "markdownLifetime", { value: stats })
|
||||
|
||||
@@ -14,9 +14,9 @@ for (const scenario of ["mounted", "leave", "shared"]) {
|
||||
await expect(page.locator("body")).toHaveAttribute("data-ready", "true")
|
||||
await expect(page.locator("body")).toHaveAttribute("data-settled", "true")
|
||||
const after = await session.send("Performance.getMetrics")
|
||||
const stats = await page.evaluate(() => Reflect.get(window, "markdownLifetime"))
|
||||
const stats = await page.evaluate(() => window.markdownLifetime)
|
||||
// The frozen initial baseline used a byte label for this character count.
|
||||
stats.cacheChars ??= stats.cacheBytes
|
||||
stats.cacheChars ??= stats.cacheBytes ?? 0
|
||||
delete stats.cacheBytes
|
||||
expect(stats.requests).toBe(1)
|
||||
expect(stats.responses).toBe(1)
|
||||
|
||||
@@ -50,7 +50,9 @@ function all(renderable: Renderable): Renderable[] {
|
||||
}
|
||||
|
||||
function mouseListeners(renderable: Renderable) {
|
||||
// oxlint-disable-next-line no-restricted-globals -- OpenTUI does not expose listener state through its public API.
|
||||
const general = Reflect.get(renderable, "_mouseListener")
|
||||
// oxlint-disable-next-line no-restricted-globals -- OpenTUI does not expose listener state through its public API.
|
||||
const specific = Reflect.get(renderable, "_mouseListeners")
|
||||
return Boolean(general) || (specific && typeof specific === "object" && Object.keys(specific).length > 0)
|
||||
}
|
||||
@@ -87,6 +89,7 @@ export function createHarness(renderer: CliRenderer): Harness {
|
||||
// captureCharFrame follows the test renderer's output sink. Recording
|
||||
// redirects that sink to the timeline, so read the live render buffer
|
||||
// instead; it is also the source used by screenshots.
|
||||
// oxlint-disable-next-line no-restricted-globals -- OpenTUI does not expose the live render buffer publicly.
|
||||
screen: () => decoder.decode((Reflect.get(renderer, "currentRenderBuffer") as RenderBuffer).getRealCharBytes()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,14 +7,12 @@ import type { SimulationProtocol } from "../protocol"
|
||||
export type Definition = Omit<SimulationProtocol.Frontend.SemanticNode, "id" | "element" | "parent">
|
||||
|
||||
const key = Symbol.for("opencode.simulation.semantics")
|
||||
type SemanticRenderable = Renderable & { [key]?: () => Definition }
|
||||
|
||||
const bind = (definition: () => Definition) => (renderable: Renderable) => {
|
||||
Object.defineProperty(renderable, key, { value: definition, configurable: true })
|
||||
}
|
||||
|
||||
export const read = (renderable: Renderable) => {
|
||||
const definition: unknown = Reflect.get(renderable, key)
|
||||
return typeof definition === "function" ? (definition as () => Definition) : undefined
|
||||
}
|
||||
export const read = (renderable: Renderable) => (renderable as SemanticRenderable)[key]
|
||||
|
||||
export const SimulationSemantics = { bind, read }
|
||||
|
||||
@@ -82,7 +82,9 @@ test.each(["pointer", "output"])("joins both recording streams after an early %s
|
||||
if (failed === "pointer") await mkdir(join(directory, "timeline.pointers.jsonl"))
|
||||
const error = new Promise<Error>((resolve) => timeline.once("error", resolve))
|
||||
timeline.pointer("move", 10, 5)
|
||||
// oxlint-disable-next-line no-restricted-globals -- This failure-path test must access Timeline's owned private stream.
|
||||
const output: unknown = Reflect.get(timeline, "output")
|
||||
// oxlint-disable-next-line no-restricted-globals -- This failure-path test must access Timeline's owned private stream.
|
||||
const pointers: unknown = Reflect.get(timeline, "pointers")
|
||||
if (!(output instanceof WriteStream) || !(pointers instanceof WriteStream)) throw new Error("missing owned streams")
|
||||
if (failed === "output") {
|
||||
@@ -95,7 +97,9 @@ test.each(["pointer", "output"])("joins both recording streams after an early %s
|
||||
await expect(finishing).rejects.toBe(failure)
|
||||
expect(output.closed).toBe(true)
|
||||
expect(pointers.closed).toBe(true)
|
||||
// oxlint-disable-next-line no-restricted-globals -- Node does not expose the stream descriptor state publicly.
|
||||
expect(Reflect.get(output, "fd")).toBeNull()
|
||||
// oxlint-disable-next-line no-restricted-globals -- Node does not expose the stream descriptor state publicly.
|
||||
expect(Reflect.get(pointers, "fd")).toBeNull()
|
||||
timeline.pointer("move", 99, 99)
|
||||
expect(timeline.finish()).toBe(finishing)
|
||||
|
||||
@@ -232,9 +232,9 @@ export function formDisplay(field: FormAnswerField, value: FormValue | undefined
|
||||
export function formErrorMessage(error: unknown) {
|
||||
if (typeof error === "string" && error.trim()) return error
|
||||
if (error && typeof error === "object") {
|
||||
const message = Reflect.get(error, "message")
|
||||
const message = "message" in error ? error.message : undefined
|
||||
if (typeof message === "string" && message.trim()) return message
|
||||
const tag = Reflect.get(error, "_tag")
|
||||
const tag = "_tag" in error ? error._tag : undefined
|
||||
if (typeof tag === "string" && tag.trim()) return tag
|
||||
}
|
||||
return "Form request failed"
|
||||
|
||||
@@ -100,6 +100,7 @@ function monoCode(renderable: CodeRenderable): void {
|
||||
const initialSetter = initialDescriptor.set.bind(renderable)
|
||||
const contentGetter = contentDescriptor.get.bind(renderable)
|
||||
const contentSetter = contentDescriptor.set.bind(renderable)
|
||||
// oxlint-disable-next-line no-restricted-globals -- OpenTUI does not expose its eager styled-text cache publicly.
|
||||
const initial = Reflect.get(renderable, "_initialStyledText")
|
||||
Object.defineProperty(renderable, "initialStyledText", {
|
||||
configurable: true,
|
||||
@@ -111,6 +112,7 @@ function monoCode(renderable: CodeRenderable): void {
|
||||
configurable: true,
|
||||
get: contentGetter,
|
||||
set(value: string) {
|
||||
// oxlint-disable-next-line no-restricted-globals -- OpenTUI does not expose its eager styled-text cache publicly.
|
||||
if (renderable.filetype !== "markdown" || !isStyledText(Reflect.get(renderable, "_initialStyledText"))) {
|
||||
renderable.drawUnstyledText = true
|
||||
renderable.initialStyledText = stringToStyledText(value)
|
||||
@@ -137,6 +139,7 @@ function monoCode(renderable: CodeRenderable): void {
|
||||
function monoTreeSitter(client: TreeSitterClient): TreeSitterClient {
|
||||
return new Proxy(client, {
|
||||
get(target, property) {
|
||||
// oxlint-disable-next-line no-restricted-globals -- Proxy forwarding requires receiver-aware property access.
|
||||
if (property !== "highlightOnce") return Reflect.get(target, property, target)
|
||||
// Keep parser failures on the chunk path instead of OpenTUI's raw-text fallback.
|
||||
return (...args: Parameters<TreeSitterClient["highlightOnce"]>) =>
|
||||
|
||||
@@ -159,7 +159,7 @@ function formRequestOptions(location: LocationRef | undefined) {
|
||||
}
|
||||
|
||||
function formAlreadySettled(error: unknown) {
|
||||
return !!error && typeof error === "object" && Reflect.get(error, "_tag") === "FormAlreadySettledError"
|
||||
return !!error && typeof error === "object" && "_tag" in error && error._tag === "FormAlreadySettledError"
|
||||
}
|
||||
|
||||
const RESIZE_DELAY = 250
|
||||
|
||||
@@ -168,9 +168,9 @@ export function formatUnknownError(error: unknown): string {
|
||||
if (typeof error === "string") return error
|
||||
if (error instanceof Error) return error.message || error.name
|
||||
if (error && typeof error === "object") {
|
||||
const message = Reflect.get(error, "message")
|
||||
const message = "message" in error ? error.message : undefined
|
||||
if (typeof message === "string" && message.trim()) return message
|
||||
const tag = Reflect.get(error, "_tag")
|
||||
const tag = "_tag" in error ? error._tag : undefined
|
||||
if (typeof tag === "string" && tag.trim()) return tag
|
||||
}
|
||||
return "unknown error"
|
||||
|
||||
@@ -104,6 +104,6 @@ export function createSourceWatcher(onChange: () => void) {
|
||||
|
||||
function isMissing(error: unknown) {
|
||||
if (!error || typeof error !== "object") return false
|
||||
const code = Reflect.get(error, "code")
|
||||
const code = "code" in error ? error.code : undefined
|
||||
return code === "ENOENT" || code === "ENOTDIR"
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ export async function discoverThemes(directories: string[]) {
|
||||
for (const directory of directories) {
|
||||
const themeDirectory = path.join(directory, "themes")
|
||||
const entries = await readdir(themeDirectory, { withFileTypes: true }).catch((error: unknown) => {
|
||||
if (error && typeof error === "object" && Reflect.get(error, "code") === "ENOENT") return []
|
||||
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return []
|
||||
return Promise.reject(error)
|
||||
})
|
||||
const files = entries
|
||||
|
||||
@@ -31,7 +31,7 @@ export async function localProjectDirectory(cwd: string) {
|
||||
|
||||
export function isMissingPath(error: unknown) {
|
||||
if (!error || typeof error !== "object") return false
|
||||
const code = Reflect.get(error, "code")
|
||||
const code = "code" in error ? error.code : undefined
|
||||
return code === "ENOENT" || code === "ENOTDIR"
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ async function setup(
|
||||
out.renderer.destroy()
|
||||
})
|
||||
out.renderer.on(CliRenderEvents.EXTERNAL_OUTPUT, () => {
|
||||
// oxlint-disable-next-line no-restricted-globals -- RunScrollbackStream does not expose its image surface publicly.
|
||||
const surface = Reflect.get(out.scrollback, "imageSurface") as ScrollbackSurface | undefined
|
||||
if (!surface || out.previews.some((preview) => preview.surface === surface)) return
|
||||
const image = surface.root.getChildren().find((child) => child instanceof ImageRenderable)
|
||||
@@ -126,6 +127,7 @@ test("reserves wrapped caption rows and allows caption-only output in a short te
|
||||
|
||||
test("uses physical cell geometry without enlarging a small image", async () => {
|
||||
const out = await setup({ imagePreview: true })
|
||||
// oxlint-disable-next-line no-restricted-globals -- OpenTUI does not expose test resolution mutation publicly.
|
||||
Reflect.set(out.renderer, "_resolution", { width: 800, height: 720 })
|
||||
await out.scrollback.append(image(wide))
|
||||
expect(out.previews[0]!.size).toEqual({ width: 6, height: 1 })
|
||||
@@ -196,6 +198,7 @@ test.each(["stream", "renderer"])("disposes an image load when the %s is destroy
|
||||
const out = await setup({ imagePreview: true })
|
||||
const pending = out.scrollback.append(image())
|
||||
await Promise.resolve()
|
||||
// oxlint-disable-next-line no-restricted-globals -- RunScrollbackStream does not expose its image surface publicly.
|
||||
const surface = Reflect.get(out.scrollback, "imageSurface") as ScrollbackSurface
|
||||
const preview = surface.root.getChildren().find((child) => child instanceof ImageRenderable)!
|
||||
expect(preview.loading).toBe(true)
|
||||
|
||||
@@ -29,6 +29,7 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
function claim(renderer: TestRenderer): ClaimedCommit[] {
|
||||
// oxlint-disable-next-line no-restricted-globals -- OpenTUI does not expose its external-output queue publicly.
|
||||
const queue = Reflect.get(renderer, "externalOutputQueue")
|
||||
if (!queue || typeof queue !== "object" || !("claim" in queue) || typeof queue.claim !== "function") {
|
||||
throw new Error("renderer missing external output queue")
|
||||
@@ -183,6 +184,7 @@ test("theme swaps restyle active reasoning without resetting the stream", async
|
||||
})
|
||||
|
||||
function activeSyntax(scrollback: RunScrollbackStream) {
|
||||
// oxlint-disable-next-line no-restricted-globals -- RunScrollbackStream does not expose its active entry publicly.
|
||||
const entry = Reflect.get(scrollback, "active") as { renderable?: { syntaxStyle?: SyntaxStyle } } | undefined
|
||||
return entry?.renderable?.syntaxStyle
|
||||
}
|
||||
@@ -277,12 +279,14 @@ test("renders monochrome scrollback as ASCII markdown", async () => {
|
||||
|
||||
try {
|
||||
await out.scrollback.append(assistant("# H"))
|
||||
// oxlint-disable-next-line no-restricted-globals -- RunScrollbackStream does not expose its active entry publicly.
|
||||
expect(Reflect.get(out.scrollback, "active")?.renderable).toBeInstanceOf(MarkdownRenderable)
|
||||
await out.scrollback.append(
|
||||
assistant(
|
||||
"éading →\n\n> “quote”\n\n---\n\n| A | B |\n| - | - |\n| α | β |\n\n• literal\n\n———\n\n[café](https://example.com/café)",
|
||||
),
|
||||
)
|
||||
// oxlint-disable-next-line no-restricted-globals -- RunScrollbackStream does not expose its active entry publicly.
|
||||
const active: unknown = Reflect.get(out.scrollback, "active")
|
||||
const renderable =
|
||||
active && typeof active === "object" && "renderable" in active && active.renderable instanceof MarkdownRenderable
|
||||
|
||||
Reference in New Issue
Block a user