[実践] Krugle Searchで類似コードを横断調査してみる - krugle-jp/Krugle-Library GitHub Wiki

— grep から始めて、コード骨格検索まで試すハンズオン —

このコンテンツでは、Krugle の創成期からの代表機能である Krugle Search を、弊社運営のデモサイトを使ったハンズオン形式で紹介します。

題材は、とあるレポジトリ内での標準化に向けたユーザ入力項目の null チェック実装状況の全体調査という初歩的なケースです。


はじめに

「この実装パターン、プロジェクト全体でどれくらい使われているんだろう?」

「grep では追い切れない“似たコード”も見つけたい」

こういう場面、日々の開発でかなり多いと思います。

今回は、Krugle Search を使って、Java コードベース全体に散らばる NullPointerException の実装箇所を調べてみます。

まずは一般的な grep から入り、そのあと Krugle Search の通常検索、さらに スニペット検索 / 揺らぎ検索 まで試します。


デモ環境

弊社デモサイト

https://kse.krugle-lab.org/k7/

使用するリポジトリ

erp-backend http://github.com/open-erp-systems/erp-backend

今回の例題

今回のテーマはこれです。

標準化に向けたユーザ入力項目の null チェック実装状況の全体調査

模範例として使うのは、LoginController.java にある次のコードです。

if (userRepository == null) {
    throw new NullPointerException("user repository cannot be null.");
}

このような null チェック + 例外送出 の実装が、他にどこにあるのかを調べていきます。


まずは普通に grep してみる

最初に思いつくのは、やはり grep です。

