jest-util
Advanced tools
Comparing version
@@ -46,5 +46,11 @@ /** | ||
* @param value the given value. | ||
* @param mode the deletion mode (see {@link #deleteProperty}). | ||
*/ | ||
export declare function deleteProperties(value: unknown): void; | ||
export declare function deleteProperties( | ||
value: unknown, | ||
mode: DeletionMode, | ||
): void; | ||
export declare type DeletionMode = 'soft' | 'hard'; | ||
export declare class ErrorWithStack extends Error { | ||
@@ -130,8 +136,12 @@ constructor( | ||
* @param properties If the array contains any property, | ||
* then only these properties will not be deleted; otherwise if the array is empty, | ||
* all properties will not be deleted. | ||
* then only these properties will be protected; otherwise if the array is empty, | ||
* all properties will be protected. | ||
* @param depth Determines how "deep" the protection should be. | ||
* A value of 0 means that only the top-most properties will be protected, | ||
* while a value larger than 0 means that deeper levels of nesting will be protected as well. | ||
*/ | ||
export declare function protectProperties<T extends object>( | ||
export declare function protectProperties<T>( | ||
value: T, | ||
properties?: Array<keyof T>, | ||
depth?: number, | ||
): boolean; | ||
@@ -138,0 +148,0 @@ |
@@ -366,4 +366,3 @@ /*! | ||
const PROTECT_PROPERTY = Symbol.for('$$jest-protect-from-deletion'); | ||
const PROTECT_SYMBOL = Symbol.for('$$jest-protect-from-deletion'); | ||
/** | ||
@@ -374,11 +373,10 @@ * Deletes all the properties from the given value (if it's an object), | ||
* @param value the given value. | ||
* @param mode the deletion mode (see {@link #deleteProperty}). | ||
*/ | ||
function deleteProperties(value) { | ||
function deleteProperties(value, mode) { | ||
if (canDeleteProperties(value)) { | ||
const protectedProperties = Reflect.get(value, PROTECT_PROPERTY); | ||
if (!Array.isArray(protectedProperties) || protectedProperties.length > 0) { | ||
for (const key of Reflect.ownKeys(value)) { | ||
if (!protectedProperties?.includes(key)) { | ||
Reflect.deleteProperty(value, key); | ||
} | ||
const protectedKeys = getProtectedKeys(value, Reflect.get(value, PROTECT_SYMBOL)); | ||
for (const key of Reflect.ownKeys(value)) { | ||
if (!protectedKeys.includes(key) && key !== PROTECT_SYMBOL) { | ||
deleteProperty(value, key, mode); | ||
} | ||
@@ -394,10 +392,36 @@ } | ||
* @param properties If the array contains any property, | ||
* then only these properties will not be deleted; otherwise if the array is empty, | ||
* all properties will not be deleted. | ||
* then only these properties will be protected; otherwise if the array is empty, | ||
* all properties will be protected. | ||
* @param depth Determines how "deep" the protection should be. | ||
* A value of 0 means that only the top-most properties will be protected, | ||
* while a value larger than 0 means that deeper levels of nesting will be protected as well. | ||
*/ | ||
function protectProperties(value, properties = []) { | ||
if (canDeleteProperties(value)) { | ||
return Reflect.set(value, PROTECT_PROPERTY, properties); | ||
function protectProperties(value, properties = [], depth = 2) { | ||
// Reflect.get may cause deprecation warnings, so we disable them temporarily | ||
const originalEmitWarning = process.emitWarning; | ||
try { | ||
// eslint-disable-next-line @typescript-eslint/no-empty-function | ||
process.emitWarning = () => {}; | ||
if (depth >= 0 && canDeleteProperties(value) && !Reflect.has(value, PROTECT_SYMBOL)) { | ||
const result = Reflect.defineProperty(value, PROTECT_SYMBOL, { | ||
configurable: true, | ||
enumerable: false, | ||
value: properties | ||
}); | ||
for (const key of getProtectedKeys(value, properties)) { | ||
try { | ||
const nested = Reflect.get(value, key); | ||
protectProperties(nested, [], depth - 1); | ||
} catch { | ||
// Reflect.get might fail in certain edge-cases | ||
// Instead of failing the entire process, we will skip the property. | ||
} | ||
} | ||
return result; | ||
} | ||
return false; | ||
} finally { | ||
process.emitWarning = originalEmitWarning; | ||
} | ||
return false; | ||
} | ||
@@ -418,2 +442,58 @@ | ||
/** | ||
* Deletes the property of the given key from the given object. | ||
* | ||
* @param obj the given object. | ||
* @param key the given key. | ||
* @param mode there are two possible modes of deletion: | ||
* - <b>soft</b>: doesn't delete the object, but instead wraps its getter/setter with a deprecation warning. | ||
* - <b>hard</b>: actually deletes the object (`delete`). | ||
* | ||
* @returns whether the deletion was successful or not. | ||
*/ | ||
function deleteProperty(obj, key, mode) { | ||
const descriptor = Reflect.getOwnPropertyDescriptor(obj, key); | ||
if (!descriptor?.configurable) { | ||
return false; | ||
} | ||
if (mode === 'hard') { | ||
return Reflect.deleteProperty(obj, key); | ||
} | ||
const originalGetter = descriptor.get ?? (() => descriptor.value); | ||
const originalSetter = descriptor.set ?? (value => Reflect.set(obj, key, value)); | ||
return Reflect.defineProperty(obj, key, { | ||
configurable: true, | ||
enumerable: descriptor.enumerable, | ||
get() { | ||
emitAccessWarning(obj, key); | ||
return originalGetter(); | ||
}, | ||
set(value) { | ||
emitAccessWarning(obj, key); | ||
return originalSetter(value); | ||
} | ||
}); | ||
} | ||
const warningCache = new WeakSet(); | ||
function emitAccessWarning(obj, key) { | ||
if (warningCache.has(obj)) { | ||
return; | ||
} | ||
const objName = obj?.constructor?.name ?? 'unknown'; | ||
const propertyName = typeof key === 'symbol' ? key.description : key; | ||
process.emitWarning(`'${propertyName}' property was accessed on [${objName}] after it was soft deleted`, { | ||
code: 'JEST-01', | ||
detail: ['Jest deletes objects that were set on the global scope between test files to reduce memory leaks.', 'Currently it only "soft" deletes them and emits this warning if those objects were accessed after their deletion.', 'In future versions of Jest, this behavior will change to "hard", which will likely fail tests.', 'You can change the behavior in your test configuration now to reduce memory usage.'].map(s => ` ${s}`).join('\n'), | ||
type: 'DeprecationWarning' | ||
}); | ||
warningCache.add(obj); | ||
} | ||
function getProtectedKeys(value, properties) { | ||
if (properties === undefined) { | ||
return []; | ||
} | ||
const protectedKeys = properties.length > 0 ? properties : Reflect.ownKeys(value); | ||
return protectedKeys.filter(key => PROTECT_SYMBOL !== key); | ||
} | ||
/***/ }), | ||
@@ -420,0 +500,0 @@ |
{ | ||
"name": "jest-util", | ||
"version": "30.0.0-beta.3", | ||
"version": "30.0.0-beta.6", | ||
"repository": { | ||
@@ -22,3 +22,3 @@ "type": "git", | ||
"dependencies": { | ||
"@jest/types": "30.0.0-beta.3", | ||
"@jest/types": "30.0.0-beta.6", | ||
"@types/node": "*", | ||
@@ -32,6 +32,7 @@ "chalk": "^4.0.0", | ||
"@types/graceful-fs": "^4.1.3", | ||
"@types/picomatch": "^4.0.0" | ||
"@types/picomatch": "^4.0.0", | ||
"lodash": "^4.17.19" | ||
}, | ||
"engines": { | ||
"node": "^18.14.0 || ^20.0.0 || >=22.0.0" | ||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" | ||
}, | ||
@@ -41,3 +42,3 @@ "publishConfig": { | ||
}, | ||
"gitHead": "a123a3b667a178fb988662aaa1bc6308af759017" | ||
"gitHead": "4f964497dc21c06ce4d54f1349e299a9f6773d52" | ||
} |
52038
7.75%1273
7.52%3
50%+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
Updated