Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 23 additions & 16 deletions usvm-ts/src/main/kotlin/org/usvm/util/EtsHierarchy.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,22 +35,7 @@ class EtsHierarchy(private val scene: EtsScene) {

val time = measureTimeMillis {
result = scene.projectAndSdkClasses.associateWith { start ->
val visited = mutableSetOf(start)
generateSequence(listOf(start)) { classes ->
if (classes.isEmpty()) return@generateSequence null

val result = classes.flatMap { current ->
val superClasses = current.superClass?.let { resolveClassesBySignature(it) }.orEmpty()
val interfaces = current.implementedInterfaces

val resolvedInterfaces = interfaces.flatMap { resolveClassesBySignature(it) }

superClasses.toMutableSet() + resolvedInterfaces // TODO optimize
}

val newAncestors = result.filter { visited.add(it) }
newAncestors.takeIf { it.isNotEmpty() }
}.flatten().toMutableSet() + scene.sdkClasses.filter { it.name == "Object" }
collectAncestors(start) + scene.sdkClasses.filter { it.name == "Object" }
}
}

Expand All @@ -61,6 +46,28 @@ class EtsHierarchy(private val scene: EtsScene) {
return@lazy result
}

private fun collectAncestors(start: EtsClass): Set<EtsClass> {
// Virtual dispatch selects the first ancestor declaring the requested method.
val result = linkedSetOf<EtsClass>()
val pending = ArrayDeque<EtsClass>()
pending.add(start)

while (pending.isNotEmpty()) {
val current = pending.removeFirst()
val firstVisit = result.add(current)
if (!firstVisit) continue

val superClasses = current.superClass?.let { resolveClassesBySignature(it) }.orEmpty()
pending.addAll(superClasses)

current.implementedInterfaces.forEach { signature ->
pending.addAll(resolveClassesBySignature(signature))
}
}

return result
}

private fun resolveClassesBySignature(superClassSignature: EtsClassSignature): Collection<EtsClass> {
val typeName = superClassSignature.name.removeTrashFromTheName()
val signature = superClassSignature.copy(name = typeName)
Expand Down
168 changes: 168 additions & 0 deletions usvm-ts/src/test/kotlin/org/usvm/util/EtsHierarchyTest.kt
Original file line number Diff line number Diff line change
@@ -1,14 +1,154 @@
package org.usvm.util

import org.jacodb.ets.model.EtsClass
import org.jacodb.ets.model.EtsClassCategory
import org.jacodb.ets.model.EtsClassImpl
import org.jacodb.ets.model.EtsClassSignature
import org.jacodb.ets.model.EtsFile
import org.jacodb.ets.model.EtsFileSignature
import org.jacodb.ets.model.EtsScene
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Timeout
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith

@Timeout(value = 5, threadMode = Timeout.ThreadMode.SEPARATE_THREAD)
class EtsHierarchyTest {
@Test
fun `self interface reference retains superclass and SDK Object`() {
val base = clazz(name = "BaseCollection")
val treeSignature = signature(name = "BinarySearchTree")
val tree = clazz(name = "BinarySearchTree", superClass = base.signature, interfaces = listOf(treeSignature))
val sdkObject = clazz(name = "Object", fileName = "lib.es5.d.ts")
val hierarchy = hierarchy(classes = listOf(base, tree), sdkClasses = listOf(sdkObject))

val ancestors = hierarchy.getAncestors(tree)
val baseInheritors = hierarchy.getInheritors(base)

assertEquals(setOf(tree, base, sdkObject), ancestors)
assertEquals(setOf(tree, base), baseInheritors)
}

@Test
fun `mutual superclass cycle retains additional interface ancestors`() {
val root = clazz(name = "Root")
val first = clazz(name = "First", superClass = signature(name = "Second"))
val second = clazz(name = "Second", superClass = first.signature, interfaces = listOf(root.signature))
val hierarchy = hierarchy(classes = listOf(first, second, root))

val firstAncestors = hierarchy.getAncestors(first)
val secondAncestors = hierarchy.getAncestors(second)
val rootInheritors = hierarchy.getInheritors(root)

assertEquals(setOf(first, second, root), firstAncestors)
assertEquals(setOf(first, second, root), secondAncestors)
assertEquals(setOf(first, second, root), rootInheritors)
}

@Test
fun `interface cycles retain every reachable parent`() {
val root = clazz(name = "Root", category = EtsClassCategory.INTERFACE)
val first = clazz(
name = "First",
category = EtsClassCategory.INTERFACE,
interfaces = listOf(signature(name = "Second")),
)
val second = clazz(
name = "Second",
category = EtsClassCategory.INTERFACE,
interfaces = listOf(first.signature, root.signature),
)
val hierarchy = hierarchy(classes = listOf(first, second, root))

val ancestors = hierarchy.getAncestors(first)

assertEquals(setOf(first, second, root), ancestors)
}

@Test
fun `unknown file retains all same-name candidates despite a cyclic branch`() {
val root = clazz(name = "Root")
val firstSignature = signature(name = "Shared", fileName = "first.ts")
val first = clazz(name = "Shared", fileName = "first.ts", interfaces = listOf(firstSignature))
val second = clazz(name = "Shared", fileName = "second.ts", superClass = root.signature)
val leaf = clazz(
name = "Leaf",
interfaces = listOf(EtsClassSignature(name = "Shared", file = EtsFileSignature.UNKNOWN)),
)
val hierarchy = hierarchy(classes = listOf(leaf, first, second, root))

val ancestors = hierarchy.getAncestors(leaf)

assertEquals(setOf(leaf, first, second, root), ancestors)
}

@Test
fun `exact file retains only its own same-name ancestor`() {
val first = clazz(name = "Shared", fileName = "first.ts")
val second = clazz(name = "Shared", fileName = "second.ts")
val leaf = clazz(name = "Leaf", superClass = second.signature)
val hierarchy = hierarchy(classes = listOf(leaf, first, second))

val ancestors = hierarchy.getAncestors(leaf)

assertEquals(setOf(leaf, second), ancestors)
}

@Test
fun `diamond ancestors are complete and unique`() {
val root = clazz(name = "Root")
val first = clazz(name = "First", superClass = root.signature)
val second = clazz(name = "Second", superClass = root.signature)
val leaf = clazz(name = "Leaf", superClass = first.signature, interfaces = listOf(second.signature))
val hierarchy = hierarchy(classes = listOf(leaf, first, second, root))

val ancestors = hierarchy.getAncestors(leaf)

assertEquals(setOf(leaf, first, second, root), ancestors)
}

@Test
fun `ancestors retain receiver-first breadth-first order across diamonds and cycles`() {
val root = clazz(name = "Root", interfaces = listOf(signature(name = "Leaf")))
val base = clazz(name = "Base", superClass = root.signature)
val left = clazz(name = "Left", interfaces = listOf(root.signature))
val right = clazz(name = "Right", interfaces = listOf(root.signature))
val leaf = clazz(
name = "Leaf",
superClass = base.signature,
interfaces = listOf(left.signature, right.signature),
)
val sdkObject = clazz(name = "Object", fileName = "lib.es5.d.ts")
val hierarchy = hierarchy(classes = listOf(leaf, base, left, right, root), sdkClasses = listOf(sdkObject))

val ancestors = hierarchy.getAncestors(leaf).toList()

assertEquals(listOf(leaf, base, left, right, root, sdkObject), ancestors)
}

@Test
fun `missing exact signature still fails instead of choosing another file`() {
val parent = clazz(name = "Shared", fileName = "first.ts")
val leaf = clazz(name = "Leaf", superClass = signature(name = "Shared", fileName = "missing.ts"))
val hierarchy = hierarchy(classes = listOf(leaf, parent))

assertFailsWith<IllegalStateException> {
hierarchy.getAncestors(leaf)
}
}

@Test
fun `duplicate runtime declarations still fail instead of choosing one`() {
val first = clazz(name = "Duplicate")
val second = clazz(name = "Duplicate")
val leaf = clazz(name = "Leaf", superClass = first.signature)
val hierarchy = hierarchy(classes = listOf(leaf, first, second))

assertFailsWith<IllegalArgumentException> {
hierarchy.getAncestors(leaf)
}
}

@Test
fun `cyclic ancestry terminates and keeps first encounter order`() {
val fileSignature = EtsFileSignature(projectName = "test", fileName = "cycle.ts")
Expand Down Expand Up @@ -43,4 +183,32 @@ class EtsHierarchyTest {
assertEquals(listOf(a, b), aAncestors.toList())
assertEquals(listOf(b, a), bAncestors.toList())
}

private fun signature(name: String, fileName: String = "hierarchy.ts") = EtsClassSignature(
name = name,
file = EtsFileSignature(projectName = "test", fileName = fileName),
)

private fun clazz(
name: String,
fileName: String = "hierarchy.ts",
category: EtsClassCategory = EtsClassCategory.CLASS,
superClass: EtsClassSignature? = null,
interfaces: List<EtsClassSignature> = emptyList(),
): EtsClass = EtsClassImpl(
signature = signature(name = name, fileName = fileName),
fields = emptyList(),
methods = emptyList(),
category = category,
superClass = superClass,
implementedInterfaces = interfaces,
)

private fun hierarchy(classes: List<EtsClass>, sdkClasses: List<EtsClass> = emptyList()): EtsHierarchy {
fun files(classes: List<EtsClass>) = classes.groupBy { it.signature.file }.map { (signature, declarations) ->
EtsFile(signature = signature, classes = declarations, namespaces = emptyList())
}

return EtsHierarchy(EtsScene(projectFiles = files(classes), sdkFiles = files(sdkClasses)))
}
}
Loading