From 5c993bd3be3e00503d50150ca70192340ee0b2f6 Mon Sep 17 00:00:00 2001 From: Daniil Logunov <43185213+DanielELog@users.noreply.github.com> Date: Thu, 14 May 2026 07:03:02 +0300 Subject: [PATCH 1/7] Fix tree node merge not accounting for any --- .../ap/ifds/access/tree/AccessTree.kt | 16 ++- .../access/tree/AccessTreeAnySuffixMatcher.kt | 116 ++++++++++++++++++ 2 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessTreeAnySuffixMatcher.kt 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 394e4d320..f26945077 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 @@ -583,8 +583,20 @@ class AccessTree( val isFinal = this.isFinal || other.isFinal + val thisAny = this.getChild(ANY_ACCESSOR_IDX) + val (otherAccessors, otherAccessorNodes) = + if (thisAny != null) + AccessTreeAnySuffixMatcher(thisAny).getNonMatchingNode(other) + else other.accessors to other.accessorNodes + + val otherAny = other.getChild(ANY_ACCESSOR_IDX) + val (thisAccessors, thisAccessorNodes) = + if (otherAny != null) + AccessTreeAnySuffixMatcher(otherAny).getNonMatchingNode(this) + else accessors to accessorNodes + val mergedAccessors = mergeAccessors( - other.accessors, other.accessorNodes, onOtherNode = { _, _ -> } + thisAccessors, thisAccessorNodes, otherAccessors, otherAccessorNodes, onOtherNode = { _, _ -> } ) { _, thisNode, otherNode -> thisNode.mergeAdd(otherNode) } @@ -1345,7 +1357,7 @@ class AccessTree( ) @JvmStatic - private fun TreeApManager.create( + fun TreeApManager.create( isAbstract: Boolean, isFinal: Boolean, accessors: IntArray?, 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 new file mode 100644 index 000000000..ccc92f7c3 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessTreeAnySuffixMatcher.kt @@ -0,0 +1,116 @@ +package org.opentaint.dataflow.ap.ifds.access.tree + +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap +import org.opentaint.dataflow.ap.ifds.access.tree.AccessTree.AccessNode.Companion.create +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 + +class AccessTreeAnySuffixMatcher(suffixNode: AccessTree.AccessNode) { + private val manager = suffixNode.manager + private val root = TrieNode(false, null) + + private data class TrieNode( + val isAbstract: Boolean, + val prefixLink: TrieNode?, + val children: Int2ObjectOpenHashMap = Int2ObjectOpenHashMap() + ) { + fun findChild(accessor: Int): TrieNode? { + val child = children.get(accessor) + if (child != null) + return child + return prefixLink?.findChild(accessor) + } + } + + private data class RawNodeWithParent( + val node: AccessTree.AccessNode, + val accessor: Int, + val parent: TrieNode + ) + + init { + if (suffixNode.accessors != null && suffixNode.accessorNodes != null) { + val unprocessed = ArrayDeque() + suffixNode.forEachAccessor { accessor, accessorNode -> + unprocessed.addLast(RawNodeWithParent(accessorNode, accessor, root)) + } + + while (unprocessed.isNotEmpty()) { + val (node, accessor, triePar) = unprocessed.removeFirst() + // disallowing [any]->...->[any] + check(accessor != ANY_ACCESSOR_IDX) + + var prefix = triePar.prefixLink + while (prefix != null) { + val next = prefix.children.get(accessor) + if (next != null) { + prefix = next + break + } + prefix = prefix.prefixLink + } + if (triePar === root) { + prefix = root + } + if (prefix == null) { + prefix = root.children.get(accessor) ?: root + } + val newTrieNode = TrieNode(node.isAbstract || prefix.isAbstract, prefix) + triePar.children.put(accessor, newTrieNode) + + node.forEachAccessor{ accessor, accessorNode -> + unprocessed.addLast(RawNodeWithParent(accessorNode, accessor, newTrieNode)) + } + } + } + } + + fun getNonMatchingNode(node: AccessTree.AccessNode): Pair> { + val accessorIdx = mutableListOf() + val accessorNodes = mutableListOf() + + node.forEachAccessor { accessor, accessorNode -> + if (accessor != ANY_ACCESSOR_IDX) { + val child = getNonMatchingNode(root, accessorNode) + if (child != null) { + accessorIdx.add(accessor) + accessorNodes.add(child) + } + } + else { + // two [any]-branches can be merged naturally + accessorIdx.add(accessor) + accessorNodes.add(accessorNode) + } + } + + return accessorIdx.toIntArray() to accessorNodes.toTypedArray() + } + + private fun getNonMatchingNode(trie: TrieNode, node: AccessTree.AccessNode): AccessTree.AccessNode? { + val accessorIdx = mutableListOf() + val accessorNodes = mutableListOf() + + node.forEachAccessor { accessor, accessorNode -> + val next = + if (accessor == ANY_ACCESSOR_IDX) root + else trie.findChild(accessor) ?: root + val child = getNonMatchingNode(next, accessorNode) + if (child != null) { + accessorIdx.add(accessor) + accessorNodes.add(child) + } + } + + val thisAbstract = node.isAbstract && !trie.isAbstract + + // all branches matched the any-suffix + if (!thisAbstract && accessorIdx.isEmpty()) + return null + + val thisFinal = node.isFinal && accessorIdx.any { it == FINAL_ACCESSOR_IDX } + + return manager.create(thisAbstract, thisFinal, accessorIdx.toIntArray(), accessorNodes.toTypedArray()) + } +} From a66f7f89a2c46db82adb17080cdaed319bcc1629 Mon Sep 17 00:00:00 2001 From: Daniil Logunov <43185213+DanielELog@users.noreply.github.com> Date: Thu, 14 May 2026 19:02:37 +0300 Subject: [PATCH 2/7] Add accessor filtering to any suffix matcher --- .../access/tree/AccessTreeAnySuffixMatcher.kt | 53 +++++++++++++------ 1 file changed, 38 insertions(+), 15 deletions(-) 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 ccc92f7c3..5236db9be 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 @@ -4,15 +4,18 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap import org.opentaint.dataflow.ap.ifds.access.tree.AccessTree.AccessNode.Companion.create 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.ELEMENT_ACCESSOR_IDX import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.FINAL_ACCESSOR_IDX +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isFieldAccessor class AccessTreeAnySuffixMatcher(suffixNode: AccessTree.AccessNode) { private val manager = suffixNode.manager - private val root = TrieNode(false, null) + private val root = TrieNode(false, null, 0) private data class TrieNode( val isAbstract: Boolean, val prefixLink: TrieNode?, + val depth: Int, val children: Int2ObjectOpenHashMap = Int2ObjectOpenHashMap() ) { fun findChild(accessor: Int): TrieNode? { @@ -21,30 +24,47 @@ class AccessTreeAnySuffixMatcher(suffixNode: AccessTree.AccessNode) { return child return prefixLink?.findChild(accessor) } + + override fun toString(): String { + return "(isAbstract=$isAbstract, children=$children)" + } } + private fun AccessorIdx.coveredByAny(): Boolean = + this == ELEMENT_ACCESSOR_IDX || this.isFieldAccessor() + private data class RawNodeWithParent( val node: AccessTree.AccessNode, - val accessor: Int, - val parent: TrieNode + val accessor: AccessorIdx, + val parent: TrieNode, + val depth: Int, + val notCoveredByAny: Int?, ) init { if (suffixNode.accessors != null && suffixNode.accessorNodes != null) { val unprocessed = ArrayDeque() suffixNode.forEachAccessor { accessor, accessorNode -> - unprocessed.addLast(RawNodeWithParent(accessorNode, accessor, root)) + val notCoveredByAny = if (accessor.coveredByAny()) null else 1 + unprocessed.addLast(RawNodeWithParent(accessorNode, accessor, root, 1, notCoveredByAny)) } while (unprocessed.isNotEmpty()) { - val (node, accessor, triePar) = unprocessed.removeFirst() + val (node, accessor, triePar, depth, notCoveredByAny) = unprocessed.removeFirst() // disallowing [any]->...->[any] check(accessor != ANY_ACCESSOR_IDX) + val curNotCoveredByAny = when { + notCoveredByAny != null -> notCoveredByAny + !accessor.coveredByAny() -> depth + else -> null + } + var prefix = triePar.prefixLink while (prefix != null) { val next = prefix.children.get(accessor) - if (next != null) { + val notCoveredStillInSuffix = curNotCoveredByAny == null || depth - next.depth > curNotCoveredByAny + if (next != null && notCoveredStillInSuffix) { prefix = next break } @@ -56,11 +76,11 @@ class AccessTreeAnySuffixMatcher(suffixNode: AccessTree.AccessNode) { if (prefix == null) { prefix = root.children.get(accessor) ?: root } - val newTrieNode = TrieNode(node.isAbstract || prefix.isAbstract, prefix) + val newTrieNode = TrieNode(node.isAbstract || prefix.isAbstract, prefix, depth) triePar.children.put(accessor, newTrieNode) node.forEachAccessor{ accessor, accessorNode -> - unprocessed.addLast(RawNodeWithParent(accessorNode, accessor, newTrieNode)) + unprocessed.addLast(RawNodeWithParent(accessorNode, accessor, newTrieNode, depth + 1, curNotCoveredByAny)) } } } @@ -71,15 +91,15 @@ class AccessTreeAnySuffixMatcher(suffixNode: AccessTree.AccessNode) { val accessorNodes = mutableListOf() node.forEachAccessor { accessor, accessorNode -> - if (accessor != ANY_ACCESSOR_IDX) { - val child = getNonMatchingNode(root, accessorNode) + if (accessor.coveredByAny()) { + val child = getNonMatchingNode(root, accessorNode, true) if (child != null) { accessorIdx.add(accessor) accessorNodes.add(child) } } else { - // two [any]-branches can be merged naturally + // two [any]-branches can be merged naturally, as those not accepted by [any] accessorIdx.add(accessor) accessorNodes.add(accessorNode) } @@ -88,15 +108,18 @@ class AccessTreeAnySuffixMatcher(suffixNode: AccessTree.AccessNode) { return accessorIdx.toIntArray() to accessorNodes.toTypedArray() } - private fun getNonMatchingNode(trie: TrieNode, node: AccessTree.AccessNode): AccessTree.AccessNode? { + private fun getNonMatchingNode(trie: TrieNode, node: AccessTree.AccessNode, prefixCoveredByAny: Boolean): AccessTree.AccessNode? { val accessorIdx = mutableListOf() val accessorNodes = mutableListOf() node.forEachAccessor { accessor, accessorNode -> + val prefixStillCovered = prefixCoveredByAny && accessor.coveredByAny() + // prefix has an accessor not covered by [any], so the whole suffix is not matched + val fallback = if (prefixStillCovered) root else null val next = - if (accessor == ANY_ACCESSOR_IDX) root - else trie.findChild(accessor) ?: root - val child = getNonMatchingNode(next, accessorNode) + if (accessor == ANY_ACCESSOR_IDX) fallback + else trie.findChild(accessor) ?: fallback + val child = next?.let { getNonMatchingNode(it, accessorNode, prefixStillCovered) } if (child != null) { accessorIdx.add(accessor) accessorNodes.add(child) From 683583cbcf9e2dcb1aa71ab11c2ba99e2a6fa806 Mon Sep 17 00:00:00 2001 From: Daniil Logunov <43185213+DanielELog@users.noreply.github.com> Date: Fri, 15 May 2026 14:42:20 +0300 Subject: [PATCH 3/7] Fix isFinal node behaviour inside matcher --- .../access/tree/AccessTreeAnySuffixMatcher.kt | 68 +++++++++++++------ 1 file changed, 49 insertions(+), 19 deletions(-) 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 5236db9be..8420fa4ad 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 @@ -10,10 +10,11 @@ import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isF class AccessTreeAnySuffixMatcher(suffixNode: AccessTree.AccessNode) { private val manager = suffixNode.manager - private val root = TrieNode(false, null, 0) + private val root = TrieNode(isAbstract = false, isFinal = false, prefixLink = null, depth = 0) private data class TrieNode( val isAbstract: Boolean, + val isFinal: Boolean, val prefixLink: TrieNode?, val depth: Int, val children: Int2ObjectOpenHashMap = Int2ObjectOpenHashMap() @@ -26,7 +27,28 @@ class AccessTreeAnySuffixMatcher(suffixNode: AccessTree.AccessNode) { } override fun toString(): String { - return "(isAbstract=$isAbstract, children=$children)" + val abstraction = if (isAbstract) "A" else "" + val final = if (isFinal) "F" else "" + return "($abstraction$final, depth=$depth, $children)" + } + + override fun equals(other: Any?): Boolean { + if (other !is TrieNode) + return false + if (this === other) + return true + if (prefixLink !== other.prefixLink) + return false + if (isAbstract != other.isAbstract || isFinal != other.isFinal || depth != other.depth) + return false + return children == other.children + } + + override fun hashCode(): Int { + var result = 31 * depth + if (isAbstract) result += 17 + if (isFinal) result += 13 + return result * 31 + children.hashCode() } } @@ -63,7 +85,7 @@ class AccessTreeAnySuffixMatcher(suffixNode: AccessTree.AccessNode) { var prefix = triePar.prefixLink while (prefix != null) { val next = prefix.children.get(accessor) - val notCoveredStillInSuffix = curNotCoveredByAny == null || depth - next.depth > curNotCoveredByAny + val notCoveredStillInSuffix = curNotCoveredByAny == null || depth - next.depth < curNotCoveredByAny if (next != null && notCoveredStillInSuffix) { prefix = next break @@ -76,7 +98,11 @@ class AccessTreeAnySuffixMatcher(suffixNode: AccessTree.AccessNode) { if (prefix == null) { prefix = root.children.get(accessor) ?: root } - val newTrieNode = TrieNode(node.isAbstract || prefix.isAbstract, prefix, depth) + val newTrieNode = TrieNode( + isAbstract = node.isAbstract || prefix.isAbstract, + isFinal = node.isFinal || prefix.isFinal, + prefix, depth + ) triePar.children.put(accessor, newTrieNode) node.forEachAccessor{ accessor, accessorNode -> @@ -91,18 +117,20 @@ class AccessTreeAnySuffixMatcher(suffixNode: AccessTree.AccessNode) { val accessorNodes = mutableListOf() node.forEachAccessor { accessor, accessorNode -> - if (accessor.coveredByAny()) { - val child = getNonMatchingNode(root, accessorNode, true) + val afterAny = root.children.get(accessor) + val accessorCovered = accessor.coveredByAny() + if (afterAny == null && !accessorCovered) { + // two [any]-branches can be merged naturally, as those not accepted by [any] + accessorIdx.add(accessor) + accessorNodes.add(accessorNode) + } + else { + val child = getNonMatchingNode(afterAny ?: root, accessorNode, accessorCovered) if (child != null) { accessorIdx.add(accessor) accessorNodes.add(child) } } - else { - // two [any]-branches can be merged naturally, as those not accepted by [any] - accessorIdx.add(accessor) - accessorNodes.add(accessorNode) - } } return accessorIdx.toIntArray() to accessorNodes.toTypedArray() @@ -113,12 +141,15 @@ class AccessTreeAnySuffixMatcher(suffixNode: AccessTree.AccessNode) { val accessorNodes = mutableListOf() node.forEachAccessor { accessor, accessorNode -> - val prefixStillCovered = prefixCoveredByAny && accessor.coveredByAny() - // prefix has an accessor not covered by [any], so the whole suffix is not matched + val prefixStillCovered = prefixCoveredByAny && (accessor == ANY_ACCESSOR_IDX || accessor.coveredByAny()) + // if prefix has an accessor not covered by [any], we cannot go back to root val fallback = if (prefixStillCovered) root else null - val next = - if (accessor == ANY_ACCESSOR_IDX) fallback - else trie.findChild(accessor) ?: fallback + val next = trie.findChild(accessor) ?: fallback + if (next == null) { + // fell out of suffix + accessorIdx.add(accessor) + accessorNodes.add(accessorNode) + } val child = next?.let { getNonMatchingNode(it, accessorNode, prefixStillCovered) } if (child != null) { accessorIdx.add(accessor) @@ -127,13 +158,12 @@ class AccessTreeAnySuffixMatcher(suffixNode: AccessTree.AccessNode) { } val thisAbstract = node.isAbstract && !trie.isAbstract + val thisFinal = node.isFinal && !trie.isFinal // all branches matched the any-suffix - if (!thisAbstract && accessorIdx.isEmpty()) + if (!thisAbstract && !thisFinal && accessorIdx.isEmpty()) return null - val thisFinal = node.isFinal && accessorIdx.any { it == FINAL_ACCESSOR_IDX } - return manager.create(thisAbstract, thisFinal, accessorIdx.toIntArray(), accessorNodes.toTypedArray()) } } From 288fd55301bfdbff45fa2283e67557a1e812051d Mon Sep 17 00:00:00 2001 From: Daniil Logunov <43185213+DanielELog@users.noreply.github.com> Date: Wed, 20 May 2026 14:09:58 +0300 Subject: [PATCH 4/7] Normalize merge behaviour and fix null pointer bug --- .../ap/ifds/access/tree/AccessTree.kt | 68 +++++++++++-------- .../access/tree/AccessTreeAnySuffixMatcher.kt | 11 +-- 2 files changed, 44 insertions(+), 35 deletions(-) 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 f26945077..0a2c58db0 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 @@ -566,7 +566,7 @@ class AccessTree( val addedNodes = Array(uniqueAccessors.size) { uniqueAccessors[it].second() } val mergedAccessors = mergeAccessors( - addedAccessors, addedNodes, onOtherNode = { _, _ -> } + addedAccessors, addedNodes, foldToAny = true, onOtherNode = { _, _ -> } ) { _, thisNode, otherNode -> thisNode.mergeAdd(otherNode) } @@ -576,29 +576,17 @@ class AccessTree( return manager.create(isAbstract, isFinal, mergedAccessors.first, mergedAccessors.second) } - fun mergeAdd(other: AccessNode): AccessNode { + fun mergeAdd(other: AccessNode, foldToAny: Boolean = true): AccessNode { if (this === other) return this val isAbstract = this.isAbstract || other.isAbstract val isFinal = this.isFinal || other.isFinal - val thisAny = this.getChild(ANY_ACCESSOR_IDX) - val (otherAccessors, otherAccessorNodes) = - if (thisAny != null) - AccessTreeAnySuffixMatcher(thisAny).getNonMatchingNode(other) - else other.accessors to other.accessorNodes - - val otherAny = other.getChild(ANY_ACCESSOR_IDX) - val (thisAccessors, thisAccessorNodes) = - if (otherAny != null) - AccessTreeAnySuffixMatcher(otherAny).getNonMatchingNode(this) - else accessors to accessorNodes - val mergedAccessors = mergeAccessors( - thisAccessors, thisAccessorNodes, otherAccessors, otherAccessorNodes, onOtherNode = { _, _ -> } + other.accessors, other.accessorNodes, foldToAny, onOtherNode = { _, _ -> } ) { _, thisNode, otherNode -> - thisNode.mergeAdd(otherNode) + thisNode.mergeAdd(otherNode, foldToAny) } if ( isAbstract == this.isAbstract @@ -614,7 +602,7 @@ class AccessTree( return manager.create(isAbstract, isFinal, accessors, accessorNodes) } - fun mergeAddDelta(other: AccessNode): Pair { + fun mergeAddDelta(other: AccessNode, foldToAny: Boolean = true): Pair { if (this === other) return this to null val isFinal = this.isFinal || other.isFinal @@ -627,13 +615,13 @@ class AccessTree( val deltaAccessorNodes = arrayListOf() val mergedAccessors = mergeAccessors( - other.accessors, other.accessorNodes, + other.accessors, other.accessorNodes, foldToAny, onOtherNode = { field, node -> deltaAccessors.add(field) deltaAccessorNodes.add(node) } ) { field, thisNode, otherNode -> - val (addedNode, addedNodeDelta) = thisNode.mergeAddDelta(otherNode) + val (addedNode, addedNodeDelta) = thisNode.mergeAddDelta(otherNode, foldToAny) if (addedNodeDelta != null) { deltaAccessors.add(field) @@ -966,31 +954,51 @@ class AccessTree( private inline fun mergeAccessors( otherFields: IntArray?, otherNodesE: Array?, + foldToAny: Boolean, onOtherNode: (AccessorIdx, AccessNode) -> Unit, - merge: (AccessorIdx, AccessNode, AccessNode) -> AccessNode - ) = mergeAccessors(accessors, accessorNodes, otherFields, otherNodesE, onOtherNode, merge) + merge: (AccessorIdx, AccessNode, AccessNode) -> AccessNode, + ) = mergeAccessors(accessors, accessorNodes, otherFields, otherNodesE, foldToAny, onOtherNode, merge) private inline fun mergeAccessors( accessors: IntArray?, nodes: Array?, - otherAccessors: IntArray?, + otherAccessorsE: IntArray?, otherNodesE: Array?, + foldToAny: Boolean, onOtherNode: (AccessorIdx, AccessNode) -> Unit, - merge: (AccessorIdx, AccessNode, AccessNode) -> AccessNode + merge: (AccessorIdx, AccessNode, AccessNode) -> AccessNode, ): Pair>? { - if (otherAccessors == null) return null - val otherNodes = otherNodesE!! + if (otherAccessorsE == null) return null + val otherNodesBeforeAny = otherNodesE!! if (accessors == null) { - for (i in otherAccessors.indices) { - onOtherNode(otherAccessors[i], otherNodes[i]) + for (i in otherAccessorsE.indices) { + onOtherNode(otherAccessorsE[i], otherNodesBeforeAny[i]) } - return otherAccessors to otherNodes + return otherAccessorsE to otherNodesBeforeAny } - val thisAccessors = accessors - val thisNodes = nodes!! + val thisAccessorsBeforeAny = accessors + val thisNodesBeforeAny = nodes!! + + var thisAnyIdx: Int = -1 + if (foldToAny) + thisAnyIdx = accessors.indexOf(ANY_ACCESSOR_IDX) +// val (otherAccessors, otherNodes) = otherAccessorsE to otherNodesBeforeAny + val (otherAccessors, otherNodes) = + if (thisAnyIdx > 0) + AccessTreeAnySuffixMatcher(nodes[thisAnyIdx]).getNonMatchingNode(otherAccessorsE, otherNodesBeforeAny) + else otherAccessorsE to otherNodesBeforeAny + + var otherAnyIdx: Int = -1 + if (foldToAny) + otherAnyIdx = otherAccessorsE.indexOf(ANY_ACCESSOR_IDX) +// val (thisAccessors, thisNodes) = thisAccessorsBeforeAny to thisNodesBeforeAny + val (thisAccessors, thisNodes) = + if (otherAnyIdx > 0) + AccessTreeAnySuffixMatcher(otherNodesBeforeAny[otherAnyIdx]).getNonMatchingNode(thisAccessorsBeforeAny, thisNodesBeforeAny) + else thisAccessorsBeforeAny to thisNodesBeforeAny var modified = false var accessorsModified = false 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 8420fa4ad..f93ae29ae 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 @@ -85,9 +85,9 @@ class AccessTreeAnySuffixMatcher(suffixNode: AccessTree.AccessNode) { var prefix = triePar.prefixLink while (prefix != null) { val next = prefix.children.get(accessor) - val notCoveredStillInSuffix = curNotCoveredByAny == null || depth - next.depth < curNotCoveredByAny - if (next != null && notCoveredStillInSuffix) { - prefix = next + if (next != null) { + val notCoveredStillInSuffix = curNotCoveredByAny == null || depth - next.depth < curNotCoveredByAny + prefix = if (notCoveredStillInSuffix) next else null break } prefix = prefix.prefixLink @@ -112,11 +112,12 @@ class AccessTreeAnySuffixMatcher(suffixNode: AccessTree.AccessNode) { } } - fun getNonMatchingNode(node: AccessTree.AccessNode): Pair> { + fun getNonMatchingNode(accessors: IntArray, nodes: Array): Pair> { val accessorIdx = mutableListOf() val accessorNodes = mutableListOf() - node.forEachAccessor { accessor, accessorNode -> + accessors.forEachIndexed { i, accessor -> + val accessorNode = nodes[i] val afterAny = root.children.get(accessor) val accessorCovered = accessor.coveredByAny() if (afterAny == null && !accessorCovered) { From e7a013417b810d88b5041955fb82c460da9f8c1c Mon Sep 17 00:00:00 2001 From: Daniil Logunov <43185213+DanielELog@users.noreply.github.com> Date: Wed, 20 May 2026 14:11:08 +0300 Subject: [PATCH 5/7] Fix TreeInitialFactAbstraction any unroll for zero accessors --- .../ap/ifds/access/tree/AccessTreeAnySuffixMatcher.kt | 1 - .../ap/ifds/access/tree/TreeInitialFactAbstraction.kt | 8 ++++++-- 2 files changed, 6 insertions(+), 3 deletions(-) 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 f93ae29ae..faf6d5879 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 @@ -5,7 +5,6 @@ import org.opentaint.dataflow.ap.ifds.access.tree.AccessTree.AccessNode.Companio 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.ELEMENT_ACCESSOR_IDX -import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.FINAL_ACCESSOR_IDX import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isFieldAccessor class AccessTreeAnySuffixMatcher(suffixNode: AccessTree.AccessNode) { diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/TreeInitialFactAbstraction.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/TreeInitialFactAbstraction.kt index 044b0473e..a1602b1ea 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/TreeInitialFactAbstraction.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/TreeInitialFactAbstraction.kt @@ -16,6 +16,7 @@ import org.opentaint.dataflow.ap.ifds.access.tree.AccessPath.AccessNode.Companio import org.opentaint.dataflow.ap.ifds.access.tree.AccessTree.AccessNode.Companion.create import org.opentaint.dataflow.ap.ifds.access.tree.AccessTree.AccessNode.Companion.createAbstractNodeFromReversedAp 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.ap.ifds.access.util.AccessorInterner.Companion.isAlwaysUnrollNext import org.opentaint.dataflow.util.forEachInt @@ -133,7 +134,7 @@ class TreeInitialFactAbstraction( } } - val mergedNewFacts = newFacts.reduceOrNull { acc, f -> acc.mergeAdd(f) } + val mergedNewFacts = newFacts.reduceOrNull { acc, f -> acc.mergeAdd(f, foldToAny = false) } ?: return null return addInitialFact(mergedNewFacts, interner) @@ -189,6 +190,9 @@ class TreeInitialFactAbstraction( if (unrollAccessors.isNotEmpty()) { unrollRequests += AnyAccessorUnrollRequest(state.currentAp, state.added, unrollAccessors) } + + val anyBranch = state.added.getChild(ANY_ACCESSOR_IDX) ?: error("impossible") + unprocessed += AbstractionState(state.analyzedTrieRoot, anyBranch, state.currentAp) } if (state.added.isFinal) { @@ -292,7 +296,7 @@ class TreeInitialFactAbstraction( fun addInitialFact(ap: AccessTreeNode, interner: AccessTreeSoftInterner): AccessTreeNode? { val currentNode = added ?: manager.create() - val (updatedAddedNode, addedInitial) = currentNode.mergeAddDelta(ap) + val (updatedAddedNode, addedInitial) = currentNode.mergeAddDelta(ap, foldToAny = false) if (addedInitial == null) return null From ab602d17d068fc28b5ce1fced5c4f1e91d5a6a3b Mon Sep 17 00:00:00 2001 From: Daniil Logunov <43185213+DanielELog@users.noreply.github.com> Date: Thu, 21 May 2026 15:24:33 +0300 Subject: [PATCH 6/7] Fix AnySuffixMatcher bugs --- .../opentaint/dataflow/ap/ifds/access/tree/AccessTree.kt | 6 ++---- .../ap/ifds/access/tree/AccessTreeAnySuffixMatcher.kt | 4 +--- .../opentaint/dataflow/ap/ifds/access/tree/TreeApManager.kt | 3 +++ 3 files changed, 6 insertions(+), 7 deletions(-) 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 0a2c58db0..d83bae396 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 @@ -985,18 +985,16 @@ class AccessTree( var thisAnyIdx: Int = -1 if (foldToAny) thisAnyIdx = accessors.indexOf(ANY_ACCESSOR_IDX) -// val (otherAccessors, otherNodes) = otherAccessorsE to otherNodesBeforeAny val (otherAccessors, otherNodes) = - if (thisAnyIdx > 0) + if (thisAnyIdx >= 0) AccessTreeAnySuffixMatcher(nodes[thisAnyIdx]).getNonMatchingNode(otherAccessorsE, otherNodesBeforeAny) else otherAccessorsE to otherNodesBeforeAny var otherAnyIdx: Int = -1 if (foldToAny) otherAnyIdx = otherAccessorsE.indexOf(ANY_ACCESSOR_IDX) -// val (thisAccessors, thisNodes) = thisAccessorsBeforeAny to thisNodesBeforeAny val (thisAccessors, thisNodes) = - if (otherAnyIdx > 0) + if (otherAnyIdx >= 0) AccessTreeAnySuffixMatcher(otherNodesBeforeAny[otherAnyIdx]).getNonMatchingNode(thisAccessorsBeforeAny, thisNodesBeforeAny) else thisAccessorsBeforeAny to thisNodesBeforeAny 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 faf6d5879..38b964ac4 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 @@ -4,8 +4,6 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap import org.opentaint.dataflow.ap.ifds.access.tree.AccessTree.AccessNode.Companion.create 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.ELEMENT_ACCESSOR_IDX -import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isFieldAccessor class AccessTreeAnySuffixMatcher(suffixNode: AccessTree.AccessNode) { private val manager = suffixNode.manager @@ -52,7 +50,7 @@ class AccessTreeAnySuffixMatcher(suffixNode: AccessTree.AccessNode) { } private fun AccessorIdx.coveredByAny(): Boolean = - this == ELEMENT_ACCESSOR_IDX || this.isFieldAccessor() + manager.isCoveredByAny(this) private data class RawNodeWithParent( val node: AccessTree.AccessNode, diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/TreeApManager.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/TreeApManager.kt index 2224c8a2a..72cb98d19 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/TreeApManager.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/TreeApManager.kt @@ -45,6 +45,9 @@ class TreeApManager( get() = interner.accessor(this) ?: error("Accessor not found: $this") + fun isCoveredByAny(accessor: AccessorIdx) = + anyAccessorUnrollStrategy.unrollAccessor(accessor.accessor) + override fun initialFactAbstraction(methodInitialStatement: CommonInst): InitialFactAbstraction = TreeInitialFactAbstraction(this) From af77ac1511f4bf1b3ad7c419930bc28c808992fa Mon Sep 17 00:00:00 2001 From: Daniil Logunov <43185213+DanielELog@users.noreply.github.com> Date: Sat, 23 May 2026 02:47:58 +0300 Subject: [PATCH 7/7] Fix AccessTree methods getChild and collectAccessorsTo --- .../ap/ifds/access/tree/AccessTree.kt | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) 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 d83bae396..0a27af250 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 @@ -391,25 +391,32 @@ class AccessTree( } } + private fun mergeAddMaybeNull(l: AccessNode?, r: AccessNode?): AccessNode? { + if (l == null) + return r + if (r == null) + return l + return r.mergeAdd(l) + } + fun getChild(accessor: AccessorIdx): AccessNode? { if (accessor == FINAL_ACCESSOR_IDX) return manager.finalNode.takeIf { this.isFinal } val node = getNodeByAccessor(accessor) - if (node != null) return node val anyAccessorNode = getNodeByAccessor(ANY_ACCESSOR_IDX) - ?: return null + ?: return node val anyChild = anyAccessorNode.getNodeByAccessor(accessor) - if (anyChild != null) return anyChild + var resultNode = mergeAddMaybeNull(anyChild, node) - with(manager) { - if (!anyAccessorUnrollStrategy.unrollAccessor(accessor.accessor)) return null + if (manager.isCoveredByAny(accessor)) { + val anyAccessorNoRepeats = anyAccessorNode.clearChild(accessor) + val originalAnyNoRepeats = anyAccessorNoRepeats.addParentIfPossible(ANY_ACCESSOR_IDX) + resultNode = mergeAddMaybeNull(originalAnyNoRepeats, resultNode) } - val childWithAny = anyAccessorNode.addParentIfPossible(ANY_ACCESSOR_IDX) - val unrolled = childWithAny?.mergeAdd(anyAccessorNode) ?: anyAccessorNode - return unrolled + return resultNode } fun addParentIfPossible(accessor: AccessorIdx): AccessNode? { @@ -536,12 +543,11 @@ class AccessTree( } forEachAccessor { accessor, accessorNode -> - if (accessor == ANY_ACCESSOR_IDX) { + if (accessor != ANY_ACCESSOR_IDX) { // note: always ignore any accessor - return@forEachAccessor + dst.add(accessor) } - dst.add(accessor) accessorNode.collectAccessorsTo(dst) } }