All Downloads are FREE. Search and download functionalities are using the official Maven repository.

se.ansman.kotshi.ksp.KotshiSymbolProcessor.kt Maven / Gradle / Ivy

Go to download

An annotations processor that generates Moshi adapters from Kotlin data classes

There is a newer version: 3.0.0
Show newest version
package se.ansman.kotshi.ksp

import com.google.devtools.ksp.getClassDeclarationByName
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
import com.google.devtools.ksp.symbol.ClassKind
import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.Modifier
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.MemberName.Companion.member
import com.squareup.kotlinpoet.ParameterSpec
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.STAR
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.asClassName
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import se.ansman.kotshi.GeneratedAdapter
import se.ansman.kotshi.GlobalConfig
import se.ansman.kotshi.InternalKotshiApi
import se.ansman.kotshi.JsonSerializable
import se.ansman.kotshi.KotshiJsonAdapterFactory
import se.ansman.kotshi.KotshiUtils
import se.ansman.kotshi.Polymorphic
import se.ansman.kotshi.SerializeNulls
import se.ansman.kotshi.addControlFlow
import se.ansman.kotshi.kapt.FactoryProcessingStep
import se.ansman.kotshi.kapt.generators.internalKotshiApi
import se.ansman.kotshi.ksp.generators.DataClassAdapterGenerator
import se.ansman.kotshi.ksp.generators.EnumAdapterGenerator
import se.ansman.kotshi.ksp.generators.ObjectAdapterGenerator
import se.ansman.kotshi.ksp.generators.SealedClassAdapterGenerator
import se.ansman.kotshi.moshiTypes
import se.ansman.kotshi.nullable
import java.lang.reflect.Type

class KotshiSymbolProcessor(private val environment: SymbolProcessorEnvironment) : SymbolProcessor {
    override fun process(resolver: Resolver): List {
        for (annotated in resolver.getSymbolsWithAnnotation(Polymorphic::class.qualifiedName!!)) {
            require(annotated is KSClassDeclaration)
            if (annotated.getAnnotation() == null) {
                environment.logger.error(
                    "Kotshi: Classes annotated with @Polymorphic must also be annotated with @JsonSerializable",
                    annotated
                )
            }
        }

        val factories = resolver.getSymbolsWithAnnotation(KotshiJsonAdapterFactory::class.qualifiedName!!)
            .toList()
        if (factories.size > 1) {
            environment.logger.error("Multiple classes found with annotations @KotshiJsonAdapterFactory", factories[0])
            return emptyList()
        }
        val factory = factories.firstOrNull()

        val globalConfig = factory
            ?.getAnnotation()
            ?.let { annotation ->
                GlobalConfig(
                    useAdaptersForPrimitives = annotation.getValue("useAdaptersForPrimitives") ?: false,
                    serializeNulls = annotation.getEnumValue("serializeNulls", SerializeNulls.DEFAULT),
                )
            }
            ?: GlobalConfig.DEFAULT

        val adapters = generateAdapters(resolver, globalConfig)
        if (factory != null) {
            try {
                generateFactory(factory as KSClassDeclaration, resolver, adapters)
            } catch (e: KspProcessingError) {
                environment.logger.error("Kotshi: ${e.message}", e.node)
            }
        }

        return emptyList()
    }

    private fun generateFactory(
        element: KSClassDeclaration,
        resolver: Resolver,
        adapters: List
    ) {
        val elementClassName = element.toClassName()
        val generatedName = elementClassName.let {
            ClassName(it.packageName, "Kotshi${it.simpleNames.joinToString("_")}")
        }

        val jsonAdapterFactory = resolver.getClassDeclarationByName()!!.asType(emptyList())
        val typeSpecBuilder = if (element.asType(emptyList())
                .isAssignableFrom(jsonAdapterFactory) && Modifier.ABSTRACT in element.modifiers
        ) {
            TypeSpec.objectBuilder(generatedName)
                .superclass(elementClassName)
        } else {
            TypeSpec.objectBuilder(generatedName)
                .addSuperinterface(jsonAdapterFactory.toTypeName())
        }

        typeSpecBuilder
            .addModifiers(KModifier.INTERNAL)
            .addOriginatingKSFile(element.containingFile!!)

        val typeParam = ParameterSpec.builder("type", Type::class)
            .build()
        val annotationsParam = ParameterSpec.builder(
            "annotations",
            Set::class.asClassName().parameterizedBy(Annotation::class.asClassName())
        )
            .build()
        val moshiParam = ParameterSpec.builder("moshi", Moshi::class)
            .build()

        val factory = typeSpecBuilder
            .addAnnotation(AnnotationSpec.builder(FactoryProcessingStep.optIn)
                .addMember("%T::class", internalKotshiApi)
                .build())
            .addFunction(makeCreateFunction(typeParam, annotationsParam, moshiParam, adapters))
            .build()

        FileSpec.builder(generatedName.packageName, generatedName.simpleName)
            .addComment("Code generated by Kotshi. Do not edit.")
            .addAnnotation(AnnotationSpec.builder(FactoryProcessingStep.suppress)
                .addMember("%S", "EXPERIMENTAL_IS_NOT_ENABLED")
                .build())
            .addType(factory)
            .build()
            .writeTo(environment.codeGenerator)
    }

