From c599ea7f7a177081939afc705f3db27d6d25279e Mon Sep 17 00:00:00 2001 From: Konstantin Chukharev Date: Fri, 27 Dec 2024 17:21:52 +0300 Subject: [PATCH] Type inference (#233) --- .github/workflows/build-and-run-tests.yml | 2 +- .gitignore | 3 + buildSrc/build.gradle.kts | 3 +- buildSrc/settings.gradle.kts | 1 - buildSrc/src/main/kotlin/Dependencies.kt | 36 +- .../usvmpython/tasks/CPythonBuildTasks.kt | 2 +- .../kotlin/usvmpython/tasks/JNIHeaderTask.kt | 2 +- .../usvmpython/tasks/PythonSamplesTasks.kt | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 43462 -> 43504 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 7 +- gradlew.bat | 2 + settings.gradle.kts | 6 + usvm-dataflow-ts/.gitignore | 1 + usvm-dataflow-ts/README.md | 167 +++ usvm-dataflow-ts/build.gradle.kts | 132 +++ .../dataflow/ts/graph/EtsApplicationGraph.kt | 282 +++++ .../org/usvm/dataflow/ts/ifds/UnitResolver.kt | 62 ++ .../org/usvm/dataflow/ts/infer/AccessPath.kt | 98 ++ .../org/usvm/dataflow/ts/infer/Accessors.kt | 26 +- .../org/usvm/dataflow/ts/infer/Alias.kt | 324 ++++++ .../usvm/dataflow/ts/infer/AnalyzerEvent.kt | 25 + .../dataflow/ts/infer/ApplicationGraph.kt | 78 ++ .../dataflow/ts/infer/BackwardAnalyzer.kt | 44 + .../ts/infer/BackwardFlowFunctions.kt | 578 ++++++++++ .../dataflow/ts/infer/EntryPointsProcessor.kt | 25 + .../dataflow/ts/infer/EtsMethodTypeFacts.kt | 8 + .../org/usvm/dataflow/ts/infer/EtsTypeFact.kt | 531 ++++++++++ .../usvm/dataflow/ts/infer/ForwardAnalyzer.kt | 44 + .../dataflow/ts/infer/ForwardFlowFunctions.kt | 586 +++++++++++ .../usvm/dataflow/ts/infer/TypeDomainFact.kt | 21 + .../org/usvm/dataflow/ts/infer/TypeGuesser.kt | 269 +++++ .../dataflow/ts/infer/TypeInferenceManager.kt | 712 +++++++++++++ .../dataflow/ts/infer/TypeInferenceResult.kt | 57 + .../ts/infer/annotation/ExprTypeAnnotator.kt | 279 +++++ .../ts/infer/annotation/StmtTypeAnnotator.kt | 83 ++ .../ts/infer/annotation/ValueTypeAnnotator.kt | 113 ++ .../ts/infer/annotation/withInferredTypes.kt | 91 ++ .../usvm/dataflow/ts/infer/cli/InferTypes.kt | 143 +++ .../org/usvm/dataflow/ts/infer/dto/Convert.kt | 195 ++++ .../org/usvm/dataflow/ts/infer/dto/DTO.kt | 96 ++ .../usvm/dataflow/ts/infer/verify/EntityId.kt | 79 ++ .../ts/infer/verify/VerificationResult.kt | 34 + .../usvm/dataflow/ts/infer/verify/Verify.kt | 28 + .../collectors/ClassSummaryCollector.kt | 39 + .../verify/collectors/ExprSummaryCollector.kt | 93 ++ .../verify/collectors/StmtSummaryCollector.kt | 80 ++ .../verify/collectors/SummaryCollector.kt | 76 ++ .../ts/infer/verify/collectors/Utils.kt | 34 + .../collectors/ValueSummaryCollector.kt | 85 ++ .../org/usvm/dataflow/ts/util/EtsTraits.kt | 252 +++++ .../ts/util/TypeInferenceStatistics.kt | 696 ++++++++++++ .../src/main/resources/logback.xml | 19 + .../org/usvm/dataflow/ts/test/EtsIfdsTest.kt | 437 ++++++++ .../ts/test/EtsProjectAnalysisTest.kt | 155 +++ .../org/usvm/dataflow/ts/test/EtsSceneTest.kt | 184 ++++ .../dataflow/ts/test/EtsTaintAnalysisTest.kt | 141 +++ .../dataflow/ts/test/EtsTypeAnnotationTest.kt | 193 ++++ .../dataflow/ts/test/EtsTypeInferenceTest.kt | 593 +++++++++++ .../ts/test/EtsTypeResolverAbcTest.kt | 93 ++ .../ts/test/EtsTypeResolverWithAstTest.kt | 366 +++++++ .../ts/test/utils/ExpectedTypesExtractor.kt | 377 +++++++ .../dataflow/ts/test/utils/TaintConfig.kt | 108 ++ .../src/test/resources/logback.xml | 19 + .../src/test/resources/ts/call.ts | 13 + .../src/test/resources/ts/cast.ts | 12 + .../src/test/resources/ts/data.ts | 17 + .../src/test/resources/ts/microphone.ts | 25 + .../src/test/resources/ts/microphone_ctor.ts | 33 + .../src/test/resources/ts/nested_init.ts | 23 + .../src/test/resources/ts/resolver_test.ts | 50 + .../src/test/resources/ts/taint.ts | 32 + .../src/test/resources/ts/testcases.ts | 994 ++++++++++++++++++ .../src/test/resources/ts/types.ts | 55 + .../kotlin/org/usvm/dataflow/ts/LoadEts.kt | 117 +++ .../kotlin/org/usvm/dataflow/ts/Resources.kt | 20 + .../org/usvm/dataflow/ts/TestFactoryDsl.kt | 52 + usvm-dataflow/build.gradle.kts | 5 +- .../org/usvm/dataflow/config/Condition.kt | 4 +- .../org/usvm/dataflow/config/Position.kt | 6 +- .../org/usvm/dataflow/config/TaintAction.kt | 10 +- .../kotlin/org/usvm/dataflow/ifds/Maybe.kt | 60 -- .../usvm/dataflow/taint/TaintFlowFunctions.kt | 3 +- usvm-jvm-dataflow/build.gradle.kts | 4 +- .../usvm/dataflow/jvm/npe/NpeFlowFunctions.kt | 2 +- .../jvm/impl/ConditionEvaluatorTest.kt | 4 +- .../impl/NullabilityAssumptionAnalysisTest.kt | 69 -- usvm-ts/build.gradle.kts | 1 + .../kotlin/org/usvm/TSApplicationGraph.kt | 8 +- .../main/kotlin/org/usvm/TSExprResolver.kt | 22 +- usvm-ts/src/main/kotlin/org/usvm/TSMachine.kt | 4 +- .../src/main/kotlin/org/usvm/TSTypeSystem.kt | 4 +- .../org/usvm/util/TSMethodTestRunner.kt | 7 +- .../src/main/kotlin/org/usvm/util/Logging.kt | 8 +- .../src/main/kotlin/org/usvm/util/Maybe.kt | 62 +- 95 files changed, 10819 insertions(+), 226 deletions(-) create mode 100644 usvm-dataflow-ts/.gitignore create mode 100644 usvm-dataflow-ts/README.md create mode 100644 usvm-dataflow-ts/build.gradle.kts create mode 100644 usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/graph/EtsApplicationGraph.kt create mode 100644 usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/ifds/UnitResolver.kt create mode 100644 usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/AccessPath.kt rename usvm-jvm-dataflow/src/samples/java/NullAssumptionAnalysisExample.java => usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/Accessors.kt (63%) create mode 100644 usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/Alias.kt create mode 100644 usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/AnalyzerEvent.kt create mode 100644 usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/ApplicationGraph.kt create mode 100644 usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/BackwardAnalyzer.kt create mode 100644 usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/BackwardFlowFunctions.kt create mode 100644 usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/EntryPointsProcessor.kt create mode 100644 usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/EtsMethodTypeFacts.kt create mode 100644 usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/EtsTypeFact.kt create mode 100644 usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/ForwardAnalyzer.kt create mode 100644 usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/ForwardFlowFunctions.kt create mode 100644 usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/TypeDomainFact.kt create mode 100644 usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/TypeGuesser.kt create mode 100644 usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/TypeInferenceManager.kt create mode 100644 usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/TypeInferenceResult.kt create mode 100644 usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/annotation/ExprTypeAnnotator.kt create mode 100644 usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/annotation/StmtTypeAnnotator.kt create mode 100644 usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/annotation/ValueTypeAnnotator.kt create mode 100644 usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/annotation/withInferredTypes.kt create mode 100644 usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/cli/InferTypes.kt create mode 100644 usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/dto/Convert.kt create mode 100644 usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/dto/DTO.kt create mode 100644 usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/verify/EntityId.kt create mode 100644 usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/verify/VerificationResult.kt create mode 100644 usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/verify/Verify.kt create mode 100644 usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/verify/collectors/ClassSummaryCollector.kt create mode 100644 usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/verify/collectors/ExprSummaryCollector.kt create mode 100644 usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/verify/collectors/StmtSummaryCollector.kt create mode 100644 usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/verify/collectors/SummaryCollector.kt create mode 100644 usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/verify/collectors/Utils.kt create mode 100644 usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/verify/collectors/ValueSummaryCollector.kt create mode 100644 usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/util/EtsTraits.kt create mode 100644 usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/util/TypeInferenceStatistics.kt create mode 100644 usvm-dataflow-ts/src/main/resources/logback.xml create mode 100644 usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/EtsIfdsTest.kt create mode 100644 usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/EtsProjectAnalysisTest.kt create mode 100644 usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/EtsSceneTest.kt create mode 100644 usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/EtsTaintAnalysisTest.kt create mode 100644 usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/EtsTypeAnnotationTest.kt create mode 100644 usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/EtsTypeInferenceTest.kt create mode 100644 usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/EtsTypeResolverAbcTest.kt create mode 100644 usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/EtsTypeResolverWithAstTest.kt create mode 100644 usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/utils/ExpectedTypesExtractor.kt create mode 100644 usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/utils/TaintConfig.kt create mode 100644 usvm-dataflow-ts/src/test/resources/logback.xml create mode 100644 usvm-dataflow-ts/src/test/resources/ts/call.ts create mode 100644 usvm-dataflow-ts/src/test/resources/ts/cast.ts create mode 100644 usvm-dataflow-ts/src/test/resources/ts/data.ts create mode 100644 usvm-dataflow-ts/src/test/resources/ts/microphone.ts create mode 100644 usvm-dataflow-ts/src/test/resources/ts/microphone_ctor.ts create mode 100644 usvm-dataflow-ts/src/test/resources/ts/nested_init.ts create mode 100644 usvm-dataflow-ts/src/test/resources/ts/resolver_test.ts create mode 100644 usvm-dataflow-ts/src/test/resources/ts/taint.ts create mode 100644 usvm-dataflow-ts/src/test/resources/ts/testcases.ts create mode 100644 usvm-dataflow-ts/src/test/resources/ts/types.ts create mode 100644 usvm-dataflow-ts/src/testFixtures/kotlin/org/usvm/dataflow/ts/LoadEts.kt create mode 100644 usvm-dataflow-ts/src/testFixtures/kotlin/org/usvm/dataflow/ts/Resources.kt create mode 100644 usvm-dataflow-ts/src/testFixtures/kotlin/org/usvm/dataflow/ts/TestFactoryDsl.kt delete mode 100644 usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/Maybe.kt delete mode 100644 usvm-jvm-dataflow/src/test/kotlin/org/usvm/dataflow/jvm/impl/NullabilityAssumptionAnalysisTest.kt diff --git a/.github/workflows/build-and-run-tests.yml b/.github/workflows/build-and-run-tests.yml index 1c1eae220d..ada025afd2 100644 --- a/.github/workflows/build-and-run-tests.yml +++ b/.github/workflows/build-and-run-tests.yml @@ -40,7 +40,7 @@ jobs: DEST_DIR="arkanalyzer" MAX_RETRIES=10 RETRY_DELAY=3 # Delay between retries in seconds - BRANCH="neo/2024-08-07" + 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 3322abd66b..adbbc452c4 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,6 @@ buildSrc/.gradle # Ignore Python execution cache __pycache__/ run_python_with_gdb.sh + +# Ignore Kotlin build directory +.kotlin/ diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index cbd46db18e..5075d846cd 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -12,9 +12,8 @@ repositories { maven("https://jitpack.io") } - dependencies { implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") implementation("io.gitlab.arturbosch.detekt:detekt-gradle-plugin:$detektVersion") implementation("org.glavo:gjavah:$gjavahVersion") -} \ No newline at end of file +} diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts index c6e4cfd742..27752ba2fd 100644 --- a/buildSrc/settings.gradle.kts +++ b/buildSrc/settings.gradle.kts @@ -1,2 +1 @@ rootProject.name="usvm-conventions" - diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index b9c3771821..0ad33fec6e 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -3,7 +3,8 @@ import org.gradle.plugin.use.PluginDependenciesSpec object Versions { - const val detekt = "1.18.1" + const val clikt = "5.0.0" + const val detekt = "1.23.7" const val ini4j = "0.5.4" const val jacodb = "ad5e1f170e" const val juliet = "1.3.2" @@ -18,7 +19,7 @@ object Versions { const val mockk = "1.13.4" const val rd = "2023.2.0" const val sarif4k = "0.5.0" - const val shadow = "8.1.1" + const val shadow = "8.3.3" const val slf4j = "1.6.1" // versions for jvm samples @@ -121,6 +122,11 @@ object Libs { name = "jacodb-core", version = Versions.jacodb ) + val jacodb_api_common = dep( + group = jacodbPackage, + name = "jacodb-api-common", + version = Versions.jacodb + ) val jacodb_api_jvm = dep( group = jacodbPackage, name = "jacodb-api-jvm", @@ -136,16 +142,6 @@ object Libs { name = "jacodb-storage", version = Versions.jacodb ) - val jacodb_ets = dep( - group = jacodbPackage, - name = "jacodb-ets", - version = Versions.jacodb - ) - val jacodb_api_common = dep( - group = jacodbPackage, - name = "jacodb-api-common", - version = Versions.jacodb - ) val jacodb_approximations = dep( group = jacodbPackage, name = "jacodb-approximations", @@ -156,6 +152,11 @@ object Libs { name = "jacodb-taint-configuration", version = Versions.jacodb ) + val jacodb_ets = dep( + group = jacodbPackage, + name = "jacodb-ets", + version = Versions.jacodb + ) // https://github.com/Kotlin/kotlinx.coroutines val kotlinx_coroutines_core = dep( @@ -240,6 +241,13 @@ object Libs { name = "PythonTypesAPI", version = Versions.pythonTypesAPI ) + + // https://github.com/ajalt/clikt + val clikt = dep( + group = "com.github.ajalt.clikt", + name = "clikt", + version = Versions.clikt + ) } object Plugins { @@ -258,9 +266,9 @@ object Plugins { version = Versions.rd ) - // https://github.com/johnrengelman/shadow + // https://github.com/GradleUp/shadow object Shadow : ProjectPlugin( - id = "com.github.johnrengelman.shadow", + id = "com.gradleup.shadow", version = Versions.shadow ) } diff --git a/buildSrc/src/main/kotlin/usvmpython/tasks/CPythonBuildTasks.kt b/buildSrc/src/main/kotlin/usvmpython/tasks/CPythonBuildTasks.kt index 3eaefc8b6c..5610cb5112 100644 --- a/buildSrc/src/main/kotlin/usvmpython/tasks/CPythonBuildTasks.kt +++ b/buildSrc/src/main/kotlin/usvmpython/tasks/CPythonBuildTasks.kt @@ -127,4 +127,4 @@ fun Project.registerCPythonDistClean(): TaskProvider { commandLine(windowsBuildScript.canonicalPath, "-t", "CleanAll") } } -} \ No newline at end of file +} diff --git a/buildSrc/src/main/kotlin/usvmpython/tasks/JNIHeaderTask.kt b/buildSrc/src/main/kotlin/usvmpython/tasks/JNIHeaderTask.kt index cf8559de73..847d463911 100644 --- a/buildSrc/src/main/kotlin/usvmpython/tasks/JNIHeaderTask.kt +++ b/buildSrc/src/main/kotlin/usvmpython/tasks/JNIHeaderTask.kt @@ -17,4 +17,4 @@ fun Project.generateJNIForCPythonAdapterTask() { } task.addClass(CPYTHON_ADAPTER_CLASS) task.run() -} \ No newline at end of file +} diff --git a/buildSrc/src/main/kotlin/usvmpython/tasks/PythonSamplesTasks.kt b/buildSrc/src/main/kotlin/usvmpython/tasks/PythonSamplesTasks.kt index f405d46cb0..a486687aec 100644 --- a/buildSrc/src/main/kotlin/usvmpython/tasks/PythonSamplesTasks.kt +++ b/buildSrc/src/main/kotlin/usvmpython/tasks/PythonSamplesTasks.kt @@ -51,4 +51,4 @@ fun Project.registerBuildSamplesTask(): TaskProvider { environment("PYTHONHOME" to cpythonBuildPath) mainClass.set(BUILD_SAMPLES_ENTRY_POINT) } -} \ No newline at end of file +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d64cd4917707c1f8861d8cb53dd15194d4248596..2c3521197d7c4586c843d1d3e9090525f1898cde 100644 GIT binary patch delta 34463 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 SXRcJ{65?fGI57uQ5&!`B?F@7P delta 34554 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a80b22ce5c..7cf748e743 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.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 1aa94a4269..f5feea6d6b 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/gradlew.bat b/gradlew.bat index 25da30dbde..9d21a21834 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## diff --git a/settings.gradle.kts b/settings.gradle.kts index 121c7fcbd5..7e98903912 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -8,6 +8,7 @@ include("usvm-jvm-instrumentation") include("usvm-sample-language") include("usvm-dataflow") include("usvm-jvm-dataflow") +include("usvm-dataflow-ts") include("usvm-python") include("usvm-python:cpythonadapter") @@ -21,6 +22,11 @@ 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 { diff --git a/usvm-dataflow-ts/.gitignore b/usvm-dataflow-ts/.gitignore new file mode 100644 index 0000000000..333c1e910a --- /dev/null +++ b/usvm-dataflow-ts/.gitignore @@ -0,0 +1 @@ +logs/ diff --git a/usvm-dataflow-ts/README.md b/usvm-dataflow-ts/README.md new file mode 100644 index 0000000000..59dd9cae7f --- /dev/null +++ b/usvm-dataflow-ts/README.md @@ -0,0 +1,167 @@ +# USVM Dataflow TS + +## Type Inference + +In order to run type inference on an arbitrary TypeScript project, you need the following: +1. IR dumped into JSON files: either from TS sources or from +binary ABC/HAP files. +2. USVM with type inference CLI: `usvm-dataflow-ts-all.jar` "fat" JAR. + +**NOTE:** the instructions below are given for Linux. If you are using Windows, you need to adjust the paths and commands accordingly, or use WSL. Overall, the process should be similar, and USVM should work on any platform that supports Java. + +### ArkTS IR + +- Below, we use the term "ArkIR" to refer to the representation of ArkTS inside ArkAnalyzer in a form of TypeScript classes and interfaces, such as `ArkMethod`, `ArkAssignStmt`, `ArkInstanceInvokeExpr`. + +- In USVM, we also have a similar model of representing ArkTS, but in the form of Java/Kotlin classes. In order to differentiate between the two models, we use the prefix "Ets" for the classes in USVM, such as `EtsMethod`, `EtsAssignStmt`, `EtsInstanceCallExpr`. + +### Setup ArkAnalyzer + +First of all, you need to clone the ArkAnalyzer repo. Here, we use the fork of the repo and the specific branch (named `neo/`) that is consistent with USVM internals. Note that this branch might change in the future. +```bash +cd ~ +git clone -b neo/2024-10-31 https://gitee.com/Lipenx/arkanalyzer arkanalyzer-usvm +cd arkanalyzer-usvm +``` + +Then, you need to install the dependencies and build the project. +```bash +npm install +npm run build +``` + +**Note:** after building the ArkAnalyzer project, the script for serializing ArkIR will be located at `out/save/serializeArkIR.js` and can be run with Node.js. + +**Note:** you can also use TS script directly using `npx ts-node src/save/serializeArkIR.ts` instead of building the whole project. + +### Serialize ArkIR to JSON + +Now, you can run the `serializeArkIR` script on your TS project in order to construct its ArkIR representation and dump it into JSON files, which can later be used by USVM. +```bash +node ~/arkanalyzer-usvm/out/src/save/serializeArkIR.js --help +``` +```text +Usage: serializeArkIR [options] + +Serialize ArkIR for TypeScript files or projects to JSON + +Arguments: + input Input file or directory + output Output file or directory + +Options: + -p, --project Flag to indicate the input is a project directory (default: false) + -t, --infer-types [times] Infer types in the ArkIR + -v, --verbose Verbose output (default: false) + -h, --help display help for command +``` + +If you have a single TS file `sample.ts`, just run the following command: +```bash +node .../serializeArkIR.js sample.ts sample.json +``` +The resulting `sample.json` file will contain the ArkIR in JSON format. + +If you have a TS project in the `project` directory, use `-p` flag: +```bash +node .../serializeArkIR.js -p project etsir +``` +The resulting `etsir` directory will contain the ArkIR in JSON format. The structure of the resulting directory (hierarchy of subfolders) will be the same as the structure of the input project, but all the files will be `*.ts.json`. + +_Note:_ We call the result "EtsIR" since it is a modified version of the ArkIR model suitable for serialization. When we load IR from JSONs in USVM (Java/Kotlin), the resulting data model (structure of classes) is very similar to ArkIR in ArkAnalyzer (TypeScript), but has some minor differences. The term "EtsIR" is used to distinguish between the two. + +If you have a TS project with multiple modules, run the serialization for each module separately: +```bash +node .../serializeArkIR.js -p project/entry etsir/entry +node .../serializeArkIR.js -p project/common etsir/common +node .../serializeArkIR.js -p project/feature etsir/feature +``` + +### Type Inference with USVM + +In order to run USVM type inference, you need to obtain `usvm-dataflow-ts-all.jar` "fat" JAR (download or build it yourself) and either use it directly or use a wrapper script `src/usvm/inferTypes.ts` in ArkAnalyzer repo. + +#### Build `usvm-type-inference` binary + +In order to build the USVM binary, you need to clone the USVM repo (and also its dependency `jacodb` in the _sibling directory_) and build the project using Gradle. +```bash +cd ~ +git clone -b lipen/usvm-type-inference https://github.com/UnitTestBot/jacodb +git clone -b lipen/type-inference https://github.com/UnitTestBot/usvm +cd usvm +./gradlew :usvm-dataflow-ts:installDist +``` +The last command will build the project and create the binary at `usvm-dataflow-ts/build/install/usvm-dataflow-ts/bin/usvm-type-inference` (on Windows, the corresponding "binary" is with `.bat` extension). + +#### Build "Fat" JAR + +Alternatively, you can build the "fat" JAR (also known as "Uber JAR" or "shadow JAR") that contains all the dependencies. +```bash +./gradlew :usvm-dataflow-ts:shadowJar +``` + +#### Run Type Inference + +You can run the type inference manually using USVM CLI: +```bash +usvm-dataflow-ts/build/install/usvm-dataflow-ts/bin/usvm-type-inference --help +# OR +java -jar usvm-dataflow-ts/build/libs/usvm-dataflow-ts-all.jar --help +``` +```text +Usage: infer-types [] + +Options: +* -i, --input= Input file or directory with IR (required) +* -o, --output= Output file with inferred types in JSON format (required) + -h, --help Show this message and exit +``` + +_Note:_ `-i` option can be supplied multiple times for multi-module projects. All input IR will be merged. + +For example, if you have the `project/entry` and `project/common` directories with the dumped ArkIR, you can run the following command: +```bash +java -jar usvm-dataflow-ts/build/libs/usvm-dataflow-ts-all.jar -i project/entry -i project/common -o inferred.json +``` + +### Type Inference with Wrapper Script + +You can also use the wrapper script `src/usvm/inferTypes.ts` from the ArkAnalyzer repo. This script will run the serialization of ArkIR and type inference with USVM in a single command. + +```bash +node ~/arkanalyzer-usvm/out/src/usvm/inferTypes.js --help +``` +```text +Usage: inferTypes [options] + +Arguments: + input input directory with ETS project + +Options: + -v, --verbose Verbose output (default: false) + -t, --aa-types Run type inference in ArkAnalyzer (default: false) + -s, --substitute Substitute inferred types (default: false) + -h, --help display help for command +``` + +For example: +```bash +node .../inferTypes.js myproject/entry +``` +```text +Building scene... +Serializing Scene to '/tmp/2f8aa8b34548b808167a8f6b30121dcc/etsir'... +... +USVM command: ~/usvm/usvm-dataflow-ts/build/install/usvm-dataflow-ts/bin/usvm-type-inference --input=/tmp/2f8aa8b34548b808167a8f6b30121dcc/etsir --output=/tmp/2f8aa8b34548b808167a8f6b30121dcc/inference-result --no-skip-anonymous +... +=== Inferred Types Statistics === +Total Classes: 10 +Total Methods: 305 +... +Deserialization successful. +... +Substituting inferred types... +... +Substituting type of local '$temp16' in method '@entry/model/Calculator.ts: _DEFAULT_ARK_CLASS.getFloatNum(unknown, unknown, unknown)' from unknown to number +... +``` diff --git a/usvm-dataflow-ts/build.gradle.kts b/usvm-dataflow-ts/build.gradle.kts new file mode 100644 index 0000000000..2da676a1e1 --- /dev/null +++ b/usvm-dataflow-ts/build.gradle.kts @@ -0,0 +1,132 @@ +import java.io.FileNotFoundException +import java.nio.file.Files +import java.nio.file.attribute.FileTime + +plugins { + id("usvm.kotlin-conventions") + kotlin("plugin.serialization") version Versions.kotlin + application + id(Plugins.Shadow) + `java-test-fixtures` +} + +dependencies { + api(project(":usvm-dataflow")) + implementation(project(":usvm-util")) + + api(Libs.jacodb_api_common) + api(Libs.jacodb_ets) + implementation(Libs.jacodb_taint_configuration) + implementation(Libs.kotlinx_collections) + implementation(Libs.kotlinx_serialization_json) + implementation(Libs.clikt) + + 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) +} + +tasks.withType { + maxHeapSize = "4G" +} + +val generateTestResources by tasks.registering { + group = "build" + description = "Generates JSON resources from TypeScript files using ArkAnalyzer." + doLast { + 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'") + defaultArkAnalyzerDir + }) + if (!arkAnalyzerDir.exists()) { + throw FileNotFoundException("ArkAnalyzer directory does not exist: '$arkAnalyzerDir'. Did you forget to set the '$envVarName' environment variable?") + } + + val scriptSubPath = "src/save/serializeArkIR" + val script = arkAnalyzerDir.resolve("out").resolve("$scriptSubPath.js") + if (!script.exists()) { + throw FileNotFoundException("Script file not found: '$script'. Did you forget to execute 'npm run build' in the arkanalyzer project?") + } + + 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)}'...") + + 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) + } + + 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)}'") + } + } + } +} + +// tasks.test { +// dependsOn(generateTestResources) +// } + +application { + mainClass = "org.usvm.dataflow.ts.infer.cli.InferTypesKt" + applicationDefaultJvmArgs = listOf("-Dfile.encoding=UTF-8", "-Dsun.stdout.encoding=UTF-8") +} + +tasks.startScripts { + applicationName = "usvm-type-inference" +} + +tasks.shadowJar { + minimize { + // Note: keep 'mordant' dependency inside shadowJar, or else the following error occurs: + // ``` + // Exception in thread "main" java.util.ServiceConfigurationError: + // com.github.ajalt.mordant.terminal.TerminalInterfaceProvider: + // Provider com.github.ajalt.mordant.terminal.terminalinterface.jna.TerminalInterfaceProviderJna not found + // ``` + exclude(dependency("com.github.ajalt.mordant:.*:.*")) + } +} diff --git a/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/graph/EtsApplicationGraph.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/graph/EtsApplicationGraph.kt new file mode 100644 index 0000000000..1b11b5e2e9 --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/graph/EtsApplicationGraph.kt @@ -0,0 +1,282 @@ +/* + * 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.graph + +import mu.KotlinLogging +import org.jacodb.ets.base.CONSTRUCTOR_NAME +import org.jacodb.ets.base.EtsAssignStmt +import org.jacodb.ets.base.EtsClassType +import org.jacodb.ets.base.EtsNewExpr +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 +import org.jacodb.ets.utils.onSome +import org.usvm.dataflow.graph.ApplicationGraph + +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 { + + override fun predecessors(node: EtsStmt): Sequence { + val graph = node.method.flowGraph() + val predecessors = graph.predecessors(node) + return predecessors.asSequence() + } + + override fun successors(node: EtsStmt): Sequence { + val graph = node.method.flowGraph() + val successors = graph.successors(node) + 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 + + // 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()) { + val prevStmt = predecessors(node).singleOrNull() + if (prevStmt == null) { + // Constructor call is the first statement in the method. + // We can't resolve it without the class signature. + return emptySequence() + } + + if (prevStmt is EtsAssignStmt && prevStmt.rhv is EtsNewExpr) { + val cls = prevStmt.rhv.type + if (cls !is EtsClassType) { + return emptySequence() + } + + val sig = cls.signature + if (sig.isIdeal()) { + lookupClassWithIdealSignature(sig).onSome { c -> + return sequenceOf(c.ctor) + } + } else { + val resolved = cp.projectAndSdkClasses + .asSequence() + .filter { compareClassSignatures(it.signature, sig) != ComparisonResult.NotEqual } + .singleOrNull() + if (resolved != null) { + return sequenceOf(resolved.ctor) + } + } + } + + // 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() + .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 { + // Note: currently, nobody uses `callers`, so if is safe to disable it for now. + // Note: comparing methods by signature may be incorrect, and comparing only by name fails for constructors. + TODO("disabled for now, need re-design") + // return cp.classes.asSequence() + // .flatMap { it.methods } + // .flatMap { it.cfg.instructions } + // .filterIsInstance() + // .filter { it.expr.method == method.signature } + } + + override fun entryPoints(method: EtsMethod): Sequence { + return method.flowGraph().entries.asSequence() + } + + override fun exitPoints(method: EtsMethod): Sequence { + return method.flowGraph().exits.asSequence() + } + + override fun methodOf(node: EtsStmt): EtsMethod { + return node.location.method + } +} diff --git a/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/ifds/UnitResolver.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/ifds/UnitResolver.kt new file mode 100644 index 0000000000..ba9bba9971 --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/ifds/UnitResolver.kt @@ -0,0 +1,62 @@ +/* + * 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.ifds + +import org.jacodb.ets.model.EtsClassSignature +import org.jacodb.ets.model.EtsMethod +import org.jacodb.ets.model.EtsMethodSignature +import org.usvm.dataflow.ifds.SingletonUnit +import org.usvm.dataflow.ifds.UnitResolver +import org.usvm.dataflow.ifds.UnitType + +data class MethodUnit(val method: EtsMethodSignature) : UnitType { + override fun toString(): String { + return "MethodUnit(${method.name})" + } +} + +data class ClassUnit(val clazz: EtsClassSignature) : UnitType { + override fun toString(): String { + return "ClassUnit(${clazz.name})" + } +} + +// TODO: PackageUnit +// data class PackageUnit(val packageName: String) : UnitType { +// override fun toString(): String { +// return "PackageUnit($packageName)" +// } +// } + +fun interface EtsUnitResolver : UnitResolver + +val MethodUnitResolver = EtsUnitResolver { method -> + MethodUnit(method.signature) +} + +val ClassUnitResolver = EtsUnitResolver { method -> + ClassUnit(method.signature.enclosingClass) +} + +// TODO: PackageUnitResolver +// val PackageUnitResolver = EtsUnitResolver { method -> +// PackageUnit(method.enclosingClass.packageName) +// } + +val SingletonUnitResolver = EtsUnitResolver { + SingletonUnit +} diff --git a/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/AccessPath.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/AccessPath.kt new file mode 100644 index 0000000000..6544a6f090 --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/AccessPath.kt @@ -0,0 +1,98 @@ +package org.usvm.dataflow.ts.infer + +import org.jacodb.ets.base.EtsArrayAccess +import org.jacodb.ets.base.EtsCastExpr +import org.jacodb.ets.base.EtsConstant +import org.jacodb.ets.base.EtsEntity +import org.jacodb.ets.base.EtsInstanceFieldRef +import org.jacodb.ets.base.EtsLocal +import org.jacodb.ets.base.EtsParameterRef +import org.jacodb.ets.base.EtsStaticFieldRef +import org.jacodb.ets.base.EtsThis +import org.jacodb.ets.base.EtsValue + +data class AccessPath(val base: AccessPathBase, val accesses: List) { + operator fun plus(accessor: Accessor) = AccessPath(base, accesses + accessor) + operator fun plus(accessors: List) = AccessPath(base, accesses + accessors) + + override fun toString(): String { + return base.toString() + accesses.joinToString("") { it.toSuffix() } + } +} + +fun List.startsWith(other: List): Boolean { + return this.take(other.size) == other +} + +fun AccessPath?.startsWith(other: AccessPath?): Boolean { + if (this == null || other == null) { + return false + } + if (this.base != other.base) { + return false + } + return this.accesses.startsWith(other.accesses) +} + +fun List.hasDuplicateFields(limit: Int = 2): Boolean { + val counts = this.groupingBy { it }.eachCount() + return counts.any { it.value >= limit } +} + +sealed interface AccessPathBase { + object This : AccessPathBase { + override fun toString(): String = "" + } + + object Static : AccessPathBase { + override fun toString(): String = "" + } + + data class Arg(val index: Int) : AccessPathBase { + override fun toString(): String = "arg($index)" + } + + data class Local(val name: String) : AccessPathBase { + override fun toString(): String = "local($name)" + } + + data class Const(val constant: EtsConstant) : AccessPathBase { + override fun toString(): String = "const($constant)" + } +} + +fun EtsValue.toBase(): AccessPathBase = when (this) { + is EtsConstant -> AccessPathBase.Const(this) + is EtsLocal -> if (name == "this") AccessPathBase.This else AccessPathBase.Local(name) + is EtsThis -> AccessPathBase.This + is EtsParameterRef -> AccessPathBase.Arg(index) + else -> error("$this is not access path base") +} + +fun EtsEntity.toPathOrNull(): AccessPath? = when (this) { + is EtsConstant -> AccessPath(toBase(), emptyList()) + + is EtsLocal -> AccessPath(toBase(), emptyList()) + + is EtsThis -> AccessPath(toBase(), emptyList()) + + is EtsParameterRef -> AccessPath(toBase(), emptyList()) + + is EtsArrayAccess -> array.toPathOrNull()?.let { + it + ElementAccessor + } + + is EtsInstanceFieldRef -> instance.toPathOrNull()?.let { + it + FieldAccessor(field.name) + } + + is EtsStaticFieldRef -> AccessPath(AccessPathBase.Static, listOf(FieldAccessor(field.name))) + + is EtsCastExpr -> arg.toPathOrNull() + + else -> null +} + +fun EtsEntity.toPath(): AccessPath { + return toPathOrNull() ?: error("Unable to build access path for value $this") +} diff --git a/usvm-jvm-dataflow/src/samples/java/NullAssumptionAnalysisExample.java b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/Accessors.kt similarity index 63% rename from usvm-jvm-dataflow/src/samples/java/NullAssumptionAnalysisExample.java rename to usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/Accessors.kt index abdcec3540..7473b06672 100644 --- a/usvm-jvm-dataflow/src/samples/java/NullAssumptionAnalysisExample.java +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/Accessors.kt @@ -14,16 +14,20 @@ * limitations under the License. */ -public class NullAssumptionAnalysisExample { - public void test1(String a) { - System.out.println("Hello from test1"); - System.out.println(a.length()); - } +package org.usvm.dataflow.ts.infer - public void test2(Object a) { - System.out.println("Hello from test2"); - System.out.println(a.hashCode()); - String x = (String) a; - System.out.println(x.length()); - } +sealed interface Accessor { + fun toSuffix(): String +} + +data class FieldAccessor( + val name: String, +) : Accessor { + override fun toSuffix(): String = ".$name" + override fun toString(): String = name +} + +object ElementAccessor : Accessor { + override fun toSuffix(): String = "[*]" + override fun toString(): String = "*" } diff --git a/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/Alias.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/Alias.kt new file mode 100644 index 0000000000..1669320d9c --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/Alias.kt @@ -0,0 +1,324 @@ +/* + * 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 + +import kotlinx.collections.immutable.PersistentMap +import kotlinx.collections.immutable.mutate +import kotlinx.collections.immutable.persistentHashMapOf +import org.jacodb.ets.base.EtsAssignStmt +import org.jacodb.ets.base.EtsCallExpr +import org.jacodb.ets.base.EtsCastExpr +import org.jacodb.ets.base.EtsConstant +import org.jacodb.ets.base.EtsLocal +import org.jacodb.ets.base.EtsNewArrayExpr +import org.jacodb.ets.base.EtsNewExpr +import org.jacodb.ets.base.EtsParameterRef +import org.jacodb.ets.base.EtsRef +import org.jacodb.ets.base.EtsStmt +import org.jacodb.ets.model.EtsMethod + +sealed interface Allocation { + class New : Allocation + data class Arg(val index: Int) : Allocation + class CallResult : Allocation + class Imm : Allocation +} + +class AliasInfo( + // B: Base -> Object + val baseToAlloc: PersistentMap, + // F: Object x Field -> Object + val allocToFields: PersistentMap>, +) { + // A: Base x Field* -> Object + fun find(path: AccessPath): Allocation? { + val b = baseToAlloc[path.base] ?: return null + if (path.accesses.isEmpty()) return b + // TODO: handle multiple accesses + check(path.accesses.size == 1) + return when (val a = path.accesses.single()) { + is FieldAccessor -> { + val f = allocToFields[b] ?: return null + f[a.name] + } + + is ElementAccessor -> { + null + } + } + } + + fun merge(other: AliasInfo): AliasInfo { + // Intersect B: + val newB = persistentHashMapOf().mutate { newB -> + for ((base, obj) in this.baseToAlloc) { + if (other.baseToAlloc[base] == obj) { + newB[base] = obj + } + } + } + + // Intersect F: + val newF = persistentHashMapOf>().mutate { newF -> + for ((obj, fields) in this.allocToFields) { + newF[obj] = persistentHashMapOf().mutate { newFields -> + val otherFields = other.allocToFields[obj] + if (otherFields != null) { + for ((field, alloc) in fields) { + if (otherFields[field] == alloc) { + newFields[field] = alloc + } + } + } + } + } + } + + return AliasInfo(newB, newF) + } + + /** + * Returns the set of access paths that must alias with the given path (excluding the path itself). + */ + fun getAliases(path: AccessPath): Set { + val obj = find(path) ?: return emptySet() + return getAliases(obj) - path + } + + private fun getAliases(obj: Allocation): Set { + val paths = mutableSetOf() + + val allocToFields = hashMapOf>>() + for ((obj1, fields) in this.allocToFields) { + for ((field, obj2) in fields) { + allocToFields.computeIfAbsent(obj2) { hashMapOf() } + allocToFields.computeIfAbsent(obj2) { hashMapOf() } + .computeIfAbsent(field) { mutableListOf() } + .add(obj1) + } + } + + val allocToBases = hashMapOf>() + for ((base, alloc) in baseToAlloc) { + allocToBases.computeIfAbsent(alloc) { mutableListOf() } + .add(base) + } + + val queue = ArrayDeque>>(listOf(obj to emptyList())) + while (queue.isNotEmpty()) { + val (cur, path) = queue.removeFirst() + // TODO: eliminate loops as in computeAliases via DFS with PATH/STACK + // TODO: think about loop-edges + if (path.size > MAX_PATH_SIZE) continue + if (cur in allocToBases) { + for (base in allocToBases.getValue(cur)) { + paths.add(AccessPath(base, path.reversed())) + } + } + if (cur in allocToFields) { + for ((field, objs) in allocToFields.getValue(cur)) { + for (alloc in objs) { + queue.add(alloc to path + FieldAccessor(field)) + } + } + } + } + + return paths + } + + companion object { + // Maximum number of fields in an access path. Used to avoid infinite loops. + private const val MAX_PATH_SIZE = 10 + } +} + +fun computeAliases(method: EtsMethod): Map> { + val preAliases = mutableMapOf() + val postAliases = mutableMapOf() + + val visited: MutableSet = hashSetOf() + val order: MutableList = mutableListOf() + val preds: MutableMap> = hashMapOf() + + fun postOrderDfs(node: EtsStmt) { + if (visited.add(node)) { + for (next in method.cfg.successors(node)) { + if (next !in visited) { + preds.computeIfAbsent(next) { mutableListOf() } += node + } + postOrderDfs(next) + } + order += node + } + } + + val root = method.cfg.stmts[0] + postOrderDfs(root) + order.reverse() + + fun computePostAliases(stmt: EtsStmt): AliasInfo { + if (stmt in postAliases) return postAliases.getValue(stmt) + + val pre = preAliases.getValue(stmt) + var newF = pre.allocToFields + val newB = pre.baseToAlloc.mutate { newB -> + newF = newF.mutate { newF -> + if (stmt is EtsAssignStmt) { + val lhv = stmt.lhv + val rhv = stmt.rhv + + if (rhv is EtsLocal || rhv is EtsRef || (rhv is EtsCastExpr && rhv.arg is EtsRef)) { + val lhs = lhv.toPath() + val rhs = rhv.toPath() + + if (rhv is EtsParameterRef) { + check(lhs.accesses.isEmpty()) + newB[lhs.base] = Allocation.Arg(rhv.index) + } else { + if (lhs.accesses.isEmpty() && rhs.accesses.isEmpty()) { + // x := y + newB[lhs.base] = newB.computeIfAbsent(rhs.base) { Allocation.Imm() } + } else if (lhs.accesses.isEmpty()) { + // x := y.f OR x := y[i] + when (val a = rhs.accesses.single()) { + is FieldAccessor -> { + // x := y.f + val b: Allocation = newB.computeIfAbsent(rhs.base) { Allocation.Imm() } + newF[b] = newF.getOrElse(b) { persistentHashMapOf() }.mutate { f -> + newB[lhs.base] = f.computeIfAbsent(a.name) { Allocation.Imm() } + } + } + + is ElementAccessor -> { + // x := y[i] + newB.remove(lhs.base) + } + } + } else if (rhs.accesses.isEmpty()) { + // x.f := y OR x[i] := y + when (val a = lhs.accesses.single()) { + is FieldAccessor -> { + // x.f := y + val b: Allocation = newB.computeIfAbsent(rhs.base) { Allocation.Imm() } + newF[b] = newF.getOrElse(b) { persistentHashMapOf() }.mutate { f -> + f[a.name] = newB.computeIfAbsent(rhs.base) { Allocation.Imm() } + } + } + + is ElementAccessor -> { + // x[i] := y + // do nothing + } + } + } else { + error("Incorrect 3AC: $stmt") + } + } + } + + if (rhv is EtsConstant || (rhv is EtsCastExpr && rhv.arg is EtsConstant)) { + val lhs = lhv.toPath() + if (lhs.accesses.isEmpty()) { + // x := const + newB.remove(lhs.base) + } else { + when (val a = lhs.accesses.single()) { + is FieldAccessor -> { + // x.f := const + val b = newB.computeIfAbsent(lhs.base) { Allocation.Imm() } + newF[b] = newF.getOrElse(b) { persistentHashMapOf() }.mutate { f -> + f.remove(a.name) + } + } + + is ElementAccessor -> { + // x[i] := const + // do nothing + } + } + } + } + + if (rhv is EtsNewExpr || rhv is EtsNewArrayExpr) { + val lhs = lhv.toPath() + if (lhs.accesses.isEmpty()) { + // x := new() + newB.computeIfAbsent(lhs.base) { Allocation.New() } + } else { + when (val a = lhs.accesses.single()) { + is FieldAccessor -> { + // x.f := new() + val b = newB.computeIfAbsent(lhs.base) { Allocation.Imm() } + newF[b] = newF.getOrElse(b) { persistentHashMapOf() }.mutate { f -> + f[a.name] = Allocation.New() + } + } + + is ElementAccessor -> { + // x[i] := new() + // do nothing + } + } + } + } + + if (rhv is EtsCastExpr) { + check(rhv.arg !is EtsCallExpr) + check(rhv.arg is EtsLocal || rhv.arg is EtsRef || rhv.arg is EtsConstant) + } + + if (rhv is EtsCallExpr) { + val lhs = lhv.toPath() + check(lhs.accesses.isEmpty()) + newB[lhs.base] = Allocation.CallResult() + } + } + } + } + + val newInfo = AliasInfo(newB, newF) + postAliases[stmt] = newInfo + return newInfo + } + + fun computePreAliases(stmt: EtsStmt): AliasInfo { + if (stmt in preAliases) return preAliases.getValue(stmt) + + val merged = preds[stmt] + ?.map { postAliases.getValue(it) } + ?.reduceOrNull { a, b -> a.merge(b) } + ?: AliasInfo(persistentHashMapOf(), persistentHashMapOf()) + + preAliases[stmt] = merged + return merged + } + + for (stmt in order) { + computePreAliases(stmt) + computePostAliases(stmt) + } + + for (stmt in method.cfg.stmts) { + check(stmt in preAliases) + check(stmt in postAliases) + } + + return method.cfg.stmts.associateWithTo(HashMap()) { stmt -> + Pair(preAliases.getValue(stmt), postAliases.getValue(stmt)) + } +} diff --git a/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/AnalyzerEvent.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/AnalyzerEvent.kt new file mode 100644 index 0000000000..c44ebbb026 --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/AnalyzerEvent.kt @@ -0,0 +1,25 @@ +package org.usvm.dataflow.ts.infer + +import org.jacodb.ets.base.EtsStmt +import org.jacodb.ets.model.EtsMethod +import org.usvm.dataflow.ifds.Vertex + +sealed interface AnalyzerEvent + +data class ForwardSummaryAnalyzerEvent( + val method: EtsMethod, + val initialVertex: Vertex, + val exitVertex: Vertex, +) : AnalyzerEvent { + val initialFact get() = initialVertex.fact + val exitFact get() = exitVertex.fact +} + +data class BackwardSummaryAnalyzerEvent( + val method: EtsMethod, + val initialVertex: Vertex, + val exitVertex: Vertex, +) : AnalyzerEvent { + val initialFact get() = initialVertex.fact + val exitFact get() = exitVertex.fact +} diff --git a/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/ApplicationGraph.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/ApplicationGraph.kt new file mode 100644 index 0000000000..4493764869 --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/ApplicationGraph.kt @@ -0,0 +1,78 @@ +/* + * 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 + +import org.jacodb.ets.base.EtsInstLocation +import org.jacodb.ets.base.EtsNopStmt +import org.jacodb.ets.base.EtsStmt +import org.jacodb.ets.model.EtsMethod +import org.jacodb.ets.model.EtsScene +import org.usvm.dataflow.ts.graph.EtsApplicationGraph +import org.usvm.dataflow.ts.graph.EtsApplicationGraphImpl + +fun createApplicationGraph(cp: EtsScene): EtsApplicationGraph { + val base = EtsApplicationGraphImpl(cp) + val explicit = EtsApplicationGraphWithExplicitEntryPoint(base) + return explicit +} + +class EtsApplicationGraphWithExplicitEntryPoint( + private val graph: EtsApplicationGraphImpl, +) : EtsApplicationGraph { + + override val cp: EtsScene + get() = graph.cp + + override fun methodOf(node: EtsStmt): EtsMethod = node.location.method + + override fun exitPoints(method: EtsMethod): Sequence = graph.exitPoints(method) + + private fun methodEntryPoint(method: EtsMethod) = + EtsNopStmt(EtsInstLocation(method, index = -1)) + + override fun entryPoints(method: EtsMethod): Sequence = sequenceOf(methodEntryPoint(method)) + + override fun callers(method: EtsMethod): Sequence = graph.callers(method) + + override fun callees(node: EtsStmt): Sequence = graph.callees(node) + + override fun successors(node: EtsStmt): Sequence { + val method = methodOf(node) + val methodEntry = methodEntryPoint(method) + + if (node == methodEntry) { + return graph.entryPoints(method) + } + + return graph.successors(node) + } + + override fun predecessors(node: EtsStmt): Sequence { + val method = methodOf(node) + val methodEntry = methodEntryPoint(method) + + if (node == methodEntry) { + return emptySequence() + } + + if (node in graph.entryPoints(method)) { + return sequenceOf(methodEntry) + } + + return graph.predecessors(node) + } +} diff --git a/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/BackwardAnalyzer.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/BackwardAnalyzer.kt new file mode 100644 index 0000000000..114b1ca269 --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/BackwardAnalyzer.kt @@ -0,0 +1,44 @@ +package org.usvm.dataflow.ts.infer + +import org.jacodb.ets.base.EtsStmt +import org.jacodb.ets.base.EtsType +import org.jacodb.ets.model.EtsMethod +import org.jacodb.impl.cfg.graphs.GraphDominators +import org.usvm.dataflow.graph.ApplicationGraph +import org.usvm.dataflow.ifds.Analyzer +import org.usvm.dataflow.ifds.Edge +import org.usvm.dataflow.ifds.Vertex + +class BackwardAnalyzer( + val graph: ApplicationGraph, + savedTypes: MutableMap>, + dominators: (EtsMethod) -> GraphDominators, +) : Analyzer { + + override val flowFunctions = BackwardFlowFunctions(graph, dominators, savedTypes) + + override fun handleCrossUnitCall( + caller: Vertex, + callee: Vertex, + ): List { + error("No cross unit calls") + } + + override fun handleNewEdge(edge: Edge): List { + val (startVertex, currentVertex) = edge + val (current, currentFact) = currentVertex + + val method = graph.methodOf(current) + val currentIsExit = current in graph.exitPoints(method) + + if (!currentIsExit) return emptyList() + + return listOf( + BackwardSummaryAnalyzerEvent( + method = method, + initialVertex = startVertex, + exitVertex = currentVertex, + ) + ) + } +} diff --git a/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/BackwardFlowFunctions.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/BackwardFlowFunctions.kt new file mode 100644 index 0000000000..c8867cb1e3 --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/BackwardFlowFunctions.kt @@ -0,0 +1,578 @@ +package org.usvm.dataflow.ts.infer + +import mu.KotlinLogging +import org.jacodb.ets.base.EtsAssignStmt +import org.jacodb.ets.base.EtsCastExpr +import org.jacodb.ets.base.EtsEntity +import org.jacodb.ets.base.EtsEqExpr +import org.jacodb.ets.base.EtsIfStmt +import org.jacodb.ets.base.EtsInExpr +import org.jacodb.ets.base.EtsInstanceCallExpr +import org.jacodb.ets.base.EtsLValue +import org.jacodb.ets.base.EtsNewExpr +import org.jacodb.ets.base.EtsNumberConstant +import org.jacodb.ets.base.EtsRef +import org.jacodb.ets.base.EtsReturnStmt +import org.jacodb.ets.base.EtsStmt +import org.jacodb.ets.base.EtsStringConstant +import org.jacodb.ets.base.EtsThrowStmt +import org.jacodb.ets.base.EtsType +import org.jacodb.ets.base.EtsValue +import org.jacodb.ets.model.EtsMethod +import org.jacodb.ets.utils.callExpr +import org.jacodb.impl.cfg.graphs.GraphDominators +import org.usvm.dataflow.graph.ApplicationGraph +import org.usvm.dataflow.ifds.FlowFunction +import org.usvm.dataflow.ifds.FlowFunctions +import org.usvm.dataflow.ts.infer.BackwardTypeDomainFact.TypedVariable +import org.usvm.dataflow.ts.infer.BackwardTypeDomainFact.Zero +import org.usvm.util.Maybe + +private val logger = KotlinLogging.logger {} + +class BackwardFlowFunctions( + val graph: ApplicationGraph, + val dominators: (EtsMethod) -> GraphDominators, + val savedTypes: MutableMap>, +) : FlowFunctions { + + // private val aliasesCache: MutableMap>> = hashMapOf() + // + // private fun getAliases(method: EtsMethod): Map> { + // return aliasesCache.computeIfAbsent(method) { computeAliases(method) } + // } + + override fun obtainPossibleStartFacts(method: EtsMethod) = listOf(Zero) + + override fun obtainSequentFlowFunction( + current: EtsStmt, + next: EtsStmt, + ): FlowFunction = FlowFunction { fact -> + if (current is EtsAssignStmt) { + val lhvPath = current.lhv.toPathOrNull() + val rhvPath = current.rhv.toPathOrNull() + if (lhvPath != null && rhvPath != null && lhvPath == rhvPath) { + return@FlowFunction listOf(fact) + } + } + when (fact) { + Zero -> sequentZero(current) + is TypedVariable -> sequent(current, fact).myFilter() + } + } + + private fun TypedVariable.withTypeGuards(current: EtsStmt): TypedVariable { + val dominators = dominators(current.method).dominators(current).asReversed() + + var result = this + + for (stmt in dominators.filterIsInstance()) { + val (guardedVariable, typeGuard) = resolveTypeGuard(stmt) ?: continue + + if (guardedVariable != result.variable) continue + + val branches = graph.predecessors(stmt).toList() // graph is reversed + check(branches.size == 2) { "Unexpected IF branches: $branches" } + + val (falseBranch, trueBranch) = branches + + val isTrueStatement = current.isReachableFrom(trueBranch) + val isFalseStatement = current.isReachableFrom(falseBranch) + + if (isTrueStatement && !isFalseStatement) { + val type = result.type.withGuard(typeGuard, guardNegated = false) + result = TypedVariable(result.variable, type) + } + + if (!isTrueStatement && isFalseStatement) { + val type = result.type.withGuard(typeGuard, guardNegated = true) + result = TypedVariable(result.variable, type) + } + } + + return result + } + + private fun resolveTypeGuard(branch: EtsIfStmt): Pair? { + val condition = branch.condition as? EtsEqExpr ?: return null + + if (condition.right == EtsNumberConstant(0.0)) { + return resolveTypeGuard(condition.left, branch) + } + + return null + } + + private fun resolveTypeGuard( + value: EtsEntity, + stmt: EtsStmt, + ): Pair? { + val valueAssignment = findAssignment(value, stmt) ?: return null + + return when (val rhv = valueAssignment.rhv) { + is EtsRef, is EtsLValue -> { + resolveTypeGuard(rhv, valueAssignment) + } + + is EtsInExpr -> { + resolveTypeGuardFromIn(rhv.left, rhv.right) + } + + else -> null + } + } + + private fun findAssignment(value: EtsEntity, stmt: EtsStmt): EtsAssignStmt? { + val cache = hashMapOf>() + findAssignment(value, stmt, cache) + val maybeValue = cache.getValue(stmt) + + return if (maybeValue.isNone) null else maybeValue.getOrThrow() + } + + private fun findAssignment( + value: EtsEntity, + stmt: EtsStmt, + cache: MutableMap>, + ) { + if (stmt in cache) return + + if (stmt is EtsAssignStmt && stmt.lhv == value) { + cache[stmt] = Maybe.some(stmt) + return + } + + // val predecessors = graph.successors(stmt) // graph is reversed + // val predecessors = dominators(stmt.method).dominators(stmt) - stmt + val predecessors = dominators(stmt.method).dominators(stmt).toSet().intersect(graph.successors(stmt).toSet()) + predecessors.forEach { findAssignment(value, it, cache) } + + val predecessorValues = predecessors.map { cache.getValue(it) } + if (predecessorValues.any { it.isNone }) { + cache[stmt] = Maybe.none() + return + } + + val values = predecessorValues.map { it.getOrThrow() }.toHashSet() + if (values.size == 1) { + cache[stmt] = Maybe.some(values.single()) + return + } + + cache[stmt] = Maybe.none() + } + + private fun resolveTypeGuardFromIn( + left: EtsEntity, + right: EtsEntity, + ): Pair? { + if (left !is EtsStringConstant) return null + + check(right is EtsValue) + val base = right.toBase() + val type = EtsTypeFact.ObjectEtsTypeFact( + cls = null, + properties = mapOf(left.value to EtsTypeFact.UnknownEtsTypeFact) + ) + return base to type + } + + private fun EtsStmt.isReachableFrom(stmt: EtsStmt): Boolean { + val visited = hashSetOf() + val queue = mutableListOf(stmt) + + while (queue.isNotEmpty()) { + val s = queue.removeLast() + if (this == s) return true + + if (!visited.add(s)) continue + + val successors = graph.predecessors(s) // graph is reversed + queue.addAll(successors) + } + + return false + } + + private fun sequentZero(current: EtsStmt): List { + val result = mutableListOf(Zero) + + // Case `return x` + // ∅ |= x:unknown + if (current is EtsReturnStmt) { + val variable = current.returnValue?.toBase() + if (variable != null) { + result += TypedVariable(variable, EtsTypeFact.UnknownEtsTypeFact) + } + } + + if (current is EtsAssignStmt) { + val rhv = when (val r = current.rhv) { + is EtsRef -> r.toPath() // This, FieldRef, ArrayAccess + is EtsLValue -> r.toPath() // Local + else -> { + // logger.info { "TODO backward assign zero: $current" } + null + } + } + + // When RHS is not const-like, handle possible new facts for RHV: + if (rhv != null) { + val y = rhv.base + + if (rhv.accesses.isEmpty()) { + // Case `x... := y` + // ∅ |= y:unknown + result += TypedVariable(y, EtsTypeFact.UnknownEtsTypeFact) + } else { + // Case `x := y.f` OR `x := y[i]` + + check(rhv.accesses.size == 1) + when (val accessor = rhv.accesses.single()) { + // Case `x := y.f` + // ∅ |= y:{f:unknown} + is FieldAccessor -> { + val type = EtsTypeFact.ObjectEtsTypeFact( + cls = null, + properties = mapOf(accessor.name to EtsTypeFact.UnknownEtsTypeFact) + ) + result += TypedVariable(y, type).withTypeGuards(current) + } + + // Case `x := y[i]` + // ∅ |= y:Array + is ElementAccessor -> { + // Note: ElementAccessor guarantees that `y` is an array, + // since `y[i]` for property access (i.e. access property + // with name "i") is represented via FieldAccessor. + val type = EtsTypeFact.ArrayEtsTypeFact( + elementType = EtsTypeFact.UnknownEtsTypeFact + ) + result += TypedVariable(y, type).withTypeGuards(current) + } + } + } + } + + val lhv = when (val r = current.lhv) { + is EtsRef -> r.toPath() // This, FieldRef, ArrayAccess + is EtsLValue -> r.toPath() // Local + else -> { + logger.info { "TODO backward assign zero: $current" } + error("Unexpected LHV in assignment: $current") + } + } + + // Handle new possible facts for LHS: + if (lhv.accesses.isNotEmpty()) { + // Case `x.f := y` OR `x[i] := y` + val x = lhv.base + + when (val a = lhv.accesses.single()) { + // Case `x.f := y` + // ∅ |= x:{f:unknown} + is FieldAccessor -> { + val type = EtsTypeFact.ObjectEtsTypeFact( + cls = null, + properties = mapOf(a.name to EtsTypeFact.UnknownEtsTypeFact) + ) + result += TypedVariable(x, type).withTypeGuards(current) + } + + // Case `x[i] := y` + // ∅ |= x:Array + is ElementAccessor -> { + // Note: ElementAccessor guarantees that `y` is an array, + // since `y[i]` for property access (i.e. access property + // with name "i") is represented via FieldAccessor. + val type = EtsTypeFact.ArrayEtsTypeFact( + elementType = EtsTypeFact.UnknownEtsTypeFact + ) + result += TypedVariable(x, type) + } + } + } + } + + return result + } + + private fun sequent( + current: EtsStmt, + fact: TypedVariable, + ): List { + if (current !is EtsAssignStmt) { + return listOf(fact) + } + + val lhv = current.lhv.toPath() + + val rhv = when (val r = current.rhv) { + is EtsRef -> r.toPath() // This, FieldRef, ArrayAccess + is EtsLValue -> r.toPath() // Local + is EtsCastExpr -> r.toPath() // Cast + is EtsNewExpr -> { + // TODO: what about `x.f := new T()` ? + // `x := new T()` with fact `x:U` => `saved[T] += U` + if (fact.variable == lhv.base) { + savedTypes.getOrPut(r.type) { mutableListOf() }.add(fact.type) + } + return listOf(fact) + } + + else -> { + // logger.info { "TODO backward assign: $current" } + return listOf(fact) + } + } + + // Pass-through completely unrelated facts: + if (fact.variable != lhv.base) return listOf(fact) + + // val (preAliases, _) = getAliases(current.method)[current]!! + + if (lhv.accesses.isEmpty() && rhv.accesses.isEmpty()) { + // Case `x := y` + + // x:T |= x:T (keep) + y:T + val y = rhv.base + val newFact = TypedVariable(y, fact.type).withTypeGuards(current) + return listOf(fact, newFact) + + } else if (lhv.accesses.isEmpty()) { + // Case `x := y.f` OR `x := y[i]` + + check(rhv.accesses.size == 1) + when (val a = rhv.accesses.single()) { + // Case `x := y.f` + is FieldAccessor -> { + // // Drop facts containing duplicate fields + // if (fact.type is EtsTypeFact.ObjectEtsTypeFact && a.name in fact.type.properties) { + // // can just drop? + // return listOf(fact) + // } + + // x:T |= x:T (keep) + y:{f:T} + aliases + val result = mutableListOf(fact) + val y = rhv.base + val type = EtsTypeFact.ObjectEtsTypeFact( + cls = null, + properties = mapOf(a.name to fact.type) + ) + result += TypedVariable(y, type).withTypeGuards(current) + // aliases: +|= z:{f:T} + // for (z in preAliases.getAliases(AccessPath(y, emptyList()))) { + // val type2 = unrollAccessorsToTypeFact(z.accesses + a, fact.type) + // result += TypedVariable(z.base, type2).withTypeGuards(current) + // } + return result + } + + // Case `x := y[i]` + is ElementAccessor -> { + // x:T |= x:T (keep) + y:Array + val y = rhv.base + val type = EtsTypeFact.ArrayEtsTypeFact(elementType = fact.type) + val newFact = TypedVariable(y, type).withTypeGuards(current) + return listOf(fact, newFact) + } + } + + } else if (rhv.accesses.isEmpty()) { + // Case `x.f := y` OR `x[i] := y` + + check(lhv.accesses.size == 1) + when (val a = lhv.accesses.single()) { + // Case `x.f := y` + is FieldAccessor -> { + if (fact.type is EtsTypeFact.UnionEtsTypeFact) { + TODO("Support union type for x.f := y in BW-sequent") + } + + if (fact.type is EtsTypeFact.IntersectionEtsTypeFact) { + TODO("Support intersection type for x.f := y in BW-sequent") + } + + // x:primitive |= x:primitive (pass) + if (fact.type !is EtsTypeFact.ObjectEtsTypeFact) { + return listOf(fact) + } + + // x:{no f} |= only keep x:{..} + + // x:{f:T} |= x:{f:T} (keep) + y:T + val (typeWithoutProperty, removedPropertyType) = fact.type.removePropertyType(a.name) + // val updatedFact = TypedVariable(fact.variable, typeWithoutProperty) + val updatedFact = fact + val y = rhv.base + val newType = removedPropertyType?.let { type -> TypedVariable(y, type).withTypeGuards(current) } + return listOfNotNull(updatedFact, newType) + } + + // Case `x[i] := y` + is ElementAccessor -> { + if (fact.type is EtsTypeFact.UnionEtsTypeFact) { + TODO("Support union type for x[i] := y in BW-sequent") + } + + if (fact.type is EtsTypeFact.IntersectionEtsTypeFact) { + TODO("Support intersection type for x[i] := y in BW-sequent") + } + + // x:Array |= x:Array (pass) + if (fact.type !is EtsTypeFact.ArrayEtsTypeFact) { + return listOf(fact) + } + + // x:Array |= x:Array (keep) + y:T + val y = rhv.base + val type = fact.type.elementType + val newFact = TypedVariable(y, type).withTypeGuards(current) + return listOf(fact, newFact) + } + } + } else { + error("Incorrect 3AC: $current") + } + } + + private fun EtsTypeFact.ObjectEtsTypeFact.removePropertyType(propertyName: String): Pair { + val propertyType = properties[propertyName] + val updatedThis = EtsTypeFact.ObjectEtsTypeFact(cls, properties - propertyName) + return updatedThis to propertyType + } + + override fun obtainCallToReturnSiteFlowFunction( + callStatement: EtsStmt, + returnSite: EtsStmt, + ): FlowFunction = FlowFunction { fact -> + when (fact) { + Zero -> callZero(callStatement) + is TypedVariable -> call(callStatement, fact) + } + } + + private fun callZero( + callStatement: EtsStmt, + ): List { + val result = mutableListOf(Zero) + + val callExpr = callStatement.callExpr ?: error("No call") + + if (callExpr is EtsInstanceCallExpr) { + val instance = callExpr.instance + val path = instance.toBase() + val objectWithMethod = EtsTypeFact.ObjectEtsTypeFact( + cls = null, + properties = mapOf( + callExpr.method.name to EtsTypeFact.FunctionEtsTypeFact + ) + ) + result += TypedVariable(path, objectWithMethod) + } + + return result + } + + private fun call( + callStatement: EtsStmt, + fact: TypedVariable, + ): List { + val result = mutableListOf() + + val callResult = (callStatement as? EtsAssignStmt)?.lhv?.toBase() + if (callResult != null) { + // If fact was for LHS, drop it as overridden + if (fact.variable == callResult) return result + } + + result += fact + return result + } + + override fun obtainCallToStartFlowFunction( + callStatement: EtsStmt, + calleeStart: EtsStmt, + ): FlowFunction = FlowFunction { fact -> + when (fact) { + Zero -> listOf(fact) + is TypedVariable -> start(callStatement, calleeStart, fact) + } + } + + private fun start( + callStatement: EtsStmt, + calleeStart: EtsStmt, + fact: TypedVariable, + ): List { + val callResult = (callStatement as? EtsAssignStmt)?.lhv?.toBase() ?: return emptyList() + + if (fact.variable != callResult) return emptyList() + + if (calleeStart is EtsThrowStmt) return emptyList() // TODO support throwStmt + + check(calleeStart is EtsReturnStmt) + + val exitValue = calleeStart.returnValue?.toBase() ?: return emptyList() + + return listOf(TypedVariable(exitValue, fact.type)) + } + + override fun obtainExitToReturnSiteFlowFunction( + callStatement: EtsStmt, + returnSite: EtsStmt, + exitStatement: EtsStmt, + ): FlowFunction = FlowFunction { fact -> + when (fact) { + Zero -> listOf(fact) + is TypedVariable -> exit(callStatement, fact) + } + } + + private fun exit( + callStatement: EtsStmt, + fact: TypedVariable, + ): List { + val callExpr = callStatement.callExpr ?: error("No call") + + when (fact.variable) { + is AccessPathBase.This -> { + if (callExpr !is EtsInstanceCallExpr) { + return emptyList() + } + + val instance = callExpr.instance + val instancePath = instance.toBase() + return listOf(TypedVariable(instancePath, fact.type)) + } + + is AccessPathBase.Arg -> { + val arg = callExpr.args.getOrNull(fact.variable.index)?.toBase() ?: return emptyList() + return listOf(TypedVariable(arg, fact.type)) + } + + else -> return emptyList() + } + } +} + +private const val COMPLEXITY_LIMIT = 5 + +private fun Iterable.myFilter(): List = filter { + if (it.type.complexity() >= COMPLEXITY_LIMIT) { + logger.warn { "Dropping too complex fact: $it" } + return@filter false + } + true +} + +/** + * Complexity of a type fact is the maximum depth of nested types. + */ +private fun EtsTypeFact.complexity(): Int = when (this) { + is EtsTypeFact.ObjectEtsTypeFact -> (properties.values.maxOfOrNull { it.complexity() } ?: 0) + 1 + is EtsTypeFact.ArrayEtsTypeFact -> elementType.complexity() + 1 + is EtsTypeFact.UnionEtsTypeFact -> (types.maxOfOrNull { it.complexity() } ?: 0) + 1 + is EtsTypeFact.IntersectionEtsTypeFact -> (types.maxOfOrNull { it.complexity() } ?: 0) + 1 + else -> 0 +} diff --git a/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/EntryPointsProcessor.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/EntryPointsProcessor.kt new file mode 100644 index 0000000000..a686fcf463 --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/EntryPointsProcessor.kt @@ -0,0 +1,25 @@ +package org.usvm.dataflow.ts.infer + +import org.jacodb.ets.model.EtsMethod +import org.jacodb.ets.model.EtsScene + +object EntryPointsProcessor { + fun extractEntryPoints( + scene: EtsScene, + ): ArtificialMainWithAllMethods { + val artificialMainMethods = scene.projectClasses + .asSequence() + .flatMap { it.methods } + .filter { it.name == "@dummyMain" } + .toList() + return ArtificialMainWithAllMethods( + mainMethods = artificialMainMethods, + allMethods = scene.projectClasses.flatMap { it.methods }, + ) + } +} + +data class ArtificialMainWithAllMethods( + val mainMethods: List, + val allMethods: List, +) diff --git a/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/EtsMethodTypeFacts.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/EtsMethodTypeFacts.kt new file mode 100644 index 0000000000..058cba690d --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/EtsMethodTypeFacts.kt @@ -0,0 +1,8 @@ +package org.usvm.dataflow.ts.infer + +import org.jacodb.ets.model.EtsMethod + +data class EtsMethodTypeFacts( + val method: EtsMethod, + val types: Map, +) diff --git a/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/EtsTypeFact.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/EtsTypeFact.kt new file mode 100644 index 0000000000..24b2118431 --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/EtsTypeFact.kt @@ -0,0 +1,531 @@ +package org.usvm.dataflow.ts.infer + +import mu.KotlinLogging +import org.jacodb.ets.base.ANONYMOUS_CLASS_PREFIX +import org.jacodb.ets.base.CONSTRUCTOR_NAME +import org.jacodb.ets.base.EtsAnyType +import org.jacodb.ets.base.EtsArrayType +import org.jacodb.ets.base.EtsBooleanType +import org.jacodb.ets.base.EtsClassType +import org.jacodb.ets.base.EtsFunctionType +import org.jacodb.ets.base.EtsNullType +import org.jacodb.ets.base.EtsNumberType +import org.jacodb.ets.base.EtsStringType +import org.jacodb.ets.base.EtsType +import org.jacodb.ets.base.EtsUnclearRefType +import org.jacodb.ets.base.EtsUndefinedType +import org.jacodb.ets.base.EtsUnionType +import org.jacodb.ets.base.EtsUnknownType +import org.jacodb.ets.base.INSTANCE_INIT_METHOD_NAME + +private val logger = KotlinLogging.logger {} + +sealed interface EtsTypeFact { + + fun toPrettyString(): String { + return toString() + } + + sealed interface BasicType : EtsTypeFact + + fun union(other: EtsTypeFact): EtsTypeFact { + if (this == other) return this + + return when { + this is ObjectEtsTypeFact && other is ObjectEtsTypeFact -> union(this, other) + this is ObjectEtsTypeFact && other is StringEtsTypeFact -> union(this, other) + this is UnionEtsTypeFact -> union(this, other) + this is IntersectionEtsTypeFact -> union(this, other) + this is GuardedTypeFact -> union(this, other) + other is UnionEtsTypeFact -> union(other, this) + other is IntersectionEtsTypeFact -> union(other, this) + other is GuardedTypeFact -> union(other, this) + else -> mkUnionType(this, other) + } + } + + fun intersect(other: EtsTypeFact): EtsTypeFact? { + if (this == other) return this + + if (other is UnknownEtsTypeFact) return this + if (other is AnyEtsTypeFact) return other + + return when (this) { + is UnknownEtsTypeFact -> other + + is AnyEtsTypeFact -> this + + is StringEtsTypeFact, + is NumberEtsTypeFact, + is BooleanEtsTypeFact, + is NullEtsTypeFact, + is UndefinedEtsTypeFact, + -> when (other) { + is UnionEtsTypeFact -> intersect(other, this) + is IntersectionEtsTypeFact -> intersect(other, this) + is GuardedTypeFact -> intersect(other, this) + else -> null + } + + is FunctionEtsTypeFact -> when (other) { + is ObjectEtsTypeFact -> mkIntersectionType(this, other) + is UnionEtsTypeFact -> intersect(other, this) + is IntersectionEtsTypeFact -> intersect(other, this) + is GuardedTypeFact -> intersect(other, this) + else -> null + } + + is ArrayEtsTypeFact -> when (other) { + is ArrayEtsTypeFact -> { + val t = elementType.intersect(other.elementType) + if (t == null) { + logger.warn { "Empty intersection of array element types: $elementType & ${other.elementType}" } + null + } else { + ArrayEtsTypeFact(t) + } + } + + else -> null + } + + is ObjectEtsTypeFact -> when (other) { + is ObjectEtsTypeFact -> intersect(this, other) + is StringEtsTypeFact -> intersect(this, other) + is FunctionEtsTypeFact -> mkIntersectionType(this, other) + is UnionEtsTypeFact -> intersect(other, this) + is IntersectionEtsTypeFact -> intersect(other, this) + is GuardedTypeFact -> intersect(other, this) + else -> null + } + + is UnionEtsTypeFact -> intersect(this, other) + is IntersectionEtsTypeFact -> intersect(this, other) + is GuardedTypeFact -> intersect(this, other) + } + } + + object UnknownEtsTypeFact : EtsTypeFact, BasicType { + override fun toString(): String = "unknown" + } + + object AnyEtsTypeFact : BasicType { + override fun toString(): String = "any" + } + + object StringEtsTypeFact : BasicType { + override fun toString(): String = "string" + } + + object NumberEtsTypeFact : BasicType { + override fun toString(): String = "number" + } + + object BooleanEtsTypeFact : BasicType { + override fun toString(): String = "boolean" + } + + object NullEtsTypeFact : BasicType { + override fun toString(): String = "null" + } + + object UndefinedEtsTypeFact : BasicType { + override fun toString(): String = "undefined" + } + + // object VoidEtsTypeFact : BasicType { + // override fun toString(): String = "void" + // } + // + // object NeverEtsTypeFact : BasicType { + // override fun toString(): String = "never" + // } + + object FunctionEtsTypeFact : BasicType { + override fun toString(): String = "function" + } + + data class ArrayEtsTypeFact( + val elementType: EtsTypeFact, + ) : BasicType { + override fun toString(): String = "Array<$elementType>" + } + + data class ObjectEtsTypeFact( + val cls: EtsType?, + val properties: Map, + ) : BasicType { + override fun toString(): String { + val clsName = cls?.typeName?.takeUnless { it.startsWith(ANONYMOUS_CLASS_PREFIX) } ?: "Object" + val funProps = properties.entries + .filter { it.value is FunctionEtsTypeFact } + .filterNot { it.key == CONSTRUCTOR_NAME } + .filterNot { it.key == INSTANCE_INIT_METHOD_NAME } + .sortedBy { it.key } + val nonFunProps = properties.entries + .filter { it.value !is FunctionEtsTypeFact } + .sortedBy { it.key } + val props = (funProps + nonFunProps).joinToString(", ") { (name, type) -> "$name: $type" } + return "$clsName { $props }" + } + + // Object { + // ..foo: Object { + // ....bar: string + // ..} + // } + override fun toPrettyString(): String { + val clsName = cls?.typeName ?: "Object" + val funProps = properties.entries + .filter { it.value is FunctionEtsTypeFact } + .sortedBy { it.key } + val nonFunProps = properties.entries + .filter { it.value !is FunctionEtsTypeFact } + .sortedBy { it.key } + return buildString { + appendLine("$clsName {") + for ((name, type) in funProps) { + appendLine(" $name: $type") + } + for ((name, type) in nonFunProps) { + appendLine("$name: ${type.toPrettyString()}".lines().joinToString("\n") { " $it" }) + } + append("}") + } + } + + override fun equals(other: Any?): Boolean { + if (other !is ObjectEtsTypeFact) return false + + if (other.cls != null && other.cls == cls) return true + + return properties == other.properties + } + + override fun hashCode(): Int { + if (cls == null) return properties.hashCode() + + return cls.hashCode() + } + } + + data class UnionEtsTypeFact( + val types: Set, + ) : EtsTypeFact { + init { + require(types.isNotEmpty()) { + "An empty set of types is passed as an union type" + } + } + + override fun toString(): String { + return types.map { + when (it) { + is UnionEtsTypeFact, is IntersectionEtsTypeFact -> "($it)" + else -> it.toString() + } + }.sorted().joinToString(" | ") + } + + override fun toPrettyString(): String { + return types.map { + when (it) { + is UnionEtsTypeFact, is IntersectionEtsTypeFact -> "($it)" + else -> it.toString() + } + }.sorted().joinToString(" | ") + } + } + + data class IntersectionEtsTypeFact( + val types: Set, + ) : EtsTypeFact { + init { + require(types.isNotEmpty()) { + "An empty set of types is passed as an intersection type" + } + } + + override fun toString(): String { + return types.map { + when (it) { + is UnionEtsTypeFact, is IntersectionEtsTypeFact -> "($it)" + else -> it.toString() + } + }.sorted().joinToString(" & ") + } + + override fun toPrettyString(): String { + return types.map { + when (it) { + is UnionEtsTypeFact, is IntersectionEtsTypeFact -> "($it)" + else -> it.toString() + } + }.sorted().joinToString(" & ") + } + } + + data class GuardedTypeFact( + val guard: BasicType, + val guardNegated: Boolean, + val type: EtsTypeFact, + ) : EtsTypeFact + + companion object { + internal val allStringProperties = listOf( + "length", + "constructor", + "anchor", + "at", + "big", + "blink", + "bold", + "charAt", + "charCodeAt", + "codePointAt", + "concat", + "endsWith", + "fontcolor", + "fontsize", + "fixed", + "includes", + "indexOf", + "isWellFormed", + "italics", + "lastIndexOf", + "link", + "localeCompare", + "match", + "matchAll", + "normalize", + "padEnd", + "padStart", + "repeat", + "replace", + "replaceAll", + "search", + "slice", + "small", + "split", + "strike", + "sub", + "substr", + "substring", + "sup", + "startsWith", + "toString", + "toWellFormed", + "trim", + "trimStart", + "trimLeft", + "trimEnd", + "trimRight", + "toLocaleLowerCase", + "toLocaleUpperCase", + "toLowerCase", + "toUpperCase", + "valueOf", + ) + + private fun intersect(unionType: UnionEtsTypeFact, other: EtsTypeFact): EtsTypeFact { + // todo: push intersection + return mkIntersectionType(unionType, other) + } + + private fun intersect(intersectionType: IntersectionEtsTypeFact, other: EtsTypeFact): EtsTypeFact? { + val result = hashSetOf() + for (type in intersectionType.types) { + val intersection = type.intersect(other) ?: return null + if (intersection is IntersectionEtsTypeFact) { + result.addAll(intersection.types) + } else { + result.add(intersection) + } + } + return mkIntersectionType(result) + } + + private fun intersect(guardedType: GuardedTypeFact, other: EtsTypeFact): EtsTypeFact? { + if (other is GuardedTypeFact) { + if (other.guard == guardedType.guard) { + return if (other.guardNegated == guardedType.guardNegated) { + guardedType.type.intersect(other.type)?.withGuard(guardedType.guard, guardedType.guardNegated) + } else { + guardedType.type.union(other.type) + } + } + } + + // todo: evaluate types + return mkIntersectionType(guardedType, other) + } + + private fun intersect(obj1: ObjectEtsTypeFact, obj2: ObjectEtsTypeFact): EtsTypeFact? { + val intersectionProperties = obj1.properties.toMutableMap() + for ((property, type) in obj2.properties) { + val currentType = intersectionProperties[property] + if (currentType == null) { + intersectionProperties[property] = type + continue + } + + intersectionProperties[property] = currentType.intersect(type) ?: return null + } + + val intersectionCls = if (obj1.cls != null && obj2.cls != null) { + obj1.cls.takeIf { it == obj2.cls } + } else { + obj1.cls ?: obj2.cls + } + return ObjectEtsTypeFact(intersectionCls, intersectionProperties) + } + + private fun intersect(obj: ObjectEtsTypeFact, string: StringEtsTypeFact): EtsTypeFact? { + if (obj.cls == EtsStringType) return string + if (obj.cls != null) return null + + val intersectionProperties = obj.properties + .filter { it.key in allStringProperties } + .mapValues { (_, type) -> + // TODO: intersect with the corresponding type of String's property + type + } + + return ObjectEtsTypeFact(obj.cls, intersectionProperties) + } + + private fun union(unionType: UnionEtsTypeFact, other: EtsTypeFact): EtsTypeFact { + val result = hashSetOf() + for (type in unionType.types) { + val union = type.union(other) + if (union is UnionEtsTypeFact) { + result.addAll(union.types) + } else { + result.add(union) + } + } + return mkUnionType(result) + } + + private fun union(guardedType: GuardedTypeFact, other: EtsTypeFact): EtsTypeFact { + // todo: evaluate types + return mkUnionType(guardedType, other) + } + + private fun union(intersectionType: IntersectionEtsTypeFact, other: EtsTypeFact): EtsTypeFact { + // todo: push union + return mkUnionType(intersectionType, other) + } + + private fun union(obj1: ObjectEtsTypeFact, obj2: ObjectEtsTypeFact): EtsTypeFact { + if (obj1.cls != null && obj2.cls != null && obj1.cls != obj2.cls) { + return mkUnionType(obj1, obj2) + } + + val commonProperties = obj1.properties.keys.intersect(obj2.properties.keys).associateWith { property -> + val thisType = obj1.properties.getValue(property) + val otherType = obj2.properties.getValue(property) + thisType.union(otherType) + } + + val o1OnlyProperties = obj1.properties.filter { it.key !in obj2.properties } + val o2OnlyProperties = obj2.properties.filter { it.key !in obj1.properties } + + val o1 = ObjectEtsTypeFact(obj1.cls, o1OnlyProperties) + val o2 = ObjectEtsTypeFact(obj2.cls, o2OnlyProperties) + + if (commonProperties.isEmpty()) { + return mkUnionType(o1, o2) + } + + val commonCls = obj1.cls.takeIf { it == obj2.cls } + val commonObject = ObjectEtsTypeFact(commonCls, commonProperties) + + if (o1OnlyProperties.isEmpty() && o2OnlyProperties.isEmpty()) { + return commonObject + } + + return mkIntersectionType(commonObject, mkUnionType(o1, o2)) + } + + private fun union(obj: ObjectEtsTypeFact, string: StringEtsTypeFact): EtsTypeFact { + if (obj.cls == EtsStringType) return string + if (obj.cls != null) return mkUnionType(obj, string) + + for (p in obj.properties.keys) { + if (p !in allStringProperties) { + return mkUnionType(obj, string) + } + } + + return string + } + + fun mkUnionType(vararg types: EtsTypeFact): EtsTypeFact = mkUnionType(types.toHashSet()) + + fun mkUnionType(types: Set): EtsTypeFact { + if (types.size == 1) return types.single() + return UnionEtsTypeFact(types) + } + + fun mkIntersectionType(vararg types: EtsTypeFact): EtsTypeFact = mkIntersectionType(types.toHashSet()) + + fun mkIntersectionType(types: Set): EtsTypeFact { + if (types.size == 1) return types.single() + return IntersectionEtsTypeFact(types) + } + + fun from(type: EtsType): EtsTypeFact { + return when (type) { + is EtsAnyType -> AnyEtsTypeFact + is EtsUnknownType -> UnknownEtsTypeFact + is EtsUnionType -> UnionEtsTypeFact(type.types.map { from(it) }.toSet()) + is EtsBooleanType -> BooleanEtsTypeFact + is EtsNumberType -> NumberEtsTypeFact + is EtsStringType -> StringEtsTypeFact + is EtsNullType -> NullEtsTypeFact + is EtsUndefinedType -> UndefinedEtsTypeFact + is EtsClassType -> ObjectEtsTypeFact(type, emptyMap()) + is EtsFunctionType -> FunctionEtsTypeFact + // is EtsArrayType -> ObjectEtsTypeFact( + // cls = type, + // properties = mapOf( + // "index" to ObjectEtsTypeFact( + // cls = null, + // properties = mapOf( + // "name" to StringEtsTypeFact, + // "value" to from(type.elementType) + // ) + // ), + // "length" to NumberEtsTypeFact + // ) + // ) + is EtsArrayType -> ArrayEtsTypeFact(elementType = from(type.elementType)) + is EtsUnclearRefType -> ObjectEtsTypeFact(type, emptyMap()) + // is EtsGenericType -> TODO() + else -> { + logger.error { "Unsupported type: $type" } + UnknownEtsTypeFact + } + } + } + } +} + +fun EtsTypeFact.withGuard(guard: EtsTypeFact.BasicType, guardNegated: Boolean): EtsTypeFact { + val duplicateGuard = findDuplicateGuard(this, guard) + + if (duplicateGuard != null) { + if (guardNegated == duplicateGuard.guardNegated) return this + + TODO("Same guard with different sign") + } + + return EtsTypeFact.GuardedTypeFact(guard, guardNegated, this) +} + +private fun findDuplicateGuard(fact: EtsTypeFact, guard: EtsTypeFact.BasicType): EtsTypeFact.GuardedTypeFact? { + if (fact !is EtsTypeFact.GuardedTypeFact) return null + if (fact.guard == guard) return fact + return findDuplicateGuard(fact.type, guard) +} diff --git a/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/ForwardAnalyzer.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/ForwardAnalyzer.kt new file mode 100644 index 0000000000..a8aebbe64f --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/ForwardAnalyzer.kt @@ -0,0 +1,44 @@ +package org.usvm.dataflow.ts.infer + +import org.jacodb.ets.base.EtsStmt +import org.jacodb.ets.base.EtsType +import org.jacodb.ets.model.EtsMethod +import org.usvm.dataflow.ifds.Analyzer +import org.usvm.dataflow.ifds.Edge +import org.usvm.dataflow.ifds.Vertex +import org.usvm.dataflow.ts.graph.EtsApplicationGraph + +class ForwardAnalyzer( + val graph: EtsApplicationGraph, + methodInitialTypes: Map, + typeInfo: Map, + doAddKnownTypes: Boolean = true, +) : Analyzer { + + override val flowFunctions = ForwardFlowFunctions(graph, methodInitialTypes, typeInfo, doAddKnownTypes) + + override fun handleCrossUnitCall( + caller: Vertex, + callee: Vertex, + ): List { + error("No cross unit calls") + } + + override fun handleNewEdge(edge: Edge): List { + val (startVertex, currentVertex) = edge + val (current, currentFact) = currentVertex + + val method = graph.methodOf(current) + val currentIsExit = current in graph.exitPoints(method) + + if (!currentIsExit) return emptyList() + + return listOf( + ForwardSummaryAnalyzerEvent( + method = method, + initialVertex = startVertex, + exitVertex = currentVertex, + ) + ) + } +} diff --git a/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/ForwardFlowFunctions.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/ForwardFlowFunctions.kt new file mode 100644 index 0000000000..b9971a846f --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/ForwardFlowFunctions.kt @@ -0,0 +1,586 @@ +package org.usvm.dataflow.ts.infer + +import mu.KotlinLogging +import org.jacodb.ets.base.EtsAnyType +import org.jacodb.ets.base.EtsArithmeticExpr +import org.jacodb.ets.base.EtsAssignStmt +import org.jacodb.ets.base.EtsBooleanConstant +import org.jacodb.ets.base.EtsCastExpr +import org.jacodb.ets.base.EtsFieldRef +import org.jacodb.ets.base.EtsInstanceCallExpr +import org.jacodb.ets.base.EtsLValue +import org.jacodb.ets.base.EtsNewArrayExpr +import org.jacodb.ets.base.EtsNewExpr +import org.jacodb.ets.base.EtsNullConstant +import org.jacodb.ets.base.EtsNumberConstant +import org.jacodb.ets.base.EtsRef +import org.jacodb.ets.base.EtsRelationExpr +import org.jacodb.ets.base.EtsReturnStmt +import org.jacodb.ets.base.EtsStmt +import org.jacodb.ets.base.EtsStringConstant +import org.jacodb.ets.base.EtsType +import org.jacodb.ets.base.EtsUndefinedConstant +import org.jacodb.ets.base.EtsUnknownType +import org.jacodb.ets.model.EtsMethod +import org.jacodb.ets.utils.callExpr +import org.usvm.dataflow.ifds.FlowFunction +import org.usvm.dataflow.ifds.FlowFunctions +import org.usvm.dataflow.ts.graph.EtsApplicationGraph +import org.usvm.dataflow.ts.infer.ForwardTypeDomainFact.TypedVariable +import org.usvm.dataflow.ts.infer.ForwardTypeDomainFact.Zero + +private val logger = KotlinLogging.logger {} + +class ForwardFlowFunctions( + val graph: EtsApplicationGraph, + val methodInitialTypes: Map, + val typeInfo: Map, + val doAddKnownTypes: Boolean = true, +) : FlowFunctions { + + private val aliasesCache: MutableMap>> = hashMapOf() + + private fun getAliases(method: EtsMethod): Map> { + return aliasesCache.computeIfAbsent(method) { computeAliases(method) } + } + + override fun obtainPossibleStartFacts(method: EtsMethod): Collection { + val result = mutableListOf(Zero) + + val initialTypes = methodInitialTypes[method] + if (initialTypes != null) { + for ((base, type) in initialTypes.types) { + val path = AccessPath(base, accesses = emptyList()) + addTypes(path, type, result) + } + } + + if (doAddKnownTypes) { + for (local in method.locals) { + if (local.type != EtsUnknownType && local.type != EtsAnyType) { + val path = AccessPath(AccessPathBase.Local(local.name), accesses = emptyList()) + val type = EtsTypeFact.from(local.type) + if (type != EtsTypeFact.UnknownEtsTypeFact && type != EtsTypeFact.AnyEtsTypeFact) { + logger.debug { "Adding known type for $path: $type" } + addTypes(path, type, result) + } + } + } + } + + return result + } + + private fun addTypes( + path: AccessPath, + type: EtsTypeFact, + facts: MutableList, + ) { + when (type) { + EtsTypeFact.UnknownEtsTypeFact -> { + facts += TypedVariable(path, EtsTypeFact.AnyEtsTypeFact) + } + + is EtsTypeFact.ObjectEtsTypeFact -> { + for ((propertyName, propertyType) in type.properties) { + val propertyPath = path + FieldAccessor(propertyName) + addTypes(propertyPath, propertyType, facts) + } + + facts += TypedVariable(path, type) + } + + is EtsTypeFact.ArrayEtsTypeFact -> { + // check(type.elementType !is EtsTypeFact.ArrayEtsTypeFact) + facts += TypedVariable(path, type) + addTypes(path + ElementAccessor, type.elementType, facts) + } + + is EtsTypeFact.GuardedTypeFact -> { + addTypes(path, type.type, facts) + } + + is EtsTypeFact.IntersectionEtsTypeFact -> { + type.types.forEach { addTypes(path, it, facts) } + } + + is EtsTypeFact.UnionEtsTypeFact -> { + type.types.forEach { addTypes(path, it, facts) } + } + + else -> { + facts += TypedVariable(path, type) + } + } + } + + override fun obtainSequentFlowFunction( + current: EtsStmt, + next: EtsStmt, + ): FlowFunction = FlowFunction { fact -> + if (current is EtsAssignStmt) { + val lhvPath = current.lhv.toPathOrNull() + val rhvPath = current.rhv.toPathOrNull() + if (lhvPath != null && rhvPath != null && lhvPath == rhvPath) { + return@FlowFunction listOf(fact) + } + } + when (fact) { + Zero -> sequentZero(current) + is TypedVariable -> sequentFact(current, fact).myFilter() + } + } + + private fun sequentZero(current: EtsStmt): List { + if (current !is EtsAssignStmt) return listOf(Zero) + + val lhv = current.lhv.toPath() + val result = mutableListOf(Zero) + val (preAliases, _) = getAliases(current.method).getValue(current) + + fun addTypeFactWithAliases(path: AccessPath, type: EtsTypeFact) { + result += TypedVariable(path, type) + if (path.accesses.isNotEmpty()) { + check(path.accesses.size == 1) + val base = AccessPath(path.base, emptyList()) + for (alias in preAliases.getAliases(base)) { + val newPath = alias + path.accesses.single() + result += TypedVariable(newPath, type) + } + } + } + + when (val rhv = current.rhv) { + is EtsNewExpr -> { + // val newType = rhv.type + // if (newType is EtsClassType) { + // val cls = graph.cp.classes + // .firstOrNull { it.name == newType.typeName } + // if (cls != null) { + // for (f in cls.fields) { + // val path = lhv + FieldAccessor(f.name) + // result += TypedVariable(path, EtsTypeFact.from(f.type)) + // } + // } + // } + + val type = typeInfo[rhv.type] + ?: EtsTypeFact.ObjectEtsTypeFact(cls = rhv.type, properties = emptyMap()) + addTypeFactWithAliases(lhv, type) + } + + is EtsNewArrayExpr -> { + // TODO: check + val elementType = EtsTypeFact.from(rhv.elementType) + val type = EtsTypeFact.ArrayEtsTypeFact(elementType = elementType) + result += TypedVariable(lhv, type) + result += TypedVariable(lhv + ElementAccessor, elementType) + } + + is EtsStringConstant -> { + addTypeFactWithAliases(lhv, EtsTypeFact.StringEtsTypeFact) + } + + is EtsNumberConstant -> { + addTypeFactWithAliases(lhv, EtsTypeFact.NumberEtsTypeFact) + } + + is EtsBooleanConstant -> { + addTypeFactWithAliases(lhv, EtsTypeFact.BooleanEtsTypeFact) + } + + is EtsNullConstant -> { + addTypeFactWithAliases(lhv, EtsTypeFact.NullEtsTypeFact) + } + + is EtsUndefinedConstant -> { + addTypeFactWithAliases(lhv, EtsTypeFact.UndefinedEtsTypeFact) + } + + // Note: do not handle cast in forward ff! + // is EtsCastExpr -> { + // result += TypedVariable(lhv, EtsTypeFact.from(rhv.type)) + // } + + is EtsFieldRef -> { + if (doAddKnownTypes && rhv.type != EtsUnknownType && rhv.type != EtsAnyType) { + val type = EtsTypeFact.from(rhv.type) + logger.debug { "Adding known type for $lhv from $rhv: $type" } + addTypeFactWithAliases(lhv, type) + } + } + + is EtsArithmeticExpr -> { + result += TypedVariable(lhv, EtsTypeFact.StringEtsTypeFact) + result += TypedVariable(lhv, EtsTypeFact.NumberEtsTypeFact) + } + + is EtsRelationExpr -> { + result += TypedVariable(lhv, EtsTypeFact.BooleanEtsTypeFact) + } + + else -> { + // logger.info { "TODO: forward assign $current" } + } + } + + return result + } + + private fun sequentFact(current: EtsStmt, fact: TypedVariable): List { + if (current !is EtsAssignStmt) return listOf(fact) + + val lhv = current.lhv.toPath() + + val rhv = when (val r = current.rhv) { + is EtsRef -> r.toPath() // This, FieldRef, ArrayAccess + is EtsLValue -> r.toPath() // Local + is EtsCastExpr -> r.toPath() // Cast + else -> { + // logger.info { "TODO forward assign: $current" } + null + } + } + + val (preAliases, _) = getAliases(current.method).getValue(current) + + // Override LHS when RHS is const-like: + if (rhv == null) { + if (lhv.accesses.isEmpty()) { + // x := const + + // TODO: `x := const as T` + + // x.*:T |= drop + if (fact.variable.startsWith(lhv)) { + return emptyList() + } + + } else { + // x.f := const OR x[i] := const + + check(lhv.accesses.size == 1) + when (val a = lhv.accesses.single()) { + // x.f := const + is FieldAccessor -> { + val base = AccessPath(lhv.base, emptyList()) + + // x.f.*:T |= drop + if (fact.variable.startsWith(lhv)) { + return emptyList() + } + // z in G(x), z.f.*:T |= drop + if (preAliases.getAliases(base).any { fact.variable.startsWith(it + a) }) { + return emptyList() + } + } + + // x[i] := const + is ElementAccessor -> { + // do nothing, pass-through + } + } + } + + // Pass-through unrelated facts: + return listOf(fact) + } + + if (lhv.accesses.isEmpty() && rhv.accesses.isEmpty()) { + // x := y + + // TODO: x := x + // Note: handled outside + + // x.*:T |= drop + if (fact.variable.startsWith(lhv)) { + return emptyList() + } + + // y.*:T |= y.*:T (keep) + x.*:T (same tail) + if (fact.variable.startsWith(rhv)) { + // Extra case with cast: `x := y as U`: + // `y.*:T` |= keep + new fact `x.*:W`, where `W = T intersect U` + // TODO: Currently, we just take the type from the CastExpr, without intersecting. + // The problem is that when we have fact `y:any`, the intersection (though probably correctly) + // produces `x:any`, so we just lose type information from the cast. + // Using the cast type directly is just a temporary solution to satisfy simple tests. + if (current.rhv is EtsCastExpr) { + val path = AccessPath(lhv.base, fact.variable.accesses) + // val type = EtsTypeFact.from((current.rhv as EtsCastExpr).type).intersect(fact.type) ?: fact.type + val type = EtsTypeFact.from((current.rhv as EtsCastExpr).type) + return listOf(fact, TypedVariable(path, type)) + } + + val path = AccessPath(lhv.base, fact.variable.accesses) + return listOf(fact, TypedVariable(path, fact.type)) + } + + } else if (lhv.accesses.isEmpty()) { + // x := y.f OR x := y[i] + + // TODO: x := x.f + // ??????? x.f:T |= drop + + // x.*:T |= drop + if (fact.variable.startsWith(lhv)) { + return emptyList() + } + + check(rhv.accesses.size == 1) + when (val a = rhv.accesses.single()) { + // x := y.f + is FieldAccessor -> { + // y.f.*:T |= y.f.*:T (keep) + x.*:T (same tail after .f) + if (fact.variable.startsWith(rhv)) { + val path = lhv + fact.variable.accesses.drop(1) + return listOf(fact, TypedVariable(path, fact.type)) + } + // Note: the following is unnecessary due to `z := y` alias + // // z in G(y), z.f.*:T |= z.f.*:T (keep) + x.*:T (same tail after .f) + // val y = AccessPath(rhv.base, emptyList()) + // for (z in preAliases.getAliases(y)) { + // if (fact.variable.startsWith(z + a)) { + // val path = lhv + fact.variable.accesses.drop(z.accesses.size + 1) + // return listOf(fact, TypedVariable(path, fact.type)) + // } + // } + } + + // x := y[i] + is ElementAccessor -> { + // do nothing, pass-through + // TODO: ??? + + // TODO: do we need to add type fact `x.*:T` here? + // y[i].*:T |= y[i].*:T (keep) + x.*:T (same tail after [i]) + if (fact.variable.startsWith(rhv)) { + val path = lhv + fact.variable.accesses.drop(1) + return listOf(fact, TypedVariable(path, fact.type)) + } + } + } + + } else if (rhv.accesses.isEmpty()) { + // x.f := y OR x[i] := y + + check(lhv.accesses.size == 1) + when (val a = lhv.accesses.single()) { + // x.f := y + is FieldAccessor -> { + // TODO: x.f := x + + // x.f.*:T |= drop + if (fact.variable.startsWith(lhv)) { + return emptyList() + } + // z in G(x), z.f.*:T |= drop + val x = AccessPath(lhv.base, emptyList()) + if (preAliases.getAliases(x).any { z -> fact.variable.startsWith(z + a) }) { + return emptyList() + } + + // x.*:T |= x.*:T (keep) + // Note: .* does NOT start with .f, which is handled above + if (fact.variable.base == lhv.base) { + return listOf(fact) + } + + // y.*:T |= y.*:T (keep) + x.f.*:T (same tail) + aliases + // aliases: z in G(x), z.f.*:T |= x.f.*:T (same tail) + if (fact.variable.startsWith(rhv)) { + val result = mutableListOf(fact) + // skip duplicate fields + // if (fact.variable.accesses.firstOrNull() != a) { + val path1 = lhv + fact.variable.accesses + result += TypedVariable(path1, fact.type) + // } + for (z in preAliases.getAliases(x)) { + // skip duplicate fields + // if (z.accesses.firstOrNull() != a) { + // TODO: what about z.accesses.last == a ? + val path2 = z + a + fact.variable.accesses + result += TypedVariable(path2, fact.type) + // } + } + return result + } + // Note: the following is unnecessary due to `z := y` alias + // // z in G(y), z.*:T |= x.f.*:T (same tail) + // for (z in preAliases.getAliases(rhv)) { + // if (fact.variable.startsWith(z)) { + // val path = lhv + fact.variable.accesses + // return listOf(fact, TypedVariable(path, fact.type)) + // } + // } + } + + // x[i] := y + is ElementAccessor -> { + // do nothing, pass-through + // TODO: ??? + + // TODO: do we really want to add type fact `x[i]:T` here? + // y.*:T |= y.*:T (keep) + x[i].*:T (same tail) + // TODO: what about aliases of x? + if (fact.variable.startsWith(rhv)) { + val path = lhv + fact.variable.accesses + return listOf(fact, TypedVariable(path, fact.type)) + } + } + } + + } else { + error("Incorrect 3AC: $current") + } + + return listOf(fact) + } + + override fun obtainCallToReturnSiteFlowFunction( + callStatement: EtsStmt, + returnSite: EtsStmt, + ): FlowFunction = FlowFunction { fact -> + when (fact) { + // TODO: add known return type of the function call + Zero -> listOf(Zero) + is TypedVariable -> call(callStatement, fact) + } + } + + private fun call( + callStatement: EtsStmt, + fact: TypedVariable, + ): List { + val callResultValue = (callStatement as? EtsAssignStmt)?.lhv?.toPath() + if (callResultValue != null) { + // Drop fact on LHS as it will be overwritten by the call result + if (fact.variable.base == callResultValue.base) return emptyList() + } + + val callExpr = callStatement.callExpr ?: error("No call") + + // todo: hack, keep fact if call was not resolved + if (graph.callees(callStatement).none()) { + return listOf(fact) + } + + if (callExpr is EtsInstanceCallExpr) { + val instance = callExpr.instance.toPath() + if (fact.variable.base == instance.base) return emptyList() + } + + for (arg in callExpr.args) { + val argPath = arg.toPath() + if (fact.variable.base == argPath.base) return emptyList() + } + + return listOf(fact) + } + + override fun obtainCallToStartFlowFunction( + callStatement: EtsStmt, + calleeStart: EtsStmt, + ): FlowFunction = FlowFunction { fact -> + when (fact) { + Zero -> listOf(Zero) + is TypedVariable -> start(callStatement, fact) + } + } + + private fun start( + callStatement: EtsStmt, + fact: TypedVariable, + ): List { + val result = mutableListOf() + + val callExpr = callStatement.callExpr ?: error("No call") + + if (callExpr is EtsInstanceCallExpr) { + val instance = callExpr.instance.toPath() + if (fact.variable.base == instance.base) { + val path = AccessPath(AccessPathBase.This, fact.variable.accesses) + result += TypedVariable(path, fact.type) + } + } + + for ((index, arg) in callExpr.args.withIndex()) { + val argPath = arg.toPath() + if (fact.variable.base == argPath.base) { + val path = AccessPath(AccessPathBase.Arg(index), fact.variable.accesses) + result += TypedVariable(path, fact.type) + } + } + + return result + } + + override fun obtainExitToReturnSiteFlowFunction( + callStatement: EtsStmt, + returnSite: EtsStmt, + exitStatement: EtsStmt, + ): FlowFunction = FlowFunction { fact -> + when (fact) { + Zero -> listOf(Zero) + is TypedVariable -> exit(callStatement, exitStatement, fact) + } + } + + private fun exit( + callStatement: EtsStmt, + exitStatement: EtsStmt, + fact: TypedVariable, + ): List { + val factVariableBase = fact.variable.base + val callExpr = callStatement.callExpr ?: error("No call") + + when (factVariableBase) { + is AccessPathBase.This -> { + // Drop facts on This if the call was static + if (callExpr !is EtsInstanceCallExpr) { + return emptyList() + } + + val instance = callExpr.instance.toPath() + check(instance.accesses.isEmpty()) + + val path = AccessPath(instance.base, fact.variable.accesses) + return listOf(TypedVariable(path, fact.type)) + } + + is AccessPathBase.Arg -> { + val arg = callExpr.args.getOrNull(factVariableBase.index)?.toPath() ?: return emptyList() + val path = AccessPath(arg.base, fact.variable.accesses) + return listOf(TypedVariable(path, fact.type)) + } + + else -> { + if (exitStatement !is EtsReturnStmt) return emptyList() + val exitValue = exitStatement.returnValue?.toPath() ?: return emptyList() + + if (fact.variable.base != exitValue.base) return emptyList() + + val callResult = (callStatement as? EtsAssignStmt)?.lhv?.toPath() ?: return emptyList() + check(callResult.accesses.isEmpty()) + + val path = AccessPath(callResult.base, fact.variable.accesses) + return listOf(TypedVariable(path, fact.type)) + } + } + } +} + +private const val ACCESSES_LIMIT = 5 +private const val DUPLICATE_FIELDS_LIMIT = 3 + +private fun Iterable.myFilter(): List = filter { + if (it.variable.accesses.size > ACCESSES_LIMIT) { + logger.warn { "Dropping too long fact: $it" } + return@filter false + } + if (it.variable.accesses.hasDuplicateFields(DUPLICATE_FIELDS_LIMIT)) { + logger.warn { "Dropping fact with duplicate fields: $it" } + return@filter false + } + true +} diff --git a/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/TypeDomainFact.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/TypeDomainFact.kt new file mode 100644 index 0000000000..196dea887c --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/TypeDomainFact.kt @@ -0,0 +1,21 @@ +package org.usvm.dataflow.ts.infer + +sealed interface BackwardTypeDomainFact { + data object Zero : BackwardTypeDomainFact + + // Requirement + data class TypedVariable( + val variable: AccessPathBase, + val type: EtsTypeFact, + ) : BackwardTypeDomainFact +} + +sealed interface ForwardTypeDomainFact { + data object Zero : ForwardTypeDomainFact + + // Exact type + data class TypedVariable( + val variable: AccessPath, + val type: EtsTypeFact, // primitive or Object + ) : ForwardTypeDomainFact +} diff --git a/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/TypeGuesser.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/TypeGuesser.kt new file mode 100644 index 0000000000..87358c78e0 --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/TypeGuesser.kt @@ -0,0 +1,269 @@ +/* + * 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 + +import mu.KotlinLogging +import org.jacodb.ets.base.EtsClassType +import org.jacodb.ets.model.EtsClass +import org.jacodb.ets.model.EtsMethod +import org.jacodb.ets.model.EtsScene + +private val logger = KotlinLogging.logger {} + +fun guessTypes( + scene: EtsScene, + facts: Map>, + propertyNameToClasses: Map>, +): Map> { + return facts.mapValues { (method, types) -> + if (types.isEmpty()) { + logger.warn { "Facts are empty for method ${method.signature}" } + return@mapValues types + } + + val updatedTypes = types.mapValues { (accessPath, fact) -> + // logger.info { + // "Resolving a type for a fact \"$fact\" for access path \"$accessPath\" in the method \"$method\"" + // } + + val resultingType = fact.resolveType(scene, propertyNameToClasses) + // logger.info { "The result is $resultingType" } + + resultingType + } + + updatedTypes + } +} + +fun EtsTypeFact.resolveType( + scene: EtsScene, + propertyNameToClasses: Map>, +): EtsTypeFact { + return when (val simplifiedFact = simplify()) { + is EtsTypeFact.ArrayEtsTypeFact -> simplifiedFact.resolveArrayTypeFact(scene, propertyNameToClasses) + is EtsTypeFact.ObjectEtsTypeFact -> simplifiedFact.resolveObjectTypeFact(scene, propertyNameToClasses) + is EtsTypeFact.FunctionEtsTypeFact -> simplifiedFact + is EtsTypeFact.GuardedTypeFact -> TODO("guarded") + is EtsTypeFact.IntersectionEtsTypeFact -> { + val updatedTypes = simplifiedFact.types.mapTo(hashSetOf()) { + it.resolveType(scene, propertyNameToClasses) + } + EtsTypeFact.mkIntersectionType(updatedTypes).simplify() + } + + is EtsTypeFact.UnionEtsTypeFact -> { + val updatedTypes = simplifiedFact.types.mapNotNullTo(mutableSetOf()) { type -> + val resolvedType = type.resolveType(scene, propertyNameToClasses) + resolvedType.takeIf { it !is EtsTypeFact.AnyEtsTypeFact } + } + + if (updatedTypes.isEmpty()) { + EtsTypeFact.AnyEtsTypeFact + } else { + EtsTypeFact.mkUnionType(updatedTypes).simplify() + } + } + + else -> simplify() + } +} + +private fun EtsTypeFact.ObjectEtsTypeFact.resolveObjectTypeFact( + scene: EtsScene, + propertyNameToClasses: Map>, +): EtsTypeFact { + if (cls != null) { + return this + } + + val touchedPropertiesNames = properties.keys + val classesInSystem = collectSuitableClasses(touchedPropertiesNames, propertyNameToClasses) + + if (classesInSystem.isEmpty()) { + return tryToDetermineSpecialObjects(scene, touchedPropertiesNames, propertyNameToClasses) + } + + val suitableTypes = resolveTypesFromClasses(classesInSystem, scene, propertyNameToClasses) + + // TODO process arrays here (and strings) + + return when { + suitableTypes.isEmpty() -> error("Should be processed earlier") + suitableTypes.size == 1 -> suitableTypes.single() + suitableTypes.size in 2..5 -> EtsTypeFact.mkUnionType(suitableTypes).simplify() + else -> this + } +} + +private fun EtsTypeFact.ObjectEtsTypeFact.resolveTypesFromClasses( + classesInSystem: Iterable, + scene: EtsScene, + propertyNameToClasses: Map>, +) = classesInSystem + .mapTo(hashSetOf()) { cls -> + EtsTypeFact.ObjectEtsTypeFact( + cls = EtsClassType(signature = cls.signature), + properties = properties.mapValues { + it.value.resolveType(scene, propertyNameToClasses) + } + ) + } + +private fun collectSuitableClasses( + touchedPropertiesNames: Set, + propertyNameToClasses: Map>, +): Set { + val classesWithProperties = touchedPropertiesNames.map { propertyNameToClasses[it].orEmpty() } + + return classesWithProperties.reduceOrNull { a, b -> a intersect b }.orEmpty() +} + +private fun EtsTypeFact.ObjectEtsTypeFact.tryToDetermineSpecialObjects( + scene: EtsScene, + touchedPropertiesNames: Set, + propertyNameToClasses: Map>, +): EtsTypeFact.BasicType { + val indicesProperties = properties.filter { (k, _) -> k.toIntOrNull() != null } + if (indicesProperties.isNotEmpty()) { + val elementTypeFacts = indicesProperties.mapTo(hashSetOf()) { + it.value.resolveType(scene, propertyNameToClasses) + } + + val typeFact = EtsTypeFact.mkUnionType(elementTypeFacts).simplify() + + return EtsTypeFact.ArrayEtsTypeFact(typeFact) + } + + if ("length" in touchedPropertiesNames && "splice" in touchedPropertiesNames) { + return EtsTypeFact.ArrayEtsTypeFact(EtsTypeFact.AnyEtsTypeFact) + } + + return this +} + +private fun EtsTypeFact.ArrayEtsTypeFact.resolveArrayTypeFact( + scene: EtsScene, + propertyNameToClasses: Map>, +): EtsTypeFact.ArrayEtsTypeFact { + return if (elementType is EtsTypeFact.UnknownEtsTypeFact) { + this + } else { + val updatedElementType = elementType.resolveType(scene, propertyNameToClasses) + + if (updatedElementType === elementType) this else copy(elementType = elementType) + } +} + +fun EtsTypeFact.simplify(): EtsTypeFact { + return when (this) { + is EtsTypeFact.UnionEtsTypeFact -> simplifyUnionTypeFact() + is EtsTypeFact.IntersectionEtsTypeFact -> simplifyIntersectionTypeFact() + is EtsTypeFact.GuardedTypeFact -> TODO("Guarded type facts are unsupported in simplification") + is EtsTypeFact.ArrayEtsTypeFact -> { + val elementType = elementType.simplify() + + if (elementType === this.elementType) this else copy(elementType = elementType) + } + + is EtsTypeFact.ObjectEtsTypeFact -> { + if (cls != null) { + return this + } + + val props = properties.mapValues { it.value.simplify() } + copy(properties = props) + } + + else -> this + } +} + +private fun EtsTypeFact.IntersectionEtsTypeFact.simplifyIntersectionTypeFact(): EtsTypeFact { + val simplifiedArgs = types.map { it.simplify() } + + simplifiedArgs.singleOrNull()?.let { return it } + + val updatedTypeFacts = hashSetOf() + + val (objectClassFacts, otherFacts) = simplifiedArgs.partition { + it is EtsTypeFact.ObjectEtsTypeFact && it.cls == null + } + + updatedTypeFacts.addAll(otherFacts) + + if (objectClassFacts.isNotEmpty()) { + val allProperties = hashMapOf>().withDefault { hashSetOf() } + + objectClassFacts.forEach { fact -> + fact as EtsTypeFact.ObjectEtsTypeFact + + fact.properties.forEach { (name, propertyFact) -> + allProperties.getValue(name).add(propertyFact) + } + } + + val mergedAllProperties = hashMapOf() + allProperties.forEach { (name, propertyFact) -> + mergedAllProperties[name] = EtsTypeFact.mkUnionType(propertyFact) + } + + updatedTypeFacts += EtsTypeFact.ObjectEtsTypeFact(cls = null, properties = mergedAllProperties) + } + + return EtsTypeFact.mkIntersectionType(updatedTypeFacts) +} + +private fun EtsTypeFact.UnionEtsTypeFact.simplifyUnionTypeFact(): EtsTypeFact { + val simplifiedArgs = types.map { it.simplify() } + + simplifiedArgs.singleOrNull()?.let { return it } + + val updatedTypeFacts = hashSetOf() + + var atLeastOneNonEmptyObjectFound = false + var emptyTypeObjectFact: EtsTypeFact? = null + + simplifiedArgs.forEach { + if (it !is EtsTypeFact.ObjectEtsTypeFact) { + updatedTypeFacts += it + return@forEach + } + + if (it.cls != null) { + atLeastOneNonEmptyObjectFound = true + updatedTypeFacts += it + return@forEach + } + + if (it.properties.isEmpty() && emptyTypeObjectFact == null) { + emptyTypeObjectFact = it + } else { + updatedTypeFacts += it + atLeastOneNonEmptyObjectFound = true + } + } + + // take a fact `Object {}` only if there were no other objects in the facts + emptyTypeObjectFact?.let { + if (!atLeastOneNonEmptyObjectFound) { + updatedTypeFacts += it + } + } + + return EtsTypeFact.mkUnionType(updatedTypeFacts) +} diff --git a/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/TypeInferenceManager.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/TypeInferenceManager.kt new file mode 100644 index 0000000000..09e37311b5 --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/TypeInferenceManager.kt @@ -0,0 +1,712 @@ +package org.usvm.dataflow.ts.infer + +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout +import org.jacodb.ets.base.ANONYMOUS_CLASS_PREFIX +import org.jacodb.ets.base.CONSTRUCTOR_NAME +import org.jacodb.ets.base.EtsReturnStmt +import org.jacodb.ets.base.EtsStmt +import org.jacodb.ets.base.EtsStringType +import org.jacodb.ets.base.EtsType +import org.jacodb.ets.base.INSTANCE_INIT_METHOD_NAME +import org.jacodb.ets.graph.findDominators +import org.jacodb.ets.model.EtsMethod +import org.jacodb.impl.cfg.graphs.GraphDominators +import org.usvm.dataflow.graph.reversed +import org.usvm.dataflow.ifds.ControlEvent +import org.usvm.dataflow.ifds.Edge +import org.usvm.dataflow.ifds.Manager +import org.usvm.dataflow.ifds.QueueEmptinessChanged +import org.usvm.dataflow.ifds.SingletonUnit +import org.usvm.dataflow.ifds.UniRunner +import org.usvm.dataflow.ts.graph.EtsApplicationGraph +import org.usvm.dataflow.ts.infer.EtsTypeFact.Companion.allStringProperties +import org.usvm.dataflow.ts.util.EtsTraits +import java.util.concurrent.ConcurrentHashMap +import kotlin.time.Duration.Companion.seconds + +class TypeInferenceManager( + val traits: EtsTraits, + val graph: EtsApplicationGraph, +) : Manager { + private lateinit var runnerFinished: CompletableDeferred + + private val backwardSummaries = ConcurrentHashMap>() + private val forwardSummaries = ConcurrentHashMap>() + + private val methodDominatorsCache = ConcurrentHashMap>() + + private fun methodDominators(method: EtsMethod): GraphDominators = + methodDominatorsCache.computeIfAbsent(method) { + method.flowGraph().findDominators() + } + + private val savedTypes: ConcurrentHashMap> = ConcurrentHashMap() + + fun analyze( + entrypoints: List, + allMethods: List = entrypoints, + doAddKnownTypes: Boolean = true, + doInferAllLocals: Boolean = true, + ): TypeInferenceResult = runBlocking(Dispatchers.Default) { + val methodTypeScheme = collectSummaries( + startMethods = entrypoints, + doAddKnownTypes = doAddKnownTypes, + ) + val remainingMethodsForAnalysis = allMethods.filter { it !in methodTypeScheme.keys } + + val updatedTypeScheme = if (remainingMethodsForAnalysis.isEmpty()) { + methodTypeScheme + } else { + collectSummaries( + startMethods = remainingMethodsForAnalysis, + doAddKnownTypes = doAddKnownTypes, + ) + } + + createResultsFromSummaries(updatedTypeScheme, doInferAllLocals) + } + + private suspend fun collectSummaries( + startMethods: List, + doAddKnownTypes: Boolean = true, + ): Map = coroutineScope { + logger.info { "Preparing backward analysis" } + val backwardGraph = graph.reversed + val backwardAnalyzer = BackwardAnalyzer(backwardGraph, savedTypes, ::methodDominators) + val backwardRunner = UniRunner( + traits = traits, + manager = this@TypeInferenceManager, + graph = backwardGraph, + analyzer = backwardAnalyzer, + unitResolver = { SingletonUnit }, + unit = SingletonUnit, + zeroFact = BackwardTypeDomainFact.Zero, + ) + + val backwardJob = launch(start = CoroutineStart.LAZY) { + backwardRunner.run(startMethods) + } + + logger.info { "Running backward analysis" } + runnerFinished = CompletableDeferred() + backwardJob.start() + runnerFinished.await() + backwardJob.cancelAndJoin() + logger.info { "Backward analysis finished" } + + // logger.info { + // buildString { + // appendLine("Backward summaries: (${backwardSummaries.size})") + // for ((method, summaries) in backwardSummaries) { + // appendLine("=== Backward summaries for ${method.signature.enclosingClass.name}::${method.name}: (${summaries.size})") + // for (summary in summaries) { + // appendLine(" ${summary.initialFact} -> ${summary.exitFact}") + // } + // } + // } + // } + + val methodTypeScheme = methodTypeScheme() + + logger.info { + buildString { + appendLine("Backward types:") + for ((method, typeFacts) in methodTypeScheme) { + appendLine("Backward types for ${method.enclosingClass.name}::${method.name} in ${method.enclosingClass.file}:") + for ((base, fact) in typeFacts.types.entries.sortedBy { + when (val key = it.key) { + is AccessPathBase.This -> 0 + is AccessPathBase.Arg -> key.index + 1 + else -> 1_000_000 + } + }) { + appendLine("$base: ${fact.toPrettyString()}") + } + } + } + } + + val typeInfo: Map = savedTypes.mapValues { (type, facts) -> + val typeFact = EtsTypeFact.ObjectEtsTypeFact(type, properties = emptyMap()) + facts.fold(typeFact as EtsTypeFact) { acc, it -> + acc.intersect(it) ?: run { + logger.error { "Empty intersection type: $acc & $it" } + acc + } + } + } + + logger.info { "Preparing forward analysis" } + val forwardGraph = graph + val forwardAnalyzer = ForwardAnalyzer(forwardGraph, methodTypeScheme, typeInfo, doAddKnownTypes) + val forwardRunner = UniRunner( + traits = traits, + manager = this@TypeInferenceManager, + graph = forwardGraph, + analyzer = forwardAnalyzer, + unitResolver = { SingletonUnit }, + unit = SingletonUnit, + zeroFact = ForwardTypeDomainFact.Zero, + ) + + val forwardJob = launch(start = CoroutineStart.LAZY) { + forwardRunner.run(startMethods) + } + + logger.info { "Running forward analysis" } + runnerFinished = CompletableDeferred() + forwardJob.start() + withTimeout(3600.seconds) { + runnerFinished.await() + } + forwardJob.cancelAndJoin() + logger.info { "Forward analysis finished" } + + // logger.info { + // buildString { + // appendLine("Forward summaries: (${forwardSummaries.size})") + // for ((method, summaries) in forwardSummaries) { + // appendLine("=== Forward summaries for ${method.signature.enclosingClass.name}::${method.name}: (${summaries.size})") + // for (summary in summaries) { + // appendLine(" ${summary.initialFact} -> ${summary.exitFact}") + // } + // } + // } + // } + + methodTypeScheme + } + + private fun createResultsFromSummaries( + methodTypeScheme: Map, + doInferAllLocals: Boolean, + ): TypeInferenceResult { + val refinedTypes = refineMethodTypes(methodTypeScheme).toMutableMap() + logger.info { + buildString { + appendLine("Forward types:") + for ((method, typeFacts) in refinedTypes) { + appendLine("Forward types for ${method.signature.enclosingClass.name}::${method.name} in ${method.signature.enclosingClass.file}:") + for ((base, fact) in typeFacts.types.entries.sortedBy { + when (val key = it.key) { + is AccessPathBase.This -> 0 + is AccessPathBase.Arg -> key.index + 1 + else -> 1_000_000 + } + }) { + appendLine("$base: ${fact.toPrettyString()}") + } + } + } + } + + // Infer types for 'this' in each class + val inferredCombinedThisTypes = run { + val allClasses = methodTypeScheme.keys + .map { it.enclosingClass } + .distinct() + .map { sig -> graph.cp.projectAndSdkClasses.first { cls -> cls.signature == sig } } + .filterNot { it.name.startsWith(ANONYMOUS_CLASS_PREFIX) } + allClasses.mapNotNull { cls -> + val combinedBackwardType = + methodTypeScheme.asSequence().filter { (method, _) -> method in (cls.methods + cls.ctor) } + .mapNotNull { (_, facts) -> facts.types[AccessPathBase.This] }.reduceOrNull { acc, type -> + val intersection = acc.intersect(type) + + if (intersection == null) { + System.err.println("Empty intersection type: $acc & $type") + } + + intersection ?: acc + } + logger.info { + buildString { + appendLine("Combined backward type for This in class '${cls.signature}': ${combinedBackwardType?.toPrettyString()}") + } + } + + if (combinedBackwardType == null) { + return@mapNotNull null + } + + val typeFactsOnThisMethods = forwardSummaries.asSequence() + .filter { (method, _) -> method.enclosingClass == cls.signature } + .filter { (method, _) -> method.name != INSTANCE_INIT_METHOD_NAME } + .flatMap { (_, summaries) -> summaries.asSequence() } + .mapNotNull { it.initialFact as? ForwardTypeDomainFact.TypedVariable } + .filter { it.variable.base is AccessPathBase.This } + .toList() + .distinct() + + val typeFactsOnThisCtor = forwardSummaries.asSequence() + .filter { (method, _) -> method.enclosingClass == cls.signature } + .filter { (method, _) -> method.name == CONSTRUCTOR_NAME || method.name == INSTANCE_INIT_METHOD_NAME } + .flatMap { (_, summaries) -> summaries.asSequence() } + .mapNotNull { it.exitFact as? ForwardTypeDomainFact.TypedVariable } + .filter { it.variable.base is AccessPathBase.This } + .toList() + .distinct() + + val typeFactsOnThis = (typeFactsOnThisMethods + typeFactsOnThisCtor).distinct() + + val propertyRefinements = typeFactsOnThis + .groupBy({ it.variable.accesses }, { it.type }) + .mapValues { (_, types) -> types.reduce { acc, t -> acc.union(t) } } + + logger.info { + buildString { + appendLine("Property refinements for This in class '${cls.signature}':") + for ((property, propertyType) in propertyRefinements.toList() + .sortedBy { it.first.joinToString(".") }) { + appendLine(" this.${property.joinToString(".")}: $propertyType") + } + } + } + + var refined: EtsTypeFact = combinedBackwardType + for ((property, propertyType) in propertyRefinements) { + refined = refined.refineProperty(property, propertyType) ?: this@TypeInferenceManager.run { + System.err.println("Empty intersection type: $combinedBackwardType[$property] & $propertyType") + refined + } + } + + combinedBackwardType.let {} + typeFactsOnThisMethods.let {} + typeFactsOnThisCtor.let {} + typeFactsOnThis.let {} + propertyRefinements.let {} + cls.let {} + + cls.signature to refined + }.toMap() + } + logger.info { + buildString { + appendLine("Combined and refined types for This:") + for ((cls, type) in inferredCombinedThisTypes) { + appendLine("Combined This in class '${cls}': ${type.toPrettyString()}") + } + } + } + + // Infer return types for each method + val inferredReturnTypes = run { + forwardSummaries.asSequence().mapNotNull { (method, summaries) -> + val typeFacts = summaries.asSequence().map { it.exitVertex }.mapNotNull { + val stmt = it.statement as? EtsReturnStmt ?: return@mapNotNull null + val fact = it.fact as? ForwardTypeDomainFact.TypedVariable ?: return@mapNotNull null + val r = stmt.returnValue?.toPath() ?: return@mapNotNull null + check(r.accesses.isEmpty()) + if (fact.variable.base != r.base) return@mapNotNull null + r.base to fact + }.groupBy({ it.first }, { it.second }) + + val returnFact = typeFacts.mapValues { (_, typeRefinements) -> + val propertyRefinements = typeRefinements.groupBy({ it.variable.accesses }, { it.type }) + .mapValues { (_, types) -> types.reduce { acc, t -> acc.union(t) } } + + val rootType = propertyRefinements[emptyList()] ?: run { + if (propertyRefinements.keys.any { it.isNotEmpty() }) { + EtsTypeFact.ObjectEtsTypeFact(null, emptyMap()) + } else { + EtsTypeFact.AnyEtsTypeFact + } + } + + val refined = rootType.refineProperties(emptyList(), propertyRefinements) + + refined + }.values.reduceOrNull { acc, type -> acc.union(type) } + if (returnFact != null) { + method to returnFact + } else { + null + } + }.toMap() + } + logger.info { + buildString { + appendLine("Return types:") + for ((method, type) in inferredReturnTypes) { + appendLine("Return type for ${method.signature.enclosingClass.file}::${method.signature.enclosingClass.name}::${method.name}: ${type.toPrettyString()}") + } + } + } + + val inferredLocalTypes: Map>? = if (doInferAllLocals) { + forwardSummaries.asSequence().map { (method, summaries) -> + val typeFacts = summaries.asSequence() + .mapNotNull { it.exitVertex.fact as? ForwardTypeDomainFact.TypedVariable } + .filter { it.variable.base is AccessPathBase.Local } + .groupBy { it.variable.base } + + val localTypes = typeFacts.mapValues { (_, typeFacts) -> + val propertyRefinements = typeFacts + .groupBy({ it.variable.accesses }, { it.type }) + .mapValues { (_, types) -> types.reduce { acc, t -> acc.union(t) } } + + val rootType = propertyRefinements[emptyList()] ?: run { + if (propertyRefinements.keys.any { it.isNotEmpty() }) { + EtsTypeFact.ObjectEtsTypeFact(null, emptyMap()) + } else { + EtsTypeFact.AnyEtsTypeFact + } + } + + val refined = rootType.refineProperties(emptyList(), propertyRefinements) + + refined + } + + method to localTypes + }.toMap() + } else { + null + } + + if (inferredLocalTypes != null) { + logger.info { + buildString { + appendLine("Local types:") + for ((method, localTypes) in inferredLocalTypes) { + appendLine("Local types for ${method.signature.enclosingClass.name}::${method.name} in ${method.signature.enclosingClass.file}:") + for ((base, fact) in localTypes.entries.sortedBy { (it.key as AccessPathBase.Local).name }) { + appendLine("$base: ${fact.toPrettyString()}") + } + } + } + } + + for ((method, localFacts) in inferredLocalTypes) { + val facts = refinedTypes.getValue(method) + refinedTypes[method] = facts.copy(types = facts.types + localFacts) + } + } + + val inferredTypes = refinedTypes + // Extract 'types': + .mapValues { (_, facts) -> facts.types } + // Sort by 'base': + .mapValues { (_, types) -> + types.entries.sortedBy { + when (val key = it.key) { + is AccessPathBase.This -> 0 + is AccessPathBase.Arg -> key.index + 1 + else -> 1_000_000 + } + }.associate { it.key to it.value } + }.mapValues { (_, methodFacts) -> + methodFacts.mapValues { (_, fact) -> + fact.simplify() + } + } + + return TypeInferenceResult( + inferredTypes = inferredTypes, + inferredReturnType = inferredReturnTypes, + inferredCombinedThisType = inferredCombinedThisTypes, + ) + } + + private fun methodTypeScheme(): Map = + backwardSummaries.mapValues { (method, summaries) -> + buildMethodTypeScheme(method, summaries) + } + + private fun refineMethodTypes(schema: Map): Map = + schema.mapValues { (method, facts) -> + val summaries = forwardSummaries[method].orEmpty() + refineMethodTypes(facts, summaries) + } + + private fun buildMethodTypeScheme( + method: EtsMethod, + summaries: Iterable, + ): EtsMethodTypeFacts { + val types = summaries + .mapNotNull { it.exitFact as? BackwardTypeDomainFact.TypedVariable } + .groupBy({ it.variable }, { it.type }) + .filter { (base, _) -> base is AccessPathBase.This || base is AccessPathBase.Arg } + .mapValues { (_, typeFacts) -> + typeFacts.reduce { acc, typeFact -> + val intersection = acc.intersect(typeFact) + + if (intersection == null) { + System.err.println("Empty intersection type: $acc & $typeFact") + } + + intersection ?: acc + } + } + + return EtsMethodTypeFacts(method, types) + } + + private fun refineMethodTypes( + facts: EtsMethodTypeFacts, // backward types + summaries: Iterable, + ): EtsMethodTypeFacts { + // Contexts + val typeFacts = summaries.asSequence() + .mapNotNull { it.initialFact as? ForwardTypeDomainFact.TypedVariable } + .filter { it.variable.base is AccessPathBase.This || it.variable.base is AccessPathBase.Arg } + .groupBy { it.variable.base } + + val refinedTypes = facts.types.mapValues { (base, type) -> + val typeRefinements = typeFacts[base] ?: return@mapValues type + + val propertyRefinements = typeRefinements + .groupBy({ it.variable.accesses }, { it.type }) + .mapValues { (_, types) -> types.reduce { acc, t -> acc.union(t) } } + + val rootType = propertyRefinements[emptyList()] ?: run { + if (propertyRefinements.keys.any { it.isNotEmpty() }) { + EtsTypeFact.ObjectEtsTypeFact(null, emptyMap()) + } else { + EtsTypeFact.AnyEtsTypeFact + } + } + + val refined = rootType.refineProperties(emptyList(), propertyRefinements) + + refined + } + + typeFacts.let {} + + return EtsMethodTypeFacts(facts.method, refinedTypes) + } + + private fun EtsTypeFact.refineProperties( + pathFromRootObject: List, + typeRefinements: Map, EtsTypeFact>, + ): EtsTypeFact = when (this) { + is EtsTypeFact.NumberEtsTypeFact -> this + is EtsTypeFact.StringEtsTypeFact -> this + is EtsTypeFact.FunctionEtsTypeFact -> this + is EtsTypeFact.AnyEtsTypeFact -> this + is EtsTypeFact.BooleanEtsTypeFact -> this + is EtsTypeFact.NullEtsTypeFact -> this + is EtsTypeFact.UndefinedEtsTypeFact -> this + + is EtsTypeFact.UnknownEtsTypeFact -> { + // logger.warn { "Unknown type after forward analysis" } + EtsTypeFact.AnyEtsTypeFact + } + + is EtsTypeFact.ArrayEtsTypeFact -> { + // TODO: array types + logger.error { "TODO: Array type $this" } + + val elementPath = pathFromRootObject + ElementAccessor + val refinedElemType = + typeRefinements[elementPath]?.intersect(elementType) ?: elementType // todo: mb exception??? + val elemType = refinedElemType.refineProperties(elementPath, typeRefinements) + + EtsTypeFact.ArrayEtsTypeFact(elemType) + } + + is EtsTypeFact.ObjectEtsTypeFact -> refineProperties(pathFromRootObject, typeRefinements) + + is EtsTypeFact.UnionEtsTypeFact -> EtsTypeFact.mkUnionType( + types.mapTo(hashSetOf()) { + it.refineProperties( + pathFromRootObject, + typeRefinements, + ) + } + ) + + is EtsTypeFact.IntersectionEtsTypeFact -> EtsTypeFact.mkIntersectionType( + types.mapTo(hashSetOf()) { + it.refineProperties( + pathFromRootObject, + typeRefinements, + ) + } + ) + + is EtsTypeFact.GuardedTypeFact -> type + .refineProperties(pathFromRootObject, typeRefinements) + .withGuard(guard, guardNegated) + } + + private fun EtsTypeFact.ObjectEtsTypeFact.refineProperties( + pathFromRootObject: List, + typeRefinements: Map, EtsTypeFact>, + ): EtsTypeFact { + val refinedProperties = properties.mapValues { (propertyName, type) -> + val propertyAccessor = FieldAccessor(propertyName) + val propertyPath = pathFromRootObject + propertyAccessor + val refinedType = typeRefinements[propertyPath]?.intersect(type) ?: type // todo: mb exception??? + refinedType.refineProperties(propertyPath, typeRefinements) + } + return EtsTypeFact.ObjectEtsTypeFact(cls, refinedProperties) + } + + private fun EtsTypeFact.refineProperty(property: List, type: EtsTypeFact): EtsTypeFact? = when (this) { + is EtsTypeFact.BasicType -> refineProperty(property, type) + + is EtsTypeFact.GuardedTypeFact -> this.type.refineProperty(property, type)?.withGuard(guard, guardNegated) + + is EtsTypeFact.IntersectionEtsTypeFact -> EtsTypeFact.mkIntersectionType(types.mapTo(hashSetOf()) { + it.refineProperty( + property, + type + ) ?: return null + }) + + is EtsTypeFact.UnionEtsTypeFact -> EtsTypeFact.mkUnionType(types.mapNotNullTo(hashSetOf()) { + it.refineProperty( + property, + type + ) + }) + } + + private fun EtsTypeFact.BasicType.refineProperty( + property: List, + type: EtsTypeFact, + ): EtsTypeFact? { + when (this) { + EtsTypeFact.AnyEtsTypeFact, + EtsTypeFact.FunctionEtsTypeFact, + EtsTypeFact.NumberEtsTypeFact, + EtsTypeFact.BooleanEtsTypeFact, + EtsTypeFact.StringEtsTypeFact, + EtsTypeFact.NullEtsTypeFact, + EtsTypeFact.UndefinedEtsTypeFact, + -> return if (property.isNotEmpty()) this else intersect(type) + + is EtsTypeFact.ArrayEtsTypeFact -> { + // TODO: the following check(property.size == 1) fails on multiple projects + // check(property.size == 1) + if (property.size == 1) { + // val p = property.single() + // check(p is ElementAccessor) + val t = elementType.intersect(type) ?: error("Empty intersection") + return EtsTypeFact.ArrayEtsTypeFact(elementType = t) + } else { + return EtsTypeFact.AnyEtsTypeFact + } + } + + is EtsTypeFact.UnknownEtsTypeFact -> { + // .f.g:T -> {f: {g: T}} + // .f[i].g:T -> {f: Array<{g: T}>} + var t = type + for (p in property.reversed()) { + if (p is FieldAccessor) { + t = EtsTypeFact.ObjectEtsTypeFact( + cls = null, + properties = mapOf(p.name to t), + ) + } else { + t = EtsTypeFact.ArrayEtsTypeFact(elementType = t) + } + } + return t + } + + is EtsTypeFact.ObjectEtsTypeFact -> { + val propertyAccessor = property.firstOrNull() as? FieldAccessor + if (propertyAccessor == null) { + // TODO: handle 'type=union' by exploding it into multiple ObjectFacts (later combined with union) with class names from union. + if (type is EtsTypeFact.UnionEtsTypeFact) { + return type.types.map { + refineProperty(property, it) ?: return null + }.reduce { acc: EtsTypeFact, t: EtsTypeFact -> acc.union(t) } + } + + if (type is EtsTypeFact.StringEtsTypeFact) { + // intersect(this:object, type:string) + + if (cls == EtsStringType) return type + if (cls != null) return null + + val intersectionProperties = properties + .filter { it.key in allStringProperties } + .mapValues { (_, type) -> + // TODO: intersect with the corresponding type of String's property + type + } + + return EtsTypeFact.ObjectEtsTypeFact(cls, intersectionProperties) + } + + if (type is EtsTypeFact.NullEtsTypeFact) { + // intersect(this:object, type:null) + // return EtsTypeFact.NullEtsTypeFact + return EtsTypeFact.mkUnionType(this, EtsTypeFact.NullEtsTypeFact) + } + + if (type is EtsTypeFact.UndefinedEtsTypeFact) { + // intersect(this:object, type:undefined) + // return EtsTypeFact.UndefinedEtsTypeFact + return EtsTypeFact.mkUnionType(this, EtsTypeFact.UndefinedEtsTypeFact) + } + + if (type !is EtsTypeFact.ObjectEtsTypeFact || cls != null) { + // todo: hack + if (type is EtsTypeFact.AnyEtsTypeFact) return this + + // TODO("Unexpected: $this & $type") + return this + } + + return EtsTypeFact.ObjectEtsTypeFact(type.cls, properties) + } + + val propertyType = properties[propertyAccessor.name] ?: return this + val refinedProperty = propertyType.refineProperty(property.drop(1), type) ?: return null + val properties = this.properties + (propertyAccessor.name to refinedProperty) + return EtsTypeFact.ObjectEtsTypeFact(cls, properties) + } + } + } + + override fun handleEvent(event: AnalyzerEvent) { + when (event) { + is BackwardSummaryAnalyzerEvent -> { + backwardSummaries.computeIfAbsent(event.method) { + ConcurrentHashMap.newKeySet() + }.add(event) + } + + is ForwardSummaryAnalyzerEvent -> { + forwardSummaries.computeIfAbsent(event.method) { + ConcurrentHashMap.newKeySet() + }.add(event) + } + } + } + + override fun subscribeOnSummaryEdges( + method: EtsMethod, + scope: CoroutineScope, + handler: (Edge) -> Unit, + ) { + error("No cross unit subscriptions") + } + + override fun handleControlEvent(event: ControlEvent) { + if (event is QueueEmptinessChanged) { + if (event.isEmpty) { + runnerFinished.complete(Unit) + } + } + } + + companion object { + val logger = mu.KotlinLogging.logger {} + } +} diff --git a/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/TypeInferenceResult.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/TypeInferenceResult.kt new file mode 100644 index 0000000000..d4d991bab7 --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/TypeInferenceResult.kt @@ -0,0 +1,57 @@ +/* + * 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 + +import org.jacodb.ets.model.EtsClass +import org.jacodb.ets.model.EtsClassSignature +import org.jacodb.ets.model.EtsMethod +import org.jacodb.ets.model.EtsScene + +data class TypeInferenceResult( + val inferredTypes: Map>, + val inferredReturnType: Map, + val inferredCombinedThisType: Map, +) { + fun withGuessedTypes(scene: EtsScene): TypeInferenceResult { + val propertyNameToClasses = precalculateCaches(scene) + + return TypeInferenceResult( + inferredTypes = guessTypes(scene, inferredTypes, propertyNameToClasses), + inferredReturnType = inferredReturnType.mapValues { (_, fact) -> + fact.resolveType(scene, propertyNameToClasses) + }, + inferredCombinedThisType = inferredCombinedThisType.mapValues { (_, fact) -> + fact.resolveType(scene, propertyNameToClasses = propertyNameToClasses) + }, + ) + } + + private fun precalculateCaches(scene: EtsScene): Map> { + val result = hashMapOf>() + + scene.projectAndSdkClasses.forEach { clazz -> + clazz.methods.forEach { + result.computeIfAbsent(it.name) { hashSetOf() }.add(clazz) + } + clazz.fields.forEach { + result.computeIfAbsent(it.name) { hashSetOf() }.add(clazz) + } + } + + return result + } +} diff --git a/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/annotation/ExprTypeAnnotator.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/annotation/ExprTypeAnnotator.kt new file mode 100644 index 0000000000..16cdbf1419 --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/annotation/ExprTypeAnnotator.kt @@ -0,0 +1,279 @@ +/* + * 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.annotation + +import org.jacodb.ets.base.EtsAddExpr +import org.jacodb.ets.base.EtsAndExpr +import org.jacodb.ets.base.EtsAwaitExpr +import org.jacodb.ets.base.EtsBitAndExpr +import org.jacodb.ets.base.EtsBitNotExpr +import org.jacodb.ets.base.EtsBitOrExpr +import org.jacodb.ets.base.EtsBitXorExpr +import org.jacodb.ets.base.EtsCastExpr +import org.jacodb.ets.base.EtsClassType +import org.jacodb.ets.base.EtsCommaExpr +import org.jacodb.ets.base.EtsDeleteExpr +import org.jacodb.ets.base.EtsDivExpr +import org.jacodb.ets.base.EtsEntity +import org.jacodb.ets.base.EtsEqExpr +import org.jacodb.ets.base.EtsExpExpr +import org.jacodb.ets.base.EtsExpr +import org.jacodb.ets.base.EtsGtEqExpr +import org.jacodb.ets.base.EtsGtExpr +import org.jacodb.ets.base.EtsInExpr +import org.jacodb.ets.base.EtsInstanceCallExpr +import org.jacodb.ets.base.EtsInstanceOfExpr +import org.jacodb.ets.base.EtsLeftShiftExpr +import org.jacodb.ets.base.EtsLengthExpr +import org.jacodb.ets.base.EtsLocal +import org.jacodb.ets.base.EtsLtEqExpr +import org.jacodb.ets.base.EtsLtExpr +import org.jacodb.ets.base.EtsMulExpr +import org.jacodb.ets.base.EtsNegExpr +import org.jacodb.ets.base.EtsNewArrayExpr +import org.jacodb.ets.base.EtsNewExpr +import org.jacodb.ets.base.EtsNotEqExpr +import org.jacodb.ets.base.EtsNotExpr +import org.jacodb.ets.base.EtsNullishCoalescingExpr +import org.jacodb.ets.base.EtsOrExpr +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.EtsRightShiftExpr +import org.jacodb.ets.base.EtsStaticCallExpr +import org.jacodb.ets.base.EtsStrictEqExpr +import org.jacodb.ets.base.EtsStrictNotEqExpr +import org.jacodb.ets.base.EtsSubExpr +import org.jacodb.ets.base.EtsTernaryExpr +import org.jacodb.ets.base.EtsTypeOfExpr +import org.jacodb.ets.base.EtsUnaryPlusExpr +import org.jacodb.ets.base.EtsUnsignedRightShiftExpr +import org.jacodb.ets.base.EtsValue +import org.jacodb.ets.base.EtsVoidExpr +import org.jacodb.ets.base.EtsYieldExpr +import org.jacodb.ets.model.EtsScene + +class ExprTypeAnnotator( + private val valueAnnotator: ValueTypeAnnotator, + private val scene: EtsScene, +) : EtsExpr.Visitor { + private fun inferEntity(entity: EtsEntity) = when (entity) { + is EtsExpr -> entity.accept(this) + is EtsValue -> entity.accept(valueAnnotator) + else -> error("Unsupported entity") + } + + private fun inferValue(value: EtsValue) = value.accept(valueAnnotator) + + override fun visit(expr: EtsNewExpr) = expr + + override fun visit(expr: EtsNewArrayExpr) = expr + + override fun visit(expr: EtsLengthExpr) = expr + + override fun visit(expr: EtsCastExpr) = expr.copy(arg = inferEntity(expr.arg)) + + override fun visit(expr: EtsInstanceOfExpr) = expr.copy(arg = inferEntity(expr.arg)) + + override fun visit(expr: EtsDeleteExpr) = expr.copy(arg = inferEntity(expr.arg)) + + override fun visit(expr: EtsAwaitExpr) = expr.copy(arg = inferEntity(expr.arg)) + + override fun visit(expr: EtsYieldExpr) = expr.copy(arg = inferEntity(expr.arg)) + + override fun visit(expr: EtsTypeOfExpr) = expr.copy(arg = inferEntity(expr.arg)) + + override fun visit(expr: EtsVoidExpr) = expr.copy(arg = inferEntity(expr.arg)) + + override fun visit(expr: EtsNotExpr) = expr.copy(arg = inferEntity(expr.arg)) + + override fun visit(expr: EtsBitNotExpr) = expr.copy(arg = inferEntity(expr.arg)) + + override fun visit(expr: EtsNegExpr) = expr.copy(arg = inferEntity(expr.arg)) + + override fun visit(expr: EtsUnaryPlusExpr) = expr.copy(arg = inferEntity(expr.arg)) + + override fun visit(expr: EtsPreIncExpr) = expr.copy(arg = inferEntity(expr.arg)) + + override fun visit(expr: EtsPreDecExpr) = expr.copy(arg = inferEntity(expr.arg)) + + override fun visit(expr: EtsPostIncExpr) = expr.copy(arg = inferEntity(expr.arg)) + + override fun visit(expr: EtsPostDecExpr) = expr.copy(arg = inferEntity(expr.arg)) + + override fun visit(expr: EtsEqExpr) = expr.copy( + left = inferEntity(expr.left), + right = inferEntity(expr.right) + ) + + override fun visit(expr: EtsNotEqExpr) = expr.copy( + left = inferEntity(expr.left), + right = inferEntity(expr.right) + ) + + override fun visit(expr: EtsStrictEqExpr) = expr.copy( + left = inferEntity(expr.left), + right = inferEntity(expr.right) + ) + + override fun visit(expr: EtsStrictNotEqExpr) = expr.copy( + left = inferEntity(expr.left), + right = inferEntity(expr.right) + ) + + override fun visit(expr: EtsLtExpr) = expr.copy( + left = inferEntity(expr.left), + right = inferEntity(expr.right) + ) + + override fun visit(expr: EtsLtEqExpr) = expr.copy( + left = inferEntity(expr.left), + right = inferEntity(expr.right) + ) + + override fun visit(expr: EtsGtExpr) = expr.copy( + left = inferEntity(expr.left), + right = inferEntity(expr.right) + ) + + override fun visit(expr: EtsGtEqExpr) = expr.copy( + left = inferEntity(expr.left), + right = inferEntity(expr.right) + ) + + override fun visit(expr: EtsInExpr) = expr.copy( + left = inferEntity(expr.left), + right = inferEntity(expr.right) + ) + + override fun visit(expr: EtsAddExpr) = expr.copy( + left = inferEntity(expr.left), + right = inferEntity(expr.right) + ) + + override fun visit(expr: EtsSubExpr) = expr.copy( + left = inferEntity(expr.left), + right = inferEntity(expr.right) + ) + + override fun visit(expr: EtsMulExpr) = expr.copy( + left = inferEntity(expr.left), + right = inferEntity(expr.right) + ) + + override fun visit(expr: EtsDivExpr) = expr.copy( + left = inferEntity(expr.left), + right = inferEntity(expr.right) + ) + + override fun visit(expr: EtsRemExpr) = expr.copy( + left = inferEntity(expr.left), + right = inferEntity(expr.right) + ) + + override fun visit(expr: EtsExpExpr) = expr.copy( + left = inferEntity(expr.left), + right = inferEntity(expr.right) + ) + + override fun visit(expr: EtsBitAndExpr) = expr.copy( + left = inferEntity(expr.left), + right = inferEntity(expr.right) + ) + + override fun visit(expr: EtsBitOrExpr) = expr.copy( + left = inferEntity(expr.left), + right = inferEntity(expr.right) + ) + + override fun visit(expr: EtsBitXorExpr) = expr.copy( + left = inferEntity(expr.left), + right = inferEntity(expr.right) + ) + + override fun visit(expr: EtsLeftShiftExpr) = expr.copy( + left = inferEntity(expr.left), + right = inferEntity(expr.right) + ) + + override fun visit(expr: EtsRightShiftExpr) = expr.copy( + left = inferEntity(expr.left), + right = inferEntity(expr.right) + ) + + override fun visit(expr: EtsUnsignedRightShiftExpr) = expr.copy( + left = inferEntity(expr.left), + right = inferEntity(expr.right) + ) + + override fun visit(expr: EtsAndExpr) = expr.copy( + left = inferEntity(expr.left), + right = inferEntity(expr.right) + ) + + override fun visit(expr: EtsOrExpr) = expr.copy( + left = inferEntity(expr.left), + right = inferEntity(expr.right) + ) + + override fun visit(expr: EtsNullishCoalescingExpr) = expr.copy( + left = inferEntity(expr.left), + right = inferEntity(expr.right) + ) + + override fun visit(expr: EtsInstanceCallExpr): EtsExpr { + val baseInferred = inferValue(expr.instance) as EtsLocal + val argsInferred = expr.args.map(this::inferValue) + val methodInferred = when (val baseType = baseInferred.type) { + is EtsClassType -> { + val etsClass = scene.projectAndSdkClasses.find { it.signature == baseType.signature } + ?: return expr.copy(instance = baseInferred, args = argsInferred) + val callee = etsClass.methods.find { it.signature == expr.method } + ?: return expr.copy(instance = baseInferred, args = argsInferred) + callee.signature + } + + else -> expr.method + } + return EtsInstanceCallExpr(baseInferred, methodInferred, argsInferred) + } + + override fun visit(expr: EtsStaticCallExpr): EtsExpr { + val argsInferred = expr.args.map(this::inferValue) + return EtsStaticCallExpr(expr.method, argsInferred) + } + + override fun visit(expr: EtsPtrCallExpr): EtsExpr { + val ptrInferred = inferValue(expr.ptr) as EtsLocal + val argsInferred = expr.args.map(this::inferValue) + return EtsPtrCallExpr(ptrInferred, expr.method, argsInferred) + } + + override fun visit(expr: EtsCommaExpr) = expr.copy( + left = inferEntity(expr.left), + right = inferEntity(expr.right) + ) + + override fun visit(expr: EtsTernaryExpr) = expr.copy( + condition = inferEntity(expr.condition), + thenExpr = inferEntity(expr.thenExpr), + elseExpr = inferEntity(expr.elseExpr) + ) +} diff --git a/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/annotation/StmtTypeAnnotator.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/annotation/StmtTypeAnnotator.kt new file mode 100644 index 0000000000..110ca68abb --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/annotation/StmtTypeAnnotator.kt @@ -0,0 +1,83 @@ +/* + * 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.annotation + +import org.jacodb.ets.base.EtsAssignStmt +import org.jacodb.ets.base.EtsCallExpr +import org.jacodb.ets.base.EtsCallStmt +import org.jacodb.ets.base.EtsEntity +import org.jacodb.ets.base.EtsExpr +import org.jacodb.ets.base.EtsGotoStmt +import org.jacodb.ets.base.EtsIfStmt +import org.jacodb.ets.base.EtsNopStmt +import org.jacodb.ets.base.EtsReturnStmt +import org.jacodb.ets.base.EtsStmt +import org.jacodb.ets.base.EtsSwitchStmt +import org.jacodb.ets.base.EtsThrowStmt +import org.jacodb.ets.base.EtsValue +import org.jacodb.ets.model.EtsScene +import org.usvm.dataflow.ts.infer.AccessPathBase +import org.usvm.dataflow.ts.infer.EtsTypeFact + +class StmtTypeAnnotator( + types: Map, + thisType: EtsTypeFact?, + scene: EtsScene, +) : EtsStmt.Visitor { + private val valueAnnotator = ValueTypeAnnotator(types, thisType, scene) + private val exprAnnotator = ExprTypeAnnotator(valueAnnotator, scene) + + private fun inferValue(value: EtsValue) = value.accept(valueAnnotator) + + private fun inferExpr(expr: EtsExpr) = expr.accept(exprAnnotator) + + private fun inferEntity(entity: EtsEntity) = when (entity) { + is EtsExpr -> entity.accept(exprAnnotator) + is EtsValue -> entity.accept(valueAnnotator) + else -> error("Unsupported entity") + } + + override fun visit(stmt: EtsNopStmt) = stmt + + override fun visit(stmt: EtsAssignStmt) = stmt.copy( + lhv = inferValue(stmt.lhv), + rhv = inferEntity(stmt.rhv) + ) + + override fun visit(stmt: EtsCallStmt) = stmt.copy( + expr = inferExpr(stmt.expr) as EtsCallExpr + ) + + override fun visit(stmt: EtsReturnStmt) = stmt.copy( + returnValue = stmt.returnValue?.let(this::inferValue) + ) + + override fun visit(stmt: EtsThrowStmt) = stmt.copy( + arg = inferEntity(stmt.arg) + ) + + override fun visit(stmt: EtsGotoStmt) = stmt + + override fun visit(stmt: EtsIfStmt) = stmt.copy( + condition = inferEntity(stmt.condition) + ) + + override fun visit(stmt: EtsSwitchStmt) = stmt.copy( + arg = inferEntity(stmt.arg), + cases = stmt.cases.map(this::inferEntity) + ) +} diff --git a/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/annotation/ValueTypeAnnotator.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/annotation/ValueTypeAnnotator.kt new file mode 100644 index 0000000000..01df46cae6 --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/annotation/ValueTypeAnnotator.kt @@ -0,0 +1,113 @@ +/* + * 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.annotation + +import org.jacodb.ets.base.EtsArrayAccess +import org.jacodb.ets.base.EtsArrayType +import org.jacodb.ets.base.EtsClassType +import org.jacodb.ets.base.EtsInstanceFieldRef +import org.jacodb.ets.base.EtsLocal +import org.jacodb.ets.base.EtsParameterRef +import org.jacodb.ets.base.EtsStaticFieldRef +import org.jacodb.ets.base.EtsThis +import org.jacodb.ets.base.EtsType +import org.jacodb.ets.base.EtsValue +import org.jacodb.ets.model.EtsClassSignature +import org.jacodb.ets.model.EtsField +import org.jacodb.ets.model.EtsScene +import org.usvm.dataflow.ts.infer.AccessPathBase +import org.usvm.dataflow.ts.infer.EtsTypeFact +import org.usvm.dataflow.ts.infer.dto.toType + +class ValueTypeAnnotator( + private val types: Map, + private val thisType: EtsTypeFact?, + private val scene: EtsScene, +) : EtsValue.Visitor.Default { + private inline fun V.infer(base: AccessPathBase, apply: V.(T) -> V): V { + val type = types[base]?.toType() as? T ?: return this + return apply.invoke(this, type) + } + + override fun visit(value: EtsLocal): EtsLocal = + value.infer(AccessPathBase.Local(value.name)) { copy(type = it) } + + override fun visit(value: EtsThis): EtsValue = + (thisType?.toType() as? EtsClassType)?.let { value.copy(type = it) } + ?: value.infer(AccessPathBase.This) { copy(type = it) } + + override fun visit(value: EtsParameterRef) = + value.infer(AccessPathBase.Arg(value.index)) { copy(type = it) } + + override fun visit(value: EtsArrayAccess): EtsArrayAccess { + val arrayInferred = value.array.accept(this) + val arrayType = arrayInferred.type as? EtsArrayType ?: return value + val indexInferred = value.index.accept(this) + return EtsArrayAccess(arrayInferred, indexInferred, arrayType.elementType) + } + + // TODO: discuss (labeled with (Q)) + override fun visit(value: EtsInstanceFieldRef): EtsValue { + val instance = visit(value.instance) + val name = value.field.name + + fun findInClass(signature: EtsClassSignature): EtsField? = + scene.projectClasses + .singleOrNull { it.signature == signature } + ?.fields + ?.singleOrNull { it.name == name } + + // Try to determine field type using the scene + // (Q) Do we really need this step? + with(value.field) { + val etsField = findInClass(enclosingClass) ?: return@with + // Field was found in the scene + + // Check that inferred type is same with the declared one + // (Q) How should we (do we should?) handle the check violation here? + check(instance.type == EtsClassType(enclosingClass)) + + return EtsInstanceFieldRef(instance = instance, field = etsField.signature) + } + + // Field was not found by signature, then try infer instance type + val instanceTypeInfo = types[AccessPathBase.Local(instance.name)] as? EtsTypeFact.ObjectEtsTypeFact + // Instance type was neither specified in signature nor inferred, so no type info can be provided + // (Q) Should we check special properties of primitives (like `string.length`)? + ?: return value.copy(instance = instance) + + // Find field signature in inferred class fields + (instanceTypeInfo.cls as? EtsClassType)?.run { + val etsField = findInClass(signature) ?: return@run + // Field was found in the inferred class + return EtsInstanceFieldRef(instance = instance, field = etsField.signature) + } + + // Find field type in inferred fields - we don't know precisely the base class, but can infer the field type + instanceTypeInfo.properties[name]?.toType()?.let { fieldType -> + val fieldSubSignature = value.field.sub.copy(type = fieldType) + // (Q) General: Should we use our inferred type if there is a non-trivial type in the original scene? + return EtsInstanceFieldRef(instance = instance, field = value.field.copy(sub = fieldSubSignature)) + } + + return value.copy(instance = instance) + } + + override fun visit(value: EtsStaticFieldRef): EtsValue = value + + override fun defaultVisit(value: EtsValue): EtsValue = value +} diff --git a/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/annotation/withInferredTypes.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/annotation/withInferredTypes.kt new file mode 100644 index 0000000000..ee71d2cbe9 --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/annotation/withInferredTypes.kt @@ -0,0 +1,91 @@ +/* + * 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. + */ + +@file:Suppress("MemberVisibilityCanBePrivate") + +package org.usvm.dataflow.ts.infer.annotation + +import org.jacodb.ets.graph.EtsCfg +import org.jacodb.ets.model.EtsClass +import org.jacodb.ets.model.EtsClassImpl +import org.jacodb.ets.model.EtsFile +import org.jacodb.ets.model.EtsMethod +import org.jacodb.ets.model.EtsMethodImpl +import org.jacodb.ets.model.EtsScene +import org.usvm.dataflow.ts.infer.AccessPathBase +import org.usvm.dataflow.ts.infer.EtsTypeFact +import org.usvm.dataflow.ts.infer.TypeInferenceResult + +data class EtsTypeAnnotator( + val scene: EtsScene, + val typeInferenceResult: TypeInferenceResult, +) { + private fun selectTypesFor(method: EtsMethod) = typeInferenceResult.inferredTypes[method] ?: emptyMap() + + private fun combinedThisFor(method: EtsMethod) = typeInferenceResult.inferredCombinedThisType[method.enclosingClass] + + fun annotateWithTypes(scene: EtsScene) = with(scene) { + EtsScene( + projectFiles = projectFiles.map { annotateWithTypes(it) } + ) + } + + fun annotateWithTypes(file: EtsFile) = with(file) { + EtsFile( + signature = signature, + classes = classes.map { annotateWithTypes(it) }, + namespaces = namespaces, + ) + } + + fun annotateWithTypes(clazz: EtsClass) = with(clazz) { + EtsClassImpl( + signature = signature, + fields = fields, + methods = methods.map { annotateWithTypes(it) }, + ctor = annotateWithTypes(ctor), + superClass = superClass, // TODO: replace with inferred superclass + modifiers = modifiers, + decorators = decorators, + typeParameters = typeParameters, + ) + } + + fun annotateWithTypes(method: EtsMethod) = with(method) { + EtsMethodImpl( + signature = signature, + typeParameters = typeParameters, + locals = locals, + modifiers = modifiers, + decorators = decorators, + ).also { + it._cfg = annotateWithTypes(cfg, selectTypesFor(this), combinedThisFor(this)) + } + } + + fun annotateWithTypes( + cfg: EtsCfg, + types: Map, + thisType: EtsTypeFact?, + ) = with(cfg) { + with(StmtTypeAnnotator(types, thisType, scene)) { + EtsCfg( + stmts = stmts.map { it.accept(this) }, + successorMap = stmts.associateWith { successors(it).toList() }, + ) + } + } +} diff --git a/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/cli/InferTypes.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/cli/InferTypes.kt new file mode 100644 index 0000000000..42aaad2877 --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/cli/InferTypes.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. + */ + +@file:Suppress("MemberVisibilityCanBePrivate") + +package org.usvm.dataflow.ts.infer.cli + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.context +import com.github.ajalt.clikt.core.main +import com.github.ajalt.clikt.output.MordantHelpFormatter +import com.github.ajalt.clikt.parameters.options.convert +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.flag +import com.github.ajalt.clikt.parameters.options.multiple +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.required +import com.github.ajalt.clikt.parameters.types.path +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.encodeToStream +import mu.KotlinLogging +import org.jacodb.ets.base.ANONYMOUS_CLASS_PREFIX +import org.jacodb.ets.base.ANONYMOUS_METHOD_PREFIX +import org.jacodb.ets.utils.loadEtsProjectFromMultipleIR +import org.usvm.dataflow.ts.infer.EntryPointsProcessor +import org.usvm.dataflow.ts.infer.TypeInferenceManager +import org.usvm.dataflow.ts.infer.TypeInferenceResult +import org.usvm.dataflow.ts.infer.createApplicationGraph +import org.usvm.dataflow.ts.infer.dto.toDto +import org.usvm.dataflow.ts.util.EtsTraits +import java.nio.file.Path +import kotlin.io.path.outputStream +import kotlin.time.measureTimedValue + +private val logger = KotlinLogging.logger {} + +class InferTypes : CliktCommand() { + init { + context { + helpFormatter = { + MordantHelpFormatter( + it, + requiredOptionMarker = "*", + showDefaultValues = true, + showRequiredTag = true + ) + } + } + } + + val input by option("-i", "--input", help = "Input file or directory with IR").path().multiple(required = true) + val output by option("-o", "--output", help = "Output file with inferred types in JSON format").path().required() + + val sdkPaths by option( + "--sdk", + help = "Path to SDK directory" + ).path().multiple() + + val sdkNames by option( + "--sdk-names", + help = "Comma-separated list of SDK names" + ).convert { it.split(",") }.default(emptyList(), defaultForHelp = "empty") + + val skipAnonymous by option( + "--skip-anonymous", + help = "Skip anonymous classes and method" + ).flag("--no-skip-anonymous", default = false) + + override fun run() { + logger.info { "Running InferTypes" } + val startTime = System.currentTimeMillis() + + logger.info { "Input: $input" } + logger.info { "Output: $output" } + + val project = loadEtsProjectFromMultipleIR(input, sdkPaths) + val graph = createApplicationGraph(project) + + val (dummyMains, allMethods) = EntryPointsProcessor.extractEntryPoints(project) + val publicMethods = allMethods.filter { m -> m.isPublic } + + val manager = TypeInferenceManager(EtsTraits(), graph) + + val (result, timeAnalyze) = measureTimedValue { + manager.analyze(dummyMains, publicMethods).withGuessedTypes(project) + } + logger.info { "Inferred types for ${result.inferredTypes.size} methods in $timeAnalyze" } + + dumpTypeInferenceResult(result, output, skipAnonymous) + + logger.info { "All done in %.1f s".format((System.currentTimeMillis() - startTime) / 1000.0) } + } +} + +fun main(args: Array) { + InferTypes().main(args) +} + +@OptIn(ExperimentalSerializationApi::class) +fun dumpTypeInferenceResult( + result: TypeInferenceResult, + path: Path, + skipAnonymous: Boolean = true, +) { + logger.info { "Dumping inferred types to '$path'" } + val dto = result.toDto() + // Filter out anonymous classes and methods + .let { dto -> + if (skipAnonymous) { + dto.copy( + classes = dto.classes.filterNot { cls -> + cls.signature.name.startsWith(ANONYMOUS_CLASS_PREFIX) + }, + methods = dto.methods.filterNot { method -> + method.signature.declaringClass.name.startsWith(ANONYMOUS_CLASS_PREFIX) + || method.signature.name.startsWith(ANONYMOUS_METHOD_PREFIX) + } + ) + } else { + dto + } + } + path.outputStream().use { stream -> + val json = Json { + prettyPrint = true + } + json.encodeToStream(dto, stream) + } +} diff --git a/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/dto/Convert.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/dto/Convert.kt new file mode 100644 index 0000000000..eda5340277 --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/dto/Convert.kt @@ -0,0 +1,195 @@ +/* + * 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 mu.KotlinLogging +import org.jacodb.ets.base.EtsAnyType +import org.jacodb.ets.base.EtsArrayObjectType +import org.jacodb.ets.base.EtsArrayType +import org.jacodb.ets.base.EtsBooleanType +import org.jacodb.ets.base.EtsClassType +import org.jacodb.ets.base.EtsFunctionType +import org.jacodb.ets.base.EtsGenericType +import org.jacodb.ets.base.EtsLiteralType +import org.jacodb.ets.base.EtsNeverType +import org.jacodb.ets.base.EtsNullType +import org.jacodb.ets.base.EtsNumberType +import org.jacodb.ets.base.EtsStringType +import org.jacodb.ets.base.EtsTupleType +import org.jacodb.ets.base.EtsType +import org.jacodb.ets.base.EtsUnclearRefType +import org.jacodb.ets.base.EtsUndefinedType +import org.jacodb.ets.base.EtsUnionType +import org.jacodb.ets.base.EtsUnknownType +import org.jacodb.ets.base.EtsVoidType +import org.jacodb.ets.dto.AnyTypeDto +import org.jacodb.ets.dto.ArrayTypeDto +import org.jacodb.ets.dto.BooleanTypeDto +import org.jacodb.ets.dto.ClassSignatureDto +import org.jacodb.ets.dto.ClassTypeDto +import org.jacodb.ets.dto.FileSignatureDto +import org.jacodb.ets.dto.FunctionTypeDto +import org.jacodb.ets.dto.GenericTypeDto +import org.jacodb.ets.dto.LiteralTypeDto +import org.jacodb.ets.dto.MethodParameterDto +import org.jacodb.ets.dto.MethodSignatureDto +import org.jacodb.ets.dto.NamespaceSignatureDto +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.EtsClassSignature +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.usvm.dataflow.ts.infer.EtsTypeFact + +private val logger = KotlinLogging.logger {} + +fun EtsTypeFact.toType(): EtsType? = when (this) { + is EtsTypeFact.ObjectEtsTypeFact -> if (cls is EtsClassType) cls else null + is EtsTypeFact.ArrayEtsTypeFact -> EtsArrayType( + elementType = elementType.toType() ?: EtsUnknownType, + dimensions = 1, + ) + + EtsTypeFact.AnyEtsTypeFact -> EtsAnyType + EtsTypeFact.BooleanEtsTypeFact -> EtsBooleanType + EtsTypeFact.FunctionEtsTypeFact -> null // TODO: function type + EtsTypeFact.NullEtsTypeFact -> EtsNullType + EtsTypeFact.NumberEtsTypeFact -> EtsNumberType + EtsTypeFact.StringEtsTypeFact -> EtsStringType + EtsTypeFact.UndefinedEtsTypeFact -> EtsUndefinedType + EtsTypeFact.UnknownEtsTypeFact -> EtsUnknownType + + is EtsTypeFact.GuardedTypeFact -> null + is EtsTypeFact.IntersectionEtsTypeFact -> null + is EtsTypeFact.UnionEtsTypeFact -> { + val types = this.types.map { it.toType() } + + if (types.any { it == null }) { + logger.warn { "Cannot convert union type fact to type: $this" } + null + } else { + EtsUnionType(types.map { it!! }) + } + } +} + +fun EtsType.toDto(): TypeDto = when (this) { + is EtsAnyType -> AnyTypeDto + is EtsUnknownType -> UnknownTypeDto + is EtsUnionType -> UnionTypeDto(types = this.types.map { it.toDto() }) + is EtsTupleType -> TupleTypeDto(types = this.types.map { it.toDto() }) + is EtsBooleanType -> BooleanTypeDto + is EtsNumberType -> NumberTypeDto + is EtsStringType -> StringTypeDto + is EtsNullType -> NullTypeDto + is EtsUndefinedType -> UndefinedTypeDto + is EtsVoidType -> VoidTypeDto + is EtsNeverType -> NeverTypeDto + + is EtsLiteralType -> { + val literal = when { + this.literalTypeName.equals("true", ignoreCase = true) -> PrimitiveLiteralDto.BooleanLiteral(true) + this.literalTypeName.equals("false", ignoreCase = true) -> PrimitiveLiteralDto.BooleanLiteral(false) + else -> { + val x = this.literalTypeName.toDoubleOrNull() + if (x != null) { + PrimitiveLiteralDto.NumberLiteral(x) + } else { + PrimitiveLiteralDto.StringLiteral(this.literalTypeName) + } + } + } + LiteralTypeDto(literal = literal) + } + + is EtsClassType -> ClassTypeDto( + signature = this.signature.toDto(), + typeParameters = this.typeParameters.map { it.toDto() } + ) + + is EtsFunctionType -> FunctionTypeDto( + signature = this.method.toDto(), + typeParameters = this.typeParameters.map { it.toDto() } + ) + + is EtsArrayType -> ArrayTypeDto( + elementType = this.elementType.toDto(), + dimensions = this.dimensions + ) + + is EtsArrayObjectType -> TODO("EtsArrayObjectType was removed") + + is EtsUnclearRefType -> UnclearReferenceTypeDto( + name = this.typeName, + typeParameters = this.typeParameters.map { it.toDto() } + ) + + is EtsGenericType -> GenericTypeDto( + name = this.typeName, + defaultType = this.defaultType?.toDto(), + constraint = this.constraint?.toDto(), + ) + + else -> error("Cannot convert ${this::class.java} to DTO: $this") +} + +fun EtsClassSignature.toDto(): ClassSignatureDto = + ClassSignatureDto( + name = this.name, + declaringFile = this.file.toDto(), + declaringNamespace = this.namespace?.toDto(), + ) + +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 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-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/dto/DTO.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/dto/DTO.kt new file mode 100644 index 0000000000..467443d0ec --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/dto/DTO.kt @@ -0,0 +1,96 @@ +package org.usvm.dataflow.ts.infer.dto + +import kotlinx.serialization.Serializable +import org.jacodb.ets.dto.ClassSignatureDto +import org.jacodb.ets.dto.MethodSignatureDto +import org.jacodb.ets.dto.TypeDto +import org.usvm.dataflow.ts.infer.AccessPathBase +import org.usvm.dataflow.ts.infer.EtsTypeFact +import org.usvm.dataflow.ts.infer.TypeInferenceResult + +@Serializable +data class InferredTypesDto( + val classes: List, + val methods: List, +) + +@Serializable +data class ClassTypeResultDto( + val signature: ClassSignatureDto, + val fields: List, + val methods: List, +) + +@Serializable +data class FieldTypeResultDto( + val name: String, + val type: TypeDto, +) + +@Serializable +data class MethodTypeResultDto( + val signature: MethodSignatureDto, + val args: List, + val returnType: TypeDto? = null, + val locals: List, +) + +@Serializable +data class ArgumentTypeResultDto( + val index: Int, + val type: TypeDto, +) + +@Serializable +data class LocalTypeResultDto( + val name: String, + val type: TypeDto, +) + +fun TypeInferenceResult.toDto(): InferredTypesDto { + 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 { + FieldTypeResultDto(name, it.toDto()) + } + } + .sortedBy { it.name } + ClassTypeResultDto(clazz.toDto(), fields, 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 ArgumentTypeResultDto(base.index, type.toDto()) + } + } + null + }.sortedBy { it.index } + val returnType = inferredReturnType[method]?.toType()?.toDto() + val locals = facts.mapNotNull { (base, fact) -> + if (base is AccessPathBase.Local) { + val type = fact.toType() + if (type != null) { + return@mapNotNull LocalTypeResultDto(base.name, type.toDto()) + } + } + null + }.sortedBy { it.name } + MethodTypeResultDto(method.signature.toDto(), args, returnType, locals) + }.sortedBy { + it.signature.toString() + } + + return InferredTypesDto(classTypeInferenceResult, methodTypeInferenceResult) +} diff --git a/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/verify/EntityId.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/verify/EntityId.kt new file mode 100644 index 0000000000..73c0d88270 --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/verify/EntityId.kt @@ -0,0 +1,79 @@ +/* + * 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.verify + +import org.jacodb.ets.base.EtsLocal +import org.jacodb.ets.base.EtsParameterRef +import org.jacodb.ets.model.EtsClassSignature +import org.jacodb.ets.model.EtsFieldSignature +import org.jacodb.ets.model.EtsMethodParameter +import org.jacodb.ets.model.EtsMethodSignature + +sealed interface EntityId + +data class ClassId( + val signature: EtsClassSignature, +) : EntityId + +data class MethodId( + val name: String, + val enclosingClass: ClassId, +) { + constructor(signature: EtsMethodSignature) + : this(signature.name, ClassId(signature.enclosingClass)) +} + +data class FieldId( + val name: String, + val enclosingClass: ClassId, +) : EntityId { + constructor(signature: EtsFieldSignature) + : this(signature.name, ClassId(signature.enclosingClass)) +} + +data class ParameterId( + val index: Int, + val method: MethodId, +) : EntityId { + constructor(parameter: EtsMethodParameter, methodSignature: EtsMethodSignature) + : this(parameter.index, MethodId(methodSignature)) + + constructor(etsParameterRef: EtsParameterRef, methodSignature: EtsMethodSignature) + : this(etsParameterRef.index, MethodId(methodSignature)) +} + +data class ReturnId( + val method: MethodId, +) : EntityId { + constructor(methodSignature: EtsMethodSignature) + : this(MethodId(methodSignature)) +} + +data class LocalId( + val name: String, + val method: MethodId, +) : EntityId { + constructor(local: EtsLocal, methodSignature: EtsMethodSignature) + : this(local.name, MethodId(methodSignature)) +} + +data class ThisId( + val method: MethodId, +) : EntityId { + constructor(methodSignature: EtsMethodSignature) + : this(MethodId(methodSignature)) +} diff --git a/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/verify/VerificationResult.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/verify/VerificationResult.kt new file mode 100644 index 0000000000..1859346d90 --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/verify/VerificationResult.kt @@ -0,0 +1,34 @@ +/* + * 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.verify + +import org.jacodb.ets.base.EtsType + +sealed interface VerificationResult { + data class Success(val mapping: Map) : VerificationResult + + data class Fail(val mapping: Map>) : VerificationResult + + companion object { + fun from(mapping: Map>): VerificationResult = + if (mapping.values.all { it.size == 1 }) { + Success(mapping.mapValues { (_, types) -> types.single() }) + } else { + Fail(mapping) + } + } +} diff --git a/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/verify/Verify.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/verify/Verify.kt new file mode 100644 index 0000000000..ede486afe6 --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/verify/Verify.kt @@ -0,0 +1,28 @@ +/* + * 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.verify + +import org.jacodb.ets.base.EtsType +import org.jacodb.ets.model.EtsScene +import org.usvm.dataflow.ts.infer.verify.collectors.ClassSummaryCollector + +fun collectSummary(scene: EtsScene): Map> = + ClassSummaryCollector(mutableMapOf()).apply { + scene.projectAndSdkClasses.forEach { collect(it) } + }.typeSummary + +fun verify(scene: EtsScene) = VerificationResult.from(collectSummary(scene)) diff --git a/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/verify/collectors/ClassSummaryCollector.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/verify/collectors/ClassSummaryCollector.kt new file mode 100644 index 0000000000..03bcef0d02 --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/verify/collectors/ClassSummaryCollector.kt @@ -0,0 +1,39 @@ +/* + * 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.verify.collectors + +import org.jacodb.ets.base.EtsType +import org.jacodb.ets.model.EtsClass +import org.usvm.dataflow.ts.infer.verify.EntityId + +class ClassSummaryCollector( + override val typeSummary: MutableMap>, +) : SummaryCollector { + fun collect(etsClass: EtsClass) { + etsClass.fields.forEach { field -> + yield(field.signature) + } + + etsClass.methods.forEach { method -> + yield(method.signature) + val stmtCollector = StmtSummaryCollector(method.signature, typeSummary) + method.cfg.stmts.forEach { + it.accept(stmtCollector) + } + } + } +} diff --git a/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/verify/collectors/ExprSummaryCollector.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/verify/collectors/ExprSummaryCollector.kt new file mode 100644 index 0000000000..960f770074 --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/verify/collectors/ExprSummaryCollector.kt @@ -0,0 +1,93 @@ +/* + * 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.verify.collectors + +import org.jacodb.ets.base.EtsBinaryExpr +import org.jacodb.ets.base.EtsEntity +import org.jacodb.ets.base.EtsExpr +import org.jacodb.ets.base.EtsInstanceCallExpr +import org.jacodb.ets.base.EtsInstanceOfExpr +import org.jacodb.ets.base.EtsLengthExpr +import org.jacodb.ets.base.EtsPtrCallExpr +import org.jacodb.ets.base.EtsStaticCallExpr +import org.jacodb.ets.base.EtsTernaryExpr +import org.jacodb.ets.base.EtsType +import org.jacodb.ets.base.EtsUnaryExpr +import org.jacodb.ets.base.EtsValue +import org.jacodb.ets.model.EtsMethodSignature +import org.usvm.dataflow.ts.infer.verify.EntityId + +class ExprSummaryCollector( + override val enclosingMethod: EtsMethodSignature, + override val typeSummary: MutableMap>, +) : MethodSummaryCollector, EtsExpr.Visitor.Default { + private val valueSummaryCollector by lazy { + ValueSummaryCollector(enclosingMethod, typeSummary) + } + + private fun collect(entity: EtsEntity) { + when (entity) { + is EtsValue -> entity.accept(valueSummaryCollector) + is EtsExpr -> entity.accept(this) + else -> error("Unsupported entity kind") + } + } + + override fun defaultVisit(expr: EtsExpr) = when (expr) { + is EtsUnaryExpr -> { + collect(expr.arg) + } + + is EtsBinaryExpr -> { + collect(expr.left) + collect(expr.right) + } + + is EtsTernaryExpr -> { + collect(expr.condition) + collect(expr.thenExpr) + collect(expr.elseExpr) + } + + is EtsInstanceCallExpr -> { + yield(expr.method) + collect(expr.instance) + expr.args.forEach { collect(it) } + } + + is EtsStaticCallExpr -> { + yield(expr.method) + expr.args.forEach { collect(it) } + } + + is EtsPtrCallExpr -> { + yield(expr.method) + collect(expr.ptr) + expr.args.forEach { collect(it) } + } + + is EtsLengthExpr -> { + collect(expr.arg) + } + + is EtsInstanceOfExpr -> { + collect(expr.arg) + } + + else -> {} + } +} diff --git a/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/verify/collectors/StmtSummaryCollector.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/verify/collectors/StmtSummaryCollector.kt new file mode 100644 index 0000000000..20d0ce61fd --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/verify/collectors/StmtSummaryCollector.kt @@ -0,0 +1,80 @@ +/* + * 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.verify.collectors + +import org.jacodb.ets.base.EtsAssignStmt +import org.jacodb.ets.base.EtsCallStmt +import org.jacodb.ets.base.EtsEntity +import org.jacodb.ets.base.EtsExpr +import org.jacodb.ets.base.EtsGotoStmt +import org.jacodb.ets.base.EtsIfStmt +import org.jacodb.ets.base.EtsNopStmt +import org.jacodb.ets.base.EtsReturnStmt +import org.jacodb.ets.base.EtsStmt +import org.jacodb.ets.base.EtsSwitchStmt +import org.jacodb.ets.base.EtsThrowStmt +import org.jacodb.ets.base.EtsType +import org.jacodb.ets.base.EtsValue +import org.jacodb.ets.model.EtsMethodSignature +import org.usvm.dataflow.ts.infer.verify.EntityId + +class StmtSummaryCollector( + override val enclosingMethod: EtsMethodSignature, + override val typeSummary: MutableMap>, +) : EtsStmt.Visitor, MethodSummaryCollector { + private val exprCollector = ExprSummaryCollector(enclosingMethod, typeSummary) + private val valueCollector = ValueSummaryCollector(enclosingMethod, typeSummary) + + private fun collect(entity: EtsEntity) { + when (entity) { + is EtsValue -> entity.accept(valueCollector) + is EtsExpr -> entity.accept(exprCollector) + else -> error("Unsupported entity kind") + } + } + + override fun visit(stmt: EtsNopStmt) {} + + override fun visit(stmt: EtsAssignStmt) { + collect(stmt.lhv) + collect(stmt.rhv) + } + + override fun visit(stmt: EtsCallStmt) { + collect(stmt.expr) + } + + override fun visit(stmt: EtsReturnStmt) { + stmt.returnValue?.let { collect(it) } + } + + override fun visit(stmt: EtsThrowStmt) { + collect(stmt.arg) + } + + override fun visit(stmt: EtsGotoStmt) {} + + override fun visit(stmt: EtsIfStmt) { + collect(stmt.condition) + } + + override fun visit(stmt: EtsSwitchStmt) { + collect(stmt.arg) + stmt.cases.forEach { collect(it) } + } + +} diff --git a/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/verify/collectors/SummaryCollector.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/verify/collectors/SummaryCollector.kt new file mode 100644 index 0000000000..618f5e3104 --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/verify/collectors/SummaryCollector.kt @@ -0,0 +1,76 @@ +/* + * 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.verify.collectors + +import org.jacodb.ets.base.EtsLocal +import org.jacodb.ets.base.EtsParameterRef +import org.jacodb.ets.base.EtsThis +import org.jacodb.ets.base.EtsType +import org.jacodb.ets.model.EtsFieldSignature +import org.jacodb.ets.model.EtsMethodSignature +import org.usvm.dataflow.ts.infer.verify.EntityId +import org.usvm.dataflow.ts.infer.verify.FieldId +import org.usvm.dataflow.ts.infer.verify.LocalId +import org.usvm.dataflow.ts.infer.verify.ParameterId +import org.usvm.dataflow.ts.infer.verify.ReturnId +import org.usvm.dataflow.ts.infer.verify.ThisId + +interface SummaryCollector { + val typeSummary: MutableMap> + + fun yield(field: EtsFieldSignature) { + if (!field.type.isUnresolved) { + typeSummary.getOrPut(FieldId(field), ::mutableSetOf) + .add(field.type) + } + } + + fun yield(method: EtsMethodSignature) { + if (!method.returnType.isUnresolved) { + typeSummary.getOrPut(ReturnId(method), ::mutableSetOf) + .add(method.returnType) + } + method.parameters.forEach { + if (!it.type.isUnresolved) { + typeSummary.getOrPut(ParameterId(it, method), ::mutableSetOf) + .add(it.type) + } + } + } +} + +interface MethodSummaryCollector : SummaryCollector { + val enclosingMethod: EtsMethodSignature + fun yield(parameter: EtsParameterRef) { + if (!parameter.type.isUnresolved) { + typeSummary.getOrPut(ParameterId(parameter, enclosingMethod), ::mutableSetOf) + .add(parameter.type) + } + } + + fun yield(local: EtsLocal) { + if (!local.type.isUnresolved) { + typeSummary.getOrPut(LocalId(local, enclosingMethod), ::mutableSetOf) + .add(local.type) + } + } + + fun yield(etsThis: EtsThis) { + typeSummary.getOrPut(ThisId(enclosingMethod), ::mutableSetOf) + .add(etsThis.type) + } +} diff --git a/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/verify/collectors/Utils.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/verify/collectors/Utils.kt new file mode 100644 index 0000000000..b7a77c8e9d --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/verify/collectors/Utils.kt @@ -0,0 +1,34 @@ +/* + * 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.verify.collectors + +import org.jacodb.ets.base.EtsAnyType +import org.jacodb.ets.base.EtsArrayType +import org.jacodb.ets.base.EtsTupleType +import org.jacodb.ets.base.EtsType +import org.jacodb.ets.base.EtsUnionType +import org.jacodb.ets.base.EtsUnknownType + +val EtsType.isUnresolved: Boolean + get() = when (this) { + is EtsAnyType -> true + is EtsUnknownType -> true + is EtsUnionType -> types.any { it.isUnresolved } + is EtsTupleType -> types.any { it.isUnresolved } + is EtsArrayType -> elementType.isUnresolved + else -> false + } \ No newline at end of file diff --git a/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/verify/collectors/ValueSummaryCollector.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/verify/collectors/ValueSummaryCollector.kt new file mode 100644 index 0000000000..707e76dad0 --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/verify/collectors/ValueSummaryCollector.kt @@ -0,0 +1,85 @@ +/* + * 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.verify.collectors + +import org.jacodb.ets.base.EtsArrayAccess +import org.jacodb.ets.base.EtsArrayLiteral +import org.jacodb.ets.base.EtsEntity +import org.jacodb.ets.base.EtsExpr +import org.jacodb.ets.base.EtsInstanceFieldRef +import org.jacodb.ets.base.EtsLocal +import org.jacodb.ets.base.EtsObjectLiteral +import org.jacodb.ets.base.EtsParameterRef +import org.jacodb.ets.base.EtsStaticFieldRef +import org.jacodb.ets.base.EtsThis +import org.jacodb.ets.base.EtsType +import org.jacodb.ets.base.EtsValue +import org.jacodb.ets.model.EtsMethodSignature +import org.usvm.dataflow.ts.infer.verify.EntityId + +class ValueSummaryCollector( + override val enclosingMethod: EtsMethodSignature, + override val typeSummary: MutableMap>, +) : MethodSummaryCollector, EtsValue.Visitor.Default { + private val exprSummaryCollector by lazy { + ExprSummaryCollector(enclosingMethod, typeSummary) + } + + private fun collect(entity: EtsEntity) { + when (entity) { + is EtsValue -> entity.accept(this) + is EtsExpr -> entity.accept(exprSummaryCollector) + else -> error("Unsupported entity kind") + } + } + + override fun defaultVisit(value: EtsValue) {} + + override fun visit(value: EtsLocal) { + yield(value) + } + + override fun visit(value: EtsArrayLiteral) { + value.elements.forEach { collect(it) } + } + + override fun visit(value: EtsObjectLiteral) { + value.properties.forEach { (_, it) -> collect(it) } + } + + override fun visit(value: EtsThis) { + yield(value) + } + + override fun visit(value: EtsParameterRef) { + yield(value) + } + + override fun visit(value: EtsArrayAccess) { + value.array.accept(this) + value.index.accept(this) + } + + override fun visit(value: EtsInstanceFieldRef) { + yield(value.field) + value.instance.accept(this) + } + + override fun visit(value: EtsStaticFieldRef) { + yield(value.field) + } +} diff --git a/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/util/EtsTraits.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/util/EtsTraits.kt new file mode 100644 index 0000000000..81820a9f0a --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/util/EtsTraits.kt @@ -0,0 +1,252 @@ +/* + * 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.util + +import org.jacodb.api.common.CommonMethodParameter +import org.jacodb.api.common.cfg.CommonAssignInst +import org.jacodb.api.common.cfg.CommonCallExpr +import org.jacodb.api.common.cfg.CommonExpr +import org.jacodb.api.common.cfg.CommonValue +import org.jacodb.ets.base.CONSTRUCTOR_NAME +import org.jacodb.ets.base.EtsArrayAccess +import org.jacodb.ets.base.EtsAssignStmt +import org.jacodb.ets.base.EtsBinaryExpr +import org.jacodb.ets.base.EtsBooleanConstant +import org.jacodb.ets.base.EtsCallExpr +import org.jacodb.ets.base.EtsCastExpr +import org.jacodb.ets.base.EtsClassType +import org.jacodb.ets.base.EtsConstant +import org.jacodb.ets.base.EtsEntity +import org.jacodb.ets.base.EtsIfStmt +import org.jacodb.ets.base.EtsImmediate +import org.jacodb.ets.base.EtsInstanceFieldRef +import org.jacodb.ets.base.EtsNewArrayExpr +import org.jacodb.ets.base.EtsNumberConstant +import org.jacodb.ets.base.EtsParameterRef +import org.jacodb.ets.base.EtsStaticFieldRef +import org.jacodb.ets.base.EtsStmt +import org.jacodb.ets.base.EtsStringConstant +import org.jacodb.ets.base.EtsThis +import org.jacodb.ets.base.EtsUnaryExpr +import org.jacodb.ets.base.EtsValue +import org.jacodb.ets.model.EtsMethod +import org.jacodb.ets.model.EtsMethodImpl +import org.jacodb.ets.model.EtsMethodParameter +import org.jacodb.ets.utils.callExpr +import org.jacodb.ets.utils.getOperands +import org.jacodb.ets.utils.getValues +import org.jacodb.taint.configuration.ConstantBooleanValue +import org.jacodb.taint.configuration.ConstantIntValue +import org.jacodb.taint.configuration.ConstantStringValue +import org.jacodb.taint.configuration.ConstantValue +import org.jacodb.taint.configuration.TypeMatches +import org.usvm.dataflow.ifds.AccessPath +import org.usvm.dataflow.ifds.ElementAccessor +import org.usvm.dataflow.ifds.FieldAccessor +import org.usvm.dataflow.util.Traits + +/** + * ETS-specific extensions for analysis. + */ +class EtsTraits : Traits { + + override fun convertToPathOrNull(expr: CommonExpr): AccessPath? { + check(expr is EtsEntity) + return expr.toPathOrNull() + } + + override fun convertToPathOrNull(value: CommonValue): AccessPath? { + check(value is EtsEntity) + return value.toPathOrNull() + } + + override fun convertToPath(value: CommonValue): AccessPath { + check(value is EtsEntity) + return value.toPath() + } + + override fun getThisInstance(method: EtsMethod): EtsThis { + return EtsThis(EtsClassType(method.signature.enclosingClass)) + } + + override fun getArgument(param: CommonMethodParameter): EtsParameterRef { + check(param is EtsMethodParameter) + return EtsParameterRef(index = param.index, type = param.type) + } + + override fun getArgumentsOf(method: EtsMethod): List { + return method.parameters.map { getArgument(it) } + } + + override fun getCallee(callExpr: CommonCallExpr): EtsMethod { + check(callExpr is EtsCallExpr) + return EtsMethodImpl(callExpr.method) + } + + override fun getCallExpr(statement: EtsStmt): EtsCallExpr? { + return statement.callExpr + } + + override fun getValues(expr: CommonExpr): Set { + check(expr is EtsEntity) + return expr.getValues().toSet() + } + + override fun getOperands(statement: EtsStmt): List { + return statement.getOperands().toList() + } + + override fun getArrayAllocation(statement: EtsStmt): EtsEntity? { + if (statement !is EtsAssignStmt) return null + return statement.rhv as? EtsNewArrayExpr + } + + override fun getArrayAccessIndex(statement: EtsStmt): EtsValue? { + if (statement !is EtsAssignStmt) return null + + val lhv = statement.lhv + if (lhv is EtsArrayAccess) return lhv.index + + val rhv = statement.rhv + if (rhv is EtsArrayAccess) return rhv.index + + return null + } + + override fun getBranchExprCondition(statement: EtsStmt): EtsEntity? { + if (statement !is EtsIfStmt) return null + return statement.condition + } + + override fun isConstant(value: CommonValue): Boolean { + check(value is EtsValue) + return value is EtsConstant + } + + override fun eqConstant(value: CommonValue, constant: ConstantValue): Boolean { + check(value is EtsValue) + return when (constant) { + is ConstantBooleanValue -> { + value is EtsBooleanConstant && value.value == constant.value + } + + is ConstantIntValue -> { + value is EtsNumberConstant && value.value == constant.value.toDouble() + } + + is ConstantStringValue -> { + value is EtsStringConstant && value.value == constant.value + } + } + } + + override fun ltConstant(value: CommonValue, constant: ConstantValue): Boolean { + check(value is EtsValue) + return when (constant) { + is ConstantIntValue -> { + value is EtsNumberConstant && value.value < constant.value.toDouble() + } + + else -> error("Unexpected constant: $constant") + } + } + + override fun gtConstant(value: CommonValue, constant: ConstantValue): Boolean { + check(value is EtsValue) + return when (constant) { + is ConstantIntValue -> { + value is EtsNumberConstant && value.value > constant.value.toDouble() + } + + else -> error("Unexpected constant: $constant") + } + } + + override fun matches(value: CommonValue, pattern: String): Boolean { + check(value is EtsValue) + val s = value.toString() + val re = pattern.toRegex() + return re.matches(s) + } + + override fun typeMatches(value: CommonValue, condition: TypeMatches): Boolean { + check(value is EtsValue) + TODO("Not yet implemented") + } + + override fun isConstructor(method: EtsMethod): Boolean { + return method.name == CONSTRUCTOR_NAME + } + + override fun isLoopHead(statement: EtsStmt): Boolean { + TODO("Not yet implemented") + } + + override fun lineNumber(statement: EtsStmt): Int { + TODO("Not yet implemented") + } + + override fun locationFQN(statement: EtsStmt): String { + TODO("Not yet implemented") + } + + override fun taintFlowRhsValues(statement: CommonAssignInst): List { + check(statement is EtsAssignStmt) + return when (val rhv = statement.rhv) { + is EtsBinaryExpr -> listOf(rhv.left, rhv.right) + is EtsUnaryExpr -> listOf(rhv.arg) + is EtsCastExpr -> listOf(rhv.arg) + else -> listOf(rhv) + } + } + + override fun taintPassThrough(statement: EtsStmt): List>? { + return null + } +} + +fun EtsEntity.toPathOrNull(): AccessPath? = when (this) { + is EtsImmediate -> AccessPath(this, emptyList()) + + is EtsThis -> AccessPath(this, emptyList()) + + is EtsParameterRef -> AccessPath(this, emptyList()) + + is EtsArrayAccess -> { + array.toPathOrNull()?.let { + it + ElementAccessor + } + } + + is EtsInstanceFieldRef -> { + instance.toPathOrNull()?.let { + it + FieldAccessor(field.name) + } + } + + is EtsStaticFieldRef -> { + AccessPath(null, listOf(FieldAccessor(field.name, isStatic = true))) + } + + is EtsCastExpr -> arg.toPathOrNull() + + else -> null +} + +fun EtsEntity.toPath(): AccessPath { + return toPathOrNull() ?: error("Unable to build access path for value $this") +} diff --git a/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/util/TypeInferenceStatistics.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/util/TypeInferenceStatistics.kt new file mode 100644 index 0000000000..54fb50ff70 --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/util/TypeInferenceStatistics.kt @@ -0,0 +1,696 @@ +package org.usvm.dataflow.ts.util + +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.EtsArrayType +import org.jacodb.ets.base.EtsAssignStmt +import org.jacodb.ets.base.EtsBooleanType +import org.jacodb.ets.base.EtsClassType +import org.jacodb.ets.base.EtsFunctionType +import org.jacodb.ets.base.EtsNullType +import org.jacodb.ets.base.EtsNumberType +import org.jacodb.ets.base.EtsParameterRef +import org.jacodb.ets.base.EtsStringType +import org.jacodb.ets.base.EtsType +import org.jacodb.ets.base.EtsUnclearRefType +import org.jacodb.ets.base.EtsUndefinedType +import org.jacodb.ets.base.EtsUnionType +import org.jacodb.ets.base.EtsUnknownType +import org.jacodb.ets.base.INSTANCE_INIT_METHOD_NAME +import org.jacodb.ets.base.STATIC_INIT_METHOD_NAME +import org.jacodb.ets.model.EtsClassSignature +import org.jacodb.ets.model.EtsMethod +import org.jacodb.ets.model.EtsScene +import org.usvm.dataflow.ts.graph.EtsApplicationGraph +import org.usvm.dataflow.ts.infer.AccessPathBase +import org.usvm.dataflow.ts.infer.EtsTypeFact +import org.usvm.dataflow.ts.infer.TypeInferenceResult +import java.io.File + +class TypeInferenceStatistics { + private val noTypesInferred: MutableSet = hashSetOf() + private val allTypesAndFacts: MutableMap> = + hashMapOf() + + private var overallTypes: Long = 0 + + private var exactTypeInferredPreviouslyUnknown = 0L + private var exactTypeInferredCorrectlyPreviouslyKnown = 0L + private var exactTypeInferredPreviouslyWasAny = 0L + private var exactTypeInferredIncorrectlyPreviouslyKnown = 0L + + private var typeInfoInferredPreviouslyUnknown = 0L + private var typeInfoInferredPreviouslyKnownExactly = 0L + private var arrayInfoPreviouslyUnknown = 0L + private var arrayInfoPreviouslyKnown = 0L + + private var noInfoInferredPreviouslyKnown = 0L + private var noInfoInferredPreviouslyUnknown = 0L + + private var unhandled = 0L + + private var undefinedUnknownCombination = 0L + private var unknownAnyCombination = 0L + + fun compareSingleMethodFactsWithTypesInScene( + methodTypeFacts: MethodTypesFacts, + method: EtsMethod, + graph: EtsApplicationGraph, + ) { + overallTypes += 1 // thisType + overallTypes += method.parameters.size + method.locals.size + + methodTypeFacts.apply { + if (combinedThisFact == null + && argumentsFacts.all { it == null } + && returnFact == null + && localFacts.isEmpty() + ) { + noTypesInferred += method + } + } + + val thisType = getEtsClassType(method.enclosingClass, graph.cp) + val argTypes = method.parameters.map { it.type } + val locals = method.locals + + val methodFacts = mutableListOf() + + thisType?.let { + val thisPosition = AccessPathBase.This + val fact = methodTypeFacts.combinedThisFact + + val status = if (fact == null) { + // TODO check how unknown is represented + if (it.signature.name == "Unknown") { + noInfoInferredPreviouslyUnknown++ + InferenceStatus.NO_INFO_PREVIOUSLY_UNKNOWN + } else { + noInfoInferredPreviouslyKnown++ + InferenceStatus.NO_INFO_PREVIOUSLY_KNOWN + } + } else { + when { + fact.matchesWith(it) -> { + exactTypeInferredCorrectlyPreviouslyKnown++ + InferenceStatus.EXACT_MATCH_PREVIOUSLY_KNOWN + } + + (fact as? EtsTypeFact.ObjectEtsTypeFact)?.cls != null -> { + exactTypeInferredIncorrectlyPreviouslyKnown++ + InferenceStatus.DIFFERENT_TYPE_FOUND + } + + else -> { + typeInfoInferredPreviouslyKnownExactly++ + InferenceStatus.TYPE_INFO_FOUND_PREV_KNOWN + } + } + } + + methodFacts += InferenceResult(thisPosition, it, fact, status) + } + + argTypes.forEachIndexed { index, type -> + val fact = methodTypeFacts.argumentsFacts.getOrNull(index) + + val status = if (fact == null) { + if (type is EtsUnknownType) { + noInfoInferredPreviouslyUnknown++ + InferenceStatus.NO_INFO_PREVIOUSLY_UNKNOWN + } else { + noInfoInferredPreviouslyKnown++ + InferenceStatus.NO_INFO_PREVIOUSLY_KNOWN + } + } else { + checkForFact(fact, type) + } + + methodFacts += InferenceResult(AccessPathBase.Arg(index), type, fact, status) + } + + + + locals.forEach { + val type = it.type + val local = AccessPathBase.Local(it.name) + val fact = methodTypeFacts.localFacts[local] + + val status = if (fact == null) { + if (type is EtsUnknownType) { + noInfoInferredPreviouslyUnknown++ + InferenceStatus.NO_INFO_PREVIOUSLY_UNKNOWN + } else { + noInfoInferredPreviouslyKnown++ + InferenceStatus.NO_INFO_PREVIOUSLY_KNOWN + } + } else { + checkForFact(fact, type) + } + + methodFacts += InferenceResult(local, type, fact, status) + } + + allTypesAndFacts[method] = methodFacts + } + + private fun checkForFact(fact: EtsTypeFact, type: EtsType): InferenceStatus { + return when (fact) { + EtsTypeFact.AnyEtsTypeFact -> { + when (type) { + is EtsUnknownType -> { + unknownAnyCombination++ + InferenceStatus.UNKNOWN_ANY_COMBINATION + } + + is EtsAnyType -> { + exactTypeInferredPreviouslyWasAny++ + InferenceStatus.EXACT_MATCH_PREVIOUSLY_WAS_ANY + } + + else -> { + exactTypeInferredIncorrectlyPreviouslyKnown++ + InferenceStatus.DIFFERENT_TYPE_FOUND + } + } + } + + is EtsTypeFact.ArrayEtsTypeFact -> { + when (type) { + is EtsArrayType -> { + arrayInfoPreviouslyKnown++ + InferenceStatus.ARRAY_INFO + } + + is EtsUnclearRefType -> { + arrayInfoPreviouslyKnown++ + InferenceStatus.ARRAY_INFO + } + + is EtsUnknownType -> { + arrayInfoPreviouslyUnknown++ + InferenceStatus.ARRAY_INFO_PREV_UNKNOWN + } + + else -> { + arrayInfoPreviouslyKnown++ + InferenceStatus.ARRAY_INFO + } + } + } + + EtsTypeFact.BooleanEtsTypeFact -> { + when (type) { + is EtsBooleanType -> { + exactTypeInferredCorrectlyPreviouslyKnown++ + InferenceStatus.EXACT_MATCH_PREVIOUSLY_KNOWN + } + + is EtsUnknownType -> { + exactTypeInferredPreviouslyUnknown++ + InferenceStatus.EXACT_MATCH_PREVIOUSLY_UNKNOWN + } + + else -> { + when { + (type as? EtsClassType)?.typeName == "Boolean" -> { + exactTypeInferredCorrectlyPreviouslyKnown++ + InferenceStatus.EXACT_MATCH_PREVIOUSLY_KNOWN + } + + (type as? EtsUnclearRefType)?.typeName == "Boolean" -> { + exactTypeInferredCorrectlyPreviouslyKnown++ + InferenceStatus.EXACT_MATCH_PREVIOUSLY_KNOWN + } + + else -> { + exactTypeInferredIncorrectlyPreviouslyKnown++ + InferenceStatus.DIFFERENT_TYPE_FOUND + } + } + } + } + } + + EtsTypeFact.FunctionEtsTypeFact -> { + when (type) { + is EtsUnknownType -> { + undefinedUnknownCombination++ + InferenceStatus.UNKNOWN_UNDEFINED_COMBINATION + } + + is EtsFunctionType -> { + exactTypeInferredCorrectlyPreviouslyKnown++ + InferenceStatus.EXACT_MATCH_PREVIOUSLY_KNOWN + } + + else -> { + exactTypeInferredIncorrectlyPreviouslyKnown++ + InferenceStatus.DIFFERENT_TYPE_FOUND + } + } + } + + EtsTypeFact.NullEtsTypeFact -> { + when (type) { + is EtsUnknownType -> { + undefinedUnknownCombination++ + InferenceStatus.UNKNOWN_UNDEFINED_COMBINATION + } + + is EtsNullType -> { + exactTypeInferredCorrectlyPreviouslyKnown++ + InferenceStatus.EXACT_MATCH_PREVIOUSLY_KNOWN + } + + else -> { + exactTypeInferredIncorrectlyPreviouslyKnown++ + InferenceStatus.DIFFERENT_TYPE_FOUND + } + } + } + + EtsTypeFact.NumberEtsTypeFact -> { + when (type) { + is EtsNumberType -> { + exactTypeInferredCorrectlyPreviouslyKnown++ + InferenceStatus.EXACT_MATCH_PREVIOUSLY_KNOWN + } + + is EtsUnknownType -> { + exactTypeInferredPreviouslyUnknown++ + InferenceStatus.EXACT_MATCH_PREVIOUSLY_UNKNOWN + } + + else -> { + when { + (type as? EtsClassType)?.typeName == "Number" -> { + exactTypeInferredCorrectlyPreviouslyKnown++ + InferenceStatus.EXACT_MATCH_PREVIOUSLY_KNOWN + } + + (type as? EtsUnclearRefType)?.typeName == "Number" -> { + exactTypeInferredCorrectlyPreviouslyKnown++ + InferenceStatus.EXACT_MATCH_PREVIOUSLY_KNOWN + } + + else -> { + exactTypeInferredIncorrectlyPreviouslyKnown++ + InferenceStatus.DIFFERENT_TYPE_FOUND + } + } + } + } + } + + is EtsTypeFact.ObjectEtsTypeFact -> { + if (type is EtsUnknownType) { + return if (fact.cls != null) { + exactTypeInferredPreviouslyUnknown++ + InferenceStatus.EXACT_MATCH_PREVIOUSLY_UNKNOWN + } else { + typeInfoInferredPreviouslyUnknown++ + InferenceStatus.TYPE_INFO_FOUND_PREV_UNKNOWN + } + } + + if (type is EtsAnyType) { + return if (fact.cls != null) { + exactTypeInferredPreviouslyWasAny++ + InferenceStatus.EXACT_MATCH_PREVIOUSLY_WAS_ANY + } else { + typeInfoInferredPreviouslyKnownExactly++ + InferenceStatus.TYPE_INFO_FOUND_PREV_KNOWN + } + } + + if (fact.cls == null) { + return InferenceStatus.TYPE_INFO_FOUND_PREV_KNOWN + } + + val typeName = fact.cls.typeName + + if ((type is EtsClassType || type is EtsUnclearRefType) && type.typeName == typeName) { + exactTypeInferredCorrectlyPreviouslyKnown++ + InferenceStatus.EXACT_MATCH_PREVIOUSLY_KNOWN + } else { + exactTypeInferredIncorrectlyPreviouslyKnown++ + InferenceStatus.DIFFERENT_TYPE_FOUND + } + } + + EtsTypeFact.StringEtsTypeFact -> { + when (type) { + is EtsStringType -> { + exactTypeInferredCorrectlyPreviouslyKnown++ + InferenceStatus.EXACT_MATCH_PREVIOUSLY_KNOWN + } + + is EtsUnknownType -> { + exactTypeInferredPreviouslyUnknown++ + InferenceStatus.EXACT_MATCH_PREVIOUSLY_UNKNOWN + } + + else -> { + when { + (type as? EtsClassType)?.typeName == "String" -> { + exactTypeInferredCorrectlyPreviouslyKnown++ + InferenceStatus.EXACT_MATCH_PREVIOUSLY_KNOWN + } + + (type as? EtsUnclearRefType)?.typeName == "String" -> { + exactTypeInferredCorrectlyPreviouslyKnown++ + InferenceStatus.EXACT_MATCH_PREVIOUSLY_KNOWN + } + + else -> { + exactTypeInferredIncorrectlyPreviouslyKnown++ + InferenceStatus.DIFFERENT_TYPE_FOUND + } + } + } + } + } + + EtsTypeFact.UndefinedEtsTypeFact -> { + when (type) { + is EtsUnknownType -> { + undefinedUnknownCombination++ + InferenceStatus.UNKNOWN_UNDEFINED_COMBINATION + } + + is EtsUndefinedType -> { + exactTypeInferredCorrectlyPreviouslyKnown++ + InferenceStatus.EXACT_MATCH_PREVIOUSLY_KNOWN + } + + else -> { + exactTypeInferredIncorrectlyPreviouslyKnown++ + InferenceStatus.DIFFERENT_TYPE_FOUND + } + } + } + + EtsTypeFact.UnknownEtsTypeFact -> { + if (type is EtsUnknownType) { + exactTypeInferredCorrectlyPreviouslyKnown++ + InferenceStatus.EXACT_MATCH_PREVIOUSLY_KNOWN + } else { + exactTypeInferredIncorrectlyPreviouslyKnown++ + InferenceStatus.DIFFERENT_TYPE_FOUND + } + } + + is EtsTypeFact.GuardedTypeFact -> { + unhandled++ + InferenceStatus.UNHANDLED + } + + is EtsTypeFact.IntersectionEtsTypeFact -> { + unhandled++ + InferenceStatus.UNHANDLED + } + + is EtsTypeFact.UnionEtsTypeFact -> { + when (type) { + is EtsUnionType -> { + unhandled++ + InferenceStatus.UNHANDLED + } + + is EtsUnknownType -> { + InferenceStatus.UNION_INSTEAD_OF_UNKNOWN + } + + else -> { + InferenceStatus.DIFFERENT_TYPE_FOUND + } + } + } + } + } + + fun dumpStatistics(outputFilePath: String? = null) { + val data = buildString { + appendLine(this@TypeInferenceStatistics.toString()) + appendLine() + + appendLine("Specifically: ${"=".repeat(42)}") + + val comparator = + Comparator { fst, snd -> + when (fst.position) { + is AccessPathBase.This -> when { + snd.position is AccessPathBase.This -> 0 + else -> -1 + } + + is AccessPathBase.Arg -> when (snd.position) { + is AccessPathBase.This -> 1 + is AccessPathBase.Arg -> { + fst.position.index.compareTo(snd.position.index) + } + + else -> -1 + } + + else -> when (snd.position) { + is AccessPathBase.This, is AccessPathBase.Arg -> 1 + else -> if (fst.position is AccessPathBase.Local && snd.position is AccessPathBase.Local) { + fst.position.name.compareTo(snd.position.name) + } else { + 0 + } + } + } + } + + allTypesAndFacts + .toList() + .sortedBy { it.first.signature.toString() } + .forEach { (method, types) -> + appendLine("${method.signature}:") + + types + .sortedWith(comparator) + .forEach { (path, typeInfo, typeFact, status) -> + appendLine("[${status.message}]: ${path}: $typeInfo -> $typeFact ") + } + appendLine() + } + + appendLine() + appendLine("=".repeat(42)) + appendLine("No types inferred for methods:") + + noTypesInferred + .filterNot { it.name == INSTANCE_INIT_METHOD_NAME || it.name == STATIC_INIT_METHOD_NAME } + .filterNot { it.name == DEFAULT_ARK_METHOD_NAME && it.enclosingClass.name == DEFAULT_ARK_CLASS_NAME } + .sortedBy { it.signature.toString() } + .forEach { + appendLine(it) + } + } + + if (outputFilePath == null) { + println(data) + return + } + + val file = File(outputFilePath) + println("File with statistics is located: ${file.absolutePath}") + file.writeText(data) + } + + private fun calculateImprovement(): Double { + val newExactTypes = exactTypeInferredPreviouslyUnknown + + exactTypeInferredPreviouslyWasAny + + exactTypeInferredIncorrectlyPreviouslyKnown + + arrayInfoPreviouslyUnknown + + val prevTypes = exactTypeInferredCorrectlyPreviouslyKnown + + typeInfoInferredPreviouslyKnownExactly + + arrayInfoPreviouslyKnown + + noInfoInferredPreviouslyKnown + + return newExactTypes.toDouble() / prevTypes * 100 + } + + override fun toString(): String = """ + Total types number: $overallTypes + + Compared to the first state of the Scene: + + Improvement: ${calculateImprovement()}% + + Inferred types that were unknown: $exactTypeInferredPreviouslyUnknown + Inferred types that were already inferred: $exactTypeInferredCorrectlyPreviouslyKnown + Inferred types that were previously inferred as any: $exactTypeInferredPreviouslyWasAny + Inferred types are different from the ones in the Scene: $exactTypeInferredIncorrectlyPreviouslyKnown + + Array types instead of unknown: $arrayInfoPreviouslyUnknown + Array types instead of previously known type: $arrayInfoPreviouslyKnown + + Some facts found about unknown type: $typeInfoInferredPreviouslyUnknown + Some facts found about already inferred type: $typeInfoInferredPreviouslyKnownExactly + + Unhandled type info: $unhandled + + Lost info about type: $noInfoInferredPreviouslyKnown + Nothing inferred, but it was unknown previously as well: $noInfoInferredPreviouslyUnknown + + Was unknown, became undefined: $undefinedUnknownCombination + Was unknown, became any: $unknownAnyCombination + """.trimIndent() + +} + +private fun getEtsClassType(signature: EtsClassSignature, scene: EtsScene): EtsClassType? { + if (signature.name == DEFAULT_ARK_CLASS_NAME || signature.name.isBlank()) { + return null + } + + val clazz = scene.projectAndSdkClasses.firstOrNull { it.signature == signature } + ?: error("No class found in the classpath with signature $signature") + return EtsClassType(clazz.signature) +} + +data class MethodTypesFacts( + val combinedThisFact: EtsTypeFact?, + val argumentsFacts: List, + val localFacts: Map, + val returnFact: EtsTypeFact?, +) { + companion object { + fun from( + result: TypeInferenceResult, + m: EtsMethod, + ): MethodTypesFacts { + val combinedThisFact = result.inferredCombinedThisType.entries.firstOrNull { + it.key.name == m.enclosingClass.name + }?.value + + val factsForMethod = result.inferredTypes.entries.singleOrNull { + compareByMethodNameAndEnclosingClass(it.key, m) + }?.value + + val inferredReturnType = result.inferredReturnType.entries.firstOrNull { + compareByMethodNameAndEnclosingClass(it.key, m) + }?.value + + val arguments = m.parameters.indices.map { + val stmts = m.cfg.stmts + if (stmts.isEmpty()) return@map null + + val realIndex = ((stmts[it] as EtsAssignStmt).rhv as EtsParameterRef).index + factsForMethod?.get(AccessPathBase.Arg(realIndex)) + } + + val locals = factsForMethod?.filterKeys { it is AccessPathBase.Local }.orEmpty() + + return MethodTypesFacts(combinedThisFact, arguments, locals, inferredReturnType) + } + } +} + +// TODO hack because of an issue with signatures +private val compareByMethodNameAndEnclosingClass = { fst: EtsMethod, snd: EtsMethod -> + fst.name === snd.name && fst.enclosingClass.name === snd.enclosingClass.name +} + +private fun EtsTypeFact.matchesWith(type: EtsType): Boolean { + val result = when (this) { + EtsTypeFact.AnyEtsTypeFact -> { + type is EtsAnyType + } + + is EtsTypeFact.ObjectEtsTypeFact -> { + val typeName = this.cls?.typeName + + if (type is EtsUnknownType || type is EtsAnyType) { + this.cls != null + } else { + (type is EtsClassType || type is EtsUnclearRefType) && type.typeName == typeName + } + } + + is EtsTypeFact.ArrayEtsTypeFact -> when (type) { + is EtsArrayType -> this.elementType.matchesWith(type.elementType) + + is EtsUnclearRefType -> { + val elementType = this.elementType as? EtsTypeFact.ObjectEtsTypeFact + elementType?.cls?.typeName == type.typeName + } + + else -> false + } + + EtsTypeFact.BooleanEtsTypeFact -> { + type is EtsBooleanType + || type is EtsUnknownType + || (type as? EtsClassType)?.typeName == "Boolean" + || (type as? EtsUnclearRefType)?.typeName == "Boolean" + } + + EtsTypeFact.FunctionEtsTypeFact -> type is EtsFunctionType || type is EtsUnknownType + EtsTypeFact.NullEtsTypeFact -> type is EtsNullType || type is EtsUnknownType + EtsTypeFact.NumberEtsTypeFact -> { + type is EtsNumberType + || type is EtsUnknownType + || (type as? EtsClassType)?.typeName == "Number" + || (type as? EtsUnclearRefType)?.typeName == "Number" + } + + EtsTypeFact.StringEtsTypeFact -> { + type is EtsStringType + || type is EtsUnknownType + || (type as? EtsClassType)?.typeName == "String" + || (type as? EtsUnclearRefType)?.typeName == "String" + } + + EtsTypeFact.UndefinedEtsTypeFact -> type is EtsUndefinedType + EtsTypeFact.UnknownEtsTypeFact -> type is EtsUnknownType + is EtsTypeFact.GuardedTypeFact -> TODO() + is EtsTypeFact.IntersectionEtsTypeFact -> { + // TODO intersections checks are not supported yet + false + } + + is EtsTypeFact.UnionEtsTypeFact -> types.all { it.matchesWith(type) } + } + + return result +} + +private data class InferenceResult( + val position: AccessPathBase, + val type: EtsType, + val typeFact: EtsTypeFact?, + val status: InferenceStatus, +) + +private enum class InferenceStatus(val message: String) { + EXACT_MATCH_PREVIOUSLY_KNOWN("Exactly matched, previously known"), + EXACT_MATCH_PREVIOUSLY_UNKNOWN("Exactly matched, previously unknown"), + EXACT_MATCH_PREVIOUSLY_WAS_ANY("Exactly matched with any type"), + + TYPE_INFO_FOUND_PREV_KNOWN("Some type facts found, previously known exactly"), + TYPE_INFO_FOUND_PREV_UNKNOWN("Some type facts found, previously unknown"), + DIFFERENT_TYPE_FOUND("Another type is inferred"), + + UNHANDLED("Unhandled type info"), + + UNKNOWN_ANY_COMBINATION("Unknown any combination"), + UNKNOWN_UNDEFINED_COMBINATION("Unknown undefined combination"), + + ARRAY_INFO_PREV_UNKNOWN("Found an array type, previously unknown"), + + ARRAY_INFO("Array information"), + + UNION_INSTEAD_OF_UNKNOWN("Discovered union type, previously unknown"), + + NO_INFO_PREVIOUSLY_UNKNOWN("Not inferred, previously unknown"), + NO_INFO_PREVIOUSLY_KNOWN("Not inferred, previously known") + +} diff --git a/usvm-dataflow-ts/src/main/resources/logback.xml b/usvm-dataflow-ts/src/main/resources/logback.xml new file mode 100644 index 0000000000..656c393170 --- /dev/null +++ b/usvm-dataflow-ts/src/main/resources/logback.xml @@ -0,0 +1,19 @@ + + + + %highlight([%level]) %replace(%c{0}){'(\$Companion)?\$logger\$1',''} - %msg%n + + + + + logs/app.log + + %highlight([%level]) %replace(%c{0}){'(\$Companion)?\$logger\$1',''} - %msg%n + + + + + + + + diff --git a/usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/EtsIfdsTest.kt b/usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/EtsIfdsTest.kt new file mode 100644 index 0000000000..d40ef9a675 --- /dev/null +++ b/usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/EtsIfdsTest.kt @@ -0,0 +1,437 @@ +/* + * 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.test + +import org.jacodb.ets.model.EtsFile +import org.jacodb.ets.model.EtsMethod +import org.jacodb.ets.model.EtsScene +import org.jacodb.taint.configuration.Argument +import org.jacodb.taint.configuration.AssignMark +import org.jacodb.taint.configuration.ConstantTrue +import org.jacodb.taint.configuration.ContainsMark +import org.jacodb.taint.configuration.CopyAllMarks +import org.jacodb.taint.configuration.RemoveMark +import org.jacodb.taint.configuration.Result +import org.jacodb.taint.configuration.TaintConfigurationItem +import org.jacodb.taint.configuration.TaintMark +import org.jacodb.taint.configuration.TaintMethodSink +import org.jacodb.taint.configuration.TaintMethodSource +import org.jacodb.taint.configuration.TaintPassThrough +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Disabled +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.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 +import kotlin.time.Duration.Companion.seconds + +private val logger = mu.KotlinLogging.logger {} + +@Disabled("Have several issues with EtsIR") +class EtsIfdsTest { + + companion object { + private const val BASE_PATH = "/etsir/samples" + + private fun loadSample(programName: String): EtsFile { + return loadEtsFileFromResource("$BASE_PATH/${programName}.ts.json") + } + } + + private fun projectAvailable(): Boolean { + val resource = object {}::class.java.getResource("/samples/source/project1")?.toURI() + return resource != null && resource.toPath().exists() + } + + @Test + fun `test taint analysis on MethodCollision`() { + val file = loadSample("MethodCollision") + val project = EtsScene(listOf(file)) + val graph = EtsApplicationGraphImpl(project) + val unitResolver = UnitResolver { SingletonUnit } + val getConfigForMethod: (EtsMethod) -> List? = + { method -> + val rules = buildList { + if (method.name == "isSame" && method.signature.enclosingClass.name == "Foo") add( + TaintMethodSource( + method = method, + condition = ConstantTrue, + actionsAfter = listOf( + AssignMark(mark = TaintMark("TAINT"), position = Result), + ), + ) + ) + if (method.name == "log") add( + TaintMethodSink( + method = method, + ruleNote = "CUSTOM SINK", // FIXME + cwe = listOf(), // FIXME + condition = ContainsMark(position = Argument(0), mark = TaintMark("TAINT")) + ) + ) + } + rules.ifEmpty { null } + } + val manager = TaintManager( + traits = EtsTraits(), + graph = graph, + unitResolver = unitResolver, + getConfigForMethod = getConfigForMethod, + ) + + val methods = project.projectClasses.flatMap { it.methods }.filter { it.name == "main" } + logger.info { "Methods: ${methods.size}" } + for (method in methods) { + logger.info { " ${method.name}" } + } + val sinks = manager.analyze(methods, timeout = 60.seconds) + logger.info { "Sinks: $sinks" } + Assertions.assertTrue(sinks.isNotEmpty()) + } + + @Test + fun `test taint analysis on TypeMismatch`() { + val file = loadSample("TypeMismatch") + val project = EtsScene(listOf(file)) + val graph = EtsApplicationGraphImpl(project) + val unitResolver = UnitResolver { SingletonUnit } + val getConfigForMethod: (EtsMethod) -> List? = + { method -> + val rules = buildList { + if (method.name == "add") add( + TaintMethodSource( + method = method, + condition = ConstantTrue, + actionsAfter = listOf( + AssignMark(mark = TaintMark("TAINT"), position = Result), + ) + ) + ) + if (method.name == "log") add( + TaintMethodSink( + method = method, + ruleNote = "CUSTOM SINK", // FIXME + cwe = listOf(), // FIXME + condition = ContainsMark(position = Argument(1), mark = TaintMark("TAINT")) + ) + ) + } + rules.ifEmpty { null } + } + val manager = TaintManager( + traits = EtsTraits(), + graph = graph, + unitResolver = unitResolver, + getConfigForMethod = getConfigForMethod, + ) + + val methods = project.projectClasses.flatMap { it.methods } + logger.info { "Methods: ${methods.size}" } + for (method in methods) { + logger.info { " ${method.name}" } + } + val sinks = manager.analyze(methods, timeout = 60.seconds) + logger.info { "Sinks: $sinks" } + Assertions.assertTrue(sinks.isNotEmpty()) + } + + @Disabled("TODO: Sink should be detected in the 'good' method") + @Test + fun `test taint analysis on DataFlowSecurity`() { + val file = loadSample("DataFlowSecurity") + val project = EtsScene(listOf(file)) + val graph = EtsApplicationGraphImpl(project) + val unitResolver = UnitResolver { SingletonUnit } + val getConfigForMethod: (EtsMethod) -> List? = + { method -> + val rules = buildList { + if (method.name == "samples/source") add( + TaintMethodSource( + method = method, + condition = ConstantTrue, + actionsAfter = listOf( + AssignMark(mark = TaintMark("TAINT"), position = Result), + ), + ) + ) + if (method.name == "sink") add( + TaintMethodSink( + method = method, + ruleNote = "SINK", // FIXME + cwe = listOf(), // FIXME + condition = ContainsMark(position = Argument(0), mark = TaintMark("TAINT")) + ) + ) + if (method.name == "pass") add( + TaintPassThrough( + method = method, + condition = ConstantTrue, + actionsAfter = listOf( + CopyAllMarks(from = Argument(0), to = Result) + ), + ) + ) + if (method.name == "validate") add( + TaintPassThrough( + method = method, + condition = ConstantTrue, + actionsAfter = listOf( + RemoveMark(mark = TaintMark("TAINT"), position = Argument(0)) + ), + ) + ) + } + rules.ifEmpty { null } + } + val manager = TaintManager( + traits = EtsTraits(), + graph = graph, + unitResolver = unitResolver, + getConfigForMethod = getConfigForMethod, + ) + + val goodMethod = project.projectClasses.flatMap { it.methods }.single { it.name == "good" } + logger.info { "good() method: $goodMethod" } + val goodSinks = manager.analyze(listOf(goodMethod), timeout = 60.seconds) + logger.info { "Sinks in good(): $goodSinks" } + Assertions.assertTrue(goodSinks.isEmpty()) + + val badMethod = project.projectClasses.flatMap { it.methods }.single { it.name == "bad" } + logger.info { "bad() method: $badMethod" } + val badSinks = manager.analyze(listOf(badMethod), timeout = 60.seconds) + logger.info { "Sinks in bad(): $badSinks" } + Assertions.assertTrue(badSinks.isNotEmpty()) + } + + @Test + fun `test taint analysis on case1 - untrusted loop bound scenario`() { + val file = loadSample("cases/case1") + val project = EtsScene(listOf(file)) + val graph = EtsApplicationGraphImpl(project) + val unitResolver = UnitResolver { SingletonUnit } + val getConfigForMethod: (EtsMethod) -> List? = + { method -> + val rules = buildList { + if (method.name == "readInt") add( + TaintMethodSource( + method = method, + condition = ConstantTrue, + actionsAfter = listOf( + AssignMark(mark = TaintMark("UNTRUSTED"), position = Result), + ), + ) + ) + } + rules.ifEmpty { null } + } + val manager = TaintManager( + traits = EtsTraits(), + graph = graph, + unitResolver = unitResolver, + getConfigForMethod = getConfigForMethod, + ) + TaintAnalysisOptions.UNTRUSTED_LOOP_BOUND_SINK = true + + val methods = project.projectClasses.flatMap { it.methods } + logger.info { "Methods: ${methods.size}" } + for (method in methods) { + logger.info { " ${method.name}" } + } + val sinks = manager.analyze(methods, timeout = 60.seconds) + logger.info { "Sinks: $sinks" } + Assertions.assertTrue(sinks.isNotEmpty()) + } + + @Test + fun `test taint analysis on case2 - untrusted array buffer size scenario`() { + val file = loadSample("cases/case2") + val project = EtsScene(listOf(file)) + val graph = EtsApplicationGraphImpl(project) + val unitResolver = UnitResolver { SingletonUnit } + val getConfigForMethod: (EtsMethod) -> List? = + { method -> + val rules = buildList { + if (method.name == "readInt") add( + TaintMethodSource( + method = method, + condition = ConstantTrue, + actionsAfter = listOf( + AssignMark(mark = TaintMark("UNTRUSTED"), position = Result), + ), + ) + ) + } + rules.ifEmpty { null } + } + val manager = TaintManager( + traits = EtsTraits(), + graph = graph, + unitResolver = unitResolver, + getConfigForMethod = getConfigForMethod, + ) + TaintAnalysisOptions.UNTRUSTED_ARRAY_SIZE_SINK = true + + val methods = project.projectClasses.flatMap { it.methods } + logger.info { "Methods: ${methods.size}" } + for (method in methods) { + logger.info { " ${method.name}" } + } + val sinks = manager.analyze(methods, timeout = 60.seconds) + logger.info { "Sinks: $sinks" } + Assertions.assertTrue(sinks.isNotEmpty()) + } + + // TODO(): support AnyArgument Position type for more flexible configs + @Test + fun `test taint analysis on case3 - send plain information with sensitive data`() { + val file = loadSample("cases/case3") + val project = EtsScene(listOf(file)) + val graph = EtsApplicationGraphImpl(project) + val unitResolver = UnitResolver { SingletonUnit } + val getConfigForMethod: (EtsMethod) -> List? = + { method -> + val rules = buildList { + if (method.name == "getPassword") add( + TaintMethodSource( + method = method, + condition = ConstantTrue, + actionsAfter = listOf( + AssignMark(mark = TaintMark("TAINT"), position = Result), + ), + ) + ) + if (method.name == "publishEvent") add( + TaintMethodSink( + method = method, ruleNote = "SINK", // FIXME + cwe = listOf(), // FIXME + condition = ContainsMark(position = Argument(1), mark = TaintMark("TAINT")) + ) + ) + } + rules.ifEmpty { null } + } + val manager = TaintManager( + traits = EtsTraits(), + graph = graph, + unitResolver = unitResolver, + getConfigForMethod = getConfigForMethod, + ) + + val methods = project.projectClasses.flatMap { it.methods } + logger.info { "Methods: ${methods.size}" } + for (method in methods) { + logger.info { " ${method.name}" } + } + val sinks = manager.analyze(methods, timeout = 60.seconds) + logger.info { "Sinks: $sinks" } + Assertions.assertTrue(sinks.isNotEmpty()) + } + + @EnabledIf("projectAvailable") + @Test + fun `test taint analysis on AccountManager`() { + val file = loadEtsFileFromResource("/etsir/project1/entry/src/main/ets/base/account/AccountManager.ts.json") + val project = EtsScene(listOf(file)) + val graph = EtsApplicationGraphImpl(project) + val unitResolver = UnitResolver { SingletonUnit } + val getConfigForMethod: (EtsMethod) -> List? = + { method -> + val rules = buildList { + // adhoc taint second argument (cursor: string) + if (method.name == "taintSink") add( + TaintMethodSink( + method = method, + cwe = listOf(), + ruleNote = "SINK", + condition = ContainsMark(position = Argument(0), mark = TaintMark("TAINT")), + ) + ) +// // encodeURI* +// if (method.name.startsWith("encodeURI")) add( +// TaintMethodSource( +// method = method, +// condition = ContainsMark(position = Argument(0), mark = TaintMark("UNSAFE")), +// actionsAfter = listOf( +// RemoveMark(position = Result, mark = TaintMark("UNSAFE")), +// ), +// ) +// ) +// // RequestOption.setUrl +// if (method.name == "setUrl") add( +// TaintMethodSource( +// method = method, +// condition = ConstantTrue, +// actionsAfter = listOf( +// CopyMark( +// mark = TaintMark("UNSAFE"), +// from = Argument(0), +// to = Result +// ), +// ), +// ) +// ) +// // HttpManager.requestSync +// if (method.name == "requestSync") add( +// TaintMethodSink( +// method = method, +// ruleNote = "Unsafe request", // FIXME +// cwe = listOf(), // FIXME +// condition = ContainsMark(position = Argument(0), mark = TaintMark("UNSAFE")) +// ) +// ) + // SyncUtil.requestGet + if (method.name == "requestGet") add( + TaintMethodSource( + method = method, + condition = ConstantTrue, + actionsAfter = listOf(AssignMark(position = Result, mark = TaintMark("TAINT"))) + ) + ) + } + rules.ifEmpty { null } + } + val manager = TaintManager( + traits = EtsTraits(), + graph = graph, + unitResolver = unitResolver, + getConfigForMethod = getConfigForMethod, + ) + + val methodNames = setOf( + "getDeviceIdListWithCursor", + "requestGet", + "taintRun", + "taintSink" + ) + + val methods = project.projectClasses.flatMap { it.methods }.filter { it.name in methodNames } + logger.info { "Methods: ${methods.size}" } + for (method in methods) { + logger.info { " ${method.name}" } + } + val sinks = manager.analyze(methods, timeout = 60.seconds) + logger.info { "Sinks: $sinks" } + Assertions.assertTrue(sinks.isNotEmpty()) + } +} diff --git a/usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/EtsProjectAnalysisTest.kt b/usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/EtsProjectAnalysisTest.kt new file mode 100644 index 0000000000..c8af281ee6 --- /dev/null +++ b/usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/EtsProjectAnalysisTest.kt @@ -0,0 +1,155 @@ +/* + * 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.test + +import mu.KotlinLogging +import org.jacodb.ets.base.EtsStmt +import org.jacodb.ets.model.EtsFile +import org.jacodb.ets.model.EtsMethod +import org.jacodb.ets.model.EtsScene +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 +import org.usvm.dataflow.util.getPathEdges +import java.nio.file.Files +import kotlin.io.path.exists +import kotlin.io.path.toPath +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds + +private val logger = KotlinLogging.logger {} + +class EtsProjectAnalysisTest { + private var tsLinesSuccess = 0L + private var tsLinesFailed = 0L + private var analysisTime: Duration = Duration.ZERO + private var totalPathEdges = 0 + private var totalSinks: MutableList> = mutableListOf() + + companion object { + private const val SOURCE_PROJECT_PATH = + "/projects/applications_app_samples/source/applications_app_samples/code/SuperFeature/DistributedAppDev/ArkTSDistributedCalc" + private const val PROJECT_PATH = "/projects/applications_app_samples/etsir/ast/ArkTSDistributedCalc" + private const val START_PATH = "/entry/src/main/ets" + private const val BASE_PATH = PROJECT_PATH + private const val SOURCE_BASE_PATH = SOURCE_PROJECT_PATH + START_PATH + + private fun loadFromProject(filename: String): EtsFile { + return loadEtsFileFromResource("$BASE_PATH/$filename.json") + } + + private fun countFileLines(path: String): Long { + return getResourceStream(path).bufferedReader().use { reader -> + reader.lines().count() + } + } + + val rules by lazy { loadRules("config1.json") } + } + + private fun projectAvailable(): Boolean { + val path = object {}::class.java.getResource(PROJECT_PATH)?.toURI()?.toPath() + return path != null && path.exists() + } + + @EnabledIf("projectAvailable") + @Test + fun processAllFiles() { + val baseDir = getResourcePath(BASE_PATH) + Files.walk(baseDir) + .filter { it.toString().endsWith(".json") } + .map { baseDir.relativize(it).toString().replace("\\", "/").substringBeforeLast('.') } + .forEach { filename -> + handleFile(filename) + } + makeReport() + } + + private fun makeReport() { + logger.info { "Analysis Report On $PROJECT_PATH" } + logger.info { "====================" } + logger.info { "Total files processed: ${tsLinesSuccess + tsLinesFailed}" } + logger.info { "Successfully processed lines: $tsLinesSuccess" } + logger.info { "Failed lines: $tsLinesFailed" } + logger.info { "Total analysis time: $analysisTime" } + logger.info { "Total path edges: $totalPathEdges" } + logger.info { "Found sinks: ${totalSinks.size}" } + + if (totalSinks.isNotEmpty()) { + totalSinks.forEachIndexed { idx, sink -> + logger.info { + """Detailed Sink Information: + | + |Sink ID: $idx + |Statement: ${sink.sink.statement} + |Fact: ${sink.sink.fact} + |Condition: ${sink.rule?.condition} + | + """.trimMargin() + } + } + } else { + logger.info { "No sinks found." } + } + logger.info { "====================" } + logger.info { "End of report" } + } + + private fun handleFile(filename: String) { + val fileLines = countFileLines("$SOURCE_BASE_PATH/$filename") + try { + logger.info { "Processing '$filename'" } + val file = loadFromProject(filename) + val project = EtsScene(listOf(file)) + val startTime = System.currentTimeMillis() + runAnalysis(project) + val endTime = System.currentTimeMillis() + analysisTime += (endTime - startTime).milliseconds + tsLinesSuccess += fileLines + } catch (e: Exception) { + logger.warn { "Failed to process '$filename': $e" } + logger.warn { e.stackTraceToString() } + tsLinesFailed += fileLines + } + } + + private fun runAnalysis(project: EtsScene) { + val graph = EtsApplicationGraphImpl(project) + val unitResolver = UnitResolver { SingletonUnit } + val manager = TaintManager( + traits = EtsTraits(), + graph = graph, + unitResolver = unitResolver, + getConfigForMethod = { method -> getConfigForMethod(method, rules) }, + ) + val methods = project.projectClasses.flatMap { it.methods } + val sinks = manager.analyze(methods, timeout = 10.seconds) + totalPathEdges += manager.runnerForUnit.values.sumOf { it.getPathEdges().size } + totalSinks += sinks + } +} diff --git a/usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/EtsSceneTest.kt b/usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/EtsSceneTest.kt new file mode 100644 index 0000000000..5ed683007b --- /dev/null +++ b/usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/EtsSceneTest.kt @@ -0,0 +1,184 @@ +/* + * 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.test + +import org.jacodb.ets.base.CONSTRUCTOR_NAME +import org.jacodb.ets.base.EtsArrayType +import org.jacodb.ets.base.EtsAssignStmt +import org.jacodb.ets.base.EtsCallStmt +import org.jacodb.ets.base.EtsClassType +import org.jacodb.ets.base.EtsInstLocation +import org.jacodb.ets.base.EtsInstanceCallExpr +import org.jacodb.ets.base.EtsLocal +import org.jacodb.ets.base.EtsReturnStmt +import org.jacodb.ets.base.EtsStringType +import org.jacodb.ets.base.EtsThis +import org.jacodb.ets.base.EtsVoidType +import org.jacodb.ets.graph.EtsCfg +import org.jacodb.ets.model.EtsClassImpl +import org.jacodb.ets.model.EtsClassSignature +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.EtsMethodImpl +import org.jacodb.ets.model.EtsMethodSignature +import org.jacodb.ets.model.EtsScene +import org.usvm.dataflow.ts.graph.EtsApplicationGraphImpl +import kotlin.test.Test +import kotlin.test.assertEquals + +class EtsSceneTest { + + @Test + fun `test create EtsScene with multiple files`() { + val fileCatSignature = EtsFileSignature( + projectName = "TestProject", + fileName = "cat.ts", + ) + val classCatSignature = EtsClassSignature( + name = "Cat", + file = fileCatSignature, + namespace = null, + ) + val fieldName = EtsFieldImpl( + signature = EtsFieldSignature( + enclosingClass = classCatSignature, + sub = EtsFieldSubSignature( + name = "name", + type = EtsStringType, + ) + ) + ) + val methodMeow = EtsMethodImpl( + signature = EtsMethodSignature( + enclosingClass = classCatSignature, + name = "meow", + parameters = emptyList(), + returnType = EtsVoidType, + ) + ) + val ctorCat = EtsMethodImpl( + signature = EtsMethodSignature( + enclosingClass = classCatSignature, + name = CONSTRUCTOR_NAME, + parameters = emptyList(), + returnType = EtsVoidType, + ), + ) + val classCat = EtsClassImpl( + signature = classCatSignature, + fields = listOf(fieldName), + methods = listOf(methodMeow), + ctor = ctorCat, + ) + val fileCat = EtsFile( + signature = fileCatSignature, + classes = listOf(classCat), + namespaces = emptyList(), + ) + + val fileBoxSignature = EtsFileSignature( + projectName = "TestProject", + fileName = "box.ts", + ) + val classBoxSignature = EtsClassSignature( + name = "Box", + file = fileBoxSignature, + namespace = null, + ) + val fieldCats = EtsFieldImpl( + signature = EtsFieldSignature( + enclosingClass = classBoxSignature, + sub = EtsFieldSubSignature( + name = "cats", + type = EtsArrayType(EtsClassType(classCatSignature), 1), + ) + ) + ) + val methodTouch = EtsMethodImpl( + signature = EtsMethodSignature( + enclosingClass = classBoxSignature, + name = "touch", + parameters = emptyList(), + returnType = EtsVoidType, + ) + ).also { + var index = 0 + val stmts = listOf( + EtsAssignStmt( + location = EtsInstLocation(it, index++), + lhv = EtsLocal("this", EtsClassType(classBoxSignature)), + rhv = EtsThis(EtsClassType(classBoxSignature)), + ), + EtsCallStmt( + location = EtsInstLocation(it, index++), + expr = EtsInstanceCallExpr( + instance = EtsLocal("this", EtsClassType(classBoxSignature)), + method = methodMeow.signature, + args = emptyList(), + ) + ), + EtsReturnStmt( + location = EtsInstLocation(it, index++), + returnValue = null, + ) + ) + check(index == stmts.size) + val successors = mapOf( + stmts[0] to listOf(stmts[1]), + stmts[1] to listOf(stmts[2]), + ) + it._cfg = EtsCfg( + stmts = stmts, + successorMap = successors, + ) + } + val ctorBox = EtsMethodImpl( + signature = EtsMethodSignature( + enclosingClass = classBoxSignature, + name = CONSTRUCTOR_NAME, + parameters = emptyList(), + returnType = EtsVoidType, + ), + ) + val classBox = EtsClassImpl( + signature = classBoxSignature, + fields = listOf(fieldCats), + methods = listOf(methodTouch), + ctor = ctorBox, + ) + val fileBox = EtsFile( + signature = fileBoxSignature, + classes = listOf(classBox), + namespaces = emptyList(), + ) + + val project = EtsScene(listOf(fileCat, fileBox)) + val graph = EtsApplicationGraphImpl(project) + + val callStmt = project.projectClasses + .asSequence() + .flatMap { it.methods } + .filter { it.name == "touch" } + .flatMap { it.cfg.stmts } + .filterIsInstance() + .first() + assertEquals(methodMeow, graph.callees(callStmt).first()) + } +} diff --git a/usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/EtsTaintAnalysisTest.kt b/usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/EtsTaintAnalysisTest.kt new file mode 100644 index 0000000000..6592c48f83 --- /dev/null +++ b/usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/EtsTaintAnalysisTest.kt @@ -0,0 +1,141 @@ +/* + * 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.test + +import mu.KotlinLogging +import org.jacodb.ets.model.EtsFile +import org.jacodb.ets.model.EtsMethod +import org.jacodb.ets.model.EtsScene +import org.jacodb.taint.configuration.Argument +import org.jacodb.taint.configuration.AssignMark +import org.jacodb.taint.configuration.ConstantTrue +import org.jacodb.taint.configuration.ContainsMark +import org.jacodb.taint.configuration.CopyAllMarks +import org.jacodb.taint.configuration.RemoveMark +import org.jacodb.taint.configuration.Result +import org.jacodb.taint.configuration.TaintConfigurationItem +import org.jacodb.taint.configuration.TaintMark +import org.jacodb.taint.configuration.TaintMethodSink +import org.jacodb.taint.configuration.TaintMethodSource +import org.jacodb.taint.configuration.TaintPassThrough +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +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 + +private val logger = KotlinLogging.logger {} + +@Disabled("Need to update IR") +class EtsTaintAnalysisTest { + + companion object { + private const val BASE_PATH = "/samples/etsir/ast" + private const val DECOMPILED_PATH = "/decompiled" + + private fun loadFromProject(name: String): EtsFile { + return loadEtsFileFromResource("$BASE_PATH/$name.ts.json") + } + + private fun loadDecompiled(name: String): EtsFile { + return loadEtsFileFromResource("$DECOMPILED_PATH/$name.abc.json") + } + + val getConfigForMethod: (EtsMethod) -> List? = + { method -> + val rules = buildList { + if (method.name == "source") add( + TaintMethodSource( + method = method, + condition = ConstantTrue, + actionsAfter = listOf( + AssignMark(mark = TaintMark("TAINT"), position = Result), + ), + ) + ) + if (method.name == "sink") add( + TaintMethodSink( + method = method, + ruleNote = "SINK", // FIXME + cwe = listOf(), // FIXME + condition = ContainsMark(position = Argument(0), mark = TaintMark("TAINT")) + ) + ) + if (method.name == "pass") add( + TaintPassThrough( + method = method, + condition = ConstantTrue, + actionsAfter = listOf( + CopyAllMarks(from = Argument(0), to = Result) + ), + ) + ) + if (method.name == "validate") add( + TaintPassThrough( + method = method, + condition = ConstantTrue, + actionsAfter = listOf( + RemoveMark(mark = TaintMark("TAINT"), position = Argument(0)) + ), + ) + ) + } + rules.ifEmpty { null } + } + } + + fun runTaintAnalysis(project: EtsScene) { + val graph = EtsApplicationGraphImpl(project) + val unitResolver = UnitResolver { SingletonUnit } + + val manager = TaintManager( + traits = EtsTraits(), + graph = graph, + unitResolver = unitResolver, + getConfigForMethod = getConfigForMethod, + ) + + val methods = project.projectClasses.flatMap { it.methods }.filter { it.name == "bad" } + logger.info { "Methods: ${methods.size}" } + for (method in methods) { + logger.info { " ${method.name}" } + } + val sinks = manager.analyze(methods, timeout = 60.seconds) + logger.info { "Sinks: $sinks" } + assertTrue(sinks.isNotEmpty()) + } + + @Test + fun `test taint analysis`() { + val file = loadFromProject("TaintAnalysis") + val project = EtsScene(listOf(file)) + runTaintAnalysis(project) + } + + @Disabled("Need to update the EtsIR-ABC json file") + @Test + fun `test taint analysis on decompiled file`() { + val file = loadDecompiled("TaintAnalysis") + val project = EtsScene(listOf(file)) + runTaintAnalysis(project) + } +} diff --git a/usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/EtsTypeAnnotationTest.kt b/usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/EtsTypeAnnotationTest.kt new file mode 100644 index 0000000000..6683bf7fcf --- /dev/null +++ b/usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/EtsTypeAnnotationTest.kt @@ -0,0 +1,193 @@ +/* + * 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.test + +import org.jacodb.ets.base.CONSTRUCTOR_NAME +import org.jacodb.ets.base.EtsAddExpr +import org.jacodb.ets.base.EtsAssignStmt +import org.jacodb.ets.base.EtsClassType +import org.jacodb.ets.base.EtsInstLocation +import org.jacodb.ets.base.EtsLocal +import org.jacodb.ets.base.EtsNumberType +import org.jacodb.ets.base.EtsParameterRef +import org.jacodb.ets.base.EtsReturnStmt +import org.jacodb.ets.base.EtsStmt +import org.jacodb.ets.base.EtsStringType +import org.jacodb.ets.base.EtsThis +import org.jacodb.ets.base.EtsType +import org.jacodb.ets.base.EtsUnknownType +import org.jacodb.ets.graph.EtsCfg +import org.jacodb.ets.model.EtsClassImpl +import org.jacodb.ets.model.EtsClassSignature +import org.jacodb.ets.model.EtsDecorator +import org.jacodb.ets.model.EtsFile +import org.jacodb.ets.model.EtsFileSignature +import org.jacodb.ets.model.EtsMethod +import org.jacodb.ets.model.EtsMethodParameter +import org.jacodb.ets.model.EtsMethodSignature +import org.jacodb.ets.model.EtsModifiers +import org.jacodb.ets.model.EtsScene +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.annotation.EtsTypeAnnotator +import org.usvm.dataflow.ts.infer.verify.LocalId +import org.usvm.dataflow.ts.infer.verify.MethodId +import org.usvm.dataflow.ts.infer.verify.ParameterId +import org.usvm.dataflow.ts.infer.verify.VerificationResult +import org.usvm.dataflow.ts.infer.verify.verify +import kotlin.test.Test + +internal class EtsTypeAnnotationTest { + @Test + fun `test EtsTypeAnnotator`() { + val typeInferenceResult = TypeInferenceResult( + inferredTypes = mapOf( + mainMethod to mapOf( + AccessPathBase.Arg(1) to EtsTypeFact.StringEtsTypeFact, + AccessPathBase.Arg(2) to EtsTypeFact.NumberEtsTypeFact, + AccessPathBase.Local("v1") to EtsTypeFact.StringEtsTypeFact, + AccessPathBase.Local("v2") to EtsTypeFact.NumberEtsTypeFact, + AccessPathBase.Local("v3") to EtsTypeFact.StringEtsTypeFact, + ) + ), + inferredReturnType = mapOf( + mainMethod to EtsTypeFact.StringEtsTypeFact + ), + inferredCombinedThisType = mapOf( + mainClassSignature to EtsTypeFact.ObjectEtsTypeFact( + cls = EtsClassType(mainClassSignature), + properties = mapOf(), + ) + ) + ) + + val annotatedScene = EtsTypeAnnotator(sampleScene, typeInferenceResult) + .annotateWithTypes(sampleScene) + + val verificationResult = verify(annotatedScene) + + assert(verificationResult is VerificationResult.Success) + + with(verificationResult as VerificationResult.Success) { + val main = MethodId(mainMethodSignature) + + assert(mapping[ParameterId(1, main)] == EtsStringType) + assert(mapping[ParameterId(2, main)] == EtsNumberType) + + assert(mapping[LocalId("v1", main)] == EtsStringType) + assert(mapping[LocalId("v2", main)] == EtsNumberType) + assert(mapping[LocalId("v3", main)] == EtsStringType) + } + } + + private val mainTs = EtsFileSignature( + projectName = "sampleProject", + fileName = "main.ts", + ) + + private val mainClassSignature = EtsClassSignature( + name = "MainClass", + file = mainTs, + namespace = null, + ) + + private val mainMethodSignature = EtsMethodSignature( + enclosingClass = mainClassSignature, + name = "mainMethod", + parameters = parameters(2), + returnType = EtsUnknownType, + ) + + private val mainMethod = buildMethod(mainMethodSignature) { + val arg1 = EtsParameterRef(1, EtsUnknownType) + val arg2 = EtsParameterRef(2, EtsUnknownType) + val v1 = EtsLocal("v1", EtsUnknownType) + val v2 = EtsLocal("v2", EtsUnknownType) + val v3 = EtsLocal("v3", EtsUnknownType) + + push(EtsAssignStmt(location, v1, arg1)) + push(EtsAssignStmt(location, v2, arg2)) + push(EtsAssignStmt(location, v3, EtsAddExpr(EtsUnknownType, v1, v2))) + push(EtsReturnStmt(location, v3)) + } + + private val mainClassCtorSignature = EtsMethodSignature( + enclosingClass = mainClassSignature, + name = CONSTRUCTOR_NAME, + parameters = parameters(1), + returnType = EtsUnknownType, + ) + + private val mainClassCtor = buildMethod(mainClassCtorSignature) { + val etsThis = EtsThis(EtsClassType(mainClassSignature)) + push(EtsReturnStmt(location, etsThis)) + } + + private val mainClass = EtsClassImpl( + signature = mainClassSignature, + fields = listOf(), + methods = listOf(mainMethod), + ctor = mainClassCtor, + superClass = null, + ) + + private val mainFile = EtsFile( + signature = mainTs, + classes = listOf(mainClass), + namespaces = listOf(), + ) + + private val sampleScene = EtsScene(listOf(mainFile)) + + private class CfgBuilderContext( + val method: EtsMethod, + ) { + private val stmts = mutableListOf() + private val successorsMap = mutableMapOf>() + + fun build(): EtsCfg = EtsCfg(stmts, successorsMap) + + fun push(stmt: EtsStmt) { + stmts.lastOrNull()?.let { link(it, stmt) } + successorsMap[stmt] = mutableListOf() + stmts.add(stmt) + } + + fun link(from: EtsStmt, to: EtsStmt) { + successorsMap.getOrPut(from, ::mutableListOf).add(to) + } + + val location: EtsInstLocation + get() = EtsInstLocation(method, stmts.size) + } + + private fun buildMethod( + signature: EtsMethodSignature, + cfgBuilder: CfgBuilderContext.() -> Unit, + ) = object : EtsMethod { + override val signature = signature + override val typeParameters: List = emptyList() + override val modifiers: EtsModifiers = EtsModifiers.EMPTY + override val decorators: List = emptyList() + override val locals: List = emptyList() + override val cfg = CfgBuilderContext(this).apply(cfgBuilder).build() + } + + private fun parameters(n: Int) = + List(n) { EtsMethodParameter(it, "a$it", EtsUnknownType) } +} diff --git a/usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/EtsTypeInferenceTest.kt b/usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/EtsTypeInferenceTest.kt new file mode 100644 index 0000000000..bf11d2f5d6 --- /dev/null +++ b/usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/EtsTypeInferenceTest.kt @@ -0,0 +1,593 @@ +/* + * 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.test + +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.runInterruptible +import kotlinx.coroutines.withTimeout +import kotlinx.serialization.SerializationException +import mu.KotlinLogging +import org.jacodb.ets.base.CONSTRUCTOR_NAME +import org.jacodb.ets.base.EtsAnyType +import org.jacodb.ets.base.EtsAssignStmt +import org.jacodb.ets.base.EtsLocal +import org.jacodb.ets.base.EtsStringConstant +import org.jacodb.ets.base.EtsType +import org.jacodb.ets.base.EtsUnknownType +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 org.jacodb.ets.utils.loadEtsFileAutoConvert +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.TypeInferenceManager +import org.usvm.dataflow.ts.infer.TypeInferenceResult +import org.usvm.dataflow.ts.infer.annotation.EtsTypeAnnotator +import org.usvm.dataflow.ts.infer.createApplicationGraph +import org.usvm.dataflow.ts.infer.dto.toType +import org.usvm.dataflow.ts.loadEtsProjectFromResources +import org.usvm.dataflow.ts.testFactory +import org.usvm.dataflow.ts.util.EtsTraits +import java.io.File +import kotlin.io.path.div +import kotlin.io.path.exists +import kotlin.io.path.toPath +import kotlin.test.assertContains +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotEquals +import kotlin.test.assertTrue + +private val logger = KotlinLogging.logger {} + +class EtsTypeInferenceTest { + + companion object { + private fun load(path: String): EtsFile { + return loadEtsFileAutoConvert(getResourcePath(path), useArkAnalyzerTypeInference = null) + } + } + + @Test + fun `type inference for microphone`() { + val name = "microphone" + val file = load("/ts/$name.ts") + val project = EtsScene(listOf(file)) + val graph = createApplicationGraph(project) + + val entrypoints = project.projectClasses + .flatMap { it.methods } + .filter { it.name.startsWith("entrypoint") } + println("entrypoints: (${entrypoints.size})") + entrypoints.forEach { + println(" ${it.signature.enclosingClass.name}::${it.name}") + } + + val manager = TypeInferenceManager(EtsTraits(), graph) + val result = manager.analyze(entrypoints, doAddKnownTypes = false) + val types = result.inferredTypes + + run { + val m = types.keys.first { it.name == "getMicrophoneUuid" } + + // arg0 = 'devices' + val devices = types[m]!![AccessPathBase.Arg(0)]!! + assertIs(devices) + + val devicesCls = devices.cls + assertEquals("VirtualDevices", devicesCls?.typeName) + + assertContains(devices.properties, "microphone") + val microphone = devices.properties["microphone"]!! + assertIs(microphone) + + assertContains(microphone.properties, "uuid") + val uuid = microphone.properties["uuid"]!! + assertIs(uuid) + } + } + + @Test + fun `type inference for types`() { + val name = "types" + val file = load("/ts/$name.ts") + val project = EtsScene(listOf(file)) + val graph = createApplicationGraph(project) + + val entrypoints = project.projectClasses + .flatMap { it.methods } + .filter { it.name.startsWith("entrypoint") } + println("entrypoints: (${entrypoints.size})") + entrypoints.forEach { + println(" ${it.signature.enclosingClass.name}::${it.name}") + } + + val manager = TypeInferenceManager(EtsTraits(), graph) + manager.analyze(entrypoints) + } + + @Test + fun `type inference for data`() { + val name = "data" + val file = load("/ts/$name.ts") + val project = EtsScene(listOf(file)) + val graph = createApplicationGraph(project) + + val entrypoints = project.projectClasses + .flatMap { it.methods } + .filter { it.name.startsWith("entrypoint") } + println("entrypoints: (${entrypoints.size})") + entrypoints.forEach { + println(" ${it.signature.enclosingClass.name}::${it.name}") + } + + val manager = TypeInferenceManager(EtsTraits(), graph) + manager.analyze(entrypoints) + } + + @Test + fun `type inference for call`() { + val name = "call" + val file = load("/ts/$name.ts") + val project = EtsScene(listOf(file)) + val graph = createApplicationGraph(project) + + val entrypoints = project.projectClasses + .flatMap { it.methods } + .filter { it.name.startsWith("entrypoint") } + println("entrypoints: (${entrypoints.size})") + entrypoints.forEach { + println(" ${it.signature.enclosingClass.name}::${it.name}") + } + + val manager = TypeInferenceManager(EtsTraits(), graph) + manager.analyze(entrypoints) + } + + @Test + fun `type inference for nested_init`() { + val name = "nested_init" + val file = load("/ts/$name.ts") + val project = EtsScene(listOf(file)) + val graph = createApplicationGraph(project) + + val entrypoints = project.projectClasses + .flatMap { it.methods } + .filter { it.name.startsWith("entrypoint") } + println("entrypoints: (${entrypoints.size})") + entrypoints.forEach { + println(" ${it.signature.enclosingClass.name}::${it.name}") + } + + val manager = TypeInferenceManager(EtsTraits(), graph) + manager.analyze(entrypoints) + } + + private fun resourceAvailable(dirName: String): Boolean = + object {}::class.java.getResource(dirName) != null + + private fun testHapSet(setPath: String) { + val abcDir = object {}::class.java.getResource(setPath)?.toURI()?.toPath() + ?: error("Resource not found: $setPath") + val haps = abcDir.toFile().listFiles()?.toList() ?: emptyList() + processAllHAPs(haps) + } + + private val TEST_PROJECTS_PATH = "/projects/abcir/" + private fun testProjectsAvailable() = resourceAvailable(TEST_PROJECTS_PATH) + + @Test + @EnabledIf("testProjectsAvailable") + fun `type inference for test projects`() = testHapSet(TEST_PROJECTS_PATH) + + @Test + @Disabled("No project found") + fun `test single HAP`() { + val abcDirName = "/TestProjects/CertificateManager_240801_843398b" + val projectDir = object {}::class.java.getResource(abcDirName)?.toURI()?.toPath() + ?: error("Resource not found: $abcDirName") + val (scene, result) = testHap(projectDir.toString()) + val scene2 = EtsTypeAnnotator(scene, result).annotateWithTypes(scene) + } + + private fun processAllHAPs(haps: Collection) { + val succeed = mutableListOf() + val timeout = mutableListOf() + val failed = mutableListOf() + + haps.forEach { project -> + try { + runBlocking { + withTimeout(60_000) { + runInterruptible { + testHap(project.path) + } + } + } + succeed += project.name + println("$project - SUCCESS") + } catch (_: TimeoutCancellationException) { + timeout += project.name + println("$project - TIMEOUT") + } catch (e: SerializationException) { + e.printStackTrace() + error("Serialization exception") + } catch (e: Throwable) { + failed += project.name + println("$project - FAILED") + e.printStackTrace() + } + } + + println("Total: ${haps.size} HAPs") + println("Succeed: ${succeed.size}") + println("Timeout: ${timeout.size}") + println("Failed: ${failed.size}") + + println("PASSED") + succeed.forEach { + println(it) + } + println("TIMEOUT") + timeout.forEach { + println(it) + } + println("FAILED") + failed.forEach { + println(it) + } + } + + private fun testHap(projectDir: String): Pair { + val dir = File(projectDir).takeIf { it.isDirectory } ?: error("Not found project dir $projectDir") + println("Found project dir: '$dir'") + + val files = dir + .walk() + .filter { it.extension == "json" } + .toList() + println("Found files: (${files.size})") + for (path in files) { + println(" ${path.relativeTo(dir)}") + } + + println("Processing ${files.size} files...") + val etsFiles = files.map { convertToEtsFile(EtsFileDto.loadFromJson(it.inputStream())) } + val project = EtsScene(etsFiles) + val graph = createApplicationGraph(project) + + val entrypoints = project.projectClasses + .flatMap { it.methods + it.ctor } + .filter { it.isPublic || it.name == CONSTRUCTOR_NAME } + .filter { !it.enclosingClass.name.startsWith("AnonymousClass") } + println("entrypoints: (${entrypoints.size})") + entrypoints.forEach { + println(" ${it.signature.enclosingClass.name}::${it.name}") + } + + val manager = TypeInferenceManager(EtsTraits(), graph) + val result = manager.analyze(entrypoints) + return Pair(project, result) + } + + @Test + fun `test if guesser does anything`() { + val name = "testcases" + val file = load("/ts/$name.ts") + val project = EtsScene(listOf(file)) + val graph = createApplicationGraph(project) + + val entrypoints = project.projectClasses + .flatMap { it.methods } + .filter { it.name == "entrypoint" } + println("entrypoints: (${entrypoints.size})") + entrypoints.forEach { + println(" ${it.signature.enclosingClass.name}::${it.name}") + } + + val manager = TypeInferenceManager(EtsTraits(), graph) + val resultWithoutGuessed = manager.analyze(entrypoints) + val resultWithGuessed = resultWithoutGuessed.withGuessedTypes(project) + + assertNotEquals(resultWithoutGuessed.inferredTypes, resultWithGuessed.inferredTypes) + + println("=".repeat(42)) + println("Inferred types WITHOUT guesser: ") + for ((method, types) in resultWithoutGuessed.inferredTypes) { + println(method.enclosingClass.name to types) + } + + println("=".repeat(42)) + println("Inferred types with guesser: ") + for ((method, types) in resultWithGuessed.inferredTypes) { + println(method.enclosingClass.name to types) + } + } + + // TODO: support these complex tests + private val disabledTests = setOf( + "CaseAssignFieldToSelf", + "CaseLoop", + "CaseNew", + "CaseRecursion", + ) + + @TestFactory + fun `type inference on testcases`() = testFactory { + val file = load("/ts/testcases.ts") + val project = EtsScene(listOf(file)) + val graph = createApplicationGraph(project) + + val allCases = project.projectClasses.filter { it.name.startsWith("Case") } + + for (cls in allCases) { + if (cls.name in disabledTests) continue + test(name = cls.name) { + logger.info { "Analyzing testcase: ${cls.name}" } + + val inferMethod = cls.methods.single { it.name == "infer" } + logger.info { "Found infer: ${inferMethod.signature}" } + + val expectedTypeString = mutableMapOf() + var expectedReturnTypeString = "" + for (inst in inferMethod.cfg.stmts) { + if (inst is EtsAssignStmt) { + val lhv = inst.lhv + if (lhv is EtsLocal) { + val rhv = inst.rhv + if (lhv.name.startsWith("EXPECTED_ARG_")) { + check(rhv is EtsStringConstant) + val arg = lhv.name.removePrefix("EXPECTED_ARG_").toInt() + val pos = AccessPathBase.Arg(arg) + expectedTypeString[pos] = rhv.value + logger.info { "Expected type for $pos: ${rhv.value}" } + } else if (lhv.name == "EXPECTED_RETURN") { + check(rhv is EtsStringConstant) + expectedReturnTypeString = rhv.value + logger.info { "Expected return type: ${rhv.value}" } + } else if (lhv.name.startsWith("EXPECTED")) { + logger.error { "Skipping unexpected local: $lhv" } + } + } + } + } + + val entrypoint = cls.methods.single { it.name == "entrypoint" } + logger.info { "Found entrypoint: ${entrypoint.signature}" } + + val manager = TypeInferenceManager(EtsTraits(), graph) + val result = manager.analyze(listOf(entrypoint), doAddKnownTypes = false) + + val inferredTypes = result.inferredTypes[inferMethod] + ?: error("No inferred types for method ${inferMethod.enclosingClass.name}::${inferMethod.name}") + + for (position in expectedTypeString.keys.sortedBy { + when (it) { + is AccessPathBase.This -> -1 + is AccessPathBase.Arg -> it.index + else -> 1_000_000 + } + }) { + val expected = expectedTypeString[position]!! + val inferred = inferredTypes[position] + logger.info { "Inferred type for $position: $inferred" } + val passed = inferred.toString() == expected + assertTrue( + passed, + "Inferred type for $position does not match: inferred = $inferred, expected = $expected" + ) + } + if (expectedReturnTypeString.isNotBlank()) { + val expected = expectedReturnTypeString + val inferred = result.inferredReturnType[inferMethod] + logger.info { "Inferred return type: $inferred" } + val passed = inferred.toString() == expected + assertTrue( + passed, + "Inferred return type does not match: inferred = $inferred, expected = $expected" + ) + } + } + } + } + + @TestFactory + fun `test type inference on projects`() = 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 }.sorted() + logger.info { + buildString { + appendLine("Found projects: ${availableProjectNames.size}") + for (name in availableProjectNames) { + appendLine(" - $name") + } + } + } + if (availableProjectNames.isEmpty()) { + logger.warn { "No projects found" } + return@testFactory + } + for (projectName in availableProjectNames) { + // if (projectName != "Launcher") continue + // if (projectName != "Demo_Calc") continue + test("infer types in $projectName") { + 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@test + } + 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@test + } + val project = loadEtsProjectFromResources(modules, "/projects/$projectName/etsir") + logger.info { + buildString { + appendLine("Loaded project with ${project.projectAndSdkClasses.size} classes and ${project.projectClasses.sumOf { it.methods.size }} methods") + for (cls in project.projectAndSdkClasses.sortedBy { it.name }) { + appendLine("= ${cls.signature} with ${cls.methods.size} methods:") + for (method in cls.methods.sortedBy { it.name }) { + appendLine(" - ${method.signature}") + } + } + } + } + val graph = createApplicationGraph(project) + + val entrypoints = project.projectAndSdkClasses + .flatMap { it.methods } + .filter { it.isPublic } + logger.info { "Found ${entrypoints.size} entrypoints" } + + val manager = TypeInferenceManager(EtsTraits(), graph) + val result = manager.analyze(entrypoints) + + logger.info { + buildString { + appendLine("Inferred types: ${result.inferredTypes.size}") + for ((method, types) in result.inferredTypes.entries.sortedBy { "${it.key.enclosingClass.name}::${it.key.name}" }) { + appendLine() + appendLine("- $method") + for ((pos, type) in types.entries.sortedBy { + when (val base = it.key) { + is AccessPathBase.This -> -1 + is AccessPathBase.Arg -> base.index + else -> 1_000_000 + } + }) { + appendLine("$pos: $type") + } + } + } + } + logger.info { + buildString { + appendLine("Inferred return types: ${result.inferredReturnType.size}") + for ((method, returnType) in result.inferredReturnType.entries.sortedBy { it.key.toString() }) { + appendLine("${method.enclosingClass.name}::${method.name}: $returnType") + } + } + } + logger.info { + buildString { + appendLine("Inferred combined this types: ${result.inferredCombinedThisType.size}") + for ((clazz, thisType) in result.inferredCombinedThisType.entries.sortedBy { it.key.toString() }) { + appendLine("${clazz.name} in ${clazz.file}: $thisType") + } + } + } + + var totalNumMatchedNormal = 0 + var totalNumMatchedUnknown = 0 + var totalNumMismatchedNormal = 0 + var totalNumLostNormal = 0 + var totalNumBetterThanUnknown = 0 + + for ((method, inferredTypes) in result.inferredTypes) { + var numMatchedNormal = 0 + var numMatchedUnknown = 0 + var numMismatchedNormal = 0 + var numLostNormal = 0 + var numBetterThanUnknown = 0 + + for (local in method.locals) { + val inferredType = inferredTypes[AccessPathBase.Local(local.name)]?.toType() + + logger.info { + "Local ${local.name} in ${method.enclosingClass.name}::${method.name}, known type: ${local.type}, inferred type: $inferredType" + } + + if (inferredType != null) { + if (local.type.isUnknown()) { + if (inferredType.isUnknown()) { + logger.info { "Matched unknown" } + numMatchedUnknown++ + } else { + logger.info { "Better than unknown" } + numBetterThanUnknown++ + } + } else { + if (inferredType == local.type) { + logger.info { "Matched normal" } + numMatchedNormal++ + } else { + logger.info { "Mismatched normal" } + numMismatchedNormal++ + } + } + } else { + if (local.type.isUnknown()) { + logger.info { "Matched (lost) unknown" } + numMatchedUnknown++ + } else { + logger.info { "Lost normal" } + numLostNormal++ + } + } + } + + logger.info { + buildString { + appendLine("Local type matching for ${method.enclosingClass.name}::${method.name}:") + appendLine(" Matched normal: $numMatchedNormal") + appendLine(" Matched unknown: $numMatchedUnknown") + appendLine(" Mismatched normal: $numMismatchedNormal") + appendLine(" Lost normal: $numLostNormal") + appendLine(" Better than unknown: $numBetterThanUnknown") + } + } + totalNumMatchedNormal += numMatchedNormal + totalNumMatchedUnknown += numMatchedUnknown + totalNumMismatchedNormal += numMismatchedNormal + totalNumLostNormal += numLostNormal + totalNumBetterThanUnknown += numBetterThanUnknown + } + + logger.info { + buildString { + appendLine("Total local type matching statistics:") + appendLine(" Matched normal: $totalNumMatchedNormal") + appendLine(" Matched unknown: $totalNumMatchedUnknown") + appendLine(" Mismatched normal: $totalNumMismatchedNormal") + appendLine(" Lost normal: $totalNumLostNormal") + appendLine(" Better than unknown: $totalNumBetterThanUnknown") + } + } + + logger.info { "Done analyzing project: $projectName" } + } + } + } +} + +private fun EtsType.isUnknown(): Boolean = + this == EtsUnknownType || this == EtsAnyType diff --git a/usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/EtsTypeResolverAbcTest.kt b/usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/EtsTypeResolverAbcTest.kt new file mode 100644 index 0000000000..41dd572fe3 --- /dev/null +++ b/usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/EtsTypeResolverAbcTest.kt @@ -0,0 +1,93 @@ +package org.usvm.dataflow.ts.test + +import org.jacodb.ets.utils.loadEtsProjectFromIR +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.condition.EnabledIf +import org.usvm.dataflow.ts.infer.EntryPointsProcessor +import org.usvm.dataflow.ts.infer.TypeInferenceManager +import org.usvm.dataflow.ts.infer.createApplicationGraph +import org.usvm.dataflow.ts.util.EtsTraits +import org.usvm.dataflow.ts.util.MethodTypesFacts +import org.usvm.dataflow.ts.util.TypeInferenceStatistics +import kotlin.io.path.Path +import kotlin.io.path.exists + +@EnabledIf("projectAvailable") +class EtsTypeResolverAbcTest { + + companion object { + private val yourPrefixForTestFolders = "C:/work/TestProjects" + private val testProjectsVersion = "TestProjects_2024_12_5" + private val pathToSDK: String? = null // TODO: Put your path here + + @JvmStatic + private fun projectAvailable(): Boolean { + return Path(yourPrefixForTestFolders).exists() + } + } + + private fun runOnAbcProject(projectID: String, abcPath: String) { + val projectAbc = "$yourPrefixForTestFolders/$testProjectsVersion/$abcPath" + val abcScene = loadEtsProjectFromIR(Path(projectAbc), pathToSDK?.let { Path(it) }) + val graphAbc = createApplicationGraph(abcScene) + + val entrypoint = EntryPointsProcessor.extractEntryPoints(abcScene) + val allMethods = entrypoint.allMethods.filter { it.isPublic }.filter { it.cfg.stmts.isNotEmpty() } + + val manager = TypeInferenceManager(EtsTraits(), graphAbc) + val result = manager + .analyze(entrypoint.mainMethods, allMethods) + .withGuessedTypes(abcScene) + + val sceneStatistics = TypeInferenceStatistics() + entrypoint.allMethods + .filter { it.cfg.stmts.isNotEmpty() } + .forEach { + val methodTypeFacts = MethodTypesFacts.from(result, it) + sceneStatistics.compareSingleMethodFactsWithTypesInScene(methodTypeFacts, it, graphAbc) + } + sceneStatistics.dumpStatistics("${projectID}_scene_comparison.txt") + } + + @Test + fun testLoadAbcProject1() = runOnAbcProject( + projectID = "project1", + abcPath = "callui-default-signed", + ) + + @Test + fun testLoadAbcProject2() = runOnAbcProject( + projectID = "project2", + abcPath = "CertificateManager_240801_843398b", + ) + + @Test + fun testLoadAbcProject3() = runOnAbcProject( + projectID = "project3", + abcPath = "mobiledatasettings-callui-default-signed", + ) + + @Test + fun testLoadAbcProject4() = runOnAbcProject( + projectID = "project4", + abcPath = "Music_Demo_240727_98a3500", + ) + + @Test + fun testLoadAbcProject5() = runOnAbcProject( + projectID = "project5", + abcPath = "phone_photos-default-signed_20240905_151755", + ) + + @Test + fun testLoadAbcProject6() = runOnAbcProject( + projectID = "project6", + abcPath = "phone-default-signed_20240409_144519", + ) + + @Test + fun testLoadAbcProject7() = runOnAbcProject( + projectID = "project7", + abcPath = "SecurityPrivacyCenter_240801_843998b", + ) +} diff --git a/usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/EtsTypeResolverWithAstTest.kt b/usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/EtsTypeResolverWithAstTest.kt new file mode 100644 index 0000000000..74b4c87a76 --- /dev/null +++ b/usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/EtsTypeResolverWithAstTest.kt @@ -0,0 +1,366 @@ +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.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.graph.EtsApplicationGraph +import org.usvm.dataflow.ts.infer.AccessPathBase +import org.usvm.dataflow.ts.infer.EntryPointsProcessor +import org.usvm.dataflow.ts.infer.EtsApplicationGraphWithExplicitEntryPoint +import org.usvm.dataflow.ts.infer.EtsTypeFact +import org.usvm.dataflow.ts.infer.TypeInferenceManager +import org.usvm.dataflow.ts.infer.TypeInferenceResult +import org.usvm.dataflow.ts.infer.createApplicationGraph +import org.usvm.dataflow.ts.test.utils.ClassMatcherStatistics +import org.usvm.dataflow.ts.test.utils.ExpectedTypesExtractor +import org.usvm.dataflow.ts.util.EtsTraits +import org.usvm.dataflow.ts.util.MethodTypesFacts +import org.usvm.dataflow.ts.util.TypeInferenceStatistics +import java.nio.file.Paths +import kotlin.io.path.Path +import kotlin.io.path.exists +import kotlin.test.assertTrue + +@EnabledIf("projectAvailable") +class EtsTypeResolverWithAstTest { + companion object { + private val yourPrefixForTestFolders = "C:/work/TestProjects" + private val testProjectsVersion = "TestProjects_2024_12_5" + private val pathToSDK: String? = null // TODO: Put your path here + + @JvmStatic + private fun projectAvailable(): Boolean { + return Path(yourPrefixForTestFolders).exists() + } + + private fun load(name: String): EtsFile { + return loadEtsFileAutoConvert(Paths.get("/ts/$name.ts")) + } + } + + @Test + fun testTestHap() { + val projectAbc = "$yourPrefixForTestFolders/$testProjectsVersion/CallUI" + val abcScene = loadEtsProjectFromIR(Path(projectAbc), pathToSDK?.let { Path(it) }) + val graphAbc = createApplicationGraph(abcScene) + + val entrypoint = EntryPointsProcessor.extractEntryPoints(abcScene) // TODO fix error with abc and ast methods + + val manager = TypeInferenceManager(EtsTraits(), graphAbc) + val resultBasic = manager.analyze( + entrypoints = entrypoint.mainMethods, + allMethods = entrypoint.allMethods, + ) + val result = resultBasic.withGuessedTypes(abcScene) + + val classMatcherStatistics = ClassMatcherStatistics() + + // TODO fix error with abc and ast methods + saveTypeInferenceComparison( + entrypoint.allMethods, + entrypoint.allMethods, + graphAbc, + graphAbc, + result, + classMatcherStatistics, + abcScene, + ) + classMatcherStatistics.dumpStatistics("callkit.txt") + } + + fun runOnProjectWithAstComparison(projectID: String, abcPath: String, astPath: String) { + val projectAbc = "$yourPrefixForTestFolders/$testProjectsVersion/$abcPath" + val abcScene = loadEtsProjectFromIR(Path(projectAbc), pathToSDK?.let { Path(it) }) + + val projectAst = "$yourPrefixForTestFolders/AST/$astPath" + val astScene = loadEtsProjectAutoConvert(Paths.get(projectAst)) + + val graphAbc = createApplicationGraph(abcScene) as EtsApplicationGraphWithExplicitEntryPoint + val graphAst = createApplicationGraph(astScene) as EtsApplicationGraphWithExplicitEntryPoint + + val entrypoint = EntryPointsProcessor.extractEntryPoints(abcScene) + val astMethods = extractAllAstMethods(astScene, abcScene) + + println(entrypoint.mainMethods.map { it.signature }) + println(entrypoint.allMethods.map { it.signature }) + + val manager = TypeInferenceManager(EtsTraits(), graphAbc) + val resultBasic = manager.analyze( + entrypoints = entrypoint.mainMethods, + allMethods = entrypoint.allMethods.filter { it.isPublic }, + ) + val result = resultBasic.withGuessedTypes(abcScene) + + val classMatcherStatistics = ClassMatcherStatistics() + saveTypeInferenceComparison( + astMethods, + entrypoint.allMethods, + graphAst, + graphAbc, + result, + classMatcherStatistics, + abcScene + ) + classMatcherStatistics.dumpStatistics("$projectID.txt") + + val sceneStatistics = TypeInferenceStatistics() + abcScene.projectAndSdkClasses + .flatMap { it.methods } + .forEach { + val facts = MethodTypesFacts.from(result, it) + sceneStatistics.compareSingleMethodFactsWithTypesInScene(facts, it, graphAbc) + } + sceneStatistics.dumpStatistics("${projectID}_scene_comparison.txt") + } + + @Test + fun testLoadProject1() = runOnProjectWithAstComparison( + projectID = "project1", + abcPath = "callui-default-signed", + astPath = "16_CallUI/applications_call_230923_4de8" + ) + + @Test + fun testLoadProject2() = runOnProjectWithAstComparison( + projectID = "project2", + abcPath = "CertificateManager_240801_843398b", + astPath = "13_SecurityPrivacyCenter/security_privacy_center" + ) + + @Test + fun testLoadProject3() = runOnProjectWithAstComparison( + projectID = "project3", + abcPath = "mobiledatasettings-callui-default-signed", + astPath = "16_CallUI/applications_call_230923_4de8" + ) + + @Test + fun testLoadProject4() = runOnProjectWithAstComparison( + projectID = "project4", + abcPath = "Music_Demo_240727_98a3500", + astPath = "16_CallUI/applications_call_230923_4de8" + ) + + @Test + fun testLoadProject5() = runOnProjectWithAstComparison( + projectID = "project5", + abcPath = "phone_photos-default-signed_20240905_151755", + astPath = "15_Photos/applications_photos_240905_ea8d1" + ) + + @Test + fun testLoadProject6() = runOnProjectWithAstComparison( + projectID = "project6", + abcPath = "phone-default-signed_20240409_144519", + astPath = "17_Camera/applications_camera_240409_1da805f8" + ) + + @Test + fun testLoadProject7() = runOnProjectWithAstComparison( + projectID = "project7", + abcPath = "SecurityPrivacyCenter_240801_843998b", + astPath = "13_SecurityPrivacyCenter/security_privacy_center" + ) + + @Test + fun `use non unique fields`() { + val file = load("resolver_test") + + val project = EtsScene(listOf(file)) + val graph = createApplicationGraph(project) + + val entrypoint = project.projectClasses + .flatMap { it.methods } + .single { it.name == "useNonUniqueField" } + + val manager = TypeInferenceManager(EtsTraits(), graph) + val resultBasic = manager.analyze(listOf(entrypoint)) + val result = resultBasic.withGuessedTypes(project) + + checkAnObjectTypeOfSingleArgument(result.inferredTypes[entrypoint]!!) { typeFact: EtsTypeFact.ObjectEtsTypeFact -> + typeFact.cls == null && typeFact.properties.keys.single() == "defaultA" + } + + val expectedTypes = ExpectedTypesExtractor(graph).extractTypes(entrypoint) + val actualTypes = MethodTypesFacts.from(result, entrypoint) + + assertFalse(expectedTypes.matchesWithTypeFacts(actualTypes, ignoreReturnType = true, project)) + } + + @Test + fun `use unique fields`() { + val file = load("resolver_test") + + val project = EtsScene(listOf(file)) + val graph = createApplicationGraph(project) + + val entrypoint = project.projectClasses + .flatMap { it.methods } + .single { it.name == "useUniqueFields" } + + val manager = TypeInferenceManager(EtsTraits(), graph) + val resultBasic = manager.analyze(listOf(entrypoint)) + val result = resultBasic.withGuessedTypes(project) + + checkAnObjectTypeOfSingleArgument(result.inferredTypes[entrypoint]!!) { fact: EtsTypeFact.ObjectEtsTypeFact -> + fact.cls?.typeName == "FieldContainerToInfer" && fact.properties.isEmpty() + } + + val expectedTypes = ExpectedTypesExtractor(graph).extractTypes(entrypoint) + val actualTypes = MethodTypesFacts.from(result, entrypoint) + + assertTrue(expectedTypes.matchesWithTypeFacts(actualTypes, ignoreReturnType = true, project)) + } + + @Test + fun `use unique and non unique fields`() { + val file = load("resolver_test") + + val project = EtsScene(listOf(file)) + val graph = createApplicationGraph(project) + + val entrypoint = project.projectClasses + .flatMap { it.methods } + .single { it.name == "useBothA" } + + val expectedTypes = ExpectedTypesExtractor(graph).extractTypes(entrypoint) + + val manager = TypeInferenceManager(EtsTraits(), graph) + val resultBasic = manager.analyze(listOf(entrypoint)) + val result = resultBasic.withGuessedTypes(project) + + checkAnObjectTypeOfSingleArgument(result.inferredTypes[entrypoint]!!) { fact: EtsTypeFact.ObjectEtsTypeFact -> + fact.cls?.typeName == "FieldContainerToInfer" && fact.properties.isEmpty() + } + + val actualTypes = MethodTypesFacts.from(result, entrypoint) + + assertTrue(expectedTypes.matchesWithTypeFacts(actualTypes, ignoreReturnType = true, project)) + } + + @Test + fun `use unique methods`() { + val file = load("resolver_test") + + val project = EtsScene(listOf(file)) + val graph = createApplicationGraph(project) + + val entrypoint = project.projectClasses + .flatMap { it.methods } + .single { it.name == "useUniqueMethods" } + + val manager = TypeInferenceManager(EtsTraits(), graph) + val resultBasic = manager.analyze(listOf(entrypoint)) + val result = resultBasic.withGuessedTypes(project) + + checkAnObjectTypeOfSingleArgument(result.inferredTypes[entrypoint]!!) { typeFact: EtsTypeFact.ObjectEtsTypeFact -> + typeFact.cls?.typeName == "MethodsContainerToInfer" && typeFact.properties.isEmpty() + } + + val expectedTypes = ExpectedTypesExtractor(graph).extractTypes(entrypoint) + val actualTypes = MethodTypesFacts.from(result, entrypoint) + + assertTrue(expectedTypes.matchesWithTypeFacts(actualTypes, ignoreReturnType = true, project)) + } + + @Test + fun `use non unique methods`() { + val file = load("resolver_test") + + val project = EtsScene(listOf(file)) + val graph = createApplicationGraph(project) + + val entrypoint = project.projectClasses + .flatMap { it.methods } + .single { it.name == "useNotUniqueMethod" } + + val manager = TypeInferenceManager(EtsTraits(), graph) + val resultBasic = manager.analyze(listOf(entrypoint)) + val result = resultBasic.withGuessedTypes(project) + + checkAnObjectTypeOfSingleArgument(result.inferredTypes[entrypoint]!!) { typeFact: EtsTypeFact.ObjectEtsTypeFact -> + typeFact.cls == null && typeFact.properties.keys.single() == "notUniqueFunction" + } + + val expectedTypes = ExpectedTypesExtractor(graph).extractTypes(entrypoint) + val actualTypes = MethodTypesFacts.from(result, entrypoint) + + assertFalse(expectedTypes.matchesWithTypeFacts(actualTypes, ignoreReturnType = true, project)) + } + + @Test + fun `use function and field`() { + val file = load("resolver_test") + + val project = EtsScene(listOf(file)) + val graph = createApplicationGraph(project) + + val entrypoint = project.projectClasses + .flatMap { it.methods } + .single { it.name == "useFunctionAndField" } + + val manager = TypeInferenceManager(EtsTraits(), graph) + val resultBasic = manager.analyze(listOf(entrypoint)) + val result = resultBasic.withGuessedTypes(project) + + checkAnObjectTypeOfSingleArgument(result.inferredTypes[entrypoint]!!) { typeFact: EtsTypeFact.ObjectEtsTypeFact -> + typeFact.cls?.typeName == "FieldContainerToInfer" && typeFact.properties.isEmpty() + } + + val expectedTypes = ExpectedTypesExtractor(graph).extractTypes(entrypoint) + val actualTypes = MethodTypesFacts.from(result, entrypoint) + + assertTrue(expectedTypes.matchesWithTypeFacts(actualTypes, ignoreReturnType = true, project)) + } + + private fun extractAllAstMethods(astScene: EtsScene, abcScene: EtsScene): List { + val abcMethods = abcScene.projectAndSdkClasses + .flatMapTo(hashSetOf()) { + it.methods.map { method -> method.name } + } + return astScene.projectAndSdkClasses + .flatMap { it.methods } + .filter { it.name in abcMethods } + } + + private fun saveTypeInferenceComparison( + astMethods: List, + abcMethods: List, + graphAst: EtsApplicationGraph, + graphAbc: EtsApplicationGraph, + result: TypeInferenceResult, + classMatcherStatistics: ClassMatcherStatistics, + abcScene: EtsScene, + ) { + astMethods.forEach { m -> + val expectedTypes = ExpectedTypesExtractor(graphAst).extractTypes(m) + val abcMethod = abcMethods.singleOrNull { + it.name == m.name && it.enclosingClass.name == m.enclosingClass.name + } ?: return@forEach + val actualTypes = MethodTypesFacts.from(result, m) + classMatcherStatistics.calculateStats( + actualTypes, + expectedTypes, + abcScene, + m, + abcMethod, + graphAst, + graphAbc + ) + } + } + + private inline fun checkAnObjectTypeOfSingleArgument( + types: Map, + predicate: (T) -> Boolean, + ) { + val type = types.filterKeys { it is AccessPathBase.Arg }.values.single() as T + assertTrue(predicate(type)) + } +} diff --git a/usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/utils/ExpectedTypesExtractor.kt b/usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/utils/ExpectedTypesExtractor.kt new file mode 100644 index 0000000000..82aa4fb2dc --- /dev/null +++ b/usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/utils/ExpectedTypesExtractor.kt @@ -0,0 +1,377 @@ +package org.usvm.dataflow.ts.test.utils + +import org.jacodb.ets.base.DEFAULT_ARK_CLASS_NAME +import org.jacodb.ets.base.EtsAnyType +import org.jacodb.ets.base.EtsArrayType +import org.jacodb.ets.base.EtsBooleanType +import org.jacodb.ets.base.EtsClassType +import org.jacodb.ets.base.EtsFunctionType +import org.jacodb.ets.base.EtsNullType +import org.jacodb.ets.base.EtsNumberType +import org.jacodb.ets.base.EtsStringType +import org.jacodb.ets.base.EtsType +import org.jacodb.ets.base.EtsUnclearRefType +import org.jacodb.ets.base.EtsUndefinedType +import org.jacodb.ets.base.EtsUnknownType +import org.jacodb.ets.model.EtsMethod +import org.jacodb.ets.model.EtsScene +import org.usvm.dataflow.ts.graph.EtsApplicationGraph +import org.usvm.dataflow.ts.infer.AccessPathBase +import org.usvm.dataflow.ts.infer.EtsTypeFact +import org.usvm.dataflow.ts.util.MethodTypesFacts +import java.io.File + +private val logger = mu.KotlinLogging.logger {} + +class ExpectedTypesExtractor(private val graph: EtsApplicationGraph) { + fun extractTypes(method: EtsMethod): MethodTypes { + val returnType = method.returnType + val argumentsTypes = method.parameters.map { it.type } + val thisType = getEtsClassType(method, graph) + + return MethodTypes(thisType, argumentsTypes, returnType) + } + +} + +private fun getEtsClassType(method: EtsMethod, graph: EtsApplicationGraph) = + if (method.enclosingClass.name == DEFAULT_ARK_CLASS_NAME || method.enclosingClass.name.isBlank()) { + null + } else { + val clazz = graph.cp + .projectAndSdkClasses + // .filterNot { it.name.startsWith(ANONYMOUS_CLASS_PREFIX) } + // TODO different representation in abc and ast, replace with signatures + // .singleOrNull { it.name == method.enclosingClass.name } + // ?: error("TODO") + .firstOrNull { it.name == method.enclosingClass.name } + ?: error("") /*return MethodTypes(null, argumentsTypes, returnType)*/ + + EtsClassType(clazz.signature) + } + +class ClassMatcherStatistics { + private val overallTypes: Long + get() = overallThisTypes + overallArgsTypes + overallReturnTypes + private val matched: Long + get() = exactlyMatchedThisTypes + exactlyMatchedArgsTypes + exactlyMatchedReturnTypes + private val someFactsFound: Long + get() = someFactsAboutThisTypes + someFactsAboutArgsTypes + someFactsAboutReturnTypes + returnIsAnyType + argIsAnyType + + private var failedMethods: MutableList = mutableListOf() + + private var overallThisTypes: Long = 0L + private var exactlyMatchedThisTypes: Long = 0L + private var someFactsAboutThisTypes: Long = 0L + + private var overallArgsTypes: Long = 0L + private var exactlyMatchedArgsTypes: Long = 0L + private var someFactsAboutArgsTypes: Long = 0L + private var argIsAnyType: Long = 0L + + private var overallReturnTypes: Long = 0L + private var exactlyMatchedReturnTypes: Long = 0L + private var someFactsAboutReturnTypes: Long = 0L + private var returnIsAnyType: Long = 0L + + private val methodToTypes: MutableMap>> = + hashMapOf() + private val methodToReturnTypes: MutableMap> = hashMapOf() + + private fun EtsMethod.saveComparisonInfo( + position: AccessPathBase, + type: EtsType, + fact: EtsTypeFact?, + ) { + val methodTypes = methodToTypes.getOrPut(this) { hashMapOf() } + check(position !in methodTypes) + methodTypes[position] = type to fact + } + + fun calculateStats( + methodResults: MethodTypesFacts, + types: MethodTypes?, + scene: EtsScene, + astMethod: EtsMethod, + abcMethod: EtsMethod, + astGraph: EtsApplicationGraph, + abcGraph: EtsApplicationGraph, + ) { + methodResults.apply { + if (combinedThisFact == null && argumentsFacts.all { it == null } && returnFact == null && localFacts.isEmpty()) { + saveAbsentResult(astMethod) + return + } + } + + compareTypesWithExpected( + methodResults, + requireNotNull(types), + scene, + astMethod + ) + + } + + private fun compareTypesWithExpected( + facts: MethodTypesFacts, + types: MethodTypes, + scene: EtsScene, + method: EtsMethod, + ) { + // 'this' type + types.thisType?.let { + overallThisTypes++ + + method.saveComparisonInfo(AccessPathBase.This, it, facts.combinedThisFact) + + val thisFact = facts.combinedThisFact ?: return@let + if (thisFact.matchesWith(it, strictMode = false)) { + exactlyMatchedThisTypes++ + } else { + someFactsAboutThisTypes++ + } + } + + // args + types.argumentsTypes.forEachIndexed { index, type -> + overallArgsTypes++ + + val fact = facts.argumentsFacts.getOrNull(index) + + + method.saveComparisonInfo(AccessPathBase.Arg(index), type, fact) + + if (fact is EtsTypeFact.AnyEtsTypeFact) { + argIsAnyType++ + return@forEachIndexed + } + + if (fact == null) return@forEachIndexed + + if (fact.matchesWith(type, strictMode = false)) { + exactlyMatchedArgsTypes++ + } else { + someFactsAboutArgsTypes++ + } + } + + // return type + val inferredReturnType = facts.returnFact ?: EtsTypeFact.AnyEtsTypeFact + + overallReturnTypes++ + methodToReturnTypes[method] = method.returnType to inferredReturnType + + if (inferredReturnType is EtsTypeFact.AnyEtsTypeFact) { + returnIsAnyType++ + return + } + + if (inferredReturnType.matchesWith(types.returnType, strictMode = false)) { + exactlyMatchedReturnTypes++ + } else if (inferredReturnType.partialMatchedBy(types.returnType)) { + someFactsAboutReturnTypes++ + } + } + + fun saveAbsentResult(method: EtsMethod) { + failedMethods += method + } + + override fun toString(): String = """ + Total types number: $overallTypes + Exactly matched: $matched + Partially matched: $someFactsFound + Not found: ${overallTypes - matched - someFactsFound} + + Specifically: + + This types total: $overallThisTypes + Exactly matched this types: $exactlyMatchedThisTypes + Partially matched this types: $someFactsAboutThisTypes + Not found: ${overallThisTypes - exactlyMatchedThisTypes - someFactsAboutThisTypes} + + Args types total: $overallArgsTypes + Exactly matched args types: $exactlyMatchedArgsTypes + Partially matched args types: $someFactsAboutArgsTypes + Any type as arg: $argIsAnyType + Not found: ${overallArgsTypes - exactlyMatchedArgsTypes - someFactsAboutArgsTypes - argIsAnyType} + + Return types total: $overallReturnTypes + Exactly matched return types: $exactlyMatchedReturnTypes + Partially matched return types: $someFactsAboutReturnTypes + Any type is returned: $returnIsAnyType + Not found: ${overallReturnTypes - exactlyMatchedReturnTypes - someFactsAboutReturnTypes - returnIsAnyType} + + Didn't find any types for ${failedMethods.size} methods + """.trimIndent() + + fun dumpStatistics(outputFilePath: String? = null) { + val data = buildString { + appendLine(this@ClassMatcherStatistics.toString()) + appendLine() + + appendLine("Specifically: ${"=".repeat(42)}") + + val comparator = + Comparator>> { fst, snd -> + when (fst.key) { + is AccessPathBase.This -> when { + snd.key is AccessPathBase.This -> 0 + else -> -1 + } + + is AccessPathBase.Arg -> when (snd.key) { + is AccessPathBase.This -> 1 + is AccessPathBase.Arg -> { + (fst.key as AccessPathBase.Arg).index.compareTo((snd.key as AccessPathBase.Arg).index) + } + + else -> -1 + } + + else -> when (snd.key) { + is AccessPathBase.This, is AccessPathBase.Arg -> 1 + else -> 0 + } + } + } + + methodToTypes.forEach { (method, types) -> + appendLine("${method.signature}:") + + types + .entries + .sortedWith(comparator) + .forEach { (path, typeInfo) -> + appendLine("${path}: ${typeInfo.first} -> ${typeInfo.second}") + } + appendLine() + } + + appendLine() + appendLine("=".repeat(42)) + appendLine("Failed methods:") + failedMethods.forEach { + appendLine(it) + } + } + + if (outputFilePath == null) { + println(data) + return + } + + val file = File(outputFilePath) + println("File with statistics is located: ${file.absolutePath}") + file.writeText(data) + } +} + +data class MethodTypes( + val thisType: EtsType?, + val argumentsTypes: List, + val returnType: EtsType, +) { + fun matchesWithTypeFacts(other: MethodTypesFacts, ignoreReturnType: Boolean, scene: EtsScene): Boolean { + if (thisType == null && other.combinedThisFact != null) return false + + if (thisType != null && other.combinedThisFact != null) { + if (!other.combinedThisFact.matchesWith(thisType, strictMode = false)) return false + } + + for ((i, fact) in other.argumentsFacts.withIndex()) { + if (!fact.matchesWith(argumentsTypes[i], strictMode = false)) return false + } + + if (ignoreReturnType) return true + + return other.returnFact.matchesWith(returnType, strictMode = false) + } +} + +private fun EtsTypeFact?.matchesWith(type: EtsType, strictMode: Boolean): Boolean { + val result = when (this) { + null, EtsTypeFact.AnyEtsTypeFact -> { + // TODO any other combination? + type is EtsAnyType || (!strictMode && type is EtsUnknownType) + } + + is EtsTypeFact.ObjectEtsTypeFact -> { + // TODO it should be replaced with signatures + val typeName = this.cls?.typeName + + if ((type is EtsUnknownType || type is EtsAnyType) && !strictMode) { + this.cls != null + } else { + (type is EtsClassType || type is EtsUnclearRefType) && type.typeName == typeName + } + } + + is EtsTypeFact.ArrayEtsTypeFact -> when (type) { + is EtsArrayType -> this.elementType.matchesWith(type.elementType, strictMode) + + is EtsUnclearRefType -> { + val elementType = this.elementType as? EtsTypeFact.ObjectEtsTypeFact + elementType?.cls?.typeName == type.typeName + } + + else -> false + } + + EtsTypeFact.BooleanEtsTypeFact -> { + type is EtsBooleanType + || (type is EtsUnknownType && !strictMode) + || (type as? EtsClassType)?.typeName == "Boolean" + || (type as? EtsUnclearRefType)?.typeName == "Boolean" + } + + EtsTypeFact.FunctionEtsTypeFact -> type is EtsFunctionType || (type is EtsUnknownType && !strictMode) + EtsTypeFact.NullEtsTypeFact -> type is EtsNullType || (type is EtsUnknownType && !strictMode) + EtsTypeFact.NumberEtsTypeFact -> { + type is EtsNumberType + || (type is EtsUnknownType && !strictMode) + || (type as? EtsClassType)?.typeName == "Number" + || (type as? EtsUnclearRefType)?.typeName == "Number" + } + + EtsTypeFact.StringEtsTypeFact -> { + type is EtsStringType + || (type is EtsUnknownType && !strictMode) + || (type as? EtsClassType)?.typeName == "String" + || (type as? EtsUnclearRefType)?.typeName == "String" + } + + EtsTypeFact.UndefinedEtsTypeFact -> type is EtsUndefinedType + EtsTypeFact.UnknownEtsTypeFact -> type is EtsUnknownType + is EtsTypeFact.GuardedTypeFact -> TODO() + is EtsTypeFact.IntersectionEtsTypeFact -> { + // TODO intersections checks are not supported yet + false + } + + is EtsTypeFact.UnionEtsTypeFact -> if (strictMode) { + types.all { it.matchesWith(type, strictMode) } + } else { + types.any { it.matchesWith(type, strictMode) } + } + } + + if (!result) { + logger.warn { + """ + Fact: $this + Type: $type + + """.trimIndent() + } + } + + return result +} + +private fun EtsTypeFact.partialMatchedBy(type: EtsType): Boolean { + if (type is EtsUnknownType) return true + logger.warn { "Not implemented partial match for fact $this and type $type" } + return false +} diff --git a/usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/utils/TaintConfig.kt b/usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/utils/TaintConfig.kt new file mode 100644 index 0000000000..8402634d0f --- /dev/null +++ b/usvm-dataflow-ts/src/test/kotlin/org/usvm/dataflow/ts/test/utils/TaintConfig.kt @@ -0,0 +1,108 @@ +/* + * 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.test.utils + +import kotlinx.serialization.json.Json +import kotlinx.serialization.modules.SerializersModule +import org.jacodb.api.common.CommonMethod +import org.jacodb.taint.configuration.NameExactMatcher +import org.jacodb.taint.configuration.NamePatternMatcher +import org.jacodb.taint.configuration.SerializedTaintCleaner +import org.jacodb.taint.configuration.SerializedTaintConfigurationItem +import org.jacodb.taint.configuration.SerializedTaintEntryPointSource +import org.jacodb.taint.configuration.SerializedTaintMethodSink +import org.jacodb.taint.configuration.SerializedTaintMethodSource +import org.jacodb.taint.configuration.SerializedTaintPassThrough +import org.jacodb.taint.configuration.TaintCleaner +import org.jacodb.taint.configuration.TaintConfigurationItem +import org.jacodb.taint.configuration.TaintEntryPointSource +import org.jacodb.taint.configuration.TaintMethodSink +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 = "_" + serializersModule = SerializersModule { + include(conditionModule) + include(actionModule) + } +} + +fun loadRules(configFileName: String): List { + val configJson = getResourceStream("/$configFileName").bufferedReader().readText() + val rules: List = json.decodeFromString(configJson) + // println("Loaded ${rules.size} rules from '$configFileName'") + // for (rule in rules) { + // println(rule) + // } + return rules +} + +fun getConfigForMethod( + method: CommonMethod, + rules: List, +): List? { + val res = buildList { + for (item in rules) { + val matcher = item.methodInfo.functionName + if (matcher is NameExactMatcher) { + if (method.name == matcher.name) add(item.toItem(method)) + } else if (matcher is NamePatternMatcher) { + if (method.name.matches(matcher.pattern.toRegex())) add(item.toItem(method)) + } + } + } + return res.ifEmpty { null } +} + +fun SerializedTaintConfigurationItem.toItem(method: CommonMethod): TaintConfigurationItem { + return when (this) { + is SerializedTaintEntryPointSource -> TaintEntryPointSource( + method = method, + condition = condition, + actionsAfter = actionsAfter + ) + + is SerializedTaintMethodSource -> TaintMethodSource( + method = method, + condition = condition, + actionsAfter = actionsAfter + ) + + is SerializedTaintMethodSink -> TaintMethodSink( + method = method, + ruleNote = ruleNote, + cwe = cwe, + condition = condition + ) + + is SerializedTaintPassThrough -> TaintPassThrough( + method = method, + condition = condition, + actionsAfter = actionsAfter + ) + + is SerializedTaintCleaner -> TaintCleaner( + method = method, + condition = condition, + actionsAfter = actionsAfter + ) + } +} diff --git a/usvm-dataflow-ts/src/test/resources/logback.xml b/usvm-dataflow-ts/src/test/resources/logback.xml new file mode 100644 index 0000000000..30f8844725 --- /dev/null +++ b/usvm-dataflow-ts/src/test/resources/logback.xml @@ -0,0 +1,19 @@ + + + + %highlight([%level]) %replace(%c{0}){'(\$Companion)?\$logger\$1',''} - %msg%n + + + + + logs/app.log + + [%level] %replace(%c{0}){'(\$Companion)?\$logger\$1',''} - %msg%n + + + + + + + + diff --git a/usvm-dataflow-ts/src/test/resources/ts/call.ts b/usvm-dataflow-ts/src/test/resources/ts/call.ts new file mode 100644 index 0000000000..507c49e9df --- /dev/null +++ b/usvm-dataflow-ts/src/test/resources/ts/call.ts @@ -0,0 +1,13 @@ +function entrypoint() { + f(42); + let x= 7 + g(x); +} + +function f(x: any) { + console.log(x); +} + +function g(x: any) { + console.log(x); +} diff --git a/usvm-dataflow-ts/src/test/resources/ts/cast.ts b/usvm-dataflow-ts/src/test/resources/ts/cast.ts new file mode 100644 index 0000000000..53a869a2fd --- /dev/null +++ b/usvm-dataflow-ts/src/test/resources/ts/cast.ts @@ -0,0 +1,12 @@ +declare function getData(): any; + +interface Data {} + +function entrypoint() { + let x = getData() as Data; + infer(x); +} + +function infer(arg: any) { + console.log(arg); +} diff --git a/usvm-dataflow-ts/src/test/resources/ts/data.ts b/usvm-dataflow-ts/src/test/resources/ts/data.ts new file mode 100644 index 0000000000..3bbce38da3 --- /dev/null +++ b/usvm-dataflow-ts/src/test/resources/ts/data.ts @@ -0,0 +1,17 @@ +class Data { + a: number; + b: number; + constructor(a: number, b: number) { + this.a = a; + this.b = b; + } +} + +function entrypoint() { + let data = new Data(7, 42); + process(data); +} + +function process(data: any) { + console.log(data); +} diff --git a/usvm-dataflow-ts/src/test/resources/ts/microphone.ts b/usvm-dataflow-ts/src/test/resources/ts/microphone.ts new file mode 100644 index 0000000000..e436b5f498 --- /dev/null +++ b/usvm-dataflow-ts/src/test/resources/ts/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/usvm-dataflow-ts/src/test/resources/ts/microphone_ctor.ts b/usvm-dataflow-ts/src/test/resources/ts/microphone_ctor.ts new file mode 100644 index 0000000000..0a5692a4c8 --- /dev/null +++ b/usvm-dataflow-ts/src/test/resources/ts/microphone_ctor.ts @@ -0,0 +1,33 @@ +interface Microphone { + uuid: string +} + +class VirtualMicro implements Microphone { + uuid: string; + + constructor() { + this.uuid = "virtual_micro_v3" + } +} + +interface Devices { + microphone: Microphone +} + +class VirtualDevices implements Devices { + microphone: Microphone; + + constructor() { + this.microphone = new VirtualMicro(); + } +} + +function getMicrophoneUuid(devices: Devices): string { + return devices.microphone.uuid; +} + +function entrypoint() { + let devices = new VirtualDevices() + let uuid = getMicrophoneUuid(devices) + console.log(uuid) +} diff --git a/usvm-dataflow-ts/src/test/resources/ts/nested_init.ts b/usvm-dataflow-ts/src/test/resources/ts/nested_init.ts new file mode 100644 index 0000000000..beda346630 --- /dev/null +++ b/usvm-dataflow-ts/src/test/resources/ts/nested_init.ts @@ -0,0 +1,23 @@ +class Foo { +} + +class Cat { + foo: Foo = new Foo(); +} + +// class Cat { +// foo: Foo; +// +// constructor() { +// this.foo = new Foo(); +// } +// } + +function entrypoint() { + let cat = new Cat(); + infer(cat); +} + +function infer(x: any) { + console.log(x); +} diff --git a/usvm-dataflow-ts/src/test/resources/ts/resolver_test.ts b/usvm-dataflow-ts/src/test/resources/ts/resolver_test.ts new file mode 100644 index 0000000000..338e41b26a --- /dev/null +++ b/usvm-dataflow-ts/src/test/resources/ts/resolver_test.ts @@ -0,0 +1,50 @@ +class FieldContainerToInfer { + defaultA: number = 13; + uniqueA: number = 12; + + notUniqueFunction(): number { + return 21 + } +} + +class MethodsContainerToInfer { + uniqueFunction() { + console.log("Hi") + } + + notUniqueFunction(): number { + return 42 + } +} + +class A { + defaultA: number = 23 +} + +function useNonUniqueField(x : A) { + return x.defaultA +} + +function useUniqueFields(x : FieldContainerToInfer) { + return x.uniqueA +} + +function useBothA(x : FieldContainerToInfer): number { + return x.uniqueA + x.defaultA +} + +function useUniqueMethods(x : MethodsContainerToInfer) { + x.uniqueFunction() +} + +function useNotUniqueMethod(x : MethodsContainerToInfer) { + x.notUniqueFunction() +} + +function useFunctionAndField(x : FieldContainerToInfer): number { + x.notUniqueFunction() + return x.defaultA +} + + +// TODO examples with another scope of view and anonymous properties \ No newline at end of file diff --git a/usvm-dataflow-ts/src/test/resources/ts/taint.ts b/usvm-dataflow-ts/src/test/resources/ts/taint.ts new file mode 100644 index 0000000000..ea6ee035b7 --- /dev/null +++ b/usvm-dataflow-ts/src/test/resources/ts/taint.ts @@ -0,0 +1,32 @@ +function source(): number | null { + return null; +} + +function pass(data: number | null): number | null { + return data; +} + +function validate(data: number | null): number { + if (data == null) { + return 0; + } + return data; +} + +function sink(data: number | null) { + if (data == null) { + throw new Error("Error!"); + } +} + +function bad() { + let data = source(); + data = pass(data); + sink(data); +} + +function good() { + let data = source(); + data = validate(data); + sink(data); +} diff --git a/usvm-dataflow-ts/src/test/resources/ts/testcases.ts b/usvm-dataflow-ts/src/test/resources/ts/testcases.ts new file mode 100644 index 0000000000..1731296432 --- /dev/null +++ b/usvm-dataflow-ts/src/test/resources/ts/testcases.ts @@ -0,0 +1,994 @@ +// Case `x := y` +class CaseAssignLocalToLocal { + entrypoint() { + let x = 52; // x: number + let y = x; // y: number + this.infer(y); + } + + infer(a: any) { + const EXPECTED_ARG_0 = "number"; + } +} + +// ---------------------------------------- + +// Case `x := y.f` +class CaseAssignFieldToLocal1 { + entrypoint(y: any) { + let x = y.f; // y: { f: any } + this.infer(y); + } + + infer(a: any) { + const EXPECTED_ARG_0 = "Object { f: any }"; + } +} + +// ---------------------------------------- + +// Case `x := y.f` +class CaseAssignFieldToLocal2 { + entrypoint() { + let y = {f: 42}; // y: { f: number } + let x = y.f; // x: number + this.infer(x); + } + + infer(a: any) { + const EXPECTED_ARG_0 = "number"; + } +} + +// ---------------------------------------- + +// Case `x := a.f` +class CaseAssignFieldToLocal3 { + entrypoint(y: any) { + this.infer(y); + } + + infer(a: any) { + let x = a.f; // a: { f: any } + const EXPECTED_ARG_0 = "Object { f: any }"; + } +} + +// ---------------------------------------- + +// Case `x := x.f` +class CaseAssignFieldToSelf { + entrypoint(a: any) { + let x = { f: a }; + x = x.f; // x: any + this.infer(x); + } + + infer(a: any) { + const EXPECTED_ARG_0 = "Object { f: any }" + } +} + +// Case `x.f := x` +class CaseAssignSelfToField { + entrypoint(a: any) { + let x = { f: a }; // x: { f: any } + x.f = x; // x: { f: any } + this.infer(x); + } + + infer(a: any) { + const EXPECTED_ARG_0 = "Object { f: any }" + } +} + +// ---------------------------------------- + +// Case `x.f := y` +class CaseAssignLocalNumberToField { + entrypoint(x: any) { + let y = 100; // y: number + x.f = y; // x: { f: number } + this.infer(x); + } + + infer(a: any) { + const EXPECTED_ARG_0 = "Object { f: number }"; + } +} + +// ---------------------------------------- + +// Case `x.f := y` +class CaseAssignLocalObjectToField { + entrypoint(x: any) { + let y = { t: 32 }; // y: { t: number } + x.f = y; // x: { f: { t: number } } + this.infer(x); + } + + infer(a: any) { + const EXPECTED_ARG_0 = "Object { f: Object { t: number } }"; + } +} + +// ---------------------------------------- + +// Case `x.f.f := const` +class CaseNestedDuplicateFields { + entrypoint(x: any) { + x.f.f = 2; // x: { f: { f: number } } + this.infer(x); + } + + infer(a: any) { + const EXPECTED_ARG_0 = "Object { f: Object { f: number } }"; + } +} + +// ---------------------------------------- + +// Case `y := [...]` +class CaseAssignArrayToLocal { + entrypoint() { + let y = [1, 2, 3]; // y: Array + this.infer(y); + } + + infer(a: any) { + const EXPECTED_ARG_0 = "Array"; + } +} + +// ---------------------------------------- + +// Case `x := y[i]` +class CaseAssignArrayElementToLocal1 { + entrypoint() { + let y = [33]; // y: Array + let x = y[0]; // y: Array + this.infer(y); + } + + infer(a: any) { + const EXPECTED_ARG_0 = "Array"; + } +} + +// ---------------------------------------- + +// Case `x := y[i]` +class CaseAssignArrayElementToLocal2 { + entrypoint() { + let y = [22]; // y: Array + let x = y[0]; // x: number + this.infer(x); + } + + infer(a: any) { + const EXPECTED_ARG_0 = "number"; + } +} + +// ---------------------------------------- + +// Case `x := y[i]` +class CaseAssignArgumentArrayElementToLocal1 { + entrypoint(y: number[]) { + let x = y[0]; // y: Array + this.infer(y); + } + + infer(a: any) { + const EXPECTED_ARG_0 = "Array"; + } +} + +// ---------------------------------------- + +// Case `x := y[i]` +class CaseAssignArgumentArrayElementToLocal2 { + entrypoint(y: number[]) { + let x = y[0]; // x: any + this.infer(x); + } + + infer(a: any) { + const EXPECTED_ARG_0 = "any"; + } +} + +// ---------------------------------------- + +// Case `x[i] := y` +class CaseAssignLocalToArrayElementNumber { + entrypoint(x: any[]) { + let y = 100; // y: number + x[0] = y; // x: Array + this.infer(x); + } + + infer(a: any) { + const EXPECTED_ARG_0 = "Array"; + } +} + +// ---------------------------------------- + +interface ICustom { + a: number; + b: string; +} + +// Case `x := y as T` +class CaseCastToInterface { + entrypoint(y: any) { + let x = y as ICustom; // x: ICustom + this.infer(x); + } + + infer(a: any) { + const EXPECTED_ARG_0 = "ICustom { }"; + } +} + +// ---------------------------------------- + +// Case `x := y as T[]` +class CaseCastToArrayInterface { + entrypoint(y: any) { + let x = y as ICustom[]; // x: Array + this.infer(x); + } + + infer(a: any) { + const EXPECTED_ARG_0 = "Array"; + } +} + +// // ---------------------------------------- +// +// // Case `x := y + z` +// class CaseAddNumbers { +// entrypoint() { +// let y = 5; // y: number +// let z = 10; // z: number +// let x = y + z; // x: number +// this.infer(x); +// } +// +// infer(a: any) { +// const EXPECTED_ARG_0 = "number"; +// } +// } +// +// // ---------------------------------------- +// +// // Case `x := y && z` +// class CaseLogicalAndBooleans { +// entrypoint() { +// let y = true; // y: boolean +// let z = false; // z: boolean +// let x = y && z; // x: boolean +// this.infer(x); +// } +// +// infer(a: any) { +// const EXPECTED_ARG_0 = "boolean"; +// } +// } +// +// // ---------------------------------------- +// +// // Case `x := y || z` +// class CaseLogicalOrBooleanAndString { +// entrypoint() { +// let y = false; // y: boolean +// let z = "default"; // z: string +// let x = y || z; // x: string +// this.infer(x); +// } +// +// infer(a: any) { +// const EXPECTED_ARG_0 = "boolean | string"; +// } +// } +// +// // ---------------------------------------- +// +// // Case `x := y + z` +// class CaseAddStrings { +// entrypoint() { +// let y = "Hello, "; // y: string +// let z = "World!"; // z: string +// let x = y + z; // x: string +// this.infer(x); +// } +// +// infer(a: any) { +// const EXPECTED_ARG_0 = "string"; +// } +// } +// +// // ---------------------------------------- +// +// // Case `x := number + string` +// class CaseAddNumberToString { +// entrypoint() { +// let y = 12; // y: number +// let z = " is the answer"; // z: string +// let x = y + z; // x: string +// this.infer(x); +// } +// +// infer(a: any) { +// const EXPECTED_ARG_0 = "string"; +// } +// } +// +// // ---------------------------------------- +// +// // Case `x := string + number` +// class CaseAddStringToNumber { +// entrypoint() { +// let y = "The answer is "; // y: string +// let z = 13; // z: number +// let x = y + z; // x: string +// this.infer(x); +// } +// +// infer(a: any) { +// const EXPECTED_ARG_0 = "string"; +// } +// } +// +// // ---------------------------------------- +// +// // Case `x := string - number` +// class CaseSubtractNumberFromString { +// entrypoint() { +// let y = "73"; // y: string +// let z = 10; // z: number +// let x = y - z; // x: number (63) +// this.infer(x); +// } +// +// infer(a: any) { +// const EXPECTED_ARG_0 = "number"; +// } +// } +// +// // ---------------------------------------- +// +// // Case `x := number - string` +// class CaseSubtractStringFromNumber { +// entrypoint() { +// let y = 96; // y: number +// let z = "51"; // z: string +// let x = y - z; // x: number (45) +// this.infer(x); +// } +// +// infer(a: any) { +// const EXPECTED_ARG_0 = "number"; +// } +// } +// +// // ---------------------------------------- +// +// // Case `x := string * number` +// class CaseMultiplyStringByNumber { +// entrypoint() { +// let y = "100"; // y: string +// let z = 30; // z: number +// let x = y * z; // x: number (3000) +// this.infer(x); +// } +// +// infer(a: any) { +// const EXPECTED_ARG_0 = "number"; +// } +// } +// +// // ---------------------------------------- +// +// // Case `x := number * string` +// class CaseMultiplyNumberByString { +// entrypoint() { +// let y = 40; // y: number +// let z = "500"; // z: string +// let x = y * z; // x: number (20000) +// this.infer(x); +// } +// +// infer(a: any) { +// const EXPECTED_ARG_0 = "number"; +// } +// } +// +// // ---------------------------------------- +// +// // Case `x := boolean + number` +// class CaseAddBooleanToNumber { +// entrypoint() { +// let y = true; // y: boolean +// let z = 1; // z: number +// let x = y + z; // x: number (2, as true is coerced to 1) +// this.infer(x); +// } +// +// infer(a: any) { +// const EXPECTED_ARG_0 = "number"; +// } +// } +// +// // ---------------------------------------- +// +// // Case `x := number + boolean` +// class CaseAddNumberToBoolean { +// entrypoint() { +// let y = 1; // y: number +// let z = false; // z: boolean +// let x = y + z; // x: number (1, as false is coerced to 0) +// this.infer(x); +// } +// +// infer(a: any) { +// const EXPECTED_ARG_0 = "number"; +// } +// } +// +// // ---------------------------------------- +// +// // Case `x := null + number` +// class CaseAddNullToNumber { +// entrypoint() { +// let y = null; // y: null +// let z = 105; // z: number +// let x = y + z; // x: number (10, as null is coerced to 0) +// this.infer(x); +// } +// +// infer(a: any) { +// const EXPECTED_ARG_0 = "number"; +// } +// } +// +// // ---------------------------------------- +// +// // Case `x := number + null` +// class CaseAddNumberToNull { +// entrypoint() { +// let y = 115; // y: number +// let z = null; // z: null +// let x = y + z; // x: number (10, as null is coerced to 0) +// this.infer(x); +// } +// +// infer(a: any) { +// const EXPECTED_ARG_0 = "number"; +// } +// } +// +// // ---------------------------------------- +// +// // Case `x := undefined + number` +// class CaseAddUndefinedToNumber { +// entrypoint() { +// let y = undefined; // y: undefined +// let z = 125; // z: number +// let x = y + z; // x: number (NaN) +// this.infer(x); +// } +// +// infer(a: any) { +// const EXPECTED_ARG_0 = "number"; +// } +// } +// +// // ---------------------------------------- +// +// // Case `x := number + undefined` +// class CaseAddNumberToUndefined { +// entrypoint() { +// let y = 135; // y: number +// let z = undefined; // z: undefined +// let x = y + z; // x: number (NaN) +// this.infer(x); +// } +// +// infer(a: any) { +// const EXPECTED_ARG_0 = "number"; +// } +// } +// +// // ---------------------------------------- +// +// // Case `x := string / number` +// class CaseDivideStringByNumber { +// entrypoint() { +// let y = "185"; // y: string +// let z = 5; // z: number +// let x = y / z; // x: number (37) +// this.infer(x); +// } +// +// infer(a: any) { +// const EXPECTED_ARG_0 = "number"; +// } +// } +// +// // ---------------------------------------- +// +// // Case `x := number / string` +// class CaseDivideNumberByString { +// entrypoint() { +// let y = 195; // y: number +// let z = "5"; // z: string +// let x = y / z; // x: number (39) +// this.infer(x); +// } +// +// infer(a: any) { +// const EXPECTED_ARG_0 = "number"; +// } +// } +// +// ---------------------------------------- + +// Case `return x` +class CaseReturnNumber { + entrypoint() { + this.infer(); + } + + infer(): any { + const EXPECTED_RETURN = "number"; + let x = 93; // x: number + return x; + } +} + +// ---------------------------------------- + + // Case `return arg` + class CaseReturnArgumentNumber { + entrypoint() { + let x = 94; // x: number + this.infer(x); + } + + infer(a: any): any { + const EXPECTED_RETURN = "number"; + return a; + } + } + +// ---------------------------------------- + +// Case `return obj` +class CaseReturnObject { + entrypoint() { + this.infer(); + } + + infer(): any { + const EXPECTED_RETURN = "Object { f: number }"; + let x = { f: 95 }; // x: Object { f: number } + return x; + } +} + +// ---------------------------------------- + +// Case `return obj` +class CaseReturnArgumentObject { + entrypoint() { + let x = { f: 96 }; // x: Object { f: number } + this.infer(x); + } + + infer(a: any): any { + const EXPECTED_RETURN = "Object { f: number }"; + return a; + } +} + +// ---------------------------------------- + +// // Case `x.f[0].g := y` +// class CaseAssignToNestedObjectField { +// entrypoint(x: any) { +// let y = 134; // y: number +// x.f[0].g = y; // x: { f: Array<{ g: number }> } +// this.infer(x); +// } +// +// infer(a: any) { +// const EXPECTED_ARG_0 = "Object { f: Array }" +// } +// } +// +// // ---------------------------------------- +// +// // Case `x.f.g.h := y` +// class CaseAssignDeeplyNestedField { +// entrypoint(x: any) { +// let y = "abc"; // y: string +// x.f.g.h = y; // x: { f: { g: { h: string } } } +// this.infer(x); +// } +// +// infer(a: any) { +// const EXPECTED_ARG_0 = "Object { f: Object { g: Object { h: string } } }" +// } +// } +// +// // ---------------------------------------- +// +// // Case `x.f[i].g.h := y` +// class CaseAssignToArrayObjectField { +// entrypoint(x: any) { +// let y = false; // y: boolean +// x.f[2].g.h = y; // x: { f: Array<{ g: { h: boolean } }> } +// this.infer(x); +// } +// +// infer(a: any) { +// const EXPECTED_ARG_0 = "Object { f: Array }" +// } +// } +// +// // ---------------------------------------- +// +// // Case `x[i].f.g := y` +// class CaseAssignArrayFieldToNestedObject { +// entrypoint(x: any) { +// let y = 219; // y: number +// x[0].f.g = y; // x: Array<{ f: { g: number } }> +// this.infer(x); +// } +// +// infer(a: any) { +// const EXPECTED_ARG_0 = "Array<{ f: Object { g: number } }>" +// } +// } +// +// // ---------------------------------------- +// +// // Case `x.f[i][j] := y` +// class CaseAssignToMultiDimensionalArray { +// entrypoint(x: any) { +// let y = "data"; // y: string +// x.f[1][2] = y; // x: { f: Array> } +// this.infer(x); +// } +// +// infer(a: any) { +// const EXPECTED_ARG_0 = "Object { f: Array> }" +// } +// } +// +// // ---------------------------------------- +// +// // Case `x.f[0].g[1].h := y` +// class CaseAssignToComplexNestedArrayField { +// entrypoint(x: any) { +// let y = true; // y: boolean +// x.f[0].g[1].h = y; // x: { f: Array<{ g: Array<{ h: boolean }> }> } +// this.infer(x); +// } +// +// infer(a: any) { +// const EXPECTED_ARG_0 = "Object { f: Array }> }" +// } +// } +// +// // ---------------------------------------- +// +// // Case `x.f.g.h[i] := y` +// class CaseAssignToArrayInNestedObject { +// entrypoint(x: any) { +// let y = 3.14; // y: number +// x.f.g.h[2] = y; // x: { f: { g: { h: Array } } } +// this.infer(x); +// } +// +// infer(a: any) { +// const EXPECTED_ARG_0 = "Object { f: Object { g: Object { h: Array } } }" +// } +// } +// +// // ---------------------------------------- +// +// // Case `x.f[0].g.h[i] := y` +// class CaseAssignToArrayInDeeplyNestedObject { +// entrypoint(x: any) { +// let y = null; // y: null +// x.f[0].g.h[3] = y; // x: { f: Array<{ g: { h: Array } }> } +// this.infer(x); +// } +// +// infer(a: any) { +// const EXPECTED_ARG_0 = "Object { f: Array } }> }" +// } +// } +// +// // ---------------------------------------- +// +// // Case `x.f.g[i].h.j := y` +// class CaseAssignToDeeplyNestedObjectArray { +// entrypoint(x: any) { +// let y = "nested"; // y: string +// x.f.g[1].h.j = y; // x: { f: { g: Array<{ h: { j: string } }> } } +// this.infer(x); +// } +// +// infer(a: any) { +// const EXPECTED_ARG_0 = "Object { f: Object { g: Array } }" +// } +// } +// +// // ---------------------------------------- +// +// // Case `x.f.g.h[0][i] := y` +// class CaseAssignToMultiDimensionalArrayField { +// entrypoint(x: any) { +// let y = 99; // y: number +// x.f.g.h[0][1] = y; // x: { f: { g: { h: Array> } } } +// this.infer(x); +// } +// +// infer(a: any) { +// const EXPECTED_ARG_0 = "Object { f: Object { g: { h: Array> } } }" +// } +// } + +// ---------------------------------------- + +class MyType { + f: number = 15; +} + +// Case `x := new T()` +class CaseNew { + entrypoint() { + let y = new MyType(); // y: MyType + // hidden: + // y := new MyType() + // -> y.constructor() + // -> this.() + // -> this.f := 15 + // -> this: { f: number } + // -> y: { f.number } + this.infer(y); + } + + infer(a: any): any { + const EXPECTED_ARG_0 = "MyType { f: number }" + } +} + +// ---------------------------------------- + +// Case `x := number | string` +class CaseUnion { + entrypoint() { + let x: string | number = "str"; // x: string + x = 42; // x: number + this.infer(x); + } + + infer(a: any): any { + const EXPECTED_ARG_0 = "number"; + } +} + +// Case `x := number | string` +class CaseArgumentUnion { + entrypoint(x: string | number) { + x = "kek"; + x = 42; + this.infer(x); + } + + infer(a: any): any { + const EXPECTED_ARG_0 = "number"; + } +} + +// Case `x := y` +class CaseUnion2 { + entrypoint() { + let y = "str"; + let x: string | number = y; + x = 42; + this.infer(y); + } + + infer(a: any): any { + const EXPECTED_ARG_0 = "string"; + } +} + +// Case `x := y` +class CaseArgumentUnion2 { + entrypoint(y: string) { + let x: string | number = y; + x = 42; + this.infer(y); + } + + infer(a: any): any { + const EXPECTED_ARG_0 = "any"; + } +} + +// Case `x := "string" | number` +class CaseUnion3 { + entrypoint() { + let x: string | number; + if (Math.random() > 0.5) { + x = "str"; + } else { + x = 42; + } + this.infer(x); + } + + infer(a: any): any { + const EXPECTED_ARG_0 = "number | string"; + } +} + +// Case `x := "string" | number` +class CaseArgumentUnion3 { + entrypoint(x: string | number) { + if (Math.random() > 0.5) { + x = "str"; + } else { + x = 42; + } + this.infer(x); + } + + infer(a: any): any { + const EXPECTED_ARG_0 = "number | string"; + } +} + +// Case `x := "string", x := number` +class CaseUnion4 { + entrypoint() { + let x: string | number = "str"; + if (Math.random() > 0.5) { + x = 42; + } + this.infer(x); + } + + infer(a: any): any { + const EXPECTED_ARG_0 = "number | string"; + } +} + +// Case `x := "string", x := number` +// class CaseUnion5 { +// entrypoint() { +// let x: string | number; +// if (Math.random() > 0.5) { +// x = 42; +// } +// this.infer(x); +// } +// +// infer(a: any): any { +// // Currently, `number | undefined` is inferred due to the lack of DeclareStmt +// const EXPECTED_ARG_0 = "number | string"; +// } +// } + +// ---------------------------------------- + +// Case `y := x.f.g` +class CaseAliasChain1 { + entrypoint(x: any) { + let y = x.f; // x: { f: any } + y.g = 42; // x: { f: { g: number } } + this.infer(x); + } + + infer(a: any): any { + const EXPECTED_ARG_0 = "Object { f: Object { g: number } }" + } +} + +// Case `x.f.g := number` +class CaseAssignNumberToNestedField { + entrypoint(x: any) { + x.f.g = 100; // x: { f: { g: number } } + this.infer(x); + } + + infer(a: any) { + const EXPECTED_ARG_0 = "Object { f: Object { g: number } }"; + } +} + +// Case `x.f := (y: number)` +class CaseAssignLocalNumberToNestedField { + entrypoint(x: any) { + let y = 98; // y: number + x.f.g = y; // x: { f: { g: number } } + this.infer(x); + } + + infer(a: any) { + const EXPECTED_ARG_0 = "Object { f: Object { g: number } }"; + } +} + +// ---------------------------------------- + +class CaseLoop { + entrypoint() { + let x: any = {}; + let a: any = 42; + for (let i = 0; i < 10; i++) { + x.f = a; + a = x; + } + this.infer(x); + } + + infer(a: any) { + const EXPECTED_ARG_0 = "any"; + } +} + +class CaseFindAssignmentAfterLoop { + entrypoint(y: number) { + let x = []; + for (let i = 0; i < y; i++) { + x.push(i); + } + if (x.length == 0) { + x.push(42); + } + + this.infer(x) + } + + infer(a: any) { + // + } +} + +// ---------------------------------------- + +class Tree { + children: Tree[] = []; + + getChildren(): Tree[] { + return this.children; + } +} + +class CaseRecursion { + entrypoint(root: Tree) { + this.traverse([root]); + this.infer(root); + } + + traverse(xs: Tree[]) { + for (let i = 0; i < xs.length; i++) { + let child: Tree = xs[i]; + let children: Tree[] = child.getChildren(); + this.traverse(children); + } + } + + infer(a: any) { + const EXPECTED_ARG_0 = "Tree { children: Array }"; + } +} diff --git a/usvm-dataflow-ts/src/test/resources/ts/types.ts b/usvm-dataflow-ts/src/test/resources/ts/types.ts new file mode 100644 index 0000000000..4e8d90ccb3 --- /dev/null +++ b/usvm-dataflow-ts/src/test/resources/ts/types.ts @@ -0,0 +1,55 @@ +interface A { + aStr: string + bObj: B +} + +interface B { + bNum: number +} + +function conditional(x: A, cond: boolean): number { + if (cond) { + return x.aStr.length + } else { + return x.bObj.bNum + } +} + +function entrypoint1(arg: A) { + console.log(conditional(arg, false)) +} + +interface X { + a: string +} + +interface Y { + b: number +} + +function foo(x: X | Y) { + if ("a" in x) { + strBar(x.a) + } else { + numberBar(x.b) + } +} + +function baz(x: X & Y) { + strBar(x.a) + numberBar(x.b) +} + +function strBar(x: string) { + +} + +function numberBar(x: number) { + +} + +function entrypoint2(arg0: X, arg1: Y) { + foo(arg0) + foo(arg1) + baz({...arg0, ...arg1}) +} diff --git a/usvm-dataflow-ts/src/testFixtures/kotlin/org/usvm/dataflow/ts/LoadEts.kt b/usvm-dataflow-ts/src/testFixtures/kotlin/org/usvm/dataflow/ts/LoadEts.kt new file mode 100644 index 0000000000..d6195a60b1 --- /dev/null +++ b/usvm-dataflow-ts/src/testFixtures/kotlin/org/usvm/dataflow/ts/LoadEts.kt @@ -0,0 +1,117 @@ +package org.usvm.dataflow.ts + +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.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") + * ``` + */ +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")) + * ``` + */ +fun loadMultipleEtsFilesFromDirectory(dirPath: Path): Sequence { + return dirPath.walk().filter { it.extension == "json" }.map { loadEtsFile(it) } +} diff --git a/usvm-dataflow-ts/src/testFixtures/kotlin/org/usvm/dataflow/ts/Resources.kt b/usvm-dataflow-ts/src/testFixtures/kotlin/org/usvm/dataflow/ts/Resources.kt new file mode 100644 index 0000000000..6b00cd961a --- /dev/null +++ b/usvm-dataflow-ts/src/testFixtures/kotlin/org/usvm/dataflow/ts/Resources.kt @@ -0,0 +1,20 @@ +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-dataflow-ts/src/testFixtures/kotlin/org/usvm/dataflow/ts/TestFactoryDsl.kt b/usvm-dataflow-ts/src/testFixtures/kotlin/org/usvm/dataflow/ts/TestFactoryDsl.kt new file mode 100644 index 0000000000..a710f1c896 --- /dev/null +++ b/usvm-dataflow-ts/src/testFixtures/kotlin/org/usvm/dataflow/ts/TestFactoryDsl.kt @@ -0,0 +1,52 @@ +package org.usvm.dataflow.ts + +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/usvm-dataflow/build.gradle.kts b/usvm-dataflow/build.gradle.kts index a4724708fc..5de8fa92be 100644 --- a/usvm-dataflow/build.gradle.kts +++ b/usvm-dataflow/build.gradle.kts @@ -3,9 +3,10 @@ plugins { } dependencies { - implementation(Libs.jacodb_api_common) + implementation(project(":usvm-util")) + api(Libs.jacodb_api_common) implementation(Libs.jacodb_taint_configuration) - implementation(Libs.sarif4k) + api(Libs.sarif4k) } publishing { diff --git a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/config/Condition.kt b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/config/Condition.kt index ee9c915b16..95968d320b 100644 --- a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/config/Condition.kt +++ b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/config/Condition.kt @@ -35,11 +35,11 @@ import org.jacodb.taint.configuration.Or import org.jacodb.taint.configuration.PositionResolver import org.jacodb.taint.configuration.SourceFunctionMatches import org.jacodb.taint.configuration.TypeMatches -import org.usvm.dataflow.ifds.Maybe -import org.usvm.dataflow.ifds.onSome import org.usvm.dataflow.taint.Tainted import org.usvm.dataflow.util.Traits import org.usvm.dataflow.util.removeTrailingElementAccessors +import org.usvm.util.Maybe +import org.usvm.util.onSome open class BasicConditionEvaluator( val traits: Traits, diff --git a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/config/Position.kt b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/config/Position.kt index e51204caba..f8b95b9cde 100644 --- a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/config/Position.kt +++ b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/config/Position.kt @@ -30,10 +30,10 @@ import org.jacodb.taint.configuration.ResultAnyElement import org.jacodb.taint.configuration.This import org.usvm.dataflow.ifds.AccessPath import org.usvm.dataflow.ifds.ElementAccessor -import org.usvm.dataflow.ifds.Maybe -import org.usvm.dataflow.ifds.fmap -import org.usvm.dataflow.ifds.toMaybe import org.usvm.dataflow.util.Traits +import org.usvm.util.Maybe +import org.usvm.util.fmap +import org.usvm.util.toMaybe class CallPositionToAccessPathResolver( private val traits: Traits, diff --git a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/config/TaintAction.kt b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/config/TaintAction.kt index 622dc56767..5ef603b45a 100644 --- a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/config/TaintAction.kt +++ b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/config/TaintAction.kt @@ -16,17 +16,17 @@ package org.usvm.dataflow.config -import org.usvm.dataflow.ifds.AccessPath -import org.usvm.dataflow.ifds.Maybe -import org.usvm.dataflow.ifds.fmap -import org.usvm.dataflow.ifds.map -import org.usvm.dataflow.taint.Tainted import org.jacodb.taint.configuration.AssignMark import org.jacodb.taint.configuration.CopyAllMarks import org.jacodb.taint.configuration.CopyMark import org.jacodb.taint.configuration.PositionResolver import org.jacodb.taint.configuration.RemoveAllMarks import org.jacodb.taint.configuration.RemoveMark +import org.usvm.dataflow.ifds.AccessPath +import org.usvm.dataflow.taint.Tainted +import org.usvm.util.Maybe +import org.usvm.util.fmap +import org.usvm.util.map class TaintActionEvaluator( private val positionResolver: PositionResolver>, diff --git a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/Maybe.kt b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/Maybe.kt deleted file mode 100644 index e804812aa5..0000000000 --- a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/Maybe.kt +++ /dev/null @@ -1,60 +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.ifds - -@JvmInline -value class Maybe private constructor( - private val rawValue: Any?, -) { - val isSome: Boolean get() = rawValue !== NONE_VALUE - val isNone: Boolean get() = rawValue === NONE_VALUE - - fun getOrThrow(): T { - check(isSome) { "Maybe is None" } - @Suppress("UNCHECKED_CAST") - return rawValue as T - } - - companion object { - private val NONE_VALUE = Any() - private val NONE = Maybe(NONE_VALUE) - - fun none(): Maybe = NONE - - fun some(value: T): Maybe = Maybe(value) - - fun from(value: T?): Maybe = if (value == null) none() else some(value) - } -} - -inline fun Maybe.map(body: (T) -> Maybe): Maybe = - if (isNone) Maybe.none() else body(getOrThrow()) - -inline fun Maybe.fmap(body: (T) -> R): Maybe = - if (isNone) Maybe.none() else Maybe.some(body(getOrThrow())) - -inline fun Maybe.onSome(body: (T) -> Unit): Maybe { - if (isSome) body(getOrThrow()) - return this -} - -inline fun Maybe.onNone(body: () -> Unit): Maybe { - if (isNone) body() - return this -} - -fun T?.toMaybe(): Maybe = Maybe.from(this) diff --git a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/taint/TaintFlowFunctions.kt b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/taint/TaintFlowFunctions.kt index d64c6bed74..4858c09921 100644 --- a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/taint/TaintFlowFunctions.kt +++ b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/taint/TaintFlowFunctions.kt @@ -48,9 +48,9 @@ import org.usvm.dataflow.ifds.FlowFunctions import org.usvm.dataflow.ifds.isOnHeap import org.usvm.dataflow.ifds.isStatic import org.usvm.dataflow.ifds.minus -import org.usvm.dataflow.ifds.onSome import org.usvm.dataflow.util.Traits import org.usvm.dataflow.util.startsWith +import org.usvm.util.onSome private val logger = mu.KotlinLogging.logger {} @@ -462,7 +462,6 @@ class ForwardTaintFlowFunctions( } } } - class BackwardTaintFlowFunctions( private val traits: Traits, private val graph: ApplicationGraph, diff --git a/usvm-jvm-dataflow/build.gradle.kts b/usvm-jvm-dataflow/build.gradle.kts index e22790a6d9..cf5e9bb997 100644 --- a/usvm-jvm-dataflow/build.gradle.kts +++ b/usvm-jvm-dataflow/build.gradle.kts @@ -10,16 +10,14 @@ val samples by sourceSets.creating { dependencies { api(project(":usvm-dataflow")) + implementation(project(":usvm-util")) - implementation(Libs.jacodb_api_common) implementation(Libs.jacodb_api_jvm) implementation(Libs.jacodb_core) implementation(Libs.jacodb_api_storage) implementation(Libs.jacodb_storage) implementation(Libs.jacodb_taint_configuration) - implementation(Libs.sarif4k) - testImplementation(Libs.mockk) testImplementation(Libs.junit_jupiter_params) diff --git a/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/npe/NpeFlowFunctions.kt b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/npe/NpeFlowFunctions.kt index 5f1fa363b4..a82e18d4a7 100644 --- a/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/npe/NpeFlowFunctions.kt +++ b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/npe/NpeFlowFunctions.kt @@ -63,13 +63,13 @@ import org.usvm.dataflow.ifds.FlowFunctions import org.usvm.dataflow.ifds.isOnHeap import org.usvm.dataflow.ifds.isStatic import org.usvm.dataflow.ifds.minus -import org.usvm.dataflow.ifds.onSome import org.usvm.dataflow.jvm.graph.JcApplicationGraph import org.usvm.dataflow.jvm.util.JcTraits import org.usvm.dataflow.taint.TaintDomainFact import org.usvm.dataflow.taint.TaintZeroFact import org.usvm.dataflow.taint.Tainted import org.usvm.dataflow.util.startsWith +import org.usvm.util.onSome private val logger = mu.KotlinLogging.logger {} diff --git a/usvm-jvm-dataflow/src/test/kotlin/org/usvm/dataflow/jvm/impl/ConditionEvaluatorTest.kt b/usvm-jvm-dataflow/src/test/kotlin/org/usvm/dataflow/jvm/impl/ConditionEvaluatorTest.kt index 2141b9b026..e3545a3c28 100644 --- a/usvm-jvm-dataflow/src/test/kotlin/org/usvm/dataflow/jvm/impl/ConditionEvaluatorTest.kt +++ b/usvm-jvm-dataflow/src/test/kotlin/org/usvm/dataflow/jvm/impl/ConditionEvaluatorTest.kt @@ -53,10 +53,10 @@ import org.jacodb.taint.configuration.TypeMatches import org.junit.jupiter.api.Test import org.usvm.dataflow.config.BasicConditionEvaluator import org.usvm.dataflow.config.FactAwareConditionEvaluator -import org.usvm.dataflow.ifds.Maybe -import org.usvm.dataflow.ifds.toMaybe import org.usvm.dataflow.jvm.util.JcTraits import org.usvm.dataflow.taint.Tainted +import org.usvm.util.Maybe +import org.usvm.util.toMaybe import kotlin.test.assertFailsWith import kotlin.test.assertFalse import kotlin.test.assertTrue diff --git a/usvm-jvm-dataflow/src/test/kotlin/org/usvm/dataflow/jvm/impl/NullabilityAssumptionAnalysisTest.kt b/usvm-jvm-dataflow/src/test/kotlin/org/usvm/dataflow/jvm/impl/NullabilityAssumptionAnalysisTest.kt deleted file mode 100644 index 3d4bad0591..0000000000 --- a/usvm-jvm-dataflow/src/test/kotlin/org/usvm/dataflow/jvm/impl/NullabilityAssumptionAnalysisTest.kt +++ /dev/null @@ -1,69 +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.jvm.impl - -import NullAssumptionAnalysisExample -import org.jacodb.api.jvm.JcClassOrInterface -import org.jacodb.api.jvm.JcMethod -import org.jacodb.api.jvm.cfg.JcAssignInst -import org.jacodb.api.jvm.cfg.JcInstanceCallExpr -import org.jacodb.api.jvm.cfg.JcLocal -import org.jacodb.api.jvm.ext.findClass -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance -import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS -import org.usvm.dataflow.jvm.flow.NullAssumptionAnalysis - -@TestInstance(PER_CLASS) -class NullabilityAssumptionAnalysisTest : BaseAnalysisTest() { - - @Test - fun `null-assumption analysis should work`() { - val clazz = cp.findClass() - with(clazz.findMethod("test1").flowGraph()) { - val analysis = NullAssumptionAnalysis(this).also { - it.run() - } - val sout = (instructions[0] as JcAssignInst).lhv as JcLocal - val a = ((instructions[3] as JcAssignInst).rhv as JcInstanceCallExpr).instance - - assertTrue(analysis.isAssumedNonNullBefore(instructions[2], a)) - assertTrue(analysis.isAssumedNonNullBefore(instructions[0], sout)) - } - } - - @Test - fun `null-assumption analysis should work 2`() { - val clazz = cp.findClass() - with(clazz.findMethod("test2").flowGraph()) { - val analysis = NullAssumptionAnalysis(this).also { - it.run() - } - val sout = (instructions[0] as JcAssignInst).lhv as JcLocal - val a = ((instructions[3] as JcAssignInst).rhv as JcInstanceCallExpr).instance - val x = (instructions[5] as JcAssignInst).lhv as JcLocal - - assertTrue(analysis.isAssumedNonNullBefore(instructions[2], a)) - assertTrue(analysis.isAssumedNonNullBefore(instructions[0], sout)) - analysis.isAssumedNonNullBefore(instructions[5], x) - } - } - - private fun JcClassOrInterface.findMethod(name: String): JcMethod = declaredMethods.first { it.name == name } - -} diff --git a/usvm-ts/build.gradle.kts b/usvm-ts/build.gradle.kts index f1d7ed9655..f9121b88ce 100644 --- a/usvm-ts/build.gradle.kts +++ b/usvm-ts/build.gradle.kts @@ -4,6 +4,7 @@ plugins { dependencies { implementation(project(":usvm-core")) + implementation(project(":usvm-dataflow-ts")) implementation(Libs.jacodb_core) implementation(Libs.jacodb_ets) diff --git a/usvm-ts/src/main/kotlin/org/usvm/TSApplicationGraph.kt b/usvm-ts/src/main/kotlin/org/usvm/TSApplicationGraph.kt index 0d2d92a735..524a0974a5 100644 --- a/usvm-ts/src/main/kotlin/org/usvm/TSApplicationGraph.kt +++ b/usvm-ts/src/main/kotlin/org/usvm/TSApplicationGraph.kt @@ -1,12 +1,14 @@ package org.usvm import org.jacodb.ets.base.EtsStmt -import org.jacodb.ets.model.EtsFile import org.jacodb.ets.model.EtsMethod +import org.jacodb.ets.model.EtsScene +import org.usvm.dataflow.ts.graph.EtsApplicationGraph +import org.usvm.dataflow.ts.graph.EtsApplicationGraphImpl import org.usvm.statistics.ApplicationGraph -class TSApplicationGraph(project: EtsFile) : ApplicationGraph { - private val applicationGraph: ApplicationGraph = TODO() +class TSApplicationGraph(scene: EtsScene) : ApplicationGraph { + private val applicationGraph: EtsApplicationGraph = EtsApplicationGraphImpl(scene) override fun predecessors(node: EtsStmt): Sequence = applicationGraph.predecessors(node) diff --git a/usvm-ts/src/main/kotlin/org/usvm/TSExprResolver.kt b/usvm-ts/src/main/kotlin/org/usvm/TSExprResolver.kt index b383922130..cd26df8888 100644 --- a/usvm-ts/src/main/kotlin/org/usvm/TSExprResolver.kt +++ b/usvm-ts/src/main/kotlin/org/usvm/TSExprResolver.kt @@ -87,8 +87,8 @@ class TSExprResolver( fun resolveLValue(value: EtsValue): ULValue<*, *>? = when (value) { - is EtsParameterRef, - is EtsLocal -> simpleValueResolver.resolveLocal(value) + is EtsParameterRef, is EtsLocal -> simpleValueResolver.resolveLocal(value) + else -> error("Unexpected value: $value") } @@ -115,8 +115,6 @@ class TSExprResolver( return block(result0, result1) } - - override fun visit(value: EtsLocal): UExpr { return simpleValueResolver.visit(value) } @@ -157,6 +155,10 @@ class TSExprResolver( TODO("Not yet implemented") } + override fun visit(expr: EtsAwaitExpr): UExpr? { + TODO("Not yet implemented") + } + override fun visit(expr: EtsBitAndExpr): UExpr { TODO("Not yet implemented") } @@ -185,14 +187,6 @@ class TSExprResolver( TODO("Not yet implemented") } - override fun visit(expr: EtsAwaitExpr): UExpr? { - TODO("Not yet implemented") - } - - override fun visit(expr: EtsYieldExpr): UExpr? { - TODO("Not yet implemented") - } - override fun visit(expr: EtsDivExpr): UExpr { TODO("Not yet implemented") } @@ -337,6 +331,10 @@ class TSExprResolver( TODO("Not yet implemented") } + override fun visit(expr: EtsYieldExpr): UExpr? { + TODO("Not yet implemented") + } + override fun visit(value: EtsArrayAccess): UExpr { TODO("Not yet implemented") } diff --git a/usvm-ts/src/main/kotlin/org/usvm/TSMachine.kt b/usvm-ts/src/main/kotlin/org/usvm/TSMachine.kt index 4e56356488..4d9ac76b79 100644 --- a/usvm-ts/src/main/kotlin/org/usvm/TSMachine.kt +++ b/usvm-ts/src/main/kotlin/org/usvm/TSMachine.kt @@ -1,8 +1,8 @@ package org.usvm import org.jacodb.ets.base.EtsStmt -import org.jacodb.ets.model.EtsFile import org.jacodb.ets.model.EtsMethod +import org.jacodb.ets.model.EtsScene import org.usvm.ps.createPathSelector import org.usvm.state.TSMethodResult import org.usvm.state.TSState @@ -20,7 +20,7 @@ import org.usvm.stopstrategies.createStopStrategy import kotlin.time.Duration.Companion.seconds class TSMachine( - private val project: EtsFile, + private val project: EtsScene, private val options: UMachineOptions, ) : UMachine() { private val typeSystem = TSTypeSystem(typeOperationsTimeout = 1.seconds, project) diff --git a/usvm-ts/src/main/kotlin/org/usvm/TSTypeSystem.kt b/usvm-ts/src/main/kotlin/org/usvm/TSTypeSystem.kt index 9402a88501..d6f821ada2 100644 --- a/usvm-ts/src/main/kotlin/org/usvm/TSTypeSystem.kt +++ b/usvm-ts/src/main/kotlin/org/usvm/TSTypeSystem.kt @@ -1,14 +1,14 @@ package org.usvm import org.jacodb.ets.base.EtsType -import org.jacodb.ets.model.EtsFile +import org.jacodb.ets.model.EtsScene import org.usvm.types.UTypeStream import org.usvm.types.UTypeSystem import kotlin.time.Duration class TSTypeSystem( override val typeOperationsTimeout: Duration, - val project: EtsFile, + val project: EtsScene, ) : UTypeSystem { override fun isSupertype(supertype: EtsType, type: EtsType): Boolean { 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 fa82d2177d..970861388c 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,6 @@ package org.usvm.util +import org.jacodb.ets.base.DEFAULT_ARK_CLASS_NAME import org.jacodb.ets.base.EtsAnyType import org.jacodb.ets.base.EtsBooleanType import org.jacodb.ets.base.EtsNumberType @@ -10,6 +11,7 @@ import org.jacodb.ets.dto.EtsFileDto import org.jacodb.ets.dto.convertToEtsFile import org.jacodb.ets.model.EtsFile import org.jacodb.ets.model.EtsMethod +import org.jacodb.ets.model.EtsScene import org.jacodb.ets.utils.loadEtsFileAutoConvert import org.usvm.NoCoverage import org.usvm.PathSelectionStrategy @@ -29,7 +31,7 @@ typealias CoverageChecker = (TSMethodCoverage) -> Boolean open class TSMethodTestRunner : TestRunner() { - protected val globalClassName = "_DEFAULT_ARK_CLASS" + protected val globalClassName = DEFAULT_ARK_CLASS_NAME protected val doNotCheckCoverage: CoverageChecker = { _ -> true } @@ -205,10 +207,11 @@ open class TSMethodTestRunner : TestRunner + TSMachine(project, options).use { machine -> val states = machine.analyze(listOf(method)) states.map { state -> val resolver = TSTestResolver() diff --git a/usvm-util/src/main/kotlin/org/usvm/util/Logging.kt b/usvm-util/src/main/kotlin/org/usvm/util/Logging.kt index cd74322328..5ece6e5a1e 100644 --- a/usvm-util/src/main/kotlin/org/usvm/util/Logging.kt +++ b/usvm-util/src/main/kotlin/org/usvm/util/Logging.kt @@ -40,10 +40,10 @@ inline fun LoggerWithLogMethod.bracket( val startNano = System.nanoTime() var alreadyLogged = false - var res : Maybe = Maybe.empty() + var res: Maybe = Maybe.none() try { // Note: don't replace this one with runCatching, otherwise return from lambda breaks "finished" logging. - res = Maybe(block()) + res = Maybe.some(block()) return res.getOrThrow() } catch (t: Throwable) { logMethod { "Finished (in ${elapsedSecFrom(startNano)}): $msg :: EXCEPTION :: ${closingComment(Result.failure(t))}" } @@ -51,7 +51,7 @@ inline fun LoggerWithLogMethod.bracket( throw t } finally { if (!alreadyLogged) { - if (res.hasValue) + if (res.isSome) logMethod { "Finished (in ${elapsedSecFrom(startNano)}): $msg ${closingComment(Result.success(res.getOrThrow()))}" } else logMethod { "Finished (in ${elapsedSecFrom(startNano)}): $msg " } @@ -75,4 +75,4 @@ inline fun KLogger.logException(block: () -> T): T { this.error("Exception occurred", e) throw e } -} \ No newline at end of file +} diff --git a/usvm-util/src/main/kotlin/org/usvm/util/Maybe.kt b/usvm-util/src/main/kotlin/org/usvm/util/Maybe.kt index c630f4d4d1..3ee025572e 100644 --- a/usvm-util/src/main/kotlin/org/usvm/util/Maybe.kt +++ b/usvm-util/src/main/kotlin/org/usvm/util/Maybe.kt @@ -1,23 +1,51 @@ package org.usvm.util -/** - * Analogue of java's [java.util.Optional] - */ -class Maybe private constructor(val hasValue: Boolean, val value:T? ) { - constructor(v: T) : this(true, v) - companion object { - fun empty() = Maybe(false, null) +@JvmInline +value class Maybe private constructor( + private val rawValue: Any?, +) { + val isSome: Boolean get() = rawValue !== NONE_VALUE + val isNone: Boolean get() = rawValue === NONE_VALUE + + fun getOrThrow(): T { + check(isSome) { "Maybe is None" } + @Suppress("UNCHECKED_CAST") + return rawValue as T + } + + override fun toString(): String { + return if (isSome) "Some($rawValue)" else "None" } - /** - * Returns [value] if [hasValue]. Otherwise, throws exception - */ - @Suppress("UNCHECKED_CAST") - fun getOrThrow() : T = if (!hasValue) { - error("Maybe hasn't value") - } else { - value as T + companion object { + private val NONE_VALUE = object { + // Note: toString() for debugger + override fun toString(): String = "None" + } + private val NONE = Maybe(NONE_VALUE) + + fun none(): Maybe = NONE + + fun some(value: T): Maybe = Maybe(value) + + fun from(value: T?): Maybe = if (value == null) none() else some(value) } +} + +inline fun Maybe.map(body: (T) -> Maybe): Maybe = + if (isNone) Maybe.none() else body(getOrThrow()) + +inline fun Maybe.fmap(body: (T) -> R): Maybe = + if (isNone) Maybe.none() else Maybe.some(body(getOrThrow())) + +inline fun Maybe.onSome(body: (T) -> Unit): Maybe { + if (isSome) body(getOrThrow()) + return this +} + +inline fun Maybe.onNone(body: () -> Unit): Maybe { + if (isNone) body() + return this +} - override fun toString(): String = if (hasValue) "Maybe($value)" else "" -} \ No newline at end of file +fun T?.toMaybe(): Maybe = Maybe.from(this)