Skip to content

Commit

Permalink
Merge pull request #31 from IZIVIA/feat/0.0.15-migrate-to-ksp
Browse files Browse the repository at this point in the history
  • Loading branch information
lilgallon authored Dec 7, 2023
2 parents c353dcb + 660cb04 commit 119a67e
Show file tree
Hide file tree
Showing 50 changed files with 156 additions and 1,497 deletions.
3 changes: 0 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,5 @@ jobs:
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
gradle-home-cache-cleanup: true

- name: Publish annotation processor to maven local
run: ./gradlew :annotation-processor:publishToMavenLocal

- name: Build OCPI toolkit
run: ./gradlew build
5 changes: 0 additions & 5 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,6 @@ jobs:
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
gradle-home-cache-cleanup: true

- name: Publish OCPI annotation processor to maven local
env:
VERSION: ${{ github.ref }}
run: ./gradlew :annotation-processor:publishToMavenLocal

- name: Build OCPI toolkit
env:
VERSION: ${{ github.ref }}
Expand Down
17 changes: 10 additions & 7 deletions annotation-processor/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
plugins {
base
`java-library`
kotlin("kapt")
kotlin("jvm")
}

kotlinProject()
repositories {
mavenCentral()
}

java {
withJavadocJar()
Expand All @@ -22,16 +22,19 @@ publishing {
pom {
name.set("OCPI Annotation processor")
artifactId = "ocpi-annotation-processor"
description.set("This module processes annotations during compilation time in order to generate some code (partial representation, etc ...)")
description.set(
"This module processes annotations during compilation time in order to generate some code (partial representation, etc ...)"
)
}
}
}
}

dependencies {
implementation("com.squareup:kotlinpoet:${Versions.kotlinPoet}")
compileOnly("org.jetbrains.kotlin:kotlin-compiler-embeddable")
implementation("de.jensklingenberg:mpapt-runtime:${Versions.mpapt}")
implementation("com.squareup:kotlinpoet-ksp:${Versions.kotlinPoet}")
implementation("com.google.devtools.ksp:symbol-processing-api:${Versions.ksp}")
implementation(project(":common"))
}

