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

com.airbnb.android.showkase.processor.models.ShowkaseMetadata.kt Maven / Gradle / Ivy

package com.airbnb.android.showkase.processor.models

import androidx.room.compiler.processing.XAnnotation
import androidx.room.compiler.processing.XAnnotationBox
import androidx.room.compiler.processing.XElement
import androidx.room.compiler.processing.XFieldElement
import androidx.room.compiler.processing.XMemberContainer
import androidx.room.compiler.processing.XMethodElement
import androidx.room.compiler.processing.XType
import androidx.room.compiler.processing.XTypeElement
import androidx.room.compiler.processing.compat.XConverters.toJavac
import com.airbnb.android.showkase.annotation.ShowkaseCodegenMetadata
import com.airbnb.android.showkase.annotation.ShowkaseColor
import com.airbnb.android.showkase.annotation.ShowkaseComposable
import com.airbnb.android.showkase.annotation.ShowkaseTypography
import com.airbnb.android.showkase.processor.ShowkaseProcessor.Companion.PREVIEW_PARAMETER_SIMPLE_NAME
import com.airbnb.android.showkase.processor.ShowkaseProcessor.Companion.PREVIEW_SIMPLE_NAME
import com.airbnb.android.showkase.processor.exceptions.ShowkaseProcessorException
import com.airbnb.android.showkase.processor.logging.ShowkaseValidator
import com.airbnb.android.showkase.processor.utils.findAnnotationBySimpleName
import com.airbnb.android.showkase.processor.utils.getFieldWithReflection
import com.airbnb.android.showkase.processor.utils.requireAnnotationBySimpleName
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.javapoet.toKClassName
import com.squareup.kotlinpoet.javapoet.toKTypeName
import kotlinx.metadata.jvm.KotlinClassHeader.Companion.FILE_FACADE_KIND
import kotlinx.metadata.jvm.KotlinClassMetadata
import java.util.Locale

@Suppress("LongParameterList")
internal sealed class ShowkaseMetadata {
    abstract val element: XElement
    abstract val packageName: String
    abstract val packageSimpleName: String
    abstract val elementName: String
    abstract val showkaseName: String
    abstract val showkaseGroup: String
    abstract val showkaseKDoc: String
    abstract val enclosingClassName: ClassName?
    abstract val insideWrapperClass: Boolean
    abstract val insideObject: Boolean

    data class Component(
        override val element: XElement,
        override val packageName: String,
        override val packageSimpleName: String,
        override val elementName: String,
        override val showkaseName: String,
        override val showkaseGroup: String,
        override val showkaseKDoc: String,
        override val enclosingClassName: ClassName? = null,
        override val insideWrapperClass: Boolean = false,
        override val insideObject: Boolean = false,
        val componentIndex: Int? = null,
        val showkaseWidthDp: Int? = null,
        val showkaseHeightDp: Int? = null,
        val previewParameterProviderType: TypeName? = null,
        val previewParameterName: String? = null,
        val showkaseStyleName: String? = null,
        val isDefaultStyle: Boolean = false
    ) : ShowkaseMetadata()

    data class Color(
        override val element: XElement,
        override val packageSimpleName: String,
        override val packageName: String,
        override val elementName: String,
        override val showkaseName: String,
        override val showkaseGroup: String,
        override val showkaseKDoc: String,
        override val enclosingClassName: ClassName? = null,
        override val insideWrapperClass: Boolean = false,
        override val insideObject: Boolean = false,
    ) : ShowkaseMetadata()

    data class Typography(
        override val element: XElement,
        override val packageSimpleName: String,
        override val packageName: String,
        override val elementName: String,
        override val showkaseName: String,
        override val showkaseGroup: String,
        override val showkaseKDoc: String,
        override val enclosingClassName: ClassName? = null,
        override val insideWrapperClass: Boolean = false,
        override val insideObject: Boolean = false,
    ) : ShowkaseMetadata()
}

