Skip to content

Commit 77d8f76

Browse files
committed
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, ...).
1 parent 061c242 commit 77d8f76

2 files changed

Lines changed: 95 additions & 2 deletions

File tree

__tests__/base.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1819,6 +1819,97 @@ function runBaseTest(
18191819
})
18201820
})
18211821

1822+
// Regression: the plugin hand-rolls filter/find/findLast and used to
1823+
// drop the optional thisArg argument (ECMA-262 23.1.3.8, 23.1.3.12.1).
1824+
describe("thisArg forwarding", () => {
1825+
test("filter() passes thisArg to the predicate", () => {
1826+
const base = createTestData()
1827+
const ctx = {threshold: 25}
1828+
const seen = []
1829+
const result = produce(base, draft => {
1830+
const filtered = draft.items.filter(function (item) {
1831+
seen.push(this)
1832+
return item.value > this.threshold
1833+
}, ctx)
1834+
expect(filtered.map(item => item.id)).toEqual([3, 4, 5])
1835+
})
1836+
expect(seen).toHaveLength(5)
1837+
seen.forEach(self => expect(self).toBe(ctx))
1838+
expect(result).toBe(base)
1839+
})
1840+
1841+
test("find() passes thisArg to the predicate", () => {
1842+
const base = createTestData()
1843+
const ctx = {targetId: 3}
1844+
const result = produce(base, draft => {
1845+
const found = draft.items.find(function (item) {
1846+
return item.id === this.targetId
1847+
}, ctx)
1848+
expect(found.value).toBe(30)
1849+
})
1850+
expect(result).toBe(base)
1851+
})
1852+
1853+
test("findLast() passes thisArg to the predicate", () => {
1854+
const base = createTestData()
1855+
const ctx = {threshold: 25}
1856+
const result = produce(base, draft => {
1857+
const found = draft.items.findLast(function (item) {
1858+
return item.value > this.threshold
1859+
}, ctx)
1860+
expect(found.id).toBe(5)
1861+
})
1862+
expect(result).toBe(base)
1863+
})
1864+
1865+
test("thisArg still reaches predicates the plugin does not hand-roll", () => {
1866+
const base = createTestData()
1867+
const ctx = {threshold: 25}
1868+
const result = produce(base, draft => {
1869+
expect(
1870+
draft.items.findIndex(function (item) {
1871+
return item.value > this.threshold
1872+
}, ctx)
1873+
).toBe(2)
1874+
expect(
1875+
draft.items.findLastIndex(function (item) {
1876+
return item.value > this.threshold
1877+
}, ctx)
1878+
).toBe(4)
1879+
expect(
1880+
draft.items.some(function (item) {
1881+
return item.value > this.threshold
1882+
}, ctx)
1883+
).toBe(true)
1884+
expect(
1885+
draft.items.every(function (item) {
1886+
return item.value > this.threshold
1887+
}, ctx)
1888+
).toBe(false)
1889+
})
1890+
expect(result).toBe(base)
1891+
})
1892+
1893+
test("omitting thisArg leaves the predicate's this undefined", () => {
1894+
const base = createTestData()
1895+
const result = produce(base, draft => {
1896+
draft.items.filter(function () {
1897+
expect(this).toBeUndefined()
1898+
return false
1899+
})
1900+
draft.items.find(function () {
1901+
expect(this).toBeUndefined()
1902+
return false
1903+
})
1904+
draft.items.findLast(function () {
1905+
expect(this).toBeUndefined()
1906+
return false
1907+
})
1908+
})
1909+
expect(result).toBe(base)
1910+
})
1911+
})
1912+
18221913
describe("comparison: filter vs concat behavior", () => {
18231914
test("filter returns drafts that can affect original", () => {
18241915
const base = {

src/plugins/arrayMethods.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -447,11 +447,12 @@ export function enableArrayMethods() {
447447
// Methods that return arrays with selected items - need to return drafts
448448
if (method === "filter") {
449449
const predicate = args[0]
450+
const thisArg = args[1]
450451
const result: any[] = []
451452

452453
// First pass: call predicate on base values to determine which items pass
453454
for (let i = 0; i < source.length; i++) {
454-
if (predicate(source[i], i, source)) {
455+
if (predicate.call(thisArg, source[i], i, source)) {
455456
// Only create draft for items that passed the predicate
456457
result.push(state.draft_[i])
457458
}
@@ -462,12 +463,13 @@ export function enableArrayMethods() {
462463

463464
if (FIND_METHODS.has(method)) {
464465
const predicate = args[0]
466+
const thisArg = args[1]
465467
const isForward = method === "find"
466468
const step = isForward ? 1 : -1
467469
const start = isForward ? 0 : source.length - 1
468470

469471
for (let i = start; i >= 0 && i < source.length; i += step) {
470-
if (predicate(source[i], i, source)) {
472+
if (predicate.call(thisArg, source[i], i, source)) {
471473
return state.draft_[i]
472474
}
473475
}

0 commit comments

Comments
 (0)