Skip to content

Commit

Permalink
[Java8] Improve support for method references
Browse files Browse the repository at this point in the history
The implementation of t he `ConstantPoolTypeIntrospector` supported:
 - Reference to a static method
 - Reference to an instance method of a particular object
 - Reference to a constructor

 But did not support:
  - Reference to an instance method of an arbitrary object of a particular type

Referencing instances methods would result in an index out of bounds exception
as  the step definition interface was expecting exactly 1 more argument then
was bound to the body. We now use this discrepancy to detect if this is an
instance method reference and include the the instance class as the first type.

Related issues:
 - #1123
 - #1126

 This fixes #1126
  • Loading branch information
mpkorstanje committed Jun 12, 2017
1 parent c8c9eab commit 004e192
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 30 deletions.
5 changes: 0 additions & 5 deletions java8/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,6 @@
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.sourceforge.cobertura</groupId>
<artifactId>cobertura</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
package cucumber.runtime.java8;

import cucumber.runtime.CucumberException;
import static java.lang.Class.forName;
import static java.lang.System.arraycopy;
import static jdk.internal.org.objectweb.asm.Type.getObjectType;

import cucumber.api.java8.StepdefBody;
import cucumber.runtime.CucumberException;
import cucumber.runtime.java.TypeIntrospector;
import sun.reflect.ConstantPool;

import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.List;

