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 index 6544a6f09..f5f4374d0 100644 --- 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 @@ -10,6 +10,7 @@ import org.jacodb.ets.base.EtsParameterRef import org.jacodb.ets.base.EtsStaticFieldRef import org.jacodb.ets.base.EtsThis import org.jacodb.ets.base.EtsValue +import org.jacodb.ets.model.EtsClassSignature data class AccessPath(val base: AccessPathBase, val accesses: List) { operator fun plus(accessor: Accessor) = AccessPath(base, accesses + accessor) @@ -44,8 +45,8 @@ sealed interface AccessPathBase { override fun toString(): String = "" } - object Static : AccessPathBase { - override fun toString(): String = "" + data class Static(val clazz: EtsClassSignature) : AccessPathBase { + override fun toString(): String = "static(${clazz.name})" } data class Arg(val index: Int) : AccessPathBase { @@ -54,6 +55,34 @@ sealed interface AccessPathBase { data class Local(val name: String) : AccessPathBase { override fun toString(): String = "local($name)" + + fun tryGetOrdering(): Int? { + if (name.startsWith("%")) { + val ix = name.substring(1).toIntOrNull() + if (ix != null) { + return ix + } + } + if (name.startsWith("\$v")) { + val ix = name.substring(2).toIntOrNull() + if (ix != null) { + return 10_000 + ix + } + } + if (name.startsWith("\$temp")) { + val ix = name.substring(5).toIntOrNull() + if (ix != null) { + return 20_000 + ix + } + } + if (name.startsWith("_tmp")) { + val ix = name.substring(4).toIntOrNull() + if (ix != null) { + return 30_000 + ix + } + } + return null + } } data class Const(val constant: EtsConstant) : AccessPathBase { @@ -86,7 +115,10 @@ fun EtsEntity.toPathOrNull(): AccessPath? = when (this) { it + FieldAccessor(field.name) } - is EtsStaticFieldRef -> AccessPath(AccessPathBase.Static, listOf(FieldAccessor(field.name))) + is EtsStaticFieldRef -> { + val base = AccessPathBase.Static(field.enclosingClass) + AccessPath(base, listOf(FieldAccessor(field.name))) + } is EtsCastExpr -> arg.toPathOrNull() 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 index c8867cb1e..758c6590b 100644 --- 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 @@ -34,6 +34,7 @@ class BackwardFlowFunctions( val graph: ApplicationGraph, val dominators: (EtsMethod) -> GraphDominators, val savedTypes: MutableMap>, + val doAddKnownTypes: Boolean = true, ) : FlowFunctions { // private val aliasesCache: MutableMap>> = hashMapOf() @@ -200,9 +201,21 @@ class BackwardFlowFunctions( // Case `return x` // ∅ |= x:unknown if (current is EtsReturnStmt) { - val variable = current.returnValue?.toBase() - if (variable != null) { - result += TypedVariable(variable, EtsTypeFact.UnknownEtsTypeFact) + val returnValue = current.returnValue + if (returnValue != null) { + val variable = returnValue.toBase() + val type = if (doAddKnownTypes) { + EtsTypeFact.from(returnValue.type).let { + if (it is EtsTypeFact.AnyEtsTypeFact) { + EtsTypeFact.UnknownEtsTypeFact + } else { + it + } + } + } else { + EtsTypeFact.UnknownEtsTypeFact + } + result += TypedVariable(variable, type) } } @@ -223,10 +236,23 @@ class BackwardFlowFunctions( if (rhv.accesses.isEmpty()) { // Case `x... := y` // ∅ |= y:unknown - result += TypedVariable(y, EtsTypeFact.UnknownEtsTypeFact) + val type = if (doAddKnownTypes) { + EtsTypeFact.from(current.rhv.type).let { it -> + if (it is EtsTypeFact.AnyEtsTypeFact) { + EtsTypeFact.UnknownEtsTypeFact + } else { + it + } + } + } else { + EtsTypeFact.UnknownEtsTypeFact + } + result += TypedVariable(y, type) } else { // Case `x := y.f` OR `x := y[i]` + // TODO: handle known (real) type + check(rhv.accesses.size == 1) when (val accessor = rhv.accesses.single()) { // Case `x := y.f` @@ -359,6 +385,11 @@ class BackwardFlowFunctions( cls = null, properties = mapOf(a.name to fact.type) ) + // val realType = EtsTypeFact.from(current.rhv.type) + // val type = newType.intersect(realType) ?: run { + // logger.warn { "Empty intersection of fact and real type: $newType & $realType" } + // newType + // } result += TypedVariable(y, type).withTypeGuards(current) // aliases: +|= z:{f:T} // for (z in preAliases.getAliases(AccessPath(y, emptyList()))) { @@ -373,6 +404,11 @@ class BackwardFlowFunctions( // x:T |= x:T (keep) + y:Array val y = rhv.base val type = EtsTypeFact.ArrayEtsTypeFact(elementType = fact.type) + // val realType = EtsTypeFact.from(current.rhv.type) + // val type = newType.intersect(realType) ?: run { + // logger.warn { "Empty intersection of fact and real type: $newType & $realType" } + // newType + // } val newFact = TypedVariable(y, type).withTypeGuards(current) return listOf(fact, newFact) } @@ -386,11 +422,11 @@ class BackwardFlowFunctions( // Case `x.f := y` is FieldAccessor -> { if (fact.type is EtsTypeFact.UnionEtsTypeFact) { - TODO("Support union type for x.f := y in BW-sequent") + // 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") + // TODO("Support intersection type for x.f := y in BW-sequent") } // x:primitive |= x:primitive (pass) @@ -412,11 +448,11 @@ class BackwardFlowFunctions( // Case `x[i] := y` is ElementAccessor -> { if (fact.type is EtsTypeFact.UnionEtsTypeFact) { - TODO("Support union type for x[i] := y in BW-sequent") + // 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") + // TODO("Support intersection type for x[i] := y in BW-sequent") } // x:Array |= x:Array (pass) 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 index 24b211843..3a58ce02e 100644 --- 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 @@ -17,6 +17,7 @@ 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.usvm.dataflow.ts.util.Globals private val logger = KotlinLogging.logger {} @@ -44,7 +45,9 @@ sealed interface EtsTypeFact { } } - fun intersect(other: EtsTypeFact): EtsTypeFact? { + fun intersect(other: EtsTypeFact?): EtsTypeFact? { + if (other == null) return this + if (this == other) return this if (other is UnknownEtsTypeFact) return this @@ -151,10 +154,41 @@ sealed interface EtsTypeFact { override fun toString(): String = "Array<$elementType>" } - data class ObjectEtsTypeFact( + @ConsistentCopyVisibility + data class ObjectEtsTypeFact private constructor( val cls: EtsType?, val properties: Map, ) : BasicType { + companion object { + operator fun invoke( + cls: EtsType?, + properties: Map, + ): ObjectEtsTypeFact { + if (cls is EtsUnclearRefType && cls.name == "Object") { + return ObjectEtsTypeFact(null, properties) + } + return ObjectEtsTypeFact(cls, properties) + } + } + + fun getRealProperties(): Map { + val scene = Globals.scene ?: return properties + if (cls == null || cls !is EtsClassType) { + return properties + } + val clazz = scene.projectAndSdkClasses.firstOrNull { it.signature == cls.signature } + ?: return properties + val props = properties.toMutableMap() + clazz.methods.forEach { m -> + props.merge(m.name, FunctionEtsTypeFact) { old, new -> + old.intersect(new).also { + if (it == null) logger.warn { "Empty intersection: $old & $new" } + } + } + } + return props + } + override fun toString(): String { val clsName = cls?.typeName?.takeUnless { it.startsWith(ANONYMOUS_CLASS_PREFIX) } ?: "Object" val funProps = properties.entries @@ -360,23 +394,26 @@ sealed interface EtsTypeFact { return mkIntersectionType(guardedType, other) } + private fun tryIntersect(cls1: EtsType?, cls2: EtsType?): EtsType? { + if (cls1 == cls2) return cls1 + if (cls1 == null) return cls2 + if (cls2 == null) return cls1 + // TODO: isSubtype + return null + } + private fun intersect(obj1: ObjectEtsTypeFact, obj2: ObjectEtsTypeFact): EtsTypeFact? { - val intersectionProperties = obj1.properties.toMutableMap() - for ((property, type) in obj2.properties) { + val intersectionProperties = obj1.getRealProperties().toMutableMap() + for ((property, type) in obj2.getRealProperties()) { val currentType = intersectionProperties[property] if (currentType == null) { intersectionProperties[property] = type - continue + } else { + intersectionProperties[property] = currentType.intersect(type) + ?: return null } - - 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 } + val intersectionCls = tryIntersect(obj1.cls, obj2.cls) return ObjectEtsTypeFact(intersectionCls, intersectionProperties) } @@ -391,7 +428,7 @@ sealed interface EtsTypeFact { type } - return ObjectEtsTypeFact(obj.cls, intersectionProperties) + return ObjectEtsTypeFact(null, intersectionProperties) } private fun union(unionType: UnionEtsTypeFact, other: EtsTypeFact): EtsTypeFact { @@ -504,7 +541,7 @@ sealed interface EtsTypeFact { is EtsUnclearRefType -> ObjectEtsTypeFact(type, emptyMap()) // is EtsGenericType -> TODO() else -> { - logger.error { "Unsupported type: $type" } + logger.warn { "Unsupported type: $type" } UnknownEtsTypeFact } } 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 index a8aebbe64..8bfce4798 100644 --- 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 @@ -1,5 +1,6 @@ package org.usvm.dataflow.ts.infer +import org.jacodb.ets.base.EtsNopStmt import org.jacodb.ets.base.EtsStmt import org.jacodb.ets.base.EtsType import org.jacodb.ets.model.EtsMethod @@ -10,7 +11,7 @@ import org.usvm.dataflow.ts.graph.EtsApplicationGraph class ForwardAnalyzer( val graph: EtsApplicationGraph, - methodInitialTypes: Map, + methodInitialTypes: Map>, typeInfo: Map, doAddKnownTypes: Boolean = true, ) : Analyzer { @@ -27,18 +28,18 @@ class ForwardAnalyzer( 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, + val currentIsExit = current in graph.exitPoints(method) || + (current is EtsNopStmt && graph.successors(current).none()) + if (currentIsExit) { + return listOf( + ForwardSummaryAnalyzerEvent( + method = method, + initialVertex = startVertex, + exitVertex = currentVertex, + ) ) - ) + } + return emptyList() } } 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 index b9971a846..e25d6f599 100644 --- 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 @@ -6,11 +6,14 @@ 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.EtsClassType import org.jacodb.ets.base.EtsFieldRef import org.jacodb.ets.base.EtsInstanceCallExpr import org.jacodb.ets.base.EtsLValue +import org.jacodb.ets.base.EtsNegExpr import org.jacodb.ets.base.EtsNewArrayExpr import org.jacodb.ets.base.EtsNewExpr +import org.jacodb.ets.base.EtsNotExpr import org.jacodb.ets.base.EtsNullConstant import org.jacodb.ets.base.EtsNumberConstant import org.jacodb.ets.base.EtsRef @@ -28,12 +31,22 @@ 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 +import org.usvm.dataflow.ts.util.getRealLocals private val logger = KotlinLogging.logger {} +fun EtsType.unwrapPromise(): EtsType { + if (this is EtsClassType) { + if (this.signature.name == "Promise" && this.typeParameters.isNotEmpty()) { + return this.typeParameters[0] + } + } + return this +} + class ForwardFlowFunctions( val graph: EtsApplicationGraph, - val methodInitialTypes: Map, + val methodInitialTypes: Map>, val typeInfo: Map, val doAddKnownTypes: Boolean = true, ) : FlowFunctions { @@ -45,27 +58,35 @@ class ForwardFlowFunctions( } override fun obtainPossibleStartFacts(method: EtsMethod): Collection { - val result = mutableListOf(Zero) + val initialTypes = methodInitialTypes[method] ?: return listOf(Zero) - val initialTypes = methodInitialTypes[method] - if (initialTypes != null) { - for ((base, type) in initialTypes.types) { - val path = AccessPath(base, accesses = emptyList()) - addTypes(path, type, result) - } - } + val result = mutableListOf(Zero) - 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) + val fakeLocals = method.locals.toSet() - method.getRealLocals() + + for ((base, type) in initialTypes) { + if (base is AccessPathBase.Local) { + val fake = fakeLocals.find { it.toBase() == base } + if (fake != null) { + val path = AccessPath(base, emptyList()) + val realType = EtsTypeFact.from(fake.type).let { + if (it is EtsTypeFact.AnyEtsTypeFact) { + EtsTypeFact.UnknownEtsTypeFact + } else { + it + } } + val type2 = type.intersect(realType) ?: run { + logger.warn { "Empty intersection: $type & $realType" } + type + } + addTypes(path, type2, result) + continue } } + + val path = AccessPath(base, emptyList()) + addTypes(path, type, result) } return result @@ -219,6 +240,14 @@ class ForwardFlowFunctions( result += TypedVariable(lhv, EtsTypeFact.BooleanEtsTypeFact) } + is EtsNegExpr -> { + result += TypedVariable(lhv, EtsTypeFact.NumberEtsTypeFact) + } + + is EtsNotExpr -> { + result += TypedVariable(lhv, EtsTypeFact.BooleanEtsTypeFact) + } + else -> { // logger.info { "TODO: forward assign $current" } } @@ -442,8 +471,21 @@ class ForwardFlowFunctions( returnSite: EtsStmt, ): FlowFunction = FlowFunction { fact -> when (fact) { - // TODO: add known return type of the function call - Zero -> listOf(Zero) + Zero -> { + val callExpr = callStatement.callExpr ?: error("No call in $callStatement") + + val result = mutableListOf(Zero) + + // `x := f()` + if (callStatement is EtsAssignStmt) { + val left = callStatement.lhv.toPath() + val type = EtsTypeFact.from(callExpr.method.returnType.unwrapPromise()) + addTypes(left, type, result) + } + + result + } + is TypedVariable -> call(callStatement, fact) } } @@ -452,28 +494,36 @@ class ForwardFlowFunctions( 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") + val callExpr = callStatement.callExpr ?: error("No call in $callStatement") - // 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() + if (callStatement is EtsAssignStmt) { + val left = callStatement.lhv.toPath() + if (fact.variable.base == left.base) { + // Fact on LHS is overwritten by the call result + return emptyList() + } } - for (arg in callExpr.args) { - val argPath = arg.toPath() - if (fact.variable.base == argPath.base) return emptyList() - } + // // todo: hack, keep fact if call was not resolved + // if (graph.callees(callStatement).none()) { + // return listOf(fact) + // } + // + // graph.callees(callStatement).singleOrNull()?.let { q -> + // if (q.cfg.stmts.isEmpty()) { + // 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) } 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 index 87358c78e..1101e9164 100644 --- 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 @@ -16,254 +16,134 @@ 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 } +class TypeGuesser( + private val scene: EtsScene, +) { + private val propertyNameToClasses: Map> by lazy { + val result: MutableMap> = hashMapOf() + scene.projectAndSdkClasses.forEach { clazz -> + clazz.methods.forEach { + result.computeIfAbsent(it.name) { hashSetOf() }.add(clazz) } - - if (updatedTypes.isEmpty()) { - EtsTypeFact.AnyEtsTypeFact - } else { - EtsTypeFact.mkUnionType(updatedTypes).simplify() + clazz.fields.forEach { + result.computeIfAbsent(it.name) { hashSetOf() }.add(clazz) } } - - 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) + result } - 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 + fun guess(fact: EtsTypeFact): EtsTypeFact { + return fact.resolveType() } -} -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 EtsTypeFact.resolveType(): EtsTypeFact = + when (val simplifiedFact = simplify()) { + is EtsTypeFact.ArrayEtsTypeFact -> simplifiedFact.resolveArrayTypeFact() + is EtsTypeFact.ObjectEtsTypeFact -> simplifiedFact.resolveObjectTypeFact() + is EtsTypeFact.FunctionEtsTypeFact -> simplifiedFact + is EtsTypeFact.GuardedTypeFact -> TODO("guarded") + + is EtsTypeFact.IntersectionEtsTypeFact -> { + val updatedTypes = simplifiedFact.types.mapTo(hashSetOf()) { + it.resolveType() + } + EtsTypeFact.mkIntersectionType(updatedTypes).simplify() } - ) - } -private fun collectSuitableClasses( - touchedPropertiesNames: Set, - propertyNameToClasses: Map>, -): Set { - val classesWithProperties = touchedPropertiesNames.map { propertyNameToClasses[it].orEmpty() } - - return classesWithProperties.reduceOrNull { a, b -> a intersect b }.orEmpty() -} + is EtsTypeFact.UnionEtsTypeFact -> { + val updatedTypes = simplifiedFact.types.mapNotNullTo(hashSetOf()) { + val resolved = it.resolveType() + if (resolved is EtsTypeFact.AnyEtsTypeFact) { + null + } else { + resolved + } + } + + if (updatedTypes.isEmpty()) { + EtsTypeFact.AnyEtsTypeFact + } else { + EtsTypeFact.mkUnionType(updatedTypes).simplify() + } + } -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) + else -> simplify() } - 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) + private fun EtsTypeFact.ObjectEtsTypeFact.resolveObjectTypeFact(): EtsTypeFact { + if (cls != null) { + // TODO: inspect this.properties and maybe find more exact class-type than 'cls' + return this } - is EtsTypeFact.ObjectEtsTypeFact -> { - if (cls != null) { - return this - } + val touchedPropertiesNames = properties.keys + val classesInSystem = collectSuitableClasses(touchedPropertiesNames) - val props = properties.mapValues { it.value.simplify() } - copy(properties = props) + if (classesInSystem.isEmpty()) { + return tryToDetermineSpecialObjects(touchedPropertiesNames) } - 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) + val suitableTypes = resolveTypesFromClasses(classesInSystem) - if (objectClassFacts.isNotEmpty()) { - val allProperties = hashMapOf>().withDefault { hashSetOf() } + // TODO process arrays here (and strings) - objectClassFacts.forEach { fact -> - fact as EtsTypeFact.ObjectEtsTypeFact - - fact.properties.forEach { (name, propertyFact) -> - allProperties.getValue(name).add(propertyFact) - } + 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 } + } - val mergedAllProperties = hashMapOf() - allProperties.forEach { (name, propertyFact) -> - mergedAllProperties[name] = EtsTypeFact.mkUnionType(propertyFact) + private fun EtsTypeFact.ObjectEtsTypeFact.resolveTypesFromClasses( + classes: Iterable, + ): Set = + classes.mapTo(hashSetOf()) { cls -> + EtsTypeFact.ObjectEtsTypeFact( + cls = EtsClassType(signature = cls.signature), + properties = properties.mapValues { + it.value.resolveType() + } + ) } - updatedTypeFacts += EtsTypeFact.ObjectEtsTypeFact(cls = null, properties = mergedAllProperties) + private fun collectSuitableClasses( + touchedPropertiesNames: Set, + ): Set { + val classesWithProperties = touchedPropertiesNames.map { propertyNameToClasses[it] ?: emptySet() } + return classesWithProperties.reduceOrNull { a, b -> a intersect b } ?: emptySet() } - return EtsTypeFact.mkIntersectionType(updatedTypeFacts) -} - -private fun EtsTypeFact.UnionEtsTypeFact.simplifyUnionTypeFact(): EtsTypeFact { - val simplifiedArgs = types.map { it.simplify() } - - simplifiedArgs.singleOrNull()?.let { return it } - - val updatedTypeFacts = hashSetOf() + private fun EtsTypeFact.ObjectEtsTypeFact.tryToDetermineSpecialObjects( + touchedPropertiesNames: Set, + ): EtsTypeFact.BasicType { + val indicesProperties = properties.filter { (k, _) -> k.toIntOrNull() != null } + if (indicesProperties.isNotEmpty()) { + val elementTypeFacts = indicesProperties.mapTo(hashSetOf()) { + it.value.resolveType() + } - var atLeastOneNonEmptyObjectFound = false - var emptyTypeObjectFact: EtsTypeFact? = null + val typeFact = EtsTypeFact.mkUnionType(elementTypeFacts).simplify() - simplifiedArgs.forEach { - if (it !is EtsTypeFact.ObjectEtsTypeFact) { - updatedTypeFacts += it - return@forEach + return EtsTypeFact.ArrayEtsTypeFact(typeFact) } - if (it.cls != null) { - atLeastOneNonEmptyObjectFound = true - updatedTypeFacts += it - return@forEach + if ("length" in touchedPropertiesNames && "splice" in touchedPropertiesNames) { + return EtsTypeFact.ArrayEtsTypeFact(EtsTypeFact.AnyEtsTypeFact) } - if (it.properties.isEmpty() && emptyTypeObjectFact == null) { - emptyTypeObjectFact = it - } else { - updatedTypeFacts += it - atLeastOneNonEmptyObjectFound = true - } + return this } - // take a fact `Object {}` only if there were no other objects in the facts - emptyTypeObjectFact?.let { - if (!atLeastOneNonEmptyObjectFound) { - updatedTypeFacts += it + private fun EtsTypeFact.ArrayEtsTypeFact.resolveArrayTypeFact(): EtsTypeFact.ArrayEtsTypeFact = + if (elementType is EtsTypeFact.UnknownEtsTypeFact) { + this + } else { + val updatedElementType = elementType.resolveType() + if (updatedElementType === elementType) this else copy(elementType = elementType) } - } - - 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 index 09e37311b..97a192b90 100644 --- 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 @@ -1,20 +1,24 @@ package org.usvm.dataflow.ts.infer import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.cancelAndJoin -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch +import kotlinx.coroutines.newSingleThreadContext import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeout +import mu.KotlinLogging 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.EtsUnclearRefType import org.jacodb.ets.base.INSTANCE_INIT_METHOD_NAME import org.jacodb.ets.graph.findDominators import org.jacodb.ets.model.EtsMethod @@ -29,13 +33,19 @@ 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 org.usvm.dataflow.ts.util.getRealLocals +import org.usvm.dataflow.ts.util.sortedBy +import org.usvm.dataflow.ts.util.sortedByBase import java.util.concurrent.ConcurrentHashMap import kotlin.time.Duration.Companion.seconds +private val logger = KotlinLogging.logger {} + +@OptIn(ExperimentalCoroutinesApi::class) class TypeInferenceManager( val traits: EtsTraits, val graph: EtsApplicationGraph, -) : Manager { +) : Manager, AutoCloseable { private lateinit var runnerFinished: CompletableDeferred private val backwardSummaries = ConcurrentHashMap>() @@ -50,12 +60,23 @@ class TypeInferenceManager( private val savedTypes: ConcurrentHashMap> = ConcurrentHashMap() + @OptIn(DelicateCoroutinesApi::class) + private val analyzerDispatcher = newSingleThreadContext( + name = "${this::class.java.name}-worker" + ) + + private val analyzerScope = CoroutineScope(analyzerDispatcher) + + override fun close() { + analyzerDispatcher.close() + } + fun analyze( entrypoints: List, allMethods: List = entrypoints, doAddKnownTypes: Boolean = true, doInferAllLocals: Boolean = true, - ): TypeInferenceResult = runBlocking(Dispatchers.Default) { + ): TypeInferenceResult = runBlocking { val methodTypeScheme = collectSummaries( startMethods = entrypoints, doAddKnownTypes = doAddKnownTypes, @@ -74,10 +95,13 @@ class TypeInferenceManager( createResultsFromSummaries(updatedTypeScheme, doInferAllLocals) } + lateinit var backwardRunner: UniRunner + lateinit var forwardRunner: UniRunner + private suspend fun collectSummaries( startMethods: List, doAddKnownTypes: Boolean = true, - ): Map = coroutineScope { + ): Map> { logger.info { "Preparing backward analysis" } val backwardGraph = graph.reversed val backwardAnalyzer = BackwardAnalyzer(backwardGraph, savedTypes, ::methodDominators) @@ -90,8 +114,14 @@ class TypeInferenceManager( unit = SingletonUnit, zeroFact = BackwardTypeDomainFact.Zero, ) + this@TypeInferenceManager.backwardRunner = backwardRunner - val backwardJob = launch(start = CoroutineStart.LAZY) { + val exceptionHandler = CoroutineExceptionHandler { _, exception -> + logger.error { "Got exception from runner, stopping analysis" } + runnerFinished.completeExceptionally(exception) + } + + val backwardJob = analyzerScope.launch(exceptionHandler, start = CoroutineStart.LAZY) { backwardRunner.run(startMethods) } @@ -114,31 +144,37 @@ class TypeInferenceManager( // } // } - 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 methodTypeSchemeBasic = methodTypeScheme() + val guesser = TypeGuesser(graph.cp) + val methodTypeScheme = methodTypeSchemeBasic.mapValues { (_, facts) -> + facts.mapValues { (_, fact) -> + guesser.guess(fact) } } + // 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.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" } + logger.warn { "Empty intersection type: $acc & $it" } acc } } @@ -156,8 +192,9 @@ class TypeInferenceManager( unit = SingletonUnit, zeroFact = ForwardTypeDomainFact.Zero, ) + this@TypeInferenceManager.forwardRunner = forwardRunner - val forwardJob = launch(start = CoroutineStart.LAZY) { + val forwardJob = analyzerScope.launch(exceptionHandler, start = CoroutineStart.LAZY) { forwardRunner.run(startMethods) } @@ -182,31 +219,31 @@ class TypeInferenceManager( // } // } - methodTypeScheme + return methodTypeScheme } private fun createResultsFromSummaries( - methodTypeScheme: Map, + 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()}") - } - } - } - } + // 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.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 { @@ -216,22 +253,20 @@ class TypeInferenceManager( .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 + val combinedBackwardType = methodTypeScheme + .asSequence() + .filter { (method, _) -> method in (cls.methods + cls.ctor) } + .mapNotNull { (_, facts) -> facts[AccessPathBase.This] }.reduceOrNull { acc, type -> + acc.intersect(type) ?: run { + logger.warn { "Empty intersection type: $acc & $type" } + acc } - logger.info { - buildString { - appendLine("Combined backward type for This in class '${cls.signature}': ${combinedBackwardType?.toPrettyString()}") } - } + // logger.info { + // buildString { + // appendLine("Combined backward type for This in class '${cls.signature}': ${combinedBackwardType?.toPrettyString()}") + // } + // } if (combinedBackwardType == null) { return@mapNotNull null @@ -243,8 +278,8 @@ class TypeInferenceManager( .flatMap { (_, summaries) -> summaries.asSequence() } .mapNotNull { it.initialFact as? ForwardTypeDomainFact.TypedVariable } .filter { it.variable.base is AccessPathBase.This } - .toList() .distinct() + .toList() val typeFactsOnThisCtor = forwardSummaries.asSequence() .filter { (method, _) -> method.enclosingClass == cls.signature } @@ -252,8 +287,8 @@ class TypeInferenceManager( .flatMap { (_, summaries) -> summaries.asSequence() } .mapNotNull { it.exitFact as? ForwardTypeDomainFact.TypedVariable } .filter { it.variable.base is AccessPathBase.This } - .toList() .distinct() + .toList() val typeFactsOnThis = (typeFactsOnThisMethods + typeFactsOnThisCtor).distinct() @@ -261,20 +296,20 @@ class TypeInferenceManager( .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") - } - } - } + // 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") + logger.warn { "Empty intersection type: $refined & $propertyType" } refined } } @@ -289,14 +324,14 @@ class TypeInferenceManager( 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()}") - } - } - } + // 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 { @@ -387,28 +422,20 @@ class TypeInferenceManager( } for ((method, localFacts) in inferredLocalTypes) { - val facts = refinedTypes.getValue(method) - refinedTypes[method] = facts.copy(types = facts.types + localFacts) + val facts = refinedTypes[method] + if (facts == null) { + logger.warn { "No refined types for $method" } + continue + } + refinedTypes[method] = facts + 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() - } - } + .mapValues { (_, facts) -> facts.sortedByBase() } + // Simplify types: + .mapValues { (_, facts) -> facts.mapValues { (_, fact) -> fact.simplify() } } return TypeInferenceResult( inferredTypes = inferredTypes, @@ -417,51 +444,69 @@ class TypeInferenceManager( ) } - private fun methodTypeScheme(): Map = + private fun methodTypeScheme(): Map> = backwardSummaries.mapValues { (method, summaries) -> - buildMethodTypeScheme(method, summaries) - } + buildMethodTypeScheme(method, summaries).sortedByBase() + }.sortedBy { it.key.signature.toString() } - private fun refineMethodTypes(schema: Map): Map = + private fun refineMethodTypes(schema: Map>): Map> = schema.mapValues { (method, facts) -> val summaries = forwardSummaries[method].orEmpty() - refineMethodTypes(facts, summaries) + refineMethodTypes(method, facts, summaries) } + private val realLocalsBaseCache: MutableMap> = hashMapOf() + + private fun getRealLocalsBase(method: EtsMethod): Set { + return realLocalsBaseCache.computeIfAbsent(method) { + method.getRealLocals().mapTo(hashSetOf()) { it.toBase() } + } + } + private fun buildMethodTypeScheme( method: EtsMethod, summaries: Iterable, - ): EtsMethodTypeFacts { + ): Map { val types = summaries .mapNotNull { it.exitFact as? BackwardTypeDomainFact.TypedVariable } .groupBy({ it.variable }, { it.type }) - .filter { (base, _) -> base is AccessPathBase.This || base is AccessPathBase.Arg } + .filter { (base, _) -> + base is AccessPathBase.This + || base is AccessPathBase.Arg + || base is AccessPathBase.Static + || (base is AccessPathBase.Local && base !in getRealLocalsBase(method)) + } .mapValues { (_, typeFacts) -> typeFacts.reduce { acc, typeFact -> - val intersection = acc.intersect(typeFact) - - if (intersection == null) { - System.err.println("Empty intersection type: $acc & $typeFact") + acc.intersect(typeFact) ?: run { + logger.warn { "Empty intersection type: $acc & $typeFact" } + acc } - - intersection ?: acc } } - return EtsMethodTypeFacts(method, types) + return types } private fun refineMethodTypes( - facts: EtsMethodTypeFacts, // backward types + method: EtsMethod, + facts: Map, // backward types summaries: Iterable, - ): EtsMethodTypeFacts { + ): Map { // Contexts val typeFacts = summaries.asSequence() .mapNotNull { it.initialFact as? ForwardTypeDomainFact.TypedVariable } - .filter { it.variable.base is AccessPathBase.This || it.variable.base is AccessPathBase.Arg } + .filter { + val base = it.variable.base + base is AccessPathBase.This + || base is AccessPathBase.Arg + || base is AccessPathBase.Static + || (base is AccessPathBase.Local && base !in getRealLocalsBase(method)) + } .groupBy { it.variable.base } - val refinedTypes = facts.types.mapValues { (base, type) -> + val refinedTypes = facts.mapValues { (base, type) -> + // TODO: throw an error val typeRefinements = typeFacts[base] ?: return@mapValues type val propertyRefinements = typeRefinements @@ -483,7 +528,7 @@ class TypeInferenceManager( typeFacts.let {} - return EtsMethodTypeFacts(facts.method, refinedTypes) + return refinedTypes } private fun EtsTypeFact.refineProperties( @@ -505,7 +550,7 @@ class TypeInferenceManager( is EtsTypeFact.ArrayEtsTypeFact -> { // TODO: array types - logger.error { "TODO: Array type $this" } + logger.warn { "TODO: Array type $this" } val elementPath = pathFromRootObject + ElementAccessor val refinedElemType = @@ -553,24 +598,27 @@ class TypeInferenceManager( return EtsTypeFact.ObjectEtsTypeFact(cls, refinedProperties) } - private fun EtsTypeFact.refineProperty(property: List, type: EtsTypeFact): EtsTypeFact? = when (this) { + 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 - ) - }) + is EtsTypeFact.IntersectionEtsTypeFact -> EtsTypeFact.mkIntersectionType( + types = types.mapTo(hashSetOf()) { + // Note: intersection is empty if any of the types is empty + it.refineProperty(property, type) ?: return null + } + ) + + is EtsTypeFact.UnionEtsTypeFact -> EtsTypeFact.mkUnionType( + types = types.mapNotNullTo(hashSetOf()) { + // Note: ignore empty types in the union + it.refineProperty(property, type) + } + ) } private fun EtsTypeFact.BasicType.refineProperty( @@ -593,7 +641,10 @@ class TypeInferenceManager( if (property.size == 1) { // val p = property.single() // check(p is ElementAccessor) - val t = elementType.intersect(type) ?: error("Empty intersection") + val t = elementType.intersect(type) ?: run { + logger.warn { "Empty intersection of array element and refinement types: $elementType & $type" } + elementType + } return EtsTypeFact.ArrayEtsTypeFact(elementType = t) } else { return EtsTypeFact.AnyEtsTypeFact @@ -618,7 +669,7 @@ class TypeInferenceManager( } is EtsTypeFact.ObjectEtsTypeFact -> { - val propertyAccessor = property.firstOrNull() as? FieldAccessor + val propertyAccessor = property.firstOrNull() 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) { @@ -631,6 +682,7 @@ class TypeInferenceManager( // intersect(this:object, type:string) if (cls == EtsStringType) return type + if (cls is EtsUnclearRefType && cls.name == "String") return type if (cls != null) return null val intersectionProperties = properties @@ -666,10 +718,19 @@ class TypeInferenceManager( 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) + when (propertyAccessor) { + is FieldAccessor -> { + 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) + } + + is ElementAccessor -> { + // TODO: think about a proper way of handling the refinement with * accessor + return this + } + } } } } @@ -705,8 +766,4 @@ class TypeInferenceManager( } } } - - 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 index d4d991bab..b03d45791 100644 --- 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 @@ -16,42 +16,26 @@ 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), + fun withGuessedTypes(guesser: TypeGuesser): TypeInferenceResult = + TypeInferenceResult( + inferredTypes = inferredTypes.mapValues { (_, facts) -> + facts.mapValues { (_, fact) -> + guesser.guess(fact) + } + }, inferredReturnType = inferredReturnType.mapValues { (_, fact) -> - fact.resolveType(scene, propertyNameToClasses) + guesser.guess(fact) }, inferredCombinedThisType = inferredCombinedThisType.mapValues { (_, fact) -> - fact.resolveType(scene, propertyNameToClasses = propertyNameToClasses) + guesser.guess(fact) }, ) - } - - 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/TypeSimplification.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/TypeSimplification.kt new file mode 100644 index 000000000..9974bbb87 --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/TypeSimplification.kt @@ -0,0 +1,102 @@ +package org.usvm.dataflow.ts.infer + +fun EtsTypeFact.simplify(): EtsTypeFact = 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) { + val properties = properties.mapValues { it.value.simplify() } + EtsTypeFact.ObjectEtsTypeFact(cls = null, properties = properties) + } else { + this + } + } + + 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/annotation/withInferredTypes.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/annotation/EtsTypeAnnotator.kt similarity index 79% rename from usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/annotation/withInferredTypes.kt rename to usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/infer/annotation/EtsTypeAnnotator.kt index ee71d2cbe..31b5f4152 100644 --- 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/EtsTypeAnnotator.kt @@ -33,17 +33,20 @@ data class EtsTypeAnnotator( val scene: EtsScene, val typeInferenceResult: TypeInferenceResult, ) { - private fun selectTypesFor(method: EtsMethod) = typeInferenceResult.inferredTypes[method] ?: emptyMap() + private fun selectTypesFor(method: EtsMethod): Map = + typeInferenceResult.inferredTypes[method].orEmpty() - private fun combinedThisFor(method: EtsMethod) = typeInferenceResult.inferredCombinedThisType[method.enclosingClass] + private fun combinedThisFor(method: EtsMethod): EtsTypeFact? = + typeInferenceResult.inferredCombinedThisType[method.enclosingClass] - fun annotateWithTypes(scene: EtsScene) = with(scene) { + fun annotateWithTypes(scene: EtsScene): EtsScene = with(scene) { EtsScene( - projectFiles = projectFiles.map { annotateWithTypes(it) } + projectFiles = projectFiles.map { annotateWithTypes(it) }, + sdkFiles = sdkFiles.map { annotateWithTypes(it) }, ) } - fun annotateWithTypes(file: EtsFile) = with(file) { + fun annotateWithTypes(file: EtsFile): EtsFile = with(file) { EtsFile( signature = signature, classes = classes.map { annotateWithTypes(it) }, @@ -51,7 +54,7 @@ data class EtsTypeAnnotator( ) } - fun annotateWithTypes(clazz: EtsClass) = with(clazz) { + fun annotateWithTypes(clazz: EtsClass): EtsClass = with(clazz) { EtsClassImpl( signature = signature, fields = fields, @@ -64,7 +67,7 @@ data class EtsTypeAnnotator( ) } - fun annotateWithTypes(method: EtsMethod) = with(method) { + fun annotateWithTypes(method: EtsMethod): EtsMethod = with(method) { EtsMethodImpl( signature = signature, typeParameters = typeParameters, @@ -80,7 +83,7 @@ data class EtsTypeAnnotator( cfg: EtsCfg, types: Map, thisType: EtsTypeFact?, - ) = with(cfg) { + ): EtsCfg = with(cfg) { with(StmtTypeAnnotator(types, thisType, scene)) { EtsCfg( stmts = stmts.map { it.accept(this) }, 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 index 01df46cae..e3e964e43 100644 --- 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 @@ -66,7 +66,7 @@ class ValueTypeAnnotator( val name = value.field.name fun findInClass(signature: EtsClassSignature): EtsField? = - scene.projectClasses + scene.projectAndSdkClasses .singleOrNull { it.signature == signature } ?.fields ?.singleOrNull { it.name == name } 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 index 42aaad287..01a3aa002 100644 --- 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 @@ -35,14 +35,21 @@ 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.model.EtsFile +import org.jacodb.ets.model.EtsScene import org.jacodb.ets.utils.loadEtsProjectFromMultipleIR import org.usvm.dataflow.ts.infer.EntryPointsProcessor +import org.usvm.dataflow.ts.infer.TypeGuesser import org.usvm.dataflow.ts.infer.TypeInferenceManager import org.usvm.dataflow.ts.infer.TypeInferenceResult import org.usvm.dataflow.ts.infer.createApplicationGraph import org.usvm.dataflow.ts.infer.dto.toDto import org.usvm.dataflow.ts.util.EtsTraits +import org.usvm.dataflow.ts.util.loadEtsFile +import org.usvm.dataflow.ts.util.loadMultipleEtsFilesFromDirectory import java.nio.file.Path +import kotlin.io.path.exists +import kotlin.io.path.isRegularFile import kotlin.io.path.outputStream import kotlin.time.measureTimedValue @@ -89,16 +96,22 @@ class InferTypes : CliktCommand() { val project = loadEtsProjectFromMultipleIR(input, sdkPaths) val graph = createApplicationGraph(project) + val guesser = TypeGuesser(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) + val (resultBasic, timeAnalyze) = measureTimedValue { + manager.analyze(dummyMains, publicMethods) } - logger.info { "Inferred types for ${result.inferredTypes.size} methods in $timeAnalyze" } + logger.info { "Inferred types for ${resultBasic.inferredTypes.size} methods in $timeAnalyze" } + + val (result, timeGuess) = measureTimedValue { + resultBasic.withGuessedTypes(guesser) + } + logger.info { "Guessed types for ${result.inferredTypes.size} methods in $timeGuess" } dumpTypeInferenceResult(result, output, skipAnonymous) @@ -110,6 +123,39 @@ fun main(args: Array) { InferTypes().main(args) } +private fun loadEtsScene(projectPaths: List, sdkIRPath: List): EtsScene { + logger.info { "Loading ETS scene from $projectPaths, sdkPath is $sdkIRPath" } + + val projectFiles = loadIRFiles(projectPaths) + val sdkFiles = loadIRFiles(sdkIRPath) + + return EtsScene(projectFiles, sdkFiles) +} + +private fun loadIRFiles(filePaths: List): List { + val files = filePaths.flatMap { path -> + check(path.exists()) { "Path does not exist: $path" } + if (path.isRegularFile()) { + logger.info { "Loading single ETS file: $path" } + val file = loadEtsFile(path) + listOf(file) + } else { + logger.info { "Loading multiple ETS files: $path/**" } + loadMultipleEtsFilesFromDirectory(path).asIterable() + } + } + logger.info { + "Loaded ${files.size} files with ${ + files.sumOf { it.classes.size } + } classes and ${ + // Note: +1 for constructor + files.sumOf { it.classes.sumOf { cls -> cls.methods.size + 1 } } + } methods" + } + + return files +} + @OptIn(ExperimentalSerializationApi::class) fun dumpTypeInferenceResult( result: TypeInferenceResult, diff --git a/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/util/Globals.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/util/Globals.kt new file mode 100644 index 000000000..727ea9a0c --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/util/Globals.kt @@ -0,0 +1,7 @@ +package org.usvm.dataflow.ts.util + +import org.jacodb.ets.model.EtsScene + +object Globals { + var scene: EtsScene? = null +} diff --git a/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/util/LoadEts.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/util/LoadEts.kt new file mode 100644 index 000000000..17cd6bc26 --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/util/LoadEts.kt @@ -0,0 +1,49 @@ +package org.usvm.dataflow.ts.util + +import org.jacodb.ets.dto.EtsFileDto +import org.jacodb.ets.dto.convertToEtsFile +import org.jacodb.ets.model.EtsFile +import java.nio.file.Path +import kotlin.io.path.extension +import kotlin.io.path.inputStream +import kotlin.io.path.walk + +/** + * Load an [EtsFileDto] from a file. + * + * For example, `data/sample.json` can be loaded with: + * ``` + * val dto: EtsFileDto = loadEtsFileDto(Path("data/sample.json")) + * ``` + */ +fun loadEtsFileDto(path: Path): EtsFileDto { + require(path.extension == "json") { "File must have a '.json' extension: $path" } + path.inputStream().use { stream -> + return EtsFileDto.loadFromJson(stream) + } +} + +/** + * Load an [EtsFile] from a file. + * + * For example, `data/sample.json` can be loaded with: + * ``` + * val file: EtsFile = loadEtsFile(Path("data/sample.json")) + * ``` + */ +fun loadEtsFile(path: Path): EtsFile { + val etsFileDto = loadEtsFileDto(path) + return 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/main/kotlin/org/usvm/dataflow/ts/util/Map.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/util/Map.kt new file mode 100644 index 000000000..1969fb6cf --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/util/Map.kt @@ -0,0 +1,33 @@ +package org.usvm.dataflow.ts.util + +import org.usvm.dataflow.ts.infer.AccessPathBase + +fun Iterable>.toMap(): Map { + return associate { it.toPair() } +} + +inline fun > Map.sortedBy( + crossinline selector: (Map.Entry) -> R, +): Map { + return entries.sortedBy(selector).toMap() +} + +fun Map.sortedWith( + comparator: Comparator>, +): Map { + return entries.sortedWith(comparator).toMap() +} + +fun Map.sortedByBase(): Map = + sortedWith( + compareBy> { + when (val key = it.key) { + is AccessPathBase.This -> -1 + is AccessPathBase.Arg -> key.index + is AccessPathBase.Local -> 1_000_000 + (key.tryGetOrdering() ?: -1) + else -> Int.MAX_VALUE + } + }.thenBy { + it.key.toString() + } + ) diff --git a/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/util/RealLocals.kt b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/util/RealLocals.kt new file mode 100644 index 000000000..30bb4c1e0 --- /dev/null +++ b/usvm-dataflow-ts/src/main/kotlin/org/usvm/dataflow/ts/util/RealLocals.kt @@ -0,0 +1,20 @@ +package org.usvm.dataflow.ts.util + +import org.jacodb.ets.base.EtsAssignStmt +import org.jacodb.ets.base.EtsLocal +import org.jacodb.ets.model.EtsMethod + +private val realLocalsCache: MutableMap> = hashMapOf() + +/** + * Returns the set of "real" locals in this method, i.e. the locals that are assigned to in the method. + */ +fun EtsMethod.getRealLocals(): Set = + realLocalsCache.computeIfAbsent(this) { + cfg.stmts + .asSequence() + .filterIsInstance() + .map { it.lhv } + .filterIsInstance() + .toHashSet() + } 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 index 54fb50ff7..7d50e44a3 100644 --- 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 @@ -26,6 +26,7 @@ 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 org.usvm.dataflow.ts.infer.toBase import java.io.File class TypeInferenceStatistics { @@ -53,6 +54,8 @@ class TypeInferenceStatistics { private var undefinedUnknownCombination = 0L private var unknownAnyCombination = 0L + private var knownTypeToUndefined = 0L + fun compareSingleMethodFactsWithTypesInScene( methodTypeFacts: MethodTypesFacts, method: EtsMethod, @@ -64,21 +67,22 @@ class TypeInferenceStatistics { methodTypeFacts.apply { if (combinedThisFact == null && argumentsFacts.all { it == null } - && returnFact == null && localFacts.isEmpty() ) { noTypesInferred += method + // Note: no return here! + // Without taking into account the stats for such "empty" methods, + // the statistic would not be correct. } } val thisType = getEtsClassType(method.enclosingClass, graph.cp) val argTypes = method.parameters.map { it.type } - val locals = method.locals + val locals = method.getRealLocals().filterNot { it.name == "this" } val methodFacts = mutableListOf() thisType?.let { - val thisPosition = AccessPathBase.This val fact = methodTypeFacts.combinedThisFact val status = if (fact == null) { @@ -109,7 +113,7 @@ class TypeInferenceStatistics { } } - methodFacts += InferenceResult(thisPosition, it, fact, status) + methodFacts += InferenceResult(AccessPathBase.This, it, fact, status) } argTypes.forEachIndexed { index, type -> @@ -130,15 +134,12 @@ class TypeInferenceStatistics { 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 realType = it.type + val base = it.toBase() + val fact = methodTypeFacts.localFacts[base] val status = if (fact == null) { - if (type is EtsUnknownType) { + if (realType is EtsUnknownType) { noInfoInferredPreviouslyUnknown++ InferenceStatus.NO_INFO_PREVIOUSLY_UNKNOWN } else { @@ -146,10 +147,9 @@ class TypeInferenceStatistics { InferenceStatus.NO_INFO_PREVIOUSLY_KNOWN } } else { - checkForFact(fact, type) + checkForFact(fact, realType) } - - methodFacts += InferenceResult(local, type, fact, status) + methodFacts += InferenceResult(base, realType, fact, status) } allTypesAndFacts[method] = methodFacts @@ -386,8 +386,8 @@ class TypeInferenceStatistics { } else -> { - exactTypeInferredIncorrectlyPreviouslyKnown++ - InferenceStatus.DIFFERENT_TYPE_FOUND + knownTypeToUndefined++ + InferenceStatus.KNOWN_UNDEFINED_COMBINATION } } } @@ -538,6 +538,7 @@ class TypeInferenceStatistics { Unhandled type info: $unhandled Lost info about type: $noInfoInferredPreviouslyKnown + Was known, became undefined: $knownTypeToUndefined Nothing inferred, but it was unknown previously as well: $noInfoInferredPreviouslyUnknown Was unknown, became undefined: $undefinedUnknownCombination @@ -683,6 +684,7 @@ private enum class InferenceStatus(val message: String) { UNKNOWN_ANY_COMBINATION("Unknown any combination"), UNKNOWN_UNDEFINED_COMBINATION("Unknown undefined combination"), + KNOWN_UNDEFINED_COMBINATION("Known type became undefined"), ARRAY_INFO_PREV_UNKNOWN("Found an array type, previously unknown"), diff --git a/usvm-dataflow-ts/src/main/resources/logback.xml b/usvm-dataflow-ts/src/main/resources/logback.xml index 656c39317..417fafc13 100644 --- a/usvm-dataflow-ts/src/main/resources/logback.xml +++ b/usvm-dataflow-ts/src/main/resources/logback.xml @@ -7,6 +7,7 @@ logs/app.log + false %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 index d40ef9a67..2e18c960c 100644 --- 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 @@ -67,7 +67,7 @@ class EtsIfdsTest { @Test fun `test taint analysis on MethodCollision`() { val file = loadSample("MethodCollision") - val project = EtsScene(listOf(file)) + val project = EtsScene(listOf(file), sdkFiles = emptyList()) val graph = EtsApplicationGraphImpl(project) val unitResolver = UnitResolver { SingletonUnit } val getConfigForMethod: (EtsMethod) -> List? = @@ -113,7 +113,7 @@ class EtsIfdsTest { @Test fun `test taint analysis on TypeMismatch`() { val file = loadSample("TypeMismatch") - val project = EtsScene(listOf(file)) + val project = EtsScene(listOf(file), sdkFiles = emptyList()) val graph = EtsApplicationGraphImpl(project) val unitResolver = UnitResolver { SingletonUnit } val getConfigForMethod: (EtsMethod) -> List? = @@ -160,7 +160,7 @@ class EtsIfdsTest { @Test fun `test taint analysis on DataFlowSecurity`() { val file = loadSample("DataFlowSecurity") - val project = EtsScene(listOf(file)) + val project = EtsScene(listOf(file), sdkFiles = emptyList()) val graph = EtsApplicationGraphImpl(project) val unitResolver = UnitResolver { SingletonUnit } val getConfigForMethod: (EtsMethod) -> List? = @@ -227,7 +227,7 @@ class EtsIfdsTest { @Test fun `test taint analysis on case1 - untrusted loop bound scenario`() { val file = loadSample("cases/case1") - val project = EtsScene(listOf(file)) + val project = EtsScene(listOf(file), sdkFiles = emptyList()) val graph = EtsApplicationGraphImpl(project) val unitResolver = UnitResolver { SingletonUnit } val getConfigForMethod: (EtsMethod) -> List? = @@ -266,7 +266,7 @@ class EtsIfdsTest { @Test fun `test taint analysis on case2 - untrusted array buffer size scenario`() { val file = loadSample("cases/case2") - val project = EtsScene(listOf(file)) + val project = EtsScene(listOf(file), sdkFiles = emptyList()) val graph = EtsApplicationGraphImpl(project) val unitResolver = UnitResolver { SingletonUnit } val getConfigForMethod: (EtsMethod) -> List? = @@ -306,7 +306,7 @@ class EtsIfdsTest { @Test fun `test taint analysis on case3 - send plain information with sensitive data`() { val file = loadSample("cases/case3") - val project = EtsScene(listOf(file)) + val project = EtsScene(listOf(file), sdkFiles = emptyList()) val graph = EtsApplicationGraphImpl(project) val unitResolver = UnitResolver { SingletonUnit } val getConfigForMethod: (EtsMethod) -> List? = @@ -352,7 +352,7 @@ class EtsIfdsTest { @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 project = EtsScene(listOf(file), sdkFiles = emptyList()) val graph = EtsApplicationGraphImpl(project) val unitResolver = UnitResolver { SingletonUnit } val getConfigForMethod: (EtsMethod) -> List? = 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 index c8af281ee..194229d18 100644 --- 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 @@ -125,7 +125,7 @@ class EtsProjectAnalysisTest { try { logger.info { "Processing '$filename'" } val file = loadFromProject(filename) - val project = EtsScene(listOf(file)) + val project = EtsScene(listOf(file), sdkFiles = emptyList()) val startTime = System.currentTimeMillis() runAnalysis(project) val endTime = System.currentTimeMillis() 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 index 6683bf7fc..8f365eece 100644 --- 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 @@ -152,7 +152,7 @@ internal class EtsTypeAnnotationTest { namespaces = listOf(), ) - private val sampleScene = EtsScene(listOf(mainFile)) + private val sampleScene = EtsScene(listOf(mainFile), sdkFiles = emptyList()) private class CfgBuilderContext( val method: EtsMethod, 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 index bf11d2f5d..0fa417e13 100644 --- 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 @@ -42,6 +42,7 @@ import org.usvm.dataflow.ts.getResourcePath import org.usvm.dataflow.ts.getResourcePathOrNull import org.usvm.dataflow.ts.infer.AccessPathBase import org.usvm.dataflow.ts.infer.EtsTypeFact +import org.usvm.dataflow.ts.infer.TypeGuesser import org.usvm.dataflow.ts.infer.TypeInferenceManager import org.usvm.dataflow.ts.infer.TypeInferenceResult import org.usvm.dataflow.ts.infer.annotation.EtsTypeAnnotator @@ -50,6 +51,7 @@ 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 org.usvm.dataflow.ts.util.sortedByBase import java.io.File import kotlin.io.path.div import kotlin.io.path.exists @@ -74,7 +76,7 @@ class EtsTypeInferenceTest { fun `type inference for microphone`() { val name = "microphone" val file = load("/ts/$name.ts") - val project = EtsScene(listOf(file)) + val project = EtsScene(listOf(file), sdkFiles = emptyList()) val graph = createApplicationGraph(project) val entrypoints = project.projectClasses @@ -113,7 +115,7 @@ class EtsTypeInferenceTest { fun `type inference for types`() { val name = "types" val file = load("/ts/$name.ts") - val project = EtsScene(listOf(file)) + val project = EtsScene(listOf(file), sdkFiles = emptyList()) val graph = createApplicationGraph(project) val entrypoints = project.projectClasses @@ -132,7 +134,7 @@ class EtsTypeInferenceTest { fun `type inference for data`() { val name = "data" val file = load("/ts/$name.ts") - val project = EtsScene(listOf(file)) + val project = EtsScene(listOf(file), sdkFiles = emptyList()) val graph = createApplicationGraph(project) val entrypoints = project.projectClasses @@ -151,7 +153,7 @@ class EtsTypeInferenceTest { fun `type inference for call`() { val name = "call" val file = load("/ts/$name.ts") - val project = EtsScene(listOf(file)) + val project = EtsScene(listOf(file), sdkFiles = emptyList()) val graph = createApplicationGraph(project) val entrypoints = project.projectClasses @@ -170,7 +172,7 @@ class EtsTypeInferenceTest { fun `type inference for nested_init`() { val name = "nested_init" val file = load("/ts/$name.ts") - val project = EtsScene(listOf(file)) + val project = EtsScene(listOf(file), sdkFiles = emptyList()) val graph = createApplicationGraph(project) val entrypoints = project.projectClasses @@ -275,7 +277,7 @@ class EtsTypeInferenceTest { println("Processing ${files.size} files...") val etsFiles = files.map { convertToEtsFile(EtsFileDto.loadFromJson(it.inputStream())) } - val project = EtsScene(etsFiles) + val project = EtsScene(etsFiles, sdkFiles = emptyList()) val graph = createApplicationGraph(project) val entrypoints = project.projectClasses @@ -298,6 +300,7 @@ class EtsTypeInferenceTest { val file = load("/ts/$name.ts") val project = EtsScene(listOf(file)) val graph = createApplicationGraph(project) + val guesser = TypeGuesser(project) val entrypoints = project.projectClasses .flatMap { it.methods } @@ -309,7 +312,7 @@ class EtsTypeInferenceTest { val manager = TypeInferenceManager(EtsTraits(), graph) val resultWithoutGuessed = manager.analyze(entrypoints) - val resultWithGuessed = resultWithoutGuessed.withGuessedTypes(project) + val resultWithGuessed = resultWithoutGuessed.withGuessedTypes(guesser) assertNotEquals(resultWithoutGuessed.inferredTypes, resultWithGuessed.inferredTypes) @@ -337,7 +340,7 @@ class EtsTypeInferenceTest { @TestFactory fun `type inference on testcases`() = testFactory { val file = load("/ts/testcases.ts") - val project = EtsScene(listOf(file)) + val project = EtsScene(listOf(file), sdkFiles = emptyList()) val graph = createApplicationGraph(project) val allCases = project.projectClasses.filter { it.name.startsWith("Case") } @@ -383,14 +386,7 @@ class EtsTypeInferenceTest { 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]!! + for ((position, expected) in expectedTypeString.sortedByBase()) { val inferred = inferredTypes[position] logger.info { "Inferred type for $position: $inferred" } val passed = inferred.toString() == expected @@ -433,8 +429,7 @@ class EtsTypeInferenceTest { return@testFactory } for (projectName in availableProjectNames) { - // if (projectName != "Launcher") continue - // if (projectName != "Demo_Calc") continue + // if (projectName != "...") continue test("infer types in $projectName") { logger.info { "Loading project: $projectName" } val projectPath = getResourcePath("/projects/$projectName") @@ -452,8 +447,8 @@ class EtsTypeInferenceTest { 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("Loaded project with ${project.projectClasses.size} classes and ${project.projectClasses.sumOf { it.methods.size }} methods") + for (cls in project.projectClasses.sortedBy { it.name }) { appendLine("= ${cls.signature} with ${cls.methods.size} methods:") for (method in cls.methods.sortedBy { it.name }) { appendLine(" - ${method.signature}") @@ -463,7 +458,7 @@ class EtsTypeInferenceTest { } val graph = createApplicationGraph(project) - val entrypoints = project.projectAndSdkClasses + val entrypoints = project.projectClasses .flatMap { it.methods } .filter { it.isPublic } logger.info { "Found ${entrypoints.size} entrypoints" } 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 index 41dd572fe..d94f7c18c 100644 --- 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 @@ -4,6 +4,8 @@ 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.EtsApplicationGraphWithExplicitEntryPoint +import org.usvm.dataflow.ts.infer.TypeGuesser import org.usvm.dataflow.ts.infer.TypeInferenceManager import org.usvm.dataflow.ts.infer.createApplicationGraph import org.usvm.dataflow.ts.util.EtsTraits @@ -28,8 +30,10 @@ class EtsTypeResolverAbcTest { 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 graphAbc = createApplicationGraph(abcScene) as EtsApplicationGraphWithExplicitEntryPoint + val guesser = TypeGuesser(abcScene) val entrypoint = EntryPointsProcessor.extractEntryPoints(abcScene) val allMethods = entrypoint.allMethods.filter { it.isPublic }.filter { it.cfg.stmts.isNotEmpty() } @@ -37,7 +41,7 @@ class EtsTypeResolverAbcTest { val manager = TypeInferenceManager(EtsTraits(), graphAbc) val result = manager .analyze(entrypoint.mainMethods, allMethods) - .withGuessedTypes(abcScene) + .withGuessedTypes(guesser) val sceneStatistics = TypeInferenceStatistics() entrypoint.allMethods 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 index 74b4c87a7..64fec691e 100644 --- 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 @@ -9,11 +9,12 @@ import org.jacodb.ets.utils.loadEtsProjectFromIR import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Test import org.junit.jupiter.api.condition.EnabledIf +import org.usvm.dataflow.ts.getResourcePath import org.usvm.dataflow.ts.graph.EtsApplicationGraph import org.usvm.dataflow.ts.infer.AccessPathBase import org.usvm.dataflow.ts.infer.EntryPointsProcessor -import org.usvm.dataflow.ts.infer.EtsApplicationGraphWithExplicitEntryPoint import org.usvm.dataflow.ts.infer.EtsTypeFact +import org.usvm.dataflow.ts.infer.TypeGuesser import org.usvm.dataflow.ts.infer.TypeInferenceManager import org.usvm.dataflow.ts.infer.TypeInferenceResult import org.usvm.dataflow.ts.infer.createApplicationGraph @@ -22,13 +23,13 @@ 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" @@ -40,49 +41,19 @@ class EtsTypeResolverWithAstTest { } private fun load(name: String): EtsFile { - return loadEtsFileAutoConvert(Paths.get("/ts/$name.ts")) + return loadEtsFileAutoConvert(getResourcePath("/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 astScene = loadEtsProjectAutoConvert(Path(projectAst)) - val graphAbc = createApplicationGraph(abcScene) as EtsApplicationGraphWithExplicitEntryPoint - val graphAst = createApplicationGraph(astScene) as EtsApplicationGraphWithExplicitEntryPoint + val graphAbc = createApplicationGraph(abcScene) + val graphAst = createApplicationGraph(astScene) val entrypoint = EntryPointsProcessor.extractEntryPoints(abcScene) val astMethods = extractAllAstMethods(astScene, abcScene) @@ -90,12 +61,14 @@ class EtsTypeResolverWithAstTest { println(entrypoint.mainMethods.map { it.signature }) println(entrypoint.allMethods.map { it.signature }) + val guesser = TypeGuesser(abcScene) + val manager = TypeInferenceManager(EtsTraits(), graphAbc) val resultBasic = manager.analyze( entrypoints = entrypoint.mainMethods, allMethods = entrypoint.allMethods.filter { it.isPublic }, ) - val result = resultBasic.withGuessedTypes(abcScene) + val result = resultBasic.withGuessedTypes(guesser) val classMatcherStatistics = ClassMatcherStatistics() saveTypeInferenceComparison( @@ -174,6 +147,7 @@ class EtsTypeResolverWithAstTest { val project = EtsScene(listOf(file)) val graph = createApplicationGraph(project) + val guesser = TypeGuesser(project) val entrypoint = project.projectClasses .flatMap { it.methods } @@ -181,7 +155,7 @@ class EtsTypeResolverWithAstTest { val manager = TypeInferenceManager(EtsTraits(), graph) val resultBasic = manager.analyze(listOf(entrypoint)) - val result = resultBasic.withGuessedTypes(project) + val result = resultBasic.withGuessedTypes(guesser) checkAnObjectTypeOfSingleArgument(result.inferredTypes[entrypoint]!!) { typeFact: EtsTypeFact.ObjectEtsTypeFact -> typeFact.cls == null && typeFact.properties.keys.single() == "defaultA" @@ -199,6 +173,7 @@ class EtsTypeResolverWithAstTest { val project = EtsScene(listOf(file)) val graph = createApplicationGraph(project) + val guesser = TypeGuesser(project) val entrypoint = project.projectClasses .flatMap { it.methods } @@ -206,7 +181,7 @@ class EtsTypeResolverWithAstTest { val manager = TypeInferenceManager(EtsTraits(), graph) val resultBasic = manager.analyze(listOf(entrypoint)) - val result = resultBasic.withGuessedTypes(project) + val result = resultBasic.withGuessedTypes(guesser) checkAnObjectTypeOfSingleArgument(result.inferredTypes[entrypoint]!!) { fact: EtsTypeFact.ObjectEtsTypeFact -> fact.cls?.typeName == "FieldContainerToInfer" && fact.properties.isEmpty() @@ -224,6 +199,7 @@ class EtsTypeResolverWithAstTest { val project = EtsScene(listOf(file)) val graph = createApplicationGraph(project) + val guesser = TypeGuesser(project) val entrypoint = project.projectClasses .flatMap { it.methods } @@ -233,7 +209,7 @@ class EtsTypeResolverWithAstTest { val manager = TypeInferenceManager(EtsTraits(), graph) val resultBasic = manager.analyze(listOf(entrypoint)) - val result = resultBasic.withGuessedTypes(project) + val result = resultBasic.withGuessedTypes(guesser) checkAnObjectTypeOfSingleArgument(result.inferredTypes[entrypoint]!!) { fact: EtsTypeFact.ObjectEtsTypeFact -> fact.cls?.typeName == "FieldContainerToInfer" && fact.properties.isEmpty() @@ -250,6 +226,7 @@ class EtsTypeResolverWithAstTest { val project = EtsScene(listOf(file)) val graph = createApplicationGraph(project) + val guesser = TypeGuesser(project) val entrypoint = project.projectClasses .flatMap { it.methods } @@ -257,7 +234,7 @@ class EtsTypeResolverWithAstTest { val manager = TypeInferenceManager(EtsTraits(), graph) val resultBasic = manager.analyze(listOf(entrypoint)) - val result = resultBasic.withGuessedTypes(project) + val result = resultBasic.withGuessedTypes(guesser) checkAnObjectTypeOfSingleArgument(result.inferredTypes[entrypoint]!!) { typeFact: EtsTypeFact.ObjectEtsTypeFact -> typeFact.cls?.typeName == "MethodsContainerToInfer" && typeFact.properties.isEmpty() @@ -275,6 +252,7 @@ class EtsTypeResolverWithAstTest { val project = EtsScene(listOf(file)) val graph = createApplicationGraph(project) + val guesser = TypeGuesser(project) val entrypoint = project.projectClasses .flatMap { it.methods } @@ -282,7 +260,7 @@ class EtsTypeResolverWithAstTest { val manager = TypeInferenceManager(EtsTraits(), graph) val resultBasic = manager.analyze(listOf(entrypoint)) - val result = resultBasic.withGuessedTypes(project) + val result = resultBasic.withGuessedTypes(guesser) checkAnObjectTypeOfSingleArgument(result.inferredTypes[entrypoint]!!) { typeFact: EtsTypeFact.ObjectEtsTypeFact -> typeFact.cls == null && typeFact.properties.keys.single() == "notUniqueFunction" @@ -300,6 +278,7 @@ class EtsTypeResolverWithAstTest { val project = EtsScene(listOf(file)) val graph = createApplicationGraph(project) + val guesser = TypeGuesser(project) val entrypoint = project.projectClasses .flatMap { it.methods } @@ -307,7 +286,7 @@ class EtsTypeResolverWithAstTest { val manager = TypeInferenceManager(EtsTraits(), graph) val resultBasic = manager.analyze(listOf(entrypoint)) - val result = resultBasic.withGuessedTypes(project) + val result = resultBasic.withGuessedTypes(guesser) checkAnObjectTypeOfSingleArgument(result.inferredTypes[entrypoint]!!) { typeFact: EtsTypeFact.ObjectEtsTypeFact -> typeFact.cls?.typeName == "FieldContainerToInfer" && typeFact.properties.isEmpty() diff --git a/usvm-dataflow-ts/src/test/resources/logback.xml b/usvm-dataflow-ts/src/test/resources/logback.xml index 30f884472..12683fcbc 100644 --- a/usvm-dataflow-ts/src/test/resources/logback.xml +++ b/usvm-dataflow-ts/src/test/resources/logback.xml @@ -7,6 +7,7 @@ logs/app.log + false [%level] %replace(%c{0}){'(\$Companion)?\$logger\$1',''} - %msg%n diff --git a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/Runner.kt b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/Runner.kt index 997cc3b05..d413c9ba3 100644 --- a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/Runner.kt +++ b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/Runner.kt @@ -54,9 +54,9 @@ class UniRunner( Statement : CommonInst { private val flowSpace: FlowFunctions = analyzer.flowFunctions private val workList: Channel> = Channel(Channel.UNLIMITED) - internal val pathEdges: MutableSet> = ConcurrentHashMap.newKeySet() - private val reasons = - ConcurrentHashMap, MutableSet>>() + val pathEdges: MutableSet> = ConcurrentHashMap.newKeySet() + // private val reasons = + // ConcurrentHashMap, MutableSet>>() private val summaryEdges: MutableMap, MutableSet>> = hashMapOf() @@ -99,7 +99,7 @@ class UniRunner( "Propagated edge must be in the same unit" } - reasons.computeIfAbsent(edge) { ConcurrentHashMap.newKeySet() }.add(reason) + // reasons.computeIfAbsent(edge) { ConcurrentHashMap.newKeySet() }.add(reason) // Handle only NEW edges: if (pathEdges.add(edge)) { @@ -255,7 +255,8 @@ class UniRunner( } override fun getIfdsResult(): IfdsResult { - val facts = getFinalFacts() - return IfdsResult(pathEdges, facts, reasons, zeroFact) + // val facts = getFinalFacts() + // return IfdsResult(pathEdges, facts, reasons, zeroFact) + error("Not supported") } }