diff --git a/sootup.core/src/main/java/sootup/core/typehierarchy/TypeHierarchy.java b/sootup.core/src/main/java/sootup/core/typehierarchy/TypeHierarchy.java index 4dfa5a53a4b..a0c0dcfb432 100644 --- a/sootup.core/src/main/java/sootup/core/typehierarchy/TypeHierarchy.java +++ b/sootup.core/src/main/java/sootup/core/typehierarchy/TypeHierarchy.java @@ -22,6 +22,7 @@ */ import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.stream.Stream; @@ -203,4 +204,6 @@ default Stream superClassesOf(@Nonnull ClassType classType) { // checks if a Type is contained int the TypeHierarchy - should return the equivalent to // View.getClass(...).isPresent() boolean contains(ClassType type); + + Collection getLowestCommonAncestors(ClassType a, ClassType b); } diff --git a/sootup.core/src/main/java/sootup/core/typehierarchy/ViewTypeHierarchy.java b/sootup.core/src/main/java/sootup/core/typehierarchy/ViewTypeHierarchy.java index 6f7392529be..9afd437dec9 100644 --- a/sootup.core/src/main/java/sootup/core/typehierarchy/ViewTypeHierarchy.java +++ b/sootup.core/src/main/java/sootup/core/typehierarchy/ViewTypeHierarchy.java @@ -27,13 +27,16 @@ import java.util.stream.Stream; import java.util.stream.StreamSupport; import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.apache.commons.lang3.tuple.ImmutablePair; import org.jgrapht.Graph; import org.jgrapht.graph.SimpleDirectedGraph; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import sootup.core.model.SootClass; import sootup.core.typehierarchy.ViewTypeHierarchy.ScanResult.Edge; import sootup.core.typehierarchy.ViewTypeHierarchy.ScanResult.EdgeType; import sootup.core.typehierarchy.ViewTypeHierarchy.ScanResult.Vertex; -import sootup.core.typehierarchy.ViewTypeHierarchy.ScanResult.VertexType; import sootup.core.types.ClassType; import sootup.core.views.View; @@ -45,8 +48,11 @@ */ public class ViewTypeHierarchy implements MutableTypeHierarchy { + private static final Logger logger = LoggerFactory.getLogger(ViewTypeHierarchy.class); + private final Supplier lazyScanResult; private final ClassType objectClassType; + private final Map> lcaCache = new HashMap<>(); /** to allow caching use Typehierarchy.fromView() to get/create the Typehierarchy. */ public ViewTypeHierarchy(@Nonnull View view) { @@ -61,7 +67,7 @@ public Stream implementersOf(@Nonnull ClassType interfaceType) { if (vertex == null) { throw new IllegalArgumentException("Could not find '" + interfaceType + "' in hierarchy."); } - if (vertex.type != VertexType.Interface) { + if (vertex instanceof ScanResult.ClassVertex) { throw new IllegalArgumentException("'" + interfaceType + "' is not an interface."); } return subtypesOf(interfaceType); @@ -74,7 +80,7 @@ public Stream subclassesOf(@Nonnull ClassType classType) { if (vertex == null) { throw new IllegalArgumentException("Could not find '" + classType + "' in hierarchy."); } - if (vertex.type != VertexType.Class) { + if (vertex instanceof ScanResult.InterfaceVertex) { throw new IllegalArgumentException("'" + classType + "' is not a class."); } return subtypesOf(classType); @@ -102,31 +108,8 @@ public Stream directSubtypesOf(@Nonnull ClassType type) { throw new IllegalArgumentException("Could not find '" + type + "' in hierarchy."); } - Set subclasses = new HashSet<>(); - Graph graph = scanResult.graph; - - switch (vertex.type) { - case Interface: - graph.incomingEdgesOf(vertex).stream() - .filter( - edge -> - edge.type == EdgeType.ClassDirectlyImplements - || edge.type == EdgeType.InterfaceDirectlyExtends) - .map(graph::getEdgeSource) - .forEach(directSubclass -> subclasses.add(directSubclass.javaClassType)); - break; - case Class: - graph.incomingEdgesOf(vertex).stream() - .filter(edge -> edge.type == EdgeType.ClassDirectlyExtends) - .map(graph::getEdgeSource) - .forEach(directSubclass -> subclasses.add(directSubclass.javaClassType)); - break; - default: - throw new AssertionError("Unknown vertex type!"); - } - - return subclasses.stream(); + return vertex.directSubTypesOf(graph, vertex); } @Nonnull @@ -169,7 +152,7 @@ public Stream directlyImplementedInterfacesOf(@Nonnull ClassType clas if (vertex == null) { throw new IllegalArgumentException("Could not find '" + classType + "' in hierarchy."); } - if (vertex.type != VertexType.Class) { + if (vertex instanceof ScanResult.InterfaceVertex) { throw new IllegalArgumentException(classType + " is not a class."); } return directImplementedInterfacesOf(vertex).map(v -> v.javaClassType); @@ -182,7 +165,7 @@ public Stream directlyExtendedInterfacesOf(@Nonnull ClassType interfa if (vertex == null) { throw new IllegalArgumentException("Could not find " + interfaceType + " in hierarchy."); } - if (vertex.type != VertexType.Interface) { + if (vertex instanceof ScanResult.ClassVertex) { throw new IllegalArgumentException(interfaceType + " is not an interface."); } return directExtendedInterfacesOf(vertex).map(v -> v.javaClassType); @@ -193,6 +176,69 @@ public boolean contains(ClassType type) { return lazyScanResult.get().typeToVertex.get(type) != null; } + protected Set findAncestors(ClassType type) { + Graph graph = lazyScanResult.get().graph; + Vertex vertex = lazyScanResult.get().typeToVertex.get(type); + if (vertex == null) { + logger.warn("Could not find {} in this hierarchy!", type.toString()); + return Collections.emptySet(); + } + Set ancestors = new HashSet<>(); + for (Edge edge : graph.outgoingEdgesOf(vertex)) { + Vertex parent = graph.getEdgeTarget(edge); + ancestors.add(parent); + ancestors.addAll(findAncestors(parent.javaClassType)); + } + return ancestors; + } + + /** + * This algorithm is implementation of the algorithm + * https://www.baeldung.com/cs/lowest-common-ancestor-acyclic-graph + */ + @Override + public Collection getLowestCommonAncestors(ClassType a, ClassType b) { + // search in cache + SymmetricKey pair = new SymmetricKey(a, b); + Set lcas = lcaCache.get(pair); + if (lcas != null) { + return lcas; + } + + Graph graph = lazyScanResult.get().graph; + Set ancestorsOfA = findAncestors(a); + Set ancestorsOfB = findAncestors(b); + lcas = new HashSet<>(); + + if (ancestorsOfA.isEmpty() || ancestorsOfB.isEmpty()) { + lcas.add(objectClassType); + lcaCache.put(pair, lcas); + return lcas; + } + // ancestorsOfA contains now common ancestors of a and b + ancestorsOfA.retainAll(ancestorsOfB); + boolean notLca = false; + for (Vertex ca : ancestorsOfA) { + Set incomingEdges = graph.incomingEdgesOf(ca); + for (Edge ie : incomingEdges) { + if (ancestorsOfA.contains(graph.getEdgeSource(ie))) { + notLca = true; + break; + } + } + if (notLca) { + notLca = false; + } else { + lcas.add(ca.javaClassType); + } + } + if (lcas.isEmpty()) { + lcas = Collections.singleton(objectClassType); + } + lcaCache.put(pair, lcas); + return lcas; + } + @Nonnull @Override public Stream implementedInterfacesOf(@Nonnull ClassType type) { @@ -203,21 +249,18 @@ public Stream implementedInterfacesOf(@Nonnull ClassType type) { throw new IllegalArgumentException("Could not find " + type + " in this hierarchy."); } - switch (vertex.type) { - case Class: - // We ascend from vertex through its superclasses to java.lang.Object. - // For each superclass, we take the interfaces it implements and merge - // them together in a Set. - return superClassesOf(vertex, false) - .flatMap(this::directImplementedInterfacesOf) - .flatMap(this::selfAndImplementedInterfaces) - .distinct(); - case Interface: - return directExtendedInterfacesOf(vertex) - .flatMap(this::selfAndImplementedInterfaces) - .distinct(); - default: - throw new AssertionError("Unexpected vertex type!"); + if (vertex instanceof ScanResult.ClassVertex) { + // We ascend from vertex through its superclasses to java.lang.Object. + // For each superclass, we take the interfaces it implements and merge + // them together in a Set. + return superClassesOf(vertex, false) + .flatMap(this::directImplementedInterfacesOf) + .flatMap(this::selfAndImplementedInterfaces) + .distinct(); + } else { + return directExtendedInterfacesOf(vertex) + .flatMap(this::selfAndImplementedInterfaces) + .distinct(); } } @@ -226,7 +269,7 @@ public Stream implementedInterfacesOf(@Nonnull ClassType type) { * interfaces. */ @Nonnull - private Stream selfAndImplementedInterfaces(Vertex vertex) { + protected Stream selfAndImplementedInterfaces(Vertex vertex) { ScanResult scanResult = lazyScanResult.get(); Graph graph = scanResult.graph; @@ -257,7 +300,7 @@ public Optional superClassOf(@Nonnull ClassType classType) { if (superclassOpt.isPresent()) { return superclassOpt; } else { - if (classVertex.type == VertexType.Interface) { + if (classVertex instanceof ScanResult.InterfaceVertex) { return Optional.of(objectClassType); } return Optional.empty(); @@ -270,7 +313,7 @@ public boolean isInterface(@Nonnull ClassType type) { if (vertex == null) { throw new IllegalArgumentException("Could not find '" + type + "' in hierarchy."); } - return vertex.type == VertexType.Interface; + return vertex instanceof ScanResult.InterfaceVertex; } public boolean isClass(@Nonnull ClassType type) { @@ -278,7 +321,7 @@ public boolean isClass(@Nonnull ClassType type) { if (vertex == null) { throw new IllegalArgumentException("Could not find '" + type + "' in hierarchy."); } - return vertex.type == VertexType.Class; + return vertex instanceof ScanResult.ClassVertex; } /** @@ -289,26 +332,23 @@ public boolean isClass(@Nonnull ClassType type) { private Stream visitSubgraph( Graph graph, Vertex vertex, boolean includeSelf) { Stream subgraph = includeSelf ? Stream.of(vertex.javaClassType) : Stream.empty(); - switch (vertex.type) { - case Interface: - return Stream.concat( - subgraph, - graph.incomingEdgesOf(vertex).stream() - .filter( - edge -> - edge.type == EdgeType.ClassDirectlyImplements - || edge.type == EdgeType.InterfaceDirectlyExtends) - .map(graph::getEdgeSource) - .flatMap(directSubtype -> visitSubgraph(graph, directSubtype, true))); - case Class: - return Stream.concat( - subgraph, - graph.incomingEdgesOf(vertex).stream() - .filter(edge -> edge.type == EdgeType.ClassDirectlyExtends) - .map(graph::getEdgeSource) - .flatMap(directSubclass -> visitSubgraph(graph, directSubclass, true))); - default: - throw new AssertionError("Unknown vertex type!"); + if (vertex instanceof ScanResult.InterfaceVertex) { + return Stream.concat( + subgraph, + graph.incomingEdgesOf(vertex).stream() + .filter( + edge -> + edge.type == EdgeType.ClassDirectlyImplements + || edge.type == EdgeType.InterfaceDirectlyExtends) + .map(graph::getEdgeSource) + .flatMap(directSubtype -> visitSubgraph(graph, directSubtype, true))); + } else { + return Stream.concat( + subgraph, + graph.incomingEdgesOf(vertex).stream() + .filter(edge -> edge.type == EdgeType.ClassDirectlyExtends) + .map(graph::getEdgeSource) + .flatMap(directSubclass -> visitSubgraph(graph, directSubclass, true))); } } @@ -323,7 +363,7 @@ private Stream visitSubgraph( * *

In the graph structure, a type is only connected to its direct subtypes. */ - private ScanResult scanView(View view) { + private ScanResult scanView(@Nonnull View view) { Map typeToVertex = new HashMap<>(); Graph graph = new SimpleDirectedGraph<>(null, null, false); @@ -368,14 +408,14 @@ private static void addSootClassToGraph( @Nonnull private static Vertex createAndAddClassVertex(Graph graph, ClassType type) { - Vertex classVertex = new Vertex(type, VertexType.Class); + Vertex classVertex = new ScanResult.ClassVertex(type); graph.addVertex(classVertex); return classVertex; } @Nonnull private static Vertex createAndAddInterfaceVertex(Graph graph, ClassType type) { - Vertex interfaceVertex = new Vertex(type, VertexType.Interface); + Vertex interfaceVertex = new ScanResult.InterfaceVertex(type); graph.addVertex(interfaceVertex); return interfaceVertex; } @@ -389,23 +429,46 @@ public void addType(@Nonnull SootClass sootClass) { /** Holds a vertex for each {@link ClassType} encountered during the scan. */ protected static class ScanResult { - enum VertexType { - Class, - Interface - } - - /** - * @see #javaClassType - * @see #type - */ - protected static class Vertex { + /** @see #javaClassType */ + protected abstract static class Vertex { @Nonnull final ClassType javaClassType; - @Nonnull final VertexType type; - int depth = -1; - Vertex(@Nonnull ClassType javaClassType, @Nonnull VertexType type) { + private Vertex(@Nonnull ClassType javaClassType) { this.javaClassType = javaClassType; - this.type = type; + } + + public abstract Stream directSubTypesOf(Graph graph, Vertex vertex); + } + + private static class InterfaceVertex extends Vertex { + public InterfaceVertex(ClassType javaClassType) { + super(javaClassType); + } + + public Stream directSubTypesOf(Graph graph, Vertex vertex) { + return graph.incomingEdgesOf(vertex).stream() + .filter( + edge -> + edge.type == EdgeType.ClassDirectlyImplements + || edge.type == EdgeType.InterfaceDirectlyExtends) + .map(graph::getEdgeSource) + .map(directSubclass -> directSubclass.javaClassType) + .distinct(); + } + } + + private static class ClassVertex extends Vertex { + public ClassVertex(ClassType javaClassType) { + super(javaClassType); + } + + @Override + public Stream directSubTypesOf(Graph graph, Vertex vertex) { + return graph.incomingEdgesOf(vertex).stream() + .filter(edge -> edge.type == EdgeType.ClassDirectlyExtends) + .map(graph::getEdgeSource) + .map(directSubclass -> directSubclass.javaClassType) + .distinct(); } } @@ -440,24 +503,36 @@ private ScanResult( } private class SuperClassVertexIterator implements Iterator { - @Nonnull private final Graph graph; - @Nonnull private Optional classVertexItBase; + @Nullable private Vertex classVertexItBase; - public SuperClassVertexIterator(Vertex classVertex) { - graph = lazyScanResult.get().graph; - classVertexItBase = Optional.of(classVertex); + public SuperClassVertexIterator(@Nonnull Vertex classVertex) { + classVertexItBase = classVertex; } @Override public boolean hasNext() { - return classVertexItBase.isPresent(); + return classVertexItBase != null; } @Override public Vertex next() { - Optional currentSuperClass = classVertexItBase; - classVertexItBase = directSuperClassOf(classVertexItBase.get()).findAny(); - return currentSuperClass.get(); + if (classVertexItBase == null) { + throw new NoSuchElementException("Iterator is already iterated."); + } + Vertex currentSuperClass = classVertexItBase; + classVertexItBase = directSuperClassOf(classVertexItBase).findAny().orElse(null); + return currentSuperClass; + } + } + + static class SymmetricKey extends ImmutablePair { + public SymmetricKey(ClassType left, ClassType right) { + super(left, right); + } + + @Override + public int hashCode() { + return Objects.hash(getKey()) + Objects.hash(getValue()); } } } diff --git a/sootup.interceptors/src/main/java/sootup/interceptors/typeresolving/BytecodeHierarchy.java b/sootup.interceptors/src/main/java/sootup/interceptors/typeresolving/BytecodeHierarchy.java index 15588c4650d..16d29900fb9 100644 --- a/sootup.interceptors/src/main/java/sootup/interceptors/typeresolving/BytecodeHierarchy.java +++ b/sootup.interceptors/src/main/java/sootup/interceptors/typeresolving/BytecodeHierarchy.java @@ -22,9 +22,7 @@ */ import java.util.*; -import java.util.stream.Stream; import javax.annotation.Nonnull; -import javax.annotation.Nullable; import sootup.core.IdentifierFactory; import sootup.core.typehierarchy.TypeHierarchy; import sootup.core.types.*; @@ -34,6 +32,7 @@ /** @author Zun Wang */ public class BytecodeHierarchy { + private final TypeHierarchy typeHierarchy; public final ClassType objectClassType; public final ClassType throwableClassType; @@ -115,7 +114,7 @@ public boolean isAncestor(@Nonnull Type ancestor, @Nonnull Type child) { return false; } - public Collection getLeastCommonAncestor(Type a, Type b) { + public Collection getLeastCommonAncestors(Type a, Type b) { Set ret = new HashSet<>(); if (a instanceof TopType || b instanceof TopType) { return Collections.singleton(TopType.getInstance()); @@ -152,7 +151,7 @@ public Collection getLeastCommonAncestor(Type a, Type b) { if (et_a instanceof PrimitiveType || et_b instanceof PrimitiveType) { temp = Collections.emptySet(); } else { - temp = getLeastCommonAncestor(et_a, et_b); + temp = getLeastCommonAncestors(et_a, et_b); } if (temp.isEmpty()) { ret.add(objectClassType); @@ -177,43 +176,7 @@ public Collection getLeastCommonAncestor(Type a, Type b) { ret.add(objectClassType); } } else { - // if a and b are both ClassType - Set pathsA = buildAncestryPaths((ClassType) a); - Set pathsB = buildAncestryPaths((ClassType) b); - // TODO: [ms] implement an algorithm with better wc runtime costs.. e.g. - // https://www.baeldung.com/cs/tree-lowest-common-ancestor / - for (AncestryPath pathA : pathsA) { - for (AncestryPath pathB : pathsB) { - ClassType lcn = null; - while (pathA != null && pathB != null && pathA.type == pathB.type) { - lcn = pathA.type; - pathA = pathA.next; - pathB = pathB.next; - } - if (lcn == null) { - continue; - } - - boolean isLcn = true; - Iterator it = ret.iterator(); - while (it.hasNext()) { - Type l = it.next(); - if (isAncestor(lcn, l)) { - isLcn = false; - break; - } - if (isAncestor(l, lcn)) { - it.remove(); - } - } - if (isLcn) { - ret.add(lcn); - } - } - } - if (ret.isEmpty()) { - ret.add(objectClassType); - } + ret.addAll(typeHierarchy.getLowestCommonAncestors((ClassType) a, (ClassType) b)); } return ret; } @@ -223,59 +186,4 @@ private boolean canStoreType(ClassType ancestor, ClassType child) { || (typeHierarchy.contains(ancestor) && typeHierarchy.subtypesOf(ancestor).anyMatch(t -> t == child)); } - - private Set buildAncestryPaths(ClassType type) { - Deque pathNodeQ = new ArrayDeque<>(); - pathNodeQ.add(new AncestryPath(type, null)); - Set paths = new HashSet<>(); - while (!pathNodeQ.isEmpty()) { - AncestryPath node = pathNodeQ.removeFirst(); - if (!typeHierarchy.contains(node.type)) { - break; - } - if (node.type == objectClassType) { - paths.add(node); - } else { - if (typeHierarchy.isInterface(node.type)) { - Stream superInterfaces = typeHierarchy.directlyExtendedInterfacesOf(node.type); - Optional any = - superInterfaces - .peek( - superInterface -> { - AncestryPath superNode = new AncestryPath(superInterface, node); - pathNodeQ.add(superNode); - }) - .findAny(); - if (!any.isPresent()) { - paths.add(node); - } - } else { - - typeHierarchy - .directlyImplementedInterfacesOf(node.type) - .forEach( - superInterface -> { - pathNodeQ.add(new AncestryPath(superInterface, node)); - }); - - Optional superClass = typeHierarchy.superClassOf(node.type); - if (superClass.isPresent()) { - pathNodeQ.add(new AncestryPath(superClass.get(), node)); - } - } - } - } - return paths; - } - - // TODO: [ms] thats a linked list.. please refactor that - private static class AncestryPath { - public AncestryPath next; - public ClassType type; - - public AncestryPath(@Nonnull ClassType type, @Nullable AncestryPath next) { - this.type = type; - this.next = next; - } - } } diff --git a/sootup.interceptors/src/main/java/sootup/interceptors/typeresolving/TypePromotionVisitor.java b/sootup.interceptors/src/main/java/sootup/interceptors/typeresolving/TypePromotionVisitor.java index 069b1d5b4f1..c2593bd9b5f 100644 --- a/sootup.interceptors/src/main/java/sootup/interceptors/typeresolving/TypePromotionVisitor.java +++ b/sootup.interceptors/src/main/java/sootup/interceptors/typeresolving/TypePromotionVisitor.java @@ -76,7 +76,7 @@ public void visit(@Nonnull Value value, @Nonnull Type stdType, @Nonnull Stmt stm assert value instanceof Local; // The type of the local and the type that is required in the statement are incompatible, // so the type of the local needs to be upgraded to a common ancestor. - Collection lca = hierarchy.getLeastCommonAncestor(evaType, stdType); + Collection lca = hierarchy.getLeastCommonAncestors(evaType, stdType); assert !lca.isEmpty(); // Only use the first of the common ancestors, because this is an edge case. // The proper way to do this would be to create a completely new visitor that can yield diff --git a/sootup.interceptors/src/main/java/sootup/interceptors/typeresolving/TypeResolver.java b/sootup.interceptors/src/main/java/sootup/interceptors/typeresolving/TypeResolver.java index 274ca9ab8f2..d5957c4107d 100644 --- a/sootup.interceptors/src/main/java/sootup/interceptors/typeresolving/TypeResolver.java +++ b/sootup.interceptors/src/main/java/sootup/interceptors/typeresolving/TypeResolver.java @@ -244,7 +244,7 @@ private Collection applyAssignmentConstraint( // when `local` has an array type, the type of `rhs` needs to be assignable as an element // of that array Collection leastCommonAncestorsElement = - hierarchy.getLeastCommonAncestor(elementType, rhsType); + hierarchy.getLeastCommonAncestors(elementType, rhsType); leastCommonAncestors = leastCommonAncestorsElement.stream() .map(type -> Type.createArrayType(type, 1)) @@ -253,10 +253,10 @@ private Collection applyAssignmentConstraint( // when `local` isn't an array type, but is used as an array, its type has to be // compatible with `[rhs][]` leastCommonAncestors = - hierarchy.getLeastCommonAncestor(oldType, Type.createArrayType(rhsType, 1)); + hierarchy.getLeastCommonAncestors(oldType, Type.createArrayType(rhsType, 1)); } } else { - leastCommonAncestors = hierarchy.getLeastCommonAncestor(oldType, rhsType); + leastCommonAncestors = hierarchy.getLeastCommonAncestors(oldType, rhsType); } assert !leastCommonAncestors.isEmpty(); diff --git a/sootup.java.bytecode.frontend/src/test/java/sootup/java/bytecode/frontend/interceptors/typeresolving/BytecodeHierarchyTest.java b/sootup.java.bytecode.frontend/src/test/java/sootup/java/bytecode/frontend/interceptors/typeresolving/BytecodeHierarchyTest.java index 05f87dfe237..3e01056eccd 100644 --- a/sootup.java.bytecode.frontend/src/test/java/sootup/java/bytecode/frontend/interceptors/typeresolving/BytecodeHierarchyTest.java +++ b/sootup.java.bytecode.frontend/src/test/java/sootup/java/bytecode/frontend/interceptors/typeresolving/BytecodeHierarchyTest.java @@ -135,55 +135,57 @@ public void testLCA() { Collection actualSet; Collection expectedSet; - actualSet = hierarchy.getLeastCommonAncestor(double_class1, int_class); + actualSet = hierarchy.getLeastCommonAncestors(double_class1, int_class); expectedSet = ImmutableUtils.immutableSet(number, comparable); assertEquals(expectedSet, actualSet); - actualSet = hierarchy.getLeastCommonAncestor(rootInterface1, class1); + actualSet = hierarchy.getLeastCommonAncestors(rootInterface1, class1); expectedSet = Collections.singleton(rootInterface1); assertEquals(expectedSet, actualSet); - actualSet = hierarchy.getLeastCommonAncestor(PrimitiveType.getDouble(), PrimitiveType.getInt()); + actualSet = + hierarchy.getLeastCommonAncestors(PrimitiveType.getDouble(), PrimitiveType.getInt()); expectedSet = Collections.singleton(TopType.getInstance()); assertEquals(expectedSet, actualSet); - actualSet = hierarchy.getLeastCommonAncestor(rootInterface1, rootInterface2); + actualSet = hierarchy.getLeastCommonAncestors(rootInterface1, rootInterface2); expectedSet = Collections.singleton(object); assertEquals(expectedSet, actualSet); - actualSet = hierarchy.getLeastCommonAncestor(intArr_prim, doubleArr_prim); + actualSet = hierarchy.getLeastCommonAncestors(intArr_prim, doubleArr_prim); expectedSet = ImmutableUtils.immutableSet(object, serializable, cloneable); assertEquals(expectedSet, actualSet); - actualSet = hierarchy.getLeastCommonAncestor(class1AArr, class3Arr); + actualSet = hierarchy.getLeastCommonAncestors(class1AArr, class3Arr); expectedSet = ImmutableUtils.immutableSet(objArr); assertEquals(expectedSet, actualSet); - actualSet = hierarchy.getLeastCommonAncestor(class1AArr, class2AArr); + actualSet = hierarchy.getLeastCommonAncestors(class1AArr, class2AArr); expectedSet = ImmutableUtils.immutableSet(class1AArr); assertEquals(expectedSet, actualSet); - actualSet = hierarchy.getLeastCommonAncestor(class3Arr, class4Arr); + actualSet = hierarchy.getLeastCommonAncestors(class3Arr, class4Arr); expectedSet = Collections.singleton(class2Arr); assertEquals(expectedSet, actualSet); - actualSet = hierarchy.getLeastCommonAncestor(class4, class4Arr); + actualSet = hierarchy.getLeastCommonAncestors(class4, class4Arr); expectedSet = ImmutableUtils.immutableSet(serializable, cloneable); assertEquals(expectedSet, actualSet); - actualSet = hierarchy.getLeastCommonAncestor(class2, class4Arr); + actualSet = hierarchy.getLeastCommonAncestors(class2, class4Arr); expectedSet = Collections.singleton(object); assertEquals(expectedSet, actualSet); - actualSet = hierarchy.getLeastCommonAncestor(class2, class4Arr); + actualSet = hierarchy.getLeastCommonAncestors(class2, class4Arr); expectedSet = Collections.singleton(object); assertEquals(expectedSet, actualSet); - actualSet = hierarchy.getLeastCommonAncestor(PrimitiveType.getShort(), PrimitiveType.getByte()); + actualSet = + hierarchy.getLeastCommonAncestors(PrimitiveType.getShort(), PrimitiveType.getByte()); expectedSet = Collections.singleton(PrimitiveType.getShort()); assertEquals(expectedSet, actualSet); - actualSet = hierarchy.getLeastCommonAncestor(shortArr, byteArr); + actualSet = hierarchy.getLeastCommonAncestors(shortArr, byteArr); expectedSet = ImmutableUtils.immutableSet(object, serializable, cloneable); assertEquals(expectedSet, actualSet); }