Skip to content

Commit a456874

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 a456874

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: 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: 212 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,31 @@ 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.BottomSignature
60+
import sun.reflect.generics.tree.ClassTypeSignature
61+
import sun.reflect.generics.tree.SimpleClassTypeSignature
62+
import sun.reflect.generics.tree.TypeArgument
63+
import sun.reflect.generics.tree.TypeTree
64+
import sun.reflect.generics.tree.TypeVariableSignature
65+
import sun.reflect.generics.tree.Wildcard
5166
import java.io.File
67+
import java.lang.reflect.Constructor
68+
import java.lang.reflect.GenericSignatureFormatError
69+
import java.lang.reflect.Method
5270
import java.lang.reflect.Modifier
5371
import kotlin.contracts.ExperimentalContracts
5472
import kotlin.contracts.contract
5573
import kotlin.jvm.internal.CallableReference
5674
import kotlin.reflect.KCallable
5775
import kotlin.reflect.KClass
5876
import kotlin.reflect.KFunction
77+
import kotlin.reflect.KType
5978
import kotlin.reflect.full.instanceParameter
6079
import kotlin.reflect.jvm.javaConstructor
6180
import kotlin.reflect.jvm.javaType
81+
import kotlin.reflect.jvm.kotlinFunction
6282

6383
const val SYMBOLIC_NULL_ADDR: Int = 0
6484

@@ -264,7 +284,11 @@ data class UtError(
264284
*/
265285
sealed class UtModel(
266286
open val classId: ClassId
267-
)
287+
) {
288+
open fun processGenerics(type: KType) {}
289+
290+
open fun fillGenericsAsObjects() {}
291+
}
268292

269293
/**
270294
* Class representing models for values that might have an address.
@@ -402,6 +426,10 @@ data class UtCompositeModel(
402426
val fields: MutableMap<FieldId, UtModel> = mutableMapOf(),
403427
val mocks: MutableMap<ExecutableId, List<UtModel>> = mutableMapOf(),
404428
) : UtReferenceModel(id, classId) {
429+
override fun processGenerics(type: KType) {
430+
classId.typeParameters.fromType(type)
431+
}
432+
405433
//TODO: SAT-891 - rewrite toString() method
406434
override fun toString() = withToStringThreadLocalReentrancyGuard {
407435
buildString {
@@ -505,6 +533,151 @@ data class UtAssembleModel(
505533
val finalInstantiationModel
506534
get() = instantiationChain.lastOrNull()
507535

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

763-
open val typeParameters: TypeParameters
764-
get() = TypeParameters()
936+
open val typeParameters: TypeParameters = TypeParameters()
765937

766938
open val outerClass: Class<*>?
767939
get() = jClass.enclosingClass
@@ -904,6 +1076,19 @@ open class FieldId(val declaringClass: ClassId, val name: String) {
9041076
open val type: ClassId
9051077
get() = strategy.type
9061078

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

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

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

10711278
interface CodeGenerationSettingItem {
10721279
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)