diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/CfgDtoToDot.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/CfgDtoToDot.kt new file mode 100644 index 000000000..23e49d87a --- /dev/null +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/CfgDtoToDot.kt @@ -0,0 +1,278 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * 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 + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * 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 + +import org.jacodb.ets.dto.AliasTypeDto +import org.jacodb.ets.dto.AnyTypeDto +import org.jacodb.ets.dto.ArrayRefDto +import org.jacodb.ets.dto.ArrayTypeDto +import org.jacodb.ets.dto.AssignStmtDto +import org.jacodb.ets.dto.AwaitExprDto +import org.jacodb.ets.dto.BinaryOperationDto +import org.jacodb.ets.dto.BooleanTypeDto +import org.jacodb.ets.dto.CallStmtDto +import org.jacodb.ets.dto.CastExprDto +import org.jacodb.ets.dto.CaughtExceptionRefDto +import org.jacodb.ets.dto.CfgDto +import org.jacodb.ets.dto.ClassTypeDto +import org.jacodb.ets.dto.ClosureFieldRefDto +import org.jacodb.ets.dto.ConstantDto +import org.jacodb.ets.dto.DeleteExprDto +import org.jacodb.ets.dto.EnumValueTypeDto +import org.jacodb.ets.dto.FunctionTypeDto +import org.jacodb.ets.dto.GenericTypeDto +import org.jacodb.ets.dto.GlobalRefDto +import org.jacodb.ets.dto.IfStmtDto +import org.jacodb.ets.dto.InstanceCallExprDto +import org.jacodb.ets.dto.InstanceFieldRefDto +import org.jacodb.ets.dto.InstanceOfExprDto +import org.jacodb.ets.dto.IntersectionTypeDto +import org.jacodb.ets.dto.LexicalEnvTypeDto +import org.jacodb.ets.dto.LiteralTypeDto +import org.jacodb.ets.dto.LocalDto +import org.jacodb.ets.dto.NeverTypeDto +import org.jacodb.ets.dto.NewArrayExprDto +import org.jacodb.ets.dto.NewExprDto +import org.jacodb.ets.dto.NopStmtDto +import org.jacodb.ets.dto.NullTypeDto +import org.jacodb.ets.dto.NumberTypeDto +import org.jacodb.ets.dto.ParameterRefDto +import org.jacodb.ets.dto.PrimitiveLiteralDto +import org.jacodb.ets.dto.PtrCallExprDto +import org.jacodb.ets.dto.RawStmtDto +import org.jacodb.ets.dto.RawTypeDto +import org.jacodb.ets.dto.RawValueDto +import org.jacodb.ets.dto.RelationOperationDto +import org.jacodb.ets.dto.ReturnStmtDto +import org.jacodb.ets.dto.ReturnVoidStmtDto +import org.jacodb.ets.dto.StaticCallExprDto +import org.jacodb.ets.dto.StaticFieldRefDto +import org.jacodb.ets.dto.StmtDto +import org.jacodb.ets.dto.StringTypeDto +import org.jacodb.ets.dto.ThisRefDto +import org.jacodb.ets.dto.ThrowStmtDto +import org.jacodb.ets.dto.TupleTypeDto +import org.jacodb.ets.dto.TypeDto +import org.jacodb.ets.dto.TypeOfExprDto +import org.jacodb.ets.dto.UnaryOperationDto +import org.jacodb.ets.dto.UnclearReferenceTypeDto +import org.jacodb.ets.dto.UndefinedTypeDto +import org.jacodb.ets.dto.UnionTypeDto +import org.jacodb.ets.dto.UnknownTypeDto +import org.jacodb.ets.dto.ValueDto +import org.jacodb.ets.dto.VoidTypeDto +import org.jacodb.ets.dto.YieldExprDto + +/** + * Convert a CFG DTO to DOT format for visualization with Graphviz. + * + * This is a secondary conversion function for DTO-based CFGs. + * For the main ETS model, use [org.jacodb.ets.model.EtsBlockCfg.toDot] instead. + * + * @param useHtml if true, uses HTML table format for blocks; if false, uses plain text + * @return DOT graph string + */ +fun CfgDto.toDot( + useHtml: Boolean = true, +): String { + val lines = mutableListOf() + lines += "digraph cfg {" + lines += " node [shape=${if (useHtml) "none" else "rect"} fontname=\"monospace\"]" + + // Nodes + for (block in blocks) { + if (useHtml) { + val s = block.stmts.joinToString("") { + it.toDotLabel().htmlEncode() + "
" + } + val h = "" + + "" + + "" + + "
" + "Block #${block.id}" + "
" + s + "
" + lines += " ${block.id} [label=<${h}>]" + } else { + val s = block.stmts.joinToString("") { it.toDotLabel() + "\\l" } + lines += " ${block.id} [label=\"Block #${block.id}\\n$s\"]" + } + } + + // Edges + for (block in blocks) { + val succs = block.successors + if (succs.isEmpty()) continue + if (succs.size == 1) { + lines += " ${block.id} -> ${succs.single()}" + } else { + check(succs.size == 2) { "Block ${block.id} has ${succs.size} successors, expected 1 or 2" } + val (trueBranch, falseBranch) = succs + lines += " ${block.id} -> $trueBranch [label=\"true\"]" + lines += " ${block.id} -> $falseBranch [label=\"false\"]" + } + } + + lines += "}" + return lines.joinToString("\n") +} + +/** + * Convert a statement DTO to a DOT label string. + * + * This is a simple conversion that uses custom formatting for values. + * For better formatting, consider converting to ETS model first and using + * [org.jacodb.ets.model.EtsStmt.toDotLabel] instead. + */ +private fun StmtDto.toDotLabel(): String { + val label = when (this) { + is NopStmtDto -> "nop" + is AssignStmtDto -> "${left.toDotLabel()} := ${right.toDotLabel()}" + is ReturnVoidStmtDto -> "return" + is ReturnStmtDto -> "return ${arg.toDotLabel()}" + is ThrowStmtDto -> "throw ${arg.toDotLabel()}" + is IfStmtDto -> "if (${condition.toDotLabel()})" + is CallStmtDto -> expr.toDotLabel() + is RawStmtDto -> "raw $kind" + } + return label.replace("\"", "\\\"") +} + +/** + * Convert a value DTO to a DOT label string. + * + * Provides concise formatting for various value types without verbose data class toString(). + */ +private fun ValueDto.toDotLabel(): String { + return when (this) { + // Immediates + is LocalDto -> name + is ConstantDto -> when (type) { + is StringTypeDto -> "\"$value\"" + else -> value + } + + // References + is ThisRefDto -> "this" + is ParameterRefDto -> "arg$index" + is CaughtExceptionRefDto -> "@caught ${type.toDotLabel()}" + is GlobalRefDto -> name + is ClosureFieldRefDto -> "${base.name}.$fieldName" + is ArrayRefDto -> "${array.toDotLabel()}[${index.toDotLabel()}]" + is InstanceFieldRefDto -> "${instance.toDotLabel()}.${field.name}" + is StaticFieldRefDto -> "${field.declaringClass.name}.${field.name}" + + // Expressions + is NewExprDto -> "new ${classType.toDotLabel()}" + is NewArrayExprDto -> "new Array<${elementType.toDotLabel()}>(${size.toDotLabel()})" + is DeleteExprDto -> "delete ${arg.toDotLabel()}" + is AwaitExprDto -> "await ${arg.toDotLabel()}" + is YieldExprDto -> "yield ${arg.toDotLabel()}" + is TypeOfExprDto -> "typeof ${arg.toDotLabel()}" + is InstanceOfExprDto -> "${arg.toDotLabel()} instanceof ${checkType.toDotLabel()}" + is CastExprDto -> "${arg.toDotLabel()} as ${type.toDotLabel()}" + + // Unary operations + is UnaryOperationDto -> "$op ${arg.toDotLabel()}" + + // Binary operations + is BinaryOperationDto -> "${left.toDotLabel()} $op ${right.toDotLabel()}" + is RelationOperationDto -> "${left.toDotLabel()} $op ${right.toDotLabel()}" + + // Call expressions + is InstanceCallExprDto -> { + val argsStr = args.joinToString(", ") { it.toDotLabel() } + "call ${instance.toDotLabel()}.${method.name}($argsStr)" + } + + is StaticCallExprDto -> { + val argsStr = args.joinToString(", ") { it.toDotLabel() } + "static_call ${method.declaringClass.name}.${method.name}($argsStr)" + } + + is PtrCallExprDto -> { + val argsStr = args.joinToString(", ") { it.toDotLabel() } + "ptr_call ${ptr.toDotLabel()}.${method.name}($argsStr)" + } + + // Raw value + is RawValueDto -> "raw:$kind" + } +} + +/** + * Convert a type DTO to a concise DOT label string. + */ +private fun TypeDto.toDotLabel(): String { + return when (this) { + // Primitive types + is AnyTypeDto -> "any" + is UnknownTypeDto -> "unknown" + is UndefinedTypeDto -> "undefined" + is NullTypeDto -> "null" + is VoidTypeDto -> "void" + is NeverTypeDto -> "never" + is NumberTypeDto -> "number" + is StringTypeDto -> "string" + is BooleanTypeDto -> "boolean" + + is LiteralTypeDto -> when (literal) { + is PrimitiveLiteralDto.StringLiteral -> "\"${literal.value}\"" + is PrimitiveLiteralDto.NumberLiteral -> literal.value.toString() + is PrimitiveLiteralDto.BooleanLiteral -> literal.value.toString() + } + + // Complex types + is ClassTypeDto -> if (typeParameters.isNotEmpty()) { + val generics = typeParameters.joinToString(", ") { it.toDotLabel() } + "${signature.name}<$generics>" + } else { + signature.name + } + + is UnclearReferenceTypeDto -> if (typeParameters.isNotEmpty()) { + val generics = typeParameters.joinToString(", ") { it.toDotLabel() } + "$name<$generics>" + } else { + name + } + + is ArrayTypeDto -> "${elementType.toDotLabel()}${"[]".repeat(dimensions)}" + is TupleTypeDto -> types.joinToString(", ", "[", "]") { it.toDotLabel() } + + is UnionTypeDto -> types.joinToString(" | ") { + val s = it.toDotLabel() + if (it is UnionTypeDto || it is IntersectionTypeDto) "($s)" else s + } + + is IntersectionTypeDto -> types.joinToString(" & ") { + val s = it.toDotLabel() + if (it is UnionTypeDto || it is IntersectionTypeDto) "($s)" else s + } + + is FunctionTypeDto -> { + val params = signature.parameters.joinToString(", ") { it.type.toDotLabel() } + "($params) => ${signature.returnType.toDotLabel()}" + } + + // Special types + is GenericTypeDto -> name + is AliasTypeDto -> "$name=${originalType.toDotLabel()}" + is EnumValueTypeDto -> "${signature.name}${name?.let { ".$it" } ?: ""}" + is LexicalEnvTypeDto -> "<${method.name}>(${closures.joinToString { it.name }})" + + // Raw type + is RawTypeDto -> "raw:$kind" + } +} diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/ViewDot.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/ViewDot.kt index 8edb162b4..dd1a1facf 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/ViewDot.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/ViewDot.kt @@ -16,6 +16,7 @@ package org.jacodb.ets.utils +import org.jacodb.ets.dto.CfgDto import org.jacodb.ets.dto.EtsFileDto import org.jacodb.ets.model.EtsBlockCfg import org.jacodb.ets.model.EtsFile @@ -66,3 +67,7 @@ fun EtsBlockCfg.view() { fun EtsLinearCfg.view() { view(toDot()) } + +fun CfgDto.view() { + view(toDot()) +}