diff --git a/annotations/src/main/kotlin/io/mcarle/konvert/api/Konverter.kt b/annotations/src/main/kotlin/io/mcarle/konvert/api/Konverter.kt index a90cc92..79eac15 100644 --- a/annotations/src/main/kotlin/io/mcarle/konvert/api/Konverter.kt +++ b/annotations/src/main/kotlin/io/mcarle/konvert/api/Konverter.kt @@ -30,6 +30,10 @@ annotation class Konverter( val options: Array = [] ) { + @Retention(AnnotationRetention.RUNTIME) + @Target(AnnotationTarget.VALUE_PARAMETER) + annotation class Source + /** * This object can be used to load the generated class of an interface, which is annotated with `@Konverter`. */ diff --git a/processor/src/main/kotlin/io/mcarle/konvert/processor/codegen/CodeBuilder.kt b/processor/src/main/kotlin/io/mcarle/konvert/processor/codegen/CodeBuilder.kt index 86ad8bd..e8cedec 100644 --- a/processor/src/main/kotlin/io/mcarle/konvert/processor/codegen/CodeBuilder.kt +++ b/processor/src/main/kotlin/io/mcarle/konvert/processor/codegen/CodeBuilder.kt @@ -22,11 +22,14 @@ class CodeBuilder private constructor( addFunction( funSpec = funBuilder.apply { if (Configuration.addGeneratedKonverterAnnotation) { - addAnnotation( - AnnotationSpec.builder(GeneratedKonverter::class) - .addMember("${GeneratedKonverter::priority.name} = %L", priority) - .build() - ) + // do not add annotation to functions with multiple parameters + if (this.parameters.size <= 1) { + addAnnotation( + AnnotationSpec.builder(GeneratedKonverter::class) + .addMember("${GeneratedKonverter::priority.name} = %L", priority) + .build() + ) + } } }.build(), toType = toType, diff --git a/processor/src/main/kotlin/io/mcarle/konvert/processor/codegen/CodeGenerator.kt b/processor/src/main/kotlin/io/mcarle/konvert/processor/codegen/CodeGenerator.kt index 2928591..2f2e7d3 100644 --- a/processor/src/main/kotlin/io/mcarle/konvert/processor/codegen/CodeGenerator.kt +++ b/processor/src/main/kotlin/io/mcarle/konvert/processor/codegen/CodeGenerator.kt @@ -32,7 +32,8 @@ class CodeGenerator( targetClassImportName: String?, source: KSType, target: KSType, - mappingCodeParentDeclaration: KSDeclaration + mappingCodeParentDeclaration: KSDeclaration, + additionalSourceParameters: List ): CodeBlock { if (paramName != null) { val existingTypeConverter = TypeConverterRegistry @@ -48,7 +49,7 @@ class CodeGenerator( } } - val sourceProperties = PropertyMappingResolver(logger).determinePropertyMappings(paramName, mappings, source) + val sourceProperties = PropertyMappingResolver(logger).determinePropertyMappings(paramName, mappings, source, additionalSourceParameters) val targetClassDeclaration = target.classDeclaration()!! diff --git a/processor/src/main/kotlin/io/mcarle/konvert/processor/codegen/PropertyMappingResolver.kt b/processor/src/main/kotlin/io/mcarle/konvert/processor/codegen/PropertyMappingResolver.kt index d6e05d1..7a11d14 100644 --- a/processor/src/main/kotlin/io/mcarle/konvert/processor/codegen/PropertyMappingResolver.kt +++ b/processor/src/main/kotlin/io/mcarle/konvert/processor/codegen/PropertyMappingResolver.kt @@ -4,9 +4,9 @@ import com.google.devtools.ksp.processing.KSPLogger import com.google.devtools.ksp.symbol.KSClassDeclaration import com.google.devtools.ksp.symbol.KSPropertyDeclaration import com.google.devtools.ksp.symbol.KSType +import com.google.devtools.ksp.symbol.KSValueParameter import io.mcarle.konvert.api.Mapping import io.mcarle.konvert.converter.api.classDeclaration -import io.mcarle.konvert.converter.api.isNullable class PropertyMappingResolver( private val logger: KSPLogger @@ -14,7 +14,8 @@ class PropertyMappingResolver( fun determinePropertyMappings( mappingParamName: String?, mappings: List, - type: KSType + type: KSType, + additionalSourceParameters: List ): List { val classDeclaration = type.classDeclaration()!! val properties = classDeclaration.getAllProperties().toList() @@ -23,11 +24,30 @@ class PropertyMappingResolver( val propertiesWithoutSource = getPropertyMappingsWithoutSource(mappings, mappingParamName) val propertiesWithSource = getPropertyMappingsWithSource(mappings, properties, mappingParamName) + val propertiesFromAdditionalParameters = getPropertyMappingsFromAdditionalParameters(additionalSourceParameters) val propertiesWithoutMappings = getPropertyMappingsWithoutMappings(properties, mappingParamName) - return propertiesWithoutSource + propertiesWithSource + propertiesWithoutMappings + return propertiesWithoutSource + propertiesWithSource + propertiesFromAdditionalParameters + propertiesWithoutMappings } + private fun getPropertyMappingsFromAdditionalParameters( + properties: List, + ) = properties + .map { property -> + val paramName = property.name!!.asString() + PropertyMappingInfo( + mappingParamName = null, + sourceName = null, + targetName = paramName, + constant = paramName, + expression = null, + ignore = false, + enableConverters = emptyList(), + declaration = null, + isBasedOnAnnotation = false + ) + } + private fun getPropertyMappingsWithoutMappings( properties: List, mappingParamName: String? diff --git a/processor/src/main/kotlin/io/mcarle/konvert/processor/konvert/KonvertData.kt b/processor/src/main/kotlin/io/mcarle/konvert/processor/konvert/KonvertData.kt index 85e8540..2c1e38c 100644 --- a/processor/src/main/kotlin/io/mcarle/konvert/processor/konvert/KonvertData.kt +++ b/processor/src/main/kotlin/io/mcarle/konvert/processor/konvert/KonvertData.kt @@ -7,6 +7,7 @@ import com.google.devtools.ksp.symbol.KSClassDeclaration import com.google.devtools.ksp.symbol.KSFunctionDeclaration import com.google.devtools.ksp.symbol.KSType import com.google.devtools.ksp.symbol.KSTypeReference +import com.google.devtools.ksp.symbol.KSValueParameter import io.mcarle.konvert.api.DEFAULT_KONVERTER_PRIORITY import io.mcarle.konvert.api.Konfig import io.mcarle.konvert.api.Konvert @@ -15,12 +16,13 @@ import io.mcarle.konvert.api.Priority import io.mcarle.konvert.converter.api.classDeclaration import io.mcarle.konvert.processor.from -class KonvertData( +class KonvertData constructor( val annotationData: AnnotationData, val isAbstract: Boolean, val sourceTypeReference: KSTypeReference, val targetTypeReference: KSTypeReference, val mapKSFunctionDeclaration: KSFunctionDeclaration, + val additionalParameters: List ) { val sourceType: KSType = sourceTypeReference.resolve() @@ -28,7 +30,7 @@ class KonvertData( val targetType: KSType = targetTypeReference.resolve() val targetClassDeclaration: KSClassDeclaration = targetType.classDeclaration()!! val mapFunctionName: String = mapKSFunctionDeclaration.simpleName.asString() - val paramName: String = mapKSFunctionDeclaration.parameters.first().name!!.asString() + val paramName: String = (mapKSFunctionDeclaration.parameters - additionalParameters).first().name!!.asString() val priority = annotationData.priority diff --git a/processor/src/main/kotlin/io/mcarle/konvert/processor/konvert/KonverterCodeGenerator.kt b/processor/src/main/kotlin/io/mcarle/konvert/processor/konvert/KonverterCodeGenerator.kt index 2a2db32..2d19d2e 100644 --- a/processor/src/main/kotlin/io/mcarle/konvert/processor/konvert/KonverterCodeGenerator.kt +++ b/processor/src/main/kotlin/io/mcarle/konvert/processor/konvert/KonverterCodeGenerator.kt @@ -7,6 +7,7 @@ import com.google.devtools.ksp.symbol.KSType import com.google.devtools.ksp.symbol.KSTypeReference import com.squareup.kotlinpoet.FunSpec import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.ParameterSpec import com.squareup.kotlinpoet.TypeSpec import com.squareup.kotlinpoet.ksp.toTypeName import io.mcarle.konvert.converter.api.config.Configuration @@ -47,7 +48,17 @@ object KonverterCodeGenerator { funBuilder = FunSpec.builder(konvertData.mapFunctionName) .addModifiers(KModifier.OVERRIDE) .returns(konvertData.targetTypeReference.toTypeName()) - .addParameter(konvertData.paramName, konvertData.sourceTypeReference.toTypeName()) + .addParameters(konvertData.mapKSFunctionDeclaration.parameters.map { + val builder = ParameterSpec.builder( + name = it.name!!.asString(), + type = it.type.toTypeName(), + modifiers = emptyArray() + ) + if (it.isVararg) { + builder.addModifiers(KModifier.VARARG) + } + builder.build() + }) .addCode( "return super.${konvertData.mapFunctionName}(${konvertData.paramName})" ), @@ -81,7 +92,17 @@ object KonverterCodeGenerator { funBuilder = FunSpec.builder(konvertData.mapFunctionName) .addModifiers(KModifier.OVERRIDE) .returns(konvertData.targetTypeReference.toTypeName()) - .addParameter(konvertData.paramName, konvertData.sourceTypeReference.toTypeName()) + .addParameters(konvertData.mapKSFunctionDeclaration.parameters.map { + val builder = ParameterSpec.builder( + name = it.name!!.asString(), + type = it.type.toTypeName(), + modifiers = emptyArray() + ) + if (it.isVararg) { + builder.addModifiers(KModifier.VARARG) + } + builder.build() + }) .addCode( mapper.generateCode( konvertData.annotationData.mappings.asIterable().validated(konvertData.mapKSFunctionDeclaration, logger), @@ -90,7 +111,8 @@ object KonverterCodeGenerator { targetClassImportName, konvertData.sourceType, konvertData.targetType, - konvertData.mapKSFunctionDeclaration + konvertData.mapKSFunctionDeclaration, + konvertData.additionalParameters ) ), priority = konvertData.priority, @@ -130,9 +152,11 @@ object KonverterCodeGenerator { } fun toFunctionFullyQualifiedNames(data: KonverterData): List { - return data.konvertData.map { - "${data.mapKSClassDeclaration.qualifiedName?.asString()}Impl.${it.mapFunctionName}" - } + return data.konvertData + .filter { it.additionalParameters.isEmpty() } // filter out mappings with more than one parameter + .map { + "${data.mapKSClassDeclaration.qualifiedName?.asString()}Impl.${it.mapFunctionName}" + } } } diff --git a/processor/src/main/kotlin/io/mcarle/konvert/processor/konvert/KonverterDataCollector.kt b/processor/src/main/kotlin/io/mcarle/konvert/processor/konvert/KonverterDataCollector.kt index fd2def8..52c4df0 100644 --- a/processor/src/main/kotlin/io/mcarle/konvert/processor/konvert/KonverterDataCollector.kt +++ b/processor/src/main/kotlin/io/mcarle/konvert/processor/konvert/KonverterDataCollector.kt @@ -1,11 +1,16 @@ package io.mcarle.konvert.processor.konvert +import com.google.devtools.ksp.KspExperimental import com.google.devtools.ksp.getClassDeclarationByName +import com.google.devtools.ksp.isAnnotationPresent import com.google.devtools.ksp.isPrivate import com.google.devtools.ksp.processing.KSPLogger import com.google.devtools.ksp.processing.Resolver import com.google.devtools.ksp.symbol.ClassKind import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSFunctionDeclaration +import com.google.devtools.ksp.symbol.KSValueParameter +import com.google.devtools.ksp.symbol.Modifier import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.ksp.toTypeName import io.mcarle.konvert.api.Konvert @@ -50,15 +55,19 @@ object KonverterDataCollector { return@mapNotNull null } + if (Modifier.INLINE in it.modifiers) { + // ignore inline functions + return@mapNotNull null + } + if (it.extensionReceiver != null) { // ignore extension functions return@mapNotNull null } - val source = - if (it.parameters.size > 1 || it.parameters.isEmpty()) null - else it.parameters.first().type - val target = it.returnType?.let { returnType -> + val sourceValueParameter = determineSourceParam(it, logger) + val source = sourceValueParameter?.type + val target = it.returnType?.let { returnType -> if (returnType.resolve().declaration == resolver.getClassDeclarationByName()) { null } else { @@ -66,7 +75,6 @@ object KonverterDataCollector { } } - val annotation = it.annotations.firstOrNull { annotation -> (annotation.annotationType.toTypeName() as? ClassName)?.canonicalName == Konvert::class.qualifiedName }?.let { annotation -> @@ -85,7 +93,8 @@ object KonverterDataCollector { isAbstract = true, sourceTypeReference = source, targetTypeReference = target, - mapKSFunctionDeclaration = it + mapKSFunctionDeclaration = it, + additionalParameters = determineAdditionalParams(it, sourceValueParameter) ) } else if (source != null && target != null) { KonvertData( @@ -93,7 +102,8 @@ object KonverterDataCollector { isAbstract = it.isAbstract, sourceTypeReference = source, targetTypeReference = target, - mapKSFunctionDeclaration = it + mapKSFunctionDeclaration = it, + additionalParameters = determineAdditionalParams(it, sourceValueParameter) ) } else if (it.isAbstract) { throw RuntimeException("Method $it is abstract and does not meet criteria for automatic source and target detection") @@ -108,4 +118,33 @@ object KonverterDataCollector { }.toList() } + @OptIn(KspExperimental::class) + private fun determineSourceParam(function: KSFunctionDeclaration, logger: KSPLogger): KSValueParameter? { + val parameters = function.parameters + return when { + parameters.isEmpty() -> null + parameters.size > 1 -> { + val sourceParameter = parameters.filter { it.isAnnotationPresent(Konverter.Source::class) } + when { + sourceParameter.isEmpty() -> null + sourceParameter.size > 1 -> { + logger.error("Ignored method as multiple parameters were annotated with @Konverter.Source", function) + null + } + + else -> sourceParameter.first() + } + } + + else -> parameters.first() + } + } + + @OptIn(KspExperimental::class) + private fun determineAdditionalParams(function: KSFunctionDeclaration, sourceParam: KSValueParameter?): List { + return function.parameters + .filterNot { it.isAnnotationPresent(Konverter.Source::class) } + .filterNot { it == sourceParam } + } + } diff --git a/processor/src/main/kotlin/io/mcarle/konvert/processor/konvertfrom/KonvertFromCodeGenerator.kt b/processor/src/main/kotlin/io/mcarle/konvert/processor/konvertfrom/KonvertFromCodeGenerator.kt index 1751047..48bf87c 100644 --- a/processor/src/main/kotlin/io/mcarle/konvert/processor/konvertfrom/KonvertFromCodeGenerator.kt +++ b/processor/src/main/kotlin/io/mcarle/konvert/processor/konvertfrom/KonvertFromCodeGenerator.kt @@ -36,7 +36,8 @@ object KonvertFromCodeGenerator { data.targetClassDeclaration.simpleName.asString(), data.sourceClassDeclaration.asStarProjectedType(), data.targetClassDeclaration.asStarProjectedType(), - data.targetCompanionDeclaration + data.targetCompanionDeclaration, + emptyList() ) ), priority = data.priority, diff --git a/processor/src/main/kotlin/io/mcarle/konvert/processor/konvertto/KonvertToCodeGenerator.kt b/processor/src/main/kotlin/io/mcarle/konvert/processor/konvertto/KonvertToCodeGenerator.kt index 953334e..55ab1c7 100644 --- a/processor/src/main/kotlin/io/mcarle/konvert/processor/konvertto/KonvertToCodeGenerator.kt +++ b/processor/src/main/kotlin/io/mcarle/konvert/processor/konvertto/KonvertToCodeGenerator.kt @@ -42,7 +42,8 @@ object KonvertToCodeGenerator { targetClassImportName, data.sourceClassDeclaration.asStarProjectedType(), data.targetClassDeclaration.asStarProjectedType(), - data.sourceClassDeclaration + data.sourceClassDeclaration, + emptyList() ) ), priority = data.priority, diff --git a/processor/src/test/kotlin/io/mcarle/konvert/processor/MetaInfKonverterResourcesITest.kt b/processor/src/test/kotlin/io/mcarle/konvert/processor/MetaInfKonverterResourcesITest.kt index e23f060..9482b4c 100644 --- a/processor/src/test/kotlin/io/mcarle/konvert/processor/MetaInfKonverterResourcesITest.kt +++ b/processor/src/test/kotlin/io/mcarle/konvert/processor/MetaInfKonverterResourcesITest.kt @@ -3,6 +3,7 @@ package io.mcarle.konvert.processor import com.tschuchort.compiletesting.SourceFile import io.mcarle.konvert.converter.SameTypeConverter import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource @@ -185,4 +186,29 @@ interface Mapper: IMapper { ) } + @Test + fun doNotGenerateLineForKonverterFunctionsWithMultipleParameters() { + val (compilation) = compileWith( + enabledConverters = listOf(SameTypeConverter()), + code = SourceFile.kotlin( + name = "TestCode.kt", + contents = + """ +import io.mcarle.konvert.api.Konverter + +class SourceClass(val property: String) +class TargetClass(val property: String, val other: Int) + +@Konverter +interface Mapper { + fun toTarget(@Konverter.Source source: SourceClass, other: Int): TargetClass +} + """.trimIndent() + ) + ) + assertThrows { + compilation.generatedSourceFor("io.mcarle.konvert.api.Konvert") + } + } + } diff --git a/processor/src/test/kotlin/io/mcarle/konvert/processor/konvert/KonvertITest.kt b/processor/src/test/kotlin/io/mcarle/konvert/processor/konvert/KonvertITest.kt index eda02a4..8032ee6 100644 --- a/processor/src/test/kotlin/io/mcarle/konvert/processor/konvert/KonvertITest.kt +++ b/processor/src/test/kotlin/io/mcarle/konvert/processor/konvert/KonvertITest.kt @@ -1215,4 +1215,49 @@ interface Mapper { ) } + @Test + fun allowMultipleFunctionParametersIfOneIsAnnotatedWithSource() { + addGeneratedKonverterAnnotation = true + val (compilation) = compileWith( + enabledConverters = listOf(SameTypeConverter()), + code = arrayOf( + SourceFile.kotlin( + contents = + """ +import io.mcarle.konvert.api.Konverter + +class SourceClass(val property: String) +class TargetClass(val property: String, val otherValue: Int) + +@Konverter +interface Mapper { + fun toTarget(@Konverter.Source source: SourceClass, otherValue: Int, vararg furtherParams: String): TargetClass +} + """.trimIndent(), + name = "TestCode.kt" + ) + ) + ) + val extensionFunctionCode = compilation.generatedSourceFor("MapperKonverter.kt") + println(extensionFunctionCode) + + assertSourceEquals( + """ + import kotlin.Int + import kotlin.String + + public object MapperImpl : Mapper { + override fun toTarget( + source: SourceClass, + otherValue: Int, + vararg furtherParams: String, + ): TargetClass = TargetClass( + property = source.property, + otherValue = otherValue + ) + } + """.trimIndent(), extensionFunctionCode + ) + } + }