← ブログに戻る

静的解析コードKGが落とす61%の動的呼び出し

静的解析ベースのコードナレッジグラフは、リフレクション経由の呼び出しを見ません。 blast radius 0 と返ってきても、それは「呼び出し元が無い」ではなく「静的解析からは見えない」の意味であることがあります。決済のように落ちると被害が出る経路でこれを踏むと、影響範囲ゼロと信じてマージしたあとに壊れます。ISSTA 2024 のある論文が、Android の静的解析ツール 13 個を実機実行と突合して、動的に実行されたメソッドの平均 61% を静的解析側が捕捉できていなかった と報告しています (Call Graph Soundness in Android Static Analysis, ISSTA 2024)。Python 側でも PyCG は recall 69.9% です (PyCG, ICSE 2021)。

典型例は Python の getattr(handler, action_name)() 経由です。コードKGが静的呼び出しグラフしか持っていないと、charge を呼ぶ関数を1個も見つけられず、blast radius はゼロで返ります。ゼロと未知が同じ表示になるのが、この構成のいちばん危ないところです。

静的解析コードKGが穴を開ける6パターン

「動的呼び出し」と一括りにしても、6 パターンに分解できます。塞ぎ方は全部違います。

静的呼び出しグラフと実際の呼び出しのズレ

パターン静的解析での扱い
属性アクセスの動的呼び出しgetattr(obj, name)() / obj[key]()取れない
import の動的解決importlib.import_module(name)name が変数なら取れない
リフレクションJava Method.invoke()取れない
依存性注入Spring @Autowired / FastAPI Depends設定を別途読めば部分的
イベント/コールバックNode.js EventEmitterパターンマッチで部分的
メタプログラミングPython メタクラス / Ruby method_missing取れない

決済まわりで踏みやすいのは 1 番目の「属性アクセスの動的呼び出し」です。action_map[action] が payment controller をルックアップしていたのを、コードKGは 1 本のエッジも張れていませんでした。「取れる / 部分的 / 取れない」の境界を知らずにコードKGを信じるのが一番危険です。

塞ぎ方1: パターンマッチで「取れそうな型」だけ拾う

最初に入れて効くのは正規表現/AST パターンマッチです。getattr(obj, "save")() のように 第2引数が文字列リテラル のときだけ拾う。

# tree-sitter ノードから getattr の第2引数を抽出
def detect_static_getattr(node):
    if node.type == "call" and node.children[0].type == "call":
        outer = node.children[0]
        if (outer.children[0].text == b"getattr"
            and outer.arguments[1].type == "string"):
            method_name = outer.arguments[1].text.decode().strip('"')
            return ("dynamic_call", method_name)

実装コストは小さく、精度の上限は recall 70% 付近 (PyCG 相当) です。ただし getattr の第2引数が変数で渡されるコードが多いリポジトリでは、この手法で拾える割合が大きく落ちます。効きはコードの書き方に依存するので、採用の前に自分のリポジトリで実際に何割が文字列リテラル渡しかを数えてから決めるのが確実です。

塞ぎ方2: 動的トレースでコードKGに実行時エッジを継ぎ足す

静的解析の根本的限界を超えるには実行時情報を混ぜます。

  • pytest 実行中に sys.settrace で関数呼び出しを記録
  • 本番ログから「実際に呼ばれた関数」を抽出 (関数名だけ抽出、引数は落とす)
  • coverage.py の中間データを利用

これらから拾ったエッジをコードKGに CALLS_DYNAMIC として追加します。confidence は 0.9-1.0。「実際に呼ばれた記録」なので静的推論より確度が高い扱いにします。

DyPyBench (2024) が 50 OSS 約 68 万 LOC で動的トレース併用の効果を定量化していて、静的解析のカバレッジを大きく拡張できることが示されています。

制約は 3 つ。

  • テストカバレッジが弱い箇所は拾えない
  • 本番ログを触る場合は PII 対応が必須 (関数名だけ抽出、引数値は落とす)
  • sys.settrace は実行を数倍遅くする — CI 常時は無理、夜間バッチで足りる

塞ぎ方3: LLM に「呼ばれそうなメソッド候補」を推論させる

パターンマッチと動的トレースを入れても、action_map[action] の穴は残ります。ここは LLM (Pass 2 型) に頼ります。

prompt = f"""
Given this code context, what methods are likely called by `{dynamic_call_source}`?
Return up to 3 candidates with confidence scores.

Code context:
{code_snippet}

Available methods on `obj` (from KG):
{method_list_from_kg}
"""

