Android开发Android开发经验谈Android技术知识

如何自定义KtLint Rules

2020-03-13  本文已影响0人  0xCAFEBABE51

背景:我司产品序列化类的成员变量需要序列化时没赋默认值,反序列化时可能会出现空指针崩溃,因此在初始化时需要给需要序列化的成员变量赋默认值,因此需要自定义KtLint Rules去在打包前扫描代码,发现没有给序列化成员变量赋默认值的代码报错提示开发人员处理。

1. KtLint是什么

KtLint就是Kotlin版的Lint检查Kotlin代码的规范。
通过如下代码安装,可以在命令行打印某个文件的抽象语法树等功能,后面我们需要用到。

curl -sSLO https://github.com/pinterest/ktlint/releases/download/0.36.0/ktlint &&
  chmod a+x ktlint &&
  sudo mv ktlint /usr/local/bin/

2. 怎么引入自己写的KtLint

刚开始想使用不引入插件的方式去使用自定义Rules,但是发现Gradle找不到ktlint方法,搜索了一番,说有可能是自定义了一个ktlint任务导致重名找不到,尝试重命名ktlint任务为ktlint2任务,还是找不到这个方法,再猜想可能是Gradle版本过低,升级了一下发现也是没有,Google搜索了一下也没说哪个Plugin提供ktlint方法,因此就使用了Pinterest推荐的使用第三方插件实现。

使用pinterrest推荐的第三方Gradle插件,有两个推荐,我用的是jlleitschuh/ktlint-gradle
with Gradle
(with a plugin - Recommended)

Gradle plugins (in order of appearance):

在root build.gradle加入配置

buildscript {
    dependencies {
        classpath "org.jlleitschuh.gradle:ktlint-gradle:9.1.1"
    }

    repositories {
       // org.jlleitschuh.gradle:ktlint-gradle:9.1.1的仓库
       maven {
            url "https://plugins.gradle.org/m2/"
       }
    }
}

使用这个插件需要把Gradle插件升级到 >= 3.5.3,分发版本 >= 5.4.1

// root build.gradle
classpath 'com.android.tools.build:gradle:3.5.3'

// gradle-wapper.properties
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip

在根build.gradle的allProject下加上如下配置

// 在allProject下,表明所有模块都需要加载这个ktlint配置
ktlint {
        version = "0.36.0" //pinterrest.ktlint标准插件版本
        debug = true
        verbose = true
        android = true // 是否为android平台
        outputToConsole = true
        outputColorName = "RED"
        ignoreFailures = true
        enableExperimentalRules = false // 是否开启实验性规则
    // additionalEditorconfigFile = file("/some/additional/.editorconfig")
        // 禁用规则,ktlint报错时会附带错误规则,不需要的加在这里就可以
        disabledRules = ["import-ordering", "max-line-length", "parameter-list-wrapping"]
        reporters {
            reporter "plain"
            reporter "checkstyle"
      // 这里可以自定义reporter的位置
//        customReporters {
//            "csv" {
//                fileExtension = "csv"
//                dependency = project(":project-reporters:csv-reporter")
//            }
//            "yaml" {
//                fileExtension = "yml"
//                dependency = "com.example:ktlint-yaml-reporter:1.0.0"
//            }
//        }
        }
        kotlinScriptAdditionalPaths {
            include fileTree("scripts/")
        }
        filter {
            exclude("**/generated/**")
            include("**/kotlin/**")
            include("**/java/**")
        }
    }

当上面的配置加完以后,可以新建一个custom-ktlint模块来编写自己的规则,然后通过如下代码在其他模块引用自己写的KtLint规则,ktlintRuleset就是jlleitschuh/ktlint-gradle提供的方法。

ktlintRuleset project(":custom-ktlint")

3. 自己定义的规则怎么写

需要自己编写KtLint检测规则就需要用到PSI,PSI是Program Structure Interface(编程结构接口)的缩写
首先新建一个module,命名为custom-ktlintsrc/main/java/packageName下新建一个规则代码继承com.pinterest.ktlint.core.Rule,需要实现父类的visit接口:

override fun visit(
    node: ASTNode,
    autoCorrect: Boolean,
    emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
)

