Skip to content

Commit

Permalink
fix lambda types generated as FunctionX type. Closes #60
Browse files Browse the repository at this point in the history
  • Loading branch information
F43nd1r committed Aug 23, 2024
1 parent 27a96df commit a87cf80
Show file tree
Hide file tree
Showing 7 changed files with 170 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,16 @@ package com.faendir.kotlin.autodsl.kapt

import com.faendir.kotlin.autodsl.SourceInfoResolver
import com.faendir.kotlin.autodsl.nonnull
import com.faendir.kotlin.autodsl.toRawType
import com.google.devtools.ksp.symbol.ClassKind
import com.squareup.kotlinpoet.*
import com.squareup.kotlinpoet.metadata.*
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.LambdaTypeName
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.asClassName
import com.squareup.kotlinpoet.asTypeName
import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview
import com.squareup.kotlinpoet.metadata.toKmClass
import kotlinx.metadata.KmClass
import kotlinx.metadata.KmConstructor
import kotlinx.metadata.KmValueParameter
Expand Down Expand Up @@ -36,7 +43,7 @@ class KaptSourceInfoResolver(private val processingEnv: ProcessingEnvironment, p
override fun getClassesWithAnnotation(annotation: Type): List<Type> =
roundEnv.getElementsAnnotatedWith(annotation.element).filterIsInstance<TypeElement>().map { Type(it) }

override fun Type.getClassKind(): ClassKind = when(kmClass.kind) {
override fun Type.getClassKind(): ClassKind = when (kmClass.kind) {
kotlinx.metadata.ClassKind.CLASS -> ClassKind.CLASS
kotlinx.metadata.ClassKind.INTERFACE -> ClassKind.INTERFACE
kotlinx.metadata.ClassKind.ENUM_CLASS -> ClassKind.ENUM_CLASS
Expand Down Expand Up @@ -69,6 +76,8 @@ class KaptSourceInfoResolver(private val processingEnv: ProcessingEnvironment, p
if (eType is ParameterizedTypeName && kType is ParameterizedTypeName) {
//Invariant kotlin parameters are variant in java, just check erased type
eType.rawType == kType.rawType
} else if (eType is ParameterizedTypeName && kType is LambdaTypeName) {
eType.typeArguments.map { it.toRawType() } == listOfNotNull(kType.receiver) + kType.parameters.map { it.type } + kType.returnType
} else {
eType == kType
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,56 @@
package com.faendir.kotlin.autodsl.kapt

import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.LambdaTypeName
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.STAR
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.WildcardTypeName
import com.squareup.kotlinpoet.metadata.*
import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview
import io.github.enjoydambience.kotlinbard.nullable
import kotlinx.metadata.*
import kotlinx.metadata.KmClass
import kotlinx.metadata.KmClassifier
import kotlinx.metadata.KmType
import kotlinx.metadata.KmTypeProjection
import kotlinx.metadata.KmVariance
import kotlinx.metadata.isNullable
import kotlinx.metadata.jvm.annotations

fun KmClass.asClassName() = ClassName.bestGuess(this.name.replace("/", "."))

fun KmType.asTypeName(): TypeName {
val className = when (val kmClassifier = classifier) {
is KmClassifier.Class -> ClassName.bestGuess(kmClassifier.name.replace("/", "."))
var name = when (val kmClassifier = classifier) {
is KmClassifier.Class -> {
if (kmClassifier.name.matches(Regex("kotlin/Function\\d+"))) {
val arguments = arguments.mapNotNullTo(mutableListOf()) { it.type?.asTypeName() }
val receiver =
if (annotations.any { it.className.replace("/", ".") == ExtensionFunctionType::class.qualifiedName }) arguments.removeFirst() else null
val returnType = arguments.removeLast()
LambdaTypeName.get(receiver = receiver, returnType = returnType, parameters = arguments.toTypedArray())
} else {
ClassName.bestGuess(kmClassifier.name.replace("/", "."))
}
}

is KmClassifier.TypeAlias -> ClassName.bestGuess(kmClassifier.name.replace("/", "."))
else -> throw IllegalArgumentException()
}
var typeName = if (arguments.isNotEmpty()) className.parameterizedBy(arguments.map { it.asTypeName() }) else className
if (isNullable) typeName = typeName.nullable
return typeName
if (arguments.isNotEmpty() && name is ClassName) {
name = name.parameterizedBy(arguments.map { it.asTypeName() })
}
if (isNullable) name = name.nullable
return name
}

fun KmTypeProjection.asTypeName(): TypeName = when (variance) {
null, KmVariance.INVARIANT -> {
type?.asTypeName()?.copy(nullable = type!!.isNullable) ?: STAR
}

KmVariance.IN -> {
WildcardTypeName.consumerOf(type!!.asTypeName().copy(nullable = type!!.isNullable))
}

KmVariance.OUT -> {
WildcardTypeName.producerOf(type!!.asTypeName().copy(nullable = type!!.isNullable))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package com.faendir.kotlin.autodsl

import com.squareup.kotlinpoet.*
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.LambdaTypeName
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.WildcardTypeName
import com.squareup.kotlinpoet.asClassName

@Suppress("UNCHECKED_CAST")
val <T : TypeName> T.nonnull: T
Expand All @@ -9,16 +14,13 @@ val <T : TypeName> T.nonnull: T
fun TypeName.toRawType(): ClassName = when (this) {
is ParameterizedTypeName -> this.rawType
is ClassName -> this
else -> throw IllegalArgumentException()
is WildcardTypeName -> this.inTypes.firstOrNull()?.toRawType() ?: this.outTypes.first().toRawType()
is LambdaTypeName -> ClassName("kotlin", "Function${(if (receiver != null) 1 else 0) + parameters.size}")
else -> throw IllegalArgumentException("Unsupported conversion to raw type from $this")
}

fun ClassName.withBuilderSuffix() = ClassName(packageName, "${simpleName}Builder")

fun TypeName.withBuilderSuffix() = toRawType().withBuilderSuffix()

fun TypeName.asLambdaReceiver() = LambdaTypeName.get(receiver = this, returnType = Unit::class.asClassName())

inline fun <reified T : Annotation> ParameterSpec.hasAnnotation(): Boolean {
val typeName = T::class.asTypeName()
return this.annotations.any { it.typeName == typeName }
}
fun TypeName.asLambdaReceiver() = LambdaTypeName.get(receiver = this, returnType = Unit::class.asClassName())
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
package com.faendir.kotlin.autodsl.ksp

import com.faendir.kotlin.autodsl.*
import com.faendir.kotlin.autodsl.SourceInfoResolver
import com.google.devtools.ksp.isInternal
import com.google.devtools.ksp.isPublic
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.symbol.*
import com.google.devtools.ksp.symbol.ClassKind
import com.google.devtools.ksp.symbol.KSAnnotated
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.KSValueParameter
import com.google.devtools.ksp.symbol.Modifier
import com.google.devtools.ksp.validate
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.TypeName
import kotlin.reflect.KClass
import kotlin.reflect.KProperty1
import com.google.devtools.ksp.getConstructors as superGetConstructors
import com.faendir.kotlin.autodsl.ksp.asClassName as asClassNameUtil
import com.google.devtools.ksp.getConstructors as superGetConstructors

class KspSourceInfoResolver(private val resolver: Resolver) : SourceInfoResolver<KSAnnotated, KSClassDeclaration, KSFunctionDeclaration, KSValueParameter> {
private fun getClassesWithAnnotation(annotation: String): List<KSClassDeclaration> =
Expand All @@ -23,8 +29,7 @@ class KspSourceInfoResolver(private val resolver: Resolver) : SourceInfoResolver

override fun KSClassDeclaration.getClassKind(): ClassKind = classKind

override fun KSAnnotated.hasAnnotation(annotation: KClass<out Annotation>): Boolean =
annotations.filter { it.couldBe(annotation) }.any { it.isEqualTo(annotation) }
override fun KSAnnotated.hasAnnotation(annotation: KClass<out Annotation>): Boolean = findAnnotation(annotation) != null

override fun <T : Annotation> KSAnnotated.getAnnotationTypeProperty(annotation: KClass<T>, property: KProperty1<T, KClass<*>>): ClassName? =
(findAnnotation(annotation)?.arguments?.firstOrNull { it.name?.asString() == property.name }?.value as? KSType?)?.asClassNameUtil()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package com.faendir.kotlin.autodsl.ksp

import com.google.devtools.ksp.symbol.*
import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSAnnotation
import com.google.devtools.ksp.symbol.KSDeclaration
import com.google.devtools.ksp.symbol.KSType
import com.google.devtools.ksp.symbol.KSTypeReference
import com.google.devtools.ksp.symbol.Variance
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.LambdaTypeName
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.STAR
import com.squareup.kotlinpoet.TypeName
Expand Down Expand Up @@ -32,7 +38,12 @@ fun KSTypeReference.asTypeName() = resolve().asTypeName()

fun KSType.asTypeName(): TypeName {
var name: TypeName = asClassName()
if (arguments.isNotEmpty()) {
if (declaration.qualifiedName?.asString()?.matches(Regex("kotlin\\.Function\\d+")) == true) {
val arguments = arguments.mapNotNullTo(mutableListOf()) { it.type?.asTypeName() }
val receiver = if (annotations.any { it.couldBe(ExtensionFunctionType::class) }) arguments.removeFirst() else null
val returnType = arguments.removeLast()
name = LambdaTypeName.get(receiver = receiver, returnType = returnType, parameters = arguments.toTypedArray())
} else if (arguments.isNotEmpty()) {
name = (name as ClassName).parameterizedBy(arguments.map {
when (it.variance) {
Variance.STAR -> STAR
Expand Down
92 changes: 92 additions & 0 deletions processor/src/test/kotlin/com/faendir/kotlin/autodsl/LambdaTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.faendir.kotlin.autodsl

import org.junit.jupiter.api.TestFactory

class LambdaTest {

@TestFactory
fun `simple lambda`() = compile(
"""
import com.faendir.kotlin.autodsl.AutoDsl
@AutoDsl
class Entity(val a: () -> String)
""",
"""
import strikt.api.expectThat
import strikt.assertions.isEqualTo
fun test() {
expectThat(entity {
a = {"Hi"}
}.a()).isEqualTo("Hi")
}
"""
)

@TestFactory
fun `lambda with parameter`() = compile(
"""
import com.faendir.kotlin.autodsl.AutoDsl
@AutoDsl
class Entity(val a: (name: String) -> String)
""",
"""
import strikt.api.expectThat
import strikt.assertions.isEqualTo
fun test() {
expectThat(entity {
a = { "Hi ${'$'}it" }
}.a("F43nd1r")).isEqualTo("Hi F43nd1r")
}
"""
)

@TestFactory
fun `lambda with receiver`() = compile(
"""
import com.faendir.kotlin.autodsl.AutoDsl
@AutoDsl
class Entity(val a: String.() -> String)
""",
"""
import strikt.api.expectThat
import strikt.assertions.isEqualTo
fun test() {
expectThat(entity {
a = { "Hi ${'$'}this" }
}.a("F43nd1r")).isEqualTo("Hi F43nd1r")
}
"""
)

@TestFactory
fun `lambda with default value`() = compile(
"""
import com.faendir.kotlin.autodsl.AutoDsl
@AutoDsl
class Entity(val a: (name: String) -> String = { "Hi ${'$'}it" })
""",
"""
import strikt.api.expectThat
import strikt.assertions.isEqualTo
fun test() {
expectThat(entity {}.a("F43nd1r")).isEqualTo("Hi F43nd1r")
}
"""
)

@TestFactory
fun `lambda with receiver and default value`() = compile(
"""
import com.faendir.kotlin.autodsl.AutoDsl
@AutoDsl
class Entity(val a: String.() -> String = { "Hi ${'$'}this" })
""",
"""
import strikt.api.expectThat
import strikt.assertions.isEqualTo
fun test() {
expectThat(entity {}.a("F43nd1r")).isEqualTo("Hi F43nd1r")
}
"""
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ fun compileKsp(
): List<File> {
val compilation = KotlinCompilation().apply {
inheritClassPath = true
jvmTarget = "1.8"
jvmTarget = "17"
sources = listOf(kotlin("Source.kt", source))
configureKsp(useKsp2 = true) {
symbolProcessorProviders.add(KspProcessorProvider())
Expand All @@ -56,7 +56,7 @@ fun compileKsp(
expectThat(pass1).get(JvmCompilationResult::exitCode).isEqualTo(expect)
val pass2 = KotlinCompilation().apply {
inheritClassPath = true
jvmTarget = "1.8"
jvmTarget = "17"
sources = compilation.sources + pass1.kspGeneratedSources.map { fromPath(it) } + kotlin("Eval.kt", eval)
}.compile()
expectThat(pass2).get(JvmCompilationResult::exitCode).isEqualTo(KotlinCompilation.ExitCode.OK)
Expand All @@ -71,7 +71,7 @@ fun compileKapt(
): List<File> {
val result = KotlinCompilation().apply {
inheritClassPath = true
jvmTarget = "1.8"
jvmTarget = "17"
sources = listOf(kotlin("Source.kt", source), kotlin("Eval.kt", eval))
useKapt4 = true
annotationProcessors = listOf(KaptProcessor())
Expand All @@ -84,7 +84,7 @@ fun compileKapt(
}

private fun JvmCompilationResult.callEval() = classLoader.loadClass("EvalKt").declaredMethods
.first { it.name[0] != '$' /* skip jacoco added function */ }
.first { !it.name.contains('$') /* skip generated functions */ }
.run {
isAccessible = true
invoke(null)
Expand Down

0 comments on commit a87cf80

Please sign in to comment.