From 6c426a78339cb263235ae9999b2185ad15b7ec3b Mon Sep 17 00:00:00 2001 From: Valentyn Sobol <8640896+Saloed@users.noreply.github.com> Date: Thu, 1 Oct 2026 22:25:29 +0000 Subject: [PATCH 1/5] feat(dataflow): Support [any] in tree access paths Keep [any] in AccessPath instead of dropping it, add containsAny flags to AccessPath and AccessTree nodes, match [any] paths semantically in AccessTree.delta/contains/filterStartsWith, and add AccessNode.splitAny() to partition a tree into its complete and [any] parts. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_018R4xeH4QFAH7RrPcKFoQGZ --- .../ap/ifds/access/tree/AccessPath.kt | 55 ++- .../ap/ifds/access/tree/AccessTree.kt | 205 ++++++++- .../tree/MethodTreeAccessPathSubscription.kt | 4 + .../ap/ifds/access/tree/AnyAccessPathTest.kt | 425 ++++++++++++++++++ 4 files changed, 669 insertions(+), 20 deletions(-) create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AnyAccessPathTest.kt 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 b1b73b0f3b..3e43dd7620 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() + // readAccessor, startsWithAccessor, getStartAccessors, clearAccessor and delta(AccessPath) are + // syntactic: an [any] in the path is compared as an ordinary accessor symbol. override fun startsWithAccessor(accessor: Accessor): Boolean = with(apManager) { if (access == null) return false return access.accessor.accessor == accessor @@ -63,6 +65,9 @@ class AccessPath( val accessorIdx = with(apManager) { accessor.idx } if (access == null) { + // [any].* == * + if (accessorIdx == ANY_ACCESSOR_IDX) return this + return AccessPath(apManager, base, AccessNode(apManager, accessorIdx, next = null), exclusions) } @@ -100,7 +105,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 +145,11 @@ class AccessPath( return listOfNotNull(node.filter(other.exclusions)?.let { AccessPathDelta.Delta(it) }) } + /** + * An [any] step of this path only follows the raw [any] edge of the tree (or splits at an abstract + * tree node, as a concrete accessor does). It does not unroll the [any] over the tree's concrete + * edges, so a tree that holds the [any] content only as concrete accessors is not matched. + */ override fun splitDelta(other: FinalFactAp): List> { other as AccessTree @@ -163,6 +173,8 @@ class AccessPath( } null + } else if (node.accessor == ANY_ACCESSOR_IDX) { + otherNode.getAnyChild() } else { otherNode.getChild(node.accessor) } @@ -264,7 +276,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 +294,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 +329,30 @@ class AccessPath( return node } + fun endsWithFinal(): Boolean { + var node = this + while (true) { + node = node.next ?: return node.accessor == FINAL_ACCESSOR_IDX + } + } + 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 +374,10 @@ class AccessPath( AccessNode(manager, accessor, this) } - accessor == ANY_ACCESSOR_IDX -> this // todo: All accessors are not supported in tree base ap + accessor == ANY_ACCESSOR_IDX -> { + // [any].[any] == [any] + if (this.accessor == ANY_ACCESSOR_IDX) this else 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 7d815805bf..d963446e3d 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 @@ -180,8 +180,10 @@ class AccessTree( if (base != other.base) return emptyList() - var node = access val access = other.access + if (access != null && access.containsAny) return deltaWithAny(access, other.exclusions) + + var node = this.access access?.toList()?.forEachInt { accessor -> if (accessor == FINAL_ACCESSOR_IDX) { if (!node.isFinal) return emptyList() @@ -191,7 +193,21 @@ class AccessTree( node = node.getChild(accessor) ?: return emptyList() } - val filteredNode = when (val exclusion = other.exclusions) { + return deltaAtNode(node, other.exclusions) + } + + private fun deltaWithAny(path: AccessPath.AccessNode, exclusions: ExclusionSet): List { + val matched = access.matchPathWithAny(path) + if (matched.isEmpty()) return emptyList() + + if (path.endsWithFinal()) return listOf(EmptyAccessTreeDelta(deepAccessorExclusion = null)) + + val node = matched.reduce { acc, n -> acc.mergeAdd(n) } + return deltaAtNode(node, exclusions) + } + + private fun deltaAtNode(node: AccessNode, exclusions: ExclusionSet): List { + val filteredNode = when (val exclusion = exclusions) { ExclusionSet.Empty -> node is ExclusionSet.Concrete -> node.filter(exclusion) ExclusionSet.Universe -> error("Unexpected universe exclusion in initial fact") @@ -272,6 +288,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 +301,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 +313,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 +417,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 +429,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 +452,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 +507,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 +515,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 @@ -490,6 +526,11 @@ class AccessTree( return isAbstract } + if (otherAccess.containsAny) { + val matched = matchPathWithAny(otherAccess) + return if (otherAccess.endsWithFinal()) matched.isNotEmpty() else matched.any { it.isAbstract } + } + var node = this otherAccess.toList().forEachInt { accessor -> if (accessor == FINAL_ACCESSOR_IDX) return node.isFinal @@ -498,6 +539,47 @@ class AccessTree( return node.isAbstract } + /** + * Semantic match of a path with [any]: every [any] of the path matches zero or more accessors covered + * by any, so the current position becomes a set of nodes. Returns the nodes reached after the whole + * path; a FINAL step maps every final node to [TreeApManager.finalNode]. + */ + internal fun matchPathWithAny(path: AccessPath.AccessNode): List { + var current: List = listOf(this) + var pathNode: AccessPath.AccessNode = path + while (true) { + val accessor = pathNode.accessor + current = when (accessor) { + ANY_ACCESSOR_IDX -> anyClosure(current) + FINAL_ACCESSOR_IDX -> if (current.any { it.isFinal }) listOf(manager.finalNode) else emptyList() + else -> current.mapNotNull { it.getChild(accessor) } + } + + if (current.isEmpty()) return current + pathNode = pathNode.next ?: return current + } + } + + // Closure over raw edges that [any] can match: covered accessors and [any] edges themselves. + // getChild must not be used here: it allocates fresh nodes and the closure would not terminate. + private fun anyClosure(nodes: List): List { + val visited = IdentityHashMap() + val result = mutableListOf() + val stack = nodes.toMutableList() + while (stack.isNotEmpty()) { + val node = stack.removeLast() + if (visited.put(node, Unit) != null) continue + result.add(node) + + node.forEachAccessor { accessor, child -> + if (accessor == ANY_ACCESSOR_IDX || manager.isCoveredByAny(accessor)) { + stack.add(child) + } + } + } + return result + } + sealed interface MatchResult { data object NotMatched : MatchResult data class MatchedWithRemainder(val remainder: AccessNode?) : MatchResult @@ -972,10 +1054,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 +1071,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 +1118,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) } @@ -1329,6 +1416,8 @@ class AccessTree( fun filterStartsWith(accessPath: AccessPath.AccessNode?): AccessNode? { if (accessPath == null) return this + if (accessPath.containsAny) return filterStartsWithAny(accessPath) + if (maxDepth < accessPath.size) { return null } @@ -1365,6 +1454,92 @@ class AccessTree( return parentAccessors.foldRight(filteredTreeNode, ::create) } + /** + * The result keeps only the concrete path prefix before the first [any] and the whole subtree below it, + * so its delta for [accessPath] is exactly the delta of this tree. Rebuilding the [any] part of the path + * as a tree edge would over-approximate that delta and could nest [any] edges. + */ + private fun filterStartsWithAny(accessPath: AccessPath.AccessNode): AccessNode? { + if (matchPathWithAny(accessPath).isEmpty()) return null + + val parentAccessors = IntArrayList() + var node = this + var pathNode: AccessPath.AccessNode = accessPath + while (pathNode.accessor != ANY_ACCESSOR_IDX) { + node = node.getChild(pathNode.accessor) ?: return null + parentAccessors.add(pathNode.accessor) + pathNode = pathNode.next ?: break + } + + return parentAccessors.foldRight(node, ::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/MethodTreeAccessPathSubscription.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/MethodTreeAccessPathSubscription.kt index beabda52bb..b427408304 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/MethodTreeAccessPathSubscription.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/MethodTreeAccessPathSubscription.kt @@ -8,6 +8,7 @@ import org.opentaint.dataflow.ap.ifds.access.common.CommonFactNDEdgeSubBuilder import org.opentaint.dataflow.ap.ifds.access.common.CommonZeroEdgeSubBuilder import org.opentaint.dataflow.ap.ifds.access.common.ndf2f.DefaultNDF2FSubStorage import org.opentaint.dataflow.ap.ifds.access.util.AccessorIdx +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.ANY_ACCESSOR_IDX import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.FINAL_ACCESSOR_IDX import org.opentaint.dataflow.util.PersistentBitSet.Companion.emptyPersistentBitSet import org.opentaint.dataflow.util.SoftReferenceManager @@ -276,6 +277,9 @@ private class AccessTreeIndexImpl { var currentPath = path while (true) { + // [any] matches zero or more accessors: every tree with the concrete prefix is a candidate + if (currentPath.accessor == ANY_ACCESSOR_IDX) return currentNode.index + currentNode = currentNode.findChild(currentPath.accessor) ?: return null currentPath = currentPath.next ?: return currentNode.index } 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 0000000000..769ac3b579 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AnyAccessPathTest.kt @@ -0,0 +1,425 @@ +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 + + /** Built without normalization, so a trailing [any] is kept. */ + private fun rawInitial(vararg accessors: Accessor): AccessPath { + val node = accessors.foldRight(null as AccessPath.AccessNode?) { a, n -> + AccessPath.AccessNode(manager, with(manager) { a.idx }, n) + } + return AccessPath(manager, base, node, ExclusionSet.Empty) + } + + 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 `any over the abstract path is the abstract path`() { + val abstract = manager.mostAbstractInitialAp(base) + assertSame(abstract, abstract.prependAccessor(AnyAccessor)) + assertEquals(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 = rawInitial(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 ---------- */ + + @Test + fun `any matches zero accessors`() { + val tree = abstractTree(A, F) + assertTrue(tree.hasEmptyDeltaFor(initial(A, AnyAccessor, F))) + assertTrue(tree.contains(initial(A, AnyAccessor, F))) + } + + @Test + fun `any matches several covered accessors`() { + val tree = abstractTree(A, G, H, F) + assertTrue(tree.hasEmptyDeltaFor(initial(A, AnyAccessor, F))) + assertTrue(tree.contains(initial(A, AnyAccessor, F))) + assertTrue(tree.contains(initial(AnyAccessor, F))) + } + + @Test + fun `any does not match an uncovered accessor`() { + val tree = abstractTree(A, NOT_COVERED, F) + assertTrue(tree.delta(initial(A, AnyAccessor, F)).isEmpty()) + assertFalse(tree.contains(initial(A, AnyAccessor, F))) + } + + @Test + fun `delta merges every match`() { + val tree = merged(finalTree(A, F, X), finalTree(A, G, F, Y), finalTree(A, X)) + val delta = tree.deltaNode(initial(A, AnyAccessor, F)) + assertNotNull(delta) + + assertEquals(merged(finalTree(X), finalTree(Y)).access, delta) + assertFalse(tree.contains(initial(A, AnyAccessor, F))) + } + + @Test + fun `trailing any collects every suffix`() { + val tree = merged(finalTree(A, F, X), finalTree(A, MARK)) + val delta = tree.deltaNode(rawInitial(A, AnyAccessor)) + assertNotNull(delta) + + val expected = merged(finalTree(F, X), finalTree(X), finalTree(MARK), finalTree()).access + assertEquals(expected, delta) + } + + @Test + fun `any path matches a tree any edge`() { + val tree = abstractTree(A, AnyAccessor, MARK) + assertTrue(tree.hasEmptyDeltaFor(initial(A, AnyAccessor, MARK))) + assertTrue(tree.contains(initial(A, AnyAccessor, MARK))) + assertFalse(tree.contains(initial(A, MARK, AnyAccessor, F))) + } + + @Test + fun `any path matches final`() { + val tree = finalTree(A, G, H) + assertTrue(tree.hasEmptyDeltaFor(finalInitial(A, AnyAccessor))) + assertTrue(tree.contains(finalInitial(A, AnyAccessor))) + assertFalse(abstractTree(A, G).contains(finalInitial(A, AnyAccessor))) + assertFalse(finalTree(A, MARK).contains(finalInitial(A, AnyAccessor))) + } + + @Test + fun `delta with any applies the exclusions to the merged node`() { + val tree = merged(finalTree(A, F, X), finalTree(A, G, 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 keeps the delta exact`() { + val tree = merged(finalTree(A, F, X), finalTree(A, G, F, Y), finalTree(H)) + val path = initial(A, AnyAccessor, F) + + val filtered = tree.access.filterStartsWith(path.access) + assertNotNull(filtered) + assertFalse(filtered.contains(with(manager) { H.idx })) + + val filteredTree = AccessTree(manager, base, filtered, ExclusionSet.Empty) + assertEquals(tree.delta(path), filteredTree.delta(path)) + } + + @Test + fun `filterStartsWith with any drops trees without a match`() { + val tree = merged(finalTree(A, NOT_COVERED, F), finalTree(H)) + assertNull(tree.access.filterStartsWith(initial(A, AnyAccessor, F).access)) + assertNull(tree.access.filterStartsWith(initial(G, AnyAccessor).access)) + assertNotNull(finalTree(A, G, 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 + } +} From 1f99c1cf26dfb56827f97a60238437ea0a5408ab Mon Sep 17 00:00:00 2001 From: Valentyn Sobol <8640896+Saloed@users.noreply.github.com> Date: Thu, 1 Oct 2026 22:34:51 +0000 Subject: [PATCH 2/5] feat(dataflow): Split tree summary storage into complete and any edges Store edges with [any] in the initial or the final separately from complete edges in the F2F, Z2F and ND-F2F tree summary storages, splitting mixed finals with splitAny(). Tries that can hold [any] keys (any-edge F2F, fact side effects, side-effect requirements) now return everything under an [any] key for a pattern that reaches it. The any-suffix matcher skips nested [any] edges instead of failing a check. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_018R4xeH4QFAH7RrPcKFoQGZ --- .../ap/ifds/access/tree/AccessBasedStorage.kt | 23 +- .../access/tree/AccessTreeAnySuffixMatcher.kt | 6 +- .../FactSideEffectSummariesTreeApStorage.kt | 10 +- .../tree/MethodFinalTreeApSummariesStorage.kt | 30 +- .../tree/MethodInitialToFinalApSummaries.kt | 82 +++- .../tree/MethodNDInitialToFinalApSummaries.kt | 71 +++- .../SideEffectRequirementTreeApStorage.kt | 10 +- .../ifds/access/tree/AnySummaryStorageTest.kt | 381 ++++++++++++++++++ 8 files changed, 564 insertions(+), 49 deletions(-) create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AnySummaryStorageTest.kt diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessBasedStorage.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessBasedStorage.kt index 68039f863d..bd6d16ecd9 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessBasedStorage.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessBasedStorage.kt @@ -3,6 +3,7 @@ package org.opentaint.dataflow.ap.ifds.access.tree import it.unimi.dsi.fastutil.ints.IntArrayList import org.opentaint.dataflow.ap.ifds.access.tree.AccessPath.AccessNode.Companion.createNodeFromAccessors import org.opentaint.dataflow.ap.ifds.access.util.AccessorIdx +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.ANY_ACCESSOR_IDX import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.FINAL_ACCESSOR_IDX import org.opentaint.dataflow.util.forEachEntry import org.opentaint.dataflow.util.forEachInt @@ -46,31 +47,43 @@ abstract class AccessBasedStorage>( return storage as S } + /** + * A trie whose keys may contain [any] must enable this (checked once per [filterContains] at the root). + * Then every node under an [any] key is a candidate for any pattern that reaches the key's parent: + * an over-approximation, the exact match is done later by [AccessTree.delta]. + */ + protected open val matchesAnyKeys: Boolean get() = false + fun filterContains(pattern: AccessTree.AccessNode): Sequence { val nodes = mutableListOf() - collectNodesContains(pattern, nodes) + collectNodesContains(pattern, nodes, matchesAnyKeys) return nodes.asSequence() } - private fun collectNodesContains(pattern: AccessTree.AccessNode, nodes: MutableList) { + private fun collectNodesContains(pattern: AccessTree.AccessNode, nodes: MutableList, anyKeys: Boolean) { @Suppress("UNCHECKED_CAST") nodes.add(this as S) + if (anyKeys) { + children.get(ANY_ACCESSOR_IDX)?.let { nodes += it.allNodes() } + } + if (pattern.isFinal) { children.get(FINAL_ACCESSOR_IDX)?.let { nodes.add(it) } } pattern.forEachAccessor { accessor, accessorPattern -> - collectNodesContainsAccessor(accessorPattern, accessor, nodes) + collectNodesContainsAccessor(accessorPattern, accessor, nodes, anyKeys) } } open fun collectNodesContainsAccessor( pattern: AccessTree.AccessNode, accessor: AccessorIdx, - nodes: MutableList + nodes: MutableList, + anyKeys: Boolean, ) { - children.get(accessor)?.collectNodesContains(pattern, nodes) + children.get(accessor)?.collectNodesContains(pattern, nodes, anyKeys) } fun allNodes(): Sequence { diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessTreeAnySuffixMatcher.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessTreeAnySuffixMatcher.kt index 5fd19a61b8..7ca7c72437 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessTreeAnySuffixMatcher.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessTreeAnySuffixMatcher.kt @@ -61,18 +61,19 @@ class AccessTreeAnySuffixMatcher(suffixNode: AccessTree.AccessNode) { val notCoveredByAny: Int?, ) + // A nested [any] edge of the suffix is left out of the trie: the trie then matches a subset of the + // suffix language, so getNonMatchingNode trims less but never trims a path the suffix does not cover. init { if (suffixNode.accessors != null && suffixNode.accessorNodes != null) { val unprocessed = ArrayDeque() suffixNode.forEachAccessor { accessor, accessorNode -> + if (accessor == ANY_ACCESSOR_IDX) return@forEachAccessor val notCoveredByAny = if (accessor.coveredByAny()) null else 1 unprocessed.addLast(RawNodeWithParent(accessorNode, accessor, root, 1, notCoveredByAny)) } while (unprocessed.isNotEmpty()) { val (node, accessor, triePar, depth, notCoveredByAny) = unprocessed.removeFirst() - // disallowing [any]->...->[any] - check(accessor != ANY_ACCESSOR_IDX) val curNotCoveredByAny = when { notCoveredByAny != null -> notCoveredByAny @@ -104,6 +105,7 @@ class AccessTreeAnySuffixMatcher(suffixNode: AccessTree.AccessNode) { triePar.children.put(accessor, newTrieNode) node.forEachAccessor { accessor, accessorNode -> + if (accessor == ANY_ACCESSOR_IDX) return@forEachAccessor unprocessed.addLast(RawNodeWithParent(accessorNode, accessor, newTrieNode, depth + 1, curNotCoveredByAny)) } } diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/FactSideEffectSummariesTreeApStorage.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/FactSideEffectSummariesTreeApStorage.kt index 7ee3f5e361..dbe1d3d747 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/FactSideEffectSummariesTreeApStorage.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/FactSideEffectSummariesTreeApStorage.kt @@ -21,10 +21,16 @@ private class TaintedSESummariesInitialApStorage(val apManager: TreeApManager) : AccessBasedStorage(apManager) { private var current: TaintedSESummariesMergingStorage? = null + // set on the root once a key with [any] is stored + private var hasAnyKeys = false + override val matchesAnyKeys: Boolean get() = hasAnyKeys + override fun createStorage() = TaintedSESummariesInitialApStorage(apManager) - fun getOrCreate(initialAccess: AccessPath.AccessNode?): TaintedSESummariesMergingStorage = - getOrCreateNode(initialAccess).getOrCreateCurrent(initialAccess) + fun getOrCreate(initialAccess: AccessPath.AccessNode?): TaintedSESummariesMergingStorage { + if (initialAccess != null && initialAccess.containsAny) hasAnyKeys = true + return getOrCreateNode(initialAccess).getOrCreateCurrent(initialAccess) + } fun filterSummariesTo(dst: MutableList>, containsPattern: AccessTreeNode) { filterContains(containsPattern).forEach { node -> 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 9f0715063b..08831f299a 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 5c93912759..8364a45fd8 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 @@ -38,14 +38,15 @@ private abstract class F2FInitialStorage, S : Modi override fun collectNodesContainsAccessor( pattern: AccessTreeNode, accessor: AccessorIdx, - nodes: MutableList + nodes: MutableList, + anyKeys: Boolean, ) { if (accessor == ANY_ACCESSOR_IDX) { nodes += allNodes() return } - super.collectNodesContainsAccessor(pattern, accessor, nodes) + super.collectNodesContainsAccessor(pattern, accessor, nodes, anyKeys) } fun collectAllSummariesTo(dst: MutableList>) { @@ -59,8 +60,11 @@ private abstract class F2FInitialStorage, S : Modi private class MethodTaintedSummariesInitialApStorage( apManager: TreeApManager, + private val anyKeys: Boolean, ) : F2FInitialStorage(apManager) { - override fun createStorage() = MethodTaintedSummariesInitialApStorage(manager) + override fun createStorage() = MethodTaintedSummariesInitialApStorage(manager, anyKeys) + + override val matchesAnyKeys: Boolean get() = anyKeys fun getOrCreate(initialAccess: AccessPath.AccessNode?): MethodTaintedSummariesMergingStorage = getOrCreateNode(initialAccess).getOrCreateCurrent(initialAccess) @@ -178,8 +182,12 @@ 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) + private val nonUniverseAccessPath = MethodTaintedSummariesInitialApStorage(apManager, anyKeys = false) + + // any-edges: [any] in the initial (whole edge), or the [any] part of a final + private val anyEdges = MethodTaintedSummariesInitialApStorage(apManager, anyKeys = true) override fun add( edges: List>, @@ -195,12 +203,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 +275,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 db3505dda3..4403ad4ff9 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/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/SideEffectRequirementTreeApStorage.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/SideEffectRequirementTreeApStorage.kt index bbbeb896b0..fbe76ba9ba 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/SideEffectRequirementTreeApStorage.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/SideEffectRequirementTreeApStorage.kt @@ -50,10 +50,16 @@ private class SideEffectRequirementStorage( ) : AccessBasedStorage(apManager) { var requirement: AccessPath? = null + // set on the root once a requirement with [any] is stored + private var hasAnyKeys = false + override val matchesAnyKeys: Boolean get() = hasAnyKeys + override fun createStorage() = SideEffectRequirementStorage(apManager) - fun mergeAdd(requirement: AccessPath): SideEffectRequirementStorage? = - getOrCreateNode(requirement.access).mergeAddCurrent(requirement) + fun mergeAdd(requirement: AccessPath): SideEffectRequirementStorage? { + if (requirement.access?.containsAny == true) hasAnyKeys = true + return getOrCreateNode(requirement.access).mergeAddCurrent(requirement) + } fun findRequirements(access: AccessTree.AccessNode): Sequence = filterContains(access).mapNotNull { it.requirement } 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 0000000000..57acbaf53e --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AnySummaryStorageTest.kt @@ -0,0 +1,381 @@ +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 `merging trees with nested any does not crash`() { + // a.[any].f.[any].g.$ : what a path with two [any]s becomes as a tree + val nested = merged(finalTree(A, AnyAccessor, F, AnyAccessor, G)) + val other = finalTree(A, AnyAccessor, K) + + val storage = MergingTreeSummaryStorage(manager) + assertTrue(storage.add(nested.access)) + assertTrue(storage.add(other.access)) + + val edges = AccessTree(manager, base, storage.edges()!!, ExclusionSet.Empty) + assertTrue(edges.contains(finalInitialOf(initial(A, AnyAccessor, F, AnyAccessor, G)))) + assertTrue(edges.contains(finalInitialOf(initial(A, AnyAccessor, K)))) + + // the other direction of the merge builds the matcher from the nested side + val reversed = MergingTreeSummaryStorage(manager) + assertTrue(reversed.add(other.access)) + assertTrue(reversed.add(nested.access)) + assertEquals(storage.edges(), reversed.edges()) + } + + @Test + fun `nested any is still trimmed only where it is covered`() { + // a.[any].f.[any].g.$ covers a.x.f.g.$ (x covered), so the merge may drop it, but it must keep a.m... + val nested = finalTree(A, AnyAccessor, F, AnyAccessor, G).access + val covered = finalTree(A, H, F, G).access + val notCovered = finalTree(A, F, K).access + + val merged = nested.mergeAdd(covered).mergeAdd(notCovered) + val tree = AccessTree(manager, base, merged, ExclusionSet.Empty) + + assertTrue(tree.contains(finalInitialOf(initial(A, F, K)))) + assertTrue(tree.contains(finalInitialOf(initial(A, AnyAccessor, F, AnyAccessor, G)))) + } + + 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 through the any key`() { + 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()) + + // a concrete caller fact a.g.f.$ walks a, then reaches the [any] key + assertEquals(listOf(edge), storage.find(finalTree(A, G, F))) + assertEquals(listOf(edge), storage.find(finalTree(A, F))) + assertTrue(storage.find(finalTree(H, F)).isEmpty()) + assertEquals(listOf(edge), storage.find(null)) + } + + @Test + fun `f2f any key at the root is found by any pattern`() { + val storage = f2fStorage() + val edge = storage.add(initial(AnyAccessor, F) to finalTree(G)).single() + assertEquals(listOf(edge), storage.find(finalTree(H, H, F))) + } + + @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 nested any finals 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 through the any key`() { + 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, G, F)) + assertEquals(listOf(summary), found) + + val foundComplete = mutableListOf() + storage.filterTaintedTo(foundComplete, finalTree(H, G)) + assertEquals(listOf(complete), foundComplete) + } + + @Test + fun `side effect requirement with any is found through the any key`() { + 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, G, F)) + assertEquals(listOf(requirement), found) + + val notFound = mutableListOf() + storage.filterTo(notFound, finalTree(H, 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") + } + } + } +} From ff5b855d609d87560fe5c9f997f5da9fc884e86f Mon Sep 17 00:00:00 2001 From: Valentyn Sobol <8640896+Saloed@users.noreply.github.com> Date: Fri, 2 Oct 2026 16:51:10 +0000 Subject: [PATCH 3/5] fix(dataflow): Match [any] in access paths syntactically An [any] in an AccessPath is now matched exactly everywhere: against another path it is an ordinary symbol, and against a tree (delta, contains, filterStartsWith) it matches only the tree's own [any] edge. Drop the semantic closure matcher and the over-approximating [any]-key lookups in the summary, side-effect and subscription tries; a plain trie walk is now exact. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_018R4xeH4QFAH7RrPcKFoQGZ --- .../ap/ifds/access/tree/AccessBasedStorage.kt | 23 +---- .../ap/ifds/access/tree/AccessPath.kt | 17 +--- .../ap/ifds/access/tree/AccessTree.kt | 95 ++----------------- .../FactSideEffectSummariesTreeApStorage.kt | 10 +- .../tree/MethodInitialToFinalApSummaries.kt | 14 +-- .../tree/MethodTreeAccessPathSubscription.kt | 4 - .../SideEffectRequirementTreeApStorage.kt | 10 +- .../ap/ifds/access/tree/AnyAccessPathTest.kt | 86 ++++++----------- .../ifds/access/tree/AnySummaryStorageTest.kt | 28 +++--- 9 files changed, 74 insertions(+), 213 deletions(-) diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessBasedStorage.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessBasedStorage.kt index bd6d16ecd9..68039f863d 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessBasedStorage.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessBasedStorage.kt @@ -3,7 +3,6 @@ package org.opentaint.dataflow.ap.ifds.access.tree import it.unimi.dsi.fastutil.ints.IntArrayList import org.opentaint.dataflow.ap.ifds.access.tree.AccessPath.AccessNode.Companion.createNodeFromAccessors import org.opentaint.dataflow.ap.ifds.access.util.AccessorIdx -import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.ANY_ACCESSOR_IDX import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.FINAL_ACCESSOR_IDX import org.opentaint.dataflow.util.forEachEntry import org.opentaint.dataflow.util.forEachInt @@ -47,43 +46,31 @@ abstract class AccessBasedStorage>( return storage as S } - /** - * A trie whose keys may contain [any] must enable this (checked once per [filterContains] at the root). - * Then every node under an [any] key is a candidate for any pattern that reaches the key's parent: - * an over-approximation, the exact match is done later by [AccessTree.delta]. - */ - protected open val matchesAnyKeys: Boolean get() = false - fun filterContains(pattern: AccessTree.AccessNode): Sequence { val nodes = mutableListOf() - collectNodesContains(pattern, nodes, matchesAnyKeys) + collectNodesContains(pattern, nodes) return nodes.asSequence() } - private fun collectNodesContains(pattern: AccessTree.AccessNode, nodes: MutableList, anyKeys: Boolean) { + private fun collectNodesContains(pattern: AccessTree.AccessNode, nodes: MutableList) { @Suppress("UNCHECKED_CAST") nodes.add(this as S) - if (anyKeys) { - children.get(ANY_ACCESSOR_IDX)?.let { nodes += it.allNodes() } - } - if (pattern.isFinal) { children.get(FINAL_ACCESSOR_IDX)?.let { nodes.add(it) } } pattern.forEachAccessor { accessor, accessorPattern -> - collectNodesContainsAccessor(accessorPattern, accessor, nodes, anyKeys) + collectNodesContainsAccessor(accessorPattern, accessor, nodes) } } open fun collectNodesContainsAccessor( pattern: AccessTree.AccessNode, accessor: AccessorIdx, - nodes: MutableList, - anyKeys: Boolean, + nodes: MutableList ) { - children.get(accessor)?.collectNodesContains(pattern, nodes, anyKeys) + children.get(accessor)?.collectNodesContains(pattern, nodes) } fun allNodes(): Sequence { 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 3e43dd7620..9b057eedbf 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 @@ -44,8 +44,8 @@ class AccessPath( override fun getAllAccessors(): Set = access?.allAccessors().orEmpty() - // readAccessor, startsWithAccessor, getStartAccessors, clearAccessor and delta(AccessPath) are - // syntactic: an [any] in the path is compared as an ordinary accessor symbol. + // [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 @@ -145,11 +145,7 @@ class AccessPath( return listOfNotNull(node.filter(other.exclusions)?.let { AccessPathDelta.Delta(it) }) } - /** - * An [any] step of this path only follows the raw [any] edge of the tree (or splits at an abstract - * tree node, as a concrete accessor does). It does not unroll the [any] over the tree's concrete - * edges, so a tree that holds the [any] content only as concrete accessors is not matched. - */ + // 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 @@ -329,13 +325,6 @@ class AccessPath( return node } - fun endsWithFinal(): Boolean { - var node = this - while (true) { - node = node.next ?: return node.accessor == FINAL_ACCESSOR_IDX - } - } - fun accessorList(): List = toList().map { with(manager) { it.accessor } } /** All accessors of the path except [any], consistent with [AccessTree.AccessNode.collectAccessorsTo]. */ 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 d963446e3d..f23eb87757 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 @@ -180,34 +180,18 @@ class AccessTree( if (base != other.base) return emptyList() + var node = access val access = other.access - if (access != null && access.containsAny) return deltaWithAny(access, other.exclusions) - - var node = this.access access?.toList()?.forEachInt { accessor -> if (accessor == FINAL_ACCESSOR_IDX) { if (!node.isFinal) return emptyList() return listOf(EmptyAccessTreeDelta(deepAccessorExclusion = null)) } - node = node.getChild(accessor) ?: return emptyList() + node = node.getPathChild(accessor) ?: return emptyList() } - return deltaAtNode(node, other.exclusions) - } - - private fun deltaWithAny(path: AccessPath.AccessNode, exclusions: ExclusionSet): List { - val matched = access.matchPathWithAny(path) - if (matched.isEmpty()) return emptyList() - - if (path.endsWithFinal()) return listOf(EmptyAccessTreeDelta(deepAccessorExclusion = null)) - - val node = matched.reduce { acc, n -> acc.mergeAdd(n) } - return deltaAtNode(node, exclusions) - } - - private fun deltaAtNode(node: AccessNode, exclusions: ExclusionSet): List { - val filteredNode = when (val exclusion = exclusions) { + val filteredNode = when (val exclusion = other.exclusions) { ExclusionSet.Empty -> node is ExclusionSet.Concrete -> node.filter(exclusion) ExclusionSet.Universe -> error("Unexpected universe exclusion in initial fact") @@ -526,59 +510,20 @@ class AccessTree( return isAbstract } - if (otherAccess.containsAny) { - val matched = matchPathWithAny(otherAccess) - return if (otherAccess.endsWithFinal()) matched.isNotEmpty() else matched.any { it.isAbstract } - } - 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 } /** - * Semantic match of a path with [any]: every [any] of the path matches zero or more accessors covered - * by any, so the current position becomes a set of nodes. Returns the nodes reached after the whole - * path; a FINAL step maps every final node to [TreeApManager.finalNode]. + * 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. */ - internal fun matchPathWithAny(path: AccessPath.AccessNode): List { - var current: List = listOf(this) - var pathNode: AccessPath.AccessNode = path - while (true) { - val accessor = pathNode.accessor - current = when (accessor) { - ANY_ACCESSOR_IDX -> anyClosure(current) - FINAL_ACCESSOR_IDX -> if (current.any { it.isFinal }) listOf(manager.finalNode) else emptyList() - else -> current.mapNotNull { it.getChild(accessor) } - } - - if (current.isEmpty()) return current - pathNode = pathNode.next ?: return current - } - } - - // Closure over raw edges that [any] can match: covered accessors and [any] edges themselves. - // getChild must not be used here: it allocates fresh nodes and the closure would not terminate. - private fun anyClosure(nodes: List): List { - val visited = IdentityHashMap() - val result = mutableListOf() - val stack = nodes.toMutableList() - while (stack.isNotEmpty()) { - val node = stack.removeLast() - if (visited.put(node, Unit) != null) continue - result.add(node) - - node.forEachAccessor { accessor, child -> - if (accessor == ANY_ACCESSOR_IDX || manager.isCoveredByAny(accessor)) { - stack.add(child) - } - } - } - return result - } + fun getPathChild(accessor: AccessorIdx): AccessNode? = + if (accessor == ANY_ACCESSOR_IDX) getAnyChild() else getChild(accessor) sealed interface MatchResult { data object NotMatched : MatchResult @@ -1416,8 +1361,6 @@ class AccessTree( fun filterStartsWith(accessPath: AccessPath.AccessNode?): AccessNode? { if (accessPath == null) return this - if (accessPath.containsAny) return filterStartsWithAny(accessPath) - if (maxDepth < accessPath.size) { return null } @@ -1438,7 +1381,7 @@ class AccessTree( } else -> { - filteredTreeNode.getChild(accessor) + filteredTreeNode.getPathChild(accessor) ?.also { parentAccessors.add(accessor) } ?: return null } @@ -1454,26 +1397,6 @@ class AccessTree( return parentAccessors.foldRight(filteredTreeNode, ::create) } - /** - * The result keeps only the concrete path prefix before the first [any] and the whole subtree below it, - * so its delta for [accessPath] is exactly the delta of this tree. Rebuilding the [any] part of the path - * as a tree edge would over-approximate that delta and could nest [any] edges. - */ - private fun filterStartsWithAny(accessPath: AccessPath.AccessNode): AccessNode? { - if (matchPathWithAny(accessPath).isEmpty()) return null - - val parentAccessors = IntArrayList() - var node = this - var pathNode: AccessPath.AccessNode = accessPath - while (pathNode.accessor != ANY_ACCESSOR_IDX) { - node = node.getChild(pathNode.accessor) ?: return null - parentAccessors.add(pathNode.accessor) - pathNode = pathNode.next ?: break - } - - return parentAccessors.foldRight(node, ::create) - } - class AnySplit(val complete: AccessNode?, val any: AccessNode?) /** diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/FactSideEffectSummariesTreeApStorage.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/FactSideEffectSummariesTreeApStorage.kt index dbe1d3d747..7ee3f5e361 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/FactSideEffectSummariesTreeApStorage.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/FactSideEffectSummariesTreeApStorage.kt @@ -21,16 +21,10 @@ private class TaintedSESummariesInitialApStorage(val apManager: TreeApManager) : AccessBasedStorage(apManager) { private var current: TaintedSESummariesMergingStorage? = null - // set on the root once a key with [any] is stored - private var hasAnyKeys = false - override val matchesAnyKeys: Boolean get() = hasAnyKeys - override fun createStorage() = TaintedSESummariesInitialApStorage(apManager) - fun getOrCreate(initialAccess: AccessPath.AccessNode?): TaintedSESummariesMergingStorage { - if (initialAccess != null && initialAccess.containsAny) hasAnyKeys = true - return getOrCreateNode(initialAccess).getOrCreateCurrent(initialAccess) - } + fun getOrCreate(initialAccess: AccessPath.AccessNode?): TaintedSESummariesMergingStorage = + getOrCreateNode(initialAccess).getOrCreateCurrent(initialAccess) fun filterSummariesTo(dst: MutableList>, containsPattern: AccessTreeNode) { filterContains(containsPattern).forEach { node -> 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 8364a45fd8..fd496bd474 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 @@ -38,15 +38,14 @@ private abstract class F2FInitialStorage, S : Modi override fun collectNodesContainsAccessor( pattern: AccessTreeNode, accessor: AccessorIdx, - nodes: MutableList, - anyKeys: Boolean, + nodes: MutableList ) { if (accessor == ANY_ACCESSOR_IDX) { nodes += allNodes() return } - super.collectNodesContainsAccessor(pattern, accessor, nodes, anyKeys) + super.collectNodesContainsAccessor(pattern, accessor, nodes) } fun collectAllSummariesTo(dst: MutableList>) { @@ -60,11 +59,8 @@ private abstract class F2FInitialStorage, S : Modi private class MethodTaintedSummariesInitialApStorage( apManager: TreeApManager, - private val anyKeys: Boolean, ) : F2FInitialStorage(apManager) { - override fun createStorage() = MethodTaintedSummariesInitialApStorage(manager, anyKeys) - - override val matchesAnyKeys: Boolean get() = anyKeys + override fun createStorage() = MethodTaintedSummariesInitialApStorage(manager) fun getOrCreate(initialAccess: AccessPath.AccessNode?): MethodTaintedSummariesMergingStorage = getOrCreateNode(initialAccess).getOrCreateCurrent(initialAccess) @@ -184,10 +180,10 @@ private class MethodTaintedSummariesGroupedByFactStorage( ) : CommonF2FSummary.Storage { // complete edges: no [any] in the initial nor in the final private val idEdges = MethodTaintedSummariesIdStorage(apManager) - private val nonUniverseAccessPath = MethodTaintedSummariesInitialApStorage(apManager, anyKeys = false) + 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, anyKeys = true) + private val anyEdges = MethodTaintedSummariesInitialApStorage(apManager) override fun add( edges: List>, diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/MethodTreeAccessPathSubscription.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/MethodTreeAccessPathSubscription.kt index b427408304..beabda52bb 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/MethodTreeAccessPathSubscription.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/MethodTreeAccessPathSubscription.kt @@ -8,7 +8,6 @@ import org.opentaint.dataflow.ap.ifds.access.common.CommonFactNDEdgeSubBuilder import org.opentaint.dataflow.ap.ifds.access.common.CommonZeroEdgeSubBuilder import org.opentaint.dataflow.ap.ifds.access.common.ndf2f.DefaultNDF2FSubStorage import org.opentaint.dataflow.ap.ifds.access.util.AccessorIdx -import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.ANY_ACCESSOR_IDX import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.FINAL_ACCESSOR_IDX import org.opentaint.dataflow.util.PersistentBitSet.Companion.emptyPersistentBitSet import org.opentaint.dataflow.util.SoftReferenceManager @@ -277,9 +276,6 @@ private class AccessTreeIndexImpl { var currentPath = path while (true) { - // [any] matches zero or more accessors: every tree with the concrete prefix is a candidate - if (currentPath.accessor == ANY_ACCESSOR_IDX) return currentNode.index - currentNode = currentNode.findChild(currentPath.accessor) ?: return null currentPath = currentPath.next ?: return currentNode.index } diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/SideEffectRequirementTreeApStorage.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/SideEffectRequirementTreeApStorage.kt index fbe76ba9ba..bbbeb896b0 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/SideEffectRequirementTreeApStorage.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/SideEffectRequirementTreeApStorage.kt @@ -50,16 +50,10 @@ private class SideEffectRequirementStorage( ) : AccessBasedStorage(apManager) { var requirement: AccessPath? = null - // set on the root once a requirement with [any] is stored - private var hasAnyKeys = false - override val matchesAnyKeys: Boolean get() = hasAnyKeys - override fun createStorage() = SideEffectRequirementStorage(apManager) - fun mergeAdd(requirement: AccessPath): SideEffectRequirementStorage? { - if (requirement.access?.containsAny == true) hasAnyKeys = true - return getOrCreateNode(requirement.access).mergeAddCurrent(requirement) - } + fun mergeAdd(requirement: AccessPath): SideEffectRequirementStorage? = + getOrCreateNode(requirement.access).mergeAddCurrent(requirement) fun findRequirements(access: AccessTree.AccessNode): Sequence = filterContains(access).mapNotNull { it.requirement } 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 index 769ac3b579..b1b404e0b3 100644 --- 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 @@ -200,70 +200,49 @@ class AnyAccessPathTest { assertEquals(finalTree(A, AnyAccessor, F), withNode) } - /* ---------- AccessTree.delta / contains with [any] in the path ---------- */ + /* ---------- AccessTree.delta / contains with [any] in the path: exact match ---------- */ @Test - fun `any matches zero accessors`() { - val tree = abstractTree(A, F) - assertTrue(tree.hasEmptyDeltaFor(initial(A, AnyAccessor, F))) - assertTrue(tree.contains(initial(A, AnyAccessor, F))) + 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 matches several covered accessors`() { - val tree = abstractTree(A, G, H, F) + 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))) - assertTrue(tree.contains(initial(AnyAccessor, F))) - } - - @Test - fun `any does not match an uncovered accessor`() { - val tree = abstractTree(A, NOT_COVERED, F) - assertTrue(tree.delta(initial(A, AnyAccessor, F)).isEmpty()) - assertFalse(tree.contains(initial(A, AnyAccessor, F))) - } - - @Test - fun `delta merges every match`() { - val tree = merged(finalTree(A, F, X), finalTree(A, G, F, Y), finalTree(A, X)) - val delta = tree.deltaNode(initial(A, AnyAccessor, F)) - assertNotNull(delta) - - assertEquals(merged(finalTree(X), finalTree(Y)).access, delta) - assertFalse(tree.contains(initial(A, AnyAccessor, F))) + assertFalse(tree.contains(initial(A, AnyAccessor, G))) + assertFalse(tree.contains(initial(AnyAccessor, F))) } @Test - fun `trailing any collects every suffix`() { - val tree = merged(finalTree(A, F, X), finalTree(A, MARK)) - val delta = tree.deltaNode(rawInitial(A, AnyAccessor)) - assertNotNull(delta) - - val expected = merged(finalTree(F, X), finalTree(X), finalTree(MARK), finalTree()).access - assertEquals(expected, delta) + 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 `any path matches a tree any edge`() { - val tree = abstractTree(A, AnyAccessor, MARK) - assertTrue(tree.hasEmptyDeltaFor(initial(A, AnyAccessor, MARK))) - assertTrue(tree.contains(initial(A, AnyAccessor, MARK))) - assertFalse(tree.contains(initial(A, MARK, AnyAccessor, F))) + 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(rawInitial(A, AnyAccessor))) } @Test - fun `any path matches final`() { - val tree = finalTree(A, G, H) - assertTrue(tree.hasEmptyDeltaFor(finalInitial(A, AnyAccessor))) - assertTrue(tree.contains(finalInitial(A, AnyAccessor))) - assertFalse(abstractTree(A, G).contains(finalInitial(A, AnyAccessor))) - assertFalse(finalTree(A, MARK).contains(finalInitial(A, AnyAccessor))) + 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 to the merged node`() { - val tree = merged(finalTree(A, F, X), finalTree(A, G, F, Y)) + 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) } @@ -282,24 +261,21 @@ class AnyAccessPathTest { /* ---------- filterStartsWith ---------- */ @Test - fun `filterStartsWith with any keeps the delta exact`() { - val tree = merged(finalTree(A, F, X), finalTree(A, G, F, Y), finalTree(H)) + 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) - assertNotNull(filtered) - assertFalse(filtered.contains(with(manager) { H.idx })) + assertEquals(finalTree(A, AnyAccessor, F, X).access, filtered) - val filteredTree = AccessTree(manager, base, filtered, ExclusionSet.Empty) + val filteredTree = AccessTree(manager, base, filtered!!, ExclusionSet.Empty) assertEquals(tree.delta(path), filteredTree.delta(path)) } @Test - fun `filterStartsWith with any drops trees without a match`() { - val tree = merged(finalTree(A, NOT_COVERED, F), finalTree(H)) - assertNull(tree.access.filterStartsWith(initial(A, AnyAccessor, F).access)) - assertNull(tree.access.filterStartsWith(initial(G, AnyAccessor).access)) - assertNotNull(finalTree(A, G, F).access.filterStartsWith(initial(AnyAccessor, F).access)) + 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 ---------- */ 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 index 57acbaf53e..1b473723b5 100644 --- 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 @@ -183,7 +183,7 @@ class AnySummaryStorageTest { } @Test - fun `f2f initial with any is stored whole and found through the any key`() { + 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)) @@ -193,18 +193,20 @@ class AnySummaryStorageTest { assertEquals(initialAp, edge.initialFactAp) assertEquals(final.access, edge.factAp.node()) - // a concrete caller fact a.g.f.$ walks a, then reaches the [any] key - assertEquals(listOf(edge), storage.find(finalTree(A, G, F))) - assertEquals(listOf(edge), storage.find(finalTree(A, F))) + // [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 by any pattern`() { + 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(H, H, F))) + assertEquals(listOf(edge), storage.find(finalTree(AnyAccessor, F))) + assertTrue(storage.find(finalTree(H, H, F)).isEmpty()) } @Test @@ -330,7 +332,7 @@ class AnySummaryStorageTest { /* ---------- side effects ---------- */ @Test - fun `fact side effect with any initial is found through the any key`() { + 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) @@ -340,26 +342,30 @@ class AnySummaryStorageTest { assertEquals(setOf(summary, complete), added.toSet()) val found = mutableListOf() - storage.filterTaintedTo(found, finalTree(A, G, F)) + 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 through the any key`() { + 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, G, F)) + storage.filterTo(found, finalTree(A, AnyAccessor, F)) assertEquals(listOf(requirement), found) val notFound = mutableListOf() - storage.filterTo(notFound, finalTree(H, F)) + storage.filterTo(notFound, finalTree(A, G, F)) assertTrue(notFound.isEmpty()) } From 4974ae6f2be9a5cc1c9296d695ef34aff15384fd Mon Sep 17 00:00:00 2001 From: Valentyn Sobol <8640896+Saloed@users.noreply.github.com> Date: Fri, 2 Oct 2026 16:58:09 +0000 Subject: [PATCH 4/5] fix(dataflow): Keep a trailing [any] in access paths With exact [any] matching, x.[any].* is no longer the same as x.*: the former matches only a tree with an [any] edge under x. Prepending [any] to the abstract path now keeps it instead of normalising it away. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_018R4xeH4QFAH7RrPcKFoQGZ --- .../ap/ifds/access/tree/AccessPath.kt | 3 --- .../ap/ifds/access/tree/AnyAccessPathTest.kt | 21 +++++++------------ 2 files changed, 7 insertions(+), 17 deletions(-) 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 9b057eedbf..de9c6150c5 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 @@ -65,9 +65,6 @@ class AccessPath( val accessorIdx = with(apManager) { accessor.idx } if (access == null) { - // [any].* == * - if (accessorIdx == ANY_ACCESSOR_IDX) return this - return AccessPath(apManager, base, AccessNode(apManager, accessorIdx, next = null), exclusions) } 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 index b1b404e0b3..24fb9b31b9 100644 --- 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 @@ -52,14 +52,6 @@ class AnyAccessPathTest { accessors.foldRight(manager.mostAbstractInitialAp(base)) { a, f -> f.prependAccessor(a) } .replaceExclusions(exclusions) as AccessPath - /** Built without normalization, so a trailing [any] is kept. */ - private fun rawInitial(vararg accessors: Accessor): AccessPath { - val node = accessors.foldRight(null as AccessPath.AccessNode?) { a, n -> - AccessPath.AccessNode(manager, with(manager) { a.idx }, n) - } - return AccessPath(manager, base, node, ExclusionSet.Empty) - } - private fun finalInitial(vararg accessors: Accessor): AccessPath = accessors.foldRight(manager.createFinalInitialAp(base, ExclusionSet.Empty)) { a, f -> f.prependAccessor(a) } as AccessPath @@ -162,10 +154,11 @@ class AnyAccessPathTest { } @Test - fun `any over the abstract path is the abstract path`() { - val abstract = manager.mostAbstractInitialAp(base) - assertSame(abstract, abstract.prependAccessor(AnyAccessor)) - assertEquals(initial(A), initial(A, AnyAccessor)) + 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 @@ -190,7 +183,7 @@ class AnyAccessPathTest { @Test fun `path with any concatenates into a tree with an any edge`() { - val path = rawInitial(A, AnyAccessor) + 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) @@ -229,7 +222,7 @@ class AnyAccessPathTest { @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(rawInitial(A, AnyAccessor))) + assertEquals(finalTree(X).access, tree.deltaNode(initial(A, AnyAccessor))) } @Test From be7d92904c1b731e299362e9b691dbe6f7d65183 Mon Sep 17 00:00:00 2001 From: Valentyn Sobol <8640896+Saloed@users.noreply.github.com> Date: Fri, 2 Oct 2026 17:08:43 +0000 Subject: [PATCH 5/5] fix(dataflow): Collapse a second [any] back to the first A path holds at most one [any]: a.[any].b.[any].c == a.[any].c. Prepending [any] to a path keeps only the part after its existing [any]; prepending [any] to a tree collapses every [any] below it, and a tree concat that puts an [any] of the delta under an [any] of the fact collapses it the same way. With the invariant restored, the any-suffix matcher keeps its original check against nested [any] edges. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_018R4xeH4QFAH7RrPcKFoQGZ --- .../ap/ifds/access/tree/AccessPath.kt | 15 ++++-- .../ap/ifds/access/tree/AccessTree.kt | 53 ++++++++++++++++--- .../access/tree/AccessTreeAnySuffixMatcher.kt | 6 +-- .../ap/ifds/access/tree/AnyAccessPathTest.kt | 44 +++++++++++++++ .../ifds/access/tree/AnySummaryStorageTest.kt | 36 +++---------- 5 files changed, 110 insertions(+), 44 deletions(-) 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 de9c6150c5..454930f544 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 @@ -322,6 +322,15 @@ 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]. */ @@ -360,10 +369,8 @@ class AccessPath( AccessNode(manager, accessor, this) } - accessor == ANY_ACCESSOR_IDX -> { - // [any].[any] == [any] - if (this.accessor == ANY_ACCESSOR_IDX) this else AccessNode(manager, accessor, this) - } + // 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 f23eb87757..5b18777cb3 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 @@ -613,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) + } + } + } + + /** 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) } - return create(ANY_ACCESSOR_IDX, nextNode) + + cache[this] = result + return result } private fun limitElementAccess(limit: Int): AccessNode { @@ -1149,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( diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessTreeAnySuffixMatcher.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessTreeAnySuffixMatcher.kt index 7ca7c72437..5fd19a61b8 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessTreeAnySuffixMatcher.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessTreeAnySuffixMatcher.kt @@ -61,19 +61,18 @@ class AccessTreeAnySuffixMatcher(suffixNode: AccessTree.AccessNode) { val notCoveredByAny: Int?, ) - // A nested [any] edge of the suffix is left out of the trie: the trie then matches a subset of the - // suffix language, so getNonMatchingNode trims less but never trims a path the suffix does not cover. init { if (suffixNode.accessors != null && suffixNode.accessorNodes != null) { val unprocessed = ArrayDeque() suffixNode.forEachAccessor { accessor, accessorNode -> - if (accessor == ANY_ACCESSOR_IDX) return@forEachAccessor val notCoveredByAny = if (accessor.coveredByAny()) null else 1 unprocessed.addLast(RawNodeWithParent(accessorNode, accessor, root, 1, notCoveredByAny)) } while (unprocessed.isNotEmpty()) { val (node, accessor, triePar, depth, notCoveredByAny) = unprocessed.removeFirst() + // disallowing [any]->...->[any] + check(accessor != ANY_ACCESSOR_IDX) val curNotCoveredByAny = when { notCoveredByAny != null -> notCoveredByAny @@ -105,7 +104,6 @@ class AccessTreeAnySuffixMatcher(suffixNode: AccessTree.AccessNode) { triePar.children.put(accessor, newTrieNode) node.forEachAccessor { accessor, accessorNode -> - if (accessor == ANY_ACCESSOR_IDX) return@forEachAccessor unprocessed.addLast(RawNodeWithParent(accessorNode, accessor, newTrieNode, depth + 1, curNotCoveredByAny)) } } 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 index 24fb9b31b9..4d773c5d39 100644 --- 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 @@ -153,6 +153,50 @@ class AnyAccessPathTest { 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 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 index 1b473723b5..55173de219 100644 --- 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 @@ -85,38 +85,18 @@ class AnySummaryStorageTest { /* ---------- nested [any] ---------- */ @Test - fun `merging trees with nested any does not crash`() { - // a.[any].f.[any].g.$ : what a path with two [any]s becomes as a tree - val nested = merged(finalTree(A, AnyAccessor, F, AnyAccessor, G)) - val other = finalTree(A, AnyAccessor, K) + 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(nested.access)) - assertTrue(storage.add(other.access)) + 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, F, AnyAccessor, G)))) + assertTrue(edges.contains(finalInitialOf(initial(A, AnyAccessor, G)))) assertTrue(edges.contains(finalInitialOf(initial(A, AnyAccessor, K)))) - - // the other direction of the merge builds the matcher from the nested side - val reversed = MergingTreeSummaryStorage(manager) - assertTrue(reversed.add(other.access)) - assertTrue(reversed.add(nested.access)) - assertEquals(storage.edges(), reversed.edges()) - } - - @Test - fun `nested any is still trimmed only where it is covered`() { - // a.[any].f.[any].g.$ covers a.x.f.g.$ (x covered), so the merge may drop it, but it must keep a.m... - val nested = finalTree(A, AnyAccessor, F, AnyAccessor, G).access - val covered = finalTree(A, H, F, G).access - val notCovered = finalTree(A, F, K).access - - val merged = nested.mergeAdd(covered).mergeAdd(notCovered) - val tree = AccessTree(manager, base, merged, ExclusionSet.Empty) - - assertTrue(tree.contains(finalInitialOf(initial(A, F, K)))) - assertTrue(tree.contains(finalInitialOf(initial(A, AnyAccessor, F, AnyAccessor, G)))) } private fun finalInitialOf(path: AccessPath): AccessPath { @@ -231,7 +211,7 @@ class AnySummaryStorageTest { } @Test - fun `f2f any edges with nested any finals merge`() { + 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)