Skip to content

Commit 7d4a524

Browse files
authored
Support lambda expressions (#825)
1 parent fcc4d81 commit 7d4a524

38 files changed

+1555
-106
lines changed

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

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ package org.utbot.framework.plugin.api
1111
import org.utbot.common.isDefaultValue
1212
import org.utbot.common.withToStringThreadLocalReentrancyGuard
1313
import org.utbot.framework.UtSettings
14-
import org.utbot.framework.plugin.api.MockFramework.MOCKITO
1514
import org.utbot.framework.plugin.api.impl.FieldIdReflectionStrategy
1615
import org.utbot.framework.plugin.api.impl.FieldIdSootStrategy
1716
import org.utbot.framework.plugin.api.util.booleanClassId
@@ -512,6 +511,46 @@ data class UtAssembleModel(
512511
}
513512
}
514513

514+
/**
515+
* Model for lambdas.
516+
*
517+
* Lambdas in Java represent the implementation of a single abstract method (SAM) of a functional interface.
518+
* They can be used to create an instance of said functional interface, but **they are not classes**.
519+
* In Java lambdas are compiled into synthetic methods of a class they are declared in.
520+
* Depending on the captured variables, this method will be either static or non-static.
521+
*
522+
* Since lambdas are not classes we cannot use a class loader to get info about them as we can do for other models.
523+
* Hence, the necessity for this specific lambda model that will be processed differently:
524+
* instead of working with a class we will be working with the synthetic method that represents our lambda.
525+
*
526+
* @property id see documentation on [UtReferenceModel.id]
527+
* @property samType the type of functional interface that this lambda will be used for (e.g. [java.util.function.Predicate]).
528+
* `sam` means single abstract method. See https://kotlinlang.org/docs/fun-interfaces.html for more details about it in Kotlin.
529+
* In Java it means the same.
530+
* @property declaringClass a class where the lambda is located.
531+
* We need this class, because the synthetic method the lambda is compiled into will be located in it.
532+
* @property lambdaName the name of synthetic method the lambda is compiled into.
533+
* We need it to find this method in the [declaringClass]
534+
* @property capturedValues models of values captured by lambda.
535+
* Lambdas can capture local variables, method arguments, static and non-static fields.
536+
*/
537+
// TODO: what about support for Kotlin lambdas and function types? See https://github.com/UnitTestBot/UTBotJava/issues/852
538+
class UtLambdaModel(
539+
override val id: Int?,
540+
val samType: ClassId,
541+
val declaringClass: ClassId,
542+
val lambdaName: String,
543+
val capturedValues: MutableList<UtModel> = mutableListOf(),
544+
) : UtReferenceModel(id, samType) {
545+
546+
val lambdaMethodId: MethodId
547+
get() = declaringClass.jClass
548+
.declaredMethods
549+
.singleOrNull { it.name == lambdaName }
550+
?.executableId // synthetic lambda methods should not have overloads, so we always expect there to be only one method with the given name
551+
?: error("More than one method with name $lambdaName found in class: ${declaringClass.canonicalName}")
552+
}
553+
515554
/**
516555
* Model for a step to obtain [UtAssembleModel].
517556
*/
@@ -755,6 +794,12 @@ open class ClassId @JvmOverloads constructor(
755794
open val outerClass: Class<*>?
756795
get() = jClass.enclosingClass
757796

797+
open val superclass: Class<*>?
798+
get() = jClass.superclass
799+
800+
open val interfaces: Array<Class<*>>
801+
get() = jClass.interfaces
802+
758803
/**
759804
* For member classes returns a name including
760805
* enclosing classes' simple names e.g. `A.B`.
@@ -807,7 +852,7 @@ class BuiltinClassId(
807852
elementClassId: ClassId? = null,
808853
override val canonicalName: String,
809854
override val simpleName: String,
810-
// by default we assume that the class is not a member class
855+
// by default, we assume that the class is not a member class
811856
override val simpleNameWithEnclosings: String = simpleName,
812857
override val isNullable: Boolean = false,
813858
override val isPublic: Boolean = true,
@@ -825,6 +870,10 @@ class BuiltinClassId(
825870
override val allMethods: Sequence<MethodId> = emptySequence(),
826871
override val allConstructors: Sequence<ConstructorId> = emptySequence(),
827872
override val outerClass: Class<*>? = null,
873+
// by default, we assume that the class does not have a superclass (other than Object)
874+
override val superclass: Class<*>? = java.lang.Object::class.java,
875+
// by default, we assume that the class does not implement any interfaces
876+
override val interfaces: Array<Class<*>> = emptyArray(),
828877
override val packageName: String =
829878
when (val index = canonicalName.lastIndexOf('.')) {
830879
-1, 0 -> ""
@@ -1010,12 +1059,12 @@ open class MethodId(
10101059
get() = method.modifiers
10111060
}
10121061

1013-
class ConstructorId(
1062+
open class ConstructorId(
10141063
override val classId: ClassId,
10151064
override val parameters: List<ClassId>
10161065
) : ExecutableId() {
1017-
override val name: String = "<init>"
1018-
override val returnType: ClassId = voidClassId
1066+
final override val name: String = "<init>"
1067+
final override val returnType: ClassId = voidClassId
10191068

10201069
override val modifiers: Int
10211070
get() = constructor.modifiers
@@ -1040,6 +1089,20 @@ class BuiltinMethodId(
10401089
(if (isPrivate) Modifier.PRIVATE else 0)
10411090
}
10421091

1092+
class BuiltinConstructorId(
1093+
classId: ClassId,
1094+
parameters: List<ClassId>,
1095+
// by default, we assume that the builtin constructor is public
1096+
isPublic: Boolean = true,
1097+
isProtected: Boolean = false,
1098+
isPrivate: Boolean = false
1099+
) : ConstructorId(classId, parameters) {
1100+
override val modifiers: Int =
1101+
(if (isPublic) Modifier.PUBLIC else 0) or
1102+
(if (isProtected) Modifier.PROTECTED else 0) or
1103+
(if (isPrivate) Modifier.PRIVATE else 0)
1104+
}
1105+
10431106
open class TypeParameters(val parameters: List<ClassId> = emptyList())
10441107

10451108
class WildcardTypeParameter : TypeParameters(emptyList())

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

Lines changed: 11 additions & 5 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
@@ -52,7 +53,6 @@ val ClassId.denotableType: ClassId
5253
}
5354
}
5455

55-
5656
@Suppress("unused")
5757
val ClassId.enclosingClass: ClassId?
5858
get() = jClass.enclosingClass?.id
@@ -118,17 +118,19 @@ infix fun ClassId.isSubtypeOf(type: ClassId): Boolean {
118118
if (left == right) {
119119
return true
120120
}
121-
val leftClass = this.jClass
121+
val leftClass = this
122122
val interfaces = sequence {
123123
var types = listOf(leftClass)
124124
while (types.isNotEmpty()) {
125125
yieldAll(types)
126-
types = types.map { it.interfaces }.flatMap { it.toList() }
126+
types = types
127+
.flatMap { it.interfaces.toList() }
128+
.map { it.id }
127129
}
128130
}
129-
val superclasses = generateSequence(leftClass) { it.superclass }
131+
val superclasses = generateSequence(leftClass) { it.superclass?.id }
130132
val superTypes = interfaces + superclasses
131-
return right in superTypes.map { it.id }
133+
return right in superTypes
132134
}
133135

134136
infix fun ClassId.isNotSubtypeOf(type: ClassId): Boolean = !(this isSubtypeOf type)
@@ -526,6 +528,10 @@ fun builtinMethodId(classId: BuiltinClassId, name: String, returnType: ClassId,
526528
return BuiltinMethodId(classId, name, returnType, arguments.toList())
527529
}
528530

531+
fun builtinConstructorId(classId: BuiltinClassId, vararg arguments: ClassId): BuiltinConstructorId {
532+
return BuiltinConstructorId(classId, arguments.toList())
533+
}
534+
529535
fun builtinStaticMethodId(classId: ClassId, name: String, returnType: ClassId, vararg arguments: ClassId): BuiltinMethodId {
530536
return BuiltinMethodId(classId, name, returnType, arguments.toList(), isStatic = true)
531537
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package org.utbot.examples.lambda
2+
3+
import org.junit.jupiter.api.Test
4+
import org.utbot.framework.plugin.api.CodegenLanguage
5+
import org.utbot.testcheckers.eq
6+
import org.utbot.tests.infrastructure.CodeGeneration
7+
import org.utbot.tests.infrastructure.DoNotCalculate
8+
import org.utbot.tests.infrastructure.UtValueTestCaseChecker
9+
import org.utbot.tests.infrastructure.isException
10+
11+
class CustomPredicateExampleTest : UtValueTestCaseChecker(
12+
testClass = CustomPredicateExample::class,
13+
languagePipelines = listOf(
14+
CodeGenerationLanguageLastStage(CodegenLanguage.JAVA),
15+
// TODO: https://github.com/UnitTestBot/UTBotJava/issues/88 (generics in Kotlin)
16+
// At the moment, when we create an instance of a functional interface via lambda (through reflection),
17+
// we need to do a type cast (e.g. `obj as Predicate<Int>`), but since generics are not supported yet,
18+
// we use a raw type (e.g. `Predicate`) instead (which is not allowed in Kotlin).
19+
CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration)
20+
)
21+
) {
22+
@Test
23+
fun testNoCapturedValuesPredicateCheck() {
24+
checkWithException(
25+
CustomPredicateExample::noCapturedValuesPredicateCheck,
26+
eq(3),
27+
{ predicate, x, r -> !predicate.test(x) && r.getOrNull() == false },
28+
{ predicate, x, r -> predicate.test(x) && r.getOrNull() == true },
29+
{ predicate, _, r -> predicate == null && r.isException<NullPointerException>() },
30+
coverage = DoNotCalculate
31+
)
32+
}
33+
34+
@Test
35+
fun testCapturedLocalVariablePredicateCheck() {
36+
checkWithException(
37+
CustomPredicateExample::capturedLocalVariablePredicateCheck,
38+
eq(3),
39+
{ predicate, x, r -> !predicate.test(x) && r.getOrNull() == false },
40+
{ predicate, x, r -> predicate.test(x) && r.getOrNull() == true },
41+
{ predicate, _, r -> predicate == null && r.isException<NullPointerException>() },
42+
coverage = DoNotCalculate
43+
)
44+
}
45+
46+
@Test
47+
fun testCapturedParameterPredicateCheck() {
48+
checkWithException(
49+
CustomPredicateExample::capturedParameterPredicateCheck,
50+
eq(3),
51+
{ predicate, x, r -> !predicate.test(x) && r.getOrNull() == false },
52+
{ predicate, x, r -> predicate.test(x) && r.getOrNull() == true },
53+
{ predicate, _, r -> predicate == null && r.isException<NullPointerException>() },
54+
coverage = DoNotCalculate
55+
)
56+
}
57+
58+
@Test
59+
fun testCapturedStaticFieldPredicateCheck() {
60+
checkWithException(
61+
CustomPredicateExample::capturedStaticFieldPredicateCheck,
62+
eq(3),
63+
{ predicate, x, r -> !predicate.test(x) && r.getOrNull() == false },
64+
{ predicate, x, r -> predicate.test(x) && r.getOrNull() == true },
65+
{ predicate, _, r -> predicate == null && r.isException<NullPointerException>() },
66+
coverage = DoNotCalculate
67+
)
68+
}
69+
70+
@Test
71+
fun testCapturedNonStaticFieldPredicateCheck() {
72+
checkWithException(
73+
CustomPredicateExample::capturedNonStaticFieldPredicateCheck,
74+
eq(3),
75+
{ predicate, x, r -> !predicate.test(x) && r.getOrNull() == false },
76+
{ predicate, x, r -> predicate.test(x) && r.getOrNull() == true },
77+
{ predicate, _, r -> predicate == null && r.isException<NullPointerException>() },
78+
coverage = DoNotCalculate
79+
)
80+
}
81+
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,11 @@ private val isAnonymousRegex = ".*\\$\\d+$".toRegex()
192192
val SootClass.isAnonymous
193193
get() = name matches isAnonymousRegex
194194

195+
private val isLambdaRegex = ".*(\\$)lambda_.*".toRegex()
196+
197+
val SootClass.isLambda: Boolean
198+
get() = this.isArtificialEntity && this.name matches isLambdaRegex
199+
195200
val Type.numDimensions get() = if (this is ArrayType) numDimensions else 0
196201

197202
/**

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ enum class MockStrategy {
2525

2626
OTHER_CLASSES {
2727
override fun eligibleToMock(classToMock: ClassId, classUnderTest: ClassId): Boolean =
28-
classToMock != classUnderTest && !isSystemPackage(classToMock.packageName)
28+
classToMock != classUnderTest && !isSystemPackage(classToMock.packageName)
2929
};
3030

3131
/**

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import org.utbot.framework.plugin.api.UtExecutionSuccess
4747
import org.utbot.framework.plugin.api.UtExplicitlyThrownException
4848
import org.utbot.framework.plugin.api.UtImplicitlyThrownException
4949
import org.utbot.framework.plugin.api.UtInstrumentation
50+
import org.utbot.framework.plugin.api.UtLambdaModel
5051
import org.utbot.framework.plugin.api.UtModel
5152
import org.utbot.framework.plugin.api.UtNewInstanceInstrumentation
5253
import org.utbot.framework.plugin.api.UtNullModel
@@ -509,6 +510,13 @@ class Resolver(
509510
}
510511

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

514522
if (clazz.isEnum) {
@@ -630,6 +638,41 @@ class Resolver(
630638
return constructedType.classId.jClass
631639
}
632640

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

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

Lines changed: 0 additions & 17 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
@@ -3408,22 +3407,6 @@ class Traverser(
34083407
val returnValue = (symbolicResult as? SymbolicSuccess)?.value as? ObjectValue
34093408
if (returnValue != null) {
34103409
queuedSymbolicStateUpdates += constructConstraintForType(returnValue, returnValue.possibleConcreteTypes).asSoftConstraint()
3411-
3412-
// We only remove anonymous classes if there are regular classes available.
3413-
// If there are no other options, then we do use anonymous classes.
3414-
workaround(REMOVE_ANONYMOUS_CLASSES) {
3415-
val sootClass = returnValue.type.sootClass
3416-
val isInNestedMethod = environment.state.isInNestedMethod()
3417-
3418-
if (!isInNestedMethod && sootClass.isArtificialEntity) {
3419-
return
3420-
}
3421-
3422-
val onlyAnonymousTypesAvailable = returnValue.typeStorage.possibleConcreteTypes.all { (it as? RefType)?.sootClass?.isAnonymous == true }
3423-
if (!isInNestedMethod && sootClass.isAnonymous && !onlyAnonymousTypesAvailable) {
3424-
return
3425-
}
3426-
}
34273410
}
34283411

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

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,13 @@ class TypeResolver(private val typeRegistry: TypeRegistry, private val hierarchy
205205
}
206206
when {
207207
sootClass.isUtMock -> unwantedTypes += it
208-
sootClass.isArtificialEntity -> if (keepArtificialEntities) concreteTypes += it else Unit
208+
sootClass.isArtificialEntity -> {
209+
if (sootClass.isLambda) {
210+
unwantedTypes += it
211+
} else if (keepArtificialEntities) {
212+
concreteTypes += it
213+
}
214+
}
209215
workaround(WorkaroundReason.HACK) { leastCommonSootClass == OBJECT_TYPE && sootClass.isOverridden } -> Unit
210216
else -> concreteTypes += it
211217
}

0 commit comments

Comments
 (0)