Skip to content

Support lambda expressions #825

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Sep 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ package org.utbot.framework.plugin.api
import org.utbot.common.isDefaultValue
import org.utbot.common.withToStringThreadLocalReentrancyGuard
import org.utbot.framework.UtSettings
import org.utbot.framework.plugin.api.MockFramework.MOCKITO
import org.utbot.framework.plugin.api.impl.FieldIdReflectionStrategy
import org.utbot.framework.plugin.api.impl.FieldIdSootStrategy
import org.utbot.framework.plugin.api.util.booleanClassId
Expand Down Expand Up @@ -512,6 +511,46 @@ data class UtAssembleModel(
}
}

/**
* Model for lambdas.
*
* Lambdas in Java represent the implementation of a single abstract method (SAM) of a functional interface.
* They can be used to create an instance of said functional interface, but **they are not classes**.
* In Java lambdas are compiled into synthetic methods of a class they are declared in.
* Depending on the captured variables, this method will be either static or non-static.
*
* Since lambdas are not classes we cannot use a class loader to get info about them as we can do for other models.
* Hence, the necessity for this specific lambda model that will be processed differently:
* instead of working with a class we will be working with the synthetic method that represents our lambda.
*
* @property id see documentation on [UtReferenceModel.id]
* @property samType the type of functional interface that this lambda will be used for (e.g. [java.util.function.Predicate]).
* `sam` means single abstract method. See https://kotlinlang.org/docs/fun-interfaces.html for more details about it in Kotlin.
* In Java it means the same.
* @property declaringClass a class where the lambda is located.
* We need this class, because the synthetic method the lambda is compiled into will be located in it.
* @property lambdaName the name of synthetic method the lambda is compiled into.
* We need it to find this method in the [declaringClass]
* @property capturedValues models of values captured by lambda.
* Lambdas can capture local variables, method arguments, static and non-static fields.
*/
// TODO: what about support for Kotlin lambdas and function types? See https://github.com/UnitTestBot/UTBotJava/issues/852
class UtLambdaModel(
override val id: Int?,
val samType: ClassId,
val declaringClass: ClassId,
val lambdaName: String,
val capturedValues: MutableList<UtModel> = mutableListOf(),
) : UtReferenceModel(id, samType) {

val lambdaMethodId: MethodId
get() = declaringClass.jClass
.declaredMethods
.singleOrNull { it.name == lambdaName }
?.executableId // synthetic lambda methods should not have overloads, so we always expect there to be only one method with the given name
?: error("More than one method with name $lambdaName found in class: ${declaringClass.canonicalName}")
}

