Android靜態代碼掃描實踐—四、自定義ktlint規則

「本文已參與好文召集令活動,點擊查看:後端、大前端雙賽道投稿,2萬元獎池等你挑戰!html

前面3篇文章,咱們介紹了靜態代碼掃描在團隊的重要性以及在實際團隊實踐中如何使用Gitlab CI/CD配合靜態代碼掃描實現讓團隊成員低感知地遵照代碼規範。而在以前咱們的實踐中僅僅是使用了 ktlint 實現了 Kotlind的官方代碼風格規範 檢查,但在實際開發過程當中,咱們還會有更多團隊中的代碼規範,如日誌打印方法的統1、每一個activity文件必需要有註釋等。
所以,做爲Android靜態代碼掃描實踐的收官文章,我將帶着你們如何使用 ktlint 寫出自定義規則。前端

ktlint加載規則的流程

雖然官方也有簡單的文檔教咱們如何自定義ktlint規則,可是我以爲先搞懂執行 ./gradlew ktlint 後如何加載規則,更有助於咱們自定義ktlint規則。node

首先先找到定義 ktlint 這個gradle任務的地方,在項目根目錄下的app目錄下的build.gradle裏面git

...
  configurations {
    ktlint
  }
  ...
  task ktlint(type: JavaExec, group: "verification") {
    description = "Check Kotlin code style."
    classpath = configurations.ktlint
    main = "com.pinterest.ktlint.Main"
    args "-a", "src/**/*.kt", "--reporter=html,output=${buildDir}/ktlint.html"
  }
  ...
  dependencies {
    ...
    ktlint("com.pinterest:ktlint:0.41.0") {
        attributes {
            attribute(Bundling.BUNDLING_ATTRIBUTE, getObjects().named(Bundling, Bundling.EXTERNAL))
        }
    }
    ...
  }
複製代碼

其中定義了一個 name 爲 ktlint 的 gradle 任務,類型爲 JavaExec,執行後將會在子進程中執行 Java 應用程序(Jar),classpath 定義了要執行的 Jar 的路徑,而 configurations.ktlint 是一個定義好的名爲 ktlint 的引用集合,在這裏面僅引用了 "com.pinterest:ktlint:0.41.0" ,後續你能夠添加本身的Jar。 main 表示要執行的 main 方法爲 com.pinterest.ktlint.Main.程序員

所以咱們能夠直接在 ktlint 的源碼看到 com.pinterest.ktlint.Main方法github

main方法

咱們執行 ./gradlew ktlint 的時候並無帶子命令,所以直接進入下一步 ktlintCommand.run() 方法。編程

ktlintCommand-run方法

其中 failOnOldRulesetProviderUsage() 是判斷使用的 Jar 是否有繼承老的規則方法,若是有,直接報錯。然後續就是咱們要找的加載規則的方法。後端

val ruleSetProviders = rulesets.loadRulesets(experimental, debug, disabledRules)
複製代碼

再進入看看loadRulesets方法api

load-RuleSetProvider

能夠看到加載了 Jar 裏全部實現 RuleSetProvider 抽象類的類,固然還有一些過濾條件,而 RuleSetProvider 抽象類 get 方法返回了一系列實現 com.pinterest.ktlint.core.Rule 抽象類的規則類, 對後面的步驟還感興趣的,你們能夠去看ktlint的源碼,這裏咱們只須要了解加載規則的流程。粗略總結以下圖:markdown

ktlint加載規則流程.png

所以咱們自定義規則就是自定義一個類實現 RuleSetProvider 抽象類,在這個類中返回自定義的規則集合,而後導出成 Jar , 而後在項目根目錄下的app目錄下的build.gradle裏面經過 ktlint 引用你的 Jar。

程序結構接口 (PSI)

上面簡單介紹了 ktlin 如何加載自定義規則,瞭解後明白咱們須要自定義一個類實現 RuleSetProvider 抽象類,在這個類中返回自定義的規則集合,而規則是一個實現 com.pinterest.ktlint.core.Rule 抽象類,在這個實現了規則的類中的 visit 抽象方法,在這個方法裏面咱們要完成識別不符合規範的代碼塊並輸出警告提醒文本的功能,而該抽象方法的 ASTNode 參數就是咱們識別代碼塊的關鍵。

ASTNode 是 JetBrains 對於旗下 IDE 的抽象語法樹(Abstract Syntax Tree,AST)的實現 -- PSI(程序結構接口)其中的一個類。以樹狀的形式表現編程語言,將咱們程序員所編寫的源代碼語法結構進行抽象表示。能夠理解爲 PSI 將程序員編寫的代碼轉換爲方便進行代碼語法分析的樹狀結構代碼。

而咱們能夠使用 PsiViewer插件 來直觀的查看經過 PSI 生成的樹狀結構,下面兩張圖能夠直觀的看出該插件的使用以及樹狀結構的展現:

PSI示例1.png

PSI示例2.png

實現你的第一個自定義 ktlint 規則

