Compare map keys recursively in recursive comparisons - #4417
Closed
Islandec235 wants to merge 3 commits into
Closed
Islandec235 wants to merge 3 commits into
Islandec235 wants to merge 3 commits into
Conversation
Member
|
Answering the feedback:
thanks ! |
Match whole entries with isolated trial state and bounded reuse of completed comparisons. Preserve value paths, sorted order, and configured equality rules. Deep nested map comparisons still use nested Java calls at this intermediate step; the following commit replaces them with explicit frames.
Suspend and resume nested map comparisons on an explicit frame stack. Restore temporary type-selection locations when frames finish or a comparison throws. Add depth regressions for sorted and unordered maps under both introspection strategies.
Continue ordered entry comparison when type selection filters out the map-level size diagnostic. Compare unmatched keys and values independently against null so selected types are still checked. Add regressions for both sides, empty and nested maps, selected values, and ignored entries.
Islandec235
force-pushed
the
fix/recursive-map-keys-3835
branch
from
September 27, 2026 17:46
2dae4f4 to
0d1e30f
Compare
Member
|
This PR did more than addressing the bug, I ended up only taking the tests (not all, since a few were not relevant), the sheer complexity of the execution frame deterred me from integrating this PR. Thanks for the tests ! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #3835
This makes map keys follow the recursive comparison configuration by default, as discussed for 4.0.0. Keys with equal fields can match across types, ignored fields and custom comparators apply to keys, and differences hidden by a key's overridden
equalsare detected unless overridden equality is explicitly requested.I gave the entry-based approach a try. It grew beyond the small change I initially expected, so I'm opening this as a draft to get feedback on the structure and tradeoffs.
Approach
The comparison works with
Map.Entryobjects but compares their keys and values explicitly. This preserves existing value paths and avoids treating an entry as an ordinary JDK object or swapping key/value roles when collection order is ignored. Sorted maps retain ordered comparison, including when ignored entries are filtered out. No public option or runtime dependency is added.For unordered maps, matching has to consider the whole entry. If ignoring
idmakes two keys equivalent,Sam/1 -> red, Sam/2 -> bluemust matchSam/3 -> blue, Sam/4 -> red. Each entry is used once, and an augmenting-path search can revise an earlier choice when a comparator allows overlapping matches. Hashes prioritize candidates but never exclude a recursive match. When a full match fails, a key-only pass retains missing-key diagnostics and value differences at their existing paths.What made this less straightforward
The cache is deliberately conservative. Path-dependent rules, cycle-dependent results and eviction can still cause expensive repeated work, potentially exponential over a shared graph. For one map, worst-case matching is O(n³ × C), where C includes nested comparison cost; this is not a bound for the entire object graph. Comparators are assumed to return stable results. Ambiguous matches also do not have a unique diagnostic pairing.
Validation
fieldsandlegacystrategies; all 116 map cases pass. Coverage includes ambiguous matching, reassignment, nulls, cycles, type selection, shared references, sorted order, deep nesting and repeated expansion../mvnw -pl assertj-tests/assertj-integration-tests/assertj-core-tests -am clean verifywith JDK 25 and local JVM argument/encoding overrides for Windows paths. The latest run passed: 20,819 passed, 68 skipped, no failures. Before this update, an earlier run had one failure in the unchangedproperties.RecursiveComparisonAssert_isEqualTo_Test.should_be_faster_the_second_time_as_the_getter_introspection_is_cached(0 ms < 0 ms); upstream subsequently disabled that flaky timing test in 7ca2715, which is now the base of this branch. The entire monorepo reactor was not run.Review follow-up
The change is now split into three commits, following Joel's feedback:
The bounded cache and its limitations are unchanged; the cache discussion remains open for later review. This remains a draft for feedback on the implementation.
Check List: