diff --git a/compiler-plugin/src/main/kotlin/tech/mappie/generation/MappieIrTransformer.kt b/compiler-plugin/src/main/kotlin/tech/mappie/generation/MappieIrTransformer.kt index 67afe11d..9cd7be64 100644 --- a/compiler-plugin/src/main/kotlin/tech/mappie/generation/MappieIrTransformer.kt +++ b/compiler-plugin/src/main/kotlin/tech/mappie/generation/MappieIrTransformer.kt @@ -53,7 +53,7 @@ class MappieIrTransformer(private val symbols: List) : 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 -> { @@ -92,9 +92,7 @@ class MappieIrTransformer(private val symbols: List) : IrEleme } } } else { - invalids.first().second.problems.forEach { problem -> - logError(problem.description, problem.location ?: location(declaration)) - } + logAll(invalids.first().second.problems, location(declaration)) } } return declaration @@ -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 = this@toIr.dispatchReceiver } return via?.let { diff --git a/compiler-plugin/src/main/kotlin/tech/mappie/resolving/classes/GettersCollector.kt b/compiler-plugin/src/main/kotlin/tech/mappie/resolving/classes/GettersCollector.kt index 58bbd0eb..fb370d4d 100644 --- a/compiler-plugin/src/main/kotlin/tech/mappie/resolving/classes/GettersCollector.kt +++ b/compiler-plugin/src/main/kotlin/tech/mappie/resolving/classes/GettersCollector.kt @@ -1,5 +1,6 @@ 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 @@ -7,13 +8,21 @@ import org.jetbrains.kotlin.ir.declarations.IrValueParameter import org.jetbrains.kotlin.ir.types.getClass import org.jetbrains.kotlin.ir.util.properties -class GettersCollector : BaseVisitor, Unit>() { +class GettersCollector : BaseVisitor, Unit>() { - override fun visitValueParameter(declaration: IrValueParameter, data: Unit): List { - return declaration.type.getClass()!!.properties.flatMap { it.accept(Unit) }.toList() + override fun visitValueParameter(declaration: IrValueParameter, data: Unit): List { + return declaration.type.getClass()!!.properties.flatMap { it.accept(data) }.toList() + + declaration.type.getClass()!!.declarations.filterIsInstance().flatMap { it.accept(data) } } - override fun visitProperty(declaration: IrProperty, data: Unit): List { - return if (declaration.getter != null) declaration.getter?.let { listOf(it) } ?: emptyList() else emptyList() + override fun visitFunction(declaration: IrFunction, data: Unit): List { + 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 { + return declaration.getter?.let { listOf(MappiePropertyGetter(it)) } ?: emptyList() } } \ No newline at end of file diff --git a/compiler-plugin/src/main/kotlin/tech/mappie/resolving/classes/MappieGetter.kt b/compiler-plugin/src/main/kotlin/tech/mappie/resolving/classes/MappieGetter.kt new file mode 100644 index 00000000..fbff8502 --- /dev/null +++ b/compiler-plugin/src/main/kotlin/tech/mappie/resolving/classes/MappieGetter.kt @@ -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() +} diff --git a/compiler-plugin/src/main/kotlin/tech/mappie/resolving/classes/ObjectMappingSource.kt b/compiler-plugin/src/main/kotlin/tech/mappie/resolving/classes/ObjectMappingSource.kt index 7fe770a5..81105f66 100644 --- a/compiler-plugin/src/main/kotlin/tech/mappie/resolving/classes/ObjectMappingSource.kt +++ b/compiler-plugin/src/main/kotlin/tech/mappie/resolving/classes/ObjectMappingSource.kt @@ -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( diff --git a/compiler-plugin/src/main/kotlin/tech/mappie/resolving/classes/ObjectMappingsConstructor.kt b/compiler-plugin/src/main/kotlin/tech/mappie/resolving/classes/ObjectMappingsConstructor.kt index b82780a1..77362f84 100644 --- a/compiler-plugin/src/main/kotlin/tech/mappie/resolving/classes/ObjectMappingsConstructor.kt +++ b/compiler-plugin/src/main/kotlin/tech/mappie/resolving/classes/ObjectMappingsConstructor.kt @@ -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 @@ -17,7 +16,7 @@ class ObjectMappingsConstructor(val targetType: IrType, val source: IrValueParam var symbols = listOf() - var getters = mutableListOf() + var getters = mutableListOf() var explicit = mutableMapOf>() @@ -37,12 +36,12 @@ 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 { @@ -50,7 +49,7 @@ class ObjectMappingsConstructor(val targetType: IrType, val source: IrValueParam 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 { diff --git a/compiler-plugin/src/main/kotlin/tech/mappie/util/Ir.kt b/compiler-plugin/src/main/kotlin/tech/mappie/util/Ir.kt index 38e8fdfb..57f96ad6 100644 --- a/compiler-plugin/src/main/kotlin/tech/mappie/util/Ir.kt +++ b/compiler-plugin/src/main/kotlin/tech/mappie/util/Ir.kt @@ -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 @@ -26,8 +27,13 @@ internal fun IrClass.isStrictSubclassOf(clazz: KClass<*>): Boolean = internal fun IrClass.allSuperTypes(): List = 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) { diff --git a/compiler-plugin/src/main/kotlin/tech/mappie/util/MessageCollector.kt b/compiler-plugin/src/main/kotlin/tech/mappie/util/MessageCollector.kt index bd4c794d..3e1450b2 100644 --- a/compiler-plugin/src/main/kotlin/tech/mappie/util/MessageCollector.kt +++ b/compiler-plugin/src/main/kotlin/tech/mappie/util/MessageCollector.kt @@ -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) = - problems.forEach { log(it) } +fun logAll(problems: List, 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) = diff --git a/compiler-plugin/src/main/kotlin/tech/mappie/validation/MappingValidation.kt b/compiler-plugin/src/main/kotlin/tech/mappie/validation/MappingValidation.kt index 8f0ef1e3..acd9dea4 100644 --- a/compiler-plugin/src/main/kotlin/tech/mappie/validation/MappingValidation.kt +++ b/compiler-plugin/src/main/kotlin/tech/mappie/validation/MappingValidation.kt @@ -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 @@ -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 -> { @@ -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") diff --git a/website/src/changelog.md b/website/src/changelog.md index 8fcb1338..2fca2336 100644 --- a/website/src/changelog.md +++ b/website/src/changelog.md @@ -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" diff --git a/website/src/posts/object-mapping/posts/resolving.md b/website/src/posts/object-mapping/posts/resolving.md index da353116..8ac0243b 100644 --- a/website/src/posts/object-mapping/posts/resolving.md +++ b/website/src/posts/object-mapping/posts/resolving.md @@ -118,4 +118,13 @@ object PersonMapper : ObjectMappie() { } } ``` -where `to::streetname` is equivalent to `PersonDto::streetname`. \ No newline at end of file +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. \ No newline at end of file