From 1eeaaab7a6acbbd9143af913728df7a933206412 Mon Sep 17 00:00:00 2001 From: Dax Date: Sun, 13 Sep 2026 17:08:43 -0400 Subject: [PATCH] refactor: restrict reflective access (#48860) --- .../workflows/{typecheck.yml => check.yml} | 8 +-- .husky/pre-push | 2 +- .oxlintrc.json | 51 +++++-------------- AGENTS.md | 5 +- package.json | 1 + packages/ai/test/provider-package.test.ts | 11 ++++ packages/ai/test/tool-runtime.test.ts | 14 ++--- .../performance/markdown/lifetime.bench.ts | 8 +-- .../app/e2e/performance/markdown/probe.ts | 17 +++++++ .../app/e2e/performance/terminals/probe.ts | 2 +- .../terminals/terminal-benchmark.spec.ts | 2 +- .../timeline/session-timeline-stream-probe.ts | 2 +- .../e2e/regression/browser-polyfills.spec.ts | 4 +- .../app/src/runtime/server/health.test.ts | 4 +- packages/app/test-browser/pwa-route.test.ts | 2 +- .../app/test-browser/solid-virtual.test.ts | 1 + packages/cli/src/acp/permission.ts | 2 +- packages/cli/src/run/noninteractive.ts | 2 +- packages/cli/src/session-target.ts | 2 +- packages/cli/test/acp/event.test.ts | 2 +- .../cli/test/acp/permission-behavior.test.ts | 2 +- packages/cli/test/acp/service-usage.test.ts | 2 +- packages/cli/test/acp/sse-fixture.ts | 2 +- packages/cli/test/config.test.ts | 1 + .../cli/test/fixture/config-concurrency.ts | 1 + packages/codemode/src/stdlib/array.ts | 2 +- packages/core/src/config/plugin/skill-file.ts | 2 +- packages/core/src/plugin/hooks.ts | 6 +-- packages/core/src/plugin/host.ts | 2 + packages/core/src/rpc.ts | 2 +- .../desktop/src/main/windows/headers.test.ts | 2 +- packages/desktop/src/main/windows/headers.ts | 8 +-- .../http-recorder/src/websocket/recorder.ts | 5 +- packages/http-recorder/test/websocket.test.ts | 1 + packages/httpapi-codegen/src/index.ts | 1 + .../httpapi-codegen/test/generate.test.ts | 1 + packages/plugin/src/promise/adapter.ts | 1 + .../performance/markdown-lifetime/fixture.tsx | 6 +++ .../markdown-lifetime/lifetime.bench.ts | 4 +- packages/simulation/src/frontend/actions.ts | 3 ++ packages/simulation/src/frontend/semantics.ts | 6 +-- packages/simulation/test/recording.test.ts | 4 ++ packages/tui/src/mini/form.shared.ts | 4 +- packages/tui/src/mini/mono.ts | 3 ++ packages/tui/src/mini/runtime.ts | 2 +- packages/tui/src/mini/stream-v2.transport.ts | 4 +- packages/tui/src/plugin/watch.ts | 2 +- packages/tui/src/theme/discovery.ts | 2 +- packages/tui/src/util/config-directories.ts | 2 +- .../tui/test/mini/scrollback.image.test.ts | 3 ++ .../tui/test/mini/scrollback.surface.test.ts | 4 ++ 51 files changed, 135 insertions(+), 97 deletions(-) rename .github/workflows/{typecheck.yml => check.yml} (80%) diff --git a/.github/workflows/typecheck.yml b/.github/workflows/check.yml similarity index 80% rename from .github/workflows/typecheck.yml rename to .github/workflows/check.yml index 5c83a8e691d..9782586179e 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/check.yml @@ -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 diff --git a/.husky/pre-push b/.husky/pre-push index 5d3cc53411b..77a90681e27 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -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 diff --git a/.oxlintrc.json b/.oxlintrc.json index 5cdb26f48c5..c76d2168f9e 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -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"] } diff --git a/AGENTS.md b/AGENTS.md index 01d55bc1c71..0a6c4fb2363 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/package.json b/package.json index 3415fe7cb57..35e392e5c0b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/ai/test/provider-package.test.ts b/packages/ai/test/provider-package.test.ts index f74bd59eecf..070dd082b9f 100644 --- a/packages/ai/test/provider-package.test.ts +++ b/packages/ai/test/provider-package.test.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" }, ]), diff --git a/packages/ai/test/tool-runtime.test.ts b/packages/ai/test/tool-runtime.test.ts index bcd53b2c382..25ebed88f38 100644 --- a/packages/ai/test/tool-runtime.test.ts +++ b/packages/ai/test/tool-runtime.test.ts @@ -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) }) diff --git a/packages/app/e2e/performance/markdown/lifetime.bench.ts b/packages/app/e2e/performance/markdown/lifetime.bench.ts index 0f12e4d8958..03f905e9ca9 100644 --- a/packages/app/e2e/performance/markdown/lifetime.bench.ts +++ b/packages/app/e2e/performance/markdown/lifetime.bench.ts @@ -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, diff --git a/packages/app/e2e/performance/markdown/probe.ts b/packages/app/e2e/performance/markdown/probe.ts index 8bd250e7add..a8bb278843b 100644 --- a/packages/app/e2e/performance/markdown/probe.ts +++ b/packages/app/e2e/performance/markdown/probe.ts @@ -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 }, diff --git a/packages/app/e2e/performance/terminals/probe.ts b/packages/app/e2e/performance/terminals/probe.ts index d2c1c98c71c..9d4617db042 100644 --- a/packages/app/e2e/performance/terminals/probe.ts +++ b/packages/app/e2e/performance/terminals/probe.ts @@ -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) } } } diff --git a/packages/app/e2e/performance/terminals/terminal-benchmark.spec.ts b/packages/app/e2e/performance/terminals/terminal-benchmark.spec.ts index d26e661148a..e46f672d8d5 100644 --- a/packages/app/e2e/performance/terminals/terminal-benchmark.spec.ts +++ b/packages/app/e2e/performance/terminals/terminal-benchmark.spec.ts @@ -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) } }) } diff --git a/packages/app/e2e/performance/timeline/session-timeline-stream-probe.ts b/packages/app/e2e/performance/timeline/session-timeline-stream-probe.ts index eae67478a97..9276766cbf7 100644 --- a/packages/app/e2e/performance/timeline/session-timeline-stream-probe.ts +++ b/packages/app/e2e/performance/timeline/session-timeline-stream-probe.ts @@ -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", { diff --git a/packages/app/e2e/regression/browser-polyfills.spec.ts b/packages/app/e2e/regression/browser-polyfills.spec.ts index 1f6dd3bf4cb..5f12906b7da 100644 --- a/packages/app/e2e/regression/browser-polyfills.spec.ts +++ b/packages/app/e2e/regression/browser-polyfills.spec.ts @@ -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).groupBy + delete (Promise as Partial).withResolvers localStorage.setItem( "opencode.global.dat:server", JSON.stringify({ diff --git a/packages/app/src/runtime/server/health.test.ts b/packages/app/src/runtime/server/health.test.ts index f4787c18f5a..9f145285ab5 100644 --- a/packages/app/src/runtime/server/health.test.ts +++ b/packages/app/src/runtime/server/health.test.ts @@ -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).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).timeout }) expect(aborted).toBe(true) diff --git a/packages/app/test-browser/pwa-route.test.ts b/packages/app/test-browser/pwa-route.test.ts index 78587ba74f3..044f865b8cc 100644 --- a/packages/app/test-browser/pwa-route.test.ts +++ b/packages/app/test-browser/pwa-route.test.ts @@ -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 } }) diff --git a/packages/app/test-browser/solid-virtual.test.ts b/packages/app/test-browser/solid-virtual.test.ts index 8b8bfa20be7..0862e5c3e32 100644 --- a/packages/app/test-browser/solid-virtual.test.ts +++ b/packages/app/test-browser/solid-virtual.test.ts @@ -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) }, }) diff --git a/packages/cli/src/acp/permission.ts b/packages/cli/src/acp/permission.ts index 83c8e2f28f1..0ae41f04130 100644 --- a/packages/cli/src/acp/permission.ts +++ b/packages/cli/src/acp/permission.ts @@ -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] : [] }) : [] diff --git a/packages/cli/src/run/noninteractive.ts b/packages/cli/src/run/noninteractive.ts index e97898d5b01..ac21f21d1ab 100644 --- a/packages/cli/src/run/noninteractive.ts +++ b/packages/cli/src/run/noninteractive.ts @@ -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) { diff --git a/packages/cli/src/session-target.ts b/packages/cli/src/session-target.ts index 06b5f1b67d4..2896492b3eb 100644 --- a/packages/cli/src/session-target.ts +++ b/packages/cli/src/session-target.ts @@ -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 }) diff --git a/packages/cli/test/acp/event.test.ts b/packages/cli/test/acp/event.test.ts index 5ed9f5da066..8b01fc3ac73 100644 --- a/packages/cli/test/acp/event.test.ts +++ b/packages/cli/test/acp/event.test.ts @@ -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 diff --git a/packages/cli/test/acp/permission-behavior.test.ts b/packages/cli/test/acp/permission-behavior.test.ts index 0290c873040..02c688cc049 100644 --- a/packages/cli/test/acp/permission-behavior.test.ts +++ b/packages/cli/test/acp/permission-behavior.test.ts @@ -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]] : [] }) } diff --git a/packages/cli/test/acp/service-usage.test.ts b/packages/cli/test/acp/service-usage.test.ts index 2e104c27d4d..99ac2545af5 100644 --- a/packages/cli/test/acp/service-usage.test.ts +++ b/packages/cli/test/acp/service-usage.test.ts @@ -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 } diff --git a/packages/cli/test/acp/sse-fixture.ts b/packages/cli/test/acp/sse-fixture.ts index 8b8516a99aa..8ec7be8b5f1 100644 --- a/packages/cli/test/acp/sse-fixture.ts +++ b/packages/cli/test/acp/sse-fixture.ts @@ -190,7 +190,7 @@ export async function withTimeout(promise: Promise, 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)[key] return typeof field === "string" ? field : undefined } diff --git a/packages/cli/test/config.test.ts b/packages/cli/test/config.test.ts index 335f7b0ac21..5b0d92dd7f3 100644 --- a/packages/cli/test/config.test.ts +++ b/packages/cli/test/config.test.ts @@ -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) }, }) diff --git a/packages/cli/test/fixture/config-concurrency.ts b/packages/cli/test/fixture/config-concurrency.ts index 2460267506d..a85e8a9032e 100644 --- a/packages/cli/test/fixture/config-concurrency.ts +++ b/packages/cli/test/fixture/config-concurrency.ts @@ -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) }, }) diff --git a/packages/codemode/src/stdlib/array.ts b/packages/codemode/src/stdlib/array.ts index dd41ee7eed3..25c8b0b6218 100644 --- a/packages/codemode/src/stdlib/array.ts +++ b/packages/codemode/src/stdlib/array.ts @@ -249,7 +249,7 @@ export const arrayGlobal = (runner: Runner) => { items[index] = item }) Array.from({ length: holeCount }, (_, index) => itemCount + index).forEach((index) => { - Reflect.deleteProperty(items, index) + delete items[index] }) return target }) diff --git a/packages/core/src/config/plugin/skill-file.ts b/packages/core/src/config/plugin/skill-file.ts index c7e04d64ffa..5b029bc095d 100644 --- a/packages/core/src/config/plugin/skill-file.ts +++ b/packages/core/src/config/plugin/skill-file.ts @@ -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)[key] if (typeof value === "boolean") return value if (typeof value !== "string") return undefined const normalized = value.trim().toLowerCase() diff --git a/packages/core/src/plugin/hooks.ts b/packages/core/src/plugin/hooks.ts index 1cfd2e63772..1325837f9d5 100644 --- a/packages/core/src/plugin/hooks.ts +++ b/packages/core/src/plugin/hooks.ts @@ -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 = Reflect.apply( - entry.callback, - undefined, - [event], - ) + const result: Effect.Effect = entry.callback(event) yield* result } return event diff --git a/packages/core/src/plugin/host.ts b/packages/core/src/plugin/host.ts index bd95149db81..0f1c4a0c534 100644 --- a/packages/core/src/plugin/host.ts +++ b/packages/core/src/plugin/host.ts @@ -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))), ) diff --git a/packages/core/src/rpc.ts b/packages/core/src/rpc.ts index f7e65a9054a..4e4c054ecd8 100644 --- a/packages/core/src/rpc.ts +++ b/packages/core/src/rpc.ts @@ -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 = Reflect.apply(handler, undefined, [parsed, callContext]) + const execution: Effect.Effect = handler(parsed, callContext) return execution }).pipe( Effect.catch((error) => encodeError(method, error)), diff --git a/packages/desktop/src/main/windows/headers.test.ts b/packages/desktop/src/main/windows/headers.test.ts index b3289ce9201..a08fd42ce57 100644 --- a/packages/desktop/src/main/windows/headers.test.ts +++ b/packages/desktop/src/main/windows/headers.test.ts @@ -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) diff --git a/packages/desktop/src/main/windows/headers.ts b/packages/desktop/src/main/windows/headers.ts index 8ef360121a0..a092d5dee69 100644 --- a/packages/desktop/src/main/windows/headers.ts +++ b/packages/desktop/src/main/windows/headers.ts @@ -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, 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, 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, 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 } diff --git a/packages/http-recorder/src/websocket/recorder.ts b/packages/http-recorder/src/websocket/recorder.ts index b2b38502142..489e5bdb940 100644 --- a/packages/http-recorder/src/websocket/recorder.ts +++ b/packages/http-recorder/src/websocket/recorder.ts @@ -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), }) } diff --git a/packages/http-recorder/test/websocket.test.ts b/packages/http-recorder/test/websocket.test.ts index 9e641c5c5db..2856d157cee 100644 --- a/packages/http-recorder/test/websocket.test.ts +++ b/packages/http-recorder/test/websocket.test.ts @@ -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((resume) => { socket.addEventListener("open", () => { diff --git a/packages/httpapi-codegen/src/index.ts b/packages/httpapi-codegen/src/index.ts index d945ece0b74..84a4c78662b 100644 --- a/packages/httpapi-codegen/src/index.ts +++ b/packages/httpapi-codegen/src/index.ts @@ -1726,6 +1726,7 @@ function streamDataSchema(schema: Extract) { + // 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) diff --git a/packages/httpapi-codegen/test/generate.test.ts b/packages/httpapi-codegen/test/generate.test.ts index 78cf1852f5b..40dbe076c5e 100644 --- a/packages/httpapi-codegen/test/generate.test.ts +++ b/packages/httpapi-codegen/test/generate.test.ts @@ -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, [ diff --git a/packages/plugin/src/promise/adapter.ts b/packages/plugin/src/promise/adapter.ts index ca6373cd2f3..a770a159712 100644 --- a/packages/plugin/src/promise/adapter.ts +++ b/packages/plugin/src/promise/adapter.ts @@ -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, { diff --git a/packages/session-ui/performance/markdown-lifetime/fixture.tsx b/packages/session-ui/performance/markdown-lifetime/fixture.tsx index 3f5d9a3c120..e5eb7040e5d 100644 --- a/packages/session-ui/performance/markdown-lifetime/fixture.tsx +++ b/packages/session-ui/performance/markdown-lifetime/fixture.tsx @@ -127,4 +127,10 @@ render(() => { ) }, document.getElementById("root")!) +declare global { + interface Window { + markdownLifetime: Omit & { cacheChars?: number; cacheBytes?: number } + } +} + Object.defineProperty(window, "markdownLifetime", { value: stats }) diff --git a/packages/session-ui/performance/markdown-lifetime/lifetime.bench.ts b/packages/session-ui/performance/markdown-lifetime/lifetime.bench.ts index 46992e30261..daf85b9c3a3 100644 --- a/packages/session-ui/performance/markdown-lifetime/lifetime.bench.ts +++ b/packages/session-ui/performance/markdown-lifetime/lifetime.bench.ts @@ -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) diff --git a/packages/simulation/src/frontend/actions.ts b/packages/simulation/src/frontend/actions.ts index 03dbed9d504..e1530fa5b90 100644 --- a/packages/simulation/src/frontend/actions.ts +++ b/packages/simulation/src/frontend/actions.ts @@ -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()), } } diff --git a/packages/simulation/src/frontend/semantics.ts b/packages/simulation/src/frontend/semantics.ts index 91aaa6e88ee..622ecf90519 100644 --- a/packages/simulation/src/frontend/semantics.ts +++ b/packages/simulation/src/frontend/semantics.ts @@ -7,14 +7,12 @@ import type { SimulationProtocol } from "../protocol" export type Definition = Omit 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 } diff --git a/packages/simulation/test/recording.test.ts b/packages/simulation/test/recording.test.ts index b54f6201711..00b54d37c73 100644 --- a/packages/simulation/test/recording.test.ts +++ b/packages/simulation/test/recording.test.ts @@ -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((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) diff --git a/packages/tui/src/mini/form.shared.ts b/packages/tui/src/mini/form.shared.ts index da17b5a953f..50f7a13b57f 100644 --- a/packages/tui/src/mini/form.shared.ts +++ b/packages/tui/src/mini/form.shared.ts @@ -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" diff --git a/packages/tui/src/mini/mono.ts b/packages/tui/src/mini/mono.ts index 9760996e28f..b2254e24599 100644 --- a/packages/tui/src/mini/mono.ts +++ b/packages/tui/src/mini/mono.ts @@ -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) => diff --git a/packages/tui/src/mini/runtime.ts b/packages/tui/src/mini/runtime.ts index ad7297dcf00..3d3a200deaf 100644 --- a/packages/tui/src/mini/runtime.ts +++ b/packages/tui/src/mini/runtime.ts @@ -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 diff --git a/packages/tui/src/mini/stream-v2.transport.ts b/packages/tui/src/mini/stream-v2.transport.ts index a775a244cb2..e5937e32443 100644 --- a/packages/tui/src/mini/stream-v2.transport.ts +++ b/packages/tui/src/mini/stream-v2.transport.ts @@ -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" diff --git a/packages/tui/src/plugin/watch.ts b/packages/tui/src/plugin/watch.ts index 35617a65a7d..9a33a4d36ae 100644 --- a/packages/tui/src/plugin/watch.ts +++ b/packages/tui/src/plugin/watch.ts @@ -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" } diff --git a/packages/tui/src/theme/discovery.ts b/packages/tui/src/theme/discovery.ts index 43e685ae6d4..d2bbfa7a819 100644 --- a/packages/tui/src/theme/discovery.ts +++ b/packages/tui/src/theme/discovery.ts @@ -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 diff --git a/packages/tui/src/util/config-directories.ts b/packages/tui/src/util/config-directories.ts index 176599a9ac8..da61a3451dc 100644 --- a/packages/tui/src/util/config-directories.ts +++ b/packages/tui/src/util/config-directories.ts @@ -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" } diff --git a/packages/tui/test/mini/scrollback.image.test.ts b/packages/tui/test/mini/scrollback.image.test.ts index 666e9c01dfd..5cc020a6513 100644 --- a/packages/tui/test/mini/scrollback.image.test.ts +++ b/packages/tui/test/mini/scrollback.image.test.ts @@ -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) diff --git a/packages/tui/test/mini/scrollback.surface.test.ts b/packages/tui/test/mini/scrollback.surface.test.ts index 52aab18367d..1af365ad1d8 100644 --- a/packages/tui/test/mini/scrollback.surface.test.ts +++ b/packages/tui/test/mini/scrollback.surface.test.ts @@ -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