You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android自定义Lint中如何检测被调用函数是否为@Composable?

自定义Composable函数调用命名参数Lint规则:检测被调用函数是否带@Composable注解

我们正在为Android项目开发自定义Lint规则,要求Composable函数调用必须使用命名参数。当前Detector已能检测调用方是否处于Composable环境,但无法判断被调用的函数本身是否带有@Composable注解,导致规则可能误判非Composable函数的调用场景。

示例代码

@Composable
fun MyComposable(
    a: String,
    b: Int,
    onClick: () -> Unit = {},
) {}

@Composable
fun success1() {
    MyComposable(
        a = "success",
        b = 1,
    ) {

    }
}

现有Detector代码(存在的问题:未检测被调用函数的@Composable注解)

class NamedArgumentDetector : Detector(), SourceCodeScanner {

    override fun getApplicableUastTypes() = listOf(
        UExpression::class.java,
    )

    override fun createUastHandler(context: JavaContext) = object : UElementHandler() {
        override fun visitExpression(node: UExpression) {
            if (node !is KotlinUFunctionCallExpression || !node.isInvokedWithinComposable()) return

            val firstMethodName = node.methodName?.first() ?: return
            if (firstMethodName !in 'A'..'Z') return

            val lastArgumentIndex = node.valueArguments.lastIndex
            node.valueArguments.fastForEachIndexed { index, argument ->
                if (index == lastArgumentIndex && argument is KotlinULambdaExpression) return

                val expressionSourcePsi = argument.sourcePsi

                val argumentParent = expressionSourcePsi?.node?.treeParent ?: return

                val argumentFirstChildNode = argumentParent.firstChildNode
                val argumentParentFirstChildNode = argumentParent.treeParent.firstChildNode

                if (!(argumentFirstChildNode.isValueArgumentName() || argumentParentFirstChildNode.isValueArgumentName())) {
                    context.report(
                        issue = NamedArgumentIssue,
                        scope = expressionSourcePsi,
                        location = context.getLocation(expressionSourcePsi),
                        message = Explanation,
                    )
                    return
                }
            }
        }
    }

    private fun ASTNode.isValueArgumentName() =
        this.elementType == VALUE_ARGUMENT_NAME
}

解决方案:检测被调用函数的@Composable注解

要实现这一点,我们需要通过KotlinUFunctionCallExpression解析出被调用函数的符号信息,然后检查该符号是否带有@Composable注解。

1. 定义Composable注解常量

首先添加一个常量,存储@Composable注解的全类名:

private const val COMPOSABLE_ANNOTATION = "androidx.compose.runtime.Composable"

2. 添加注解检查辅助函数

新增一个扩展函数,用于检查Psi元素是否带有@Composable注解:

private fun PsiModifierListOwner.hasComposableAnnotation(): Boolean {
    return this.annotations.any { annotation ->
        annotation.qualifiedName == COMPOSABLE_ANNOTATION
    }
}

3. 修改Detector的visitExpression方法

在现有逻辑中,添加对被调用函数的注解检查:

override fun visitExpression(node: UExpression) {
    if (node !is KotlinUFunctionCallExpression || !node.isInvokedWithinComposable()) return

    // 新增:获取被调用函数的符号,若无法解析则跳过
    val functionSymbol = node.resolve() ?: return
    // 新增:检查被调用函数是否带有@Composable注解,没有则跳过
    if (!functionSymbol.hasComposableAnnotation()) return

    val firstMethodName = node.methodName?.first() ?: return
    if (firstMethodName !in 'A'..'Z') return

    val lastArgumentIndex = node.valueArguments.lastIndex
    node.valueArguments.fastForEachIndexed { index, argument ->
        if (index == lastArgumentIndex && argument is KotlinULambdaExpression) return

        val expressionSourcePsi = argument.sourcePsi

        val argumentParent = expressionSourcePsi?.node?.treeParent ?: return

        val argumentFirstChildNode = argumentParent.firstChildNode
        val argumentParentFirstChildNode = argumentParent.treeParent.firstChildNode

        if (!(argumentFirstChildNode.isValueArgumentName() || argumentParentFirstChildNode.isValueArgumentName())) {
            context.report(
                issue = NamedArgumentIssue,
                scope = expressionSourcePsi,
                location = context.getLocation(expressionSourcePsi),
                message = Explanation,
            )
            return
        }
    }
}

4. 完整修改后的Detector代码

class NamedArgumentDetector : Detector(), SourceCodeScanner {

    private const val COMPOSABLE_ANNOTATION = "androidx.compose.runtime.Composable"

    override fun getApplicableUastTypes() = listOf(
        UExpression::class.java,
    )

    override fun createUastHandler(context: JavaContext) = object : UElementHandler() {
        override fun visitExpression(node: UExpression) {
            if (node !is KotlinUFunctionCallExpression || !node.isInvokedWithinComposable()) return

            val functionSymbol = node.resolve() ?: return
            if (!functionSymbol.hasComposableAnnotation()) return

            val firstMethodName = node.methodName?.first() ?: return
            if (firstMethodName !in 'A'..'Z') return

            val lastArgumentIndex = node.valueArguments.lastIndex
            node.valueArguments.fastForEachIndexed { index, argument ->
                if (index == lastArgumentIndex && argument is KotlinULambdaExpression) return

                val expressionSourcePsi = argument.sourcePsi

                val argumentParent = expressionSourcePsi?.node?.treeParent ?: return

                val argumentFirstChildNode = argumentParent.firstChildNode
                val argumentParentFirstChildNode = argumentParent.treeParent.firstChildNode

                if (!(argumentFirstChildNode.isValueArgumentName() || argumentParentFirstChildNode.isValueArgumentName())) {
                    context.report(
                        issue = NamedArgumentIssue,
                        scope = expressionSourcePsi,
                        location = context.getLocation(expressionSourcePsi),
                        message = Explanation,
                    )
                    return
                }
            }
        }
    }

    private fun ASTNode.isValueArgumentName() =
        this.elementType == VALUE_ARGUMENT_NAME

    private fun PsiModifierListOwner.hasComposableAnnotation(): Boolean {
        return this.annotations.any { annotation ->
            annotation.qualifiedName == COMPOSABLE_ANNOTATION
        }
    }
}

说明

通过node.resolve()可以获取到被调用函数的PsiMethod符号(实现了PsiModifierListOwner接口),然后通过检查该符号的注解列表,就能判断被调用函数是否带有@Composable注解。这样就能确保只有调用Composable函数时,才会触发命名参数的检查逻辑,避免误判普通函数。

内容的提问来源于stack exchange,提问作者임세현

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 19:50:43