AST トラバーサル

bca.analyze(...)メトリクス を返します。すべての関数定義を見つける、docstring を取り出す、py-tree-sitter のマッチャーを移植するなど、構文木 そのものが必要な場合は、一度だけ Ast にパースし、遅延評価される Node ハンドルで走査してください。

Ast ハンドル

bca.Ast.parse(code, language)(または bca.Ast.from_path(path))はソースを一度だけパースし、メトリクスと構文木の両方を取り出せるハンドルを返します。py-tree-sitter で一度、analyze() でもう一度と、二度パースする必要はありません。

import big_code_analysis as bca

ast = bca.Ast.parse("fn main() { let x = 1 + 2; }", "rust")
ast.metrics()      # analyze_source(...) と同じ dict
ast.root_node      # 構文木。遅延走査される(後述)

このハンドルはイミュータブルかつスレッドセーフなので、analyze とまったく同様に ThreadPoolExecutor によるファンアウトと組み合わせられます。

Node ハンドル

ast.root_node は木のルートを遅延評価の Node として返します。ノードごとに dict を実体化する ast.dump() と異なり、Node は保持された木へのカーソルです。読み取るまでコストはかからず、選択的な抽出処理は訪問したノードの分だけコストを払います。

root = ast.root_node
root.kind                       # "source_file"
root.type                       # "source_file"(kind の py-tree-sitter 互換エイリアス)
root.children                   # list[Node]、直接の子ノード
root.child_by_field_name("…")   # フィールドの子ノード、なければ None
node.text                       # ノードのソースバイト列

走査 API は py-tree-sitter を踏襲しています。children / named_childrenparentnext_sibling / prev_sibling(および *_named_* 系)、child(i) / named_child(i)child_by_field_name(name) / children_by_field_name(name)、そして親がそのノードに到達する際に経由する field_name が使えます。

サブツリー全体の走査

walk() はノードとその子孫を先行順(pre-order)でたどる遅延イテレータです。descendants_by_kind(kinds) は一回の走査でマッチを収集し、ast.find(filters)bca count と同じ語彙(functioncallcommentstring、正確な kind 名など)を受け付けて木全体を検索します。

# ファイル内のすべての関数名を遅延評価で取得する。
for fn in ast.find(["function_item"]):
    name = fn.child_by_field_name("name")
    print(name.text.decode())

# あるいは生の文法 kind でサブツリーを絞り込む。
idents = root.descendants_by_kind(["identifier"])

これらには Rust 側の対応物 — Node::preorderNode::descendants_by_kind — があるため、ライブラリの呼び出し側でも同じヘルパーを利用できます。

座標

ノードは自身の位置をあらゆる語彙で報告するため、手作業での変換は不要です。

アクセサ意味
start_byte / end_byteast.source へのバイトオフセット
start_point / end_point0 始まり(row, col)(py-tree-sitter 互換)
start_line / end_line1 始まりの行番号
spandump() が出力する 1 始まりの {start_line, start_col, …, start_byte, end_byte} dict

したがって node.start_line == node.start_point[0] + 1 であり、ast.source[node.start_byte:node.end_byte] == node.text が成り立ちます。

node.typenode.kind の py-tree-sitter 互換エイリアスです。py-tree-sitter の node.type を前提に書かれたマッチャーはそのまま移植できます。bca における正準の綴りは kind のままです。

遅延ノードと dump() の比較

ast.dump() は木をネストした dict として返し、ast.root_node は遅延ハンドルを返します。両者には重要な違いが 2 つあります。

  1. メモリ。 dump() はノードごとに(spanvaluechildren を持つ)dict を 1 つ構築します。小さなファイルでは問題ありませんが、大きなファイルではコストがかさみます。Node による走査では、触れたハンドルの分しかアロケーションが発生しません。

  2. 分類。 Nodekind生の文法 kind です。dump() の kind は bca の Alterator を通過して整形されます。たとえば文字列リテラルのノードは "string" にリネームされ、フラット化 されます(文法上の子ノードが除去されます)。そのため、変換対象のノードでは 2 つの表面が意図的に食い違います。

    ast = bca.Ast.parse('fn f() { let s = "hi"; }', "rust")
    # 生の木では、文字列は引用符や内容の子ノードを保持している。
    raw = next(n for n in ast.root_node.walk() if "string" in n.kind)
    assert raw.children
    

    文法が生成したものをそのまま扱いたい場合(py-tree-sitter のマッチャーを移植するならこちらが正解です)は遅延ノードを、bca が整形した JSON シリアライズ可能なビューが欲しい場合は dump() を使ってください。

ライフタイムとスレッド

Node はその Ast を生かし続けます。パース結果への他の参照をすべて手放しても有効なままなので、Ast をローカルに構築する関数からノード(またはノードのリスト)を返しても安全です。ノードはスレッド間で共有しても安全です。

C/C++ プリプロセッサ。 プリプロセッサ入力付きでパースした Cpp では、ast.source — したがってすべてのノードのバイトオフセット — は、ディスク上のファイルではなく、パーサーが見た 展開後 のソースを指します。

次のステップ

  • メトリクスの選択 — 同じパース結果から必要なメトリクスだけを計算します。
  • CLI の dumpcount コマンドは、dump()find() のシェルレベルの対応物です。