public class ConstantPoolTypeIntrospector implements TypeIntrospector {
private static final Method Class_getConstantPool;
private static final int REFERENCE_CLASS = 0;
private static final int REFERENCE_METHOD = 1;
private static final int REFERENCE_ARGUMENT_TYPES = 2;

static {
try {
Expand All @@ -26,37 +31,63 @@ public class ConstantPoolTypeIntrospector implements TypeIntrospector {

@Override
public Type[] getGenericTypes(Class<? extends StepdefBody> clazz, Class<? extends StepdefBody> interfac3) throws Exception {
ConstantPool constantPool = (ConstantPool) Class_getConstantPool.invoke(clazz);
String typeString = getLambdaTypeString(constantPool);
int typeParameterCount = interfac3.getTypeParameters().length;
jdk.internal.org.objectweb.asm.Type[] argumentTypes = jdk.internal.org.objectweb.asm.Type.getArgumentTypes(typeString);
final ConstantPool constantPool = (ConstantPool) Class_getConstantPool.invoke(clazz);
final String[] member = getMemberReference(constantPool);
final int parameterCount = interfac3.getTypeParameters().length;

final jdk.internal.org.objectweb.asm.Type[] argumentTypes = jdk.internal.org.objectweb.asm.Type.getArgumentTypes(member[REFERENCE_ARGUMENT_TYPES]);

// If we are one parameter short, this is a
// - Reference to an instance method of an arbitrary object of a particular type
if (parameterCount - 1 == argumentTypes.length) {
return handleMethodReferenceToObjectOfType(member[REFERENCE_CLASS], handleLambda(argumentTypes, parameterCount - 1));
}
// If we are not short on parameters this either
// - Reference to a static method
// - Reference to an instance method of a particular object
// - Reference to a constructor
// - A lambda expression
// We can all treat these as lambda's for figuring out the types.
return handleLambda(argumentTypes, parameterCount);
}

private static Type[] handleMethodReferenceToObjectOfType(String containingType, Type[] methodArgumentTypes) throws ClassNotFoundException {
Type[] containingTypeAndMethodArgumentTypes = new Type[methodArgumentTypes.length + 1];
containingTypeAndMethodArgumentTypes[0] = forName(getObjectType(containingType).getClassName());
arraycopy(methodArgumentTypes, 0, containingTypeAndMethodArgumentTypes, 1, methodArgumentTypes.length);
return containingTypeAndMethodArgumentTypes;
}

private static Type[] handleLambda(jdk.internal.org.objectweb.asm.Type[] argumentTypes, int typeParameterCount) throws ClassNotFoundException {
if (argumentTypes.length < typeParameterCount) {
throw new CucumberException(String.format("Expected at least %s arguments but found only %s", typeParameterCount, argumentTypes.length));
}

// Only look at the N last arguments to the lambda static method, since the first ones might be variables
// who only pass in the states of closed variables
List<jdk.internal.org.objectweb.asm.Type> interestingArgumentTypes = Arrays.asList(argumentTypes)
.subList(argumentTypes.length - typeParameterCount, argumentTypes.length);
jdk.internal.org.objectweb.asm.Type[] interestingArgumentTypes = new jdk.internal.org.objectweb.asm.Type[typeParameterCount];
arraycopy(argumentTypes, argumentTypes.length - typeParameterCount, interestingArgumentTypes, 0, typeParameterCount);

Type[] typeArguments = new Type[typeParameterCount];
for (int i = 0; i < typeParameterCount; i++) {
typeArguments[i] = Class.forName(interestingArgumentTypes.get(i).getClassName());
typeArguments[i] = forName(interestingArgumentTypes[i].getClassName());
}
return typeArguments;
}

private String getLambdaTypeString(ConstantPool constantPool) {
private static String[] getMemberReference(ConstantPool constantPool) {
int size = constantPool.getSize();
String[] memberRef = null;

// find last element in constantPool with valid memberRef
// - previously always at size-2 index but changed with JDK 1.8.0_60
for (int i = size - 1; i > -1; i--) {
try {
memberRef = constantPool.getMemberRefInfoAt(i);
return memberRef[2];
return constantPool.getMemberRefInfoAt(i);
} catch (IllegalArgumentException e) {
// eat error; null entry at ConstantPool index?
}
}
throw new CucumberException("Couldn't find memberRef.");
throw new CucumberException("Couldn't find memberRef.");
}

}
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
package cucumber.runtime.java8.test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertTrue;

import cucumber.api.DataTable;
import cucumber.api.Scenario;
import cucumber.api.java8.En;

import java.util.List;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;

public class LambdaStepdefs implements En {
private static LambdaStepdefs lastInstance;

private final int outside = 41;

public LambdaStepdefs() {
Before((Scenario scenario) -> {
assertNotSame(this, lastInstance);
Expand Down Expand Up @@ -41,33 +44,72 @@ public LambdaStepdefs() {
assertEquals("hello", localState);
});

int localInt = 1;
Given("^A statement with a simple match$", () -> {
assertEquals(2, localInt+1);
assertTrue(true);
});

int localInt = 1;
Given("^A statement with a scoped argument$", () -> {
assertEquals(2, localInt + 1);
assertEquals(42, outside + 1);
});

Given("^I will give you (\\d+) and ([\\d\\.]+) and (\\w+) and (\\d+)$", (Integer a, Float b, String c,
Integer d)
-> {
-> {
assertEquals((Integer) 1, a);
assertEquals((Float) 2.2f, b);
assertEquals("three", c);
assertEquals((Integer) 4, d);
});

Given("^A lambda that declares an exception$", this::methodThatDeclaresException);
Given("^A method reference that declares an exception$", this::methodThatDeclaresException);
Given("^A method reference with an argument (\\d+)$", this::methodWithAnArgument);
Given("^A constructor reference with an argument (.*)$", Contact::new);
Given("^A static method reference with an argument (\\d+)$", LambdaStepdefs::staticMethodWithAnArgument);

Given("^A method reference to an arbitrary object of a particular type (\\d+)$", Contact::call);
Given("^A method reference to an arbitrary object of a particular type (.*) with argument (.*)$", Contact::update);

}

private void methodThatDeclaresException() throws Throwable {
}
private void methodWithAnArgument(Integer cuckes) throws Throwable {
assertEquals(42, cuckes.intValue());
}

public static void staticMethodWithAnArgument(Integer cuckes) throws Throwable {
assertEquals(42, cuckes.intValue());
}


private void hookWithArgs(Scenario scenario) throws Throwable {
}


public static class Person {
String first;
String last;
}

public static class Contact {

private final String number;

public Contact(String number){
this.number = number;
assertEquals("42", number);
}

public void call(){
assertEquals("42", number);
}

public void update(String number){
assertEquals("42", this.number);
assertEquals("314", number);
}
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Feature: Java8

Scenario: Parameterless lambdas
Given A statement with a simple match
Given A statement with a scoped argument

Scenario: Multi-param lambdas
Given I will give you 1 and 2.2 and three and 4
Expand All @@ -20,5 +21,10 @@ Feature: Java8
| Aslak | Hellesøy |
| Donald | Duck |

Scenario: using lambdas with exceptions
Given A lambda that declares an exception
Scenario: using method references
Given A method reference that declares an exception
Given A method reference with an argument 42
Given A static method reference with an argument 42
Given A constructor reference with an argument 42
Given A method reference to an arbitrary object of a particular type 42
Given A method reference to an arbitrary object of a particular type 42 with argument 314

0 comments on commit 004e192

Please sign in to comment.