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.*
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.*
import com.squareup.kotlinpoet.asClassName
import com.squareup.kotlinpoet.ksp.*
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonQualifier
import se.ansman.kotshi.*
import se.ansman.kotshi.Errors.abstractFactoriesAreDeprecated
import se.ansman.kotshi.Errors.javaClassNotSupported
import se.ansman.kotshi.Errors.polymorphicClassMustHaveJsonSerializable
import se.ansman.kotshi.Errors.unsupportedFactoryType
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.model.*
import se.ansman.kotshi.model.JsonAdapterFactory.Companion.getManualAdapter
import se.ansman.kotshi.renderer.JsonAdapterFactoryRenderer

class KotshiSymbolProcessor(private val environment: SymbolProcessorEnvironment) : SymbolProcessor {
    private val createAnnotationsUsingConstructor = environment.options[Options.createAnnotationsUsingConstructor]
        ?.toBooleanStrict()
        ?: environment.kotlinVersion.isAtLeast(1, 6)

    private val useLegacyDataClassRenderer = environment.options[Options.useLegacyDataClassRenderer]
        ?.toBooleanStrict()
        ?: false

    private val generatedAnnotation = environment.options[Options.generatedAnnotation]
        ?.let { name ->
            Options.possibleGeneratedAnnotations[name] ?: run {
                environment.logger.logKotshiError(Errors.invalidGeneratedAnnotation(name), node = null)
                null
            }
        }
        ?.let { GeneratedAnnotation(it, KotshiSymbolProcessor::class.asClassName()) }

    @OptIn(ExperimentalKotshiApi::class, KspExperimental::class)
    override fun process(resolver: Resolver): List {
        for (annotated in resolver.getSymbolsWithAnnotation(Polymorphic::class.qualifiedName!!)) {
            if (annotated.origin == Origin.JAVA || annotated.origin == Origin.JAVA_LIB) {
                environment.logger.logKotshiError(javaClassNotSupported, annotated)
                continue
            }
            require(annotated is KSClassDeclaration)
            if (annotated.getAnnotation() == null) {
                environment.logger.logKotshiError(polymorphicClassMustHaveJsonSerializable, annotated)
            }
        }

        for (annotated in resolver.getSymbolsWithAnnotation(JsonDefaultValue::class.qualifiedName!!)) {
            if (annotated.origin == Origin.JAVA || annotated.origin == Origin.JAVA_LIB) {
                environment.logger.logKotshiError(javaClassNotSupported, annotated)
                continue
            }
            when (annotated) {
                is KSClassDeclaration -> {
                    when (annotated.classKind) {
                        ClassKind.ENUM_ENTRY,
                        ClassKind.OBJECT -> {
                            // OK
                        }

                        ClassKind.CLASS ->
                            if (Modifier.DATA !in annotated.modifiers) {
                                environment.logger.logKotshiError(
                                    Errors.jsonDefaultValueAppliedToInvalidType,
                                    annotated
                                )
                            }

                        else -> environment.logger.logKotshiError(
                            Errors.jsonDefaultValueAppliedToInvalidType,
                            annotated
                        )
                    }
                }

                else -> environment.logger.logKotshiError(Errors.jsonDefaultValueAppliedToInvalidType, annotated)
            }
        }

        val factories = resolver.getSymbolsWithAnnotation(KotshiJsonAdapterFactory::class.qualifiedName!!)
            .mapNotNull {
                if (it.origin == Origin.JAVA || it.origin == Origin.JAVA_LIB) {
                    environment.logger.logKotshiError(javaClassNotSupported, it)
                    return@mapNotNull null
                }
                if (it !is KSClassDeclaration) {
                    environment.logger.logKotshiError(unsupportedFactoryType, it)
                    return@mapNotNull null
                }

                val isValid = it.classKind == ClassKind.CLASS && Modifier.ABSTRACT in it.modifiers ||
                    it.classKind == ClassKind.OBJECT ||
                    it.classKind == ClassKind.INTERFACE
                if (isValid) {
                    it
                } else {
                    environment.logger.logKotshiError(unsupportedFactoryType, it)
                    null
                }
            }
            .toList()
        if (factories.size > 1) {
            environment.logger.logKotshiError(
                Errors.multipleFactories(factories.map { it.qualifiedName?.asString() ?: it.simpleName.asString() }),
                factories[0]
            )
            return emptyList()
        }
        val targetFactory = factories.firstOrNull()

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


        val manualAdapters = resolver.getSymbolsWithAnnotation(RegisterJsonAdapter::class.qualifiedName!!)
            .onEach {
                if (targetFactory == null) {
                    environment.logger.logKotshiError(Errors.registeredAdapterWithoutFactory, it)
                }
            }
            .mapNotNull {
                if (it !is KSClassDeclaration) {
                    environment.logger.logKotshiError(Errors.invalidRegisterAdapterType, it)
                    null
                } else when (it.classKind) {
                    ClassKind.CLASS,
                    ClassKind.OBJECT -> {
                        if (it.isAbstract()) {
                            environment.logger.logKotshiError(Errors.invalidRegisterAdapterType, it)
                            null
                        } else {
                            it
                        }
                    }

                    ClassKind.INTERFACE,
                    ClassKind.ENUM_CLASS,
                    ClassKind.ENUM_ENTRY,
                    ClassKind.ANNOTATION_CLASS -> {
                        environment.logger.logKotshiError(Errors.invalidRegisterAdapterType, it)
                        null
                    }
                }
            }
            .onEach {
                when (it.getVisibility()) {
                    Visibility.PUBLIC,
                    Visibility.INTERNAL -> {
                        // Ok
                    }

                    else -> {
                        environment.logger.logKotshiError(Errors.invalidRegisterAdapterVisibility, it)
                    }
                }
            }
            .mapNotNull { adapter ->
                val annotation = adapter.getAnnotation()!!
                adapter.getManualAdapter(
                    logError = environment.logger::logKotshiError,
                    getSuperClass = { superClass()?.declaration as KSClassDeclaration },
                    getSuperTypeName = { superClass()?.toTypeName(toTypeParameterResolver()) },
                    adapterClassName = adapter.asClassName(),
                    typeVariables = { typeParameters.map { it.toTypeVariableName(toTypeParameterResolver()) } },
                    isObject = adapter.isCompanionObject || adapter.classKind == ClassKind.OBJECT,
                    isAbstract = adapter.isAbstract(),
                    priority = annotation.getValueOrDefault("priority") { 0 },
                    getKotshiConstructor = {
                        val typeResolver = toTypeParameterResolver()

                        getAllConstructors().findKotshiConstructor(
                            parameters = { parameters },
                            type = { type.toTypeName(typeResolver) },
                            hasDefaultValue = { hasDefault }
                        ) { requireNotNull(name).asString() }
                    },
                    getJsonQualifiers = {
                        annotations
                            .filter {
                                it.annotationType.resolve().declaration.isAnnotationPresent(JsonQualifier::class)
                            }
                            .map { annotation ->
                                annotation.toAnnotationModel()
                            }
                            .toSet()
                    }
                )
            }
            .toList()

        val generatedAdapters = generateAdapters(resolver, globalConfig)

        if (targetFactory != null) {
            try {
                val jsonAdapterFactoryType = resolver.getClassDeclarationByName()!!
                    .asType(emptyList())
                val factory = JsonAdapterFactory(
                    targetType = targetFactory.toClassName(),
                    usageType = if (
                        targetFactory.classKind == ClassKind.INTERFACE ||
                        targetFactory.asType(emptyList()).isAssignableFrom(jsonAdapterFactoryType)
                    ) {
                        environment.logger.logKotshiWarning(abstractFactoriesAreDeprecated, targetFactory)
                        JsonAdapterFactory.UsageType.Subclass(
                            parent = targetFactory.toClassName(),
                            parentIsInterface = targetFactory.classKind == ClassKind.INTERFACE
                        )
                    } else {
                        JsonAdapterFactory.UsageType.Standalone
                    },
                    generatedAdapters = generatedAdapters,
                    manuallyRegisteredAdapters = manualAdapters,
                )
                JsonAdapterFactoryRenderer(factory, createAnnotationsUsingConstructor)
                    .render(generatedAnnotation) { addOriginatingKSFile(targetFactory.containingFile!!) }
                    .writeTo(environment.codeGenerator, aggregating = true)
            } catch (e: KspProcessingError) {
                environment.logger.logKotshiError(e)
            } catch (e: Exception) {
                environment.logger.logKotshiError("Failed to analyze class:", targetFactory)
                environment.logger.exception(e)
            }
        }

        return emptyList()
    }