新建一个继承com.pinterest.ktlint.core.RuleSetProvider的类,这个是为了提供自定义规则的提供器,插件会扫描到Provider提供的规则代码,执行其中的visit方法,这样我们就可以使用我们自己的自定义规则。如下所示:

package com.vega.ktlint

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

class VegaRuleSetProvider : RuleSetProvider {
    override fun get(): RuleSet = RuleSet(
        "vega-ktlint-rules",
    // 可以提供多个规则,现在只加了一个需要序列化的成员变量没有赋默认值的规则。
        KtSerializationDefaultValueRule()
    )
}

src/main/resources下,新建一个META-INF/services/com.pinterest.ktlint.core.RuleSetProvider纯文本文件,在里面把我们的Provider的全限定名写在里面,插件就可以扫描到我们的Provider了,文件内容如下:

com.vega.ktlint.VegaRuleSetProvider

4. PSI是什么

PSI是Program Structure Interface(编程结构接口)的缩写,其实是JetBrain自己内部定义的一种结构,用于存储AST抽象语法树

图4-1:add函数与代表它的抽象语法树 图4-2:add函数对应的PSI描述 图4-3:KtBlockExpression的子树

可以通过命令获取一个文件的PSI描述

ktlint libveapi/src/main/java/com/vega/ve/api/data.kt --print-ast >> ~/Desktop/data_kt_ast.txt

5. 一个具体描述PSI的例子

FeedItem这个类的PSI描述:

