Skip to main content

icb_parser/lang/
common.rs

1//! Common tree-sitter traversal utilities shared across language parsers.
2//!
3//! Provides functions to create [`RawNode`] entries and to recursively walk
4//! a [`tree_sitter::Tree`] with a language-specific node classifier.  Using
5//! this module avoids duplicating the traversal logic for each supported
6//! language.
7
8use crate::facts::RawNode;
9use icb_common::{Language, NodeKind};
10use tree_sitter::Node;
11
12/// Create a [`RawNode`] from the supplied metadata and append it to `facts`.
13///
14/// Returns the index of the newly inserted node so that parent/child
15/// relationships can be maintained.
16pub fn push_node(
17    facts: &mut Vec<RawNode>,
18    language: Language,
19    kind: NodeKind,
20    name: Option<String>,
21    node: &Node,
22    parent_idx: Option<usize>,
23) -> usize {
24    let start = node.start_position();
25    let end = node.end_position();
26    let start_line = start.row + 1;
27    let end_line = std::cmp::max(end.row + 1, start_line);
28
29    let idx = facts.len();
30    facts.push(RawNode {
31        language,
32        kind,
33        name,
34        usr: None,
35        start_line,
36        start_col: start.column,
37        end_line,
38        end_col: end.column,
39        children: Vec::new(),
40        source_file: None,
41    });
42
43    if let Some(pidx) = parent_idx {
44        facts[pidx].children.push(idx);
45    }
46    idx
47}
48
49/// Find the first direct child of `node` whose `kind()` equals `kind`.
50///
51/// The search stops at the first match and does **not** allocate an
52/// intermediate collection.
53pub fn child_of_kind<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
54    let mut cursor = node.walk();
55    let mut children = node.children(&mut cursor);
56    children.find(|c| c.kind() == kind)
57}
58
59/// Generic recursive traversal of a tree-sitter sub‑tree.
60///
61/// For every node the `classifier` closure is called.  If it returns
62/// `Some((node_kind, name, is_container))` a [`RawNode`] is recorded and
63/// – when `is_container` is `true` – its children are traversed with the
64/// node itself becoming the new parent.  If `classifier` returns `None`
65/// the children are still visited, but the current parent remains
66/// unchanged.
67pub fn traverse_node<F>(
68    node: Node,
69    source: &str,
70    facts: &mut Vec<RawNode>,
71    parent_idx: Option<usize>,
72    language: Language,
73    classifier: &F,
74) -> Option<usize>
75where
76    F: Fn(&Node, &str) -> Option<(NodeKind, Option<String>, bool)>,
77{
78    if let Some((kind, name, is_container)) = classifier(&node, source) {
79        let idx = push_node(facts, language, kind, name, &node, parent_idx);
80        if is_container {
81            let new_parent = Some(idx);
82            let mut current_parent = new_parent;
83            let mut cursor = node.walk();
84            for child in node.children(&mut cursor) {
85                current_parent =
86                    traverse_node(child, source, facts, current_parent, language, classifier);
87            }
88            new_parent
89        } else {
90            parent_idx
91        }
92    } else {
93        let mut current_parent = parent_idx;
94        let mut cursor = node.walk();
95        for child in node.children(&mut cursor) {
96            current_parent =
97                traverse_node(child, source, facts, current_parent, language, classifier);
98        }
99        current_parent
100    }
101}