diff --git a/kategory-annotations-processor/src/main/java/kategory/fold/AnnotatedAutoFold.kt b/kategory-annotations-processor/src/main/java/kategory/fold/AnnotatedAutoFold.kt new file mode 100644 index 00000000000..3fbc0ef64c0 --- /dev/null +++ b/kategory-annotations-processor/src/main/java/kategory/fold/AnnotatedAutoFold.kt @@ -0,0 +1,12 @@ +package kategory.fold + +import kategory.common.utils.ClassOrPackageDataWrapper +import javax.lang.model.element.TypeElement + +data class AnnotatedFold( + val type: TypeElement, + val typeParams: List, + val classData: ClassOrPackageDataWrapper.Class, + val targets: List +) +data class Variant(val fullName: String, val typeParams: List, val simpleName: String) diff --git a/kategory-annotations-processor/src/main/java/kategory/fold/AnnotationInfo.kt b/kategory-annotations-processor/src/main/java/kategory/fold/AnnotationInfo.kt new file mode 100644 index 00000000000..04db4669166 --- /dev/null +++ b/kategory-annotations-processor/src/main/java/kategory/fold/AnnotationInfo.kt @@ -0,0 +1,7 @@ +package kategory.fold + +import kategory.autofold + +val foldAnnotationKClass = autofold::class +val foldAnnotationClass = foldAnnotationKClass.java +val foldAnnotationName = "@" + foldAnnotationClass.simpleName diff --git a/kategory-annotations-processor/src/main/java/kategory/fold/AutoFoldFileGenerator.kt b/kategory-annotations-processor/src/main/java/kategory/fold/AutoFoldFileGenerator.kt new file mode 100644 index 00000000000..fe013011b2c --- /dev/null +++ b/kategory-annotations-processor/src/main/java/kategory/fold/AutoFoldFileGenerator.kt @@ -0,0 +1,64 @@ +package kategory.fold + +import kategory.common.utils.fullName +import me.eugeniomarletti.kotlin.metadata.escapedClassName +import java.io.File + +class AutoFoldFileGenerator( + private val annotatedList: Collection, + private val generatedDir: File +) { + + fun generate() = annotatedList.map(this::processElement) + .map { (element, fold) -> + "${foldAnnotationClass.simpleName}.${element.type.simpleName.toString().toLowerCase()}.kt" to + fileHeader(element.classData.`package`.escapedClassName) + fold + }.map { (name, fileString) -> File(generatedDir, name).writeText(fileString) } + + private fun processElement(annotatedFold: AnnotatedFold): Pair = + annotatedFold to annotatedFold.targets.let { targets -> + val sourceClassName = annotatedFold.classData.fullName.escapedClassName + val sumTypeParams = typeParams(annotatedFold.typeParams) + val returnType = getFoldType(annotatedFold.typeParams) + val functionTypeParams = functionTypeParams(annotatedFold.typeParams, returnType) + + """inline fun $functionTypeParams $sourceClassName$sumTypeParams.fold( + |${params(targets, returnType)} + |): $returnType = when (this) { + |${patternMatching(targets)} + |} + """.trimMargin() + } + + fun typeParams(params: List): String = + if (params.isNotEmpty()) params.joinToString(prefix = "<", postfix = ">") + else "" + + fun params(variants: List, returnType: String): String = variants.joinToString(transform = { variant -> + " crossinline ${variant.simpleName.decapitalize()}: (${variant.fullName.escapedClassName}${typeParams(variant.typeParams)}) -> $returnType" + }, separator = ",\n") + + fun patternMatching(variants: List): String = variants.joinToString(transform = { variant -> + " is ${variant.fullName.escapedClassName} -> ${variant.simpleName.decapitalize().escapedClassName}(this)" + }, separator = "\n") + + fun functionTypeParams(params: List, returnType: String): String = + if (params.isEmpty()) "" + else params.joinToString(prefix = "<", postfix = ", $returnType>") + + fun getFoldType(params: List): String { + fun check(param: String, next: List): String = (param[0] + 1).let { + if (next.contains(it.toString())) check(next.firstOrNull() ?: "", next.drop(1)) + else it.toString() + } + + return check(params.firstOrNull() ?: "", params.drop(1)) + } + + fun fileHeader(packageName: String): String = + """package $packageName + | + |""".trimMargin() + +} + diff --git a/kategory-annotations-processor/src/main/java/kategory/fold/AutoFoldProcessor.kt b/kategory-annotations-processor/src/main/java/kategory/fold/AutoFoldProcessor.kt new file mode 100644 index 00000000000..ab44cd3cd3c --- /dev/null +++ b/kategory-annotations-processor/src/main/java/kategory/fold/AutoFoldProcessor.kt @@ -0,0 +1,65 @@ +package kategory.fold + +import com.google.auto.service.AutoService +import kategory.common.utils.AbstractProcessor +import kategory.common.utils.asClassOrPackageDataWrapper +import kategory.common.utils.isSealed +import kategory.common.utils.knownError +import me.eugeniomarletti.kotlin.metadata.KotlinClassMetadata +import me.eugeniomarletti.kotlin.metadata.kotlinMetadata +import java.io.File +import javax.annotation.processing.Processor +import javax.annotation.processing.RoundEnvironment +import javax.lang.model.SourceVersion +import javax.lang.model.element.TypeElement +import javax.lang.model.element.TypeParameterElement + +@AutoService(Processor::class) +class AutoFoldProcessor : AbstractProcessor() { + + private val annotatedList = mutableListOf() + + override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latestSupported() + + override fun getSupportedAnnotationTypes(): Set = setOf(foldAnnotationClass.canonicalName) + + /** + * Processor entry point + */ + override fun onProcess(annotations: Set, roundEnv: RoundEnvironment) { + annotatedList += roundEnv + .getElementsAnnotatedWith(foldAnnotationClass) + .map { element -> + when { + element.let { it.kotlinMetadata as? KotlinClassMetadata }?.data?.classProto?.isSealed == true -> { + val (nameResolver, classProto) = element.kotlinMetadata.let { it as KotlinClassMetadata }.data + + AnnotatedFold( + element as TypeElement, + element.typeParameters.map(TypeParameterElement::toString), + element.kotlinMetadata + .let { it as KotlinClassMetadata } + .data + .asClassOrPackageDataWrapper(elementUtils.getPackageOf(element).toString()), + classProto.sealedSubclassFqNameList + .map(nameResolver::getString) + .map { it.replace('/', '.') } + .map { + Variant(it, + elementUtils.getTypeElement(it).typeParameters.map(TypeParameterElement::toString), + it.substringAfterLast(".")) + } + ) + } + + else -> knownError("Generation of fold is only supported for sealed classes.") + } + } + + if (roundEnv.processingOver()) { + val generatedDir = File(this.generatedDir!!, foldAnnotationClass.simpleName).also { it.mkdirs() } + AutoFoldFileGenerator(annotatedList, generatedDir).generate() + } + } + +} diff --git a/kategory-annotations/src/main/java/kategory/autofold.kt b/kategory-annotations/src/main/java/kategory/autofold.kt new file mode 100644 index 00000000000..5c02f4f0f52 --- /dev/null +++ b/kategory-annotations/src/main/java/kategory/autofold.kt @@ -0,0 +1,6 @@ +package kategory + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.CLASS) +@MustBeDocumented +annotation class autofold \ No newline at end of file