Coverage Summary for Class: TextChannelPlanner (com.ghost.serialization.compiler)
| Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| TextChannelPlanner |
66.7%
(4/6)
|
38.5%
(30/78)
|
63.2%
(36/57)
|
54.9%
(226/412)
|
| TextChannelPlanner$AnalyzedClass |
100%
(1/1)
|
|
100%
(3/3)
|
100%
(12/12)
|
| Total |
71.4%
(5/7)
|
38.5%
(30/78)
|
65%
(39/60)
|
56.1%
(238/424)
|
package com.ghost.serialization.compiler
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSType
import com.google.devtools.ksp.symbol.Modifier
import com.ghost.serialization.compiler.GhostEmitterConstants as C
/**
* Resolves which `@GhostSerialization` models in a compilation round need the native
* string reader/writer overloads.
*
* Priority:
* 1. `ghost.textChannel=true` KSP option → every model in the module.
* 2. Otherwise `@GhostSerialization(textChannel = true)` on a model, plus transitive
* `@GhostSerialization` types reachable from its property graph (lists, maps, sealed
* subclasses, inferred variants).
*/
internal object TextChannelPlanner {
data class AnalyzedClass(
val declaration: KSClassDeclaration,
val properties: List<GhostPropertyModel>,
)
fun plan(
analyzed: List<AnalyzedClass>,
moduleTextChannelEnabled: Boolean,
): Map<KSClassDeclaration, Boolean> {
if (analyzed.isEmpty()) {
return emptyMap()
}
val byDeclaration = analyzed.associate { it.declaration to it.properties }
if (moduleTextChannelEnabled) {
return byDeclaration.keys.associateWith { true }
}
val enabled = mutableSetOf<KSClassDeclaration>()
analyzed.forEach { entry ->
if (entry.declaration.declaresTextChannel()) {
enabled.add(entry.declaration)
}
}
val pending = ArrayDeque(enabled)
while (pending.isNotEmpty()) {
val current = pending.removeFirst()
val properties = byDeclaration[current] ?: emptyList()
for (dependency in ghostDependencies(current, properties)) {
if (enabled.add(dependency)) {
pending.addLast(dependency)
}
}
}
return byDeclaration.keys.associateWith { it in enabled }
}
private fun KSClassDeclaration.declaresTextChannel(): Boolean {
val annotation = annotations.firstOrNull {
it.shortName.asString() == C.ANNOTATION_GHOST_SERIALIZATION
} ?: return false
return annotation.arguments.any { arg ->
arg.name?.asString() == C.ARG_TEXT_CHANNEL && arg.value == true
}
}
private fun ghostDependencies(
classDeclaration: KSClassDeclaration,
properties: List<GhostPropertyModel>,
): Set<KSClassDeclaration> {
val deps = mutableSetOf<KSClassDeclaration>()
if (classDeclaration.modifiers.contains(Modifier.SEALED)) {
classDeclaration.getSealedSubclasses().forEach { subclass ->
subclass.toGhostDeclaration()?.let { deps.add(it) }
}
}
for (property in properties) {
if (property.isGhost) {
property.type.toGhostDeclaration()?.let { deps.add(it) }
}
if (property.listInnerIsGhost) {
property.listInnerType?.toGhostDeclaration()?.let { deps.add(it) }
}
if (property.mapValueIsGhost) {
property.mapValueType?.toGhostDeclaration()?.let { deps.add(it) }
}
property.valueClassProperty?.let { inner ->
if (inner.isGhost) {
inner.type.toGhostDeclaration()?.let { deps.add(it) }
}
}
for (subclass in property.inferredSubclasses) {
subclass.declaration.toGhostDeclaration()?.let { deps.add(it) }
for (subProperty in subclass.properties) {
collectPropertyDependencies(subProperty, deps)
}
}
}
return deps
}
private fun collectPropertyDependencies(
property: GhostPropertyModel,
deps: MutableSet<KSClassDeclaration>,
) {
if (property.isGhost) {
property.type.toGhostDeclaration()?.let { deps.add(it) }
}
if (property.listInnerIsGhost) {
property.listInnerType?.toGhostDeclaration()?.let { deps.add(it) }
}
if (property.mapValueIsGhost) {
property.mapValueType?.toGhostDeclaration()?.let { deps.add(it) }
}
}
private fun KSType.toGhostDeclaration(): KSClassDeclaration? {
val declaration = declaration as? KSClassDeclaration ?: return null
if (!declaration.annotations.any { it.shortName.asString() == C.ANNOTATION_GHOST_SERIALIZATION }) {
return null
}
return declaration
}
private fun KSClassDeclaration.toGhostDeclaration(): KSClassDeclaration? {
if (!annotations.any { it.shortName.asString() == C.ANNOTATION_GHOST_SERIALIZATION }) {
return null
}
return this
}
}