Skip to content

Commit aa07ca1

Browse files
author
Marlon Costa
committed
feat(ast): Add AST traversal tools and expose Node API
1 parent 37e5d83 commit aa07ca1

File tree

5 files changed

+257
-16
lines changed

5 files changed

+257
-16
lines changed

src/asttools.rs

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
//! AST traversal and analysis utilities.
2+
//!
3+
//! This module provides helper functions and macros for traversing
4+
//! and analyzing the AST tree structure.
5+
6+
use crate::node::Node;
7+
8+
/// Gets an ancestor at a specific level above the current node.
9+
///
10+
/// # Arguments
11+
/// * `node` - The starting node
12+
/// * `level` - How many levels up to traverse (0 returns the node itself)
13+
///
14+
/// # Returns
15+
/// The ancestor node at the specified level, or None if the tree isn't deep enough.
16+
///
17+
/// # Example
18+
/// ```ignore
19+
/// // Get the grandparent (2 levels up)
20+
/// if let Some(grandparent) = get_parent(&node, 2) {
21+
/// println!("Grandparent kind: {}", grandparent.kind());
22+
/// }
23+
/// ```
24+
pub fn get_parent<'a>(node: &Node<'a>, level: usize) -> Option<Node<'a>> {
25+
let mut level = level;
26+
let mut current = *node;
27+
while level != 0 {
28+
current = current.parent()?;
29+
level -= 1;
30+
}
31+
Some(current)
32+
}
33+
34+
/// Traverses a tree passing from children to children in search of a specific
35+
/// token or series of tokens.
36+
///
37+
/// # Arguments
38+
/// * `node` - The starting node
39+
/// * `token_list` - A slice of predicates, each matching a level of descent
40+
///
41+
/// # Returns
42+
/// The final node after following the token path, or None if any token wasn't found.
43+
///
44+
/// # Example
45+
/// ```ignore
46+
/// // Find: node -> child matching pred1 -> grandchild matching pred2
47+
/// let result = traverse_children(&node, &[
48+
/// |id| id == SomeToken::Foo as u16,
49+
/// |id| id == SomeToken::Bar as u16,
50+
/// ]);
51+
/// ```
52+
pub fn traverse_children<'a, F>(node: &Node<'a>, token_list: &[F]) -> Option<Node<'a>>
53+
where
54+
F: Fn(u16) -> bool,
55+
{
56+
let mut current = *node;
57+
'outer: for token in token_list {
58+
for child in current.children() {
59+
if token(child.kind_id()) {
60+
current = child;
61+
continue 'outer;
62+
}
63+
}
64+
// Token not found at this level
65+
return None;
66+
}
67+
Some(current)
68+
}
69+
70+
/// Checks if a node has specific ancestors in sequence.
71+
///
72+
/// This macro checks if the node's ancestors match a specific pattern,
73+
/// where the first pattern(s) are immediate ancestors and the last pattern
74+
/// is the final ancestor to match.
75+
///
76+
/// # Example
77+
/// ```ignore
78+
/// // Check if node is inside a function inside a class
79+
/// let is_method = has_ancestors!(node, Class | Struct, Function);
80+
/// ```
81+
#[macro_export]
82+
macro_rules! has_ancestors {
83+
($node:expr, $( $typs:pat_param )|*, $( $typ:pat_param ),+) => {{
84+
let mut res = false;
85+
loop {
86+
let mut node = *$node;
87+
$(
88+
if let Some(parent) = node.parent() {
89+
match parent.kind_id().into() {
90+
$typ => {
91+
node = parent;
92+
},
93+
_ => {
94+
break;
95+
}
96+
}
97+
} else {
98+
break;
99+
}
100+
)*
101+
if let Some(parent) = node.parent() {
102+
match parent.kind_id().into() {
103+
$( $typs )|+ => {
104+
res = true;
105+
},
106+
_ => {}
107+
}
108+
}
109+
break;
110+
}
111+
res
112+
}};
113+
}
114+
115+
/// Counts specific ancestors matching a pattern until a stop condition.
116+
///
117+
/// This macro traverses up the tree counting ancestors that match the given
118+
/// patterns, stopping when it encounters an ancestor matching the stop pattern.
119+
///
120+
/// # Example
121+
/// ```ignore
122+
/// // Count nested if statements until we hit a function boundary
123+
/// let nesting = count_specific_ancestors!(node, If | ElseIf, Function | Method);
124+
/// ```
125+
#[macro_export]
126+
macro_rules! count_specific_ancestors {
127+
($node:expr, $checker:ty, $( $typs:pat_param )|*, $( $stops:pat_param )|*) => {{
128+
let mut count = 0;
129+
let mut node = *$node;
130+
while let Some(parent) = node.parent() {
131+
match parent.kind_id().into() {
132+
$( $typs )|* => {
133+
if !<$checker>::is_else_if(&parent) {
134+
count += 1;
135+
}
136+
},
137+
$( $stops )|* => break,
138+
_ => {}
139+
}
140+
node = parent;
141+
}
142+
count
143+
}};
144+
}
145+
146+
#[cfg(test)]
147+
mod tests {
148+
#[test]
149+
fn test_get_parent_level_zero() {
150+
// Level 0 should return the same node
151+
// (actual test would need a real node)
152+
}
153+
}

