Edited at

ktlintで独自ルールを追加する

More than 1 year has passed since last update.


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の木構造として表すことができます。

スクリーンショット 2018-03-11 17.57.02.png

例えば、↓の関数は 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のオプションで調べることができます。


Hoge.kt

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())

}

そして、このCustomRuleSetProvidercom.github.shyiko.ktlint.core.RuleSetProvider内に追加します。

スクリーンショット 2018-03-13 22.34.20.png

スクリーンショット 2018-03-13 22.39.06.png

これで、独自のLintがKtlint内で使用できるようになります。

実際に、lintをかけるにはjarを生成し、ktlintのオプションとしてjarを指定します。

./gradlew jar


build.gradle

task sourcesJar(type: Jar, dependsOn: classes) {

classifier = 'sources'
from sourceSets.main.allSource
}

artifacts {
archives sourcesJar
}


./gradlew ktlint


build.gradle

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をかけることができます。


ExpressionFunctionTest.kt

@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!!!")))

}
}
})



テスト結果

スクリーンショット 2018-03-13 22.46.57.png


CIとの連携

毎回手動でlintを回すのは大変なので、CI上でlintをチェックするのがいいと思います。

Dangerを使うと、PullRequest作成時に、DangerがLintエラーを指摘したりすることができます。

# ktlint

checkstyle_format.base_path = Dir.pwd
checkstyle_format.report 'your/ktlint/output.xml'

image.png


最後に

今回はKtlintを使った独自ルールの追加方法を紹介しました。

基本的はktlintの使い方はこちらの記事を書きましたので、見てみてください。

https://qiita.com/takusemba/items/04c9ad7d28c4b91dc1a4