Skip to content

Conversation

@Vishalup29
Copy link

Description

This PR adds consistent equals and hashCode implementations to the SDK’s
EvaluationContext implementations (ImmutableContext and LayeredEvaluationContext).

A user reported in Issue #1754 that comparing context instances did not behave as expected,
especially after the introduction of LayeredEvaluationContext. Because the SDK does not
guarantee object identity, equality should be defined based on the resolved attribute map
returned by asMap().

This PR implements that behavior and adds regression tests to ensure we maintain consistent
equality semantics going forward.

Changes

  • Added equals(Object) and hashCode() to:
    • ImmutableContext
    • LayeredEvaluationContext
  • Equality is now defined in terms of their asMap() representations.
  • Added unit tests validating:
    • Equality for two identical ImmutableContext instances.
    • Equality for two identical LayeredEvaluationContext instances.
    • Map-based equality between ImmutableContext and LayeredEvaluationContext.

Related Issue

Fixes #1754

Notes

  • Equality is intentionally based on asMap() rather than structural depth or source layering.
  • This aligns the context behavior with user expectations and simplifies future comparisons.

@Vishalup29 Vishalup29 requested review from a team as code owners November 29, 2025 09:26
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @Vishalup29, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a reported issue where EvaluationContext instances, particularly LayeredEvaluationContext, were not comparing as expected due to a lack of consistent equals and hashCode implementations. The changes introduce these methods to ImmutableContext and LayeredEvaluationContext, ensuring that context equality is determined by their resolved attribute maps. This aligns comparison behavior with user expectations, simplifies future context comparisons, and fixes a reported bug, making the SDK more robust.

Highlights

  • Consistent Equality: Implemented equals and hashCode methods for ImmutableContext and LayeredEvaluationContext to ensure consistent comparison behavior across different context types.
  • Attribute-Based Comparison: Defined equality for EvaluationContext implementations based on their resolved attribute maps (asMap()), ensuring that contexts are considered equal if their effective attributes match, regardless of their internal structure or layering.
  • Comprehensive Testing: Added new unit tests to validate equality for identical ImmutableContext and LayeredEvaluationContext instances, as well as map-based equality between different context types, providing regression coverage for future changes.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@Vishalup29 Vishalup29 changed the title Issue #1754 Implement equals/hashCode for EvaluationContext and add tests fix: Define equality behavior for EvaluationContext implementations Nov 29, 2025
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This PR adds consistent equals and hashCode implementations to ImmutableContext and LayeredEvaluationContext, which is a great improvement for SDK users. The logic to base equality on the resolved map from asMap() is correct and aligns with user expectations.

My review focuses on two main areas:

  1. Performance: The current implementations of equals and hashCode call asMap() on every invocation. This can be inefficient, particularly for LayeredEvaluationContext. I've suggested caching strategies for both classes to mitigate potential performance bottlenecks.
  2. Testing: One of the new tests can be improved to more directly validate the equals contract between different context types.

Additionally, I noticed that ImmutableContext still has a Lombok @EqualsAndHashCode annotation. Since you've provided a manual implementation, this annotation is now misleading and should be removed to improve code clarity, though I was unable to comment on that line directly as it was not part of the diff.

Comment on lines +255 to +270
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof EvaluationContext)) {
return false;
}

EvaluationContext that = (EvaluationContext) o;
return this.asMap().equals(that.asMap());
}

