-
Notifications
You must be signed in to change notification settings - Fork 1
Updates #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Updates #9
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -64,4 +64,6 @@ jobs: | |
|
|
||
| - run: pnpm install | ||
|
|
||
| - run: pnpm build | ||
|
|
||
| - run: npm publish --provenance | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,90 +1,78 @@ | ||
| import { isObject, isRegex } from './helpers'; | ||
| import type { AnyObject } from './types'; | ||
|
|
||
| type Seen = WeakMap<object, WeakSet<object>>; | ||
|
|
||
| /** | ||
| * Internal comparison with circular reference tracking. | ||
| * Compare two objects for deep equality. | ||
| */ | ||
| function compareValues(left: unknown, right: unknown, seen: Seen): boolean { | ||
| if (left === right) { | ||
| function compareObjects(left: AnyObject, right: AnyObject, seen: Seen): boolean { | ||
| if (hasSeen(seen, left, right)) { | ||
| return true; | ||
| } | ||
|
|
||
| if (left && isObject(left) && right && isObject(right)) { | ||
| // Check for circular reference | ||
| if (hasSeen(seen, left as object, right as object)) { | ||
| return true; | ||
| } | ||
|
|
||
| markSeen(seen, left as object, right as object); | ||
| markSeen(seen, left, right); | ||
|
|
||
| if (left.constructor !== right.constructor) { | ||
| return false; | ||
| } | ||
|
|
||
| if (Array.isArray(left) && Array.isArray(right)) { | ||
| return equalArray(left, right, seen); | ||
| } | ||
|
|
||
| if (left instanceof Map && right instanceof Map) { | ||
| return equalMap(left, right, seen); | ||
| } | ||
|
|
||
| if (left instanceof Set && right instanceof Set) { | ||
| return equalSet(left, right); | ||
| } | ||
| if (left.constructor !== right.constructor) { | ||
| return false; | ||
| } | ||
|
|
||
| if (ArrayBuffer.isView(left) && ArrayBuffer.isView(right)) { | ||
| return equalArrayBuffer(left, right); | ||
| } | ||
| if (Array.isArray(left) && Array.isArray(right)) { | ||
| return equalArray(left, right, seen); | ||
| } | ||
|
|
||
| if (isRegex(left) && isRegex(right)) { | ||
| return left.source === right.source && left.flags === right.flags; | ||
| } | ||
| if (left instanceof Map && right instanceof Map) { | ||
| return equalMap(left, right, seen); | ||
| } | ||
|
|
||
| if (left.valueOf !== Object.prototype.valueOf) { | ||
| return left.valueOf() === right.valueOf(); | ||
| } | ||
| if (left instanceof Set && right instanceof Set) { | ||
| return equalSet(left, right); | ||
| } | ||
|
|
||
| if (left.toString !== Object.prototype.toString) { | ||
| return left.toString() === right.toString(); | ||
| } | ||
| if (left instanceof WeakMap || left instanceof WeakSet) { | ||
| return false; | ||
| } | ||
|
|
||
| const leftKeys = Object.keys(left); | ||
| const rightKeys = Object.keys(right); | ||
| if (ArrayBuffer.isView(left) && ArrayBuffer.isView(right)) { | ||
| return equalArrayBuffer(left, right); | ||
| } | ||
|
|
||
| if (leftKeys.length !== rightKeys.length) { | ||
| return false; | ||
| } | ||
| if (isRegex(left) && isRegex(right)) { | ||
| return left.source === right.source && left.flags === right.flags; | ||
| } | ||
|
|
||
| for (let index = leftKeys.length; index-- !== 0; ) { | ||
| if (!Object.prototype.hasOwnProperty.call(right, leftKeys[index])) { | ||
| return false; | ||
| } | ||
| } | ||
| if (left instanceof Error && right instanceof Error) { | ||
| return equalError(left, right, seen); | ||
| } | ||
|
|
||
| for (let index = leftKeys.length; index-- !== 0; ) { | ||
| const key = leftKeys[index]; | ||
| if (left.valueOf !== Object.prototype.valueOf) { | ||
| return left.valueOf() === right.valueOf(); | ||
| } | ||
|
|
||
| // React-specific: avoid comparing React elements' _owner | ||
| // which contains different fiber references between renders | ||
| if (key === '_owner' && left.$$typeof) { | ||
| continue; | ||
| } | ||
| if (left.toString !== Object.prototype.toString) { | ||
| return left.toString() === right.toString(); | ||
| } | ||
|
|
||
| if (!compareValues(left[key], right[key], seen)) { | ||
| return false; | ||
| } | ||
| } | ||
| return equalPlainObject(left, right, seen); | ||
| } | ||
|
|
||
| /** | ||
| * Internal comparison with circular reference tracking. | ||
| */ | ||
| function compareValues(left: unknown, right: unknown, seen: Seen): boolean { | ||
| if (left === right) { | ||
| return true; | ||
| } | ||
|
|
||
| if (Number.isNaN(left) && Number.isNaN(right)) { | ||
| return true; | ||
| } | ||
|
|
||
| return left === right; | ||
| if (!left || !isObject(left) || !right || !isObject(right)) { | ||
| return false; | ||
| } | ||
|
|
||
| return compareObjects(left, right, seen); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -128,6 +116,17 @@ function equalArrayBuffer(left: ArrayBufferView, right: ArrayBufferView) { | |
| return true; | ||
| } | ||
|
|
||
| /** | ||
| * Check if errors are equal. | ||
| */ | ||
| function equalError(left: Error, right: Error, seen: Seen): boolean { | ||
| return ( | ||
| left.message === right.message && | ||
| left.name === right.name && | ||
| compareValues(left.cause, right.cause, seen) | ||
| ); | ||
| } | ||
|
Comment on lines
+119
to
+128
|
||
|
|
||
| /** | ||
| * Check if maps are equal. | ||
| */ | ||
|
|
@@ -151,6 +150,37 @@ function equalMap(left: Map<unknown, unknown>, right: Map<unknown, unknown>, see | |
| return true; | ||
| } | ||
|
|
||
| /** | ||
| * Check if plain objects are equal. | ||
| */ | ||
| function equalPlainObject(left: AnyObject, right: AnyObject, seen: Seen): boolean { | ||
| const leftKeys = Object.keys(left); | ||
|
|
||
| if (leftKeys.length !== Object.keys(right).length) { | ||
| return false; | ||
| } | ||
|
|
||
| for (let index = leftKeys.length; index-- !== 0; ) { | ||
| if (!Object.prototype.hasOwnProperty.call(right, leftKeys[index])) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| for (let index = leftKeys.length; index-- !== 0; ) { | ||
| const key = leftKeys[index]; | ||
|
|
||
| if (key === '_owner' && left.$$typeof) { | ||
| continue; | ||
| } | ||
|
|
||
| if (!compareValues(left[key], right[key], seen)) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| /** | ||
| * Check if sets are equal. | ||
| */ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,49 @@ | ||
| import equal from '../src'; | ||
|
|
||
| describe('objects', () => { | ||
| describe('errors', () => { | ||
| it.each([ | ||
| { | ||
| description: 'errors with same message are equal', | ||
| value1: new Error('a'), | ||
| value2: new Error('a'), | ||
| expected: true, | ||
| }, | ||
| { | ||
| description: 'errors with different message are not equal', | ||
| value1: new Error('a'), | ||
| value2: new Error('b'), | ||
| expected: false, | ||
| }, | ||
| { | ||
| description: 'TypeError and Error with same message are not equal', | ||
| value1: new TypeError('a'), | ||
| value2: new Error('a'), | ||
| expected: false, | ||
| }, | ||
| { | ||
| description: 'errors with same cause are equal', | ||
| value1: new Error('a', { cause: 'x' }), | ||
| value2: new Error('a', { cause: 'x' }), | ||
| expected: true, | ||
| }, | ||
| { | ||
| description: 'errors with different cause are not equal', | ||
| value1: new Error('a', { cause: 'x' }), | ||
| value2: new Error('a', { cause: 'y' }), | ||
| expected: false, | ||
| }, | ||
| { | ||
| description: 'errors with deep cause are equal', | ||
| value1: new Error('a', { cause: { n: 1 } }), | ||
| value2: new Error('a', { cause: { n: 1 } }), | ||
| expected: true, | ||
| }, | ||
| ])('$description', ({ expected, value1, value2 }) => { | ||
| expect(equal(value1, value2)).toBe(expected); | ||
| }); | ||
|
Comment on lines
+5
to
+44
|
||
| }); | ||
|
|
||
| describe('circular references', () => { | ||
| it('should handle self-referential objects', () => { | ||
| const a: any = { value: 1 }; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import equal from '../src'; | ||
|
|
||
| describe('WeakMap and WeakSet', () => { | ||
| it('WeakMaps are never equal (not comparable)', () => { | ||
| expect(equal(new WeakMap(), new WeakMap())).toBe(false); | ||
| }); | ||
|
|
||
| it('WeakSets are never equal (not comparable)', () => { | ||
| expect(equal(new WeakSet(), new WeakSet())).toBe(false); | ||
| }); | ||
|
|
||
| it('same WeakMap reference is equal', () => { | ||
| const wm = new WeakMap(); | ||
|
|
||
| expect(equal(wm, wm)).toBe(true); | ||
| }); | ||
|
|
||
| it('same WeakSet reference is equal', () => { | ||
| const ws = new WeakSet(); | ||
|
|
||
| expect(equal(ws, ws)).toBe(true); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The WeakMap/WeakSet check is inconsistent with the pattern used for other types in this function. All other instanceof checks (Map, Set, Error, etc.) verify both left and right parameters, but this check only verifies left. While technically correct due to the constructor equality check on line 16, this inconsistency makes the code harder to understand and maintain. Consider changing to
if ((left instanceof WeakMap && right instanceof WeakMap) || (left instanceof WeakSet && right instanceof WeakSet))to match the established pattern.