Skip to content

Commit

Permalink
Add support for identity comparison in hash code and equals methods.
Browse files Browse the repository at this point in the history
  • Loading branch information
raphw committed Sep 24, 2024
1 parent 717fa70 commit 4e920e3
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,8 @@ public DynamicType.Builder<?> apply(DynamicType.Builder<?> builder, TypeDescript
? ElementMatchers.<FieldDescription>none()
: ElementMatchers.<FieldDescription>isSynthetic())
.withIgnoredFields(new ValueMatcher(ValueHandling.Sort.IGNORE))
.withNonNullableFields(nonNullable(new ValueMatcher(ValueHandling.Sort.REVERSE_NULLABILITY))));
.withNonNullableFields(nonNullable(new ValueMatcher(ValueHandling.Sort.REVERSE_NULLABILITY)))
.withIdentityFields(isAnnotatedWith(Identity.class)));
}
if (typeDescription.getDeclaredMethods().filter(isEquals()).isEmpty()) {
EqualsMethod equalsMethod = enhance.getValue(ENHANCE_INVOKE_SUPER).load(Enhance.class.getClassLoader()).resolve(Enhance.InvokeSuper.class)
Expand All @@ -160,6 +161,7 @@ public DynamicType.Builder<?> apply(DynamicType.Builder<?> builder, TypeDescript
: ElementMatchers.<FieldDescription>isSynthetic())
.withIgnoredFields(new ValueMatcher(ValueHandling.Sort.IGNORE))
.withNonNullableFields(nonNullable(new ValueMatcher(ValueHandling.Sort.REVERSE_NULLABILITY)))
.withIdentityFields(isAnnotatedWith(Identity.class))
.withFieldOrder(AnnotationOrderComparator.INSTANCE);
if (enhance.getValue(ENHANCE_SIMPLE_COMPARISON_FIRST).resolve(Boolean.class)) {
equalsMethod = equalsMethod
Expand Down Expand Up @@ -465,6 +467,17 @@ enum Sort {
int value();
}

/**
* Indicates that a field should be compared by identity. Hash codes are then determined by
* {@link System#identityHashCode(Object)}. Fields that are compared by identity are implicitly null-safe.
*/
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Identity {
/* empty */
}

/**
* A comparator that arranges fields in the order of {@link Sorted}.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ public class EqualsMethod implements Implementation {
*/
private final ElementMatcher.Junction<? super FieldDescription.InDefinedShape> nonNullable;

/**
* A matcher to determine that a field should be considered by its identity.
*/
private final ElementMatcher.Junction<? super FieldDescription.InDefinedShape> identity;

/**
* The comparator to apply for ordering fields.
*/
Expand All @@ -89,7 +94,7 @@ public class EqualsMethod implements Implementation {
* @param superClassCheck The baseline equality to check.
*/
protected EqualsMethod(SuperClassCheck superClassCheck) {
this(superClassCheck, TypeCompatibilityCheck.EXACT, none(), none(), NaturalOrderComparator.INSTANCE);
this(superClassCheck, TypeCompatibilityCheck.EXACT, none(), none(), none(), NaturalOrderComparator.INSTANCE);
}

/**
Expand All @@ -99,17 +104,20 @@ protected EqualsMethod(SuperClassCheck superClassCheck) {
* @param typeCompatibilityCheck The instance type compatibility check.
* @param ignored A matcher to filter fields that should not be used for a equality resolution.
* @param nonNullable A matcher to determine fields of a reference type that cannot be {@code null}.
* @param identity A matcher to determine that a field should be considered by its identity.
* @param comparator The comparator to apply for ordering fields.
*/
private EqualsMethod(SuperClassCheck superClassCheck,
TypeCompatibilityCheck typeCompatibilityCheck,
ElementMatcher.Junction<? super FieldDescription.InDefinedShape> ignored,
ElementMatcher.Junction<? super FieldDescription.InDefinedShape> nonNullable,
ElementMatcher.Junction<? super FieldDescription.InDefinedShape> identity,
Comparator<? super FieldDescription.InDefinedShape> comparator) {
this.superClassCheck = superClassCheck;
this.typeCompatibilityCheck = typeCompatibilityCheck;
this.ignored = ignored;
this.nonNullable = nonNullable;
this.identity = identity;
this.comparator = comparator;
}

Expand Down Expand Up @@ -139,7 +147,7 @@ public static EqualsMethod isolated() {
* @return A new version of this equals method implementation that also ignores any fields matched by the provided matcher.
*/
public EqualsMethod withIgnoredFields(ElementMatcher<? super FieldDescription.InDefinedShape> ignored) {
return new EqualsMethod(superClassCheck, typeCompatibilityCheck, this.ignored.<FieldDescription.InDefinedShape>or(ignored), nonNullable, comparator);
return new EqualsMethod(superClassCheck, typeCompatibilityCheck, this.ignored.<FieldDescription.InDefinedShape>or(ignored), nonNullable, identity, comparator);
}

/**
Expand All @@ -151,7 +159,17 @@ public EqualsMethod withIgnoredFields(ElementMatcher<? super FieldDescription.In
* the provided matcher.
*/
public EqualsMethod withNonNullableFields(ElementMatcher<? super FieldDescription.InDefinedShape> nonNullable) {
return new EqualsMethod(superClassCheck, typeCompatibilityCheck, ignored, this.nonNullable.<FieldDescription.InDefinedShape>or(nonNullable), comparator);
return new EqualsMethod(superClassCheck, typeCompatibilityCheck, ignored, this.nonNullable.<FieldDescription.InDefinedShape>or(nonNullable), identity, comparator);
}

/**
* Returns a new version of this equals method implementation that considers the matched fields by their identity.
*
* @param identity A matcher to determine that a field should be considered by its identity.
* @return A new version of this equals method implementation that also considers the matched fields by their identity.
*/
public EqualsMethod withIdentityFields(ElementMatcher<? super FieldDescription.InDefinedShape> identity) {
return new EqualsMethod(superClassCheck, typeCompatibilityCheck, ignored, nonNullable, this.identity.<FieldDescription.InDefinedShape>or(identity), comparator);
}

/**
Expand Down Expand Up @@ -199,7 +217,7 @@ public EqualsMethod withStringTypedFieldsFirst() {
*/
@SuppressWarnings("unchecked") // In absence of @SafeVarargs
public EqualsMethod withFieldOrder(Comparator<? super FieldDescription.InDefinedShape> comparator) {
return new EqualsMethod(superClassCheck, typeCompatibilityCheck, ignored, nonNullable, new CompoundComparator(this.comparator, comparator));
return new EqualsMethod(superClassCheck, typeCompatibilityCheck, ignored, nonNullable, identity, new CompoundComparator(this.comparator, comparator));
}

/**
Expand All @@ -210,7 +228,7 @@ public EqualsMethod withFieldOrder(Comparator<? super FieldDescription.InDefined
* of the instrumented type instead of requiring an exact match.
*/
public Implementation withSubclassEquality() {
return new EqualsMethod(superClassCheck, TypeCompatibilityCheck.SUBCLASS, ignored, nonNullable, comparator);
return new EqualsMethod(superClassCheck, TypeCompatibilityCheck.SUBCLASS, ignored, nonNullable, identity, comparator);
}

/**
Expand All @@ -237,7 +255,7 @@ public ByteCodeAppender appender(Target implementationTarget) {
MethodVariableAccess.REFERENCE.loadFrom(1),
ConditionalReturn.onIdentity().returningTrue(),
typeCompatibilityCheck.resolve(implementationTarget.getInstrumentedType())
), fields, nonNullable);
), fields, nonNullable, identity);
}

/**
Expand Down Expand Up @@ -732,22 +750,30 @@ protected static class Appender implements ByteCodeAppender {
*/
private final ElementMatcher<? super FieldDescription.InDefinedShape> nonNullable;

/**
* A matcher to determine fields of a reference type that cannot be {@code null}.
*/
private final ElementMatcher<? super FieldDescription.InDefinedShape> identity;

/**
* Creates a new appender.
*
* @param instrumentedType The instrumented type.
* @param baseline The baseline stack manipulation.
* @param fieldDescriptions A list of fields to use for the comparison.
* @param nonNullable A matcher to determine fields of a reference type that cannot be {@code null}.
* @param identity A matcher to determine that a field should be considered by its identity.
*/
protected Appender(TypeDescription instrumentedType,
StackManipulation baseline,
List<FieldDescription.InDefinedShape> fieldDescriptions,
ElementMatcher<? super FieldDescription.InDefinedShape> nonNullable) {
ElementMatcher<? super FieldDescription.InDefinedShape> nonNullable,
ElementMatcher<? super FieldDescription.InDefinedShape> identity) {
this.instrumentedType = instrumentedType;
this.baseline = baseline;
this.fieldDescriptions = fieldDescriptions;
this.nonNullable = nonNullable;
this.identity = identity;
}

/**
Expand All @@ -770,13 +796,17 @@ public Size apply(MethodVisitor methodVisitor, Context implementationContext, Me
stackManipulations.add(MethodVariableAccess.REFERENCE.loadFrom(1));
stackManipulations.add(TypeCasting.to(instrumentedType));
stackManipulations.add(FieldAccess.forField(fieldDescription).read());
NullValueGuard nullValueGuard = fieldDescription.getType().isPrimitive() || fieldDescription.getType().isArray() || nonNullable.matches(fieldDescription)
? NullValueGuard.NoOp.INSTANCE
: new NullValueGuard.UsingJump(instrumentedMethod);
stackManipulations.add(nullValueGuard.before());
stackManipulations.add(ValueComparator.of(fieldDescription.getType()));
stackManipulations.add(nullValueGuard.after());
padding = Math.max(padding, nullValueGuard.getRequiredVariablePadding());
if (!fieldDescription.getType().isPrimitive() && identity.matches(fieldDescription)) {
stackManipulations.add(ConditionalReturn.onNonIdentity());
} else {
NullValueGuard nullValueGuard = fieldDescription.getType().isPrimitive() || fieldDescription.getType().isArray() || nonNullable.matches(fieldDescription)
? NullValueGuard.NoOp.INSTANCE
: new NullValueGuard.UsingJump(instrumentedMethod);
stackManipulations.add(nullValueGuard.before());
stackManipulations.add(ValueComparator.of(fieldDescription.getType()));
stackManipulations.add(nullValueGuard.after());
padding = Math.max(padding, nullValueGuard.getRequiredVariablePadding());
}
}
stackManipulations.add(IntegerConstant.forValue(true));
stackManipulations.add(MethodReturn.INTEGER);
Expand All @@ -790,11 +820,6 @@ public Size apply(MethodVisitor methodVisitor, Context implementationContext, Me
@HashCodeAndEqualsPlugin.Enhance
protected static class ConditionalReturn extends StackManipulation.AbstractBase {

/**
* An empty array.
*/
private static final Object[] EMPTY = new Object[0];

/**
* The conditional jump instruction upon which the return is not triggered.
*/
Expand Down
Loading

0 comments on commit 4e920e3

Please sign in to comment.