Skip to content
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

Replace Kotlin getters/setters with property access in codegen #496 #1002

Merged
merged 9 commits into from
Sep 27, 2022
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,12 @@ fun ClassId.defaultValueModel(): UtModel = when (this) {
else -> UtNullModel(this)
}

val ClassId.allDeclaredFieldIds: Sequence<FieldId>
get() =
generateSequence(this.jClass) { it.superclass }
.flatMap { it.declaredFields.asSequence() }
.map { it.fieldId }

// FieldId utils
val FieldId.safeJField: Field?
get() = declaringClass.jClass.declaredFields.firstOrNull { it.name == name }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import org.utbot.framework.codegen.model.tree.CgValue
import org.utbot.framework.codegen.model.tree.CgVariable
import org.utbot.framework.codegen.model.util.at
import org.utbot.framework.codegen.model.util.isAccessibleFrom
import org.utbot.framework.codegen.model.util.canBeReadFrom
import org.utbot.framework.codegen.model.util.nullLiteral
import org.utbot.framework.codegen.model.util.resolve
import org.utbot.framework.plugin.api.BuiltinConstructorId
Expand Down Expand Up @@ -293,7 +294,7 @@ internal class CgCallableAccessManagerImpl(val context: CgContext) : CgCallableA

private fun FieldId.accessSuitability(accessor: CgExpression?): FieldAccessorSuitability {
// Check field accessibility.
if (!isAccessibleFrom(testClassPackageName)) {
if (!canBeReadFrom(context)) {
return ReflectionOnly
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import org.utbot.framework.codegen.model.tree.CgValue
import org.utbot.framework.codegen.model.tree.CgVariable
import org.utbot.framework.codegen.model.util.at
import org.utbot.framework.codegen.model.util.isAccessibleFrom
import org.utbot.framework.codegen.model.util.canBeReadFrom
import org.utbot.framework.codegen.model.util.stringLiteral
import org.utbot.framework.fields.ArrayElementAccess
import org.utbot.framework.fields.FieldAccess
Expand Down Expand Up @@ -182,7 +183,7 @@ internal class CgFieldStateManagerImpl(val context: CgContext)
for ((index, fieldPathElement) in path.withIndex()) {
when (fieldPathElement) {
is FieldAccess -> {
if (!fieldPathElement.field.isAccessibleFrom(testClassPackageName)) {
if (!fieldPathElement.field.canBeReadFrom(context)) {
lastAccessibleIndex = index - 1
break
}
Expand Down Expand Up @@ -246,7 +247,7 @@ internal class CgFieldStateManagerImpl(val context: CgContext)

private fun variableForStaticFieldState(owner: ClassId, fieldPath: FieldPath, customName: String?): CgVariable {
val firstField = (fieldPath.elements.first() as FieldAccess).field
val firstAccessor = if (owner.isAccessibleFrom(testClassPackageName) && firstField.isAccessibleFrom(testClassPackageName)) {
val firstAccessor = if (owner.isAccessibleFrom(testClassPackageName) && firstField.canBeReadFrom(context)) {
owner[firstField]
} else {
// TODO: there is a function getClassOf() for these purposes, but it is not accessible from here for now
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,10 @@ import org.utbot.framework.codegen.model.tree.buildParameterizedTestDataProvider
import org.utbot.framework.codegen.model.tree.buildTestMethod
import org.utbot.framework.codegen.model.tree.convertDocToCg
import org.utbot.framework.codegen.model.tree.toStatement
import org.utbot.framework.codegen.model.util.canBeSetIn
import org.utbot.framework.codegen.model.util.canBeSetFrom
import org.utbot.framework.codegen.model.util.equalTo
import org.utbot.framework.codegen.model.util.inc
import org.utbot.framework.codegen.model.util.isAccessibleFrom
import org.utbot.framework.codegen.model.util.canBeReadFrom
import org.utbot.framework.codegen.model.util.length
import org.utbot.framework.codegen.model.util.lessThan
import org.utbot.framework.codegen.model.util.nullLiteral
Expand Down Expand Up @@ -218,7 +218,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c
val accessibleStaticFields = statics.accessibleFields()
for ((field, _) in accessibleStaticFields) {
val declaringClass = field.declaringClass
val fieldAccessible = field.isAccessibleFrom(testClassPackageName)
val fieldAccessible = field.canBeReadFrom(context)

// prevValue is nullable if not accessible because of getStaticFieldValue(..) : Any?
val prevValue = newVar(
Expand All @@ -243,7 +243,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c
val accessibleStaticFields = statics.accessibleFields()
for ((field, model) in accessibleStaticFields) {
val declaringClass = field.declaringClass
val fieldAccessible = field.canBeSetIn(testClassPackageName)
val fieldAccessible = field.canBeSetFrom(context)

val fieldValue = if (isParametrized) {
currentMethodParameters[CgParameterKind.Statics(model)]
Expand All @@ -264,7 +264,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c

private fun recoverStaticFields() {
for ((field, prevValue) in prevStaticFieldValues.accessibleFields()) {
if (field.canBeSetIn(testClassPackageName)) {
if (field.canBeSetFrom(context)) {
field.declaringClass[field] `=` prevValue
} else {
val declaringClass = getClassOf(field.declaringClass)
Expand Down Expand Up @@ -1039,7 +1039,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c
private fun FieldId.getAccessExpression(variable: CgVariable): CgExpression =
// Can directly access field only if it is declared in variable class (or in its ancestors)
// and is accessible from current package
if (variable.type.hasField(this) && isAccessibleFrom(testClassPackageName)) {
if (variable.type.hasField(this) && canBeReadFrom(context)) {
if (jField.isStatic) CgStaticFieldAccess(this) else CgFieldAccess(variable, this)
} else {
utilsClassId[getFieldValue](variable, this.declaringClass.name, this.name)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import org.utbot.framework.codegen.model.constructor.util.isDefaultValueOf
import org.utbot.framework.codegen.model.constructor.util.isNotDefaultValueOf
import org.utbot.framework.codegen.model.constructor.util.typeCast
import org.utbot.framework.codegen.model.tree.CgAllocateArray
import org.utbot.framework.codegen.model.tree.CgAssignment
import org.utbot.framework.codegen.model.tree.CgDeclaration
import org.utbot.framework.codegen.model.tree.CgEnumConstantAccess
import org.utbot.framework.codegen.model.tree.CgExecutableCall
Expand All @@ -22,18 +23,22 @@ import org.utbot.framework.codegen.model.tree.CgFieldAccess
import org.utbot.framework.codegen.model.tree.CgGetJavaClass
import org.utbot.framework.codegen.model.tree.CgLiteral
import org.utbot.framework.codegen.model.tree.CgMethodCall
import org.utbot.framework.codegen.model.tree.CgStatement
import org.utbot.framework.codegen.model.tree.CgStaticFieldAccess
import org.utbot.framework.codegen.model.tree.CgValue
import org.utbot.framework.codegen.model.tree.CgVariable
import org.utbot.framework.codegen.model.util.at
import org.utbot.framework.codegen.model.util.canBeSetIn
import org.utbot.framework.codegen.model.util.canBeSetFrom
import org.utbot.framework.codegen.model.util.fieldThatIsGotWith
import org.utbot.framework.codegen.model.util.fieldThatIsSetWith
import org.utbot.framework.codegen.model.util.inc
import org.utbot.framework.codegen.model.util.isAccessibleFrom
import org.utbot.framework.codegen.model.util.lessThan
import org.utbot.framework.codegen.model.util.nullLiteral
import org.utbot.framework.codegen.model.util.resolve
import org.utbot.framework.plugin.api.BuiltinClassId
import org.utbot.framework.plugin.api.ClassId
import org.utbot.framework.plugin.api.CodegenLanguage
import org.utbot.framework.plugin.api.ConstructorId
import org.utbot.framework.plugin.api.MethodId
import org.utbot.framework.plugin.api.UtArrayModel
Expand Down Expand Up @@ -188,7 +193,7 @@ internal class CgVariableConstructor(val context: CgContext) :
// byteBuffer is field of type ByteBuffer and upper line is incorrect
val canFieldBeDirectlySetByVariableAndFieldTypeRestrictions =
fieldFromVariableSpecifiedType != null && fieldFromVariableSpecifiedType.type.id == variableForField.type
if (canFieldBeDirectlySetByVariableAndFieldTypeRestrictions && fieldId.canBeSetIn(testClassPackageName)) {
if (canFieldBeDirectlySetByVariableAndFieldTypeRestrictions && fieldId.canBeSetFrom(context)) {
// TODO: check if it is correct to use declaringClass of a field here
val fieldAccess = if (field.isStatic) CgStaticFieldAccess(fieldId) else CgFieldAccess(obj, fieldId)
fieldAccess `=` variableForField
Expand All @@ -212,7 +217,12 @@ internal class CgVariableConstructor(val context: CgContext) :
instance[statementModel.fieldId] `=` declareOrGet(statementModel.fieldModel)
}
is UtExecutableCallModel -> {
+createCgExecutableCallFromUtExecutableCall(statementModel)
val call = createCgExecutableCallFromUtExecutableCall(statementModel)
val equivalentFieldAccess = replaceCgExecutableCallWithFieldAccessIfNeeded(call)
if (equivalentFieldAccess != null)
+equivalentFieldAccess
else
+call
}
}
}
Expand Down Expand Up @@ -261,6 +271,37 @@ internal class CgVariableConstructor(val context: CgContext) :
return cgCall
}

/**
* If executable is getter/setter that should be syntactically replaced with field access
* (e.g., getter/setter generated by Kotlin in Kotlin code), this method returns [CgStatement]
* with which [call] should be replaced.
*
* Otherwise, returns null.
*/
private fun replaceCgExecutableCallWithFieldAccessIfNeeded(call: CgExecutableCall): CgStatement? {
when (context.codegenLanguage) {
CodegenLanguage.JAVA -> return null
CodegenLanguage.KOTLIN -> {
if (call !is CgMethodCall)
return null

val caller = call.caller ?: return null

caller.type.fieldThatIsSetWith(call.executableId)?.let {
return CgAssignment(caller[it], call.arguments.single())
}
caller.type.fieldThatIsGotWith(call.executableId)?.let {
require(call.arguments.isEmpty()) {
"Method $call was detected as getter for $it, but its arguments list isn't empty"
}
return caller[it]
}

return null
}
}
}

/**
* Makes a replacement of constructor call to instantiate a primitive wrapper
* with direct setting of the value. The reason is that in Kotlin constructors
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import org.utbot.framework.codegen.model.constructor.builtin.anyLong
import org.utbot.framework.codegen.model.constructor.builtin.anyOfClass
import org.utbot.framework.codegen.model.constructor.builtin.anyShort
import org.utbot.framework.codegen.model.constructor.builtin.argumentMatchersClassId
import org.utbot.framework.codegen.model.constructor.builtin.forName
import org.utbot.framework.codegen.model.constructor.builtin.mockMethodId
import org.utbot.framework.codegen.model.constructor.builtin.mockedConstructionContextClassId
import org.utbot.framework.codegen.model.constructor.builtin.mockitoClassId
Expand All @@ -24,7 +23,6 @@ import org.utbot.framework.codegen.model.constructor.context.CgContextOwner
import org.utbot.framework.codegen.model.constructor.util.CgComponents
import org.utbot.framework.codegen.model.constructor.util.CgStatementConstructor
import org.utbot.framework.codegen.model.constructor.util.CgStatementConstructorImpl
import org.utbot.framework.codegen.model.constructor.util.classCgClassId
import org.utbot.framework.codegen.model.constructor.util.hasAmbiguousOverloadsOf
import org.utbot.framework.codegen.model.tree.CgAnonymousFunction
import org.utbot.framework.codegen.model.tree.CgAssignment
Expand All @@ -33,7 +31,6 @@ import org.utbot.framework.codegen.model.tree.CgConstructorCall
import org.utbot.framework.codegen.model.tree.CgDeclaration
import org.utbot.framework.codegen.model.tree.CgExecutableCall
import org.utbot.framework.codegen.model.tree.CgExpression
import org.utbot.framework.codegen.model.tree.CgGetJavaClass
import org.utbot.framework.codegen.model.tree.CgLiteral
import org.utbot.framework.codegen.model.tree.CgMethodCall
import org.utbot.framework.codegen.model.tree.CgParameterDeclaration
Expand Down Expand Up @@ -63,7 +60,6 @@ import org.utbot.framework.plugin.api.util.byteClassId
import org.utbot.framework.plugin.api.util.charClassId
import org.utbot.framework.plugin.api.util.doubleClassId
import org.utbot.framework.plugin.api.util.floatClassId
import org.utbot.framework.plugin.api.util.id
import org.utbot.framework.plugin.api.util.intClassId
import org.utbot.framework.plugin.api.util.longClassId
import org.utbot.framework.plugin.api.util.shortClassId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import org.utbot.framework.codegen.StaticImport
import org.utbot.framework.codegen.model.constructor.context.CgContextOwner
import org.utbot.framework.codegen.model.tree.CgClassId
import org.utbot.framework.codegen.model.tree.CgExpression
import org.utbot.framework.codegen.model.tree.CgGetClass
import org.utbot.framework.codegen.model.tree.CgGetJavaClass
import org.utbot.framework.codegen.model.tree.CgTypeCast
import org.utbot.framework.codegen.model.tree.CgValue
import org.utbot.framework.codegen.model.tree.CgVariable
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package org.utbot.framework.codegen.model.util

import org.utbot.framework.plugin.api.ClassId
import org.utbot.framework.plugin.api.FieldId
import org.utbot.framework.plugin.api.MethodId
import org.utbot.framework.plugin.api.util.allDeclaredFieldIds
import org.utbot.framework.plugin.api.util.id
import org.utbot.framework.plugin.api.util.isArray

Expand Down Expand Up @@ -28,4 +31,16 @@ infix fun ClassId.isAccessibleFrom(packageName: String): Boolean {
} else {
isPublic || (this.packageName == packageName && (isPackagePrivate || isProtected))
}
}
}

/**
* Returns field of [this], such that [methodId] is a getter for it (or null if methodId doesn't represent a getter)
*/
internal fun ClassId.fieldThatIsGotWith(methodId: MethodId): FieldId? =
allDeclaredFieldIds.singleOrNull { !it.isStatic && it.getter == methodId }

/**
* Returns field of [this], such that [methodId] is a setter for it (or null if methodId doesn't represent a setter)
*/
internal fun ClassId.fieldThatIsSetWith(methodId: MethodId): FieldId? =
allDeclaredFieldIds.singleOrNull { !it.isStatic && it.setter == methodId }
Original file line number Diff line number Diff line change
@@ -1,23 +1,70 @@
package org.utbot.framework.codegen.model.util

import org.utbot.framework.codegen.model.constructor.context.CgContext
import org.utbot.framework.plugin.api.CodegenLanguage
import org.utbot.framework.plugin.api.FieldId
import org.utbot.framework.plugin.api.MethodId
import org.utbot.framework.plugin.api.util.voidClassId

/**
* For now we will count field accessible if it is not private and its class is also accessible,
* because we generate tests in the same package with the class under test,
* which means we can access public, protected and package-private fields
*
* @param packageName name of the package we check accessibility from
* @param context context in which code is generated (it is needed because the method needs to know package and language)
*/
infix fun FieldId.isAccessibleFrom(packageName: String): Boolean {
// TODO: change parameter from packageName: String to context: CgContext in ClassId.isAccessibleFrom and ExecutableId.isAccessibleFrom ?
private fun FieldId.isAccessibleFrom(context: CgContext): Boolean {
val packageName = context.testClassPackageName
val isClassAccessible = declaringClass.isAccessibleFrom(packageName)
val isAccessibleByVisibility = isPublic || (declaringClass.packageName == packageName && (isPackagePrivate || isProtected))
val isAccessibleFromPackageByModifiers = isAccessibleByVisibility && !isSynthetic

return isClassAccessible && isAccessibleFromPackageByModifiers
}

private fun FieldId.canBeReadViaGetterFrom(context: CgContext): Boolean =
declaringClass.allMethods.contains(getter) && getter.isAccessibleFrom(context.testClassPackageName)

/**
* Returns whether you can read field's value without reflection
*/
internal infix fun FieldId.canBeReadFrom(context: CgContext): Boolean {
if (context.codegenLanguage == CodegenLanguage.KOTLIN) {
// Kotlin will allow direct field access for non-static fields with accessible getter
if (!isStatic && canBeReadViaGetterFrom(context))
return true
}

return isAccessibleFrom(context)
}

private fun FieldId.canBeSetViaSetterFrom(context: CgContext): Boolean =
declaringClass.allMethods.contains(setter) && setter.isAccessibleFrom(context.testClassPackageName)

/**
* Whether or not a field can be set without reflection
*/
fun FieldId.canBeSetIn(packageName: String): Boolean = isAccessibleFrom(packageName) && !isFinal
internal fun FieldId.canBeSetFrom(context: CgContext): Boolean {
if (context.codegenLanguage == CodegenLanguage.KOTLIN) {
// Kotlin will allow direct write access if both getter and setter is defined
// !isAccessibleFrom(context) is important here because above rule applies to final fields only if they are not accessible in Java terms
if (!isAccessibleFrom(context) && !isStatic && canBeReadViaGetterFrom(context) && canBeSetViaSetterFrom(context)) {
return true
}
}

return isAccessibleFrom(context) && !isFinal
}

/**
* The default getter method for field (the one which is generated by Kotlin compiler)
*/
val FieldId.getter: MethodId
get() = MethodId(declaringClass, "get${name.replaceFirstChar { it.uppercase() } }", type, emptyList())

/**
* The default setter method for field (the one which is generated by Kotlin compiler)
*/
val FieldId.setter: MethodId
get() = MethodId(declaringClass, "set${name.replaceFirstChar { it.uppercase() } }", voidClassId, listOf(type))
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ private fun createInstance(visibility: Visibility, language: CodegenLanguage): S
}
CodegenLanguage.KOTLIN -> {
"""
${visibility by language}fun createInstance(className: String): kotlin.Any? {
${visibility by language}fun createInstance(className: String): kotlin.Any {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest, I don't understand this change, but can believe that is is possible :)

Copy link
Collaborator Author

@volivan239 volivan239 Sep 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK, this method can't return null (at least, no compilation error is produced after this change), this may help to avoid unnecessary ?.-calls

val clazz: Class<*> = Class.forName(className)
return Class.forName("sun.misc.Unsafe").getDeclaredMethod("allocateInstance", Class::class.java)
.invoke(getUnsafeInstance(), clazz)
Expand Down