/*
* SonarQube Java
* Copyright (C) 2012-2016 SonarSource SA
* mailto:contact AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* 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 GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.java.resolve;
import org.sonar.java.resolve.JavaSymbol.TypeJavaSymbol;
import org.sonar.plugins.java.api.semantic.Type;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
public class TypeSubstitutionSolver {
private final ParametrizedTypeCache parametrizedTypeCache;
private final Symbols symbols;
private final LeastUpperBound leastUpperBound;
private final TypeInferenceSolver typeInferenceSolver;
private Deque<JavaSymbol.TypeVariableJavaSymbol> typevarExplored = new LinkedList<>();
public TypeSubstitutionSolver(ParametrizedTypeCache parametrizedTypeCache, Symbols symbols) {
this.parametrizedTypeCache = parametrizedTypeCache;
this.symbols = symbols;
this.leastUpperBound = new LeastUpperBound(this, parametrizedTypeCache, symbols);
this.typeInferenceSolver = new TypeInferenceSolver(leastUpperBound, symbols, this);
}
Type leastUpperBound(Set<Type> refTypes) {
return leastUpperBound.leastUpperBound(refTypes);
}
@CheckForNull
TypeSubstitution getTypeSubstitution(JavaSymbol.MethodJavaSymbol method, JavaType site, List<JavaType> typeParams, List<JavaType> argTypes) {
List<JavaType> formals = ((MethodJavaType) method.type).argTypes;
TypeSubstitution substitution = new TypeSubstitution();
if (method.isParametrized() || constructParametrizedTypeWithoutSubstitution(method, site)) {
if (!typeParams.isEmpty()) {
substitution = getSubstitutionFromTypeParams(method.typeVariableTypes, typeParams);
} else if (formals.isEmpty()) {
// substitution can not be inferred, as it is not based on arguments, method call is still valid
return substitution;
} else {
formals = applySiteSubstitutionToFormalParameters(formals, site);
substitution = typeInferenceSolver.inferTypeSubstitution(method, formals, argTypes);
}
if (!isValidSubtitution(substitution, site)) {
// substitution discarded
return null;
}
}
return substitution;
}
private static boolean constructParametrizedTypeWithoutSubstitution(JavaSymbol.MethodJavaSymbol method, JavaType site) {
return method.isConstructor() && site.isParameterized() && ((ParametrizedTypeJavaType) site).typeSubstitution.isIdentity();
}
JavaType getReturnType(@Nullable JavaType returnType, JavaType defSite, JavaType callSite, TypeSubstitution substitution, JavaSymbol.MethodJavaSymbol method) {
JavaType resultType = returnType;
if (method.isConstructor()) {
if (constructParametrizedTypeWithoutSubstitution(method, defSite)) {
resultType = applySubstitution(defSite, substitution);
} else {
return defSite;
}
}
// As per getClass javadoc:
// The actual result type [of getClass] is Class<? extends |X|> where |X| is the erasure of the static type of the expression on which getClass is called.
if(defSite == symbols.objectType && "getClass".equals(method.name())) {
TypeJavaSymbol classSymbol = symbols.classType.symbol;
JavaType wildcardType = parametrizedTypeCache.getWildcardType(callSite.erasure(), WildCardType.BoundType.EXTENDS);
resultType = parametrizedTypeCache.getParametrizedTypeType(classSymbol, new TypeSubstitution().add(classSymbol.typeVariableTypes.get(0), wildcardType));
}
resultType = applySiteSubstitution(resultType, defSite);
if (callSite != defSite) {
resultType = applySiteSubstitution(resultType, callSite);
}
resultType = applySubstitution(resultType, substitution);
if (!isReturnTypeCompletelySubstituted(resultType, method.typeVariableTypes)
|| (method.isConstructor() && !isReturnTypeCompletelySubstituted(resultType, defSite.symbol.typeVariableTypes))) {
resultType = symbols.deferedType(resultType);
}
return resultType;
}
private static boolean isReturnTypeCompletelySubstituted(JavaType resultType, List<TypeVariableJavaType> typeVariables){
if(typeVariables.contains(resultType)) {
return false;
}
if(resultType.isArray()) {
return isReturnTypeCompletelySubstituted(((ArrayJavaType) resultType).elementType, typeVariables);
}
if(resultType.isTagged(JavaType.WILDCARD)) {
return isReturnTypeCompletelySubstituted(((WildCardType) resultType).bound, typeVariables);
}
if (resultType.isParameterized()) {
for (JavaType substitutedType : ((ParametrizedTypeJavaType) resultType).typeSubstitution.substitutedTypes()) {
if(!isReturnTypeCompletelySubstituted(substitutedType, typeVariables)) {
return false;
}
}
}
return true;
}
List<JavaType> applySiteSubstitutionToFormalParameters(List<JavaType> formals, JavaType site) {
TypeSubstitution typeSubstitution = new TypeSubstitution();
if (site.isParameterized()) {
typeSubstitution = ((ParametrizedTypeJavaType) site).typeSubstitution;
}
TypeJavaSymbol siteSymbol = site.getSymbol();
List<JavaType> newFormals = formals;
Type superClass = siteSymbol.superClass();
if (superClass != null) {
JavaType newSuperClass = applySubstitution((JavaType) superClass, typeSubstitution);
newFormals = applySiteSubstitutionToFormalParameters(newFormals, newSuperClass);
}
for (Type interfaceType : siteSymbol.interfaces()) {
JavaType newInterfaceType = applySubstitution((JavaType) interfaceType, typeSubstitution);
newFormals = applySiteSubstitutionToFormalParameters(newFormals, newInterfaceType);
}
return applySubstitutionToFormalParameters(newFormals, typeSubstitution);
}
JavaType applySiteSubstitution(JavaType type, JavaType site) {
if (site.isParameterized()) {
return applySubstitution(type, ((ParametrizedTypeJavaType) site).typeSubstitution);
}
return type;
}
JavaType applySiteSubstitution(@Nullable JavaType resolvedType, JavaType callSite, JavaType resolvedTypeDefinition) {
if (resolvedType == null) {
// case of constructors
return null;
}
if(resolvedType.isTagged(JavaType.METHOD)) {
MethodJavaType methodType = (MethodJavaType) resolvedType;
JavaType resultType = applySiteSubstitution(methodType.resultType, callSite, resolvedTypeDefinition);
List<JavaType> argTypes = methodType.argTypes.stream().map(argType -> applySiteSubstitution(argType, callSite, resolvedTypeDefinition)).collect(Collectors.toList());
return new MethodJavaType(argTypes, resultType, methodType.thrown, methodType.symbol);
}
return applySiteSubstitution(applySiteSubstitution(resolvedType, resolvedTypeDefinition), callSite);
}
List<JavaType> applySubstitutionToFormalParameters(List<JavaType> types, TypeSubstitution substitution) {
if (substitution.size() == 0 || types.isEmpty()) {
return types;
}
return types.stream().map(type -> applySubstitution(type, substitution)).collect(Collectors.toList());
}
JavaType applySubstitution(JavaType type, TypeSubstitution substitution) {
JavaType substitutedType = substitution.substitutedType(type);
if (substitutedType != null) {
return substitutedType;
}
if (type.isTagged(JavaType.TYPEVAR)) {
return substituteInTypeVar((TypeVariableJavaType) type, substitution);
}
if (type.isParameterized()) {
return substituteInParametrizedType((ParametrizedTypeJavaType) type, substitution);
}
if (type.isTagged(JavaType.WILDCARD)) {
return substituteInWildCardType((WildCardType) type, substitution);
}
if (type.isArray()) {
return substituteInArrayType((ArrayJavaType) type, substitution);
}
return type;
}
private JavaType substituteInParametrizedType(ParametrizedTypeJavaType type, TypeSubstitution substitution) {
TypeSubstitution newSubstitution = new TypeSubstitution();
for (Map.Entry<TypeVariableJavaType, JavaType> entry : type.typeSubstitution.substitutionEntries()) {
newSubstitution.add(entry.getKey(), applySubstitution(entry.getValue(), substitution));
}
return parametrizedTypeCache.getParametrizedTypeType(type.rawType.getSymbol(), newSubstitution);
}
private JavaType substituteInTypeVar(TypeVariableJavaType typevar, TypeSubstitution substitution) {
if(typevarExplored.contains(typevar.symbol)) {
return typevar;
}
typevarExplored.push((JavaSymbol.TypeVariableJavaSymbol) typevar.symbol);
List<JavaType> subtitutedBounds = typevar.bounds.stream().map(t -> applySubstitution(t, substitution)).collect(Collectors.toList());
typevarExplored.pop();
if(subtitutedBounds.equals(typevar.bounds)) {
return typevar;
}
TypeVariableJavaType typeVariableJavaType = new TypeVariableJavaType((JavaSymbol.TypeVariableJavaSymbol) typevar.symbol);
typeVariableJavaType.bounds = subtitutedBounds;
return typeVariableJavaType;
}
private JavaType substituteInWildCardType(WildCardType wildcard, TypeSubstitution substitution) {
JavaType substitutedType = applySubstitution(wildcard.bound, substitution);
if (substitutedType != wildcard.bound) {
return parametrizedTypeCache.getWildcardType(substitutedType, wildcard.boundType);
}
return wildcard;
}
private JavaType substituteInArrayType(ArrayJavaType arrayType, TypeSubstitution substitution) {
JavaType rootElementType = arrayType.elementType;
int nbDimensions = 1;
while (rootElementType.isArray()) {
rootElementType = ((ArrayJavaType) rootElementType).elementType;
nbDimensions++;
}
JavaType substitutedType = applySubstitution(rootElementType, substitution);
if (substitutedType != rootElementType) {
// FIXME SONARJAVA-1574 a new array type should not be created but reused if already existing for the current element type
for (int i = 0; i < nbDimensions; i++) {
substitutedType = new ArrayJavaType(substitutedType, symbols.arrayClass);
}
return substitutedType;
}
return arrayType;
}
TypeSubstitution getSubstitutionFromTypeParams(List<TypeVariableJavaType> typeVariableTypes, List<JavaType> typeParams) {
TypeSubstitution substitution = new TypeSubstitution();
if (typeVariableTypes.size() == typeParams.size()) {
// create naive substitution
for (int i = 0; i < typeVariableTypes.size(); i++) {
TypeVariableJavaType typeVariableType = typeVariableTypes.get(i);
JavaType typeParam = typeParams.get(i);
substitution.add(typeVariableType, typeParam);
}
}
return substitution;
}
private boolean isValidSubtitution(TypeSubstitution substitutions, JavaType site) {
for (Map.Entry<TypeVariableJavaType, JavaType> substitution : substitutions.substitutionEntries()) {
if (!isValidSubstitution(substitutions, substitution.getKey(), substitution.getValue(), site)) {
return false;
}
}
return true;
}
private boolean isValidSubstitution(TypeSubstitution candidate, TypeVariableJavaType typeVar, JavaType typeParam, JavaType site) {
for (JavaType bound : typeVar.bounds) {
JavaType currentBound = applySubstitution(bound, candidate);
while (currentBound.isTagged(JavaType.TYPEVAR) && !currentBound.symbol().owner().isMethodSymbol()) {
JavaType newBound = candidate.substitutedType(currentBound);
if (newBound == null && site.isParameterized()) {
newBound = ((ParametrizedTypeJavaType) site).typeSubstitution.substitutedType(currentBound);
}
if (newBound == null) {
return ((JavaSymbol.TypeJavaSymbol) site.symbol()).typeVariableTypes.contains(currentBound);
}
if (currentBound.equals(newBound)) {
// exploring the same substitution, we cannot deduce anything
break;
}
currentBound = newBound;
}
if (!isUnboundedWildcard(typeParam) && !typeParam.isSubtypeOf(currentBound)) {
return false;
}
}
return true;
}
private static boolean isUnboundedWildcard(JavaType type) {
return type.isTagged(JavaType.WILDCARD) && ((WildCardType) type).boundType == WildCardType.BoundType.UNBOUNDED;
}
JavaType erasureSubstitution(ParametrizedTypeJavaType type) {
TypeSubstitution substitution = new TypeSubstitution();
for (Map.Entry<TypeVariableJavaType, JavaType> entry : type.typeSubstitution.substitutionEntries()) {
TypeVariableJavaType typeVar = entry.getKey();
JavaType subs = entry.getValue();
if (typeVar == subs) {
subs = subs.erasure();
}
substitution.add(typeVar, subs);
}
return parametrizedTypeCache.getParametrizedTypeType(type.symbol, substitution);
}
static TypeSubstitution substitutionFromSuperType(ParametrizedTypeJavaType target, ParametrizedTypeJavaType source) {
TypeSubstitution result = new TypeSubstitution(target.typeSubstitution);
if (target.rawType != source.rawType) {
TypeJavaSymbol targetSymbol = target.symbol;
Type superClass = targetSymbol.superClass();
if (superClass != null && ((JavaType) superClass).isParameterized()) {
TypeSubstitution newSub = substitutionFromSuperType((ParametrizedTypeJavaType) superClass, source);
result = result.combine(newSub);
}
for (Type superInterface : targetSymbol.interfaces()) {
if (((JavaType) superInterface).isParameterized()) {
TypeSubstitution newSub = substitutionFromSuperType((ParametrizedTypeJavaType) superInterface, source);
result = result.combine(newSub);
}
}
} else {
result = target.typeSubstitution.combine(source.typeSubstitution);
}
return result;
}
}