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

Class Method, % Branch, % Line, % Instruction, %
DeserializeCodeEmitter 83.3% (15/18) 63% (68/108) 84.3% (253/300) 83.6% (1291/1544)
DeserializeCodeEmitter$InferredSealedContext 100% (1/1) 100% (6/6) 100% (27/27)
Total 84.2% (16/19) 63% (68/108) 84.6% (259/306) 83.9% (1318/1571)


 package com.ghost.serialization.compiler
 
 import com.ghost.serialization.compiler.GhostEmitterConstants.PROPERTY_MAX_SIZE
 import com.google.devtools.ksp.symbol.KSClassDeclaration
 import com.squareup.kotlinpoet.BOOLEAN
 import com.squareup.kotlinpoet.ClassName
 import com.squareup.kotlinpoet.CodeBlock
 import com.squareup.kotlinpoet.FunSpec
 import com.squareup.kotlinpoet.KModifier
 import com.squareup.kotlinpoet.PropertySpec
 import com.squareup.kotlinpoet.TypeSpec
 import com.squareup.kotlinpoet.ksp.toClassName
 import com.ghost.serialization.compiler.GhostEmitterConstants as C
 
 /**
  * Main coordinator (Orchestrator) for the deserialization code generation process.
  *
  * This class implements the Strategy Pattern to select the most efficient deserialization
  * logic for a given DTO. It acts as the central hub that inspects the DTO's metadata
  * (sealed hierarchy, enum, size, or structural complexity) and delegates the generation
  * task to specialized emitters.
  *
  * ### Orchestration Strategy:
  * - **Polymorphic Types:** Direct delegation for Sealed or [Enum] types.
  * - **Large DTOs (> [PROPERTY_MAX_SIZE]):** Delegates to [FragmentedEmitter] to bypass JVM 64KB method limits.
  * - **Standard DTOs:** Delegates to [StandardEmitter] for highly-optimized, inlinable code.
  *
  * @property properties The list of property metadata to be deserialized.
  * @property originalClassName The canonical name of the class being generated.
  * @property readerClass The specific reader implementation (e.g., GhostJsonFlatReader).
  * @property isSealed Identifies if the class is part of a sealed hierarchy.
  * @property isValue Identifies if the class is a Kotlin Value Class (inline class).
  * @property isEnum Identifies if the class is an Enum.
  * @property isInferred Handles polymorphic types where the discriminator is absent,
  * relying on property presence to identify the subclass.
  */
 internal class DeserializeCodeEmitter(
     properties: List<GhostPropertyModel>,
     originalClassName: ClassName,
     readerClass: ClassName,
     private val isSealed: Boolean,
     private val isValue: Boolean,
     private val isEnum: Boolean,
     private val sealedSubclasses: List<KSClassDeclaration>,
     private val sealedDiscriminatorKey: String = C.DEFAULT_DISCRIMINATOR_KEY,
     private val isResilientClass: Boolean = false,
     private val isInferred: Boolean = false,
     private val isObject: Boolean = false,
     private val hasFallback: Boolean = false
 ) : BaseDeserializeEmitter(properties, originalClassName, readerClass) {
 
     /**
      * Entry point for the code generation pipeline.
      *
      * Evaluates the DTO structure to determine the optimal generation strategy.
      * It manages the injection of contextual serializers and ensures [isResilientClass]
      * metadata is correctly propagated to the generated serializer.
      *
      * @param typeSpecBuilder The builder for the serializer object/class.
      * @param isFlatPath Whether the code is generating for a flat reader vs standard.
      */
     fun build(
         typeSpecBuilder: TypeSpec.Builder,
         isFlatPath: Boolean = false
     ) {
         val body = CodeBlock.builder()
 
         when {
             isObject -> {
                 emitObjectReturn(body)
             }
             isSealed && isInferred -> {
                 emitInferredSealed(body)
             }
             isSealed -> {
                 emitSealed(body)
             }
             isValue -> {
                 emitValue(body)
             }
             isEnum -> {
                 emitEnum(body)
             }
             properties.size > PROPERTY_MAX_SIZE -> {
                 emitFragmented(body, typeSpecBuilder, isFlatPath)
             }
             else -> {
                 emitStandard(body, typeSpecBuilder, isFlatPath)
             }
         }
 
         addDeserializeFunction(typeSpecBuilder, body.build())
         injectResilienceProperty(typeSpecBuilder, isFlatPath)
     }
 
     /**
      * Instantiates and delegates code generation to [FragmentedEmitter].
      */
     private fun emitFragmented(
         body: CodeBlock.Builder,
         typeSpecBuilder: TypeSpec.Builder,
         isFlatPath: Boolean
     ) {
         val emitter = FragmentedEmitter(
             properties,
             originalClassName,
             readerClass
         )
         emitter.emit(body, typeSpecBuilder, isFlatPath = isFlatPath)
         if (!isFlatPath) {
             emitter.injectContextualSerializers(typeSpecBuilder)
         }
     }
 
     /**
      * Instantiates and delegates code generation to [StandardEmitter].
      */
     private fun emitStandard(
         body: CodeBlock.Builder,
         typeSpecBuilder: TypeSpec.Builder,
         isFlatPath: Boolean
     ) {
         val emitter = StandardEmitter(
             properties,
             originalClassName,
             readerClass
         )
         emitter.emit(body, typeSpecBuilder)
         if (!isFlatPath) {
             emitter.injectContextualSerializers(typeSpecBuilder)
         }
     }
 
     /**
      * Emits deserialization logic for object types (singletons). Consumes the JSON object
      * body and returns the singleton instance.
      */
     private fun emitObjectReturn(body: CodeBlock.Builder) {
         body.addStatement(C.STR_BEGIN_OBJECT)
         body.beginControlFlow(C.STR_WHILE_TRUE)
         body.addStatement(C.STR_SELECT_NAME_AND_CONSUME)
         body.beginControlFlow(C.STR_WHEN_INDEX)
         body.addStatement(C.STR_MINUS_ONE_BREAK)
         body.beginControlFlow(C.STR_MINUS_TWO_ARROW)
         body.addStatement(C.STR_SKIP_VALUE)
         body.endControlFlow()
         body.endControlFlow()
         body.endControlFlow()
         body.addStatement(C.STR_END_OBJECT)
         body.addStatement("return %T", originalClassName)
     }
 
     /**
      * Reads @GhostSerialization(name) from a class declaration, falling back to simple class name.
      */
     private fun getSubclassDiscriminator(subclass: KSClassDeclaration): String {
         val customName = subclass.annotations
             .find { it.shortName.asString() == C.ANNOTATION_GHOST_SERIALIZATION }
             ?.arguments?.find { it.name?.asString() == C.ARG_NAME }?.value as? String
         return if (!customName.isNullOrEmpty()) customName else subclass.simpleName.asString()
     }
 
     /**
      * Conditionally injects the `isResilient` property
      * if the class is resilient and not flat.
      */
     private fun injectResilienceProperty(
         typeSpecBuilder: TypeSpec.Builder,
         isFlatPath: Boolean
     ) {
         if (isResilientClass && !isFlatPath) {
             typeSpecBuilder.addProperty(
                 PropertySpec.builder(
                     C.STR_IS_RESILIENT,
                     BOOLEAN
                 )
                     .addModifiers(KModifier.OVERRIDE)
                     .initializer(C.STR_TRUE)
                     .build()
             )
         }
     }
 
     /**
      * Adds the final deserialize function to the generated serializer.
      * This method fulfills the contract of GhostSerializer.
      */
     private fun addDeserializeFunction(
         typeSpecBuilder: TypeSpec.Builder,
         body: CodeBlock
     ) {
         typeSpecBuilder.addFunction(
             FunSpec.builder(C.STR_DESERIALIZE)
                 .addKdoc(C.STR_KDOC_DESERIALIZE, originalClassName)
                 .addModifiers(KModifier.OVERRIDE)
                 .addParameter(C.STR_READER, readerClass)
                 .returns(originalClassName)
                 .addCode(body)
                 .build()
         )
     }
 
     /**
      * Emits deserialization logic for sealed hierarchies via discriminator key checks.
      * It generates a `when` expression that inspects the JSON discriminator field to
      * decide which specialized serializer to invoke. Includes support for fallback
      * subclasses when a discriminator value doesn't match known types.
      */
     private fun emitSealed(body: CodeBlock.Builder) {
         val fallbackSubclass = sealedSubclasses.find { subclass ->
             subclass.annotations.any {
                 it.shortName.asString() == C.STR_FALLBACK_ANNOTATION
             }
         }
 
         val regularSubclasses = sealedSubclasses.filter {
             it != fallbackSubclass
         }
 
         if (fallbackSubclass != null) {
             body.addStatement(
                 C.TEMPLATE_PEEK_STRING_FIELD,
                 sealedDiscriminatorKey
             )
         } else {
             body.addStatement(
                 C.TEMPLATE_PEEK_TYPE,
                 sealedDiscriminatorKey,
                 C.STR_MISSING_TYPE
             )
         }
 
         body.beginControlFlow(C.STR_WHEN_TYPENAME)
 
         regularSubclasses.forEach { subclass ->
             val subClassName = subclass.toClassName()
             val serializerName = subClassName.serializerClassName()
             val discriminatorValue = getSubclassDiscriminator(subclass)
             body.addStatement(
                 C.TEMPLATE_DESERIALIZE_BRANCH,
                 discriminatorValue,
                 serializerName
             )
         }
         if (fallbackSubclass != null) {
             val fallbackClassName = fallbackSubclass.toClassName()
             val fallbackSerializerName = fallbackClassName.serializerClassName()
             body.beginControlFlow(C.STR_ELSE_BRANCH)
             body.addStatement(C.TEMPLATE_DESERIALIZE_T, fallbackSerializerName)
             body.endControlFlow()
         } else {
             body.addStatement(C.STR_UNKNOWN_TYPE)
         }
         body.endControlFlow()
         body.addStatement(C.STR_RETURN_RESULT)
     }
 
     /**
      * Emits deserialization logic for Inferred Polymorphism.
      *
      * This is an advanced strategy where the subclass is determined by the presence
      * of specific fields in the JSON. It uses a **Property-to-Class bitmask** to identify
      * candidate subclasses and resolves them using a voting-like logic on the eligibility bitmask.
      */
     private fun emitInferredSealed(body: CodeBlock.Builder) {
         val context = createInferredSealedContext(properties)
         if (context == null) {
             body.addStatement(C.TEMPLATE_THROW_S, C.STR_ERR_NO_SUBCLASSES)
             return
         }
 
         emitInferredSealedLocalVariables(body, context)
         emitInferredSealedMainLoop(body, context)
         emitInferredSealedRequiredMasks(body, context)
         emitInferredSealedDecisionBlock(body, context)
     }
 
     /**
      * Context data holder containing all analyzed property and subclass metadata
      * needed for inferred sealed class deserialization code generation.
      */
     private class InferredSealedContext(
         val inferredInfo: List<InferredSubclassModel>,
         val names: List<String>,
         val allProps: List<GhostPropertyModel>,
         val nameToIndex: Map<String, Int>,
         val propertyToClassMask: Map<String, Long>
     )
 
     /**
      * Resolves and builds the [InferredSealedContext] from the property models.
      * Returns null if no inferred subclasses are declared.
      */
     private fun createInferredSealedContext(properties: List<GhostPropertyModel>): InferredSealedContext? {
         val inferredInfo = properties.firstOrNull()?.inferredSubclasses ?: emptyList()
         if (inferredInfo.isEmpty()) {
             return null
         }
 
         val names = inferredInfo.flatMap { it.properties }.map { it.jsonName }.distinct()
         val allProps = names.map { name ->
             inferredInfo.flatMap { it.properties }.find { it.jsonName == name }!!
         }
 
         val nameToIndex = names.mapIndexed { index, name -> name to index }.toMap()
 
         val propertyToClassMask = names.associateWith { name ->
             var mask = C.VAL_ZERO_L
             inferredInfo.forEachIndexed { index, subclass ->
                 if (subclass.properties.any { it.jsonName == name }) {
                     mask = mask or (C.VAL_ONE_L shl index)
                 }
             }
             mask
         }
 
         return InferredSealedContext(
             inferredInfo = inferredInfo,
             names = names,
             allProps = allProps,
             nameToIndex = nameToIndex,
             propertyToClassMask = propertyToClassMask
         )
     }
 
     /**
      * Emits the local variable declarations for tracking property values and
      * managing the seen and eligibility bitmasks.
      */
     private fun emitInferredSealedLocalVariables(
         body: CodeBlock.Builder,
         context: InferredSealedContext
     ) {
         body.addStatement(C.STR_BEGIN_OBJECT)
         context.allProps.forEachIndexed { index, prop ->
             body.addStatement(
                 C.TEMPLATE_VAR_NULL_DECL,
                 C.STR_V_VAR_PREFIX,
                 index,
                 prop.typeName.copy(nullable = true),
                 C.STR_NULL
             )
         }
 
         body.addStatement(
             C.TEMPLATE_VAR_LONG_INIT,
             C.STR_ELIGIBILITY_MASK,
             (C.VAL_ONE_L shl context.inferredInfo.size) - C.VAL_ONE,
             C.STR_L_SUFFIX
         )
 
         body.addStatement(
             C.TEMPLATE_VAR_INIT,
             C.STR_SEEN_MASK,
             C.STR_ZERO_L
         )
     }
 
     /**
      * Emits the main loops for parsing and consuming JSON field names.
      * When a field matches a known subclass signature, its value is parsed,
      * the eligibility mask is updated, and the seen mask is updated.
      */
     private fun emitInferredSealedMainLoop(
         body: CodeBlock.Builder,
         context: InferredSealedContext
     ) {
         body.beginControlFlow(C.STR_WHILE_TRUE)
         body.addStatement(C.STR_SELECT_NAME_AND_CONSUME)
         body.beginControlFlow(C.STR_WHEN_INDEX)
 
         context.names.forEachIndexed { index, name ->
             val prop = context.allProps[index]
             val classMask = context.propertyToClassMask[name] ?: C.VAL_ZERO_L
 
             body.beginControlFlow(
                 C.TEMPLATE_WHEN_BRANCH,
                 index
             )
             body.addStatement(
                 C.TEMPLATE_VAR_ASSIGN,
                 C.STR_V_VAR_PREFIX,
                 index,
                 buildCall(prop)
             )
             body.addStatement(
                 C.TEMPLATE_MASK_AND_ASSIGN,
                 C.STR_ELIGIBILITY_MASK,
                 C.STR_ELIGIBILITY_MASK,
                 classMask,
                 C.STR_L_SUFFIX
             )
             body.addStatement(
                 C.TEMPLATE_MASK_OR_SHL_ASSIGN,
                 C.STR_SEEN_MASK,
                 C.STR_SEEN_MASK,
                 C.STR_ONE_L,
                 C.STR_SHL,
                 index
             )
             body.endControlFlow()
         }
 
         body.addStatement(C.STR_MINUS_ONE_BREAK)
         body.addStatement(
             C.STR_ELSE_BRANCH +
                     C.STR_SPACE +
                     C.STR_SKIP_VALUE
         )
         body.endControlFlow()
         body.endControlFlow()
         body.addStatement(C.STR_END_OBJECT)
     }
 
     /**
      * Precomputes and emits the required property masks
      * for each subclass candidate.
      */
     private fun emitInferredSealedRequiredMasks(
         body: CodeBlock.Builder,
         context: InferredSealedContext
     ) {
         context.inferredInfo.forEachIndexed { index, subclass ->
             var reqMask = C.VAL_ZERO_L
             subclass.properties.forEach { prop ->
                 if (!prop.isNullable && !prop.hasDefaultValue) {
                     val pIdx = context.nameToIndex[prop.jsonName]!!
                     reqMask = reqMask or (C.VAL_ONE_L shl pIdx)
                 }
             }
             body.addStatement(
                 C.TEMPLATE_VAL_LONG_INIT,
                 C.STR_REQ_MASK_PREFIX,
                 index,
                 reqMask,
                 C.STR_L_SUFFIX
             )
         }
     }
 
     /**
      * Emits the decision block logic to determine the matched subclass and
      * instantiate it using the parsed arguments. Throws a GhostJsonException if no
      * unique matching subclass can be resolved.
      */
     private fun emitInferredSealedDecisionBlock(
         body: CodeBlock.Builder,
         context: InferredSealedContext
     ) {
         val jsonExClass = ClassName(
             C.PKG_EXCEPTION,
             C.STR_GHOST_JSON_EXCEPTION
         )
 
         body.beginControlFlow(C.TEMPLATE_RESULT_WHEN)
         context.inferredInfo.forEachIndexed { subclassIndex, subclass ->
             val subclassClassName = subclass.declaration.toClassName()
             val maskBit = C.VAL_ONE_L shl subclassIndex
             val reqMaskVar = "${C.STR_REQ_MASK_PREFIX}$subclassIndex"
 
             body.beginControlFlow(
                 C.TEMPLATE_INFERRED_DECISION_BRANCH,
                 C.STR_ELIGIBILITY_MASK,
                 maskBit,
                 C.STR_L_SUFFIX,
                 C.STR_ZERO_L,
                 C.STR_SEEN_MASK,
                 reqMaskVar,
                 reqMaskVar
             )
 
             val requiredProps = subclass.properties.filter { !it.hasDefaultValue }
             val defaultProps = subclass.properties.filter { it.hasDefaultValue }
 
             val requiredArgs = CodeBlock.builder()
             requiredProps.forEachIndexed { i, prop ->
                 val pIdx = context.nameToIndex[prop.jsonName]!!
                 val vVar = "${C.STR_V_VAR_PREFIX}$pIdx"
                 if (!prop.isNullable) {
                     val msg = C.STR_REQUIRED_FIELD_MISSING.format(
                         prop.jsonName,
                         subclassClassName.simpleName
                     )
                     requiredArgs.add(
                         C.TEMPLATE_REQUIRED_ARG,
                         prop.kotlinName,
                         vVar,
                         jsonExClass,
                         msg
                     )
                 } else {
                     requiredArgs.add(
                         C.TEMPLATE_OPTIONAL_ARG,
                         prop.kotlinName,
                         vVar
                     )
                 }
 
                 if (i < requiredProps.size - C.VAL_ONE) {
                     requiredArgs.add(C.STR_COMMA_SPACE)
                 }
             }
 
             if (defaultProps.isEmpty()) {
                 body.addStatement(
                     C.TEMPLATE_CONSTRUCTOR,
                     subclassClassName,
                     requiredArgs.build()
                 )
             } else {
                 body.addStatement(
                     C.TEMPLATE_DATA_CLASS_COPY_INIT,
                     C.STR_INSTANCE_VAR,
                     subclassClassName,
                     requiredArgs.build()
                 )
                 defaultProps.forEach { prop ->
                     val pIdx = context.nameToIndex[prop.jsonName]!!
                     val vVar = "${C.STR_V_VAR_PREFIX}$pIdx"
                     body.addStatement(
                         C.TEMPLATE_IF_NOT_NULL_COPY,
                         vVar,
                         C.STR_INSTANCE_VAR,
                         C.STR_INSTANCE_VAR,
                         C.STR_COPY,
                         prop.kotlinName,
                         vVar
                     )
                 }
                 body.addStatement(
                     C.TEMPLATE_VARIABLE,
                     C.STR_INSTANCE_VAR
                 )
             }
             body.endControlFlow()
         }
         body.addStatement(
             C.STR_ELSE_BRANCH + C.STR_SPACE + C.TEMPLATE_THROW_EXCEPTION,
             jsonExClass,
             C.STR_INFERRED_ERROR_MSG
         )
         body.endControlFlow()
         body.addStatement(C.STR_RETURN_RESULT)
     }
 
     /**
      * Emits deserialization logic for value class types.
      */
     private fun emitValue(body: CodeBlock.Builder) {
         val prop = properties.firstOrNull() ?: return
         val call = buildCall(prop)
         body.addStatement(
             C.TEMPLATE_RETURN_CONSTRUCTOR,
             originalClassName,
             call
         )
     }
 
     /**
      * Emits deserialization logic for enum types using integer lookups.
      */
     private fun emitEnum(body: CodeBlock.Builder) {
         body.addStatement(C.STR_ENUM_SELECT_OPTIONS)
         body.beginControlFlow(C.STR_ENUM_WHEN)
 
         properties
             .firstOrNull()
             ?.enumValues
             ?.entries
             ?.forEachIndexed { index, entry ->
                 body.addStatement(
                     C.TEMPLATE_ENUM_BRANCH,
                     index,
                     originalClassName,
                     entry.key
                 )
             }
 
         body.addStatement(C.STR_ERR_INVALID_ENUM_INDEX)
         val fallbackEntry = properties.firstOrNull()?.enumValues?.entries?.find { it.key.equals("UNKNOWN", ignoreCase = true) }
             ?: if (hasFallback) properties.firstOrNull()?.enumValues?.entries?.lastOrNull() else null
         if (fallbackEntry != null) {
             body.addStatement("else -> %T.%L", originalClassName, fallbackEntry.key)
         } else {
             body.addStatement(C.STR_ERR_UNEXPECTED_INDEX)
         }
         body.endControlFlow()
     }
 }