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,提问作者임세현
相关产品推荐
相关产品推荐

