Skip to content

Commit 0985719

Browse files
Allow mapping to be used anywhere in a map function (#265)
* First attempt at allowing mapping to be called inside the function body instead of being the function body * Fixes * Added extra tests for expanded mapping usage * Added validation that only one mapping exists per body * Fixed problems * Updated changelog
1 parent 28a6b49 commit 0985719

22 files changed

+972
-133
lines changed

compiler-plugin/src/main/kotlin/tech/mappie/MappiePluginContext.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,10 @@ fun MappieContext.referenceFunctionRequireNotNull() =
5555
it.owner.hasShape(regularParameters = 2)
5656
}
5757

58+
fun MappieContext.referenceFunctionRun() =
59+
pluginContext.referenceFunctions(CallableId(PACKAGE_KOTLIN, Name.identifier("run"))).first {
60+
it.owner.hasShape(regularParameters = 1)
61+
}
62+
5863
fun MappieContext.shouldGenerateCode(clazz: IrClass) =
5964
clazz.superClass?.symbol in allMappieClasses()

compiler-plugin/src/main/kotlin/tech/mappie/fir/MappieAdditionalCheckersExtension.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,9 @@ class MappieAdditionalCheckersExtension(session: FirSession) : FirAdditionalChec
2828
override val anonymousObjectCheckers: Set<FirAnonymousObjectChecker> = setOf(
2929
AnonymousMappieObjectChecker()
3030
)
31+
32+
override val functionCheckers = setOf(
33+
FunctionBodyChecker()
34+
)
3135
}
3236
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package tech.mappie.fir.analysis
2+
3+
import org.jetbrains.kotlin.diagnostics.DiagnosticReporter
4+
import org.jetbrains.kotlin.diagnostics.reportOn
5+
import org.jetbrains.kotlin.fir.FirElement
6+
import org.jetbrains.kotlin.fir.analysis.checkers.MppCheckerKind
7+
import org.jetbrains.kotlin.fir.analysis.checkers.context.CheckerContext
8+
import org.jetbrains.kotlin.fir.analysis.checkers.declaration.FirFunctionChecker
9+
import org.jetbrains.kotlin.fir.declarations.*
10+
import org.jetbrains.kotlin.fir.expressions.*
11+
import org.jetbrains.kotlin.fir.resolve.getContainingClass
12+
import org.jetbrains.kotlin.fir.visitors.FirVisitor
13+
import tech.mappie.fir.analysis.MappieErrors.MULTIPLE_MAPPING_CALLS
14+
import tech.mappie.fir.util.isSubclassOfAnMappie
15+
import tech.mappie.util.IDENTIFIER_MAPPING
16+
17+
class FunctionBodyChecker : FirFunctionChecker(MppCheckerKind.Common) {
18+
19+
context(context: CheckerContext, reporter: DiagnosticReporter)
20+
override fun check(declaration: FirFunction) {
21+
if (declaration.symbol.hasBody && declaration.getContainingClass()?.symbol?.isSubclassOfAnMappie() ?: false) {
22+
val calls = mutableListOf<FirFunctionCall>()
23+
declaration.body?.accept(MappingFinder, calls)
24+
if (calls.size > 1) {
25+
reporter.reportOn(calls.first().source, MULTIPLE_MAPPING_CALLS)
26+
}
27+
}
28+
}
29+
}
30+
31+
private object MappingFinder : FirVisitor<Unit, MutableList<FirFunctionCall>>() {
32+
override fun visitElement(element: FirElement, data: MutableList<FirFunctionCall>) =
33+
element.acceptChildren(this, data)
34+
35+
override fun visitFunctionCall(functionCall: FirFunctionCall, data: MutableList<FirFunctionCall>) {
36+
if (functionCall.calleeReference.name == IDENTIFIER_MAPPING) {
37+
data.add(functionCall)
38+
}
39+
functionCall.acceptChildren(this, data)
40+
}
41+
}

compiler-plugin/src/main/kotlin/tech/mappie/fir/analysis/MappieErrors.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import org.jetbrains.kotlin.name.Name
1313
import org.jetbrains.kotlin.psi.KtElement
1414

1515
object MappieErrors : KtDiagnosticsContainer() {
16+
val MULTIPLE_MAPPING_CALLS by error0<KtElement>(WHOLE_ELEMENT)
1617
val INVALID_ANONYMOUS_OBJECT by error0<KtElement>(WHOLE_ELEMENT)
1718
val COMPILE_TIME_RECEIVER by error1<KtElement, Name>(WHOLE_ELEMENT)
1819
val COMPILE_TIME_EXTENSION_RECEIVER by error1<KtElement, Name>(WHOLE_ELEMENT)
@@ -28,6 +29,7 @@ object MappieErrors : KtDiagnosticsContainer() {
2829

2930
object DefaultErrorMessageMappie : BaseDiagnosticRendererFactory() {
3031
override val MAP: KtDiagnosticFactoryToRendererMap by KtDiagnosticFactoryToRendererMap("Mappie") { map ->
32+
map.put(MappieErrors.MULTIPLE_MAPPING_CALLS, "Multiple calls of the function 'mapping' while only one is allowed")
3133
map.put(MappieErrors.INVALID_ANONYMOUS_OBJECT, "Anonymous Mappie objects are not supported")
3234
map.put(MappieErrors.COMPILE_TIME_RECEIVER, "The function ''{0}'' was called on the mapping dsl which does not exist after compilation", CommonRenderers.NAME)
3335
map.put(MappieErrors.COMPILE_TIME_EXTENSION_RECEIVER, "The function ''{0}'' was called as an extension method on the mapping dsl which does not exist after compilation", CommonRenderers.NAME)

compiler-plugin/src/main/kotlin/tech/mappie/ir/generation/MappieCodeGenerator.kt

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ package tech.mappie.ir.generation
22

33
import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext
44
import org.jetbrains.kotlin.ir.IrStatement
5+
import org.jetbrains.kotlin.ir.builders.Scope
56
import org.jetbrains.kotlin.ir.declarations.*
7+
import org.jetbrains.kotlin.ir.expressions.IrCall
8+
import org.jetbrains.kotlin.ir.expressions.IrExpression
69
import org.jetbrains.kotlin.ir.util.dumpKotlinLike
710
import tech.mappie.exceptions.MappieProblemException.Companion.fail
811
import tech.mappie.ir.generation.classes.ObjectMappieCodeGenerator
@@ -14,6 +17,7 @@ import tech.mappie.ir.selection.MappingSelector
1417
import tech.mappie.ir.util.isMappieMapFunction
1518
import tech.mappie.ir.util.location
1619
import tech.mappie.ir.util.mappieType
20+
import tech.mappie.util.IDENTIFIER_MAPPING
1721

1822
class MappieCodeGenerator(private val context: CodeGenerationContext) : IrElementTransformerVoidWithContext() {
1923

@@ -63,10 +67,33 @@ class MappieCodeGenerator(private val context: CodeGenerationContext) : IrElemen
6367

6468
override fun visitFunctionNew(declaration: IrFunction) = declaration.apply {
6569
body = with(createScope(declaration)) {
66-
when (context.model) {
67-
is EnumMappieCodeGenerationModel -> EnumMappieCodeGenerator(context, context.model).construct(scope)
68-
is ClassMappieCodeGenerationModel -> ObjectMappieCodeGenerator(context, context.model).construct(scope)
70+
if (body == null) {
71+
when (context.model) {
72+
is EnumMappieCodeGenerationModel -> EnumMappieCodeGenerator(context, context.model).body(scope)
73+
is ClassMappieCodeGenerationModel -> ObjectMappieCodeGenerator(context, context.model).body(scope)
74+
}
75+
} else {
76+
declaration.body!!.transform(MappieFunctionBodyTransformer(scope, context), null)
6977
}
7078
}
7179
}
7280
}
81+
82+
class MappieFunctionBodyTransformer(private val scope: Scope, private val context: CodeGenerationContext) : IrElementTransformerVoidWithContext() {
83+
override fun visitCall(expression: IrCall): IrExpression {
84+
return when (expression.symbol.owner.name) {
85+
IDENTIFIER_MAPPING -> {
86+
when (context.model) {
87+
is EnumMappieCodeGenerationModel -> EnumMappieCodeGenerator(context, context.model).lambda(scope)
88+
is ClassMappieCodeGenerationModel -> ObjectMappieCodeGenerator(context, context.model).lambda(scope)
89+
}
90+
}
91+
else -> {
92+
expression.arguments.forEachIndexed { index, argument ->
93+
expression.arguments[index] = argument?.transform(this, null)
94+
}
95+
expression
96+
}
97+
}
98+
}
99+
}

compiler-plugin/src/main/kotlin/tech/mappie/ir/generation/classes/ObjectMappieCodeGenerator.kt

Lines changed: 58 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package tech.mappie.ir.generation.classes
22

3+
import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder
4+
import org.jetbrains.kotlin.backend.common.lower.createIrBuilder
35
import org.jetbrains.kotlin.ir.builders.*
46
import org.jetbrains.kotlin.ir.declarations.IrParameterKind
57
import org.jetbrains.kotlin.ir.declarations.IrValueParameter
6-
import org.jetbrains.kotlin.ir.expressions.IrBody
8+
import org.jetbrains.kotlin.ir.expressions.IrBlockBody
9+
import org.jetbrains.kotlin.ir.expressions.IrCall
710
import org.jetbrains.kotlin.ir.expressions.IrExpression
811
import org.jetbrains.kotlin.ir.types.IrSimpleType
912
import org.jetbrains.kotlin.ir.types.makeNotNull
@@ -22,45 +25,75 @@ import tech.mappie.ir.resolving.classes.targets.SetterTarget
2225
import tech.mappie.ir.resolving.classes.targets.ValueParameterTarget
2326
import tech.mappie.ir.util.blockBody
2427
import tech.mappie.ir.util.irLambda
28+
import tech.mappie.referenceFunctionRun
2529

2630
class ObjectMappieCodeGenerator(private val context: CodeGenerationContext, private val model: ClassMappieCodeGenerationModel) {
2731

28-
fun construct(scope: Scope): IrBody =
32+
fun lambda(scope: Scope): IrCall =
33+
with(context.pluginContext.irBuiltIns.createIrBuilder(scope.scopeOwnerSymbol)) {
34+
irCall(this@ObjectMappieCodeGenerator.context.referenceFunctionRun()).apply {
35+
arguments[0] = irLambda(model.declaration.returnType, model.declaration.returnType) {
36+
content()
37+
}
38+
}
39+
}
40+
41+
fun body(scope: Scope): IrBlockBody =
2942
context.pluginContext.blockBody(scope) {
30-
val constructor = model.constructor.symbol
31-
val call = irCallConstructor(constructor, emptyList()).apply {
32-
model.mappings.forEach { (target, source) ->
33-
if (target is ValueParameterTarget) {
34-
constructArgument(source, model.declaration.parameters.filter { it.kind == IrParameterKind.Regular })?.let { argument ->
35-
arguments[target.value.indexInParameters] = argument
36-
}
43+
content()
44+
}
45+
46+
private fun IrBlockBodyBuilder.content() {
47+
val constructor = model.constructor.symbol
48+
val call = irCallConstructor(constructor, emptyList()).apply {
49+
model.mappings.forEach { (target, source) ->
50+
if (target is ValueParameterTarget) {
51+
constructArgument(
52+
source,
53+
model.declaration.parameters.filter { it.kind == IrParameterKind.Regular })?.let { argument ->
54+
arguments[target.value.indexInParameters] = argument
3755
}
3856
}
3957
}
58+
}
4059

41-
val variable = createTmpVariable(call)
60+
val variable = createTmpVariable(call)
4261

43-
model.mappings.forEach { (target, source) ->
44-
when (target) {
45-
is SetterTarget -> {
46-
+irCall(target.value.setter!!).apply {
47-
dispatchReceiver = irGet(variable)
48-
arguments[1] = constructArgument(source, model.declaration.parameters.filter { it.kind == IrParameterKind.Regular })
49-
}
62+
model.mappings.forEach { (target, source) ->
63+
when (target) {
64+
is SetterTarget -> {
65+
+irCall(target.value.setter!!).apply {
66+
dispatchReceiver = irGet(variable)
67+
arguments[1] = constructArgument(
68+
source,
69+
model.declaration.parameters.filter { it.kind == IrParameterKind.Regular })
5070
}
51-
is FunctionCallTarget -> {
52-
+irCall(target.value).apply {
53-
dispatchReceiver = irGet(variable)
54-
arguments[1] = constructArgument(source, model.declaration.parameters.filter { it.kind == IrParameterKind.Regular })
55-
}
71+
}
72+
73+
is FunctionCallTarget -> {
74+
+irCall(target.value).apply {
75+
dispatchReceiver = irGet(variable)
76+
arguments[1] = constructArgument(
77+
source,
78+
model.declaration.parameters.filter { it.kind == IrParameterKind.Regular })
5679
}
57-
else -> { /* Applied as a constructor call argument */ }
5880
}
59-
}
6081

61-
+irReturn(irGet(variable))
82+
else -> { /* Applied as a constructor call argument */
83+
}
84+
}
6285
}
6386

87+
+irReturn(irGet(variable))
88+
}
89+
90+
fun construct(builder: DeclarationIrBuilder): IrCall {
91+
return builder.irCall(context.referenceFunctionRun()).apply {
92+
arguments[0] = builder.irLambda(model.declaration.returnType, model.declaration.returnType) {
93+
}
94+
}
95+
}
96+
6497
private fun IrBuilderWithScope.constructArgument(source: ClassMappingSource, parameters: List<IrValueParameter>): IrExpression? =
6598
when (source) {
6699
is ExplicitPropertyMappingSource -> {

compiler-plugin/src/main/kotlin/tech/mappie/ir/generation/enums/EnumMappieCodeGenerator.kt

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,57 @@
11
package tech.mappie.ir.generation.enums
22

3+
import org.jetbrains.kotlin.backend.common.lower.createIrBuilder
34
import org.jetbrains.kotlin.backend.common.lower.irThrow
45
import org.jetbrains.kotlin.ir.builders.*
6+
import org.jetbrains.kotlin.ir.expressions.IrBlockBody
7+
import org.jetbrains.kotlin.ir.expressions.IrCall
58
import tech.mappie.ir.generation.CodeGenerationContext
69
import tech.mappie.ir.generation.EnumMappieCodeGenerationModel
710
import tech.mappie.ir.generation.referenceFunctionValueOf
811
import tech.mappie.ir.resolving.enums.ExplicitEnumMappingTarget
912
import tech.mappie.ir.resolving.enums.ResolvedEnumMappingTarget
1013
import tech.mappie.ir.resolving.enums.ThrowingEnumMappingTarget
1114
import tech.mappie.ir.util.blockBody
15+
import tech.mappie.ir.util.irLambda
16+
import tech.mappie.referenceFunctionRun
1217

1318
class EnumMappieCodeGenerator(private val context: CodeGenerationContext, private val model: EnumMappieCodeGenerationModel) {
1419

15-
fun construct(scope: Scope) =
16-
context.pluginContext.blockBody(scope) {
17-
+irReturn(irWhen(model.target, buildList {
18-
model.mappings.forEach { (source, target) ->
19-
val lhs = irGet(model.declaration.parameters[1])
20-
val rhs = irCall(source.referenceFunctionValueOf()).apply {
21-
arguments[0] = irString(source.name.asString())
22-
}
23-
add(irBranch(irEquals(lhs, rhs), when (target) {
24-
is ExplicitEnumMappingTarget -> construct(target)
25-
is ResolvedEnumMappingTarget -> construct(target)
26-
is ThrowingEnumMappingTarget -> construct(target)
27-
}))
20+
fun lambda(scope: Scope): IrCall =
21+
with(context.pluginContext.irBuiltIns.createIrBuilder(scope.scopeOwnerSymbol)) {
22+
irCall(this@EnumMappieCodeGenerator.context.referenceFunctionRun()).apply {
23+
arguments[0] = irLambda(model.declaration.returnType, model.declaration.returnType) {
24+
content()
2825
}
29-
add(irElseBranch(irCall(context.irBuiltIns.noWhenBranchMatchedExceptionSymbol)))
30-
}))
26+
}
27+
}
28+
29+
fun body(scope: Scope): IrBlockBody =
30+
context.pluginContext.blockBody(scope) {
31+
content()
3132
}
3233

34+
private fun IrBlockBodyBuilder.content() {
35+
+irReturn(irWhen(model.target, buildList {
36+
model.mappings.forEach { (source, target) ->
37+
val lhs = irGet(model.declaration.parameters[1])
38+
val rhs = irCall(source.referenceFunctionValueOf()).apply {
39+
arguments[0] = irString(source.name.asString())
40+
}
41+
add(
42+
irBranch(
43+
irEquals(lhs, rhs), when (target) {
44+
is ExplicitEnumMappingTarget -> construct(target)
45+
is ResolvedEnumMappingTarget -> construct(target)
46+
is ThrowingEnumMappingTarget -> construct(target)
47+
}
48+
)
49+
)
50+
}
51+
add(irElseBranch(irCall(context.irBuiltIns.noWhenBranchMatchedExceptionSymbol)))
52+
}))
53+
}
54+
3355
private fun construct(target: ExplicitEnumMappingTarget) =
3456
target.target
3557

compiler-plugin/src/main/kotlin/tech/mappie/ir/reporting/PrettyPrinter.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,13 @@ private class PrettyPrinter : IrVisitor<KotlinStringBuilder, KotlinStringBuilder
169169
}
170170
}
171171
}
172+
173+
override fun visitVararg(expression: IrVararg, data: KotlinStringBuilder): KotlinStringBuilder {
174+
return data.apply {
175+
commas(expression.elements) { element(it) }
176+
}
177+
}
178+
172179
override fun visitConstructor(declaration: IrConstructor, data: KotlinStringBuilder): KotlinStringBuilder {
173180
return data.apply {
174181
if (!declaration.isPrimary) {
@@ -195,6 +202,17 @@ private class PrettyPrinter : IrVisitor<KotlinStringBuilder, KotlinStringBuilder
195202
return data
196203
}
197204

205+
override fun visitLocalDelegatedProperty(declaration: IrLocalDelegatedProperty, data: KotlinStringBuilder): KotlinStringBuilder {
206+
return data.apply {
207+
string { if (declaration.isVar) "var " else "val " }
208+
string { declaration.name.pretty() }
209+
string { " by " }
210+
declaration.delegate.initializer?.let {
211+
string { it.pretty() }
212+
}
213+
}
214+
}
215+
198216
override fun visitProperty(declaration: IrProperty, data: KotlinStringBuilder): KotlinStringBuilder {
199217
return data.apply {
200218
if (!declaration.isFakeOverride && declaration.origin != IrDeclarationOrigin.ENUM_CLASS_SPECIAL_MEMBER && declaration.parentAsClass.primaryConstructor?.parameters?.none { it.name == declaration.name } == true) {
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package tech.mappie.ir.resolving
2+
3+
import org.jetbrains.kotlin.ir.IrElement
4+
import org.jetbrains.kotlin.ir.expressions.IrCall
5+
import org.jetbrains.kotlin.ir.visitors.IrVisitor
6+
import tech.mappie.util.IDENTIFIER_MAPPING
7+
8+
fun findMappingStatements(element: IrElement?): List<IrCall> {
9+
return if (element != null) {
10+
val statements = mutableListOf<IrCall>()
11+
element.accept(MappingStatementsFinder, statements)
12+
statements
13+
} else {
14+
emptyList()
15+
}
16+
}
17+
18+
private object MappingStatementsFinder : IrVisitor<Unit, MutableList<IrCall>>() {
19+
override fun visitElement(element: IrElement, data: MutableList<IrCall>) {
20+
element.acceptChildren(this, data)
21+
}
22+
23+
override fun visitCall(expression: IrCall, data: MutableList<IrCall>) {
24+
when (expression.symbol.owner.name) {
25+
IDENTIFIER_MAPPING -> data.add(expression)
26+
else -> expression.acceptChildren(this, data)
27+
}
28+
}
29+
}

0 commit comments

Comments
 (0)