    private fun generateAdapters(resolver: Resolver, globalConfig: GlobalConfig): List =
        resolver.getSymbolsWithAnnotation(JsonSerializable::class.qualifiedName!!).mapNotNull { annotated ->
            if (annotated.origin == Origin.JAVA || annotated.origin == Origin.JAVA_LIB) {
                environment.logger.logKotshiError(javaClassNotSupported, annotated)
                return@mapNotNull null
            }
            if (annotated !is KSClassDeclaration) {
                environment.logger.logKotshiError(Errors.unsupportedSerializableType, annotated)
                return@mapNotNull null
            }
            try {
                val generator = when (annotated.classKind) {
                    ClassKind.CLASS -> {
                        when {
                            Modifier.DATA in annotated.modifiers -> {
                                DataClassAdapterGenerator(
                                    environment = environment,
                                    element = annotated,
                                    globalConfig = globalConfig,
                                    resolver = resolver,
                                )
                            }

                            Modifier.SEALED in annotated.modifiers -> {
                                SealedClassAdapterGenerator(
                                    environment = environment,
                                    targetElement = annotated,
                                    globalConfig = globalConfig,
                                    resolver = resolver,
                                )
                            }

                            else -> throw KspProcessingError(Errors.unsupportedSerializableType, annotated)
                        }
                    }

                    ClassKind.ENUM_CLASS -> EnumAdapterGenerator(
                        environment = environment,
                        element = annotated,
                        globalConfig = globalConfig,
                        resolver = resolver,
                    )

                    ClassKind.OBJECT -> ObjectAdapterGenerator(
                        environment = environment,
                        element = annotated,
                        globalConfig = globalConfig,
                        resolver = resolver,
                    )

                    else -> throw KspProcessingError(Errors.unsupportedSerializableType, annotated)
                }

                generator.generateAdapter(
                    createAnnotationsUsingConstructor = createAnnotationsUsingConstructor,
                    useLegacyDataClassRenderer = useLegacyDataClassRenderer,
                    generatedAnnotation = generatedAnnotation,
                )
            } catch (e: KspProcessingError) {
                environment.logger.logKotshiError(e)
                null
            } catch (e: Exception) {
                environment.logger.logKotshiError("Failed to analyze class:", annotated)
                environment.logger.exception(e)
                null
            }
        }.toList()
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy