TypeScript コールグラフの静的抽出(ts-morph と解決の難所)
ts-morph の getResolvedSignature / getSymbolAtLocation で明示的な呼び出しは宣言まで辿れ、型エラーのない TS では具象関数への解決率がほぼ 100% になる。静的解析の限界は未解決エッジ(resolved:false)ではなく、コールバック・フレームワーク登録・テンプレート経由で呼び出し構文そのものが存在しない欠損エッジ(recall)に出る。
調査時点: 2026-06。TypeScript のコールグラフ抽出ツールを検討した際、文献調査と実コードでの計測スパイクをまとめたもの。バージョンは当時のもの(ts-morph v28)。
コールグラフは、関数同士の呼び出し関係を表す有向グラフ。ts-morph は TypeScript Compiler API を扱いやすくするラッパーライブラリで、この関係をコードから抽出するために使う。
基盤の選択肢
| 基盤 | 向き | 制約 |
|---|---|---|
| ts-morph | 自作の第一候補。TypeChecker を公式にラップし、getResolvedSignature() と findReferencesAsNodes() を持つ | 低レベル API のドキュメントが薄い。大規模プロジェクトでは解析フェーズと操作フェーズを分けないと program の再構築が繰り返される |
| TypeScript Compiler API 直接 | 依存を増やしたくないとき | 公式 wiki 自身が「安定していない API」と明記。TypeScript のバージョンごとにシグネチャ変更へ追従する |
| @typescript-eslint(typed rules) | ファイル単位の型情報が欲しいとき | ルールはファイル単位で起動し状態を持ち越せない。全体グラフの構築には向かない |
ts-morph で使う API は 4 つ。
typeChecker.getResolvedSignature(callExpression): 呼び出し式から解決済みシグネチャを返す。getDeclaration()で宣言ノードへchecker.getSymbolAtLocation(node)/getTypeAtLocation(node): callee ノードからシンボル・型を取る。import 先の宣言まで追跡するfindReferences()/findReferencesAsNodes(): 宣言からプロジェクト全体の参照を逆引きするtype.getCallSignatures(): 関数型変数が受け付けるシグネチャ一覧
getResolvedSignature はオーバーロードがある関数で最良一致ではなく最初のオーバーロードを返す場合があるという報告(TypeScript Issue #48077)があり、修正済みかは未確認。
解決先の宣言の種類で分類する
ts-morph は型エラーのないコードではすべての呼び出しを「何かの宣言」に解決する。グラフのエッジとして使えるかは、解決先が具象の関数本体か型シグネチャ止まりかで決まる。合成フィクスチャで確認した対応は次のとおり。
| 呼び出しパターン | getResolvedSignature().getDeclaration() の種類 | 分類 |
|---|---|---|
helper()(素の関数) | FunctionDeclaration | 具象。エッジにできる |
obj.method()(具象クラス) | MethodDeclaration | 具象 |
a.speak()(interface 型の変数経由) | interface の MethodSignature | partial。実装クラスの候補列挙には find-implementations の第 2 パスが要る |
cb()(高階関数の引数) | 引数の FunctionType | 実体不明。resolved:false |
table[k]()(文字列キーの dispatch table) | 型エイリアスの FunctionType | 実体不明。resolved:false |
[1,2].map(x => helper()) | Array.map は external、arrow 本体の helper() は FunctionDeclaration | 本体内の呼び出しは捕捉できる |
runTwice(target)(関数を値として渡す) | runTwice の呼び出しは具象に解決。target が呼ばれるエッジは構文に存在しない | 欠損エッジ。resolved:false にすら現れない |
判定キーは解決先宣言の SyntaxKind。FunctionDeclaration / MethodDeclaration / Constructor / accessor / const に束ねた arrow を具象、MethodSignature を partial、FunctionType / Parameter を未解決として扱う。
構文ごとの解決難易度
| 構文 | 分類 | 理由 |
|---|---|---|
| クロスファイル import | 静的に解決できる | getSymbolAtLocation() が import 先の宣言まで追う |
re-export(barrel、export * from) | 静的に解決できる | getSymbolAtLocation() がエイリアスチェーンを飛ばして元の宣言を返す。中間ノードが「ライブ」扱いされなくなる副作用がある(TypeScript Issue #35973) |
| 動的ディスパッチ(オーバーライド・ポリモーフィズム) | partial | 型情報で候補クラスは列挙できるが、実行時にどれが選ばれるかは不定。resolved:false に candidates[] を付けるのが現実的 |
structural typing(implements 宣言のない互換クラス) | partial | 全ファイルを走査すれば候補は出るが、実行時の束縛は不定 |
| 高階関数・コールバック | 構造的に解決できない | 文献上、最良クラスのツールでも見逃す代表パターン |
| デコレータ DI(NestJS など)の配線 | 構造的に解決できない | 注入先は reflect-metadata の実行時情報で決まる。ただし注入されたインスタンスへのメソッド呼び出し自体は後述のとおり解決できる |
計測結果: 解決率と欠損率
getPreEmitDiagnostics が 0 件のリポジトリで、全 CallExpression の解決先を分類した。
| リポジトリ | 性格 | call sites | 具象 first-party | external | resolved:false 相当 |
|---|---|---|---|---|---|
| 自作フロントエンド A | コールバック多め | 2031 | 25.8% | 73.9% | 0.3% |
| 自作フロントエンド B | フロント / CLI | 2364 | 20.6% | 79.0% | 0.3% |
| 自作フロントエンド C | 小規模 | 353 | 22.7% | 76.5% | 0.8% |
| angular-realworld-example-app | DI 24 件 + RxJS 26 件 | 1947 | 13.4% | 85.9% | 0.7% |
- 明示的な呼び出しの具象解決率は約 99%。文献が JavaScript 解析で報告する「30〜60% が未解決」はここには現れない
- call site の 53〜86% は node_modules / lib 行き。first-party 同士の呼び出しは全体の 13〜26% にとどまる
- DI は call edge を壊さない。注入されるインスタンスの配線はフレームワークが行うが、
this.userService.getX()という呼び出しは型付きの明示構文で、first-party のメソッド宣言に解決する。「DI は未解決」は配線の話で、メソッド呼び出しの話ではない - RxJS は inline arrow が主で、
.pipe(map(x => ...))の本体内の呼び出しは捕捉できる
欠損エッジ(first-party 関数が値として渡され、直接の呼び出し構文がない箇所)を JSX prop・オブジェクトプロパティ・配列・引数まで含めて数えた上限値は、具象エッジに対して 0.2〜7.2%(Angular で 2.3%)。内訳は external の高階関数(map / addEventListener)、フレームワーク登録(route component、interceptor / guard 配列)、first-party の高階関数で、分類できる規模だった。
resolved:false と recall は別の軸
文献の CodeQL 60% 未解決、WALA recall 62% という数字は、動的コールグラフと比べて静的解析が見つけられなかったエッジ(recall)の話で、? ノードとして画面に出る未解決エッジの話ではない。
resolved:false: 呼び出し構文はあるが callee の実体が分からない。グラフに?ノードとして出せる- recall 欠損: 呼び出し構文が first-party に存在しない。グラフから消え、
?にも出ない
静的ツールの誠実さは後者をどう示すかで決まる。関数が値として渡された箇所を passed-as-value エッジとして別途拾い、「ここから先は追えない」と明示する設計が成り立つ。計測では first-party 関数の値渡し 6 件のうち 6 件を .ts 内で emit でき、silent drop は 0 だった。
計測で注意すること
- 依存解決の失敗が数字を汚す。typeorm 0.2.45 の
exportsマップにtypes条件がなく “cannot find module” が 19 件出た NestJS リポでは、resolved:false相当が 36.3% に跳ねた。中身は宣言取得不能(NO-DECL)で、動的ディスパッチではない。計測前にgetPreEmitDiagnosticsが 0 であることを確認する @types/jest未設定のテストグローバル(describe/it/beforeAll)も同じく未解決に数えられる- receiver の型が
anyに崩れたメソッド呼び出しは external 扱いになる。型の緩いリポジトリでは劣化する - Angular / Vue の HTML テンプレートのイベントバインディング(
(click)="onSubmit()")は別ファイルにあり、.tsだけを見る解析からは見えない。テンプレートからのみ呼ばれるメソッドは「呼び出し元なし」と誤検出する。React /.tsxは JSX が TS の AST 内にあるので見える
既存ツールの状況(2026-06)
| ツール | 方式 | 状態 |
|---|---|---|
| whyboris/TypeScript-Call-Graph | TypeScript AST | 最終リリース 2020-07。README に “Work in progress” |
| Yo-mah-Ya/ts-call-graph | language service + AST | v0.1.0(2023-11)、個人ツール規模 |
| Persper/js-callgraph | field-based(ICSE 2013) | 同名プロパティを混同、call / apply は無視。TypeScript の型情報は使わない |
| Deskbot/TS-Call-Graph | クラス内のメソッドと属性の関係 | 関数間コールグラフではない |
| madge | ファイル単位の import 追跡 | モジュール依存グラフであり、関数レベルの呼び出し関係は出ない。循環 import の検出が用途 |
| Jelly(cs-au-dk) | points-to 解析 | 研究ツール。Jelly に分けた |
設計への含意
- 明示的な first-party 呼び出しグラフは ts-morph で高精度に作れる。
?ノードがグラフを汚す心配は小さい - interface / abstract 経由の呼び出しは MethodSignature 止まりなので、実装クラスを出すなら find-implementations の第 2 パスを入れる
- 欠損エッジは
passed-as-valueエッジとして拾い、追えない境界を明示する - external が過半なので、node_modules を初期状態で畳むか刈るかを UI 側で決める(コールグラフの可視化の Focus+Context と整合)
- テンプレート系フレームワークはテンプレートパーサを別途入れるか、対象外と明記する
関連
更新履歴
- 2026-06-20: 初版(ローカル調査ノート。文献調査と計測スパイク)
- 2026-06-21: DI / RxJS リポでの追試、テンプレートバインディングの盲点を追記
- 2026-10-17: Wiki へ移植、概念ページとして再構成。冒頭に定義を追加
出典
- https://github.com/dsherret/ts-morph
- https://ts-morph.com/navigation/type-checker
- https://github.com/microsoft/TypeScript-wiki/blob/main/Using-the-Compiler-API.md
- https://github.com/microsoft/TypeScript/issues/35973
- https://typescript-eslint.io/developers/custom-rules/
- https://arxiv.org/abs/2405.07206
- https://drops.dagstuhl.de/entities/document/10.4230/LIPIcs.ECOOP.2022.3
- https://arxiv.org/html/2506.18191v1
- https://github.com/whyboris/TypeScript-Call-Graph
- https://github.com/Persper/js-callgraph
- https://github.com/pahen/madge