Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ASTを使って影響範囲を特定する

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.
Avatar for Nealle Nealle
September 09, 2026

 ASTを使って影響範囲を特定する

下記イベント登壇資料です。
PyCon JP 2026 アフターイベント 〜非公式リジェクトコン〜
https://sansan.connpass.com/event/403591/

数年以上運用されている型ヒント網羅率100%のPythonプロジェクトは(多分)多くないと思っています。
そうしたプロジェクトで、特定の関数・クラス・インスタンス変数の参照箇所を漏らさずに追跡する方法をご紹介します。

Avatar for Nealle

Nealle

September 09, 2026

More Decks by Nealle

Other Decks in Technology

Transcript

  1. ASTを使って 影響範囲 を 特定する 株式会社ニーリー 野呂 有我 2026.9.9 PyCon JP

    2026 アフターイベント 〜⾮公式リジェクトコン〜
  2. 1|自己紹介 氏名 野呂 有我 / Yuga NORO 所属 株式会社ニーリー プラットフォーム本部

    アーキテクチャグループ ・大学院時代に友人と楽譜販売サービスを立ち上げ ・その後、SIer企業に参画 経歴 ・副業としてニーリーでいくつかの開発に携わる ・フリーランスを経て、ニーリーへ ・ニーリーではアーキテクチャグループのGMとして 技術側の意思決定などを行っている 2
  3. 4|イントロ PREMISE 前提 数年以上運⽤されている型ヒント網羅率100%のPythonプロジェクトは (多分)多くないと思っています。 リファレンスが提供されていない外部ライブラリ、Generics や Protocol の提供以前に書かれた 多相関数が残っているのではないでしょうか。

    そうしたプロジェクトで、特定の関数‧クラス‧インスタンス変数の 参照箇所を漏らさずに追跡する。 これが本⽇の主題です。 型ヒントを完璧につけているプロジェクトには、この発表のノウハウは不要です。 でも、そういうプロジェクトはいつだって、今だって狙っています、あなたのことを…
  4. 7|課題 既存⼿段の⽐較 ⼿段 結果 残る問題 ⽂字列grep 約3,000件 appendという⽂字列が⼀般的すぎる。⽬視ではキリがない… LSP 28箇所

    型ヒントが付与されていない箇所を判別できない Agentic Search 29箇所 LSPで網羅できていない何かが増えた。 ただこれで全部であるという根拠が⽰せない 数が⼀致しない。 万全を期すには3,000件を全部⽬視するしかないのか…?
  5. 10|課題 ⽂字列解析の限界 却 下 レシーバの⽂字列で判定する。df.append なら対象。だが df は変数名なので可変。 却 そのファイルで

    pandas がimportされているかを⾒る。 ただ、これでは別ファイルに分離された def appendToDf(df1, df2) のような、 型ヒントのない関数を経由すると取りこぼす。 下 最初に出てくるdfの代⼊を探す。ただし、これは関数スコープで切らないと成⽴しない。 その上、改⾏、空⽩、括弧。考慮事項が無限に増える。 ⽂字列解析で変数を追跡するのは、Pythonのパーサを再実装するに等しい。 却 下
  6. 12|AST ast.parse の出⼒ INPUT import ast OUTPUT Module( body=[ code

    = "x = 1 + 2" parsed_src = ast.parse(code) print(ast.dump(parsed_src, indent=4)) Assign( targets=[ Name(id='x', ctx=Store())], value=BinOp( left=Constant(value=1), op=Add(), right=Constant(value=2)))], type_ignores=[])
  7. 13|AST 基本的な考え⽅ .append() が⽣えている対象(レシーバ)の型を確認し、3つに振り分ける。 1 DataFrame であることが確定すれば、置き換え対象リストに追記する。 2 DataFrame ではないことが確定すれば、その箇所は無視する。

    3 型が⼀意に定まらない場合、不明リストに追加する。 変数定義とcallが同じスコープにあれば⼤抵の場合不明にはならない。 不明リストが2,999個になるようなコードは普通は存在しない。
  8. 14|実装 ⼿順の全体像 STEP 1 STEP 2 STEP 3 STEP 4

    代⼊箇所を全部拾う 呼び出しを全部拾う レシーバの型を判定 完全修飾名に解決 対象ファイルの束縛テーブルを スコープも⼀緒に記録する 作る テーブルと突き合わせ、再帰で import情報でエイリアスを解く 辿る
  9. 15|実装 Step 1 — 代⼊の収集 通常の代⼊ x = expr 注釈付き代⼊

    x: Type = expr def visit_Assign(self, node) -> None: def visit_AnnAssign(self, node) -> None: for target in node.targets: if isinstance(node.target, ast.Name): self._bind_target( if self._scope.kind == "class": target, ("expr", node.value) ) self._scope.add_self_attr(...) self._scope.add( self.generic_visit(node) node.target.id, ("ann", node.annotation) ) self.generic_visit(node) この時点で、追えないものは unknown に分類しておく。 その代⼊がどのスコープにあるかも記録する。
  10. 16|実装 Step 2 — 呼び出しの収集 def visit_Call(self, node: ast.Call) ->

    None: 何か.append(...) の形をした呼び出しをす べて収集する。 func = node.func if isinstance(func, ast.Attribute) \ and func.attr == self.method: self.call_sites.append((node, self._scope)) self.generic_visit(node) この時点では「何か」の部分は⾒ていない。 同時に、その呼び出しがどのスコープにある かも記録する。
  11. 17|実装 Step 3 — レシーバの型判定 判定は再帰的に⾏う def classify_call(call, scope, typer,

    class_mode): # .append() の左側 receiver = call.func.value judgment = typer.type_of(receiver, scope) df = pd.DataFrame(...) なら1段で解決する。 df = create_df() なら、create_df の戻り値注釈 を辿る。 df = other_df.append(...) のようなチェーンも 再帰で追跡する。
  12. 18|実装 Step 4 — import解決 pd.DataFrame を⾒ただけでは pd が何なのかわからない。スコープ内のimport情報で完全修飾名に解決する。 エイリアスに強いのが、⽂字列grep⽐での⼤きな利点

    def resolve_dotted(self, node, scope): import pandas as pd if isinstance(node, ast.Name): found = scope.lookup_chain(node.id) # import束縛から完全修飾名を返す return dotted_values.pop() # "pandas" import pandas from pandas import DataFrame if isinstance(node, ast.Attribute): base = self.resolve_dotted(node.value, scope) return f"{base}.{node.attr}" # "pandas.DataFrame" いずれも追跡できる。 pandas.core.frame.DataFrame のようなサブモ ジュール経由のimportにも対応する。
  13. 19|実装 判定結果の振り分け 判定結果 バケット 意味 TARGET confirmed レシーバがDataFrameと確認できた。要対応 NON_TARGET excluded

    DataFrameではないと証明できた。無視してよい その他 unknown 判定できなかった。⽬視確認が必要 判定できなかったものは、必ずunknownに倒す。
  14. 20|実装 削減効果 ⽬視対象 約 約 unknown 3,000 → 14 約214倍の効率化

    3,000件の内訳が confirmed 28 / excluded 2,900 / unknown 14 になれば、 ⼈間が確認するのは14件で済む! 実際にこのケースで対象だったのは32件で、 Agentic SearchでもLSPでも取りこぼしていたケースを救えました。
  15. 22|所感 • Pythonの標準ライブラリに実⽤的なastが⼊ったのは 2.6 で、 それ以前にもそれ相当のものがあった • 当時からカスタムLintを作成するなどに利⽤されていたが、今回紹介した事例のように 影響範囲を単発で調査する⽤途ではあまり使われてこなかった •

    それは実装が複雑で、⼤抵の場合単発の調査を⾏うより⼯数がかかり、 バグの可能性も排除できないから • しかし、今はそのコストはLLMによってほぼ0に近づいている • LLMは数を数えたり、網羅的に事象を確認させると再現性が担保できないが、 コードにすればテストまで⾃動で書いてくれる • LLMに調査をさせたい場合、ただ頼むのではなく、 ゴールを明確にしてastを利⽤すると便利なことを共有したかった