ktlintとは
ktlintはkotlinの静的コード解析ツールです。
同じようなものに、detektというものもあります。
今回は、ktlintについて書いていきます。
ktlintはkotlinのコードにlintをかけるだけでなく、lintで検出した部分にフォーマットをかけることもできます。
今回は、ktlintを使って独自のlintルールを追加する方法を書いていきます。
独自ルールの追加
ktlintには、デフォルトでいくつかのルールが既に定義されています。
そのため、独自のルールを追加しなくても、ある程度のlintはかけることができます。
しかし、チーム内で決めたkotlinの書き方などを統一したいことがあります。そのような場合には、独自のlintルールを定義することによって、lintチェックすることができます。
ルールの定義
ktlintが提供しているRuleというインターフェイスを実装することによって、独自のルールを定義していきます。
class YourCustomRule : Rule("your-custom-rule") {
override fun visit(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
) {
// write your own rule
}
}
独自のルールを定義していくにはnode: ASTNode
がなにかを理解している必要があります。
AST
ASTとは、AbstractSyntaxTreeの略で、ktlin以外にも様々な言語で出てくる概念です。
全ての記述はこのASTNodeの木構造として表すことができます。
例えば、↓の関数は KtNamedFunction
というASTNodeになります。
fun add(a: Int, b: Int): Int {
return a + b
}
また、更に、そのNodeを細かく分解することができます。
このようにして、全ての記述を木構造のように取り扱うことができます。
Node | Node名 |
---|---|
fun | KtKeywordToken.fun |
add | KtToken.IDENTIFIER |
(a: Int, b: Int) | KtParameterList |
: | KtSingleValueToken.COLON |
Int | KtTypeReference |
{ return a + b } | KtBlockExpression |
Node | Node名 |
---|---|
{ | KtSingleValueToken.LBRACE |
return a + b | KtReturnExpression |
} | KtSingleValueToken.RBRACE |
Node | Node名 |
---|---|
return | KtKeywordToken.return |
a | KtToken.IDENTIFIER |
+ | KtSingleValueToken.PLUS |
b | KtToken.IDENTIFIER |
それぞれがどのASTノードに当たるかは、--print-ast
のオプションで調べることができます。
package com.takusemba
fun add(a: Int, b: Int): Int {
return a + b
}
ktlint **/Hoge.kt --print-ast
1: ~.psi.KtFile (~.psi.stubs.elements.KtFileElementType.kotlin.FILE)
1: ~.psi.KtPackageDirective (~.psi.stubs.elements.KtPlaceHolderStubElementType.PACKAGE_DIRECTIVE)
1: ~.c.i.p.impl.source.tree.LeafPsiElement (~.lexer.KtKeywordToken.package) "package"
1: ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (~.c.i.p.tree.IElementType.WHITE_SPACE) " "
1: ~.psi.KtDotQualifiedExpression (~.psi.stubs.elements.KtDotQualifiedExpressionElementType.DOT_QUALIFIED_EXPRESSION)
1: ~.psi.KtNameReferenceExpression (~.psi.stubs.elements.KtNameReferenceExpressionElementType.REFERENCE_EXPRESSION)
1: ~.c.i.p.impl.source.tree.LeafPsiElement (~.lexer.KtToken.IDENTIFIER) "com"
1: ~.c.i.p.impl.source.tree.LeafPsiElement (~.lexer.KtSingleValueToken.DOT) "."
1: ~.psi.KtNameReferenceExpression (~.psi.stubs.elements.KtNameReferenceExpressionElementType.REFERENCE_EXPRESSION)
1: ~.c.i.p.impl.source.tree.LeafPsiElement (~.lexer.KtToken.IDENTIFIER) "takusemba"
1: ~.psi.KtImportList (~.psi.stubs.elements.KtPlaceHolderStubElementType.IMPORT_LIST) ""
1: ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (~.c.i.p.tree.IElementType.WHITE_SPACE) "\n\n"
3: ~.psi.KtNamedFunction (~.psi.stubs.elements.KtFunctionElementType.FUN)
3: ~.c.i.p.impl.source.tree.LeafPsiElement (~.lexer.KtKeywordToken.fun) "fun"
3: ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (~.c.i.p.tree.IElementType.WHITE_SPACE) " "
3: ~.c.i.p.impl.source.tree.LeafPsiElement (~.lexer.KtToken.IDENTIFIER) "add"
3: ~.psi.KtParameterList (~.psi.stubs.elements.KtPlaceHolderStubElementType.VALUE_PARAMETER_LIST)
3: ~.c.i.p.impl.source.tree.LeafPsiElement (~.lexer.KtSingleValueToken.LPAR) "("
3: ~.psi.KtParameter (~.psi.stubs.elements.KtParameterElementType.VALUE_PARAMETER)
3: ~.c.i.p.impl.source.tree.LeafPsiElement (~.lexer.KtToken.IDENTIFIER) "a"
3: ~.c.i.p.impl.source.tree.LeafPsiElement (~.lexer.KtSingleValueToken.COLON) ":"
3: ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (~.c.i.p.tree.IElementType.WHITE_SPACE) " "
3: ~.psi.KtTypeReference (~.psi.stubs.elements.KtPlaceHolderStubElementType.TYPE_REFERENCE)
3: ~.psi.KtUserType (~.psi.stubs.elements.KtUserTypeElementType.USER_TYPE)
3: ~.psi.KtNameReferenceExpression (~.psi.stubs.elements.KtNameReferenceExpressionElementType.REFERENCE_EXPRESSION)
3: ~.c.i.p.impl.source.tree.LeafPsiElement (~.lexer.KtToken.IDENTIFIER) "Int"
3: ~.c.i.p.impl.source.tree.LeafPsiElement (~.lexer.KtSingleValueToken.COMMA) ","
3: ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (~.c.i.p.tree.IElementType.WHITE_SPACE) " "
3: ~.psi.KtParameter (~.psi.stubs.elements.KtParameterElementType.VALUE_PARAMETER)
3: ~.c.i.p.impl.source.tree.LeafPsiElement (~.lexer.KtToken.IDENTIFIER) "b"
3: ~.c.i.p.impl.source.tree.LeafPsiElement (~.lexer.KtSingleValueToken.COLON) ":"
3: ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (~.c.i.p.tree.IElementType.WHITE_SPACE) " "
3: ~.psi.KtTypeReference (~.psi.stubs.elements.KtPlaceHolderStubElementType.TYPE_REFERENCE)
3: ~.psi.KtUserType (~.psi.stubs.elements.KtUserTypeElementType.USER_TYPE)
3: ~.psi.KtNameReferenceExpression (~.psi.stubs.elements.KtNameReferenceExpressionElementType.REFERENCE_EXPRESSION)
3: ~.c.i.p.impl.source.tree.LeafPsiElement (~.lexer.KtToken.IDENTIFIER) "Int"
3: ~.c.i.p.impl.source.tree.LeafPsiElement (~.lexer.KtSingleValueToken.RPAR) ")"
3: ~.c.i.p.impl.source.tree.LeafPsiElement (~.lexer.KtSingleValueToken.COLON) ":"
3: ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (~.c.i.p.tree.IElementType.WHITE_SPACE) " "
3: ~.psi.KtTypeReference (~.psi.stubs.elements.KtPlaceHolderStubElementType.TYPE_REFERENCE)
3: ~.psi.KtUserType (~.psi.stubs.elements.KtUserTypeElementType.USER_TYPE)
3: ~.psi.KtNameReferenceExpression (~.psi.stubs.elements.KtNameReferenceExpressionElementType.REFERENCE_EXPRESSION)
3: ~.c.i.p.impl.source.tree.LeafPsiElement (~.lexer.KtToken.IDENTIFIER) "Int"
3: ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (~.c.i.p.tree.IElementType.WHITE_SPACE) " "
3: ~.psi.KtBlockExpression (~.KtNodeType.BLOCK)
3: ~.c.i.p.impl.source.tree.LeafPsiElement (~.lexer.KtSingleValueToken.LBRACE) "{"
3: ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (~.c.i.p.tree.IElementType.WHITE_SPACE) "\n "
4: ~.psi.KtReturnExpression (~.KtNodeType.RETURN)
4: ~.c.i.p.impl.source.tree.LeafPsiElement (~.lexer.KtKeywordToken.return) "return"
4: ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (~.c.i.p.tree.IElementType.WHITE_SPACE) " "
4: ~.psi.KtBinaryExpression (~.KtNodeType.BINARY_EXPRESSION)
4: ~.psi.KtNameReferenceExpression (~.psi.stubs.elements.KtNameReferenceExpressionElementType.REFERENCE_EXPRESSION)
4: ~.c.i.p.impl.source.tree.LeafPsiElement (~.lexer.KtToken.IDENTIFIER) "a"
4: ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (~.c.i.p.tree.IElementType.WHITE_SPACE) " "
4: ~.psi.KtOperationReferenceExpression (~.KtNodeType.OPERATION_REFERENCE)
4: ~.c.i.p.impl.source.tree.LeafPsiElement (~.lexer.KtSingleValueToken.PLUS) "+"
4: ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (~.c.i.p.tree.IElementType.WHITE_SPACE) " "
4: ~.psi.KtNameReferenceExpression (~.psi.stubs.elements.KtNameReferenceExpressionElementType.REFERENCE_EXPRESSION)
4: ~.c.i.p.impl.source.tree.LeafPsiElement (~.lexer.KtToken.IDENTIFIER) "b"
4: ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (~.c.i.p.tree.IElementType.WHITE_SPACE) "\n"
5: ~.c.i.p.impl.source.tree.LeafPsiElement (~.lexer.KtSingleValueToken.RBRACE) "}"
format: <line_number:> <node.psi::class> (<node.elementType>) "<node.text>"
legend: ~ = org.jetbrains.kotlin, c.i.p = com.intellij.psi
実際に追加する
では、実際にktlintを使って独自のlintを追加するまでをやってみたいと思います。
今回は、一行のEqualで書いた関数(Expression Function)には、戻り値をつけるというLintルールを定義してみたいと思います。
NG
fun add(a: Int, b: Int) = a + b
OK
fun add(a: Int, b: Int): Int = a + b
まずは、Ruleを実装したクラスを作っていきます。
class ExpressionFunctionRule : Rule("expression-function-rule") {
override fun visit(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
) {
if (node.elementType == KtStubElementTypes.FUNCTION
&& !node.text.contains('\n')) {
val child = node.findChildByType(KtNodeTypes.VALUE_PARAMETER_LIST) ?: return
if (child.treeNext.elementType == KtTokens.WHITE_SPACE
&& child.treeNext.treeNext.elementType == KtTokens.EQ) {
emit(node.startOffset, "need return type!!!", false)
}
}
}
}
まず、ASTノードのタイプがKtStubElementTypes.FUNCTION
かつ、一行のものを取得します。
取得したASTノードから、KtNodeTypes.VALUE_PARAMETER_LIST
の子を探し出し、treeNextで正しい並びになっているかを見ていきます。
KtNodeTypes.VALUE_PARAMETER_LIST
の後に、KtTokens.WHITE_SPACE
, KtTokens.EQ
と続くと、返り値が定義されていないことがわかるので、"need return type!!!"というlintのエラー文言を吐き出しています。
次に、先程作った独自ルールをProviderにセットしていきます。
class CustomRuleSetProvider : RuleSetProvider {
override fun get(): RuleSet = RuleSet("custom-rule", ExpressionFunctionRule())
}
そして、このCustomRuleSetProvider
を com.github.shyiko.ktlint.core.RuleSetProvider
内に追加します。
これで、独自のLintがKtlint内で使用できるようになります。
実際に、lintをかけるにはjarを生成し、ktlintのオプションとしてjarを指定します。
./gradlew jar
task sourcesJar(type: Jar, dependsOn: classes) {
classifier = 'sources'
from sourceSets.main.allSource
}
artifacts {
archives sourcesJar
}
./gradlew ktlint
task ktlint(type: JavaExec, dependsOn: classes) {
args '--debug', 'src/**/*.kt', '-R', 'path/to/your.jar'
main = 'com.github.shyiko.ktlint.Main'
classpath = configurations.ktlint + sourceSets.main.output
ignoreExitValue = true
}
テストコード
定義したLintのテストコードを書いてみます。
ruleに対して、lint()を呼ぶことでコードからlintをかけることができます。
@RunWith(JUnitPlatform::class)
class ExpressionFunctionTest : Spek({
describe("one line function rule") {
it("have to specify the return type") {
val rule = ExpressionFunctionRule()
assertThat(rule.lint(
"""
fun add(a: Int, b: Int): Int = a + b
"""
.trimIndent())
).isEqualTo(emptyList<LintError>())
assertThat(rule.lint(
"""
fun add(a: Int, b: Int) = a + b
"""
.trimIndent())
).isEqualTo(listOf(LintError(1, 1, "one-line-function", "need return type!!!")))
}
}
})
テスト結果
CIとの連携
毎回手動でlintを回すのは大変なので、CI上でlintをチェックするのがいいと思います。
Dangerを使うと、PullRequest作成時に、DangerがLintエラーを指摘したりすることができます。
# ktlint
checkstyle_format.base_path = Dir.pwd
checkstyle_format.report 'your/ktlint/output.xml'
最後に
今回はKtlintを使った独自ルールの追加方法を紹介しました。
基本的はktlintの使い方はこちらの記事を書きましたので、見てみてください。
https://qiita.com/takusemba/items/04c9ad7d28c4b91dc1a4