| Crate | Version | License |
|---|---|---|
tree-sitter |
0.26.x | MIT |
tree-sitter-rust |
0.24.x | MIT |
tree-sitter-go |
0.25.x | MIT |
tree-sitter-python |
0.25.x | MIT |
tree-sitter-typescript |
0.23.x | MIT |
All compile C code statically into the binary. 4 languages ≈ 454KB release binary.
Tree-sitter 0.26 removed descendants(). Use cursor-based DFS:
fn walk(node: &tree_sitter::Node, source: &str) {
let mut cursor = node.walk();
if cursor.goto_first_child() {
loop {
process(&cursor.node(), source);
walk(&cursor.node(), source); // recurse
if !cursor.goto_next_sibling() { break; }
}
}
}In v0.26, start_position().row and .column return usize directly. No as usize cast needed. Clippy flags unnecessary casts.
Grammar crates expose LANGUAGE as a Language constant (not a language() function):
// CORRECT
Some(tree_sitter_rust::LANGUAGE.into())
// WRONG — doesn't exist
Some(tree_sitter_rust::language())Problem: child_by_field_name("name") returns None for many grammars, even when the node clearly has a name child.
Root cause: Different grammar authors use different conventions. Rust grammar doesn't define a "name" field on function_item — the identifier is just a child with kind "identifier", not a named field.
Pattern — field-name-first + kind-fallback:
fn node_name(node: &tree_sitter::Node<'a>, source: &str) -> String {
// Try field-based access (works for some grammars)
if let Some(n) = node.child_by_field_name("name") {
return node_text(&n, source);
}
// Fallback: find by kind
if let Some(n) = find_child_by_kind(node, &[
"identifier", "type_identifier", "field_identifier"
]) {
return node_text(&n, source);
}
String::new()
}
fn find_child_by_kind<'a>(parent: &tree_sitter::Node<'a>, kinds: &[&str]) -> Option<tree_sitter::Node<'a>> {
let mut c = parent.walk();
if c.goto_first_child() {
loop {
if kinds.contains(&c.node().kind()) {
return Some(c.node());
}
if !c.goto_next_sibling() { break; }
}
}
None
}| Grammar | Function name | Struct/Type name | Method name | Trait/Interface name |
|---|---|---|---|---|
| Rust | identifier |
type_identifier |
identifier (inside impl) |
type_identifier |
| Go | identifier (field: "name" exists) |
identifier (inside type_spec) |
identifier (field: "name" exists) |
N/A |
| Python | identifier (field: "name" works) |
identifier (field: "name" works) |
identifier (field: "name" works) |
N/A |
| TypeScript | identifier (field: "name" works) |
identifier (field: "name" works) |
property_identifier |
type_identifier |
Problem: All exported declarations are wrapped in export_statement nodes. Direct children of root are export_statement, not function_declaration/class_declaration.
program
export_statement ← root child
export ← keyword
function_declaration ← actual declaration inside
function
identifier ← name here
Fix: Unwrap export_statement by recursing into non-keyword children:
"export_statement" => {
let mut c = node.walk();
if c.goto_first_child() {
loop {
let inner = c.node();
if inner.kind() != "export" && inner.kind() != ";" {
process_ts_node(&inner, source, file_path, nodes, edges);
}
if !c.goto_next_sibling() { break; }
}
}
}Also: const in TS is lexical_declaration (not variable_declaration). Handle both.
Problem: const_declaration and var_declaration nodes contain an intermediate const_spec/var_spec child. The actual identifier (the name) is inside this spec node, NOT directly under the declaration.
const_declaration ← root child kind
const ← keyword
const_spec ← intermediate node — name is HERE
identifier ← "DefaultPort"
=
integer_literal
var_declaration ← root child kind
var ← keyword
var_spec ← intermediate node — name is HERE
identifier ← "db"
:
type_identifier ← "sql.DB"
Fix: After matching const_declaration/var_declaration, walk into children looking for const_spec/var_spec, then extract the name from there:
"const_declaration" => {
let mut cc = child.walk();
if cc.goto_first_child() {
loop {
if cc.node().kind() == "const_spec" {
let name = node_name(&cc.node(), source);
// ... create AstNode
}
if !cc.goto_next_sibling() { break; }
}
}
}[features]
tree-sitter = [
"dep:tree-sitter",
"dep:tree-sitter-rust",
"dep:tree-sitter-go",
"dep:tree-sitter-python",
"dep:tree-sitter-typescript",
]
[dependencies]
tree-sitter = { version = "0.26", optional = true }
tree-sitter-rust = { version = "0.24", optional = true }
# ...Module gating: #![cfg(feature = "tree-sitter")] at top of file + #[cfg(feature = "tree-sitter")] mod ast; in parent.
Default build overhead: ~0.1s (zero tree-sitter code compiled). With feature: ~17s first build.
When you don't know the node kinds a grammar produces, add a temporary test that dumps the tree:
#[test]
fn debug_tree() {
let code = r#"your code here"#;
let lang = get_language("ts").unwrap();
let tree = parse(code.as_bytes(), lang).unwrap();
fn dump(node: &tree_sitter::Node, source: &str, depth: usize) {
eprintln!("{}{} [{},{}]-[{},{}]",
" ".repeat(depth),
node.kind(),
node.start_position().row, node.start_position().column,
node.end_position().row, node.end_position().column);
let mut c = node.walk();
if c.goto_first_child() {
loop {
dump(&c.node(), source, depth + 1);
if !c.goto_next_sibling() { break; }
}
}
}
dump(&tree.root_node(), code, 0);
}Remove before committing. Do NOT compile in /tmp — use the project's own target directory.