背景:我司产品序列化类的成员变量需要序列化时没赋默认值,反序列化时可能会出现空指针崩溃,因此在初始化时需要给需要序列化的成员变量赋默认值,因此需要自定义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-ktlint
,在src/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抽象语法树。
可以通过命令获取一个文件的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. 扫描结果
我们在libfeed
、template/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