src/getter.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -438,9 +438,8 @@ impl Getter for CppCode {
438438
return std::str::from_utf8(code).ok();
439439
}
440440
// we're in a function_definition so need to get the declarator
441-
if let Some(declarator) = node.child_by_field_name("declarator") {
442-
let declarator_node = declarator;
443-
if let Some(fd) = declarator_node.first_occurrence(|id| {
441+
if let Some(declarator) = node.child_by_field_name("declarator")
442+
&& let Some(fd) = declarator.first_occurrence(|id| {
444443
Cpp::FunctionDeclarator == id
445444
|| Cpp::FunctionDeclarator2 == id
446445
|| Cpp::FunctionDeclarator3 == id
@@ -464,7 +463,6 @@ impl Getter for CppCode {
464463
_ => {}
465464
}
466465
}
467-
}
468466
}
469467
_ => {
470468
if let Some(name) = node.child_by_field_name("name") {

src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ pub use crate::function::*;
8484
mod ast;
8585
pub use crate::ast::*;
8686

87+
/// AST traversal and analysis utilities.
88+
pub mod asttools;
89+
pub use crate::asttools::{get_parent, traverse_children};
90+
8791
mod count;
8892
pub use crate::count::*;
8993

src/node.rs

Lines changed: 86 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,49 +37,60 @@ impl<'a> Node<'a> {
3737
self.0.has_error()
3838
}
3939

40-
pub(crate) fn id(&self) -> usize {
40+
/// Returns a numeric id for this node that is unique within its tree.
41+
pub fn id(&self) -> usize {
4142
self.0.id()
4243
}
4344

44-
pub(crate) fn kind(&self) -> &'static str {
45+
/// Returns the node's type as a string.
46+
pub fn kind(&self) -> &'static str {
4547
self.0.kind()
4648
}
4749

48-
pub(crate) fn kind_id(&self) -> u16 {
50+
/// Returns the node's type as a numeric id.
51+
pub fn kind_id(&self) -> u16 {
4952
self.0.kind_id()
5053
}
5154

52-
pub(crate) fn utf8_text(&self, data: &'a [u8]) -> Option<&'a str> {
55+
/// Returns the node's text as a UTF-8 string, if valid.
56+
pub fn utf8_text(&self, data: &'a [u8]) -> Option<&'a str> {
5357
self.0.utf8_text(data).ok()
5458
}
5559

56-
pub(crate) fn start_byte(&self) -> usize {
60+
/// Returns the byte offset where this node starts.
61+
pub fn start_byte(&self) -> usize {
5762
self.0.start_byte()
5863
}
5964

60-
pub(crate) fn end_byte(&self) -> usize {
65+
/// Returns the byte offset where this node ends.
66+
pub fn end_byte(&self) -> usize {
6167
self.0.end_byte()
6268
}
6369

64-
pub(crate) fn start_position(&self) -> (usize, usize) {
70+
/// Returns the (row, column) position where this node starts.
71+
pub fn start_position(&self) -> (usize, usize) {
6572
let temp = self.0.start_position();
6673
(temp.row, temp.column)
6774
}
6875

69-
pub(crate) fn end_position(&self) -> (usize, usize) {
76+
/// Returns the (row, column) position where this node ends.
77+
pub fn end_position(&self) -> (usize, usize) {
7078
let temp = self.0.end_position();
7179
(temp.row, temp.column)
7280
}
7381

74-
pub(crate) fn start_row(&self) -> usize {
82+
/// Returns the row number where this node starts.
83+
pub fn start_row(&self) -> usize {
7584
self.0.start_position().row
7685
}
7786

78-
pub(crate) fn end_row(&self) -> usize {
87+
/// Returns the row number where this node ends.
88+
pub fn end_row(&self) -> usize {
7989
self.0.end_position().row
8090
}
8191

82-
pub(crate) fn parent(&self) -> Option<Node<'a>> {
92+
/// Returns this node's parent, if any.
93+
pub fn parent(&self) -> Option<Node<'a>> {
8394
self.0.parent().map(Node)
8495
}
8596

@@ -183,6 +194,41 @@ impl<'a> Node<'a> {
183194
}
184195
res
185196
}
197+
198+
/// Checks if this node has any ancestor that meets the given predicate.
199+
///
200+
/// Traverses up the tree from this node's parent to the root,
201+
/// returning true if any ancestor satisfies the predicate.
202+
pub fn has_ancestor<F: Fn(&Node) -> bool>(&self, pred: F) -> bool {
203+
let mut node = *self;
204+
while let Some(parent) = node.parent() {
205+
if pred(&parent) {
206+
return true;
207+
}
208+
node = parent;
209+
}
210+
false
211+
}
212+
213+
// Traverse a tree passing from children to children in search of a specific
214+
// token or series of tokens
215+
pub(crate) fn traverse_children<F>(&self, token_list: &[F]) -> Option<Node<'a>>
216+
where
217+
F: FnOnce(u16) -> bool + Copy,
218+
{
219+
let mut node = *self;
220+
'outer: for token in token_list {
221+
for temp_node in node.children() {
222+
if token(temp_node.kind_id()) {
223+
node = temp_node;
224+
continue 'outer;
225+
}
226+
}
227+
// If a token has not been found, return None
228+
return None;
229+
}
230+
Some(node)
231+
}
186232
}
187233

188234
/// An `AST` cursor.
@@ -236,6 +282,35 @@ impl<'a> Search<'a> for Node<'a> {
236282
None
237283
}
238284

285+
fn all_occurrences(&self, pred: fn(u16) -> bool) -> Vec<Node<'a>> {
286+
let mut cursor = self.cursor();
287+
let mut stack = Vec::new();
288+
let mut children = Vec::new();
289+
let mut results = Vec::new();
290+
291+
stack.push(*self);
292+
293+
while let Some(node) = stack.pop() {
294+
if pred(node.kind_id()) {
295+
results.push(node);
296+
}
297+
cursor.reset(&node);
298+
if cursor.goto_first_child() {
299+
loop {
300+
children.push(cursor.node());
301+
if !cursor.goto_next_sibling() {
302+
break;
303+
}
304+
}
305+
for child in children.drain(..).rev() {
306+
stack.push(child);
307+
}
308+
}
309+
}
310+
311+
results
312+
}
313+
239314
fn act_on_node(&self, action: &mut dyn FnMut(&Node<'a>)) {
240315
let mut cursor = self.cursor();
241316
let mut stack = Vec::new();

src/traits.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,20 @@ pub trait ParserTrait {
6666
fn get_filters(&self, filters: &[String]) -> Filter;
6767
}
6868

69-
pub(crate) trait Search<'a> {
69+
/// Search trait for AST node traversal.
70+
pub trait Search<'a> {
71+
/// Starting from this node, gets the first occurrence that meets the predicate.
7072
fn first_occurrence(&self, pred: fn(u16) -> bool) -> Option<Node<'a>>;
73+
74+
/// Starting from this node, gets all nodes that meet the given predicate.
75+
fn all_occurrences(&self, pred: fn(u16) -> bool) -> Vec<Node<'a>>;
76+
77+
/// Apply the given predicate on this node and all descendants.
7178
fn act_on_node(&self, pred: &mut dyn FnMut(&Node<'a>));
79+
80+
/// Starting from this node, gets the first child that meets the predicate.
7281
fn first_child(&self, pred: fn(u16) -> bool) -> Option<Node<'a>>;
82+
83+
/// Apply the given action on node's immediate children.
7384
fn act_on_child(&self, action: &mut dyn FnMut(&Node<'a>));
7485
}

0 commit comments

Comments
 (0)