From 383c0a3939fdca9aa894073426dad995a97a30ee Mon Sep 17 00:00:00 2001 From: ztimson Date: Thu, 8 Oct 2026 12:28:30 -0400 Subject: [PATCH] isEqual callback safety --- package.json | 2 +- src/objects.ts | 60 +++++++++++++++++++++++++++----------------------- 2 files changed, 33 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index b74834b..ce1f8b0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ztimson/utils", - "version": "0.31.0", + "version": "0.31.1", "description": "Utility library", "author": "Zak Timson", "license": "MIT", diff --git a/src/objects.ts b/src/objects.ts index d62a428..0c9fe1e 100644 --- a/src/objects.ts +++ b/src/objects.ts @@ -255,36 +255,40 @@ export function includes(target: any, values: any, allowMissing = false): boolea * @param {string} [path] - Internal, current dot-path * @returns {boolean} True if they match */ -export function isEqual(a: any, b: any, compare?: string[] | ((key: string, a: any, b: any) => boolean | void), seen = new WeakMap(), path = ''): boolean { - if(Array.isArray(compare)) { - if(compare.some(p => new RegExp(`^${p.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^.]*')}(\\.|$)`).test(path))) return true; - } else if(typeof compare === 'function') { - const r = compare(path, a, b); - if(r !== undefined) return r; - } - if(a === b) return true; - if(a instanceof Date && b instanceof Date) return a.getTime() === b.getTime(); - if(a instanceof RegExp && b instanceof RegExp) return a.source === b.source && a.flags === b.flags; - if(typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) { - if(Number.isNaN(a) && Number.isNaN(b)) return true; - if(typeof a === 'function' && typeof b === 'function') return a.toString() === b.toString(); - return false; - } - if(seen.has(a)) return seen.get(a) === b; - seen.set(a, b); - const isArrayA = Array.isArray(a), isArrayB = Array.isArray(b); - if(isArrayA && isArrayB) { - if(a.length !== b.length) return false; - for(let i = 0; i < a.length; i++) if(!isEqual(a[i], b[i], compare, seen, path ? `${path}.${i}` : `${i}`)) return false; +export function isEqual(a: any, b: any, compare?: string[] | ((key: string, a: any, b: any) => boolean | void)): boolean { + const eq = (a: any, b: any, compare: any, seen: WeakMap, path: string) => { + if(Array.isArray(compare)) { + if(compare.some(p => new RegExp(`^${p.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^.]*')}(\\.|$)`).test(path))) return true; + } else if(typeof compare === 'function') { + const r = compare(path, a, b); + if(r !== undefined) return r; + } + if(a === b) return true; + if(a instanceof Date && b instanceof Date) return a.getTime() === b.getTime(); + if(a instanceof RegExp && b instanceof RegExp) return a.source === b.source && a.flags === b.flags; + if(typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) { + if(Number.isNaN(a) && Number.isNaN(b)) return true; + if(typeof a === 'function' && typeof b === 'function') return a.toString() === b.toString(); + return false; + } + if(seen.has(a)) return seen.get(a) === b; + seen.set(a, b); + const isArrayA = Array.isArray(a), isArrayB = Array.isArray(b); + if(isArrayA && isArrayB) { + if(a.length !== b.length) return false; + for(let i = 0; i < a.length; i++) if(!eq(a[i], b[i], compare, seen, path ? `${path}.${i}` : `${i}`)) return false; + return true; + } + if(isArrayA !== isArrayB) return false; + const keysA = Object.keys(a), keysB = Object.keys(b); + if(keysA.length !== keysB.length) return false; + for(const key of keysA) { + if(!Object.prototype.hasOwnProperty.call(b, key) || !eq(a[key], b[key], compare, seen, path ? `${path}.${key}` : key)) return false; + } return true; } - if(isArrayA !== isArrayB) return false; - const keysA = Object.keys(a), keysB = Object.keys(b); - if(keysA.length !== keysB.length) return false; - for(const key of keysA) { - if(!Object.prototype.hasOwnProperty.call(b, key) || !isEqual(a[key], b[key], compare, seen, path ? `${path}.${key}` : key)) return false; - } - return true; + + return eq(a, b, compare, new WeakMap(), ''); } /**