/**
* Copyright (C) 2006-2017 INRIA and contributors
* Spoon - http://spoon.gforge.inria.fr/
*
* This software is governed by the CeCILL-C License under French law and
* abiding by the rules of distribution of free software. You can use, modify
* and/or redistribute the software under the terms of the CeCILL-C license as
* circulated by CEA, CNRS and INRIA at http://www.cecill.info.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the CeCILL-C License for more details.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL-C license and that you accept its terms.
*/
package spoon.support.visitor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import spoon.SpoonException;
import spoon.reflect.declaration.CtElement;
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.CtTypeInformation;
import spoon.reflect.declaration.CtTypeParameter;
import spoon.reflect.declaration.ModifierKind;
import spoon.reflect.factory.Factory;
import spoon.reflect.reference.CtTypeParameterReference;
import spoon.reflect.reference.CtTypeReference;
import spoon.reflect.reference.CtWildcardReference;
import spoon.reflect.visitor.chain.CtConsumer;
import spoon.reflect.visitor.chain.ScanningMode;
import spoon.reflect.visitor.filter.SuperInheritanceHierarchyFunction;
/**
* Helper class created from type X or reference to X.
* It provides access to actual type arguments
* of any super type of type X adapted to type X.<br>
* Example:<br>
* <pre>
* //reference to `ArrayList` with actual type argument `Integer`
* CtTypeReference arrayListRef = ... //ArrayList<Integer>
* //type java.util.List with type parameter `E`
* CtType list = ... //List<E>
* //adapting of type parameter `E` to scope of arrayListRef
* CtTypeReference typeParamE_adaptedTo_arrayListRef = new ClassTypingContext(arrayListRef).adaptType(list.getFormalCtTypeParameters().get(0))
* //the value of `E` in scope of arrayListRef is `Integer`
* assertEquals(Integer.class.getName(), typeParamE_adaptedTo_arrayListRef.getQualifiedName());
* </pre>
*/
public class ClassTypingContext extends AbstractTypingContext {
private final CtType<?> scopeType;
/*
* super type hierarchy of the enclosing class
*/
private ClassTypingContext enclosingClassTypingContext;
/*
* maps qualified name of the type to the actual type arguments of this type in `scope`
*/
private Map<String, List<CtTypeReference<?>>> typeToArguments = new HashMap<>();
/**
* remember which super class was last visited.
* The next super class scanning will start here
*/
private CtTypeInformation lastResolvedSuperclass;
/**
* the set of qualified names of all visited classes and interfaces, which assures that interfaces are visited only once
*/
private Set<String> visitedSet;
/**
* @param typeReference {@link CtTypeReference} whose actual type arguments are used for resolving of input type parameters
*/
public ClassTypingContext(CtTypeReference<?> typeReference) {
scopeType = typeReference.getTypeDeclaration();
lastResolvedSuperclass = typeReference;
CtTypeReference<?> enclosing = getEnclosingType(typeReference);
if (enclosing != null) {
enclosingClassTypingContext = createEnclosingHierarchy(enclosing);
}
typeToArguments.put(typeReference.getQualifiedName(), typeReference.getActualTypeArguments());
}
/**
* @param type {@link CtType} whose formal type parameters are transformed to {@link CtTypeReference}s,
* which plays role of actual type arguments, used for resolving of input type parameters
*/
public ClassTypingContext(CtType<?> type) {
scopeType = type;
lastResolvedSuperclass = type;
CtType<?> enclosing = getEnclosingType(type);
if (enclosing != null) {
enclosingClassTypingContext = createEnclosingHierarchy(enclosing);
}
typeToArguments.put(type.getQualifiedName(), getTypeReferences(type.getFormalCtTypeParameters()));
}
@Override
public CtType<?> getAdaptationScope() {
return scopeType;
}
/**
* detects if `superTypeRef` is a super type of the type or type reference,
* which was send to constructor of this instance.
* It takes into account the actual type arguments of this type and `superTypeRef`
*
* So for example:<br>
* <pre>
* CtTypeReference listInteger = ...//List<Integer>
* CtTypeReference listString = ...//List<Integer>
* assertFalse(new ClassTypingContext(listInteger).isSubtypeOf(listString))
* CtTypeReference listExtendsNumber = ...//List<? extends Number>
* assertTrue(new ClassTypingContext(listInteger).isSubtypeOf(listExtendsNumber))
* </pre>
* @param superTypeRef the reference
* @return true if this type (including actual type arguments) is a sub type of superTypeRef
*/
public boolean isSubtypeOf(CtTypeReference<?> superTypeRef) {
List<CtTypeReference<?>> adaptedArgs = resolveActualTypeArgumentsOf(superTypeRef);
if (adaptedArgs == null) {
//the superTypeRef was not found in super type hierarchy
return false;
}
if (isSubTypeByActualTypeArguments(superTypeRef, adaptedArgs) == false) {
return false;
}
CtTypeReference<?> enclosingTypeRef = getEnclosingType(superTypeRef);
if (enclosingTypeRef != null) {
if (enclosingClassTypingContext == null) {
return false;
}
return enclosingClassTypingContext.isSubtypeOf(enclosingTypeRef);
}
return true;
}
/**
* resolve actual type argument values of the provided type reference
* @param typeRef the reference to the type
* whose actual type argument values has to be resolved in scope of `scope` type
* @return actual type arguments of `typeRef` in scope of `scope` element or null if typeRef is not a super type of `scope`
*/
public List<CtTypeReference<?>> resolveActualTypeArgumentsOf(CtTypeReference<?> typeRef) {
final String typeQualifiedName = typeRef.getQualifiedName();
List<CtTypeReference<?>> args = typeToArguments.get(typeQualifiedName);
if (args != null) {
//the actual type arguments of `type` are already resolved
return args;
}
//resolve hierarchy of enclosing class first.
CtTypeReference<?> enclosingTypeRef = getEnclosingType(typeRef);
if (enclosingTypeRef != null) {
if (enclosingClassTypingContext == null) {
return null;
}
//`type` is inner class. Resolve it's enclosing class arguments first
if (enclosingClassTypingContext.resolveActualTypeArgumentsOf(enclosingTypeRef) == null) {
return null;
}
}
/*
* the `type` is either top level, static or resolved inner class.
* So it has no parent actual type arguments or they are resolved now
*/
/*
* detect where to start/continue with resolving of super classes and super interfaces
* to found actual type arguments of input `type`
*/
if (lastResolvedSuperclass == null) {
/*
* whole super inheritance hierarchy was already resolved for this level.
* It means that `type` is not a super type of `scope` on the level `level`
*/
return null;
}
final HierarchyListener listener = new HierarchyListener(getVisitedSet());
/*
* visit super inheritance class hierarchy of lastResolve type of level of `type` to found it's actual type arguments.
*/
((CtElement) lastResolvedSuperclass).map(new SuperInheritanceHierarchyFunction()
.includingSelf(false)
.returnTypeReferences(true)
.setListener(listener))
.forEach(new CtConsumer<CtTypeReference<?>>() {
@Override
public void accept(CtTypeReference<?> typeRef) {
/*
* typeRef is a reference from sub type to super type.
* It contains actual type arguments in scope of sub type,
* which are going to be substituted as arguments to formal type parameters of super type
*/
String superTypeQualifiedName = typeRef.getQualifiedName();
List<CtTypeReference<?>> superTypeActualTypeArgumentsResolvedFromSubType = resolveTypeParameters(typeRef.getActualTypeArguments());
//Remember actual type arguments of `type`
typeToArguments.put(superTypeQualifiedName, superTypeActualTypeArgumentsResolvedFromSubType);
if (typeQualifiedName.equals(superTypeQualifiedName)) {
/*
* we have found actual type arguments of input `type`
* We can finish. But only after all interfaces of last visited class are processed too
*/
listener.foundArguments = superTypeActualTypeArgumentsResolvedFromSubType;
}
}
});
return listener.foundArguments;
}
/**
* @param method to be adapted to this context
* @return new method whose parameters are adapted to `this context` or the same`method` if there is no need to adapt it
*/
public <T> CtMethod<T> adaptMethod(CtMethod<T> method) {
CtType<?> declType = method.getDeclaringType();
if (declType == null) {
throw new SpoonException("Cannot adapt method, which has no declaringType");
}
if (getAdaptationScope() == declType) {
return method;
}
//the scope method is declared in different type then scope of assigned classTypingContext
if (isSubtypeOf(declType.getReference()) == false) {
throw new SpoonException("Cannot create MethodTypingContext for method declared in different ClassTypingContext");
}
/*
* The method is declared in an supertype of classTypingContext.
* Create virtual scope method by adapting generic types of supertype method to required scope
*/
Factory factory = method.getFactory();
//create new method
CtMethod<T> adaptedMethod = factory.Core().createMethod();
adaptedMethod.setParent(getAdaptationScope());
adaptedMethod.setModifiers(method.getModifiers());
adaptedMethod.setSimpleName(method.getSimpleName());
for (CtTypeReference<? extends Throwable> thrownType : method.getThrownTypes()) {
adaptedMethod.addThrownType(thrownType.clone());
}
for (CtTypeParameter typeParam : method.getFormalCtTypeParameters()) {
CtTypeParameter newTypeParam = typeParam.clone();
newTypeParam.setSuperclass(adaptTypeForNewMethod(typeParam.getSuperclass()));
adaptedMethod.addFormalCtTypeParameter(newTypeParam);
}
//adapt return type
adaptedMethod.setType((CtTypeReference) adaptTypeForNewMethod(method.getType()));
//adapt parameters
List<CtParameter<?>> adaptedParams = new ArrayList<>(method.getParameters().size());
for (CtParameter<?> parameter : method.getParameters()) {
adaptedParams.add(factory.Executable().createParameter(null,
adaptTypeForNewMethod(parameter.getType()),
parameter.getSimpleName()));
}
adaptedMethod.setParameters(adaptedParams);
return adaptedMethod;
}
@Override
public ClassTypingContext getEnclosingGenericTypeAdapter() {
return enclosingClassTypingContext;
}
/**
* might be used to create custom chain of super type hierarchies
*/
protected ClassTypingContext createEnclosingHierarchy(CtType<?> enclosingType) {
return new ClassTypingContext(enclosingType);
}
/**
* might be used to create custom chain of super type hierarchies
*/
protected ClassTypingContext createEnclosingHierarchy(CtTypeReference<?> enclosingTypeRef) {
return new ClassTypingContext(enclosingTypeRef);
}
static List<CtTypeReference<?>> getTypeReferences(List<? extends CtType<?>> types) {
List<CtTypeReference<?>> refs = new ArrayList<>(types.size());
for (CtType<?> type : types) {
refs.add(type.getReference());
}
return refs;
}
/**
* @param type the potential inner class, whose enclosing type should be returned
* @return enclosing type of a `type` is an inner type or null if `type` is explicitly or implicitly static or top level type
*/
private CtType<?> getEnclosingType(CtType<?> type) {
if (type.hasModifier(ModifierKind.STATIC)) {
return null;
}
CtType<?> declType = type.getDeclaringType();
if (declType == null) {
return null;
}
if (declType.isInterface()) {
//nested types of interfaces are static
return null;
}
return declType;
}
/**
* @param typeRef the potential inner class, whose enclosing type should be returned
* @return enclosing type of a `type` is an inner type or null if `type` is explicitly or implicitly static or top level type
*/
private CtTypeReference<?> getEnclosingType(CtTypeReference<?> typeRef) {
CtType<?> type = typeRef.getTypeDeclaration();
if (type.hasModifier(ModifierKind.STATIC)) {
return null;
}
CtType<?> declType = type.getDeclaringType();
if (declType == null) {
return null;
}
if (declType.isInterface()) {
//nested types of interfaces are static
return null;
}
return typeRef.getDeclaringType();
}
/**
* adapts `typeParam` to the {@link CtTypeReference}
* of scope of this {@link ClassTypingContext}
* In can be {@link CtTypeParameterReference} again - depending actual type arguments of this {@link ClassTypingContext}.
*
* @param typeParam to be resolved {@link CtTypeParameter}
* @return {@link CtTypeReference} or {@link CtTypeParameterReference} adapted to scope of this {@link ClassTypingContext}
* or null if `typeParam` cannot be adapted to target `scope`
*/
@Override
protected CtTypeReference<?> adaptTypeParameter(CtTypeParameter typeParam) {
CtFormalTypeDeclarer declarer = typeParam.getTypeParameterDeclarer();
if ((declarer instanceof CtType<?>) == false) {
return null;
}
//get the actual type argument values for the declarer of `typeParam`
List<CtTypeReference<?>> actualTypeArguments = resolveActualTypeArgumentsOf(((CtType<?>) declarer).getReference());
if (actualTypeArguments == null) {
if (enclosingClassTypingContext != null) {
//try to adapt parameter using enclosing class typing context
return enclosingClassTypingContext.adaptType(typeParam);
}
return null;
}
return getValue(actualTypeArguments, typeParam, declarer);
}
/**
* Create visitedSet lazily
*/
private Set<String> getVisitedSet() {
if (visitedSet == null) {
visitedSet = new HashSet<>();
}
return visitedSet;
}
/**
* the listener which assures that
* - each interface of super inheritance hierarchy is visited only once
* - the scanning of super inheritance hierarchy early stops when we have found
*/
private class HierarchyListener extends SuperInheritanceHierarchyFunction.DistinctTypeListener {
List<CtTypeReference<?>> foundArguments;
HierarchyListener(Set<String> visitedSet) {
super(visitedSet);
}
@Override
public ScanningMode enter(CtTypeReference<?> typeRef, boolean isClass) {
ScanningMode mode = super.enter(typeRef);
if (mode == ScanningMode.SKIP_ALL) {
//this interface was already visited. Do not visit it again
return mode;
}
if (isClass) {
if (foundArguments != null) {
//we have found result then we can finish before entering super class. All interfaces of found type should be still visited
//skip before super class (and it's interfaces) of found type is visited
return ScanningMode.SKIP_ALL;
}
/*
* we are visiting class (not interface)
* Remember that, so we can continue at this place if needed.
* If we enter class, then this listener assures that that class and all it's not yet visited interfaces are visited
*/
lastResolvedSuperclass = typeRef;
}
//this type was not visited yet. Visit it normally
return ScanningMode.NORMAL;
}
}
/**
* resolve typeRefs declared in scope of declarer using actual type arguments registered in typeScopeToActualTypeArguments
* @param typeRefs to be resolved type references
* @return resolved type references - one for each `typeRefs`
* @throws SpoonException if they cannot be resolved. It should not normally happen. If it happens then spoon AST model is probably not consistent.
*/
private List<CtTypeReference<?>> resolveTypeParameters(List<CtTypeReference<?>> typeRefs) {
List<CtTypeReference<?>> result = new ArrayList<>(typeRefs.size());
for (CtTypeReference<?> typeRef : typeRefs) {
if (typeRef instanceof CtTypeParameterReference) {
CtTypeParameterReference typeParamRef = (CtTypeParameterReference) typeRef;
CtTypeParameter typeParam = typeParamRef.getDeclaration();
CtFormalTypeDeclarer declarer = typeParam.getTypeParameterDeclarer();
if ((declarer instanceof CtType<?>) == false) {
throw new SpoonException("Cannot adapt type parameters of non type scope");
}
CtType<?> typeDeclarer = (CtType<?>) declarer;
List<CtTypeReference<?>> actualTypeArguments = getActualTypeArguments(typeDeclarer.getQualifiedName());
if (actualTypeArguments == null) {
/*
* the actualTypeArguments of this declarer cannot be resolved.
* There is probably a model inconsistency
*/
throw new SpoonException("Cannot resolve " + (result.size() + 1) + ") type parameter <" + typeParamRef.getSimpleName() + "> of declarer " + declarer);
}
if (actualTypeArguments.size() != typeDeclarer.getFormalCtTypeParameters().size()) {
if (actualTypeArguments.isEmpty() == false) {
throw new SpoonException("Unexpected actual type arguments " + actualTypeArguments + " on " + typeDeclarer);
}
/*
* the scope type was delivered as type reference without appropriate type arguments.
* Use references to formal type parameters
*/
actualTypeArguments = getTypeReferences(typeDeclarer.getFormalCtTypeParameters());
typeToArguments.put(typeDeclarer.getQualifiedName(), actualTypeArguments);
}
typeRef = getValue(actualTypeArguments, typeParam, declarer);
}
result.add(typeRef);
}
return result;
}
private List<CtTypeReference<?>> getActualTypeArguments(String qualifiedName) {
List<CtTypeReference<?>> actualTypeArguments = typeToArguments.get(qualifiedName);
if (actualTypeArguments != null) {
return actualTypeArguments;
}
if (enclosingClassTypingContext != null) {
return enclosingClassTypingContext.getActualTypeArguments(qualifiedName);
}
return null;
}
private static CtTypeReference<?> getValue(List<CtTypeReference<?>> arguments, CtTypeParameter typeParam, CtFormalTypeDeclarer declarer) {
if (declarer.getFormalCtTypeParameters().size() != arguments.size()) {
throw new SpoonException("Unexpected count of actual type arguments");
}
int typeParamIdx = declarer.getFormalCtTypeParameters().indexOf(typeParam);
return arguments.get(typeParamIdx);
}
/**
* Substitutes the typeParameter by its value
* @param typeParameter - to be substituted parameter
* @param declarer - the declarer of typeParameter
* @param values - the list of parameter values
* @return the value from values on the same position as typeParameter in declarer.getFormalCtTypeParameters()
*/
static <T, U extends List<T>> T substituteBy(CtTypeParameter typeParameter, CtFormalTypeDeclarer declarer, U values) {
List<CtTypeParameter> typeParams = declarer.getFormalCtTypeParameters();
int position = typeParams.indexOf(typeParameter);
if (position == -1) {
throw new SpoonException("Type parameter <" + typeParameter.getSimpleName() + " not found in scope " + declarer.getShortRepresentation());
}
if (values.size() != typeParams.size()) {
throw new SpoonException("Unexpected count of parameters");
}
return values.get(position);
}
/**
* @return true if actualType arguments of `scope` are fitting as a subtype of superTypeArgs
*/
private boolean isSubTypeByActualTypeArguments(CtTypeReference<?> superTypeRef, List<CtTypeReference<?>> expectedSuperTypeArguments) {
List<CtTypeReference<?>> superTypeArgs = superTypeRef.getActualTypeArguments();
if (superTypeArgs.isEmpty()) {
//the raw type or not a generic type. Arguments are ignored in sub type detection
return true;
}
List<CtTypeReference<?>> subTypeArgs = expectedSuperTypeArguments;
if (subTypeArgs.isEmpty()) {
//the raw type or not a generic type
return true;
}
if (subTypeArgs.size() != superTypeArgs.size()) {
//the number of arguments is not same - it should not happen ...
return false;
}
for (int i = 0; i < subTypeArgs.size(); i++) {
CtTypeReference<?> superArg = superTypeArgs.get(i);
CtTypeReference<?> subArg = subTypeArgs.get(i);
if (isSubTypeArg(subArg, superArg) == false) {
return false;
}
}
return true;
}
/**
* @return true if actualType argument `subArg` is fitting as a subtype of actual type argument `superArg`
*/
private boolean isSubTypeArg(CtTypeReference<?> subArg, CtTypeReference<?> superArg) {
if (superArg instanceof CtWildcardReference) {
CtWildcardReference wr = (CtWildcardReference) superArg;
CtTypeReference<?> superBound = wr.getBoundingType();
if (superBound == null) {
//everything extends from object, nothing is super of Object
return wr.isUpper();
}
if (subArg instanceof CtWildcardReference) {
CtWildcardReference subWr = (CtWildcardReference) subArg;
CtTypeReference<?> subBound = subWr.getBoundingType();
if (subBound == null) {
//nothing is super of object
return false;
}
if (wr.isUpper() != subWr.isUpper()) {
//one is "super" second is "extends"
return false;
}
if (wr.isUpper()) {
//both are extends
return subBound.isSubtypeOf(superBound);
}
//both are super
return superBound.isSubtypeOf(subBound);
}
if (wr.isUpper()) {
return subArg.isSubtypeOf(superBound);
} else {
return superBound.isSubtypeOf(subArg);
}
}
//superArg is not a wildcard. Only same type is matching
return subArg.equals(superArg);
}
private CtTypeReference<?> adaptTypeForNewMethod(CtTypeReference<?> typeRef) {
if (typeRef == null) {
return null;
}
if (typeRef instanceof CtTypeParameterReference) {
CtTypeParameterReference typeParamRef = (CtTypeParameterReference) typeRef;
CtTypeParameter typeParam = typeParamRef.getDeclaration();
if (typeParam.getTypeParameterDeclarer() instanceof CtExecutable) {
//the parameter is declared in scope of Method or Constructor
return typeRef.clone();
}
}
return adaptType(typeRef);
}
}