← ブログに戻る

静的解析コードKGが61%見逃した動的呼び出しで本番決済落ちた話

静的解析ベースのコードナレッジグラフが「blast radius 0」と返した PaymentProcessor.charge を、リフレクション経由で呼んでいた決済キャンセル処理が本番で壊した。 深夜のロールバックが終わってから、静的解析が動的呼び出しをどれだけ取りこぼすかを調べ直しました。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 は本当にゼロで返してきました。信じてマージした 30 分後、決済キャンセルの Slack 通知が止まり、そこから 2 時間の緊急ロールバックです。

静的解析コード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)

私のリポジトリだと、getattr 呼び出しの 3-4 割はこれで捕まりました。実装コスト小、精度は上限 recall 70% 付近 (PyCG 相当)。手元プロジェクトによって効きが違うので、まず 1 週間ぶん計測してから採用可否を決めるのが確実です。

塞ぎ方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 段を順に積んでいくと、動的呼び出しの取りこぼしを 7-8 割まで塞げた感触があります (リフレクション中心のコードベースだと比率が下がるはずです)。

残った穴は「見える化」する — 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 レビューのトークンを削るような 「読む量」の圧縮 (Code Knowledge Graph で PR レビュー 91% を 30 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 の実践書 書籍ページを見る →