Skip to content

Commit 39d0e5f

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 39d0e5f

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

+417
-322
lines changed

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,14 @@ 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,
75+
76+
/**
77+
* Assume that in calls to methods in modification chain always require the same type parameters as class has
78+
*/
79+
MODIFICATION_CHAIN_GENERICS_FROM_CLASS,
7080
}

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

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import org.utbot.framework.plugin.api.util.isPrimitive
2828
import org.utbot.framework.plugin.api.util.jClass
2929
import org.utbot.framework.plugin.api.util.longClassId
3030
import org.utbot.framework.plugin.api.util.method
31+
import org.utbot.framework.plugin.api.util.objectClassId
3132
import org.utbot.framework.plugin.api.util.primitiveTypeJvmNameOrNull
3233
import org.utbot.framework.plugin.api.util.safeJField
3334
import org.utbot.framework.plugin.api.util.shortClassId
@@ -56,6 +57,7 @@ import kotlin.jvm.internal.CallableReference
5657
import kotlin.reflect.KCallable
5758
import kotlin.reflect.KClass
5859
import kotlin.reflect.KFunction
60+
import kotlin.reflect.KType
5961
import kotlin.reflect.full.instanceParameter
6062
import kotlin.reflect.jvm.javaConstructor
6163
import kotlin.reflect.jvm.javaType
@@ -760,8 +762,10 @@ open class ClassId @JvmOverloads constructor(
760762
open val allConstructors: Sequence<ConstructorId>
761763
get() = jClass.declaredConstructors.asSequence().map { it.executableId }
762764

763-
open val typeParameters: TypeParameters
764-
get() = TypeParameters()
765+
/**
766+
* This might cause problems in case of reusing the same [ClassId] for different models.
767+
*/
768+
open val typeParameters: TypeParameters = TypeParameters()
765769

766770
open val outerClass: Class<*>?
767771
get() = jClass.enclosingClass
@@ -904,6 +908,19 @@ open class FieldId(val declaringClass: ClassId, val name: String) {
904908
open val type: ClassId
905909
get() = strategy.type
906910

911+
// required to store and update type parameters
912+
// TODO check if by lazy works correctly in newer Kotlin (https://stackoverflow.com/questions/47638464/kotlin-lazy-var-throwing-classcastexception-kotlin-uninitialized-value)
913+
// val fixedType: ClassId by lazy { type }
914+
private var hiddenFixedType: ClassId? = null
915+
916+
val fixedType: ClassId
917+
get() {
918+
if (hiddenFixedType == null) {
919+
hiddenFixedType = type
920+
}
921+
return hiddenFixedType!!
922+
}
923+
907924
override fun equals(other: Any?): Boolean {
908925
if (this === other) return true
909926
if (javaClass != other?.javaClass) return false
@@ -979,6 +996,11 @@ sealed class ExecutableId : StatementId() {
979996
abstract val isProtected: Boolean
980997
abstract val isPrivate: Boolean
981998

999+
/**
1000+
* This might cause problems in case of reusing the same [ExecutableId] for different models.
1001+
*/
1002+
val typeParameters: TypeParameters = TypeParameters()
1003+
9821004
val isPackagePrivate: Boolean
9831005
get() = !(isPublic || isProtected || isPrivate)
9841006

@@ -1064,9 +1086,31 @@ class BuiltinMethodId(
10641086
override val isPrivate: Boolean = false
10651087
) : MethodId(classId, name, returnType, parameters)
10661088

1067-
open class TypeParameters(val parameters: List<ClassId> = emptyList())
1089+
open class TypeParameters(var parameters: List<ClassId> = emptyList()) {
1090+
fun copyFromClassId(classId: ClassId) {
1091+
parameters = classId.typeParameters.parameters
1092+
}
1093+
1094+
fun fromType(type: KType) {
1095+
if (type.arguments.isEmpty()) return
1096+
1097+
parameters = type.arguments.map {
1098+
when (val clazz = it.type?.classifier) {
1099+
is KClass<*> -> {
1100+
val classId = clazz.id
1101+
it.type?.let { t ->
1102+
classId.typeParameters.fromType(t)
1103+
} ?: error("")
1104+
1105+
classId
1106+
}
1107+
else -> objectClassId
1108+
}
1109+
}
1110+
}
1111+
}
10681112

1069-
class WildcardTypeParameter : TypeParameters(emptyList())
1113+
object WildcardTypeParameter: ClassId("org.utbot.framework.plugin.api.WildcardTypeParameter")
10701114

10711115
interface CodeGenerationSettingItem {
10721116
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

utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgCallableAccessManager.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,9 @@ internal class CgCallableAccessManagerImpl(val context: CgContext) : CgCallableA
101101
override operator fun ConstructorId.invoke(vararg args: Any?): CgExecutableCall {
102102
val resolvedArgs = args.resolve()
103103
val constructorCall = if (this canBeCalledWith resolvedArgs) {
104-
CgConstructorCall(this, resolvedArgs.guardedForDirectCallOf(this))
104+
CgConstructorCall(this, resolvedArgs.guardedForDirectCallOf(this), typeParameters)
105105
} else {
106+
// TODO typeParameters?
106107
callWithReflection(resolvedArgs)
107108
}
108109
newConstructorCall(this)
@@ -112,8 +113,9 @@ internal class CgCallableAccessManagerImpl(val context: CgContext) : CgCallableA
112113
override operator fun CgIncompleteMethodCall.invoke(vararg args: Any?): CgMethodCall {
113114
val resolvedArgs = args.resolve()
114115
val methodCall = if (method.canBeCalledWith(caller, resolvedArgs)) {
115-
CgMethodCall(caller, method, resolvedArgs.guardedForDirectCallOf(method))
116+
CgMethodCall(caller, method, resolvedArgs.guardedForDirectCallOf(method), method.typeParameters)
116117
} else {
118+
// TODO typeParams?
117119
method.callWithReflection(caller, resolvedArgs)
118120
}
119121
newMethodCall(method)

utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgMethodConstructor.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c
218218

219219
// prevValue is nullable if not accessible because of getStaticFieldValue(..) : Any?
220220
val prevValue = newVar(
221-
CgClassId(field.type, isNullable = !fieldAccessible),
221+
CgClassId(field.fixedType, typeParameters = field.fixedType.typeParameters, isNullable = !fieldAccessible),
222222
"prev${field.name.capitalize()}"
223223
) {
224224
if (fieldAccessible) {

utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/util/ConstructorUtils.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import org.utbot.framework.plugin.api.UtModel
4747
import org.utbot.framework.plugin.api.UtNullModel
4848
import org.utbot.framework.plugin.api.UtPrimitiveModel
4949
import org.utbot.framework.plugin.api.WildcardTypeParameter
50+
import org.utbot.framework.plugin.api.TypeParameters
5051
import org.utbot.framework.plugin.api.util.arrayLikeName
5152
import org.utbot.framework.plugin.api.util.builtinStaticMethodId
5253
import org.utbot.framework.plugin.api.util.methodId
@@ -129,7 +130,7 @@ internal data class CgFieldState(val variable: CgVariable, val model: UtModel)
129130

130131
data class ExpressionWithType(val type: ClassId, val expression: CgExpression)
131132

132-
val classCgClassId = CgClassId(Class::class.id, typeParameters = WildcardTypeParameter(), isNullable = false)
133+
val classCgClassId = CgClassId(Class::class.id, TypeParameters(listOf(WildcardTypeParameter)), isNullable = false)
133134

134135
/**
135136
* A [MethodId] to add an item into [ArrayList].
@@ -241,7 +242,7 @@ internal fun CgContextOwner.importIfNeeded(method: MethodId) {
241242
.takeIf { currentExecutable != method }
242243
?.let {
243244
importedStaticMethods += method
244-
collectedImports += StaticImport(method.classId.canonicalName, method.name)
245+
collectedImports += StaticImport(method)
245246
}
246247
}
247248

@@ -394,7 +395,7 @@ internal operator fun UtArrayModel.get(index: Int): UtModel = stores[index] ?: c
394395
internal fun ClassId.utilMethodId(name: String, returnType: ClassId, vararg arguments: ClassId): MethodId =
395396
BuiltinMethodId(this, name, returnType, arguments.toList())
396397

397-
fun ClassId.toImport(): RegularImport = RegularImport(packageName, simpleNameWithEnclosings)
398+
fun ClassId.toImport(): RegularImport = RegularImport(this)
398399

399400
// Immutable collections utils
400401

utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgAbstractRenderer.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,8 @@ internal abstract class CgAbstractRenderer(val context: CgContext, val printer:
692692

693693
override fun toString(): String = printer.toString()
694694

695+
// Reorder imports to keep alphabetic and logical order if their names are post-processed by renderer
696+
protected abstract fun <T: Import> reorderImports(imports: List<T>): List<T>
695697
protected abstract fun renderRegularImport(regularImport: RegularImport)
696698
protected abstract fun renderStaticImport(staticImport: StaticImport)
697699

@@ -718,8 +720,8 @@ internal abstract class CgAbstractRenderer(val context: CgContext, val printer:
718720

719721
protected abstract fun renderExceptionCatchVariable(exception: CgVariable)
720722

721-
protected fun getEscapedImportRendering(import: Import): String =
722-
import.qualifiedName
723+
protected fun getEscapedImportRendering(importName: String): String =
724+
importName
723725
.split(".")
724726
.joinToString(".") { it.escapeNamePossibleKeyword() }
725727

@@ -809,8 +811,8 @@ internal abstract class CgAbstractRenderer(val context: CgContext, val printer:
809811
}
810812

811813
private fun renderClassFileImports(element: CgTestClassFile) {
812-
val regularImports = element.imports.filterIsInstance<RegularImport>()
813-
val staticImports = element.imports.filterIsInstance<StaticImport>()
814+
val regularImports = reorderImports(element.imports.filterIsInstance<RegularImport>())
815+
val staticImports = reorderImports(element.imports.filterIsInstance<StaticImport>())
814816

815817
for (import in regularImports) {
816818
renderRegularImport(import)

utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgJavaRenderer.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.utbot.framework.codegen.model.visitor
22

33
import org.apache.commons.text.StringEscapeUtils
4+
import org.utbot.framework.codegen.Import
45
import org.utbot.framework.codegen.RegularImport
56
import org.utbot.framework.codegen.StaticImport
67
import org.utbot.framework.codegen.model.constructor.context.CgContext
@@ -198,13 +199,15 @@ internal class CgJavaRenderer(context: CgContext, printer: CgPrinter = CgPrinter
198199
renderExecutableCallArguments(element)
199200
}
200201

202+
override fun <T : Import> reorderImports(imports: List<T>): List<T> = imports
203+
201204
override fun renderRegularImport(regularImport: RegularImport) {
202-
val escapedImport = getEscapedImportRendering(regularImport)
205+
val escapedImport = getEscapedImportRendering(regularImport.qualifiedName)
203206
println("import $escapedImport$statementEnding")
204207
}
205208

206209
override fun renderStaticImport(staticImport: StaticImport) {
207-
val escapedImport = getEscapedImportRendering(staticImport)
210+
val escapedImport = getEscapedImportRendering(staticImport.qualifiedName)
208211
println("import static $escapedImport$statementEnding")
209212
}
210213

0 commit comments

Comments
 (0)