jdereg / jdereg/java-util

NullPointerException in DeepEquals.getContainingDescription when object's custom equals() is inconsistent with field-level equality

Open Beginner friendly
#362 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Java
Stars
442
Forks
115
PR merge metrics
No merged PRs in 30d

Description

Affected versions: confirmed on 3.3.0, still present in 4.107.0 (method is byte-for-byte identical).

Body:

DeepEquals.deepEquals(a, b) throws an NPE while generating the breadcrumb diff (only on the not-equal path) when the object graph contains a type whose custom equals() returns false for two instances that are nevertheless deeply field-equal.

Root cause is in getContainingDescription:

private static String getContainingDescription(List<ItemsToCompare> path) {
    ListIterator<ItemsToCompare> it = path.listIterator(path.size());
    String a = it.previous().difference.getDescription();   // <-- last node's `difference` not null-checked

    if (it.hasPrevious()) {
        Difference diff = it.previous().difference;
        if (diff != null) {                                 // parent node IS null-checked
            String b = diff.getDescription();
            if (b != null) {
                return b;
            }
        }
    }
    return a;
}

The last node's difference is dereferenced without a null-check, unlike the parent node just below it. It becomes null via the custom-equals branch in the recursive comparison: when key1.equals(key2) is false but the recursive field-by-field deep comparison finds them equal (diff_item == null), nothing is pushed onto the stack, so the root node (new ItemsToCompare(a, b), difference == null) stays on top → NPE.

Minimal reproduction:

class IdentityEquals {
    final String value = "x";
    @Override public boolean equals(Object o) { return this == o; }
    @Override public int hashCode() { return System.identityHashCode(this); }
}
DeepEquals.deepEquals(new IdentityEquals(), new IdentityEquals()); // throws NPE

Suggested fix: null-guard the last node the same way the parent node is guarded, e.g. String a = last.difference != null ? last.difference.getDescription() : null; (and fall back gracefully when both are null).

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by locating DeepEquals.getContainingDescription and reproduce the issue with the provided IdentityEquals example through DeepEquals.deepEquals(a, b). Verify the breadcrumb-diff path when the custom equals check disagrees with field-level equality, then confirm the completed change no longer throws an NPE and falls back gracefully when both descriptions are absent.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
tooling
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.