internal enum class ShowkaseFunctionType {
    TOP_LEVEL,
    INSIDE_CLASS,
    INSIDE_OBJECT,
    INSIDE_COMPANION_OBJECT,
}

internal fun ShowkaseFunctionType.insideObject() = this == ShowkaseFunctionType.INSIDE_OBJECT ||
        this == ShowkaseFunctionType.INSIDE_COMPANION_OBJECT

internal enum class ShowkaseMetadataType {
    COMPONENT,
    COLOR,
    TYPOGRAPHY
}

internal fun XAnnotationBox.toModel(element: XElement): ShowkaseMetadata {
    val (enclosingClassType, previewParameterClassType) = getCodegenMetadataTypes()

    // The box is needed to get all Class values, primitives can be accessed dirctly
    val props = value

    return when (ShowkaseMetadataType.valueOf(props.showkaseMetadataType)) {
        ShowkaseMetadataType.COMPONENT -> {
            ShowkaseMetadata.Component(
                packageSimpleName = props.packageSimpleName,
                packageName = props.packageName,
                enclosingClassName = enclosingClassType?.typeElement?.className?.toKClassName(),
                elementName = props.showkaseElementName,
                showkaseName = props.showkaseName,
                showkaseGroup = props.showkaseGroup,
                showkaseStyleName = props.showkaseStyleName,
                showkaseWidthDp = props.showkaseWidthDp.parseAnnotationProperty(),
                showkaseHeightDp = props.showkaseHeightDp.parseAnnotationProperty(),
                insideWrapperClass = props.insideWrapperClass,
                insideObject = props.insideObject,
                showkaseKDoc = props.showkaseKDoc,
                element = element,
                previewParameterProviderType = previewParameterClassType?.typeName?.toKTypeName(),
                previewParameterName = props.previewParameterName,
                isDefaultStyle = props.isDefaultStyle
            )
        }
        ShowkaseMetadataType.COLOR -> {
            ShowkaseMetadata.Color(
                packageSimpleName = props.packageSimpleName,
                packageName = props.packageName,
                enclosingClassName = enclosingClassType?.typeElement?.className?.toKClassName(),
                elementName = props.showkaseElementName,
                showkaseName = props.showkaseName,
                showkaseGroup = props.showkaseGroup,
                insideWrapperClass = props.insideWrapperClass,
                insideObject = props.insideObject,
                showkaseKDoc = props.showkaseKDoc,
                element = element
            )
        }
        ShowkaseMetadataType.TYPOGRAPHY -> {
            ShowkaseMetadata.Typography(
                packageSimpleName = props.packageSimpleName,
                packageName = props.packageName,
                enclosingClassName = enclosingClassType?.typeElement?.className?.toKClassName(),
                elementName = props.showkaseElementName,
                showkaseName = props.showkaseName,
                showkaseGroup = props.showkaseGroup,
                insideWrapperClass = props.insideWrapperClass,
                insideObject = props.insideObject,
                showkaseKDoc = props.showkaseKDoc,
                element = element
            )
        }
    }
}

internal fun XAnnotationBox.getCodegenMetadataTypes(): Pair {
    return Pair(
        getAsTypeList("enclosingClass").firstOrNull(),
        getAsTypeList("previewParameterClass").firstOrNull()
    )
}

private fun Int.parseAnnotationProperty() = when (this) {
    -1 -> null
    else -> this
}

