/*
* 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 com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Sets;
import org.sonar.plugins.java.api.semantic.Symbol;
import org.sonar.plugins.java.api.semantic.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class LeastUpperBound {
private final Symbols symbols;
private final ParametrizedTypeCache parametrizedTypeCache;
private final TypeSubstitutionSolver typeSubstitutionSolver;
private final Set<Set<Type>> lubCache = new HashSet<>();
public LeastUpperBound(TypeSubstitutionSolver typeSubstitutionSolver, ParametrizedTypeCache parametrizedTypeCache, Symbols symbols) {
this.symbols = symbols;
this.parametrizedTypeCache = parametrizedTypeCache;
this.typeSubstitutionSolver = typeSubstitutionSolver;
}
/**
* Compute the "Least Upper Bound" ("lub", jls8 §4.10.4) of a list of type. The "lub" is a shared supertype that is more specific than any
* other shared supertype (that is, no other shared supertype is a subtype of the least upper bound)
*
* Parameterized types are currently ignored, as the method is used only to handle Union Types Trees, themselves being used only
* in catch trees. Note that Exceptions (any subclass of Throwable) cannot be generic (jls8 §8.1.2, §11.1.1: "compile-time error if a generic
* class is a direct or indirect subclass of Throwable")
*
* @param types
* @return the least upper bound of the types
*/
public Type leastUpperBound(Set<Type> types) {
Type lub = cachedLeastUpperBound(types);
lubCache.clear();
return lub;
}
private Type cachedLeastUpperBound(Set<Type> types) {
Preconditions.checkArgument(!types.isEmpty());
Iterator<Type> iterator = types.iterator();
Type first = iterator.next();
// lub(U) = U
if (types.size() == 1) {
return first;
}
Set<Type> newTypes = primitiveWrappers(types);
List<Set<Type>> supertypes = supertypes(newTypes);
List<Set<Type>> erasedSupertypes = erased(supertypes);
List<Type> erasedCandidates = intersection(erasedSupertypes);
List<Type> minimalErasedCandidates = minimalCandidates(erasedCandidates);
if (minimalErasedCandidates.isEmpty()) {
return Symbols.unknownType;
}
Multimap<Type, Type> relevantParameterizations = relevantParameterizations(minimalErasedCandidates, supertypes);
Type erasedBest = best(minimalErasedCandidates);
Collection<Type> erasedTypeParameterizations = relevantParameterizations.get(erasedBest);
if (erasedTypeParameterizations != null && !erasedTypeParameterizations.contains(erasedBest)) {
Set<Type> searchedTypes = new HashSet<>(types);
// if we already encountered these types in LUB calculation,
// we interrupt calculation and use the erasure of the parameterized type instead
if (!lubCache.contains(searchedTypes)) {
lubCache.add(searchedTypes);
return leastContainingParameterization(new ArrayList<>(erasedTypeParameterizations));
}
}
return erasedBest;
}
private static Set<Type> primitiveWrappers(Set<Type> types) {
if (types.stream().allMatch(Type::isPrimitive)) {
return types;
}
return types.stream().map(t -> !t.isPrimitive() ? t : ((JavaType) t).primitiveWrapperType()).collect(Collectors.toCollection(LinkedHashSet::new));
}
private List<Set<Type>> supertypes(Collection<Type> types) {
return types.stream()
.map(type -> supertypes((JavaType) type).stream().collect(Collectors.toCollection(LinkedHashSet::new)))
.collect(Collectors.toList());
}
@VisibleForTesting
Set<Type> supertypes(JavaType type) {
List<Type> result = new ArrayList<>();
result.add(type);
Symbol.TypeSymbol symbol = type.symbol();
TypeSubstitution substitution = getTypeSubstitution(type);
result.addAll(interfacesWithSubstitution(symbol, substitution));
Type superClass = symbol.superClass();
while (superClass != null) {
JavaType substitutedSuperClass = applySubstitution(superClass, substitution);
result.add(substitutedSuperClass);
substitution = getTypeSubstitution(substitutedSuperClass);
JavaSymbol.TypeJavaSymbol superClassSymbol = substitutedSuperClass.getSymbol();
result.addAll(interfacesWithSubstitution(superClassSymbol, substitution));
superClass = superClassSymbol.superClass();
}
return new LinkedHashSet<>(result);
}
private Set<Type> interfacesWithSubstitution(Symbol.TypeSymbol symbol, TypeSubstitution substitution) {
return symbol.interfaces().stream()
.flatMap(interfaceType -> supertypes(applySubstitution(interfaceType, substitution)).stream())
.collect(Collectors.toSet());
}
private static TypeSubstitution getTypeSubstitution(JavaType type) {
return type.isTagged(JavaType.PARAMETERIZED) ? ((ParametrizedTypeJavaType) type).typeSubstitution : new TypeSubstitution();
}
private JavaType applySubstitution(Type type, TypeSubstitution substitution) {
return typeSubstitutionSolver.applySubstitution((JavaType) type, substitution);
}
private static List<Set<Type>> erased(List<Set<Type>> typeSets) {
return typeSets.stream().map(set -> set.stream().map(Type::erasure).collect(Collectors.toCollection(LinkedHashSet::new))).collect(Collectors.toList());
}
private static List<Type> intersection(List<Set<Type>> supertypes) {
return new ArrayList<>(supertypes.stream().reduce(union(supertypes), Sets::intersection));
}
private static Set<Type> union(List<Set<Type>> supertypes) {
return supertypes.stream().flatMap(Collection::stream).collect(Collectors.toCollection(LinkedHashSet::new));
}
/**
* Let MEC, the minimal erased candidate set for U1 ... Uk, be:
* MEC = { V | V in EC, and for all W != V in EC, it is not the case that W <: V }
* @param erasedCandidates
* @return
*/
private static List<Type> minimalCandidates(List<Type> erasedCandidates) {
List<Type> results = new ArrayList<>();
for (Type v : erasedCandidates) {
if (erasedCandidates.stream().noneMatch(w -> !w.equals(v) && w.isSubtypeOf(v))) {
results.add(v);
}
}
return results;
}
/**
* For any element G of MEC that is a generic type, let the "relevant" parameterizations of G, Relevant(G), be:
* Relevant(G) = { V | 1 ≤ i ≤ k: V in ST(Ui) and V = G<...> }
*
* @param minimalErasedCandidates MEC
* @param supertypes
* @return the set of known parameterizations for each generic type G of MEC
*/
private static Multimap<Type, Type> relevantParameterizations(List<Type> minimalErasedCandidates, List<Set<Type>> supertypes) {
Multimap<Type, Type> result = Multimaps.newSetMultimap(new HashMap<>(), LinkedHashSet::new);
for (Set<Type> supertypesSet : supertypes) {
for (Type supertype : supertypesSet) {
Type erasedSupertype = supertype.erasure();
if (minimalErasedCandidates.contains(erasedSupertype)) {
result.put(erasedSupertype, supertype);
}
}
}
return result;
}
@VisibleForTesting
static Type best(List<Type> minimalCandidates) {
Collections.sort(minimalCandidates, (t1, t2) -> {
// Sort minimal candidates by name with classes before interfaces, to guarantee always the same type is returned when approximated.
Symbol.TypeSymbol t1Symbol = t1.symbol();
Symbol.TypeSymbol t2Symbol = t2.symbol();
if (t1Symbol.isInterface() && t2Symbol.isInterface()) {
return t1.name().compareTo(t2.name());
} else if (t1Symbol.isInterface()) {
return 1;
} else if (t2Symbol.isInterface()) {
return -1;
}
return t1.name().compareTo(t2.name());
});
// FIXME SONARJAVA-1632 should return union of types
return minimalCandidates.get(0);
}
/**
* Let the "candidate" parameterization of G, Candidate(G), be the most specific parameterization of the generic type G that contains all
* the relevant parameterizations of G: Candidate(G) = lcp(Relevant(G)), where lcp() is the least containing parameterization.
* @param types
* @return
*/
private Type leastContainingParameterization(List<Type> types) {
if (types.size() == 1) {
return types.get(0);
}
JavaType type1 = (JavaType) types.get(0);
JavaType type2 = (JavaType) types.get(1);
Type reduction = leastContainingTypeArgument(type1, type2);
List<Type> reducedList = Lists.newArrayList(reduction);
reducedList.addAll(types.subList(2, types.size()));
return leastContainingParameterization(reducedList);
}
private Type leastContainingTypeArgument(JavaType type1, JavaType type2) {
Preconditions.checkArgument(type1.isTagged(JavaType.PARAMETERIZED) && type2.isTagged(JavaType.PARAMETERIZED));
TypeSubstitution typeSubstitution1 = ((ParametrizedTypeJavaType) type1).typeSubstitution;
TypeSubstitution typeSubstitution2 = ((ParametrizedTypeJavaType) type2).typeSubstitution;
TypeSubstitution newTypeSubstitution = new TypeSubstitution();
for (TypeVariableJavaType typeVar : typeSubstitution1.typeVariables()) {
JavaType subs1 = typeSubstitution1.substitutedType(typeVar);
JavaType subs2 = typeSubstitution2.substitutedType(typeVar);
JavaType newSubs = getNewTypeArgumentType(subs1, subs2);
newTypeSubstitution.add(typeVar, newSubs);
}
return parametrizedTypeCache.getParametrizedTypeType(type1.symbol, newTypeSubstitution);
}
private JavaType getNewTypeArgumentType(JavaType type1, JavaType type2) {
boolean isWildcard1 = type1.isTagged(JavaType.WILDCARD);
boolean isWildcard2 = type2.isTagged(JavaType.WILDCARD);
JavaType result;
if (type1.equals(type2)) {
result = type1;
} else if (isWildcard1 && isWildcard2) {
result = lctaBothWildcards((WildCardType) type1, (WildCardType) type2);
} else if (isWildcard1 ^ isWildcard2) {
JavaType rawType = isWildcard1 ? type2 : type1;
WildCardType wildcardType = (WildCardType) (isWildcard1 ? type1 : type2);
result = lctaOneWildcard(rawType, wildcardType);
} else {
result = lctaNoWildcard(type1, type2);
}
return result;
}
private JavaType lctaOneWildcard(JavaType rawType, WildCardType wildcardType) {
if (wildcardType.boundType == WildCardType.BoundType.SUPER) {
JavaType glb = (JavaType) greatestLowerBound(Lists.newArrayList(rawType, wildcardType.bound));
return parametrizedTypeCache.getWildcardType(glb, WildCardType.BoundType.SUPER);
}
JavaType lub = (JavaType) cachedLeastUpperBound(Sets.newHashSet(rawType, wildcardType.bound));
return parametrizedTypeCache.getWildcardType(lub, WildCardType.BoundType.EXTENDS);
}
private JavaType lctaBothWildcards(WildCardType type1, WildCardType type2) {
if (type1.boundType == WildCardType.BoundType.SUPER && type2.boundType == WildCardType.BoundType.SUPER) {
JavaType glb = (JavaType) greatestLowerBound(Lists.newArrayList(type1.bound, type2.bound));
return parametrizedTypeCache.getWildcardType(glb, WildCardType.BoundType.SUPER);
}
if (type1.boundType == WildCardType.BoundType.EXTENDS && type2.boundType == WildCardType.BoundType.EXTENDS) {
JavaType lub = (JavaType) cachedLeastUpperBound(Sets.newHashSet(type1.bound, type2.bound));
return parametrizedTypeCache.getWildcardType(lub, WildCardType.BoundType.EXTENDS);
}
if (type1.bound.equals(type2.bound)) {
return type1.bound;
}
return symbols.unboundedWildcard;
}
private JavaType lctaNoWildcard(JavaType type1, JavaType type2) {
JavaType lub = (JavaType) cachedLeastUpperBound(Sets.newHashSet(type1, type2));
return parametrizedTypeCache.getWildcardType(lub, WildCardType.BoundType.EXTENDS);
}
/**
* From JLS 8 5.1.10 - greatest lower bound : glb(V1,...,Vm) is defined as V1 & ... & Vm.
* @param types
* @return
*/
private static Type greatestLowerBound(List<Type> types) {
// TODO SONARJAVA-1632 implement, should return intersection of all types, not only first one
return types.iterator().next();
}
}