runCatchingつかっていますか?
kotlinではJavaの検査例外もcatchしなくてもエラーにならなくなったこともあり、よく分からないけど念のためcatchしておくかー、などと使ってしまいがちです
runCatching {
someMethod()
}.getOrNull()
としておけば、仮にsomeMethodが何らかのExceptionを投げたとしてもすべてcatchした上で結果をResultにラップしてくれます。getOrNull()をコールすれば、Exceptionが投げられた場合はnullと置換することもできます。
ただ、このrunCatchingの実装は以下のようになっていて、Throwableすべてをcatchしています。
public inline fun <R> runCatching(block: () -> R): Result<R> {
return try {
Result.success(block())
} catch (e: Throwable) {
Result.failure(e)
}
}
ThrowableのサブクラスにはErrorとExceptionがあります。
Errorは「通常のアプリケーションであればキャッチすべきではない重大な問題」を通知する際に使われます。実行環境の問題など、アプリケーションではcatchしたところで回復不能な問題が発生しているので。ドキュメントにあるとおり通常はcatchすべきではありません。
また、ExceptionのサブクラスにはRuntimeExceptionとそれ以外のExceptionがあり、RuntimeExceptionは非検査例外です。こちらも通常はcatchすべきではありません。例えば、NullPointerExceptionはcatchするのではなく、発生しないように実装を変更すべきです。ただ、catchしたところで、コストなどの問題はあれど、即座に重大な問題になるわけではありません。
しかし、CancellationExceptionについては、意図せずcatchしてしまったことで、トラブルにというがよく聞く問題ですね。Threadやcoroutinesのcancelを行うとこのExceptionが発生しcancelされるのですが、catchしてしまうと正しくcancelされなくなってしまいます。
本来の話をすれば、きちんとどのようなExceptionが発生するのかを調査し、そのExceptionに絞ってcatchを行い、Exceptionの種類ごとに適切な処理を行うというのが理想です。
しかし、Exceptionが発生する処理が制御外であるため、想定外のExceptionも含めてcatchしておく必要があるということもよくあります。
そんなわけで、良くはないんだけどrunCatchingを使うよりは、という妥協案として以下のようなcatchしちゃよろしくなさそうなものを除外したメソッドを用意したとしましょう。
inline fun <R> runExceptionCatching(block: () -> R): Result<R> =
try {
Result.success(block())
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
Result.failure(e)
}
今回これは本題ではありません。なのでこのメソッドの是非はおいておきます。
Cunstom Lintを作って警告&Quick Fixを提供してみる
ここからが本題です、標準ライブラリ提供関数の代替関数を用意したので、プロジェクト内ではこちらに置き換えていきたい。しかし、標準ライブラリで提供されている関数を使わないというルールはなかなか徹底できません、人員の入れ替わりがあればなおさらです。
そういった場合は、単にルールとしてチーム内で周知するだけでなく、使ったら警告が出て、できれば自動的に置換までしてくれるようにしておくと良いですよね。
ということで、Custom Lintを作りましょう。
Custom Lint Moduleを作成
New -> Moduleを選択
Java or Kotlin Libraryを選択しモジュール名、パッケージ名などを入力して「Finish」をクリック
Custom LintはAndroidのモジュールではなく、java libraryとして作る必要があるのと、lintプラグインが必要なので、プロジェクトルートのbuild.gradle.ktsには以下のpluginを追加しておきます。
plugins {
...
id("org.jetbrains.kotlin.jvm") version "1.9.21" apply false
id("com.android.lint") version "8.2.0" apply false
}
lintモジュールのbuild.gradle.ktsはいったん以下のようにしておきます。
plugins {
id("org.gradle.java-library")
id("org.jetbrains.kotlin.jvm")
id("com.android.lint")
}
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
dependencies {
compileOnly("com.android.tools.lint:lint-api:31.2.0")
compileOnly("com.android.tools.lint:lint-checks:31.2.0")
testImplementation("junit:junit:4.13.2")
testImplementation("com.android.tools.lint:lint:31.2.0")
testImplementation("com.android.tools.lint:lint-tests:31.2.0")
}
Detectorの実装
runCatchingの使用箇所を検出し、issueを報告するDetectorを実装します。
まずは報告するISSUEを定義しましょう。ここで検出するissueは一つなので単純にISSUEという名前にしていますが、複数ある場合は適切な名前で定義しておきましょう。ここで必要なパラメータはそれぞれなんとなく意味が分かると思うので見よう見まねで埋めましょう。今回の対象はKotlinファイルですので、Scope.JAVA_FILE_SCOPE
を指定しています。名前としてはJAVAですが、JavaとKotlinがこのスコープになります。
そして、この後Detecotorの登録時に必要になるので、このDetecotorで検出するISSUEをまとめたListも定数として持っておきます。
class RunCatchingDetector : Detector(), Detector.UastScanner {
...
companion object {
val ISSUE = Issue.create(
id = "RunCatching",
briefDescription = "runCatching は推奨されません",
explanation = "runCatching は推奨されません、 runExceptionCatching を使用してください",
category = Category.CORRECTNESS,
priority = 5,
severity = Severity.WARNING,
implementation = Implementation(
RunCatchingDetector::class.java,
Scope.JAVA_FILE_SCOPE,
),
)
val ISSUES = listOf(ISSUE)
}
}
続いて実際の検出機能を実装します。
getApplicableMethodNames
をoverrideして、検出対象のメソッド名を返しておきます。
visitMethodCall
にて、メソッドのパッケージをチェックして、issueを報告します。
class RunCatchingDetector : Detector(), Detector.UastScanner {
override fun getApplicableMethodNames(): List<String> = listOf("runCatching")
override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
if (context.evaluator.isMemberInClass(method, "kotlin.ResultKt")) {
context.report(
Incident(
issue = ISSUE,
scope = node,
location = context.getLocation(node),
message = ISSUE.getExplanation(TextFormat.TEXT),
fix = fix().replace()
.text("runCatching")
.with("com.example.runExceptionCatching")
.shortenNames()
.build(),
)
)
}
}
...
今回の件は、単に警告を出すだけでなく、自動的に修正が可能なので、fixパラメータを提供します。
fix().replace()
.text("runCatching")
.with("com.example.runExceptionCatching")
.shortenNames()
.build(),
replaceで置換によるQuickFix、対象がtext、置換内容をwithで記述します。
置換内容は完全修飾名で書いておき、IDEのcleanup機能でimportに置換してもらうようにします。
IssueRegistoryの実装
Detecotrを実装したら、IssueRegistoryを実装します。
class RunCatchingIssueRegistry: IssueRegistry() {
override val issues: List<Issue> = RunCatchingDetector.ISSUES
override val api: Int = CURRENT_API
override val vendor = Vendor(
vendorName = "example/example",
identifier = "com.example:lint:{version}",
feedbackUrl = "https://github.com/example/issues",
)
}
vendor情報はプロジェクトに閉じているなら不要だと思いますが、無いと警告がでるので適当に設定しておきます。
Manifestへの登録
IssueRegistoryをManifestに登録します。
tasks.jar {
manifest {
attributes(
"Lint-Registry-v2" to "com.example.lint.RunCatchingIssueRegistry",
)
}
}
これでCustomLintによってrunCatching使用箇所に警告が出るようになり、QuickFixによってrunExceptionCatchingに置換することもできるようになっているはずです。
Lintのユニットテストを書く
Lintのユニットテストを書くこともできます。
以下のように、入力となるソースコードに対して、どのような警告がでるか、QuickFixの結果どうなるかを書いてテストを行います。
class RunCatchingDetectorTest {
@Test
fun detectAndFix() {
lint().files(
kotlin(
"""
package test.pkg
class Test {
fun test() {
runCatching {
println("test")
}
}
}
""".trimIndent()
),
)
.issues(RunCatchingDetector.ISSUE)
.run()
.expect(
"""
src/test/pkg/Test.kt:4: Warning: runCatching は推奨されません、 runExceptionCatching を使用してください [RunCatching]
runCatching {
^
0 errors, 1 warnings
""".trimIndent()
)
.expectFixDiffs(
"""
Fix for src/test/pkg/Test.kt line 4: Replace with com.example.runExceptionCatching:
@@ -4 +4
- runCatching {
+ com.example.runExceptionCatching {
""".trimIndent()
)
}
}
QuickFix機能付きのCuston Lintを実装することができました。
以上です。