Coverage Summary for Class: StandardEmitter (com.ghost.serialization.compiler)
| Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| StandardEmitter |
80%
(12/15)
|
57.5%
(61/106)
|
73.2%
(210/287)
|
70.9%
(1345/1896)
|
| StandardEmitter$emitMultiBranchReturn$$inlined$sortedByDescending$1 |
0%
(0/1)
|
|
| Total |
75%
(12/16)
|
57.5%
(61/106)
|
73.2%
(210/287)
|
70.9%
(1345/1896)
|
@file:Suppress("unused")
package com.ghost.serialization.compiler
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.ghost.serialization.compiler.GhostEmitterConstants as C
/**
* Emitter for standard deserialization logic.
*
* This emitter orchestrates code generation for classes that fit within standard JVM limits
* (typically under 40 properties). It implements the strategy to declare local tracking variables,
* initialize validation bitmasks, emit parsing loops for incoming JSON, validate required fields,
* and instantiate the target class with an optimized return statement.
*
* @property properties The list of property models containing metadata for code generation.
* @property originalClassName The target class name to instantiate.
* @property readerClass The reader class used for deserialization.
*/
internal class StandardEmitter(
properties: List<GhostPropertyModel>,
originalClassName: ClassName,
readerClass: ClassName
) : BaseDeserializeEmitter(properties, originalClassName, readerClass) {
/**
* Entry point to emit all standard deserialization code block builders.
*
* This method delegates the emission of variable declarations, parsing loops,
* field validation checks, and DTO construction to specialized helper functions.
*
* @param body The target KotlinPoet [CodeBlock.Builder] to append generated code to.
* @param typeSpecBuilder The serializer class builder.
*/
fun emit(body: CodeBlock.Builder, typeSpecBuilder: TypeSpec.Builder) {
emitPropertyMaskConstants(typeSpecBuilder)
WrappedKeysEmitter.addWrappedKeyConstants(typeSpecBuilder, properties)
emitLocalVariables(body)
WrappedKeysEmitter.emitCaptureVariables(body, properties)
emitMaskVariables(body)
emitParseLoop(body)
WrappedKeysEmitter.emitPostLoopDeserialization(
body,
properties,
propertyIndices,
readerClass,
)
emitFieldValidationCall(body)
emitReturnStatement(body, typeSpecBuilder)
emitValidationHelper(typeSpecBuilder)
}
/**
* Emits the local placeholder variable declarations for tracking property values.
*
* @param body The target KotlinPoet [CodeBlock.Builder].
*/
private fun emitLocalVariables(body: CodeBlock.Builder) {
properties.forEach {
val varType = it.getVariableType()
val initialValue = it.getInitialValue()
body.addStatement(
C.TEMPLATE_VAR_VALUE_DECL,
it.kotlinName,
varType,
initialValue
)
}
}
/**
* Emits the initialization declarations of bitmasks to track parsed properties.
*/
private fun emitMaskVariables(body: CodeBlock.Builder) {
for (i in C.VAL_ZERO until maskCount) {
body.addStatement(C.STR_MASK_INIT, i)
}
}
/**
* Generates the main field parsing loop using the perfect hash options.
*
* This method writes:
* - `reader.beginObject()`
* - A `while (true)` loop with a `reader.selectNameAndConsume(OPTIONS)` index match.
* - A `when (index)` routing table that dispatches fields based on pre-compiled indices.
* Handles single-depth fields directly, and delegates multi-depth fields to [emitFlattenedGroup].
*
* @param body The target KotlinPoet [CodeBlock.Builder].
*/
private fun emitParseLoop(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)
val wrappedDispatch = WrappedKeysEmitter.wrappedKeyDispatch(properties)
val topLevelNames = DispatchNamesResolver.topLevelNames(properties)
val normalProperties = properties.filter { it.wrappedSourceKeys == null }
topLevelNames.forEachIndexed { topIndex, topName ->
body.beginControlFlow(
C.TEMPLATE_WHEN_BRANCH,
topIndex
)
val wrappedEntry = wrappedDispatch[topName]
if (wrappedEntry != null) {
val (prop, keyIndex) = wrappedEntry
WrappedKeysEmitter.emitWrappedKeyCapture(body, prop, keyIndex)
} else {
val propsForThisName = normalProperties
.filter { fullPaths[propertyIndices[it]!!].first() == topName }
if (
propsForThisName.size == C.VAL_ONE &&
fullPaths[propertyIndices[propsForThisName[C.VAL_ZERO]]!!].size == C.VAL_ONE
) {
emitPropertyAssignment(
body,
propsForThisName[C.VAL_ZERO],
propertyIndices[propsForThisName[C.VAL_ZERO]]!!
)
} else {
emitFlattenedGroup(
body,
topName,
propsForThisName,
C.VAL_ONE,
C.STR_EMPTY
)
}
}
body.endControlFlow()
}
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)
}
/**
* Recursively generates nested parsing loops for flattened structure properties.
*
* This handles `@GhostFlatten` properties by nesting `beginObject()` loops corresponding
* to the structured hierarchy of keys inside the JSON source.
*
* @param body The target KotlinPoet [CodeBlock.Builder].
* @param name The name of the current node in the property path hierarchy.
* @param props The list of property models sharing this path prefix.
* @param pathIndex The current depth level of recursion.
* @param parentPrefix Cumulative path key prefix to locate nested perfect-hash lookup tables.
*/
private fun emitFlattenedGroup(
body: CodeBlock.Builder,
name: String,
props: List<GhostPropertyModel>,
pathIndex: Int,
parentPrefix: String
) {
val currentPrefix = if (parentPrefix.isEmpty()) {
name
} else {
"${parentPrefix}${C.STR_UNDERSCORE}$name"
}
val optionsName = C.STR_OPTIONS_PREFIX + currentPrefix.uppercase()
body.addStatement(C.STR_BEGIN_OBJECT)
body.beginControlFlow(C.STR_WHILE_TRUE)
val subIndexName = "${C.STR_SUB_INDEX_PREFIX}$pathIndex"
body.addStatement(C.STR_SELECT_SUB_NAME, subIndexName, optionsName)
body.beginControlFlow(C.STR_WHEN_SUB_INDEX, subIndexName)
val nextLevelNames = props.map {
val path = fullPaths[propertyIndices[it]!!]
if (pathIndex < path.size) {
path[pathIndex]
} else {
it.jsonName
}
}.distinct()
nextLevelNames.forEachIndexed { subIndex, subName ->
body.beginControlFlow(
C.TEMPLATE_WHEN_BRANCH,
subIndex
)
val subProps = props.filter {
val path = fullPaths[propertyIndices[it]!!]
val currentName = if (pathIndex < path.size) {
path[pathIndex]
} else {
it.jsonName
}
currentName == subName
}
if (subProps.size > 1 && subProps.all { pathIndex >= fullPaths[propertyIndices[it]!!].size }) {
throw IllegalStateException("Infinite loop detected in emitFlattenedGroup! Duplicate JSON properties or paths found in class $originalClassName: " + subProps.map { it.kotlinName + " -> " + it.jsonName })
}
if (
subProps.size == C.VAL_ONE &&
pathIndex >= fullPaths[propertyIndices[subProps[C.VAL_ZERO]]!!].size - C.VAL_ONE
) {
emitPropertyAssignment(
body,
subProps[C.VAL_ZERO],
propertyIndices[subProps[C.VAL_ZERO]]!!
)
} else {
emitFlattenedGroup(
body,
subName,
subProps,
pathIndex + C.VAL_ONE,
currentPrefix
)
}
body.endControlFlow()
}
body.addStatement(C.STR_MINUS_ONE_BREAK)
body.beginControlFlow(C.STR_ELSE_BRANCH)
body.addStatement(C.STR_SKIP_VALUE)
body.endControlFlow()
body.endControlFlow() // when
body.endControlFlow() // while
body.addStatement(C.STR_END_OBJECT)
}
/**
* Emits property value assignment statement and bitwise mask update.
*
* @param body The target KotlinPoet [CodeBlock.Builder].
* @param prop The property model.
* @param index The global index of this property.
*/
private fun emitPropertyAssignment(
body: CodeBlock.Builder,
prop: GhostPropertyModel,
index: Int
) {
val call = buildCall(prop)
val maskIdx = index / C.MASK_SIZE_BITS.toInt()
val constName = C.STR_MASK_PREFIX + prop.kotlinName.uppercase()
val varName = C.TEMPLATE_VAR_NAME.format(prop.kotlinName)
if (prop.isResilient) {
body.beginControlFlow(C.TEMPLATE_DECODE_RESILIENT, call)
body.addStatement(varName + C.TEMPLATE_ASSIGN_L, C.STR_IT)
body.addStatement(C.STR_MASK_BITWISE_OR, maskIdx, maskIdx, constName)
body.endControlFlow()
} else {
body.addStatement(varName + C.TEMPLATE_ASSIGN_L, call)
body.addStatement(C.STR_MASK_BITWISE_OR, maskIdx, maskIdx, constName)
}
}
/**
* Emits a call to the descriptive private helper method validating that all required properties
* were present in the parsed JSON stream.
*
* @param body The target KotlinPoet [CodeBlock.Builder].
*/
private fun emitFieldValidationCall(body: CodeBlock.Builder) {
val requiredProps = properties.filter { !it.isNullable && !it.hasDefaultValue }
if (requiredProps.isNotEmpty()) {
body.addStatement(C.TEMPLATE_CALL_VALIDATION, C.STR_FUN_VALIDATE_FIELDS, C.STR_PARAM_MASK0, C.STR_READER_VAR)
}
}
/**
* Generates a descriptive private helper method validating that all required properties
* were present in the bitmask, throwing a GhostJsonException for any missing field.
*
* @param typeSpecBuilder The serializer class builder.
*/
private fun emitValidationHelper(typeSpecBuilder: TypeSpec.Builder) {
val requiredProps = properties.filter { !it.isNullable && !it.hasDefaultValue }
if (requiredProps.isEmpty()) {
return
}
val requiredMask0Name = C.STR_MASK_REQUIRED_0
val funBuilder = FunSpec.builder(C.STR_FUN_VALIDATE_FIELDS)
.addModifiers(KModifier.PRIVATE)
.addParameter(C.STR_PARAM_MASK0, com.squareup.kotlinpoet.LONG)
.addParameter(C.STR_READER_VAR, readerClass)
val funBody = CodeBlock.builder()
if (requiredProps.size == 1) {
val prop = requiredProps[0]
val constName = C.STR_MASK_PREFIX + prop.kotlinName.uppercase()
funBody.beginControlFlow(C.TEMPLATE_IF_MASK_ZERO_STMT, constName)
funBody.addStatement(
C.TEMPLATE_THROW_S,
C.STR_REQ_FIELD_1 + prop.jsonName + C.STR_REQ_FIELD_2
)
funBody.endControlFlow()
} else {
funBody.beginControlFlow(C.TEMPLATE_IF_MASK_NOT_MET_STMT, requiredMask0Name, requiredMask0Name)
requiredProps.forEachIndexed { propIdx, prop ->
val constName = C.STR_MASK_PREFIX + prop.kotlinName.uppercase()
if (propIdx == 0) {
funBody.beginControlFlow(C.TEMPLATE_IF_MASK_ZERO_STMT, constName)
} else {
funBody.nextControlFlow(C.TEMPLATE_ELSE_IF_MASK_ZERO_STMT_NEW, constName)
}
funBody.addStatement(
C.TEMPLATE_THROW_S,
C.STR_REQ_FIELD_1 + prop.jsonName + C.STR_REQ_FIELD_2
)
}
funBody.endControlFlow()
funBody.endControlFlow()
}
funBuilder.addCode(funBody.build())
typeSpecBuilder.addFunction(funBuilder.build())
}
/**
* Emits the return statements using optimal constructor dispatch or copy-based logic.
*
* @param body The target KotlinPoet [CodeBlock.Builder].
* @param typeSpecBuilder The serializer class builder.
*/
private fun emitCreateInstanceHelper(typeSpecBuilder: TypeSpec.Builder) {
val hasCreateInstance = typeSpecBuilder.funSpecs.any { it.name == "createInstance" }
if (hasCreateInstance) return
val funBuilder = FunSpec.builder("createInstance")
.addModifiers(KModifier.PRIVATE)
.returns(originalClassName)
for (i in 0 until maskCount) {
funBuilder.addParameter("mask$i", com.squareup.kotlinpoet.LONG)
}
properties.forEach { prop ->
val varType = prop.getVariableType()
funBuilder.addParameter("${prop.kotlinName}Value", varType)
}
val helperBody = CodeBlock.builder()
val requiredProps = properties.filter { it.isInConstructor && !it.hasDefaultValue }
val defaultPropsWithIndex = properties.mapIndexedNotNull { globalIdx, prop ->
if (prop.isInConstructor && prop.hasDefaultValue) Pair(globalIdx, prop) else null
}
if (defaultPropsWithIndex.size <= C.MAX_DEFAULT_BRANCH_COUNT) {
emitMultiBranchReturn(helperBody, requiredProps, defaultPropsWithIndex, typeSpecBuilder)
} else {
emitCopyReturn(helperBody, requiredProps, defaultPropsWithIndex, typeSpecBuilder)
}
funBuilder.addCode(helperBody.build())
typeSpecBuilder.addFunction(funBuilder.build())
}
private fun emitReturnStatement(body: CodeBlock.Builder, typeSpecBuilder: TypeSpec.Builder) {
val hasDefaults = properties.any { it.isInConstructor && it.hasDefaultValue }
if (!hasDefaults) {
body.addStatement(
C.TEMPLATE_RETURN_T_PAREN,
originalClassName
)
properties.filter { it.isInConstructor }.forEach { prop ->
body.addStatement(
C.TEMPLATE_NAMED_ARG,
prop.kotlinName,
prop.getReturnExpression()
)
}
body.addStatement(C.STR_PAREN)
return
}
val defaultPropsWithIndex = properties.mapIndexedNotNull { globalIdx, prop ->
if (prop.isInConstructor && prop.hasDefaultValue) Pair(globalIdx, prop) else null
}
if (defaultPropsWithIndex.size <= C.MAX_DEFAULT_BRANCH_COUNT) {
val requiredProps = properties.filter { it.isInConstructor && !it.hasDefaultValue }
emitMultiBranchReturn(body, requiredProps, defaultPropsWithIndex, typeSpecBuilder)
return
}
emitCreateInstanceHelper(typeSpecBuilder)
val args = mutableListOf<String>()
for (i in 0 until maskCount) {
args.add("mask$i")
}
properties.forEach { prop ->
args.add("${prop.kotlinName}Value")
}
body.addStatement("return createInstance(${args.joinToString(", ")})")
}
/**
* Emits the optimal return strategy when some properties have default values.
*
* To support Kotlin's default argument logic, it decides whether to generate a multi-branch
* return (no copy allocation overhead) or a copy-based return fallback (avoids code bloat).
*
* @param body The target KotlinPoet [CodeBlock.Builder].
* @param typeSpecBuilder The serializer class builder.
*/
private fun emitDefaultValueReturn(body: CodeBlock.Builder, typeSpecBuilder: TypeSpec.Builder) {
val requiredProps = properties.filter { it.isInConstructor && !it.hasDefaultValue }
val defaultPropsWithIndex = properties.mapIndexedNotNull { globalIdx, prop ->
if (prop.isInConstructor && prop.hasDefaultValue) Pair(globalIdx, prop) else null
}
if (defaultPropsWithIndex.size <= C.MAX_DEFAULT_BRANCH_COUNT) {
emitMultiBranchReturn(body, requiredProps, defaultPropsWithIndex, typeSpecBuilder)
} else {
emitCopyReturn(body, requiredProps, defaultPropsWithIndex, typeSpecBuilder)
}
}
/**
* Generates 2^N if-branches, each calling the primary constructor exactly once.
*
* Subsets of default values present in the input JSON are evaluated using bitmask checking.
* Iterates subsets from most bits set to fewest. If all else fails, it calls the constructor
* omitting all default properties so Kotlin's default values are used.
*
* @param body The target KotlinPoet [CodeBlock.Builder].
* @param requiredProps Required (non-default) property models.
* @param defaultPropsWithIndex Property models with default values and their index.
* @param typeSpecBuilder The serializer class builder.
*/
private fun emitMultiBranchReturn(
body: CodeBlock.Builder,
requiredProps: List<GhostPropertyModel>,
defaultPropsWithIndex: List<Pair<Int, GhostPropertyModel>>,
typeSpecBuilder: TypeSpec.Builder
) {
val size = defaultPropsWithIndex.size
// Iterate subsets from most bits set → fewest; skip the empty set (handled as fallback).
val subsets = (C.VAL_ONE until (C.VAL_ONE shl size))
.sortedByDescending { mask -> (C.VAL_ZERO until size).sumOf { bit -> (mask shr bit) and C.VAL_ONE } }
for (subsetBits in subsets) {
val conditionStr = buildSubsetCondition(subsetBits, defaultPropsWithIndex, typeSpecBuilder)
body.beginControlFlow(C.STR_IF_OPEN + conditionStr + C.STR_CLOSE_PAREN_FLOW)
body.addStatement(C.TEMPLATE_RETURN_T_PAREN, originalClassName)
requiredProps.forEach { prop ->
body.addStatement(C.TEMPLATE_NAMED_ARG, prop.kotlinName, prop.getReturnExpression())
}
for (i in C.VAL_ZERO until size) {
if (subsetBits and (C.VAL_ONE shl i) != C.VAL_ZERO) {
val (_, prop) = defaultPropsWithIndex[i]
body.addStatement(C.TEMPLATE_NAMED_ARG, prop.kotlinName, prop.getReturnExpression())
}
}
body.addStatement(C.STR_PAREN)
body.endControlFlow()
}
// Fallback: no default props present — omit them so Kotlin uses their default values.
body.addStatement(C.TEMPLATE_RETURN_T_PAREN, originalClassName)
requiredProps.forEach { prop ->
body.addStatement(C.TEMPLATE_NAMED_ARG, prop.kotlinName, prop.getReturnExpression())
}
body.addStatement(C.STR_PAREN)
}
/**
* Builds the condition string for a subset bitmask of default props.
*
* Groups properties by their mask index and builds conditions checking if
* all subset bits are active in the target mask.
*
* @param subsetBits Bitmask of default properties present in the subset.
* @param defaultPropsWithIndex Default properties and their global indices.
* @param typeSpecBuilder The serializer class builder.
* @return Formatted boolean logic string.
*/
private fun buildSubsetCondition(
subsetBits: Int,
defaultPropsWithIndex: List<Pair<Int, GhostPropertyModel>>,
typeSpecBuilder: TypeSpec.Builder
): String {
val maskGroups = mutableMapOf<Int, Long>()
val defaultPropsInSubset = mutableListOf<GhostPropertyModel>()
for (i in defaultPropsWithIndex.indices) {
if (subsetBits and (C.VAL_ONE shl i) != C.VAL_ZERO) {
val pair = defaultPropsWithIndex[i]
val globalIdx = pair.first
val prop = pair.second
defaultPropsInSubset.add(prop)
val maskIdx = globalIdx / C.MASK_SIZE_BITS.toInt()
val bitIdx = globalIdx % C.MASK_SIZE_BITS.toInt()
maskGroups[maskIdx] = (maskGroups[maskIdx] ?: C.VAL_ZERO_L) or (C.VAL_ONE_L shl bitIdx)
}
}
val constName = C.STR_MASK_OPTS_PREFIX + defaultPropsInSubset
.map { it.kotlinName.uppercase() }
.sorted()
.joinToString(C.STR_UNDERSCORE)
val combinedBits = maskGroups[0] ?: C.VAL_ZERO_L
val bitsStr = formatMaskString(combinedBits)
if (typeSpecBuilder.propertySpecs.none { it.name == constName }) {
typeSpecBuilder.addProperty(
PropertySpec.builder(constName, com.squareup.kotlinpoet.LONG)
.addModifiers(KModifier.PRIVATE, KModifier.CONST)
.initializer(C.TEMPLATE_L, bitsStr)
.build()
)
}
return C.TEMPLATE_MASK_ALL_MET_SIMPLE.format(0, constName, constName)
}
/**
* Legacy copy-based return for classes with > MAX_DEFAULT_BRANCH_COUNT default props.
*
* Instantiates the class with required arguments first, then calls `.copy(...)` on the
* result for any default properties that were encountered, using if-statements to check their
* corresponding bits.
*
* @param body The target KotlinPoet [CodeBlock.Builder].
* @param requiredProps Required (non-default) property models.
* @param defaultPropsWithIndex Property models with default values and their index.
* @param typeSpecBuilder The serializer class builder.
*/
private fun emitCopyReturn(
body: CodeBlock.Builder,
requiredProps: List<GhostPropertyModel>,
defaultPropsWithIndex: List<Pair<Int, GhostPropertyModel>>,
typeSpecBuilder: TypeSpec.Builder
) {
body.addStatement(C.TEMPLATE_VAL_RESULT, originalClassName)
requiredProps.forEach { prop ->
val varName = C.TEMPLATE_VAR_NAME.format(prop.kotlinName)
val isPrimitive = prop.type.isPrimitive() && !prop.isNullable
val expr = if (prop.isNullable || isPrimitive) {
varName
} else {
varName + C.STR_BANG_BANG
}
body.addStatement(C.TEMPLATE_NAMED_ARG, prop.kotlinName, expr)
}
body.addStatement(C.STR_PAREN)
if (defaultPropsWithIndex.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_MASK_CHECK_MATCH.format(i, constName))
}
}
body.add(conditions.joinToString(C.STR_OR))
body.beginControlFlow(C.STR_CLOSE_PAREN_FLOW)
body.addStatement(C.STR_RETURN_RESULT_COPY)
defaultPropsWithIndex.forEach { (propIndex, prop) ->
val maskIdx = propIndex / C.MASK_SIZE_BITS.toInt()
val constName = C.STR_MASK_PREFIX + prop.kotlinName.uppercase()
val valueExpr = prop.getDefaultValueReturnExpression(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)
}
}
}