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

# ktlintとは
[ktlint](https://github.com/shyiko/ktlint)はkotlinの静的コード解析ツールです。
同じようなものに、[detekt](https://github.com/arturbosch/detekt)というものもあります。
今回は、ktlintについて書いていきます。

ktlintはkotlinのコードにlintをかけるだけでなく、lintで検出した部分にフォーマットをかけることもできます。

今回は、ktlintを使って独自のlintルールを追加する方法を書いていきます。

# 独自ルールの追加
ktlintには、デフォルトでいくつかのルールが既に定義されています。
そのため、独自のルールを追加しなくても、ある程度のlintはかけることができます。

[デフォルトルール一覧](https://github.com/shyiko/ktlint#standard-rules)

しかし、チーム内で決めたkotlinの書き方などを統一したいことがあります。そのような場合には、独自のlintルールを定義することによって、lintチェックすることができます。

## ルールの定義

ktlintが提供しているRuleというインターフェイスを実装することによって、独自のルールを定義していきます。

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


<img width="723" alt="スクリーンショット 2018-03-11 17.57.02.png" src="https://qiita-image-store.s3.amazonaws.com/0/93589/81ea8451-899f-c364-f228-cb72a92305bf.png">



例えば、↓の関数は `KtNamedFunction` というASTNodeになります。

```kotlin
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
```

```kotlin

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

```kotlin
fun add(a: Int, b: Int) = a + b
```

#### OK

```kotlin
fun add(a: Int, b: Int): Int = a + b
```


まずは、Ruleを実装したクラスを作っていきます。

```kotlin

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にセットしていきます。

```kotlin
class CustomRuleSetProvider : RuleSetProvider {

  override fun get(): RuleSet = RuleSet("custom-rule", ExpressionFunctionRule())
  
}
```

そして、この`CustomRuleSetProvider`を `com.github.shyiko.ktlint.core.RuleSetProvider`内に追加します。

<img width="380" alt="スクリーンショット 2018-03-13 22.34.20.png" src="https://qiita-image-store.s3.amazonaws.com/0/93589/dc6a5bfa-a065-0093-c8be-5c57a5542f1b.png">


<img width="369" alt="スクリーンショット 2018-03-13 22.39.06.png" src="https://qiita-image-store.s3.amazonaws.com/0/93589/d25cd053-2c43-08d6-c08a-32ea7597c1ab.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!!!")))

    }
  }
})

```

#### テスト結果

<img width="1085" alt="スクリーンショット 2018-03-13 22.46.57.png" src="https://qiita-image-store.s3.amazonaws.com/0/93589/7c579c4e-d4c2-9a75-8d6a-b889d518e698.png">


## CIとの連携

毎回手動でlintを回すのは大変なので、CI上でlintをチェックするのがいいと思います。
[Danger](https://github.com/danger/danger)を使うと、PullRequest作成時に、DangerがLintエラーを指摘したりすることができます。


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

![image.png](https://qiita-image-store.s3.amazonaws.com/0/93589/e0ed2bee-d281-94d0-1364-dc4faeffd1f0.png)

# 最後に
今回はKtlintを使った独自ルールの追加方法を紹介しました。
基本的はktlintの使い方はこちらの記事を書きましたので、見てみてください。
https://qiita.com/takusemba/items/04c9ad7d28c4b91dc1a4