Skip to content

Support anonymous classes #760

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Sep 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import org.utbot.framework.plugin.api.util.method
import org.utbot.framework.plugin.api.util.primitiveTypeJvmNameOrNull
import org.utbot.framework.plugin.api.util.safeJField
import org.utbot.framework.plugin.api.util.shortClassId
import org.utbot.framework.plugin.api.util.supertypeOfAnonymousClass
import org.utbot.framework.plugin.api.util.toReferenceTypeBytecodeSignature
import org.utbot.framework.plugin.api.util.voidClassId
import soot.ArrayType
Expand Down Expand Up @@ -679,8 +680,14 @@ open class ClassId @JvmOverloads constructor(
*/
val prettifiedName: String
get() {
val className = jClass.canonicalName ?: name // Explicit jClass reference to get null instead of exception
return className
val baseName = when {
// anonymous classes have empty simpleName and their canonicalName is null,
// so we create a specific name for them
isAnonymous -> "Anonymous${supertypeOfAnonymousClass.prettifiedName}"
// in other cases where canonical name is still null, we use ClassId.name instead
else -> jClass.canonicalName ?: name // Explicit jClass reference to get null instead of exception
}
return baseName
.substringAfterLast(".")
.replace(Regex("[^a-zA-Z0-9]"), "")
.let { if (this.isArray) it + "Array" else it }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,28 @@ import kotlin.reflect.jvm.javaMethod

// ClassId utils

/**
* A type is called **non-denotable** if its name cannot be used in the source code.
* For example, anonymous classes **are** non-denotable types.
* On the other hand, [java.lang.Integer], for example, **is** denotable.
*
* This property returns the same type for denotable types,
* and it returns the supertype when given an anonymous class.
*
* **NOTE** that in Java there are non-denotable types other than anonymous classes.
* For example, null-type, intersection types, capture types.
* But [ClassId] cannot contain any of these (at least at the moment).
* So we only consider the case of anonymous classes.
*/
val ClassId.denotableType: ClassId
get() {
return when {
this.isAnonymous -> this.supertypeOfAnonymousClass
else -> this
}
}


@Suppress("unused")
val ClassId.enclosingClass: ClassId?
get() = jClass.enclosingClass?.id
Expand Down Expand Up @@ -111,6 +133,37 @@ infix fun ClassId.isSubtypeOf(type: ClassId): Boolean {

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

/**
* - Anonymous class that extends a class will have this class as its superclass and no interfaces.
* - Anonymous class that implements an interface, will have the only interface
* and [java.lang.Object] as its superclass.
*
* @return [ClassId] of a type that the given anonymous class inherits
*/
val ClassId.supertypeOfAnonymousClass: ClassId
get() {
if (this is BuiltinClassId) error("Cannot obtain info about supertypes of BuiltinClassId $canonicalName")
require(isAnonymous) { "An anonymous class expected, but got $canonicalName" }

val clazz = jClass
val superclass = clazz.superclass.id
val interfaces = clazz.interfaces.map { it.id }

return when (superclass) {
objectClassId -> {
// anonymous class actually inherits from Object, e.g. Object obj = new Object() { ... };
if (interfaces.isEmpty()) {
objectClassId
} else {
// anonymous class implements some interface
interfaces.singleOrNull() ?: error("Anonymous class can have no more than one interface")
}
}
// anonymous class inherits from some class other than java.lang.Object
else -> superclass
}
}

val ClassId.kClass: KClass<*>
get() = jClass.kotlin

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package org.utbot.examples.objects

import org.utbot.tests.infrastructure.Full
import org.utbot.tests.infrastructure.UtValueTestCaseChecker
import org.utbot.tests.infrastructure.DoNotCalculate
import org.utbot.tests.infrastructure.isException
import org.junit.jupiter.api.Test
import org.utbot.testcheckers.eq
Expand All @@ -11,37 +11,43 @@ class AnonymousClassesExampleTest : UtValueTestCaseChecker(testClass = Anonymous
fun testAnonymousClassAsParam() {
checkWithException(
AnonymousClassesExample::anonymousClassAsParam,
eq(2),
eq(3),
{ abstractAnonymousClass, r -> abstractAnonymousClass == null && r.isException<NullPointerException>() },
{ abstractAnonymousClass, r -> abstractAnonymousClass != null && r.getOrNull() == 0 },
coverage = DoNotCalculate
{ abstractAnonymousClass, r -> abstractAnonymousClass != null && abstractAnonymousClass::class.java.isAnonymousClass && r.getOrNull() == 42 },
coverage = Full
)
}

@Test
fun testNonFinalAnonymousStatic() {
check(
checkStaticsAndException(
AnonymousClassesExample::nonFinalAnonymousStatic,
eq(0), // we remove all anonymous classes in statics
coverage = DoNotCalculate
eq(3),
{ statics, r -> statics.values.single().value == null && r.isException<NullPointerException>() },
{ _, r -> r.getOrNull() == 0 },
{ _, r -> r.getOrNull() == 42 },
coverage = Full
)
}

@Test
fun testAnonymousClassAsStatic() {
check(
AnonymousClassesExample::anonymousClassAsStatic,
eq(0), // we remove all anonymous classes in statics
coverage = DoNotCalculate
eq(1),
{ r -> r == 42 },
coverage = Full
)
}

@Test
fun testAnonymousClassAsResult() {
check(
AnonymousClassesExample::anonymousClassAsResult,
eq(0), // we remove anonymous classes from the params and the result
coverage = DoNotCalculate
eq(1),
{ abstractAnonymousClass -> abstractAnonymousClass != null && abstractAnonymousClass::class.java.isAnonymousClass },
coverage = Full
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ import org.utbot.framework.util.SootUtils
import org.utbot.framework.util.instanceCounter
import org.utbot.framework.util.modelIdCounter
import kotlin.reflect.full.functions
import org.utbot.examples.assemble.*
import org.utbot.framework.codegen.model.constructor.util.arrayTypeOf

/**
* Test classes must be located in the same folder as [AssembleTestUtils] class.
Expand Down Expand Up @@ -1117,7 +1119,7 @@ class AssembleModelGeneratorTests {
testClassId,
"array" to UtArrayModel(
modelIdCounter.incrementAndGet(),
ClassId("[L${innerClassId.canonicalName}", innerClassId),
arrayTypeOf(innerClassId),
length = 3,
UtNullModel(innerClassId),
mutableMapOf(
Expand Down Expand Up @@ -1187,11 +1189,11 @@ class AssembleModelGeneratorTests {
val testClassId = ArrayOfComplexArrays::class.id
val innerClassId = PrimitiveFields::class.id

val innerArrayClassId = ClassId("[L${innerClassId.canonicalName}", innerClassId)
val innerArrayClassId = arrayTypeOf(innerClassId)

val arrayOfArraysModel = UtArrayModel(
modelIdCounter.incrementAndGet(),
ClassId("[Lorg.utbot.examples.assemble.ComplexArray", testClassId),
arrayTypeOf(testClassId),
length = 2,
UtNullModel(innerArrayClassId),
mutableMapOf(
Expand Down
11 changes: 10 additions & 1 deletion utbot-framework/src/main/kotlin/org/utbot/engine/Traverser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3405,9 +3405,18 @@ class Traverser(
if (returnValue != null) {
queuedSymbolicStateUpdates += constructConstraintForType(returnValue, returnValue.possibleConcreteTypes).asSoftConstraint()

// We only remove anonymous classes if there are regular classes available.
// If there are no other options, then we do use anonymous classes.
workaround(REMOVE_ANONYMOUS_CLASSES) {
val sootClass = returnValue.type.sootClass
if (!environment.state.isInNestedMethod() && (sootClass.isAnonymous || sootClass.isArtificialEntity)) {
val isInNestedMethod = environment.state.isInNestedMethod()

if (!isInNestedMethod && sootClass.isArtificialEntity) {
return
}

val onlyAnonymousTypesAvailable = returnValue.typeStorage.possibleConcreteTypes.all { (it as? RefType)?.sootClass?.isAnonymous == true }
if (!isInNestedMethod && sootClass.isAnonymous && !onlyAnonymousTypesAvailable) {
return
}
}
Expand Down
33 changes: 14 additions & 19 deletions utbot-framework/src/main/kotlin/org/utbot/engine/TypeResolver.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package org.utbot.engine

import org.utbot.common.WorkaroundReason
import org.utbot.common.heuristic
import org.utbot.common.workaround
import org.utbot.engine.pc.UtAddrExpression
import org.utbot.engine.pc.UtBoolExpression
Expand Down Expand Up @@ -187,11 +186,9 @@ class TypeResolver(private val typeRegistry: TypeRegistry, private val hierarchy
}

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

heuristic(WorkaroundReason.REMOVE_ANONYMOUS_CLASSES) {
possibleConcreteTypes.forEach {
val sootClass = (it.baseType as? RefType)?.sootClass ?: run {
// All not RefType should be included in the concreteTypes, e.g., arrays
concreteTypes += it
return@forEach
}
when {
sootClass.isAnonymous || sootClass.isUtMock -> unwantedTypes += it
sootClass.isArtificialEntity -> if (keepArtificialEntities) concreteTypes += it else Unit
workaround(WorkaroundReason.HACK) { leastCommonSootClass == OBJECT_TYPE && sootClass.isOverridden } -> Unit
else -> concreteTypes += it
}
possibleConcreteTypes.forEach {
val sootClass = (it.baseType as? RefType)?.sootClass ?: run {
// All not RefType should be included in the concreteTypes, e.g., arrays
concreteTypes += it
return@forEach
}
when {
sootClass.isUtMock -> unwantedTypes += it
sootClass.isArtificialEntity -> if (keepArtificialEntities) concreteTypes += it else Unit
workaround(WorkaroundReason.HACK) { leastCommonSootClass == OBJECT_TYPE && sootClass.isOverridden } -> Unit
else -> concreteTypes += it
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@ import mu.KotlinLogging
import org.utbot.analytics.EngineAnalyticsContext
import org.utbot.analytics.FeatureProcessor
import org.utbot.analytics.Predictors
import org.utbot.common.WorkaroundReason.REMOVE_ANONYMOUS_CLASSES
import org.utbot.common.bracket
import org.utbot.common.debug
import org.utbot.common.workaround
import org.utbot.engine.MockStrategy.NO_MOCKS
import org.utbot.engine.pc.UtArraySelectExpression
import org.utbot.engine.pc.UtBoolExpression
Expand Down Expand Up @@ -60,6 +58,8 @@ import org.utbot.framework.plugin.api.UtNullModel
import org.utbot.framework.plugin.api.UtOverflowFailure
import org.utbot.framework.plugin.api.UtResult
import org.utbot.framework.plugin.api.UtSymbolicExecution
import org.utbot.framework.util.graph
import org.utbot.framework.plugin.api.util.executableId
import org.utbot.framework.plugin.api.util.isStatic
import org.utbot.framework.plugin.api.onSuccess
import org.utbot.framework.plugin.api.util.description
Expand Down Expand Up @@ -469,15 +469,6 @@ class UtBotSymbolicEngine(
// in case an exception occurred from the concrete execution
concreteExecutionResult ?: return@forEach

workaround(REMOVE_ANONYMOUS_CLASSES) {
concreteExecutionResult.result.onSuccess {
if (it.classId.isAnonymous) {
logger.debug("Anonymous class found as a concrete result, symbolic one will be returned")
return@flow
}
}
}

val coveredInstructions = concreteExecutionResult.coverage.coveredInstructions
if (coveredInstructions.isNotEmpty()) {
val coverageKey = coveredInstructionTracker.add(coveredInstructions)
Expand Down Expand Up @@ -586,16 +577,6 @@ class UtBotSymbolicEngine(
instrumentation
)

workaround(REMOVE_ANONYMOUS_CLASSES) {
concreteExecutionResult.result.onSuccess {
if (it.classId.isAnonymous) {
logger.debug("Anonymous class found as a concrete result, symbolic one will be returned")
emit(symbolicUtExecution)
return
}
}
}

val concolicUtExecution = symbolicUtExecution.copy(
stateAfter = concreteExecutionResult.stateAfter,
result = concreteExecutionResult.result,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,11 @@ class AssembleModelGenerator(private val methodPackageName: String) {
private fun assembleModel(utModel: UtModel): UtModel {
val collectedCallChain = callChain.toMutableList()

// we cannot create an assemble model for an anonymous class instance
if (utModel.classId.isAnonymous) {
return utModel
}

val assembledModel = withCleanState {
try {
when (utModel) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import org.utbot.framework.plugin.api.util.intClassId
import org.utbot.framework.plugin.api.util.isArray
import org.utbot.framework.plugin.api.util.isPrimitiveWrapperOrString
import org.utbot.framework.plugin.api.util.stringClassId
import org.utbot.framework.plugin.api.util.supertypeOfAnonymousClass
import org.utbot.framework.plugin.api.util.wrapperByPrimitive
import java.lang.reflect.Field
import java.lang.reflect.Modifier
Expand Down Expand Up @@ -119,7 +120,9 @@ internal class CgVariableConstructor(val context: CgContext) :
val obj = if (model.isMock) {
mockFrameworkManager.createMockFor(model, baseName)
} else {
newVar(model.classId, baseName) { utilsClassId[createInstance](model.classId.name) }
val modelType = model.classId
val variableType = if (modelType.isAnonymous) modelType.supertypeOfAnonymousClass else modelType
newVar(variableType, baseName) { utilsClassId[createInstance](model.classId.name) }
}

valueByModelId[model.id] = obj
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import org.utbot.framework.plugin.api.util.constructorClassId
import org.utbot.framework.plugin.api.util.fieldClassId
import org.utbot.framework.plugin.api.util.isPrimitive
import org.utbot.framework.plugin.api.util.methodClassId
import org.utbot.framework.plugin.api.util.denotableType
import java.lang.reflect.Constructor
import java.lang.reflect.Method
import kotlin.reflect.KFunction
Expand Down Expand Up @@ -244,9 +245,12 @@ internal class CgStatementConstructorImpl(context: CgContext) :
isMutable: Boolean,
init: () -> CgExpression
): CgVariable {
// it is important that we use a denotable type for declaration, because that allows
// us to avoid creating `Object` variables for instances of anonymous classes,
// where we can instead use the supertype of the anonymous class
val declarationOrVar: Either<CgDeclaration, CgVariable> =
createDeclarationForNewVarAndUpdateVariableScopeOrGetExistingVariable(
baseType,
baseType.denotableType,
model,
baseName,
isMock,
Expand Down Expand Up @@ -558,8 +562,8 @@ internal class CgStatementConstructorImpl(context: CgContext) :
val isGetFieldUtilMethod = (expression is CgMethodCall && expression.executableId.isGetFieldUtilMethod)
val shouldCastBeSafety = expression == nullLiteral() || isGetFieldUtilMethod

type = baseType
expr = typeCast(baseType, expression, shouldCastBeSafety)
type = expr.type
}
expression.type isNotSubtypeOf baseType && !typeAccessible -> {
type = if (expression.type.isArray) objectArrayClassId else objectClassId
Expand Down
Loading