mirror of
https://github.com/stablyai/orca.git
synced 2026-09-23 00:02:29 +00:00
`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".
81 lines
2.9 KiB
TypeScript
81 lines
2.9 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
import { SearchSubprocessLineAccumulator } from './search-subprocess-lines'
|
|
|
|
describe('SearchSubprocessLineAccumulator', () => {
|
|
it('keeps complete decoded batches as strings without allocating byte copies', () => {
|
|
const parser = new SearchSubprocessLineAccumulator()
|
|
const lines: string[] = []
|
|
const from = vi.spyOn(Buffer, 'from')
|
|
let copies: number
|
|
try {
|
|
parser.push('first🐋\n\nlast\n', (line) => lines.push(line))
|
|
copies = from.mock.calls.length
|
|
} finally {
|
|
from.mockRestore()
|
|
}
|
|
expect(copies).toBe(0)
|
|
expect(lines).toEqual(['first🐋', '', 'last'])
|
|
expect(parser.finish()).toBeNull()
|
|
})
|
|
|
|
it('still enforces per-line UTF-8 byte limits for decoded batches', () => {
|
|
const parser = new SearchSubprocessLineAccumulator(4)
|
|
const lines: string[] = []
|
|
expect(parser.push('éé\n漢\n', (line) => lines.push(line))).toBe(true)
|
|
expect(parser.push('漢é\n', (line) => lines.push(line))).toBe(false)
|
|
expect(lines).toEqual(['éé', '漢'])
|
|
expect(parser.finish()).toBeNull()
|
|
})
|
|
|
|
it('preserves UTF-8 records split across raw byte chunks', () => {
|
|
const parser = new SearchSubprocessLineAccumulator(32)
|
|
const bytes = Buffer.from('first🐋\nsecond')
|
|
const lines: string[] = []
|
|
|
|
expect(parser.push(bytes.subarray(0, 7), (line) => lines.push(line))).toBe(true)
|
|
expect(parser.push(bytes.subarray(7), (line) => lines.push(line))).toBe(true)
|
|
|
|
expect(lines).toEqual(['first🐋'])
|
|
expect(parser.finish()).toBe('second')
|
|
})
|
|
|
|
it('accepts an exact byte limit and rejects the next byte without decoding it', () => {
|
|
const parser = new SearchSubprocessLineAccumulator(4)
|
|
const lines: string[] = []
|
|
|
|
expect(parser.push(Buffer.from('four\n'), (line) => lines.push(line))).toBe(true)
|
|
expect(parser.push(Buffer.from('fives'), (line) => lines.push(line))).toBe(false)
|
|
|
|
expect(lines).toEqual(['four'])
|
|
expect(parser.finish()).toBeNull()
|
|
})
|
|
|
|
it('preserves empty lines and line order within one chunk', () => {
|
|
const parser = new SearchSubprocessLineAccumulator(8)
|
|
const lines: string[] = []
|
|
|
|
expect(parser.push(Buffer.from('\na\n\n'), (line) => lines.push(line))).toBe(true)
|
|
|
|
expect(lines).toEqual(['', 'a', ''])
|
|
})
|
|
|
|
it('retains one growable buffer for adversarial one-byte fragments', () => {
|
|
const parser = new SearchSubprocessLineAccumulator(256 * 1024)
|
|
const byte = Buffer.from('x')
|
|
let accepted = true
|
|
|
|
for (let index = 0; index < 200_000; index += 1) {
|
|
accepted = parser.push(byte, () => {}) && accepted
|
|
}
|
|
|
|
expect(accepted).toBe(true)
|
|
expect(parser.retainedCapacityBytes()).toBeGreaterThanOrEqual(200_000)
|
|
expect(parser.finish()).toBe('x'.repeat(200_000))
|
|
expect(parser.retainedCapacityBytes()).toBeNull()
|
|
})
|
|
|
|
it('rejects invalid byte limits', () => {
|
|
expect(() => new SearchSubprocessLineAccumulator(-1)).toThrow(RangeError)
|
|
})
|
|
})
|