internal fun getShowkaseMetadata(
    element: XMethodElement,
    showkaseValidator: ShowkaseValidator
): List {
    val showkaseAnnotations = element.getAnnotations(ShowkaseComposable::class)

    val commonMetadata = element.extractCommonMetadata(showkaseValidator)
    val previewParameterMetadata = element.getPreviewParameterMetadata()

    return showkaseAnnotations.mapNotNull { annotation ->
        // If this component was configured to be skipped, return early
        if (annotation.value.skip) return@mapNotNull null

        val showkaseName = getShowkaseName(annotation.value.name, element.name)
        val showkaseGroup = getShowkaseGroup(
            annotation.value.group,
            commonMetadata.enclosingClass,
        )
        val isDefaultStyle = annotation.value.defaultStyle
        val showkaseStyleName = getShowkaseStyleName(annotation.value.styleName, isDefaultStyle)

        ShowkaseMetadata.Component(
            packageSimpleName = commonMetadata.moduleName,
            packageName = commonMetadata.packageName,
            enclosingClassName = commonMetadata.enclosingClassName,
            elementName = element.name,
            showkaseName = showkaseName,
            showkaseGroup = showkaseGroup,
            showkaseStyleName = showkaseStyleName,
            showkaseWidthDp = annotation.value.widthDp.parseAnnotationProperty(),
            showkaseHeightDp = annotation.value.heightDp.parseAnnotationProperty(),
            insideObject = commonMetadata.showkaseFunctionType.insideObject(),
            insideWrapperClass = commonMetadata.showkaseFunctionType == ShowkaseFunctionType.INSIDE_CLASS,
            element = element,
            showkaseKDoc = commonMetadata.kDoc,
            previewParameterName = previewParameterMetadata?.first,
            previewParameterProviderType = previewParameterMetadata?.second,
            isDefaultStyle = isDefaultStyle,
            componentIndex = showkaseAnnotations.indexOf(annotation),
        )
    }
}

internal fun XMethodElement.extractCommonMetadata(showkaseValidator: ShowkaseValidator): CommonMetadata {
    return extractCommonMetadata(enclosingElement, showkaseValidator)
}

internal fun XFieldElement.extractCommonMetadata(showkaseValidator: ShowkaseValidator): CommonMetadata {
    return extractCommonMetadata(enclosingElement, showkaseValidator)
}

internal fun XElement.extractCommonMetadata(
    enclosingElement: XMemberContainer,
    showkaseValidator: ShowkaseValidator
): CommonMetadata {
    val showkaseFunctionType: ShowkaseFunctionType = getShowkaseFunctionType(enclosingElement)

    return CommonMetadata(
        packageName = enclosingElement.className.packageName(),
        moduleName = enclosingElement.className.packageName().substringAfterLast("."),
        kDoc = docComment.orEmpty().trim(),
        showkaseFunctionType = showkaseFunctionType,
        enclosingClass = getEnclosingClass(showkaseFunctionType, enclosingElement)
    ).also {
        showkaseValidator.validateEnclosingClass(it.enclosingClass)
    }
}

internal data class CommonMetadata(
    val packageName: String,
    val moduleName: String,
    val kDoc: String,
    val showkaseFunctionType: ShowkaseFunctionType,
    val enclosingClass: XTypeElement?,
) {
    val enclosingClassName: ClassName? = enclosingClass?.className?.toKClassName()
}

@Suppress("LongParameterList", "LongMethod")
internal fun getShowkaseMetadataFromPreview(
    element: XMethodElement,
    showkaseValidator: ShowkaseValidator,
): List {
    val previewAnnotations = element.requireAnnotationBySimpleName(PREVIEW_SIMPLE_NAME)

    val showkaseComosableAnnotation = element.getAnnotation(ShowkaseComposable::class)?.value
    // If this component was configured to be skipped, return early
    if (showkaseComosableAnnotation != null && showkaseComosableAnnotation.skip) return listOf() // Will be mapped out
    return previewAnnotations.mapIndexed { index, annotation ->
        val commonMetadata = element.extractCommonMetadata(showkaseValidator)
        val showkaseName = getShowkaseName(
            annotation.getAsString("name"),
            element.name
        )
        val showkaseGroup = getShowkaseGroup(
            annotation.getAsString("group"),
            commonMetadata.enclosingClass,
        )

        val width = annotation.getAsInt("widthDp")
        val height = annotation.getAsInt("heightDp")

        val previewParameterMetadata = element.getPreviewParameterMetadata()

        ShowkaseMetadata.Component(
            packageSimpleName = commonMetadata.moduleName,
            packageName = commonMetadata.packageName,
            enclosingClassName = commonMetadata.enclosingClassName,
            elementName = element.name,
            showkaseKDoc = commonMetadata.kDoc,
            showkaseName = showkaseName,
            showkaseGroup = showkaseGroup,
            showkaseWidthDp = if (width == -1) null else width,
            showkaseHeightDp = if (height == -1) null else height,
            insideWrapperClass = commonMetadata.showkaseFunctionType == ShowkaseFunctionType.INSIDE_CLASS,
            insideObject = commonMetadata.showkaseFunctionType.insideObject(),
            element = element,
            previewParameterName = previewParameterMetadata?.first,
            previewParameterProviderType = previewParameterMetadata?.second,
            componentIndex = index,
        )
    }
}