tasks.build {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,48 +1,59 @@
package com.izivia.ocpi.toolkit.processor

import com.google.devtools.ksp.getConstructors
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.KSFunctionDeclaration
import com.google.devtools.ksp.symbol.KSNode
import com.google.devtools.ksp.validate
import com.izivia.ocpi.toolkit.annotations.Partial
import com.squareup.kotlinpoet.*
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import de.jensklingenberg.mpapt.common.canonicalFilePath
import de.jensklingenberg.mpapt.model.*
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
import org.jetbrains.kotlin.descriptors.containingPackage
import org.jetbrains.kotlin.platform.TargetPlatform
import java.io.File

class PartialAnnotationProcessor : AbstractProcessor() {

private val annotationFqdn = "com.izivia.ocpi.toolkit.annotations.Partial"
private val partialClasses: MutableList<ClassDescriptor> = mutableListOf()
private val toPartialMethodName = "toPartial"
import com.squareup.kotlinpoet.ksp.writeTo

override fun getSupportedAnnotationTypes(): Set<String> = setOf(annotationFqdn)
class PartialProcessor(
private val codeGenerator: CodeGenerator,
private val logger: KSPLogger
) : SymbolProcessor {

override fun isTargetPlatformSupported(platform: TargetPlatform): Boolean = true
private val toPartialMethodName = "toPartial"
private val partialClasses: MutableList<KSClassDeclaration> = mutableListOf()

override fun process(roundEnvironment: RoundEnvironment) {
partialClasses.addAll(
roundEnvironment.getElementsAnnotatedWith(annotationFqdn)
.filterIsInstance<Element.ClassElement>()
.filter { it.classDescriptor.isData }
.map { it.classDescriptor }
)
override fun process(resolver: Resolver): List<KSAnnotated> {
// partialClasses.isEmpty() is a hack to make sure process is only called once.
// According to the documentation, ksp supports multiple rounds. And if we want
// multiple rounds, we need to return Symbols that need to be processed in the
// next round. Here as you can see, we return an emptyList, but ksp still calls
// process two times. When called on the second time, it crashes because the files
// are already created. I do not understand how it works, but this workaround does
// the job for now.
if (partialClasses.isEmpty()) {
partialClasses.addAll(resolver.getPartialAnnotatedClasses())
genPartialClasses(partialClasses.toSet())
.forEach {
it.writeTo(codeGenerator, Dependencies(true))
}
}

return emptyList()
}

override fun processingOver() {
partialClasses.forEach { classDescriptor ->
val packageName = classDescriptor.containingPackage().toString()
val className = classDescriptor.name.asString()
val classFilePath = classDescriptor.canonicalFilePath().toString()
val classFile = File(classFilePath)
private fun Resolver.getPartialAnnotatedClasses(): Set<KSClassDeclaration> =
getSymbolsWithAnnotation(Partial::class.qualifiedName.orEmpty())
.filterIsInstance<KSClassDeclaration>()
.filter(KSNode::validate)
.toSet()

private fun genPartialClasses(partialAnnotatedClasses: Set<KSClassDeclaration>): List<FileSpec> =
partialAnnotatedClasses.map { classDeclaration ->
val packageName = classDeclaration.packageName.asString()
val className = classDeclaration.simpleName.asString()

val partialClassName = className.toPartial()
val partialClassType = buildPartialDataClassType(classDescriptor, partialClassName)
val partialBuilderFun = buildPartialDataClassBuilderFunction(classDescriptor)
val partialListBuilderFun = buildPartialDataClassListBuilderFunction(classDescriptor)
val partialGenFolder = classFile.parent
.replace("/src/main/kotlin", "/src/main/kotlinGen")
.replace(packageName.replace(".", "/"), "")
val partialClassType = buildPartialDataClassType(classDeclaration, partialClassName)
val partialBuilderFun = buildPartialDataClassBuilderFunction(classDeclaration)
val partialListBuilderFun = buildPartialDataClassListBuilderFunction(classDeclaration)

FileSpec
.builder(packageName, partialClassName)
Expand All @@ -55,13 +66,14 @@ class PartialAnnotationProcessor : AbstractProcessor() {
.forEach { addImport(it.packageName, toPartialMethodName) }
}
.build()
.writeTo(File(partialGenFolder))
}
}

private fun buildPartialDataClassType(classDescriptor: ClassDescriptor, partialClassName: String): TypeSpec {
val baseClassConstructorParameters = classDescriptor.constructors.first()
private fun buildPartialDataClassType(classDescriptor: KSClassDeclaration, partialClassName: String): TypeSpec {
val baseClassConstructorParameters = classDescriptor
.getConstructors()
.first()
.getFunctionParameters()

val partialClassConstructorParameters = baseClassConstructorParameters
.map { param ->
ParameterSpec
Expand All @@ -76,7 +88,8 @@ class PartialAnnotationProcessor : AbstractProcessor() {
.map { param ->
PropertySpec
.builder(
param.parameterName, param.type.copy(nullable = true)
param.parameterName,
param.type.copy(nullable = true)
)
.initializer(param.parameterName)
.build()
Expand All @@ -87,8 +100,8 @@ class PartialAnnotationProcessor : AbstractProcessor() {
.addModifiers(KModifier.DATA)
.addKdoc(
"Partial representation of [${
classDescriptor.containingPackage().toString()
}.${classDescriptor.name.asString()}]"
classDescriptor.packageName.asString()
}.${classDescriptor.simpleName.asString()}]"
)
.primaryConstructor(
FunSpec.constructorBuilder()
Expand All @@ -99,16 +112,17 @@ class PartialAnnotationProcessor : AbstractProcessor() {
.build()
}

private fun buildPartialDataClassBuilderFunction(classDescriptor: ClassDescriptor): FunSpec {
val packageName = classDescriptor.containingPackage().toString()
val className = classDescriptor.name.asString()
val baseClassConstructorParameters = classDescriptor.constructors.first()
.getFunctionParameters()
private fun buildPartialDataClassBuilderFunction(classDescriptor: KSClassDeclaration): FunSpec {
val packageName = classDescriptor.packageName.asString()
val className = classDescriptor.simpleName.asString()
val baseClassConstructorParameters = classDescriptor.getConstructors().first().getFunctionParameters()
val partialClassConstructorParameters = baseClassConstructorParameters
.joinToString(separator = ",\n ") { param ->
"${param.parameterName} = ${
findPartialClassByName(param.type.asString()!!.toBase())
?.let { "${param.parameterName}${if (param.nullable) "?" else ""}.${toPartialMethodName}()" } ?: param.parameterName
?.let {
"${param.parameterName}${if (param.nullable) "?" else ""}.$toPartialMethodName()"
} ?: param.parameterName
}"
}
return FunSpec.builder(toPartialMethodName)
Expand Down Expand Up @@ -145,9 +159,9 @@ class PartialAnnotationProcessor : AbstractProcessor() {
.toSet()
}

private fun buildPartialDataClassListBuilderFunction(classDescriptor: ClassDescriptor): FunSpec {
val packageName = classDescriptor.containingPackage().toString()
val className = classDescriptor.name.asString()
private fun buildPartialDataClassListBuilderFunction(classDescriptor: KSClassDeclaration): FunSpec {
val packageName = classDescriptor.packageName.asString()
val className = classDescriptor.simpleName.asString()
return FunSpec.builder(toPartialMethodName)
.receiver(
ClassName("kotlin.collections", "List")
Expand All @@ -159,50 +173,46 @@ class PartialAnnotationProcessor : AbstractProcessor() {
)
.addCode(
"""
| return mapNotNull { it.${toPartialMethodName}() }
| return mapNotNull { it.$toPartialMethodName() }
""".trimMargin()
)
.build()
}

private fun FunctionDescriptor.getFunctionParameters(): List<FunctionParameter> {
return if (valueParameters.isNotEmpty()) {
this.valueParameters.map { parameter ->
val fullPackage = parameter.toString()
.substringAfter(": ")
.substringBefore(" defined")
.substringBefore(" /* =")
.substringBefore("=")
.trim()

val typeName = if (fullPackage.contains("<")) {
val type = fullPackage.substringBefore("<")
.split(".").last().replace("?", "")
.wireWithExistingPartial()
val packageName = fullPackage.substringBefore("<").split(".")
.dropLast(1).joinToString(".")
val parameterType = fullPackage.substringAfter("<").substringBefore(">")
.split(".").last().replace("?", "")
.wireWithExistingPartial()
val parameterTypePackage = fullPackage.substringAfter("<").substringBefore(">")
.split(".").dropLast(1).joinToString(".")
private fun KSFunctionDeclaration.getFunctionParameters(): List<FunctionParameter> {
return if (parameters.toList().isNotEmpty()) {
parameters.map { parameter ->
val resolvedType = parameter.type.resolve()
val typeName = if (resolvedType.arguments.isNotEmpty()) {
// It's a type with generics, example: List<String>
ClassName(
packageName,
type
packageName = resolvedType.declaration.packageName.asString(),
simpleNames = listOf(
resolvedType.declaration.simpleName.asString()
)
).parameterizedBy(
resolvedType.arguments.map { generic ->
val genericResolved = generic.type!!.resolve()
ClassName(
packageName = genericResolved.declaration.packageName.asString(),
simpleNames = listOf(
genericResolved.declaration.simpleName.asString().wireWithExistingPartial()
)
)
}
)
.parameterizedBy(ClassName(parameterTypePackage, parameterType))
} else {
val packageName = fullPackage.split(".").dropLast(1).joinToString(".")
val typeName = fullPackage.split(".").last().replace("?", "")
.wireWithExistingPartial()
ClassName(
packageName,
typeName
packageName = resolvedType.declaration.packageName.asString(),
simpleNames = listOf(
resolvedType.declaration.simpleName.asString().wireWithExistingPartial()
)
)
}

FunctionParameter(
parameter.name.asString(),
parameter.type.toString().endsWith("?"),
parameter.name?.asString() ?: throw IllegalStateException("null param name $parameter"),
resolvedType.isMarkedNullable,
typeName
)
}.toList()
Expand All @@ -216,23 +226,23 @@ class PartialAnnotationProcessor : AbstractProcessor() {
?.toPartial()
?: this

private fun findPartialClassByName(name: String) =
private fun findPartialClassByName(name: String): String? =
partialClasses
.map { it.name.asString() }
.map { it.simpleName.asString() }
.find { it == name }

private fun TypeName.asString(): String? = when (this) {
is ClassName -> simpleName
is ParameterizedTypeName -> this.typeArguments.first().asString()
else -> null
}

private fun String.toPartial() = "${this}Partial"

private fun ClassName.isPartialType() = simpleName.contains("Partial")

private fun String.toBase() = replace("Partial", "")

private fun TypeName.asString(): String? = when (this) {
is ClassName -> simpleName
is ParameterizedTypeName -> this.typeArguments.first().asString()
else -> null
}

private fun FileSpec.Builder.generateComments() =
addFileComment(
"""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.izivia.ocpi.toolkit.processor

import com.google.devtools.ksp.processing.*

class PartialProcessorProvider : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor =
PartialProcessor(environment.codeGenerator, environment.logger)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
com.izivia.ocpi.toolkit.processor.PartialProcessorProvider

This file was deleted.

Loading

0 comments on commit 119a67e

Please sign in to comment.