EMSE 2025 (論文) は 24 個の LLM を Python/JS で評価して、型推論では LLM が伝統的手法を上回る一方、コールグラフでは静的解析 (PyCG / Jelly) が依然優位 と報告しています。単独で使うと精度が振れるので、コードKGから取れた「オブジェクトの型 + そのクラスの持つメソッド一覧」をコンテキストに必ず入れる運用にします。

塞ぎ方4: 設定ファイル/アノテーションを別パスで読む

依存性注入は設定を読めば静的に解決できます。

# Spring の @Configuration クラスから @Bean を抽出
for cls in classes_with_annotation("@Configuration"):
    for method in cls.methods:
        if method.has_annotation("@Bean"):
            bean_type = method.return_type
            for impl in find_implementations(bean_type):
                add_edge(method, impl, type="INJECTS", confidence=0.95)

Spring の @Autowired UserService userService;UserServiceImpl に解決される、というエッジがコードKGに載ります。同じ手法で FastAPI Depends、NestJS @Injectable、Guice binding も拾えます。

4戦略 × 動的呼び出しパターンの適用マトリクス

どの戦略がどのパターンに効くかは 1 枚に整理しないと現場で選べません。

4戦略と動的呼び出しパターンの適用マトリクス

現実的な順序は「戦略1 (パターンマッチ) を全部に入れる → 戦略4 (設定読み) をフレームワーク単位で追加 → 戦略2 (動的トレース) を CI 体力があれば夜間バッチで → 戦略3 (LLM) を残る穴の最終手段」。この 4 段を順に積むと、動的呼び出しの取りこぼしをかなり塞げます。ただしリフレクション中心のコードベースでは塞げる比率が下がります。

残った穴は「見える化」する — UNKNOWN_DYNAMIC フラグ

4 戦略を尽くしても、Java reflection の一部やメタプログラミングは根本的に解決できません。TOSEM 2017 (Understanding Java Reflection) が、Java reflection の完全解析は研究レベルでも sound な解決に至っていない、と整理しています。

このとき 「解決できない箇所を隠す」より「解決できない箇所を明示する」ほうが安全 です。コードKG側に UNKNOWN_DYNAMIC フラグを立てて、blast radius 計算のときに「ここから先は静的解析では追えない」と表示させます。PaymentProcessor.charge の周辺に UNKNOWN_DYNAMIC が立っていれば、レビュワーは「本当に影響ゼロ?」を確認できます。ゼロと未知が同じ表示になる問題は、これで消えます。

本番投入前の運用: 「危険箇所は静的解析結果を信じない」

事故のあと変えた運用ルールは 3 つ。

  1. コードKGが blast_radius = 0 を返しても、決済 / 認証 / 権限 / 外部通知の 4 領域は自動マージを外す
  2. UNKNOWN_DYNAMIC フラグが変更ファイルの周辺にあれば PR に警告コメントを自動投稿
  3. 危険領域だけは Claude Code / ChatGPT Codex の full-context PR レビューを並走させる (このコスト対比は ChatGPT Codex vs Claude Code: 3.4x Cost Gap Across 47 PRs (2026) にまとめてあります)

コードKG単独で完結させないのがコツです。静的解析ベースのコードKGが最強なのは、tree-sitter ベースで PR レビューのトークンを削るような 「読む量」の圧縮 に効く場面で、動的呼び出しの safety net は別レイヤーで組む、という分業が現実的でした。

まとめ

  • 静的解析ベースのコードKGは Android で平均 61%、Python で recall 70% 程度の動的呼び出しを取りこぼす
  • 動的呼び出しは 6 パターンに分解でき、塞ぎ方は 4 戦略で覆えるが単独では不十分
  • 4 戦略を順に積んで残った穴は UNKNOWN_DYNAMIC フラグで見える化する
  • 決済 / 認証 / 権限 / 外部通知の 4 領域だけは静的解析結果を信じず、full-context 型の LLM レビューを並走させる

コードKGを実務で組む設計 — 静的グラフ + 動的トレース + LLM Pass 2 + 設定読みの 4 層をどう繋ぐか — は書籍にまとめました。ナレッジグラフ実務ガイド

ナレッジグラフ活用大全 関連書籍 ナレッジグラフ活用大全 ナレッジグラフ 活用大全 | GraphRAG・Neo4j・RDF・Property Graph・Emotion AI の実践書 書籍ページを見る →
この記事が答えている課題 課題の地図を見る →