private fun XMethodElement.getPreviewParameterMetadata(): Pair? {
    val previewParameterPair = getPreviewParameterAnnotation()
    return previewParameterPair?.let {
        it.first to it.second.getAsType("provider")
            .typeName
            .toKTypeName()
    }
}

private fun XMethodElement.getPreviewParameterAnnotation(): Pair? {
    return parameters.mapNotNull { parameter ->
        val previewParamAnnotation = parameter.findAnnotationBySimpleName(PREVIEW_PARAMETER_SIMPLE_NAME)
        previewParamAnnotation?.let {
            parameter.name to previewParamAnnotation
        }
    }.firstOrNull()
}

internal fun getShowkaseColorMetadata(
    element: XFieldElement,
    showkaseValidator: ShowkaseValidator
): ShowkaseMetadata {
    val showkaseColorAnnotation = element.requireAnnotation(ShowkaseColor::class).value
    // TODO(vinaygaba): Color properties aren't working properly with companion objects. This is
    // because the properties are generated outside the companion object in java land(as opposed to
    // inside the companion class for functions). Need to investigate more.
    val commonMetadata = element.extractCommonMetadata(showkaseValidator)
    val showkaseName = getShowkaseName(showkaseColorAnnotation.name, element.name)
    val showkaseGroup = getShowkaseGroup(
        showkaseColorAnnotation.group, commonMetadata.enclosingClass,
    )

    return ShowkaseMetadata.Color(
        element = element,
        showkaseName = showkaseName,
        showkaseGroup = showkaseGroup,
        showkaseKDoc = commonMetadata.kDoc,
        elementName = element.name,
        packageSimpleName = commonMetadata.moduleName,
        packageName = commonMetadata.packageName,
        enclosingClassName = commonMetadata.enclosingClassName,
        insideWrapperClass = commonMetadata.showkaseFunctionType == ShowkaseFunctionType.INSIDE_CLASS,
        insideObject = commonMetadata.showkaseFunctionType.insideObject()
    )
}

internal fun getShowkaseTypographyMetadata(
    element: XFieldElement,
    showkaseValidator: ShowkaseValidator
): ShowkaseMetadata {
    val showkaseTypographyAnnotation = element.requireAnnotation(ShowkaseTypography::class).value

    val commonMetadata = element.extractCommonMetadata(showkaseValidator)
    // TODO(vinaygaba): Typography properties aren't working properly with companion objects.
    // This is because the properties are generated outside the companion object in java land(as
    // opposed to inside the companion class for functions). Need to investigate more.
    val showkaseName = getShowkaseName(showkaseTypographyAnnotation.name, element.name)
    val showkaseGroup = getShowkaseGroup(
        showkaseTypographyAnnotation.group,
        commonMetadata.enclosingClass,
    )

    return ShowkaseMetadata.Typography(
        element = element,
        showkaseName = showkaseName,
        showkaseGroup = showkaseGroup,
        showkaseKDoc = commonMetadata.kDoc,
        elementName = element.name,
        packageSimpleName = commonMetadata.moduleName,
        packageName = commonMetadata.packageName,
        enclosingClassName = commonMetadata.enclosingClassName,
        insideWrapperClass = commonMetadata.showkaseFunctionType == ShowkaseFunctionType.INSIDE_CLASS,
        insideObject = commonMetadata.showkaseFunctionType.insideObject()
    )
}

