15
11

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-03-15

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

15
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
11