レベル: 中級 Chris Grindstaff (chris@gstaff.org), Software Engineer, IBM
2004年 5月 25日
静的な分析ツールは開発者の側に大きな努力を要求することなく、コード中にあるバグを見つけてくれます。当然ながら長年プログラミングをしている人ならば、こうしたうたい文句が必ずしも正しいものではないことを知っているでしょう。とは言え静的分析ツールは、良質なものであればツールボックスの中に入れておくだけの価値があるものです。2部構成のこのシリーズでは、上級ソフトウェア・エンジニアのChris
Grindstaffが、FindBugsがコードの品質改善や、隠れているバグを取り去るのにどれほど役に立つかを説明します。2回シリーズの本記事の後半、
第2回目
も忘れずにお読み下さい。
コード品質ツールの問題の一つは、実際には問題ではないようなもの、つまりfalse
positivesまで拾い上げ、開発者を圧倒してしまうことにあります。false
positiveが起きると開発者はツールの出力を無視するようになるか、あるいは結局ツールを使わなくなってしまいます。FindBugsを作ったDavid
HovemeyerとWilliam Pughはこの問題をよく考え、false
positiveを減らすように大いに努力しています。他の静的解析ツールとは異なり、FindBugsはスタイルやフォーマットには注目せず、本当のバグや潜在的なパフォーマンス問題のみを見つけようとするのです。
FindBugsとは何か?
FindBugsはクラスやJARファイルを検査する静的解析ツールであり、バイトコードとバグ・パターン・リストを比較することで潜在的な問題を探し出します。静的解析ツールを使うと、実際にプログラムを実行せずにソフトウェアを解析することができます。プログラムの意図する所を確かめるために、プログラムを実行する代わりに多くの場合Visitorパターン(
参考文献
)を使って、クラスファイルの形式や構造を解析します。図1はある無名のプロジェクトを分析した結果を示しています。
図1. FindBugsのUI
FindBugsで検出できる問題のいくつかを見て行きましょう。
見つけられる問題の例
次のリストはFindBugsで見つけられる問題の全てではありませんが、より皆さんに関心のありそうななものに注目しています。
Detector: Find hash equals mismatch
このチェック機能(detector)は
equals()
と
hashCode()
の実装に関連した問題を見つけます。この2つのメソッドはほとんど全てのCollectionsベースのクラス、つまりList、Maps、Setsなどから呼ばれるので、非常に重要です。一般的に、このチェック機能は次のような2種類の問題を検出します。
-
クラスがObjectの
equals()
メソッドを上書きするが、その
hashCode
は上書きしない時。又はその逆の時。
-
クラスが
equals()
や
compareTo()
メソッドのco-variant を定義する時。例えば
Bob
クラスがその
equals()
メソッドをブール値の
equals(Bob)
として定義すると、これはObjectで定義した
equals()
メソッドをオーバーロードになります。Javaコードによるコンパイル時のオーバーロードメソッド解決方式のために、(
equals()
への引数をタイプ
Bob
に明示的にキャストしない限り)ランタイムに使うメソッドはほとんど必ずObjectで定義するものになり、
Bob
で定義したものではありません。その結果このクラスのインスタンスの一つが集合クラスに入れられる時には、このメソッドの
Object.equals()
版が使われ、
Bob
で定義したものは使われません。この場合では
Bob
クラスは、タイプObjectの引数を受け付ける
equals()
メソッドを定義すべきなのです。
Detector: Return value of method ignored
このチェック機能はメソッドの戻り値が(無視されるべきではないのに)無視されている場所を探し出します。このシナリオの一般的な例は
String
メソッドを呼び出す時に見ることができます(リスト1)。
リスト1. 無視された戻り値の例
1 String aString = "bob";
2 b.replace('b', 'p');
3 if(b.equals("pop")) |
この間違いはごく一般的なものです。2行目で、このプログラマーはストリングにある全てのbをpで置き換えたと思っています。確かに置き換えているのですが、ストリングの不変性(immutable)を忘れています。このタイプのメソッドは全て新しいストリングを戻し、このメッセージの受信者を変えません。
Detector: Null pointer dereference and redundant
comparisons to null
このチェック機能は2種類の問題を見つけます。コードのパスがnull
ポインター例外を引き起こすか、引き起こす可能性がある場合、またnull
との冗長比較がある場合を見つけ出します。例えば2つの値を比較して両方とも明らかにnull
だった場合、この2つは冗長ということになり、コーディング間違いの可能性があります。FindBugsは一方の値がnull
で他方がnull ではないと判定できる時にも、似たような問題を検出します(リスト2)。
リスト2. null ポインターの例
1 Person person = aMap.get("bob");
2 if (person != null) {
3 person.updateAccessTime();
4 }
5 String name = person.getName(); |
この例では、1行目のMapが「bob」という名前の人を持っていない場合には、5行目でpersonの名前を尋ねられた時にヌル・ポインター例外が発生します。FindBugsはmapに「bob」が入っているかどうかを判別できないため、5行目に対してヌル・ポインター例外の可能性があるというフラグを立てるのです。
Detector: Field read before being initialized
このチェック機能はコンストラクターで読まれるフィールドのうち、初期化前に読まれてしまうフィールドを見つけます。この間違いは(常にではありませんが)多くの場合、コンストラクター引き数の代わりに間違ってフィールド名を使ってしまう結果起こります(リスト3)。
リスト3. コンストラクターにあるフィールドを初期化前に読んでしまう
1 public class Thing {
2 private List actions;
3 public Thing(String startingActions) {
4 StringTokenizer tokenizer = new StringTokenizer(startingActions);
5 while (tokenizer.hasMoreTokens()) {
6 actions.add(tokenizer.nextToken());
7 }
8 }
9 } |
この例では
actions
が初期化されていないので、6行目でヌル・ポインター例外が起きることになります。
こうした例はFindBugsが検出する問題のごく一部にすぎません(他の問題に関しては
参考文献
を見てください)。この記事の執筆時点でFindBugsには合計35のチェック機能があります。
FindBugsを使ってみる
FindBugsを実行するにはJava Development Kit (JDK), version
1.4以上が必要です。ただし、古いJDKで生成したクラスファイルも解析は可能です。まず最初にFindBugsの最新リリース(現在は0.7.1です。
参考文献
)をダウンロードしてインストールします。幸いダウンロードもインストールもごく単純です。zipまたはtarを適当なディレクトリにダウンロードしたらunzipします。それが終わればインストールは完了です。
インストールができたので、サンプルクラスに対して実行してみましょう。多くの記事と同様、この記事でもWindowsユーザーを念頭に書いていますが、Unix派の人も苦もなく理解できると想定しています。コマンドプロンプトを開き、FindBugsをインストールしたディレクトリに行きます。私の場合はC:\apps\FindBugs-0.7.3.です。
FindBugsのホーム・ディレクトリにはいくつか面白いディレクトリがあります。説明ファイルはdocディレクトリにあります。もっと大事なこととして、binディレクトリにはFindBugsを実行するためのバッチファイルがあります。これを次に説明します。
FindBugsを実行する
最近のツールはどれもそうですが、FindBugsもいくつかの方法で実行できます。GUIでも実行できますし、コマンドラインからでも、Antを使っても、Eclipseのプラグインとしても、またはMavenを使って実行することもできます。ここではFindBugsをGUIで実行する方法を簡単に説明しますが、中心的に説明するのはAntやコマンドラインから実行する方法です。理由は、GUIではコマンドラインにあるようなオプションが全て使えるわけではないためです。例えばUIでは、フィルターに対して特定のクラスを含むか含まないようにするかを指定することができません。もっと大事な理由として、私の意見ではFindBugsはビルドに統合された一部として使う時に最高の力を発揮するものですが、自動ビルドにUIは向かないからです。
FindBugsのUIを使う
FindBugsのUIはごく単純に使えますが、いくつか注意すべき点があります。
図1
で分かるようにFindBugsのUIを使う利点の一つは、検出された各問題のタイプに対して説明が表示されることです。図1では
Naked notify in method
というバグの説明を示しています。他のバグのパターンにも似たような説明が用意されており、FindBugsに慣れるには大いに役立ちます。ウィンドウの下側の区画にあるSource
codeタブも同様に便利です。このタブに切り替えて、「ソースがどこにあるかを探せ」とFindBugsに言うと、違反しているコード行がハイライトされます。
もう一つ大事な点はFindBugsをAntのタスクとして、またはコマンドラインから実行している時に
output
オプションとして
xml
を選択すると、以前実行した結果をUIにロードしてくることができるのです。こうすることでコマンドライン・ベースのツールとUIツールを同時に使う利点を最大限に生かすことができます。
FindBugsをAntのタスクとして実行する
Antのビルド・スクリプトからFindBugsをどのように使うかを見て行きましょう。まずFindBugsのAntタスクをAntのlibディレクトリにコピーし、Antが新しいタスクを認識するようにします。FIND_BUGS_HOME\lib\FindBugs-ant.jarをANT_HOME\libにコピーします。
FindBugsタスクを使うために、ビルド・スクリプトに何を追加すべきかを見てみましょう。FindBugsはカスタム・タスクなので、Antにどのクラスをロードすべきかが分かるように
taskdef
タスクを使う必要があります。それにはビルドファイルに次の行を追加します。
<taskdef name="FindBugs"
classname="edu.umd.cs.FindBugs.anttask.FindBugsTask"/>
|
taskdef
を定義した後は、それを
FindBugs
という名前で呼ぶことができます。次に新しいタスクを使うビルドにターゲットを追加します(図4)。
リスト4. FindBugsターゲットを作る
1 <target name="FindBugs" depends="compile">
2 <FindBugs home="${FindBugs.home}" output="xml" outputFile="jedit-output.xml">
3 <class location="c:\apps\JEdit4.1\jedit.jar" />
4 <auxClasspath path="${basedir}/lib/Regex.jar" />
5 <sourcePath path="c:\tempcbg\jedit" />
6 </FindBugs>
7 </target> |
このコードが何をしているかを少し詳しく見てみましょう。
1行目:
target
がコンパイルに依存することに注意してください。ここは覚えておくべき重要な点ですが、FindBugsはソースファイルではなくクラスファイル上で動作するので、targetをコンパイル・ターゲット依存とすることでFindBugsがどんな最新クラスファイルででも確実に実行できるようになります。FindBugsは入力に関して柔軟であり、クラスファイル一式、JARファイル、一連のディレクトリなども入力として受け付けます。
2行目:
FindBugsを含むディレクトリを指定する必要があります。これはAntプロパティを使って次のようにします。
<property name="FindBugs.home"
value="C:\apps\FindBugs-0.7.3" />
|
オプションとしての属性
output
はFindBugsが使う出力フォーマットを指定します。使える値としては
xml
か
text
または
emacs
です。もし
outputFile
を何も指定しないと、FindBugsは標準出力に出力します。先に書いたように、XMLフォーマットはUI内で見られるという点でより有利です。
3行目:
どのセットのJAR、クラスファイル、ディレクトリが必要なのかを指定するために
class
要素を使います。複数のJARファイルやクラスファイルを解析するには、それぞれに対して別々の
class
要素を指定します。
projectFile
要素が含まれているのでない限り
class
要素は必要です。詳細についてはFindBugsのマニュアルを見てください。
4行目:
ネストされた要素
auxClasspath
を使ってアプリケーションの依存性をリストアップします。これらのクラスはアプリケーションに必要なものですがFindBugsには解析させたくないものです。アプリケーションの依存性をリストアップしなくてもFindBugsはやはりそうしたクラスを解析しますが、探しているクラスが見つからない時には文句を言ってきます。
class
要素の場合と同じく、FindBugs要素にも複数の
auxClasspath
要素を指定することができます。
auxClasspath
要素はオプションです。
5行目:
sourcePath
要素が指定されている場合には、
path
属性はアプリケーションのソースコードがあるディレクトリを指します。ディレクトリを指定することで、GUIでXMLの結果を見る時にFindBugsがエラーのあるソースコードをハイライトするようになります。この要素はオプションです。
これで基本的な所を説明しました。では何週間分かを早送りしましょう。
フィルター
あなたはFindBugsをチームに導入し、日々のビルド・プロセスの一部として使い始めました。チームがこのツールに慣れてくるに従って、(何らかの理由で)検出されるバグの一部は重要ではないと判定したとしましょう。あなたは一部のクラスが、悪意に変更される可能性があるオブジェクトを戻しても気にしないかもしれませんし、あるいはJEditのように、神に誓って正直に
System.gc()
を呼び出すだけの理由があるかもしれません。
特定のチェック機能を指定して、いつでもそのチェック機能をオフすることができます。さらにきめ細かなレベルとして、チェック機能を設定して特定のクラス群、または特定のメソッド群であっても、その一群の中にある問題をチェックしないようにすることができるのです。FindBugsではincludeとexcludeフィルターを使って細かな制御ができます。Excludeとincludeフィルターは現在FindBugsのコマンドライン版とAnt版でのみサポートしています。その名前の示す通り、excludeフィルターはある種のバグのレポートを含まないようにするために使います。Includeフィルターはexcludeほどには使われませんが、やはり便利なもので、特定なバグのみをレポートします。フィルターはXMLファイルで定義します。Includeやexcludeを指定するにはコマンドラインでexclude/includeのスイッチをつけるか、Antビルド・ファイルの中で
excludeFilter
または
includeFilter
を使います。下の例ではexcludeスイッチを使っていると想定してください。また、この先の説明では「バグコード」「バグ」「チェック機能」をそれぞれを、ある意味で置き換え可能なものとして使っていることに注意してください。
フィルターには様々な定義の仕方があります。
-
あるクラスに一致するものをフィルターにかける。このフィルターは、ある特定なクラスで見つかる全ての問題を無視するのに使います。
-
あるクラスの中にある、特定なバグコードに一致するものをフィルターにかける。このフィルターは、ある特定なクラスで見つかる一部のバグを無視するのに使います。
-
一連のバグに一致するものをフィルターにかける。このフィルターは、解析するクラス全体に渡る一連のバグを無視するのに使います。
-
解析するクラスの中にある、特定なメソッドに一致するものをフィルターにかける。このフィルターは、あるクラスの一連のメソッドで見つかる全てのバグを無視するのに使います。
-
解析するクラスの中のメソッドで見つかる、一部のバグに一致するものをフィルターにかける。このフィルターは非常にバグの多い一連のメソッドで見つかるバグの一部を無視するのに使います。
始めるにあたって必要なのはこれだけです。FindBugsのタスクをカスタム化する他の方法の詳細についてはFindBugsの説明資料を読んでください。さてこれでビルドファイルの設定方法が分かったので、FindBugsをビルドプロセスに統合するにはどうするかを見て行きましょう。
FindBugsをビルドプロセスに統合する
FindBugsをビルドプロセスに統合するにはいくつかのオプションがあります。FindBugsをコマンドラインから実行することももちろんできますが、おそらくビルドにAntを使っているでしょうから、FindBugs
Antタスクを使うのが一番自然でしょう。FindBugs
Antタスクを使うための基礎は先に説明したので、FindBugsをビルドプロセスに加える理由と、進める中で突き当たりそうな問題をいくつか説明することにします。
なぜFindBugsをビルドプロセスに統合すべきなのか
私が頻繁に受ける質問の中で最初に聞かれるのが、なぜFindBugsをビルドプロセスに加える必要があるのか、というものです。理由はたくさんありますが、最も明らかな答えとしてはビルドを実行したらできるだけ早く問題を検出しておきたい、というものです。チームが成長するにつれて必然的により多くの開発の初心者が加わることになるので、FindBugsを安全策として既知のバグ・パターンの検出に使うことができます。FindBugsの資料の一つにも説明があるのですが、開発者がある程度の数になるとコードにバグが入り込むことになります。確かにFindBugsのようなツールは全てのバグを検出するわけではありませんが、一部を見つける手助けになります。(特にFindBugsをビルドプロセスに組み込むためのコストが非常に低いことを考えれば)一部でも今見つけておいた方が、後で顧客に見つけられるよりもずっと良いと言えるでしょう。
どのフィルターを入れるか、どのクラスを入れるかが固まればFindBugsを実行するためのコストはごく僅かであり、しかも新しいバグを検出できるという利点が付きます。アプリケーション専用のチェック機能を書けばその利点はさらに大きいはずです。
意味のある結果を生成する
このコスト対利点の分析は、false
positivesを大量に生成することがない、という前提でのみ意味を持つことを認識する必要があります。つまりビルドからビルドへ繰り返していった時、新しいバグが出てきたかどうかが簡単に判別できないのだとしたらツールの価値は無くなってしまう、ということです。解析は自動化されていれば自動化されているほど良いものです。バグ修正が、検出されても実際には関係のないバグの中をかき分けて進むような作業を意味するのであれば、おそらく誰もツールを使わなくなるか、少なくともうまい使い方はしなくなるでしょう。
意味のある結果を生成するには、どの問題が自分にとっては気にする必要がないのかを確定し、それらをビルドから排除します。あるいは自分が本当に問題にする、一連の小さなチェック機能を選び、それだけを実行するようにします。別の手段としては、ある一連のチェック機能は個々のクラスから排除し、他のものは残すことです。FindBugsはフィルターの使い方に非常に柔軟性があるので、意味のある結果を生成しやすくなっています。これを次にとりあげます。
FindBugsの結果をどう処理するかを決める
結果をどう処理するか、は明らかな質問に思えるかも知れませんが、FindBugs風のツールを単に面白いからという理由でビルドに加えているとしか思えないようなチームが、皆さんの想像以上の数にのぼるのです。この質問をもう少し詳しく考えてみましょう。この結果をどうすべきなのか?
これはチームがどのように構成されているか、コード所有権の問題をどうするのか、といった問題に大きく依存するので、具体的に答えるのは困難な問題です。参考になりそうな指針としては次のようなものがあります。
-
FindBugsの結果をソースコード管理(source
code management:
SCM)システムに加えることを考慮する。大まかに言って、ビルドの成果物はSCMシステムに入れるべきではありません。ただしこの場合に限っては、時間と共に変わるコード品質を監視することができるので、このルールを破った方が良いのかも知れません。
-
チームのWebサイトに掲示できるようにXMLの結果ファイルをHTMLレポートに変換する。この変換はXSLスタイルシートやスクリプトで行うことができます。FindBugsのWebサイトやメーリング・リストにあるサンプルをチェックして見てください(
参考文献
)。
-
FindBugsのようなツールは、チームやある個人を追い立てるための政治的な武器になってしまいがちなものです。そうした使い方を奨励すべきではありませんし、そんな使い方にならないようにしましょう。繰り返しますが、これはコード品質を改善するために意図したツールなのです。そうした精神論は別として、次回の記事ではカスタムのバグチェック機能を書くにはどうすべきかを説明する予定です。
まとめ
FindBugsであれ、PMDであれ、その他でも、何らかの静的分析ツールを使ってコードを解析してみることをお勧めします。こうしたツールは便利なものであり、本当の問題を見つけることができます。FindBugsはそのなかでも、false
positivesを除去してくれるという点で他のものよりは優れています。さらにプラグ可能な構成のため、アプリケーションに特化した重宝なチェック機能を書くためのテスト・ベッドとなります。このシリーズ
第2回
ではアプリケーション特有の問題を見つけるためのカスタムのバグチェック機能を書くにはどうすべきかを説明する予定です。
参考文献
著者について  | |  | Chris Grindstaffはノースキャロライナ州にあるIBM in Research Triangle Parkの上級ソフトウェアエンジニアです。7歳の時に初めてプログラムを書いたのですが、その際に文章をタイプするのは手書きするのと同じくらいの罰になりうるのだと先生に納得させたのです。現在は様々なオープンソース・プロジェクトに興味を持っています。Eclipseで豊富な経験を積んでおり、よく使われるEclipseプラグインをいくつか書いています(彼のWebサイトを参照)。連絡先はcgrinds@us.ibm.comまたはchris@gstaff.orgです。 |
記事の評価
|