    private fun generateAdapters(resolver: Resolver, globalConfig: GlobalConfig): List =
        resolver.getSymbolsWithAnnotation(JsonSerializable::class.qualifiedName!!).mapNotNull { annotated ->
            require(annotated is KSClassDeclaration)
            try {
                val generator = when (annotated.classKind) {
                    ClassKind.CLASS -> {
                        when {
                            Modifier.DATA in annotated.modifiers -> {
                                DataClassAdapterGenerator(
                                    environment = environment,
                                    resolver = resolver,
                                    element = annotated,
                                    globalConfig = globalConfig
                                )
                            }
                            Modifier.SEALED in annotated.modifiers -> {
                                SealedClassAdapterGenerator(
                                    environment = environment,
                                    resolver = resolver,
                                    element = annotated,
                                    globalConfig = globalConfig
                                )
                            }
                            else -> {
                                throw KspProcessingError(
                                    "@JsonSerializable can only be applied to enums, objects, sealed classes and data classes",
                                    annotated
                                )
                            }
                        }
                    }
                    ClassKind.ENUM_CLASS -> EnumAdapterGenerator(
                        environment = environment,
                        resolver = resolver,
                        element = annotated,
                        globalConfig = globalConfig
                    )
                    ClassKind.OBJECT -> ObjectAdapterGenerator(
                        environment = environment,
                        resolver = resolver,
                        element = annotated,
                        globalConfig = globalConfig
                    )
                    else -> {
                        throw KspProcessingError(
                            "@JsonSerializable can only be applied to enums, objects, sealed classes and data classes",
                            annotated
                        )
                    }
                }

                generator.generateAdapter()
            } catch (e: KspProcessingError) {
                environment.logger.error("Kotshi: ${e.message}", e.node)
                null
            }
        }.toList()

    private fun makeCreateFunction(
        typeParam: ParameterSpec,
        annotationsParam: ParameterSpec,
        moshiParam: ParameterSpec,
        adapters: List
    ): FunSpec {
        val createSpec = FunSpec.builder("create")
            .addModifiers(KModifier.OVERRIDE)
            .returns(JsonAdapter::class.asClassName().parameterizedBy(STAR).nullable())
            .addParameter(typeParam)
            .addParameter(annotationsParam)
            .addParameter(moshiParam)


        if (adapters.isEmpty()) {
            return createSpec
                .addStatement("return null")
                .build()
        }

        return createSpec
            .addStatement("if (%N.isNotEmpty()) return null", annotationsParam)
            .addCode("\n")
            .addControlFlow("return when (%T.getRawType(%N))", moshiTypes, typeParam) {
                for (adapter in adapters.sortedBy { it.className }) {
                    addCode("«%T::class.java ->\n%T", adapter.targetType, adapter.className)
                    if (adapter.typeVariables.isNotEmpty()) {
                        addCode(adapter.typeVariables.joinToString(", ", prefix = "<", postfix = ">") { "Nothing" })
                    }
                    addCode("(")
                    if (adapter.requiresMoshi) {
                        addCode("%N", moshiParam)
                    }
                    if (adapter.requiresTypes) {
                        if (adapter.requiresMoshi) {
                            addCode(", ")
                        }
                        addCode("%N.%M", typeParam, typeArgumentsOrFail)
                    }
                    addCode(")\n»")
                }
                addStatement("else -> null")
            }
            .build()
    }

    companion object {
        @OptIn(InternalKotshiApi::class)
        private val typeArgumentsOrFail = KotshiUtils::class.java.member("typeArgumentsOrFail")
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy