Skip to content

Commit b2ed51f

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 25707e0 commit b2ed51f

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

+402
-332
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,9 @@ enum class WorkaroundReason {
6767
* requires thorough [investigation](https://github.com/UnitTestBot/UTBotJava/issues/716).
6868
*/
6969
IGNORE_STATICS_FROM_TRUSTED_LIBRARIES,
70+
71+
/**
72+
* Special handling of collection constructors from other collections (for generics processing)
73+
*/
74+
COLLECTION_CONSTRUCTOR_FROM_COLLECTION,
7075
}

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

Lines changed: 209 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,29 @@ 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.ArrayTypeSignature
59+
import sun.reflect.generics.tree.ClassTypeSignature
60+
import sun.reflect.generics.tree.SimpleClassTypeSignature
61+
import sun.reflect.generics.tree.TypeArgument
62+
import sun.reflect.generics.tree.TypeTree
63+
import sun.reflect.generics.tree.TypeVariableSignature
5164
import java.io.File
65+
import java.lang.reflect.Constructor
66+
import java.lang.reflect.GenericSignatureFormatError
67+
import java.lang.reflect.Method
5268
import java.lang.reflect.Modifier
5369
import kotlin.contracts.ExperimentalContracts
5470
import kotlin.contracts.contract
5571
import kotlin.jvm.internal.CallableReference
5672
import kotlin.reflect.KCallable
5773
import kotlin.reflect.KClass
5874
import kotlin.reflect.KFunction
75+
import kotlin.reflect.KType
5976
import kotlin.reflect.full.instanceParameter
6077
import kotlin.reflect.jvm.javaConstructor
6178
import kotlin.reflect.jvm.javaType
79+
import kotlin.reflect.jvm.kotlinFunction
6280

6381
const val SYMBOLIC_NULL_ADDR: Int = 0
6482

@@ -264,7 +282,11 @@ data class UtError(
264282
*/
265283
sealed class UtModel(
266284
open val classId: ClassId
267-
)
285+
) {
286+
open fun processGenerics(type: KType) {}
287+
288+
open fun fillGenericsAsObjects() {}
289+
}
268290

269291
/**
270292
* Class representing models for values that might have an address.
@@ -402,6 +424,10 @@ data class UtCompositeModel(
402424
val fields: MutableMap<FieldId, UtModel> = mutableMapOf(),
403425
val mocks: MutableMap<ExecutableId, List<UtModel>> = mutableMapOf(),
404426
) : UtReferenceModel(id, classId) {
427+
override fun processGenerics(type: KType) {
428+
classId.typeParameters.fromType(type)
429+
}
430+
405431
//TODO: SAT-891 - rewrite toString() method
406432
override fun toString() = withToStringThreadLocalReentrancyGuard {
407433
buildString {
@@ -505,6 +531,150 @@ data class UtAssembleModel(
505531
val finalInstantiationModel
506532
get() = instantiationChain.lastOrNull()
507533

534+
private fun TypeTree.cmp(other: TypeTree): Boolean {
535+
if (this::class != other::class) return false
536+
537+
when (this) {
538+
is TypeVariableSignature -> return identifier == (other as TypeVariableSignature).identifier
539+
is ClassTypeSignature -> {
540+
val otherPath = (other as ClassTypeSignature).path
541+
return path.foldIndexed(true) { i, prev, it ->
542+
prev && (otherPath.getOrNull(i)?.cmp(it) ?: false)
543+
}
544+
}
545+
is SimpleClassTypeSignature -> {
546+
val otherTypeArgs = (other as SimpleClassTypeSignature).typeArguments
547+
return typeArguments.foldIndexed(true) { i, prev, it ->
548+
prev && (otherTypeArgs.getOrNull(i)?.cmp(it) ?: false)
549+
}
550+
}
551+
is ArrayTypeSignature -> return componentType.cmp((other as ArrayTypeSignature).componentType)
552+
// other cases are trivial and handled by class comparison
553+
else -> return true
554+
}
555+
}
556+
557+
override fun processGenerics(type: KType) {
558+
classId.typeParameters.fromType(type)
559+
560+
// TODO might cause problems with type params when program synthesis comes
561+
// assume that last statement is constructor call
562+
instantiationChain.lastOrNull()?.let inst@ { lastStatement ->
563+
(lastStatement as? UtExecutableCallModel)?.let {
564+
when (val executable = it.executable) {
565+
is ConstructorId -> executable.classId.typeParameters.copyFromClassId(classId)
566+
is MethodId -> executable.returnType.typeParameters.copyFromClassId(classId)
567+
}
568+
569+
try {
570+
val function = when (val executable = it.executable.executable) {
571+
is Constructor<*> -> executable.kotlinFunction
572+
is Method -> executable.kotlinFunction
573+
else -> unreachableBranch("this executable does not exist $executable")
574+
}
575+
576+
it.params.mapIndexed { i, param ->
577+
function?.parameters?.getOrNull(i)?.type?.let { it1 -> param.processGenerics(it1) }
578+
}
579+
} catch (e: Error) {
580+
// KotlinReflectionInternalError can't be imported, but it is assumed here
581+
// it can be thrown here because, i.e., Int(Int) constructor does not exist in Kotlin
582+
}
583+
584+
workaround(WorkaroundReason.COLLECTION_CONSTRUCTOR_FROM_COLLECTION) {
585+
val propagateFromReturnTypeToParameter = { id: Int ->
586+
((it.params[id] as? UtAssembleModel)?.instantiationChain?.get(0) as? UtExecutableCallModel)
587+
?.executable?.classId?.typeParameters?.copyFromClassId(classId)
588+
}
589+
590+
when (val executable = it.executable.executable) {
591+
is Constructor<*> -> {
592+
// Can't parse signature here, since constructors return void
593+
// This part only works for cases like Collection<T>(collection: Collection<T>)
594+
if (it.executable is ConstructorId) {
595+
if (it.executable.classId.isSubtypeOf(Collection::class.id)) {
596+
if (it.executable.parameters.size == 1 && it.executable.parameters[0].isSubtypeOf(Collection::class.id)) {
597+
propagateFromReturnTypeToParameter(0)
598+
}
599+
}
600+
}
601+
}
602+
is Method -> {
603+
try {
604+
val f = Method::class.java.getDeclaredField("signature")
605+
f.isAccessible = true
606+
val signature = f.get(executable) as? String ?: return@inst
607+
val parsedSignature = SignatureParser.make().parseMethodSig(signature)
608+
609+
// check if parameter types are equal to return types
610+
// e.g. <T:Ljava/lang/Object;>(Ljava/util/List<TT;>;)Ljava/util/List<TT;>;
611+
parsedSignature.parameterTypes.forEachIndexed { paramId, param ->
612+
if (param::class != parsedSignature.returnType::class) return@forEachIndexed
613+
614+
param as? TypeArgument ?: error("Only TypeArgument is expected")
615+
parsedSignature as? TypeArgument ?: error("Only TypeArgument is expected")
616+
if (param.cmp(parsedSignature)) {
617+
propagateFromReturnTypeToParameter(paramId)
618+
}
619+
}
620+
} catch (e: GenericSignatureFormatError) {
621+
// TODO log
622+
}
623+
}
624+
else -> unreachableBranch("this executable does not exist $executable")
625+
}
626+
}
627+
}
628+
}
629+
630+
for (model in modificationsChain) {
631+
if (model is UtExecutableCallModel) {
632+
model.params.mapIndexed { i, param ->
633+
type.arguments.getOrNull(i)?.type?.let { it1 -> param.processGenerics(it1) }
634+
}
635+
}
636+
}
637+
}
638+
639+
override fun fillGenericsAsObjects() {
640+
// TODO might cause problems with type params when program synthesis comes
641+
// assume that last statement is constructor call
642+
instantiationChain.lastOrNull()?.let { lastStatement ->
643+
(lastStatement as? UtExecutableCallModel)?.let {
644+
try {
645+
val function = when (val executable = it.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+
classId.typeParameters.parameters = List(f.typeParameters.size) { objectClassId }
652+
}
653+
654+
it.params.map { param -> param.fillGenericsAsObjects() }
655+
} catch (e: Error) {
656+
// KotlinReflectionInternalError can't be imported, but it is assumed here
657+
// it can be thrown here because, i.e., Int(Int) constructor does not exist in Kotlin
658+
}
659+
}
660+
}
661+
662+
for (model in modificationsChain) {
663+
if (model is UtExecutableCallModel) {
664+
val function = when (val executable = model.executable.executable) {
665+
is Constructor<*> -> executable.kotlinFunction
666+
is Method -> executable.kotlinFunction
667+
else -> unreachableBranch("this executable does not exist $executable")
668+
}
669+
function?.let { f ->
670+
model.executable.classId.typeParameters.parameters = List(f.typeParameters.size) { objectClassId }
671+
}
672+
673+
model.params.map { it.fillGenericsAsObjects() }
674+
}
675+
}
676+
}
677+
508678
override fun toString() = withToStringThreadLocalReentrancyGuard {
509679
buildString {
510680
append("UtAssembleModel(${classId.simpleName} $modelName) ")
@@ -760,8 +930,7 @@ open class ClassId @JvmOverloads constructor(
760930
open val allConstructors: Sequence<ConstructorId>
761931
get() = jClass.declaredConstructors.asSequence().map { it.executableId }
762932

763-
open val typeParameters: TypeParameters
764-
get() = TypeParameters()
933+
open val typeParameters: TypeParameters = TypeParameters()
765934

766935
open val outerClass: Class<*>?
767936
get() = jClass.enclosingClass
@@ -904,6 +1073,19 @@ open class FieldId(val declaringClass: ClassId, val name: String) {
9041073
open val type: ClassId
9051074
get() = strategy.type
9061075

1076+
// required to store and update type parameters
1077+
// TODO check if by lazy works correctly in newer Kotlin (https://stackoverflow.com/questions/47638464/kotlin-lazy-var-throwing-classcastexception-kotlin-uninitialized-value)
1078+
// val fixedType: ClassId by lazy { type }
1079+
private var hiddenFixedType: ClassId? = null
1080+
1081+
val fixedType: ClassId
1082+
get() {
1083+
if (hiddenFixedType == null) {
1084+
hiddenFixedType = type
1085+
}
1086+
return hiddenFixedType!!
1087+
}
1088+
9071089
override fun equals(other: Any?): Boolean {
9081090
if (this === other) return true
9091091
if (javaClass != other?.javaClass) return false
@@ -1064,9 +1246,31 @@ class BuiltinMethodId(
10641246
override val isPrivate: Boolean = false
10651247
) : MethodId(classId, name, returnType, parameters)
10661248

1067-
open class TypeParameters(val parameters: List<ClassId> = emptyList())
1249+
open class TypeParameters(var parameters: List<ClassId> = emptyList()) {
1250+
fun copyFromClassId(classId: ClassId) {
1251+
parameters = classId.typeParameters.parameters
1252+
}
1253+
1254+
fun fromType(type: KType) {
1255+
if (type.arguments.isEmpty()) return
1256+
1257+
parameters = type.arguments.map {
1258+
when (val clazz = it.type?.classifier) {
1259+
is KClass<*> -> {
1260+
val classId = clazz.id
1261+
it.type?.let { t ->
1262+
classId.typeParameters.fromType(t)
1263+
} ?: error("")
1264+
1265+
classId
1266+
}
1267+
else -> objectClassId
1268+
}
1269+
}
1270+
}
1271+
}
10681272

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

10711275
interface CodeGenerationSettingItem {
10721276
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)