% grep -r "new NullPointerException" 
erp-core/src/main/java/com/jukusoft/erp/core/module/base/service/login/LoginController.java:            throw new NullPointerException("user repository cannot be null.");
erp-library/src/main/java/com.jukusoft/erp/lib/controller/AbstractController.java:            throw new NullPointerException("vertx cannot be null.");
erp-library/src/main/java/com.jukusoft/erp/lib/controller/AbstractController.java:            throw new NullPointerException("app context cannot be null.");
erp-library/src/main/java/com.jukusoft/erp/lib/controller/AbstractController.java:            throw new NullPointerException("logger cannot be null.");
erp-library/src/main/java/com.jukusoft/erp/lib/cache/ICache.java:            throw new NullPointerException("array cannot be null.");
erp-library/src/main/java/com.jukusoft/erp/lib/cache/impl/LocalMemoryCache.java:            throw new NullPointerException("json object cannot be null.");
erp-library/src/main/java/com.jukusoft/erp/lib/cache/impl/HazelcastCache.java:            throw new NullPointerException("json object cannot be null.");
erp-library/src/main/java/com.jukusoft/erp/lib/cache/impl/FileSystemCache.java:            throw new NullPointerException("json object cannot be null.");
erp-library/src/main/java/com.jukusoft/erp/lib/module/AbstractModule.java:            throw new NullPointerException("vertx cannot be null.");
erp-library/src/main/java/com.jukusoft/erp/lib/module/AbstractModule.java:            throw new NullPointerException("app context cannot be null.");
erp-library/src/main/java/com.jukusoft/erp/lib/module/AbstractModule.java:            throw new NullPointerException("logger cannot be null.");
erp-library/src/main/java/com.jukusoft/erp/lib/module/AbstractModule.java:                        throw new NullPointerException("injected object cannot be null.");
erp-library/src/main/java/com.jukusoft/erp/lib/module/AbstractModule.java:                        throw new NullPointerException("injected object cannot be null.");
erp-library/src/main/java/com.jukusoft/erp/lib/module/AbstractModule.java:                    throw new NullPointerException("injected object cannot be null.");
erp-library/src/main/java/com.jukusoft/erp/lib/module/AbstractModule.java:            throw new NullPointerException("event name cannot be null or empty");
erp-library/src/main/java/com.jukusoft/erp/lib/module/AbstractModule.java:            throw new NullPointerException("handler cannot be null.");
erp-library/src/main/java/com.jukusoft/erp/lib/module/AbstractModule.java:            throw new NullPointerException("start future cannot be null.");
erp-library/src/main/java/com.jukusoft/erp/lib/message/response/ApiResponseCodec.java:            throw new NullPointerException("json data of api response '" + res.getClass().getSimpleName() + "' cannot be null.");
erp-library/src/main/java/com.jukusoft/erp/lib/context/AppContextImpl.java:            throw new NullPointerException("vertx cannot be null.");
erp-library/src/main/java/com.jukusoft/erp/lib/context/AppContextImpl.java:            throw new NullPointerException("logger cannot be null.");
erp-library/src/main/java/com.jukusoft/erp/lib/context/AppContextImpl.java:            throw new NullPointerException("hazelcast instance cannot be null.");
erp-library/src/main/java/com.jukusoft/erp/lib/context/AppContextImpl.java:            throw new NullPointerException("session manager cannot be null.");
erp-library/src/main/java/com.jukusoft/erp/lib/context/AppContextImpl.java:            throw new NullPointerException("database manager cannot be null.");
erp-library/src/main/java/com.jukusoft/erp/lib/context/AppContextImpl.java:            throw new NullPointerException("cache manager cannot be null.");
erp-library/src/main/java/com.jukusoft/erp/lib/context/AppContextImpl.java:            throw new NullPointerException("permission manager cannot be null.");
erp-library/src/main/java/com.jukusoft/erp/lib/context/AppContextImpl.java:            throw new NullPointerException("permission service cannot be null.");
erp-library/src/main/java/com.jukusoft/erp/lib/context/AppContextImpl.java:            throw new NullPointerException("permission manager cannot be null.");
erp-library/src/main/java/com.jukusoft/erp/lib/permission/PermissionService.java:            throw new NullPointerException("group permission cache cannot be null.");
erp-library/src/main/java/com.jukusoft/erp/lib/permission/PermissionService.java:            throw new NullPointerException("json array cannot be null.");
erp-library/src/main/java/com.jukusoft/erp/lib/permission/PermissionService.java:            throw new NullPointerException("group permission cache cannot be null.");
erp-library/src/main/java/com.jukusoft/erp/lib/database/impl/DatabaseManagerImpl.java:            throw new NullPointerException("vertx instance cannot be null.");
erp-library/src/main/java/com.jukusoft/erp/lib/database/impl/DatabaseManagerImpl.java:            throw new NullPointerException("mysql database cannot be null.");
erp-library/src/main/java/com.jukusoft/erp/lib/database/impl/DatabaseManagerImpl.java:            throw new NullPointerException("hazelcast instance cannot be null.");
erp-server/src/main/java/com/jukusoft/erp/server/message/ResponseGenerator.java:            throw new NullPointerException("json data cannot be null.");
erp-data/src/main/java/com/jukusoft/data/entity/User.java:            throw new NullPointerException("row cannot be null.");
erp-data/src/main/java/com/jukusoft/data/entity/GroupMember.java:            throw new NullPointerException("row cannot be null.");
erp-data/src/main/java/com/jukusoft/data/entity/Group.java:            throw new NullPointerException("row cannot be null.");
erp-data/src/main/java/com/jukusoft/data/repository/PermissionRepository.java:            throw new NullPointerException("group permission cache cannot be null.");
erp-data/src/main/java/com/jukusoft/data/repository/PermissionRepository.java:            throw new NullPointerException("group repository cannot be null.");
erp-data/src/main/java/com/jukusoft/data/repository/GroupRepository.java:            throw new NullPointerException("group cache cannot be null.");
erp-data/src/main/java/com/jukusoft/data/repository/MenuRepository.java:            throw new NullPointerException("menu cache cannot be null.");
app-server/src/main/java/com/jukusoft/erp/app/server/impl/DefaultAppServer.java:                    throw new NullPointerException("vertx cannot be null.");
app-server/src/main/java/com/jukusoft/erp/app/server/impl/DefaultAppServer.java:            throw new NullPointerException("module cannot be null.");

