From 77d8f76d9ae5495da3739862db679e3cce026c6c Mon Sep 17 00:00:00 2001 From: Bao Nguyen Date: Sat, 29 Aug 2026 15:43:28 +0700 Subject: [PATCH] fix: forward thisArg in the array-methods plugin The plugin hand-rolls filter, find and findLast and invoked the predicate as a bare call, dropping the optional thisArg argument. Any predicate reading `this` threw a TypeError once enableArrayMethods() was on. findIndex, findLastIndex, some and every were unaffected: they fall through to the native method, which forwards thisArg itself. The Map/Set plugin already does the same with cb.call(thisArg, ...). --- __tests__/base.js | 91 +++++++++++++++++++++++++++++++++++++ src/plugins/arrayMethods.ts | 6 ++- 2 files changed, 95 insertions(+), 2 deletions(-) diff --git a/__tests__/base.js b/__tests__/base.js index c76e359c..10e0de28 100644 --- a/__tests__/base.js +++ b/__tests__/base.js @@ -1819,6 +1819,97 @@ function runBaseTest( }) }) + // Regression: the plugin hand-rolls filter/find/findLast and used to + // drop the optional thisArg argument (ECMA-262 23.1.3.8, 23.1.3.12.1). + describe("thisArg forwarding", () => { + test("filter() passes thisArg to the predicate", () => { + const base = createTestData() + const ctx = {threshold: 25} + const seen = [] + const result = produce(base, draft => { + const filtered = draft.items.filter(function (item) { + seen.push(this) + return item.value > this.threshold + }, ctx) + expect(filtered.map(item => item.id)).toEqual([3, 4, 5]) + }) + expect(seen).toHaveLength(5) + seen.forEach(self => expect(self).toBe(ctx)) + expect(result).toBe(base) + }) + + test("find() passes thisArg to the predicate", () => { + const base = createTestData() + const ctx = {targetId: 3} + const result = produce(base, draft => { + const found = draft.items.find(function (item) { + return item.id === this.targetId + }, ctx) + expect(found.value).toBe(30) + }) + expect(result).toBe(base) + }) + + test("findLast() passes thisArg to the predicate", () => { + const base = createTestData() + const ctx = {threshold: 25} + const result = produce(base, draft => { + const found = draft.items.findLast(function (item) { + return item.value > this.threshold + }, ctx) + expect(found.id).toBe(5) + }) + expect(result).toBe(base) + }) + + test("thisArg still reaches predicates the plugin does not hand-roll", () => { + const base = createTestData() + const ctx = {threshold: 25} + const result = produce(base, draft => { + expect( + draft.items.findIndex(function (item) { + return item.value > this.threshold + }, ctx) + ).toBe(2) + expect( + draft.items.findLastIndex(function (item) { + return item.value > this.threshold + }, ctx) + ).toBe(4) + expect( + draft.items.some(function (item) { + return item.value > this.threshold + }, ctx) + ).toBe(true) + expect( + draft.items.every(function (item) { + return item.value > this.threshold + }, ctx) + ).toBe(false) + }) + expect(result).toBe(base) + }) + + test("omitting thisArg leaves the predicate's this undefined", () => { + const base = createTestData() + const result = produce(base, draft => { + draft.items.filter(function () { + expect(this).toBeUndefined() + return false + }) + draft.items.find(function () { + expect(this).toBeUndefined() + return false + }) + draft.items.findLast(function () { + expect(this).toBeUndefined() + return false + }) + }) + expect(result).toBe(base) + }) + }) + describe("comparison: filter vs concat behavior", () => { test("filter returns drafts that can affect original", () => { const base = { diff --git a/src/plugins/arrayMethods.ts b/src/plugins/arrayMethods.ts index edd4f1b1..0310e237 100644 --- a/src/plugins/arrayMethods.ts +++ b/src/plugins/arrayMethods.ts @@ -447,11 +447,12 @@ export function enableArrayMethods() { // Methods that return arrays with selected items - need to return drafts if (method === "filter") { const predicate = args[0] + const thisArg = args[1] const result: any[] = [] // First pass: call predicate on base values to determine which items pass for (let i = 0; i < source.length; i++) { - if (predicate(source[i], i, source)) { + if (predicate.call(thisArg, source[i], i, source)) { // Only create draft for items that passed the predicate result.push(state.draft_[i]) } @@ -462,12 +463,13 @@ export function enableArrayMethods() { if (FIND_METHODS.has(method)) { const predicate = args[0] + const thisArg = args[1] const isForward = method === "find" const step = isForward ? 1 : -1 const start = isForward ? 0 : source.length - 1 for (let i = start; i >= 0 && i < source.length; i += step) { - if (predicate(source[i], i, source)) { + if (predicate.call(thisArg, source[i], i, source)) { return state.draft_[i] } }