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

com.freeletics.mad.whetstone.WhetstoneCodeGenerator.kt Maven / Gradle / Ivy

There is a newer version: 0.14.1
Show newest version
package com.freeletics.mad.whetstone

import com.freeletics.mad.whetstone.codegen.FileGenerator
import com.freeletics.mad.whetstone.codegen.util.composeDialogFragmentFqName
import com.freeletics.mad.whetstone.codegen.util.composeFqName
import com.freeletics.mad.whetstone.codegen.util.composeFragmentFqName
import com.freeletics.mad.whetstone.codegen.util.dialogFragment
import com.freeletics.mad.whetstone.codegen.util.emptyNavigationHandler
import com.freeletics.mad.whetstone.codegen.util.emptyNavigator
import com.freeletics.mad.whetstone.codegen.util.fragment
import com.freeletics.mad.whetstone.codegen.util.moduleFqName
import com.freeletics.mad.whetstone.codegen.util.navEntryComponentFqName
import com.freeletics.mad.whetstone.codegen.util.rendererDialogFragmentFqName
import com.freeletics.mad.whetstone.codegen.util.rendererFragmentFqName
import com.google.auto.service.AutoService
import com.squareup.anvil.annotations.ExperimentalAnvilApi
import com.squareup.anvil.compiler.api.AnvilCompilationException
import com.squareup.anvil.compiler.api.AnvilContext
import com.squareup.anvil.compiler.api.CodeGenerator
import com.squareup.anvil.compiler.api.GeneratedFile
import com.squareup.anvil.compiler.api.createGeneratedFile
import com.squareup.anvil.compiler.internal.asClassName
import com.squareup.anvil.compiler.internal.classesAndInnerClass
import com.squareup.anvil.compiler.internal.findAnnotation
import com.squareup.anvil.compiler.internal.findAnnotationArgument
import com.squareup.anvil.compiler.internal.requireFqName
import com.squareup.kotlinpoet.ClassName
import java.io.File
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
import org.jetbrains.kotlin.psi.KtAnnotationEntry
import org.jetbrains.kotlin.psi.KtClassLiteralExpression
import org.jetbrains.kotlin.psi.KtConstantExpression
import org.jetbrains.kotlin.psi.KtDeclaration
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtNamedFunction

@OptIn(ExperimentalAnvilApi::class)
@AutoService(CodeGenerator::class)
class WhetstoneCodeGenerator : CodeGenerator {

    override fun isApplicable(context: AnvilContext): Boolean = !context.disableComponentMerging

    override fun generateCode(
        codeGenDir: File,
        module: ModuleDescriptor,
        projectFiles: Collection
    ): Collection {
        val rendererFragment = projectFiles
            .classesAndInnerClass(module)
            .mapNotNull { clazz -> generateRendererCode(codeGenDir, module, clazz) }
        val rendererDialogFragment = projectFiles
            .classesAndInnerClass(module)
            .mapNotNull { clazz -> generateRendererDialogCode(codeGenDir, module, clazz) }

        val composeScreen = projectFiles
            .flatMap { it.declarations.filterIsInstance() }
            .mapNotNull { function -> generateComposeScreenCode(codeGenDir, module, function) }

        val composeFragment = projectFiles
            .flatMap { it.declarations.filterIsInstance() }
            .mapNotNull { function -> generateComposeFragmentCode(codeGenDir, module, function) }

        val composeDialogFragment = projectFiles
            .flatMap { it.declarations.filterIsInstance() }
            .mapNotNull { clazz -> generateComposeDialogFragmentCode(codeGenDir, module, clazz) }

        val navEntry = projectFiles
            .classesAndInnerClass(module)
            .filter { it.findAnnotation(moduleFqName, module) != null}
            .flatMap { it.declarations }
            .mapNotNull { clazz -> generateNavEntryCode(codeGenDir, module, clazz) }

        return rendererFragment.toList() + rendererDialogFragment +
                composeFragment + composeScreen + composeDialogFragment + navEntry
    }

    private fun generateRendererCode(
        codeGenDir: File,
        module: ModuleDescriptor,
        declaration: KtDeclaration
    ): GeneratedFile? {
        val renderer = declaration.findAnnotation(rendererFragmentFqName, module) ?: return null
        val data = RendererFragmentData(
            baseName = declaration.name!!,
            packageName = declaration.containingKtFile.packageFqName.pathSegments().joinToString(separator = "."),
            scope = renderer.requireClassArgument("scope", 0, module),
            parentScope = renderer.requireClassArgument("parentScope", 1, module),
            dependencies = renderer.requireClassArgument("dependencies", 2, module),
            factory = renderer.requireClassArgument("rendererFactory", 3, module),
            stateMachine = renderer.requireClassArgument("stateMachine", 4, module),
            fragmentBaseClass = fragment,
            navigation = renderer.toNavigation(5, 6, module),
            coroutinesEnabled = renderer.optionalBooleanArgument("coroutinesEnabled", 7) ?: false,
            rxJavaEnabled = renderer.optionalBooleanArgument("rxJavaEnabled", 8) ?: false,
        )
        //TODO check that navigationHandler type fits to fragment

        val file = FileGenerator().generate(data)
        return createGeneratedFile(
            codeGenDir = codeGenDir,
            packageName = file.packageName,
            fileName = file.name,
            content = file.toString()
        )
    }

