refactor(lint): enable anti-slop/no-reflect-apply (#20782)

`anti-slop/no-reflect-apply` rejects `Reflect.apply(fn, thisArg, argsArray)`.
It defeats the call-signature checks TypeScript applies to an ordinary call:
the args array is checked as an array, not positionally against the callee's
parameters, so arity and type errors pass silently. Dynamic dispatch belongs
behind a named interface, not behind a reflective call.

Flipped the rule from "off" to "error" and cleared all 17 baseline violations
across `src config tests mobile` (16 sites; one file had two).

Fix pattern: `Reflect.apply(fn, recv, args)` becomes `fn.call(recv, ...args)`,
or a direct method call when the implicit receiver is already the right object.
The receiver is preserved at every site.

Where the callee is a captured built-in whose overloads split on an argument's
shape (`String.prototype.split`, `JSON.stringify`), a call-signature capture no
longer compiles once the args are passed positionally. Those three sites capture
the function through a method-shaped type
(`{ split(separator: unknown, limit?: number): string[] }['split']`), which keeps
the forwarding call checked rather than asserted.

Behaviour notes:
- `diff-section-layout.test.ts` drops a `limit === undefined ? [sep] : [sep, limit]`
  conditional. Equivalent: `String.prototype.split` maps an undefined limit to
  2^32-1, and the `Symbol.split` path forwards undefined either way.
- `workspace-space-compaction.test.ts` forwards `reduce`'s two arguments unchanged,
  so the `arguments.length >= 2` initial-value branch is unaffected.
- `agent-session-history-byte-accounting.test.ts` is the one site where the receiver
  is not literally preserved (`JSON` -> undefined). `JSON.stringify` never reads
  `this` per spec, and restoring `.call(JSON, ...)` would reintroduce the overload
  failure under strictBindCallApply.

No suppression comments added — the rule has zero `oxlint-disable` sites.

`Reflect.apply` still appears at electron.vite.config.ts:159, inside a template
literal of generated bootstrap source. That is string content, not lintable code.
This commit is contained in:
Neil
2026-09-15 00:10:11 -07:00
committed by GitHub
parent c9ae17fe3d
commit 49e5fa597a
17 changed files with 47 additions and 32 deletions
+1 -1
View File
@@ -32,7 +32,7 @@
"anti-slop/no-module-mocking": "off",
"anti-slop/no-object-parameters": "off",
"anti-slop/no-reduce-accumulator-copy": "error",
"anti-slop/no-reflect-apply": "off",
"anti-slop/no-reflect-apply": "error",
"anti-slop/no-reflect-get": "off",
"anti-slop/no-runtime-typeof": "off",
"anti-slop/no-shape-in-symbol-names": "off",
+1 -1
View File
@@ -12,7 +12,7 @@ export function installMainBlockingProbe() {
const epoch = Date.now()
let result
try {
result = Reflect.apply(original, this, args)
result = original.call(this, ...args)
return result
} finally {
const durationMs = performance.now() - start
+1 -1
View File
@@ -21,7 +21,7 @@ export function installPersistenceCallProbe() {
const epoch = Date.now()
let result
try {
result = Reflect.apply(original, this, args)
result = original.call(this, ...args)
return result
} finally {
const durationMs = performance.now() - start
@@ -88,7 +88,7 @@ function runWithNativeCallCount(fn) {
let calls = 0
Buffer.byteLength = (...args) => {
calls += 1
return Reflect.apply(nativeByteLength, Buffer, args)
return nativeByteLength.call(Buffer, ...args)
}
try {
return { output: fn(), calls }
@@ -95,7 +95,7 @@ describe('gitlab client — MR operations', () => {
if (this[0] === 'gitlab.com' && this.every((value) => typeof value === 'string')) {
knownHostCacheScans += 1
}
return Reflect.apply(originalMap, this, [callback, thisArg])
return originalMap.call(this, callback, thisArg)
})
try {
@@ -69,13 +69,17 @@ it.each([1, 100, 200])('serializes each of %i unchanged forward page items once'
await appendItems(count, 'x'.repeat(8_000))
const snapshot = journal.snapshot()
const stringify = JSON.stringify
// Method-shaped type: the JSON.stringify overloads split on replacer shape and reject a forwarded one.
const forwardStringify: {
stringify(value: unknown, replacer?: unknown, space?: unknown): string
}['stringify'] = stringify
let itemSerializations = 0
JSON.stringify = ((value: unknown, ...args: unknown[]) => {
JSON.stringify = (value: unknown, replacer?: unknown, space?: unknown): string => {
if (value && typeof value === 'object' && 'itemId' in value && 'body' in value) {
itemSerializations++
}
return Reflect.apply(stringify, JSON, [value, ...args])
}) as typeof JSON.stringify
return forwardStringify(value, replacer, space)
}
try {
const result = readAgentSessionHistory(
journal,
@@ -312,9 +312,7 @@ describe('RuntimeBrowserCommands client-hosted routing', () => {
.spyOn(registry, 'publishClientPage')
.mockImplementation((input) => {
order.push('publish')
return Reflect.apply(RuntimeBrowserPageRegistry.prototype.publishClientPage, registry, [
input
])
return RuntimeBrowserPageRegistry.prototype.publishClientPage.call(registry, input)
})
const notifyHeadlessBrowserSessionTabsChanged = vi.fn(() => order.push('notify'))
const issueClientPageCommand = vi.fn(() => {
@@ -66,7 +66,7 @@ export function installRuntimeLinearCommandSurface(target: object): void {
const method = {
[name](this: LinearFacadeInstance, ...args: unknown[]): unknown {
const commands = this.linearCommands as unknown as LinearMethodBag
return Reflect.apply(commands[name], overrideAwareReceiver(this, commands, names), args)
return commands[name].call(overrideAwareReceiver(this, commands, names), ...args)
}
}[name]
delegators.add(method)
@@ -90,7 +90,9 @@ describe('RuntimeFileCommands', () => {
submatches: [{ start: 0, end: 6 }]
}
})
const originalSplit = String.prototype.split
// Method-shaped type: a call-signature capture would reject `split`'s splitter-object overload.
const originalSplit: { split(separator: unknown, limit?: number): string[] }['split'] =
String.prototype.split
let scanned = 0
const spy = vi.spyOn(String.prototype, 'split').mockImplementation(function (
this: string,
@@ -100,7 +102,7 @@ describe('RuntimeFileCommands', () => {
if (separator === '\n') {
scanned += this.length
}
return Reflect.apply(originalSplit, this, [separator, limit])
return originalSplit.call(this, separator, limit)
})
try {
for (let offset = 0; offset < line.length; offset += 1024) {
+4 -2
View File
@@ -83,7 +83,9 @@ describe.each(searchCases)('relay $name line fragments', ({ search, encode }) =>
for (let offset = 0; offset < wire.length; offset += 4096) {
chunks.push(wire.slice(offset, offset + 4096))
}
const originalSplit = String.prototype.split
// Method-shaped type: a call-signature capture would reject `split`'s splitter-object overload.
const originalSplit: { split(separator: unknown, limit?: number): string[] }['split'] =
String.prototype.split
let scannedCharacters = 0
const spy = vi.spyOn(String.prototype, 'split').mockImplementation(function (
this: string,
@@ -93,7 +95,7 @@ describe.each(searchCases)('relay $name line fragments', ({ search, encode }) =>
if (separator === '\n') {
scannedCharacters += this.length
}
return Reflect.apply(originalSplit, this, [separator, limit])
return originalSplit.call(this, separator, limit)
})
let fragmented
try {
@@ -43,8 +43,8 @@ function layoutWithNumericMapSetCount(worktrees: ReturnType<typeof buildChain>)
if (typeof key === 'number') {
numericMapSets += 1
}
return Reflect.apply(set, this, [key, value])
} as typeof Map.prototype.set
return set.call(this, key, value)
}
try {
return { layout: layoutAgentMapWorktreeLineage(worktrees), numericMapSets }
} finally {
@@ -64,7 +64,7 @@ function layoutWithWorktreePushCount(count: number) {
typeof item.id === 'string' &&
item.id.startsWith('worktree-')
).length
return Reflect.apply(push, this, items)
return push.call(this, ...items)
}
try {
return {
@@ -145,8 +145,8 @@ describe('packAgentMapWorktrees', () => {
if (typeof key === 'number') {
numericMapSets += 1
}
return Reflect.apply(set, this, [key, value])
} as typeof Map.prototype.set
return set.call(this, key, value)
}
try {
const packed = packAgentMapWorktrees(
Array.from({ length: 5 }, (_, index) => ({
@@ -110,8 +110,10 @@ describe('diff section layout', () => {
})
it('estimates line-count height without allocating split arrays', () => {
const originalSplit = String.prototype.split
const patchedSplit = function patchedSplit(
// Method-shaped type: a call-signature capture would reject `split`'s splitter-object overload.
const originalSplit: { split(separator: unknown, limit?: number): string[] }['split'] =
String.prototype.split
const patchedSplit: typeof String.prototype.split = function patchedSplit(
this: string,
separator?: unknown,
limit?: number
@@ -119,9 +121,8 @@ describe('diff section layout', () => {
if (String(this).startsWith('line 0')) {
throw new Error('layout should not split full diff content')
}
const args = limit === undefined ? [separator] : [separator, limit]
return Reflect.apply(originalSplit, this, args) as string[]
} as typeof String.prototype.split
return originalSplit.call(this, separator, limit)
}
String.prototype.split = patchedSplit
try {
@@ -32,7 +32,8 @@ export function createTiptapMarkedFacade(): typeof marked {
const lexer = (src: string, options?: MarkedOptions): TokensList =>
new RegistryLexer(options).lex(src)
const facade = new Proxy(marked, {
apply: (_target, _thisArg, args) => Reflect.apply(registry.parse, registry, args),
apply: (_target, _thisArg, args: [src: string, options?: MarkedOptions | null]) =>
registry.parse(...args),
get: (target, property, receiver) => {
switch (property) {
case 'defaults':
@@ -33,7 +33,12 @@ it('avoids allocating copies of unaffected pages during an item mutation', () =>
if (inputs.has(this)) {
allocations++
}
return Reflect.apply(map, this, [callback, thisArg]) as U[]
// Explicit `call` type arguments: inference through `call` erases `map`'s own `U` to `unknown`.
return map.call<T[], [(value: T, index: number, array: T[]) => U, unknown], U[]>(
this,
callback,
thisArg
)
}
Array.prototype.slice = function (this: unknown[], ...args: Parameters<typeof slice>) {
if (inputs.has(this)) {
@@ -14,14 +14,16 @@ it('keeps a multiline commit body intact without materializing every message lin
'',
message
].join('\n')
const original = String.prototype.split
// Method-shaped type: a call-signature capture would reject `split`'s splitter-object overload.
const original: { split(separator: unknown, limit?: number): string[] }['split'] =
String.prototype.split
let allocatedFields = 0
const spy = vi.spyOn(String.prototype, 'split').mockImplementation(function (
this: string,
separator: string | RegExp | { [Symbol.split](value: string, limit?: number): string[] },
separator: unknown,
limit?: number
) {
const result = Reflect.apply(original, this, [separator, limit]) as string[]
const result = original.call(this, separator, limit)
if (separator === '\n' && String(this).includes('body line')) {
allocatedFields += result.length
}
@@ -19,7 +19,7 @@ it('sums omitted sizes without constructing a replacement object per omitted ite
if (initial && typeof initial === 'object' && 'name' in initial && initial.name === 'Other') {
objectAccumulators += this.length
}
return Reflect.apply(original, this, [callback, initial])
return original.call(this, callback, initial)
})
let result: ReturnType<typeof compactWorkspaceSpaceItems>
try {