コードレビューで気付きにくい言語仕様の話

この文章は日記として書きました。 うちの会社のアプリが急に使えなくなったという話が弊社slackの#generalチャンネルで話されていた。気になって会話を遡り状況を整理してみると、iOS版及びWebは問題ないがAndroid版だけが週末からずっと通信できなくなっているとのことだった。この不具合が報告されているのは一人で自分を含め他の人は問題なく動いていた。僕の所属部署はそのアプリを保守する立場にあり、自分はそのアプリのコードベースに詳しかったので原因を調査してみることにした。
まず調査のきっかけとして、一番手軽でかつ情報がありそうなサーバーログをKibanaを使って探した。しかし直近のログから該当ユーザーが送信したと思われるものは1件も見当たらなかった。そこでネットワークリクエストを投げる前にクライアント側でエラーが発生していると考え、モバイルアプリ側のエラー収集サービスであるCrashlyticsで該当のuser_idに関連するエラーを探した。こちらは多数見つかった。日時の新しいセッションを一つ選びスタックトレースを追ってみるとAPIリクエストをする際に呼ばれる認証系のコード内で「String から longへの変換」に失敗してクラッシュしていることがわかった。初歩的で単純な例外に思えたが該当の行を読んでもなぜNumberFormatException: For input string "null"となるのかまるで理解できなかった。重要なのは null.toLong() ではなく "null".toLong()であるところだった。周辺のコードを読む限りにおいて文字列の"null"なんて入る余地が無いように思えた。僕の頭では解決できそうになかったので、認証周りに詳しい同僚に調査の続きをお願いした。
結果から言うと問題は同僚が解決してくれた。原因はKolinのAny?.toString()の振る舞いで、このメソッドは直感に反してString?ではなくStringを返り型とする。ではレシーバーがnullだった時にどうするか、文字列の"null"を返すのだ。原因がわかり問題は既に修正された。

toString - Kotlin Programming Language

Returns a string representation of the object. 
Can be called with a null receiver, in which case it returns the string "null".

KotlinとAndroidStudioの優れた静的解析のおかげでNullableの扱いによるミスは減ったように思うけど、今回のケースでは逆にIDEの検査もPR時のlintも警告しなかったことで安心して不具合を入れてしまったのではないかとふと思った。
nullがレシーバーの場合に"null"文字列を返す仕様はどのようにして入ってしまったんだろうかと思いを馳せた出来事だった。