diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessPath.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessPath.kt index b1b73b0f3..454930f54 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessPath.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessPath.kt @@ -42,8 +42,10 @@ class AccessPath( AccessPath(apManager, base, access, exclusions) override fun getAllAccessors(): Set = - access?.accessorList()?.toSet().orEmpty() + access?.allAccessors().orEmpty() + // [any] is matched syntactically everywhere: it is compared as an ordinary accessor symbol, + // and against a tree it matches only the tree's own [any] edge. override fun startsWithAccessor(accessor: Accessor): Boolean = with(apManager) { if (access == null) return false return access.accessor.accessor == accessor @@ -100,7 +102,7 @@ class AccessPath( with(node.manager) { setOf(node.accessor.accessor) } override fun getAllAccessors(): Set = - node.accessorList().toSet() + node.allAccessors() override fun readAccessor(accessor: Accessor): InitialFactAp.Delta? = with(node.manager) { if (node.accessor.accessor == accessor) return node.next?.let { Delta(it) } @@ -140,6 +142,7 @@ class AccessPath( return listOfNotNull(node.filter(other.exclusions)?.let { AccessPathDelta.Delta(it) }) } + // An [any] step of this path matches only the raw [any] edge of the tree (or splits at an abstract tree node) override fun splitDelta(other: FinalFactAp): List> { other as AccessTree @@ -163,6 +166,8 @@ class AccessPath( } null + } else if (node.accessor == ANY_ACCESSOR_IDX) { + otherNode.getAnyChild() } else { otherNode.getChild(node.accessor) } @@ -264,7 +269,14 @@ class AccessPath( val next: AccessNode? ) { private val hash: Int - val size: Int + + // size in the low 31 bits, containsAny in the sign bit: keeps the node at its old footprint + private val sizeAndAnyFlag: Int + + val size: Int get() = sizeAndAnyFlag and Int.MAX_VALUE + + /** This node or one of its successors is an [any] accessor. */ + val containsAny: Boolean get() = sizeAndAnyFlag < 0 init { var hash = accessor @@ -275,7 +287,8 @@ class AccessPath( init { var size = 1 if (next != null) size += next.size - this.size = size + val containsAny = accessor == ANY_ACCESSOR_IDX || next?.containsAny == true + this.sizeAndAnyFlag = if (containsAny) size or Int.MIN_VALUE else size } override fun hashCode(): Int = hash @@ -309,8 +322,32 @@ class AccessPath( return node } + private fun firstAnyNode(): AccessNode? { + if (!containsAny) return null + var node = this + while (node.accessor != ANY_ACCESSOR_IDX) { + node = node.next ?: return null + } + return node + } + fun accessorList(): List = toList().map { with(manager) { it.accessor } } + /** All accessors of the path except [any], consistent with [AccessTree.AccessNode.collectAccessorsTo]. */ + fun allAccessors(): Set { + if (!containsAny) return accessorList().toSet() + + val result = hashSetOf() + var node = this + while (true) { + val accessor = node.accessor + if (accessor != ANY_ACCESSOR_IDX) { + result.add(with(manager) { accessor.accessor }) + } + node = node.next ?: return result + } + } + override fun toString(): String = accessorList().joinToString("") { it.toSuffix() } fun addParent(accessor: AccessorIdx): AccessNode { @@ -332,7 +369,8 @@ class AccessPath( AccessNode(manager, accessor, this) } - accessor == ANY_ACCESSOR_IDX -> this // todo: All accessors are not supported in tree base ap + // A path holds at most one [any]: [any].p.[any].y == [any].y + accessor == ANY_ACCESSOR_IDX -> firstAnyNode() ?: AccessNode(manager, accessor, this) accessor == TYPE_INFO_GROUP_ACCESSOR_IDX -> AccessNode(manager, accessor, this) accessor.isTypeInfoAccessor() -> AccessNode(manager, accessor, this) diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessTree.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessTree.kt index 7d815805b..5b18777cb 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessTree.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessTree.kt @@ -188,7 +188,7 @@ class AccessTree( return listOf(EmptyAccessTreeDelta(deepAccessorExclusion = null)) } - node = node.getChild(accessor) ?: return emptyList() + node = node.getPathChild(accessor) ?: return emptyList() } val filteredNode = when (val exclusion = other.exclusions) { @@ -272,6 +272,9 @@ class AccessTree( @JvmField val maxDepth: Int @JvmField val containsStatic: Boolean + /** This node or a node below it has an [any] edge. Derived from the structure; not part of hash/equals. */ + @JvmField val containsAny: Boolean + init { check(deepAccessorExclusion == null || isAbstract) { "AnyFieldAccessorExclusions on a non-abstract node" @@ -282,6 +285,8 @@ class AccessTree( var hash = 0L var depth = 0 var containsStatic = false + var hasAnyEdge = false + var containsAny = false if (isAbstract) hash += 1 if (deepAccessorExclusion != null) hash += deepAccessorExclusion.hashCode().toLong() shl 3 @@ -292,25 +297,35 @@ class AccessTree( } if (accessors != null) { - containsStatic = accessors.any { it.isStaticAccessor() } + for (accessor in accessors) { + if (accessor.isStaticAccessor()) containsStatic = true + if (accessor == ANY_ACCESSOR_IDX) hasAnyEdge = true + } + containsAny = hasAnyEdge } if (accessorNodes != null) { - val accessorsHash = accessorNodes.sumOf { it.hash } + var accessorsHash = 0L + var childDepth = 0 + for (node in accessorNodes) { + accessorsHash += node.hash + if (node.maxDepth > childDepth) childDepth = node.maxDepth + if (node.containsStatic) containsStatic = true + if (node.containsAny) containsAny = true + } hash += accessorsHash shl 5 - depth = accessorNodes.maxOf { it.maxDepth } + 1 - - containsStatic = containsStatic || accessorNodes.any { it.containsStatic } + depth = childDepth + 1 } - if (containsAnyAccessor()) { + if (hasAnyEdge) { depth += 10_000 } this.hash = hash this.maxDepth = depth this.containsStatic = containsStatic + this.containsAny = containsAny } init { @@ -386,7 +401,11 @@ class AccessTree( accessorNodes?.getOrNull(accessorIndex(accessor)) fun containsAnyAccessor(): Boolean = - accessorIndex(ANY_ACCESSOR_IDX) >= 0 + containsAny && accessorIndex(ANY_ACCESSOR_IDX) >= 0 + + /** The raw child of this node's own [any] edge, without any unrolling. */ + fun getAnyChild(): AccessNode? = + if (containsAny) getNodeByAccessor(ANY_ACCESSOR_IDX) else null fun contains(accessor: AccessorIdx): Boolean { if (accessor == FINAL_ACCESSOR_IDX) return isFinal @@ -394,7 +413,7 @@ class AccessTree( val accessorIdx = accessorIndex(accessor) if (accessorIdx >= 0) return true - val anyAccessorNode = getNodeByAccessor(ANY_ACCESSOR_IDX) + val anyAccessorNode = getAnyChild() ?: return false if (anyAccessorNode.contains(accessor)) return true @@ -417,7 +436,7 @@ class AccessTree( val node = getNodeByAccessor(accessor) - val anyAccessorNode = getNodeByAccessor(ANY_ACCESSOR_IDX) + val anyAccessorNode = getAnyChild() ?: return node val anyChild = anyAccessorNode.getNodeByAccessor(accessor) @@ -472,6 +491,7 @@ class AccessTree( return isEmptyAbstract } + // Syntactic: the tree must be exactly this chain, an [any] in the path matches only a raw [any] edge var node = this otherAccess.toList().forEachInt { accessor -> if (accessor == FINAL_ACCESSOR_IDX) { @@ -479,7 +499,7 @@ class AccessTree( } if (node.accessors?.size != 1) return false - node = node.getChild(accessor) ?: return false + node = node.getNodeByAccessor(accessor) ?: return false } return node.isEmptyAbstract @@ -493,11 +513,18 @@ class AccessTree( var node = this otherAccess.toList().forEachInt { accessor -> if (accessor == FINAL_ACCESSOR_IDX) return node.isFinal - node = node.getChild(accessor) ?: return false + node = node.getPathChild(accessor) ?: return false } return node.isAbstract } + /** + * Child for an access path step. An [any] of the path is matched syntactically: only by the raw + * [any] edge of the tree, never by its concrete accessors. + */ + fun getPathChild(accessor: AccessorIdx): AccessNode? = + if (accessor == ANY_ACCESSOR_IDX) getAnyChild() else getChild(accessor) + sealed interface MatchResult { data object NotMatched : MatchResult data class MatchedWithRemainder(val remainder: AccessNode?) : MatchResult @@ -586,14 +613,47 @@ class AccessTree( return annotated } - private fun prependAnyAccessor(): AccessNode { - val anyNode = getNodeByAccessor(ANY_ACCESSOR_IDX) - val nextNode = if (anyNode == null) { - this - } else { - removeSingleAccessor(ANY_ACCESSOR_IDX).mergeAdd(anyNode) + private fun prependAnyAccessor(): AccessNode = + create(ANY_ACCESSOR_IDX, collapseAny()) + + /** + * A path holds at most one [any]: `[any].p.[any].y == [any].y`. Removes every [any] of this tree by + * replacing each path `p.[any].y` with `y`, so the result can be placed under a new [any] edge. + */ + private fun collapseAny(): AccessNode { + var node = this + while (node.containsAny) { + val split = node.splitAny() + var collapsed = split.complete ?: manager.emptyNode + split.any?.forEachAnySubtree(IdentityHashMap()) { collapsed = collapsed.mergeAdd(it) } + node = collapsed + } + return node + } + + // Calls body with the child of the first [any] edge on every path of this tree + private fun forEachAnySubtree(visited: IdentityHashMap, body: (AccessNode) -> Unit) { + if (visited.put(this, Unit) != null) return + forEachAccessor { accessor, child -> + if (accessor == ANY_ACCESSOR_IDX) { + body(child) + } else if (child.containsAny) { + child.forEachAnySubtree(visited, body) + } } - return create(ANY_ACCESSOR_IDX, nextNode) + } + + /** Restores the at-most-one-[any]-per-path invariant: collapses every [any] nested under another one. */ + private fun normalizeNestedAny(cache: IdentityHashMap): AccessNode { + if (!containsAny) return this + cache[this]?.let { return it } + + val result = transformAccessors { accessor, child -> + if (accessor == ANY_ACCESSOR_IDX) child.collapseAny() else child.normalizeNestedAny(cache) + } + + cache[this] = result + return result } private fun limitElementAccess(limit: Int): AccessNode { @@ -972,10 +1032,15 @@ class AccessTree( if (a.accessors == null || b.accessors == null) return + if (!a.containsAny && !b.containsAny) { + pushSharedChildPairs(a, b, stack) + return + } + val aAccessorsUntrimmed = a.accessors val aNodesUntrimmed = a.accessorNodes!! - val aAnyIdx = aAccessorsUntrimmed.indexOf(ANY_ACCESSOR_IDX) + val aAnyIdx = if (a.containsAny) aAccessorsUntrimmed.indexOf(ANY_ACCESSOR_IDX) else -1 val bTrimmed = if (aAnyIdx >= 0) AccessTreeAnySuffixMatcher(aNodesUntrimmed[aAnyIdx]).getNonMatchingNode(b) @@ -984,7 +1049,7 @@ class AccessTree( val bAccessorsUntrimmed = bTrimmed.accessors val bNodesUntrimmed = bTrimmed.accessorNodes - val bAnyIdx = bAccessorsUntrimmed?.indexOf(ANY_ACCESSOR_IDX) ?: -1 + val bAnyIdx = if (bTrimmed.containsAny) bAccessorsUntrimmed?.indexOf(ANY_ACCESSOR_IDX) ?: -1 else -1 val aTrimmed = if (bAnyIdx >= 0) AccessTreeAnySuffixMatcher(bNodesUntrimmed!![bAnyIdx]).getNonMatchingNode(a) @@ -1031,7 +1096,7 @@ class AccessTree( fun filterAccessNode(filter: FactTypeChecker.FactApFilter): AccessNode? = with(manager) { // An any-accessor matches zero or more accessors. If the filter rejects it, only the // empty match stays: remove the edge, keep its subtree at this node, then filter. - val anyNode = getNodeByAccessor(ANY_ACCESSOR_IDX) + val anyNode = getAnyChild() if (anyNode != null && filter.check(ANY_ACCESSOR_IDX.accessor) === FactTypeChecker.FilterResult.Reject) { return removeSingleAccessor(ANY_ACCESSOR_IDX).mergeAdd(anyNode).filterAccessNode(filter) } @@ -1117,9 +1182,13 @@ class AccessTree( ): AccessNode? { val filteredOther = FilteredNode.create(manager, other) - return concatToLeafAbstractNodes( + val result = concatToLeafAbstractNodes( typeChecker, filteredOther, IntArrayList(), SUBSEQUENT_ARRAY_ELEMENTS_LIMIT, ) + + // Only an [any] of other placed below an [any] of this can nest + if (result == null || !containsAny || !other.containsAny) return result + return result.normalizeNestedAny(IdentityHashMap()) } fun internNodes( @@ -1349,7 +1418,7 @@ class AccessTree( } else -> { - filteredTreeNode.getChild(accessor) + filteredTreeNode.getPathChild(accessor) ?.also { parentAccessors.add(accessor) } ?: return null } @@ -1365,6 +1434,72 @@ class AccessTree( return parentAccessors.foldRight(filteredTreeNode, ::create) } + class AnySplit(val complete: AccessNode?, val any: AccessNode?) + + /** + * Partitions the root-to-leaf paths of this tree: a path through an [any] edge goes to [AnySplit.any], + * every other path (including this node's own abstract/final marks) goes to [AnySplit.complete]. + */ + fun splitAny(): AnySplit { + if (!containsAny) return AnySplit(this, null) + return splitAnyCached(IdentityHashMap()) + } + + private fun splitAnyCached(cache: IdentityHashMap): AnySplit { + cache[this]?.let { return it } + + manager.cancellation.checkpoint() + + val accessors = accessors!! + val accessorNodes = accessorNodes!! + + val completeAccessors = IntArrayList(accessors.size) + val completeNodes = ArrayList(accessors.size) + val anyAccessors = IntArrayList(accessors.size) + val anyNodes = ArrayList(accessors.size) + + for (i in accessors.indices) { + val accessor = accessors[i] + val child = accessorNodes[i] + + if (accessor == ANY_ACCESSOR_IDX) { + anyAccessors.add(accessor) + anyNodes.add(child) + continue + } + + if (!child.containsAny) { + completeAccessors.add(accessor) + completeNodes.add(child) + continue + } + + val childSplit = child.splitAnyCached(cache) + childSplit.complete?.let { + completeAccessors.add(accessor) + completeNodes.add(it) + } + childSplit.any?.let { + anyAccessors.add(accessor) + anyNodes.add(it) + } + } + + val complete = manager.create( + isAbstract, isFinal, deepAccessorExclusion, + completeAccessors.toIntArray(), completeNodes.toTypedArray() + ).takeIf { !it.isEmpty } + + val any = manager.create( + isAbstract = false, isFinal = false, deepAccessorExclusion = null, + anyAccessors.toIntArray(), anyNodes.toTypedArray() + ).takeIf { !it.isEmpty } + + val result = AnySplit(complete, any) + cache[this] = result + return result + } + private inline fun mergeAccessors( otherFields: IntArray?, otherNodesE: Array?, diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/MethodFinalTreeApSummariesStorage.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/MethodFinalTreeApSummariesStorage.kt index 9f0715063..08831f299 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/MethodFinalTreeApSummariesStorage.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/MethodFinalTreeApSummariesStorage.kt @@ -11,17 +11,37 @@ class MethodFinalTreeApSummariesStorage( override fun createStorage(): Storage = MethodZeroToFactSummaryEdgeStorage(apManager) private class MethodZeroToFactSummaryEdgeStorage(val apManager: TreeApManager): Storage { - private val treeStorage = MergingTreeSummaryStorage(apManager) + // finals without [any], and the [any] parts of finals + private val completeStorage = MergingTreeSummaryStorage(apManager) + private val anyStorage = MergingTreeSummaryStorage(apManager) override fun add(edges: List, added: MutableList>) { - edges.forEach { treeStorage.add(it) } + edges.forEach { final -> + if (!final.containsAny) { + completeStorage.add(final) + return@forEach + } - val delta = treeStorage.getAndResetDelta() ?: return - added += ZeroEdgeBuilderBuilder(apManager).setNode(delta) + val split = final.splitAny() + split.complete?.let { completeStorage.add(it) } + split.any?.let { anyStorage.add(it) } + } + + completeStorage.getAndResetDelta()?.let { added += ZeroEdgeBuilderBuilder(apManager).setNode(it) } + anyStorage.getAndResetDelta()?.let { added += ZeroEdgeBuilderBuilder(apManager).setNode(it) } } override fun collectEdges(dst: MutableList>) { - treeStorage.edges()?.let { dst += ZeroEdgeBuilderBuilder(apManager).setNode(it) } + collectCompleteEdges(dst) + collectAnyEdges(dst) + } + + fun collectCompleteEdges(dst: MutableList>) { + completeStorage.edges()?.let { dst += ZeroEdgeBuilderBuilder(apManager).setNode(it) } + } + + fun collectAnyEdges(dst: MutableList>) { + anyStorage.edges()?.let { dst += ZeroEdgeBuilderBuilder(apManager).setNode(it) } } } diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/MethodInitialToFinalApSummaries.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/MethodInitialToFinalApSummaries.kt index 5c9391275..fd496bd47 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/MethodInitialToFinalApSummaries.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/MethodInitialToFinalApSummaries.kt @@ -178,9 +178,13 @@ private class SummariesIdStorageNode( private class MethodTaintedSummariesGroupedByFactStorage( apManager: TreeApManager, ) : CommonF2FSummary.Storage { + // complete edges: no [any] in the initial nor in the final private val idEdges = MethodTaintedSummariesIdStorage(apManager) private val nonUniverseAccessPath = MethodTaintedSummariesInitialApStorage(apManager) + // any-edges: [any] in the initial (whole edge), or the [any] part of a final + private val anyEdges = MethodTaintedSummariesInitialApStorage(apManager) + override fun add( edges: List>, added: MutableList> @@ -195,12 +199,45 @@ private class MethodTaintedSummariesGroupedByFactStorage( val modifiedStorages = mutableListOf() for (edge in edges) { - addNonUniverseEdge(edge.initial, edge.final, edge.exclusion, modifiedStorages) + addEdge(edge.initial, edge.final, edge.exclusion, modifiedStorages) } modifiedStorages.flatMapTo(added) { it.getAndResetDelta() } } + private fun addEdge( + initialAccess: AccessPath.AccessNode?, + exitAccess: AccessTreeNode, + exclusion: ExclusionSet, + modifiedStorages: MutableList + ) { + if (initialAccess != null && initialAccess.containsAny) { + addAnyEdge(initialAccess, exitAccess, exclusion, modifiedStorages) + return + } + + if (!exitAccess.containsAny) { + addNonUniverseEdge(initialAccess, exitAccess, exclusion, modifiedStorages) + return + } + + val split = exitAccess.splitAny() + split.complete?.let { addNonUniverseEdge(initialAccess, it, exclusion, modifiedStorages) } + split.any?.let { addAnyEdge(initialAccess, it, exclusion, modifiedStorages) } + } + + private fun addAnyEdge( + initialAccess: AccessPath.AccessNode?, + exitAccess: AccessTreeNode, + exclusion: ExclusionSet, + modifiedStorages: MutableList + ) { + val storage = anyEdges.getOrCreate(initialAccess) + if (storage.add(exitAccess, exclusion)) { + modifiedStorages.add(storage) + } + } + private fun addNonUniverseEdge( initialAccess: AccessPath.AccessNode?, exitAccess: AccessTreeNode, @@ -234,22 +271,33 @@ private class MethodTaintedSummariesGroupedByFactStorage( override fun collectSummariesTo( dst: MutableList>, initialFactPatter: AccessTreeNode? + ) { + collectCompleteSummariesTo(dst, initialFactPatter) + collectAnySummariesTo(dst, initialFactPatter) + } + + fun collectCompleteSummariesTo( + dst: MutableList>, + initialFactPatter: AccessTreeNode? ) { if (initialFactPatter != null) { - filterSummariesTo(dst, initialFactPatter) + idEdges.filterSummariesTo(dst, initialFactPatter) + nonUniverseAccessPath.filterSummariesTo(dst, initialFactPatter) } else { - collectAllSummariesTo(dst) + idEdges.collectAllSummariesTo(dst) + nonUniverseAccessPath.collectAllSummariesTo(dst) } } - private fun filterSummariesTo(dst: MutableList>, containsPattern: AccessTreeNode) { - idEdges.filterSummariesTo(dst, containsPattern) - nonUniverseAccessPath.filterSummariesTo(dst, containsPattern) - } - - private fun collectAllSummariesTo(dst: MutableList>) { - idEdges.collectAllSummariesTo(dst) - nonUniverseAccessPath.collectAllSummariesTo(dst) + fun collectAnySummariesTo( + dst: MutableList>, + initialFactPatter: AccessTreeNode? + ) { + if (initialFactPatter != null) { + anyEdges.filterSummariesTo(dst, initialFactPatter) + } else { + anyEdges.collectAllSummariesTo(dst) + } } } diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/MethodNDInitialToFinalApSummaries.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/MethodNDInitialToFinalApSummaries.kt index db3505dda..4403ad4ff 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/MethodNDInitialToFinalApSummaries.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/MethodNDInitialToFinalApSummaries.kt @@ -44,33 +44,68 @@ class MethodNDInitialToFinalApSummaries( private inner class FactStorage( override val storageIdx: Int, ) : Storage { - private var edges: AccessNode? = null - private var edgesDelta: AccessNode? = null + // an index whose initial set has an AP with [any] keeps everything in the any part + private val anyInitial: Boolean = initialApStorage[storageIdx].stream().anyMatch { apIdx -> + (getInitialApByIdx(apIdx) as AccessPath).access?.containsAny == true + } + + private val complete = FinalTrees() + private val any = FinalTrees() override fun add(element: AccessNode): Storage? { - val currentEdges = edges - if (currentEdges == null) { - edges = element - edgesDelta = element - return this - } - - val (modifiedEdges, modificationDelta) = currentEdges.mergeAddDelta(element) - if (modificationDelta == null) return null - - edges = modifiedEdges - edgesDelta = edgesDelta?.mergeAdd(modificationDelta) ?: modificationDelta - return this + if (anyInitial) return if (any.add(element)) this else null + if (!element.containsAny) return if (complete.add(element)) this else null + + val split = element.splitAny() + val completeModified = split.complete?.let { complete.add(it) } ?: false + val anyModified = split.any?.let { any.add(it) } ?: false + return if (completeModified || anyModified) this else null } override fun getAndResetDelta(delta: MutableList) { - delta += edgesDelta ?: return - edgesDelta = null + complete.getAndResetDelta(delta) + any.getAndResetDelta(delta) } override fun collectTo(dst: MutableList) { - edges?.let { dst += it } + collectCompleteTo(dst) + collectAnyTo(dst) + } + + fun collectCompleteTo(dst: MutableList) { + complete.edges?.let { dst += it } + } + + fun collectAnyTo(dst: MutableList) { + any.edges?.let { dst += it } + } + } + } + + private class FinalTrees { + var edges: AccessNode? = null + private set + private var edgesDelta: AccessNode? = null + + fun add(element: AccessNode): Boolean { + val currentEdges = edges + if (currentEdges == null) { + edges = element + edgesDelta = element + return true } + + val (modifiedEdges, modificationDelta) = currentEdges.mergeAddDelta(element) + if (modificationDelta == null) return false + + edges = modifiedEdges + edgesDelta = edgesDelta?.mergeAdd(modificationDelta) ?: modificationDelta + return true + } + + fun getAndResetDelta(delta: MutableList) { + delta += edgesDelta ?: return + edgesDelta = null } } } diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AnyAccessPathTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AnyAccessPathTest.kt new file mode 100644 index 000000000..4d773c5d3 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AnyAccessPathTest.kt @@ -0,0 +1,438 @@ +package org.opentaint.dataflow.ap.ifds.access.tree + +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.Accessor +import org.opentaint.dataflow.ap.ifds.AnyAccessor +import org.opentaint.dataflow.ap.ifds.ElementAccessor +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.FactTypeChecker +import org.opentaint.dataflow.ap.ifds.FieldAccessor +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy +import org.opentaint.dataflow.ap.ifds.access.InitialFactAp +import org.opentaint.dataflow.ap.ifds.access.tree.AccessTree.AccessNode.Companion.create +import org.opentaint.dataflow.ap.ifds.serialization.SummarySerializationContext +import org.opentaint.dataflow.util.Cancellation +import org.opentaint.dataflow.util.RefManager +import org.opentaint.ir.api.common.CommonMethod +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.DataInputStream +import java.io.DataOutputStream +import java.util.IdentityHashMap +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertSame +import kotlin.test.assertTrue + +class AnyAccessPathTest { + private companion object { + val A = FieldAccessor("C", "a", "C") + val F = FieldAccessor("C", "f", "C") + val G = FieldAccessor("C", "g", "C") + val H = FieldAccessor("C", "h", "C") + val X = FieldAccessor("C", "x", "C") + val Y = FieldAccessor("C", "y", "C") + val NOT_COVERED = FieldAccessor("C", "nc", "C") + val MARK = TaintMarkAccessor("m") + } + + private object UnrollStrategy : AnyAccessorUnrollStrategy { + override fun unrollAccessor(accessor: Accessor): Boolean = + (accessor is FieldAccessor && accessor != NOT_COVERED) || accessor is ElementAccessor + } + + private val manager = TreeApManager(UnrollStrategy, RefManager(), Cancellation()) + private val base = AccessPathBase.This + + private fun initial(vararg accessors: Accessor, exclusions: ExclusionSet = ExclusionSet.Empty): AccessPath = + accessors.foldRight(manager.mostAbstractInitialAp(base)) { a, f -> f.prependAccessor(a) } + .replaceExclusions(exclusions) as AccessPath + + private fun finalInitial(vararg accessors: Accessor): AccessPath = + accessors.foldRight(manager.createFinalInitialAp(base, ExclusionSet.Empty)) { a, f -> f.prependAccessor(a) } + as AccessPath + + /** this..* */ + private fun abstractTree(vararg accessors: Accessor): AccessTree = + accessors.foldRight(manager.mostAbstractFinalAp(base)) { a, f -> f.prependAccessor(a) } as AccessTree + + /** this..$ */ + private fun finalTree(vararg accessors: Accessor): AccessTree = + accessors.foldRight(manager.createFinalAp(base, ExclusionSet.Empty)) { a, f -> f.prependAccessor(a) } + as AccessTree + + private fun merged(vararg trees: AccessTree): AccessTree = + trees.reduce { acc, t -> AccessTree(manager, base, acc.access.mergeAdd(t.access), acc.exclusions) } + + private fun AccessTree.node(vararg accessors: Accessor): AccessTree.AccessNode = + accessors.fold(access) { n, a -> n.getChild(with(manager) { a.idx })!! } + + private fun AccessTree.deltaNode(path: InitialFactAp): AccessTree.AccessNode? = + delta(path).filterIsInstance().singleOrNull()?.node + + private fun AccessTree.hasEmptyDeltaFor(path: InitialFactAp): Boolean = + delta(path).any { it.isEmpty } + + /* ---------- flags ---------- */ + + @Test + fun `tree containsAny marks only the branch with an any edge`() { + val tree = merged(abstractTree(A, AnyAccessor, F), abstractTree(G, X)) + + assertTrue(tree.access.containsAny) + assertTrue(tree.node(A).containsAny) + assertFalse(tree.node(G).containsAny) + assertFalse(abstractTree(A, F).access.containsAny) + assertFalse(manager.abstractNode.containsAny) + } + + @Test + fun `tree containsAny survives interning`() { + val tree = merged(abstractTree(A, AnyAccessor, F), abstractTree(G, X)) + val interned = tree.access.internNodes(AccessTreeInterner(), IdentityHashMap()) + + assertTrue(interned.interned) + assertTrue(interned.containsAny) + assertEquals(tree.access, interned) + assertEquals(tree.access.hashCode(), interned.hashCode()) + } + + @Test + fun `path containsAny and size`() { + val path = initial(A, AnyAccessor, F).access!! + assertTrue(path.containsAny) + assertFalse(path.next!!.next!!.containsAny) + assertEquals(3, path.size) + assertEquals(2, initial(A, F).access!!.size) + assertFalse(initial(A, F).access!!.containsAny) + } + + @Test + fun `serializer round trip keeps any and the flags`() { + val serializer = TreeSerializer(manager, TestSerializationContext()) + + val path = initial(A, AnyAccessor, F) + val tree = merged(abstractTree(A, AnyAccessor, F), finalTree(G)) + + val bytes = ByteArrayOutputStream() + with(serializer) { + DataOutputStream(bytes).use { + it.writeInitialAp(path) + it.writeFinalAp(tree) + } + } + + val (readPath, readTree) = with(serializer) { + DataInputStream(ByteArrayInputStream(bytes.toByteArray())).use { + it.readInitialAp() to it.readFinalAp() + } + } + + assertEquals(path, readPath) + assertTrue((readPath as AccessPath).access!!.containsAny) + assertEquals(tree, readTree) + assertTrue((readTree as AccessTree).access.containsAny) + assertFalse(readTree.node(G).containsAny) + } + + /* ---------- AccessPath with [any] ---------- */ + + @Test + fun `prepend any keeps it in the path`() { + val path = initial(A, AnyAccessor, F) + assertEquals(listOf(A, AnyAccessor, F), path.access!!.accessorList()) + } + + @Test + fun `adjacent any collapses`() { + val path = initial(A, AnyAccessor, AnyAccessor, F) + assertEquals(listOf(A, AnyAccessor, F), path.access!!.accessorList()) + } + + @Test + fun `second any collapses everything back to the first`() { + assertEquals(listOf(A, AnyAccessor, F), initial(A, AnyAccessor, G, AnyAccessor, F).access!!.accessorList()) + assertEquals(listOf(A, AnyAccessor, F), initial(A, AnyAccessor, G, H, AnyAccessor, AnyAccessor, F).access!!.accessorList()) + assertEquals(listOf(AnyAccessor), initial(AnyAccessor, G, AnyAccessor).access!!.accessorList()) + assertEquals(listOf(A, AnyAccessor, MARK), initial(A, AnyAccessor, G, AnyAccessor, MARK).access!!.accessorList()) + } + + @Test + fun `path concat collapses a second any`() { + val delta = initial(AnyAccessor, F).delta(initial()).single() + val path = initial(A, AnyAccessor, G).concat(delta) as AccessPath + assertEquals(listOf(A, AnyAccessor, F), path.access!!.accessorList()) + } + + @Test + fun `tree prepend collapses a second any`() { + assertEquals(finalTree(A, AnyAccessor, F), finalTree(A, AnyAccessor, G, AnyAccessor, F)) + + // only the branch with an [any] is collapsed: [any].(g.$ + h.[any].f.$) == [any].(g.$ + f.$) + val below = merged(finalTree(G), finalTree(H, AnyAccessor, F)) + val tree = below.prependAccessor(AnyAccessor).prependAccessor(A) as AccessTree + assertFalse(tree.node(A).getAnyChild()!!.containsAny) + assertEquals(merged(finalTree(A, AnyAccessor, G), finalTree(A, AnyAccessor, F)), tree) + } + + @Test + fun `tree concat collapses an any below an any`() { + val delta = AccessTree.NodeAccessTreeDelta(manager, finalTree(AnyAccessor, F).access) + val tree = abstractTree(A, AnyAccessor, G).concat(FactTypeChecker.Dummy, delta) as AccessTree + assertEquals(finalTree(A, AnyAccessor, F), tree) + + // without an [any] above the leaf nothing collapses + val plain = abstractTree(A, G).concat(FactTypeChecker.Dummy, delta) as AccessTree + assertEquals(finalTree(A, G, AnyAccessor, F), plain) + } + + @Test + fun `path concat with a tree delta collapses a second any`() { + val nodeDelta = finalTree(AnyAccessor, F).delta(initial()).single() + val tree = initial(A, AnyAccessor, G).concat(FactTypeChecker.Dummy, nodeDelta) as AccessTree + assertEquals(finalTree(A, AnyAccessor, F), tree) + } + + @Test + fun `trailing any is kept`() { + val anyOnly = manager.mostAbstractInitialAp(base).prependAccessor(AnyAccessor) as AccessPath + assertEquals(listOf(AnyAccessor), anyOnly.access!!.accessorList()) + assertEquals(listOf(A, AnyAccessor), initial(A, AnyAccessor).access!!.accessorList()) + assertFalse(initial(A) == initial(A, AnyAccessor)) + } + + @Test + fun `getAllAccessors ignores any`() { + val path = initial(A, AnyAccessor, F) + assertEquals(setOf(A, F), path.getAllAccessors()) + + val delta = path.delta(initial(A)).single() + assertEquals(setOf(F), delta.getAllAccessors()) + assertTrue(delta.startsWithAccessor(AnyAccessor)) + } + + @Test + fun `path operations treat any as a symbol`() { + val path = initial(AnyAccessor, F) + assertTrue(path.startsWithAccessor(AnyAccessor)) + assertFalse(path.startsWithAccessor(F)) + assertEquals(initial(F), path.readAccessor(AnyAccessor)) + assertNull(path.readAccessor(F)) + assertTrue(initial(A, AnyAccessor, F).delta(initial(A, F)).isEmpty()) + } + + @Test + fun `path with any concatenates into a tree with an any edge`() { + val path = initial(A, AnyAccessor) + val delta = abstractTree().delta(initial()).single() + val tree = path.concat(FactTypeChecker.Dummy, delta) as AccessTree + assertEquals(abstractTree(A, AnyAccessor), tree) + + val nodeDelta = finalTree(F).delta(initial()).single() + val withNode = path.concat(FactTypeChecker.Dummy, nodeDelta) as AccessTree + assertEquals(finalTree(A, AnyAccessor, F), withNode) + } + + /* ---------- AccessTree.delta / contains with [any] in the path: exact match ---------- */ + + @Test + fun `any in the path does not match concrete accessors`() { + val path = initial(A, AnyAccessor, F) + for (tree in listOf(abstractTree(A, F), abstractTree(A, G, H, F))) { + assertTrue(tree.delta(path).isEmpty()) + assertFalse(tree.contains(path)) + } + } + + @Test + fun `any in the path matches the tree any edge`() { + val tree = abstractTree(A, AnyAccessor, F) + assertTrue(tree.hasEmptyDeltaFor(initial(A, AnyAccessor, F))) + assertTrue(tree.contains(initial(A, AnyAccessor, F))) + assertFalse(tree.contains(initial(A, AnyAccessor, G))) + assertFalse(tree.contains(initial(AnyAccessor, F))) + } + + @Test + fun `delta through the any edge ignores concrete branches`() { + val tree = merged(finalTree(A, AnyAccessor, F, X), finalTree(A, F, Y), finalTree(A, G, F, Y)) + assertEquals(finalTree(X).access, tree.deltaNode(initial(A, AnyAccessor, F))) + } + + @Test + fun `trailing any takes only the any edge subtree`() { + val tree = merged(finalTree(A, AnyAccessor, X), finalTree(A, F)) + assertEquals(finalTree(X).access, tree.deltaNode(initial(A, AnyAccessor))) + } + + @Test + fun `any path matches final only through the any edge`() { + assertTrue(finalTree(A, AnyAccessor).hasEmptyDeltaFor(finalInitial(A, AnyAccessor))) + assertTrue(finalTree(A, AnyAccessor).contains(finalInitial(A, AnyAccessor))) + assertTrue(finalTree(A, G).delta(finalInitial(A, AnyAccessor)).isEmpty()) + assertFalse(finalTree(A, G).contains(finalInitial(A, AnyAccessor))) + } + + @Test + fun `delta with any applies the exclusions`() { + val tree = merged(finalTree(A, AnyAccessor, F, X), finalTree(A, AnyAccessor, F, Y)) + val delta = tree.deltaNode(initial(A, AnyAccessor, F, exclusions = ExclusionSet.Empty.add(X))) + assertEquals(finalTree(Y).access, delta) + } + + @Test + fun `paths without any keep the old behaviour`() { + val tree = merged(finalTree(A, F, X), abstractTree(A, G)) + assertEquals(finalTree(X).access, tree.deltaNode(initial(A, F))) + assertTrue(tree.hasEmptyDeltaFor(initial(A, G))) + assertTrue(tree.delta(initial(A, H)).isEmpty()) + assertTrue(tree.contains(initial(A, G))) + assertFalse(tree.contains(initial(A, G, H))) + assertFalse(tree.contains(initial(A, F))) + } + + /* ---------- filterStartsWith ---------- */ + + @Test + fun `filterStartsWith with any follows the any edge exactly`() { + val tree = merged(finalTree(A, AnyAccessor, F, X), finalTree(A, G, F, Y), finalTree(H)) + val path = initial(A, AnyAccessor, F) + + val filtered = tree.access.filterStartsWith(path.access) + assertEquals(finalTree(A, AnyAccessor, F, X).access, filtered) + + val filteredTree = AccessTree(manager, base, filtered!!, ExclusionSet.Empty) + assertEquals(tree.delta(path), filteredTree.delta(path)) + } + + @Test + fun `filterStartsWith with any drops trees without an any edge`() { + assertNull(finalTree(A, G, F).access.filterStartsWith(initial(A, AnyAccessor, F).access)) + assertNull(finalTree(A, F).access.filterStartsWith(initial(AnyAccessor, F).access)) + } + + /* ---------- syntactic operations ---------- */ + + @Test + fun `equalTo is syntactic`() { + assertTrue(abstractTree(A, AnyAccessor, F).equalTo(initial(A, AnyAccessor, F))) + assertFalse(abstractTree(A, G, F).equalTo(initial(A, AnyAccessor, F))) + assertFalse(abstractTree(A, AnyAccessor, F).equalTo(initial(A, F))) + assertTrue(abstractTree(A, F).equalTo(initial(A, F))) + } + + @Test + fun `splitDelta follows the raw any edge`() { + val path = initial(A, AnyAccessor, F) + + val split = path.splitDelta(abstractTree(A, AnyAccessor)).single() + assertEquals(listOf(A, AnyAccessor), (split.first as AccessPath).access!!.accessorList()) + assertEquals(setOf(F), split.second.getAllAccessors()) + + val atAbstract = path.splitDelta(abstractTree(A)).single() + assertEquals(initial(A), atAbstract.first) + assertTrue(atAbstract.second.startsWithAccessor(AnyAccessor)) + + assertTrue(path.splitDelta(finalTree(A, G, F)).isEmpty()) + } + + /* ---------- splitAny ---------- */ + + private fun AccessTree.AccessNode.allPathsCrossAny(): Boolean { + if (isAbstract || isFinal) return false + var result = true + forEachAccessor { accessor, child -> + if (accessor != with(manager) { AnyAccessor.idx } && !child.allPathsCrossAny()) result = false + } + return result + } + + @Test + fun `splitAny of a tree without any is the tree itself`() { + val tree = merged(finalTree(A, F), abstractTree(G)) + val split = tree.access.splitAny() + assertSame(tree.access, split.complete) + assertNull(split.any) + } + + @Test + fun `splitAny of a pure any tree`() { + val tree = abstractTree(AnyAccessor, F) + val split = tree.access.splitAny() + assertNull(split.complete) + assertEquals(tree.access, split.any) + } + + @Test + fun `splitAny of a mixed tree`() { + val tree = merged(abstractTree(), finalTree(A, F), abstractTree(A, AnyAccessor, G), finalTree(H, AnyAccessor)) + val split = tree.access.splitAny() + + val complete = assertNotNull(split.complete) + val any = assertNotNull(split.any) + + assertEquals(merged(abstractTree(), finalTree(A, F)).access, complete) + assertEquals(merged(abstractTree(A, AnyAccessor, G), finalTree(H, AnyAccessor)).access, any) + + assertFalse(complete.containsAny) + assertTrue(any.allPathsCrossAny()) + assertEquals(tree.access, complete.mergeAdd(any, foldToAny = false)) + } + + @Test + fun `splitAny shares the split of a shared subtree`() { + val shared = abstractTree(F, AnyAccessor, G).access + val sharedWithComplete = shared.mergeAdd(finalTree(X).access, foldToAny = false) + + val aIdx = with(manager) { A.idx } + val hIdx = with(manager) { H.idx } + val yIdx = with(manager) { Y.idx } + val plain = finalTree(X).access + + val accessors = intArrayOf(aIdx, hIdx, yIdx) + val order = accessors.indices.sortedBy { accessors[it] } + val children = arrayOf(sharedWithComplete, sharedWithComplete, plain) + + val root = manager.create( + isAbstract = false, isFinal = false, deepAccessorExclusion = null, + accessors = IntArray(3) { accessors[order[it]] }, + accessorNodes = Array(3) { children[order[it]] }, + ) + + val split = root.splitAny() + val complete = assertNotNull(split.complete) + val any = assertNotNull(split.any) + + assertSame(complete.getChild(aIdx), complete.getChild(hIdx)) + assertSame(any.getChild(aIdx), any.getChild(hIdx)) + assertSame(plain, complete.getChild(yIdx)) + assertNull(any.getChild(yIdx)) + + assertFalse(complete.containsAny) + assertTrue(any.allPathsCrossAny()) + assertEquals(root, complete.mergeAdd(any, foldToAny = false)) + } + + private class TestSerializationContext : SummarySerializationContext { + private val accessors = mutableListOf() + + override fun getIdByAccessor(accessor: Accessor): Long { + val idx = accessors.indexOf(accessor) + if (idx >= 0) return idx.toLong() + accessors.add(accessor) + return (accessors.size - 1).toLong() + } + + override fun getAccessorById(id: Long): Accessor = accessors[id.toInt()] + + override fun getIdByMethod(method: CommonMethod): Long = error("unused") + override fun getMethodById(id: Long): CommonMethod = error("unused") + override fun loadSummaries(method: CommonMethod): ByteArray? = error("unused") + override fun storeSummaries(method: CommonMethod, summaries: ByteArray) = error("unused") + override fun flush() = Unit + } +} diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AnySummaryStorageTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AnySummaryStorageTest.kt new file mode 100644 index 000000000..55173de21 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AnySummaryStorageTest.kt @@ -0,0 +1,367 @@ +package org.opentaint.dataflow.ap.ifds.access.tree + +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.Accessor +import org.opentaint.dataflow.ap.ifds.AnyAccessor +import org.opentaint.dataflow.ap.ifds.Edge +import org.opentaint.dataflow.ap.ifds.ElementAccessor +import org.opentaint.dataflow.ap.ifds.EmptyMethodContext +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.FactToFactEdgeBuilder +import org.opentaint.dataflow.ap.ifds.FieldAccessor +import org.opentaint.dataflow.ap.ifds.MethodEntryPoint +import org.opentaint.dataflow.ap.ifds.NDFactToFactEdgeBuilder +import org.opentaint.dataflow.ap.ifds.SideEffectKind +import org.opentaint.dataflow.ap.ifds.SideEffectSummary.FactSideEffectSummary +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.ZeroToFactEdgeBuilder +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy +import org.opentaint.dataflow.ap.ifds.access.FinalFactAp +import org.opentaint.dataflow.ap.ifds.access.InitialFactAp +import org.opentaint.dataflow.util.Cancellation +import org.opentaint.dataflow.util.RefManager +import org.opentaint.ir.api.common.CommonMethod +import org.opentaint.ir.api.common.CommonMethodParameter +import org.opentaint.ir.api.common.CommonTypeName +import org.opentaint.ir.api.common.cfg.CommonInst +import org.opentaint.ir.api.common.cfg.CommonInstLocation +import org.opentaint.ir.api.common.cfg.ControlFlowGraph +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class AnySummaryStorageTest { + private companion object { + val A = FieldAccessor("C", "a", "C") + val F = FieldAccessor("C", "f", "C") + val G = FieldAccessor("C", "g", "C") + val H = FieldAccessor("C", "h", "C") + val K = FieldAccessor("C", "k", "C") + val MARK = TaintMarkAccessor("m") + } + + private object UnrollStrategy : AnyAccessorUnrollStrategy { + override fun unrollAccessor(accessor: Accessor): Boolean = + accessor is FieldAccessor || accessor is ElementAccessor + } + + private object Kind : SideEffectKind + + private val manager = TreeApManager(UnrollStrategy, RefManager(), Cancellation()) + private val base = AccessPathBase.This + private val arg = AccessPathBase.LocalVar(0) + + private val exitInst = DummyInst("exit") + private val entryInst = DummyInst("entry") + private val entryPoint = MethodEntryPoint(EmptyMethodContext, entryInst) + + private fun initial( + vararg accessors: Accessor, + base: AccessPathBase = this.base, + exclusions: ExclusionSet = ExclusionSet.Empty, + ): AccessPath = + accessors.foldRight(manager.mostAbstractInitialAp(base)) { a, f -> f.prependAccessor(a) } + .replaceExclusions(exclusions) as AccessPath + + /** this..$ */ + private fun finalTree(vararg accessors: Accessor, exclusions: ExclusionSet = ExclusionSet.Empty): AccessTree = + accessors.foldRight(manager.createFinalAp(base, exclusions)) { a, f -> f.prependAccessor(a) } as AccessTree + + private fun merged(vararg trees: AccessTree): AccessTree = + trees.reduce { acc, t -> AccessTree(manager, acc.base, acc.access.mergeAdd(t.access), acc.exclusions) } + + private fun FinalFactAp.node(): AccessTree.AccessNode = (this as AccessTree).access + + private fun AccessTree.AccessNode.allPathsCrossAny(): Boolean { + if (isAbstract || isFinal) return false + var result = true + forEachAccessor { accessor, child -> + if (accessor != with(manager) { AnyAccessor.idx } && !child.allPathsCrossAny()) result = false + } + return result + } + + /* ---------- nested [any] ---------- */ + + @Test + fun `trees with a collapsed second any merge`() { + // a.[any].f.[any].g.$ is built as a.[any].g.$ + val collapsed = finalTree(A, AnyAccessor, F, AnyAccessor, G) + assertEquals(finalTree(A, AnyAccessor, G), collapsed) + + val storage = MergingTreeSummaryStorage(manager) + assertTrue(storage.add(collapsed.access)) + assertTrue(storage.add(finalTree(A, AnyAccessor, K).access)) + + val edges = AccessTree(manager, base, storage.edges()!!, ExclusionSet.Empty) + assertTrue(edges.contains(finalInitialOf(initial(A, AnyAccessor, G)))) + assertTrue(edges.contains(finalInitialOf(initial(A, AnyAccessor, K)))) + } + + private fun finalInitialOf(path: AccessPath): AccessPath { + val accessors = path.access?.accessorList().orEmpty() + return accessors.foldRight(manager.createFinalInitialAp(base, ExclusionSet.Empty)) { a, f -> + f.prependAccessor(a) + } as AccessPath + } + + /* ---------- F2F ---------- */ + + private fun f2fStorage() = MethodInitialToFinalApSummaries(entryInst, manager) + + private fun MethodInitialToFinalApSummaries.add(vararg edges: Pair): List { + val added = mutableListOf() + add(edges.map { (i, f) -> Edge.FactToFact(entryPoint, i, exitInst, f) }, added) + return added.map { it.setEntryPoint(entryPoint).build() } + } + + private fun MethodInitialToFinalApSummaries.find(pattern: FinalFactAp?): List { + val dst = mutableListOf() + filterEdgesTo(dst, pattern, finalFactBase = null) + return dst.map { it.setEntryPoint(entryPoint).build() } + } + + @Test + fun `f2f complete edge stays complete`() { + val storage = f2fStorage() + val added = storage.add(initial(A) to finalTree(H, G)) + + val edge = added.single() + assertFalse(edge.factAp.node().containsAny) + assertEquals(listOf(edge), storage.find(finalTree(A))) + } + + @Test + fun `f2f any-only final goes to the any storage`() { + val storage = f2fStorage() + val final = finalTree(H, AnyAccessor, G) + val added = storage.add(initial(A) to final) + + assertEquals(final.node(), added.single().factAp.node()) + assertEquals(added, storage.find(finalTree(A))) + } + + @Test + fun `f2f mixed final is split into a complete and an any edge`() { + val storage = f2fStorage() + val final = merged(finalTree(H, G), finalTree(H, AnyAccessor, K), finalTree(F)) + val added = storage.add(initial(A) to final) + + assertEquals(2, added.size) + assertTrue(added.all { it.initialFactAp == initial(A) }) + + val (anyEdges, completeEdges) = added.partition { it.factAp.node().containsAny } + val complete = completeEdges.single().factAp.node() + val any = anyEdges.single().factAp.node() + + assertEquals(merged(finalTree(H, G), finalTree(F)).access, complete) + assertTrue(any.allPathsCrossAny()) + assertEquals(final.access, complete.mergeAdd(any, foldToAny = false)) + + assertEquals(added.toSet(), storage.find(finalTree(A)).toSet()) + } + + @Test + fun `f2f initial with any is stored whole and found only by an any pattern`() { + val storage = f2fStorage() + val initialAp = initial(A, AnyAccessor, F) + val final = merged(finalTree(H, G), finalTree(H, AnyAccessor, K)) + val added = storage.add(initialAp to final) + + val edge = added.single() + assertEquals(initialAp, edge.initialFactAp) + assertEquals(final.access, edge.factAp.node()) + + // [any] is matched exactly: only a caller fact with the [any] edge reaches the key + assertEquals(listOf(edge), storage.find(finalTree(A, AnyAccessor, F))) + assertTrue(storage.find(finalTree(A, G, F)).isEmpty()) + assertTrue(storage.find(finalTree(A, F)).isEmpty()) + assertTrue(storage.find(finalTree(H, F)).isEmpty()) + assertEquals(listOf(edge), storage.find(null)) + } + + @Test + fun `f2f any key at the root is found only by an any pattern`() { + val storage = f2fStorage() + val edge = storage.add(initial(AnyAccessor, F) to finalTree(G)).single() + assertEquals(listOf(edge), storage.find(finalTree(AnyAccessor, F))) + assertTrue(storage.find(finalTree(H, H, F)).isEmpty()) + } + + @Test + fun `f2f repeated adds report only the new part`() { + val storage = f2fStorage() + val final = merged(finalTree(H, G), finalTree(H, AnyAccessor, K)) + assertEquals(2, storage.add(initial(A) to final).size) + + assertTrue(storage.add(initial(A) to final).isEmpty()) + + val newAny = storage.add(initial(A) to finalTree(H, AnyAccessor, MARK)).single() + assertTrue(newAny.factAp.node().allPathsCrossAny()) + assertFalse(newAny.factAp.contains(finalInitialOf(initial(H, AnyAccessor, K)))) + + val newComplete = storage.add(initial(A) to finalTree(F)).single() + assertEquals(finalTree(F).access, newComplete.factAp.node()) + + val anyInitial = initial(A, AnyAccessor, F) + assertEquals(1, storage.add(anyInitial to finalTree(G)).size) + assertTrue(storage.add(anyInitial to finalTree(G)).isEmpty()) + assertEquals(finalTree(K).access, storage.add(anyInitial to finalTree(K)).single().factAp.node()) + } + + @Test + fun `f2f any edges with a collapsed second any merge`() { + val storage = f2fStorage() + assertEquals(1, storage.add(initial(A) to finalTree(A, AnyAccessor, F, AnyAccessor, G)).size) + assertEquals(1, storage.add(initial(A) to finalTree(A, AnyAccessor, K)).size) + } + + /* ---------- Z2F ---------- */ + + private fun z2fStorage() = MethodFinalTreeApSummariesStorage(entryInst, manager) + + private fun MethodFinalTreeApSummariesStorage.add(vararg finals: FinalFactAp): List { + val added = mutableListOf() + add(finals.map { Edge.ZeroToFact(entryPoint, exitInst, it.replaceExclusions(ExclusionSet.Universe)) }, added) + return added.map { it.setEntryPoint(entryPoint).build() } + } + + private fun MethodFinalTreeApSummariesStorage.all(): List { + val dst = mutableListOf() + filterEdgesTo(dst, finalFactBase = null) + return dst.map { it.setEntryPoint(entryPoint).build() } + } + + @Test + fun `z2f splits finals and reports deltas of both parts`() { + val storage = z2fStorage() + + assertEquals(1, storage.add(finalTree(F)).size) + assertTrue(storage.add(finalTree(F)).isEmpty()) + + val added = storage.add(merged(finalTree(G), finalTree(H, AnyAccessor, K))) + assertEquals(2, added.size) + val (any, complete) = added.partition { it.factAp.node().containsAny } + assertEquals(finalTree(G).access, complete.single().factAp.node()) + assertTrue(any.single().factAp.node().allPathsCrossAny()) + + val onlyAny = storage.add(finalTree(H, AnyAccessor, MARK)).single() + assertTrue(onlyAny.factAp.node().allPathsCrossAny()) + + val all = storage.all() + assertEquals(2, all.size) + val (allAny, allComplete) = all.partition { it.factAp.node().containsAny } + assertEquals(merged(finalTree(F), finalTree(G)).access, allComplete.single().factAp.node()) + assertTrue(allAny.single().factAp.node().allPathsCrossAny()) + } + + /* ---------- NDF2F ---------- */ + + private fun ndStorage() = MethodNDInitialToFinalApSummaries(entryInst, manager) + + private fun MethodNDInitialToFinalApSummaries.add(initial: Set, final: FinalFactAp): List { + val added = mutableListOf() + val edge = Edge.NDFactToFact( + entryPoint, + initial.mapTo(hashSetOf()) { it.replaceExclusions(ExclusionSet.Universe) }, + exitInst, + final.replaceExclusions(ExclusionSet.Universe), + ) + add(listOf(edge), added) + return added.map { it.setEntryPoint(entryPoint).build() } + } + + private fun MethodNDInitialToFinalApSummaries.find(pattern: FinalFactAp): List { + val dst = mutableListOf() + filterEdgesTo(dst, pattern, finalFactBase = null) + return dst.map { it.setEntryPoint(entryPoint).build() } + } + + @Test + fun `ndf2f splits the final of a complete initial set`() { + val storage = ndStorage() + val initials = setOf(initial(A), initial(F, base = arg)) + val final = merged(finalTree(G), finalTree(H, AnyAccessor, K)) + + val added = storage.add(initials, final) + assertEquals(2, added.size) + val (any, complete) = added.partition { it.factAp.node().containsAny } + assertEquals(finalTree(G).access, complete.single().factAp.node()) + assertTrue(any.single().factAp.node().allPathsCrossAny()) + + assertTrue(storage.add(initials, final).isEmpty()) + assertEquals(finalTree(F).access, storage.add(initials, finalTree(F)).single().factAp.node()) + + assertEquals(2, storage.find(finalTree(A)).size) + } + + @Test + fun `ndf2f initial set with any keeps the final whole`() { + val storage = ndStorage() + val initials = setOf(initial(A, AnyAccessor, F), initial(F, base = arg)) + val final = merged(finalTree(G), finalTree(H, AnyAccessor, K)) + + val added = storage.add(initials, final).single() + assertEquals(final.access, added.factAp.node()) + assertTrue(storage.add(initials, final).isEmpty()) + + assertEquals(listOf(added), storage.find(finalTree(A, G, F))) + } + + /* ---------- side effects ---------- */ + + @Test + fun `fact side effect with any initial is found only by an any pattern`() { + val storage = FactSideEffectSummariesTreeApStorage(entryInst, manager) + val summary = FactSideEffectSummary(initial(A, AnyAccessor, F), Kind) + val complete = FactSideEffectSummary(initial(H), Kind) + + val added = mutableListOf() + storage.add(listOf(summary, complete), added) + assertEquals(setOf(summary, complete), added.toSet()) + + val found = mutableListOf() + storage.filterTaintedTo(found, finalTree(A, AnyAccessor, F)) + assertEquals(listOf(summary), found) + + val notFound = mutableListOf() + storage.filterTaintedTo(notFound, finalTree(A, G, F)) + assertTrue(notFound.isEmpty()) + + val foundComplete = mutableListOf() + storage.filterTaintedTo(foundComplete, finalTree(H, G)) + assertEquals(listOf(complete), foundComplete) + } + + @Test + fun `side effect requirement with any is found only by an any pattern`() { + val storage = SideEffectRequirementTreeApStorage(manager) + val requirement = initial(A, AnyAccessor, F) + assertEquals(listOf(requirement), storage.add(listOf(requirement))) + + val found = mutableListOf() + storage.filterTo(found, finalTree(A, AnyAccessor, F)) + assertEquals(listOf(requirement), found) + + val notFound = mutableListOf() + storage.filterTo(notFound, finalTree(A, G, F)) + assertTrue(notFound.isEmpty()) + } + + private class DummyInst(private val name: String) : CommonInst { + override fun toString(): String = name + override val location: CommonInstLocation = object : CommonInstLocation { + override val index: Int = 0 + override val method: CommonMethod = object : CommonMethod { + override val name: String = "dummy" + override val parameters: List = emptyList() + override val returnType: CommonTypeName = object : CommonTypeName { + override val typeName: String = "void" + } + + override fun flowGraph(): ControlFlowGraph = error("unused") + } + } + } +}