/**
* Model for a step to obtain [UtAssembleModel].
*/
Expand Down Expand Up @@ -755,6 +794,12 @@ open class ClassId @JvmOverloads constructor(
open val outerClass: Class<*>?
get() = jClass.enclosingClass

open val superclass: Class<*>?
get() = jClass.superclass

open val interfaces: Array<Class<*>>
get() = jClass.interfaces

/**
* For member classes returns a name including
* enclosing classes' simple names e.g. `A.B`.
Expand Down Expand Up @@ -807,7 +852,7 @@ class BuiltinClassId(
elementClassId: ClassId? = null,
override val canonicalName: String,
override val simpleName: String,
// by default we assume that the class is not a member class
// by default, we assume that the class is not a member class
override val simpleNameWithEnclosings: String = simpleName,
override val isNullable: Boolean = false,
override val isPublic: Boolean = true,
Expand All @@ -825,6 +870,10 @@ class BuiltinClassId(
override val allMethods: Sequence<MethodId> = emptySequence(),
override val allConstructors: Sequence<ConstructorId> = emptySequence(),
override val outerClass: Class<*>? = null,
// by default, we assume that the class does not have a superclass (other than Object)
override val superclass: Class<*>? = java.lang.Object::class.java,
// by default, we assume that the class does not implement any interfaces
override val interfaces: Array<Class<*>> = emptyArray(),
override val packageName: String =
when (val index = canonicalName.lastIndexOf('.')) {
-1, 0 -> ""
Expand Down Expand Up @@ -1010,12 +1059,12 @@ open class MethodId(
get() = method.modifiers
}

class ConstructorId(
open class ConstructorId(
override val classId: ClassId,
override val parameters: List<ClassId>
) : ExecutableId() {
override val name: String = "<init>"
override val returnType: ClassId = voidClassId
final override val name: String = "<init>"
final override val returnType: ClassId = voidClassId

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

class BuiltinConstructorId(
classId: ClassId,
parameters: List<ClassId>,
// by default, we assume that the builtin constructor is public
isPublic: Boolean = true,
isProtected: Boolean = false,
isPrivate: Boolean = false
) : ConstructorId(classId, parameters) {
override val modifiers: Int =
(if (isPublic) Modifier.PUBLIC else 0) or
(if (isProtected) Modifier.PROTECTED else 0) or
(if (isPrivate) Modifier.PRIVATE else 0)
}

open class TypeParameters(val parameters: List<ClassId> = emptyList())

class WildcardTypeParameter : TypeParameters(emptyList())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.utbot.framework.plugin.api.util

import org.utbot.framework.plugin.api.BuiltinClassId
import org.utbot.framework.plugin.api.BuiltinConstructorId
import org.utbot.framework.plugin.api.BuiltinMethodId
import org.utbot.framework.plugin.api.ClassId
import org.utbot.framework.plugin.api.ConstructorId
Expand Down Expand Up @@ -52,7 +53,6 @@ val ClassId.denotableType: ClassId
}
}


@Suppress("unused")
val ClassId.enclosingClass: ClassId?
get() = jClass.enclosingClass?.id
Expand Down Expand Up @@ -118,17 +118,19 @@ infix fun ClassId.isSubtypeOf(type: ClassId): Boolean {
if (left == right) {
return true
}
val leftClass = this.jClass
val leftClass = this
val interfaces = sequence {
var types = listOf(leftClass)
while (types.isNotEmpty()) {
yieldAll(types)
types = types.map { it.interfaces }.flatMap { it.toList() }
types = types
.flatMap { it.interfaces.toList() }
.map { it.id }
}
}
val superclasses = generateSequence(leftClass) { it.superclass }
val superclasses = generateSequence(leftClass) { it.superclass?.id }
val superTypes = interfaces + superclasses
return right in superTypes.map { it.id }
return right in superTypes
}

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

fun builtinConstructorId(classId: BuiltinClassId, vararg arguments: ClassId): BuiltinConstructorId {
return BuiltinConstructorId(classId, arguments.toList())
}

fun builtinStaticMethodId(classId: ClassId, name: String, returnType: ClassId, vararg arguments: ClassId): BuiltinMethodId {
return BuiltinMethodId(classId, name, returnType, arguments.toList(), isStatic = true)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package org.utbot.examples.lambda

import org.junit.jupiter.api.Test
import org.utbot.framework.plugin.api.CodegenLanguage
import org.utbot.testcheckers.eq
import org.utbot.tests.infrastructure.CodeGeneration
import org.utbot.tests.infrastructure.DoNotCalculate
import org.utbot.tests.infrastructure.UtValueTestCaseChecker
import org.utbot.tests.infrastructure.isException

class CustomPredicateExampleTest : UtValueTestCaseChecker(
testClass = CustomPredicateExample::class,
languagePipelines = listOf(
CodeGenerationLanguageLastStage(CodegenLanguage.JAVA),
// TODO: https://github.com/UnitTestBot/UTBotJava/issues/88 (generics in Kotlin)
// At the moment, when we create an instance of a functional interface via lambda (through reflection),
// we need to do a type cast (e.g. `obj as Predicate<Int>`), but since generics are not supported yet,
// we use a raw type (e.g. `Predicate`) instead (which is not allowed in Kotlin).
CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration)
)
) {
@Test
fun testNoCapturedValuesPredicateCheck() {
checkWithException(
CustomPredicateExample::noCapturedValuesPredicateCheck,
eq(3),
{ predicate, x, r -> !predicate.test(x) && r.getOrNull() == false },
{ predicate, x, r -> predicate.test(x) && r.getOrNull() == true },
{ predicate, _, r -> predicate == null && r.isException<NullPointerException>() },
coverage = DoNotCalculate
)
}

@Test
fun testCapturedLocalVariablePredicateCheck() {
checkWithException(
CustomPredicateExample::capturedLocalVariablePredicateCheck,
eq(3),
{ predicate, x, r -> !predicate.test(x) && r.getOrNull() == false },
{ predicate, x, r -> predicate.test(x) && r.getOrNull() == true },
{ predicate, _, r -> predicate == null && r.isException<NullPointerException>() },
coverage = DoNotCalculate
)
}

@Test
fun testCapturedParameterPredicateCheck() {
checkWithException(
CustomPredicateExample::capturedParameterPredicateCheck,
eq(3),
{ predicate, x, r -> !predicate.test(x) && r.getOrNull() == false },
{ predicate, x, r -> predicate.test(x) && r.getOrNull() == true },
{ predicate, _, r -> predicate == null && r.isException<NullPointerException>() },
coverage = DoNotCalculate
)
}

@Test
fun testCapturedStaticFieldPredicateCheck() {
checkWithException(
CustomPredicateExample::capturedStaticFieldPredicateCheck,
eq(3),
{ predicate, x, r -> !predicate.test(x) && r.getOrNull() == false },
{ predicate, x, r -> predicate.test(x) && r.getOrNull() == true },
{ predicate, _, r -> predicate == null && r.isException<NullPointerException>() },
coverage = DoNotCalculate
)
}

@Test
fun testCapturedNonStaticFieldPredicateCheck() {
checkWithException(
CustomPredicateExample::capturedNonStaticFieldPredicateCheck,
eq(3),
{ predicate, x, r -> !predicate.test(x) && r.getOrNull() == false },
{ predicate, x, r -> predicate.test(x) && r.getOrNull() == true },
{ predicate, _, r -> predicate == null && r.isException<NullPointerException>() },
coverage = DoNotCalculate
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,11 @@ private val isAnonymousRegex = ".*\\$\\d+$".toRegex()
val SootClass.isAnonymous
get() = name matches isAnonymousRegex

private val isLambdaRegex = ".*(\\$)lambda_.*".toRegex()

val SootClass.isLambda: Boolean
get() = this.isArtificialEntity && this.name matches isLambdaRegex

val Type.numDimensions get() = if (this is ArrayType) numDimensions else 0

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ enum class MockStrategy {

OTHER_CLASSES {
override fun eligibleToMock(classToMock: ClassId, classUnderTest: ClassId): Boolean =
classToMock != classUnderTest && !isSystemPackage(classToMock.packageName)
classToMock != classUnderTest && !isSystemPackage(classToMock.packageName)
};

/**
Expand Down
43 changes: 43 additions & 0 deletions utbot-framework/src/main/kotlin/org/utbot/engine/Resolver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import org.utbot.framework.plugin.api.UtExecutionSuccess
import org.utbot.framework.plugin.api.UtExplicitlyThrownException
import org.utbot.framework.plugin.api.UtImplicitlyThrownException
import org.utbot.framework.plugin.api.UtInstrumentation
import org.utbot.framework.plugin.api.UtLambdaModel
import org.utbot.framework.plugin.api.UtModel
import org.utbot.framework.plugin.api.UtNewInstanceInstrumentation
import org.utbot.framework.plugin.api.UtNullModel
Expand Down Expand Up @@ -509,6 +510,13 @@ class Resolver(
}

val sootClass = actualType.sootClass

if (sootClass.isLambda) {
return constructLambda(concreteAddr, sootClass).also { lambda ->
lambda.capturedValues += collectFieldModels(addr, actualType).values
}
}

val clazz = classLoader.loadClass(sootClass.name)

if (clazz.isEnum) {
Expand Down Expand Up @@ -630,6 +638,41 @@ class Resolver(
return constructedType.classId.jClass
}

private fun constructLambda(addr: Address, sootClass: SootClass): UtLambdaModel {
val samType = sootClass.interfaces.singleOrNull()?.id
?: error("Lambda must implement single interface, but ${sootClass.interfaces.size} found for ${sootClass.name}")

val declaringClass = classLoader.loadClass(sootClass.name.substringBefore("\$lambda"))

// Java compiles lambdas into synthetic methods with specific names.
// However, Soot represents lambdas as classes.
// Names of these classes are the modified names of these synthetic methods.
// Specifically, Soot replaces some `$` signs by `_`, adds two underscores and some number
// to the end of the synthetic method name to form the name of a SootClass for lambda.
// For example, given a synthetic method `lambda$foo$1` (lambda declared in method `foo` of class `org.utbot.MyClass`),
// 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).
// Here we obtain the synthetic method name of lambda from the name of its SootClass.
val lambdaName = sootClass.name
.let { name ->
val start = name.indexOf("\$lambda") + 1
val end = name.lastIndexOf("__")
name.substring(start, end)
}
.let {
val builder = StringBuilder(it)
builder[it.indexOfFirst { c -> c == '_' }] = '$'
builder[it.indexOfLast { c -> c == '_' }] = '$'
builder.toString()
}

return UtLambdaModel(
id = addr,
samType = samType,
declaringClass = declaringClass.id,
lambdaName = lambdaName
)
}

private fun constructEnum(addr: Address, type: RefType, clazz: Class<*>): UtEnumConstantModel {
val descriptor = MemoryChunkDescriptor(ENUM_ORDINAL, type, IntType.v())
val array = findArray(descriptor, state)
Expand Down
17 changes: 0 additions & 17 deletions utbot-framework/src/main/kotlin/org/utbot/engine/Traverser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import kotlinx.collections.immutable.toPersistentSet
import org.utbot.common.WorkaroundReason.HACK
import org.utbot.framework.UtSettings.ignoreStaticsFromTrustedLibraries
import org.utbot.common.WorkaroundReason.IGNORE_STATICS_FROM_TRUSTED_LIBRARIES
import org.utbot.common.WorkaroundReason.REMOVE_ANONYMOUS_CLASSES
import org.utbot.common.unreachableBranch
import org.utbot.common.withAccessibility
import org.utbot.common.workaround
Expand Down Expand Up @@ -3408,22 +3407,6 @@ class Traverser(
val returnValue = (symbolicResult as? SymbolicSuccess)?.value as? ObjectValue
if (returnValue != null) {
queuedSymbolicStateUpdates += constructConstraintForType(returnValue, returnValue.possibleConcreteTypes).asSoftConstraint()

// We only remove anonymous classes if there are regular classes available.
// If there are no other options, then we do use anonymous classes.
workaround(REMOVE_ANONYMOUS_CLASSES) {
val sootClass = returnValue.type.sootClass
val isInNestedMethod = environment.state.isInNestedMethod()

if (!isInNestedMethod && sootClass.isArtificialEntity) {
return
}

val onlyAnonymousTypesAvailable = returnValue.typeStorage.possibleConcreteTypes.all { (it as? RefType)?.sootClass?.isAnonymous == true }
if (!isInNestedMethod && sootClass.isAnonymous && !onlyAnonymousTypesAvailable) {
return
}
}
}

//fill arrays with default 0/null and other stuff
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,13 @@ class TypeResolver(private val typeRegistry: TypeRegistry, private val hierarchy
}
when {
sootClass.isUtMock -> unwantedTypes += it
sootClass.isArtificialEntity -> if (keepArtificialEntities) concreteTypes += it else Unit
sootClass.isArtificialEntity -> {
if (sootClass.isLambda) {
unwantedTypes += it
} else if (keepArtificialEntities) {
concreteTypes += it
}
}
workaround(WorkaroundReason.HACK) { leastCommonSootClass == OBJECT_TYPE && sootClass.isOverridden } -> Unit
else -> concreteTypes += it
}
Expand Down
Loading