~.psi.KtClass (CLASS)
 87:     ~.psi.KtDeclarationModifierList (MODIFIER_LIST)                //6treePrev,找到了类的修饰符列表,
 87:       ~.psi.KtAnnotationEntry (ANNOTATION_ENTRY)           //Kotlin Serialization是通过注解来描述这个类可以序列化
 87:         ~.c.i.p.impl.source.tree.LeafPsiElement (AT) "@"       //注解需要通过修饰符列表来找到,看第五节代码是怎么获取修饰符列表节点的
 87:         ~.psi.KtConstructorCalleeExpression (CONSTRUCTOR_CALLEE)
 87:           ~.psi.KtTypeReference (TYPE_REFERENCE)
 87:             ~.psi.KtUserType (USER_TYPE)
 87:               ~.psi.KtNameReferenceExpression (REFERENCE_EXPRESSION)
 87:                 ~.c.i.p.impl.source.tree.LeafPsiElement (IDENTIFIER) "Keep"
 87:       ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) "\n"
 88:       ~.c.i.p.impl.source.tree.LeafPsiElement (DATA_KEYWORD) "data"
 88:     ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "                   //5treePrev
 88:     ~.c.i.p.impl.source.tree.LeafPsiElement (CLASS_KEYWORD) "class"                //4treePrev
 88:     ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "                   //3treePrev
 88:     ~.c.i.p.impl.source.tree.LeafPsiElement (IDENTIFIER) "Author"                  //2treePrev
 88:     ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "                   //1treePrev
 88:     ~.psi.KtPrimaryConstructor (PRIMARY_CONSTRUCTOR)               //关键,找的是构造函数
 88:       ~.c.i.p.impl.source.tree.LeafPsiElement (CONSTRUCTOR_KEYWORD) "constructor"
 88:       ~.psi.KtParameterList (VALUE_PARAMETER_LIST)
 88:         ~.c.i.p.impl.source.tree.LeafPsiElement (LPAR) "("             //构造函数的左括号
 88:         ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) "\n    "
 89:         ~.psi.KtParameter (VALUE_PARAMETER)                    //参数
 89:           ~.psi.KtDeclarationModifierList (MODIFIER_LIST)
 89:             ~.psi.KtAnnotationEntry (ANNOTATION_ENTRY)
 89:               ~.c.i.p.impl.source.tree.LeafPsiElement (AT) "@"
 89:               ~.psi.KtConstructorCalleeExpression (CONSTRUCTOR_CALLEE)
 89:                 ~.psi.KtTypeReference (TYPE_REFERENCE)
 89:                   ~.psi.KtUserType (USER_TYPE)
 89:                     ~.psi.KtNameReferenceExpression (REFERENCE_EXPRESSION)
 89:                       ~.c.i.p.impl.source.tree.LeafPsiElement (IDENTIFIER) "SerializedName" // 注意看这里的层级关系,代码中会获取成员变量是否带有SerializedName注解
 89:               ~.psi.KtValueArgumentList (VALUE_ARGUMENT_LIST)
 89:                 ~.c.i.p.impl.source.tree.LeafPsiElement (LPAR) "("
 89:                 ~.psi.KtValueArgument (VALUE_ARGUMENT)
 89:                   ~.psi.KtStringTemplateExpression (STRING_TEMPLATE)
 89:                     ~.c.i.p.impl.source.tree.LeafPsiElement (OPEN_QUOTE) """
 89:                     ~.psi.KtLiteralStringTemplateEntry (LITERAL_STRING_TEMPLATE_ENTRY)
 89:                       ~.c.i.p.impl.source.tree.LeafPsiElement (REGULAR_STRING_PART) "uid"
 89:                     ~.c.i.p.impl.source.tree.LeafPsiElement (CLOSING_QUOTE) """
 89:                 ~.c.i.p.impl.source.tree.LeafPsiElement (RPAR) ")"
 89:           ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) "\n    "
 90:           ~.c.i.p.impl.source.tree.LeafPsiElement (VAL_KEYWORD) "val"
 90:           ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "
 90:           ~.c.i.p.impl.source.tree.LeafPsiElement (IDENTIFIER) "uid"
 90:           ~.c.i.p.impl.source.tree.LeafPsiElement (COLON) ":"
 90:           ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "
 90:           ~.psi.KtTypeReference (TYPE_REFERENCE)
 90:             ~.psi.KtUserType (USER_TYPE)
 90:               ~.psi.KtNameReferenceExpression (REFERENCE_EXPRESSION)
 90:                 ~.c.i.p.impl.source.tree.LeafPsiElement (IDENTIFIER) "Long"
 90:           ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "
 90:           ~.c.i.p.impl.source.tree.LeafPsiElement (EQ) "="
 90:           ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "
 90:           ~.psi.KtNameReferenceExpression (REFERENCE_EXPRESSION)
 90:             ~.c.i.p.impl.source.tree.LeafPsiElement (IDENTIFIER) "INVALID_ID"
 90:         ~.c.i.p.impl.source.tree.LeafPsiElement (COMMA) ","
 90:         ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) "\n    "
 91:         ~.psi.KtParameter (VALUE_PARAMETER)                    // 下一个参数
 91:           
 93:         ...中间省略一节VALUE_PARAMETER的声明,和上面的一样...
 97:         
 99:         ~.c.i.p.impl.source.tree.LeafPsiElement (RPAR) ")"     //这里是构造函数的右括号,构造函数PSI的叶子节点,也是结束点了
 99:     ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "       //空格字符,1次treeNext
 99:     ~.c.i.p.impl.source.tree.LeafPsiElement (COLON) ":"            //空格前面是冒号,Kotlin用冒号代表继承实现,2次treeNext
 99:     ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "       //空格字符,3次treeNext
 99:     ~.psi.KtSuperTypeList (SUPER_TYPE_LIST)                //类的超类列表(父类,接口等),4次treeNext
 99:       ~.psi.KtSuperTypeEntry (SUPER_TYPE_ENTRY)                //看第五节代码是怎么获取超类列表节点的
 99:         ~.psi.KtTypeReference (TYPE_REFERENCE)
 99:           ~.psi.KtUserType (USER_TYPE)
 99:             ~.psi.KtNameReferenceExpression (REFERENCE_EXPRESSION)
 99:               ~.c.i.p.impl.source.tree.LeafPsiElement (IDENTIFIER) "Serializable"
 99:     ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "
 99:     ~.psi.KtClassBody (CLASS_BODY)             //类的Body,其实我们还有一种情况是序列化的成员变量声明在类Body里还没处理
 99:       ~.c.i.p.impl.source.tree.LeafPsiElement (LBRACE) "{"
 99:       ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) "\n    "