internal fun XElement.getShowkaseFunctionType(enclosingElement: XMemberContainer): ShowkaseFunctionType {
    return when {
        this.isTopLevel(enclosingElement) -> ShowkaseFunctionType.TOP_LEVEL
        (enclosingElement as? XTypeElement)?.isCompanionObject() == true -> ShowkaseFunctionType.INSIDE_COMPANION_OBJECT
        (enclosingElement as? XTypeElement)?.isKotlinObject() == true -> ShowkaseFunctionType.INSIDE_OBJECT
        enclosingElement is XTypeElement -> ShowkaseFunctionType.INSIDE_CLASS
        else -> throw ShowkaseProcessorException(
            "Function is declared in a way that is not supported by Showkase.",
            this
        )
    }
}


fun XElement.isTopLevel(enclosingElement: XMemberContainer): Boolean {
    return if (isJavac()) {
        // Per enclosingElement kdoc:
        // When running with KAPT, the value will be an XTypeElement.
        // Right now xprocessing doesn't expose the top level details, so we have to use
        // reflection to get kotlin metadata
        val xTypeElement = enclosingElement as? XTypeElement
            ?: throw ShowkaseProcessorException(
                "Expected a type element but got $enclosingElement",
                this
            )

        // JavacTypeElement has a kotlinMetadata property with a custom "KotlinMetadataElement"
        // class type. This is null though if the type doesn't have metadata, such as in the case
        // of a top level function.
        val kotlinMetadata = xTypeElement.getFieldWithReflection("kotlinMetadata")
            ?: return true

        val enclosingElementKind = kotlinMetadata
            .getFieldWithReflection("classMetadata")
            .header
            .kind

        enclosingElementKind == FILE_FACADE_KIND
    } else {
        // Per enclosingElement kdoc:
        // When running with KSP, if this function is in source, the value will NOT be an XTypeElement.
        // We don't expect to handle functions from classpath because we only process annotations in source
        enclosingElement !is XTypeElement
    }
}

fun XElement.isJavac(): Boolean {
    @Suppress("TooGenericExceptionCaught")
    return try {
        toJavac()
        true
    } catch (e: Throwable) {
        false
    }
}

internal fun getEnclosingClass(
    showkaseFunctionType: ShowkaseFunctionType,
    enclosingElement: XMemberContainer
): XTypeElement? = when (showkaseFunctionType) {
    ShowkaseFunctionType.TOP_LEVEL -> null
    ShowkaseFunctionType.INSIDE_CLASS, ShowkaseFunctionType.INSIDE_OBJECT -> enclosingElement as XTypeElement
    // Get the class that holds the companion object instead of using the intermediate element
    // that's used to represent the companion object.
    ShowkaseFunctionType.INSIDE_COMPANION_OBJECT -> (enclosingElement as XTypeElement).enclosingTypeElement
}

internal fun getShowkaseName(
    showkaseNameFromAnnotation: String,
    elementName: String
) = when {
    showkaseNameFromAnnotation.isBlank() -> elementName.capitalize(Locale.getDefault())
    else -> showkaseNameFromAnnotation
}

internal fun getShowkaseGroup(
    showkaseGroupFromAnnotation: String,
    enclosingClass: XTypeElement?,
) = when {
    showkaseGroupFromAnnotation.isNotBlank() -> showkaseGroupFromAnnotation
    showkaseGroupFromAnnotation.isBlank() && enclosingClass != null -> enclosingClass.name.capitalize(
        Locale.getDefault()
    )
    else -> "Default Group"
}

internal fun getShowkaseStyleName(
    showkaseStyleFromAnnotation: String,
    isDefaultStyle: Boolean,
) = when {
    showkaseStyleFromAnnotation.isNotBlank() -> showkaseStyleFromAnnotation.replaceFirstChar { it.uppercase() }
    // If style name is not specified but its the default style, just provide the default style name
    isDefaultStyle -> "Default Style"
    else -> null
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy