diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/typeresolution/MethodTypeResolution.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/typeresolution/MethodTypeResolution.java index 9fb3c15305..5c7739e49c 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/typeresolution/MethodTypeResolution.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/typeresolution/MethodTypeResolution.java @@ -484,6 +484,10 @@ public final class MethodTypeResolution { return isSubtypeable(parameter, argument.getTypeDefinition()); } + public static boolean isSubtypeable(Class parameter, Class argument) { + return isSubtypeable(JavaTypeDefinition.forClass(parameter), JavaTypeDefinition.forClass(argument)); + } + /** * Subtypeability rules. * https://docs.oracle.com/javase/specs/jls/se7/html/jls-4.html#jls-4.10 @@ -496,7 +500,21 @@ public final class MethodTypeResolution { // this covers arrays, simple class/interface cases if (parameter.getType().isAssignableFrom(argument.getType())) { - return true; + if (!parameter.isGeneric() || parameter.isRawType() || argument.isRawType()) { + return true; + } + + // parameter is a non-raw generic type + // argument is a non-generic or a non-raw generic type + + // example result: List.getAsSuper(Collection) becomes Collection + JavaTypeDefinition argSuper = argument.getAsSuper(parameter.getType()); + // argSuper can't be null because isAssignableFrom check above returned true + + // right now we only check if generic arguments are the same + // TODO: add support for wildcard types + // (future note: can't call subtype as it is recursively, infinite types) + return parameter.equals(argSuper); } int indexOfParameter = PRIMITIVE_SUBTYPE_ORDER.indexOf(parameter.getType()); @@ -528,12 +546,12 @@ public final class MethodTypeResolution { public static List getMethodExplicitTypeArugments(Node node) { ASTMemberSelector memberSelector = node.getFirstChildOfType(ASTMemberSelector.class); if (memberSelector == null) { - return Collections.emptyList(); // empty list + return Collections.emptyList(); } ASTTypeArguments typeArguments = memberSelector.getFirstChildOfType(ASTTypeArguments.class); if (typeArguments == null) { - return Collections.emptyList(); // empty list + return Collections.emptyList(); } List result = new ArrayList<>(); diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/typeresolution/typedefinition/JavaTypeDefinition.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/typeresolution/typedefinition/JavaTypeDefinition.java index e272bfae6d..969c17f569 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/typeresolution/typedefinition/JavaTypeDefinition.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/typeresolution/typedefinition/JavaTypeDefinition.java @@ -12,8 +12,10 @@ import java.lang.reflect.WildcardType; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; public class JavaTypeDefinition implements TypeDefinition { // contains TypeDefs where only the clazz field is used @@ -21,11 +23,14 @@ public class JavaTypeDefinition implements TypeDefinition { private final Class clazz; private final List genericArgs; + // cached because calling clazz.getTypeParameters().length create a new array every time + private final int typeParameterCount; private final boolean isGeneric; private final JavaTypeDefinition enclosingClass; private JavaTypeDefinition(final Class clazz) { this.clazz = clazz; + this.typeParameterCount = clazz.getTypeParameters().length; final TypeVariable[] typeParameters; // the anonymous class can't have generics, but we may be binding generics from super classes @@ -64,10 +69,7 @@ public class JavaTypeDefinition implements TypeDefinition { final JavaTypeDefinition newDef = new JavaTypeDefinition(clazz); - // We can only cache types without generics, since their values are context-based - if (!newDef.isGeneric) { - CLASS_TYPE_DEF_CACHE.put(clazz, newDef); - } + CLASS_TYPE_DEF_CACHE.put(clazz, newDef); return newDef; } @@ -80,9 +82,7 @@ public class JavaTypeDefinition implements TypeDefinition { // With generics there is no cache final JavaTypeDefinition typeDef = new JavaTypeDefinition(clazz); - for (final JavaTypeDefinition generic : boundGenerics) { - typeDef.genericArgs.add(generic); - } + Collections.addAll(typeDef.genericArgs, boundGenerics); return typeDef; } @@ -237,7 +237,7 @@ public class JavaTypeDefinition implements TypeDefinition { } public int getTypeParameterCount() { - return clazz.getTypeParameters().length; + return typeParameterCount; } public boolean isArrayType() { @@ -252,4 +252,108 @@ public class JavaTypeDefinition implements TypeDefinition { .append(']').toString(); } + + @Override + public boolean equals(Object obj) { + if (obj == null || !(obj instanceof JavaTypeDefinition)) { + return false; + } + + // raw vs raw + // we assume that this covers raw types, because they are cached + if (this == obj) { + return true; + } + + JavaTypeDefinition otherTypeDef = (JavaTypeDefinition) obj; + + if (clazz != otherTypeDef.clazz) { + return false; + } + + + // This should cover + // raw vs proper + // proper vs raw + // proper vs proper + + // Note: we have to force raw types to compute their generic args, class Stuff> + // Stuff a; + // Stuff> b; + // Stuff> c; + // all of the above should be equal + + for (int i = 0; i < getTypeParameterCount(); ++i) { + // Note: we assume that cycles can only exist because of raw types + if (!getGenericType(i).equals(otherTypeDef.getGenericType(i))) { + return false; + } + } + + return true; + } + + @Override + public int hashCode() { + return clazz.hashCode(); + } + + public Set getSuperTypeSet() { + return getSuperTypeSet(new HashSet()); + } + + private Set getSuperTypeSet(Set destinationSet) { + destinationSet.add(this); + + if (this.clazz != Object.class) { + + resolveTypeDefinition(clazz.getGenericSuperclass()).getSuperTypeSet(destinationSet); + + for (Type type : clazz.getGenericInterfaces()) { + resolveTypeDefinition(type).getSuperTypeSet(destinationSet); + } + } + + return destinationSet; + } + + public Set> getErasedSuperTypeSet() { + Set> result = new HashSet<>(); + result.add(Object.class); + return getErasedSuperTypeSet(this.clazz, result); + } + + private static Set> getErasedSuperTypeSet(Class clazz, Set> destinationSet) { + if (clazz != null) { + destinationSet.add(clazz); + getErasedSuperTypeSet(clazz.getSuperclass(), destinationSet); + + for (Class interfaceType : clazz.getInterfaces()) { + getErasedSuperTypeSet(interfaceType, destinationSet); + } + } + + return destinationSet; + } + + /** + * @return true if clazz is generic and had not been parameterized + */ + public boolean isRawType() { + return isGeneric() && CLASS_TYPE_DEF_CACHE.containsKey(getType()); + } + + public JavaTypeDefinition getAsSuper(Class superClazz) { + if (clazz == superClazz) { // optimize for same class calls + return this; + } + + for (JavaTypeDefinition superTypeDef : getSuperTypeSet()) { + if (superTypeDef.clazz == superClazz) { + return superTypeDef; + } + } + + return null; + } } diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/typeresolution/typeinference/BoundOrConstraint.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/typeresolution/typeinference/BoundOrConstraint.java index 376536b26e..a7dc1cea1f 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/typeresolution/typeinference/BoundOrConstraint.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/typeresolution/typeinference/BoundOrConstraint.java @@ -5,6 +5,7 @@ package net.sourceforge.pmd.lang.java.typeresolution.typeinference; import java.util.List; +import java.util.Set; import net.sourceforge.pmd.lang.java.typeresolution.typedefinition.JavaTypeDefinition; @@ -134,4 +135,36 @@ public abstract class BoundOrConstraint { } public abstract List reduce(); + + public void addVariablesToSet(Set variables) { + if (leftTypeVariable != null) { + variables.add(leftTypeVariable); + } + + if (rightTypeVariable != null) { + variables.add(rightTypeVariable); + } + } + + /** + * @return true, if the left-hand side mentions variables + */ + public boolean leftHasMentionedVariable() { + return leftTypeVariable != null; + } + + /** + * @return true, if the right-hand side mentions variales + */ + public boolean rightHasMentionedVariable() { + return rightTypeVariable != null; + } + + public Variable getLeftMentionedVariable() { + return leftTypeVariable; + } + + public Variable getRightMentionedVariable() { + return rightTypeVariable; + } } diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/typeresolution/typeinference/TypeInferenceResolver.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/typeresolution/typeinference/TypeInferenceResolver.java index 2cce89db6f..9cbddf8e7e 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/typeresolution/typeinference/TypeInferenceResolver.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/typeresolution/typeinference/TypeInferenceResolver.java @@ -8,15 +8,454 @@ import static net.sourceforge.pmd.lang.java.typeresolution.typeinference.Inferen import static net.sourceforge.pmd.lang.java.typeresolution.typeinference.InferenceRuleType.SUBTYPE; import java.util.ArrayList; +import java.util.BitSet; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; import java.util.List; +import java.util.Map; +import java.util.Set; + +import net.sourceforge.pmd.lang.java.typeresolution.MethodTypeResolution; +import net.sourceforge.pmd.lang.java.typeresolution.typedefinition.JavaTypeDefinition; public final class TypeInferenceResolver { + public static class ResolutionFailedException extends RuntimeException { + + } + private TypeInferenceResolver() { } + public static JavaTypeDefinition lub(List types) { + for (JavaTypeDefinition type : types) { + if (type.isArrayType()) { + // TODO: add support for array types + return JavaTypeDefinition.forClass(Object.class); + } + } + + // step 1 - EC + Set> erasedCandidateSet = getErasedCandidateSet(types); + // step 2 - MEC + Set> minimalSet = getMinimalErasedCandidateSet(erasedCandidateSet); + + List candidates = new ArrayList<>(); + + // for each element G n MEC + for (Class erasedSupertype : minimalSet) { + JavaTypeDefinition lci = types.get(0).getAsSuper(erasedSupertype); + + for (JavaTypeDefinition type : types) { + if (lci == null) { + throw new ResolutionFailedException(); + } + + lci = intersect(lci, type.getAsSuper(erasedSupertype)); + } + + candidates.add(lci); + } + + if (candidates.isEmpty()) { + throw new ResolutionFailedException(); + } + + JavaTypeDefinition result = candidates.get(0); + + for (JavaTypeDefinition candidate : candidates) { + if (MethodTypeResolution.isSubtypeable(candidate, result)) { + result = candidate; + } else if (!MethodTypeResolution.isSubtypeable(result, candidate)) { + // TODO: add support for compound types + throw new ResolutionFailedException(); + } // else: result contains candidate, nothing else to do + } + + return result; + } + + /** + * @return the intersection of the two types + */ + public static JavaTypeDefinition intersect(JavaTypeDefinition first, JavaTypeDefinition second) { + if (first.equals(second)) { // two types equal + return first; + } else if (first.getType() == second.getType()) { + if (!first.isRawType() && !second.isRawType()) { // are generic + return merge(first, second); + } else { // one of them is raw + return JavaTypeDefinition.forClass(first.getType()); + } + } + + throw new ResolutionFailedException(); + } + + /** + * Merge two types of the same class to something both can be assigned to and is most specific. + */ + public static JavaTypeDefinition merge(JavaTypeDefinition first, JavaTypeDefinition second) { + if (first.getType() != second.getType()) { + throw new IllegalStateException("Must be called with typedefinitions of the same class"); + } + + if (!first.isGeneric()) { + return first; + } + + + JavaTypeDefinition[] mergedGeneric = new JavaTypeDefinition[first.getTypeParameterCount()]; + + for (int i = 0; i < first.getTypeParameterCount(); ++i) { + if (MethodTypeResolution.isSubtypeable(first.getGenericType(i), second.getGenericType(i))) { + mergedGeneric[i] = first.getGenericType(i); + } else if (MethodTypeResolution.isSubtypeable(second.getGenericType(i), first.getGenericType(i))) { + mergedGeneric[i] = second.getGenericType(i); + } else { + return JavaTypeDefinition.forClass(Object.class); + // TODO: Generic types of the same class can be merged like so: + // List List -> List + // but we don't have wildcards yet + } + } + + return JavaTypeDefinition.forClass(first.getType(), mergedGeneric); + } + + public static Set> getErasedCandidateSet(List erasedSuperTypeSets) { + Set> result = null; + + if (!erasedSuperTypeSets.isEmpty()) { + result = erasedSuperTypeSets.get(0).getErasedSuperTypeSet(); + } + + for (int i = 1; i < erasedSuperTypeSets.size(); ++i) { + result.retainAll(erasedSuperTypeSets.get(i).getErasedSuperTypeSet()); + } + + return result; + } + + public static Set> getMinimalErasedCandidateSet(Set> erasedSet) { + Set> result = new HashSet<>(); + + outter: + for (Class candidate : erasedSet) { + for (Class erasedSetMember : erasedSet) { + if (candidate != erasedSetMember + && MethodTypeResolution.isSubtypeable(candidate, erasedSetMember)) { + continue outter; // skip candidate from result set + } + } + + result.add(candidate); + } + + return result; + } + + /** + * Resolve unresolved variables in a list of bounds. + */ + public static Map resolveVariables(List bounds) { + Map> variableDependencies = getVariableDependencies(bounds); + Map instantiations = getInstantiations(bounds); + + List uninstantiatedVariables = new ArrayList<>(getUninstantiatedVariables(bounds)); + + // If every variable in V has an instantiation, then resolution succeeds and this procedure terminates. + while (!uninstantiatedVariables.isEmpty()) { + // "... ii) there exists no non-empty proper subset of { α1, ..., αn } with this property. ..." + + // Note: since the Combinations class enumerates the power set from least numerous to most numerous sets + // the above requirement is satisfied + + List variablesToResolve = null; + + for (List variableSet : new Combinations(uninstantiatedVariables)) { + if (isProperSubsetOfVariables(variableSet, instantiations, variableDependencies, bounds)) { + variablesToResolve = variableSet; + break; + } + } + + if (variablesToResolve == null) { + throw new ResolutionFailedException(); + } + + // if there are least upper bounds + for (Variable var : variablesToResolve) { + List lowerBounds = getLowerBoundsOf(var, bounds); + // TODO: should call incorporation + instantiations.put(var, lub(lowerBounds)); + } + + uninstantiatedVariables.removeAll(variablesToResolve); + } + + return instantiations; + } + + public static List getLowerBoundsOf(Variable var, List bounds) { + List result = new ArrayList<>(); + for (Bound bound : bounds) { + if (bound.ruleType() == SUBTYPE && bound.rightVariable() == var) { + // TODO: add support for variables depending on other variables + if (bound.isLeftVariable()) { + throw new ResolutionFailedException(); + } + + result.add(bound.leftProper()); + } + } + + return result; + } + + /** + * Given a set of inference variables to resolve, let V be the union of this set and all variables upon which + * the resolution of at least one variable in this set depends. + *

+ * ... + *

+ * Otherwise, let { α1, ..., αn } be a non-empty subset of uninstantiated variables in V such that i) for all + * i (1 ≤ i ≤ n), if αi depends on the resolution of a variable β, then either β has an instantiation or + * there is some j such that β = αj; and Resolution proceeds by generating an instantiation for each of α1, + * ..., αn based on the bounds in the bound set: + * + * @return true, if 'variables' is a resolvable subset + */ + public static boolean isProperSubsetOfVariables(List variables, + Map instantiations, + Map> dependencies, + List bounds) { + + + for (Variable unresolvedVariable : variables) { + for (Variable dependency : dependencies.get(unresolvedVariable)) { + if (!instantiations.containsKey(dependency) + && unresolvedVariable != dependency + && !boundsHaveAnEqualityBetween(variables, dependency, bounds)) { + return false; + } + } + } + + return true; + } + + /** + * @return true, if 'bounds' contains an equality between 'second' and an element from 'firstList' + */ + public static boolean boundsHaveAnEqualityBetween(List firstList, Variable second, List bounds) { + for (Bound bound : bounds) { + for (Variable first : firstList) { + if (bound.ruleType == EQUALITY + && ((bound.leftVariable() == first && bound.rightVariable() == second) + || (bound.leftVariable() == second && bound.rightVariable() == first))) { + return true; + } + } + } + + return false; + } + + /** + * Makes it possible to iterate over the power set of a List. The order is from the least numerous + * to the most numerous. + * Example list: ABC + * Order: A, B, C, AB, AC, BC, ABC + */ + private static class Combinations implements Iterable> { + private int n; + private int k; + private List permuteThis; + private List resultList = new ArrayList<>(); + private List unmodifyableViewOfResult = Collections.unmodifiableList(resultList); + + Combinations(List permuteThis) { + this.permuteThis = permuteThis; + this.n = permuteThis.size(); + this.k = 0; + } + + @Override + public Iterator> iterator() { + return new Iterator>() { + private BitSet nextBitSet = new BitSet(n); + + { + advanceToNextK(); + } + + @Override + public void remove() { + + } + + private void advanceToNextK() { + if (++k > n) { + nextBitSet = null; + } else { + nextBitSet.clear(); + nextBitSet.set(0, k); + } + } + + @Override + public boolean hasNext() { + return nextBitSet != null; + } + + @Override + public List next() { + BitSet resultBitSet = (BitSet) nextBitSet.clone(); + + int b = nextBitSet.previousClearBit(n - 1); + int b1 = nextBitSet.previousSetBit(b); + + if (b1 == -1) { + advanceToNextK(); + } else { + nextBitSet.clear(b1); + nextBitSet.set(b1 + 1, b1 + (n - b) + 1); + nextBitSet.clear(b1 + (n - b) + 1, n); + } + + resultList.clear(); + for (int i = 0; i < n; ++i) { + if (resultBitSet.get(i)) { + resultList.add(permuteThis.get(i)); + } + } + + return unmodifyableViewOfResult; + } + }; + } + } + + + /** + * @return A map of variable -> proper type produced by searching for α = T or T = α bounds + */ + public static Map getInstantiations(List bounds) { + Map result = new HashMap<>(); + + // The term "type" is used loosely in this chapter to include type-like syntax that contains inference + // variables. The term proper type excludes such "types" that mention inference variables. Assertions that + // involve inference variables are assertions about every proper type that can be produced by replacing each + // inference variable with a proper type. + + // Some bounds relate an inference variable to a proper type. Let T be a proper type. Given a bound of the + // form α = T or T = α, we say T is an instantiation of α. Similarly, given a bound of the form α <: T, we + // say T is a proper upper bound of α, and given a bound of the form T <: α, we say T is a proper lower bound + // of α. + for (Bound bound : bounds) { + if (bound.ruleType() == EQUALITY) { + // Note: JLS's wording is not clear, but proper type excludes arrays, nulls, primitives, etc. + if (bound.isLeftVariable() && bound.isRightProper()) { + result.put(bound.leftVariable(), bound.rightProper()); + } else if (bound.isLeftProper() && bound.isRightVariable()) { + result.put(bound.rightVariable(), bound.leftProper()); + } + } + } + + return result; + } + + /** + * @return A list of variables which have no direct instantiations + */ + public static Set getUninstantiatedVariables(List bounds) { + Set result = getMentionedVariables(bounds); + result.removeAll(getInstantiations(bounds).keySet()); + return result; + } + + public static Map> getVariableDependencies(List bounds) { + Map> dependencies = new HashMap<>(); + + for (Variable mentionedVariable : getMentionedVariables(bounds)) { + Set set = new HashSet<>(); + // An inference variable α depends on the resolution of itself. + set.add(mentionedVariable); + + dependencies.put(mentionedVariable, set); + } + + // produce initial dependencies + for (Bound bound : bounds) { + // Given a bound of one of the following forms, where T is either an inference variable β or a type that + // mentions β: + + if (bound.leftVariable() != null && bound.rightHasMentionedVariable()) { + if (bound.ruleType == EQUALITY || bound.ruleType() == SUBTYPE) { + // α = T + // α <: T + dependencies.get(bound.leftVariable()).add(bound.getRightMentionedVariable()); + } + } else if (bound.leftHasMentionedVariable() && bound.rightVariable() != null) { + if (bound.ruleType == EQUALITY || bound.ruleType() == SUBTYPE) { + // T = α + // T <: α + dependencies.get(bound.getLeftMentionedVariable()).add(bound.rightVariable()); + } + } + + // If α appears on the left-hand side of another bound of the form G<..., α, ...> = capture(G<...>), then + // β depends on the resolution of α. Otherwise, α depends on the resolution of β. TODO + + // An inference variable α appearing on the left-hand side of a bound of the form G<..., α, ...> = + // capture(G<...>) depends on the resolution of every other inference variable mentioned in this bound + // (on both sides of the = sign). TODO + } + + + // An inference variable α depends on the resolution of an inference variable β if there exists an inference + // variable γ such that α depends on the resolution of γ and γ depends on the resolution of β. + + for (int i = 0; i < dependencies.size(); ++i) { // do this n times, where n is the count of variables + boolean noMoreChanges = true; + + for (Map.Entry> entry : dependencies.entrySet()) { + // take the Variable's dependency list + for (Variable variable : entry.getValue()) { + // add those variable's dependencies + if (entry.getValue().addAll(dependencies.get(variable))) { + noMoreChanges = false; + } + } + } + + if (noMoreChanges) { + break; + } + } + + return dependencies; + } + + /** + * @return a set of variables mentioned by the bounds + */ + public static Set getMentionedVariables(List bounds) { + Set result = new HashSet<>(); + + for (Bound bound : bounds) { + bound.addVariablesToSet(result); + } + + return result; + } + /** * https://docs.oracle.com/javase/specs/jls/se8/html/jls-18.html#jls-18.3 */ diff --git a/pmd-java/src/test/java/net/sourceforge/pmd/typeresolution/ClassTypeResolverTest.java b/pmd-java/src/test/java/net/sourceforge/pmd/typeresolution/ClassTypeResolverTest.java index c805bebfd5..f26809c61f 100644 --- a/pmd-java/src/test/java/net/sourceforge/pmd/typeresolution/ClassTypeResolverTest.java +++ b/pmd-java/src/test/java/net/sourceforge/pmd/typeresolution/ClassTypeResolverTest.java @@ -4,7 +4,9 @@ package net.sourceforge.pmd.typeresolution; +import static junit.framework.TestCase.assertTrue; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; @@ -12,8 +14,10 @@ import java.io.IOException; import java.io.InputStream; import java.io.StringReader; import java.util.ArrayList; +import java.util.Collection; import java.util.Comparator; import java.util.List; +import java.util.Set; import java.util.StringTokenizer; import org.apache.commons.io.IOUtils; @@ -86,6 +90,7 @@ import net.sourceforge.pmd.typeresolution.testdata.SuperExpression; import net.sourceforge.pmd.typeresolution.testdata.ThisExpression; import net.sourceforge.pmd.typeresolution.testdata.dummytypes.Converter; import net.sourceforge.pmd.typeresolution.testdata.dummytypes.GenericClass; +import net.sourceforge.pmd.typeresolution.testdata.dummytypes.JavaTypeDefinitionEquals; import net.sourceforge.pmd.typeresolution.testdata.dummytypes.StaticMembers; import net.sourceforge.pmd.typeresolution.testdata.dummytypes.SuperClassA; import net.sourceforge.pmd.typeresolution.testdata.dummytypes.SuperClassA2; @@ -1472,6 +1477,61 @@ public class ClassTypeResolverTest { assertEquals("All expressions not tested", index, expressions.size()); } + @Test + public void testJavaTypeDefinitionEquals() { + JavaTypeDefinition a = JavaTypeDefinition.forClass(Integer.class); + JavaTypeDefinition b = JavaTypeDefinition.forClass(Integer.class); + + // test non-generic types + assertEquals(a, b); + assertNotEquals(a, null); + + // test generic arg equality + b = JavaTypeDefinition.forClass(List.class, a); + a = JavaTypeDefinition.forClass(List.class, a); + + assertEquals(a, b); + a = JavaTypeDefinition.forClass(List.class, JavaTypeDefinition.forClass(String.class)); + assertNotEquals(a, b); + assertNotEquals(b, a); + + + // test raw vs proper, proper vs raw + a = JavaTypeDefinition.forClass(JavaTypeDefinitionEquals.class); + b = JavaTypeDefinition.forClass(JavaTypeDefinitionEquals.class, + JavaTypeDefinition.forClass(List.class, a)); + assertEquals(a, b); + assertEquals(b, a); + } + + @Test + public void testJavaTypeDefinitionGetSuperTypeSet() { + JavaTypeDefinition originalTypeDef = JavaTypeDefinition.forClass(List.class, + JavaTypeDefinition.forClass(Integer.class)); + Set set = originalTypeDef.getSuperTypeSet(); + + assertEquals(set.size(), 4); + assertTrue(set.contains(JavaTypeDefinition.forClass(Object.class))); + assertTrue(set.contains(originalTypeDef)); + assertTrue(set.contains(JavaTypeDefinition.forClass(Collection.class, + JavaTypeDefinition.forClass(Integer.class)))); + assertTrue(set.contains(JavaTypeDefinition.forClass(Iterable.class, + JavaTypeDefinition.forClass(Integer.class)))); + } + + @Test + public void testJavaTypeDefinitionGetErasedSuperTypeSet() { + JavaTypeDefinition originalTypeDef = JavaTypeDefinition.forClass(List.class, + JavaTypeDefinition.forClass(Integer.class)); + Set> set = originalTypeDef.getErasedSuperTypeSet(); + assertEquals(set.size(), 4); + assertTrue(set.contains(Object.class)); + assertTrue(set.contains(Collection.class)); + assertTrue(set.contains(Iterable.class)); + assertTrue(set.contains(List.class)); + } + + private Class getChildType(Node node, int childIndex) { return ((TypeNode) node.jjtGetChild(childIndex)).getType(); diff --git a/pmd-java/src/test/java/net/sourceforge/pmd/typeresolution/TypeInferenceTest.java b/pmd-java/src/test/java/net/sourceforge/pmd/typeresolution/TypeInferenceTest.java index ed31b98676..bf6f019498 100644 --- a/pmd-java/src/test/java/net/sourceforge/pmd/typeresolution/TypeInferenceTest.java +++ b/pmd-java/src/test/java/net/sourceforge/pmd/typeresolution/TypeInferenceTest.java @@ -12,8 +12,10 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Set; import org.junit.Test; @@ -25,6 +27,11 @@ import net.sourceforge.pmd.lang.java.typeresolution.typeinference.InferenceRuleT import net.sourceforge.pmd.lang.java.typeresolution.typeinference.TypeInferenceResolver; import net.sourceforge.pmd.lang.java.typeresolution.typeinference.Variable; +import net.sourceforge.pmd.typeresolution.testdata.dummytypes.SuperClassA; +import net.sourceforge.pmd.typeresolution.testdata.dummytypes.SuperClassA2; +import net.sourceforge.pmd.typeresolution.testdata.dummytypes.SuperClassAOther; +import net.sourceforge.pmd.typeresolution.testdata.dummytypes.SuperClassAOther2; + public class TypeInferenceTest { private JavaTypeDefinition number = JavaTypeDefinition.forClass(Number.class); private JavaTypeDefinition integer = JavaTypeDefinition.forClass(Integer.class); @@ -278,7 +285,7 @@ public class TypeInferenceTest { @Test public void testIncorporationSubtypeAndSubtype() { List result; - + // ### Original rule 4. : S <: α and α <: T imply ‹S <: T› result = incorporationResult(new Bound(s, alpha, EQUALITY), new Bound(alpha, t, SUBTYPE)); assertEquals(result.size(), 1); @@ -291,6 +298,49 @@ public class TypeInferenceTest { } + @Test + public void testErasedCandidateSet() { + List types = new ArrayList<>(); + types.add(JavaTypeDefinition.forClass(List.class)); + types.add(JavaTypeDefinition.forClass(Set.class)); + + Set> erasedCandidate = TypeInferenceResolver.getErasedCandidateSet(types); + + assertEquals(erasedCandidate.size(), 3); + assertTrue(erasedCandidate.contains(Object.class)); + assertTrue(erasedCandidate.contains(Collection.class)); + assertTrue(erasedCandidate.contains(Iterable.class)); + } + + @Test + public void testMinimalErasedCandidateSet() { + Set> minimalSet = TypeInferenceResolver.getMinimalErasedCandidateSet( + JavaTypeDefinition.forClass(List.class).getErasedSuperTypeSet()); + + assertEquals(minimalSet.size(), 1); + assertTrue(minimalSet.contains(List.class)); + } + + @Test + public void testLeastUpperBound() { + List lowerBounds = new ArrayList<>(); + lowerBounds.add(JavaTypeDefinition.forClass(SuperClassA.class)); + lowerBounds.add(JavaTypeDefinition.forClass(SuperClassAOther.class)); + lowerBounds.add(JavaTypeDefinition.forClass(SuperClassAOther2.class)); + + assertEquals(TypeInferenceResolver.lub(lowerBounds), JavaTypeDefinition.forClass(SuperClassA2.class)); + } + + @Test + public void testResolution() { + List bounds = new ArrayList<>(); + bounds.add(new Bound(JavaTypeDefinition.forClass(SuperClassA.class), alpha, SUBTYPE)); + bounds.add(new Bound(JavaTypeDefinition.forClass(SuperClassAOther.class), alpha, SUBTYPE)); + Map result = TypeInferenceResolver.resolveVariables(bounds); + assertEquals(result.size(), 1); + assertEquals(result.get(alpha), JavaTypeDefinition.forClass(SuperClassA2.class)); + } + private List incorporationResult(Bound firstBound, Bound secondBound) { List current = new ArrayList<>(); List newBounds = new ArrayList<>(); diff --git a/pmd-java/src/test/java/net/sourceforge/pmd/typeresolution/testdata/dummytypes/JavaTypeDefinitionEquals.java b/pmd-java/src/test/java/net/sourceforge/pmd/typeresolution/testdata/dummytypes/JavaTypeDefinitionEquals.java new file mode 100644 index 0000000000..752fe513e6 --- /dev/null +++ b/pmd-java/src/test/java/net/sourceforge/pmd/typeresolution/testdata/dummytypes/JavaTypeDefinitionEquals.java @@ -0,0 +1,11 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + + +package net.sourceforge.pmd.typeresolution.testdata.dummytypes; + +import java.util.List; + +public class JavaTypeDefinitionEquals> { +} diff --git a/pmd-java/src/test/java/net/sourceforge/pmd/typeresolution/testdata/dummytypes/SuperClassAOther.java b/pmd-java/src/test/java/net/sourceforge/pmd/typeresolution/testdata/dummytypes/SuperClassAOther.java new file mode 100644 index 0000000000..625c09dcb9 --- /dev/null +++ b/pmd-java/src/test/java/net/sourceforge/pmd/typeresolution/testdata/dummytypes/SuperClassAOther.java @@ -0,0 +1,9 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + + +package net.sourceforge.pmd.typeresolution.testdata.dummytypes; + +public class SuperClassAOther extends SuperClassA2 { +} diff --git a/pmd-java/src/test/java/net/sourceforge/pmd/typeresolution/testdata/dummytypes/SuperClassAOther2.java b/pmd-java/src/test/java/net/sourceforge/pmd/typeresolution/testdata/dummytypes/SuperClassAOther2.java new file mode 100644 index 0000000000..6dac1db7da --- /dev/null +++ b/pmd-java/src/test/java/net/sourceforge/pmd/typeresolution/testdata/dummytypes/SuperClassAOther2.java @@ -0,0 +1,9 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + + +package net.sourceforge.pmd.typeresolution.testdata.dummytypes; + +public class SuperClassAOther2 extends SuperClassA2 { +} diff --git a/src/site/markdown/overview/changelog.md b/src/site/markdown/overview/changelog.md index 394da3cc92..d301c00833 100644 --- a/src/site/markdown/overview/changelog.md +++ b/src/site/markdown/overview/changelog.md @@ -166,3 +166,5 @@ All existing rules have been updated to reflect these changes. If you have custo * [#542](https://github.com/pmd/pmd/pull/542): \[java] Metrics abstraction - [Clément Fournier](https://github.com/oowekyala) * [#545](https://github.com/pmd/pmd/pull/545): \[apex] Apex metrics framework - [Clément Fournier](https://github.com/oowekyala) * [#548](https://github.com/pmd/pmd/pull/548): \[java] Metrics documentation - [Clément Fournier](https://github.com/oowekyala) +* [#550](https://github.com/pmd/pmd/pull/550): \[java] Add basic resolution to type inference - [Bendegúz Nagy](https://github.com/WinterGrascph) +