Coverage Summary for Class: SerializerImportResolver (com.ghost.serialization.compiler)

Class Method, % Branch, % Line, % Instruction, %
SerializerImportResolver 100% (12/12) 80.5% (132/164) 98.7% (155/157) 95.6% (1084/1134)
SerializerImportResolver$ByteArrayCoverage 100% (1/1) 100% (1/1) 100% (18/18)
Total 100% (13/13) 80.5% (132/164) 98.7% (156/158) 95.7% (1102/1152)


 package com.ghost.serialization.compiler
 
 import com.google.devtools.ksp.symbol.KSType
 import com.squareup.kotlinpoet.FileSpec
 import com.ghost.serialization.compiler.GhostEmitterConstants as C
 
 /**
  * Plans and applies conditional parser/type imports for a generated serializer file.
  */
 internal class SerializerImportResolver(
     private val ctx: GhostSerializerContext,
 ) {
 
     fun applyTo(fileBuilder: FileSpec.Builder) {
         if (ctx.needsObjectParsingImports()) {
             fileBuilder.addImport(
                 C.PKG_PARSER,
                 C.STR_BEGIN_OBJECT_NAME,
                 C.STR_END_OBJECT_NAME,
                 C.STR_SELECT_NAME_AND_CONSUME_NAME,
                 C.STR_SKIP_VALUE_NAME
             )
         }
 
         val (allTypes, hasNullable) = resolveAllTypes()
         addParserImports(fileBuilder, allTypes, hasNullable)
 
         if (ctx.needsCachedByteStringHeaders()) {
             fileBuilder.addImport(C.OKIO_PACKAGE, C.STR_BYTESTRING_IMPORT)
         }
     }
 
     private fun resolveAllTypes(): Pair<List<String>, Boolean> {
         var hasNullable = ctx.properties.any { it.isNullable }
         val allTypes = ctx.properties.flatMap { prop ->
             val types = mutableListOf<String>()
             fun collectTypes(type: KSType) {
                 types.add(type.toString())
                 if (type.isMarkedNullable) {
                     hasNullable = true
                 }
                 if (isValueClassType(type)) {
                     val inner = resolveValueClassInnerType(type)
                     if (inner != null) {
                         collectTypes(inner)
                     }
                 }
                 for (arg in type.arguments) {
                     val resolved = arg.type?.resolve()
                     if (resolved != null) {
                         collectTypes(resolved)
                     }
                 }
             }
             collectTypes(prop.type)
             prop.valueClassProperty?.let { collectTypes(it.type) }
 
             prop.inferredSubclasses.forEach { sub ->
                 if (ctx.isInferred) {
                     sub.properties.forEach { subProp ->
                         collectTypes(subProp.type)
                         subProp.valueClassProperty?.let { collectTypes(it.type) }
                     }
                 }
             }
             types
         }
         return allTypes to hasNullable
     }
 
     private fun addParserImports(
         fileBuilder: FileSpec.Builder,
         allTypes: List<String>,
         hasNullable: Boolean,
     ) {
         val hasList = ctx.properties.any { it.isList } ||
             allTypes.any { it.contains(C.STR_LIST) }
         val hasSet = ctx.properties.any { it.isSet } ||
             allTypes.any { it.contains(C.STR_SET) }
         val hasMap = ctx.properties.any { it.isMap } ||
             allTypes.any { it.contains(C.STR_MAP) }
 
         if (hasList || hasSet) {
             fileBuilder.addImport(
                 C.PKG_PARSER,
                 C.STR_BEGIN_ARRAY,
                 C.STR_END_ARRAY
             )
         }
         if (hasList) {
             fileBuilder.addImport(C.PKG_PARSER, C.STR_READ_LIST)
         }
         if (hasSet) {
             fileBuilder.addImport(C.PKG_PARSER, C.STR_READ_SET)
         }
         if (hasMap) {
             fileBuilder.addImport(
                 C.PKG_PARSER,
                 C.STR_READ_MAP,
                 C.STR_NEXT_KEY
             )
         }
         if (hasNullable) {
             fileBuilder.addImport(
                 C.PKG_PARSER,
                 C.STR_CONSUME_NULL_NAME,
                 C.STR_IS_NEXT_NULL_VALUE_NAME
             )
         }
         if (ctx.isSealed && !ctx.isInferred) {
             fileBuilder.addImport(C.PKG_PARSER, C.STR_PEEK_STRING_FIELD)
         }
         if (ctx.isEnum) {
             fileBuilder.addImport(C.PKG_PARSER, C.STR_SELECT_STRING)
         }
         if (ctx.properties.any { it.isResilient }) {
             fileBuilder.addImport(C.PKG_PARSER, C.DECODE_RESILIENT)
         }
         if (ctx.properties.any { it.wrappedSourceKeys != null }) {
             fileBuilder.addImport(
                 C.PKG_PARSER,
                 C.STR_GHOST_WRAPPED_KEYS_CAPTURE,
                 C.STR_CAPTURE_WRAPPED_KEY_NAME,
             )
         }
 
         val allTypeStrings = allTypes.joinToString()
         if (allTypeStrings.contains(C.STR_INT)) {
             fileBuilder.addImport(C.PKG_PARSER, C.STR_NEXT_INT_NAME)
         }
         if (allTypeStrings.contains(C.STR_LONG_TYPE)) {
             fileBuilder.addImport(C.PKG_PARSER, C.STR_NEXT_LONG_NAME)
         }
         if (needsNextStringImport()) {
             fileBuilder.addImport(C.PKG_PARSER, C.STR_NEXT_STRING_NAME)
         }
         if (allTypeStrings.contains(C.STR_DOUBLE)) {
             fileBuilder.addImport(C.PKG_PARSER, C.STR_NEXT_DOUBLE_NAME)
         }
         if (allTypeStrings.contains(C.STR_FLOAT)) {
             fileBuilder.addImport(C.PKG_PARSER, C.STR_NEXT_FLOAT_NAME)
         }
         if (allTypeStrings.contains(C.K_BYTE) || allTypeStrings.contains(C.K_SHORT) ||
             ctx.properties.any { it.type.isPrimitiveByte() || it.type.isPrimitiveShort() }
         ) {
             fileBuilder.addImport(C.PKG_PARSER, C.STR_NEXT_INT_NAME)
         }
         if (allTypeStrings.contains(C.K_CHAR) ||
             ctx.properties.any { it.type.isPrimitiveChar() }
         ) {
             fileBuilder.addImport(C.PKG_PARSER, C.STR_NEXT_CHAR_NAME)
         }
         if (allTypeStrings.contains(C.STR_BOOLEAN)) {
             fileBuilder.addImport(C.PKG_PARSER, C.STR_NEXT_BOOLEAN_NAME)
         }
 
         val byteArrayClassifications = classifyAllByteArrayUsages()
         if (byteArrayClassifications.contains(ByteArrayCoverage.COVERED)) {
             fileBuilder.addImport(
                 C.PKG_PARSER,
                 C.STR_DECODE_BASE64_STRING_NAME,
                 C.STR_ENCODE_BASE64_STRING_NAME,
                 C.STR_NEXT_STRING_NAME
             )
         }
 
         val needsCaptureRawJsonBytes = allTypeStrings.contains(C.STR_BYTE_ARRAY_TYPE) &&
             byteArrayClassifications.contains(ByteArrayCoverage.UNCOVERED)
         val needsCaptureRawJson = allTypeStrings.contains(C.K_RAW_JSON) ||
             allTypeStrings.contains(C.STR_RAW_JSON_TYPE)
         if (needsCaptureRawJsonBytes) {
             fileBuilder.addImport(C.PKG_PARSER, C.STR_CAPTURE_RAW_JSON_BYTES_NAME)
         }
         if (needsCaptureRawJson) {
             fileBuilder.addImport(C.PKG_PARSER, C.STR_CAPTURE_RAW_JSON_NAME)
         }
         if (allTypeStrings.contains(C.K_RAW_JSON) ||
             allTypeStrings.contains(C.STR_RAW_JSON_TYPE)
         ) {
             fileBuilder.addImport(C.PKG_TYPES, C.STR_RAW_JSON_TYPE)
         }
         if (ctx.isEnum) {
             fileBuilder.addImport(C.PKG_EXCEPTION, C.STR_GHOST_JSON_EXCEPTION)
         }
     }
 
     private enum class ByteArrayCoverage { COVERED, UNCOVERED }
 
     /**
      * Classifies every `ByteArray` occurrence reachable from this class's properties (directly,
      * through `List`/`Set`/`Map` elements, through a value-class wrapper, or through an inferred
      * sealed subclass) as [ByteArrayCoverage.COVERED] by the proto3 Base64 codegen path (needs
      * `decodeBase64String`/`encodeBase64String`), or [ByteArrayCoverage.UNCOVERED] (still needs
      * the raw-JSON-passthrough `captureRawJsonBytes` import). Coverage mirrors exactly what
      * `BaseSerializeEmitter`/`BaseDeserializeEmitter` do: `isProto` propagates unchanged through
      * `List`/`Set`/`Map` recursion, but inferred sealed subclass properties are never proto-aware.
      */
     private fun classifyAllByteArrayUsages(): List<ByteArrayCoverage> {
         fun classify(type: KSType, isProto: Boolean): ByteArrayCoverage? {
             if (type.isByteArray()) {
                 return if (isProto) ByteArrayCoverage.COVERED else ByteArrayCoverage.UNCOVERED
             }
             if (isValueClassType(type)) {
                 val inner = resolveValueClassInnerType(type) ?: return null
                 return classify(inner, isProto)
             }
             if (type.isList() || type.isSet()) {
                 val inner = type.arguments.firstOrNull()?.type?.resolve() ?: return null
                 return classify(inner, isProto)
             }
             if (type.isMap()) {
                 val value = type.arguments.getOrNull(1)?.type?.resolve() ?: return null
                 return classify(value, isProto)
             }
             return null
         }
 
         val direct = ctx.properties.mapNotNull { classify(it.type, it.isProto) }
         val valueClass = ctx.properties.mapNotNull {
             it.valueClassProperty?.let { vcp -> classify(vcp.type, vcp.isProto) }
         }
         val inferred = ctx.properties.flatMap { it.inferredSubclasses }.flatMap { it.properties }
             .mapNotNull { classify(it.type, it.isProto) }
         return direct + valueClass + inferred
     }
 
     private fun needsNextStringImport(): Boolean {
         if (ctx.properties.any { propertyNeedsNextString(it) }) return true
         if (ctx.isInferred) {
             return ctx.properties.flatMap { it.inferredSubclasses }
                 .flatMap { it.properties }
                 .any { propertyNeedsNextString(it) }
         }
         return false
     }
 
     private fun propertyNeedsNextString(property: GhostPropertyModel): Boolean {
         if (typeNeedsNextString(property.type)) return true
         property.valueClassProperty?.let { underlying ->
             if (typeNeedsNextString(underlying.type)) return true
         }
         return false
     }
 
     private fun typeNeedsNextString(type: KSType): Boolean {
         if (type.isString()) return true
         if (type.isList() || type.isSet()) {
             val element = type.arguments.firstOrNull()?.type?.resolve() ?: return false
             return typeNeedsNextString(element)
         }
         if (type.isMap()) {
             val value = type.arguments.getOrNull(1)?.type?.resolve() ?: return false
             return typeNeedsNextString(value)
         }
         return false
     }
 
     private fun isValueClassType(type: KSType): Boolean {
         val declaration = type.declaration as? com.google.devtools.ksp.symbol.KSClassDeclaration ?: return false
         return declaration.modifiers.contains(com.google.devtools.ksp.symbol.Modifier.VALUE) ||
                 declaration.modifiers.contains(com.google.devtools.ksp.symbol.Modifier.INLINE)
     }
 
     private fun resolveValueClassInnerType(type: KSType): KSType? {
         val declaration = type.declaration as? com.google.devtools.ksp.symbol.KSClassDeclaration ?: return null
         val primaryConstructor = declaration.primaryConstructor ?: return null
         val param = primaryConstructor.parameters.firstOrNull() ?: return null
         return param.type.resolve()
     }
 }