package spoon.test.ctType;
import java.lang.reflect.Executable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
import org.junit.Test;
import spoon.reflect.declaration.CtClass;
import spoon.reflect.declaration.CtConstructor;
import spoon.reflect.declaration.CtExecutable;
import spoon.reflect.declaration.CtFormalTypeDeclarer;
import spoon.reflect.declaration.CtMethod;
import spoon.reflect.declaration.CtParameter;
import spoon.reflect.declaration.CtType;
import spoon.reflect.declaration.CtTypeMember;
import spoon.reflect.declaration.CtTypeParameter;
import spoon.reflect.reference.CtTypeReference;
import spoon.reflect.visitor.filter.NameFilter;
import spoon.test.ctType.testclasses.ErasureModelA;
import spoon.testing.utils.ModelUtils;
import static org.junit.Assert.*;
public class CtTypeParameterTest {
@Test
public void testTypeErasure() throws Exception {
//contract: the type erasure computed by getTypeErasure is same as the one computed by the Java compiler
CtClass<?> ctModel = (CtClass<?>) ModelUtils.buildClass(ErasureModelA.class);
//visit all methods of type ctModel
//visit all inner types and their methods recursively `getTypeErasure` returns expected value
//for each formal type parameter or method parameter check if
checkType(ctModel);
}
private void checkType(CtType<?> type) throws NoSuchFieldException, SecurityException {
List<CtTypeParameter> formalTypeParameters = type.getFormalCtTypeParameters();
for (CtTypeParameter ctTypeParameter : formalTypeParameters) {
checkTypeParamErasureOfType(ctTypeParameter, type.getActualClass());
}
for (CtTypeMember member : type.getTypeMembers()) {
if (member instanceof CtFormalTypeDeclarer) {
CtFormalTypeDeclarer ftDecl = (CtFormalTypeDeclarer) member;
formalTypeParameters = ftDecl.getFormalCtTypeParameters();
if (member instanceof CtExecutable<?>) {
CtExecutable<?> exec = (CtExecutable<?>) member;
for (CtTypeParameter ctTypeParameter : formalTypeParameters) {
checkTypeParamErasureOfExecutable(ctTypeParameter);
}
for (CtParameter<?> param : exec.getParameters()) {
checkParameterErasureOfExecutable(param);
}
} else if (member instanceof CtType<?>) {
CtType<?> nestedType = (CtType<?>) member;
// recursive call for nested type
checkType(nestedType);
}
}
}
}
private void checkTypeParamErasureOfType(CtTypeParameter typeParam, Class<?> clazz) throws NoSuchFieldException, SecurityException {
Field field = clazz.getDeclaredField("param"+typeParam.getSimpleName());
assertEquals("TypeErasure of type param "+getTypeParamIdentification(typeParam), field.getType().getName(), typeParam.getTypeErasure().getQualifiedName());
}
private void checkTypeParamErasureOfExecutable(CtTypeParameter typeParam) throws NoSuchFieldException, SecurityException {
CtExecutable<?> exec = (CtExecutable<?>) typeParam.getParent();
CtParameter<?> param = exec.filterChildren(new NameFilter<>("param"+typeParam.getSimpleName())).first();
assertNotNull("Missing param"+typeParam.getSimpleName() + " in "+ exec.getSignature(), param);
int paramIdx = exec.getParameters().indexOf(param);
Class declClass = exec.getParent(CtType.class).getActualClass();
Executable declExec;
if (exec instanceof CtConstructor) {
declExec = declClass.getDeclaredConstructors()[0];
} else {
declExec = getMethodByName(declClass, exec.getSimpleName());
}
Class<?> paramType = declExec.getParameterTypes()[paramIdx];
// contract the type erasure given with Java reflection is the same as the one computed by spoon
assertEquals("TypeErasure of executable param "+getTypeParamIdentification(typeParam), paramType.getName(), typeParam.getTypeErasure().toString());
}
private void checkParameterErasureOfExecutable(CtParameter<?> param) {
CtExecutable<?> exec = param.getParent();
CtTypeReference<?> typeErasure = param.getType().getTypeErasure();
int paramIdx = exec.getParameters().indexOf(param);
Class declClass = exec.getParent(CtType.class).getActualClass();
Executable declExec;
if (exec instanceof CtConstructor) {
declExec = declClass.getDeclaredConstructors()[0];
} else {
declExec = getMethodByName(declClass, exec.getSimpleName());
}
Class<?> paramType = declExec.getParameterTypes()[paramIdx];
assertEquals(0, typeErasure.getActualTypeArguments().size());
// contract the type erasure of the method parameter given with Java reflection is the same as the one computed by spoon
assertEquals("TypeErasure of executable "+exec.getSignature()+" parameter "+param.getSimpleName(), paramType.getName(), typeErasure.getQualifiedName());
}
private Executable getMethodByName(Class declClass, String simpleName) {
for (Method method : declClass.getDeclaredMethods()) {
if(method.getName().equals(simpleName)) {
return method;
}
}
fail("Method "+simpleName+" not found in "+declClass.getName());
return null;
}
private String getTypeParamIdentification(CtTypeParameter typeParam) {
String result = "<"+typeParam.getSimpleName()+">";
CtFormalTypeDeclarer l_decl = typeParam.getParent(CtFormalTypeDeclarer.class);
if (l_decl instanceof CtType) {
return ((CtType) l_decl).getQualifiedName()+result;
}
if (l_decl instanceof CtExecutable) {
CtExecutable exec = (CtExecutable) l_decl;
if (exec instanceof CtMethod) {
result=exec.getSignature()+result;
}
return exec.getParent(CtType.class).getQualifiedName()+"#"+result;
}
throw new AssertionError();
}
@Test
public void testTypeSame() throws Exception {
CtClass<?> ctModel = (CtClass<?>) ModelUtils.buildClass(ErasureModelA.class);
CtTypeParameter tpA = ctModel.getFormalCtTypeParameters().get(0);
CtTypeParameter tpB = ctModel.getFormalCtTypeParameters().get(1);
CtTypeParameter tpC = ctModel.getFormalCtTypeParameters().get(2);
CtTypeParameter tpD = ctModel.getFormalCtTypeParameters().get(3);
CtConstructor<?> ctModelCons = ctModel.getConstructors().iterator().next();
CtMethod<?> ctModelMethod = ctModel.getMethodsByName("method").get(0);
CtMethod<?> ctModelMethod2 = ctModel.getMethodsByName("method2").get(0);
CtClass<?> ctModelB = ctModel.filterChildren(new NameFilter<>("ModelB")).first();
CtTypeParameter tpA2 = ctModelB.getFormalCtTypeParameters().get(0);
CtTypeParameter tpB2 = ctModelB.getFormalCtTypeParameters().get(1);
CtTypeParameter tpC2 = ctModelB.getFormalCtTypeParameters().get(2);
CtTypeParameter tpD2 = ctModelB.getFormalCtTypeParameters().get(3);
CtConstructor<?> ctModelBCons = ctModelB.getConstructors().iterator().next();
CtMethod<?> ctModelBMethod = ctModelB.getMethodsByName("method").get(0);
//the type parameters of ErasureModelA and ErasureModelA$ModelB are same if they are on the same position.
checkIsSame(ctModel.getFormalCtTypeParameters(), ctModelB.getFormalCtTypeParameters(), true);
//the type parameters of ErasureModelA#constructor and ErasureModelA$ModelB constructor are NOT same, because constructors does not override
checkIsSame(ctModelCons.getFormalCtTypeParameters(), ctModelBCons.getFormalCtTypeParameters(), false);
//the type parameters of ctModel ErasureModelA#method and ErasureModelA$ModelB#method are same if they are on the same position.
checkIsSame(ctModelMethod.getFormalCtTypeParameters(), ctModelBMethod.getFormalCtTypeParameters(), true);
//the type parameters of ctModel ErasureModelA#constructor and ErasureModelA$ModelB#method are never same, because they have different type of scope (Method!=Constructor)
checkIsSame(ctModelCons.getFormalCtTypeParameters(), ctModelBMethod.getFormalCtTypeParameters(), false);
//the type parameters of ctModel ErasureModelA#method and ErasureModelA#method2 are never same, because we can be sure that method does not override method2
checkIsSame(ctModelMethod.getFormalCtTypeParameters(), ctModelMethod2.getFormalCtTypeParameters(), false);
CtClass<?> ctModelC = ctModel.filterChildren(new NameFilter<>("ModelC")).first();
}
/**
* checks that parameters on the same position are same and parameters on other positions are not same
* @param isSameOnSameIndex TODO
*/
private void checkIsSame(List<CtTypeParameter> tps1, List<CtTypeParameter> tps2, boolean isSameOnSameIndex) {
for(int i=0; i<tps1.size(); i++) {
CtTypeParameter tp1 = tps1.get(i);
for(int j=0; j<tps2.size(); j++) {
CtTypeParameter tp2 = tps2.get(j);
if(i==j && isSameOnSameIndex) {
checkIsSame(tp1, tp2);
} else {
checkIsNotSame(tp1, tp2);
}
}
}
}
private void checkIsSame(CtTypeParameter tp1, CtTypeParameter tp2) {
assertTrue(isSame(tp1, tp2, false, true) || isSame(tp2, tp1, false, true));
}
private void checkIsNotSame(CtTypeParameter tp1, CtTypeParameter tp2) {
assertFalse(isSame(tp1, tp2, false, true) || isSame(tp2, tp1, false, true));
}
private static boolean isSame(CtTypeParameter thisType, CtTypeParameter thatType, boolean canTypeErasure, boolean checkMethodOverrides) {
CtTypeReference<?> thatAdaptedType = thisType.getFactory().Type().createTypeAdapter(thisType.getTypeParameterDeclarer()).adaptType(thatType);
if (thatAdaptedType == null) {
return false;
}
return thisType.getQualifiedName().equals(thatAdaptedType.getQualifiedName());
}
}