Skip to content

fix(jest-util): stop globalsCleanup 'soft' mode from infinite-recursing on data-property writes (#16044)#16210

Open
LeSingh1 wants to merge 1 commit into
jestjs:mainfrom
LeSingh1:fix/globalscleanup-soft-mode-infinite-recursion
Open

fix(jest-util): stop globalsCleanup 'soft' mode from infinite-recursing on data-property writes (#16044)#16210
LeSingh1 wants to merge 1 commit into
jestjs:mainfrom
LeSingh1:fix/globalscleanup-soft-mode-infinite-recursion

Conversation

@LeSingh1

Copy link
Copy Markdown
Contributor

Fixes #16044.

Bug

In soft deletion mode, deleteProperty in jest-util/src/garbage-collection-utils.ts wraps every visited property with a setter that emits a deprecation warning and then forwards to the original setter. For data descriptors (the common case), the original setter defaulted to:

const originalSetter = descriptor.set ?? (value => Reflect.set(obj, key, value));

That Reflect.set(obj, key, value) routes the write back through the same wrapping setter we just installed on obj[key], producing RangeError: Maximum call stack size exceeded on the first write to any soft-deleted data property.

Reproducer from the issue:

const obj = { foo: 'bar' };
const descriptor = Reflect.getOwnPropertyDescriptor(obj, 'foo');
const originalSetter = descriptor.set ?? (value => Reflect.set(obj, 'foo', value));
Reflect.defineProperty(obj, 'foo', {
  configurable: true,
  enumerable: descriptor.enumerable,
  get() { return descriptor.value; },
  set(value) { return originalSetter(value); },
});
obj.foo = 'baz'; // RangeError: Maximum call stack size exceeded

Fix

Keep the data-property value in a closure-scoped slot instead of routing back through Reflect.set. Accessor descriptors that already carry their own setter keep using it because calling descriptor.set(value) directly does not re-enter the wrapping setter.

let storedValue: unknown = descriptor.value;
const originalGetter = descriptor.get ?? (() => storedValue);
const originalSetter =
  descriptor.set ??
  ((value: unknown) => {
    storedValue = value;
    return true;
  });

Verification

Added regression coverage in packages/jest-util/src/__tests__/garbage-collection-utils.test.ts covering:

  1. A single write to a soft-deleted data property does not recurse and updates the value.
  2. Repeated writes each take effect.
  3. Accessor descriptors with their own setter are still routed through the original setter (the non-buggy path is preserved).

Local A/B run:

without fix (main): 2 failed, 2 passed
  ✕ writing to a soft-deleted data property does not recurse
  ✕ repeated writes to a soft-deleted data property each take effect
  Stack trace: RangeError: Maximum call stack size exceeded at Reflect.set

with fix:           4 passed, 0 failed

The tests construct prototype-less objects (Object.create(null)) so they bypass the PROTECT_SYMBOL value jest-circus installs on the global scope, which would otherwise cause deleteProperties to treat every own key as protected and skip the wrapping path entirely.

CHANGELOG

Added an entry under ## main -> ### Fixes referencing this PR.

@netlify

netlify Bot commented May 24, 2026

Copy link
Copy Markdown

Deploy Preview for jestjs ready!

Built without sensitive environment variables

Name Link
🔨 Latest commit 485e4c9
🔍 Latest deploy log https://app.netlify.com/projects/jestjs/deploys/6a13339c5af0820009823024
😎 Deploy Preview https://deploy-preview-16210--jestjs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions github-actions Bot added the require-changelog If a PR does requires a changelog entry label May 24, 2026
…ng on data-property writes (jestjs#16044)

In soft deletion mode, deleteProperty wraps every visited property with
a setter that emits a deprecation warning and forwards to the original
setter. For data descriptors (the common case), the original setter
defaulted to (value => Reflect.set(obj, key, value)) -- which routed
the write back through the exact same wrapping setter we had just
installed, producing RangeError: Maximum call stack size exceeded on
the first write after soft delete.

Keep the data-property value in a closure-scoped slot instead. Accessor
descriptors that already carry their own setter keep using it because
descriptor.set(value) does not re-enter the wrapping setter.

Adds regression coverage in
packages/jest-util/src/__tests__/garbage-collection-utils.test.ts
that fails on main with the recursion error and passes on this branch.

Fixes jestjs#16044.
@LeSingh1 LeSingh1 force-pushed the fix/globalscleanup-soft-mode-infinite-recursion branch from 9ede299 to 485e4c9 Compare May 24, 2026 17:21
@pkg-pr-new

pkg-pr-new Bot commented May 24, 2026

Copy link
Copy Markdown

Open in StackBlitz

babel-jest

npm i https://pkg.pr.new/babel-jest@16210

babel-plugin-jest-hoist

npm i https://pkg.pr.new/babel-plugin-jest-hoist@16210

babel-preset-jest

npm i https://pkg.pr.new/babel-preset-jest@16210

create-jest

npm i https://pkg.pr.new/create-jest@16210

@jest/diff-sequences

npm i https://pkg.pr.new/@jest/diff-sequences@16210

expect

npm i https://pkg.pr.new/expect@16210

@jest/expect-utils

npm i https://pkg.pr.new/@jest/expect-utils@16210

jest

npm i https://pkg.pr.new/jest@16210

jest-changed-files

npm i https://pkg.pr.new/jest-changed-files@16210

jest-circus

npm i https://pkg.pr.new/jest-circus@16210

jest-cli

npm i https://pkg.pr.new/jest-cli@16210

jest-config

npm i https://pkg.pr.new/jest-config@16210

@jest/console

npm i https://pkg.pr.new/@jest/console@16210

@jest/core

npm i https://pkg.pr.new/@jest/core@16210

@jest/create-cache-key-function

npm i https://pkg.pr.new/@jest/create-cache-key-function@16210

jest-diff

npm i https://pkg.pr.new/jest-diff@16210

jest-docblock

npm i https://pkg.pr.new/jest-docblock@16210

jest-each

npm i https://pkg.pr.new/jest-each@16210

@jest/environment

npm i https://pkg.pr.new/@jest/environment@16210

jest-environment-jsdom

npm i https://pkg.pr.new/jest-environment-jsdom@16210

@jest/environment-jsdom-abstract

npm i https://pkg.pr.new/@jest/environment-jsdom-abstract@16210

jest-environment-node

npm i https://pkg.pr.new/jest-environment-node@16210

@jest/expect

npm i https://pkg.pr.new/@jest/expect@16210

@jest/fake-timers

npm i https://pkg.pr.new/@jest/fake-timers@16210

@jest/get-type

npm i https://pkg.pr.new/@jest/get-type@16210

@jest/globals

npm i https://pkg.pr.new/@jest/globals@16210

jest-haste-map

npm i https://pkg.pr.new/jest-haste-map@16210

jest-jasmine2

npm i https://pkg.pr.new/jest-jasmine2@16210

jest-leak-detector

npm i https://pkg.pr.new/jest-leak-detector@16210

jest-matcher-utils

npm i https://pkg.pr.new/jest-matcher-utils@16210

jest-message-util

npm i https://pkg.pr.new/jest-message-util@16210

jest-mock

npm i https://pkg.pr.new/jest-mock@16210

@jest/pattern

npm i https://pkg.pr.new/@jest/pattern@16210

jest-phabricator

npm i https://pkg.pr.new/jest-phabricator@16210

jest-regex-util

npm i https://pkg.pr.new/jest-regex-util@16210

@jest/reporters

npm i https://pkg.pr.new/@jest/reporters@16210

jest-resolve

npm i https://pkg.pr.new/jest-resolve@16210

jest-resolve-dependencies

npm i https://pkg.pr.new/jest-resolve-dependencies@16210

jest-runner

npm i https://pkg.pr.new/jest-runner@16210

jest-runtime

npm i https://pkg.pr.new/jest-runtime@16210

@jest/schemas

npm i https://pkg.pr.new/@jest/schemas@16210

jest-snapshot

npm i https://pkg.pr.new/jest-snapshot@16210

@jest/snapshot-utils

npm i https://pkg.pr.new/@jest/snapshot-utils@16210

@jest/source-map

npm i https://pkg.pr.new/@jest/source-map@16210

@jest/test-result

npm i https://pkg.pr.new/@jest/test-result@16210

@jest/test-sequencer

npm i https://pkg.pr.new/@jest/test-sequencer@16210

@jest/transform

npm i https://pkg.pr.new/@jest/transform@16210

@jest/types

npm i https://pkg.pr.new/@jest/types@16210

jest-util

npm i https://pkg.pr.new/jest-util@16210

jest-validate

npm i https://pkg.pr.new/jest-validate@16210

jest-watcher

npm i https://pkg.pr.new/jest-watcher@16210

jest-worker

npm i https://pkg.pr.new/jest-worker@16210

pretty-format

npm i https://pkg.pr.new/pretty-format@16210

commit: 485e4c9

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

Labels

require-changelog If a PR does requires a changelog entry

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: globalsCleanup 'soft' mode causes infinite setter recursion (RangeError: Maximum call stack size exceeded)

1 participant