Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

AST トラバーサル

bca.analyze(...) gives you metrics. When you need the syntax tree itself (to find every function definition, pull a docstring, or port a py-tree-sitter matcher), parse once into an Ast and walk it with lazy Node handles. This page assumes you know what a tree-sitter parse tree is; the Node API mirrors py-tree-sitter’s.

Ast ハンドル

bca.Ast.parse(code, language) (or bca.Ast.from_path(path)) parses the source once and hands back a handle you can draw both metrics and the tree from, instead of parsing twice (once in py-tree-sitter, once in 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_children、parent、next_sibling / prev_sibling(および *_named_* 系)、child(i) / named_child(i)、child_by_field_name(name) / children_by_field_name(name)、そして親がそのノードに到達する際に経由する field_name が使えます。

サブツリー全体の走査

walk() is a lazy pre-order iterator over a node and its descendants; descendants_by_kind(kinds) collects the matches in one pass; and ast.find(filters) searches the whole tree, accepting the same vocabulary as bca count (all, function, call, comment, string, error, an exact kind, or a numeric kind_id):

# ファイル内のすべての関数名を遅延評価で取得する。
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"])

These have Rust counterparts, Node::preorder and Node::descendants_by_kind, so library callers get the same helpers.

座標

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

アクセサ意味
start_byte / end_byteast.source へのバイトオフセット
start_point / end_point0 始まりの (row, col)(py-tree-sitter 互換)
start_line / end_line1 始まりの行番号
spanthe {start_line, start_col, …, start_byte, end_byte} dict dump() emits: lines and columns are 1-based; byte offsets are 0-based and half-open

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

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

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

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

  1. Memory. dump() builds one dict (with span, value, children) per node: fine for small files, costly for large ones. A Node walk allocates only the handles you touch.

  2. Taxonomy. A Node’s kind is the raw grammar kind. dump() kinds pass through bca’s Alterator and are curated; for example, string-literal nodes are renamed to "string" and flattened (their grammar children removed). So the two surfaces intentionally disagree on altered nodes:

    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++ preprocessor. The Python Ast takes no preprocessor inputs: Ast.parse accepts only code, language and name, and Ast.from_path does not run the C/C++ preprocessor pass. So ast.source, and every node’s byte offsets, always index into the bytes the parser was given: the code you passed to parse, or the file as from_path read it (through the same EOL-normalising reader analyze uses). The expanded-source behaviour described in the Rust chapter applies only to a Rust Source built with preprocessor results.

次のステップ

  • Metric selection: compute only the metrics you need from the same parse.
  • The CLI’s dump and count commands: the shell-level equivalents of dump() and find().