本人所有文章禁止任何形式的转载
前言
在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 有多个,比如XmlScanner,SourceCodeScanner。我们要用的是UastScanner
,它和SourceCodeScanner
没有什么不同,只是按照doc 所说是为了兼容性,我们暂时按照demo 中的样子也使用这个。
谷歌提供的demo 项目android-custom-lint-rules
Uast 意指“通用语法树”,与PSI 不同,后者在不同语言上结构不同,无法通用。
创建
-
创建一个checks 模块,导入com.android.tools.lint 依赖。
实现我们的Detector,同时选择我们的Scanner。
实现我们的IssueRegistry。
在com.android.tools.lint.client.api.IssueRegistry 中注册我们的IssueRegistry。然后Issue 会注册我们的Detector。
-
创建一个library Android module,导入checks,同时发布。
就像这样:
dependencies { implementation project(':checks') lintPublish project(':checks') }
-
然后在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
实际的结果不包含短线,是空格。