From d1251b24e13984220f430e18705c11b696459316 Mon Sep 17 00:00:00 2001 From: Konstantin Chukharev Date: Fri, 20 Jun 2025 16:03:13 +0300 Subject: [PATCH] USVM Type Inference as a Service --- .github/workflows/ci.yml | 46 +- build.gradle.kts | 1 + buildSrc/build.gradle.kts | 2 + buildSrc/src/main/kotlin/Dependencies.kt | 76 ++- buildSrc/src/main/kotlin/ProcessUtil.kt | 77 +++ settings.gradle.kts | 66 +-- usvm-ts-dataflow/build.gradle.kts | 77 ++- .../dataflow/ts/infer/TypeInferenceManager.kt | 4 +- .../ts/infer/annotation/EtsSceneAnnotator.kt | 4 +- .../dataflow/ts/infer/dto/ConvertToDto.kt | 55 +-- .../dataflow/ts/infer/dto/EtsTypeToDto.kt | 195 -------- .../dataflow/ts/infer/dto/EtsValueToDto.kt | 115 ----- .../org/usvm/dataflow/ts/util/LoadEts.kt | 49 -- .../src/main/resources/logback.xml | 2 +- .../org/usvm/dataflow/ts/test/EtsIfdsTest.kt | 2 +- .../ts/test/EtsProjectAnalysisTest.kt | 6 +- .../dataflow/ts/test/EtsTaintAnalysisTest.kt | 2 +- .../dataflow/ts/test/EtsTypeInferenceTest.kt | 15 +- .../ts/test/EtsTypeResolverWithAstTest.kt | 2 +- .../dataflow/ts/test/utils/TaintConfig.kt | 2 +- .../kotlin/org/usvm/dataflow/ts/LoadEts.kt | 120 ----- .../kotlin/org/usvm/dataflow/ts/Resources.kt | 20 - usvm-ts-service/build.gradle.kts | 44 ++ .../src/main/extraproto/greeter.proto | 20 + .../src/main/extraproto/manager.proto | 15 + .../src/main/extraproto/model.proto | 461 ++++++++++++++++++ .../org/usvm/ts/service/ConvertToProto.kt | 98 ++++ .../kotlin/org/usvm/ts/service/UsvmServer.kt | 143 ++++++ usvm-ts-service/src/main/proto/usvm.proto | 48 ++ usvm-ts/build.gradle.kts | 104 +--- .../org/usvm/machine/expr/TsExprResolver.kt | 78 +-- .../usvm/machine/interpreter/TsInterpreter.kt | 19 +- .../main/kotlin/org/usvm/util/EtsHierarchy.kt | 43 +- .../src/test/kotlin/org/usvm/util/LoadEts.kt | 24 +- .../test/kotlin/org/usvm/util/SetupServer.kt | 30 ++ .../org/usvm/util/TsMethodTestRunner.kt | 52 +- .../kotlin/org/usvm/util/TsTestResolver.kt | 2 +- 37 files changed, 1295 insertions(+), 824 deletions(-) create mode 100644 buildSrc/src/main/kotlin/ProcessUtil.kt delete mode 100644 usvm-ts-dataflow/src/main/kotlin/org/usvm/dataflow/ts/infer/dto/EtsTypeToDto.kt delete mode 100644 usvm-ts-dataflow/src/main/kotlin/org/usvm/dataflow/ts/infer/dto/EtsValueToDto.kt delete mode 100644 usvm-ts-dataflow/src/main/kotlin/org/usvm/dataflow/ts/util/LoadEts.kt delete mode 100644 usvm-ts-dataflow/src/testFixtures/kotlin/org/usvm/dataflow/ts/LoadEts.kt delete mode 100644 usvm-ts-dataflow/src/testFixtures/kotlin/org/usvm/dataflow/ts/Resources.kt create mode 100644 usvm-ts-service/build.gradle.kts create mode 100644 usvm-ts-service/src/main/extraproto/greeter.proto create mode 100644 usvm-ts-service/src/main/extraproto/manager.proto create mode 100644 usvm-ts-service/src/main/extraproto/model.proto create mode 100644 usvm-ts-service/src/main/kotlin/org/usvm/ts/service/ConvertToProto.kt create mode 100644 usvm-ts-service/src/main/kotlin/org/usvm/ts/service/UsvmServer.kt create mode 100644 usvm-ts-service/src/main/proto/usvm.proto create mode 100644 usvm-ts/src/test/kotlin/org/usvm/util/SetupServer.kt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c9d3960d6..4b0161a583 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,13 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Checkout local jacodb + uses: actions/checkout@v4 + with: + repository: UnitTestBot/jacodb + ref: lipen/grpc + path: jacodb + - name: Setup Java JDK uses: actions/setup-java@v4 with: @@ -45,6 +52,13 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Checkout local jacodb + uses: actions/checkout@v4 + with: + repository: UnitTestBot/jacodb + ref: lipen/grpc + path: jacodb + - name: Setup Java JDK uses: actions/setup-java@v4 with: @@ -74,6 +88,13 @@ jobs: # 'usvm-python/cpythonadapter/cpython' is a submodule submodules: true + - name: Checkout local jacodb + uses: actions/checkout@v4 + with: + repository: UnitTestBot/jacodb + ref: lipen/grpc + path: jacodb + - name: Setup Java JDK uses: actions/setup-java@v4 with: @@ -107,6 +128,16 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Checkout local jacodb + uses: actions/checkout@v4 + with: + repository: UnitTestBot/jacodb + ref: lipen/grpc + path: jacodb + + - name: Enable ETS modules in jacodb + run: echo "enableEts=true" >> jacodb/local.properties + - name: Setup Java JDK uses: actions/setup-java@v4 with: @@ -149,7 +180,7 @@ jobs: npm run build - name: Run TS tests - run: ./gradlew :usvm-ts:check :usvm-ts-dataflow:check + run: ./gradlew :usvm-ts:check :usvm-ts-dataflow:check :usvm-ts-service:check - name: Upload Gradle reports if: (!cancelled()) @@ -165,6 +196,16 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Checkout local jacodb + uses: actions/checkout@v4 + with: + repository: UnitTestBot/jacodb + ref: lipen/grpc + path: jacodb + + - name: Enable ETS modules in jacodb + run: echo "enableEts=true" >> jacodb/local.properties + - name: Setup Java JDK uses: actions/setup-java@v4 with: @@ -174,6 +215,9 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 + - name: Show project list + run: ./gradlew projects + - name: Validate Project List run: ./gradlew validateProjectList diff --git a/build.gradle.kts b/build.gradle.kts index 8223024eeb..ca482a53fe 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,6 +22,7 @@ tasks.register("validateProjectList") { project(":usvm-python"), project(":usvm-ts"), project(":usvm-ts-dataflow"), + project(":usvm-ts-service"), ) // Gather the actual subprojects from the current root project. diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 5075d846cd..b46a5d26df 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -3,6 +3,7 @@ plugins { } val kotlinVersion = "2.1.0" +val coroutinesVersion = "1.10.2" val detektVersion = "1.23.5" val gjavahVersion = "0.3.1" @@ -14,6 +15,7 @@ repositories { dependencies { implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") implementation("io.gitlab.arturbosch.detekt:detekt-gradle-plugin:$detektVersion") implementation("org.glavo:gjavah:$gjavahVersion") } diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index 17f3f22abc..53afca900f 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -5,6 +5,9 @@ import org.gradle.plugin.use.PluginDependenciesSpec object Versions { const val clikt = "5.0.0" const val detekt = "1.23.7" + const val gradle_protobuf = "0.9.5" + const val grpc = "1.72.0" + const val grpc_kotlin = "1.4.3" const val ini4j = "0.5.4" const val jacodb = "5889d3c784" const val juliet = "1.3.2" @@ -15,12 +18,16 @@ object Versions { const val kotlinx_coroutines = "1.10.0" const val kotlinx_serialization = "1.7.3" const val ksmt = "0.5.26" + const val ktor = "3.1.3" const val logback = "1.4.8" const val mockk = "1.13.4" + const val protobuf = "4.30.2" const val rd = "2023.2.0" const val sarif4k = "0.5.0" const val shadow = "8.3.3" const val slf4j = "1.6.1" + const val wire = "5.3.1" + const val wire_grpc_server = "1.0.0-alpha04" // versions for jvm samples object Samples { @@ -116,7 +123,8 @@ object Libs { ) // https://github.com/UnitTestBot/jacodb - private const val jacodbPackage = "com.github.UnitTestBot.jacodb" // use "org.jacodb" with includeBuild + // private const val jacodbPackage = "com.github.UnitTestBot.jacodb" // use "org.jacodb" with includeBuild + private const val jacodbPackage = "org.jacodb" val jacodb_core = dep( group = jacodbPackage, name = "jacodb-core", @@ -173,14 +181,14 @@ object Libs { ) // https://github.com/Kotlin/kotlinx.serialization - val kotlinx_serialization_core = dep( + val kotlinx_serialization_json = dep( group = "org.jetbrains.kotlinx", - name = "kotlinx-serialization-core", + name = "kotlinx-serialization-json", version = Versions.kotlinx_serialization ) - val kotlinx_serialization_json = dep( + val kotlinx_serialization_protobuf = dep( group = "org.jetbrains.kotlinx", - name = "kotlinx-serialization-json", + name = "kotlinx-serialization-protobuf", version = Versions.kotlinx_serialization ) @@ -248,6 +256,52 @@ object Libs { name = "clikt", version = Versions.clikt ) + + // https://github.com/grpc/grpc-java + val grpc_api = dep( + group = "io.grpc", + name = "grpc-api", + version = Versions.grpc + ) + val grpc_protobuf = dep( + group = "io.grpc", + name = "grpc-protobuf", + version = Versions.grpc + ) + val grpc_stub = dep( + group = "io.grpc", + name = "grpc-stub", + version = Versions.grpc + ) + val grpc_netty_shaded = dep( + group = "io.grpc", + name = "grpc-netty-shaded", + version = Versions.grpc + ) + + // https://github.com/square/wire + val wire_runtime = dep( + group = "com.squareup.wire", + name = "wire-runtime", + version = Versions.wire + ) + val wire_grpc_client = dep( + group = "com.squareup.wire", + name = "wire-grpc-client", + version = Versions.wire + ) + + // https://github.com/square/wire-grpc-server + val wire_grpc_server = dep( + group = "com.squareup.wiregrpcserver", + name = "server", + version = Versions.wire_grpc_server + ) + val wire_grpc_server_generator = dep( + group = "com.squareup.wiregrpcserver", + name = "server-generator", + version = Versions.wire_grpc_server + ) } object Plugins { @@ -271,6 +325,18 @@ object Plugins { id = "com.gradleup.shadow", version = Versions.shadow ) + + // https://github.com/google/protobuf-gradle-plugin + object GradleProtobuf : ProjectPlugin( + id = "com.google.protobuf", + version = Versions.gradle_protobuf + ) + + // https://github.com/square/wire + object Wire : ProjectPlugin( + version = Versions.wire, + id = "com.squareup.wire" + ) } fun PluginDependenciesSpec.id(plugin: Plugins.ProjectPlugin) { diff --git a/buildSrc/src/main/kotlin/ProcessUtil.kt b/buildSrc/src/main/kotlin/ProcessUtil.kt new file mode 100644 index 0000000000..4d06edfc93 --- /dev/null +++ b/buildSrc/src/main/kotlin/ProcessUtil.kt @@ -0,0 +1,77 @@ +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import java.io.Reader +import kotlin.time.Duration +import java.util.concurrent.TimeUnit + +object ProcessUtil { + data class Result( + val exitCode: Int, + val stdout: String, + val stderr: String, + val isTimeout: Boolean, // true if the process was terminated due to timeout + ) + + fun run( + command: List, + input: Reader = "".reader(), + timeout: Duration? = null, + builder: ProcessBuilder.() -> Unit = {}, + ): Result { + val process = ProcessBuilder(command).apply(builder).start() + return communicate(process, input, timeout) + } + + private fun communicate( + process: Process, + input: Reader, + timeout: Duration? = null, + ): Result { + val stdout = StringBuilder() + val stderr = StringBuilder() + + val scope = CoroutineScope(Dispatchers.IO) + + // Handle process input + val stdinJob = scope.launch { + process.outputStream.bufferedWriter().use { writer -> + input.copyTo(writer) + } + } + + // Launch output capture coroutines + val stdoutJob = scope.launch { + process.inputStream.bufferedReader().useLines { lines -> + lines.forEach { stdout.appendLine(it) } + } + } + val stderrJob = scope.launch { + process.errorStream.bufferedReader().useLines { lines -> + lines.forEach { stderr.appendLine(it) } + } + } + + // Wait for completion + val isTimeout = if (timeout != null) { + !process.waitFor(timeout.inWholeNanoseconds, TimeUnit.NANOSECONDS) + } else { + process.waitFor() + false + } + + // Wait for all coroutines to finish + runBlocking { + joinAll(stdinJob, stdoutJob, stderrJob) + } + + return Result( + exitCode = process.exitValue(), + stdout = stdout.toString(), + stderr = stderr.toString(), + isTimeout = isTimeout, + ) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index a0f5b624aa..cf06e95efc 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,35 +1,5 @@ rootProject.name = "usvm" -include("usvm-core") -include("usvm-jvm") -include("usvm-jvm:usvm-jvm-api") -include("usvm-jvm:usvm-jvm-test-api") -include("usvm-jvm:usvm-jvm-util") -include("usvm-ts") -include("usvm-util") -include("usvm-jvm-instrumentation") -include("usvm-sample-language") -include("usvm-dataflow") -include("usvm-jvm-dataflow") -include("usvm-ts-dataflow") - -include("usvm-python") -include("usvm-python:cpythonadapter") -findProject(":usvm-python:cpythonadapter")?.name = "cpythonadapter" -include("usvm-python:usvm-python-annotations") -findProject(":usvm-python:usvm-python-annotations")?.name = "usvm-python-annotations" -include("usvm-python:usvm-python-main") -findProject(":usvm-python:usvm-python-main")?.name = "usvm-python-main" -include("usvm-python:usvm-python-runner") -findProject(":usvm-python:usvm-python-runner")?.name = "usvm-python-runner" -include("usvm-python:usvm-python-commons") -findProject(":usvm-python:usvm-python-commons")?.name = "usvm-python-commons" - -// Actually, `includeBuild("../jacodb")` is enough, but there is a bug in IDEA when path is a symlink. -// As a workaround, we convert it to a real absolute path. -// See IDEA bug: https://youtrack.jetbrains.com/issue/IDEA-329756 -// includeBuild(file("../jacodb").toPath().toRealPath().toAbsolutePath()) - pluginManagement { resolutionStrategy { eachPlugin { @@ -39,3 +9,39 @@ pluginManagement { } } } + +// Actually, `includeBuild("../jacodb")` is enough, but there is a bug in IDEA when path is a symlink. +// As a workaround, we convert it to a real absolute path. +// See IDEA bug: https://youtrack.jetbrains.com/issue/IDEA-329756 +val jacodbPath = file("jacodb").takeIf { it.exists() } + ?: file("../jacodb").takeIf { it.exists() } + ?: error("Local JacoDB directory not found") +includeBuild(jacodbPath.toPath().toRealPath().toAbsolutePath()) + +val usvmPython = "usvm-python" +fun includePy(name: String) { + include("$usvmPython:$name") + project(":$usvmPython:$name").name = name +} +include(usvmPython) +includePy("cpythonadapter") +includePy("usvm-python-annotations") +includePy("usvm-python-main") +includePy("usvm-python-runner") +includePy("usvm-python-commons") + +include("usvm-core") +include("usvm-util") +include("usvm-dataflow") +include("usvm-sample-language") + +include("usvm-jvm") +include("usvm-jvm:usvm-jvm-api") +include("usvm-jvm:usvm-jvm-test-api") +include("usvm-jvm:usvm-jvm-util") +include("usvm-jvm-instrumentation") +include("usvm-jvm-dataflow") + +include("usvm-ts") +include("usvm-ts-service") +include("usvm-ts-dataflow") diff --git a/usvm-ts-dataflow/build.gradle.kts b/usvm-ts-dataflow/build.gradle.kts index af70818e26..c4bc0149d7 100644 --- a/usvm-ts-dataflow/build.gradle.kts +++ b/usvm-ts-dataflow/build.gradle.kts @@ -1,6 +1,5 @@ import java.io.FileNotFoundException -import java.nio.file.Files -import java.nio.file.attribute.FileTime +import kotlin.time.Duration.Companion.minutes plugins { id("usvm.kotlin-conventions") @@ -20,13 +19,13 @@ dependencies { implementation(Libs.jacodb_taint_configuration) implementation(Libs.kotlinx_collections) implementation(Libs.kotlinx_serialization_json) + implementation(Libs.kotlinx_serialization_protobuf) implementation(Libs.clikt) runtimeOnly(Libs.logback) testImplementation(Libs.mockk) testImplementation(Libs.junit_jupiter_params) testImplementation(Libs.logback) - testImplementation(Libs.kotlinx_serialization_core) testFixturesImplementation(Libs.kotlin_logging) testFixturesImplementation(Libs.junit_jupiter_api) @@ -38,13 +37,16 @@ tasks.withType { val generateTestResources by tasks.registering { group = "build" - description = "Generates JSON resources from TypeScript files using ArkAnalyzer." + description = "Generates test resources from TypeScript files using ArkAnalyzer." doLast { + logger.lifecycle("Generating test resources using ArkAnalyzer...") + val startTime = System.currentTimeMillis() + val envVarName = "ARKANALYZER_DIR" val defaultArkAnalyzerDir = "../arkanalyzer" val arkAnalyzerDir = rootDir.resolve(System.getenv(envVarName) ?: run { - println("Please, set $envVarName environment variable. Using default value: '$defaultArkAnalyzerDir'") + logger.lifecycle("Please, set $envVarName environment variable. Using default value: '$defaultArkAnalyzerDir'") defaultArkAnalyzerDir }) if (!arkAnalyzerDir.exists()) { @@ -60,50 +62,41 @@ val generateTestResources by tasks.registering { val resources = projectDir.resolve("src/test/resources") val inputDir = resources.resolve("ts") val outputDir = resources.resolve("ir") - println("Generating test resources in '${outputDir.relativeTo(projectDir)}'...") + logger.lifecycle("Generating test resources in '${outputDir.relativeTo(projectDir)}'...") inputDir.walkTopDown().filter { it.isFile }.forEach { input -> val output = outputDir .resolve(input.relativeTo(inputDir)) .resolveSibling(input.name + ".json") - val inputFileTime = Files.getLastModifiedTime(input.toPath()) - val outputFileTime = if (output.exists()) { - Files.getLastModifiedTime(output.toPath()) - } else { - FileTime.fromMillis(0) + logger.lifecycle("Regenerating JSON for '${output.relativeTo(outputDir)}'...") + + val cmd: List = listOf( + "node", + script.absolutePath, + input.relativeTo(resources).path, + output.relativeTo(resources).path, + ) + logger.lifecycle("Running: ${cmd.joinToString(" ")}") + val result = ProcessUtil.run(cmd, timeout = 1.minutes) { + directory(resources) } - - if (!output.exists() || inputFileTime > outputFileTime) { - println("Regenerating JSON for '${output.relativeTo(outputDir)}'...") - - val cmd: List = listOf( - "node", - script.absolutePath, - input.relativeTo(resources).path, - output.relativeTo(resources).path, - ) - println("Running: '${cmd.joinToString(" ")}'") - val process = ProcessBuilder(cmd).directory(resources).start() - val ok = process.waitFor(10, TimeUnit.MINUTES) - - val stdout = process.inputStream.bufferedReader().readText().trim() - if (stdout.isNotBlank()) { - println("[STDOUT]:\n--------\n$stdout\n--------") - } - val stderr = process.errorStream.bufferedReader().readText().trim() - if (stderr.isNotBlank()) { - println("[STDERR]:\n--------\n$stderr\n--------") - } - - if (!ok) { - println("Timeout!") - process.destroy() - } else { - println("Done running!") - } - } else { - println("Skipping '${output.relativeTo(outputDir)}'") + if (result.stdout.isNotBlank()) { + logger.lifecycle("[STDOUT]:\n--------\n${result.stdout}--------") + } + if (result.stderr.isNotBlank()) { + logger.lifecycle("[STDERR]:\n--------\n${result.stderr}--------") } + if (result.isTimeout) { + logger.warn("Timeout!") + } + if (result.exitCode != 0) { + logger.warn("Exit code: ${result.exitCode}") + } + + logger.lifecycle( + "Done generating test resources in %.1fs" + .format((System.currentTimeMillis() - startTime) / 1000.0) + ) } } } diff --git a/usvm-ts-dataflow/src/main/kotlin/org/usvm/dataflow/ts/infer/TypeInferenceManager.kt b/usvm-ts-dataflow/src/main/kotlin/org/usvm/dataflow/ts/infer/TypeInferenceManager.kt index e43f70137d..bab97e9bba 100644 --- a/usvm-ts-dataflow/src/main/kotlin/org/usvm/dataflow/ts/infer/TypeInferenceManager.kt +++ b/usvm-ts-dataflow/src/main/kotlin/org/usvm/dataflow/ts/infer/TypeInferenceManager.kt @@ -112,7 +112,9 @@ class TypeInferenceManager( this@TypeInferenceManager.backwardRunner = backwardRunner val exceptionHandler = CoroutineExceptionHandler { _, exception -> - logger.error { "Got exception from runner, stopping analysis" } + logger.error { + "Got exception from runner, stopping analysis: $exception\n${exception.stackTraceToString()}" + } runnerFinished.completeExceptionally(exception) } diff --git a/usvm-ts-dataflow/src/main/kotlin/org/usvm/dataflow/ts/infer/annotation/EtsSceneAnnotator.kt b/usvm-ts-dataflow/src/main/kotlin/org/usvm/dataflow/ts/infer/annotation/EtsSceneAnnotator.kt index 5252c4c9e8..f88e573996 100644 --- a/usvm-ts-dataflow/src/main/kotlin/org/usvm/dataflow/ts/infer/annotation/EtsSceneAnnotator.kt +++ b/usvm-ts-dataflow/src/main/kotlin/org/usvm/dataflow/ts/infer/annotation/EtsSceneAnnotator.kt @@ -62,7 +62,9 @@ class EtsSceneAnnotator( signature = signature, fields = fields, methods = methods.map { it.annotateWithTypes() }, - superClass = superClass, // TODO: replace with inferred superclass + category = category, + superClassName = superClassName, // TODO: replace with inferred superclass + implementedInterfaceNames = implementedInterfaceNames, typeParameters = typeParameters, modifiers = modifiers, decorators = decorators, diff --git a/usvm-ts-dataflow/src/main/kotlin/org/usvm/dataflow/ts/infer/dto/ConvertToDto.kt b/usvm-ts-dataflow/src/main/kotlin/org/usvm/dataflow/ts/infer/dto/ConvertToDto.kt index 0afe1110e4..5a2626d92e 100644 --- a/usvm-ts-dataflow/src/main/kotlin/org/usvm/dataflow/ts/infer/dto/ConvertToDto.kt +++ b/usvm-ts-dataflow/src/main/kotlin/org/usvm/dataflow/ts/infer/dto/ConvertToDto.kt @@ -16,18 +16,7 @@ package org.usvm.dataflow.ts.infer.dto -import org.jacodb.ets.dto.ClassSignatureDto -import org.jacodb.ets.dto.FieldSignatureDto -import org.jacodb.ets.dto.FileSignatureDto -import org.jacodb.ets.dto.MethodParameterDto -import org.jacodb.ets.dto.MethodSignatureDto -import org.jacodb.ets.dto.NamespaceSignatureDto -import org.jacodb.ets.model.EtsClassSignature -import org.jacodb.ets.model.EtsFieldSignature -import org.jacodb.ets.model.EtsFileSignature -import org.jacodb.ets.model.EtsMethodParameter -import org.jacodb.ets.model.EtsMethodSignature -import org.jacodb.ets.model.EtsNamespaceSignature +import org.jacodb.ets.dto.toDto import org.usvm.dataflow.ts.infer.AccessPathBase import org.usvm.dataflow.ts.infer.EtsTypeFact import org.usvm.dataflow.ts.infer.TypeInferenceResult @@ -80,45 +69,3 @@ fun TypeInferenceResult.toDto(): InferredTypesDto { return InferredTypesDto(classTypeInferenceResult, methodTypeInferenceResult) } - -fun EtsFileSignature.toDto(): FileSignatureDto = - FileSignatureDto( - projectName = this.projectName, - fileName = this.fileName, - ) - -fun EtsNamespaceSignature.toDto(): NamespaceSignatureDto = - NamespaceSignatureDto( - name = this.name, - declaringFile = this.file.toDto(), - declaringNamespace = this.namespace?.toDto(), - ) - -fun EtsClassSignature.toDto(): ClassSignatureDto = - ClassSignatureDto( - name = this.name, - declaringFile = this.file.toDto(), - declaringNamespace = this.namespace?.toDto(), - ) - -fun EtsFieldSignature.toDto(): FieldSignatureDto = - FieldSignatureDto( - declaringClass = this.enclosingClass.toDto(), - name = this.name, - type = this.type.toDto(), - ) - -fun EtsMethodSignature.toDto(): MethodSignatureDto = - MethodSignatureDto( - declaringClass = this.enclosingClass.toDto(), - name = this.name, - parameters = this.parameters.map { it.toDto() }, - returnType = this.returnType.toDto(), - ) - -fun EtsMethodParameter.toDto(): MethodParameterDto = - MethodParameterDto( - name = this.name, - type = this.type.toDto(), - isOptional = this.isOptional, - ) diff --git a/usvm-ts-dataflow/src/main/kotlin/org/usvm/dataflow/ts/infer/dto/EtsTypeToDto.kt b/usvm-ts-dataflow/src/main/kotlin/org/usvm/dataflow/ts/infer/dto/EtsTypeToDto.kt deleted file mode 100644 index c62b006dcb..0000000000 --- a/usvm-ts-dataflow/src/main/kotlin/org/usvm/dataflow/ts/infer/dto/EtsTypeToDto.kt +++ /dev/null @@ -1,195 +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.usvm.dataflow.ts.infer.dto - -import org.jacodb.ets.dto.AliasTypeDto -import org.jacodb.ets.dto.AnyTypeDto -import org.jacodb.ets.dto.ArrayTypeDto -import org.jacodb.ets.dto.BooleanTypeDto -import org.jacodb.ets.dto.ClassTypeDto -import org.jacodb.ets.dto.EnumValueTypeDto -import org.jacodb.ets.dto.FunctionTypeDto -import org.jacodb.ets.dto.GenericTypeDto -import org.jacodb.ets.dto.IntersectionTypeDto -import org.jacodb.ets.dto.LiteralTypeDto -import org.jacodb.ets.dto.LocalSignatureDto -import org.jacodb.ets.dto.NeverTypeDto -import org.jacodb.ets.dto.NullTypeDto -import org.jacodb.ets.dto.NumberTypeDto -import org.jacodb.ets.dto.PrimitiveLiteralDto -import org.jacodb.ets.dto.StringTypeDto -import org.jacodb.ets.dto.TupleTypeDto -import org.jacodb.ets.dto.TypeDto -import org.jacodb.ets.dto.UnclearReferenceTypeDto -import org.jacodb.ets.dto.UndefinedTypeDto -import org.jacodb.ets.dto.UnionTypeDto -import org.jacodb.ets.dto.UnknownTypeDto -import org.jacodb.ets.dto.VoidTypeDto -import org.jacodb.ets.model.EtsAliasType -import org.jacodb.ets.model.EtsAnyType -import org.jacodb.ets.model.EtsArrayType -import org.jacodb.ets.model.EtsBooleanType -import org.jacodb.ets.model.EtsClassType -import org.jacodb.ets.model.EtsEnumValueType -import org.jacodb.ets.model.EtsFunctionType -import org.jacodb.ets.model.EtsGenericType -import org.jacodb.ets.model.EtsIntersectionType -import org.jacodb.ets.model.EtsLiteralType -import org.jacodb.ets.model.EtsNeverType -import org.jacodb.ets.model.EtsNullType -import org.jacodb.ets.model.EtsNumberType -import org.jacodb.ets.model.EtsRawType -import org.jacodb.ets.model.EtsStringType -import org.jacodb.ets.model.EtsTupleType -import org.jacodb.ets.model.EtsType -import org.jacodb.ets.model.EtsUnclearRefType -import org.jacodb.ets.model.EtsUndefinedType -import org.jacodb.ets.model.EtsUnionType -import org.jacodb.ets.model.EtsUnknownType -import org.jacodb.ets.model.EtsVoidType - -fun EtsType.toDto(): TypeDto = accept(EtsTypeToDto) - -private object EtsTypeToDto : EtsType.Visitor { - override fun visit(type: EtsRawType): TypeDto { - return UnknownTypeDto - } - - override fun visit(type: EtsAnyType): TypeDto { - return AnyTypeDto - } - - override fun visit(type: EtsUnknownType): TypeDto { - return UnknownTypeDto - } - - override fun visit(type: EtsUnionType): TypeDto { - return UnionTypeDto(types = type.types.map { it.accept(this) }) - } - - override fun visit(type: EtsIntersectionType): TypeDto { - return IntersectionTypeDto(types = type.types.map { it.accept(this) }) - } - - override fun visit(type: EtsGenericType): TypeDto { - return GenericTypeDto( - name = type.typeName, - defaultType = type.defaultType?.toDto(), - constraint = type.constraint?.toDto(), - ) - } - - override fun visit(type: EtsAliasType): TypeDto { - return AliasTypeDto( - name = type.name, - originalType = type.originalType.toDto(), - signature = LocalSignatureDto( - type.signature.name, - type.signature.method.toDto(), - ), - ) - } - - override fun visit(type: EtsEnumValueType): TypeDto { - return EnumValueTypeDto( - signature = type.signature.toDto(), - constant = type.constant?.toDto(), - ) - } - - override fun visit(type: EtsBooleanType): TypeDto { - return BooleanTypeDto - } - - override fun visit(type: EtsNumberType): TypeDto { - return NumberTypeDto - } - - override fun visit(type: EtsStringType): TypeDto { - return StringTypeDto - } - - override fun visit(type: EtsNullType): TypeDto { - return NullTypeDto - } - - override fun visit(type: EtsUndefinedType): TypeDto { - return UndefinedTypeDto - } - - override fun visit(type: EtsVoidType): TypeDto { - return VoidTypeDto - } - - override fun visit(type: EtsNeverType): TypeDto { - return NeverTypeDto - } - - override fun visit(type: EtsLiteralType): TypeDto { - val literal = when { - type.literalTypeName.equals("true", ignoreCase = true) -> { - PrimitiveLiteralDto.BooleanLiteral(true) - } - - type.literalTypeName.equals("false", ignoreCase = true) -> { - PrimitiveLiteralDto.BooleanLiteral(false) - } - - else -> { - val x = type.literalTypeName.toDoubleOrNull() - if (x != null) { - PrimitiveLiteralDto.NumberLiteral(x) - } else { - PrimitiveLiteralDto.StringLiteral(type.literalTypeName) - } - } - } - return LiteralTypeDto(literal = literal) - } - - override fun visit(type: EtsClassType): TypeDto { - return ClassTypeDto( - signature = type.signature.toDto(), - typeParameters = type.typeParameters.map { it.toDto() }, - ) - } - - override fun visit(type: EtsUnclearRefType): TypeDto { - return UnclearReferenceTypeDto( - name = type.typeName, - typeParameters = type.typeParameters.map { it.toDto() }, - ) - } - - override fun visit(type: EtsArrayType): TypeDto { - return ArrayTypeDto( - elementType = type.elementType.toDto(), - dimensions = type.dimensions, - ) - } - - override fun visit(type: EtsTupleType): TypeDto { - return TupleTypeDto(types = type.types.map { it.accept(this) }) - } - - override fun visit(type: EtsFunctionType): TypeDto { - return FunctionTypeDto( - signature = type.signature.toDto(), - typeParameters = type.typeParameters.map { it.toDto() }, - ) - } -} diff --git a/usvm-ts-dataflow/src/main/kotlin/org/usvm/dataflow/ts/infer/dto/EtsValueToDto.kt b/usvm-ts-dataflow/src/main/kotlin/org/usvm/dataflow/ts/infer/dto/EtsValueToDto.kt deleted file mode 100644 index f52eb1575f..0000000000 --- a/usvm-ts-dataflow/src/main/kotlin/org/usvm/dataflow/ts/infer/dto/EtsValueToDto.kt +++ /dev/null @@ -1,115 +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.usvm.dataflow.ts.infer.dto - -import org.jacodb.ets.dto.ArrayRefDto -import org.jacodb.ets.dto.ConstantDto -import org.jacodb.ets.dto.InstanceFieldRefDto -import org.jacodb.ets.dto.LocalDto -import org.jacodb.ets.dto.ParameterRefDto -import org.jacodb.ets.dto.StaticFieldRefDto -import org.jacodb.ets.dto.ThisRefDto -import org.jacodb.ets.dto.ValueDto -import org.jacodb.ets.model.EtsArrayAccess -import org.jacodb.ets.model.EtsBooleanConstant -import org.jacodb.ets.model.EtsConstant -import org.jacodb.ets.model.EtsInstanceFieldRef -import org.jacodb.ets.model.EtsLocal -import org.jacodb.ets.model.EtsNullConstant -import org.jacodb.ets.model.EtsNumberConstant -import org.jacodb.ets.model.EtsParameterRef -import org.jacodb.ets.model.EtsStaticFieldRef -import org.jacodb.ets.model.EtsStringConstant -import org.jacodb.ets.model.EtsThis -import org.jacodb.ets.model.EtsUndefinedConstant -import org.jacodb.ets.model.EtsValue - -fun EtsValue.toDto(): ValueDto = accept(EtsValueToDto) - -fun EtsLocal.toDto(): LocalDto = LocalDto( - name = name, - type = type.toDto(), -) - -fun EtsConstant.toDto(): ConstantDto = ConstantDto( - value = toString(), - type = type.toDto(), -) - -private object EtsValueToDto : EtsValue.Visitor { - override fun visit(value: EtsLocal): LocalDto { - return value.toDto() - } - - override fun visit(value: EtsConstant): ValueDto { - return value.toDto() - } - - override fun visit(value: EtsStringConstant): ValueDto { - return value.toDto() - } - - override fun visit(value: EtsBooleanConstant): ValueDto { - return value.toDto() - } - - override fun visit(value: EtsNumberConstant): ValueDto { - return value.toDto() - } - - override fun visit(value: EtsNullConstant): ValueDto { - return value.toDto() - } - - override fun visit(value: EtsUndefinedConstant): ValueDto { - return value.toDto() - } - - override fun visit(value: EtsThis): ValueDto { - return ThisRefDto( - type = value.type.toDto(), - ) - } - - override fun visit(value: EtsParameterRef): ValueDto { - return ParameterRefDto( - index = value.index, - type = value.type.toDto(), - ) - } - - override fun visit(value: EtsArrayAccess): ValueDto { - return ArrayRefDto( - array = value.array.toDto(), - index = value.index.toDto(), - type = value.type.toDto(), - ) - } - - override fun visit(value: EtsInstanceFieldRef): ValueDto { - return InstanceFieldRefDto( - instance = value.instance.toDto(), - field = value.field.toDto(), - ) - } - - override fun visit(value: EtsStaticFieldRef): ValueDto { - return StaticFieldRefDto( - field = value.field.toDto(), - ) - } -} diff --git a/usvm-ts-dataflow/src/main/kotlin/org/usvm/dataflow/ts/util/LoadEts.kt b/usvm-ts-dataflow/src/main/kotlin/org/usvm/dataflow/ts/util/LoadEts.kt deleted file mode 100644 index 9ccfd9f1dc..0000000000 --- a/usvm-ts-dataflow/src/main/kotlin/org/usvm/dataflow/ts/util/LoadEts.kt +++ /dev/null @@ -1,49 +0,0 @@ -package org.usvm.dataflow.ts.util - -import org.jacodb.ets.dto.EtsFileDto -import org.jacodb.ets.dto.toEtsFile -import org.jacodb.ets.model.EtsFile -import java.nio.file.Path -import kotlin.io.path.extension -import kotlin.io.path.inputStream -import kotlin.io.path.walk - -/** - * 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 etsFileDto.toEtsFile() -} - -/** - * Load multiple [EtsFile]s from a directory. - * - * For example, all files in `data` can be loaded with: - * ``` - * val files: Sequence = loadMultipleEtsFilesFromDirectory(Path("data")) - * ``` - */ -fun loadMultipleEtsFilesFromDirectory(dirPath: Path): Sequence { - return dirPath.walk().filter { it.extension == "json" }.map { loadEtsFile(it) } -} diff --git a/usvm-ts-dataflow/src/main/resources/logback.xml b/usvm-ts-dataflow/src/main/resources/logback.xml index 40d03b09a6..286612bef2 100644 --- a/usvm-ts-dataflow/src/main/resources/logback.xml +++ b/usvm-ts-dataflow/src/main/resources/logback.xml @@ -1,7 +1,7 @@ - %highlight([%level]) %replace(%c{0}){'(\$Companion)?\$logger\$1',''} - %msg%n + %highlight([%level]) %replace(%c{0}){'(\$Companion)?\$org.usvm.ts.service.logger\$1',''} - %msg%n diff --git a/usvm-ts-dataflow/src/test/kotlin/org/usvm/dataflow/ts/test/EtsIfdsTest.kt b/usvm-ts-dataflow/src/test/kotlin/org/usvm/dataflow/ts/test/EtsIfdsTest.kt index 2e18c960c0..83b1603efa 100644 --- a/usvm-ts-dataflow/src/test/kotlin/org/usvm/dataflow/ts/test/EtsIfdsTest.kt +++ b/usvm-ts-dataflow/src/test/kotlin/org/usvm/dataflow/ts/test/EtsIfdsTest.kt @@ -19,6 +19,7 @@ package org.usvm.dataflow.ts.test import org.jacodb.ets.model.EtsFile import org.jacodb.ets.model.EtsMethod import org.jacodb.ets.model.EtsScene +import org.jacodb.ets.utils.loadEtsFileFromResource import org.jacodb.taint.configuration.Argument import org.jacodb.taint.configuration.AssignMark import org.jacodb.taint.configuration.ConstantTrue @@ -40,7 +41,6 @@ import org.usvm.dataflow.ifds.UnitResolver import org.usvm.dataflow.taint.TaintAnalysisOptions import org.usvm.dataflow.taint.TaintManager import org.usvm.dataflow.ts.graph.EtsApplicationGraphImpl -import org.usvm.dataflow.ts.loadEtsFileFromResource import org.usvm.dataflow.ts.util.EtsTraits import kotlin.io.path.exists import kotlin.io.path.toPath diff --git a/usvm-ts-dataflow/src/test/kotlin/org/usvm/dataflow/ts/test/EtsProjectAnalysisTest.kt b/usvm-ts-dataflow/src/test/kotlin/org/usvm/dataflow/ts/test/EtsProjectAnalysisTest.kt index 84d9f04225..d109d9add4 100644 --- a/usvm-ts-dataflow/src/test/kotlin/org/usvm/dataflow/ts/test/EtsProjectAnalysisTest.kt +++ b/usvm-ts-dataflow/src/test/kotlin/org/usvm/dataflow/ts/test/EtsProjectAnalysisTest.kt @@ -21,16 +21,16 @@ import org.jacodb.ets.model.EtsFile import org.jacodb.ets.model.EtsMethod import org.jacodb.ets.model.EtsScene import org.jacodb.ets.model.EtsStmt +import org.jacodb.ets.utils.getResourcePath +import org.jacodb.ets.utils.getResourceStream +import org.jacodb.ets.utils.loadEtsFileFromResource import org.junit.jupiter.api.Test import org.junit.jupiter.api.condition.EnabledIf import org.usvm.dataflow.ifds.SingletonUnit import org.usvm.dataflow.ifds.UnitResolver import org.usvm.dataflow.taint.TaintManager import org.usvm.dataflow.taint.TaintVulnerability -import org.usvm.dataflow.ts.getResourcePath -import org.usvm.dataflow.ts.getResourceStream import org.usvm.dataflow.ts.graph.EtsApplicationGraphImpl -import org.usvm.dataflow.ts.loadEtsFileFromResource import org.usvm.dataflow.ts.test.utils.getConfigForMethod import org.usvm.dataflow.ts.test.utils.loadRules import org.usvm.dataflow.ts.util.EtsTraits diff --git a/usvm-ts-dataflow/src/test/kotlin/org/usvm/dataflow/ts/test/EtsTaintAnalysisTest.kt b/usvm-ts-dataflow/src/test/kotlin/org/usvm/dataflow/ts/test/EtsTaintAnalysisTest.kt index 6592c48f83..bcf9f1a843 100644 --- a/usvm-ts-dataflow/src/test/kotlin/org/usvm/dataflow/ts/test/EtsTaintAnalysisTest.kt +++ b/usvm-ts-dataflow/src/test/kotlin/org/usvm/dataflow/ts/test/EtsTaintAnalysisTest.kt @@ -20,6 +20,7 @@ import mu.KotlinLogging import org.jacodb.ets.model.EtsFile import org.jacodb.ets.model.EtsMethod import org.jacodb.ets.model.EtsScene +import org.jacodb.ets.utils.loadEtsFileFromResource import org.jacodb.taint.configuration.Argument import org.jacodb.taint.configuration.AssignMark import org.jacodb.taint.configuration.ConstantTrue @@ -38,7 +39,6 @@ import org.usvm.dataflow.ifds.SingletonUnit import org.usvm.dataflow.ifds.UnitResolver import org.usvm.dataflow.taint.TaintManager import org.usvm.dataflow.ts.graph.EtsApplicationGraphImpl -import org.usvm.dataflow.ts.loadEtsFileFromResource import org.usvm.dataflow.ts.util.EtsTraits import kotlin.test.assertTrue import kotlin.time.Duration.Companion.seconds diff --git a/usvm-ts-dataflow/src/test/kotlin/org/usvm/dataflow/ts/test/EtsTypeInferenceTest.kt b/usvm-ts-dataflow/src/test/kotlin/org/usvm/dataflow/ts/test/EtsTypeInferenceTest.kt index f530d25418..5c732f425a 100644 --- a/usvm-ts-dataflow/src/test/kotlin/org/usvm/dataflow/ts/test/EtsTypeInferenceTest.kt +++ b/usvm-ts-dataflow/src/test/kotlin/org/usvm/dataflow/ts/test/EtsTypeInferenceTest.kt @@ -22,8 +22,6 @@ import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.withTimeout import kotlinx.serialization.SerializationException import mu.KotlinLogging -import org.jacodb.ets.dto.EtsFileDto -import org.jacodb.ets.dto.toEtsFile import org.jacodb.ets.model.EtsAnyType import org.jacodb.ets.model.EtsAssignStmt import org.jacodb.ets.model.EtsFile @@ -34,13 +32,15 @@ import org.jacodb.ets.model.EtsType import org.jacodb.ets.model.EtsUnknownType import org.jacodb.ets.utils.CONSTRUCTOR_NAME import org.jacodb.ets.utils.getLocals +import org.jacodb.ets.utils.getResourcePath +import org.jacodb.ets.utils.getResourcePathOrNull +import org.jacodb.ets.utils.loadEtsFile import org.jacodb.ets.utils.loadEtsFileAutoConvert +import org.jacodb.ets.utils.loadEtsProjectFromResources import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestFactory import org.junit.jupiter.api.condition.EnabledIf -import org.usvm.dataflow.ts.getResourcePath -import org.usvm.dataflow.ts.getResourcePathOrNull import org.usvm.dataflow.ts.infer.AccessPathBase import org.usvm.dataflow.ts.infer.EtsTypeFact import org.usvm.dataflow.ts.infer.TypeGuesser @@ -50,7 +50,6 @@ import org.usvm.dataflow.ts.infer.annotation.InferredTypeScheme import org.usvm.dataflow.ts.infer.annotation.annotateWithTypes import org.usvm.dataflow.ts.infer.createApplicationGraph import org.usvm.dataflow.ts.infer.toType -import org.usvm.dataflow.ts.loadEtsProjectFromResources import org.usvm.dataflow.ts.testFactory import org.usvm.dataflow.ts.util.EtsTraits import org.usvm.dataflow.ts.util.sortedBy @@ -284,8 +283,10 @@ class EtsTypeInferenceTest { } println("Processing ${files.size} files...") - val etsFiles = files.map { EtsFileDto.loadFromJson(it.inputStream()).toEtsFile() } - val project = EtsScene(etsFiles, sdkFiles = emptyList()) + val etsFiles = files.map { + loadEtsFile(it.toPath()) + } + val project = EtsScene(etsFiles) val graph = createApplicationGraph(project) val entrypoints = project.projectClasses diff --git a/usvm-ts-dataflow/src/test/kotlin/org/usvm/dataflow/ts/test/EtsTypeResolverWithAstTest.kt b/usvm-ts-dataflow/src/test/kotlin/org/usvm/dataflow/ts/test/EtsTypeResolverWithAstTest.kt index 2a489de0bb..d61a747546 100644 --- a/usvm-ts-dataflow/src/test/kotlin/org/usvm/dataflow/ts/test/EtsTypeResolverWithAstTest.kt +++ b/usvm-ts-dataflow/src/test/kotlin/org/usvm/dataflow/ts/test/EtsTypeResolverWithAstTest.kt @@ -3,13 +3,13 @@ package org.usvm.dataflow.ts.test import org.jacodb.ets.model.EtsFile import org.jacodb.ets.model.EtsMethod import org.jacodb.ets.model.EtsScene +import org.jacodb.ets.utils.getResourcePath import org.jacodb.ets.utils.loadEtsFileAutoConvert import org.jacodb.ets.utils.loadEtsProjectAutoConvert import org.jacodb.ets.utils.loadEtsProjectFromIR import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Test import org.junit.jupiter.api.condition.EnabledIf -import org.usvm.dataflow.ts.getResourcePath import org.usvm.dataflow.ts.graph.EtsApplicationGraph import org.usvm.dataflow.ts.infer.AccessPathBase import org.usvm.dataflow.ts.infer.EntryPointsProcessor diff --git a/usvm-ts-dataflow/src/test/kotlin/org/usvm/dataflow/ts/test/utils/TaintConfig.kt b/usvm-ts-dataflow/src/test/kotlin/org/usvm/dataflow/ts/test/utils/TaintConfig.kt index 8402634d0f..8239f75ae3 100644 --- a/usvm-ts-dataflow/src/test/kotlin/org/usvm/dataflow/ts/test/utils/TaintConfig.kt +++ b/usvm-ts-dataflow/src/test/kotlin/org/usvm/dataflow/ts/test/utils/TaintConfig.kt @@ -19,6 +19,7 @@ package org.usvm.dataflow.ts.test.utils import kotlinx.serialization.json.Json import kotlinx.serialization.modules.SerializersModule import org.jacodb.api.common.CommonMethod +import org.jacodb.ets.utils.getResourceStream import org.jacodb.taint.configuration.NameExactMatcher import org.jacodb.taint.configuration.NamePatternMatcher import org.jacodb.taint.configuration.SerializedTaintCleaner @@ -35,7 +36,6 @@ import org.jacodb.taint.configuration.TaintMethodSource import org.jacodb.taint.configuration.TaintPassThrough import org.jacodb.taint.configuration.actionModule import org.jacodb.taint.configuration.conditionModule -import org.usvm.dataflow.ts.getResourceStream private val json = Json { classDiscriminator = "_" diff --git a/usvm-ts-dataflow/src/testFixtures/kotlin/org/usvm/dataflow/ts/LoadEts.kt b/usvm-ts-dataflow/src/testFixtures/kotlin/org/usvm/dataflow/ts/LoadEts.kt deleted file mode 100644 index 3a63ce09cb..0000000000 --- a/usvm-ts-dataflow/src/testFixtures/kotlin/org/usvm/dataflow/ts/LoadEts.kt +++ /dev/null @@ -1,120 +0,0 @@ -package org.usvm.dataflow.ts - -import mu.KotlinLogging -import org.jacodb.ets.dto.EtsFileDto -import org.jacodb.ets.dto.toEtsFile -import org.jacodb.ets.model.EtsFile -import org.jacodb.ets.model.EtsScene -import java.nio.file.Path -import kotlin.io.path.PathWalkOption -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 etsFileDto.toEtsFile() -} - -/** - * Load multiple [EtsFile]s from a resource directory. - * - * For example, all files in `resources/project/` can be loaded with: - * ``` - * val files: Sequence = loadMultipleEtsFilesFromResourceDirectory("/project") - * ``` - */ -fun loadMultipleEtsFilesFromResourceDirectory(dirPath: String): Sequence { - val rootPath = getResourcePath(dirPath) - return rootPath - .walk(PathWalkOption.BREADTH_FIRST) - .filter { it.extension == "json" } - .map { it.relativeTo(rootPath) } - .map { loadEtsFileFromResource("$dirPath/$it") } -} - -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.size} 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 etsFileDto.toEtsFile() -} - -/** - * Load multiple [EtsFile]s from a directory. - * - * For example, all files in `data` can be loaded with: - * ``` - * val files: Sequence = loadMultipleEtsFilesFromDirectory(Path("data")) - * ``` - */ -fun loadMultipleEtsFilesFromDirectory(dirPath: Path): Sequence { - return dirPath.walk().filter { it.extension == "json" }.map { loadEtsFile(it) } -} diff --git a/usvm-ts-dataflow/src/testFixtures/kotlin/org/usvm/dataflow/ts/Resources.kt b/usvm-ts-dataflow/src/testFixtures/kotlin/org/usvm/dataflow/ts/Resources.kt deleted file mode 100644 index 6b00cd961a..0000000000 --- a/usvm-ts-dataflow/src/testFixtures/kotlin/org/usvm/dataflow/ts/Resources.kt +++ /dev/null @@ -1,20 +0,0 @@ -package org.usvm.dataflow.ts - -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/usvm-ts-service/build.gradle.kts b/usvm-ts-service/build.gradle.kts new file mode 100644 index 0000000000..130d70e759 --- /dev/null +++ b/usvm-ts-service/build.gradle.kts @@ -0,0 +1,44 @@ +import com.squareup.wire.kotlin.grpcserver.GrpcServerSchemaHandler + +plugins { + id("usvm.kotlin-conventions") + id(Plugins.Wire) +} + +buildscript { + dependencies { + classpath(Libs.wire_grpc_server_generator) + } +} + +dependencies { + implementation(project(":usvm-ts")) + implementation(project(":usvm-ts-dataflow")) + + protoSource("org.jacodb:wire-protos") // TODO: fix to 'Libs.jacodb_ets_wire_protos' + implementation(Libs.jacodb_ets) + implementation(Libs.grpc_protobuf) + implementation(Libs.grpc_stub) +} + +wire { + // This is the replacement for `protoSource` dependency. + // It works, but breaks reflection in grpc_cli, + // even with manually specified `--proto_path` option. + // protoPath { + // srcDir("src/main/extraproto") + // } + custom { + schemaHandlerFactory = GrpcServerSchemaHandler.Factory() + options = mapOf( + "rpcCallStyle" to "blocking", + "singleMethodServices" to "false", + ) + exclusive = false + } + kotlin { + rpcRole = "server" + rpcCallStyle = "blocking" + singleMethodServices = false + } +} diff --git a/usvm-ts-service/src/main/extraproto/greeter.proto b/usvm-ts-service/src/main/extraproto/greeter.proto new file mode 100644 index 0000000000..0bfca42c54 --- /dev/null +++ b/usvm-ts-service/src/main/extraproto/greeter.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; +package greeter; + +option java_multiple_files = true; + +// The greeter service definition. +service Greeter { + // Sends a greeting + rpc SayHello(HelloRequest) returns (HelloReply); +} + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// The response message containing the greetings +message HelloReply { + string message = 1; +} diff --git a/usvm-ts-service/src/main/extraproto/manager.proto b/usvm-ts-service/src/main/extraproto/manager.proto new file mode 100644 index 0000000000..afc7d66c86 --- /dev/null +++ b/usvm-ts-service/src/main/extraproto/manager.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; +package manager; + +option java_multiple_files = true; + +import "model.proto"; + +service Manager { + rpc GetScene(GetSceneRequest) returns (model.Scene); +} + +message GetSceneRequest { + string path = 1; + optional bool infer_types = 2; +} diff --git a/usvm-ts-service/src/main/extraproto/model.proto b/usvm-ts-service/src/main/extraproto/model.proto new file mode 100644 index 0000000000..9f9d98d6bd --- /dev/null +++ b/usvm-ts-service/src/main/extraproto/model.proto @@ -0,0 +1,461 @@ +syntax = "proto3"; +package model; + +option java_multiple_files = true; + +message Scene { + repeated File files = 1; + repeated File sdkFiles = 2; +} + +message File { + FileSignature signature = 1; + repeated Class classes = 2; + repeated Namespace namespaces = 3; + repeated ImportInfo importInfos = 5; + repeated ExportInfo exportInfos = 6; +} + +message Namespace { + NamespaceSignature signature = 1; + repeated Class classes = 2; + repeated Namespace namespaces = 3; +} + +message Class { + ClassSignature signature = 1; + int32 modifiers = 2; + repeated Decorator decorators = 3; + int32 category = 4; + repeated Type type_parameters = 5; + string super_class_name = 6; + repeated string implemented_interface_names = 7; + repeated Field fields = 8; + repeated Method methods = 9; +} + +message Field { + FieldSignature signature = 1; + int32 modifiers = 2; + repeated Decorator decorators = 3; + bool is_optional = 4; + bool is_definitely_assigned = 5; +} + +message Method { + MethodSignature signature = 1; + repeated Type type_parameters = 2; + int32 modifiers = 3; + repeated Decorator decorators = 4; + BlockCfg cfg = 5; +} + +message Decorator { + string kind = 1; +} + +message ImportInfo { + string import_clause_name = 1; + string import_type = 2; + optional string import_from = 3; + int32 modifiers = 4; + optional string name_before_as = 5; +} + +message ExportInfo { + string export_clause_name = 1; + int32 export_clause_type = 2; + optional string export_from = 3; + int32 modifiers = 4; + optional string name_before_as = 5; +} + +message FileSignature { + string project_name = 1; + string file_name = 2; +} + +message NamespaceSignature { + string name = 1; + FileSignature file = 2; + optional NamespaceSignature parent = 3; +} + +message ClassSignature { + string name = 1; + FileSignature file = 2; + optional NamespaceSignature namespace = 3; +} + +message FieldSignature { + ClassSignature enclosing_class = 1; + string name = 2; + Type type = 3; +} + +message MethodSignature { + ClassSignature enclosing_class = 1; + string name = 2; + repeated MethodParameter parameters = 3; + Type returnType = 4; +} + +message MethodParameter { + string name = 1; + Type type = 2; + bool isOptional = 3; + bool isRest = 4; +} + +message BlockCfg { + repeated Block blocks = 1; +} + +message Block { + int32 id = 1; + repeated Stmt statements = 2; + repeated int32 successors = 3; + repeated int32 predecessors = 4; +} + +message Type { + oneof kind { + RawType raw_type = 1; + AnyType any_type = 2; + UnknownType unknown_type = 3; + UnionType union_type = 4; + IntersectionType intersection_type = 5; + GenericType generic_type = 6; + AliasType alias_type = 7; + BooleanType boolean_type = 8; + NumberType number_type = 9; + StringType string_type = 10; + NullType null_type = 11; + UndefinedType undefined_type = 12; + VoidType void_type = 13; + NeverType never_type = 14; + LiteralType literal_type = 15; + ClassType class_type = 16; + UnclearRefType unclear_ref_type = 17; + ArrayType array_type = 18; + TupleType tuple_type = 19; + FunctionType function_type = 20; + } +} + +message RawType { + string kind = 1; + string text = 2; + // map extra = 2; + // map extra = 2; +} + +message AnyType {} +message UnknownType {} +message BooleanType {} +message NumberType {} +message StringType {} +message NullType {} +message UndefinedType {} +message VoidType {} +message NeverType {} + +message UnionType { + repeated Type types = 1; +} + +message IntersectionType { + repeated Type types = 1; +} + +message GenericType { + string type_name = 1; + optional Type constraint = 2; + optional Type default_type = 3; +} + +message AliasType { + string name = 1; + Type original_type = 2; + // TODO: LocalSignature signature = 3; +} + +message LiteralType { + string literal_name = 1; +} + +message ClassType { + ClassSignature signature = 1; + repeated Type type_parameters = 2; +} + +message UnclearRefType { + string name = 1; + repeated Type type_parameters = 2; +} + +message ArrayType { + Type element_type = 1; + int32 dimensions = 2; +} + +message TupleType { + repeated Type types = 1; +} + +message FunctionType { + MethodSignature signature = 1; + repeated Type type_parameters = 2; +} + +message Stmt { + oneof kind { + RawStmt raw_stmt = 1; + NopStmt nop_stmt = 2; + AssignStmt assign_stmt = 3; + ReturnStmt return_stmt = 4; + ThrowStmt throw_stmt = 5; + IfStmt if_stmt = 6; + CallStmt call_stmt = 7; + } +} + +message RawStmt { + string kind = 1; + string text = 2; +} + +message NopStmt {} + +message AssignStmt { + Value lhv = 1; + Value rhv = 2; +} + +message ReturnStmt { + optional Value return_value = 1; +} + +message ThrowStmt { + Value exception = 1; +} + +message IfStmt { + Value condition = 1; +} + +message CallStmt { + CallExpr expr = 1; +} + +message Value { + oneof kind { + RawValue raw_value = 1; + Local local = 2; + Constant constant = 3; + Expr expr = 4; + Ref ref = 5; + } +} + +message RawValue { + string kind = 1; + string text = 2; + Type type = 3; +} + +message Local { + string name = 1; + Type type = 2; +} + +message Constant { + string value = 1; + Type type = 2; +} + +message Expr { + oneof kind { + NewExpr new_expr = 1; + NewArrayExpr new_array_expr = 2; + DeleteExpr delete_expr = 3; + AwaitExpr await_expr = 4; + YieldExpr yield_expr = 5; + TypeOfExpr type_of_expr = 6; + InstanceOfExpr instance_of_expr = 7; + CastExpr cast_expr = 8; + UnaryExpr unary_expr = 9; + BinaryExpr binary_expr = 10; + RelationExpr relation_expr = 11; + CallExpr call_expr = 12; + } +} + +message NewExpr { + Type type = 1; +} + +message NewArrayExpr { + Type element_type = 1; + Value size = 2; +} + +message DeleteExpr { + Value arg = 1; +} + +message AwaitExpr { + Value arg = 1; + Type type = 2; +} + +message YieldExpr { + Value arg = 1; + Type type = 2; +} + +message TypeOfExpr { + Value arg = 1; +} + +message CastExpr { + Value arg = 1; + Type type = 2; +} + +message InstanceOfExpr { + Value arg = 1; + Type check_type = 2; +} + +message UnaryExpr { + UnaryOperator op = 1; + Value arg = 2; + Type type = 3; +} + +enum UnaryOperator { + UNARY_UNKNOWN = 0; + NEG = 1; // - + BITWISE_NOT = 2; // ~ + LOGICAL_NOT = 3; // ! +} + +message BinaryExpr { + BinaryOperator op = 1; + Value left = 2; + Value right = 3; + Type type = 4; +} + +enum BinaryOperator { + BINARY_UNKNOWN = 0; + + ADDITION = 1; // '+' + SUBTRACTION = 2; // '-' + MULTIPLICATION = 3; // '*' + DIVISION = 4; // '/' + REMAINDER = 5; // '%' + EXPONENTIATION = 6; // '**' + + LEFT_SHIFT = 7; // '<<' + RIGHT_SHIFT = 8; // '>>' + UNSIGNED_RIGHT_SHIFT = 9; // '>>>' + + BITWISE_AND = 10; // '&' + BITWISE_OR = 11; // '|' + BITWISE_XOR = 12; // '^' + + LOGICAL_AND = 13; // '&&' + LOGICAL_OR = 14; // '||' + + NULLISH_COALESCING = 15; // ?? +} + +// Relation Expressions +message RelationExpr { + RelationOperator op = 1; + Value left = 2; + Value right = 3; +} + +// Relation Operators +enum RelationOperator { + RELATION_UNKNOWN = 0; + EQ = 1; // '==' + NEQ = 2; // '!=' + STRICT_EQ = 3; // '===' + STRICT_NEQ = 4; // '!==' + LT = 5; // '<' + LTE = 6; // '<=' + GT = 7; // '>' + GTE = 8; // '>=' + IN = 9; // 'in' +} + +// Call Expressions +message CallExpr { + MethodSignature callee = 1; + repeated Value args = 2; + Type type = 3; + + oneof kind { + InstanceCall instance_call = 4; + StaticCall static_call = 5; + PtrCall ptr_call = 6; + } +} + +message InstanceCall { + Local instance = 1; +} + +// Note: class name is included in the callee's signature +message StaticCall {} + +message PtrCall { + Value ptr = 1; +} + +// References +message Ref { + oneof kind { + This this = 1; + ParameterRef parameter = 2; + ArrayAccess array_access = 3; + FieldRef field_ref = 4; + } +} + +message This { + Type type = 1; +} + +message ParameterRef { + int32 index = 1; + Type type = 2; +} + +message ArrayAccess { + Local array = 1; + Value index = 2; + Type type = 3; +} + +message FieldRef { + oneof kind { + InstanceFieldRef instance = 1; + StaticFieldRef static = 2; + } +} + +message InstanceFieldRef { + Local instance = 1; + FieldSignature field = 2; + Type type = 3; +} + +message StaticFieldRef { + FieldSignature field = 1; + Type type = 2; +} diff --git a/usvm-ts-service/src/main/kotlin/org/usvm/ts/service/ConvertToProto.kt b/usvm-ts-service/src/main/kotlin/org/usvm/ts/service/ConvertToProto.kt new file mode 100644 index 0000000000..b5bb65d2e2 --- /dev/null +++ b/usvm-ts-service/src/main/kotlin/org/usvm/ts/service/ConvertToProto.kt @@ -0,0 +1,98 @@ +/* + * 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.usvm.ts.service + +import org.jacodb.ets.proto.toProto +import org.usvm.dataflow.ts.infer.AccessPathBase +import org.usvm.dataflow.ts.infer.EtsTypeFact +import org.usvm.dataflow.ts.infer.TypeInferenceResult +import org.usvm.dataflow.ts.infer.toType +import usvm.ArgumentTypeResult as ProtoArgumentTypeResult +import usvm.ClassTypeResult as ProtoClassTypeResult +import usvm.FieldTypeResult as ProtoFieldTypeResult +import usvm.InferredTypes as ProtoInferredTypes +import usvm.LocalTypeResult as ProtoLocalTypeResult +import usvm.MethodTypeResult as ProtoMethodTypeResult + +fun TypeInferenceResult.toProto(): ProtoInferredTypes { + val classTypeInferenceResult = inferredCombinedThisType.map { (clazz, fact) -> + val properties = (fact as? EtsTypeFact.ObjectEtsTypeFact)?.properties ?: emptyMap() + val methods = properties + .filter { it.value is EtsTypeFact.FunctionEtsTypeFact } + .keys + .sortedBy { it } + val fields = properties + .filterNot { it.value is EtsTypeFact.FunctionEtsTypeFact } + .mapNotNull { (name, fact) -> + fact.toType()?.let { + ProtoFieldTypeResult( + name = name, + type = it.toProto(), + ) + } + } + .sortedBy { it.name } + ProtoClassTypeResult( + signature = clazz.toProto(), + fields = fields, + methods = methods, + ) + }.sortedBy { + it.signature.toString() + } + + val methodTypeInferenceResult = inferredTypes.map { (method, facts) -> + val args = facts.mapNotNull { (base, fact) -> + if (base is AccessPathBase.Arg) { + val type = fact.toType() + if (type != null) { + return@mapNotNull ProtoArgumentTypeResult( + index = base.index, + type = type.toProto(), + ) + } + } + null + }.sortedBy { it.index } + val returnType = inferredReturnType[method]?.toType()?.toProto() + val locals = facts.mapNotNull { (base, fact) -> + if (base is AccessPathBase.Local) { + val type = fact.toType() + if (type != null) { + return@mapNotNull ProtoLocalTypeResult( + name = base.name, + type = type.toProto(), + ) + } + } + null + }.sortedBy { it.name } + ProtoMethodTypeResult( + signature = method.signature.toProto(), + args = args, + returnType = returnType, + locals = locals, + ) + }.sortedBy { + it.signature.toString() + } + + return ProtoInferredTypes( + classes = classTypeInferenceResult, + methods = methodTypeInferenceResult, + ) +} diff --git a/usvm-ts-service/src/main/kotlin/org/usvm/ts/service/UsvmServer.kt b/usvm-ts-service/src/main/kotlin/org/usvm/ts/service/UsvmServer.kt new file mode 100644 index 0000000000..25ff09163a --- /dev/null +++ b/usvm-ts-service/src/main/kotlin/org/usvm/ts/service/UsvmServer.kt @@ -0,0 +1,143 @@ +/* + * 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.usvm.ts.service + +import io.grpc.Server +import io.grpc.stub.StreamObserver +import manager.GetSceneRequest +import manager.ManagerClient +import mu.KotlinLogging +import org.jacodb.ets.proto.toEts +import org.jacodb.ets.service.GreeterService +import org.jacodb.ets.service.createGrpcClient +import org.jacodb.ets.service.grpcServer +import org.usvm.dataflow.ts.infer.EntryPointsProcessor +import org.usvm.dataflow.ts.infer.TypeGuesser +import org.usvm.dataflow.ts.infer.TypeInferenceManager +import org.usvm.dataflow.ts.infer.createApplicationGraph +import org.usvm.dataflow.ts.util.EtsTraits +import usvm.InferTypesRequest +import usvm.InferredTypes +import usvm.UsvmBlockingServer +import usvm.UsvmWireGrpc +import kotlin.time.measureTimedValue + +private val logger = KotlinLogging.logger {} + +class UsvmImpl : UsvmBlockingServer { + override fun InferTypes( + request: InferTypesRequest, + ): InferredTypes { + if (request.path != null) { + logger.info { "Received type inference request for '${request.path}'" } + } else if (request.scene != null) { + logger.info { "Received type inference request for Scene" } + } else { + logger.info { "Received type inference request WITHOUT path or scene" } + } + val startTime = System.currentTimeMillis() + + val sceneProto = request.scene ?: run { + logger.info { "Scene is null, requesting scene from ArkAnalyzer" } + val path = checkNotNull(request.path) + logger.info { "call GetScene(path = \"$path\")" } + // TODO: make AA port configurable + val port = 9999 // local AA + val manager = createGrpcClient(port) + val request = GetSceneRequest(path = path) + manager.GetScene().executeBlocking(request) + } + val scene = sceneProto.toEts() + val graph = createApplicationGraph(scene) + val guesser = TypeGuesser(scene) + + val (dummyMains, allMethods) = EntryPointsProcessor(scene).extractEntryPoints() + val publicMethods = allMethods.filter { m -> m.isPublic } + + val manager = TypeInferenceManager(EtsTraits(), graph) + + val useKnownTypes = true + val enableAliasAnalysis = true + + val (resultBasic, timeAnalyze) = measureTimedValue { + manager.analyze( + entrypoints = dummyMains, + allMethods = publicMethods, + doAddKnownTypes = useKnownTypes, + doAliasAnalysis = enableAliasAnalysis, + ) + } + logger.info { "Inferred types for ${resultBasic.inferredTypes.size} methods in $timeAnalyze" } + + val (result, timeGuess) = measureTimedValue { + resultBasic.withGuessedTypes(guesser) + } + logger.info { "Guessed types for ${result.inferredTypes.size} methods in $timeGuess" } + logger.info { "Done type inference in %.1f s".format((System.currentTimeMillis() - startTime) / 1000.0) } + + logger.info { "Converting to proto..." } + val resultProto = result.toProto() + logger.info { "All done in %.1f s".format((System.currentTimeMillis() - startTime) / 1000.0) } + return resultProto + } +} + +class UsvmService : UsvmWireGrpc.UsvmImplBase() { + private val impl = UsvmImpl() + + override fun InferTypes( + request: InferTypesRequest, + response: StreamObserver, + ) { + response.onNext(impl.InferTypes(request)) + response.onCompleted() + } +} + +const val DEFAULT_PORT = 7777 + +fun usvmServer(port: Int = DEFAULT_PORT): Server { + return grpcServer(port) { + maxInboundMessageSize(64 * 1024 * 1024) + addService(GreeterService()) + addService(UsvmService()) + } +} + +/** + * Example usage: + * + * 1. Start ArkAnalyzer server on port 9999: + * ``` + * cd arkanalyzer + * ARKANALYZER_PORT=9999 npm run server + * ``` + * + * 2. Run this USVM server via IDEA. + * + * 3. Send a request to the USVM server via `grpc_cli`: + * ``` + * grpc_cli call --max_recv_msg_size -1 localhost:7777 InferTypes "path: \"$(realpath ~/dev/jacodb/jacodb-ets/src/test/resources/repos/applications_app_samples/code/SuperFeature/DistributedAppDev/ArkTSDistributedCalc)\"" + * ``` + */ +fun main() { + val server = usvmServer() + server.start() + logger.info { "USVM server listening on port ${server.port}" } + server.awaitTermination() + logger.info { "USVM server stopped" } +} diff --git a/usvm-ts-service/src/main/proto/usvm.proto b/usvm-ts-service/src/main/proto/usvm.proto new file mode 100644 index 0000000000..6d845d52ef --- /dev/null +++ b/usvm-ts-service/src/main/proto/usvm.proto @@ -0,0 +1,48 @@ +syntax = "proto3"; +package usvm; + +option java_multiple_files = true; + +import "model.proto"; + +service Usvm { + rpc InferTypes(InferTypesRequest) returns (InferredTypes); +} + +message InferTypesRequest { + optional model.Scene scene = 1; + optional string path = 2; +} + +message InferredTypes { + repeated ClassTypeResult classes = 1; + repeated MethodTypeResult methods = 2; +} + +message ClassTypeResult { + model.ClassSignature signature = 1; + repeated FieldTypeResult fields = 2; + repeated string methods = 3; +} + +message FieldTypeResult { + string name = 1; + model.Type type = 2; +} + +message MethodTypeResult { + model.MethodSignature signature = 1; + repeated ArgumentTypeResult args = 2; + model.Type returnType = 3; + repeated LocalTypeResult locals = 4; +} + +message ArgumentTypeResult { + int32 index = 1; + model.Type type = 2; +} + +message LocalTypeResult { + string name = 1; + model.Type type = 2; +} diff --git a/usvm-ts/build.gradle.kts b/usvm-ts/build.gradle.kts index 088ddb8710..192606794a 100644 --- a/usvm-ts/build.gradle.kts +++ b/usvm-ts/build.gradle.kts @@ -1,10 +1,4 @@ -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import java.io.FileNotFoundException -import java.io.Reader -import kotlin.time.Duration plugins { id("usvm.kotlin-conventions") @@ -16,6 +10,7 @@ dependencies { implementation(Libs.jacodb_core) implementation(Libs.jacodb_ets) + implementation(Libs.grpc_api) implementation(Libs.ksmt_yices) implementation(Libs.ksmt_cvc5) @@ -25,16 +20,15 @@ dependencies { testImplementation(Libs.mockk) testImplementation(Libs.junit_jupiter_params) testImplementation(Libs.logback) - - // https://mvnrepository.com/artifact/org.burningwave/core - // Use it to export all modules to all - testImplementation("org.burningwave:core:12.62.7") } val generateSdkIR by tasks.registering { group = "build" description = "Generates SDK IR using ArkAnalyzer." doLast { + logger.lifecycle("Generating SDK IR using ArkAnalyzer...") + val startTime = System.currentTimeMillis() + val envVarName = "ARKANALYZER_DIR" val arkAnalyzerPath = System.getenv(envVarName) ?: run { @@ -66,100 +60,26 @@ val generateSdkIR by tasks.registering { inputDir.relativeTo(resources).path, outputDir.relativeTo(resources).path, ) - println("Running: '${cmd.joinToString(" ")}'") + logger.lifecycle("Running command: ${cmd.joinToString(" ")}") val result = ProcessUtil.run(cmd) { directory(resources) } if (result.stdout.isNotBlank()) { - println("[STDOUT]:\n--------\n${result.stdout}\n--------") + logger.lifecycle("[STDOUT]:\n--------\n${result.stdout}--------") } if (result.stderr.isNotBlank()) { - println("[STDERR]:\n--------\n${result.stderr}\n--------") + logger.lifecycle("[STDERR]:\n--------\n${result.stderr}--------") } if (result.isTimeout) { - println("Timeout!") + logger.warn("Timeout!") } if (result.exitCode != 0) { - println("Exit code: ${result.exitCode}") - } - } -} - -object ProcessUtil { - data class Result( - val exitCode: Int, - val stdout: String, - val stderr: String, - val isTimeout: Boolean, // true if the process was terminated due to timeout - ) - - fun run( - command: List, - input: String? = null, - timeout: Duration? = null, - builder: ProcessBuilder.() -> Unit = {}, - ): Result { - val reader = input?.reader() ?: "".reader() - return run(command, reader, timeout, builder) - } - - fun run( - command: List, - input: Reader, - timeout: Duration? = null, - builder: ProcessBuilder.() -> Unit = {}, - ): Result { - val process = ProcessBuilder(command).apply(builder).start() - return communicate(process, input, timeout) - } - - private fun communicate( - process: Process, - input: Reader, - timeout: Duration? = null, - ): Result { - val stdout = StringBuilder() - val stderr = StringBuilder() - - val scope = CoroutineScope(Dispatchers.IO) - - // Handle process input - val stdinJob = scope.launch { - process.outputStream.bufferedWriter().use { writer -> - input.copyTo(writer) - } - } - - // Launch output capture coroutines - val stdoutJob = scope.launch { - process.inputStream.bufferedReader().useLines { lines -> - lines.forEach { stdout.appendLine(it) } - } - } - val stderrJob = scope.launch { - process.errorStream.bufferedReader().useLines { lines -> - lines.forEach { stderr.appendLine(it) } - } - } - - // Wait for completion - val isTimeout = if (timeout != null) { - !process.waitFor(timeout.inWholeNanoseconds, TimeUnit.NANOSECONDS) - } else { - process.waitFor() - false - } - runBlocking { - stdinJob.join() - stdoutJob.join() - stderrJob.join() + logger.warn("Exit code: ${result.exitCode}") } - return Result( - exitCode = process.exitValue(), - stdout = stdout.toString(), - stderr = stderr.toString(), - isTimeout = isTimeout, + logger.lifecycle( + "Done generating SDK IR in %.1fs" + .format((System.currentTimeMillis() - startTime) / 1000.0) ) } } diff --git a/usvm-ts/src/main/kotlin/org/usvm/machine/expr/TsExprResolver.kt b/usvm-ts/src/main/kotlin/org/usvm/machine/expr/TsExprResolver.kt index c0fa342030..777acddfba 100644 --- a/usvm-ts/src/main/kotlin/org/usvm/machine/expr/TsExprResolver.kt +++ b/usvm-ts/src/main/kotlin/org/usvm/machine/expr/TsExprResolver.kt @@ -826,14 +826,12 @@ class TsExprResolver( fun checkUndefinedOrNullPropertyRead(instance: UHeapRef) = with(ctx) { val ref = instance.unwrapRef(scope) - - val neqNull = mkAnd( - mkHeapRefEq(ref, mkUndefinedValue()).not(), - mkHeapRefEq(ref, mkTsNullValue()).not(), + val notNull = mkAnd( + mkHeapRefEq(ref, ctx.mkUndefinedValue()).not(), + mkHeapRefEq(ref, ctx.mkTsNullValue()).not() ) - scope.fork( - neqNull, + condition = notNull, blockOnFalseState = allocateException(EtsStringType) // TODO incorrect exception type ) } @@ -947,21 +945,21 @@ class TsExprResolver( } override fun visit(value: EtsInstanceFieldRef): UExpr? = with(ctx) { - val instanceRefResolved = resolve(value.instance) ?: return null - if (instanceRefResolved.sort != addressSort) { - logger.error("InstanceFieldRef access on not address sort: $instanceRefResolved") + val instanceResolved = resolve(value.instance) ?: return null + if (instanceResolved.sort != addressSort) { + logger.error("InstanceFieldRef access on not address sort: $instanceResolved") scope.assert(falseExpr) return null } - val instanceRef = instanceRefResolved.asExpr(addressSort) + val instance = instanceResolved.asExpr(addressSort) - checkUndefinedOrNullPropertyRead(instanceRef) ?: return null + checkUndefinedOrNullPropertyRead(instance) ?: return null // TODO It is a hack for array's length if (value.field.name == "length") { if (value.instance.type is EtsArrayType) { val arrayType = value.instance.type as EtsArrayType - val lengthLValue = mkArrayLengthLValue(instanceRef, arrayType) + val lengthLValue = mkArrayLengthLValue(instance, arrayType) val length = scope.calcOnState { memory.read(lengthLValue) } scope.doWithState { pathConstraints += mkBvSignedGreaterOrEqualExpr(length, mkBv(0)) } @@ -969,13 +967,13 @@ class TsExprResolver( } // TODO: handle "length" property for arrays inside fake objects - if (instanceRef.isFakeObject()) { - val fakeType = instanceRef.getFakeType(scope) + if (instance.isFakeObject()) { + val fakeType = instance.getFakeType(scope) // If we want to get length from a fake object, we assume that it is an array. scope.doWithState { pathConstraints += fakeType.refTypeExpr } - val refLValue = getIntermediateRefLValue(instanceRef.address) + val refLValue = getIntermediateRefLValue(instance.address) val obj = scope.calcOnState { memory.read(refLValue) } val type = value.instance.type @@ -1003,7 +1001,12 @@ class TsExprResolver( } } - return handleFieldRef(value.instance, instanceRef, value.field, hierarchy) + return handleFieldRef( + instance = value.instance, + instanceRef = instance, + field = value.field, + hierarchy = hierarchy, + ) } override fun visit(value: EtsStaticFieldRef): UExpr? = with(ctx) { @@ -1011,7 +1014,7 @@ class TsExprResolver( it.signature == value.field.enclosingClass } ?: return null - val instanceRef = scope.calcOnState { getStaticInstance(clazz) } + val instance = scope.calcOnState { getStaticInstance(clazz) } val initializer = clazz.methods.singleOrNull { it.name == STATIC_INIT_METHOD_NAME } if (initializer != null) { @@ -1030,14 +1033,19 @@ class TsExprResolver( pushSortsForArguments(instance = null, args = emptyList(), localToIdx) registerCallee(currentStatement, initializer.cfg) callStack.push(initializer, currentStatement) - memory.stack.push(arrayOf(instanceRef), initializer.localsCount) + memory.stack.push(arrayOf(instance), initializer.localsCount) newStmt(initializer.cfg.stmts.first()) } return null } } - return handleFieldRef(instance = null, instanceRef, value.field, hierarchy) + return handleFieldRef( + instance = null, + instanceRef = instance, + field = value.field, + hierarchy = hierarchy, + ) } // endregion @@ -1125,16 +1133,16 @@ class TsSimpleValueResolver( private val localToIdx: (EtsMethod, EtsValue) -> Int?, ) : EtsValue.Visitor?> { - private fun resolveLocal(local: EtsValue): ULValue<*, USort> { + private fun resolveLocal(local: EtsValue): ULValue<*, USort> = with(ctx) { val currentMethod = scope.calcOnState { lastEnteredMethod } val entrypoint = scope.calcOnState { entrypoint } - val localIdx = localToIdx(currentMethod, local) + val idx = localToIdx(currentMethod, local) // If there is no local variable corresponding to the local, // we treat it as a field of some global object with the corresponding name. // It helps us to support global variables that are missed in the IR. - if (localIdx == null) { + if (idx == null) { require(local is EtsLocal) val globalObject = scope.calcOnState { globalObject } @@ -1167,36 +1175,34 @@ class TsSimpleValueResolver( val sort = scope.calcOnState { val type = local.tryGetKnownType(currentMethod) - getOrPutSortForLocal(localIdx, type) + getOrPutSortForLocal(idx, type) } // If we are not in the entrypoint, all correct values are already resolved and we can just return // a registerStackLValue for the local if (currentMethod != entrypoint) { - return mkRegisterStackLValue(sort, localIdx) + return mkRegisterStackLValue(sort, idx) } // arguments and this for the first stack frame - return when (sort) { - is UBoolSort -> mkRegisterStackLValue(sort, localIdx) - is KFp64Sort -> mkRegisterStackLValue(sort, localIdx) - is UAddressSort -> mkRegisterStackLValue(sort, localIdx) + when (sort) { + is UBoolSort -> mkRegisterStackLValue(sort, idx) + is KFp64Sort -> mkRegisterStackLValue(sort, idx) + is UAddressSort -> mkRegisterStackLValue(sort, idx) is TsUnresolvedSort -> { check(local is EtsThis || local is EtsParameterRef) { "Only This and ParameterRef are expected here" } - val lValue = mkRegisterStackLValue(ctx.addressSort, localIdx) + val lValue = mkRegisterStackLValue(addressSort, idx) - val boolRValue = ctx.mkRegisterReading(localIdx, ctx.boolSort) - val fpRValue = ctx.mkRegisterReading(localIdx, ctx.fp64Sort) - val refRValue = ctx.mkRegisterReading(localIdx, ctx.addressSort) + val boolRValue = mkRegisterReading(idx, boolSort) + val fpRValue = mkRegisterReading(idx, fp64Sort) + val refRValue = mkRegisterReading(idx, addressSort) - val fakeObject = ctx.mkFakeValue(scope, boolRValue, fpRValue, refRValue) + val fakeObject = mkFakeValue(scope, boolRValue, fpRValue, refRValue) scope.calcOnState { - with(ctx) { - memory.write(lValue, fakeObject.asExpr(addressSort), guard = trueExpr) - } + memory.write(lValue, fakeObject.asExpr(addressSort), guard = trueExpr) } lValue diff --git a/usvm-ts/src/main/kotlin/org/usvm/machine/interpreter/TsInterpreter.kt b/usvm-ts/src/main/kotlin/org/usvm/machine/interpreter/TsInterpreter.kt index 0a61c39664..33a2502650 100644 --- a/usvm-ts/src/main/kotlin/org/usvm/machine/interpreter/TsInterpreter.kt +++ b/usvm-ts/src/main/kotlin/org/usvm/machine/interpreter/TsInterpreter.kt @@ -191,8 +191,23 @@ class TsInterpreter( return } val cls = classes.single() - val suitableMethods = cls.methods.filter { it.name == stmt.callee.name } - concreteMethods += suitableMethods + val methods = cls.methods.filter { it.name == stmt.callee.name } + if (methods.isEmpty()) { + logger.warn { + "Could not resolve method: ${stmt.callee} on type: $type" + } + scope.assert(ctx.falseExpr) + return + } + if (methods.size > 1) { + logger.warn { + "Multiple methods with name: ${stmt.callee} on type: $type" + } + scope.assert(ctx.falseExpr) + return + } + val method = methods.single() + concreteMethods += method } else { logger.warn { "Could not resolve method: ${stmt.callee} on type: $type" diff --git a/usvm-ts/src/main/kotlin/org/usvm/util/EtsHierarchy.kt b/usvm-ts/src/main/kotlin/org/usvm/util/EtsHierarchy.kt index 213a58b50e..961183946d 100644 --- a/usvm-ts/src/main/kotlin/org/usvm/util/EtsHierarchy.kt +++ b/usvm-ts/src/main/kotlin/org/usvm/util/EtsHierarchy.kt @@ -9,7 +9,8 @@ import org.jacodb.ets.model.EtsFileSignature import org.jacodb.ets.model.EtsRefType import org.jacodb.ets.model.EtsScene import org.jacodb.ets.model.EtsUnclearRefType -import kotlin.system.measureTimeMillis +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.measureTimedValue private val logger = KotlinLogging.logger { } private typealias ClassName = String @@ -26,31 +27,41 @@ class EtsHierarchy(private val scene: EtsScene) { } private val ancestors: Map> by lazy { - val result: Map> - - val time = measureTimeMillis { - result = scene.projectAndSdkClasses.associateWith { start -> + val (result, time) = measureTimedValue { + scene.projectAndSdkClasses.associateWith { start -> generateSequence(listOf(start)) { classes -> if (classes.isEmpty()) return@generateSequence null - - val result = classes.flatMap { current -> - val superClassSignature = current.superClass ?: return@generateSequence null - + classes.flatMap { current -> + val superClassName = current.superClassName ?: return@generateSequence null + val superClassSignature = EtsClassSignature( + name = superClassName, + file = EtsFileSignature.UNKNOWN, + ) val superClasses = resolveClassesBySignature(superClassSignature) - val interfaces = current.implementedInterfaces + val interfaces = current.implementedInterfaceNames.map { + EtsClassSignature( + name = it, + file = EtsFileSignature.UNKNOWN, + ) + } val resolvedInterfaces = interfaces.flatMap { resolveClassesBySignature(it) } - superClasses.toMutableSet() + resolvedInterfaces // TODO optimize - } + superClasses.toHashSet() + resolvedInterfaces - result.takeIf { it.isNotEmpty() } - }.flatten().toMutableSet() + classesForType(OBJECT_CLASS) + // val classesWithTheSameName = resolveMap.getValue(superClassName) + // val superClasses = classesWithTheSameName.values + // val interfaces = current.implementedInterfaceNames + // // TODO support interfaces + // require(interfaces.isEmpty()) { "Interfaces are not supported" } + // superClasses + } + }.flatten().toHashSet() + classesForType(OBJECT_CLASS) } } - if (time > 100) { - logger.warn { "Ancestors map is built in $time ms" } + if (time > 100.milliseconds) { + logger.warn { "Ancestors map is built in $time " } } return@lazy result diff --git a/usvm-ts/src/test/kotlin/org/usvm/util/LoadEts.kt b/usvm-ts/src/test/kotlin/org/usvm/util/LoadEts.kt index 581758e1bc..f3056bf3eb 100644 --- a/usvm-ts/src/test/kotlin/org/usvm/util/LoadEts.kt +++ b/usvm-ts/src/test/kotlin/org/usvm/util/LoadEts.kt @@ -1,7 +1,7 @@ package org.usvm.util import mu.KotlinLogging -import org.jacodb.ets.dto.EtsFileDto +import org.jacodb.ets.dto.FileDto import org.jacodb.ets.dto.toEtsFile import org.jacodb.ets.model.EtsFile import org.jacodb.ets.model.EtsScene @@ -15,18 +15,18 @@ import kotlin.io.path.walk private val logger = KotlinLogging.logger {} /** - * Load an [EtsFileDto] from a resource file. + * Load an [FileDto] from a resource file. * * For example, `resources/ets/sample.json` can be loaded with: * ``` - * val dto: EtsFileDto = loadEtsFileDtoFromResource("/ets/sample.json") + * val dto: FileDto = loadFileDtoFromResource("/ets/sample.json") * ``` */ -fun loadEtsFileDtoFromResource(jsonPath: String): EtsFileDto { +fun loadFileDtoFromResource(jsonPath: String): FileDto { 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) + return FileDto.loadFromJson(stream) } } @@ -39,8 +39,8 @@ fun loadEtsFileDtoFromResource(jsonPath: String): EtsFileDto { * ``` */ fun loadEtsFileFromResource(jsonPath: String): EtsFile { - val etsFileDto = loadEtsFileDtoFromResource(jsonPath) - return etsFileDto.toEtsFile() + val fileDto = loadFileDtoFromResource(jsonPath) + return fileDto.toEtsFile() } /** @@ -79,17 +79,17 @@ fun loadEtsProjectFromResources( //----------------------------------------------------------------------------- /** - * Load an [EtsFileDto] from a file. + * Load an [FileDto] from a file. * * For example, `data/sample.json` can be loaded with: * ``` - * val dto: EtsFileDto = loadEtsFileDto(Path("data/sample.json")) + * val dto: FileDto = loadFileDto(Path("data/sample.json")) * ``` */ -fun loadEtsFileDto(path: Path): EtsFileDto { +fun loadFileDto(path: Path): FileDto { require(path.extension == "json") { "File must have a '.json' extension: $path" } path.inputStream().use { stream -> - return EtsFileDto.loadFromJson(stream) + return FileDto.loadFromJson(stream) } } @@ -102,7 +102,7 @@ fun loadEtsFileDto(path: Path): EtsFileDto { * ``` */ fun loadEtsFile(path: Path): EtsFile { - val etsFileDto = loadEtsFileDto(path) + val etsFileDto = loadFileDto(path) return etsFileDto.toEtsFile() } diff --git a/usvm-ts/src/test/kotlin/org/usvm/util/SetupServer.kt b/usvm-ts/src/test/kotlin/org/usvm/util/SetupServer.kt new file mode 100644 index 0000000000..3f6279d8a9 --- /dev/null +++ b/usvm-ts/src/test/kotlin/org/usvm/util/SetupServer.kt @@ -0,0 +1,30 @@ +package org.usvm.util + +import mu.KotlinLogging +import org.jacodb.ets.grpc.Server +import org.jacodb.ets.grpc.startArkAnalyzerServer +import org.junit.jupiter.api.extension.AfterAllCallback +import org.junit.jupiter.api.extension.BeforeAllCallback +import org.junit.jupiter.api.extension.ExtensionContext + +private val logger = KotlinLogging.logger {} + +class SetupServer : BeforeAllCallback, AfterAllCallback { + companion object { + const val PORT = 42000 + } + + private lateinit var server: Server + + override fun beforeAll(context: ExtensionContext) { + logger.info { "Setting up test environment..." } + server = startArkAnalyzerServer(PORT) + logger.info { "Done setting up test environment" } + } + + override fun afterAll(context: ExtensionContext?) { + logger.info { "Shutting down test environment..." } + server.stop() + logger.info { "Test environment shut down" } + } +} diff --git a/usvm-ts/src/test/kotlin/org/usvm/util/TsMethodTestRunner.kt b/usvm-ts/src/test/kotlin/org/usvm/util/TsMethodTestRunner.kt index ce5198b8f7..7a61a95dd3 100644 --- a/usvm-ts/src/test/kotlin/org/usvm/util/TsMethodTestRunner.kt +++ b/usvm-ts/src/test/kotlin/org/usvm/util/TsMethodTestRunner.kt @@ -1,5 +1,7 @@ package org.usvm.util +import mu.KotlinLogging +import org.jacodb.ets.grpc.loadScene import org.jacodb.ets.model.EtsAnyType import org.jacodb.ets.model.EtsArrayType import org.jacodb.ets.model.EtsBooleanType @@ -14,9 +16,8 @@ import org.jacodb.ets.model.EtsStringType import org.jacodb.ets.model.EtsType import org.jacodb.ets.model.EtsUndefinedType import org.jacodb.ets.model.EtsUnknownType -import org.jacodb.ets.utils.loadEtsFileAutoConvert -import org.junit.jupiter.api.TestInstance -import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS +import org.jacodb.ets.proto.toEts +import org.junit.jupiter.api.extension.ExtendWith import org.usvm.PathSelectionStrategy import org.usvm.SolverType import org.usvm.UMachineOptions @@ -33,27 +34,44 @@ import kotlin.reflect.KClass import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds +private val logger = KotlinLogging.logger {} + typealias CoverageChecker = (TsMethodCoverage) -> Boolean -@TestInstance(PER_CLASS) +@ExtendWith(SetupServer::class) abstract class TsMethodTestRunner : TestRunner() { - protected abstract val scene: EtsScene + companion object { + private const val PORT = SetupServer.PORT - protected fun loadSampleScene( - className: String, - folderPrefix: String = "", - useArkAnalyzerTypeInference: Boolean = false, - ): EtsScene { - val name = "$className.ts" - val path = getResourcePath("/samples/$folderPrefix/$name") - val file = loadEtsFileAutoConvert( - path, - useArkAnalyzerTypeInference = if (useArkAnalyzerTypeInference) 1 else null - ) - return EtsScene(listOf(file)) + fun loadSampleScene( + className: String, + folderPrefix: String = "", + useArkAnalyzerTypeInference: Boolean = false, + ): EtsScene { + val name = "$className.ts" + val path = getResourcePath("/samples/$folderPrefix/$name") + + logger.info { "Loading Scene from '$path'..." } + val sceneProto = loadScene(PORT, path, inferTypes = useArkAnalyzerTypeInference) + + logger.info { "Converting Scene from ProtoBuf to ETS..." } + val scene = sceneProto.toEts() + logger.info { + "Scene has ${ + scene.projectFiles.size + } files, ${ + scene.projectAndSdkClasses.size + } classes, ${ + scene.projectAndSdkClasses.flatMap { it.methods }.size + } methods" + } + return scene + } } + protected abstract val scene: EtsScene + protected fun getMethod(className: String, methodName: String): EtsMethod { return scene .projectAndSdkClasses.single { it.name == className } diff --git a/usvm-ts/src/test/kotlin/org/usvm/util/TsTestResolver.kt b/usvm-ts/src/test/kotlin/org/usvm/util/TsTestResolver.kt index 31d2cfdfa8..6b57b5db9f 100644 --- a/usvm-ts/src/test/kotlin/org/usvm/util/TsTestResolver.kt +++ b/usvm-ts/src/test/kotlin/org/usvm/util/TsTestResolver.kt @@ -388,7 +388,7 @@ open class TsTestStateResolver( EtsNullType -> TODO() EtsNeverType -> TODO() EtsStringType -> TsTestValue.TsString("String construction is not yet implemented") - EtsVoidType -> TODO() + EtsVoidType -> TsTestValue.TsUndefined else -> error("Unexpected type: $type") } }