Skip to content

Commit

Permalink
Rewrite ksp plugin to make it useable outside of settings.
Browse files Browse the repository at this point in the history
  • Loading branch information
kunyavskiy committed Feb 18, 2024
1 parent ee1a027 commit 41713b1
Show file tree
Hide file tree
Showing 26 changed files with 755 additions and 213 deletions.
38 changes: 9 additions & 29 deletions schemas/settings.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,17 +136,10 @@
],
"title": "atcoder"
},
"org.icpclive.cds.api.ContestResultType?<org.icpclive.cds.api.ContestResultType.ICPC,org.icpclive.cds.api.ContestResultType.IOI>": {
"oneOf": [
{
"enum": [
"ICPC",
"IOI"
]
},
{
"type": "null"
}
"org.icpclive.cds.api.ContestResultType<org.icpclive.cds.api.ContestResultType.ICPC,org.icpclive.cds.api.ContestResultType.IOI>": {
"enum": [
"ICPC",
"IOI"
]
},
"cats": {
Expand All @@ -172,7 +165,7 @@
"type": "string"
},
"resultType": {
"$ref": "#/$defs/org.icpclive.cds.api.ContestResultType?<org.icpclive.cds.api.ContestResultType.ICPC,org.icpclive.cds.api.ContestResultType.IOI>"
"$ref": "#/$defs/org.icpclive.cds.api.ContestResultType<org.icpclive.cds.api.ContestResultType.ICPC,org.icpclive.cds.api.ContestResultType.IOI>"
},
"emulation": {
"$ref": "#/$defs/org.icpclive.cds.settings.EmulationSettings?<kotlin.Double,InstantH>"
Expand Down Expand Up @@ -381,7 +374,7 @@
"type": "string"
},
"resultType": {
"$ref": "#/$defs/org.icpclive.cds.api.ContestResultType?<org.icpclive.cds.api.ContestResultType.ICPC,org.icpclive.cds.api.ContestResultType.IOI>"
"$ref": "#/$defs/org.icpclive.cds.api.ContestResultType<org.icpclive.cds.api.ContestResultType.ICPC,org.icpclive.cds.api.ContestResultType.IOI>"
},
"timeZone": {
"type": "string"
Expand All @@ -400,19 +393,6 @@
],
"title": "ejudge"
},
"kotlin.collections.ArrayList?<kotlin.String>": {
"oneOf": [
{
"type": "array",
"items": {
"type": "string"
}
},
{
"type": "null"
}
]
},
"eolymp": {
"type": "object",
"properties": {
Expand All @@ -430,7 +410,7 @@
"type": "string"
},
"previousDaysContestIds": {
"$ref": "#/$defs/kotlin.collections.ArrayList?<kotlin.String>"
"$ref": "#/$defs/kotlin.collections.ArrayList<kotlin.String>"
},
"emulation": {
"$ref": "#/$defs/org.icpclive.cds.settings.EmulationSettings?<kotlin.Double,InstantH>"
Expand Down Expand Up @@ -562,7 +542,7 @@
"type": "string"
},
"resultType": {
"$ref": "#/$defs/org.icpclive.cds.api.ContestResultType?<org.icpclive.cds.api.ContestResultType.ICPC,org.icpclive.cds.api.ContestResultType.IOI>"
"$ref": "#/$defs/org.icpclive.cds.api.ContestResultType<org.icpclive.cds.api.ContestResultType.ICPC,org.icpclive.cds.api.ContestResultType.IOI>"
},
"emulation": {
"$ref": "#/$defs/org.icpclive.cds.settings.EmulationSettings?<kotlin.Double,InstantH>"
Expand Down Expand Up @@ -622,7 +602,7 @@
"type": "number"
},
"resultType": {
"$ref": "#/$defs/org.icpclive.cds.api.ContestResultType?<org.icpclive.cds.api.ContestResultType.ICPC,org.icpclive.cds.api.ContestResultType.IOI>"
"$ref": "#/$defs/org.icpclive.cds.api.ContestResultType<org.icpclive.cds.api.ContestResultType.ICPC,org.icpclive.cds.api.ContestResultType.IOI>"
},
"emulation": {
"$ref": "#/$defs/org.icpclive.cds.settings.EmulationSettings?<kotlin.Double,InstantH>"
Expand Down
381 changes: 376 additions & 5 deletions src/cds/api/cds.api

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/cds/ksp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ repositories {

dependencies {
implementation(libs.ksp)
implementation(libs.kotlinx.serialization.json)
}
27 changes: 26 additions & 1 deletion src/cds/ksp/src/main/kotlin/org/icpclive/cds/ksp/Annotations.kt
Original file line number Diff line number Diff line change
@@ -1,4 +1,29 @@
package org.icpclive.cds.ksp

@Retention(AnnotationRetention.SOURCE)
annotation class GenerateSettings(val name: String)
@Target(AnnotationTarget.CLASS)
annotation class Builder(val name: String = "")

@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.PROPERTY)
annotation class Seconds(val name: String = "")

@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.PROPERTY)
annotation class MilliSeconds(val name: String = "")

@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.PROPERTY)
annotation class Human(val name: String = "")

@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.PROPERTY)
annotation class UnixSeconds(val name: String = "")

