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

Class Method, % Branch, % Line, % Instruction, %
GhostSerializationProcessor 100% (16/16) 45% (18/40) 84.8% (212/250) 80.6% (1114/1382)
GhostSerializationProcessor$generateModuleRegistry$$inlined$sortedBy$1 0% (0/1)
Total 94.1% (16/17) 45% (18/40) 84.8% (212/250) 80.6% (1114/1382)


 package com.ghost.serialization.compiler
 
 import com.google.devtools.ksp.processing.CodeGenerator
 import com.google.devtools.ksp.processing.Dependencies
 import com.google.devtools.ksp.processing.KSPLogger
 import com.google.devtools.ksp.processing.Resolver
 import com.google.devtools.ksp.processing.SymbolProcessor
 import com.google.devtools.ksp.symbol.KSAnnotated
 import com.google.devtools.ksp.symbol.KSClassDeclaration
 import com.google.devtools.ksp.symbol.KSFile
 import com.google.devtools.ksp.symbol.Modifier
 import com.squareup.kotlinpoet.AnnotationSpec
 import com.squareup.kotlinpoet.ClassName
 import com.squareup.kotlinpoet.CodeBlock
 import com.squareup.kotlinpoet.FileSpec
 import com.squareup.kotlinpoet.FunSpec
 import com.squareup.kotlinpoet.KModifier
 import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
 import com.squareup.kotlinpoet.PropertySpec
 import com.squareup.kotlinpoet.STAR
 import com.squareup.kotlinpoet.TypeName
 import com.squareup.kotlinpoet.TypeSpec
 import com.squareup.kotlinpoet.TypeVariableName
 import com.squareup.kotlinpoet.asClassName
 import com.squareup.kotlinpoet.ksp.toClassName
 import kotlin.reflect.KClass
 import com.ghost.serialization.compiler.GhostEmitterConstants as C
 
 /**
  * Main KSP Processor for Ghost Serialization.
  *
  * It analyzes classes annotated with `@GhostSerialization`, generates their respective
  * specialized serializers, and builds a global registry mapping classes to serializers
  * to avoid reflection at runtime.
  *
  * @property codeGenerator The KSP [CodeGenerator] used to create new serializer and registry files.
  * @property logger The [KSPLogger] used to report compilation errors, warnings, and messages.
  * @property options Map of key-value pairs representing processor options passed from build scripts.
  */
 class GhostSerializationProcessor(
     private val codeGenerator: CodeGenerator,
     private val logger: KSPLogger,
     private val options: Map<String, String> = emptyMap()
 ) : SymbolProcessor {
 
     /**
      * Map tracking class names to their generated companion serializers for the module registry.
      */
     private val classToSerializer = mutableMapOf<ClassName, ClassName>()
 
     /**
      * Origin files corresponding to processed declarations, used to define KSP incremental compilation dependencies.
      */
     private val originatingFiles = mutableSetOf<KSFile>()
 
     /**
      * Tracks processed file names to avoid double-processing declarations.
      */
     private val processedFiles = mutableSetOf<String>()
 
     /**
      * Analyzer that reads declarations and constructs property metadata models.
      */
     private val analyzer = GhostAnalyzer(logger)
     private val envelopeAnalyzer = EnvelopeAnalyzer(logger)
 
     /**
      * Lazily resolves the output name of the module-level registry class (e.g. `GhostRegistry_module_name`).
      */
     private val registryClassName: String by lazy {
         // Use the module name provided by KSP or fallback to a stable suffix
         var moduleName = options[C.OPTION_MODULE_NAME]
             ?.replace(C.STR_DASH, C.STR_UNDERSCORE)
             ?.replace(C.STR_DOT, C.STR_UNDERSCORE)
             ?: C.STR_DEFAULT_NAME
 
         // Append _Test if we are in a test source set to avoid collisions
         if (moduleName == C.STR_DEFAULT_NAME) {
             val isTest = originatingFiles.any { 
                 val path = it.filePath
                 path.contains(C.STR_SRC_TEST) ||
                         path.contains(C.STR_SRC_ANDROID_TEST) ||
                         path.contains(C.STR_SRC_TEST_KSP)
             }
             if (isTest) {
                 moduleName += C.STR_TEST_SUFFIX
             }
         }
 
         C.STR_REGISTRY_PREFIX + C.STR_UNDERSCORE + moduleName
     }
 
     /**
      * Entry point of the processor phase.
      * Searches for `@GhostSerialization` annotated classes, generates their serializers,
      * and compiles the final module registry if any class was successfully processed.
      *
      * @param resolver KSP [Resolver] used to query symbols and types.
      * @return List of symbols that couldn't be processed in this round.
      */
     override fun process(resolver: Resolver): List<KSAnnotated> {
         println("=== KSP GhostSerializationProcessor.process called ===")
         val symbols = (resolver.getSymbolsWithAnnotation(C.STR_ANNOTATION_SERIALIZATION) +
                 resolver.getSymbolsWithAnnotation(C.STR_ANNOTATION_PROTO_SERIALIZATION)).toSet()
         val validClasses = symbols.filterIsInstance<KSClassDeclaration>().toList()
         println("=== Found annotated symbols: ${symbols.size}, valid classes: ${validClasses.size} ===")
         val unableToProcess = symbols.filterNot { it is KSClassDeclaration }
 
         val analyzed = validClasses.mapNotNull { classDeclaration ->
             try {
                 TextChannelPlanner.AnalyzedClass(
                     declaration = classDeclaration,
                     properties = analyzer.analyze(classDeclaration),
                 )
             } catch (e: Exception) {
                 logger.error(
                     "${C.STR_LOG_PREFIX}${C.STR_LOG_CRITICAL}${
                         classDeclaration.simpleName.asString()
                     }${C.STR_COLON_SPACE}${e.message ?: e.toString()}",
                     classDeclaration,
                 )
                 null
             }
         }
 
         val textChannelByClass = TextChannelPlanner.plan(
             analyzed = analyzed,
             moduleTextChannelEnabled = options[C.OPTION_TEXT_CHANNEL] == C.STR_TRUE,
         )
 
         analyzed.forEach { entry ->
             processClass(
                 classDeclaration = entry.declaration,
                 propertiesModel = entry.properties,
                 textChannel = textChannelByClass[entry.declaration] == true,
             )
         }
 
         if (validClasses.isNotEmpty()) {
             generateModuleRegistry()
             generateProGuardRules()
             generateServiceFile()
         }
 
         return unableToProcess.toList()
     }
 
     /**
      * Analyzes, compiles code specs, and writes the serializer companion class file for a target class.
      *
      * @param classDeclaration KSP class declaration of the target serializable model.
      */
     private fun processClass(
         classDeclaration: KSClassDeclaration,
         propertiesModel: List<GhostPropertyModel>,
         textChannel: Boolean,
     ) {
         val className = classDeclaration.simpleName.asString()
         try {
             val serializerClassName = generateSerializer(
                 classDeclaration = classDeclaration,
                 propertiesModel = propertiesModel,
                 textChannel = textChannel,
             ) ?: return
 
             registerSerializer(classDeclaration, serializerClassName)
 
             logger.info(
                 "${
                     C.STR_LOG_PREFIX
                 }${
                     C.STR_LOG_OPTIMIZED
                 }$className"
             )
         } catch (e: Exception) {
             logger.error(
                 "${C.STR_LOG_PREFIX}${C.STR_LOG_CRITICAL}$className${C.STR_COLON_SPACE}${e.message ?: e.toString()}",
                 classDeclaration
             )
         }
     }
 
     /**
      * Generates and writes the serializer class file for the target class declaration.
      *
      * @param classDeclaration KSP class declaration of the target serializable model.
      * @param propertiesModel The properties metadata model parsed from the declaration.
      * @return The [ClassName] of the generated serializer class, or null if it was already processed.
      */
     private fun generateSerializer(
         classDeclaration: KSClassDeclaration,
         propertiesModel: List<GhostPropertyModel>,
         textChannel: Boolean,
     ): ClassName? {
         val fileGenerator = GhostCodeGenerator(
             classDeclaration = classDeclaration,
             properties = propertiesModel,
             textChannel = textChannel,
             envelopeModel = envelopeAnalyzer.analyze(classDeclaration, propertiesModel)
         )
 
         val fileSpec = fileGenerator.createSpec()
         val packageName = classDeclaration.packageName.asString()
         val fullFileName = "$packageName.${fileSpec.name}"
 
         if (processedFiles.contains(fullFileName)) {
             return null
         }
         processedFiles.add(fullFileName)
 
         fileSpec.writeTrimmedTo(
             codeGenerator = codeGenerator,
             dependencies = Dependencies(
                 aggregating = false,
                 classDeclaration.containingFile!!
             )
         )
 
         return ClassName(
             packageName,
             classDeclaration
                 .toClassName()
                 .simpleNames
                 .joinToString(C.STR_UNDERSCORE)
                     + C.STR_SERIALIZER_SUFFIX
         )
     }
 
     /**
      * Registers the generated serializer class name mapping for a given model, tracking sealed subclasses
      * and recording originating files for incremental compilation.
      *
      * @param classDeclaration KSP class declaration of the target serializable model.
      * @param serializerClassName The [ClassName] of the generated serializer.
      */
     private fun registerSerializer(
         classDeclaration: KSClassDeclaration,
         serializerClassName: ClassName
     ) {
         classToSerializer[classDeclaration.toClassName()] = serializerClassName
 
         if (classDeclaration.modifiers.contains(Modifier.SEALED)) {
             classDeclaration.getSealedSubclasses().forEach { subclass ->
                 classToSerializer[subclass.toClassName()] = serializerClassName
             }
         }
         classDeclaration.containingFile?.let { originatingFiles.add(it) }
     }
 
     /**
      * Generates a registry containing a mapping of serializable classes to their generated serializers.
      * Splitting the structure into chunks if there are many models to avoid JVM method limits.
      */
     private fun generateModuleRegistry() {
         val serializerType = ClassName(C.STR_CONTRACT_PKG, C.STR_GHOST_SERIALIZER)
         val kClassType = ClassName(C.STR_REFLECT_PKG, C.STR_KCLASS)
         val type = TypeVariableName(C.STR_TYPE_T, Any::class)
         val mapType = ClassName(C.STR_COLLECTIONS_PKG, C.STR_MAP)
             .parameterizedBy(
                 kClassType.parameterizedBy(STAR),
                 serializerType.parameterizedBy(STAR)
             )
 
         val entries = classToSerializer.entries.toList().sortedBy { it.key.canonicalName }
         val registrySpec = TypeSpec.classBuilder(registryClassName)
             .addKdoc(C.STR_KDOC_REGISTRY)
             .addSuperinterface(ClassName(C.STR_CONTRACT_PKG, C.STR_GHOST_REGISTRY))
 
         val chunks = entries.chunked(C.REGISTRY_CHUNK_SIZE)
 
         // 1. Generate full serializers map (Lazy + Fragmented)
         generateSerializersMapProperty(registrySpec, chunks, entries, mapType)
 
         // 2. Generate getSerializer method (Fragmented when)
         generateGetSerializerMethod(registrySpec, chunks, entries, serializerType, type)
 
         // 3. Generate Shard Methods if fragmented
         generateShardMethods(registrySpec, chunks, mapType, serializerType)
 
         // 4. Generate Metadata Methods & Companion
         generateMetadataMethodsAndCompanion(registrySpec, entries.size, mapType)
 
         // Write the spec to file
         writeRegistryFile(registrySpec.build())
     }
 
     /**
      * Generates the lazily-initialized full serializers map property for the registry.
      *
      * @param registrySpec The type spec builder for the module registry.
      * @param chunks Chunked lists of serializable class entry mappings.
      * @param entries All serializable class entry mappings.
      * @param mapType The parameterized type description of the mapping.
      */
     private fun generateSerializersMapProperty(
         registrySpec: TypeSpec.Builder,
         chunks: List<List<Map.Entry<ClassName, ClassName>>>,
         entries: List<Map.Entry<ClassName, ClassName>>,
         mapType: TypeName
     ) {
         val allSerializersDelegate = CodeBlock.builder()
             .add(C.STR_LAZY_START)
             .indent()
 
         if (chunks.size > 1) {
             chunks.forEachIndexed { index, _ ->
                 allSerializersDelegate.add(C.TEMPLATE_GET_SHARD_MAP_CALL, index)
                 if (index < chunks.size - 1) {
                     allSerializersDelegate.add(C.STR_PLUS_SPACED)
                 }
             }
         } else {
             allSerializersDelegate.add(buildMapBlock(entries))
         }
 
         allSerializersDelegate.unindent().add(C.STR_NEWLINE_CLOSE_CURLY)
 
         registrySpec.addProperty(
             PropertySpec.builder(C.STR_PROP_SERIALIZERS_MAP, mapType)
                 .addModifiers(KModifier.PRIVATE)
                 .delegate(allSerializersDelegate.build())
                 .build()
         )
     }
 
     /**
      * Generates the polymorphic `getSerializer` method routing requests to matches or shards.
      *
      * @param registrySpec The type spec builder for the module registry.
      * @param chunks Chunked lists of serializable class entry mappings.
      * @param entries All serializable class entry mappings.
      * @param serializerType The parameterized serializer type description.
      * @param type The type variable representation for return type casting.
      */
     private fun generateGetSerializerMethod(
         registrySpec: TypeSpec.Builder,
         chunks: List<List<Map.Entry<ClassName, ClassName>>>,
         entries: List<Map.Entry<ClassName, ClassName>>,
         serializerType: ClassName,
         type: TypeVariableName
     ) {
         val getMethodBuilder = FunSpec.builder(C.STR_FUN_GET_SERIALIZER)
             .addTypeVariable(type)
             .addParameter(
                 C.STR_PARAM_CLAZZ,
                 KClass::class.asClassName().parameterizedBy(type)
             )
             .returns(serializerType.parameterizedBy(type).copy(nullable = true))
             .addModifiers(KModifier.OVERRIDE)
             .addAnnotation(
                 AnnotationSpec.builder(Suppress::class)
                     .addMember(C.MARKER, C.STR_UNCHECKED_CAST)
                     .build()
             )
 
         val getCode = CodeBlock.builder()
         if (chunks.size > 1) {
             for (index in chunks.indices) {
                 getCode.addStatement(
                     C.TEMPLATE_GET_SHARD_CALL,
                     index,
                     serializerType.parameterizedBy(type).copy(nullable = true)
                 )
             }
             getCode.addStatement(C.STR_RETURN_NULL)
         } else {
             getCode.add(buildWhenBlock(entries, serializerType, type))
         }
         getMethodBuilder.addCode(getCode.build())
         registrySpec.addFunction(getMethodBuilder.build())
     }
 
     /**
      * Generates private helper lookup/mapping shard methods if the registry size warrants fragmentation.
      *
      * @param registrySpec The type spec builder for the module registry.
      * @param chunks Chunked lists of serializable class entry mappings.
      * @param mapType The parameterized type description of the mapping.
      * @param serializerType The parameterized serializer type description.
      */
     private fun generateShardMethods(
         registrySpec: TypeSpec.Builder,
         chunks: List<List<Map.Entry<ClassName, ClassName>>>,
         mapType: TypeName,
         serializerType: ClassName
     ) {
         if (chunks.size > 1) {
             chunks.forEachIndexed { i, chunk ->
                 registrySpec.addFunction(
                     FunSpec.builder(C.TEMPLATE_SHARD_MAP_NAME.format(i))
                         .addModifiers(KModifier.PRIVATE)
                         .returns(mapType)
                         .addCode(C.STR_RETURN_L, buildMapBlock(chunk))
                         .build()
                 )
 
                 // Shard Lookup
                 registrySpec.addFunction(
                     FunSpec.builder(C.TEMPLATE_SHARD_NAME.format(i))
                         .addModifiers(KModifier.PRIVATE)
                         .addParameter(
                             C.STR_PARAM_CLAZZ,
                             KClass::class.asClassName().parameterizedBy(STAR)
                         )
                         .returns(serializerType.parameterizedBy(STAR).copy(nullable = true))
                         .addCode(buildWhenBlock(chunk, serializerType, STAR))
                         .build()
                 )
             }
         }
     }
 
     /**
      * Generates metadata info methods (prewarm, registry size count, and global companion instance).
      *
      * @param registrySpec The type spec builder for the module registry.
      * @param entriesCount The total number of serializable class entry mappings.
      * @param mapType The parameterized type description of the mapping.
      */
     private fun generateMetadataMethodsAndCompanion(
         registrySpec: TypeSpec.Builder,
         entriesCount: Int,
         mapType: TypeName
     ) {
         registrySpec.addFunction(
             FunSpec.builder(C.STR_FUN_PREWARM)
                 .addModifiers(KModifier.OVERRIDE)
                 .addStatement(C.STR_SERIALIZERS_SIZE)
                 .build()
         )
         registrySpec.addFunction(
             FunSpec.builder(C.STR_FUN_REG_COUNT)
                 .addModifiers(KModifier.OVERRIDE)
                 .returns(Int::class)
                 .addStatement(C.STR_RETURN_L, entriesCount)
                 .build()
         )
         registrySpec.addFunction(
             FunSpec.builder(C.STR_FUN_GET_ALL_SERIALIZERS)
                 .addModifiers(KModifier.OVERRIDE)
                 .returns(mapType)
                 .addStatement(C.STR_RETURN_SERIALIZERS)
                 .build()
         )
         registrySpec.addType(
             TypeSpec.companionObjectBuilder()
                 .addProperty(
                     PropertySpec.builder(
                         C.STR_INSTANCE,
                         ClassName(C.STR_GENERATED_PKG, registryClassName)
                     )
                         .initializer(
                             C.STR_INIT_INSTANCE,
                             ClassName(C.STR_GENERATED_PKG, registryClassName)
                         )
                         .addAnnotation(JvmField::class)
                         .build()
                 )
                 .build()
         )
     }
 
     /**
      * Writes the completed registry type specification to a file.
      *
      * @param registrySpec The built registry type specification.
      */
     private fun writeRegistryFile(registrySpec: TypeSpec) {
         FileSpec.builder(C.STR_GENERATED_PKG, registryClassName)
             .addType(registrySpec)
             .build()
             .writeTrimmedTo(
                 codeGenerator,
                 Dependencies(aggregating = true, *originatingFiles.toTypedArray())
             )
     }
 
     /**
      * Generates a KotlinPoet [CodeBlock] mapping class types to their serializer instances.
      *
      * @param entries Serializable class entry mappings.
      * @return Pre-compiled registry map code block.
      */
     private fun buildMapBlock(
         entries: List<Map.Entry<ClassName, ClassName>>
     ): CodeBlock {
         val builder = CodeBlock.builder().add(C.STR_MAP_OF)
         entries.forEachIndexed { index, entry ->
             builder.add(
                 C.STR_MAP_ENTRY,
                 entry.key,
                 entry.value
             )
             if (index < entries.size - 1) {
                 builder.add(C.STR_COMMA_NEWLINE)
             }
         }
         builder.add(C.STR_PAREN_CLOSE)
         return builder.build()
     }
 
     /**
      * Generates a high-performance Kotlin `when (clazz)` lookup expression.
      *
      * @param entries Registry entry mappings.
      * @param serializerType Serializer class name representation.
      * @param type Generic type variable for mapping return types safely.
      * @return Generated routing when code block.
      */
     private fun buildWhenBlock(
         entries: List<Map.Entry<ClassName, ClassName>>,
         serializerType: ClassName,
         type: TypeName
     ): CodeBlock {
         val builder = CodeBlock
             .builder()
             .add(C.STR_WHEN_CLAZZ_START)
 
         entries.forEach { entry ->
             builder.add(
                 C.STR_WHEN_ENTRY,
                 entry.key,
                 entry.value
             )
         }
 
         builder.add(C.STR_WHEN_ELSE_NULL)
         builder.add(
             C.STR_WHEN_CLOSE_CAST,
             serializerType
                 .parameterizedBy(type)
                 .copy(nullable = true)
         )
         return builder.build()
     }
 
     /**
      * Generates ProGuard/R8 keep rules to ensure the Registry and Serializers
      * are not obfuscated or removed during the shrinking phase.
      */
     private fun generateProGuardRules() {
         val rules = C.TEMPLATE_PROGUARD_KEEP
             .trimIndent()
             .format(C.STR_GENERATED_PKG, registryClassName)
 
         try {
             codeGenerator.createNewFile(
                 dependencies = Dependencies(aggregating = true),
                 packageName = C.STR_META_INF_PROGUARD,
                 fileName = C.STR_GHOST_SERIALIZATION_FILE,
                 extensionName = C.STR_EXT_PRO
             ).use { it.write(rules.toByteArray()) }
         } catch (e: Exception) {
             logger.warn(
                 "${
                     C.STR_LOG_PREFIX
                 }${
                     C.STR_LOG_PROGUARD_WARN
                 }${
                     e.message
                 }"
             )
         }
     }
 
     /**
      * Generates a ServiceLoader entry so the Core module can automatically
      * discover this registry at runtime.
      */
     private fun generateServiceFile() {
         val serviceName = C.STR_SERVICE_REGISTRY
         val implementationName = "${
             C.STR_GENERATED_PKG
         }${
             C.STR_DOT
         }$registryClassName"
 
         try {
             codeGenerator.createNewFile(
                 dependencies = Dependencies(
                     aggregating = true,
                     *originatingFiles.toTypedArray()
                 ),
                 packageName = C.STR_META_INF_SERVICES,
                 fileName = serviceName,
                 extensionName = C.STR_EMPTY
             ).use { it.write(implementationName.toByteArray()) }
         } catch (e: Exception) {
             logger.warn(
                 "${
                     C.STR_LOG_PREFIX
                 }${
                     C.STR_LOG_SERVICE_WARN
                 }${e.message}"
             )
         }
     }
 
 }