From c147cd13f51c30f1938ff02c8efceb65146b2f68 Mon Sep 17 00:00:00 2001 From: thisconnect Date: Mon, 12 May 2025 12:29:57 +0200 Subject: [PATCH 1/6] frontend: convert equal to typescript --- frontends/web/src/utils/{equal.js => equal.ts} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename frontends/web/src/utils/{equal.js => equal.ts} (91%) diff --git a/frontends/web/src/utils/equal.js b/frontends/web/src/utils/equal.ts similarity index 91% rename from frontends/web/src/utils/equal.js rename to frontends/web/src/utils/equal.ts index 1e4cd8dce6..ad935d7928 100644 --- a/frontends/web/src/utils/equal.js +++ b/frontends/web/src/utils/equal.ts @@ -18,13 +18,13 @@ const isArray = Array.isArray; const keyList = Object.keys; const hasProp = Object.prototype.hasOwnProperty; -export function equal(a, b) { +export function equal(a: any, b: any): boolean { if (Object.is(a, b)) { return true; } if (a && b && typeof a === 'object' && typeof b === 'object') { - let arrA = isArray(a), arrB = isArray(b), i, length, key; + let arrA = isArray(a), arrB = isArray(b), i: number, length: number, key: string; if (arrA && arrB) { length = a.length; From 3c83c33ed2a4b610a061f7b495a2c84945ea4cd7 Mon Sep 17 00:00:00 2001 From: thisconnect Date: Mon, 12 May 2025 18:36:13 +0200 Subject: [PATCH 2/6] frontend: refactor equal and inline variables Simplifying so that TypeScript better understand the code and we can change from any to unknown in the future. --- frontends/web/src/utils/equal.ts | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/frontends/web/src/utils/equal.ts b/frontends/web/src/utils/equal.ts index ad935d7928..1491755b48 100644 --- a/frontends/web/src/utils/equal.ts +++ b/frontends/web/src/utils/equal.ts @@ -18,20 +18,17 @@ const isArray = Array.isArray; const keyList = Object.keys; const hasProp = Object.prototype.hasOwnProperty; -export function equal(a: any, b: any): boolean { +export const equal = (a: any, b: any): boolean => { if (Object.is(a, b)) { return true; } if (a && b && typeof a === 'object' && typeof b === 'object') { - let arrA = isArray(a), arrB = isArray(b), i: number, length: number, key: string; - - if (arrA && arrB) { - length = a.length; - if (length !== b.length) { + if (isArray(a) && isArray(b)) { + if (a.length !== b.length) { return false; } - for (i = 0; i < length; i++) { + for (let i = 0; i < a.length; i++) { if (!equal(a[i], b[i])) { return false; } @@ -39,26 +36,23 @@ export function equal(a: any, b: any): boolean { return true; } - if (arrA !== arrB) { + if (isArray(a) !== isArray(b)) { return false; } - let keys = keyList(a); - length = keys.length; - + const length = keyList(a).length; if (length !== keyList(b).length) { return false; } - for (i = 0; i < length; i++) { - if (!hasProp.call(b, keys[i])) { + for (let i = 0; i < length; i++) { + if (!hasProp.call(b, keyList(a)[i])) { return false; } } - for (i = 0; i < length; i++) { - key = keys[i]; - if (!equal(a[key], b[key])) { + for (let i = 0; i < length; i++) { + if (!equal(a[keyList(a)[i]], b[keyList(a)[i]])) { return false; } } From b642aef372e5ce75ba169a1b84e5c9a1b1b0bc93 Mon Sep 17 00:00:00 2001 From: thisconnect Date: Tue, 13 May 2025 08:28:09 +0200 Subject: [PATCH 3/6] frontend: change equal argument types to unknown for safer typing Using `unknown` enforces type safety by requiring explicit type checks or assertions before performing operations on the values. Also updated `Object.keys` usage with a more precise type assertion to satisfy TypeScript's stricter checks when working with `unknown`. This version now also supports date and regexp. --- frontends/web/src/utils/equal.test.tsx | 28 ++++++++++++++ frontends/web/src/utils/equal.ts | 53 ++++++++++++++++++++------ 2 files changed, 70 insertions(+), 11 deletions(-) diff --git a/frontends/web/src/utils/equal.test.tsx b/frontends/web/src/utils/equal.test.tsx index 8a5be45213..6c384cfe37 100644 --- a/frontends/web/src/utils/equal.test.tsx +++ b/frontends/web/src/utils/equal.test.tsx @@ -114,5 +114,33 @@ describe('equal', () => { expect(equal(a, null)).toBeFalsy(); expect(equal(null, a)).toBeFalsy(); }); + + it('deep compares nested structures', () => { + const a = { foo: [1, { bar: 'baz' }] }; + const b = { foo: [1, { bar: 'baz' }] }; + expect(equal(a, b)).toBeTruthy(); + }); + }); + + describe('RegExp, functions and dates are currently not supported', () => { + + it('compares RegExp objects correctly', () => { + expect(equal(/foo/g, /foo/g)).toBeTruthy(); + expect(equal(/foo/g, /bar/g)).toBeFalsy(); + }); + + it('compares Date objects correctly', () => { + expect(equal(new Date('2020-01-01'), new Date('2020-01-01'))).toBeTruthy(); + expect(equal(new Date('2020-01-01'), new Date('2021-01-01'))).toBeFalsy(); + }); + + it('does not consider functions equal', () => { + const a = () => {}; + const b = () => {}; + expect(equal(a, b)).toBeFalsy(); + }); + + }); + }); diff --git a/frontends/web/src/utils/equal.ts b/frontends/web/src/utils/equal.ts index 1491755b48..ea59b3caa4 100644 --- a/frontends/web/src/utils/equal.ts +++ b/frontends/web/src/utils/equal.ts @@ -15,14 +15,46 @@ */ const isArray = Array.isArray; -const keyList = Object.keys; const hasProp = Object.prototype.hasOwnProperty; +const typedKeys = (obj: Readonly): readonly (keyof T)[] => { + return Object.keys(obj) as (keyof T)[]; +}; -export const equal = (a: any, b: any): boolean => { +/** + * Performs a deep equality check between two values. + * + * This function compares primitive types, arrays, plain objects, Date instances, + * and RegExp objects. It returns true if the values are deeply equal, false otherwise. + * + * - Uses `Object.is` for primitive comparison (handles `NaN`, `-0`, etc.) + * - Recursively checks array contents and object properties + * - Properly compares Date and RegExp objects + * - Returns false for functions, symbols, maps, sets, or class instances (not handled) + * + * @param a - The first value to compare. + * @param b - The second value to compare. + * @returns `true` if values are deeply equal, `false` otherwise. + */ +export const equal = (a: unknown, b: unknown): boolean => { if (Object.is(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.toString() === b.toString(); + } + + if ( + (a instanceof Date) !== (b instanceof Date) + || (a instanceof RegExp) !== (b instanceof RegExp) + ) { + return false; + } + if (a && b && typeof a === 'object' && typeof b === 'object') { if (isArray(a) && isArray(b)) { if (a.length !== b.length) { @@ -40,19 +72,18 @@ export const equal = (a: any, b: any): boolean => { return false; } - const length = keyList(a).length; - if (length !== keyList(b).length) { + const aKeys = typedKeys(a); + const bKeys = typedKeys(b); + + if (aKeys.length !== bKeys.length) { return false; } - for (let i = 0; i < length; i++) { - if (!hasProp.call(b, keyList(a)[i])) { + for (const key of aKeys) { + if (!hasProp.call(b, key)) { return false; } - } - - for (let i = 0; i < length; i++) { - if (!equal(a[keyList(a)[i]], b[keyList(a)[i]])) { + if (!equal(a[key], b[key])) { return false; } } @@ -61,4 +92,4 @@ export const equal = (a: any, b: any): boolean => { } return false; -} +}; From f5935ce7bea0944a0bbea1e8ad944015f99d4fe3 Mon Sep 17 00:00:00 2001 From: thisconnect Date: Wed, 21 May 2025 19:26:00 +0200 Subject: [PATCH 4/6] frontend: update equal tests Added various tests with some llm help. --- frontends/web/src/utils/equal.test.tsx | 78 ++++++++++++++++++++++++-- 1 file changed, 72 insertions(+), 6 deletions(-) diff --git a/frontends/web/src/utils/equal.test.tsx b/frontends/web/src/utils/equal.test.tsx index 6c384cfe37..4e91f2f5a8 100644 --- a/frontends/web/src/utils/equal.test.tsx +++ b/frontends/web/src/utils/equal.test.tsx @@ -1,5 +1,6 @@ /** * Copyright 2018 Shift Devices AG + * Copyright 2025 Shift Crypto AG * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,11 +28,20 @@ describe('equal', () => { expect(equal(null, null)).toBeTruthy(); }); + it('compares undefined and null', () => { + expect(equal(undefined, undefined)).toBeTruthy(); + expect(equal(undefined, null)).toBeFalsy(); + }); + it('compares ints', () => { expect(equal(13, 13)).toBeTruthy(); expect(equal(1, 13)).toBeFalsy(); }); + it('compares NaN', () => { + expect(equal(NaN, NaN)).toBeTruthy(); + }); + it('compares strings', () => { expect(equal('foo', 'foo')).toBeTruthy(); expect(equal('foo', 'bar')).toBeFalsy(); @@ -83,6 +93,10 @@ describe('equal', () => { expect(equal(null, {})).toBeFalsy(); }); + it('is false for [] and {}', () => { + expect(equal([], {})).toBeFalsy(); + }); + it('is true for same key/value pairs', () => { const a = { one: 'two', three: 'four' }; const b = { one: 'two', three: 'four' }; @@ -115,16 +129,43 @@ describe('equal', () => { expect(equal(null, a)).toBeFalsy(); }); + it('doesn’t affect key order equality', () => { + const a = { a: 1, b: 2 }; + const b = { b: 2, a: 1 }; + expect(equal(a, b)).toBeTruthy(); + }); + it('deep compares nested structures', () => { const a = { foo: [1, { bar: 'baz' }] }; const b = { foo: [1, { bar: 'baz' }] }; expect(equal(a, b)).toBeTruthy(); + const c = { foo: [1, { bar: 'qux' }] }; + expect(equal(a, c)).toBeFalsy(); }); - }); + it('fails on deep nested mismatch', () => { + const a = { foo: { bar: { baz: 1 } } }; + const b = { foo: { bar: { baz: 2 } } }; + expect(equal(a, b)).toBeFalsy(); + }); + + it('compares object with mixed value types', () => { + const a = { num: 1, str: 'x', bool: true }; + const b = { num: 1, str: 'x', bool: true }; + expect(equal(a, b)).toBeTruthy(); + }); - describe('RegExp, functions and dates are currently not supported', () => { + it('returns false for two different Symbols with same description', () => { + expect(equal(Symbol('x'), Symbol('x'))).toBeFalsy(); + }); + + it('compares Symbols', () => { + const s = Symbol('x'); + expect(equal(s, s)).toBeTruthy(); + }); + }); + describe('RegExp, functions and dates', () => { it('compares RegExp objects correctly', () => { expect(equal(/foo/g, /foo/g)).toBeTruthy(); expect(equal(/foo/g, /bar/g)).toBeFalsy(); @@ -135,12 +176,37 @@ describe('equal', () => { expect(equal(new Date('2020-01-01'), new Date('2021-01-01'))).toBeFalsy(); }); - it('does not consider functions equal', () => { + it('returns true only for same reference', () => { const a = () => {}; - const b = () => {}; - expect(equal(a, b)).toBeFalsy(); + expect(equal(a, a)).toBeTruthy(); }); + it('returns false for different functions', () => { + const fn1 = () => {}; + const fn2 = () => {}; + expect(equal(fn1, fn2)).toBeFalsy(); + }); }); - }); + +describe('edge cases: array vs object structure', () => { + it('[] vs {} is not equal', () => { + expect(equal([], {})).toBeFalsy(); + }); + + it('empty array vs object with numeric key is not equal', () => { + const arr: any = []; + const obj = { 0: undefined }; + expect(equal(arr, obj)).toBeFalsy(); + }); + + it('array with undefined value vs object with matching key is not equal', () => { + const arr = [undefined]; + const obj = { 0: undefined }; + expect(equal(arr, obj)).toBeFalsy(); + }); + + it('nested empty object vs array is not equal', () => { + expect(equal({ foo: [] }, { foo: {} })).toBeFalsy(); + }); +}); \ No newline at end of file From 3eaa0897cc34603dabd2519e2c16261e021c8cde Mon Sep 17 00:00:00 2001 From: thisconnect Date: Wed, 21 May 2025 21:09:15 +0200 Subject: [PATCH 5/6] frontend: test sparse arrays --- frontends/web/src/utils/equal.test.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontends/web/src/utils/equal.test.tsx b/frontends/web/src/utils/equal.test.tsx index 4e91f2f5a8..eae26fda37 100644 --- a/frontends/web/src/utils/equal.test.tsx +++ b/frontends/web/src/utils/equal.test.tsx @@ -85,6 +85,11 @@ describe('equal', () => { expect(equal(a, b)).toBeFalsy(); expect(equal(b, a)).toBeFalsy(); }); + + it('compares sparse array vs defined array', () => { + expect(equal([1, , 3], [1, undefined, 3])).toBeFalsy(); + expect(equal([1, , 3], [1, , 3])).toBeTruthy(); + }); }); describe('objects', () => { From 5ee9762b785d36db44eef14894d7331e56c8c3b2 Mon Sep 17 00:00:00 2001 From: thisconnect Date: Wed, 21 May 2025 21:12:30 +0200 Subject: [PATCH 6/6] frontend: fix handling of sparse arrays in equal function Fixed failing test introduced in last commit and correctly handle sparse arrays. --- frontends/web/src/utils/equal.test.tsx | 4 +++- frontends/web/src/utils/equal.ts | 8 +++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/frontends/web/src/utils/equal.test.tsx b/frontends/web/src/utils/equal.test.tsx index eae26fda37..de0027c37b 100644 --- a/frontends/web/src/utils/equal.test.tsx +++ b/frontends/web/src/utils/equal.test.tsx @@ -87,7 +87,9 @@ describe('equal', () => { }); it('compares sparse array vs defined array', () => { + /*eslint no-sparse-arrays: "off"*/ expect(equal([1, , 3], [1, undefined, 3])).toBeFalsy(); + /*eslint no-sparse-arrays: "off"*/ expect(equal([1, , 3], [1, , 3])).toBeTruthy(); }); }); @@ -214,4 +216,4 @@ describe('edge cases: array vs object structure', () => { it('nested empty object vs array is not equal', () => { expect(equal({ foo: [] }, { foo: {} })).toBeFalsy(); }); -}); \ No newline at end of file +}); diff --git a/frontends/web/src/utils/equal.ts b/frontends/web/src/utils/equal.ts index ea59b3caa4..16aa54fc31 100644 --- a/frontends/web/src/utils/equal.ts +++ b/frontends/web/src/utils/equal.ts @@ -1,5 +1,6 @@ /** * Copyright 2018 Shift Devices AG + * Copyright 2025 Shift Crypto AG * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,7 +62,12 @@ export const equal = (a: unknown, b: unknown): boolean => { return false; } for (let i = 0; i < a.length; i++) { - if (!equal(a[i], b[i])) { + // handle sparse arrays + const hasA = i in a; + if (hasA !== i in b) { + return false; + } + if (hasA && !equal(a[i], b[i])) { return false; } }