AST を直接たどる
Ast::parse は、パース済みの tree_sitter::Tree を、そのパース元となったソースのバイト列とともに提供します。Ast::as_tree_sitter は、そのツリーを借用参照として渡します。本章では、これを使って独自の構文木解析(ノード種別のカウント、名前による構文要素の検索、パースエラーの検出、シンボルテーブルの抽出など)を、2 回目のパースのコストを払わずに行う方法を示します。
これを使うべきとき
次のような場合には、直接の AST 走査を選んでください。
- プロセス内で構文要素をカウントまたは検索したい場合。CLI での同等機能(
bca count -t <kind>、bca find -t <kind>、レシピ)はファイルごとに外部プロセスを起動しますが、ライブラリ経由なら 1 回のパースと 1 つの Rust ループで済みます。 - パースエラーをプログラムから検出したい場合。tree-sitter は、文法がマッチできなかった箇所に合成の
ERRORノードを出力します。Node::has_errorは O(1) であり(tree-sitter はすべてのノードにエラービットをキャッシュします)、数 MB のソースファイルでもチェックのコストは実質ゼロです。 - 1 回のパースでメトリクスとカスタム解析を組み合わせたい場合。たとえば、カバレッジマッピング、IDE のアウトライン、コードオーナーレポートのために、メトリクス値 と 関数名のリストを同時に取得するケースです。
標準のメトリクスだけが必要なら、analyze または Ast::metrics を使い続けてください。これらはツリーの走査を代行してくれます。直接走査は、メトリクスウォーカーがまだ計算していないものを求める場合のための手段です。
再エクスポートされた tree_sitter を使う
別途 tree-sitter 依存を追加するのではなく、big_code_analysis::tree_sitter から tree_sitter をインポートしてください。この再エクスポートはメトリクスウォーカーのビルドに使われた正確なバージョンに固定されているため、Tree 型は定義上一致します。この再エクスポートが持つ「値は安定ではない」という姿勢については、既存の tree-sitter Tree を再利用する と 安定性とバージョニング を参照してください。
再利用可能な DFS ウォーカー
以下の例の多くは、すべての子孫を深さ優先で走査する必要があります。tree-sitter には、これを 1 ステップあたり O(1) で行う TreeCursor が付属しています(カーソル自体以外のアロケーションはありません)。標準的な走査はインラインで書けるほど短いものです。
#![allow(unused)] fn main() { use big_code_analysis::tree_sitter; /// `tree` のすべてのノードをルートから先行順(pre-order)で訪問し、 /// 各ノードを `visit` に渡します。カーソル自体を除きアロケーションはありません。 fn walk_preorder<F: FnMut(tree_sitter::Node<'_>)>( tree: &tree_sitter::Tree, mut visit: F, ) { let mut cursor = tree.walk(); 'walk: loop { visit(cursor.node()); if cursor.goto_first_child() { continue; } loop { if cursor.goto_next_sibling() { continue 'walk; } if !cursor.goto_parent() { return; } } } } }
The pattern is: visit, descend, climb back up while there is no next sibling, repeat. Every example in this chapter is a thin wrapper around this walker — the code fences below are marked ignore because they assume walk_preorder is already in scope; the matching set of tests in tests/api/book_ast_traversal_examples.rs keeps them honest, so a refactor that broke an example would fail cargo test.
種別ごとにノードをカウントする
AST クエリのレシピ にある bca count -t if_expression -t for_expression -t while_expression のライブラリ版です。
use big_code_analysis::{Ast, LANG, Source};
use std::collections::HashMap;
let ast = Ast::parse(Source::new(
LANG::Rust,
b"fn a() { if true { 1 } else { 2 } } fn b() { for _ in 0..10 {} }",
))
.expect("rust feature enabled");
let mut counts: HashMap<&str, usize> = HashMap::new();
walk_preorder(ast.as_tree_sitter(), |node| {
*counts.entry(node.kind()).or_default() += 1;
});
assert_eq!(counts.get("if_expression").copied().unwrap_or(0), 1);
assert_eq!(counts.get("for_expression").copied().unwrap_or(0), 1);
文字列キー("if_expression"、"for_expression" など)は、tree-sitter 文法のノード型名です。新しい言語でこれらを調べる最も速い方法は、AST 全体を出力する bca dump --paths sample.rs です。
匿名トークン。 ウォーカーは、
"{"や";"、キーワードリテラルのような匿名トークンも含め、tree-sitter が出力するすべてのノードを訪問します。上記のような対象を絞ったcounts.get("if_expression")の検索は影響を受けませんが(匿名トークンは異なる種別名を持ちます)、counts.values().sum()は 名前付き の文法プロダクションの数よりはるかに大きくなります。名前付きノードだけが必要な場合は、ビジター内でtree_sitter::Node::is_named()を使ってフィルタしてください。
種別ごとにノードを検索する
bca find -t unsafe_block のライブラリ版です。
use big_code_analysis::{Ast, LANG, Source};
let ast = Ast::parse(Source::new(
LANG::Rust,
b"fn safe() {} fn risky() { unsafe { } }",
))
.expect("rust feature enabled");
let source = ast.source();
// キャプチャしたスライスは `source` から借用します。ヒットごとの `String` アロケーションはありません。
let mut hits: Vec<((usize, usize), &str)> = Vec::new();
walk_preorder(ast.as_tree_sitter(), |node| {
if node.kind() == "unsafe_block" {
let span = (node.start_position().row, node.end_position().row);
let text = node
.utf8_text(source)
.expect("source is valid utf-8");
hits.push((span, text));
}
});
assert_eq!(hits.len(), 1);
Node::utf8_text(&source[..]) は、ノードのバイト範囲でソースのバイト列をスライスします。Ast::source と組み合わせて使ってください。プリプロセッサ入力を Ast::parse に渡した C++ の場合、source は元の入力ではなく、パーサーが実際に見た 展開後 のバッファです(C++ プリプロセッサに関する注記 を参照)。
パースエラーを検出する
tree-sitter はロスレスです。不正な入力に対してもツリーを返しますが、マッチできなかったノードにはエラーのタグが付きます。最も安価なチェックはルートに対するものです。
#![allow(unused)] fn main() { use big_code_analysis::{Ast, LANG, Source}; let ast = Ast::parse(Source::new(LANG::Rust, b"fn broken(")) .expect("rust feature enabled"); // 何かが失敗したことを確認できるところまでは走査しますが、すべての // エラー箇所を列挙するわけではありません。 assert!(ast.as_tree_sitter().root_node().has_error()); }
問題のノードを列挙するには、ツリーを走査して各ノードをチェックします。
use big_code_analysis::{Ast, LANG, Source};
let ast = Ast::parse(Source::new(LANG::Rust, b"fn broken("))
.expect("rust feature enabled");
let mut error_lines = Vec::new();
walk_preorder(ast.as_tree_sitter(), |node| {
if node.is_error() || node.is_missing() {
error_lines.push(node.start_position().row);
}
});
assert!(!error_lines.is_empty());
Node::is_error() は、tree-sitter が文法にマッチできなかった箇所に挿入する合成の ERROR ノードを示します。Node::is_missing() は、欠落したトークンから回復するためにパーサーが作り出したファントムノードを示します。CLI の bca find -t ERROR レシピも同じノードを使っています。
メトリクスとカスタム走査を組み合わせる
Ast の眼目は「1 回パースして何度も計算する」ことにあります。現実的なパイプラインは、同じパースからメトリクスを計算し、さらに シンボルテーブルも抽出します。
use big_code_analysis::{Ast, LANG, MetricsOptions, Source};
let ast = Ast::parse(Source::new(
LANG::Rust,
b"fn outer() { fn inner() {} } fn alone() {}",
))
.expect("rust feature enabled");
// 1 回のパース。メトリクスウォーカーがそれを使い…
let space = ast
.metrics(MetricsOptions::default())
.expect("walker succeeds");
// …カスタム走査もまったく同じツリーに対して行われます。キャプチャ
// した名前は、関数ごとに新しい `String` をアロケートするのではなく
// `source` から借用します。上記の `find_unsafe_blocks` と同じ
// パターンです。
let source = ast.source();
let mut functions: Vec<&str> = Vec::new();
walk_preorder(ast.as_tree_sitter(), |node| {
if node.kind() == "function_item"
&& let Some(name_node) = node.child_by_field_name("name")
{
let name = name_node
.utf8_text(source)
.expect("source is valid utf-8");
functions.push(name);
}
});
assert_eq!(space.metrics.nom.functions_sum(), 3);
assert_eq!(functions, ["outer", "inner", "alone"]);
Node::child_by_field_name は名前付きの文法フィールドをたどります。これは、シリアライズされた AST(REST の /ast、Ast::dump)の field_name キーに現れるのと同じフィールドです。フィールドベースの検索は、匿名トークン(カンマ、丸括弧など)に対して文法がどの子ノードを出力するかに依存しないため、位置ベースのインデックス参照より堅牢です。
シリアライズ可能な JSON ツリーが欲しい場合
構造化された AST をデータとして扱いたいパイプライン(差分、ワイヤ越しのクエリ、言語非依存のスキーマ作業など)のために、Ast::dump はツリーを、AstNode からなる Serialize 可能な AstResponse として実体化します。これは REST の /ast エンドポイントが生成するのと同じ形状です。パースハンドルに対して呼び出します。
#![allow(unused)] fn main() { use big_code_analysis::{Ast, AstCfg, AstPayload, LANG, Source}; let payload = AstPayload { id: "snippet".to_owned(), file_name: "snippet.rs".to_owned(), code: "fn f() {}".to_owned(), comment: false, span: true, }; let cfg = AstCfg { id: payload.id.clone(), language: "rust".to_owned(), comment: payload.comment, span: payload.span, }; let response = Ast::parse( Source::new(LANG::Rust, payload.code.as_bytes()) .with_name(Some(payload.file_name.clone())), ) .expect("rust feature enabled") .dump(cfg); let json = serde_json::to_string(&response).expect("AstResponse serializes"); println!("{json}"); }
一度きりのプロセス内作業には、上記の as_tree_sitter() ウォーカーのほうが安価です(ノードごとのアロケーションがありません)。シリアライズ可能な所有ツリーが必要なときに Ast::dump を使ってください。
対象外
- 増分再パース — tree-sitter は増分更新のための
tree_sitter::InputEditをサポートしていますが、Astはスナップショットです。ソースの編集を反映するには、新しくAst::parseを実行するか、再エクスポートされたtree_sitterを介してtree_sitter::Parser::parse(&new_source, Some(&old_tree))を直接実行し、その結果をAst::from_tree_sitterに渡してください。 - クレート内部の
big_code_analysis::Nodeラッパー。 これはメトリクスウォーカーの走査ニーズのために公開されていますが、その走査メソッドの大半(kind、child_count、children、cursorなど)はpub(crate)のままです。ライブラリ利用者はas_tree_sitter().root_node()を通じて tree-sitter のNodeにアクセスしてください。それが文書化された継ぎ目です。