「Talk is cheap. Show me the code」. 所以這裏我用一個自定義的 ktlint 規則 -- 不可直接繼承 Activity() , 必須繼承 BaseActivity 的實現當示例,但願你們能從中瞭解如何實現自定義規則,示例代碼Github.爲方便調試,下面示例是在一個可用的 Android 項目下進行,這樣方便咱們調試,完成開發後,能夠遷移到一個獨立的 kotlin 項目,方便分發使用,如示例代碼Github.

建立自定義 ktlint 規則模塊

  • 在項目根文件夾中與app模塊處於同一文件夾級別建立一個單獨的模塊,這裏我將模塊命名爲 custom_rules ;

新建rules模塊.png

  • 將新建模塊下的 build.gradle 文件修改以下,其中依賴的 kotlin 版本號要與項目根目錄的 build.gradle 文件的版本一致:
plugins {
    id 'kotlin'
}

compileKotlin {
    kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
    kotlinOptions.jvmTarget = "1.8"
}

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib:1.5.20"
    compileOnly "com.pinterest.ktlint:ktlint-core:0.41.0"
}
複製代碼
  • 告訴 ktlint 查找到咱們實現了 RuleSetProvider 的類.在新建模塊下的 src->main 下面新建文件夾 resources/META-INF/services,而且在該目錄下新建 com.pinterest.ktlint.core.RuleSetProvider 文件,在文件中添加
com.tc.custom_rules.CustomRuleSetProvider
複製代碼

這時候咱們的文件目錄如圖:

自定義ktlint規則step1menu.png

新建規則類實現規則

package com.tc.custom_rules

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType
import com.pinterest.ktlint.core.ast.children
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes

class ExtendBaseRule : Rule("kclass-extend-base-rules") {
    override fun visit( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit ) {
        if (node.elementType == KtStubElementTypes.CLASS) {
            println("使用調試打印日誌:${node.text}")
            //修飾符爲 class 的ASTNode
            var isExtendActivity = false
            //判斷該class是否繼承了Activity
            for (childNode in node.children()) {
                if (childNode.elementType == KtStubElementTypes.SUPER_TYPE_LIST) {
                    //psi中繼承與實現的類
                    for (minChild in childNode.children()) {
                        if (minChild.elementType == KtStubElementTypes.SUPER_TYPE_CALL_ENTRY) {
                            //psi中繼承的類,判斷繼承的ASTNode的文本
                            if (minChild.text == "Activity()") {
                                isExtendActivity = true
                            }
                            break
                        }
                    }
                }
            }
            if (isExtendActivity) {
                //該class是繼承了Activity,再判斷是否是BaseActivity
                for (childNode in node.children()) {
                    if (childNode.elementType == ElementType.IDENTIFIER) {
                        //第一個標識符,是類名
                        if (isExtendActivity && childNode.text != "BaseActivity") {
                            //該class是繼承了Activity,也不是BaseActivity,所以輸出錯誤
                            emit(
                                childNode.startOffset,
                                "Activity請繼承BaseActivity!",
                                false
                            )
                            break
                        }
                        break
                    }
                }
            }
        }
    }
}
複製代碼
  • CustomRuleSetProvider 類實現 RuleSetProvider ,返回上面定義的規則
package com.tc.custom_rules

import com.pinterest.ktlint.core.RuleSet
import com.pinterest.ktlint.core.RuleSetProvider

class CustomRuleSetProvider : RuleSetProvider {
    override fun get(): RuleSet = RuleSet(
        "custom-rule-set",
        ExtendBaseRule()
    )
}
複製代碼

與 ktlint 共同使用

  • app 模塊的 build.gradle 依賴模塊
...
dependencies {
  ...
  ktlint project(':custom_rules')
}

複製代碼
  • 而後終端執行 ./gradlew ktlint ,能夠看到咱們自定義的規則已經產生做用

執行ktlint日誌.png

ktlint報告.png

導出自定義規則的 jar

實際實踐中,咱們並不可能每次有新項目配置規則的時候都添加一個自定義規則模塊,所以咱們須要把自定義規則模塊導出成 jar ,方便 Android 項目引用。

你能夠在剛纔的自定義規則模塊基礎上執行

./gradlew :custom_rules:build
複製代碼

或者把剛纔的自定義模塊獨立成一個 kotlin 項目,執行

./gradlew build
複製代碼

能夠在 build->lib 中看到構建出的 jar , 以後就能夠發佈到 Maven 倉庫了。

總結

講解了如何實現自定義規則,基於 ktlint 和 Gitlab CI/CD 的團隊靜態代碼規範實踐這個系列基本上也完結了。
若是個人文章對你有幫助或啓發,辛苦大佬們點個贊👍🏻,支持我一下。
若是有錯漏,歡迎大佬們指正,也歡迎你們一塊兒討論,感謝。

參考文檔

Gradle 參考文檔
Writing your first ktlint rule -- Niklas Baudy
IDEA 程序結構接口 (PSI) 官方參考文檔
自定義規則示例代碼Github

相關文章
相關標籤/搜索