Skip to content

fix(equal): one-to-one Set/Map matching, symmetric conversions, null-prototype objects - #2408

Merged
rkaraivanov merged 3 commits into
masterfrom
rkaraivanov/equal-fix
Sep 28, 2026
Merged

rkaraivanov merged 3 commits into
masterfrom
rkaraivanov/equal-fix

Conversation

@rkaraivanov

@rkaraivanov rkaraivanov commented Sep 28, 2026 •

Copy link
Copy Markdown
Member

Description

Fixes three defects in the internal equal deep comparison. Two came from porting IgniteUI/igniteui-react#193, and the third came up in review.

  • Set/Map one-to-one matching. An entry could match an entry on the other side that was already matched, so new Set([{x:1},{x:1}]) equaled new Set([{x:1},{x:2}]). Each entry now takes a distinct partner.
  • Symmetric conversions. A custom valueOf/toString was checked on the first argument only, so equal({}, Object.create({ valueOf: () => 1 })) was true and the reverse was false. This also made greedy Set/Map pairing miss valid matches. Objects now compare by conversion only when both sides override it, and are unequal when only one side does. equal is now an equivalence relation, so greedy pairing is correct.
  • Null-prototype objects. Comparing Object.create(null) objects threw valueOf is not a function. They now compare by their keys.

Ported from IgniteUI/igniteui-react#193.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)

Checklist

  • My code follows the project's coding standards
  • I have tested my changes locally

…jects

A Set or Map entry could match an entry on the other side that was
already matched, so `new Set([{x:1},{x:1}])` equaled
`new Set([{x:1},{x:2}])`. Each entry now takes a distinct match.

Null-prototype objects threw, since `valueOf` and `toString` were
called without an existence check.

Ported from IgniteUI/igniteui-react#193.
Copilot AI lite review requested due to automatic review settings September 28, 2026 16:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

A moderate issue remains when corresponding methods are missing on the second object.

Review effort: Lite
Findings: 1 Medium severity

Open (1)
What changed in this PR

Fixes deep equality for one-to-one Set/Map matching and null-prototype objects.

Changes:

  • Adds distinct collection-entry matching.
  • Guards missing object methods.
  • Adds regression tests.
File Summary
src/​internals/​utils/​objects.ts Updates equality and collection matching logic.
src/​internals/​utils/​objects.spec.ts Adds regression coverage.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/internals/utils/objects.ts Outdated
A null-prototype `a` with its own `valueOf` or `toString` threw when
`b` lacked the method. The objects now compare unequal instead.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Two moderate review issues remain unresolved.

Review effort: Lite
Findings: None

Resolved since last review (1)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Greedy matching fails with asymmetric custom comparisons

src/​internals/​utils/​objects.ts:80

This greedy choice is not safe for all values supported by equal: custom valueOf/toString dispatch makes comparisons directional (for example, equal({}, Object.create({ valueOf: () => 1 })) is true, while the reverse is false). With a plain object before that custom object on the left, the first candidate is consumed and the otherwise valid one-to-one pairing is missed, so two Sets that contain the same two objects can compare false. Use a complete bipartite matching/backtracking strategy, or make the underlying comparison symmetric before relying on greedy matching.

Comparing a custom conversion with the default one made `equal`
directional and non-transitive, so the greedy Set/Map pairing could
miss a valid match. Objects now compare by conversion only when both
override it, and are unequal when only one does. `equal` is then an
equivalence relation, and greedy pairing is safe.
@rkaraivanov
rkaraivanov requested a lite review from Copilot September 28, 2026 16:52
@rkaraivanov rkaraivanov changed the title fix(equal): match Set/Map entries one to one, allow null-prototype objects fix(equal): one-to-one Set/Map matching, symmetric conversions, null-prototype objects Sep 28, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

No unresolved review issues remain, and all reviewed changes are covered by regression tests.

Review effort: Lite
Findings: None

@rkaraivanov
rkaraivanov merged commit d3df446 into master Sep 28, 2026
8 checks passed
@rkaraivanov
rkaraivanov deleted the rkaraivanov/equal-fix branch September 28, 2026 17:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants