From 66061116883f40feea55dbbc6d38dd0afda27626 Mon Sep 17 00:00:00 2001 From: Selim Dincer Date: Thu, 25 Aug 2022 15:18:47 +0200 Subject: [PATCH] prepare 4.0 with ksp. closes #6 --- build.gradle | 2 +- codegen/build.gradle | 2 +- .../codegen/GoldfinchSymbolProcessor.kt | 138 ++++++++++++++ .../GoldfinchSymbolProcessorProvider.kt | 11 ++ .../goldfinch/codegen/ServiceProcessor.kt | 171 ------------------ ...ols.ksp.processing.SymbolProcessorProvider | 1 + .../javax.annotation.processing.Processor | 1 - example/build.gradle | 17 +- .../co/selim/goldfinch/example/Sample.kt | 5 +- 9 files changed, 169 insertions(+), 179 deletions(-) create mode 100644 codegen/src/main/kotlin/co/selim/goldfinch/codegen/GoldfinchSymbolProcessor.kt create mode 100644 codegen/src/main/kotlin/co/selim/goldfinch/codegen/GoldfinchSymbolProcessorProvider.kt delete mode 100644 codegen/src/main/kotlin/co/selim/goldfinch/codegen/ServiceProcessor.kt create mode 100644 codegen/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider delete mode 100644 codegen/src/main/resources/META-INF/services/javax.annotation.processing.Processor diff --git a/build.gradle b/build.gradle index 9c2ef16..49b2c83 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ allprojects { apply plugin: 'maven-publish' group 'co.selim.goldfinch' - version '3.0.1' + version '4.0.0' java { withSourcesJar() diff --git a/codegen/build.gradle b/codegen/build.gradle index 59f0d0d..6ba666c 100644 --- a/codegen/build.gradle +++ b/codegen/build.gradle @@ -8,8 +8,8 @@ repositories { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib" + implementation "com.google.devtools.ksp:symbol-processing-api:1.7.10-1.0.6" implementation "com.squareup:kotlinpoet:1.12.0" - implementation "com.squareup:kotlinpoet-metadata:1.12.0" implementation project(':annotation') } diff --git a/codegen/src/main/kotlin/co/selim/goldfinch/codegen/GoldfinchSymbolProcessor.kt b/codegen/src/main/kotlin/co/selim/goldfinch/codegen/GoldfinchSymbolProcessor.kt new file mode 100644 index 0000000..2e39be2 --- /dev/null +++ b/codegen/src/main/kotlin/co/selim/goldfinch/codegen/GoldfinchSymbolProcessor.kt @@ -0,0 +1,138 @@ +package co.selim.goldfinch.codegen + +import co.selim.goldfinch.annotation.GenerateProperties +import co.selim.goldfinch.annotation.Level +import co.selim.goldfinch.annotation.Visibility +import com.google.devtools.ksp.KspExperimental +import com.google.devtools.ksp.getAnnotationsByType +import com.google.devtools.ksp.getVisibility +import com.google.devtools.ksp.processing.* +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSNode +import com.google.devtools.ksp.symbol.KSType +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.TypeName + +class GoldfinchSymbolProcessor(private val logger: KSPLogger, private val codeGenerator: CodeGenerator) : SymbolProcessor { + + private fun ClassName.safelyParameterizedBy(typeNames: List?): TypeName { + return if (typeNames.isNullOrEmpty()) { + this + } else { + this.parameterizedBy(typeNames) + } + } + + private fun logError(msg: String, node: KSNode? = null): Nothing { + logger.error(msg, node) + error(msg) + } + + private fun generatePropertyExtension( + receiver: ClassName, + properties: Map, + visibilityModifier: KModifier, + nestProperties: Boolean, + ): FileSpec { + val sealedInterfaceName = ClassName(receiver.packageName, "${receiver.simpleName}Property") + val sealedInterface = generateSealedInterface(sealedInterfaceName, visibilityModifier) + val propertySpecs = properties.map { (propertyName, propertyType) -> + generatePropertyContainer(sealedInterfaceName, propertyName, propertyType, visibilityModifier, nestProperties) + } + return generateFile(receiver) + .apply { + if (nestProperties) + addType(sealedInterface.addTypes(propertySpecs).build()) + else { + addType(sealedInterface.build()) + propertySpecs.forEach(::addType) + } + } + .addProperty( + generatePropertyMapper( + sealedInterfaceName, + receiver, + properties, + visibilityModifier, + nestProperties + ) + ) + .build() + } + + private fun KSType.getFullType(): TypeName { + val typeParams = arguments.mapNotNull { argument -> + val resolvedType = argument.type?.resolve() + + resolvedType?.getFullType()?.copy(nullable = resolvedType.isMarkedNullable) + } + + val fullName = declaration.qualifiedName!!.asString() + val simpleName = fullName.substringAfter(declaration.packageName.asString()) + return ClassName(declaration.packageName.asString(), simpleName) + .safelyParameterizedBy(typeParams) + .copy(nullable = isMarkedNullable) + } + + @OptIn(KspExperimental::class) + override fun process(resolver: Resolver): List { + resolver.getSymbolsWithAnnotation(GenerateProperties::class.java.name) + .filterIsInstance(KSClassDeclaration::class.java) + .forEach { ksClassDeclaration -> + val annotatedClass = ksClassDeclaration.qualifiedName!!.asString() + val properties = ksClassDeclaration.getAllProperties() + .associate { ksPropertyDeclaration -> + val propertyName = ksPropertyDeclaration.simpleName.getShortName() + propertyName to ksPropertyDeclaration.type.resolve().getFullType() + } + + val annotation = ksClassDeclaration.getAnnotationsByType(GenerateProperties::class).first() + val visibilityModifier = annotation.visibility.toKModifier(ksClassDeclaration) + val dependencies = Dependencies(true, ksClassDeclaration.containingFile!!) + + codeGenerator.createNewFile( + dependencies, + ksClassDeclaration.packageName.asString(), + "${annotatedClass}Properties" + ) + .bufferedWriter() + .use { writer -> + generatePropertyExtension( + ClassName.bestGuess(annotatedClass), + properties, + visibilityModifier, + annotation.level == Level.NESTED + ).writeTo(writer) + } + } + + return emptyList() + } + + + private fun Visibility.toKModifier(classDeclaration: KSClassDeclaration): KModifier { + return when (this) { + Visibility.PUBLIC -> KModifier.PUBLIC + Visibility.INTERNAL -> KModifier.INTERNAL + Visibility.INHERIT -> classDeclaration.getVisibilityKModifier() + } + } + + private fun KSClassDeclaration.getVisibilityKModifier(): KModifier { + return when (val visibility = this.getVisibility()) { + com.google.devtools.ksp.symbol.Visibility.PUBLIC -> KModifier.PUBLIC + com.google.devtools.ksp.symbol.Visibility.PROTECTED -> KModifier.PROTECTED + com.google.devtools.ksp.symbol.Visibility.INTERNAL -> KModifier.INTERNAL + com.google.devtools.ksp.symbol.Visibility.PRIVATE, + com.google.devtools.ksp.symbol.Visibility.LOCAL, + com.google.devtools.ksp.symbol.Visibility.JAVA_PACKAGE -> { + val msg = "Visibility '$visibility' on class '${this.qualifiedName!!.asString()}' is not supported" + logError(msg, this) + } + } + } +} diff --git a/codegen/src/main/kotlin/co/selim/goldfinch/codegen/GoldfinchSymbolProcessorProvider.kt b/codegen/src/main/kotlin/co/selim/goldfinch/codegen/GoldfinchSymbolProcessorProvider.kt new file mode 100644 index 0000000..0af035a --- /dev/null +++ b/codegen/src/main/kotlin/co/selim/goldfinch/codegen/GoldfinchSymbolProcessorProvider.kt @@ -0,0 +1,11 @@ +package co.selim.goldfinch.codegen + +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.processing.SymbolProcessorProvider + +class GoldfinchSymbolProcessorProvider : SymbolProcessorProvider { + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { + return GoldfinchSymbolProcessor(environment.logger, environment.codeGenerator) + } +} diff --git a/codegen/src/main/kotlin/co/selim/goldfinch/codegen/ServiceProcessor.kt b/codegen/src/main/kotlin/co/selim/goldfinch/codegen/ServiceProcessor.kt deleted file mode 100644 index 1e88d03..0000000 --- a/codegen/src/main/kotlin/co/selim/goldfinch/codegen/ServiceProcessor.kt +++ /dev/null @@ -1,171 +0,0 @@ -package co.selim.goldfinch.codegen - -import co.selim.goldfinch.annotation.GenerateProperties -import co.selim.goldfinch.annotation.Level -import co.selim.goldfinch.annotation.Visibility -import com.squareup.kotlinpoet.ClassName -import com.squareup.kotlinpoet.FileSpec -import com.squareup.kotlinpoet.KModifier -import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy -import com.squareup.kotlinpoet.TypeName -import com.squareup.kotlinpoet.metadata.* -import kotlinx.metadata.KmClass -import kotlinx.metadata.KmClassifier -import kotlinx.metadata.KmProperty -import kotlinx.metadata.KmType -import javax.annotation.processing.AbstractProcessor -import javax.annotation.processing.RoundEnvironment -import javax.annotation.processing.SupportedAnnotationTypes -import javax.annotation.processing.SupportedOptions -import javax.lang.model.SourceVersion -import javax.lang.model.element.Element -import javax.lang.model.element.TypeElement -import javax.lang.model.util.ElementFilter -import javax.tools.Diagnostic - -@KotlinPoetMetadataPreview -@SupportedOptions("kapt.kotlin.generated") -@SupportedAnnotationTypes("co.selim.goldfinch.annotation.GenerateProperties") -class ServiceProcessor : AbstractProcessor() { - - override fun process(annotations: Set, roundEnv: RoundEnvironment): Boolean { - val elements = roundEnv.getElementsAnnotatedWith(GenerateProperties::class.java) - if (elements.isEmpty()) return false - - val typeElements = ElementFilter.typesIn(elements) - - typeElements.forEach { typeElement -> - val metadataClass = typeElement.toKmClass() - val propertiesWithTypes = metadataClass.properties - .sortedBy(typeElement.enclosedElements) - .filter { it.returnType.classifier is KmClassifier.Class } - .associate { property -> - val classifier = property.returnType.classifier as KmClassifier.Class - val typeParameters = property.returnType.extractFullType() - val type = classifier.toClassName() - .safelyParameterizedBy(typeParameters) - .copy(nullable = property.returnType.isNullable) - property.name to type - } - - val annotatedClass = metadataClass.name.replace('/', '.') - val annotation = typeElement.getAnnotation(GenerateProperties::class.java) - val visibilityModifier = when (annotation.visibility) { - Visibility.INHERIT -> metadataClass.getVisibilityModifier() - Visibility.PUBLIC -> { - if (!metadataClass.flags.isPublic) { - logError("Can't generate public property iterators for non-public class ${metadataClass.name}") - } - KModifier.PUBLIC - } - Visibility.INTERNAL -> KModifier.INTERNAL - } - generatePropertyExtension( - ClassName.bestGuess(annotatedClass), - propertiesWithTypes, - visibilityModifier, - annotation.level == Level.NESTED, - ).writeTo(processingEnv.filer) - } - - return true - } - - // recursively extracts the full type of a property e.g. List -> List> - private fun KmType.extractFullType(): List { - return arguments.mapNotNull { typeProjection -> - val params = typeProjection.type?.extractFullType() - val type = typeProjection.type - - when (val classifier = type?.classifier) { - is KmClassifier.Class -> classifier.toClassName() - .safelyParameterizedBy(params) - .copy(nullable = type.flags.isNullableType) - is KmClassifier.TypeParameter, - is KmClassifier.TypeAlias, - null -> null - } - } - } - - private fun ClassName.safelyParameterizedBy(typeNames: List?): TypeName { - return if (typeNames.isNullOrEmpty()) { - this - } else { - this.parameterizedBy(typeNames) - } - } - - private fun KmClass.getVisibilityModifier(): KModifier { - if (flags.isPrivate) { - logError("Can't generate property iterators for private classes") - } - - return when { - flags.isPublic -> KModifier.PUBLIC - flags.isInternal -> KModifier.INTERNAL - else -> { - val message = "Visibility of ${this.name} is not supported" - logError(message) - error(message) - } - } - } - - private fun logError(msg: String) { - processingEnv.messager.printMessage(Diagnostic.Kind.ERROR, msg) - } - - // this hack can be removed after https://youtrack.jetbrains.com/issue/KT-20980 has been fixed - private fun List.sortedBy(list: List): List { - return sortedBy { property -> - list.indexOfFirst { it.simpleName.toString() == property.name } - } - } - - private fun KmClassifier.Class.toClassName(): ClassName { - val packageName = name.substringBeforeLast('/').replace('/', '.') - val simpleName = name.substringAfterLast('/') - return ClassName(packageName, simpleName) - } - - override fun getSupportedAnnotationTypes(): Set { - return setOf(GenerateProperties::class.java.name) - } - - private fun generatePropertyExtension( - receiver: ClassName, - properties: Map, - visibilityModifier: KModifier, - nestProperties: Boolean, - ): FileSpec { - val sealedInterfaceName = ClassName(receiver.packageName, "${receiver.simpleName}Property") - val sealedInterface = generateSealedInterface(sealedInterfaceName, visibilityModifier) - val propertySpecs = properties.map { (propertyName, propertyType) -> - generatePropertyContainer(sealedInterfaceName, propertyName, propertyType, visibilityModifier, nestProperties) - } - return generateFile(receiver) - .apply { - if (nestProperties) - addType(sealedInterface.addTypes(propertySpecs).build()) - else { - addType(sealedInterface.build()) - propertySpecs.forEach(::addType) - } - } - .addProperty( - generatePropertyMapper( - sealedInterfaceName, - receiver, - properties, - visibilityModifier, - nestProperties - ) - ) - .build() - } - - override fun getSupportedSourceVersion(): SourceVersion { - return SourceVersion.latestSupported() - } -} diff --git a/codegen/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/codegen/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider new file mode 100644 index 0000000..8b481b5 --- /dev/null +++ b/codegen/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider @@ -0,0 +1 @@ +co.selim.goldfinch.codegen.GoldfinchSymbolProcessorProvider diff --git a/codegen/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/codegen/src/main/resources/META-INF/services/javax.annotation.processing.Processor deleted file mode 100644 index 2a3bb8e..0000000 --- a/codegen/src/main/resources/META-INF/services/javax.annotation.processing.Processor +++ /dev/null @@ -1 +0,0 @@ -co.selim.goldfinch.codegen.ServiceProcessor diff --git a/example/build.gradle b/example/build.gradle index b51aa3a..fa4194f 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -1,6 +1,6 @@ plugins { id 'org.jetbrains.kotlin.jvm' - id 'org.jetbrains.kotlin.kapt' + id 'com.google.devtools.ksp' version '1.7.10-1.0.6' } repositories { @@ -10,5 +10,18 @@ repositories { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib" implementation project(':annotation') - kapt project(':codegen') + ksp project(':codegen') +} + +sourceSets { + main { + java { + srcDir(file("build/generated/ksp/main/kotlin")) + } + } + test { + java { + srcDir(file("build/generated/ksp/test/kotlin")) + } + } } diff --git a/example/src/main/kotlin/co/selim/goldfinch/example/Sample.kt b/example/src/main/kotlin/co/selim/goldfinch/example/Sample.kt index b56bf2a..9274895 100644 --- a/example/src/main/kotlin/co/selim/goldfinch/example/Sample.kt +++ b/example/src/main/kotlin/co/selim/goldfinch/example/Sample.kt @@ -11,11 +11,10 @@ internal data class Person( ) @GenerateProperties -internal data class Animal( - val name: String -) +internal data class Animal(val name: String) fun main() { + val selim = Person("Selim Dinçer", LocalDate.of(1970, 1, 1)) selim.properties