    private fun generateRendererDialogCode(
        codeGenDir: File,
        module: ModuleDescriptor,
        declaration: KtDeclaration
    ): GeneratedFile? {
        val renderer = declaration.findAnnotation(rendererDialogFragmentFqName, module) ?: return null
        val data = RendererFragmentData(
            baseName = declaration.name!!,
            packageName = declaration.containingKtFile.packageFqName.pathSegments().joinToString(separator = "."),
            scope = renderer.requireClassArgument("scope", 0, module),
            parentScope = renderer.requireClassArgument("parentScope", 1, module),
            dependencies = renderer.requireClassArgument("dependencies", 2, module),
            factory = renderer.requireClassArgument("rendererFactory", 3, module),
            stateMachine = renderer.requireClassArgument("stateMachine", 4, module),
            fragmentBaseClass = renderer.optionalClassArgument("dialogFragmentBaseClass", 5, module) ?: dialogFragment,
            navigation = renderer.toNavigation(6, 7, module),
            coroutinesEnabled = renderer.optionalBooleanArgument("coroutinesEnabled", 8) ?: false,
            rxJavaEnabled = renderer.optionalBooleanArgument("rxJavaEnabled", 9) ?: false,
        )
        //TODO check that navigationHandler type fits to fragment

        val file = FileGenerator().generate(data)
        return createGeneratedFile(
            codeGenDir = codeGenDir,
            packageName = file.packageName,
            fileName = file.name,
            content = file.toString()
        )
    }

    private fun generateComposeFragmentCode(
        codeGenDir: File,
        module: ModuleDescriptor,
        declaration: KtDeclaration
    ): GeneratedFile? {
        val composeFragment = declaration.findAnnotation(composeFragmentFqName, module) ?: return null
        val data = ComposeFragmentData(
            baseName = declaration.name!!,
            packageName = declaration.containingKtFile.packageFqName.pathSegments().joinToString(separator = "."),
            scope = composeFragment.requireClassArgument("scope", 0, module),
            parentScope = composeFragment.requireClassArgument("parentScope", 1, module),
            dependencies = composeFragment.requireClassArgument("dependencies", 2, module),
            fragmentBaseClass = fragment,
            stateMachine = composeFragment.requireClassArgument("stateMachine", 3, module),
            enableInsetHandling = composeFragment.optionalBooleanArgument("enableInsetHandling", 4) ?: false,
            navigation = composeFragment.toNavigation(5, 6, module),
            coroutinesEnabled = composeFragment.optionalBooleanArgument("coroutinesEnabled", 7) ?: false,
            rxJavaEnabled = composeFragment.optionalBooleanArgument("rxJavaEnabled", 7) ?: false,
        )
        //TODO check that navigationHandler type fits to fragment

        val file = FileGenerator().generate(data)
        return createGeneratedFile(
            codeGenDir = codeGenDir,
            packageName = file.packageName,
            fileName = file.name,
            content = file.toString()
        )
    }

    private fun generateComposeDialogFragmentCode(
        codeGenDir: File,
        module: ModuleDescriptor,
        declaration: KtDeclaration
    ): GeneratedFile? {
        val composeFragment = declaration.findAnnotation(composeDialogFragmentFqName, module) ?: return null
        val data = ComposeFragmentData(
            baseName = declaration.name!!,
            packageName = declaration.containingKtFile.packageFqName.pathSegments().joinToString(separator = "."),
            scope = composeFragment.requireClassArgument("scope", 0, module),
            parentScope = composeFragment.requireClassArgument("parentScope", 1, module),
            dependencies = composeFragment.requireClassArgument("dependencies", 2, module),
            fragmentBaseClass = composeFragment.optionalClassArgument("dialogFragmentBaseClass", 3, module) ?:  dialogFragment,
            stateMachine = composeFragment.requireClassArgument("stateMachine", 4, module),
            enableInsetHandling = composeFragment.optionalBooleanArgument("enableInsetHandling", 5) ?: false,
            navigation = composeFragment.toNavigation(6, 7, module),
            coroutinesEnabled = composeFragment.optionalBooleanArgument("coroutinesEnabled", 8) ?: false,
            rxJavaEnabled = composeFragment.optionalBooleanArgument("rxJavaEnabled", 9) ?: false,
        )
        //TODO check that navigationHandler type fits to fragment

        val file = FileGenerator().generate(data)
        return createGeneratedFile(
            codeGenDir = codeGenDir,
            packageName = file.packageName,
            fileName = file.name,
            content = file.toString()
        )
    }

