diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 18e4c7367..3ce838ddc 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -64,7 +64,7 @@ jobs: DEST_DIR="arkanalyzer" MAX_RETRIES=10 RETRY_DELAY=3 # Delay between retries in seconds - BRANCH="neo/2024-08-16" + BRANCH="neo/2024-12-04" for ((i=1; i<=MAX_RETRIES; i++)); do git clone --depth=1 --branch $BRANCH $REPO_URL $DEST_DIR && break diff --git a/.gitignore b/.gitignore index e4f1b805b..fcd429928 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .idea/ .gradle/ build/ +.kotlin/ idea-community *.db /generated/ diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 2c3521197..a4b76b953 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index dedd5d1e6..7cf748e74 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/jacodb-core/src/main/kotlin/org/jacodb/impl/cfg/GraphExt.kt b/jacodb-core/src/main/kotlin/org/jacodb/impl/cfg/GraphExt.kt index 3fcd50b45..14954b6a3 100644 --- a/jacodb-core/src/main/kotlin/org/jacodb/impl/cfg/GraphExt.kt +++ b/jacodb-core/src/main/kotlin/org/jacodb/impl/cfg/GraphExt.kt @@ -58,20 +58,32 @@ import java.io.File import java.nio.file.Files import java.nio.file.Path -fun JcGraph.view(dotCmd: String, viewerCmd: String, viewCatchConnections: Boolean = false) { - Util.sh(arrayOf(viewerCmd, "file://${toFile(dotCmd, viewCatchConnections)}")) +private const val DEFAULT_DOT_CMD = "dot" + +fun JcGraph.view( + viewerCmd: String = if (System.getProperty("os.name").lowercase().contains("windows")) "start" else "xdg-open", + dotCmd: String = DEFAULT_DOT_CMD, + viewCatchConnections: Boolean = false, +) { + val path = toFile(null, dotCmd, viewCatchConnections) + Util.sh(arrayOf(viewerCmd, "file://$path")) } -fun JcGraph.toFile(dotCmd: String, viewCatchConnections: Boolean = false, file: File? = null): Path { +fun JcGraph.toFile( + file: File? = null, + dotCmd: String = DEFAULT_DOT_CMD, + viewCatchConnections: Boolean = false, +): Path { Graph.setDefaultCmd(dotCmd) val graph = Graph("jcGraph") val nodes = mutableMapOf() for ((index, inst) in instructions.withIndex()) { + val label = inst.toString().replace("\"", "\\\"") val node = Node("$index") .setShape(Shape.box) - .setLabel(inst.toString().replace("\"", "\\\"")) + .setLabel(label) .setFontSize(12.0) nodes[inst] = node graph.addNode(node) @@ -140,20 +152,31 @@ fun JcGraph.toFile(dotCmd: String, viewCatchConnections: Boolean = false, file: return resultingFile } -fun JcBlockGraph.view(dotCmd: String, viewerCmd: String) { - Util.sh(arrayOf(viewerCmd, "file://${toFile(dotCmd)}")) +fun JcBlockGraph.view( + viewerCmd: String, + dotCmd: String = DEFAULT_DOT_CMD, +) { + val path = toFile(null, dotCmd = dotCmd) + Util.sh(arrayOf(viewerCmd, "file://$path")) } -fun JcBlockGraph.toFile(dotCmd: String, file: File? = null): Path { +fun JcBlockGraph.toFile( + file: File? = null, + dotCmd: String = DEFAULT_DOT_CMD, +): Path { Graph.setDefaultCmd(dotCmd) val graph = Graph("jcGraph") val nodes = mutableMapOf() for ((index, block) in instructions.withIndex()) { + val label = instructions(block) + .joinToString("") { "$it\\l" } + .replace("\"", "\\\"") + .replace("\n", "\\n") val node = Node("$index") .setShape(Shape.box) - .setLabel(instructions(block).joinToString("") { "$it\\l" }.replace("\"", "\\\"").replace("\n", "\\n")) + .setLabel(label) .setFontSize(12.0) nodes[block] = node graph.addNode(node) diff --git a/jacodb-core/src/test/kotlin/org/jacodb/testing/cfg/IRSvgGenerator.kt b/jacodb-core/src/test/kotlin/org/jacodb/testing/cfg/IRSvgGenerator.kt index 61604e857..efb0bb14f 100644 --- a/jacodb-core/src/test/kotlin/org/jacodb/testing/cfg/IRSvgGenerator.kt +++ b/jacodb-core/src/test/kotlin/org/jacodb/testing/cfg/IRSvgGenerator.kt @@ -56,8 +56,8 @@ class IRSvgGenerator(private val folder: File) : Closeable { val fileName = "${it.enclosingClass.simpleName}-$fixedName-$index.svg" val graph = it.flowGraph() JcGraphChecker(it, graph).check() - graph.toFile("dot", false, file = File(folder, "graph-$fileName")) - graph.blockGraph().toFile("dot", file = File(folder, "block-graph-$fileName")) + graph.toFile(File(folder, "graph-$fileName")) + graph.blockGraph().toFile(File(folder, "block-graph-$fileName")) } } diff --git a/jacodb-ets/build.gradle.kts b/jacodb-ets/build.gradle.kts index 82280243b..f69e99f08 100644 --- a/jacodb-ets/build.gradle.kts +++ b/jacodb-ets/build.gradle.kts @@ -2,6 +2,7 @@ import java.io.FileNotFoundException plugins { kotlin("plugin.serialization") + `java-test-fixtures` } dependencies { @@ -15,6 +16,9 @@ dependencies { testImplementation(kotlin("test")) testImplementation(Libs.mockk) + + testFixturesImplementation(Libs.kotlin_logging) + testFixturesImplementation(Libs.junit_jupiter_api) } // Example usage: @@ -67,6 +71,7 @@ tasks.register("generateTestResources") { "--multi", inputDir.relativeTo(resources).path, outputDir.relativeTo(resources).path, + "-t", ) println("Running: '${cmd.joinToString(" ")}'") val process = ProcessBuilder(cmd).directory(resources).start() diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/base/Constants.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/base/Constants.kt new file mode 100644 index 000000000..1c6164190 --- /dev/null +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/base/Constants.kt @@ -0,0 +1,37 @@ +/* + * 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.base + +const val CONSTRUCTOR_NAME = "constructor" + +const val DEFAULT_ARK_CLASS_NAME = "%dflt" +const val DEFAULT_ARK_METHOD_NAME = "%dflt" + +const val UNKNOWN_PROJECT_NAME = "%unk" +const val UNKNOWN_FILE_NAME = "%unk" +const val UNKNOWN_NAMESPACE_NAME = "%unk" +const val UNKNOWN_CLASS_NAME = "" // TODO: consult AA/src/core/common/Const.ts +const val UNKNOWN_FIELD_NAME = "" // TODO: consult AA/src/core/common/Const.ts +const val UNKNOWN_METHOD_NAME = "" // TODO: consult AA/src/core/common/Const.ts + +const val INSTANCE_INIT_METHOD_NAME = "%instInit" +const val STATIC_INIT_METHOD_NAME = "%statInit" + +const val ANONYMOUS_CLASS_PREFIX = "%AC" +const val ANONYMOUS_METHOD_PREFIX = "%AM" + +const val TEMP_LOCAL_PREFIX = "%" diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/base/EtsExpr.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/base/EtsExpr.kt index c17cb586f..8751d607e 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/base/EtsExpr.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/base/EtsExpr.kt @@ -77,6 +77,7 @@ interface EtsExpr : EtsEntity { // Call fun visit(expr: EtsInstanceCallExpr): R fun visit(expr: EtsStaticCallExpr): R + fun visit(expr: EtsPtrCallExpr): R // Other fun visit(expr: EtsCommaExpr): R @@ -133,6 +134,7 @@ interface EtsExpr : EtsEntity { override fun visit(expr: EtsInstanceCallExpr): R = defaultVisit(expr) override fun visit(expr: EtsStaticCallExpr): R = defaultVisit(expr) + override fun visit(expr: EtsPtrCallExpr): R = defaultVisit(expr) override fun visit(expr: EtsCommaExpr): R = defaultVisit(expr) override fun visit(expr: EtsTernaryExpr): R = defaultVisit(expr) @@ -790,6 +792,20 @@ data class EtsStaticCallExpr( } } +data class EtsPtrCallExpr( + val ptr: EtsLocal, + override val method: EtsMethodSignature, + override val args: List, +) : EtsCallExpr { + override fun toString(): String { + return "${ptr}.${method.name}(${args.joinToString()})" + } + + override fun accept(visitor: EtsExpr.Visitor): R { + return visitor.visit(this) + } +} + data class EtsCommaExpr( override val left: EtsEntity, override val right: EtsEntity, diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/base/EtsType.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/base/EtsType.kt index b5e567c47..22c380522 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/base/EtsType.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/base/EtsType.kt @@ -19,7 +19,9 @@ package org.jacodb.ets.base import org.jacodb.api.common.CommonType import org.jacodb.api.common.CommonTypeName import org.jacodb.ets.model.EtsClassSignature +import org.jacodb.ets.model.EtsLocalSignature import org.jacodb.ets.model.EtsMethodSignature +import org.jacodb.ets.model.EtsNamespaceSignature interface EtsType : CommonType, CommonTypeName { override val typeName: String @@ -45,6 +47,10 @@ interface EtsType : CommonType, CommonTypeName { fun visit(type: EtsArrayType): R fun visit(type: EtsArrayObjectType): R fun visit(type: EtsUnclearRefType): R + fun visit(type: EtsGenericType): R + fun visit(type: EtsAliasType): R + fun visit(type: EtsAnnotationNamespaceType): R + fun visit(type: EtsAnnotationTypeQueryType): R interface Default : Visitor { override fun visit(type: EtsAnyType): R = defaultVisit(type) @@ -64,6 +70,10 @@ interface EtsType : CommonType, CommonTypeName { override fun visit(type: EtsArrayType): R = defaultVisit(type) override fun visit(type: EtsArrayObjectType): R = defaultVisit(type) override fun visit(type: EtsUnclearRefType): R = defaultVisit(type) + override fun visit(type: EtsGenericType): R = defaultVisit(type) + override fun visit(type: EtsAliasType): R = defaultVisit(type) + override fun visit(type: EtsAnnotationNamespaceType): R = defaultVisit(type) + override fun visit(type: EtsAnnotationTypeQueryType): R = defaultVisit(type) fun defaultVisit(type: EtsType): R } @@ -215,10 +225,16 @@ data class EtsLiteralType( interface EtsRefType : EtsType data class EtsClassType( - val classSignature: EtsClassSignature, + val signature: EtsClassSignature, + val typeParameters: List = emptyList(), ) : EtsRefType { override val typeName: String - get() = classSignature.name + get() = if (typeParameters.isNotEmpty()) { + val generics = typeParameters.joinToString() + "${signature.name}<$generics>" + } else { + signature.name + } override fun toString(): String = typeName @@ -229,9 +245,15 @@ data class EtsClassType( data class EtsFunctionType( val method: EtsMethodSignature, + val typeParameters: List = emptyList(), ) : EtsRefType { override val typeName: String - get() = method.name + get() = if (typeParameters.isNotEmpty()) { + val generics = typeParameters.joinToString() + "${method.name}<$generics>" + } else { + method.name + } override fun toString(): String = typeName @@ -268,11 +290,85 @@ data class EtsArrayObjectType( } data class EtsUnclearRefType( - override val typeName: String, + val name: String, + val typeParameters: List = emptyList(), ) : EtsRefType { + override val typeName: String + get() = if (typeParameters.isNotEmpty()) { + val generics = typeParameters.joinToString() + "$name<$generics>" + } else { + name + } + override fun toString(): String = typeName override fun accept(visitor: EtsType.Visitor): R { return visitor.visit(this) } } + +data class EtsGenericType( + val name: String, + val defaultType: EtsType? = null, + val constraint: EtsType? = null, +) : EtsRefType { + override val typeName: String + get() = name + + override fun toString(): String { + return name + (constraint?.let { " extends $it" } ?: "") + (defaultType?.let { " = $it" } ?: "") + } + + override fun accept(visitor: EtsType.Visitor): R { + return visitor.visit(this) + } +} + +data class EtsAliasType( + val name: String, + val originalType: EtsType, + val signature: EtsLocalSignature, +) : EtsType { + override val typeName: String + get() = name + + override fun toString(): String { + return "$name = $originalType" + } + + override fun accept(visitor: EtsType.Visitor): R { + return visitor.visit(this) + } +} + +data class EtsAnnotationNamespaceType( + val originType: String, + val namespaceSignature: EtsNamespaceSignature, +) : EtsType { + override val typeName: String + get() = originType + + override fun toString(): String { + return originType + } + + override fun accept(visitor: EtsType.Visitor): R { + return visitor.visit(this) + } +} + +data class EtsAnnotationTypeQueryType( + val originType: String, +) : EtsType { + override val typeName: String + get() = originType + + override fun toString(): String { + return originType + } + + override fun accept(visitor: EtsType.Visitor): R { + return visitor.visit(this) + } +} diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Convert.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Convert.kt index 477309abc..81605cd7b 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Convert.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Convert.kt @@ -16,8 +16,12 @@ package org.jacodb.ets.dto +import org.jacodb.ets.base.CONSTRUCTOR_NAME import org.jacodb.ets.base.EtsAddExpr +import org.jacodb.ets.base.EtsAliasType import org.jacodb.ets.base.EtsAndExpr +import org.jacodb.ets.base.EtsAnnotationNamespaceType +import org.jacodb.ets.base.EtsAnnotationTypeQueryType import org.jacodb.ets.base.EtsAnyType import org.jacodb.ets.base.EtsArrayAccess import org.jacodb.ets.base.EtsArrayLiteral @@ -44,6 +48,7 @@ import org.jacodb.ets.base.EtsExpExpr import org.jacodb.ets.base.EtsExpr import org.jacodb.ets.base.EtsFieldRef import org.jacodb.ets.base.EtsFunctionType +import org.jacodb.ets.base.EtsGenericType import org.jacodb.ets.base.EtsGotoStmt import org.jacodb.ets.base.EtsGtEqExpr import org.jacodb.ets.base.EtsGtExpr @@ -77,6 +82,7 @@ import org.jacodb.ets.base.EtsOrExpr import org.jacodb.ets.base.EtsParameterRef import org.jacodb.ets.base.EtsPreDecExpr import org.jacodb.ets.base.EtsPreIncExpr +import org.jacodb.ets.base.EtsPtrCallExpr import org.jacodb.ets.base.EtsRemExpr import org.jacodb.ets.base.EtsReturnStmt import org.jacodb.ets.base.EtsRightShiftExpr @@ -109,27 +115,30 @@ import org.jacodb.ets.graph.EtsCfg import org.jacodb.ets.model.EtsClass import org.jacodb.ets.model.EtsClassImpl import org.jacodb.ets.model.EtsClassSignature +import org.jacodb.ets.model.EtsDecorator import org.jacodb.ets.model.EtsField import org.jacodb.ets.model.EtsFieldImpl import org.jacodb.ets.model.EtsFieldSignature import org.jacodb.ets.model.EtsFieldSubSignature import org.jacodb.ets.model.EtsFile import org.jacodb.ets.model.EtsFileSignature +import org.jacodb.ets.model.EtsLocalSignature import org.jacodb.ets.model.EtsMethod import org.jacodb.ets.model.EtsMethodImpl import org.jacodb.ets.model.EtsMethodParameter import org.jacodb.ets.model.EtsMethodSignature -import org.jacodb.ets.model.EtsMethodSubSignature +import org.jacodb.ets.model.EtsModifiers import org.jacodb.ets.model.EtsNamespace import org.jacodb.ets.model.EtsNamespaceSignature class EtsMethodBuilder( signature: EtsMethodSignature, - // Default locals count is args + this - localsCount: Int = signature.parameters.size + 1, - modifiers: List = emptyList(), + typeParameters: List = emptyList(), + locals: List = emptyList(), + modifiers: EtsModifiers = EtsModifiers.EMPTY, + decorators: List = emptyList(), ) { - val etsMethod = EtsMethodImpl(signature, localsCount, modifiers) + private val etsMethod = EtsMethodImpl(signature, typeParameters, locals, modifiers, decorators) private val currentStmts: MutableList = mutableListOf() @@ -153,15 +162,22 @@ class EtsMethodBuilder( return etsMethod } + private fun ensureLocal(entity: EtsEntity): EtsLocal { + if (entity is EtsLocal) { + return entity + } + val newLocal = newTempLocal(entity.type) + currentStmts += EtsAssignStmt( + location = loc(), + lhv = newLocal, + rhv = entity, + ) + return newLocal + } + private fun ensureOneAddress(entity: EtsEntity): EtsValue { if (entity is EtsExpr || entity is EtsFieldRef || entity is EtsArrayAccess) { - val newLocal = newTempLocal(entity.type) - currentStmts += EtsAssignStmt( - location = loc(), - lhv = newLocal, - rhv = entity, - ) - return newLocal + return ensureLocal(entity) } else { check(entity is EtsValue) { "Expected EtsValue, but got $entity" @@ -175,7 +191,7 @@ class EtsMethodBuilder( is UnknownStmtDto -> object : EtsStmt { override val location: EtsInstLocation = loc() - override fun toString(): String = "Unknown(${stmt.stmt})" + override fun toString(): String = "UnknownStmt(${stmt.stmt})" // TODO: equals/hashCode ??? @@ -193,13 +209,17 @@ class EtsMethodBuilder( check(lhv is EtsLocal || lhv is EtsFieldRef || lhv is EtsArrayAccess) { "LHV of AssignStmt should be EtsLocal, EtsFieldRef, or EtsArrayAccess, but got $lhv" } - val rhv = convertToEtsEntity(stmt.right).let { + val rhv = convertToEtsEntity(stmt.right).let { rhv -> if (lhv is EtsLocal) { - it - } else if (it is EtsCastExpr || it is EtsNewExpr) { - it + if (rhv is EtsCastExpr && rhv.arg is EtsExpr) { + EtsCastExpr(ensureLocal(rhv.arg), rhv.type) + } else { + rhv + } + } else if (rhv is EtsCastExpr || rhv is EtsNewExpr) { + rhv } else { - ensureOneAddress(it) + ensureOneAddress(rhv) } } EtsAssignStmt( @@ -271,7 +291,7 @@ class EtsMethodBuilder( is UnknownValueDto -> object : EtsEntity { override val type: EtsType = EtsUnknownType - override fun toString(): String = "Unknown(${value.value})" + override fun toString(): String = "UnknownValue(${value.value})" override fun accept(visitor: EtsEntity.Visitor): R { if (visitor is EtsEntity.Visitor.Default) { @@ -281,10 +301,7 @@ class EtsMethodBuilder( } } - is LocalDto -> EtsLocal( - name = value.name, - type = convertToEtsType(value.type), - ) + is LocalDto -> convertToEtsLocal(value) is ConstantDto -> convertToEtsConstant(value) @@ -293,7 +310,7 @@ class EtsMethodBuilder( ) is NewArrayExprDto -> EtsNewArrayExpr( - elementType = convertToEtsType(value.type), + elementType = convertToEtsType(value.elementType), size = convertToEtsEntity(value.size), ) @@ -413,14 +430,22 @@ class EtsMethodBuilder( instance = convertToEtsEntity(value.instance as LocalDto) as EtsLocal, // safe cast method = convertToEtsMethodSignature(value.method), args = value.args.map { - ensureOneAddress(convertToEtsEntity(it)) + ensureLocal(convertToEtsEntity(it)) }, ) is StaticCallExprDto -> EtsStaticCallExpr( method = convertToEtsMethodSignature(value.method), args = value.args.map { - ensureOneAddress(convertToEtsEntity(it)) + ensureLocal(convertToEtsEntity(it)) + }, + ) + + is PtrCallExprDto -> EtsPtrCallExpr( + ptr = convertToEtsEntity(value.ptr as LocalDto) as EtsLocal, // safe cast + method = convertToEtsMethodSignature(value.method), + args = value.args.map { + ensureLocal(convertToEtsEntity(it)) }, ) @@ -464,7 +489,7 @@ class EtsMethodBuilder( "Method body should contain at least return stmt" } - val visited: MutableSet = hashSetOf() + val visited: MutableSet = hashSetOf(0) val queue: ArrayDeque = ArrayDeque() queue.add(0) @@ -518,20 +543,21 @@ fun convertToEtsClass(classDto: ClassDto): EtsClass { successors = emptyList(), predecessors = emptyList(), stmts = listOf( - ReturnVoidStmtDto - ) + ReturnVoidStmtDto, + ), ) val cfg = CfgDto(blocks = listOf(zeroBlock)) val body = BodyDto(locals = emptyList(), cfg = cfg) val signature = MethodSignatureDto( declaringClass = classSignatureDto, - name = "constructor", + name = CONSTRUCTOR_NAME, parameters = emptyList(), returnType = ClassTypeDto(classSignatureDto), ) return MethodDto( signature = signature, - modifiers = emptyList(), + modifiers = 0, + decorators = emptyList(), typeParameters = emptyList(), body = body, ) @@ -541,26 +567,40 @@ fun convertToEtsClass(classDto: ClassDto): EtsClass { val superClassSignature = classDto.superClassName?.takeIf { it != "" }?.let { name -> EtsClassSignature( name = name, - file = null, // TODO - namespace = null, // TODO + file = EtsFileSignature.DEFAULT, + ) + } + val implementedInterfaces = classDto.implementedInterfaceNames.map { name -> + EtsClassSignature( + name = name, + file = EtsFileSignature.DEFAULT, ) } val fields = classDto.fields.map { convertToEtsField(it) } - val (methodDtos, ctorDtos) = classDto.methods.partition { it.signature.name != "constructor" } + val (methodDtos, ctorDtos) = classDto.methods.partition { it.signature.name != CONSTRUCTOR_NAME } check(ctorDtos.size <= 1) { "Class should not have multiple constructors" } val ctorDto = ctorDtos.firstOrNull() ?: defaultConstructorDto(classDto.signature) val methods = methodDtos.map { convertToEtsMethod(it) } val ctor = convertToEtsMethod(ctorDto) + val typeParameters = classDto.typeParameters?.map { convertToEtsType(it) } ?: emptyList() + + val modifiers = EtsModifiers(classDto.modifiers) + val decorators = classDto.decorators.map { convertToEtsDecorator(it) } + return EtsClassImpl( signature = signature, fields = fields, methods = methods, ctor = ctor, superClass = superClassSignature, + implementedInterfaces = implementedInterfaces, + typeParameters = typeParameters, + modifiers = modifiers, + decorators = decorators, ) } @@ -579,6 +619,22 @@ fun convertToEtsType(type: TypeDto): EtsType { } } + is AliasTypeDto -> EtsAliasType( + name = type.name, + originalType = convertToEtsType(type.originalType), + signature = convertToEtsLocalSignature(type.signature), + ) + + is AnnotationNamespaceTypeDto -> EtsAnnotationNamespaceType( + originType = type.originType, + namespaceSignature = convertToEtsNamespaceSignature(type.namespaceSignature), + ) + + + is AnnotationTypeQueryTypeDto -> EtsAnnotationTypeQueryType( + originType = type.originType, + ) + AnyTypeDto -> EtsAnyType is ArrayTypeDto -> EtsArrayType( @@ -586,38 +642,47 @@ fun convertToEtsType(type: TypeDto): EtsType { dimensions = type.dimensions, ) - is FunctionTypeDto -> EtsFunctionType( - method = convertToEtsMethodSignature(type.signature) - ) + BooleanTypeDto -> EtsBooleanType is ClassTypeDto -> EtsClassType( - classSignature = convertToEtsClassSignature(type.signature) + signature = convertToEtsClassSignature(type.signature), + typeParameters = type.typeParameters.map { convertToEtsType(it) }, ) - NeverTypeDto -> EtsNeverType + is FunctionTypeDto -> EtsFunctionType( + method = convertToEtsMethodSignature(type.signature), + typeParameters = type.typeParameters.map { convertToEtsType(it) }, + ) - BooleanTypeDto -> EtsBooleanType + is GenericTypeDto -> EtsGenericType( + name = type.name, + defaultType = type.defaultType?.let { convertToEtsType(it) }, + constraint = type.constraint?.let { convertToEtsType(it) }, + ) is LiteralTypeDto -> EtsLiteralType( - literalTypeName = type.literal + literalTypeName = type.literal.toString(), ) + NeverTypeDto -> EtsNeverType + NullTypeDto -> EtsNullType NumberTypeDto -> EtsNumberType StringTypeDto -> EtsStringType - UndefinedTypeDto -> EtsUndefinedType - is TupleTypeDto -> EtsTupleType( types = type.types.map { convertToEtsType(it) } ) is UnclearReferenceTypeDto -> EtsUnclearRefType( - typeName = type.name + name = type.name, + typeParameters = type.typeParameters.map { convertToEtsType(it) }, ) + UndefinedTypeDto -> EtsUndefinedType + is UnionTypeDto -> EtsUnionType( types = type.types.map { convertToEtsType(it) } ) @@ -672,7 +737,7 @@ fun convertToEtsFileSignature(file: FileSignatureDto): EtsFileSignature { fun convertToEtsNamespaceSignature(namespace: NamespaceSignatureDto): EtsNamespaceSignature { return EtsNamespaceSignature( name = namespace.name, - file = namespace.declaringFile?.let { convertToEtsFileSignature(it) }, + file = namespace.declaringFile.let { convertToEtsFileSignature(it) }, namespace = namespace.declaringNamespace?.let { convertToEtsNamespaceSignature(it) }, ) } @@ -680,7 +745,7 @@ fun convertToEtsNamespaceSignature(namespace: NamespaceSignatureDto): EtsNamespa fun convertToEtsClassSignature(clazz: ClassSignatureDto): EtsClassSignature { return EtsClassSignature( name = clazz.name, - file = clazz.declaringFile?.let { convertToEtsFileSignature(it) }, + file = clazz.declaringFile.let { convertToEtsFileSignature(it) }, namespace = clazz.declaringNamespace?.let { convertToEtsNamespaceSignature(it) }, ) } @@ -698,36 +763,51 @@ fun convertToEtsFieldSignature(field: FieldSignatureDto): EtsFieldSignature { fun convertToEtsMethodSignature(method: MethodSignatureDto): EtsMethodSignature { return EtsMethodSignature( enclosingClass = convertToEtsClassSignature(method.declaringClass), - sub = EtsMethodSubSignature( - name = method.name, - parameters = method.parameters.mapIndexed { index, param -> - EtsMethodParameter( - index = index, - name = param.name, - type = convertToEtsType(param.type), - isOptional = param.isOptional - ) - }, - returnType = convertToEtsType(method.returnType), - ) + name = method.name, + parameters = method.parameters.mapIndexed { index, param -> + EtsMethodParameter( + index = index, + name = param.name, + type = convertToEtsType(param.type), + isOptional = param.isOptional + ) + }, + returnType = convertToEtsType(method.returnType), + ) +} + +fun convertToEtsLocalSignature(local: LocalSignatureDto): EtsLocalSignature { + return EtsLocalSignature( + name = local.name, + method = convertToEtsMethodSignature(local.method), ) } fun convertToEtsMethod(method: MethodDto): EtsMethod { val signature = convertToEtsMethodSignature(method.signature) - val modifiers = method.modifiers - .filterIsInstance() - .map { it.modifier } + val typeParameters = method.typeParameters?.map { convertToEtsType(it) } ?: emptyList() + val modifiers = EtsModifiers(method.modifiers) + val decorators = method.decorators.map { convertToEtsDecorator(it) } if (method.body != null) { - // Note: locals are not used in the current implementation - // val locals = method.body.locals.map { - // convertToEtsEntity(it) as EtsLocal // safe cast - // } - val localsCount = method.body.locals.size - val builder = EtsMethodBuilder(signature, localsCount, modifiers) + val locals = method.body.locals.map { + convertToEtsLocal(it) + } + val builder = EtsMethodBuilder( + signature = signature, + typeParameters = typeParameters, + locals = locals, + modifiers = modifiers, + decorators = decorators, + ) return builder.build(method.body.cfg) } else { - return EtsMethodImpl(signature, modifiers = modifiers) + return EtsMethodImpl( + signature = signature, + typeParameters = typeParameters, + locals = emptyList(), + modifiers = modifiers, + decorators = decorators, + ) } } @@ -740,10 +820,7 @@ fun convertToEtsField(field: FieldDto): EtsField { type = convertToEtsType(field.signature.type), ) ), - modifiers = field.modifiers - ?.filterIsInstance() - ?.map { it.modifier } - .orEmpty(), + modifiers = EtsModifiers(field.modifiers), isOptional = field.isOptional, isDefinitelyAssigned = field.isDefinitelyAssigned, ) @@ -770,3 +847,18 @@ fun convertToEtsFile(file: EtsFileDto): EtsFile { namespaces = namespaces, ) } + +fun convertToEtsDecorator(decorator: DecoratorDto): EtsDecorator { + return EtsDecorator( + name = decorator.kind, + // TODO: content + // TODO: param + ) +} + +fun convertToEtsLocal(local: LocalDto): EtsLocal { + return EtsLocal( + name = local.name, + type = convertToEtsType(local.type), + ) +} diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Model.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Model.kt index 80c85c3ac..3cb36992a 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Model.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Model.kt @@ -54,8 +54,9 @@ data class NamespaceDto( @Serializable data class ClassDto( val signature: ClassSignatureDto, - val modifiers: List, - val typeParameters: List, + val modifiers: Int, + val decorators: List, + val typeParameters: List? = null, val superClassName: String?, val implementedInterfaceNames: List, val fields: List, @@ -65,30 +66,18 @@ data class ClassDto( @Serializable data class FieldDto( val signature: FieldSignatureDto, - val typeParameters: List, - val modifiers: List? = null, - @SerialName("questionToken") val isOptional: Boolean = false, // '?' - @SerialName("exclamationToken") val isDefinitelyAssigned: Boolean = false, // '!' + val modifiers: Int, + val decorators: List, + @SerialName("questionToken") val isOptional: Boolean, // '?' + @SerialName("exclamationToken") val isDefinitelyAssigned: Boolean, // '!' ) -@Serializable(with = ModifierSerializer::class) -sealed class ModifierDto { - @Serializable - data class DecoratorItem( - val kind: String, - val content: String? = null, - val param: String? = null, - ) : ModifierDto() - - @Serializable - data class StringItem(val modifier: String) : ModifierDto() -} - @Serializable data class MethodDto( val signature: MethodSignatureDto, - val modifiers: List, - val typeParameters: List, + val modifiers: Int, + val decorators: List, + val typeParameters: List? = null, val body: BodyDto? = null, ) @@ -104,7 +93,8 @@ data class ImportInfoDto( val importType: String, val importFrom: String, val nameBeforeAs: String? = null, - val modifiers: List, + val modifiers: Int, + // val decorators: List, val originTsPosition: LineColPositionDto? = null, ) @@ -115,10 +105,18 @@ data class ExportInfoDto( val exportFrom: String? = null, val nameBeforeAs: String? = null, val isDefault: Boolean, - val modifiers: List, + val modifiers: Int, + // val decorators: List, val originTsPosition: LineColPositionDto? = null, ) +@Serializable +data class DecoratorDto( + val kind: String, + // val content: String? = null, + // val param: String? = null, +) + @Serializable data class LineColPositionDto( val line: Int, diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Serializers.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Serializers.kt index 81f73da5b..38763bd27 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Serializers.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Serializers.kt @@ -14,48 +14,50 @@ * limitations under the License. */ +@file:OptIn(ExperimentalSerializationApi::class) + package org.jacodb.ets.dto import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.KSerializer import kotlinx.serialization.SerializationException -import kotlinx.serialization.descriptors.PolymorphicKind +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.buildSerialDescriptor -import kotlinx.serialization.descriptors.element import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.JsonDecoder import kotlinx.serialization.json.JsonEncoder -import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.decodeFromJsonElement -import kotlinx.serialization.json.encodeToJsonElement +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.double -object ModifierSerializer : KSerializer { - @OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class) - override val descriptor: SerialDescriptor = - buildSerialDescriptor("Modifier", PolymorphicKind.SEALED) { - element("DecoratorItem") - element("StringItem") - } +object PrimitiveLiteralSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("PrimitiveLiteral", PrimitiveKind.STRING) - override fun serialize(encoder: Encoder, value: ModifierDto) { + override fun serialize(encoder: Encoder, value: PrimitiveLiteralDto) { require(encoder is JsonEncoder) when (value) { - is ModifierDto.DecoratorItem -> encoder.encodeJsonElement(encoder.json.encodeToJsonElement(value)) - is ModifierDto.StringItem -> encoder.encodeString(value.modifier) + is PrimitiveLiteralDto.StringLiteral -> encoder.encodeString(value.value) + is PrimitiveLiteralDto.NumberLiteral -> encoder.encodeDouble(value.value.toDouble()) + is PrimitiveLiteralDto.BooleanLiteral -> encoder.encodeBoolean(value.value) } } - override fun deserialize(decoder: Decoder): ModifierDto { + override fun deserialize(decoder: Decoder): PrimitiveLiteralDto { require(decoder is JsonDecoder) val element = decoder.decodeJsonElement() - return when { - element is JsonObject -> decoder.json.decodeFromJsonElement(element) - element is JsonPrimitive && element.isString -> ModifierDto.StringItem(element.content) - else -> throw SerializationException("Unsupported modifier: $element") + if (element !is JsonPrimitive) { + throw SerializationException("Expected JsonPrimitive, but found $element") + } + if (element.isString) { + return PrimitiveLiteralDto.StringLiteral(element.content) + } + val b = element.booleanOrNull + if (b != null) { + return PrimitiveLiteralDto.BooleanLiteral(b) + } else { + return PrimitiveLiteralDto.NumberLiteral(element.double) } } } diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Signatures.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Signatures.kt index a136bc685..beb1b5375 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Signatures.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Signatures.kt @@ -31,16 +31,14 @@ data class FileSignatureDto( @Serializable data class NamespaceSignatureDto( val name: String, - val declaringFile: FileSignatureDto? = null, + val declaringFile: FileSignatureDto, val declaringNamespace: NamespaceSignatureDto? = null, ) { override fun toString(): String { return if (declaringNamespace != null) { "$declaringNamespace::$name" - } else if (declaringFile != null) { - "$name in $declaringFile" } else { - name + "$declaringFile::$name" } } } @@ -48,16 +46,14 @@ data class NamespaceSignatureDto( @Serializable data class ClassSignatureDto( val name: String, - val declaringFile: FileSignatureDto? = null, + val declaringFile: FileSignatureDto, val declaringNamespace: NamespaceSignatureDto? = null, ) { override fun toString(): String { return if (declaringNamespace != null) { "$declaringNamespace::$name" - } else if (declaringFile != null) { - "$name in $declaringFile" } else { - name + "$declaringFile::$name" } } } @@ -69,7 +65,7 @@ data class FieldSignatureDto( val type: TypeDto, ) { override fun toString(): String { - return "$name: $type" + return "${declaringClass.name}::$name: $type" } } @@ -82,7 +78,7 @@ data class MethodSignatureDto( ) { override fun toString(): String { val params = parameters.joinToString() - return "$name($params): $returnType" + return "${declaringClass.name}::$name($params): $returnType" } } @@ -96,3 +92,13 @@ data class MethodParameterDto( return "$name: $type" } } + +@Serializable +data class LocalSignatureDto( + val name: String, + val method: MethodSignatureDto, +) { + override fun toString(): String { + return "${method}#${name}" + } +} diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Stmts.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Stmts.kt index d0e9d4897..0b4cdc77c 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Stmts.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Stmts.kt @@ -20,7 +20,6 @@ import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonClassDiscriminator -import kotlinx.serialization.json.JsonElement @Serializable @OptIn(ExperimentalSerializationApi::class) @@ -30,8 +29,12 @@ sealed interface StmtDto @Serializable @SerialName("UNKNOWN_STMT") data class UnknownStmtDto( - val stmt: JsonElement, -) : StmtDto + val stmt: String, +) : StmtDto { + override fun toString(): String { + return "UNKNOWN($stmt)" + } +} @Serializable @SerialName("NopStmt") diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Types.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Types.kt index 16ff8041d..ed395210b 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Types.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Types.kt @@ -141,13 +141,28 @@ object UndefinedTypeDto : PrimitiveTypeDto { @Serializable @SerialName("LiteralType") data class LiteralTypeDto( - val literal: String, + val literal: PrimitiveLiteralDto, ) : PrimitiveTypeDto { override val name: String - get() = "literal" + get() = literal.toString() override fun toString(): String { - return literal + return name + } +} + +@Serializable(with = PrimitiveLiteralSerializer::class) +sealed class PrimitiveLiteralDto { + data class StringLiteral(val value: String) : PrimitiveLiteralDto() { + override fun toString(): String = value + } + + data class NumberLiteral(val value: Double) : PrimitiveLiteralDto() { + override fun toString(): String = value.toString() + } + + data class BooleanLiteral(val value: Boolean) : PrimitiveLiteralDto() { + override fun toString(): String = value.toString() } } @@ -155,9 +170,15 @@ data class LiteralTypeDto( @SerialName("ClassType") data class ClassTypeDto( val signature: ClassSignatureDto, + val typeParameters: List = emptyList(), ) : TypeDto { override fun toString(): String { - return signature.toString() + return if (typeParameters.isNotEmpty()) { + val generics = typeParameters.joinToString() + "$signature<$generics>" + } else { + signature.toString() + } } } @@ -165,9 +186,16 @@ data class ClassTypeDto( @SerialName("FunctionType") data class FunctionTypeDto( val signature: MethodSignatureDto, + val typeParameters: List = emptyList(), ) : TypeDto { override fun toString(): String { - return "(${signature.parameters.joinToString()}) => ${signature.returnType}" + val params = signature.parameters.joinToString() + return if (typeParameters.isNotEmpty()) { + val generics = typeParameters.joinToString() + "${signature.name}<$generics>($params): ${signature.returnType}" + } else { + "${signature.name}($params): ${signature.returnType}" + } } } @@ -178,7 +206,7 @@ data class ArrayTypeDto( val dimensions: Int, ) : TypeDto { override fun toString(): String { - return "$elementType[]".repeat(dimensions) + return "$elementType" + "[]".repeat(dimensions) } } @@ -186,9 +214,60 @@ data class ArrayTypeDto( @SerialName("UnclearReferenceType") data class UnclearReferenceTypeDto( val name: String, + val typeParameters: List = emptyList(), ) : TypeDto { override fun toString(): String { - return name + return if (typeParameters.isNotEmpty()) { + val generics = typeParameters.joinToString() + "$name<$generics>" + } else { + name + } + } +} + +@Serializable +@SerialName("GenericType") +data class GenericTypeDto( + val name: String, + val defaultType: TypeDto? = null, + val constraint: TypeDto? = null, +) : TypeDto { + override fun toString(): String { + return name + (constraint?.let { " extends $it" } ?: "") + (defaultType?.let { " = $it" } ?: "") + } +} + +@Serializable +@SerialName("AliasType") +data class AliasTypeDto( + val name: String, + val originalType: TypeDto, + val signature: LocalSignatureDto, +) : TypeDto { + override fun toString(): String { + return "$name = $originalType" + } +} + +@Serializable +@SerialName("AnnotationNamespaceType") +data class AnnotationNamespaceTypeDto( + val originType: String, + val namespaceSignature: NamespaceSignatureDto, +) : TypeDto { + override fun toString(): String { + return originType + } +} + +@Serializable +@SerialName("AnnotationTypeQueryType") +data class AnnotationTypeQueryTypeDto( + val originType: String, +) : TypeDto { + override fun toString(): String { + return originType } } diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Values.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Values.kt index 10da148d5..0d8962b6b 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Values.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Values.kt @@ -20,7 +20,6 @@ import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonClassDiscriminator -import kotlinx.serialization.json.JsonElement @Serializable @OptIn(ExperimentalSerializationApi::class) @@ -32,7 +31,7 @@ sealed interface ValueDto { @Serializable @SerialName("UNKNOWN_VALUE") data class UnknownValueDto( - val value: JsonElement, + val value: String, ) : ValueDto { override val type: TypeDto get() = UnknownTypeDto @@ -368,6 +367,18 @@ data class StaticCallExprDto( } } +@Serializable +@SerialName("PtrCallExpr") +data class PtrCallExprDto( + val ptr: ValueDto, // Local + override val method: MethodSignatureDto, + override val args: List, +) : CallExprDto { + override fun toString(): String { + return "$ptr@${method.declaringClass.name}::${method.name}(${args.joinToString()})" + } +} + @Serializable sealed interface RefDto : ValueDto diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/graph/EtsApplicationGraph.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/graph/EtsApplicationGraph.kt index d1fbf410e..071fe78bb 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/graph/EtsApplicationGraph.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/graph/EtsApplicationGraph.kt @@ -16,16 +16,61 @@ package org.jacodb.ets.graph +import mu.KotlinLogging import org.jacodb.api.common.analysis.ApplicationGraph +import org.jacodb.ets.base.CONSTRUCTOR_NAME import org.jacodb.ets.base.EtsStmt +import org.jacodb.ets.base.UNKNOWN_FILE_NAME +import org.jacodb.ets.model.EtsClass +import org.jacodb.ets.model.EtsClassSignature +import org.jacodb.ets.model.EtsFileSignature import org.jacodb.ets.model.EtsMethod +import org.jacodb.ets.model.EtsMethodSignature import org.jacodb.ets.model.EtsScene +import org.jacodb.ets.utils.Maybe import org.jacodb.ets.utils.callExpr +private val logger = KotlinLogging.logger {} + interface EtsApplicationGraph : ApplicationGraph { val cp: EtsScene } +private fun EtsFileSignature?.isUnknown(): Boolean = + this == null || fileName.isBlank() || fileName == UNKNOWN_FILE_NAME + +private fun EtsClassSignature.isUnknown(): Boolean = + name.isBlank() + +private fun EtsClassSignature.isIdeal(): Boolean = + !isUnknown() && !file.isUnknown() + +enum class ComparisonResult { + Equal, + NotEqual, + Unknown, +} + +fun compareFileSignatures( + sig1: EtsFileSignature?, + sig2: EtsFileSignature?, +): ComparisonResult = when { + sig1.isUnknown() -> ComparisonResult.Unknown + sig2.isUnknown() -> ComparisonResult.Unknown + sig1?.fileName == sig2?.fileName -> ComparisonResult.Equal + else -> ComparisonResult.NotEqual +} + +fun compareClassSignatures( + sig1: EtsClassSignature, + sig2: EtsClassSignature, +): ComparisonResult = when { + sig1.isUnknown() -> ComparisonResult.Unknown + sig2.isUnknown() -> ComparisonResult.Unknown + sig1.name == sig2.name -> compareFileSignatures(sig1.file, sig2.file) + else -> ComparisonResult.NotEqual +} + class EtsApplicationGraphImpl( override val cp: EtsScene, ) : EtsApplicationGraph { @@ -42,13 +87,141 @@ class EtsApplicationGraphImpl( return successors.asSequence() } + private val cacheClassWithIdealSignature: MutableMap> = hashMapOf() + private val cacheMethodWithIdealSignature: MutableMap> = hashMapOf() + private val cachePartiallyMatchedCallees: MutableMap> = hashMapOf() + + private fun lookupClassWithIdealSignature(signature: EtsClassSignature): Maybe { + require(signature.isIdeal()) + + if (signature in cacheClassWithIdealSignature) { + return cacheClassWithIdealSignature.getValue(signature) + } + + val matched = cp.projectAndSdkClasses + .asSequence() + .filter { it.signature == signature } + .toList() + if (matched.isEmpty()) { + cacheClassWithIdealSignature[signature] = Maybe.none() + return Maybe.none() + } else { + val s = matched.singleOrNull() + ?: error("Multiple classes with the same signature: $matched") + cacheClassWithIdealSignature[signature] = Maybe.some(s) + return Maybe.some(s) + } + } + override fun callees(node: EtsStmt): Sequence { val expr = node.callExpr ?: return emptySequence() + val callee = expr.method - return cp.classes + + // Note: the resolving code below expects that at least the current method signature is known. + check(node.method.enclosingClass.isIdeal()) { + "Incomplete signature in method: ${node.method}" + } + + // Note: specific resolve for constructor: + if (callee.name == CONSTRUCTOR_NAME) { + if (!callee.enclosingClass.isIdeal()) { + // Constructor signature is garbage. Sorry, can't do anything in such case. + return emptySequence() + } + + // Here, we assume that the constructor signature is ideal. + check(callee.enclosingClass.isIdeal()) + + val cls = lookupClassWithIdealSignature(callee.enclosingClass) + if (cls.isSome) { + return sequenceOf(cls.getOrThrow().ctor) + } else { + return emptySequence() + } + } + + // If the callee signature is ideal, resolve it directly: + if (callee.enclosingClass.isIdeal()) { + if (callee in cacheMethodWithIdealSignature) { + val resolved = cacheMethodWithIdealSignature.getValue(callee) + if (resolved.isSome) { + return sequenceOf(resolved.getOrThrow()) + } else { + return emptySequence() + } + } + + val cls = lookupClassWithIdealSignature(callee.enclosingClass) + + val resolved = run { + if (cls.isNone) { + emptySequence() + } else { + cls.getOrThrow().methods.asSequence().filter { it.name == callee.name } + } + } + if (resolved.none()) { + cacheMethodWithIdealSignature[callee] = Maybe.none() + return emptySequence() + } + val r = resolved.singleOrNull() + ?: error("Multiple methods with the same complete signature: ${resolved.toList()}") + cacheMethodWithIdealSignature[callee] = Maybe.some(r) + return sequenceOf(r) + } + + // If the callee signature is not ideal, resolve it via a partial match... + check(!callee.enclosingClass.isIdeal()) + + val cls = lookupClassWithIdealSignature(node.method.enclosingClass).let { + if (it.isNone) { + error("Could not find the enclosing class: ${node.method.enclosingClass}") + } + it.getOrThrow() + } + + // If the complete signature match failed, + // try to find the unique not-the-same neighbour method in the same class: + val neighbors = cls.methods + .asSequence() + .filter { it.name == callee.name } + .filterNot { it.name == node.method.name } + .toList() + if (neighbors.isNotEmpty()) { + val s = neighbors.singleOrNull() + ?: error("Multiple methods with the same name: $neighbors") + cachePartiallyMatchedCallees[callee] = listOf(s) + return sequenceOf(s) + } + + // NOTE: cache lookup MUST be performed AFTER trying to match the neighbour! + if (callee in cachePartiallyMatchedCallees) { + return cachePartiallyMatchedCallees.getValue(callee).asSequence() + } + + // If the neighbour match failed, + // try to *uniquely* resolve the callee via a partial signature match: + val resolved = cp.projectAndSdkClasses .asSequence() - .flatMap { it.methods } - .filter { it.signature == callee } + .filter { compareClassSignatures(it.signature, callee.enclosingClass) != ComparisonResult.NotEqual } + // Note: exclude current class: + .filterNot { compareClassSignatures(it.signature, node.method.enclosingClass) != ComparisonResult.NotEqual } + // Note: omit constructors! + .flatMap { it.methods.asSequence() } + .filter { it.name == callee.name } + .toList() + if (resolved.isEmpty()) { + cachePartiallyMatchedCallees[callee] = emptyList() + return emptySequence() + } + val r = resolved.singleOrNull() ?: run { + logger.warn { "Multiple methods with the same partial signature '${callee}': $resolved" } + cachePartiallyMatchedCallees[callee] = emptyList() + return emptySequence() + } + cachePartiallyMatchedCallees[callee] = listOf(r) + return sequenceOf(r) } override fun callers(method: EtsMethod): Sequence { diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/graph/EtsCfg.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/graph/EtsCfg.kt index e7fda91b0..48de74a06 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/graph/EtsCfg.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/graph/EtsCfg.kt @@ -49,8 +49,8 @@ class EtsCfg( override val entries: List get() = listOfNotNull(stmts.firstOrNull()) - override val exits: List - get() = instructions.filterIsInstance() + override val exits: List = + instructions.filterIsInstance() override fun successors(node: EtsStmt): Set { return successorMap[node]!!.toSet() diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/EtsBaseModel.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/EtsBaseModel.kt new file mode 100644 index 000000000..731e89982 --- /dev/null +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/EtsBaseModel.kt @@ -0,0 +1,41 @@ +/* + * 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.model + +interface EtsBaseModel { + val modifiers: EtsModifiers + val decorators: List + + val isPrivate: Boolean get() = modifiers.isPrivate + val isProtected: Boolean get() = modifiers.isProtected + val isPublic: Boolean get() = modifiers.isPublic + val isExport: Boolean get() = modifiers.isExport + val isStatic: Boolean get() = modifiers.isStatic + val isAbstract: Boolean get() = modifiers.isAbstract + val isAsync: Boolean get() = modifiers.isAsync + val isConst: Boolean get() = modifiers.isConst + val isAccessor: Boolean get() = modifiers.isAccessor + val isDefault: Boolean get() = modifiers.isDefault + val isIn: Boolean get() = modifiers.isIn + val isReadonly: Boolean get() = modifiers.isReadonly + val isOut: Boolean get() = modifiers.isOut + val isOverride: Boolean get() = modifiers.isOverride + val isDeclare: Boolean get() = modifiers.isDeclare + + fun hasModifier(modifier: EtsModifier): Boolean = modifiers.hasModifier(modifier) + fun hasDecorator(decorator: EtsDecorator): Boolean = decorators.contains(decorator) +} diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/EtsClass.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/EtsClass.kt index d8661f88c..560dac296 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/EtsClass.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/EtsClass.kt @@ -16,12 +16,16 @@ package org.jacodb.ets.model -interface EtsClass { +import org.jacodb.ets.base.EtsType + +interface EtsClass : EtsBaseModel { val signature: EtsClassSignature + val typeParameters: List val fields: List val methods: List val ctor: EtsMethod val superClass: EtsClassSignature? + val implementedInterfaces: List val name: String get() = signature.name @@ -33,6 +37,10 @@ class EtsClassImpl( override val methods: List, override val ctor: EtsMethod, override val superClass: EtsClassSignature? = null, + override val implementedInterfaces: List = emptyList(), + override val typeParameters: List = emptyList(), + override val modifiers: EtsModifiers = EtsModifiers.EMPTY, + override val decorators: List = emptyList(), ) : EtsClass { init { require(ctor !in methods) diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/EtsDecorator.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/EtsDecorator.kt new file mode 100644 index 000000000..908628a81 --- /dev/null +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/EtsDecorator.kt @@ -0,0 +1,23 @@ +/* + * 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.model + +data class EtsDecorator( + val name: String, // kind + // TODO: content + // TODO: param +) diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/EtsField.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/EtsField.kt index 47ac1d67d..326faf74d 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/EtsField.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/EtsField.kt @@ -33,8 +33,7 @@ interface EtsField { class EtsFieldImpl( override val signature: EtsFieldSignature, - val accessFlags: AccessFlags = AccessFlags(), - val modifiers: List = emptyList(), + val modifiers: EtsModifiers = EtsModifiers.EMPTY, val isOptional: Boolean = false, // '?' val isDefinitelyAssigned: Boolean = false, // '!' ) : EtsField { @@ -45,11 +44,3 @@ class EtsFieldImpl( return signature.toString() } } - -data class AccessFlags( - var isStatic: Boolean = false, - var isPublic: Boolean = false, - var isPrivate: Boolean = false, - var isProtected: Boolean = false, - var isReadOnly: Boolean = false, -) diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/EtsFile.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/EtsFile.kt index d5290b362..d9c363c2d 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/EtsFile.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/EtsFile.kt @@ -26,6 +26,10 @@ class EtsFile( val projectName: String get() = signature.projectName + val allClasses: List by lazy { + classes + namespaces.flatMap { it.allClasses } + } + override fun toString(): String { return name } diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/EtsMethod.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/EtsMethod.kt index 44880a64f..fb294301c 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/EtsMethod.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/EtsMethod.kt @@ -14,35 +14,28 @@ * limitations under the License. */ +@file:Suppress("PropertyName") + package org.jacodb.ets.model import org.jacodb.api.common.CommonMethod +import org.jacodb.ets.base.EtsLocal import org.jacodb.ets.base.EtsType import org.jacodb.ets.graph.EtsCfg -// TODO: decorators // TODO: typeParameters -interface EtsMethod : CommonMethod { +interface EtsMethod : EtsBaseModel, CommonMethod { val signature: EtsMethodSignature - val localsCount: Int + val typeParameters: List + val locals: List val cfg: EtsCfg - val modifiers: List val enclosingClass: EtsClassSignature get() = signature.enclosingClass - val isStatic: Boolean - get() = modifiers.contains("StaticKeyword") - - val isPrivate: Boolean - get() = modifiers.contains("PrivateKeyword") - // If not specified, entity is public if not private and not protected - val isPublic: Boolean - get() = modifiers.contains("PublicKeyword") || (!isPrivate && !isProtected) - - val isProtected: Boolean - get() = modifiers.contains("ProtectedKeyword") + override val isPublic: Boolean + get() = super.isPublic || (!isPrivate && !isProtected) override val name: String get() = signature.name @@ -60,11 +53,12 @@ interface EtsMethod : CommonMethod { class EtsMethodImpl( override val signature: EtsMethodSignature, - // Default locals count is args + this - override val localsCount: Int = signature.parameters.size + 1, - override val modifiers: List = emptyList(), + override val typeParameters: List = emptyList(), + override val locals: List = emptyList(), + override val modifiers: EtsModifiers = EtsModifiers.EMPTY, + override val decorators: List = emptyList(), ) : EtsMethod { - internal var _cfg: EtsCfg? = null + var _cfg: EtsCfg? = null override val cfg: EtsCfg get() = _cfg ?: EtsCfg.empty() diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/EtsModifier.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/EtsModifier.kt new file mode 100644 index 000000000..cf584b780 --- /dev/null +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/EtsModifier.kt @@ -0,0 +1,60 @@ +/* + * 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.model + +enum class EtsModifier(val value: Int, val string: String) { + PRIVATE(1 shl 0, "private"), + PROTECTED(1 shl 1, "protected"), + PUBLIC(1 shl 2, "public"), + EXPORT(1 shl 3, "export"), + STATIC(1 shl 4, "static"), + ABSTRACT(1 shl 5, "abstract"), + ASYNC(1 shl 6, "async"), + CONST(1 shl 7, "const"), + ACCESSOR(1 shl 8, "accessor"), + DEFAULT(1 shl 9, "default"), + IN(1 shl 10, "in"), + READONLY(1 shl 11, "readonly"), + OUT(1 shl 12, "out"), + OVERRIDE(1 shl 13, "override"), + DECLARE(1 shl 14, "declare"); +} + +@JvmInline +value class EtsModifiers(val mask: Int) { + companion object { + val EMPTY = EtsModifiers(0) + } + + val isPrivate: Boolean get() = hasModifier(EtsModifier.PRIVATE) + val isProtected: Boolean get() = hasModifier(EtsModifier.PROTECTED) + val isPublic: Boolean get() = hasModifier(EtsModifier.PUBLIC) + val isExport: Boolean get() = hasModifier(EtsModifier.EXPORT) + val isStatic: Boolean get() = hasModifier(EtsModifier.STATIC) + val isAbstract: Boolean get() = hasModifier(EtsModifier.ABSTRACT) + val isAsync: Boolean get() = hasModifier(EtsModifier.ASYNC) + val isConst: Boolean get() = hasModifier(EtsModifier.CONST) + val isAccessor: Boolean get() = hasModifier(EtsModifier.ACCESSOR) + val isDefault: Boolean get() = hasModifier(EtsModifier.DEFAULT) + val isIn: Boolean get() = hasModifier(EtsModifier.IN) + val isReadonly: Boolean get() = hasModifier(EtsModifier.READONLY) + val isOut: Boolean get() = hasModifier(EtsModifier.OUT) + val isOverride: Boolean get() = hasModifier(EtsModifier.OVERRIDE) + val isDeclare: Boolean get() = hasModifier(EtsModifier.DECLARE) + + fun hasModifier(modifier: EtsModifier): Boolean = (mask and modifier.value) != 0 +} diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/EtsNamespace.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/EtsNamespace.kt index 5d982f57d..17ab2d0bc 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/EtsNamespace.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/EtsNamespace.kt @@ -20,4 +20,7 @@ class EtsNamespace( val signature: EtsNamespaceSignature, val classes: List, val namespaces: List, -) +) { + val allClasses: List + get() = classes + namespaces.flatMap { it.allClasses } +} diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/EtsScene.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/EtsScene.kt index 2d43715b9..0f593308d 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/EtsScene.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/EtsScene.kt @@ -18,9 +18,16 @@ package org.jacodb.ets.model import org.jacodb.api.common.CommonProject -class EtsScene ( - val files: List, -): CommonProject { - val classes: List - get() = files.flatMap { it.classes } +class EtsScene( + val projectFiles: List, + val sdkFiles: List = emptyList(), +) : CommonProject { + val projectClasses: List + get() = projectFiles.flatMap { it.allClasses } + + val sdkClasses: List + get() = sdkFiles.flatMap { it.allClasses } + + val projectAndSdkClasses: List + get() = projectClasses + sdkClasses } diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/EtsSignature.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/EtsSignature.kt index 927f44d21..28f8bba54 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/EtsSignature.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/EtsSignature.kt @@ -18,6 +18,10 @@ package org.jacodb.ets.model import org.jacodb.api.common.CommonMethodParameter import org.jacodb.ets.base.EtsType +import org.jacodb.ets.base.UNKNOWN_CLASS_NAME +import org.jacodb.ets.base.UNKNOWN_FILE_NAME +import org.jacodb.ets.base.UNKNOWN_NAMESPACE_NAME +import org.jacodb.ets.base.UNKNOWN_PROJECT_NAME /** * Precompiled [Regex] for `.d.ts` and `.ts` file extensions. @@ -30,56 +34,49 @@ data class EtsFileSignature( ) { override fun toString(): String { // Remove ".d.ts" and ".ts" file ext: - val tmp = fileName.replace(REGEX_TS_SUFFIX, "") - // TODO: projectName was omitted for now in toString(), since it disturbs the debugging output. - // return if (projectName.isNotBlank()) { - // "@$projectName/$tmp" - // } else { - // tmp - // } - return tmp + val name = fileName.replace(REGEX_TS_SUFFIX, "") + return "@$projectName/$name" + } + + companion object { + val DEFAULT = EtsFileSignature(projectName = UNKNOWN_PROJECT_NAME, fileName = UNKNOWN_FILE_NAME) } } data class EtsNamespaceSignature( val name: String, - val file: EtsFileSignature? = null, + val file: EtsFileSignature, val namespace: EtsNamespaceSignature? = null, ) { override fun toString(): String { - // TODO: 'file' is not included in the toString() output, - // because it only disturbs the debugging output. return if (namespace != null) { "$namespace::$name" } else { - name + "$file: $name" } } + + companion object { + val DEFAULT = EtsNamespaceSignature(name = UNKNOWN_NAMESPACE_NAME, file = EtsFileSignature.DEFAULT) + } } data class EtsClassSignature( val name: String, - val file: EtsFileSignature? = null, + val file: EtsFileSignature, val namespace: EtsNamespaceSignature? = null, ) { - // TODO: more manual testing is required in order to understand whether - // the class can have both "declaring file" and "declaring namespace". - // Until then, the following check is commented out: - // init { - // require(!(file != null && namespace != null)) { - // "Class cannot have both declaring file and declaring namespace" - // } - // } - override fun toString(): String { return if (namespace != null) { "$namespace::$name" - } else if (file != null) { - "$name in $file" } else { - name + "$file: $name" } } + + companion object { + val DEFAULT = EtsClassSignature(name = UNKNOWN_CLASS_NAME, file = EtsFileSignature.DEFAULT) + } } data class EtsFieldSignature( @@ -112,34 +109,12 @@ data class EtsMethodSignature( val parameters: List, val returnType: EtsType, ) { - - constructor( - enclosingClass: EtsClassSignature, - sub: EtsMethodSubSignature, - ) : this( - enclosingClass, - sub.name, - sub.parameters, - sub.returnType, - ) - override fun toString(): String { val params = parameters.joinToString() return "${enclosingClass.name}::$name($params): $returnType" } } -data class EtsMethodSubSignature( - val name: String, - val parameters: List, - val returnType: EtsType, -) { - override fun toString(): String { - val params = parameters.joinToString() - return "$name($params): $returnType" - } -} - data class EtsMethodParameter( val index: Int, val name: String, @@ -150,3 +125,12 @@ data class EtsMethodParameter( return "$name${if (isOptional) "?" else ""}: $type" } } + +data class EtsLocalSignature( + val name: String, + val method: EtsMethodSignature, +) { + override fun toString(): String { + return "${method}#$name" + } +} diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/EtsCfgExt.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/EtsCfgExt.kt index 671b8204c..d04db630c 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/EtsCfgExt.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/EtsCfgExt.kt @@ -29,46 +29,60 @@ import java.io.File import java.nio.file.Files import java.nio.file.Path -fun EtsCfg.view(dotCmd: String, viewerCmd: String, viewCatchConnections: Boolean = false) { - Util.sh(arrayOf(viewerCmd, "file://${toFile(dotCmd, viewCatchConnections)}")) +private const val DEFAULT_DOT_CMD = "dot" + +fun EtsCfg.view( + viewerCmd: String = if (System.getProperty("os.name").startsWith("Windows")) "start" else "xdg-open", + dotCmd: String = DEFAULT_DOT_CMD, + viewCatchConnections: Boolean = true, +) { + val path = toFile(null, dotCmd, viewCatchConnections) + Util.sh(arrayOf(viewerCmd, "file://$path")) } -fun EtsCfg.toFile(dotCmd: String, viewCatchConnections: Boolean = false, file: File? = null): Path { +fun EtsCfg.toFile( + file: File? = null, + dotCmd: String = DEFAULT_DOT_CMD, + viewCatchConnections: Boolean = true, +): Path { Graph.setDefaultCmd(dotCmd) val graph = Graph("etsCfg") + .setBgColor(Color.X11.transparent) + .setFontSize(12.0) + .setFontName("Fira Mono") val nodes = mutableMapOf() for ((index, inst) in instructions.withIndex()) { + val label = inst.toString().replace("\"", "\\\"") val node = Node("$index") .setShape(Shape.box) - .setLabel(inst.toString().replace("\"", "\\\"")) + .setLabel(label) .setFontSize(12.0) nodes[inst] = node graph.addNode(node) } - graph.setBgColor(Color.X11.transparent) - graph.setFontSize(12.0) - graph.setFontName("Fira Mono") - for ((inst, node) in nodes) { when (inst) { is EtsIfStmt -> { val successors = successors(inst).toList() - check(successors.size == 2) + // check(successors.size == 2) + check(successors.size <= 2) graph.addEdge( Edge(node.name, nodes[successors[0]]!!.name) .also { it.setLabel("false") } ) - graph.addEdge( - Edge(node.name, nodes[successors[1]]!!.name) - .also { - it.setLabel("true") - } - ) + if (successors.size == 2) { + graph.addEdge( + Edge(node.name, nodes[successors[1]]!!.name) + .also { + it.setLabel("true") + } + ) + } } else -> for (successor in successors(inst)) { diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/EtsFileDtoToDot.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/EtsFileDtoToDot.kt index e00dda059..8c6253132 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/EtsFileDtoToDot.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/EtsFileDtoToDot.kt @@ -46,17 +46,29 @@ fun EtsFileDto.toDot(useLR: Boolean = false): String { fun classLabel(clazz: ClassDto): String { val labelLines: MutableList = mutableListOf() - labelLines += clazz.signature.name + run { + val name = clazz.signature.name + val typeParameters = clazz.typeParameters.orEmpty() + val generics = if (typeParameters.isNotEmpty()) { + "<${typeParameters.joinToString()}>" + } else { + "" + } + labelLines += "$name$generics" + } labelLines += "Fields: (${clazz.fields.size})" clazz.fields.forEach { field -> - labelLines += " ${field.signature.name}: ${field.signature.type}" + val name = field.signature.name + val returnType = field.signature.type + labelLines += " $name: $returnType" } labelLines += "Methods: (${clazz.methods.size})" clazz.methods.forEach { method -> val name = method.signature.name val params = method.signature.parameters.joinToString() - val generics = if (method.typeParameters.isNotEmpty()) { - "<${method.typeParameters.joinToString()}>" + val typeParameters = method.typeParameters.orEmpty() + val generics = if (typeParameters.isNotEmpty()) { + "<${typeParameters.joinToString()}>" } else { "" } diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/GetOperands.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/GetOperands.kt index 73a54ae30..324cc8db5 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/GetOperands.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/GetOperands.kt @@ -65,6 +65,7 @@ import org.jacodb.ets.base.EtsPostDecExpr import org.jacodb.ets.base.EtsPostIncExpr import org.jacodb.ets.base.EtsPreDecExpr import org.jacodb.ets.base.EtsPreIncExpr +import org.jacodb.ets.base.EtsPtrCallExpr import org.jacodb.ets.base.EtsRemExpr import org.jacodb.ets.base.EtsReturnStmt import org.jacodb.ets.base.EtsRightShiftExpr @@ -295,6 +296,9 @@ private object EntityGetOperands : EtsEntity.Visitor> { override fun visit(expr: EtsStaticCallExpr): Sequence = expr.args.asSequence() + override fun visit(expr: EtsPtrCallExpr): Sequence = + sequenceOf(expr.ptr) + expr.args.asSequence() + override fun visit(expr: EtsCommaExpr): Sequence = sequenceOf(expr.left, expr.right) diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/LoadEtsFile.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/LoadEtsFile.kt index cb6a057cf..695b2ed1e 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/LoadEtsFile.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/LoadEtsFile.kt @@ -19,14 +19,18 @@ package org.jacodb.ets.utils import org.jacodb.ets.dto.EtsFileDto import org.jacodb.ets.dto.convertToEtsFile import org.jacodb.ets.model.EtsFile +import org.jacodb.ets.model.EtsScene import java.io.FileNotFoundException import java.nio.file.Path import kotlin.io.path.Path import kotlin.io.path.absolute +import kotlin.io.path.createTempDirectory import kotlin.io.path.exists +import kotlin.io.path.extension import kotlin.io.path.inputStream import kotlin.io.path.nameWithoutExtension import kotlin.io.path.pathString +import kotlin.io.path.walk import kotlin.time.Duration.Companion.seconds private const val ENV_VAR_ARK_ANALYZER_DIR = "ARKANALYZER_DIR" @@ -38,7 +42,12 @@ private const val DEFAULT_SERIALIZE_SCRIPT_PATH = "out/src/save/serializeArkIR.j private const val ENV_VAR_NODE_EXECUTABLE = "NODE_EXECUTABLE" private const val DEFAULT_NODE_EXECUTABLE = "node" -fun generateEtsFileIR(tsPath: Path): Path { +fun generateEtsIR( + projectPath: Path, + isProject: Boolean = false, + loadEntrypoints: Boolean = true, + useArkAnalyzerTypeInference: Int? = null, +): Path { val arkAnalyzerDir = Path(System.getenv(ENV_VAR_ARK_ANALYZER_DIR) ?: DEFAULT_ARK_ANALYZER_DIR) if (!arkAnalyzerDir.exists()) { throw FileNotFoundException( @@ -59,22 +68,78 @@ fun generateEtsFileIR(tsPath: Path): Path { } val node = System.getenv(ENV_VAR_NODE_EXECUTABLE) ?: DEFAULT_NODE_EXECUTABLE - val output = kotlin.io.path.createTempFile(prefix = tsPath.nameWithoutExtension, suffix = ".json") - val cmd: List = listOf( + val output = if (isProject) { + createTempDirectory(prefix = projectPath.nameWithoutExtension) + } else { + kotlin.io.path.createTempFile(prefix = projectPath.nameWithoutExtension, suffix = ".json") + } + + val cmd = listOfNotNull( node, script.pathString, - tsPath.pathString, + if (isProject) "-p" else null, + if (loadEntrypoints) "-e" else null, + useArkAnalyzerTypeInference?.let { "-t $it" }, + projectPath.pathString, output.pathString, ) - runProcess(cmd, 60.seconds) + runProcess(cmd, 10.seconds) return output } -fun loadEtsFileAutoConvert(tsPath: Path): EtsFile { - val irFilePath = generateEtsFileIR(tsPath) +fun generateSdkIR(sdkPath: Path): Path = generateEtsIR( + sdkPath, + isProject = true, + loadEntrypoints = false, + useArkAnalyzerTypeInference = 0, +) + +fun loadEtsFileAutoConvert(projectPath: Path): EtsFile { + val irFilePath = generateEtsIR( + projectPath, + isProject = false, + useArkAnalyzerTypeInference = 1, + ) irFilePath.inputStream().use { stream -> val etsFileDto = EtsFileDto.loadFromJson(stream) - val etsFile = convertToEtsFile(etsFileDto) - return etsFile + return convertToEtsFile(etsFileDto) } } + +fun loadEtsProjectAutoConvert( + projectPath: Path, + sdkIRPath: Path? = null, + loadEntrypoints: Boolean = false, + useArkAnalyzerTypeInference: Int? = 1, +): EtsScene { + val irFolderPath = generateEtsIR( + projectPath, + isProject = true, + loadEntrypoints = loadEntrypoints, + useArkAnalyzerTypeInference = useArkAnalyzerTypeInference, + ) + + return loadEtsProjectFromIR(irFolderPath, sdkIRPath) +} + +fun loadEtsProjectFromIR( + projectFilesPath: Path, + sdkFilesPath: Path?, +): EtsScene { + val walker = { irFolder: Path -> + irFolder.walk() + .filter { it.extension == "json" } + .map { + it.inputStream().use { stream -> + val etsFileDto = EtsFileDto.loadFromJson(stream) + convertToEtsFile(etsFileDto) + } + } + .toList() + } + + val projectFiles = walker(projectFilesPath) + val sdkFiles = sdkFilesPath?.let { walker(it) }.orEmpty() + + return EtsScene(projectFiles, sdkFiles) +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Maybe.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/Maybe.kt similarity index 95% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Maybe.kt rename to jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/Maybe.kt index 0ccec0670..b27a89db6 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Maybe.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/Maybe.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.jacodb.analysis.ifds +package org.jacodb.ets.utils @JvmInline value class Maybe private constructor( @@ -65,3 +65,5 @@ inline fun Maybe.onNone(body: () -> Unit): Maybe { } fun T?.toMaybe(): Maybe = Maybe.from(this) + +fun Maybe.getOrNull(): T? = if (isSome) getOrThrow() else null diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/Utils.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/Utils.kt index d6be175a7..380772e30 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/Utils.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/Utils.kt @@ -72,17 +72,20 @@ fun EtsFileDto.toText(): String { lines += " superClass = '${clazz.superClassName}'" lines += " typeParameters = ${clazz.typeParameters}" lines += " modifiers = ${clazz.modifiers}" + lines += " decorators = ${clazz.decorators}" lines += " fields: ${clazz.fields.size}" clazz.fields.forEach { field -> lines += " - FIELD '${field.signature}'" - lines += " typeParameters = ${field.typeParameters}" lines += " modifiers = ${field.modifiers}" + lines += " decorators = ${field.decorators}" lines += " isOptional = ${field.isOptional}" lines += " isDefinitelyAssigned = ${field.isDefinitelyAssigned}" } lines += " methods: ${clazz.methods.size}" clazz.methods.forEach { method -> lines += " - METHOD '${method.signature}'" + lines += " modifiers = ${method.modifiers}" + lines += " decorators = ${method.decorators}" lines += " typeParameters = ${method.typeParameters}" if (method.body != null) { lines += " locals = ${method.body.locals}" @@ -107,12 +110,19 @@ fun EtsFile.toText(): String { lines += "EtsFile '${signature}':" classes.forEach { clazz -> lines += "= CLASS '${clazz.signature}':" + lines += " typeParameters = ${clazz.typeParameters}" + lines += " modifiers = ${clazz.modifiers}" + lines += " decorators = ${clazz.decorators}" lines += " superClass = '${clazz.superClass}'" lines += " fields: ${clazz.fields.size}" clazz.fields.forEach { field -> lines += " - FIELD '${field.signature}'" } lines += " constructor = '${clazz.ctor.signature}'" + lines += " typeParameters = ${clazz.ctor.typeParameters}" + lines += " modifiers = ${clazz.ctor.modifiers}" + lines += " decorators = ${clazz.ctor.decorators}" + lines += " locals: ${clazz.ctor.locals.size}" lines += " stmts: ${clazz.ctor.cfg.stmts.size}" clazz.ctor.cfg.stmts.forEach { stmt -> lines += " ${stmt.location.index}. $stmt" @@ -122,7 +132,10 @@ fun EtsFile.toText(): String { lines += " methods: ${clazz.methods.size}" clazz.methods.forEach { method -> lines += " - METHOD '${method.signature}':" - lines += " locals = ${method.localsCount}" + lines += " typeParameters = ${method.typeParameters}" + lines += " modifiers = ${method.modifiers}" + lines += " decorators = ${method.decorators}" + lines += " locals: ${method.locals.size}" lines += " stmts: ${method.cfg.stmts.size}" method.cfg.stmts.forEach { stmt -> lines += " ${stmt.location.index}. $stmt" diff --git a/jacodb-ets/src/test/kotlin/org/jacodb/ets/test/EtsFileTest.kt b/jacodb-ets/src/test/kotlin/org/jacodb/ets/test/EtsFileTest.kt index 45d711599..455537b8b 100644 --- a/jacodb-ets/src/test/kotlin/org/jacodb/ets/test/EtsFileTest.kt +++ b/jacodb-ets/src/test/kotlin/org/jacodb/ets/test/EtsFileTest.kt @@ -21,12 +21,16 @@ import org.jacodb.ets.base.EtsInstanceFieldRef import org.jacodb.ets.base.EtsLocal import org.jacodb.ets.base.EtsNumberConstant import org.jacodb.ets.base.EtsReturnStmt +import org.jacodb.ets.base.EtsStaticFieldRef import org.jacodb.ets.base.EtsThis +import org.jacodb.ets.base.INSTANCE_INIT_METHOD_NAME +import org.jacodb.ets.base.STATIC_INIT_METHOD_NAME import org.jacodb.ets.model.EtsFile import org.jacodb.ets.test.utils.loadEtsFileFromResource import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertIs +import kotlin.test.assertTrue private val logger = mu.KotlinLogging.logger {} @@ -46,7 +50,7 @@ class EtsFileTest { etsFile.classes.forEach { cls -> cls.methods.forEach { method -> logger.info { - "Method '$method', locals: ${method.localsCount}, instructions: ${method.cfg.instructions.size}" + "Method '$method', locals: ${method.locals.size}, instructions: ${method.cfg.instructions.size}" } method.cfg.instructions.forEach { inst -> logger.info { "${inst.location.index}. $inst" } @@ -62,11 +66,11 @@ class EtsFileTest { cls.methods.forEach { method -> when (method.name) { "add" -> { - assertEquals(9, method.cfg.instructions.size) + assertTrue( method.cfg.instructions.size > 2) } "main" -> { - assertEquals(4, method.cfg.instructions.size) + assertTrue(method.cfg.instructions.size > 2) } } } @@ -81,10 +85,10 @@ class EtsFileTest { // instance initializer run { - val method = cls.methods.single { it.name == "@instance_init" } + val method = cls.methods.single { it.name == INSTANCE_INIT_METHOD_NAME } assertEquals(3, method.cfg.instructions.size) - // Local("this") := ThisRef + // this := ThisRef run { val stmt = method.cfg.instructions[0] assertIs(stmt) @@ -98,7 +102,7 @@ class EtsFileTest { assertEquals("Foo", rhv.type.typeName) } - // Local("this").x := 99 + // this.x := 99 run { val stmt = method.cfg.instructions[1] assertIs(stmt) @@ -128,10 +132,10 @@ class EtsFileTest { // static initializer run { - val method = cls.methods.single { it.name == "@static_init" } + val method = cls.methods.single { it.name == STATIC_INIT_METHOD_NAME } assertEquals(3, method.cfg.instructions.size) - // Local("this") := ThisRef + // this := ThisRef run { val stmt = method.cfg.instructions[0] assertIs(stmt) @@ -145,17 +149,16 @@ class EtsFileTest { assertEquals("Foo", rhv.type.typeName) } - // Local("this").y := 111 + // this.y := 111 run { val stmt = method.cfg.instructions[1] assertIs(stmt) val lhv = stmt.lhv - assertIs(lhv) + assertIs(lhv) - val instance = lhv.instance - assertIs(instance) - assertEquals("this", instance.name) + val clazz = lhv.field.enclosingClass + assertEquals("Foo", clazz.name) val field = lhv.field assertEquals("y", field.name) @@ -167,7 +170,97 @@ class EtsFileTest { // return run { - val stmt = method.cfg.instructions[2] + val stmt = method.cfg.instructions.last() + assertIs(stmt) + assertEquals(null, stmt.returnValue) + } + } + + // static field in instance method + run { + val method = cls.methods.single { it.name == "foo" } + + // this := ThisRef + run { + val stmt = method.cfg.instructions[0] + assertIs(stmt) + + val lhv = stmt.lhv + assertIs(lhv) + assertEquals("this", lhv.name) + + val rhv = stmt.rhv + assertIs(rhv) + assertEquals("Foo", rhv.type.typeName) + } + + // Foo.y := 222 + run { + val stmt = method.cfg.instructions[1] + assertIs(stmt) + + val lhv = stmt.lhv + assertIs(lhv) + + val clazz = lhv.field.enclosingClass + assertEquals("Foo", clazz.name) + + val field = lhv.field + assertEquals("y", field.name) + + val rhv = stmt.rhv + assertIs(rhv) + assertEquals(222.0, rhv.value) + } + + // return + run { + val stmt = method.cfg.instructions.last() + assertIs(stmt) + assertEquals(null, stmt.returnValue) + } + } + + // static field in static method + run { + val method = cls.methods.single { it.name == "bar" } + + // this := ThisRef + run { + val stmt = method.cfg.instructions[0] + assertIs(stmt) + + val lhv = stmt.lhv + assertIs(lhv) + assertEquals("this", lhv.name) + + val rhv = stmt.rhv + assertIs(rhv) + assertEquals("Foo", rhv.type.typeName) + } + + // this.y := 333 + run { + val stmt = method.cfg.instructions[1] + assertIs(stmt) + + val lhv = stmt.lhv + assertIs(lhv) + + val clazz = lhv.field.enclosingClass + assertEquals("Foo", clazz.name) + + val field = lhv.field + assertEquals("y", field.name) + + val rhv = stmt.rhv + assertIs(rhv) + assertEquals(333.0, rhv.value) + } + + // return + run { + val stmt = method.cfg.instructions.last() assertIs(stmt) assertEquals(null, stmt.returnValue) } diff --git a/jacodb-ets/src/test/kotlin/org/jacodb/ets/test/EtsFromJsonTest.kt b/jacodb-ets/src/test/kotlin/org/jacodb/ets/test/EtsFromJsonTest.kt index 68c0cb2c0..49c1d8c9e 100644 --- a/jacodb-ets/src/test/kotlin/org/jacodb/ets/test/EtsFromJsonTest.kt +++ b/jacodb-ets/src/test/kotlin/org/jacodb/ets/test/EtsFromJsonTest.kt @@ -19,6 +19,9 @@ package org.jacodb.ets.test import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import mu.KotlinLogging +import org.jacodb.ets.base.DEFAULT_ARK_CLASS_NAME +import org.jacodb.ets.base.DEFAULT_ARK_METHOD_NAME import org.jacodb.ets.base.EtsAnyType import org.jacodb.ets.base.EtsInstLocation import org.jacodb.ets.base.EtsLocal @@ -26,39 +29,62 @@ import org.jacodb.ets.base.EtsReturnStmt import org.jacodb.ets.base.EtsUnknownType import org.jacodb.ets.dto.AnyTypeDto import org.jacodb.ets.dto.ClassSignatureDto +import org.jacodb.ets.dto.DecoratorDto import org.jacodb.ets.dto.EtsMethodBuilder import org.jacodb.ets.dto.FieldDto import org.jacodb.ets.dto.FieldSignatureDto import org.jacodb.ets.dto.FileSignatureDto +import org.jacodb.ets.dto.LiteralTypeDto import org.jacodb.ets.dto.LocalDto import org.jacodb.ets.dto.MethodDto -import org.jacodb.ets.dto.ModifierDto import org.jacodb.ets.dto.NumberTypeDto +import org.jacodb.ets.dto.PrimitiveLiteralDto import org.jacodb.ets.dto.ReturnVoidStmtDto import org.jacodb.ets.dto.StmtDto +import org.jacodb.ets.dto.TypeDto import org.jacodb.ets.dto.convertToEtsFile import org.jacodb.ets.dto.convertToEtsMethod import org.jacodb.ets.model.EtsClassSignature +import org.jacodb.ets.model.EtsFileSignature import org.jacodb.ets.model.EtsMethodSignature +import org.jacodb.ets.test.utils.getResourcePath +import org.jacodb.ets.test.utils.getResourcePathOrNull import org.jacodb.ets.test.utils.loadEtsFileDtoFromResource +import org.jacodb.ets.test.utils.loadEtsProjectFromResources +import org.jacodb.ets.test.utils.testFactory import org.jacodb.ets.utils.loadEtsFileAutoConvert import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestFactory +import org.junit.jupiter.api.condition.EnabledIf +import kotlin.io.path.div +import kotlin.io.path.exists import kotlin.io.path.toPath +import kotlin.test.assertEquals +import kotlin.test.assertIs + +private val logger = KotlinLogging.logger {} class EtsFromJsonTest { - private val json = Json { - // classDiscriminator = "_" - prettyPrint = true - } + companion object { + private val json = Json { + // classDiscriminator = "_" + prettyPrint = true + } + + private val defaultSignature = EtsMethodSignature( + enclosingClass = EtsClassSignature( + name = DEFAULT_ARK_CLASS_NAME, + file = EtsFileSignature.DEFAULT, + ), + name = DEFAULT_ARK_METHOD_NAME, + parameters = emptyList(), + returnType = EtsAnyType, + ) - private val defaultSignature = EtsMethodSignature( - enclosingClass = EtsClassSignature(name = "_DEFAULT_ARK_CLASS"), - name = "_DEFAULT_ARK_METHOD", - parameters = emptyList(), - returnType = EtsAnyType, - ) + private const val PROJECT_PATH = "/projects/ArkTSDistributedCalc" + } @Test fun testLoadEtsFileFromJson() { @@ -67,17 +93,102 @@ class EtsFromJsonTest { println("etsDto = $etsDto") val ets = convertToEtsFile(etsDto) println("ets = $ets") + + println("Classes: ${ets.classes.size}") + for (cls in ets.classes) { + println("= $cls with ${cls.methods.size} methods:") + for (method in cls.methods) { + println(" - $method") + } + } } @Test fun testLoadEtsFileAutoConvert() { val path = "/samples/source/example.ts" - val res = this::class.java.getResource(path)?.toURI()?.toPath() - ?: error("Resource not found: $path") + val res = getResourcePath(path) val etsFile = loadEtsFileAutoConvert(res) println("etsFile = $etsFile") } + private fun projectAvailable(): Boolean { + val path = this::class.java.getResource(PROJECT_PATH)?.toURI()?.toPath() + return path != null && path.exists() + } + + @EnabledIf("projectAvailable") + @Test + fun testLoadEtsProject() { + val modules = listOf( + "entry", + ) + val prefix = "$PROJECT_PATH/etsir" + val project = loadEtsProjectFromResources(modules, prefix) + println("Classes: ${project.projectClasses.size}") + for (cls in project.projectClasses) { + println("= ${cls.signature} with ${cls.methods.size} methods:") + for (method in cls.methods) { + println(" - ${method.signature}") + } + } + } + + @TestFactory + fun testLoadAllAvailableEtsProjects() = testFactory { + val p = getResourcePathOrNull("/projects") ?: run { + logger.warn { "No projects directory found in resources" } + return@testFactory + } + val availableProjectNames = p.toFile().listFiles { f -> f.isDirectory }!!.map { it.name } + logger.info { + buildString { + appendLine("Found projects: ${availableProjectNames.size}") + for (name in availableProjectNames) { + appendLine(" - $name") + } + } + } + if (availableProjectNames.isEmpty()) { + logger.warn { "No projects found" } + return@testFactory + } + container("load ${availableProjectNames.size} projects") { + for (projectName in availableProjectNames.sorted()) { + test("load $projectName") { + dynamicLoadEtsProject(projectName) + } + } + } + } + + private fun dynamicLoadEtsProject(projectName: String) { + logger.info { "Loading project: $projectName" } + val projectPath = getResourcePath("/projects/$projectName") + val etsirPath = projectPath / "etsir" + if (!etsirPath.exists()) { + logger.warn { "No etsir directory found for project $projectName" } + return + } + val modules = etsirPath.toFile().listFiles { f -> f.isDirectory }!!.map { it.name } + logger.info { "Found ${modules.size} modules: $modules" } + if (modules.isEmpty()) { + logger.warn { "No modules found for project $projectName" } + return + } + val project = loadEtsProjectFromResources(modules, "/projects/$projectName/etsir") + logger.info { + buildString { + appendLine("Loaded project with ${project.projectClasses.size} classes and ${project.projectClasses.sumOf { it.methods.size }} methods") + for (cls in project.projectClasses) { + appendLine("= ${cls.signature} with ${cls.methods.size} methods:") + for (method in cls.methods) { + appendLine(" - ${method.signature}") + } + } + } + } + } + @Test fun testLoadValueFromJson() { val jsonString = """ @@ -111,8 +222,8 @@ class EtsFromJsonTest { name = "x", type = NumberTypeDto, ), - modifiers = emptyList(), - typeParameters = emptyList(), + modifiers = 0, + decorators = emptyList(), isOptional = true, isDefinitelyAssigned = false, ) @@ -144,15 +255,20 @@ class EtsFromJsonTest { { "signature": { "declaringClass": { - "name": "_DEFAULT_ARK_CLASS" + "name": "$DEFAULT_ARK_CLASS_NAME", + "declaringFile": { + "projectName": "TestProject", + "fileName": "test.ts" + } }, - "name": "_DEFAULT_ARK_METHOD", + "name": "$DEFAULT_ARK_METHOD_NAME", "parameters": [], "returnType": { "_": "UnknownType" } }, - "modifiers": [], + "modifiers": 0, + "decorators": [], "typeParameters": [], "body": { "locals": [], @@ -180,15 +296,19 @@ class EtsFromJsonTest { Assertions.assertEquals( EtsMethodSignature( enclosingClass = EtsClassSignature( - name = "_DEFAULT_ARK_CLASS", + name = DEFAULT_ARK_CLASS_NAME, + file = EtsFileSignature( + projectName = "TestProject", + fileName = "test.ts", + ), ), - name = "_DEFAULT_ARK_METHOD", + name = DEFAULT_ARK_METHOD_NAME, parameters = emptyList(), returnType = EtsUnknownType, ), method.signature ) - Assertions.assertEquals(0, method.localsCount) + Assertions.assertEquals(0, method.locals.size) Assertions.assertEquals(1, method.cfg.stmts.size) Assertions.assertEquals( listOf( @@ -199,41 +319,31 @@ class EtsFromJsonTest { } @Test - fun testLoadModifierFromJson() { + fun testLoadDecoratorFromJson() { val jsonString = """ { - "kind": "cat", - "content": "Brian" + "kind": "cat" } """.trimIndent() - val modifierDto = Json.decodeFromString(jsonString) - println("modifierDto = $modifierDto") - Assertions.assertEquals(ModifierDto.DecoratorItem("cat", "Brian"), modifierDto) - val jsonString2 = json.encodeToString(modifierDto) + val decoratorDto = Json.decodeFromString(jsonString) + println("decoratorDto = $decoratorDto") + Assertions.assertEquals(DecoratorDto("cat"), decoratorDto) + val jsonString2 = json.encodeToString(decoratorDto) println("json: $jsonString2") } @Test - fun testLoadListOfModifiersFromJson() { + fun testLoadLiteralTypeFromJson() { + // TS: `let x: "hello" = "hello";` val jsonString = """ - [ - { - "kind": "cat", - "content": "Bruce" - }, - "public", - "static" - ] + { + "_": "LiteralType", + "literal": "hello" + } """.trimIndent() - val modifiers = Json.decodeFromString>(jsonString) - println("modifiers = $modifiers") - Assertions.assertEquals( - listOf( - ModifierDto.DecoratorItem("cat", "Bruce"), - ModifierDto.StringItem("public"), - ModifierDto.StringItem("static"), - ), - modifiers - ) + val typeDto = Json.decodeFromString(jsonString) + println("typeDto = $typeDto") + assertIs(typeDto) + assertEquals(PrimitiveLiteralDto.StringLiteral("hello"), typeDto.literal) } } diff --git a/jacodb-ets/src/test/kotlin/org/jacodb/ets/test/utils/Entrypoints.kt b/jacodb-ets/src/test/kotlin/org/jacodb/ets/test/utils/Entrypoints.kt index 21da10fd0..f648b1eeb 100644 --- a/jacodb-ets/src/test/kotlin/org/jacodb/ets/test/utils/Entrypoints.kt +++ b/jacodb-ets/src/test/kotlin/org/jacodb/ets/test/utils/Entrypoints.kt @@ -21,14 +21,12 @@ import org.jacodb.ets.dto.convertToEtsFile import org.jacodb.ets.model.EtsFile import org.jacodb.ets.utils.dumpDot import org.jacodb.ets.utils.render -import org.jacodb.ets.utils.resolveSibling import org.jacodb.ets.utils.toText import kotlin.io.path.Path import kotlin.io.path.div import kotlin.io.path.name import kotlin.io.path.nameWithoutExtension import kotlin.io.path.relativeTo -import kotlin.io.path.toPath import kotlin.io.path.walk private val logger = mu.KotlinLogging.logger {} @@ -90,9 +88,7 @@ object DumpEtsFilesToDot { @JvmStatic fun main(args: Array) { - val res = ETSIR - val etsirDir = object {}::class.java.getResource(res)?.toURI()?.toPath() - ?: error("Resource not found: '$res'") + val etsirDir = getResourcePath(ETSIR) logger.info { "etsirDir = $etsirDir" } etsirDir.walk() diff --git a/jacodb-ets/src/test/kotlin/org/jacodb/ets/test/utils/LoadEts.kt b/jacodb-ets/src/test/kotlin/org/jacodb/ets/test/utils/LoadEts.kt deleted file mode 100644 index 05bbabf83..000000000 --- a/jacodb-ets/src/test/kotlin/org/jacodb/ets/test/utils/LoadEts.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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.test.utils - -import mu.KotlinLogging -import org.jacodb.ets.dto.EtsFileDto -import org.jacodb.ets.dto.convertToEtsFile -import org.jacodb.ets.model.EtsFile - -private val logger = KotlinLogging.logger {} - -fun loadEtsFileDtoFromResource(jsonPath: String): EtsFileDto { - require(jsonPath.startsWith("/")) { "Resource path must start with '/': $jsonPath" } - logger.debug { "Loading EtsIR from resource: '$jsonPath'" } - val stream = object {}::class.java.getResourceAsStream(jsonPath) - ?: error("Resource not found: $jsonPath") - return EtsFileDto.loadFromJson(stream) -} - -fun loadEtsFileFromResource(jsonPath: String): EtsFile { - val etsFileDto = loadEtsFileDtoFromResource(jsonPath) - return convertToEtsFile(etsFileDto) -} diff --git a/jacodb-ets/src/test/resources/.gitignore b/jacodb-ets/src/test/resources/.gitignore index 730a420e7..1fe1a8b3b 100644 --- a/jacodb-ets/src/test/resources/.gitignore +++ b/jacodb-ets/src/test/resources/.gitignore @@ -1,3 +1,4 @@ -/samples/etsir/ -/samples/abc/ +/samples +!/samples/source /projects +/repos diff --git a/jacodb-ets/src/test/resources/prepare_projects.sh b/jacodb-ets/src/test/resources/prepare_projects.sh new file mode 100644 index 000000000..57c52ef82 --- /dev/null +++ b/jacodb-ets/src/test/resources/prepare_projects.sh @@ -0,0 +1,374 @@ +#/bin/bash +set -euo pipefail + +if [[ -z "${ARKANALYZER_DIR}" ]]; then + echo "ARKANALYZER_DIR is undefined" + exit 1 +fi + +echo "ARKANALYZER_DIR=${ARKANALYZER_DIR}" +SCRIPT_TS=$ARKANALYZER_DIR/src/save/serializeArkIR.ts +SCRIPT_JS=$ARKANALYZER_DIR/out/src/save/serializeArkIR.js + +if [[ ! -f $SCRIPT_JS ]]; then + echo "Script not found: $SCRIPT_JS" + echo "Did you forget to build the ArkAnalyzer project?" + echo "Run 'npm run build' in the ArkAnalyzer project directory" + exit 1 +fi + +#if [[ $SCRIPT_JS -ot $SCRIPT_TS ]]; then +# echo "Script is outdated: $SCRIPT_JS" +# echo "Did you forget to re-build the ArkAnalyzer project?" +# echo "Run 'npm run build' in the ArkAnalyzer project directory" +# exit 1 +#fi + +do_force=0 + +while getopts ":f" opt; do + case $opt in + f) do_force=1 + echo "Force mode enabled" + ;; + *) printf "Illegal option '-%s'\n" "$opt" && exit 1 + ;; + esac +done + +cd "$(dirname $0)" +mkdir -p projects +cd projects + +function check_repo() { + if [[ $# -ne 1 ]]; then + echo "Usage: check_repo " + exit 1 + fi + if [[ ! -d $1 ]]; then + echo "Repository not found: $1" + exit 1 + fi +} + +function prepare_project_dir() { + if [[ $# -ne 1 ]]; then + echo "Usage: prepare_project " + exit 1 + fi + NAME=$1 + echo + echo "=== Preparing project: $NAME" + echo + if [[ -d $NAME ]]; then + echo "Directory already exists: $NAME" + # If `-f` (force mode) is not provided, exit the preparation for this project: + if [[ $do_force -eq 0 ]]; then + exit + fi + fi + mkdir -p $NAME + cd $NAME +} + +function prepare_module() { + if [[ $# -ne 2 ]]; then + echo "Usage: prepare_module " + exit 1 + fi + local MODULE=$1 + local ROOT=$2 + echo "= Preparing module: $MODULE" + local SRC="source/$MODULE" + local ETSIR="etsir/$MODULE" + mkdir -p $(dirname $SRC) + echo "Linking sources..." + echo "pwd = $(pwd)" + ln -srfT "$ROOT/src/main/ets" $SRC + echo "Serializing..." + # TODO: add switch for using npx/node + # npx ts-node --files --transpileOnly $SCRIPT_TS -p $SRC $ETSIR -v -t 1 + node $SCRIPT_JS -p $SRC $ETSIR -v -t 1 +} + +( + prepare_project_dir "Demo_Calc" + + REPO="../../repos/applications_app_samples" + check_repo $REPO + + BASE="$REPO/code/SuperFeature/DistributedAppDev/ArkTSDistributedCalc" + prepare_module "entry" "$BASE/entry" +) +( + prepare_project_dir "Demo_Camera" + + REPO="../../repos/applications_app_samples" + check_repo $REPO + + BASE="$REPO/code/BasicFeature/Media/Camera" + prepare_module "entry" "$BASE/entry" +) +( + prepare_project_dir "Demo_CertificateManager" + + REPO="../../repos/applications_app_samples" + check_repo $REPO + + BASE="$REPO/code/BasicFeature/Security/CertManager" + prepare_module "entry" "$BASE/entry" +) +( + prepare_project_dir "Demo_Clock" + + REPO="../../repos/applications_app_samples" + check_repo $REPO + + BASE="$REPO/code/Solutions/Tools/ArkTSClock" + prepare_module "entry" "$BASE/entry" +) +( + prepare_project_dir "Demo_KikaInput" + + REPO="../../repos/applications_app_samples" + check_repo $REPO + + BASE="$REPO/code/Solutions/InputMethod/KikaInput" + prepare_module "entry" "$BASE/entry" +) +( + prepare_project_dir "Demo_Launcher" + + REPO="../../repos/applications_app_samples" + check_repo $REPO + + BASE="$REPO/code/SystemFeature/ApplicationModels/Launcher" + prepare_module "entry" "$BASE/entry" + prepare_module "desktop" "$BASE/desktop" + prepare_module "base" "$BASE/base" + prepare_module "recents" "$BASE/recents" +) +( + prepare_project_dir "Demo_Music" + + REPO="../../repos/applications_app_samples" + check_repo $REPO + + BASE="$REPO/code/SuperFeature/DistributedAppDev/ArkTSDistributedMusicPlayer" + prepare_module "entry" "$BASE/entry" +) +( + prepare_project_dir "Demo_Photos" + + REPO="../../repos/applications_app_samples" + check_repo $REPO + + BASE="$REPO/code/SystemFeature/FileManagement/Photos" + prepare_module "entry" "$BASE/entry" +) +( + prepare_project_dir "Demo_ScreenShot" + + REPO="../../repos/applications_app_samples" + check_repo $REPO + + BASE="$REPO/code/SystemFeature/Media/Screenshot" + prepare_module "entry" "$BASE/entry" + prepare_module "Feature" "$BASE/Feature" +) +( + prepare_project_dir "Demo_Settings" + + REPO="../../repos/applications_app_samples" + check_repo $REPO + + BASE="$REPO/code/SuperFeature/MultiDeviceAppDev/Settings" + prepare_module "default" "$BASE/products/default" + prepare_module "common" "$BASE/common" + prepare_module "settingItems" "$BASE/features/settingitems" +) + +( + prepare_project_dir "CalendarData" + + REPO="../../repos/applications_calendar_data" + check_repo $REPO + + prepare_module "entry" "$REPO/entry" + prepare_module "common" "$REPO/common" + prepare_module "datastructure" "$REPO/datastructure" + prepare_module "datamanager" "$REPO/datamanager" + prepare_module "rrule" "$REPO/rrule" + prepare_module "dataprovider" "$REPO/dataprovider" +) + +( + prepare_project_dir "CallUI" + + REPO="../../repos/applications_call" + check_repo $REPO + + prepare_module "callui" "$REPO/entry" + prepare_module "common" "$REPO/common" + prepare_module "mobiledatasettings" "$REPO/mobiledatasettings" +) + +( + prepare_project_dir "Contacts" + + REPO="../../repos/applications_contacts" + check_repo $REPO + + prepare_module "entry" "$REPO/entry" + prepare_module "common" "$REPO/common" + prepare_module "phonenumber" "$REPO/feature/phonenumber" + prepare_module "contact" "$REPO/feature/contact" + prepare_module "account" "$REPO/feature/account" + prepare_module "call" "$REPO/feature/call" + prepare_module "dialpad" "$REPO/feature/dialpad" +) + +( + prepare_project_dir "FilePicker" + + REPO="../../repos/applications_filepicker" + check_repo $REPO + + prepare_module "entry" "$REPO/entry" + prepare_module "audiopicker" "$REPO/audiopicker" +) + +( + prepare_project_dir "Launcher" + + REPO="../../repos/applications_launcher" + check_repo $REPO + + prepare_module "launcher_common" "$REPO/common" + prepare_module "launcher_appcenter" "$REPO/feature/appcenter" + prepare_module "launcher_bigfolder" "$REPO/feature/bigfolder" + prepare_module "launcher_form" "$REPO/feature/form" + prepare_module "launcher_gesturenavigation" "$REPO/feature/gesturenavigation" + prepare_module "launcher_numbadge" "$REPO/feature/numbadge" + prepare_module "launcher_pagedesktop" "$REPO/feature/pagedesktop" + prepare_module "launcher_recents" "$REPO/feature/recents" + prepare_module "launcher_smartDock" "$REPO/feature/smartdock" + prepare_module "phone_launcher" "$REPO/product/phone" + prepare_module "pad_launcher" "$REPO/product/pad" + prepare_module "launcher_settings" "$REPO/feature/settings" +) + +( + prepare_project_dir "Mms" + + REPO="../../repos/applications_mms" + check_repo $REPO + + prepare_module "entry" "$REPO/entry" +) + +( + prepare_project_dir "Note" + + REPO="../../repos/applications_notes" + check_repo $REPO + + prepare_module "default" "$REPO/product/default" + prepare_module "utils" "$REPO/common/utils" + # prepare_module "resources" "$REPO/common/resources" + prepare_module "component" "$REPO/features" +) + +( + prepare_project_dir "PrintSpooler" + + REPO="../../repos/applications_print_spooler" + check_repo $REPO + + prepare_module "entry" "$REPO/entry" + prepare_module "common" "$REPO/common" + prepare_module "ippPrint" "$REPO/feature/ippPrint" +) + +( + prepare_project_dir "ScreenLock" + + REPO="../../repos/applications_screenlock" + check_repo $REPO + + prepare_module "entry" "$REPO/entry" + prepare_module "pc" "$REPO/product/pc" + prepare_module "phone" "$REPO/product/phone" + prepare_module "batterycomponent" "$REPO/features/batterycomponent" + prepare_module "clockcomponent" "$REPO/features/clockcomponent" + prepare_module "datetimecomponent" "$REPO/features/datetimecomponent" + prepare_module "noticeitem" "$REPO/features/noticeitem" + prepare_module "screenlock" "$REPO/features/screenlock" + prepare_module "shortcutcomponent" "$REPO/features/shortcutcomponent" + prepare_module "signalcomponent" "$REPO/features/signalcomponent" + prepare_module "wallpapercomponent" "$REPO/features/wallpapercomponent" + prepare_module "wificomponent" "$REPO/features/wificomponent" + prepare_module "common" "$REPO/common" +) + +( + prepare_project_dir "Settings" + + REPO="../../repos/applications_settings" + check_repo $REPO + + prepare_module "phone" "$REPO/product/phone" + # prepare_module "wearable" "$REPO/product/wearable" + prepare_module "component" "$REPO/common/component" + prepare_module "search" "$REPO/common/search" + ### prepare_module "settingsBase" "$REPO/common/settingsBase" + prepare_module "utils" "$REPO/common/utils" +) + +( + prepare_project_dir "SettingsData" + + REPO="../../repos/applications_settings_data" + check_repo $REPO + + prepare_module "entry" "$REPO/entry" +) + +( + prepare_project_dir "SystemUI" + + REPO="../../repos/applications_systemui" + check_repo $REPO + + prepare_module "phone_entry" "$REPO/entry/phone" + prepare_module "pc_entry" "$REPO/entry/pc" + prepare_module "default_navigationBar" "$REPO/product/default/navigationBar" + prepare_module "default_notificationmanagement" "$REPO/product/default/notificationmanagement" + prepare_module "default_volumepanel" "$REPO/product/default/volumepanel" + prepare_module "default_dialog" "$REPO/product/default/dialog" + prepare_module "pc_controlpanel" "$REPO/product/pc/controlpanel" + prepare_module "pc_notificationpanel" "$REPO/product/pc/notificationpanel" + prepare_module "pc_statusbar" "$REPO/product/pc/statusbar" + prepare_module "phone_dropdownpanel" "$REPO/product/phone/dropdownpanel" + prepare_module "phone_statusbar" "$REPO/product/phone/statusbar" + prepare_module "common" "$REPO/common" + prepare_module "airplanecomponent" "$REPO/features/airplanecomponent" + prepare_module "autorotatecomponent" "$REPO/features/autorotatecomponent" + prepare_module "batterycomponent" "$REPO/features/batterycomponent" + prepare_module "bluetoothcomponent" "$REPO/features/bluetoothcomponent" + prepare_module "brightnesscomponent" "$REPO/features/brightnesscomponent" + prepare_module "capsulecomponent" "$REPO/features/capsulecomponent" + prepare_module "clockcomponent" "$REPO/features/clockcomponent" + prepare_module "controlcentercomponent" "$REPO/features/controlcentercomponent" + prepare_module "locationcomponent" "$REPO/features/locationcomponent" + prepare_module "managementcomponent" "$REPO/features/managementcomponent" + prepare_module "navigationservice" "$REPO/features/navigationservice" + prepare_module "nfccomponent" "$REPO/features/nfccomponent" + prepare_module "noticeitem" "$REPO/features/noticeitem" + prepare_module "ringmodecomponent" "$REPO/features/ringmodecomponent" + prepare_module "signalcomponent" "$REPO/features/signalcomponent" + prepare_module "statusbarcomponent" "$REPO/features/statusbarcomponent" + prepare_module "volumecomponent" "$REPO/features/volumecomponent" + prepare_module "volumepanelcomponent" "$REPO/features/volumepanelcomponent" + prepare_module "wificomponent" "$REPO/features/wificomponent" +) diff --git a/jacodb-ets/src/test/resources/prepare_repos.sh b/jacodb-ets/src/test/resources/prepare_repos.sh new file mode 100644 index 000000000..5a684c3b3 --- /dev/null +++ b/jacodb-ets/src/test/resources/prepare_repos.sh @@ -0,0 +1,40 @@ +#!/bin/bash +set -euo pipefail + +cd "$(dirname $0)" +mkdir -p repos +cd repos + +function prepare_repo() { + if [[ $# -ne 1 ]]; then + echo "Usage: prepare_repo " + exit 1 + fi + REPO=$1 + DIR=$(basename -s .git $(git ls-remote --get-url $REPO)) + echo "Preparing repository: $REPO" + if [[ ! -d $DIR ]]; then + echo "Cloning..." + git clone $REPO + else + echo "Directory '$DIR' already exists. Pulling latest changes..." + git -C $DIR pull & + fi +} + +prepare_repo https://gitee.com/openharmony/applications_app_samples +prepare_repo https://gitee.com/openharmony/applications_calendar_data +prepare_repo https://gitee.com/openharmony/applications_call +prepare_repo https://gitee.com/openharmony/applications_contacts +prepare_repo https://gitee.com/openharmony/applications_filepicker +prepare_repo https://gitee.com/openharmony/applications_hap +prepare_repo https://gitee.com/openharmony/applications_launcher +prepare_repo https://gitee.com/openharmony/applications_mms +prepare_repo https://gitee.com/openharmony/applications_notes +prepare_repo https://gitee.com/openharmony/applications_print_spooler +prepare_repo https://gitee.com/openharmony/applications_screenlock +prepare_repo https://gitee.com/openharmony/applications_settings +prepare_repo https://gitee.com/openharmony/applications_settings_data +prepare_repo https://gitee.com/openharmony/applications_systemui + +wait diff --git a/jacodb-ets/src/test/resources/samples/source/lang/scoped.ts b/jacodb-ets/src/test/resources/samples/source/lang/scoped.ts new file mode 100644 index 000000000..4f5eaa0e8 --- /dev/null +++ b/jacodb-ets/src/test/resources/samples/source/lang/scoped.ts @@ -0,0 +1,8 @@ +function main() { + let x = 42; + if (true) { + let x = "kek"; + console.log(x); + } + console.log(x); +} diff --git a/jacodb-ets/src/test/resources/samples/source/typeinfer/cast.ts b/jacodb-ets/src/test/resources/samples/source/typeinfer/cast.ts new file mode 100644 index 000000000..d43f69569 --- /dev/null +++ b/jacodb-ets/src/test/resources/samples/source/typeinfer/cast.ts @@ -0,0 +1,12 @@ +declare function getData(): any; + +type Data = {} + +function entrypoint() { + let x = getData() as Data; + infer(x); +} + +function infer(arg: any) { + console.log(arg); +} diff --git a/jacodb-ets/src/test/resources/samples/source/typeinfer/microphone.ts b/jacodb-ets/src/test/resources/samples/source/typeinfer/microphone.ts new file mode 100644 index 000000000..e436b5f49 --- /dev/null +++ b/jacodb-ets/src/test/resources/samples/source/typeinfer/microphone.ts @@ -0,0 +1,25 @@ +interface Microphone { + uuid: string +} + +class VirtualMicro implements Microphone { + uuid: string = "virtual_micro_v3" +} + +interface Devices { + microphone: Microphone +} + +class VirtualDevices implements Devices { + microphone: Microphone = new VirtualMicro() +} + +function getMicrophoneUuid(device: Devices): string { + return device.microphone.uuid +} + +function entrypoint() { + let devices = new VirtualDevices() + let uuid = getMicrophoneUuid(devices) + console.log(uuid) +} diff --git a/jacodb-ets/src/testFixtures/kotlin/org/jacodb/ets/test/utils/LoadEts.kt b/jacodb-ets/src/testFixtures/kotlin/org/jacodb/ets/test/utils/LoadEts.kt new file mode 100644 index 000000000..2249870cb --- /dev/null +++ b/jacodb-ets/src/testFixtures/kotlin/org/jacodb/ets/test/utils/LoadEts.kt @@ -0,0 +1,136 @@ +/* + * 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.test.utils + +import mu.KotlinLogging +import org.jacodb.ets.dto.EtsFileDto +import org.jacodb.ets.dto.convertToEtsFile +import org.jacodb.ets.model.EtsFile +import org.jacodb.ets.model.EtsScene +import java.nio.file.Path +import kotlin.io.path.ExperimentalPathApi +import kotlin.io.path.extension +import kotlin.io.path.inputStream +import kotlin.io.path.relativeTo +import kotlin.io.path.walk + +private val logger = KotlinLogging.logger {} + +/** + * Load an [EtsFileDto] from a resource file. + * + * For example, `resources/ets/sample.json` can be loaded with: + * ``` + * val dto: EtsFileDto = loadEtsFileDtoFromResource("/ets/sample.json") + * ``` + */ +fun loadEtsFileDtoFromResource(jsonPath: String): EtsFileDto { + logger.debug { "Loading EtsIR from resource: '$jsonPath'" } + require(jsonPath.endsWith(".json")) { "File must have a '.json' extension: '$jsonPath'" } + getResourceStream(jsonPath).use { stream -> + return EtsFileDto.loadFromJson(stream) + } +} + +/** + * Load an [EtsFile] from a resource file. + * + * For example, `resources/ets/sample.json` can be loaded with: + * ``` + * val file: EtsFile = loadEtsFileFromResource("/ets/sample.json") + * ``` + */ +fun loadEtsFileFromResource(jsonPath: String): EtsFile { + val etsFileDto = loadEtsFileDtoFromResource(jsonPath) + return convertToEtsFile(etsFileDto) +} + +/** + * Load multiple [EtsFile]s from a resource directory. + * + * For example, all files in `resources/project/` can be loaded with: + * ``` + * val files: Sequence = loadMultipleEtsFilesFromResourceDirectory("/project") + * ``` + */ +@OptIn(ExperimentalPathApi::class) +fun loadMultipleEtsFilesFromResourceDirectory(dirPath: String): Sequence { + val rootPath = getResourcePath(dirPath) + return rootPath.walk().filter { it.extension == "json" }.map { path -> + loadEtsFileFromResource("$dirPath/${path.relativeTo(rootPath)}") + } +} + +fun loadMultipleEtsFilesFromMultipleResourceDirectories( + dirPaths: List, +): Sequence { + return dirPaths.asSequence().flatMap { loadMultipleEtsFilesFromResourceDirectory(it) } +} + +fun loadEtsProjectFromResources( + modules: List, + prefix: String, +): EtsScene { + logger.info { "Loading Ets project with modules $modules from '$prefix/'" } + val dirPaths = modules.map { "$prefix/$it" } + val files = loadMultipleEtsFilesFromMultipleResourceDirectories(dirPaths).toList() + logger.info { "Loaded ${files.size} files" } + return EtsScene(files, sdkFiles = emptyList()) +} + +//----------------------------------------------------------------------------- + +/** + * Load an [EtsFileDto] from a file. + * + * For example, `data/sample.json` can be loaded with: + * ``` + * val dto: EtsFileDto = loadEtsFileDto(Path("data/sample.json")) + * ``` + */ +fun loadEtsFileDto(path: Path): EtsFileDto { + require(path.extension == "json") { "File must have a '.json' extension: $path" } + path.inputStream().use { stream -> + return EtsFileDto.loadFromJson(stream) + } +} + +/** + * Load an [EtsFile] from a file. + * + * For example, `data/sample.json` can be loaded with: + * ``` + * val file: EtsFile = loadEtsFile(Path("data/sample.json")) + * ``` + */ +fun loadEtsFile(path: Path): EtsFile { + val etsFileDto = loadEtsFileDto(path) + return convertToEtsFile(etsFileDto) +} + +/** + * Load multiple [EtsFile]s from a directory. + * + * For example, all files in `data` can be loaded with: + * ``` + * val files: Sequence = loadMultipleEtsFilesFromDirectory(Path("data")) + * ``` + */ +@OptIn(ExperimentalPathApi::class) +fun loadMultipleEtsFilesFromDirectory(dirPath: Path): Sequence { + return dirPath.walk().filter { it.extension == "json" }.map { loadEtsFile(it) } +} diff --git a/jacodb-ets/src/testFixtures/kotlin/org/jacodb/ets/test/utils/Resources.kt b/jacodb-ets/src/testFixtures/kotlin/org/jacodb/ets/test/utils/Resources.kt new file mode 100644 index 000000000..f70b994e3 --- /dev/null +++ b/jacodb-ets/src/testFixtures/kotlin/org/jacodb/ets/test/utils/Resources.kt @@ -0,0 +1,36 @@ +/* + * 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.test.utils + +import java.io.InputStream +import java.nio.file.Path +import kotlin.io.path.toPath + +fun getResourcePathOrNull(res: String): Path? { + require(res.startsWith("/")) { "Resource path must start with '/': '$res'" } + return object {}::class.java.getResource(res)?.toURI()?.toPath() +} + +fun getResourcePath(res: String): Path { + return getResourcePathOrNull(res) ?: error("Resource not found: '$res'") +} + +fun getResourceStream(res: String): InputStream { + require(res.startsWith("/")) { "Resource path must start with '/': '$res'" } + return object {}::class.java.getResourceAsStream(res) + ?: error("Resource not found: '$res'") +} diff --git a/jacodb-ets/src/testFixtures/kotlin/org/jacodb/ets/test/utils/TestFactoryDsl.kt b/jacodb-ets/src/testFixtures/kotlin/org/jacodb/ets/test/utils/TestFactoryDsl.kt new file mode 100644 index 000000000..7c5250a73 --- /dev/null +++ b/jacodb-ets/src/testFixtures/kotlin/org/jacodb/ets/test/utils/TestFactoryDsl.kt @@ -0,0 +1,68 @@ +/* + * 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.test.utils + +import org.junit.jupiter.api.DynamicContainer +import org.junit.jupiter.api.DynamicNode +import org.junit.jupiter.api.DynamicTest +import org.junit.jupiter.api.function.Executable +import java.util.stream.Stream + +private interface TestProvider { + fun test(name: String, test: () -> Unit) +} + +private interface ContainerProvider { + fun container(name: String, init: TestContainerBuilder.() -> Unit) +} + +class TestContainerBuilder(var name: String) : TestProvider, ContainerProvider { + private val nodes: MutableList = mutableListOf() + + override fun test(name: String, test: () -> Unit) { + nodes += dynamicTest(name, test) + } + + override fun container(name: String, init: TestContainerBuilder.() -> Unit) { + nodes += containerBuilder(name, init) + } + + fun build(): DynamicContainer = DynamicContainer.dynamicContainer(name, nodes) +} + +private fun containerBuilder(name: String, init: TestContainerBuilder.() -> Unit): DynamicContainer = + TestContainerBuilder(name).apply(init).build() + +class TestFactoryBuilder : TestProvider, ContainerProvider { + private val nodes: MutableList = mutableListOf() + + override fun test(name: String, test: () -> Unit) { + nodes += dynamicTest(name, test) + } + + override fun container(name: String, init: TestContainerBuilder.() -> Unit) { + nodes += containerBuilder(name, init) + } + + fun build(): Stream = nodes.stream() +} + +fun testFactory(init: TestFactoryBuilder.() -> Unit): Stream = + TestFactoryBuilder().apply(init).build() + +private fun dynamicTest(name: String, test: () -> Unit): DynamicTest = + DynamicTest.dynamicTest(name, Executable(test)) diff --git a/settings.gradle.kts b/settings.gradle.kts index ab63a105f..f2d99b2ca 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,14 +1,18 @@ rootProject.name = "jacodb" plugins { - `gradle-enterprise` + id("com.gradle.develocity") version("3.18.2") id("org.danilopianini.gradle-pre-commit-git-hooks") version "1.1.11" } -gradleEnterprise { +develocity { buildScan { - termsOfServiceUrl = "https://gradle.com/terms-of-service" - termsOfServiceAgree = "yes" + // Accept the term of use for the build scan plugin: + termsOfUseUrl.set("https://gradle.com/help/legal-terms-of-use") + termsOfUseAgree.set("yes") + + // Publish build scans on-demand, when `--scan` option is provided: + publishing.onlyIf { false } } }