mirror of
https://github.com/stablyai/orca.git
synced 2026-09-26 00:02:34 +00:00
The rule rejects the broad `object` type on any function input (declarations, expressions, arrows, methods, call/construct signatures, function types), plus local aliases and unions that resolve to `object`. `object` accepts every non-primitive while exposing no properties, so it documents nothing and pushes callers into assertions at the boundary. Fixes all 185 violations across src, config, tests and mobile, and flips the rule from "off" to "error" in config/oxlint-anti-slop.json. Approach: replace each `object` input with the type its owner already has. Most sites took an existing domain type or a type-only import (36 added); 40 new aliases name shapes that had none. Where a value is genuinely only compared by reference, it gets a named identity token instead of a shape -- `Record<string, never>`, the built-in `WeakKey`, or a `unique symbol` brand, matching the branding already used in src/shared. Same treatment for WeakMap and Map key parameters. Two `as unknown as` casts became unnecessary once the parameter carried a real type and were removed; no new casts were added. Suppressions added: none. No `oxlint-disable` for this rule anywhere, and no max-lines disable or per-file bump. Three files sat exactly at their max-lines cap, so the added type imports were made line-neutral rather than suppressed: - src/main/ipc/browser.ts exports the existing guest-registration args type (renamed BrowserGuestArgs) so browser.test.ts reuses it on one line. - pane-scroll.ts takes TerminalScrollIntentTarget through the existing pane-manager-types import via a type-only re-export. - direct-rpc-client.ts drops the identity parameter entirely: the session check moved into the sendProbe callback that owns the token. Verified: anti-slop config reports zero violations over src config tests mobile; run-typecheck-projects-in-parallel exits 0; 144 affected test files pass (1749 tests); oxlint and oxfmt clean on all changed files. Mobile has no runnable test/typecheck target in this worktree (expo is not installed), so its 6 files were typechecked against a standalone config and diffed against the base branch -- error sets are byte-identical, including test files.
82 lines
2.8 KiB
TypeScript
82 lines
2.8 KiB
TypeScript
// Why: happy-dom stores each MutationObserver's internal callback in a WeakRef, so any GC pause
|
|
// under parallel test load silently and permanently stops a still-connected observer. Browsers
|
|
// keep that callback reachable for as long as the observer observes; mirror that lifetime here so
|
|
// DOM-driven tests never lose mutation records mid-run.
|
|
|
|
type HappyDomMutationListener = {
|
|
callback?: { deref: () => unknown }
|
|
}
|
|
|
|
type PatchableMutationObserver = {
|
|
observe: (target: Node, options?: MutationObserverInit) => void
|
|
disconnect: () => void
|
|
}
|
|
|
|
const MUTATION_LISTENERS_SYMBOL_DESCRIPTION = 'mutationListeners'
|
|
const RETENTION_INSTALLED = Symbol.for('orca.happyDomMutationObserverRetention')
|
|
|
|
const retainedCallbacks = new WeakMap<object, Set<unknown>>()
|
|
|
|
function readMutationListeners(target: Node): HappyDomMutationListener[] {
|
|
const listenersSymbol = Object.getOwnPropertySymbols(target).find(
|
|
(candidate) => candidate.description === MUTATION_LISTENERS_SYMBOL_DESCRIPTION
|
|
)
|
|
if (!listenersSymbol) {
|
|
return []
|
|
}
|
|
const listeners = (target as unknown as Record<symbol, unknown>)[listenersSymbol]
|
|
return Array.isArray(listeners) ? (listeners as HappyDomMutationListener[]) : []
|
|
}
|
|
|
|
/** Number of internal callbacks pinned for `observer`; drops to 0 once it disconnects. */
|
|
export function retainedMutationCallbackCount(observer: MutationObserver): number {
|
|
return retainedCallbacks.get(observer)?.size ?? 0
|
|
}
|
|
|
|
export function installHappyDomMutationObserverRetention(): boolean {
|
|
const observerClass = (globalThis as { MutationObserver?: typeof MutationObserver })
|
|
.MutationObserver
|
|
if (!observerClass) {
|
|
return false
|
|
}
|
|
const prototype = observerClass.prototype as unknown as PatchableMutationObserver &
|
|
Record<symbol, unknown>
|
|
if (prototype[RETENTION_INSTALLED] === true) {
|
|
return true
|
|
}
|
|
const observe = prototype.observe
|
|
const disconnect = prototype.disconnect
|
|
|
|
prototype.observe = function patchedObserve(
|
|
this: PatchableMutationObserver,
|
|
target: Node,
|
|
options?: MutationObserverInit
|
|
): void {
|
|
const existing = new Set(readMutationListeners(target))
|
|
observe.call(this, target, options)
|
|
const pinned = retainedCallbacks.get(this) ?? new Set<unknown>()
|
|
for (const listener of readMutationListeners(target)) {
|
|
if (existing.has(listener)) {
|
|
continue
|
|
}
|
|
const callback = listener.callback?.deref()
|
|
if (callback) {
|
|
pinned.add(callback)
|
|
}
|
|
}
|
|
if (pinned.size > 0) {
|
|
retainedCallbacks.set(this, pinned)
|
|
}
|
|
}
|
|
|
|
prototype.disconnect = function patchedDisconnect(this: PatchableMutationObserver): void {
|
|
disconnect.call(this)
|
|
retainedCallbacks.delete(this)
|
|
}
|
|
|
|
prototype[RETENTION_INSTALLED] = true
|
|
return true
|
|
}
|
|
|
|
installHappyDomMutationObserverRetention()
|