100:       ~.psi.KtObjectDeclaration (OBJECT_DECLARATION)
100:         ~.psi.KtDeclarationModifierList (MODIFIER_LIST)
100:           ~.c.i.p.impl.source.tree.LeafPsiElement (COMPANION_KEYWORD) "companion"
100:         ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "
100:         ~.c.i.p.impl.source.tree.LeafPsiElement (OBJECT_KEYWORD) "object"
100:         ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "
100:         ~.psi.KtClassBody (CLASS_BODY)
100:           ~.c.i.p.impl.source.tree.LeafPsiElement (LBRACE) "{"
100:           ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) "\n        "
101:           ~.psi.KtProperty (PROPERTY)
101:             ~.c.i.p.impl.source.tree.LeafPsiElement (VAL_KEYWORD) "val"
101:             ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "
101:             ~.c.i.p.impl.source.tree.LeafPsiElement (IDENTIFIER) "EmptyAuthor"
101:             ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "
101:             ~.c.i.p.impl.source.tree.LeafPsiElement (EQ) "="
101:             ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "
101:             ~.psi.KtCallExpression (CALL_EXPRESSION)
101:               ~.psi.KtNameReferenceExpression (REFERENCE_EXPRESSION)
101:                 ~.c.i.p.impl.source.tree.LeafPsiElement (IDENTIFIER) "Author"
101:               ~.psi.KtValueArgumentList (VALUE_ARGUMENT_LIST)
101:                 ~.c.i.p.impl.source.tree.LeafPsiElement (LPAR) "("
101:                 ~.psi.KtValueArgument (VALUE_ARGUMENT)
101:                   ~.psi.KtValueArgumentName (VALUE_ARGUMENT_NAME)
101:                     ~.psi.KtNameReferenceExpression (REFERENCE_EXPRESSION)
101:                       ~.c.i.p.impl.source.tree.LeafPsiElement (IDENTIFIER) "isAuthor"
101:                   ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "
101:                   ~.c.i.p.impl.source.tree.LeafPsiElement (EQ) "="
101:                   ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "
101:                   ~.psi.KtConstantExpression (BOOLEAN_CONSTANT)
101:                     ~.c.i.p.impl.source.tree.LeafPsiElement (FALSE_KEYWORD) "false"
101:                 ~.c.i.p.impl.source.tree.LeafPsiElement (RPAR) ")"
101:           ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) "\n    "
102:           ~.c.i.p.impl.source.tree.LeafPsiElement (RBRACE) "}"
102:       ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) "\n\n    "
104:       ~.psi.KtNamedFunction (FUN)
104:         ~.c.i.p.impl.source.tree.LeafPsiElement (FUN_KEYWORD) "fun"
104:         ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "
104:         ~.c.i.p.impl.source.tree.LeafPsiElement (IDENTIFIER) "isIllegal"
104:         ~.psi.KtParameterList (VALUE_PARAMETER_LIST)
104:           ~.c.i.p.impl.source.tree.LeafPsiElement (LPAR) "("
104:           ~.c.i.p.impl.source.tree.LeafPsiElement (RPAR) ")"
104:         ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "
104:         ~.c.i.p.impl.source.tree.LeafPsiElement (EQ) "="
104:         ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "
104:         ~.psi.KtBinaryExpression (BINARY_EXPRESSION)
104:           ~.psi.KtThisExpression (THIS_EXPRESSION)
104:             ~.psi.KtNameReferenceExpression (REFERENCE_EXPRESSION)
104:               ~.c.i.p.impl.source.tree.LeafPsiElement (THIS_KEYWORD) "this"
104:           ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "
104:           ~.psi.KtOperationReferenceExpression (OPERATION_REFERENCE)
104:             ~.c.i.p.impl.source.tree.LeafPsiElement (EQEQ) "=="
104:           ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " "
104:           ~.psi.KtNameReferenceExpression (REFERENCE_EXPRESSION)
104:             ~.c.i.p.impl.source.tree.LeafPsiElement (IDENTIFIER) "EmptyAuthor"
104:       ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) "\n"
105:       ~.c.i.p.impl.source.tree.LeafPsiElement (RBRACE) "}"
105:   ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) "\n\n"

6. 序列化数据类成员变量必须有默认值的规则

package com.vega.ktlint

import com.pinterest.ktlint.core.Rule
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.org.jline.utils.Log
import org.jetbrains.kotlin.psi.KtModifierList
import org.jetbrains.kotlin.psi.KtPrimaryConstructor
import org.jetbrains.kotlin.psi.KtSuperTypeList
import org.jetbrains.kotlin.psi.psiUtil.startOffset
import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes

class KtSerializationDefaultValueRule : Rule("kt-serialization-default-value") {
    override fun visit(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        if (node.elementType == KtStubElementTypes.PRIMARY_CONSTRUCTOR) {
            var hasSerializableAnnotation = false

            // 6MODIFIER_LIST.5WHITESPACE.4CLASS_KEYWORD.3WHITESPACE.2IDENTIFIER.1WHITESPACE.PRIMARY_CONSTRUCTOR
            val modifierListNode = node.treePrev?.treePrev?.treePrev?.treePrev?.treePrev?.treePrev
            //PRIMARY_CONSTRUCTOR.1WHITESPACE.2COLON.3WHITESPACE.4SUPER_TYPE_LIST
            val superTypeListNode = node.treeNext?.treeNext?.treeNext?.treeNext
            if (modifierListNode?.elementType == KtStubElementTypes.MODIFIER_LIST) {
                val modifierListPsi = modifierListNode?.psi as KtModifierList
                val annotationEntries = modifierListPsi.annotationEntries

                // 找出是否为序列化类,有@Serializable注解 或者继承Serializable的都是
                if (annotationEntries.isNotEmpty()) {
                    for (annotationEntry in annotationEntries) {
                        if (annotationEntry.text == "@Serializable") {
                            hasSerializableAnnotation = true
                            break
                        }
                    }
                }
            }

            // 找出继承Serializable的类,并且字段有SerializedName修饰的字段,如果没有默认值,那么就是GSON序列化类没有赋值,错误
            if (hasSerializableAnnotation.not() && superTypeListNode?.elementType == KtStubElementTypes.SUPER_TYPE_LIST) {
                val superTypeListPsi = superTypeListNode?.psi as? KtSuperTypeList
                if (superTypeListPsi != null) {
                    for (superTypeEntry in superTypeListPsi.entries) {
                        val identifier =
         superTypeEntry.typeAsUserType?.referenceExpression?.getIdentifier()
                                ?.text
                        if (identifier == "Serializable") {
                            hasSerializableAnnotation = true
                        }
                    }
                }
            }

            if (hasSerializableAnnotation) {
                val ktPrimaryConstructor = node.psi as? KtPrimaryConstructor
                val valueParamList =
                    ktPrimaryConstructor?.valueParameterList?.parameters
                if (valueParamList != null) {
                    for (param in valueParamList) {
                        val fieldAnnotationEntries = param.modifierList?.annotationEntries
                        if (fieldAnnotationEntries?.isNotEmpty() == true) {
                            var hasSerialNameAnnotation = false

                            for (fieldAnnoEntry in fieldAnnotationEntries) {
                                val fieldAnnoTextIdentifier =
                                    fieldAnnoEntry.calleeExpression?.typeReference?.text
                                // Kotlin Serialization
                                if (fieldAnnoTextIdentifier == "SerialName" ||
                                    // GSON Serialization
                                    fieldAnnoTextIdentifier == "SerializedName"
                                ) {
                                    hasSerialNameAnnotation = true
                                    break
                                }
                            }

                            if (hasSerialNameAnnotation && param.equalsToken == null) {
                                emit(
                                    param.startOffset,
                                    "constructor value param must has default value",
                                    false
                                )
                            }
                        } else {
                            Log.info("fieldModifierList.isEmpty")
                        }
                    }
                } else {
                    // Log.info("2visit valueParamList == null, className=${node.treePrev?.text} do not scan")
                }
            }
        }
    }
}

7. 扫描结果

我们在libfeedtemplate/draft模块下加了ktlintRuleset project(":custom-ktlint"),在终端下执行如下Gradle任务:

 ./gradlew ktlintCheck

得到结果如下,可以看到确实扫描出了FeedCategoryItem的需要序列化的参数没有带默认值。

检测结果

8. CI集成(TODO)

把原有的ktlint任务改成ktlintCheck任务,report的位置也需要对应改下:

"plain" report written to /Users/laizuling/Develop/vega-backup/libfeed/build/reports/ktlint/ktlintMainSourceSetCheck.txt
"checkstyle" report written to /Users/laizuling/Develop/vega-backup/libfeed/build/reports/ktlint/ktlintMainSourceSetCheck.xml

9. 参考链接

Make Your Code Clean with Ktlint
Pinterest KtLint GitHub
IntelliJ Platform SDK

上一篇下一篇

猜你喜欢

热点阅读