From 7a061dcc2e46ae88360d2590b3938362e98e65ea Mon Sep 17 00:00:00 2001 From: Michael Rittmeister Date: Wed, 20 Sep 2023 19:57:15 +0200 Subject: [PATCH] Add support for js annotations --- .../src/commonMain/kotlin/Annotator.kt | 11 +++ .../codegen/kotlinpoet/js/JsAnnotations.kt | 85 ++++++++++++++++++ .../src/main/kotlin/AnnotatorProcessor.kt | 33 +++++++ .../src/main/kotlin/annotator/Annotator.kt | 90 +++++++++++++++++++ .../processor/src/main/kotlin/utils/Names.kt | 2 + ...ols.ksp.processing.SymbolProcessorProvider | 1 + .../src/commonMain/kotlin/js/JsAnnotations.kt | 8 ++ .../generator/DataClassRepresentation.kt | 6 +- 8 files changed, 234 insertions(+), 2 deletions(-) create mode 100644 kotlinpoet/annotations/src/commonMain/kotlin/Annotator.kt create mode 100644 kotlinpoet/build/generated/ksp/generationSource/generationSourceMain/kotlin/dev/kord/codegen/kotlinpoet/js/JsAnnotations.kt create mode 100644 kotlinpoet/processor/src/main/kotlin/AnnotatorProcessor.kt create mode 100644 kotlinpoet/processor/src/main/kotlin/annotator/Annotator.kt create mode 100644 kotlinpoet/src/commonMain/kotlin/js/JsAnnotations.kt diff --git a/kotlinpoet/annotations/src/commonMain/kotlin/Annotator.kt b/kotlinpoet/annotations/src/commonMain/kotlin/Annotator.kt new file mode 100644 index 0000000..f70b0f4 --- /dev/null +++ b/kotlinpoet/annotations/src/commonMain/kotlin/Annotator.kt @@ -0,0 +1,11 @@ +package dev.kord.codegen.ksp.annotations + +@Repeatable +@MustBeDocumented +@Target(AnnotationTarget.FILE) +@Retention(AnnotationRetention.SOURCE) +@ProcessorAnnotation("dev.kord.codegen.generator.annotator") +public annotation class Annotator( + val fromPackage: String, + val ignore: Array = [] +) \ No newline at end of file diff --git a/kotlinpoet/build/generated/ksp/generationSource/generationSourceMain/kotlin/dev/kord/codegen/kotlinpoet/js/JsAnnotations.kt b/kotlinpoet/build/generated/ksp/generationSource/generationSourceMain/kotlin/dev/kord/codegen/kotlinpoet/js/JsAnnotations.kt new file mode 100644 index 0000000..d90b183 --- /dev/null +++ b/kotlinpoet/build/generated/ksp/generationSource/generationSourceMain/kotlin/dev/kord/codegen/kotlinpoet/js/JsAnnotations.kt @@ -0,0 +1,85 @@ +package dev.kord.codegen.kotlinpoet.js + +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.PropertySpec +import com.squareup.kotlinpoet.TypeSpec +import dev.kord.codegen.kotlinpoet.addAnnotation +import kotlin.String + +/** + * Adds `JsName` to this [FunSpec] + */ +public fun FunSpec.Builder.jsName(name: String) { + addAnnotation(ClassName("kotlin.js", "JsName")) { + addMember("%S", name) + } +} + +/** + * Adds `JsName` to this [TypeSpec] + */ +public fun TypeSpec.Builder.jsName(name: String) { + addAnnotation(ClassName("kotlin.js", "JsName")) { + addMember("%S", name) + } +} + +/** + * Adds `JsName` to this [PropertySpec] + */ +public fun PropertySpec.Builder.jsName(name: String) { + addAnnotation(ClassName("kotlin.js", "JsName")) { + addMember("%S", name) + } +} + +/** + * Adds `JsExport` to this [FileSpec] + */ +public fun FileSpec.Builder.jsExport() { + addAnnotation(ClassName("kotlin.js", "JsExport")) +} + +/** + * Adds `JsExport` to this [FunSpec] + */ +public fun FunSpec.Builder.jsExport() { + addAnnotation(ClassName("kotlin.js", "JsExport")) +} + +/** + * Adds `JsExport` to this [TypeSpec] + */ +public fun TypeSpec.Builder.jsExport() { + addAnnotation(ClassName("kotlin.js", "JsExport")) +} + +/** + * Adds `JsExport` to this [PropertySpec] + */ +public fun PropertySpec.Builder.jsExport() { + addAnnotation(ClassName("kotlin.js", "JsExport")) +} + +/** + * Adds `Ignore` to this [FunSpec] + */ +public fun FunSpec.Builder.ignore() { + addAnnotation(ClassName("kotlin.js", "JsExport", "Ignore")) +} + +/** + * Adds `Ignore` to this [TypeSpec] + */ +public fun TypeSpec.Builder.ignore() { + addAnnotation(ClassName("kotlin.js", "JsExport", "Ignore")) +} + +/** + * Adds `Ignore` to this [PropertySpec] + */ +public fun PropertySpec.Builder.ignore() { + addAnnotation(ClassName("kotlin.js", "JsExport", "Ignore")) +} diff --git a/kotlinpoet/processor/src/main/kotlin/AnnotatorProcessor.kt b/kotlinpoet/processor/src/main/kotlin/AnnotatorProcessor.kt new file mode 100644 index 0000000..834baf2 --- /dev/null +++ b/kotlinpoet/processor/src/main/kotlin/AnnotatorProcessor.kt @@ -0,0 +1,33 @@ +package dev.kord.codegen.generator + +import com.google.devtools.ksp.containingFile +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.processing.SymbolProcessorProvider +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSFile +import dev.kord.codegen.generator.annotator.getAnnotators +import dev.kord.codegen.generator.annotator.processAnnotators +import dev.kord.codegen.ksp.annotations.Annotator +import dev.kord.codegen.ksp.annotations.InlineConstructor +import dev.kord.codegen.ksp.getSymbolsWithAnnotation + +/** + * Please refer to the documentation of [InlineConstructor]. + */ +class AnnotatorProcessor private constructor(private val environment: SymbolProcessorEnvironment) : SymbolProcessor { + override fun process(resolver: Resolver): List { + resolver.getSymbolsWithAnnotation() + .forEach { + val annotators = it.getAnnotators() + environment.processAnnotators(resolver, it as KSFile, annotators) + } + + return emptyList() + } + + class Provider : SymbolProcessorProvider { + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor = AnnotatorProcessor(environment) + } +} diff --git a/kotlinpoet/processor/src/main/kotlin/annotator/Annotator.kt b/kotlinpoet/processor/src/main/kotlin/annotator/Annotator.kt new file mode 100644 index 0000000..817f200 --- /dev/null +++ b/kotlinpoet/processor/src/main/kotlin/annotator/Annotator.kt @@ -0,0 +1,90 @@ +@file:JvmName("AnnotatorGenerator") + +package dev.kord.codegen.generator.annotator + +import com.google.devtools.ksp.KspExperimental +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.symbol.ClassKind +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSFile +import com.squareup.kotlinpoet.* +import com.squareup.kotlinpoet.ksp.addOriginatingKSFile +import com.squareup.kotlinpoet.ksp.toClassName +import com.squareup.kotlinpoet.ksp.toTypeName +import com.squareup.kotlinpoet.ksp.writeTo +import dev.kord.codegen.generator.utils.ADD_ANNOTATION +import dev.kord.codegen.generator.utils.mapToValueParameterList +import dev.kord.codegen.kotlinpoet.CodeBlock +import dev.kord.codegen.kotlinpoet.FileSpec +import dev.kord.codegen.kotlinpoet.addFunction +import dev.kord.codegen.kotlinpoet.withControlFlow +import dev.kord.codegen.ksp.annotations.AnnotationArguments.Companion.arguments +import dev.kord.codegen.ksp.getAnnotationByType + +@OptIn(KspExperimental::class) +fun SymbolProcessorEnvironment.processAnnotators(resolver: Resolver, origin: KSFile, annotators: Sequence) { + val fileSpec = FileSpec(origin.packageName.asString(), origin.fileName.dropLast(3)) { + annotators.forEach { annotator -> + resolver.getDeclarationsFromPackage(annotator.fromPackage) + .filterIsInstance() + .filter { it.classKind == ClassKind.ANNOTATION_CLASS } + .filter { it.simpleName.asString() !in annotator.ignore } + .forEach { + val targetAnnotation = it.getAnnotationByType() + val targetsRaw = targetAnnotation.arguments() + val targets = targetsRaw[Target::allowedTargets]!! + it.generateAnnotator(origin, this) + } + } + } + + fileSpec.writeTo(codeGenerator, false) +} + +private fun KSClassDeclaration.generateAnnotator(origin: KSFile, builder: FileSpec.Builder) { + receiversByTarget.forEach { + builder.addFunction(simpleName.asString().replaceFirstChar { it.lowercase() }) { + addKdoc("Adds `%N` to this [%T]", toClassName().simpleName, it.enclosingClassName()!!) + addOriginatingKSFile(origin) + receiver(it) + primaryConstructor!!.parameters.forEach { + addParameter(it.name!!.asString(), it.type.toTypeName()) + } + + val type = toClassName() + + if (parameters.isEmpty()) { + addCode( + "%M(%L)", + ADD_ANNOTATION, + type.toLiteral() + ) + } else { + withControlFlow( + "%M(%L)", ADD_ANNOTATION, + type.toLiteral() + ) { + val placeHolders = parameters.map { + if (it.type == STRING) { + CodeBlock.of("%%S") + } else { + CodeBlock.of("%%L") + } + }.joinToCode(", ") + + addStatement("addMember(%S, %L)", placeHolders, parameters.mapToValueParameterList()) + } + } + } + } +} + +private fun ClassName.toLiteral() = CodeBlock { + add("%T(", ClassName::class.asClassName()) + val parameters = (listOf(packageName) + simpleNames).map { + CodeBlock.of("%S", it) + }.joinToCode(", ") + add("%L", parameters) + add(")") +} diff --git a/kotlinpoet/processor/src/main/kotlin/utils/Names.kt b/kotlinpoet/processor/src/main/kotlin/utils/Names.kt index ce5be7d..69ec91e 100644 --- a/kotlinpoet/processor/src/main/kotlin/utils/Names.kt +++ b/kotlinpoet/processor/src/main/kotlin/utils/Names.kt @@ -4,6 +4,7 @@ package dev.kord.codegen.generator.utils import com.squareup.kotlinpoet.DelicateKotlinPoetApi +import com.squareup.kotlinpoet.MemberName import com.squareup.kotlinpoet.asClassName import dev.kord.codegen.kotlinpoet.CodeGenDsl import dev.kord.codegen.kotlinpoet.CodeGenInternal @@ -18,4 +19,5 @@ private val produceByName: KFunction> = ::produc val CODEGEN_DSL = CodeGenDsl::class.asClassName() val EMPTY_CODE_BLOCK = ::emptyCodeBlock.asMemberName() val PRODUCE_BY_NAME = produceByName.asMemberName() +val ADD_ANNOTATION = MemberName("dev.kord.codegen.kotlinpoet", "addAnnotation") val SUB_SPEC_DELEGATE_PROVIDER = SubSpecDelegateProvider::class.asClassName() diff --git a/kotlinpoet/processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/kotlinpoet/processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider index 238c62e..afe65fd 100644 --- a/kotlinpoet/processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider +++ b/kotlinpoet/processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider @@ -1,2 +1,3 @@ dev.kord.codegen.generator.CodeProcessor$Provider dev.kord.codegen.generator.ConstructorInliner$Provider +dev.kord.codegen.generator.AnnotatorProcessor$Provider diff --git a/kotlinpoet/src/commonMain/kotlin/js/JsAnnotations.kt b/kotlinpoet/src/commonMain/kotlin/js/JsAnnotations.kt new file mode 100644 index 0000000..36c2aa6 --- /dev/null +++ b/kotlinpoet/src/commonMain/kotlin/js/JsAnnotations.kt @@ -0,0 +1,8 @@ +@file:Annotator( + "kotlin.js", + ignore = ["ExperimentalJsExport"] +) + +package dev.kord.codegen.kotlinpoet.js + +import dev.kord.codegen.ksp.annotations.Annotator diff --git a/ksp/processor/src/main/kotlin/generator/DataClassRepresentation.kt b/ksp/processor/src/main/kotlin/generator/DataClassRepresentation.kt index ba5bd1a..5b631a1 100644 --- a/ksp/processor/src/main/kotlin/generator/DataClassRepresentation.kt +++ b/ksp/processor/src/main/kotlin/generator/DataClassRepresentation.kt @@ -19,7 +19,7 @@ fun KSType.isMappedAnnotation(rootType: KSClassDeclaration): Boolean { val declaration = declaration return if (declaration is KSClassDeclaration && declaration.classKind == ClassKind.ANNOTATION_CLASS) { declaration.parentDeclaration == rootType - || declaration.annotations + || declaration.annotations .any { it.annotationType.resolve().declaration.qualifiedName!!.asString() == PROCESSOR_ANNOTATION } } else { false @@ -39,7 +39,9 @@ private fun KSTypeReference.dataClassType(rootType: KSClassDeclaration): TypeNam else -> { val declaration = resolvedType.declaration if (declaration is KSClassDeclaration && declaration.classKind == ClassKind.ANNOTATION_CLASS) { - if (resolvedType.isMappedAnnotation(rootType)) { + if (resolvedType.isMappedAnnotation(rootType) + || (resolvedType.declaration as? KSClassDeclaration)?.classKind == ClassKind.ENUM_CLASS + ) { ClassName("", declaration.simpleName.asString()) } else { KSAnnotation::class.asClassName()