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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 0 additions & 196 deletions jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/BlockCfgToDot.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,7 @@

package org.jacodb.ets.utils

import org.jacodb.ets.model.BasicBlock
import org.jacodb.ets.model.EtsBlockCfg
import org.jacodb.ets.model.EtsStmt
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths

private fun String.htmlEncode(): String = this
.replace("&", "&")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\\\"", "&quot;")
.replace("\"", "&quot;")

fun EtsBlockCfg.toDot(
useHtml: Boolean = true,
Expand Down Expand Up @@ -73,187 +61,3 @@ fun EtsBlockCfg.toDot(
lines += "}"
return lines.joinToString("\n")
}

/**
* An interprocedural CFG that contains:
* - the main control-flow graph (main)
* - all callee CFGs discovered so far at call sites (keyed by statement and its parent block id)
*/
data class InterproceduralCfg(
val main: EtsBlockCfg,
val callees: Map<Pair<EtsStmt, Int>, EtsBlockCfg>,
)

/**
* Render the interprocedural CFG (main + callees) as a single Graphviz DOT document,
* highlighting the execution path and current statement, and drawing
* each callee in its own dashed subgraph connected back to the call site.
*/
fun InterproceduralCfg.toHighlightedDotWithCalls(
pathStmts: Set<EtsStmt>,
currentStmt: EtsStmt?,
useHtml: Boolean = true,
): String {
val lines = mutableListOf<String>()
lines += "digraph world {"
lines += " compound=true"
lines += " node [shape=${if (useHtml) "none" else "rect"} fontname=\"monospace\"]"

// --- 1) Main CFG with ports on call statements ---
for (block in main.blocks) {
val nodeId = "M_${block.id}"
// compute all call hashes in this block
val callHashes = callees.keys
.filter { it.second == block.id }
.map { sanitize(it.first.hashCode()) }
.toSet()

if (useHtml) {
// build HTML table rows, adding port attribute on call lines
val rows = block.statements.joinToString(separator = "") { stmt ->
val txt = stmt.toDotLabel().htmlEncode()
val bg = when (stmt) {
currentStmt -> "lightblue"
in pathStmts -> "yellow"
else -> "white"
}
val stmtHash = sanitize(stmt.hashCode())
val portAttr = if (stmtHash in callHashes) " port=\"p$stmtHash\"" else ""
"<tr><td balign=\"left\" bgcolor=\"$bg\"$portAttr>$txt</td></tr>"
}
val table = buildString {
append("<table border=\"0\" cellborder=\"1\" cellspacing=\"0\">")
append("<tr><td><b>Block #${block.id}</b></td></tr>")
append(rows)
append("</table>")
}
lines += " $nodeId [label=<$table>];"
} else {
val lbl = buildPlainLabel(block, pathStmts, currentStmt)
lines += " $nodeId [label=\"$lbl\"];"
}
}
// Main CFG edges
for ((bid, succs) in main.successors) {
val from = "M_$bid"
when (succs.size) {
1 -> lines += " $from -> M_${succs[0]};"
2 -> {
val (t, f) = succs
lines += " $from -> M_$t [label=\"true\"];"
lines += " $from -> M_$f [label=\"false\"];"
}
}
}

// --- 2) Callee clusters with call-edge from port ---
for ((key, cfg) in callees) {
val (stmt, parentBlock) = key
val h = sanitize(stmt.hashCode())
val clusterName = "cluster_${h}_B${parentBlock}"
// method signature label
val methodSig = cfg.entries.first().location.method.signature
// open subgraph
lines += " subgraph \"$clusterName\" {"
lines += " label=\"$methodSig\";"
lines += " style=dashed;"

// render callee nodes
for (blk in cfg.blocks) {
val calleeNode = "C_${h}_${blk.id}"
if (useHtml) {
val table = buildHtmlTable(blk, pathStmts, currentStmt)
lines += " $calleeNode [label=<$table>];"
} else {
val lbl = buildPlainLabel(blk, pathStmts, currentStmt)
lines += " $calleeNode [label=\"$lbl\"];"
}
}
// render callee edges
for ((bid, succs) in cfg.successors) {
val from = "C_${h}_$bid"
when (succs.size) {
1 -> lines += " $from -> C_${h}_${succs[0]};"
2 -> {
val (t, f) = succs
lines += " $from -> C_${h}_$t [label=\"true\"];"
lines += " $from -> C_${h}_$f [label=\"false\"];"
}
}
}
lines += " }"

// connect from the specific port on the caller block
// connect from the specific port on the caller block using tailport
val caller = "M_${parentBlock}"
val entryId = cfg.blocks.first().id
val calleeEntry = "C_${h}_$entryId"
val stmtHash = sanitize(stmt.hashCode())
lines += " $caller:p$stmtHash -> $calleeEntry [tailport=\"p$stmtHash\" ltail=\"$clusterName\" lhead=\"$clusterName\" style=dotted label=\"call\"];"
}

lines += "}"
return lines.joinToString("\n")
}

private fun buildHtmlTable(
block: BasicBlock,
pathStmts: Set<EtsStmt>,
currentStmt: EtsStmt?,
): String {
var i = 0
val rows = block.statements.joinToString(separator = "") { stmt ->
val txt = stmt.toDotLabel().htmlEncode()
val stmtHash = sanitize(stmt.hashCode())
val portAttr = if (stmt.callExpr != null) " port=\"p$stmtHash\"" else ""

val bg = when (stmt) {
currentStmt -> "lightblue"
in pathStmts -> "yellow"
else -> "white"
}
i++
"<tr><td balign=\"left\" bgcolor=\"$bg\"$portAttr>$txt</td></tr>"
}
return "<table border=\"0\" cellborder=\"1\" cellspacing=\"0\">" +
"<tr><td><b>Block #${block.id}</b></td></tr>" + rows +
"</table>"
}

private fun buildPlainLabel(
block: BasicBlock,
pathStmts: Set<EtsStmt>,
currentStmt: EtsStmt?,
): String {
val body = block.statements.joinToString(separator = "") { stmt ->
val raw = stmt.toDotLabel()
val pfx = when (stmt) {
currentStmt -> "[▶] "
in pathStmts -> "[·] "
else -> ""
}
"$pfx$raw\\l"
}
return "Block #${block.id}\\n" + body
}

fun renderDotOverwrite(
dot: String,
outputDir: Path = Paths.get("."),
baseName: String = "interproc_cfg",
dotCmd: String = "dot",
viewerCmd: String = when {
System.getProperty("os.name").startsWith("Mac") -> "open"
System.getProperty("os.name").startsWith("Win") -> "cmd /c start"
else -> "xdg-open"
},
) {
val dotFile = outputDir.resolve("$baseName.dot")
val outSvg = outputDir.resolve("$baseName.svg")
Files.write(dotFile, dot.toByteArray())
Runtime.getRuntime().exec("$dotCmd -Tsvg -o $outSvg $dotFile").waitFor()
Runtime.getRuntime().exec("$viewerCmd $outSvg").waitFor()
}

// helper to sanitize negative hash codes for Graphviz IDs
fun sanitize(id: Int): String = id.toString().let { if (it.startsWith("-")) "N${it.substring(1)}" else it }
41 changes: 41 additions & 0 deletions jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/DotUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright 2022 UnitTestBot contributors (utbot.org)
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.jacodb.ets.utils

/**
* Encode a string for use in HTML, replacing special characters with their HTML entities.
* This is needed for rendering text in Graphviz nodes with HTML labels.
*
* For example, `"Hello & <world>"` becomes `"Hello &amp; &lt;world&gt;"`.
*/
internal fun String.htmlEncode(): String = this
.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\\\"", "&quot;")
.replace("\"", "&quot;")

/**
* Sanitize an integer ID for use as a Graphviz node ID.
* Negative IDs are prefixed with "N" to avoid issues with Graphviz.
*
* For example, `-123` becomes `"N123"`, while `456` remains `"456"`.
*/
internal fun sanitize(id: Int): String {
val s = id.toString()
return if (s.startsWith("-")) "N${s.substring(1)}" else s
}
Loading
Loading