AST を直接たどる
Ast::parse gives you a parsed tree_sitter::Tree together with the source bytes it was parsed from; Ast::as_tree_sitter hands that tree out as a borrowed reference. This chapter shows how to use it to drive your own syntax-tree analysis (counting node kinds, finding constructs by name, detecting parse errors, or pulling out a symbol table) without paying for a second parse. It assumes you know the tree-sitter node model: a tree of nodes, each with a kind name, a byte range and optional named fields.
これを使うべきとき
次のような場合には、直接の AST 走査を選んでください。
- You want to count or find syntactic constructs in-process. The CLI equivalents (
bca count -t <kind>,bca find -t <kind>, recipe) mean launching a separate process and parsing its output; the library path is one parse and one Rust loop. - You want to detect parse errors programmatically. Tree-sitter emits a synthetic
ERRORnode anywhere the grammar could not match.Node::has_erroris O(1) because tree-sitter caches the error bit on every node, so the check is free even on a multi-MB source file. - You want to mix metrics with custom analysis in one parse: for example, capture metric values and a list of function names for a coverage mapping, an IDE outline, or a code-owner report.
If you only need standard metrics, stay with analyze or Ast::metrics, which walk the tree for you. The direct path is for things the metric walker does not already compute.
再エクスポートされた 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 tests in tests/api/book_ast_traversal_examples.rs keep 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);
The string keys ("if_expression", "for_expression", …) are the tree-sitter grammar’s node-type names. To discover a language’s node-type names, run bca dump --paths sample.rs, which prints the full AST.
Anonymous tokens. The walker visits every node tree-sitter emits, including anonymous tokens like
"{",";", and keyword literals. The targetedcounts.get("if_expression")lookups above are unaffected, since anonymous tokens have different kind names, butcounts.values().sum()would be much larger than the count of named grammar productions. Filter withtree_sitter::Node::is_named()inside the visitor if you only want named nodes.
種別ごとにノードを検索する
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();
// Captured slices borrow from `source`: no per-hit `String` allocation.
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[..]) slices the source bytes by the node’s byte range. Pair it with Ast::source: for C++ with preprocessor inputs supplied to Ast::parse, source is the expanded buffer the parser saw, not the original input (see the C++ preprocessor note).
パースエラーを検出する
Tree-sitter is lossless: even on malformed input it returns a tree, with the nodes it could not match tagged as errors. Check the root for a cached error flag:
#![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");
// …and so does the custom walk, against the very same tree. The
// captured names borrow from `source` rather than allocating a fresh
// `String` per function, the same pattern as `find_unsafe_blocks`
// above.
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 walks the named grammar fields, the same fields that appear in the field_name key of the serialized AST (REST /ast, Ast::dump). Field-based lookup survives grammar changes that positional indexing does not, because it does not depend on which anonymous tokens (commas, parentheses, and so on) the grammar emits as children.
シリアライズ可能な JSON ツリーが欲しい場合
For pipelines that want a structured AST as data (diffing, queries on the wire, language-agnostic schema work), Ast::dump materializes the tree as a Serialize-able AstResponse of AstNodes. This is the same shape the REST /ast endpoint produces. Call it on the parse handle:
#![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 を使ってください。
Ast::dump walks the tree with one cursor, the same way the walker above does. Two siblings name the strategy explicitly: Ast::dump_cursor is that walk, and Ast::dump_indexed reaches each child through Node::child and its field through Node::field_name_for_child instead. They produce the same tree; the difference is cost. Both lookups scan toward the requested child. Lookup costs O(log n) for lists stored as balanced repeat() structures and O(index) for children stored in a flat array. Error recovery can create such flat arrays from skipped tokens. Enumerating a wide flat node by index is therefore quadratic in its width. Prefer the cursor walk for a whole-tree export, field lookup (child_by_field_name) for a grammatical role, and indexed access for picking one slot. A loop that needs repeated random access to a wide list should collect the children once and index the Vec.
対象外
- Incremental reparse. Tree-sitter supports
tree_sitter::InputEditfor incremental updates, butAstis a snapshot. To reflect a source edit, build a freshAst::parseor drivetree_sitter::Parser::parse(&new_source, Some(&old_tree))directly via the re-exportedtree_sitterand feed the result throughAst::from_tree_sitter. - Everything on
big_code_analysis::Nodethat is not listed inSTABILITY.md. The wrapper is re-exported, and its traversal methods (kind,child_count,children,cursor,preorder,child,child_by_field_name, and so on) arepub, but only theSTABILITY.mdaccessor list is shape-stable; the helpers that take aSearchorAncestorsargument are not. For anything else, reach the tree-sitterNodethroughas_tree_sitter().root_node(), the documented seam.