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

Class Class, % Method, % Branch, % Line, % Instruction, %
FragmentedEmitter 0% (0/1) 0% (0/9) 0% (0/42) 0% (0/182) 0% (0/991)


 @file:Suppress("unused", "SameParameterValue")
 
 package com.ghost.serialization.compiler
 
 import com.squareup.kotlinpoet.ClassName
 import com.squareup.kotlinpoet.CodeBlock
 import com.squareup.kotlinpoet.FunSpec
 import com.squareup.kotlinpoet.INT
 import com.squareup.kotlinpoet.KModifier
 import com.squareup.kotlinpoet.PropertySpec
 import com.squareup.kotlinpoet.TypeSpec
 import com.ghost.serialization.compiler.GhostEmitterConstants as C
 
 /**
  * Emitter for fragmented deserialization logic.
  *
  * This emitter is designed for large classes (typically > 40 properties) to bypass JVM
  * method size limits (64KB bytecode limit). It fragments the decoding process by creating
  * a helper context class (`DecodingContext`) to store deserialized properties and bitmasks,
  * and splitting the field assignment logic into multiple chunk methods (e.g. `decodeChunk0`, `decodeChunk1`).
  *
  * @property properties The list of property models.
  * @property originalClassName The target class to deserialize.
  * @property readerClass The reader implementation class used.
  */
 internal class FragmentedEmitter(
     properties: List<GhostPropertyModel>,
     originalClassName: ClassName,
     readerClass: ClassName
 ) : BaseDeserializeEmitter(properties, originalClassName, readerClass) {
 
     /**
      * Emits the fragmented deserialization logic.
      *
      * It delegates the construction of the private `DecodingContext` class, declarations
      * of chunk functions, and the emission of the main parsing loop to smaller helper methods,
      * then validates and instantiates the target DTO.
      *
      * @param body The target KotlinPoet [CodeBlock.Builder].
      * @param typeSpecBuilder The serializer class builder.
      * @param isFlatPath Whether the flat reader path is used.
      */
     fun emit(
         body: CodeBlock.Builder,
         typeSpecBuilder: TypeSpec.Builder,
         isFlatPath: Boolean = false
     ) {
         emitPropertyMaskConstants(typeSpecBuilder)
         val contextClassName = ClassName(
             C.STR_EMPTY,
             C.STR_CTX_CLASS
         )
         val chunkSize = C.DEFAULT_CHUNK_SIZE
         val chunks = properties.chunked(chunkSize)
 
         buildDecodingContext(typeSpecBuilder, contextClassName, isFlatPath)
 
         chunks.forEachIndexed { chunkIdx, chunkProps ->
             emitChunkFunction(
                 chunkIdx,
                 chunkProps,
                 chunkSize,
                 readerClass,
                 contextClassName,
                 typeSpecBuilder
             )
         }
 
         emitMainParseLoop(body, chunks, chunkSize)
         emitValidation(body)
         emitReturn(body, typeSpecBuilder)
 
         emitValidationHelper(typeSpecBuilder, contextClassName)
     }
 
     /**
      * Builds and registers the private `DecodingContext` class to track properties and masks.
      *
      * @param typeSpecBuilder The serializer class builder.
      * @param contextClassName Class name of the context object holding property variables.
      * @param isFlatPath Whether the flat reader path is used.
      */
     private fun buildDecodingContext(
         typeSpecBuilder: TypeSpec.Builder,
         contextClassName: ClassName,
         isFlatPath: Boolean
     ) {
         val contextBuilder = TypeSpec.classBuilder(contextClassName)
             .addModifiers(KModifier.PRIVATE)
 
         properties.forEach {
             val varType = it.getVariableType()
             val initialValue = it.getInitialValue()
             contextBuilder.addProperty(
                 PropertySpec.builder(
                     it.kotlinName,
                     varType
                 )
                     .mutable(true)
                     .initializer(initialValue)
                     .build()
             )
         }
 
         for (index in C.VAL_ZERO until maskCount) {
             contextBuilder.addProperty(
                 PropertySpec.builder(
                     C.STR_MASK_INDEX_FMT.format(index),
                     com.squareup.kotlinpoet.LONG
                 )
                     .mutable(true)
                     .initializer(C.STR_ZERO_L)
                     .build()
             )
         }
 
         if (!isFlatPath) {
             typeSpecBuilder.addType(contextBuilder.build())
         }
     }
 
     /**
      * Emits the main parse loop mapping selector indexes to fragmented chunk calls.
      *
      * @param body The target KotlinPoet [CodeBlock.Builder].
      * @param chunks Grouped DTO properties.
      * @param chunkSize Size of a chunk.
      */
     private fun emitMainParseLoop(
         body: CodeBlock.Builder,
         chunks: List<List<GhostPropertyModel>>,
         chunkSize: Int
     ) {
         body.addStatement(C.STR_CTX_INIT)
         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)
         
         chunks.forEachIndexed { chunkIdx, chunkProps ->
             val start = chunkIdx * chunkSize
             val end = start + chunkProps.size - C.VAL_ONE
             val chunkFunName = C.TEMPLATE_DECODE_CHUNK_NAME
                 .format(C.STR_DECODE_CHUNK_PREFIX, chunkIdx)
 
             body.addStatement(
                 C.TEMPLATE_CHUNK_CALL,
                 start,
                 end,
                 chunkFunName
             )
         }
 
         body.addStatement(C.STR_MINUS_ONE_BREAK)
 
         body.beginControlFlow(C.STR_MINUS_TWO_ARROW)
         body.addStatement(C.STR_SKIP_VALUE)
         body.endControlFlow()
         body.endControlFlow() // when
         body.endControlFlow() // while
         body.addStatement(C.STR_END_OBJECT)
     }
 
     /**
      * Emits a private chunk decoding helper function.
      *
      * This generated method maps index selections directly to field assignments and tracking masks
      * in the `DecodingContext` instance, keeping the size of each method small.
      *
      * @param chunkIdx The chunk index.
      * @param chunkProps The list of DTO properties assigned to this chunk.
      * @param chunkSize Size of a chunk.
      * @param readerClass The reader class used.
      * @param contextClassName Class name of the context object holding property variables.
      * @param typeSpecBuilder Serializer class builder.
      */
     private fun emitChunkFunction(
         chunkIdx: Int,
         chunkProps: List<GhostPropertyModel>,
         chunkSize: Int,
         readerClass: ClassName,
         contextClassName: ClassName,
         typeSpecBuilder: TypeSpec.Builder
     ) {
         val chunkFun = FunSpec
             .builder(
                 C.TEMPLATE_DECODE_CHUNK_NAME
                     .format(C.STR_DECODE_CHUNK_PREFIX, chunkIdx)
             )
             .addModifiers(KModifier.PRIVATE)
             .addParameter(C.STR_READER_VAR, readerClass)
             .addParameter(C.STR_CTX_VAR, contextClassName)
             .addParameter(C.STR_INDEX_VAR, INT)
 
         val chunkBody = CodeBlock.builder()
         chunkBody.beginControlFlow(C.STR_WHEN_INDEX_PLAIN)
         chunkProps.forEachIndexed { innerIdx, prop ->
             val globalIndex = chunkIdx * chunkSize + innerIdx
             val call = buildCall(prop)
             val maskIdx = globalIndex / C.MASK_SIZE_BITS.toInt()
             val constName = "MASK_" + prop.kotlinName.uppercase()
             
             chunkBody.beginControlFlow("$globalIndex${C.STR_ARROW}")
             if (prop.isResilient) {
                 chunkBody.beginControlFlow(C.TEMPLATE_DECODE_RESILIENT, call)
                 chunkBody.addStatement(C.TEMPLATE_CTX_FIELD_SET_IT, prop.kotlinName)
                 chunkBody.addStatement(C.TEMPLATE_CTX_MASK_OR, maskIdx, maskIdx, constName)
                 chunkBody.endControlFlow()
             } else {
                 chunkBody.addStatement(C.TEMPLATE_CTX_FIELD_ASSIGN, prop.kotlinName, call)
                 chunkBody.addStatement(C.TEMPLATE_CTX_MASK_OR, maskIdx, maskIdx, constName)
             }
             chunkBody.endControlFlow()
         }
         chunkBody.endControlFlow()
         chunkFun.addCode(chunkBody.build())
         typeSpecBuilder.addFunction(chunkFun.build())
     }
 
     /**
      * Emits required properties validation logic.
      *
      * Iterates over bitmasks. If a mask has required fields, it emits validation code that
      * checks the tracking mask in `DecodingContext`.
      *
      * @param body The target KotlinPoet [CodeBlock.Builder].
      */
     private fun emitValidation(body: CodeBlock.Builder) {
         val hasRequired = properties.any { !it.isNullable && !it.hasDefaultValue }
         if (hasRequired) {
             body.addStatement(C.TEMPLATE_CALL_VALIDATION, C.STR_FUN_VALIDATE_FIELDS, C.STR_CTX_VAR, C.STR_READER_VAR)
         }
     }
 
     /**
      * Generates a descriptive private helper method validating that all required properties
      * were present in the bitmask of the context class, throwing a GhostJsonException for any missing field.
      *
      * @param typeSpecBuilder The serializer class builder.
      * @param contextClassName Class name of the context object holding property variables.
      */
     private fun emitValidationHelper(typeSpecBuilder: TypeSpec.Builder, contextClassName: ClassName) {
         val hasRequired = properties.any { !it.isNullable && !it.hasDefaultValue }
         if (!hasRequired) {
             return
         }
 
         val funBuilder = FunSpec.builder(C.STR_FUN_VALIDATE_FIELDS)
             .addModifiers(KModifier.PRIVATE)
             .addParameter(C.STR_CTX_VAR, contextClassName)
             .addParameter(C.STR_READER_VAR, readerClass)
 
         val funBody = CodeBlock.builder()
         for (maskIdx in C.VAL_ZERO until maskCount) {
             val reqMask = requiredMasks[maskIdx]
             if (reqMask != C.VAL_ZERO_L) {
                 val requiredMaskName = "MASK_REQUIRED_$maskIdx"
 
                 funBody.beginControlFlow(
                     C.TEMPLATE_IF_MASK_NOT_MET,
                     maskIdx,
                     requiredMaskName,
                     requiredMaskName
                 )
 
                 var isFirst = true
                 properties.forEachIndexed { propIdx, prop ->
                     if (
                         !prop.isNullable &&
                         !prop.hasDefaultValue
                         && (propIdx / C.MASK_SIZE_BITS.toInt()) == maskIdx
                     ) {
                         val constName = "MASK_" + prop.kotlinName.uppercase()
 
                         if (isFirst) {
                             funBody.beginControlFlow(
                                 C.TEMPLATE_IF_MASK_MISSING,
                                 maskIdx,
                                 constName
                             )
                             isFirst = false
                         } else {
                             funBody.nextControlFlow(
                                 C.TEMPLATE_ELSE_IF_MASK_MISSING,
                                 maskIdx,
                                 constName
                             )
                         }
 
                         funBody.addStatement(
                             C.TEMPLATE_THROW_S,
                             C.STR_REQ_FIELD_1 + prop.jsonName + C.STR_REQ_FIELD_2
                         )
                     }
                 }
                 if (!isFirst) {
                     funBody.endControlFlow()
                 }
                 funBody.endControlFlow()
             }
         }
 
         funBuilder.addCode(funBody.build())
         typeSpecBuilder.addFunction(funBuilder.build())
     }
     
     /**
      * Emits the target class instantiation return statement.
      *
      * Resolves variables from `DecodingContext`. Uses copy-based updates for default properties.
      *
      * @param body The target KotlinPoet [CodeBlock.Builder].
      * @param typeSpecBuilder The serializer class builder.
      */
     private fun emitReturn(body: CodeBlock.Builder, typeSpecBuilder: TypeSpec.Builder) {
         val requiredProps = properties.filter { it.isInConstructor && !it.hasDefaultValue }
         body.addStatement(C.TEMPLATE_VAL_RESULT, originalClassName)
 
         requiredProps.forEach { prop ->
             body.addStatement(
                 C.TEMPLATE_NAMED_ARG,
                 prop.kotlinName,
                 prop.getFragmentedReturnExpression()
             )
         }
 
         body.addStatement(C.STR_PAREN)
 
         val defaultProps = properties.filter { it.isInConstructor && it.hasDefaultValue }
         if (defaultProps.isNotEmpty()) {
             body.add(C.STR_IF_OPEN)
 
             val conditions = mutableListOf<String>()
             for (i in defaultMasks.indices) {
                 val defMask = defaultMasks[i]
                 if (defMask != C.VAL_ZERO_L) {
                     val constName = emitDefaultMaskConstant(typeSpecBuilder, i)
                     conditions.add(
                         C.TEMPLATE_IF_MASK_MATCH_BIT_F
                             .format(i, constName)
                     )
                 }
             }
             body.add(conditions.joinToString(C.STR_OR))
             body.beginControlFlow(C.STR_CLOSE_PAREN_FLOW)
 
             body.addStatement(C.STR_RETURN_RESULT_COPY)
             val defaultPropsWithGlobalIndex = properties
                 .mapIndexedNotNull { globalIdx, prop ->
 
                 if (prop.isInConstructor && prop.hasDefaultValue) {
                     Pair(globalIdx, prop)
                 } else {
                     null
                 }
             }
 
             defaultPropsWithGlobalIndex.forEachIndexed { _, (propIndex, prop) ->
                 val maskIdx = propIndex / C.MASK_SIZE_BITS.toInt()
                 val constName = "MASK_" + prop.kotlinName.uppercase()
 
                 val valueExpr = prop
                     .getFragmentedDefaultValueReturnExpression(maskIdx, constName)
                 body.addStatement(C.TEMPLATE_NAMED_ARG, prop.kotlinName, valueExpr)
             }
             body.addStatement(C.STR_PAREN)
             body.nextControlFlow(C.STR_ELSE)
             body.addStatement(C.STR_RETURN_RESULT_FINAL)
             body.endControlFlow()
         } else {
             body.addStatement(C.STR_RETURN_RESULT_FINAL)
         }
     }
 }