この結果を見ると、

  • 該当箇所は 43 箇所

  • 該当ファイルは 19 ファイル

でした。

grep はやはり速いですし、最初の荒い探索には十分便利です。

ただし、ここで少し考えたくなります。

  • 文字列が少し違ったら?

  • 例外クラス側から見たら?

  • 文面は違うけれど、処理の骨格が同じコードは?

こういう問いに対して、Krugle Search が効いてきます。


Krugle Search で同じ調査をしてみる

デモサイトから、まずは grep と同じ感覚で検索してみます。

https://kse.krugle-lab.org/k7/

① 文字列でそのまま検索

結果は 19 件。

grep と同様のファイル群に到達できます。

しかも、単に結果一覧が出るだけではなく、ブラウザ上でそのままソースコードを参照できます。

ここは地味ですが、ユーザによってはかなり便利です。


② 例外クラス側から検索する

Krugle Searchの特徴的な機能の一つとして「推奨フィルター」というのがあります。

これは特定の文言が何をしているか、という観点で検索できる機能です。

今回のケース、別の切り口として例外クラスの側から検索してみます。

これでも結果は 同じく 19 件でした。

ここで面白いのは、単なる全文字列一致ではなく、コード上の呼び出しという観点で検索できることです。

文字列が多少違っても、「その例外を呼んでいる箇所」という軸で捉えられるのは、構造を意識した検索ならではです。

その他、Krugle Searchの基本的な使い方についてはこちらにありますのでぜひ確認ください。


ちなみに KLiP 側からも使えます

余談ですが、ここまでの検索は KLiP の 🔍 マークにある KrugleKnow 検索からでも行えます。

さらに便利なのは、その先です。

  • ファイル名をクリックすると、エディタ上でファイル参照 ※もちろん元ファイルの編集やコミットの権限はありません

  • 右端の 📎 クリップマークで、AI コンテキストに追加

という流れが取れます。

つまり、開発中の VS Code から、サーバ上の別プロジェクトも含めてコードを検索・参照し、そのまま AI の参考情報として使えるわけです。

この導線はかなり便利だと思います。


Web 検索結果は Excel にも出力できる

また余談ですが、Krugle Search の Web 検索結果はExcel に出力することもできます。

これが効くのは、たとえば次のような場面です。

  • 横断調査結果をレビューしたい

  • チームで共有したい

  • 改修候補を一覧で整理したい

  • 棚卸しの証跡として残したい

コード検索の結果が、そのまま報告資料寄りの形に落とせるのは、運用上かなり助かります。


ここからが本題

スニペット検索を試す

Krugle Search の特徴が色濃いところはここからです。

たとえば LoginController.java を開くと、先ほどの模範例が出てきます。

if (userRepository == null) {
    throw new NullPointerException("user repository cannot be null.");
}

この 3 行のスニペットを選択してコピーしてください。

次に、検索キーワード入力窓の横にある ⚙️ 歯車アイコンを押して、高度な検索へ移動します。


コードスニペット検索に切り替える

高度な検索画面で、コードスニペット検索のタブに移動し、先ほどのスニペットを貼り付けます。

設定は次のようにしてみてください。

  • ドキュメントタイプ: Java

  • プロジェクト名: 必要に応じて指定 (ノイズを減らしたい場合に有効)

ここで、そのまま検索してもよいのですが、少し工夫してみます。


可変になりそうな部分を削ってみる

貼り付けたスニペットの中で、他の箇所では違っていそうな部分を削ります。

この例だと、まず削りたくなるのは次のあたりです。

  • userRepository

  • "user repository cannot be null."

このあたりは、実装ごとに変わる可能性が高いからです。

その状態で検索してみると、結果は 先ほどと同じ 19 件になります。

ここでポイントなのは、Krugle がこれを単なる長めの文字列一致として見ていないことです。


スニペット検索は「文字列検索」ではなく「骨格検索」

Krugle のスニペット検索では、入力したコードを単純な文字列ではなく、**コードの骨格(スケルトン)**として認識しています。

