Skip to content

Commit a35be48

Browse files
committed
Generics in Kotlin codegen (#88)
Refactored imports to support kotlin builtins (List, Map, e.t.c). Added generics propagation using callables. Added filling generics with Any? and *. Reworked Kotlin type render. Fixed assemble model generation in concrete. Fixed bug with backticks.
1 parent 2e2d62a commit a35be48

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+365
-332
lines changed

utbot-core/src/main/kotlin/org/utbot/common/HackUtil.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ inline fun <T> heuristic(reason: WorkaroundReason, block: () -> T): T = block()
3737
* - ARRAY_ELEMENT_TYPES_ALWAYS_NULLABLE -- Can't infer nullability for array elements from allocation statement, so make them nullable
3838
* Note:
3939
* - MAKE_SYMBOLIC can lose additional path constraints or branches from function call
40+
* - COLLECTION_CONSTRUCTOR_FROM_COLLECTION -- special handling of collection constructors from other collections (for generics processing)
4041
*/
4142
enum class WorkaroundReason {
4243
HACK,
@@ -47,4 +48,5 @@ enum class WorkaroundReason {
4748
IGNORE_MODEL_TYPES_INEQUALITY,
4849
LONG_CODE_FRAGMENTS,
4950
ARRAY_ELEMENT_TYPES_ALWAYS_NULLABLE,
51+
COLLECTION_CONSTRUCTOR_FROM_COLLECTION,
5052
}

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

Lines changed: 180 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@
88

99
package org.utbot.framework.plugin.api
1010

11+
import org.utbot.common.WorkaroundReason
1112
import org.utbot.common.isDefaultValue
13+
import org.utbot.common.unreachableBranch
1214
import org.utbot.common.withToStringThreadLocalReentrancyGuard
15+
import org.utbot.common.workaround
1316
import org.utbot.framework.UtSettings
1417
import org.utbot.framework.plugin.api.MockFramework.MOCKITO
1518
import org.utbot.framework.plugin.api.impl.FieldIdReflectionStrategy
@@ -19,15 +22,18 @@ import org.utbot.framework.plugin.api.util.byteClassId
1922
import org.utbot.framework.plugin.api.util.charClassId
2023
import org.utbot.framework.plugin.api.util.constructor
2124
import org.utbot.framework.plugin.api.util.doubleClassId
25+
import org.utbot.framework.plugin.api.util.executable
2226
import org.utbot.framework.plugin.api.util.executableId
2327
import org.utbot.framework.plugin.api.util.floatClassId
2428
import org.utbot.framework.plugin.api.util.id
2529
import org.utbot.framework.plugin.api.util.intClassId
2630
import org.utbot.framework.plugin.api.util.isArray
2731
import org.utbot.framework.plugin.api.util.isPrimitive
32+
import org.utbot.framework.plugin.api.util.isSubtypeOf
2833
import org.utbot.framework.plugin.api.util.jClass
2934
import org.utbot.framework.plugin.api.util.longClassId
3035
import org.utbot.framework.plugin.api.util.method
36+
import org.utbot.framework.plugin.api.util.objectClassId
3137
import org.utbot.framework.plugin.api.util.primitiveTypeJvmNameOrNull
3238
import org.utbot.framework.plugin.api.util.safeJField
3339
import org.utbot.framework.plugin.api.util.shortClassId
@@ -48,17 +54,25 @@ import soot.Type
4854
import soot.VoidType
4955
import soot.jimple.JimpleBody
5056
import soot.jimple.Stmt
57+
import sun.reflect.generics.parser.SignatureParser
58+
import sun.reflect.generics.tree.ClassTypeSignature
59+
import sun.reflect.generics.tree.TypeVariableSignature
5160
import java.io.File
61+
import java.lang.reflect.Constructor
62+
import java.lang.reflect.GenericSignatureFormatError
63+
import java.lang.reflect.Method
5264
import java.lang.reflect.Modifier
5365
import kotlin.contracts.ExperimentalContracts
5466
import kotlin.contracts.contract
5567
import kotlin.jvm.internal.CallableReference
5668
import kotlin.reflect.KCallable
5769
import kotlin.reflect.KClass
5870
import kotlin.reflect.KFunction
71+
import kotlin.reflect.KType
5972
import kotlin.reflect.full.instanceParameter
6073
import kotlin.reflect.jvm.javaConstructor
6174
import kotlin.reflect.jvm.javaType
75+
import kotlin.reflect.jvm.kotlinFunction
6276

6377
const val SYMBOLIC_NULL_ADDR: Int = 0
6478

@@ -264,7 +278,11 @@ data class UtError(
264278
*/
265279
sealed class UtModel(
266280
open val classId: ClassId
267-
)
281+
) {
282+
open fun processGenerics(type: KType) {}
283+
284+
open fun fillGenericsAsObjects() {}
285+
}
268286

269287
/**
270288
* Class representing models for values that might have an address.
@@ -402,6 +420,10 @@ data class UtCompositeModel(
402420
val fields: MutableMap<FieldId, UtModel> = mutableMapOf(),
403421
val mocks: MutableMap<ExecutableId, List<UtModel>> = mutableMapOf(),
404422
) : UtReferenceModel(id, classId) {
423+
override fun processGenerics(type: KType) {
424+
classId.typeParameters.fromType(type)
425+
}
426+
405427
//TODO: SAT-891 - rewrite toString() method
406428
override fun toString() = withToStringThreadLocalReentrancyGuard {
407429
buildString {
@@ -505,6 +527,135 @@ data class UtAssembleModel(
505527
val finalInstantiationModel
506528
get() = instantiationChain.lastOrNull()
507529

530+
override fun processGenerics(type: KType) {
531+
classId.typeParameters.fromType(type)
532+
533+
// TODO might cause problems with type params when program synthesis comes
534+
// assume that last statement is constructor call
535+
instantiationChain.lastOrNull()?.let { lastStatement ->
536+
(lastStatement as? UtExecutableCallModel)?.let {
537+
when (val executable = it.executable) {
538+
is ConstructorId -> executable.classId.typeParameters.copyFromClassId(classId)
539+
is MethodId -> executable.returnType.typeParameters.copyFromClassId(classId)
540+
}
541+
542+
try {
543+
val function = when (val executable = it.executable.executable) {
544+
is Constructor<*> -> executable.kotlinFunction
545+
is Method -> executable.kotlinFunction
546+
else -> unreachableBranch("this executable does not exist $executable")
547+
}
548+
549+
it.params.mapIndexed { i, param ->
550+
function?.parameters?.getOrNull(i)?.type?.let { it1 -> param.processGenerics(it1) }
551+
}
552+
} catch (e: Error) {
553+
// KotlinReflectionInternalError can't be imported, but it is assumed here
554+
// it can be thrown here because, i.e., Int(Int) constructor does not exist in Kotlin
555+
}
556+
557+
workaround(WorkaroundReason.COLLECTION_CONSTRUCTOR_FROM_COLLECTION) {
558+
val propagateFromReturnTypeToParameter = {
559+
((it.params[0] as? UtAssembleModel)?.instantiationChain?.get(0) as? UtExecutableCallModel)
560+
?.executable?.classId?.typeParameters?.copyFromClassId(classId)
561+
}
562+
563+
when (val executable = it.executable.executable) {
564+
is Constructor<*> -> {
565+
// Can't parse signature here, since constructors return void
566+
// This part only works for cases like Collection<T>(collection: Collection<T>)
567+
if (it.executable is ConstructorId) {
568+
if (it.executable.classId.isSubtypeOf(Collection::class.id)) {
569+
if (it.executable.parameters.size == 1 && it.executable.parameters[0].isSubtypeOf(Collection::class.id)) {
570+
propagateFromReturnTypeToParameter()
571+
}
572+
}
573+
}
574+
}
575+
is Method -> {
576+
try {
577+
val f = Method::class.java.getDeclaredField("signature")
578+
f.isAccessible = true
579+
val signature = f.get(executable) as String
580+
val parsedSignature = SignatureParser.make().parseMethodSig(signature)
581+
582+
// check if parameter types are equal to return types
583+
// e.g. <T:Ljava/lang/Object;>(Ljava/util/List<TT;>;)Ljava/util/List<TT;>;
584+
parsedSignature.parameterTypes.forEach { param ->
585+
// TODO no idea what the `path` is supposed to mean, all we need is in the first element
586+
val paramTypeParameters =
587+
(param as ClassTypeSignature).path.first().typeArguments
588+
val returnTypeParameters =
589+
(parsedSignature.returnType as ClassTypeSignature).path.first().typeArguments
590+
591+
val genericsAreEqual = paramTypeParameters.foldIndexed(true) { i, prev, it ->
592+
val paramIdentifier = (it as TypeVariableSignature).identifier
593+
val returnTypeParamIdentifier = (returnTypeParameters.getOrNull(i) as TypeVariableSignature).identifier
594+
prev && (paramIdentifier == returnTypeParamIdentifier)
595+
}
596+
597+
if (genericsAreEqual) {
598+
propagateFromReturnTypeToParameter()
599+
}
600+
}
601+
} catch (e: GenericSignatureFormatError) {
602+
// TODO log
603+
}
604+
}
605+
else -> unreachableBranch("this executable does not exist $executable")
606+
}
607+
}
608+
}
609+
}
610+
611+
for (model in modificationsChain) {
612+
if (model is UtExecutableCallModel) {
613+
model.params.mapIndexed { i, param ->
614+
type.arguments.getOrNull(i)?.type?.let { it1 -> param.processGenerics(it1) }
615+
}
616+
}
617+
}
618+
}
619+
620+
override fun fillGenericsAsObjects() {
621+
// TODO might cause problems with type params when program synthesis comes
622+
// assume that last statement is constructor call
623+
instantiationChain.lastOrNull()?.let { lastStatement ->
624+
(lastStatement as? UtExecutableCallModel)?.let {
625+
try {
626+
val function = when (val executable = it.executable.executable) {
627+
is Constructor<*> -> executable.kotlinFunction
628+
is Method -> executable.kotlinFunction
629+
else -> unreachableBranch("this executable does not exist $executable")
630+
}
631+
function?.let { f ->
632+
classId.typeParameters.parameters = List(f.typeParameters.size) { objectClassId }
633+
}
634+
635+
it.params.map { param -> param.fillGenericsAsObjects() }
636+
} catch (e: Error) {
637+
// KotlinReflectionInternalError can't be imported, but it is assumed here
638+
// it can be thrown here because, i.e., Int(Int) constructor does not exist in Kotlin
639+
}
640+
}
641+
}
642+
643+
for (model in modificationsChain) {
644+
if (model is UtExecutableCallModel) {
645+
val function = when (val executable = model.executable.executable) {
646+
is Constructor<*> -> executable.kotlinFunction
647+
is Method -> executable.kotlinFunction
648+
else -> unreachableBranch("this executable does not exist $executable")
649+
}
650+
function?.let { f ->
651+
model.executable.classId.typeParameters.parameters = List(f.typeParameters.size) { objectClassId }
652+
}
653+
654+
model.params.map { it.fillGenericsAsObjects() }
655+
}
656+
}
657+
}
658+
508659
override fun toString() = withToStringThreadLocalReentrancyGuard {
509660
buildString {
510661
append("UtAssembleModel(${classId.simpleName} $modelName) ")
@@ -760,8 +911,7 @@ open class ClassId @JvmOverloads constructor(
760911
open val allConstructors: Sequence<ConstructorId>
761912
get() = jClass.declaredConstructors.asSequence().map { it.executableId }
762913

763-
open val typeParameters: TypeParameters
764-
get() = TypeParameters()
914+
open val typeParameters: TypeParameters = TypeParameters()
765915

766916
open val outerClass: Class<*>?
767917
get() = jClass.enclosingClass
@@ -904,6 +1054,9 @@ open class FieldId(val declaringClass: ClassId, val name: String) {
9041054
open val type: ClassId
9051055
get() = strategy.type
9061056

1057+
// required to store and update type parameters
1058+
val fixedType: ClassId by lazy { type }
1059+
9071060
override fun equals(other: Any?): Boolean {
9081061
if (this === other) return true
9091062
if (javaClass != other?.javaClass) return false
@@ -1064,9 +1217,31 @@ class BuiltinMethodId(
10641217
override val isPrivate: Boolean = false
10651218
) : MethodId(classId, name, returnType, parameters)
10661219

1067-
open class TypeParameters(val parameters: List<ClassId> = emptyList())
1220+
open class TypeParameters(var parameters: List<ClassId> = emptyList()) {
1221+
fun copyFromClassId(classId: ClassId) {
1222+
parameters = classId.typeParameters.parameters
1223+
}
1224+
1225+
fun fromType(type: KType) {
1226+
if (type.arguments.isEmpty()) return
1227+
1228+
parameters = type.arguments.map {
1229+
when (val clazz = it.type?.classifier) {
1230+
is KClass<*> -> {
1231+
val classId = clazz.id
1232+
it.type?.let { t ->
1233+
classId.typeParameters.fromType(t)
1234+
} ?: error("")
1235+
1236+
classId
1237+
}
1238+
else -> objectClassId
1239+
}
1240+
}
1241+
}
1242+
}
10681243

1069-
class WildcardTypeParameter : TypeParameters(emptyList())
1244+
class WildcardTypeParameter: ClassId("org.utbot.framework.plugin.api.WildcardTypeParameter")
10701245

10711246
interface CodeGenerationSettingItem {
10721247
val displayName: String

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,8 @@ private fun pathSelector(graph: InterProceduralUnitGraph, typeRegistry: TypeRegi
146146

147147
class UtBotSymbolicEngine(
148148
private val controller: EngineController,
149-
private val methodUnderTest: UtMethod<*>,
149+
/** methodUnderTest is internal to use in [org.utbot.framework.plugin.api.TestFlow.processGenerics]. **/
150+
internal val methodUnderTest: UtMethod<*>,
150151
classpath: String,
151152
dependencyPaths: String,
152153
mockStrategy: MockStrategy = NO_MOCKS,

utbot-framework/src/main/kotlin/org/utbot/framework/codegen/Domain.kt

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,19 +37,26 @@ data class TestClassFile(val packageName: String, val imports: List<Import>, val
3737

3838
sealed class Import(internal val order: Int) : Comparable<Import> {
3939
abstract val qualifiedName: String
40+
abstract val classId: ClassId
4041

4142
override fun compareTo(other: Import) = importComparator.compare(this, other)
4243
}
4344

4445
private val importComparator = compareBy<Import> { it.order }.thenBy { it.qualifiedName }
4546

46-
data class StaticImport(val qualifierClass: String, val memberName: String) : Import(1) {
47-
override val qualifiedName: String = "$qualifierClass.$memberName"
47+
data class StaticImport(val methodId: MethodId) : Import(1) {
48+
override val qualifiedName: String = "${methodId.classId.canonicalName}.${methodId.name}"
49+
override val classId: ClassId
50+
get() = methodId.classId
4851
}
4952

50-
data class RegularImport(val packageName: String, val className: String) : Import(2) {
51-
override val qualifiedName: String
52-
get() = if (packageName.isNotEmpty()) "$packageName.$className" else className
53+
data class RegularImport(override val classId: ClassId) : Import(2) {
54+
override val qualifiedName: String =
55+
if (classId.packageName.isNotEmpty()) {
56+
"${classId.packageName}.${classId.simpleNameWithEnclosings}"
57+
} else {
58+
classId.simpleNameWithEnclosings
59+
}
5360

5461
// TODO: check without equals() and hashCode()
5562
override fun equals(other: Any?): Boolean {
@@ -58,15 +65,14 @@ data class RegularImport(val packageName: String, val className: String) : Impor
5865

5966
other as RegularImport
6067

61-
if (packageName != other.packageName) return false
62-
if (className != other.className) return false
68+
if (classId != other.classId) return false
6369

6470
return true
6571
}
6672

6773
override fun hashCode(): Int {
68-
var result = packageName.hashCode()
69-
result = 31 * result + className.hashCode()
74+
var result = classId.hashCode()
75+
result = 31 * result + qualifiedName.hashCode()
7076
return result
7177
}
7278
}

utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/name/CgNameGenerator.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,9 @@ internal class CgNameGeneratorImpl(private val context: CgContext)
148148
private fun createNameFromKeyword(baseName: String): String = when(codegenLanguage) {
149149
CodegenLanguage.JAVA -> nextIndexedVarName(baseName)
150150
CodegenLanguage.KOTLIN -> {
151+
val backticksBaseName = "`$baseName`"
151152
// use backticks for first variable with keyword name and use indexed names for all next such variables
152-
if (baseName !in existingVariableNames) "`$baseName`" else nextIndexedVarName(baseName)
153+
if (backticksBaseName !in existingVariableNames) backticksBaseName else nextIndexedVarName(baseName)
153154
}
154155
}
155156

0 commit comments

Comments
 (0)