    private fun generateComposeScreenCode(
        codeGenDir: File,
        module: ModuleDescriptor,
        declaration: KtDeclaration
    ): GeneratedFile? {
        val compose = declaration.findAnnotation(composeFqName, module) ?: return null
        val data = ComposeScreenData(
            baseName = declaration.name!!,
            packageName = declaration.containingKtFile.packageFqName.pathSegments().joinToString(separator = "."),
            scope = compose.requireClassArgument("scope", 0, module),
            parentScope = compose.requireClassArgument("parentScope", 1, module),
            dependencies = compose.requireClassArgument("dependencies", 2, module),
            stateMachine = compose.requireClassArgument("stateMachine", 3, module),
            navigation = compose.toNavigation(4, 5, module),
            coroutinesEnabled = compose.optionalBooleanArgument("coroutinesEnabled", 6) ?: false,
            rxJavaEnabled = compose.optionalBooleanArgument("rxJavaEnabled", 7) ?: false,
        )
        //TODO check that navigationHandler type fits to compose

        val file = FileGenerator().generate(data)
        return createGeneratedFile(
            codeGenDir = codeGenDir,
            packageName = file.packageName,
            fileName = file.name,
            content = file.toString()
        )
    }

    private fun KtAnnotationEntry.toNavigation(
        navigatorIndex: Int,
        navigationHandlerIndex: Int,
        module: ModuleDescriptor
    ): CommonData.Navigation? {
        val navigator = optionalClassArgument("navigator", navigatorIndex, module)
        val navigationHandler = optionalClassArgument("navigationHandler", navigationHandlerIndex, module)

        if (navigator != null && navigationHandler != null &&
            navigator != emptyNavigator && navigationHandler != emptyNavigationHandler
        ) {
            return CommonData.Navigation(navigator, navigationHandler)
        }
        if (navigator == null && navigationHandler == null) {
            return null
        }
        if (navigator == emptyNavigator && navigationHandler == emptyNavigationHandler) {
            return null
        }

        throw IllegalStateException("navigator and navigationHandler need to be set together")
    }

    private fun generateNavEntryCode(
        codeGenDir: File,
        module: ModuleDescriptor,
        declaration: KtDeclaration
    ): GeneratedFile? {
        val component = declaration.findAnnotation(navEntryComponentFqName, module) ?: return null
        val scope = component.requireClassArgument("scope", 0, module)
        val data = NavEntryData(
            baseName = scope.simpleName,
            packageName = declaration.containingKtFile.packageFqName.pathSegments().joinToString(separator = "."),
            scope = scope,
            parentScope = component.requireClassArgument("parentScope", 1, module),
            coroutinesEnabled = component.optionalBooleanArgument("coroutinesEnabled", 2) ?: false,
            rxJavaEnabled = component.optionalBooleanArgument("rxJavaEnabled", 3) ?: false,
        )

        val file = FileGenerator().generate(data)
        return createGeneratedFile(
            codeGenDir = codeGenDir,
            packageName = file.packageName,
            fileName = file.name,
            content = file.toString()
        )
    }

    private fun KtAnnotationEntry.requireClassArgument(
        name: String,
        index: Int,
        module: ModuleDescriptor
    ): ClassName {
        val classLiteralExpression = findAnnotationArgument(name, index)
        if (classLiteralExpression != null) {
            return classLiteralExpression.requireFqName(module).asClassName(module)
        }
        throw AnvilCompilationException(
            "Couldn't find $name for ${requireFqName(module)}",
            element = this
        )
    }

    //TODO replace with a way to get default value
    private fun KtAnnotationEntry.optionalClassArgument(
        name: String,
        index: Int,
        module: ModuleDescriptor
    ): ClassName? {
        val classLiteralExpression = findAnnotationArgument(name, index)
        if (classLiteralExpression != null) {
            return classLiteralExpression.requireFqName(module).asClassName(module)
        }
        return null
    }

    //TODO replace with a way to get default value
    private fun KtAnnotationEntry.optionalBooleanArgument(
        name: String,
        index: Int,
    ): Boolean? {
        val boolean = findAnnotationArgument(name, index)
        if (boolean != null) {
            return boolean.node.firstChildNode.text.toBoolean()
        }
        return null
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy