如何自定义KtLint Rules

作者: 0xCAFEBABE51 | 来源:发表于2020-03-13 11:20 被阅读0次

    背景:我司产品序列化类的成员变量需要序列化时没赋默认值,反序列化时可能会出现空指针崩溃,因此在初始化时需要给需要序列化的成员变量赋默认值,因此需要自定义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):

    • jlleitschuh/ktlint-gradle
    • Gradle plugin that automatically creates check and format tasks for project Kotlin sources, supports different kotlin plugins and Gradle build caching.

    在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的例子

    • PSI会将一个类文件所有的描述事无巨细地记录下来,包括里面的空格有多少个,注释等。
    • 对应的我们就可以操作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

    相关文章

      网友评论

        本文标题:如何自定义KtLint Rules

        本文链接:https://www.haomeiwen.com/subject/fblqshtx.html