From db2058c26f38e695aead3ffac0354884a96f4df5 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/2] 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 c6dcaafa08c74b85ae37874ca47f0d65528527b1 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/2] 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)