Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cache reflective access in LambdaSupport #443

Merged
merged 1 commit into from
Dec 29, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 43 additions & 8 deletions api/src/main/java/net/jqwik/api/support/LambdaSupport.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.util.stream.*;

import org.apiguardian.api.*;
import org.jetbrains.annotations.*;
import org.junit.platform.commons.support.*;

import net.jqwik.api.*;
Expand All @@ -18,6 +19,39 @@ public class LambdaSupport {

private LambdaSupport() {}

private static class FieldAccessor {
final boolean isFunctionalType;
final MethodHandle handle;

FieldAccessor(boolean isFunctionalType, MethodHandle handle) {
this.isFunctionalType = isFunctionalType;
this.handle = handle;
}
}

/**
* Returns field accessor for the given class, or {@code null} if accessors are not available (e.g. no reflective access to the class)
*/
private static final ClassValue<List<FieldAccessor>> HANDLES = new ClassValue<List<FieldAccessor>>() {
@Override
protected @Nullable List<FieldAccessor> computeValue(Class<?> type) {
Field[] fields = type.getDeclaredFields();
List<FieldAccessor> res = new ArrayList<>(fields.length);
try {
for (Field field : fields) {
// Javadoc of LOOKUP.unreflectGetter(..) suggests that this may be necessary in some cases:
field.setAccessible(true);
res.add(new FieldAccessor(isFunctionalType(field.getType()), LOOKUP.unreflectGetter(field)));
}
} catch (Throwable e) {
// As of Java 17, field.setAccessible(..) throws IllegalAccessException
// if the field is private and not opened to jqwik, Predicate.not() will create lambda instances with private fields.
return null;
}
return res;
}
};

/**
* This method is used in {@linkplain Object#equals(Object)} implementations of {@linkplain Arbitrary} types
* to allow memoization of generators.
Expand All @@ -41,8 +75,12 @@ public static <T> boolean areEqual(T l1, T l2) {
}

// Check enclosed state the hard way
for (Field field : l1Class.getDeclaredFields()) {
if (!fieldIsEqualIn(field, l1, l2)) {
List<FieldAccessor> handles = HANDLES.get(l1Class);
if (handles == null) {
return false;
}
for (FieldAccessor handle : handles) {
if (!fieldIsEqualIn(handle, l1, l2)) {
return false;
}
}
Expand All @@ -58,15 +96,12 @@ private static <T> byte[] serialize(T l1) throws IOException {

private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();

private static boolean fieldIsEqualIn(Field field, Object left, Object right) {
private static boolean fieldIsEqualIn(FieldAccessor field, Object left, Object right) {
try {
// Javadoc of LOOKUP.unreflectGetter(..) suggests that this may be necessary in some cases:
field.setAccessible(true);

MethodHandle handle = LOOKUP.unreflectGetter(field);
// If field is a functional type use LambdaSupport.areEqual().
// TODO: Could there be circular references among functional types?
if (isFunctionalType(field.getType())) {
MethodHandle handle = field.handle;
if (field.isFunctionalType) {
return areEqual(handle.invoke(left), handle.invoke(right));
}
return handle.invoke(left).equals(handle.invoke(right));
Expand Down