fix(lint): enable anti-slop/no-reflect-get (#20786)

`anti-slop/no-reflect-get` rejects every call to `Reflect.get`. The
reflective read bypasses ordinary property access and throws away the
type evidence the compiler would otherwise give you: the result is
`any`/`unknown` with no narrowing, so a typo in the key or a shape drift
in the source object is invisible until runtime. The rule's remedy is to
parse dynamic input into a named domain type (or narrow it with `in`)
and then read the field normally.

Baseline: 86 violations across 67 files. Now zero unsuppressed
violations under
`npx oxlint --config config/oxlint-anti-slop.json --ignore-pattern 'config/oxlint-plugins/anti-slop/**' src config tests mobile`.

Fix pattern
-----------
44 of the 86 were rewritten. The dominant shape was an `unknown` value
read through `Reflect.get` right after a `typeof === 'object'` guard;
those became `in`-narrowed property access, which TypeScript checks:

  - Reflect.get(value, 'agents')
  + 'agents' in value ? value.agents : null

Two further shapes:
- `Reflect.get(Object(x), 'k')` on a possibly-primitive envelope became a
  small named reader that boxes once and indexes a
  `Record<string, unknown>` (`settingsField` in
  mobile/src/transport/settings-read-operations.ts).
- Tests reaching into private state moved to TypeScript's checked
  bracket-index escape hatch (`runtime['layoutQueues']`), or to a
  documented read-only accessor on the owning class
  (`SearchSubprocessLineAccumulator.retainedCapacityBytes()`,
  `CodexSubagentExecutions.retentionSizes()`).

No type assertion was added anywhere: the diff contains zero net-new
`as` casts, `as any`, `as unknown as`, `@ts-ignore`, or
`@ts-expect-error`, so nothing was laundered into the sibling
assertion rules.

Suppressions
------------
42x `// oxlint-disable-next-line anti-slop/no-reflect-get` across 38
files. Every one is the default-forward branch of a `Proxy` `get` trap:

    get(target, property, receiver) {
      ...
      return Reflect.get(target, property, receiver)
    }

`Reflect.get(target, property, receiver)` is the only construct that
forwards with correct `receiver` semantics; `target[property]` invokes
an accessor with the wrong `this` and silently breaks getters that read
sibling state. There is no typed alternative, so these are suppressed
rather than rewritten.

3x `// oxlint-disable-next-line typescript-eslint/consistent-type-definitions
-- declaration merging requires interface` in
tests/e2e/github-url-smart-input-transition.spec.ts,
tests/e2e/linear-url-workspace-entry.spec.ts, and
tests/e2e/worktree-active-delete-scroll-position.spec.ts. Replacing
`Reflect.get(window, 'x')` with typed `window.x` requires a
`declare global { interface Window }` block, and `interface` is
mandatory for declaration merging. Matches the existing convention at
tests/e2e/helpers/runtime-types.ts:63.

1x `// eslint-disable-next-line no-var -- main-process gate handle for
this spec` in tests/e2e/project-group-creation-visibility.spec.ts, for
the same reason a `var` global is needed to type the handle. Matches
tests/e2e/agent-session-log-tail-stability.spec.ts:24.

Also updates two source-text anchors in mobile's rpc-recording mutation
harness (mobile/src/test-support/rpc-recording/operation-mutations.ts
and recording-runner.test.ts), which pin the exact text of the rewritten
line in settings-read-operations.ts and would otherwise fail with
"Mutant anchor matched 0 sites, expected 1".
This commit is contained in:
Neil
2026-09-15 01:24:30 -07:00
committed by GitHub
parent f7b2736d6d
commit f107499e44
68 changed files with 211 additions and 96 deletions
+1 -1
View File
@@ -33,7 +33,7 @@
"anti-slop/no-object-parameters": "off",
"anti-slop/no-reduce-accumulator-copy": "error",
"anti-slop/no-reflect-apply": "error",
"anti-slop/no-reflect-get": "off",
"anti-slop/no-reflect-get": "error",
"anti-slop/no-runtime-typeof": "off",
"anti-slop/no-shape-in-symbol-names": "off",
"anti-slop/no-unknown-parameters": "off",
@@ -36,8 +36,8 @@ export const OPERATION_MUTATIONS = {
// Reads the overrides one level above the settings envelope.
'bot-overrides-envelope': {
file: 'settings-read-operations.ts',
before: "settings == null ? undefined : Reflect.get(Object(settings), 'prBotAuthorOverrides')",
after: "raw == null ? undefined : Reflect.get(Object(raw), 'prBotAuthorOverrides')"
before: "settings == null ? undefined : settingsField(settings, 'prBotAuthorOverrides')",
after: "raw == null ? undefined : settingsField(raw, 'prBotAuthorOverrides')"
},
// Publishes the settings envelope instead of the accepted operation value.
'workspace-context-envelope': {
@@ -445,7 +445,7 @@ describe('recording boundaries', () => {
const root = mkdtempSync(join(tmpdir(), 'rpc-mutant-'))
try {
const anchor =
"const overrides = settings == null ? undefined : Reflect.get(Object(settings), 'prBotAuthorOverrides')"
"const overrides = settings == null ? undefined : settingsField(settings, 'prBotAuthorOverrides')"
mkdirSync(join(root, 'mod'), { recursive: true })
writeFileSync(
join(root, 'mod/settings-read-operations.ts'),
@@ -7,6 +7,12 @@ function settingsMember(raw: unknown): unknown {
return boxed!.settings
}
// Box primitives so a non-object settings value reads as absent instead of throwing.
function settingsField(settings: unknown, key: string): unknown {
const boxed: Record<string, unknown> = Object(settings)
return boxed[key]
}
// Settings remain opaque: callers historically retain fields without validating their shapes.
const settingsReader: RpcCompatibleReader<unknown, 'settings-member', unknown> = (raw) => ({
compatible: true,
@@ -27,7 +33,7 @@ const optionalSettingsReader: RpcCompatibleReader<unknown, 'optional-settings-me
const botOverridesReader: RpcCompatibleReader<unknown, 'bot-logins', string[]> = (raw) => {
const settings = raw == null ? undefined : settingsMember(raw)
const overrides: unknown =
settings == null ? undefined : Reflect.get(Object(settings), 'prBotAuthorOverrides')
settings == null ? undefined : settingsField(settings, 'prBotAuthorOverrides')
return {
compatible: true,
variant: 'bot-logins',
@@ -85,7 +91,7 @@ export const newTabSettingsRead = bindDeferredRpcOperation(
const copyTrimsGutterReader: RpcCompatibleReader<unknown, 'copy-trims-gutter', boolean> = (raw) => {
const settings = raw == null ? undefined : settingsMember(raw)
const trims: unknown =
settings == null ? undefined : Reflect.get(Object(settings), 'terminalCopyTrimsGutter')
settings == null ? undefined : settingsField(settings, 'terminalCopyTrimsGutter')
return {
compatible: true,
variant: 'copy-trims-gutter',
@@ -53,10 +53,12 @@ export function readManagedHookDetectionResult(value: unknown): {
if (value === null || typeof value !== 'object') {
return { agents: [], claudeVersion: null }
}
const agents = detectedManagedHookAgents(Reflect.get(value, 'agents'))
const versions = Reflect.get(value, 'versions')
const agents = detectedManagedHookAgents('agents' in value ? value.agents : null)
const versions = 'versions' in value ? value.versions : null
const rawClaudeVersion =
versions !== null && typeof versions === 'object' ? Reflect.get(versions, 'claude') : null
versions !== null && typeof versions === 'object' && 'claude' in versions
? versions.claude
: null
return {
agents,
claudeVersion: parseClaudeCliVersion(
@@ -88,7 +88,7 @@ describe('independent JSON boundary review', () => {
expect(await parseHermesSessionDocument(file, bytes(content, 1), 'linux', options)).toEqual(
await parseHermesSessionContent(file, content, 'linux', options)
)
expect(Reflect.get({}, 'polluted')).toBeUndefined()
expect('polluted' in {}).toBe(false)
})
for (const content of [
'{"messages":[],}',
+2 -5
View File
@@ -9,6 +9,7 @@ import {
readQuestionIds,
readQuestionOptionAnswers
} from './codex-prompt-registry-bounds'
import { readRecord, readString as readRecordString } from './codex-item-field-readers'
export const CODEX_COMMAND_APPROVAL_METHOD = 'item/commandExecution/requestApproval'
export const CODEX_FILE_CHANGE_APPROVAL_METHOD = 'item/fileChange/requestApproval'
@@ -36,11 +37,7 @@ export type CodexPromptClaim = {
}
function readString(params: unknown, key: string): string | null {
if (typeof params !== 'object' || params === null) {
return null
}
const value = Reflect.get(params, key)
return typeof value === 'string' && value.length > 0 ? value : null
return readRecordString(readRecord(params), key)
}
export function isCodexPromptMethod(method: string): boolean {
@@ -13,8 +13,9 @@ describe('CodexSubagentExecutions retention and identity', () => {
executions.observeTurn(id, id, 'completed')
}
expect(executions.workingChildren().map((child) => child.agentThreadId)).toEqual(['long-lived'])
expect(Reflect.get(executions, 'children').size).toBeLessThanOrEqual(128)
expect(Reflect.get(executions, 'settledTurns').size).toBeLessThanOrEqual(256)
const { children, settledTurns } = executions.retentionSizes()
expect(children).toBeLessThanOrEqual(128)
expect(settledTurns).toBeLessThanOrEqual(256)
})
it('retains early live owner events at capacity and makes room only after settlement', () => {
@@ -45,7 +46,6 @@ describe('CodexSubagentExecutions retention and identity', () => {
executions.observeTurn('child', 'turn', 'failed')
expect(executions.workingChildren()[0]?.execution?.turnId).toBe('new-turn')
executions.clear()
expect(Reflect.get(executions, 'children').size).toBe(0)
expect(Reflect.get(executions, 'settledTurns').size).toBe(0)
expect(executions.retentionSizes()).toEqual({ children: 0, settledTurns: 0 })
})
})
@@ -106,6 +106,11 @@ export class CodexSubagentExecutions {
this.settledTurns.clear()
}
/** Retention bounds are not observable through the child/turn API, so expose the two counts. */
retentionSizes(): { children: number; settledTurns: number } {
return { children: this.children.size, settledTurns: this.settledTurns.size }
}
private child(agentThreadId: string): CodexExecutionChild | undefined {
const existing = this.children.get(agentThreadId)
if (existing) {
+5 -2
View File
@@ -59,8 +59,11 @@ export function requestDaemonRpc<T>(opts: DaemonRpcRequestOptions): Promise<T> {
const createTimeoutError = (): DaemonRequestTimeoutError =>
new DaemonRequestTimeoutError(`Request ${type} timed out after ${opts.timeoutMs}ms`)
const createSessionId =
type === 'createOrAttach' && payload !== null && typeof payload === 'object'
? Reflect.get(payload, 'sessionId')
type === 'createOrAttach' &&
payload !== null &&
typeof payload === 'object' &&
'sessionId' in payload
? payload.sessionId
: null
const requestPayload =
type === 'createOrAttach' && payload !== null && typeof payload === 'object'
@@ -91,6 +91,7 @@ function flakyClose(journal: AgentSessionJournal, failures: number): AgentSessio
return new Proxy(journal, {
get(target, property, receiver) {
if (property !== 'close') {
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver.
return Reflect.get(target, property, receiver)
}
return async () => {
@@ -24,6 +24,7 @@ function withSharedProcessSnapshot(provider: IPtyProvider): IPtyProvider {
// receiver, a provider whose own method called `this.listProcesses()`
// would silently read this sweep's cached snapshot instead of the live
// host — the batching must not leak past the calls it was built for.
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward.
const member: unknown = Reflect.get(target, property)
return typeof member === 'function' ? member.bind(target) : member
}
@@ -87,8 +87,24 @@ const store = {
}
}
/** Reclaim clears protected retention maps that no public reader exposes. */
class ObservableRuntime extends OrcaRuntimeService {
get restoreTimers(): typeof this.pendingRestoreTimers {
return this.pendingRestoreTimers
}
get softLeavers(): typeof this.pendingSoftLeavers {
return this.pendingSoftLeavers
}
get fitOverrides(): typeof this.terminalFitOverrides {
return this.terminalFitOverrides
}
get drivers(): typeof this.terminalDrivers {
return this.terminalDrivers
}
}
function createRuntime() {
const runtime = new OrcaRuntimeService(store)
const runtime = new ObservableRuntime(store)
const ptySizes = new Map<string, { cols: number; rows: number }>()
ptySizes.set('pty-1', { cols: 150, rows: 40 })
ptySizes.set('pty-2', { cols: 120, rows: 35 })
@@ -865,9 +881,9 @@ describe('mobile subscribe integration', () => {
runtime.handleMobileUnsubscribe('pty-1', 'client-a')
await runtime.handleMobileSubscribe('pty-1', 'client-b', { cols: 40, rows: 18 })
const pendingRestore = Reflect.get(runtime, 'pendingRestoreTimers') as Map<string, unknown>
const pendingRestore = runtime.restoreTimers
pendingRestore.set('pty-1', { timer: setTimeout(() => {}, 60_000), clientId: 'client-b' })
const pendingSoft = Reflect.get(runtime, 'pendingSoftLeavers') as Map<string, unknown>
const pendingSoft = runtime.softLeavers
expect(pendingSoft.has('pty-1')).toBe(true)
await runtime.reclaimTerminalForDesktop('pty-1')
expect(pendingRestore.has('pty-1')).toBe(false)
@@ -878,10 +894,10 @@ describe('mobile subscribe integration', () => {
const { runtime } = createRuntime()
await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 })
runtime.handleMobileUnsubscribe('pty-1', 'client-a')
;(Reflect.get(runtime, 'terminalFitOverrides') as Map<string, unknown>).delete('pty-1')
runtime.fitOverrides.delete('pty-1')
const pendingRestore = Reflect.get(runtime, 'pendingRestoreTimers') as Map<string, unknown>
const pendingSoft = Reflect.get(runtime, 'pendingSoftLeavers') as Map<string, unknown>
const pendingRestore = runtime.restoreTimers
const pendingSoft = runtime.softLeavers
await runtime.reclaimTerminalForDesktop('pty-1')
expect(pendingRestore.has('pty-1')).toBe(false)
expect(pendingSoft.has('pty-1')).toBe(false)
@@ -891,15 +907,11 @@ describe('mobile subscribe integration', () => {
const { runtime } = createRuntime()
await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 })
runtime.handleMobileUnsubscribe('pty-1', 'client-a')
;(Reflect.get(runtime, 'terminalFitOverrides') as Map<string, unknown>).delete('pty-1')
;(
Reflect.get(runtime, 'terminalDrivers') as {
set: (ptyId: string, driver: { kind: 'idle' }) => void
}
).set('pty-1', { kind: 'idle' })
runtime.fitOverrides.delete('pty-1')
runtime.drivers.set('pty-1', { kind: 'idle' })
const pendingRestore = Reflect.get(runtime, 'pendingRestoreTimers') as Map<string, unknown>
const pendingSoft = Reflect.get(runtime, 'pendingSoftLeavers') as Map<string, unknown>
const pendingRestore = runtime.restoreTimers
const pendingSoft = runtime.softLeavers
expect(await runtime.reclaimTerminalForDesktop('pty-1')).toBe(false)
expect(pendingRestore.has('pty-1')).toBe(false)
expect(pendingSoft.has('pty-1')).toBe(false)
@@ -436,10 +436,11 @@ describe('OrcaRuntimeService', () => {
throw new Error('onPtyData should use the PTY leaf index')
}
}
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward.
const value = Reflect.get(target, prop, target)
return typeof value === 'function' ? value.bind(target) : value
}
}) as Map<string, unknown>
})
runtime.onPtyData(`pty-${targetIndex}`, 'hello indexed\n', 123)
@@ -99,10 +99,11 @@ describe('mailbox pointer staging watermark', () => {
throw new Error('SQLITE_BUSY')
}
}
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver.
const value = Reflect.get(target, prop, receiver)
return typeof value === 'function' ? value.bind(target) : value
}
}) as OrchestrationDb
})
const state = new OrchestrationMailboxPointerState()
const args = stageArgs(db, state)
@@ -178,10 +179,11 @@ describe('mailbox pointer staging watermark', () => {
stealNextClaim = false
return () => false
}
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver.
const value = Reflect.get(target, prop, receiver)
return typeof value === 'function' ? value.bind(target) : value
}
}) as OrchestrationDb
})
const writePty = vi.fn(() => WRITE_ACCEPTED)
const delivery = new OrchestrationMailboxPointerDelivery<never>({
+9 -11
View File
@@ -340,17 +340,18 @@ describe('remote desktop viewer width driver', () => {
const { runtime } = createRuntime()
await runtime.updateRemoteDesktopViewer('pty-1', 'sub-A', 'viewer-A', 100, 30)
await runtime.updateRemoteDesktopViewer('pty-1', 'sub-B', 'viewer-B', 80, 24, false)
const layoutQueues = Reflect.get(runtime, 'layoutQueues') as Map<
string,
{ running: Promise<unknown>; pending: { target: { ownerSubscriptionKey?: string } }[] }
>
layoutQueues.set('pty-1', { running: new Promise(() => {}), pending: [] })
const layoutQueues = runtime['layoutQueues']
layoutQueues.set('pty-1', { running: new Promise<never>(() => {}), pending: [] })
void runtime.updateRemoteDesktopViewer('pty-1', 'sub-A', 'viewer-A', 90, 28)
void runtime.claimRemoteDesktopViewer('pty-1', 'sub-B')
expect(
layoutQueues.get('pty-1')?.pending.map(({ target }) => target.ownerSubscriptionKey)
layoutQueues
.get('pty-1')
?.pending.map(({ target }) =>
'ownerSubscriptionKey' in target ? target.ownerSubscriptionKey : undefined
)
).toEqual(['sub-A', 'sub-B'])
layoutQueues.delete('pty-1')
})
@@ -358,11 +359,8 @@ describe('remote desktop viewer width driver', () => {
it('makes a host claim join a pending disconnect reclaim', async () => {
const { runtime } = createRuntime()
await runtime.updateRemoteDesktopViewer('pty-1', 'sub-A', 'viewer-A', 80, 24)
const layoutQueues = Reflect.get(runtime, 'layoutQueues') as Map<
string,
{ running: Promise<unknown>; pending: { waiters: unknown[] }[] }
>
layoutQueues.set('pty-1', { running: new Promise(() => {}), pending: [] })
const layoutQueues = runtime['layoutQueues']
layoutQueues.set('pty-1', { running: new Promise<never>(() => {}), pending: [] })
void runtime.unregisterRemoteDesktopViewer('pty-1', 'sub-A')
void runtime.claimRemoteDesktopHost('pty-1', 150, 40)
@@ -47,6 +47,7 @@ function overrideAwareReceiver(
return override.bind(facade)
}
}
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: raw string|symbol pass-through; the receiver stays the target on purpose.
return Reflect.get(target, property, proxyReceiver)
}
})
@@ -35,6 +35,7 @@ it('validates large restored MRU lists with linear tab-order reads', () => {
if (typeof key === 'string' && /^\d+$/.test(key)) {
reads += 1
}
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward.
return Reflect.get(target, key, receiver)
}
})
@@ -5,7 +5,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
import type { AgentSessionJournalIdentity } from '../../shared/agent-session-journal-types'
import { agentSessionJournalCloseRetries } from '../native-chat/agent-session-journal/journal-close-retry'
import { createTrackedJournalOpener } from '../native-chat/agent-session-journal/journal-store-test-open'
import type { AgentSessionJournal } from '../native-chat/agent-session-journal/journal-store'
import type {
AgentSessionClaimStatus,
AgentSessionExecutionLocation,
@@ -346,6 +345,7 @@ describe('a teardown that fails is retried by the next stop', () => {
const flaky = new Proxy(real, {
get(target, property, receiver) {
if (property !== 'close') {
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver.
return Reflect.get(target, property, receiver)
}
return async () => {
@@ -356,7 +356,7 @@ describe('a teardown that fails is retried by the next stop', () => {
await target.close()
}
}
}) as AgentSessionJournal
})
await agentSessionJournalCloseRetries.closeOrRetain(flaky)
// The host's teardown runs the registry retry, so this stop surfaces it.
@@ -104,6 +104,7 @@ describe('skill bundle installation', () => {
}
}
}
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward.
const value = Reflect.get(target, property, target) as unknown
return typeof value === 'function' ? value.bind(target) : value
}
@@ -195,6 +195,7 @@ it.each(['skill-install-cancelled', 'skill-install-filesystem-failed'])(
if (typeof key === 'string' && /^\d+$/.test(key)) {
reads += 1
}
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver.
return Reflect.get(target, key, receiver)
}
})
@@ -4,6 +4,7 @@ import type * as NodeFsPromises from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { SkillUploadRetainedPaths } from './skill-upload-retained-paths'
import { SkillUploadSessionService } from './skill-upload-session-service'
const roots: string[] = []
@@ -30,10 +31,6 @@ vi.mock('node:fs/promises', async (importOriginal) => {
}
})
type RetainedPathCleanup = {
removeFailedCleanup(path: string): Promise<void>
}
afterEach(async () => {
vi.useRealTimers()
openGate.release = null
@@ -51,8 +48,8 @@ function identity(bytes: Buffer) {
}
}
function retainedPathCleanup(service: SkillUploadSessionService): RetainedPathCleanup {
return Reflect.get(service, 'retainedPaths') as RetainedPathCleanup
function retainedPathCleanup(service: SkillUploadSessionService): SkillUploadRetainedPaths {
return service['retainedPaths']
}
async function stagedArchiveCount(uploads: string): Promise<number> {
+2 -1
View File
@@ -146,6 +146,7 @@ export function createUpdaterMocks(): UpdaterMocks {
const loadedGeneration = currentGeneration
return new Proxy(autoUpdaterMock, {
get(target, property) {
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: raw string|symbol pass-through; the receiver stays the target on purpose.
const value = Reflect.get(target, property)
if (loadedGeneration === currentGeneration || typeof value !== 'function') {
return value
@@ -155,7 +156,7 @@ export function createUpdaterMocks(): UpdaterMocks {
set(target, property, value) {
return loadedGeneration === currentGeneration ? Reflect.set(target, property, value) : true
}
}) as AutoUpdaterMock
})
}
const reset = () => {
@@ -15,6 +15,7 @@ describe('summarizeWorkspaceSpaceRows', () => {
) {
reads[property] += 1
}
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward.
return Reflect.get(target, property, receiver)
}
})
+6 -1
View File
@@ -75,7 +75,12 @@ export function normalizeRetirableGeneratedName(name: string): string | null {
/** A sparse create error carries this marker only when its rollback also failed, leaving the path
* occupied even though creation rejected. */
export function failedWorktreeCreationNeedsRetirement(error: unknown): boolean {
return typeof error === 'object' && error !== null && Reflect.get(error, 'cleanupFailed') === true
return (
typeof error === 'object' &&
error !== null &&
'cleanupFailed' in error &&
error.cleanupFailed === true
)
}
async function getRetirementProbePath(
+3 -1
View File
@@ -45,7 +45,9 @@ function readAgents(params: unknown): AgentHookTarget[] {
function readClaudeVersion(params: unknown): string | undefined {
const raw =
params !== null && typeof params === 'object' ? Reflect.get(params, 'claudeVersion') : null
params !== null && typeof params === 'object' && 'claudeVersion' in params
? params.claudeVersion
: null
return parseClaudeCliVersion(typeof raw === 'string' ? raw : null) ?? undefined
}
@@ -100,6 +100,7 @@ function countingRows(rows: ProcessTableRow[]): {
if (typeof key === 'string' && /^\d+$/.test(key)) {
reads += 1
}
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver.
return Reflect.get(target, key, receiver)
}
})
@@ -104,6 +104,7 @@ describe('RelayPtySourceCreditLedger', () => {
if (typeof property === 'string' && /^\d+$/.test(property)) {
indexedReads += 1
}
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward.
return Reflect.get(target, property, receiver)
}
})
@@ -109,6 +109,7 @@ describe('buildDashboardSnapshot orchestration routing', () => {
if (typeof key === 'string' && Object.hasOwn(target, key)) {
runtimeValueReads += 1
}
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver.
return Reflect.get(target, key, receiver)
}
})
@@ -113,6 +113,7 @@ describe('useAgentRowConversationName', () => {
if (typeof property === 'string' && /^\d+$/.test(property)) {
tabReads += 1
}
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward.
return Reflect.get(target, property, receiver)
}
}
@@ -74,6 +74,7 @@ export function createTiptapMarkedFacade(): typeof marked {
return facade
}
default:
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver.
return Reflect.get(target, property, receiver)
}
}
@@ -55,6 +55,7 @@ it('selects a primary team without pairwise membership checks or sorting all tea
if (typeof key === 'string' && /^\d+$/.test(key)) {
reads += 1
}
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward.
return Reflect.get(target, key, receiver)
}
}
@@ -68,7 +68,8 @@ beforeEach(() => {
? element.getAttribute('data-state') === 'closed'
? 'exit'
: 'enter'
: Reflect.get(target, property)
: // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: raw string|symbol pass-through; the receiver stays the target on purpose.
Reflect.get(target, property)
})
}
return style
@@ -182,6 +182,7 @@ describe('getActiveChecksStatus caching', () => {
{
get(target, prop, receiver) {
reads.add(prop)
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward.
return Reflect.get(target, prop, receiver)
},
has(target, prop) {
@@ -61,9 +61,10 @@ describe('parent PR checks projection selector', () => {
const observedCache = new Proxy(
{},
{
get: (target, property, receiver) => {
get: (target, property) => {
cacheRead(property)
return Reflect.get(target, property, receiver)
const entries: Record<string | symbol, unknown> = target
return entries[property]
}
}
)
@@ -25,6 +25,7 @@ function trackCacheReads<K extends ReviewCacheName>(
): ReviewCacheState[K] {
return new Proxy(state[cacheName], {
get: (target, property, receiver) => {
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward.
const value = Reflect.get(target, property, receiver)
if (typeof property === 'string') {
dependencies.push({ cacheName, key: property, value })
@@ -41,7 +42,7 @@ function dependenciesAreCurrent(
): boolean {
return dependencies.every(
({ cacheName, key, value }) =>
state[cacheName] === previousState[cacheName] || Reflect.get(state[cacheName], key) === value
state[cacheName] === previousState[cacheName] || state[cacheName][key] === value
)
}
@@ -364,6 +364,7 @@ describe('selectRuntimeAgentOrchestrationBatch', () => {
if (typeof key === 'string' && Object.hasOwn(target, key)) {
runtimeValueReads += 1
}
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver.
return Reflect.get(target, key, receiver)
}
})
@@ -456,6 +457,7 @@ describe('selectRuntimeAgentOrchestrationBatch', () => {
if (typeof key === 'string' && Object.hasOwn(target, key)) {
runtimeValueReads += 1
}
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver.
return Reflect.get(target, key, receiver)
}
})
@@ -636,6 +638,7 @@ describe('selectRuntimeAgentOrchestrationBatch live-map churn', () => {
if (typeof key === 'string') {
reads.push(key)
}
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver.
return Reflect.get(source, key, receiver)
}
})
@@ -312,6 +312,7 @@ describe('selectWorktreeAgentOrchestration', () => {
if (typeof key === 'string') {
onRead()
}
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward.
return Reflect.get(source, key, receiver)
}
})
@@ -356,6 +356,7 @@ describe('hasUnreadAgentCompletionForTerminalTab', () => {
ownKeys,
get: (target, property, receiver) => {
valueReads += 1
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver.
return Reflect.get(target, property, receiver)
}
})
@@ -218,6 +218,7 @@ describe('parked terminal watcher synchronization', () => {
if (typeof property === 'string') {
harness.reconciliationPtyReads += 1
}
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward.
return Reflect.get(target, property, receiver)
}
})
@@ -79,6 +79,7 @@ describe('terminal provider snapshot capabilities', () => {
if (typeof property === 'string' && /^\d+$/.test(property)) {
indexedReads += 1
}
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver.
return Reflect.get(target, property, receiver)
}
})
@@ -118,8 +118,8 @@ describe('useIpcEvents rate-limit hydration', () => {
const makeEvents = (target: Record<string, unknown> = {}): Record<string, unknown> =>
new Proxy(target, {
get: (namespace, prop) => {
if (prop in namespace) {
return Reflect.get(namespace, prop)
if (typeof prop === 'string' && prop in namespace) {
return namespace[prop]
}
return () => () => {}
}
@@ -281,10 +281,11 @@ describe('useIpcEvents updater integration', () => {
}))
vi.doMock('@/lib/zoom-events', () => ({ dispatchZoomLevelChanged: vi.fn() }))
const makeEvents = (target: Record<string, unknown> = {}): Record<string, unknown> =>
const makeEvents = (
target: Record<string | symbol, unknown> = {}
): Record<string | symbol, unknown> =>
new Proxy(target, {
get: (namespace, prop) =>
prop in namespace ? Reflect.get(namespace, prop) : () => () => {}
get: (namespace, prop) => (prop in namespace ? namespace[prop] : () => () => {})
})
vi.stubGlobal('window', {
@@ -180,8 +180,8 @@ describe('useIpcEvents zoom routing', () => {
const makeEvents = (target: Record<string, unknown> = {}): Record<string, unknown> =>
new Proxy(target, {
get: (namespace, prop) => {
if (prop in namespace) {
return Reflect.get(namespace, prop)
if (typeof prop === 'string' && prop in namespace) {
return namespace[prop]
}
return () => () => {}
}
@@ -326,8 +326,8 @@ describe('useIpcEvents zoom routing', () => {
const makeEvents = (target: Record<string, unknown> = {}): Record<string, unknown> =>
new Proxy(target, {
get: (namespace, prop) => {
if (prop in namespace) {
return Reflect.get(namespace, prop)
if (typeof prop === 'string' && prop in namespace) {
return namespace[prop]
}
return () => () => {}
}
@@ -367,9 +367,10 @@ describe('resolveCodexPaneSelectionLane', () => {
if (property === 'worktreesByRepo') {
throw new Error('state read blew up')
}
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: raw string|symbol pass-through; the receiver stays the target on purpose.
return Reflect.get(target, property)
}
}) as LaneState
})
// Why: this call sits outside the scan's per-pane failure guard, so a throw
// would lose the notice for every pane in the batch, not just this one.
expect(
@@ -111,6 +111,7 @@ export class TerminalLigaturesAddon extends LigaturesAddon {
target.refresh(start, end)
}
}
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward.
const value = Reflect.get(target, property, target) as unknown
return typeof value === 'function' ? value.bind(target) : value
}
@@ -210,6 +210,7 @@ describe('session write subscriber allocation', () => {
if (typeof property === 'string') {
read.add(property)
}
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver.
return Reflect.get(target, property, receiver)
}
})
@@ -20,8 +20,7 @@ function countCollectionReads<T>(items: readonly T[]): {
get(array, property) {
if (property === 'map' || property === 'flatMap') {
counters[property] += 1
const method = Reflect.get(array, property) as (...args: unknown[]) => unknown
return method.bind(array)
return array[property].bind(array)
}
if (property === Symbol.iterator) {
counters.iterator += 1
@@ -30,6 +29,7 @@ function countCollectionReads<T>(items: readonly T[]): {
if (property === 'length') {
counters.length += 1
}
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward.
return Reflect.get(array, property)
}
})
+1
View File
@@ -695,6 +695,7 @@ describe('selectFloatingWorkspaceHasUnread', () => {
if (typeof property === 'string') {
terminalUnreadReads += 1
}
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver.
return Reflect.get(target, property, receiver)
}
}
@@ -35,6 +35,7 @@ describe('appendOwnedTabIdsToGroups', () => {
if (typeof property === 'string' && /^\d+$/.test(property)) {
reads++
}
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward.
return Reflect.get(target, property, receiver)
}
})
@@ -136,6 +136,7 @@ describe('whole-session workspace tab-model reconciliation', () => {
if (property === 'filter') {
scans += 1
}
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver.
return Reflect.get(target, property, receiver)
}
})
@@ -73,6 +73,7 @@ describe('terminal tab owner index', () => {
if (typeof property === 'string' && property.startsWith('wt-')) {
bucketVisits += 1
}
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward.
return Reflect.get(target, property, receiver)
}
})
@@ -140,6 +140,7 @@ describe('terminal tab title batches', () => {
if (typeof property === 'string' && property.startsWith('wt-')) {
bucketVisits += 1
}
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver.
return Reflect.get(target, property, receiver)
}
})
@@ -42,6 +42,7 @@ function countOpenFileScans(
return target.filter(predicate)
}
}
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward.
return Reflect.get(target, property, receiver)
}
})
@@ -4,6 +4,7 @@ export function withFallback<T extends object>(target: T, path: string[]): T {
return new Proxy(target, {
get(current, property, receiver) {
if (property in current) {
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver.
const value = Reflect.get(current, property, receiver) as unknown
if (value && typeof value === 'object' && !Array.isArray(value)) {
return withFallback(value as object, [...path, String(property)])
@@ -78,7 +78,8 @@ describe('web preload API composition', () => {
'telemetryAcknowledgeBanner'
])
expect(Object.keys(globals.window.api.projects)).toEqual([])
expect(Reflect.get(globals.window.api.projects, 'then')).toBeUndefined()
const projects: Record<string, unknown> = globals.window.api.projects
expect(projects.then).toBeUndefined()
})
it('snapshots E2E config before runtime storage initialization', async () => {
@@ -102,7 +102,7 @@ describe('web preload runtime calls', () => {
if (!(rejection instanceof Error)) {
throw new Error('Expected a domain Error rejection')
}
expect(Reflect.get(rejection, 'code')).toBe('repo_unavailable')
expect('code' in rejection ? rejection.code : undefined).toBe('repo_unavailable')
expect(
JSON.parse(globals.storage.getItem('orca.web.runtimeEnvironment.v1') ?? '{}')
).toMatchObject({ runtimeId: 'runtime-domain-failure' })
+1
View File
@@ -210,6 +210,7 @@ describe('projectAutomationList', () => {
if (property === 'map' || property === 'filter') {
collectionMethodReads.push(String(property))
}
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward.
return Reflect.get(target, property, receiver)
}
})
@@ -22,6 +22,7 @@ it('retires exhausted host buckets from subsequent listing rounds', () => {
if (typeof key === 'string' && /^\d+$/.test(key)) {
reads += 1
}
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver.
return Reflect.get(target, key, receiver)
}
})
@@ -18,6 +18,7 @@ describe('PR bot author override normalization', () => {
if (typeof property === 'string' && /^\d+$/.test(property)) {
reads += 1
}
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward.
return Reflect.get(target, property, receiver)
}
})
+2 -2
View File
@@ -69,9 +69,9 @@ describe('SearchSubprocessLineAccumulator', () => {
}
expect(accepted).toBe(true)
expect(Reflect.get(parser, 'buffer')).toBeInstanceOf(Buffer)
expect(parser.retainedCapacityBytes()).toBeGreaterThanOrEqual(200_000)
expect(parser.finish()).toBe('x'.repeat(200_000))
expect(Reflect.get(parser, 'buffer')).toBeNull()
expect(parser.retainedCapacityBytes()).toBeNull()
})
it('rejects invalid byte limits', () => {
+5
View File
@@ -65,6 +65,11 @@ export class SearchSubprocessLineAccumulator {
this.bytes = 0
}
/** Capacity of the retained growable buffer, or null once it has been released. */
retainedCapacityBytes(): number | null {
return this.buffer?.length ?? null
}
private append(segment: Buffer): void {
const requiredBytes = this.bytes + segment.length
if (!this.buffer || this.buffer.length < requiredBytes) {
@@ -198,6 +198,7 @@ export function createHostTerminalRuntimeStub(
}
return () => undefined
}
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward.
return Reflect.get(target, property, receiver)
}
})
@@ -69,10 +69,10 @@ type DispatcherModule = {
function registeredMethodNames(methods: readonly unknown[]): string[] {
return methods
.flatMap((method) => {
if (!method || typeof method !== 'object') {
if (!method || typeof method !== 'object' || !('name' in method)) {
return []
}
const name = Reflect.get(method, 'name')
const { name } = method
return typeof name === 'string' ? [name] : []
})
.sort()
@@ -66,13 +66,24 @@ type TransitionFrame = {
targetSelected: boolean
}
declare global {
// oxlint-disable-next-line typescript-eslint/consistent-type-definitions -- declaration merging requires interface
interface Window {
// Per-provider capture buffers written by startTransitionCapture below.
__githubUrlTransitionFrames?: TransitionFrame[]
__gitlabUrlTransitionFrames?: TransitionFrame[]
}
}
type TransitionFrameKey = '__githubUrlTransitionFrames' | '__gitlabUrlTransitionFrames'
function pasteChord(): string {
return process.platform === 'darwin' ? 'Meta+V' : 'Control+V'
}
async function startTransitionCapture(
page: Page,
frameKey: string,
frameKey: TransitionFrameKey,
wrongTitle: string,
targetTitle: string
): Promise<void> {
@@ -95,20 +106,29 @@ async function startTransitionCapture(
requestAnimationFrame(capture)
}
}
Reflect.set(window, frameKey, frames)
window[frameKey] = frames
capture()
},
{ frameKey, frameLimit: TRANSITION_FRAME_LIMIT, wrongTitle, targetTitle }
)
}
async function readTransitionFrames(page: Page, frameKey: string): Promise<TransitionFrame[]> {
return page.evaluate((key) => Reflect.get(window, key) as TransitionFrame[], frameKey)
async function readTransitionFrames(
page: Page,
frameKey: TransitionFrameKey
): Promise<TransitionFrame[]> {
return page.evaluate((key) => {
const frames = window[key]
if (!frames) {
throw new Error(`Transition capture ${key} was never installed`)
}
return frames
}, frameKey)
}
async function expectLookupHeldWithoutStaleRow(
page: Page,
frameKey: string,
frameKey: TransitionFrameKey,
targetUrl: string,
wrongOption: Locator,
targetOption: Locator
@@ -125,7 +145,7 @@ async function expectLookupHeldWithoutStaleRow(
async function expectExactTargetAfterLookup(
page: Page,
frameKey: string,
frameKey: TransitionFrameKey,
targetUrl: string,
targetOption: Locator
): Promise<void> {
@@ -268,7 +288,7 @@ test('a pasted GitHub URL never selects a stale cached issue', async ({
})
await expect(wrongOption).toBeVisible()
const frameKey = '__githubUrlTransitionFrames'
const frameKey: TransitionFrameKey = '__githubUrlTransitionFrames'
await startTransitionCapture(orcaPage, frameKey, WRONG_TITLE, TARGET_TITLE)
await orcaPage.evaluate((text) => window.api.ui.writeClipboardText(text), TARGET_URL)
@@ -317,7 +337,7 @@ test('a pasted GitLab URL never selects a stale cached merge request', async ({
})
await expect(wrongOption).toBeVisible()
const frameKey = '__gitlabUrlTransitionFrames'
const frameKey: TransitionFrameKey = '__gitlabUrlTransitionFrames'
await startTransitionCapture(orcaPage, frameKey, GITLAB_WRONG_TITLE, GITLAB_TARGET_TITLE)
await orcaPage.evaluate((text) => window.api.ui.writeClipboardText(text), GITLAB_TARGET_URL)
+10 -4
View File
@@ -25,6 +25,14 @@ const LINEAR_ISSUE: LinearIssue = {
updatedAt: '2026-08-12T00:00:00.000Z'
}
declare global {
// oxlint-disable-next-line typescript-eslint/consistent-type-definitions -- declaration merging requires interface
interface Window {
// Set by the fixture below while a Linear lookup is deliberately held open.
__orcaTestReleaseLinearLookup?: () => void
}
}
function pasteChord(): string {
return process.platform === 'darwin' ? 'Meta+V' : 'Control+V'
}
@@ -86,7 +94,7 @@ async function installLinearFixture(
async function releaseHeldLinearLookup(page: Page): Promise<void> {
await page.evaluate(() => {
const release = Reflect.get(window, '__orcaTestReleaseLinearLookup')
const release = window.__orcaTestReleaseLinearLookup
if (typeof release !== 'function') {
throw new Error('Linear lookup is not held')
}
@@ -131,9 +139,7 @@ test.describe('Linear URL workspace entry', () => {
await pasteLinearUrl(orcaPage, input)
await expect
.poll(() =>
orcaPage.evaluate(
() => typeof Reflect.get(window, '__orcaTestReleaseLinearLookup') === 'function'
)
orcaPage.evaluate(() => typeof window.__orcaTestReleaseLinearLookup === 'function')
)
.toBe(true)
await input.press('Enter')
@@ -7,6 +7,11 @@ import { runProcess } from '../../src/shared/child-process/run-process'
test.use({ seedTestRepo: false })
declare global {
// Resolved by the main-process gate this spec installs around the group-create response.
var __releaseGroupCreateResponse: (() => void) | undefined
}
for (const delayCreateResponse of [false, true]) {
test(`created groups survive sidebar expansion (${delayCreateResponse ? 'refresh first' : 'ordinary timing'})`, async ({
orcaPage,
@@ -97,7 +102,7 @@ for (const delayCreateResponse of [false, true]) {
.toBe(true)
} finally {
await electronApp.evaluate(() => {
const release = Reflect.get(globalThis, '__releaseGroupCreateResponse')
const release = globalThis.__releaseGroupCreateResponse
if (typeof release !== 'function') {
throw new Error('Group create response gate unavailable')
}
@@ -17,6 +17,14 @@ type RowRemovalFrame = {
targetExists: boolean
}
declare global {
// oxlint-disable-next-line typescript-eslint/consistent-type-definitions -- declaration merging requires interface
interface Window {
// Frame sampling started in the page and awaited once the removal animation settles.
__activeDeleteRowRemovalFrames?: Promise<RowRemovalFrame[]>
}
}
async function pauseForVisualProof(page: Page): Promise<void> {
if (process.env.ORCA_E2E_RECORD_VIDEO === '1') {
await page.waitForTimeout(VISUAL_PROOF_PAUSE_MS)
@@ -215,7 +223,7 @@ async function startRowRemovalSampling(
async function finishRowRemovalSampling(page: Page): Promise<RowRemovalFrame[]> {
return page.evaluate(async () => {
const pending = Reflect.get(window, '__activeDeleteRowRemovalFrames')
const pending = window.__activeDeleteRowRemovalFrames
if (!(pending instanceof Promise)) {
throw new Error('Row removal sampling was not started')
}