当前位置: 首页>后端>正文

[Android] 使用lint 检查kotlin 未捕获的异常

本人所有文章禁止任何形式的转载

前言

在java 中如果一个方法抛出了一个异常,任何调用他的地方要么去try-catch,要么继续抛出异常。
这种逻辑非常麻烦,明明只需要最开始的方法try-catch 就好了。

然后到了kotlin,检查直接没了。这会给我们惹上麻烦,如果漏掉了一个没有进行try-catch 的话。

@Throws(IOException::class)
fun throwException() = Unit

/**
  * 这里应该掠过检查
  */
fun middle() = throwException()

/**
  * 因为进行了try catch,所以这里没有错误
  */
fun hello() {
    try {
        middle()
    } catch (_: IOException) {}
}

/**
  * 需要提供一个错误
  */
fun test() = middle()

上面是一个示例代码,我们后续就通过这段代码做测试。正如doc 中所说,给middle 也提供一个错误是没有必要的,只要hello 进行了try-catch 就能够保证代码是能够正确运行的。而test 没有进行try-catch,所以需要提供一个错误。

所以这里我要使用Lint 检查未处理的异常。命名为Yong,取自“庸人自扰”的“庸”。

Lint

虽然这个Lint 叫作Android Lint,但是其他java 项目应该也是能用的。API 并没有使用到Android SDK,包名都是com.android.tools.lint。我们需要两个module, 一个是普通kotlin module,完成代码检查,另一个是android module,然后引入前者,再发布到一个aar 中,最终在app 模块中使用。

基本流程;我们需要实现一个Detector 完成代码检查,在出现错误的时候通过JavaContext#report 报告一个Issue

interface Scanner {
    fun test()
}

//所有scanner 的空实现
abstract class Detector {
    fun test() {
        println("from Detector")
    }
}

class MyDetector : Detector(), Scanner {

}

Detector 只是为所有的接口提供了默认实现,所以说Scanner 才是真正起作用的。同时Scanner 有多个,比如XmlScannerSourceCodeScanner。我们要用的是UastScanner,它和SourceCodeScanner 没有什么不同,只是按照doc 所说是为了兼容性,我们暂时按照demo 中的样子也使用这个。

谷歌提供的demo 项目android-custom-lint-rules

Uast 意指“通用语法树”,与PSI 不同,后者在不同语言上结构不同,无法通用。

创建

  1. 创建一个checks 模块,导入com.android.tools.lint 依赖。

    1. 实现我们的Detector,同时选择我们的Scanner。

    2. 实现我们的IssueRegistry

    3. com.android.tools.lint.client.api.IssueRegistry 中注册我们的IssueRegistry。然后Issue 会注册我们的Detector。

  2. 创建一个library Android module,导入checks,同时发布。

    就像这样:

    dependencies {
        implementation project(':checks')
        lintPublish project(':checks')
    }
    
  3. 然后在app module中导入

    implementation project(':library')
    

实现

经过上面的处理,我们可以真正的实现了。

Lint 并不会告诉我们语法树的根节点在哪,好在我们的目标明确---我们主要面对的是Android 项目,所以Android 项目的语法树的根节点就是“四大组件“(姑且这个认为,否则也没有别的更好的办法)。并且入口函数就是生命周期函数,如果不是生命周期函数,即使抛出了也会直接略过。

override fun getApplicableUastTypes(): List<Class<out UElement?>> {
    return listOf(UClass::class.java)
}

override fun createUastHandler(context: JavaContext): UElementHandler {
    return object : UElementHandler() {
        override fun visitClass(node: UClass) {
            context.log(null,"element-handler visitClass ${node.name}")
        }
    }
}

这样我们就能获取所有的Class,下一步就是从这些类中查找出Activity

val isActivity = node.supers.any {
    it.qualifiedName == "androidx.appcompat.app.AppCompatActivity"
}

然后获取所有的Method

node.methods.filter {
    it.findSuperMethods().isNotEmpty()
}.forEach {
    it.accept(visitor)
}
//......
val visitor = object : AbstractUastVisitor() {
    override fun visitMethod(node: UMethod): Boolean {
        context.log(null, "\tvisitMethod ${node.name}")
        return super.visitMethod(node)
    }
}

确保能够访问所有的method 之后,我们需要构建一个方法调用的树。

树的结构:一个root 节点当作指针。第二层是Activity,第三层是Activity 的生命周期函数。后面就是方法调用的树。

原则:

  • 从根节点到叶节点同样的方法只有一个,防止递归调用
  • 使用特殊的key 作为方法的标识,因为PsiElement 并没有重载hashCode 方法。(file 全路径 + class 全限定名称 + function signature)
  • method 节点包含:n+1 个节点。n = try catch 块的数量。1 是剩余所有的内容。包含剩余的方法。
  • 强烈依赖@Throws,毕竟这是一个静态代码检查。比如下面这样。
fun test(i: Int) {
    if (i == 0) {
        throw Exception("is 0")
    }
}
fun hello() {
    test(1)
}

像这种情况,根本不会发生崩溃,自然也没有必要抛出错误,不处理也无所谓。所以把这个判断的责任交给程序员,而不是一个“不太聪明的”静态代码检查。不过,在没有使用@Throws 的情况下,还是会查找对应的throw 表达式的。
并且这种强烈依赖还会在遇到@Throws 时停止继续遍历method 中的内容,也就是说在树中是一个叶节点。

补充:

  • @Throws 和java 中的方法后面的定义的thorw 关键字和throw 表达式具有同样的作用。
  • retrofit 等网络请求库的注解等同于@Throws(IOException)

都是些常规代码,没啥可讲的。
可以点击这里查看所有代码

问题

当前存在个问题,当我遍历method 中的function call 表达式时,无法获知他所属的class,导致我不得不调用resolve 方法,按照文档提示这个方法非常耗时,所以如果你的库比较大,可能会非常耗时。

抽象类,接口当前还没有处理。如果在声明接口方法的地方使用了@Throws 是没有问题,如果没有,需要找到所有的实现类,检查其中是否含有未捕获的异常。

使用

发布在了*jitpack

dependencies {
    lintChecks 'com.github.storytellerF:Yong:c5bb4aae20'
}

一定要注意这里需要使用lintChecks,否则不能使用。

然后执行

sh gradlew lint

Root
----Activity(MainActivity)
--------Method(onResume) java.io.IOException
------------Method(test) java.io.IOException
----------------Method(middle) java.io.IOException
--------------------Method(throwException) java.io.IOException
----------------Method(test) java.io.IOException
----------------Method(middle) java.io.IOException
---------------- Method(throwException) java.io.IOException
----------------Method(throwException) java.io.IOException

实际的结果不包含短线,是空格。


https://www.xamrdz.com/backend/3k31940670.html

相关文章: