今回の記事は、Kotlinのtrailing commaのメリットと、Androidにおけるデメリットを考え、独自のKtlintルールを作ってみた話です。
目次
Trailing commaのメリット
Kotlinでは関数やコンストラクタの引数をカンマで区切りますが、末尾の引数の後ろにもカンマを記述できます。この末尾のカンマをtrailing comma(トレーリングカンマ、末尾カンマ)と呼びます。
// 定義側
data class User(
val familyName: String,
val givenName: String, // trailing comma
)
// 呼び出し側
val user = User(
familyName = "田中",
givenName = "太郎", // trailing comma
)
Trailing commaの最大のメリットは、引数の追加、削除、入れ替えをした時のGitのdiffが見やすくなることだと思います。
例えば上記のUser
にage
プロパティを追加した場合、trailing commaを使っていれば次のように追加したプロパティの行だけがハイライトされ、変更点が明確になります。
Trailing commaを使っていない場合は、変更していないgivenName
の行にもdiffが出てしまいます。
もちろんdiffの内容を見れば、givenName
のプロパティ自体に変更はなく、カンマが追加されただけであることがわかります。しかしtrailing commaを使っている場合と比較して、変更内容の理解に必要な時間が若干増えます。コードレビュー中にあちこちにこのようなdiffが含まれていると、塵も積もれば山となり、地味にストレスを感じます。
Androidのコードにおけるtrailing comma
私個人的には、上記のメリットが大きいと感じているのでtrailing commaを有効にしたい派です。ただ、Androidアプリのコードで一部、trailing commaをつけないほうがよいと感じる部分があります。それは、Jetpack ComposeのModifier
です。
例えば次のようにText
コンポーザブルのmodifier
引数にいくつかのModifier
チェーンを記述しているコードで、trailing commaを使用しているとします。
Text(
text = "Hello",
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
)
このModifierチェーンの末尾にheight
を追加すると、diffは下記のようになります。padding
の後ろについていたカンマがなくなるため、余計な差分が出てしまいます。記事の最初で紹介したtrailing commaをつけない場合のデメリットと同じことが、trailing commaがあることによって生じてしまっています。
Composeのコードを書いていると、Modifierチェーンの追加や削除は頻繁に発生します。できればmodifier
引数の後ろにはカンマをつけたくないです。
独自のKtlintのルールを作ってみた
というわけで「末尾の引数がmodifier
の場合はtrailing commaをつけない」という独自のKtlintのルールを作ってみました。
Ktlintのルールの作り方は、「Ktlintのcustom ruleを実装する。」が参考になりました。また、JLLeitschuh/ktlint-gradle にもカスタムルールのサンプルコードが載っています。さらに、pinterest/ktlintのStandardルールのコードを参考にすると、具体的なルールの書き方がわかります。(注:pinterest/ktlintはv1.3でAPIが変更されているのですが、JLLeitschuh/ktlint-gradleはまだ対応していないようで、最新のStandardルールをそのままコピペして使おうとするとエラーになります。)
今回はTrailingCommaOnCallSiteRule.kt
をコピーして変更を加えました。変更内容は、「最後の引数の最初のNodeが”modifier”または”Modifier”だったら、trailing commaをつけない」というものです。この判定を行う関数は下記のようになりました。
private fun isLastArgumentModifier(valueArgumentList: ASTNode): Boolean {
val lastArg = valueArgumentList.children().lastOrNull { it.elementType == VALUE_ARGUMENT }
val firstTextOfLastArg = lastArg?.firstChildLeafOrSelf()?.treeParent?.text
return firstTextOfLastArg == "modifier" || firstTextOfLastArg == "Modifier"
}
この関数は、引数のリストを表すノードを受け取ります。その子供のノードのうち、最後の引数を表すノードを取得します。そして、その先頭のリーフが”modifier”または”Modifier”だったらtrue
を返しています。
このルールを適用すると、以下のように最後の引数がModifierの場合はtrailnig commaをつけないようにすることができました。
Text(
text = "Hello $name!",
modifier = Modifier
.fillMaxSize()
.padding(2.dp)
)
なお、このルールはKtlintのStandardルールのTrailing comma on call siteと競合するので、設定でこのルールは無効にします。
ktlint_standard_trailing-comma-on-call-site = disabled
カスタムルールのコードはGitHubで公開したので、興味のある方はご参照ください。
やってみて思ったこと
カスタムルールを作成して、実現したかったことは一応実現できました。ただ、上記のアプローチではコードの文字列ベースでしか判定していないので、不完全なルールになってしまいました。やりたいことは引数の型がandroidx.compose.ui.Modifier
の場合にtrailing commaをつけないということですが、この方法だと、引数名がmodifierであれば型にかかわらずルールが適用されてしまいます。
Ktlintは基本的には構文に基づいたルールの解析なので、変数の型が何なのかといった部分まで解析するのは想定外なのだろうと思いました。(ちゃんと理解しているわけではないので、間違っていたらすみません)
カスタムルールを作ってはみたものの、Ktlintの設定としては、StandardルールのTrailing comma on call siteをtrueにするかfalseにするかのどちらかを選び、Modifierの場合だけ特別扱いはしないのが無難かなと思いました。
Trailing comma on call siteのデフォルト設定
ちなみに、Trailing comma on call siteのデフォルト設定は、IntelliJ IDEAとAndroid Studioで異なっています。IntelliJ IDEAではtrue(trailing commaをつける)ですが、Android Studioでは無効(trailing commaをつけない)になっています。
Android StudioでTrailing comma on call siteを有効にするには、以下のように.editorconfig
を設定します。AndroidのプロジェクトへのKtlint導入方法は、「AndroidアプリプロジェクトにKtlintを導入する」も参考にしてください。
[*.{kt,kts}]
ktlint_code_style = android_studio
ij_kotlin_allow_trailing_comma_on_call_site = true