Skip to content

fix: draft relocated base objects at previously assigned indices in array-methods plugin - #1314

Open
OsamaAnsar wants to merge 1 commit into
immerjs:mainfrom
OsamaAnsar:fix/array-methods-relocated-base-after-assign
Open

OsamaAnsar wants to merge 1 commit into
immerjs:mainfrom
OsamaAnsar:fix/array-methods-relocated-base-after-assign

Conversation

@OsamaAnsar

Copy link
Copy Markdown

Summary

With enableArrayMethods(), a base object that an earlier call relocated onto an index that already has an assigned_ entry comes back from the get trap undrafted. Writing to it mutates the base state, and throws on a frozen base.

isRelocatedBaseRef in src/core/proxy.ts (added in #1255) returns false when state.assigned_?.get(prop) is set. push, unshift, splice and index writes all put indices into assigned_, and those entries stay there. Once the elements are reordered they describe an index, not the value that now sits there, so the guard skips base objects it should draft.

const base = {items: [{id: "a", n: 1}, {id: "b", n: 2}, {id: "c", n: 3}]}
produce(base, d => {
  d.items.push({id: "d", n: 4})
  d.items.reverse()
  d.items[3].n = 100 // items[3] is the base object `a`, returned raw
})
// base.items[0].n === 100

Without the plugin the base is untouched. With a frozen base this throws TypeError: Cannot assign to read only property 'n'. The same thing happens with push + unshift, unshift + reverse, an index write + sort, and splice + reverse. A more everyday shape:

produce(frozenBase, d => {
  d.items.push(newItem)
  d.items.sort((a, b) => a.id - b.id)
  d.items.filter(i => i.id === 3)[0].v = 5 // throws; find() and slice() too
})

Fix

Drop the assigned_ check from isRelocatedBaseRef. baseRefs_ (a snapshot of the base elements, taken when the array is first reordered) already identifies a relocated base object, and a user-provided object is never in it, so it is still returned as-is. Already-drafted values are still skipped by the value[DRAFT_STATE] check. The positional check next to it (value === base_[prop]) already drafts a base object that is assigned back to its own index, so this is consistent with that.

Test plan

Added to __tests__/base.js under "mutating array methods" (so they run in every config, including with the plugin):

  • five push/unshift/index write/splice + reverse/sort/unshift recipes, each checked for "base is not mutated" and "works on a frozen base"
  • find(), filter() and slice() after push() + sort() return drafts on a frozen base
  • guard: a newly assigned object is still returned undrafted, while the relocated base objects are drafted
  • guard: patches and inverse patches round-trip for a push/unshift + reverse case

With only the proxy.ts change reverted, 12 of the new tests fail (base mutated / Cannot assign to read only property), and they pass with the fix. The existing reverse/sort tests from #1255 still pass.

Full vitest run: 23 files, 3962 passed, 8 skipped. tsup build + vitest run --config vitest.config.build.ts also passes. I did not run yarn test:flow.

I also fuzzed 6000 random sequences of push/unshift/splice/index write/pop/shift/reverse/sort followed by a nested write, comparing plugin on vs. off. Before: 184 cases mutated the base and 184 threw on a frozen base. After: none, and the resulting state matches the plugin-off result in every case.

…rray-methods plugin

isRelocatedBaseRef (added in immerjs#1255) bails out when assigned_ has an entry for
the index. But push, unshift, splice and index writes leave such entries
behind, and after a later reverse/sort/unshift they describe an index, not the
value that now sits there. A base object relocated onto one of those indices
was returned from the get trap undrafted, so writing to it mutated the base
state (and threw on a frozen base).

baseRefs_ already identifies relocated base objects, and objects the user
newly assigned are never in it, so the assigned_ check is not needed.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant