Skip to content

Commit 75a8ccf

Browse files
authored
Support anonymous classes (#760)
Support anonymous classes
1 parent 4c60394 commit 75a8ccf

File tree

14 files changed

+238
-70
lines changed

14 files changed

+238
-70
lines changed

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import org.utbot.framework.plugin.api.util.method
3131
import org.utbot.framework.plugin.api.util.primitiveTypeJvmNameOrNull
3232
import org.utbot.framework.plugin.api.util.safeJField
3333
import org.utbot.framework.plugin.api.util.shortClassId
34+
import org.utbot.framework.plugin.api.util.supertypeOfAnonymousClass
3435
import org.utbot.framework.plugin.api.util.toReferenceTypeBytecodeSignature
3536
import org.utbot.framework.plugin.api.util.voidClassId
3637
import soot.ArrayType
@@ -679,8 +680,14 @@ open class ClassId @JvmOverloads constructor(
679680
*/
680681
val prettifiedName: String
681682
get() {
682-
val className = jClass.canonicalName ?: name // Explicit jClass reference to get null instead of exception
683-
return className
683+
val baseName = when {
684+
// anonymous classes have empty simpleName and their canonicalName is null,
685+
// so we create a specific name for them
686+
isAnonymous -> "Anonymous${supertypeOfAnonymousClass.prettifiedName}"
687+
// in other cases where canonical name is still null, we use ClassId.name instead
688+
else -> jClass.canonicalName ?: name // Explicit jClass reference to get null instead of exception
689+
}
690+
return baseName
684691
.substringAfterLast(".")
685692
.replace(Regex("[^a-zA-Z0-9]"), "")
686693
.let { if (this.isArray) it + "Array" else it }

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

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,28 @@ import kotlin.reflect.jvm.javaMethod
3131

3232
// ClassId utils
3333

34+
/**
35+
* A type is called **non-denotable** if its name cannot be used in the source code.
36+
* For example, anonymous classes **are** non-denotable types.
37+
* On the other hand, [java.lang.Integer], for example, **is** denotable.
38+
*
39+
* This property returns the same type for denotable types,
40+
* and it returns the supertype when given an anonymous class.
41+
*
42+
* **NOTE** that in Java there are non-denotable types other than anonymous classes.
43+
* For example, null-type, intersection types, capture types.
44+
* But [ClassId] cannot contain any of these (at least at the moment).
45+
* So we only consider the case of anonymous classes.
46+
*/
47+
val ClassId.denotableType: ClassId
48+
get() {
49+
return when {
50+
this.isAnonymous -> this.supertypeOfAnonymousClass
51+
else -> this
52+
}
53+
}
54+
55+
3456
@Suppress("unused")
3557
val ClassId.enclosingClass: ClassId?
3658
get() = jClass.enclosingClass?.id
@@ -111,6 +133,37 @@ infix fun ClassId.isSubtypeOf(type: ClassId): Boolean {
111133

112134
infix fun ClassId.isNotSubtypeOf(type: ClassId): Boolean = !(this isSubtypeOf type)
113135

136+
/**
137+
* - Anonymous class that extends a class will have this class as its superclass and no interfaces.
138+
* - Anonymous class that implements an interface, will have the only interface
139+
* and [java.lang.Object] as its superclass.
140+
*
141+
* @return [ClassId] of a type that the given anonymous class inherits
142+
*/
143+
val ClassId.supertypeOfAnonymousClass: ClassId
144+
get() {
145+
if (this is BuiltinClassId) error("Cannot obtain info about supertypes of BuiltinClassId $canonicalName")
146+
require(isAnonymous) { "An anonymous class expected, but got $canonicalName" }
147+
148+
val clazz = jClass
149+
val superclass = clazz.superclass.id
150+
val interfaces = clazz.interfaces.map { it.id }
151+
152+
return when (superclass) {
153+
objectClassId -> {
154+
// anonymous class actually inherits from Object, e.g. Object obj = new Object() { ... };
155+
if (interfaces.isEmpty()) {
156+
objectClassId
157+
} else {
158+
// anonymous class implements some interface
159+
interfaces.singleOrNull() ?: error("Anonymous class can have no more than one interface")
160+
}
161+
}
162+
// anonymous class inherits from some class other than java.lang.Object
163+
else -> superclass
164+
}
165+
}
166+
114167
val ClassId.kClass: KClass<*>
115168
get() = jClass.kotlin
116169

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package org.utbot.examples.objects
22

3+
import org.utbot.tests.infrastructure.Full
34
import org.utbot.tests.infrastructure.UtValueTestCaseChecker
4-
import org.utbot.tests.infrastructure.DoNotCalculate
55
import org.utbot.tests.infrastructure.isException
66
import org.junit.jupiter.api.Test
77
import org.utbot.testcheckers.eq
@@ -11,37 +11,43 @@ class AnonymousClassesExampleTest : UtValueTestCaseChecker(testClass = Anonymous
1111
fun testAnonymousClassAsParam() {
1212
checkWithException(
1313
AnonymousClassesExample::anonymousClassAsParam,
14-
eq(2),
14+
eq(3),
1515
{ abstractAnonymousClass, r -> abstractAnonymousClass == null && r.isException<NullPointerException>() },
1616
{ abstractAnonymousClass, r -> abstractAnonymousClass != null && r.getOrNull() == 0 },
17-
coverage = DoNotCalculate
17+
{ abstractAnonymousClass, r -> abstractAnonymousClass != null && abstractAnonymousClass::class.java.isAnonymousClass && r.getOrNull() == 42 },
18+
coverage = Full
1819
)
1920
}
2021

2122
@Test
2223
fun testNonFinalAnonymousStatic() {
23-
check(
24+
checkStaticsAndException(
2425
AnonymousClassesExample::nonFinalAnonymousStatic,
25-
eq(0), // we remove all anonymous classes in statics
26-
coverage = DoNotCalculate
26+
eq(3),
27+
{ statics, r -> statics.values.single().value == null && r.isException<NullPointerException>() },
28+
{ _, r -> r.getOrNull() == 0 },
29+
{ _, r -> r.getOrNull() == 42 },
30+
coverage = Full
2731
)
2832
}
2933

3034
@Test
3135
fun testAnonymousClassAsStatic() {
3236
check(
3337
AnonymousClassesExample::anonymousClassAsStatic,
34-
eq(0), // we remove all anonymous classes in statics
35-
coverage = DoNotCalculate
38+
eq(1),
39+
{ r -> r == 42 },
40+
coverage = Full
3641
)
3742
}
3843

3944
@Test
4045
fun testAnonymousClassAsResult() {
4146
check(
4247
AnonymousClassesExample::anonymousClassAsResult,
43-
eq(0), // we remove anonymous classes from the params and the result
44-
coverage = DoNotCalculate
48+
eq(1),
49+
{ abstractAnonymousClass -> abstractAnonymousClass != null && abstractAnonymousClass::class.java.isAnonymousClass },
50+
coverage = Full
4551
)
4652
}
4753
}

utbot-framework-test/src/test/kotlin/org/utbot/framework/assemble/AssembleModelGeneratorTests.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ import org.utbot.framework.util.SootUtils
5858
import org.utbot.framework.util.instanceCounter
5959
import org.utbot.framework.util.modelIdCounter
6060
import kotlin.reflect.full.functions
61+
import org.utbot.examples.assemble.*
62+
import org.utbot.framework.codegen.model.constructor.util.arrayTypeOf
6163

6264
/**
6365
* Test classes must be located in the same folder as [AssembleTestUtils] class.
@@ -1117,7 +1119,7 @@ class AssembleModelGeneratorTests {
11171119
testClassId,
11181120
"array" to UtArrayModel(
11191121
modelIdCounter.incrementAndGet(),
1120-
ClassId("[L${innerClassId.canonicalName}", innerClassId),
1122+
arrayTypeOf(innerClassId),
11211123
length = 3,
11221124
UtNullModel(innerClassId),
11231125
mutableMapOf(
@@ -1187,11 +1189,11 @@ class AssembleModelGeneratorTests {
11871189
val testClassId = ArrayOfComplexArrays::class.id
11881190
val innerClassId = PrimitiveFields::class.id
11891191

1190-
val innerArrayClassId = ClassId("[L${innerClassId.canonicalName}", innerClassId)
1192+
val innerArrayClassId = arrayTypeOf(innerClassId)
11911193

11921194
val arrayOfArraysModel = UtArrayModel(
11931195
modelIdCounter.incrementAndGet(),
1194-
ClassId("[Lorg.utbot.examples.assemble.ComplexArray", testClassId),
1196+
arrayTypeOf(testClassId),
11951197
length = 2,
11961198
UtNullModel(innerArrayClassId),
11971199
mutableMapOf(

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3405,9 +3405,18 @@ class Traverser(
34053405
if (returnValue != null) {
34063406
queuedSymbolicStateUpdates += constructConstraintForType(returnValue, returnValue.possibleConcreteTypes).asSoftConstraint()
34073407

3408+
// We only remove anonymous classes if there are regular classes available.
3409+
// If there are no other options, then we do use anonymous classes.
34083410
workaround(REMOVE_ANONYMOUS_CLASSES) {
34093411
val sootClass = returnValue.type.sootClass
3410-
if (!environment.state.isInNestedMethod() && (sootClass.isAnonymous || sootClass.isArtificialEntity)) {
3412+
val isInNestedMethod = environment.state.isInNestedMethod()
3413+
3414+
if (!isInNestedMethod && sootClass.isArtificialEntity) {
3415+
return
3416+
}
3417+
3418+
val onlyAnonymousTypesAvailable = returnValue.typeStorage.possibleConcreteTypes.all { (it as? RefType)?.sootClass?.isAnonymous == true }
3419+
if (!isInNestedMethod && sootClass.isAnonymous && !onlyAnonymousTypesAvailable) {
34113420
return
34123421
}
34133422
}

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

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package org.utbot.engine
22

33
import org.utbot.common.WorkaroundReason
4-
import org.utbot.common.heuristic
54
import org.utbot.common.workaround
65
import org.utbot.engine.pc.UtAddrExpression
76
import org.utbot.engine.pc.UtBoolExpression
@@ -187,11 +186,9 @@ class TypeResolver(private val typeRegistry: TypeRegistry, private val hierarchy
187186
}
188187

189188
/**
190-
* Remove anonymous and artificial types from the [TypeStorage] if [TypeStorage.possibleConcreteTypes]
191-
* contains non-anonymous and non-artificial types.
192-
* However, if the typeStorage contains only artificial and anonymous types, it becomes much more complicated.
193-
* If leastCommonType of the typeStorage is an artificialEntity, result will contain both artificial and anonymous
194-
* types, otherwise only anonymous types. It is required for some classes, e.g., `forEach__145`.
189+
* Where possible, remove types that are not currently supported by code generation.
190+
* For example, we filter out artificial entities (lambdas are an example of them)
191+
* if the least common type is **not** artificial itself.
195192
*/
196193
private fun TypeStorage.filterInappropriateClassesForCodeGeneration(): TypeStorage {
197194
val unwantedTypes = mutableSetOf<Type>()
@@ -200,19 +197,17 @@ class TypeResolver(private val typeRegistry: TypeRegistry, private val hierarchy
200197
val leastCommonSootClass = (leastCommonType as? RefType)?.sootClass
201198
val keepArtificialEntities = leastCommonSootClass?.isArtificialEntity == true
202199

203-
heuristic(WorkaroundReason.REMOVE_ANONYMOUS_CLASSES) {
204-
possibleConcreteTypes.forEach {
205-
val sootClass = (it.baseType as? RefType)?.sootClass ?: run {
206-
// All not RefType should be included in the concreteTypes, e.g., arrays
207-
concreteTypes += it
208-
return@forEach
209-
}
210-
when {
211-
sootClass.isAnonymous || sootClass.isUtMock -> unwantedTypes += it
212-
sootClass.isArtificialEntity -> if (keepArtificialEntities) concreteTypes += it else Unit
213-
workaround(WorkaroundReason.HACK) { leastCommonSootClass == OBJECT_TYPE && sootClass.isOverridden } -> Unit
214-
else -> concreteTypes += it
215-
}
200+
possibleConcreteTypes.forEach {
201+
val sootClass = (it.baseType as? RefType)?.sootClass ?: run {
202+
// All not RefType should be included in the concreteTypes, e.g., arrays
203+
concreteTypes += it
204+
return@forEach
205+
}
206+
when {
207+
sootClass.isUtMock -> unwantedTypes += it
208+
sootClass.isArtificialEntity -> if (keepArtificialEntities) concreteTypes += it else Unit
209+
workaround(WorkaroundReason.HACK) { leastCommonSootClass == OBJECT_TYPE && sootClass.isOverridden } -> Unit
210+
else -> concreteTypes += it
216211
}
217212
}
218213

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

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,8 @@ import mu.KotlinLogging
44
import org.utbot.analytics.EngineAnalyticsContext
55
import org.utbot.analytics.FeatureProcessor
66
import org.utbot.analytics.Predictors
7-
import org.utbot.common.WorkaroundReason.REMOVE_ANONYMOUS_CLASSES
87
import org.utbot.common.bracket
98
import org.utbot.common.debug
10-
import org.utbot.common.workaround
119
import org.utbot.engine.MockStrategy.NO_MOCKS
1210
import org.utbot.engine.pc.UtArraySelectExpression
1311
import org.utbot.engine.pc.UtBoolExpression
@@ -60,6 +58,8 @@ import org.utbot.framework.plugin.api.UtNullModel
6058
import org.utbot.framework.plugin.api.UtOverflowFailure
6159
import org.utbot.framework.plugin.api.UtResult
6260
import org.utbot.framework.plugin.api.UtSymbolicExecution
61+
import org.utbot.framework.util.graph
62+
import org.utbot.framework.plugin.api.util.executableId
6363
import org.utbot.framework.plugin.api.util.isStatic
6464
import org.utbot.framework.plugin.api.onSuccess
6565
import org.utbot.framework.plugin.api.util.description
@@ -469,15 +469,6 @@ class UtBotSymbolicEngine(
469469
// in case an exception occurred from the concrete execution
470470
concreteExecutionResult ?: return@forEach
471471

472-
workaround(REMOVE_ANONYMOUS_CLASSES) {
473-
concreteExecutionResult.result.onSuccess {
474-
if (it.classId.isAnonymous) {
475-
logger.debug("Anonymous class found as a concrete result, symbolic one will be returned")
476-
return@flow
477-
}
478-
}
479-
}
480-
481472
val coveredInstructions = concreteExecutionResult.coverage.coveredInstructions
482473
if (coveredInstructions.isNotEmpty()) {
483474
val coverageKey = coveredInstructionTracker.add(coveredInstructions)
@@ -586,16 +577,6 @@ class UtBotSymbolicEngine(
586577
instrumentation
587578
)
588579

589-
workaround(REMOVE_ANONYMOUS_CLASSES) {
590-
concreteExecutionResult.result.onSuccess {
591-
if (it.classId.isAnonymous) {
592-
logger.debug("Anonymous class found as a concrete result, symbolic one will be returned")
593-
emit(symbolicUtExecution)
594-
return
595-
}
596-
}
597-
}
598-
599580
val concolicUtExecution = symbolicUtExecution.copy(
600581
stateAfter = concreteExecutionResult.stateAfter,
601582
result = concreteExecutionResult.result,

utbot-framework/src/main/kotlin/org/utbot/framework/assemble/AssembleModelGenerator.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,11 @@ class AssembleModelGenerator(private val methodPackageName: String) {
171171
private fun assembleModel(utModel: UtModel): UtModel {
172172
val collectedCallChain = callChain.toMutableList()
173173

174+
// we cannot create an assemble model for an anonymous class instance
175+
if (utModel.classId.isAnonymous) {
176+
return utModel
177+
}
178+
174179
val assembledModel = withCleanState {
175180
try {
176181
when (utModel) {

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import org.utbot.framework.plugin.api.util.intClassId
5555
import org.utbot.framework.plugin.api.util.isArray
5656
import org.utbot.framework.plugin.api.util.isPrimitiveWrapperOrString
5757
import org.utbot.framework.plugin.api.util.stringClassId
58+
import org.utbot.framework.plugin.api.util.supertypeOfAnonymousClass
5859
import org.utbot.framework.plugin.api.util.wrapperByPrimitive
5960
import java.lang.reflect.Field
6061
import java.lang.reflect.Modifier
@@ -119,7 +120,9 @@ internal class CgVariableConstructor(val context: CgContext) :
119120
val obj = if (model.isMock) {
120121
mockFrameworkManager.createMockFor(model, baseName)
121122
} else {
122-
newVar(model.classId, baseName) { utilsClassId[createInstance](model.classId.name) }
123+
val modelType = model.classId
124+
val variableType = if (modelType.isAnonymous) modelType.supertypeOfAnonymousClass else modelType
125+
newVar(variableType, baseName) { utilsClassId[createInstance](model.classId.name) }
123126
}
124127

125128
valueByModelId[model.id] = obj

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ import org.utbot.framework.plugin.api.util.constructorClassId
7070
import org.utbot.framework.plugin.api.util.fieldClassId
7171
import org.utbot.framework.plugin.api.util.isPrimitive
7272
import org.utbot.framework.plugin.api.util.methodClassId
73+
import org.utbot.framework.plugin.api.util.denotableType
7374
import java.lang.reflect.Constructor
7475
import java.lang.reflect.Method
7576
import kotlin.reflect.KFunction
@@ -244,9 +245,12 @@ internal class CgStatementConstructorImpl(context: CgContext) :
244245
isMutable: Boolean,
245246
init: () -> CgExpression
246247
): CgVariable {
248+
// it is important that we use a denotable type for declaration, because that allows
249+
// us to avoid creating `Object` variables for instances of anonymous classes,
250+
// where we can instead use the supertype of the anonymous class
247251
val declarationOrVar: Either<CgDeclaration, CgVariable> =
248252
createDeclarationForNewVarAndUpdateVariableScopeOrGetExistingVariable(
249-
baseType,
253+
baseType.denotableType,
250254
model,
251255
baseName,
252256
isMock,
@@ -558,8 +562,8 @@ internal class CgStatementConstructorImpl(context: CgContext) :
558562
val isGetFieldUtilMethod = (expression is CgMethodCall && expression.executableId.isGetFieldUtilMethod)
559563
val shouldCastBeSafety = expression == nullLiteral() || isGetFieldUtilMethod
560564

561-
type = baseType
562565
expr = typeCast(baseType, expression, shouldCastBeSafety)
566+
type = expr.type
563567
}
564568
expression.type isNotSubtypeOf baseType && !typeAccessible -> {
565569
type = if (expression.type.isArray) objectArrayClassId else objectClassId

0 commit comments

Comments
 (0)