Skip to content

Commit c9e5aa3

Browse files
Fixed NullPointerException when generation a mapper (#253)
1 parent 70fed58 commit c9e5aa3

File tree

12 files changed

+4484
-7835
lines changed

12 files changed

+4484
-7835
lines changed

compiler-plugin/src/main/kotlin/tech/mappie/ir/analysis/problems/classes/MapperGenerationRequestProblems.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class MapperGenerationRequestProblems(
2828
return if (requests.isEmpty()) {
2929
listOf(
3030
Problem.error(
31-
"No implicit mapping can be generated from ${source.dumpKotlinLike()} to ${target.dumpKotlinLike()}",
31+
"No implicit mapping can be generated from ${transformation.source.type.dumpKotlinLike()} to ${transformation.target.type.dumpKotlinLike()}",
3232
location(context.function),
3333
listOf("Target class has no accessible constructor"),
3434
))
@@ -40,8 +40,8 @@ class MapperGenerationRequestProblems(
4040
.associateBy { request -> MappingValidation.of(context, request) }
4141

4242
if (validations.none { it.key.isValid() }) {
43-
val (validation, request) = validations.entries.first { !it.key.isValid() }
44-
val message = "No implicit mapping can be generated from ${request.source.type.dumpKotlinLike()} to ${request.target.type.dumpKotlinLike()}"
43+
val (validation, _) = validations.entries.first { !it.key.isValid() }
44+
val message = "No implicit mapping can be generated from ${transformation.source.type.dumpKotlinLike()} to ${transformation.target.type.dumpKotlinLike()}"
4545
add(Problem.error(message, location(context.function), validation.errors().map { it.description }))
4646
}
4747
}

compiler-plugin/src/main/kotlin/tech/mappie/ir/resolving/classes/ClassResolver.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class ClassResolver(
1717
override fun resolve(function: IrFunction?): List<ClassMappingRequest> =
1818
target.getClass()!!.constructors.map { constructor ->
1919
ClassMappingRequestBuilder(constructor, context)
20-
.targets(MappieTargetsCollector(function, constructor).collect())
20+
.targets(MappieTargetsCollector(target, function, constructor).collect())
2121
.sources(sources)
2222
.also { function?.body?.accept(ExplicitClassMappingCollector(context), it) }
2323
.construct()

compiler-plugin/src/main/kotlin/tech/mappie/ir/resolving/classes/targets/MappieTargetsCollector.kt

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@ import org.jetbrains.kotlin.ir.declarations.IrFunction
55
import org.jetbrains.kotlin.ir.declarations.IrParameterKind
66
import org.jetbrains.kotlin.ir.types.*
77
import org.jetbrains.kotlin.ir.util.*
8+
import org.jetbrains.kotlin.ir.util.constructedClass
89
import tech.mappie.ir.util.substituteTypeVariable
910

10-
class MappieTargetsCollector(function: IrFunction?, constructor: IrConstructor) {
11-
12-
private val type = constructor.returnType
11+
class MappieTargetsCollector(
12+
private val target: IrType,
13+
private val function: IrFunction?,
14+
private val constructor: IrConstructor
15+
) {
1316

1417
private val parameters: List<ClassMappingTarget> = run {
1518
val parameters = constructor.constructedClass.typeParameters
@@ -20,29 +23,31 @@ class MappieTargetsCollector(function: IrFunction?, constructor: IrConstructor)
2023
}
2124

2225
private val setters: Sequence<ClassMappingTarget> = run {
23-
type.classOrFail.owner.properties.mapNotNull { property ->
26+
target.classOrFail.owner.properties.mapNotNull { property ->
2427
property.setter?.let { setter ->
25-
if (function != null) {
26-
property to setter.parameters.first { it.kind == IrParameterKind.Regular }.type.substituteTypeVariable(constructor.constructedClass, (function.returnType as IrSimpleType).arguments)
27-
} else {
28-
property to type
29-
}
28+
property to setter.parameters.first { it.kind == IrParameterKind.Regular }.type.substituted()
3029
}
3130
}
3231
.filter { property -> property.first.name !in parameters.map { it.name } }
3332
.map { SetterTarget(it.first, it.second) }
3433
}
3534

3635
private val setMethods: Sequence<ClassMappingTarget> =
37-
type.classOrFail.functions
36+
target.classOrFail.functions
3837
.filter { it.owner.name.asString().startsWith("set") && it.owner.parameters.count { it.kind == IrParameterKind.Regular } == 1 }
3938
.map {
4039
FunctionCallTarget(
4140
it,
42-
it.owner.parameters.first { it.kind == IrParameterKind.Regular }.type.substituteTypeVariable(constructor.constructedClass, (function?.returnType as? IrSimpleType)?.arguments!!)
41+
it.owner.parameters.first { it.kind == IrParameterKind.Regular }.type.substituted()
4342
)
4443
}
4544

45+
private fun IrType.substituted(): IrType =
46+
if (this is IrSimpleType) {
47+
substituteTypeVariable(constructor.constructedClass, (target as IrSimpleType).arguments)
48+
} else {
49+
type
50+
}
4651

4752
fun collect(): List<ClassMappingTarget> = parameters + setters + setMethods
4853
}

compiler-plugin/src/test/kotlin/tech/mappie/testing/objects/GeneratedClassWithoutVisibleConstructorTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class GeneratedClassWithoutVisibleConstructorTest {
3030
)
3131
} satisfies {
3232
isCompilationError()
33-
hasErrorMessage(4, "No implicit mapping can be generated from LocalDate to OffsetDateTime",
33+
hasErrorMessage(4, "No implicit mapping can be generated from LocalDate? to OffsetDateTime?",
3434
listOf("Target class has no accessible constructor")
3535
)
3636
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package tech.mappie.testing.objects
2+
3+
import org.junit.jupiter.api.Test
4+
import org.junit.jupiter.api.io.TempDir
5+
import tech.mappie.testing.compilation.compile
6+
import java.io.File
7+
import java.math.BigDecimal
8+
9+
class NullablePropertyObjectToNonNullablePropertyTest {
10+
data class Input(val value: BigDecimal?)
11+
data class Output(val value: BigDecimal)
12+
13+
@TempDir
14+
lateinit var directory: File
15+
16+
@Test
17+
fun `map object with null property to non-null property should fail`() {
18+
compile(directory) {
19+
file("Test.kt",
20+
"""
21+
import tech.mappie.api.ObjectMappie
22+
import tech.mappie.testing.objects.NullablePropertyObjectToNonNullablePropertyTest.*
23+
24+
class Mapper : ObjectMappie<Input, Output>()
25+
"""
26+
)
27+
} satisfies {
28+
isCompilationError()
29+
// TODO: error message also contains `No implicit mapping can be generated from BigDecimal? to BigDecimal` which should not happen.
30+
hasErrorMessage(4, "Target Output::value automatically resolved from Input::value but cannot assign source type BigDecimal? to target type BigDecimal")
31+
}
32+
}
33+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package tech.mappie.testing.scenarios
2+
3+
import org.assertj.core.api.Assertions.assertThat
4+
import org.junit.jupiter.api.Test
5+
import org.junit.jupiter.api.io.TempDir
6+
import tech.mappie.testing.compilation.compile
7+
import tech.mappie.testing.loadObjectMappie2Class
8+
import java.io.File
9+
import java.math.BigDecimal
10+
import java.time.Instant
11+
12+
class InvoiceTest {
13+
14+
data class EventMetadataDto(
15+
val eventTimestamp: Instant,
16+
val eventSource: String? = null,
17+
)
18+
19+
data class InvoiceDto(
20+
val invoiceNumber: String,
21+
val items: List<InvoiceLineItemDto>? = null,
22+
)
23+
24+
data class WeightDetailsDto(
25+
val grossWeight: BigDecimal? = null,
26+
val unitOfWeight: String? = null,
27+
)
28+
29+
data class InvoiceLineItemDto(
30+
val invoiceItemNumber: String,
31+
val weightDetails: WeightDetailsDto? = null,
32+
)
33+
34+
data class InvoiceLineItem(
35+
val invoiceLineItemId: Int = 0,
36+
val invoiceItemNumber: String,
37+
val weightDetails: WeightDetails? = null,
38+
)
39+
40+
data class WeightDetails(
41+
val grossWeight: BigDecimal,
42+
val unitOfWeight: String,
43+
)
44+
45+
data class EventMetadata(
46+
val eventTimestamp: Instant = Instant.EPOCH,
47+
val eventSource: String? = null,
48+
)
49+
50+
data class Invoice(
51+
val invoiceId: Int = 0,
52+
val invoiceNumber: String = "",
53+
val items: MutableList<InvoiceLineItem> = mutableListOf(),
54+
val eventMetadata: EventMetadata,
55+
)
56+
57+
@TempDir
58+
lateinit var directory: File
59+
60+
@Test
61+
fun `map InvoiceDto to Invoice`() {
62+
compile(directory) {
63+
file("Test.kt",
64+
"""
65+
import tech.mappie.api.ObjectMappie
66+
import tech.mappie.api.ObjectMappie2
67+
import tech.mappie.testing.scenarios.InvoiceTest.*
68+
import java.math.BigDecimal
69+
70+
class Mapper : ObjectMappie2<InvoiceDto, EventMetadataDto, Invoice>() {
71+
override fun map(first: InvoiceDto, second: EventMetadataDto) = mapping {
72+
to::eventMetadata fromValue kotlin.run { EventMetaDataMapper.map(second) }
73+
to::items fromProperty first::items transform {
74+
it?.let { InvoiceLineMapper.mapList(it).toMutableList() } ?: mutableListOf()
75+
}
76+
}
77+
}
78+
79+
object InvoiceLineMapper : ObjectMappie<InvoiceLineItemDto, InvoiceLineItem>()
80+
81+
object WeightDetailsMapper : ObjectMappie<WeightDetailsDto, WeightDetails>() {
82+
override fun map(from: WeightDetailsDto) = mapping {
83+
to::grossWeight fromProperty from::grossWeight transform { it ?: BigDecimal.ZERO }
84+
to::unitOfWeight fromProperty from::unitOfWeight transform { it ?: "" }
85+
}
86+
}
87+
88+
object EventMetaDataMapper : ObjectMappie<EventMetadataDto, EventMetadata>()
89+
"""
90+
)
91+
} satisfies {
92+
isOk()
93+
hasNoWarningsOrErrors()
94+
95+
val mapper = classLoader
96+
.loadObjectMappie2Class<InvoiceDto, EventMetadataDto, Invoice>("Mapper")
97+
.constructors
98+
.first()
99+
.call()
100+
101+
assertThat(mapper.map(InvoiceDto("10"),EventMetadataDto(Instant.EPOCH)))
102+
.isEqualTo(Invoice(0, "10", mutableListOf(), EventMetadata()))
103+
}
104+
}
105+
}

gradle.properties

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
version=2.2.10-1.4.2
1+
version=2.2.10-1.5.0
22

33
org.gradle.configuration-cache=true
44
org.gradle.configuration-cache.parallel=true
@@ -7,4 +7,4 @@ org.gradle.jvmargs=-Xmx2048M -XX:MaxMetaspaceSize=512m
77
org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
88
org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true
99

10-
kotlin.js.yarn=false
10+
#kotlin.js.yarn=false

0 commit comments

Comments
 (0)