TypeWriter.kt

/*
 * Copyright 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.room.writer

import androidx.room.RoomProcessor
import androidx.room.compiler.codegen.CodeLanguage
import androidx.room.compiler.codegen.VisibilityModifier
import androidx.room.compiler.codegen.XFunSpec
import androidx.room.compiler.codegen.XPropertySpec
import androidx.room.compiler.codegen.XTypeName
import androidx.room.compiler.codegen.XTypeSpec
import androidx.room.compiler.codegen.XTypeSpec.Builder.Companion.apply
import androidx.room.compiler.processing.XProcessingEnv
import androidx.room.compiler.processing.writeTo
import androidx.room.solver.CodeGenScope
import com.squareup.kotlinpoet.javapoet.JAnnotationSpec
import com.squareup.kotlinpoet.javapoet.JClassName
import com.squareup.kotlinpoet.javapoet.KAnnotationSpec
import com.squareup.kotlinpoet.javapoet.KClassName
import kotlin.reflect.KClass

/**
 * Base class for all writers that can produce a class.
 */
abstract class TypeWriter(val codeLanguage: CodeLanguage) {
    private val sharedFieldSpecs = mutableMapOf<String, XPropertySpec>()
    private val sharedMethodSpecs = mutableMapOf<String, XFunSpec>()
    private val sharedFieldNames = mutableSetOf<String>()
    private val sharedMethodNames = mutableSetOf<String>()

    private val metadata = mutableMapOf<KClass<*>, Any>()

    abstract fun createTypeSpecBuilder(): XTypeSpec.Builder

    /**
     * Read additional metadata that can be put by sub code generators.
     *
     * @see set for more details.
     */
    operator fun <T> get(key: KClass<*>): T? {
        @Suppress("UNCHECKED_CAST")
        return metadata[key] as? T
    }

    /**
     * Add additional metadata to the TypeWriter that can be read back later.
     * This is useful for additional functionality where a sub code generator needs to bubble up
     * information to the main TypeWriter without copying it in every intermediate step.
     *
     * @see get
     */
    operator fun set(key: KClass<*>, value: Any) {
        metadata[key] = value
    }

    fun write(processingEnv: XProcessingEnv) {
        val builder = createTypeSpecBuilder()
        sharedFieldSpecs.values.forEach { builder.addProperty(it) }
        sharedMethodSpecs.values.forEach { builder.addFunction(it) }
        addGeneratedAnnotationIfAvailable(builder, processingEnv)
        addSuppressWarnings(builder)
        builder.build().writeTo(processingEnv.filer)
    }

    private fun addSuppressWarnings(builder: XTypeSpec.Builder) {
        builder.apply(
            javaTypeBuilder = {
                addAnnotation(
                    com.squareup.javapoet.AnnotationSpec.builder(SuppressWarnings::class.java)
                        .addMember("value", "{\$S, \$S}", "unchecked", "deprecation")
                        .build()
                )
            },
            kotlinTypeBuilder = {
                addAnnotation(
                    com.squareup.kotlinpoet.AnnotationSpec.builder(Suppress::class)
                        .addMember(
                            "names = [%S, %S, %S]",
                            "UNCHECKED_CAST",
                            "DEPRECATION",
                            "REDUNDANT_PROJECTION"
                        )
                        .build()
                )
            }
        )
    }

    private fun addGeneratedAnnotationIfAvailable(
        adapterTypeSpecBuilder: XTypeSpec.Builder,
        processingEnv: XProcessingEnv
    ) {
        processingEnv.findGeneratedAnnotation()?.let {
            val annotationName = it.asClassName().canonicalName
            val memberValue = RoomProcessor::class.java.canonicalName
            adapterTypeSpecBuilder.apply(
                javaTypeBuilder = {
                    addAnnotation(
                        JAnnotationSpec.builder(JClassName.bestGuess(annotationName))
                            .addMember("value", "\$S", memberValue)
                            .build()
                    )
                },
                kotlinTypeBuilder = {
                    addAnnotation(
                        KAnnotationSpec.builder(KClassName.bestGuess(annotationName))
                            .addMember("value = [%S]", memberValue)
                            .build()
                    )
                }
            )
        }
    }

    private fun makeUnique(set: MutableSet<String>, value: String): String {
        if (!value.startsWith(CodeGenScope.CLASS_PROPERTY_PREFIX)) {
            return makeUnique(set, "${CodeGenScope.CLASS_PROPERTY_PREFIX}$value")
        }
        if (set.add(value)) {
            return value
        }
        var index = 1
        while (true) {
            if (set.add("${value}_$index")) {
                return "${value}_$index"
            }
            index++
        }
    }

    fun getOrCreateProperty(sharedProperty: SharedPropertySpec): XPropertySpec {
        return sharedFieldSpecs.getOrPut(sharedProperty.getUniqueKey()) {
            sharedProperty.build(this, makeUnique(sharedFieldNames, sharedProperty.baseName))
        }
    }

    fun getOrCreateFunction(sharedFunction: SharedFunctionSpec): XFunSpec {
        return sharedMethodSpecs.getOrPut(sharedFunction.getUniqueKey()) {
            sharedFunction.build(this, makeUnique(sharedMethodNames, sharedFunction.baseName))
        }
    }

    abstract class SharedPropertySpec(val baseName: String, val type: XTypeName) {

        open val isMutable = false

        abstract fun getUniqueKey(): String

        abstract fun prepare(writer: TypeWriter, builder: XPropertySpec.Builder)

        fun build(classWriter: TypeWriter, name: String): XPropertySpec {
            val builder = XPropertySpec.builder(
                language = classWriter.codeLanguage,
                name = name,
                typeName = type,
                visibility = VisibilityModifier.PRIVATE,
                isMutable = isMutable
            )
            prepare(classWriter, builder)
            return builder.build()
        }
    }

    abstract class SharedFunctionSpec(val baseName: String) {

        abstract fun getUniqueKey(): String

        abstract fun prepare(
            methodName: String,
            writer: TypeWriter,
            builder: XFunSpec.Builder
        )

        fun build(writer: TypeWriter, name: String): XFunSpec {
            val builder = XFunSpec.builder(writer.codeLanguage, name, VisibilityModifier.PRIVATE)
            prepare(name, writer, builder)
            return builder.build()
        }
    }
}