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

Added support for Java get methods #46

Merged
merged 2 commits into from
Jul 3, 2024
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 @@ -53,7 +53,7 @@ class MappieIrTransformer(private val symbols: List<MappieDefinition>) : IrEleme
declaration.body = with(createScope(declaration)) {
val (mapping, validation) = MappingSelector.of(valids).select()

logAll(validation.warnings())
logAll(validation.warnings(), location(declaration))

when (mapping) {
is ConstructorCallMapping -> {
Expand Down Expand Up @@ -92,9 +92,7 @@ class MappieIrTransformer(private val symbols: List<MappieDefinition>) : IrEleme
}
}
} else {
invalids.first().second.problems.forEach { problem ->
logError(problem.description, problem.location ?: location(declaration))
}
logAll(invalids.first().second.problems, location(declaration))
}
}
return declaration
Expand All @@ -117,7 +115,7 @@ fun ExpressionSource.toIr(builder: IrBuilderWithScope): IrExpression {
}

fun ResolvedSource.toIr(builder: IrBuilderWithScope): IrExpression {
val getter = builder.irCall(property).apply {
val getter = builder.irCall(property.function).apply {
dispatchReceiver = [email protected]
}
return via?.let {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
package tech.mappie.resolving.classes

import org.jetbrains.kotlin.ir.declarations.IrFunction
import tech.mappie.BaseVisitor
import org.jetbrains.kotlin.ir.declarations.IrProperty
import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
import org.jetbrains.kotlin.ir.declarations.IrValueParameter
import org.jetbrains.kotlin.ir.types.getClass
import org.jetbrains.kotlin.ir.util.properties

class GettersCollector : BaseVisitor<List<IrSimpleFunction>, Unit>() {
class GettersCollector : BaseVisitor<List<MappieGetter>, Unit>() {

override fun visitValueParameter(declaration: IrValueParameter, data: Unit): List<IrSimpleFunction> {
return declaration.type.getClass()!!.properties.flatMap { it.accept(Unit) }.toList()
override fun visitValueParameter(declaration: IrValueParameter, data: Unit): List<MappieGetter> {
return declaration.type.getClass()!!.properties.flatMap { it.accept(data) }.toList() +
declaration.type.getClass()!!.declarations.filterIsInstance<IrSimpleFunction>().flatMap { it.accept(data) }
}

override fun visitProperty(declaration: IrProperty, data: Unit): List<IrSimpleFunction> {
return if (declaration.getter != null) declaration.getter?.let { listOf(it) } ?: emptyList() else emptyList()
override fun visitFunction(declaration: IrFunction, data: Unit): List<MappieGetter> {
if (declaration.name.asString().startsWith("get") && declaration.symbol.owner.valueParameters.isEmpty()) {
return listOf(MappieFunctionGetter(declaration))
}
return emptyList()
}

override fun visitProperty(declaration: IrProperty, data: Unit): List<MappieGetter> {
return declaration.getter?.let { listOf(MappiePropertyGetter(it)) } ?: emptyList()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package tech.mappie.resolving.classes

import org.jetbrains.kotlin.ir.declarations.IrFunction
import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
import org.jetbrains.kotlin.ir.types.IrType
import org.jetbrains.kotlin.name.Name
import tech.mappie.util.dumpKotlinLike
import tech.mappie.util.getterName

sealed interface MappieGetter {
val name: Name
val type: IrType
val function: IrFunction

fun dumpKotlinLike(): String
}

data class MappiePropertyGetter(override val function: IrSimpleFunction) : MappieGetter {
override val name = function.name
override val type = function.returnType

override fun dumpKotlinLike(): String = function.symbol.dumpKotlinLike()
}

data class MappieFunctionGetter(override val function: IrFunction) : MappieGetter {
override val name = getterName(function.name.asString().removePrefix("get").replaceFirstChar { it.lowercaseChar() })
override val type = function.returnType

override fun dumpKotlinLike(): String = function.name.asString()
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ sealed interface ObjectMappingSource {
}

data class ResolvedSource(
val property: IrSimpleFunctionSymbol,
val property: MappieGetter,
val dispatchReceiver: IrExpression,
val via: IrSimpleFunction? = null,
val viaDispatchReceiver: IrExpression? = null,
) : ObjectMappingSource {
override val type: IrType
get() = via?.returnType ?: property.owner.returnType
get() = via?.returnType ?: property.type
}

data class PropertySource(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package tech.mappie.resolving.classes

import org.jetbrains.kotlin.ir.declarations.IrConstructor
import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
import org.jetbrains.kotlin.ir.declarations.IrValueParameter
import org.jetbrains.kotlin.ir.expressions.impl.IrGetObjectValueImpl
import org.jetbrains.kotlin.ir.types.IrType
Expand All @@ -17,7 +16,7 @@ class ObjectMappingsConstructor(val targetType: IrType, val source: IrValueParam

var symbols = listOf<MappieDefinition>()

var getters = mutableListOf<IrSimpleFunction>()
var getters = mutableListOf<MappieGetter>()

var explicit = mutableMapOf<Name, List<ObjectMappingSource>>()

Expand All @@ -37,20 +36,20 @@ class ObjectMappingsConstructor(val targetType: IrType, val source: IrValueParam
getter.name == getterName(target.name)
}
if (getter != null) {
val clazz = symbols.singleOrNull { it.fits(getter.returnType, target.type) }?.clazz
val clazz = symbols.singleOrNull { it.fits(getter.type, target.type) }?.clazz
val via = when {
clazz == null -> null
getter.returnType.isList() && target.type.isList() -> clazz.functions.firstOrNull { it.name == IDENTIFIER_MAP_LIST }
getter.returnType.isSet() && target.type.isSet() -> clazz.functions.firstOrNull { it.name == IDENTIFIER_MAP_SET }
getter.returnType.isNullable() && target.type.isNullable() -> clazz.functions.firstOrNull { it.name == IDENTIFIER_MAP_NULLABLE }
getter.type.isList() && target.type.isList() -> clazz.functions.firstOrNull { it.name == IDENTIFIER_MAP_LIST }
getter.type.isSet() && target.type.isSet() -> clazz.functions.firstOrNull { it.name == IDENTIFIER_MAP_SET }
getter.type.isNullable() && target.type.isNullable() -> clazz.functions.firstOrNull { it.name == IDENTIFIER_MAP_NULLABLE }
else -> clazz.functions.firstOrNull { it.name == IDENTIFIER_MAP }
}
val viaDispatchReceiver = when {
clazz == null -> null
clazz.isObject -> IrGetObjectValueImpl(SYNTHETIC_OFFSET, SYNTHETIC_OFFSET, clazz.symbol.defaultType, clazz.symbol)
else -> clazz.constructors.firstOrNull { it.valueParameters.isEmpty() }?.let { irConstructorCall(it) }
}
listOf(ResolvedSource(getter.symbol, irGet(source), via, viaDispatchReceiver))
listOf(ResolvedSource(getter, irGet(source), via, viaDispatchReceiver))
} else if (target.hasDefaultValue() && context.configuration.useDefaultArguments) {
listOf(ValueSource(target.defaultValue!!.expression, null))
} else {
Expand Down
10 changes: 8 additions & 2 deletions compiler-plugin/src/main/kotlin/tech/mappie/util/Ir.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import org.jetbrains.kotlin.ir.util.*
import org.jetbrains.kotlin.name.CallableId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.name.StandardClassIds.Annotations.FlexibleNullability
import tech.mappie.resolving.IDENTIFIER_MAP
import kotlin.reflect.KClass

Expand All @@ -26,8 +27,13 @@ internal fun IrClass.isStrictSubclassOf(clazz: KClass<*>): Boolean =
internal fun IrClass.allSuperTypes(): List<IrType> =
superTypes + superTypes.flatMap { it.erasedUpperBound.allSuperTypes() }

fun IrType.isAssignableFrom(other: IrType): Boolean =
other.isSubtypeOf(this, IrTypeSystemContextImpl(context.irBuiltIns)) || isIntegerAssignableFrom(other)
fun IrType.isAssignableFrom(other: IrType, ignoreFlexibleNullability: Boolean = false): Boolean {
val other = if (ignoreFlexibleNullability && other.isFlexibleNullable()) other.makeNotNull() else other
return other.isSubtypeOf(this, IrTypeSystemContextImpl(context.irBuiltIns)) || isIntegerAssignableFrom(other)
}

fun IrType.isFlexibleNullable(): Boolean =
hasAnnotation(FlexibleNullability)

fun IrType.isIntegerAssignableFrom(other: IrType): Boolean =
when (this) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ import org.jetbrains.kotlin.ir.declarations.IrDeclaration
import org.jetbrains.kotlin.ir.util.fileEntry
import tech.mappie.validation.Problem

fun logAll(problems: List<Problem>) =
problems.forEach { log(it) }
fun logAll(problems: List<Problem>, location: CompilerMessageSourceLocation? = null) =
problems.forEach { log(it, location) }

fun log(problem: Problem) =
fun log(problem: Problem, location: CompilerMessageSourceLocation?) =
when(problem.severity) {
Problem.Severity.ERROR -> logError(problem.description, problem.location)
Problem.Severity.WARNING -> logWarn(problem.description, problem.location)
Problem.Severity.ERROR -> logError(problem.description, problem.location ?: location)
Problem.Severity.WARNING -> logWarn(problem.description, problem.location ?: location)
}

fun logInfo(message: String, location: CompilerMessageSourceLocation? = null) =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import tech.mappie.util.isAssignableFrom
import tech.mappie.util.location
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageLocation
import org.jetbrains.kotlin.ir.IrFileEntry
import org.jetbrains.kotlin.ir.types.removeAnnotations
import org.jetbrains.kotlin.ir.util.dumpKotlinLike
import tech.mappie.resolving.classes.ExpressionSource
import tech.mappie.resolving.classes.ResolvedSource
Expand Down Expand Up @@ -55,7 +56,7 @@ interface MappingValidation {
addAll(
mapping.mappings
.filter { (_, sources) -> sources.size == 1 }
.filter { (target, sources) -> !target.type.isAssignableFrom(sources.single().type) }
.filter { (target, sources) -> !target.type.isAssignableFrom(sources.single().type, true) }
.map { (target, sources) ->
when (val source = sources.single()) {
is PropertySource -> {
Expand All @@ -81,6 +82,37 @@ interface MappingValidation {
}
)

addAll(
mapping.mappings
.filter { (_, sources) -> sources.size == 1 }
.filter { (target, sources) ->
target.type.isAssignableFrom(sources.single().type, true) &&
!target.type.isAssignableFrom(sources.single().type, false) }
.map { (target, sources) ->
when (val source = sources.single()) {
is PropertySource -> {
val location = location(file, source.origin)
val description = "Target ${mapping.targetType.dumpKotlinLike()}::${target.name.asString()} of type ${target.type.dumpKotlinLike()} is unsafe to from ${source.property.dumpKotlinLike()} of platform type ${source.type.removeAnnotations().dumpKotlinLike()}"
Problem.warning(description, location)
}
is ExpressionSource -> {
val location = location(file, source.origin)
val description = "Target ${mapping.targetType.dumpKotlinLike()}::${target.name.asString()} of type ${target.type.dumpKotlinLike()} is unsafe to be assigned from expression of platform type ${source.type.removeAnnotations().dumpKotlinLike()}"
Problem.warning(description, location)
}
is ResolvedSource -> {
val description = "Target ${mapping.targetType.dumpKotlinLike()}::${target.name.asString()} automatically resolved from ${source.property.dumpKotlinLike()} but it is unsafe to assign source platform type ${source.type.removeAnnotations().dumpKotlinLike()} to target type ${target.type.dumpKotlinLike()}"
Problem.warning(description, null)
}
is ValueSource -> {
val location = source.origin?.let { location(file, it) }
val description = "Target ${mapping.targetType.dumpKotlinLike()}::${target.name.asString()} of type ${target.type.dumpKotlinLike()} is unsafe to assigned from value of platform type ${source.type.removeAnnotations().dumpKotlinLike()}"
Problem.warning(description, location)
}
}
}
)

addAll(
mapping.unknowns.map {
Problem.error("Parameter ${it.key.asString()} does not occur as a parameter in constructor")
Expand Down
1 change: 1 addition & 0 deletions website/src/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ changelog:
- "[#28](https://github.com/Mr-Mappie/mappie/issues/28) added implicit mapping inference of mappers with the same name but a different type, but a mapper for those types are defined."
- "[#42](https://github.com/Mr-Mappie/mappie/issues/42) added a configuration option to disable resolving via default arguments."
- "[#40](https://github.com/Mr-Mappie/mappie/issues/40) added `to` alias to refer to for target properties as an alternative to the fully written out `TO` type."
- "[#46](https://github.com/Mr-Mappie/mappie/issues/46) added support for Java object getters."
- "Several other bug fixes."
- date: "2024-06-27"
title: "v0.2.0"
Expand Down
11 changes: 10 additions & 1 deletion website/src/posts/object-mapping/posts/resolving.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,13 @@ object PersonMapper : ObjectMappie<Person, PersonDto>() {
}
}
```
where `to::streetname` is equivalent to `PersonDto::streetname`.
where `to::streetname` is equivalent to `PersonDto::streetname`.

## Java Compatibility
Java classes are different from those of Kotlin. The main difference for Mappie is that Java does not have the concept
of properties. Instead, the convention is to have a field `x`, and a getter `getX()` and setter `setX(Person value)`
method.

Mappie automatically infers such getter methods. They must follow the convention `getX()` for the property `x` to be
inferred. Also note that in Java all types are nullable. Mappie will give a warning if a Java getter is used to assign
to a non-nullable target.