本人所有文章禁止任何形式的轉(zhuǎn)載
前言
在java 中如果一個(gè)方法拋出了一個(gè)異常,任何調(diào)用他的地方要么去try-catch,要么繼續(xù)拋出異常。
這種邏輯非常麻煩,明明只需要最開始的方法try-catch 就好了。
然后到了kotlin,檢查直接沒了。這會(huì)給我們?nèi)巧下闊?,如果漏掉了一個(gè)沒有進(jìn)行try-catch 的話。
@Throws(IOException::class)
fun throwException() = Unit
/**
* 這里應(yīng)該掠過檢查
*/
fun middle() = throwException()
/**
* 因?yàn)檫M(jìn)行了try catch,所以這里沒有錯(cuò)誤
*/
fun hello() {
try {
middle()
} catch (_: IOException) {}
}
/**
* 需要提供一個(gè)錯(cuò)誤
*/
fun test() = middle()
上面是一個(gè)示例代碼,我們后續(xù)就通過這段代碼做測試。正如doc 中所說,給middle 也提供一個(gè)錯(cuò)誤是沒有必要的,只要hello 進(jìn)行了try-catch 就能夠保證代碼是能夠正確運(yùn)行的。而test 沒有進(jìn)行try-catch,所以需要提供一個(gè)錯(cuò)誤。
所以這里我要使用Lint 檢查未處理的異常。命名為Yong,取自“庸人自擾”的“庸”。
Lint
雖然這個(gè)Lint 叫作Android Lint,但是其他java 項(xiàng)目應(yīng)該也是能用的。API 并沒有使用到Android SDK,包名都是com.android.tools.lint。我們需要兩個(gè)module, 一個(gè)是普通kotlin module,完成代碼檢查,另一個(gè)是android module,然后引入前者,再發(fā)布到一個(gè)aar 中,最終在app 模塊中使用。
基本流程;我們需要實(shí)現(xiàn)一個(gè)Detector 完成代碼檢查,在出現(xiàn)錯(cuò)誤的時(shí)候通過JavaContext#report 報(bào)告一個(gè)Issue。
interface Scanner {
fun test()
}
//所有scanner 的空實(shí)現(xiàn)
abstract class Detector {
fun test() {
println("from Detector")
}
}
class MyDetector : Detector(), Scanner {
}
Detector 只是為所有的接口提供了默認(rèn)實(shí)現(xiàn),所以說Scanner 才是真正起作用的。同時(shí)Scanner 有多個(gè),比如XmlScanner,SourceCodeScanner。我們要用的是UastScanner,它和SourceCodeScanner 沒有什么不同,只是按照doc 所說是為了兼容性,我們暫時(shí)按照demo 中的樣子也使用這個(gè)。
谷歌提供的demo 項(xiàng)目android-custom-lint-rules
Uast 意指“通用語法樹”,與PSI 不同,后者在不同語言上結(jié)構(gòu)不同,無法通用。
創(chuàng)建
-
創(chuàng)建一個(gè)checks 模塊,導(dǎo)入com.android.tools.lint 依賴。
實(shí)現(xiàn)我們的Detector,同時(shí)選擇我們的Scanner。
實(shí)現(xiàn)我們的IssueRegistry。
在com.android.tools.lint.client.api.IssueRegistry 中注冊(cè)我們的IssueRegistry。然后Issue 會(huì)注冊(cè)我們的Detector。
-
創(chuàng)建一個(gè)library Android module,導(dǎo)入checks,同時(shí)發(fā)布。
就像這樣:
dependencies { implementation project(':checks') lintPublish project(':checks') } -
然后在app module中導(dǎo)入
implementation project(':library')
實(shí)現(xiàn)
經(jīng)過上面的處理,我們可以真正的實(shí)現(xiàn)了。
Lint 并不會(huì)告訴我們語法樹的根節(jié)點(diǎn)在哪,好在我們的目標(biāo)明確---我們主要面對(duì)的是Android 項(xiàng)目,所以Android 項(xiàng)目的語法樹的根節(jié)點(diǎn)就是“四大組件“(姑且這個(gè)認(rèn)為,否則也沒有別的更好的辦法)。并且入口函數(shù)就是生命周期函數(shù),如果不是生命周期函數(shù),即使拋出了也會(huì)直接略過。
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 之后,我們需要構(gòu)建一個(gè)方法調(diào)用的樹。
樹的結(jié)構(gòu):一個(gè)root 節(jié)點(diǎn)當(dāng)作指針。第二層是Activity,第三層是Activity 的生命周期函數(shù)。后面就是方法調(diào)用的樹。
原則:
- 從根節(jié)點(diǎn)到葉節(jié)點(diǎn)同樣的方法只有一個(gè),防止遞歸調(diào)用
- 使用特殊的key 作為方法的標(biāo)識(shí),因?yàn)镻siElement 并沒有重載hashCode 方法。(file 全路徑 + class 全限定名稱 + function signature)
- method 節(jié)點(diǎn)包含:n+1 個(gè)節(jié)點(diǎn)。n = try catch 塊的數(shù)量。1 是剩余所有的內(nèi)容。包含剩余的方法。
- 強(qiáng)烈依賴@Throws,畢竟這是一個(gè)靜態(tài)代碼檢查。比如下面這樣。
fun test(i: Int) {
if (i == 0) {
throw Exception("is 0")
}
}
fun hello() {
test(1)
}
像這種情況,根本不會(huì)發(fā)生崩潰,自然也沒有必要拋出錯(cuò)誤,不處理也無所謂。所以把這個(gè)判斷的責(zé)任交給程序員,而不是一個(gè)“不太聰明的”靜態(tài)代碼檢查。不過,在沒有使用@Throws 的情況下,還是會(huì)查找對(duì)應(yīng)的throw 表達(dá)式的。
并且這種強(qiáng)烈依賴還會(huì)在遇到@Throws 時(shí)停止繼續(xù)遍歷method 中的內(nèi)容,也就是說在樹中是一個(gè)葉節(jié)點(diǎn)。
補(bǔ)充:
- @Throws 和java 中的方法后面的定義的thorw 關(guān)鍵字和throw 表達(dá)式具有同樣的作用。
- retrofit 等網(wǎng)絡(luò)請(qǐng)求庫的注解等同于@Throws(IOException)
都是些常規(guī)代碼,沒啥可講的。
可以點(diǎn)擊這里查看所有代碼
問題
當(dāng)前存在個(gè)問題,當(dāng)我遍歷method 中的function call 表達(dá)式時(shí),無法獲知他所屬的class,導(dǎo)致我不得不調(diào)用resolve 方法,按照文檔提示這個(gè)方法非常耗時(shí),所以如果你的庫比較大,可能會(huì)非常耗時(shí)。
抽象類,接口當(dāng)前還沒有處理。如果在聲明接口方法的地方使用了@Throws 是沒有問題,如果沒有,需要找到所有的實(shí)現(xiàn)類,檢查其中是否含有未捕獲的異常。
使用
發(fā)布在了*jitpack。
dependencies {
lintChecks 'com.github.storytellerF:Yong:c5bb4aae20'
}
一定要注意這里需要使用
lintChecks,否則不能使用。
然后執(zhí)行
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
實(shí)際的結(jié)果不包含短線,是空格。