本文已同步發(fā)表到:https://glorin.xyz/2020/10/31/android_custom_lint/
問(wèn)題
Android lint 是一個(gè)靜態(tài)代碼掃描工具,sdk 本身已經(jīng)支持了一些檢查規(guī)則,但是有時(shí)候我們需要根據(jù)自己的業(yè)務(wù)或者代碼規(guī)范自定義一些檢查規(guī)則,這時(shí)候就需要用到自定義 lint 的功能,本文介紹如何自定義一個(gè) lint 檢查規(guī)則。
環(huán)境
MacOS Catalina + Android Studio 4.1 + Gralde 6.5
工程結(jié)構(gòu)
首先創(chuàng)建一個(gè) Android 工程,我將它命名為 CustomLintChecker,默認(rèn)帶有一個(gè) app 模塊,我們另外創(chuàng)建兩個(gè)模塊:
- checker 模塊,是一個(gè) Java Library
- wrapper 模塊,是一個(gè) Android Library
最終工程結(jié)構(gòu)如下:
CustomLintChecker
+ app
+ wrapper
+ checker
Checker 模塊
Gradle 配置
Checker 模塊是 lint 的檢查邏輯,首先我們?cè)?build.gradle 中引入 lint-api 及 com.android.lint 插件:
plugins {
id 'java-library'
id 'kotlin'
id 'com.android.lint'
}
...
dependencies {
compileOnly "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
def lint_version = '27.1.0'
compileOnly "com.android.tools.lint:lint-api:$lint_version"
compileOnly "com.android.tools.lint:lint-checks:$lint_version"
}
值得注意的是這邊的依賴全部用 compileOnly,否則可能會(huì)導(dǎo)致依賴沖突。
Detector
配置完 gradle 之后,我們便著手編寫我們的 lint 檢查規(guī)則,此處以一個(gè) log 檢查為示例,我們的目標(biāo)是檢測(cè)到代碼中使用了 android.util.Log 時(shí),提示用戶修改為 Timber
Detector 代碼如下,相關(guān)代碼作用見(jiàn)注釋
// 集成 Detector類,并實(shí)現(xiàn) SourceScanner 接口
class AndroidLogDetector : Detector(), SourceCodeScanner {
// 獲取感興趣的方法名,可以理解為讓 lint 規(guī)則應(yīng)用于返回的方法名列表,因?yàn)槲覀儾豢赡軝z測(cè)每一個(gè)方法,所以此處要限定范圍。
override fun getApplicableMethodNames(): List<String>? {
return listOf("wtf", "v", "d", "i", "w", "e")
}
// visitMethodCall 回調(diào),當(dāng) lint 運(yùn)行到上面的方法調(diào)用時(shí),便會(huì)調(diào)用這個(gè)方法,我們?cè)谶@里處理檢測(cè)邏輯
override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
// 判斷方法是否是 android.util.Log 的方法
if (context.evaluator.isMemberInClass(method, "android.util.Log")) {
// 報(bào)告問(wèn)題,第一個(gè)參數(shù) ISSUE 是問(wèn)題的數(shù)據(jù),在后面定義,第二個(gè)參數(shù)是要報(bào)告的位置,可以理解為代碼的位置,后面是一個(gè) Message
context.report(ISSUE, context.getLocation(node), "Don't use Android log directly")
}
}
companion object {
// 此處便是上面用到的 ISSUE,很容易理解相關(guān)參數(shù)的含義,此處不再贅述
val ISSUE = Issue.create(
"AndroidLogDeprecated",
"Please don't use android log directly, use Timber instead",
"Please don't use android log directly, use Timber instead",
Category.LINT,
5,
Severity.WARNING,
Implementation(AndroidLogDetector::class.java, Scope.JAVA_FILE_SCOPE)
)
}
}
Registry
編寫完 Detector 后,我們要對(duì)規(guī)則進(jìn)行注冊(cè),這樣 IDE 或者 gradle lint,才能加載我們的 Detector。此處我們需要一個(gè) Registry 類,代碼如下:
package xyz.glorin.customlint.checker
import com.android.tools.lint.client.api.IssueRegistry
import xyz.glorin.customlint.checker.detectors.AndroidLogDetector
class CustomRegistry : IssueRegistry() {
override val issues = listOf(
AndroidLogDetector.ISSUE
)
}
這個(gè)類非常簡(jiǎn)單,繼承了 IssueRegistry,并修改了 issues 字段,很容易理解這是在注冊(cè)這個(gè) ISSUE。
META-INF 配置
為了完成注冊(cè),我們還需要配置一個(gè) IssueRegistry 文件,這樣 lint 框架才能讀取到我們的 Registry,這個(gè)類的路徑是: checker/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry,文件內(nèi)容頁(yè)很簡(jiǎn)單:
xyz.glorin.customlint.checker.CustomRegistry
其實(shí)就是咱們的 Registry 類的完整類名。
如此我們的 checker 模塊算是完成了。
Wrapper
checker 模塊是一個(gè) Java 模塊,編譯完成后,是一個(gè) jar,傳統(tǒng)的應(yīng)用 lint 規(guī)則的方式是,把這個(gè) jar 放在 ~/.android/lint 目錄下,但是這樣十分不方便維護(hù),且會(huì)應(yīng)用到所有項(xiàng)目中,于是我們需要尋求其他方式。
好在 gradle 支持讀取 aar 中的 lint.jar 并應(yīng)用相應(yīng)規(guī)則,且在新版 gradle 中新增了 lintPublish 方法支持更快捷地打包 lint 檢測(cè)規(guī)則,我們要做的很簡(jiǎn)單,就是修改 wrapper 模塊的 build.gradle 文件,內(nèi)容如下:
...
dependencies {
lintPublish project(':checker')
}
由于這只是一個(gè)簡(jiǎn)單的 wrapper 項(xiàng)目,因此可以考慮將 dependencies 其他內(nèi)容都刪除。
App
wrapper 模塊也編寫完成,其實(shí) lint 檢測(cè)規(guī)則的工程已經(jīng)配置完成了,我們接下來(lái)就要試試效果,在 app 模塊的 build.gradle 文件中,依賴 wrapper 模塊:
implementation project(':wrapper')
然后在 MainActivity.kt 中,寫下 Log 代碼,此時(shí) lint 不一定即時(shí)生效,可以在 terminal 中,運(yùn)行一下命令:
./gradlew :app:lint
運(yùn)行完成后,會(huì)輸出 lint 檢查報(bào)告,不出意外地話,IDE 也會(huì)出現(xiàn)相應(yīng)的提示了。