[Android] 使用lint 檢查kotlin 未捕獲的異常

本人所有文章禁止任何形式的轉(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)建

  1. 創(chuàng)建一個(gè)checks 模塊,導(dǎo)入com.android.tools.lint 依賴。

    1. 實(shí)現(xiàn)我們的Detector,同時(shí)選擇我們的Scanner。

    2. 實(shí)現(xiàn)我們的IssueRegistry。

    3. com.android.tools.lint.client.api.IssueRegistry 中注冊(cè)我們的IssueRegistry。然后Issue 會(huì)注冊(cè)我們的Detector。

  2. 創(chuàng)建一個(gè)library Android module,導(dǎo)入checks,同時(shí)發(fā)布。

    就像這樣:

    dependencies {
        implementation project(':checks')
        lintPublish project(':checks')
    }
    
  3. 然后在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é)果不包含短線,是空格。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容