fix(form-core): use equals() for value-type equality (Temporal, Decimal.js)#2197
fix(form-core): use equals() for value-type equality (Temporal, Decimal.js)#2197mixelburg wants to merge 1 commit into
Conversation
📝 WalkthroughWalkthroughUpdated the ChangesCustom Object Equality Support
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (2)
packages/form-core/tests/utils.spec.ts (1)
696-706: ⚡ Quick winConsider expanding test coverage for edge cases.
The current test verifies the happy path (matching constructors, working
equals()method). Consider adding test cases for:
- Objects with different constructors (should fall back to key-based equality)
- Interaction with other equals-implementing types to ensure the constructor check works correctly
This would increase confidence that the constructor guard prevents unintended cross-type comparisons.
📋 Suggested additional test cases
it('should not use equals() when constructors differ', () => { class TypeA { constructor(private val: number) {} equals(other: TypeA) { return this.val === other.val } } class TypeB { constructor(private val: number) {} equals(other: TypeB) { return this.val === other.val } } // Different constructors, should fall back to key-based equality // Since both have no enumerable properties, they appear equal by keys expect(evaluate(new TypeA(1), new TypeB(1))).toEqual(true) // But if we add a public property, the constructor check prevents // equals() from being called inappropriately class TypeC { constructor(public val: number) {} equals(other: TypeC) { return this.val === other.val } } class TypeD { constructor(public val: number) {} equals(other: TypeD) { return this.val === other.val } } expect(evaluate(new TypeC(1), new TypeD(1))).toEqual(true) // same enumerable keys expect(evaluate(new TypeC(1), new TypeD(2))).toEqual(false) // different values })🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/form-core/tests/utils.spec.ts` around lines 696 - 706, Add tests exercising cross-constructor behavior of evaluate: create two classes with identical equals() implementations but different constructors (e.g., TypeA and TypeB) and assert evaluate(new TypeA(1), new TypeB(1)) falls back to key-based equality; then add classes with public/enumerable property (TypeC/TypeD) and assert evaluate(new TypeC(1), new TypeD(1)) is true for same values and evaluate(new TypeC(1), new TypeD(2)) is false, so the constructor guard preventing cross-type equals() invocation and the key-based fallback are covered.packages/form-core/src/utils.ts (1)
442-447: 💤 Low valueConsider defensive check for
objB.equalsto prevent edge-case runtime errors.While the constructor check ensures both objects are of the same type, a defensive programmer might delete or override the
equalsmethod onobjB. Adding a check fortypeof (objB as any).equals === 'function'would prevent potential runtime errors in pathological cases.🛡️ Proposed defensive check
if ( typeof (objA as any).equals === 'function' && + typeof (objB as any).equals === 'function' && objA.constructor === objB.constructor ) { return (objA as any).equals(objB) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/form-core/src/utils.ts` around lines 442 - 447, The block that calls (objA as any).equals(objB) should first defensively verify that (objB as any).equals is also a function to avoid runtime errors if objB has had equals removed or overwritten; update the condition in the equality branch (where objA and objB constructors are compared) to require typeof (objA as any).equals === 'function' && typeof (objB as any).equals === 'function' before returning (objA as any).equals(objB), referencing the existing objA, objB and equals symbols.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@packages/form-core/src/utils.ts`:
- Around line 442-447: The block that calls (objA as any).equals(objB) should
first defensively verify that (objB as any).equals is also a function to avoid
runtime errors if objB has had equals removed or overwritten; update the
condition in the equality branch (where objA and objB constructors are compared)
to require typeof (objA as any).equals === 'function' && typeof (objB as
any).equals === 'function' before returning (objA as any).equals(objB),
referencing the existing objA, objB and equals symbols.
In `@packages/form-core/tests/utils.spec.ts`:
- Around line 696-706: Add tests exercising cross-constructor behavior of
evaluate: create two classes with identical equals() implementations but
different constructors (e.g., TypeA and TypeB) and assert evaluate(new TypeA(1),
new TypeB(1)) falls back to key-based equality; then add classes with
public/enumerable property (TypeC/TypeD) and assert evaluate(new TypeC(1), new
TypeD(1)) is true for same values and evaluate(new TypeC(1), new TypeD(2)) is
false, so the constructor guard preventing cross-type equals() invocation and
the key-based fallback are covered.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 3e6440c0-d12f-4eac-b77c-687989d21d7a
📒 Files selected for processing (2)
packages/form-core/src/utils.tspackages/form-core/tests/utils.spec.ts
Fixes #2195
The
evaluatefunction in form-core used key-based deep equality for all objects. This broke for value types likeTemporal(TC39 Stage 4, now in browsers/Deno/Node) andDecimal.js, which have no enumerable properties — two instances with the same value would always be considered unequal.This PR adds a duck-typed check: if both objects have the same constructor and the left-hand object has an
equals()method, we delegate to it. This coversTemporal.PlainDate,Temporal.Instant,Temporal.Duration,Decimal.js, and similar value types.The change is placed before the Map/Set checks and after the Date check, so more specific
instanceofchecks take precedence.All 439 existing tests pass; 1 new test added.
Summary by CodeRabbit
Bug Fixes
Tests