@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.PROPERTY)
annotation class UnixMilliSeconds(val name: String = "")

@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.PROPERTY)
annotation class AlwaysSerialize
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
package org.icpclive.cds.ksp

import com.google.devtools.ksp.*
import com.google.devtools.ksp.processing.*
import com.google.devtools.ksp.symbol.*
import kotlinx.serialization.*
import java.io.PrintWriter

class GeneratedBuildersProcessor(private val generator: CodeGenerator, val logger: KSPLogger) : SymbolProcessor {
@OptIn(KspExperimental::class)
private fun KSType.render(allowImpls: Boolean) : String = buildString {
if (allowImpls && declaration.isAnnotationPresent(Builder::class)) {
append(declaration.qualifiedName?.asString()!! + "Impl")
} else {
append(declaration.qualifiedName?.asString()!!)
}
val typeArgs = arguments
if (arguments.isNotEmpty()) {
append("<")
append(
typeArgs.map {
val type = it.type?.resolve()
"${it.variance.label} ${type?.render(allowImpls)}" +
if (type?.nullability == Nullability.NULLABLE) "?" else ""
}.joinToString(", ")
)
append(">")
}
if (isMarkedNullable) append("?")
}
private val allGeneratedFiles = mutableListOf<String>()

@OptIn(KspExperimental::class)
private fun PrintWriter.printMembers(members: List<KSPropertyDeclaration>) {
for (member in members) {
val type = member.type.resolve().declaration.qualifiedName?.asString()
val memberName = member.simpleName.asString()
var serialName = memberName
member.getAnnotationsByType(SerialName::class).singleOrNull()?.let {
serialName = it.value
}
when (type) {
"org.icpclive.cds.settings.Credential", "org.icpclive.cds.settings.UrlOrLocalPath" -> {
println(" @Contextual")
}

"kotlin.text.Regex" -> {
println(" @Serializable(with = RegexSerializer::class)")
}

"java.awt.Color" -> {
println(" @Serializable(with = ColorSerializer::class)")
}

"kotlinx.datetime.Instant" -> {
when {
member.isAnnotationPresent(Human::class) -> {
println(" @Serializable(with = HumanTimeSerializer::class)")
}

member.isAnnotationPresent(UnixMilliSeconds::class) -> {
println(" @Serializable(with = UnixMillisecondsSerializer::class)")
serialName = "${serialName}UnixMs"
}

member.isAnnotationPresent(UnixSeconds::class) -> {
println(" @Serializable(with = UnixSecondsSerializer::class)")
serialName = "${serialName}UnixSeconds"
}

else -> {
logger.error("No known serializer for ${member} in ${member.closestClassDeclaration()!!.simpleName}: add @Human or @UnixSeconds or @UnixMilliSeconds", member)
}
}
}

"kotlin.time.Duration" -> {
when {
member.isAnnotationPresent(Seconds::class) -> {
println(" @Serializable(with = DurationInSecondsSerializer::class)")
serialName = "${serialName}Seconds"
}

member.isAnnotationPresent(MilliSeconds::class) -> {
println(" @Serializable(with = DurationInMillisecondsSerializer::class)")
serialName = "${serialName}Ms"
}

else -> {
logger.error("No known serializer: add @Seconds or @MilliSeconds", member)
}
}
}

"kotlinx.datetime.TimeZone" -> {
println(" @Serializable(with = TimeZoneSerializer::class)")
}
}
if (member.isAbstract() || member.isAnnotationPresent(AlwaysSerialize::class)) {
println(" @SerialName(\"$serialName\") override val $memberName: ${member.type.resolve().render(true)}")
} else {
println(" @SerialName(\"$serialName\") override var $memberName: ${member.type.resolve().render(true)} = super.${memberName}")
println(" private set")
}
}
}


@OptIn(KspExperimental::class)
override fun process(resolver: Resolver): List<KSAnnotated> {
val toGenerate = resolver
.getSymbolsWithAnnotation(Builder::class.qualifiedName!!)
val ret = toGenerate.filter { !it.validate() }.toList()
val interfacesToImplement = toGenerate
.filter { it.validate() }
.filterIsInstance<KSClassDeclaration>()
.filter {
when {
Modifier.SEALED !in it.modifiers -> {
logger.error("${it.qualifiedName!!.asString()}: Class annotatated must @Builder must be sealed", it)
false
}
else -> true
}
}.toList()
for (iface in interfacesToImplement) {
val packageName = iface.packageName.asString()
val ifaceName = iface.simpleName.asString()
val builderName = "${ifaceName}Builder"
val className = "${ifaceName}Impl"
PrintWriter(
generator.createNewFile(
dependencies = Dependencies(true, iface.containingFile!!),
packageName = packageName,
fileName = className
)
).use {
with(it) {
val allProperties = iface.getAllProperties().filter { it.isOpen() || it.isAbstract() }.toList()
val (abstractMembers, nonAbstractMembers) = allProperties.partition { it.isAbstract() }

println("package $packageName")
println()
println("import kotlinx.serialization.*")
println("import kotlinx.serialization.json.*")
println("import kotlinx.serialization.encoding.*")
println("import org.icpclive.util.*")
println()
println("@Serializable")
val classSerialName = iface.getAnnotationsByType(Builder::class).single().name.takeUnless { it.isEmpty() } ?: ifaceName
println("@SerialName(\"$classSerialName\")")
println("internal class $className : ${iface.qualifiedName!!.asString()} {")
it.printMembers(allProperties)

generateContructor(allProperties)

println(" override fun toString() = Json.encodeToString(this)")
println(" override fun equals(other: Any?): Boolean {")
println(" if (other !is ${className}) return false")
for (member in allProperties) {
println(" if (${member.simpleName.asString()} != other.${member.simpleName.asString()}) return false")
}
println(" return true")
println(" } ")

println(" override fun hashCode(): Int {")
println(" var hashCode = 0")
for (member in allProperties) {
println(" hashCode = hashCode * 31 + ${member.simpleName.asString()}.hashCode()")
}
println(" return hashCode")
println(" } ")


println("}")
println()

println("@Suppress(\"UNCHECKED_CAST\")")
println("public class $builderName : $ifaceName {")
for (member in abstractMembers) {
println(" override public var ${member.simpleName.asString()}: ${member.type.resolve().render(false)}")
}
for (member in nonAbstractMembers) {
println(" private var ${member.simpleName.asString()}_: ${member.type.resolve().makeNullable().render(false)} = null")
println(" override public var ${member.simpleName.asString()}: ${member.type.resolve().render(false)}")
println(" get() = ${member.simpleName.asString()}_ ?: super.${member.simpleName.asString()}")
println(" set(value) { ${member.simpleName.asString()}_ = value }")
}
println()
println(" public constructor(")
for (member in abstractMembers) {
if (!member.type.resolve().isMarkedNullable) {
println(" ${member.simpleName.asString()}: ${member.type.resolve().render(false)},")
}
}
println(" ) {")
for (member in abstractMembers) {
if (!member.type.resolve().isMarkedNullable) {
println(" this.${member.simpleName.asString()} = ${member.simpleName.asString()}")
} else {
println(" this.${member.simpleName.asString()} = null")
}
}
println(" }")


println(" public constructor(from: $ifaceName) {")
for (member in allProperties) {
println(" ${member.simpleName.asString()} = from.${member.simpleName.asString()}")
}
println(" }")


println(" public fun build() : ${ifaceName} = ${className}(")
for (member in allProperties) {
val cast = if (member.type.resolve().render(false) != member.type.resolve().render(true)) {
" as ${member.type.resolve().render(true)}"
} else {
""
}
println(" ${member.simpleName.asString()}$cast,")
}
println(" )")
println("}")
println()
println("public inline fun ${ifaceName}(")
for (member in abstractMembers) {
if (!member.type.resolve().isMarkedNullable) {
println(" ${member.simpleName.asString()}: ${member.type.resolve().render(false)},")
}
}
println(" initializer: ${builderName}.() -> Unit = {},")
println(") : ${ifaceName} = ${builderName}(")
for (member in abstractMembers) {
if (!member.type.resolve().isMarkedNullable) {
println(" ${member.simpleName.asString()},")
}
}
println(").apply { initializer() }.build()")
println()

println("public inline fun ${ifaceName}.copy(initializer: ${builderName}.() -> Unit = {}): ${ifaceName} =")
println(" ${builderName}(this).apply { initializer() }.build()")

println()
}
}
allGeneratedFiles.add("$packageName.${className}")
}
return ret
}

private fun PrintWriter.generateContructor(abstractMembers: List<KSPropertyDeclaration>) {
println(" internal constructor(")
for (member in abstractMembers) {
println(" ${member.simpleName.asString()}: ${member.type.resolve().render(true)},")
}
println(" ) {")
for (member in abstractMembers) {
println(" this.${member.simpleName.asString()} = ${member.simpleName.asString()}")
}
println(" }")
}
}

class GeneratedBuildersProvider : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment) : SymbolProcessor {
return GeneratedBuildersProcessor(environment.codeGenerator, environment.logger)
}
}
Loading

0 comments on commit 41713b1

Please sign in to comment.