Skip to content

Commit c08820b

Browse files
committed
Support lambda expressions
1 parent 24fe475 commit c08820b

32 files changed

+1456
-107
lines changed

utbot-framework-api/src/main/kotlin/org/utbot/framework/UtSettings.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ object UtSettings : AbstractSettings(
7373
* Timeout for symbolic execution
7474
*
7575
*/
76-
var utBotGenerationTimeoutInMillis by getLongProperty(60000L)
76+
var utBotGenerationTimeoutInMillis by getLongProperty(600000L)
7777

7878
/**
7979
* Random seed in path selector.
@@ -113,7 +113,7 @@ object UtSettings : AbstractSettings(
113113
*
114114
* False by default, set it to true if debug visualization is needed.
115115
*/
116-
var useDebugVisualization by getBooleanProperty(false)
116+
var useDebugVisualization by getBooleanProperty(true)
117117

118118
/**
119119
* Set the value to true if you want to automatically copy the path of the
@@ -136,7 +136,7 @@ object UtSettings : AbstractSettings(
136136
* @see <a href="CONFLUENCE:UtBot+Expression+Optimizations">
137137
* UtBot Expression Optimizations</a>
138138
*/
139-
var useExpressionSimplification by getBooleanProperty(true)
139+
var useExpressionSimplification by getBooleanProperty(false)
140140

141141
/*
142142
* Activate or deactivate tests on comments && names/displayNames
@@ -188,7 +188,7 @@ object UtSettings : AbstractSettings(
188188
*
189189
* True by default.
190190
*/
191-
var useConcreteExecution by getBooleanProperty(true)
191+
var useConcreteExecution by getBooleanProperty(false)
192192

193193
/**
194194
* Enable check of full coverage for methods with code generations tests.
@@ -300,7 +300,7 @@ object UtSettings : AbstractSettings(
300300
*
301301
* False by default.
302302
*/
303-
var enableUnsatCoreCalculationForHardConstraints by getBooleanProperty(false)
303+
var enableUnsatCoreCalculationForHardConstraints by getBooleanProperty(true)
304304

305305
/**
306306
* Enable it to process states with unknown solver status

utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/Api.kt

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import org.utbot.framework.plugin.api.util.method
3131
import org.utbot.framework.plugin.api.util.primitiveTypeJvmNameOrNull
3232
import org.utbot.framework.plugin.api.util.safeJField
3333
import org.utbot.framework.plugin.api.util.shortClassId
34+
import org.utbot.framework.plugin.api.util.supertypeOfAnonymousClass
3435
import org.utbot.framework.plugin.api.util.toReferenceTypeBytecodeSignature
3536
import org.utbot.framework.plugin.api.util.voidClassId
3637
import soot.ArrayType
@@ -529,6 +530,35 @@ data class UtAssembleModel(
529530
}
530531
}
531532

533+
/**
534+
* Model for lambdas.
535+
*
536+
* Lambdas in Java represent the implementation of a single abstract method (SAM) of a functional interface.
537+
* They can be used to create an instance of said functional interface, but **they are not classes**.
538+
* In Java lambdas are compiled into synthetic methods of a class they are declared in.
539+
* Depending on the captured variables, this method will be either static or non-static.
540+
*
541+
* Since lambdas are not classes we cannot use a class loader to get info about them as we can do for other models.
542+
* Hence, the necessity for this specific lambda model that will be processed differently: instead of working
543+
* with a class we will be working with the synthetic method that represents our lambda.
544+
*/
545+
// TODO: what about support for Kotlin lambdas (they are not exactly the same as Java's due to functional types)
546+
class UtLambdaModel(
547+
override val id: Int?,
548+
val samType: ClassId,
549+
val declaringClass: ClassId,
550+
val lambdaName: String,
551+
val capturedValues: MutableList<UtModel> = mutableListOf(),
552+
) : UtReferenceModel(id, samType) {
553+
554+
val lambdaMethodId: MethodId
555+
get() = declaringClass.jClass
556+
.declaredMethods
557+
.singleOrNull { it.name == lambdaName }
558+
?.executableId // synthetic lambda methods should not have overloads, so we always expect there to be only one method with the given name
559+
?: error("More than one method with name $lambdaName found in class: ${declaringClass.canonicalName}")
560+
}
561+
532562
/**
533563
* Model for a step to obtain [UtAssembleModel].
534564
*/
@@ -697,8 +727,14 @@ open class ClassId @JvmOverloads constructor(
697727
*/
698728
val prettifiedName: String
699729
get() {
700-
val className = jClass.canonicalName ?: name // Explicit jClass reference to get null instead of exception
701-
return className
730+
val baseName = when {
731+
// anonymous classes have empty simpleName and their canonicalName is null,
732+
// so we create a specific name for them
733+
isAnonymous -> "Anonymous${supertypeOfAnonymousClass.prettifiedName}"
734+
// in other cases where canonical name is still null, we use ClassId.name instead
735+
else -> jClass.canonicalName ?: name // Explicit jClass reference to get null instead of exception
736+
}
737+
return baseName
702738
.substringAfterLast(".")
703739
.replace(Regex("[^a-zA-Z0-9]"), "")
704740
.let { if (this.isArray) it + "Array" else it }
@@ -1035,12 +1071,12 @@ open class MethodId(
10351071
get() = Modifier.isPrivate(method.modifiers)
10361072
}
10371073

1038-
class ConstructorId(
1074+
open class ConstructorId(
10391075
override val classId: ClassId,
10401076
override val parameters: List<ClassId>
10411077
) : ExecutableId() {
1042-
override val name: String = "<init>"
1043-
override val returnType: ClassId = voidClassId
1078+
final override val name: String = "<init>"
1079+
final override val returnType: ClassId = voidClassId
10441080

10451081
override val isPublic: Boolean
10461082
get() = Modifier.isPublic(constructor.modifiers)
@@ -1064,6 +1100,15 @@ class BuiltinMethodId(
10641100
override val isPrivate: Boolean = false
10651101
) : MethodId(classId, name, returnType, parameters)
10661102

1103+
class BuiltinConstructorId(
1104+
classId: ClassId,
1105+
parameters: List<ClassId>,
1106+
// by default, we assume that the builtin constructor is public
1107+
override val isPublic: Boolean = true,
1108+
override val isProtected: Boolean = false,
1109+
override val isPrivate: Boolean = false
1110+
) : ConstructorId(classId, parameters)
1111+
10671112
open class TypeParameters(val parameters: List<ClassId> = emptyList())
10681113

10691114
class WildcardTypeParameter : TypeParameters(emptyList())

utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/IdUtil.kt

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.utbot.framework.plugin.api.util
22

33
import org.utbot.framework.plugin.api.BuiltinClassId
4+
import org.utbot.framework.plugin.api.BuiltinConstructorId
45
import org.utbot.framework.plugin.api.BuiltinMethodId
56
import org.utbot.framework.plugin.api.ClassId
67
import org.utbot.framework.plugin.api.ConstructorId
@@ -30,6 +31,46 @@ import kotlin.reflect.jvm.javaMethod
3031

3132
// ClassId utils
3233

34+
/**
35+
* A type is called **non-denotable** if its name cannot be used in the source code.
36+
* For example, anonymous classes **are** non-denotable types.
37+
* On the other hand, [java.lang.Integer], for example, **is** denotable.
38+
*
39+
* This property returns the same type for denotable types,
40+
* and it returns the supertype when given an anonymous class.
41+
*
42+
* **NOTE** that in Java there are non-denotable types other than anonymous classes.
43+
* For example, null-type, intersection types, capture types.
44+
* But [ClassId] cannot contain any of these (at least at the moment).
45+
* So we only consider the case of anonymous classes.
46+
*/
47+
val ClassId.denotableType: ClassId
48+
get() {
49+
return when {
50+
this.isAnonymous -> this.supertypeOfAnonymousClass
51+
else -> this
52+
}
53+
}
54+
55+
private val isLambdaRegex = ".*(\\$)lambda_.*".toRegex()
56+
57+
val ClassId.isLambda: Boolean
58+
get() = name matches isLambdaRegex
59+
60+
val ClassId.isFunctionalInterface: Boolean
61+
get() {
62+
// we cannot access jClass of a builtin type, so we have to return false
63+
if (this is BuiltinClassId) return false
64+
// we cannot access jClass for lambdas, but we know that it is not a functional interface anyway
65+
if (this.isLambda) return false
66+
67+
val clazz = this.jClass
68+
if (!clazz.isInterface) return false
69+
70+
val abstractMethods = clazz.methods.filter { java.lang.reflect.Modifier.isAbstract(it.modifiers) }
71+
return abstractMethods.size == 1
72+
}
73+
3374
@Suppress("unused")
3475
val ClassId.enclosingClass: ClassId?
3576
get() = jClass.enclosingClass?.id
@@ -110,6 +151,34 @@ infix fun ClassId.isSubtypeOf(type: ClassId): Boolean {
110151

111152
infix fun ClassId.isNotSubtypeOf(type: ClassId): Boolean = !(this isSubtypeOf type)
112153

154+
/**
155+
* - Anonymous class that extends a class will have this class as its superclass and no interfaces.
156+
* - Anonymous class that implements an interface, will have the only interface
157+
* and [java.lang.Object] as its superclass
158+
*
159+
* @return [ClassId] of a type that the given anonymous class inherits
160+
*/
161+
val ClassId.supertypeOfAnonymousClass: ClassId
162+
get() {
163+
if (this is BuiltinClassId) error("Cannot obtain info about supertypes of BuiltinClassId $canonicalName")
164+
if (!isAnonymous) error("An anonymous class expected, but got $canonicalName")
165+
166+
val clazz = jClass
167+
val superclass = clazz.superclass.id
168+
val interfaces = clazz.interfaces.map { it.id }
169+
170+
return when {
171+
// anonymous class actually inherits from Object, e.g. Object obj = new Object() { ... };
172+
superclass == objectClassId && interfaces.isEmpty() -> objectClassId
173+
// anonymous class implements some interface
174+
superclass == objectClassId -> {
175+
interfaces.singleOrNull() ?: error("Anonymous class can have no more than one interface")
176+
}
177+
// anonymous class inherits from some class other than java.lang.Object
178+
else -> superclass
179+
}
180+
}
181+
113182
val ClassId.kClass: KClass<*>
114183
get() = jClass.kotlin
115184

@@ -431,6 +500,10 @@ fun builtinMethodId(classId: BuiltinClassId, name: String, returnType: ClassId,
431500
return BuiltinMethodId(classId, name, returnType, arguments.toList())
432501
}
433502

503+
fun builtinConstructorId(classId: BuiltinClassId, vararg arguments: ClassId): BuiltinConstructorId {
504+
return BuiltinConstructorId(classId, arguments.toList())
505+
}
506+
434507
fun builtinStaticMethodId(classId: ClassId, name: String, returnType: ClassId, vararg arguments: ClassId): BuiltinMethodId {
435508
return BuiltinMethodId(classId, name, returnType, arguments.toList(), isStatic = true)
436509
}

utbot-framework/src/main/kotlin/org/utbot/engine/Extensions.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import kotlinx.collections.immutable.persistentHashMapOf
4848
import org.utbot.engine.pc.UtSolverStatusUNDEFINED
4949
import org.utbot.framework.plugin.api.ExecutableId
5050
import org.utbot.framework.plugin.api.util.executableId
51+
import org.utbot.framework.plugin.api.util.isLambda
5152
import soot.ArrayType
5253
import soot.PrimType
5354
import soot.RefLikeType
@@ -205,6 +206,9 @@ private val isAnonymousRegex = ".*\\$\\d+$".toRegex()
205206
val SootClass.isAnonymous
206207
get() = name matches isAnonymousRegex
207208

209+
val SootClass.isLambda: Boolean
210+
get() = this.id.isLambda
211+
208212
val Type.numDimensions get() = if (this is ArrayType) numDimensions else 0
209213

210214
/**

utbot-framework/src/main/kotlin/org/utbot/engine/MockStrategy.kt

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.utbot.engine
22

33
import org.utbot.framework.plugin.api.ClassId
4+
import org.utbot.framework.plugin.api.util.isFunctionalInterface
45

56
/**
67
* Mock strategies.
@@ -17,15 +18,24 @@ enum class MockStrategy {
1718
},
1819

1920
OTHER_PACKAGES {
20-
override fun eligibleToMock(classToMock: ClassId, classUnderTest: ClassId): Boolean =
21-
classToMock != classUnderTest && classToMock.packageName.let {
22-
it != classUnderTest.packageName && !isSystemPackage(it)
23-
}
21+
override fun eligibleToMock(classToMock: ClassId, classUnderTest: ClassId): Boolean {
22+
if (classToMock == classUnderTest) return false
23+
if (classToMock.packageName == classUnderTest.packageName) return false
24+
25+
// we always mock functional interfaces
26+
if (classToMock.isFunctionalInterface) return true
27+
28+
return !isSystemPackage(classToMock.packageName)
29+
}
2430
},
2531

2632
OTHER_CLASSES {
27-
override fun eligibleToMock(classToMock: ClassId, classUnderTest: ClassId): Boolean =
28-
classToMock != classUnderTest && !isSystemPackage(classToMock.packageName)
33+
override fun eligibleToMock(classToMock: ClassId, classUnderTest: ClassId): Boolean {
34+
if (classToMock == classUnderTest) return false
35+
// we always mock functional interfaces
36+
if (classToMock.isFunctionalInterface) return true
37+
return !isSystemPackage(classToMock.packageName)
38+
}
2939
};
3040

3141
/**

utbot-framework/src/main/kotlin/org/utbot/engine/Resolver.kt

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ import kotlin.math.min
7272
import kotlinx.collections.immutable.persistentListOf
7373
import kotlinx.collections.immutable.persistentSetOf
7474
import org.utbot.framework.plugin.api.SYMBOLIC_NULL_ADDR
75+
import org.utbot.framework.plugin.api.UtLambdaModel
7576
import org.utbot.framework.plugin.api.UtSandboxFailure
7677
import soot.ArrayType
7778
import soot.BooleanType
@@ -510,6 +511,13 @@ class Resolver(
510511
}
511512

512513
val sootClass = actualType.sootClass
514+
515+
if (sootClass.isLambda) {
516+
return constructLambda(concreteAddr, sootClass).also { lambda ->
517+
lambda.capturedValues += collectFieldModels(addr, actualType).values
518+
}
519+
}
520+
513521
val clazz = classLoader.loadClass(sootClass.name)
514522

515523
if (clazz.isEnum) {
@@ -631,6 +639,41 @@ class Resolver(
631639
return constructedType.classId.jClass
632640
}
633641

642+
private fun constructLambda(addr: Address, sootClass: SootClass): UtLambdaModel {
643+
val samType = sootClass.interfaces.singleOrNull()?.id
644+
?: error("Lambda must implement single interface, but ${sootClass.interfaces.size} found for ${sootClass.name}")
645+
646+
val declaringClass = classLoader.loadClass(sootClass.name.substringBeforeLast("\$lambda"))
647+
648+
// Java compiles lambdas into synthetic methods with specific names.
649+
// However, Soot represents lambdas as classes.
650+
// Names of these classes are the modified names of these synthetic methods.
651+
// Specifically, Soot replaces some `$` signs by `_`, adds two underscores and some number
652+
// to the end of the synthetic method name to form the name of a SootClass for lambda.
653+
// For example, given a synthetic method `lambda$foo$1` (lambda declared in method `foo` of class `org.utbot.MyClass`),
654+
// Soot will treat this lambda as a class named `org.utbot.MyClass$lambda_foo_1__5` (the last number is probably arbitrary, it's not important).
655+
// Here we obtain the synthetic method name of lambda from the name of its SootClass.
656+
val lambdaName = sootClass.name
657+
.let { name ->
658+
val start = name.lastIndexOf("\$lambda") + 1
659+
val end = name.lastIndexOf("__")
660+
name.substring(start, end)
661+
}
662+
.let {
663+
val builder = StringBuilder(it)
664+
builder[it.indexOfFirst { c -> c == '_' }] = '$'
665+
builder[it.indexOfLast { c -> c == '_' }] = '$'
666+
builder.toString()
667+
}
668+
669+
return UtLambdaModel(
670+
id = addr,
671+
samType = samType,
672+
declaringClass = declaringClass.id,
673+
lambdaName = lambdaName
674+
)
675+
}
676+
634677
private fun constructEnum(addr: Address, type: RefType, clazz: Class<*>): UtEnumConstantModel {
635678
val descriptor = MemoryChunkDescriptor(ENUM_ORDINAL, type, IntType.v())
636679
val array = findArray(descriptor, state)

utbot-framework/src/main/kotlin/org/utbot/engine/Traverser.kt

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import kotlinx.collections.immutable.toPersistentSet
99
import org.utbot.common.WorkaroundReason.HACK
1010
import org.utbot.framework.UtSettings.ignoreStaticsFromTrustedLibraries
1111
import org.utbot.common.WorkaroundReason.IGNORE_STATICS_FROM_TRUSTED_LIBRARIES
12-
import org.utbot.common.WorkaroundReason.REMOVE_ANONYMOUS_CLASSES
1312
import org.utbot.common.unreachableBranch
1413
import org.utbot.common.withAccessibility
1514
import org.utbot.common.workaround
@@ -1831,7 +1830,7 @@ class Traverser(
18311830
return fromMemory
18321831
}
18331832
val addr = fromMemory?.addr ?: findNewAddr()
1834-
val created = createObject(addr, classType, useConcreteType = false, mockInfoGenerator)
1833+
val created = createObject(addr, classType, useConcreteType = true, mockInfoGenerator)
18351834
queuedSymbolicStateUpdates += MemoryUpdate(staticInstanceStorage = persistentHashMapOf(classType.id to created))
18361835
return created
18371836
}
@@ -3402,13 +3401,6 @@ class Traverser(
34023401
val returnValue = (symbolicResult as? SymbolicSuccess)?.value as? ObjectValue
34033402
if (returnValue != null) {
34043403
queuedSymbolicStateUpdates += constructConstraintForType(returnValue, returnValue.possibleConcreteTypes).asSoftConstraint()
3405-
3406-
workaround(REMOVE_ANONYMOUS_CLASSES) {
3407-
val sootClass = returnValue.type.sootClass
3408-
if (!environment.state.isInNestedMethod() && (sootClass.isAnonymous || sootClass.isArtificialEntity)) {
3409-
return
3410-
}
3411-
}
34123404
}
34133405

34143406
//fill arrays with default 0/null and other stuff

0 commit comments

Comments
 (0)