Skip to content

Commit

Permalink
feature: CtTypeParamer#getTypeErasure, getTypeAdaptedTo
Browse files Browse the repository at this point in the history
  • Loading branch information
pvojtechovsky committed Mar 14, 2017
1 parent 286e6e9 commit 059792f
Show file tree
Hide file tree
Showing 4 changed files with 310 additions and 0 deletions.
14 changes: 14 additions & 0 deletions src/main/java/spoon/reflect/declaration/CtTypeParameter.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,20 @@ public interface CtTypeParameter extends CtType<Object> {
@DerivedProperty
CtFormalTypeDeclarer getTypeParameterDeclarer();

/**
* @return type (not generic one), which is used by java compiler to ensure that no new classes are created for parameterized types;
* consequently, generics incur no runtime overhead.
* See https://docs.oracle.com/javase/tutorial/java/generics/erasure.html
*/
@DerivedProperty
CtTypeReference<?> getTypeErasure();

/**
* @param targetScope - the scope where this type parameter is needed
* @return type reference adapted from origin scope to scope of `targetScope`
*/
CtTypeReference<?> getTypeAdaptedTo(CtFormalTypeDeclarer targetScope);

// override the return type
@Override
CtTypeParameter clone();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@
*/
package spoon.support.reflect.declaration;

import spoon.SpoonException;
import spoon.reflect.declaration.CtConstructor;
import spoon.reflect.declaration.CtField;
import spoon.reflect.declaration.CtFormalTypeDeclarer;
import spoon.reflect.declaration.CtMethod;
import spoon.reflect.declaration.CtModifiable;
import spoon.reflect.declaration.CtPackage;
import spoon.reflect.declaration.CtType;
import spoon.reflect.declaration.CtTypeMember;
import spoon.reflect.declaration.CtTypeParameter;
import spoon.reflect.declaration.ModifierKind;
import spoon.reflect.declaration.ParentNotInitializedException;
Expand Down Expand Up @@ -248,6 +251,148 @@ public boolean isSubtypeOf(CtTypeReference<?> type) {
return getReference().isSubtypeOf(type);
}

@Override
public CtTypeReference<?> getTypeErasure() {
CtTypeReference<?> boundType = getBound(this);
CtTypeReference<?> typeErasure = getReferenceInScope(boundType, getDeclaringType());
if (typeErasure instanceof CtTypeParameterReference) {
CtTypeParameterReference typeRef = (CtTypeParameterReference) typeErasure;
return typeRef.getDeclaration().getTypeErasure();
}
return typeErasure;
}

@Override
public CtTypeReference<?> getTypeAdaptedTo(CtFormalTypeDeclarer targetScope) {
CtFormalTypeDeclarer scope = getTypeParameterDeclarer();
if (scope == null) {
throw new SpoonException("Can't adapt type param to another scope if it has no current scope (no parent formal type declarer).");
}
if (targetScope == scope) {
//type parameter is declared in required scope. Just return it.
return getReference();
}
if (scope instanceof CtType) {
if ((targetScope instanceof CtType) == false && targetScope instanceof CtTypeMember) {
targetScope = ((CtTypeMember) targetScope).getDeclaringType();
if (targetScope == scope) {
//type parameter is declared in required scope. Just return it.
return getReference();
}
}
if (targetScope instanceof CtType) {
return getReferenceInTypeScope(this, (CtType<?>) targetScope, (CtType<?>) scope);
}
//cannot map from Type scope to another scope
return null;
} else if (scope instanceof CtMethod) {
if (targetScope instanceof CtMethod) {
if (scope.getFormalCtTypeParameters().size() != targetScope.getFormalCtTypeParameters().size()) {
//cannot map type parameter of method 1 to method 2 if they have different number of formal type parameters
return null;
}
int myPosition = getTypeParameterPosition(this, scope);
return targetScope.getFormalCtTypeParameters().get(myPosition).getReference();
}
return null;
} else if (scope instanceof CtConstructor) {
//cannot map to scope of different constructor
return null;
} else {
throw new SpoonException("Unexpected scope of type parameter of type: " + scope.getClass().getName());
}
}

/**
* Converts potentially generic parameters from namespace of some super class to namespace of `targetScope`
* @param type - the reference to to be converted parameter (generic) type or normal type.
* @param targetScope - the type which represents target scope of the generic parameter.
* @return a `type` converted into scope of `targetType` or null if conversion is not possible, because scopes are incompatible
*/
private static CtTypeReference<?> getReferenceInScope(CtTypeReference<?> type, CtType<?> targetScope) {
if (type instanceof CtTypeParameterReference) {
CtTypeParameterReference typeParamRef = (CtTypeParameterReference) type;
CtTypeParameter typeParam = typeParamRef.getDeclaration();
CtFormalTypeDeclarer scope = typeParam.getTypeParameterDeclarer();
if (scope == null) {
throw new SpoonException("Can't convert scope of type param if it has no parent formal type declarer.");
}
if (targetScope == scope) {
//type parameter is declared in required scope. Just return it.
return typeParamRef;
}
if (scope instanceof CtType) {
//this type reference is declared in scope of different type. Compute how it is declared in `targetType`
return getReferenceInTypeScope(typeParam, targetScope, (CtType<?>) scope);
} else if (scope instanceof CtMethod) {

} else if (scope instanceof CtConstructor) {

} else {
throw new SpoonException("Unexpected scope of type parameter of type: " + scope.getClass().getName());
}
}
return type;
}


/**
* Converts generic `typeParam` in scope of type `sourceScope` to namespace of `targetScope`
* @param typeParam the to be converted type parameter
* @param targetScope - type of target scope, which should be a sub type of source scope
* @param sourceScope - type of source scope
* @return reference to type converted into targetScope
*/
private static CtTypeReference<?> getReferenceInTypeScope(CtTypeParameter typeParam, CtType<?> targetScope, CtType<?> sourceScope) {
if (targetScope == sourceScope) {
return typeParam.getReference();
}
CtTypeReference<?> superTypeRef = targetScope.getSuperclass();
if (superTypeRef == null) {
return null;
}
CtType<?> superType = superTypeRef.getTypeDeclaration();
if (superType == null) {
return null;
}
CtTypeReference<?> superTypeParamRef = getReferenceInTypeScope(typeParam, superType, sourceScope);
if (superTypeParamRef instanceof CtTypeParameterReference) {
CtTypeParameter superTypeParam = ((CtTypeParameterReference) superTypeParamRef).getDeclaration();
//convert typeParam of super type to typeParam of `targetType`
int paramTypeIdxInSuperType = getTypeParameterPosition(superTypeParam, superType);
List<CtTypeReference<?>> actTRs = superTypeRef.getActualTypeArguments();
if (paramTypeIdxInSuperType < actTRs.size()) {
//the actual type argument is defined in `targetType`. Return that type parameter declared in targetScope
return actTRs.get(paramTypeIdxInSuperType);
}
//the super type actual type arguments are not defined in sub class. The type erasure is applied
return superTypeParam.getTypeErasure();
}
//else it is normal type reference.
return superTypeParamRef;
}

private static CtTypeReference<?> getBound(CtTypeParameter typeParam) {
CtTypeReference<?> bound = typeParam.getSuperclass();
if (bound == null) {
bound = typeParam.getFactory().Type().OBJECT;
}
return bound;
}

/**
* @param parameterType
* @param scope
* @return index of parameterType declaration in scope or throws SpoonException if not found (= spoon model inconsistency)
*/
private static int getTypeParameterPosition(CtTypeParameter parameterType, CtFormalTypeDeclarer scope) {
int position = scope.getFormalCtTypeParameters().indexOf(parameterType);
if (position == -1) {
throw new SpoonException("Type parameter <" + parameterType.getSimpleName() + " not found in scope " + scope.getShortRepresentation());
}
return position;
}

@Override
@UnsettableProperty
public <M, C extends CtType<Object>> C addMethod(CtMethod<M> method) {
Expand Down
111 changes: 111 additions & 0 deletions src/test/java/spoon/test/ctType/CtTypeParameterTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package spoon.test.ctType;

import java.lang.reflect.Executable;
import java.lang.reflect.Field;
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.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 {
CtClass<?> ctModel = (CtClass<?>) ModelUtils.buildClass(ErasureModelA.class);
checkType(ctModel);
}

private void checkType(CtType<?> type) throws NoSuchFieldException, SecurityException {
List<CtTypeParameter> l_typeParams = type.getFormalCtTypeParameters();
for (CtTypeParameter ctTypeParameter : l_typeParams) {
checkTypeParamErasureOfType(ctTypeParameter, type.getActualClass());
}

for (CtTypeMember typeMemeber : type.getTypeMembers()) {
if (typeMemeber instanceof CtFormalTypeDeclarer) {
CtFormalTypeDeclarer ftDecl = (CtFormalTypeDeclarer) typeMemeber;
l_typeParams = ftDecl.getFormalCtTypeParameters();
if (typeMemeber instanceof CtExecutable<?>) {
CtExecutable<?> exec = (CtExecutable<?>) typeMemeber;
for (CtTypeParameter ctTypeParameter : l_typeParams) {
checkTypeParamErasureOfExecutable(ctTypeParameter, exec);
}
} else if (typeMemeber instanceof CtType<?>) {
CtType<?> nestedType = (CtType<?>) typeMemeber;
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, CtExecutable<?> exec) throws NoSuchFieldException, SecurityException {
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 = declClass.getDeclaredMethods()[0];
}
Class<?> paramType = declExec.getParameterTypes()[paramIdx];
assertEquals("TypeErasure of executable param "+getTypeParamIdentification(typeParam), paramType.getName(), typeParam.getTypeErasure().getQualifiedName());
}

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 testTypeAdapted() 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);

CtClass<?> ctModelB = ctModel.filterChildren(new NameFilter<>("ModelB")).first();
assertEquals("A2", tpA.getTypeAdaptedTo(ctModelB).getQualifiedName());
assertEquals("B2", tpB.getTypeAdaptedTo(ctModelB).getQualifiedName());
assertEquals("C2", tpC.getTypeAdaptedTo(ctModelB).getQualifiedName());
assertEquals("D2", tpD.getTypeAdaptedTo(ctModelB).getQualifiedName());

CtClass<?> ctModelC = ctModel.filterChildren(new NameFilter<>("ModelC")).first();
assertEquals("java.lang.Integer", tpA.getTypeAdaptedTo(ctModelC).getQualifiedName());
assertEquals("java.lang.RuntimeException", tpB.getTypeAdaptedTo(ctModelC).getQualifiedName());
assertEquals("java.lang.IllegalArgumentException", tpC.getTypeAdaptedTo(ctModelC).getQualifiedName());
assertEquals("spoon.test.ctType.testclasses.ErasureModelA$ModelC", tpD.getTypeAdaptedTo(ctModelC).getQualifiedName());
}
}
40 changes: 40 additions & 0 deletions src/test/java/spoon/test/ctType/testclasses/ErasureModelA.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package spoon.test.ctType.testclasses;

public class ErasureModelA<A, B extends Exception, C extends B, D extends ErasureModelA<A,B,C,D>> {

A paramA;
B paramB;
C paramC;
D paramD;

public <I, J extends C> ErasureModelA(I paramI, J paramJ, D paramD) {
}

public <I, J extends C> void method(I paramI, J paramJ, D paramD) {
}

static class ModelB<A2,B2 extends Exception, C2 extends B2, D2 extends ErasureModelA<A2,B2,C2,D2>> extends ErasureModelA<A2,B2,C2,D2> {
A2 paramA2;
B2 paramB2;
C2 paramC2;
D2 paramD2;

public <I, J extends C2> ModelB(I paramI, J paramJ, D2 paramD2) {
super(paramI, paramJ, paramD2);
}

@Override
public <I, J extends C2> void method(I paramI, J paramJ, D2 paramD2) {
}
}

static class ModelC extends ErasureModelA<Integer, RuntimeException, IllegalArgumentException, ModelC> {

public ModelC(Float paramI, IllegalArgumentException paramJ, ModelC paramK) {
super(paramI, paramJ, null);
}

public void method(Float paramI, IllegalArgumentException paramJ, ModelC paramK) {
}
}
}

0 comments on commit 059792f

Please sign in to comment.