@Override
public int hashCode() {
return asMap().hashCode();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The logic here is correct, but it has a significant performance implication. The asMap() method on LayeredEvaluationContext is expensive, as it constructs a new map by merging all context layers every time it's called. Calling this in equals() and hashCode() can lead to performance bottlenecks, especially if these objects are used in collections.

I recommend caching the resolved map. The cache can be invalidated in putHookContext(), similar to how keySet is handled.

Here's a potential refactoring:

  1. Introduce a private Map<String, Value> cachedMap; field.
  2. Create a private helper method to get or build the map.
  3. Update asMap(), equals(), and hashCode() to use the helper.
  4. Invalidate cachedMap in putHookContext().

Example:

// Add this field
private Map<String, Value> cachedMap;

// New helper method
private Map<String, Value> getResolvedMap() {
    if (cachedMap == null) {
        // logic from asMap() to build the map
        HashMap<String, Value> map = new HashMap<>();
        // ... build map ...
        cachedMap = map;
    }
    return cachedMap;
}

@Override
public Map<String, Value> asMap() {
    // Return a copy to prevent mutation of the cached map
    return new HashMap<>(getResolvedMap());
}

@Override
public boolean equals(Object o) {
    if (this == o) {
        return true;
    }
    if (!(o instanceof EvaluationContext)) {
        return false;
    }
    EvaluationContext that = (EvaluationContext) o;
    return this.getResolvedMap().equals(that.asMap());
}

@Override
public int hashCode() {
    return getResolvedMap().hashCode();
}

void putHookContext(EvaluationContext context) {
    // ... existing logic
    this.cachedMap = null; // Invalidate cache
    this.keySet = null;
}

This change would greatly improve the performance of equals and hashCode.

Comment on lines +127 to +129
public int hashCode() {
return asMap().hashCode();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

This implementation is correct, but asMap() may be an expensive call if this object is used frequently in hash-based collections. Since ImmutableContext is immutable, you could cache the hash code after its first computation to improve performance. This is a common optimization for immutable objects.

For example, you could introduce a private volatile Integer cachedHashCode; field and use lazy initialization in a thread-safe way:

private volatile Integer cachedHashCode;

@Override
public int hashCode() {
    if (cachedHashCode == null) {
        synchronized (this) {
            if (cachedHashCode == null) {
                cachedHashCode = asMap().hashCode();
            }
        }
    }
    return cachedHashCode;
}

Comment on lines 423 to 424
// Compare resolved attributes, not the context objects
assertEquals(base.asMap(), layered.asMap());
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

This test correctly sets up a scenario for mixed-context equality, but it only verifies that the asMap() representations are equal. To properly test the new equals implementation, you should directly compare the context objects. The equals method is designed to perform this map-based comparison internally. You should also assert that the hash codes are equal, to fully validate the contract between equals and hashCode.

            // Compare resolved attributes via the equals method, which should compare the underlying maps.
            assertEquals(base, layered);
            assertEquals(layered, base);
            assertEquals(base.hashCode(), layered.hashCode());

…xt and add tests.

Signed-off-by: vishalup29 <vishalupadhyay977@gmail.com>
@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
40.9% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@toddbaert
Copy link
Member

toddbaert commented Nov 29, 2025

@Vishalup29 Gemini's comments here are accurate, I think. Lazily building and caching this map is probably a good idea, instead of doing it again with every call... Though I think there's memory trade offs here as well...

The comment about the Lombok impl is also right, I think that needs to be removed.

cc @chrfwow because I'd like your insights here as well. What do you think? I think either way we'll generate a heap object doing 'asmap' during 'hashcode', so we might as well cache it in case we need it again, right? I think this is optimal as long as we do it lazily?

@chrfwow
Copy link
Contributor

chrfwow commented Dec 1, 2025

Yes, we should definetly cache the map when we create it. I have to say I don't really like that the first invocation of equals or hashcode will be quite expensive, but if we want to provide this assurance, I don't see a way around it.
We should also keep in mind that the asMap implementation of ImmutableContext (ImmutableStructure) is quite expensive, so maybe we can cache this as well

@toddbaert
Copy link
Member

Yes, we should definetly cache the map when we create it. I have to say I don't really like that the first invocation of equals or hashcode will be quite expensive, but if we want to provide this assurance, I don't see a way around it

@chrfwow

Yes, I feel exactly the same way. It will be expensive, and allocate memory once, but I think it's still worth having. At least the use case we know of it was just used in test assertions - specifically to validate/assert in a test that a context was set... perhaps this will be the most common use-case.

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.

Cannot compare (.equals) various context implementaations

3 participants