つまり、

  • 主要な構造は残す

  • 変数名やメッセージのような可変部分は揺らぎとして扱う

ということです。

この「揺らぎ」の幅は、高度な検索画面中段にある 精度スライダーで調整できます。

デフォルト値

0.5

挙動

  • 値を上げる → より厳密に一致

  • 値を下げる → より広く揺らぎを許容


さらに骨格を削ってみる

次に、もう少し大胆にやってみます。

今度は new NullPointerException() の部分も消してみます。

すると、検索結果が 19 件から 18 件に減ります。

「広くなりそうなのに、なぜ減るの?」と思うかもしれませんが、ここが面白いところです。

骨格要素を減らしたことで、逆に “何を同じパターンとみなすか” の軸が弱くなり、一部のコードが許容範囲から外れたと考えられます。


脱落した 1 件は何か

脱落したのは ApiResponseCodec.java でした。

if (res.data == null) {
    throw new NullPointerException("json data of api response '" + res.getClass().getSimpleName() + "' cannot be null.");
}

このコードは、他のコードと比べると少し複雑です。

  • 条件式の対象が違う

  • メッセージ組み立てが複雑

  • 文脈的にも単純な null guard より一段処理が重い

そのため、与えた骨格が弱くなった結果、このコードとの差異が デフォルト精度 0.5 の揺らぎ許容範囲を超えたと解釈できます。


では精度を下げてみる

次は逆に、精度を 0.1 まで下げて再検索してみましょう。

すると結果は、今度は 20 件に増えます。

この中には、

  • 先ほど落ちた ApiResponseCodec.java

  • 新たに DefaultCacheManager.java

が含まれます。

ここで何が起きているかというと、Krugle が次のような違いまで「揺らぎ」として許容し始めているわけです。

  1. 処理内容の複雑さの違い

  2. throw される例外クラスの違い

  3. null 条件自体の違い

    たとえば == と !=

つまり、かなり広い意味での「似たコード」を拾えるようになります。


スニペット検索 / 揺らぎ検索をどう使うか

この機能は、なんとなく使うより、少し考え方を持って使うとかなり役に立ちます。

おすすめの進め方は次の通りです。

使い方のコツ

  • 最初はある程度のボリュームがあるスニペットから始める

  • 明らかに変動しそうな部分を削る

  • さらに広げたい場合は、変動の可能性がある箇所を少しずつ削る

  • 必要に応じて 精度スライダーで揺らぎ幅を調整する

この順序でやると、検索の意図を見失いにくいです。


まとめ

今回の NullPointerException 調査を通して見えてきたのは、Krugle Search が単なる検索窓ではなく、コード探索のための思考補助ツールとしてかなり強いということです。

特に価値があるのは次の点です。

Krugle Search の強み

  • 単純な文字列一致だけでなく、構造寄りの検索ができる

  • functioncall: のように、別視点から同じ対象へ辿れる

  • Web 上でそのままコード参照できる

  • KLiP / VS Code と連携して、AI 活用のコンテキスト収集にも使える

  • 検索結果を Excel 出力できる

  • スニペット検索では、コード骨格と揺らぎを使った類似検索ができる

  • 精度スライダーで、厳密検索と探索的検索を行き来できる


向いている場面

この手の検索は、次のようなシーンで特に効きます。

  • 実装標準の浸透状況調査

  • 横断的な null チェック / 例外処理の棚卸し

  • 類似実装の探索

  • 改修影響範囲の洗い出し

  • AI に渡す参考コードの収集


参考リンク

Krugle Search デモ

https://kse.krugle-lab.org/k7/

対象リポジトリ

https://github.com/open-erp-systems/erp-backend/


おわりに

grep は今でも強いです。

ただ、似ているけれど同じではないコードまで視野を広げたいとき、Krugle Search の真価が出てきます。

特にスニペット検索 / 揺らぎ検索は、最初は少しクセがあるように見えて、使い方が分かるとかなり実践的です。

コードベース全体の標準化状況を見たいとき、あるいは修正漏れを探したいとき、ぜひ一度この流れで試してみてください。