静的解析ツール ErrorProne でJavaのコンパイルエラーを強化
ErrorProneとは?
ErrorProne はGoogle製のJavaの静的解析ツールです。
他の静的解析ツール(FindBugsなど)との大きな違いは、チェックNGが コンパイルエラーとなること です。
以下、公式サイトから引用
It’s common for even the best programmers to make simple mistakes. And sometimes a refactoring which seems safe can leave behind code which will never do what’s intended.
We’re used to getting help from the compiler, but it doesn’t do much beyond static type checking. Using Error Prone to augment the compiler’s type analysis, you can catch more mistakes before they cost you time, or end up as bugs in production. We use Error Prone in Google’s Java build system to eliminate classes of serious bugs from entering our code, and we’ve open-sourced it, so you can too!
Googleでもこれを使って静的解析しているとのこと。
チェック対象のバグパターンはこちらに記載されています。
環境構築
今回は以下の検証用の環境を構築しました。
-
IDE:
IntelliJ IDEA 2019.2.2
-
Gradle:
5.6.2
-
Java:
jdk11.0.3_7 (Amazon Corretto)
IntelliJのGradleプロジェクトを作成。
なお、公式サイトを見ると以下に対応しているようです。
- ビルドツール: Bazel, Maven, Gradle, Ant
- IDE: IntelliJ IDEA, Eclipse
注意点として、ErrorProne には JDK9
以上が必要です。
Gradleの設定
ErrorProneを動かす為に、以下のビルドファイルを用意しました。
plugins {
id 'java'
id "net.ltgt.errorprone" version "0.8.1"
}
group 'errorprone-sample2'
version '1.0-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
testCompile group: 'junit', name: 'junit', version: '4.12'
errorprone("com.google.errorprone:error_prone_core:2.+")
}
def defaultEncoding = 'UTF-8'
tasks.withType(AbstractCompile) each { it.options.encoding = defaultEncoding }
compileTestJava {
options.encoding = defaultEncoding
}
//tasks.withType(JavaCompile) {
// options.errorprone {
// disableAllChecks = true // 全てのチェックを無効化する
// enable("CollectionIncompatibleType", "ArrayToString") // 先に指定したチェックを有効にする
// }
//}
検証
ErrorProneによるバグパターンのチェックについて検証します。
CollectionIncompatibleType
Incompatible type as argument to Object-accepting Java collections method
実行ファイル
public class CollectionIncompatibleTypeSample {
public static void main(String... args) {
Map<Integer, String> map = new HashMap<>();
map.put(1, "One");
map.put(2, "Two");
map.get("1"); // ジェネリクスと型が異なる
map.remove("2"); // ジェネリクスと型が異なる
}
}
ErrorProneのチェック結果
src\main\java\CollectionIncompatibleTypeSample.java:10: エラー: [CollectionIncompatibleType] Argument '"1"' should not be passed to t
his method; its type String is not compatible with its collection's type argument Integer
map.get("1"); // ジェネリクスと型が異なる
^
(see https://errorprone.info/bugpattern/CollectionIncompatibleType)
src\main\java\CollectionIncompatibleTypeSample.java:11: エラー: [CollectionIncompatibleType] Argument '"2"' should not be passed to t
his method; its type String is not compatible with its collection's type argument Integer
map.remove("2"); // ジェネリクスと型が異なる
^
(see https://errorprone.info/bugpattern/CollectionIncompatibleType)
java.util.Map
は上記のメソッドの引数の型が Object
となっているので上記は通常コンパイルエラーとなりません。
しかし、これは実装誤りである為、ErrorProneでコンパイルエラーとなる。
ArrayToString
Calling toString on an array does not provide useful information
実行ファイル
public class ArrayToStringSample {
public static void main(String... args) {
int[] ary = {1, 2, 3};
System.out.println(ary); // => [I@3ac3fd8b
System.out.println(Arrays.toString(ary)); // => [1, 2, 3]
}
}
※ System.out.println()
では暗黙的に toString()
メソッドが呼び出される。
ErrorProneのチェック結果
src\main\java\ArrayToStringSample.java:6: エラー: [ArrayToString] Calling toString on an array does not provide useful information
System.out.println(ary); // => [I@3ac3fd8b
^
(see https://errorprone.info/bugpattern/ArrayToString)
Did you mean 'System.out.println(Arrays.toString(ary));'?
配列の toString()
メソッドの使用は誤りであることが多いのでErrorProneでコンパイルエラーとなる。
FormatString
Invalid printf-style format string
実行ファイル
public class FormatStringSample {
public static void main(String... args) {
String text1 = String.format("arg1: %d. arg2: %s", 1, "a");
System.out.println(text1); // => arg1: 1. arg2: a
String text2 = String.format("arg1: %d. arg2: %s", "2", "b");
System.out.println(text2); // => java.util.IllegalFormatConversionException: d != java.lang.String
}
}
text2
は String.format()
の書式文字列と引数の型が異なるので IllegalFormatConversionException
がスローされる。
ErrorProneのチェック結果
src\main\java\FormatStringSample.java:5: エラー: [FormatString] illegal format conversion: 'java.lang.String' cannot be formatted usi
ng '%d'
String text2 = String.format("arg1: %d. arg2: %s", "2", "b");
^
(see https://errorprone.info/bugpattern/FormatString)
書式文字列と引数の型が異なる場合、通常コンパイルエラーは発生せずに実行時に例外がスローされるがErrorProneでコンパイルエラーとなる。
一部のチェックのみ有効にしたい場合
以下のような場合は一部のチェックのみを有効にしたくなると思います。
- 既存のコードでチェックNGが多い
- 意図的に書いたコードがチェックNGとなる
※他の静的解析ツールと異なりErrorProneではコンパイルエラーとなる為、単純に無視できない。
その場合は、以下のように設定することで一部のチェックのみを有効にすることができます。
tasks.withType(JavaCompile) {
options.errorprone {
disableAllChecks = true // 全てのチェックを無効化する
enable("CollectionIncompatibleType", "ArrayToString") // 先に指定したチェックを有効にする
}
}
感想
上記の箇所についてはInteliJで警告を出してくれるのでIDEの警告に対応していれば基本的には問題ないと思いますが、
世の中にはIDEの警告を無視する人もいるのでコンパイルエラーなら強制力が働くので良さそう。
また、コンパイル時点で誤りに気づくことが出来るので
テストの差し戻しのような無駄な時間が発生しづらくなると思いました。