Skip to main content

big_code_analysis/
node.rs

1// Metric counts (token, function, branch, argument, etc.) are stored as
2// `usize` and crossed with `f64` averages, ratios, and Halstead scores
3// across the cyclomatic / MI / Halstead computations. The `usize as f64`
4// and `f64 as usize` casts are intentional and snapshot-anchored — every
5// site is bounded by the count it came from. Allowing the lints at the
6// module level keeps the metric arithmetic legible.
7#![allow(
8    clippy::cast_precision_loss,
9    clippy::cast_possible_truncation,
10    clippy::cast_sign_loss
11)]
12
13use tree_sitter::Node as OtherNode;
14use tree_sitter::Tree as OtherTree;
15use tree_sitter::{Parser, TreeCursor};
16
17use crate::checker::Checker;
18use crate::traits::{LanguageInfo, Search};
19
20/// A parsed source tree wrapping a [`tree_sitter::Tree`].
21///
22/// The "open parse seam" (see issue #251) is reached by external
23/// callers through [`crate::Parser::from_tree`] or
24/// [`crate::metrics_from_tree`], which accept a caller-built
25/// `tree_sitter::Tree` directly; this wrapper stays internal so
26/// the metric walker is the only thing that observes it.
27#[derive(Clone, Debug)]
28pub(crate) struct Tree(OtherTree);
29
30impl Tree {
31    pub(crate) fn new<T: LanguageInfo>(code: &[u8]) -> Self {
32        let mut parser = Parser::new();
33        // `Tree::new::<T>` is only reachable from the `mk_action!`
34        // dispatchers, which themselves cfg-gate each `LANG::*` arm
35        // behind the matching per-language feature (see #252). When
36        // the feature is off the dispatcher returns
37        // `Err(LanguageDisabled)` before we get here, so
38        // `get_ts_language` is provably `Ok` at this call site.
39        let language = T::get_lang().get_ts_language().expect(
40            "invariant: dispatcher cfg-gates this call behind the per-language Cargo feature",
41        );
42        parser
43            .set_language(&language)
44            .expect("invariant: grammar version is pinned and compatible with bundled tree-sitter");
45
46        Self(
47            parser
48                .parse(code, None)
49                .expect("invariant: parser has a language set and no cancellation flag"),
50        )
51    }
52
53    pub(crate) fn from_ts_tree(tree: OtherTree) -> Self {
54        Self(tree)
55    }
56
57    pub(crate) fn get_root(&self) -> Node<'_> {
58        Node(self.0.root_node())
59    }
60
61    pub(crate) fn as_ts_tree(&self) -> &OtherTree {
62        &self.0
63    }
64}
65
66/// An `AST` node.
67///
68/// The inner `tree_sitter::Node` is exposed for advanced use cases
69/// where direct access to the underlying tree-sitter API is needed.
70#[derive(Clone, Copy, Debug)]
71pub struct Node<'a>(pub OtherNode<'a>);
72
73impl<'a> Node<'a> {
74    /// Checks if a node represents a syntax error or contains any syntax errors
75    /// anywhere within it.
76    #[must_use]
77    pub fn has_error(&self) -> bool {
78        self.0.has_error()
79    }
80
81    pub(crate) fn id(&self) -> usize {
82        self.0.id()
83    }
84
85    pub(crate) fn kind(&self) -> &'static str {
86        self.0.kind()
87    }
88
89    pub(crate) fn kind_id(&self) -> u16 {
90        self.0.kind_id()
91    }
92
93    pub(crate) fn utf8_text(&self, data: &'a [u8]) -> Option<&'a str> {
94        self.0.utf8_text(data).ok()
95    }
96
97    pub(crate) fn start_byte(&self) -> usize {
98        self.0.start_byte()
99    }
100
101    pub(crate) fn end_byte(&self) -> usize {
102        self.0.end_byte()
103    }
104
105    pub(crate) fn start_position(&self) -> (usize, usize) {
106        let temp = self.0.start_position();
107        (temp.row, temp.column)
108    }
109
110    pub(crate) fn end_position(&self) -> (usize, usize) {
111        let temp = self.0.end_position();
112        (temp.row, temp.column)
113    }
114
115    pub(crate) fn start_row(&self) -> usize {
116        self.0.start_position().row
117    }
118
119    pub(crate) fn end_row(&self) -> usize {
120        self.0.end_position().row
121    }
122
123    pub(crate) fn parent(&self) -> Option<Node<'a>> {
124        self.0.parent().map(Node)
125    }
126
127    #[inline]
128    pub(crate) fn has_sibling(&self, id: u16) -> bool {
129        self.0.parent().is_some_and(|parent| {
130            parent
131                .children(&mut parent.walk())
132                .any(|child| child.kind_id() == id)
133        })
134    }
135
136    pub(crate) fn previous_sibling(&self) -> Option<Node<'a>> {
137        self.0.prev_sibling().map(Node)
138    }
139
140    /// Returns `true` if any direct child has the given grammar
141    /// `kind_id`. Walks via `child(0)` + `next_sibling()` instead of
142    /// `children(&mut self.0.walk())` so the implementation avoids
143    /// the per-call `TreeCursor` heap allocation that the iterator
144    /// form requires. Each `next_sibling()` is O(1) (tree-sitter
145    /// stores siblings as a linked list), so total cost is O(n)
146    /// without cursor overhead. See #217 for the motivating perf
147    /// finding from the JS/TS template-literal hot path.
148    #[inline]
149    pub(crate) fn is_child(&self, id: u16) -> bool {
150        let mut cur = self.0.child(0);
151        while let Some(c) = cur {
152            if c.kind_id() == id {
153                return true;
154            }
155            cur = c.next_sibling();
156        }
157        false
158    }
159
160    pub(crate) fn child_count(&self) -> usize {
161        self.0.child_count()
162    }
163
164    // Returns `true` if this node is a named grammar production
165    // (as opposed to an anonymous token such as a punctuation or
166    // keyword literal). Used to skip anonymous tokens like the
167    // leading `|` in an or-pattern.
168    pub(crate) fn is_named(&self) -> bool {
169        self.0.is_named()
170    }
171
172    pub(crate) fn child_by_field_name(&self, name: &str) -> Option<Node<'_>> {
173        self.0.child_by_field_name(name).map(Node)
174    }
175
176    pub(crate) fn child(&self, pos: usize) -> Option<Node<'a>> {
177        self.0.child(pos as u32).map(Node)
178    }
179
180    /// Returns the tree-sitter grammar field name through which this
181    /// node reaches the child at `child_index`, if any. Used by the
182    /// AST builder to thread the parent's `field_name` into each child
183    /// without a parallel cursor walk.
184    pub(crate) fn field_name_for_child(&self, child_index: u32) -> Option<&'static str> {
185        self.0.field_name_for_child(child_index)
186    }
187
188    pub(crate) fn children(&self) -> impl ExactSizeIterator<Item = Node<'a>> + use<'a> {
189        let mut cursor = self.cursor();
190        cursor.goto_first_child();
191        (0..self.child_count()).map(move |_| {
192            let result = cursor.node();
193            cursor.goto_next_sibling();
194            result
195        })
196    }
197
198    pub(crate) fn cursor(&self) -> Cursor<'a> {
199        Cursor(self.0.walk())
200    }
201
202    #[allow(dead_code)]
203    pub(crate) fn get_parent(&self, level: usize) -> Option<Node<'a>> {
204        let mut level = level;
205        let mut node = *self;
206        while level != 0 {
207            if let Some(parent) = node.parent() {
208                node = parent;
209            } else {
210                return None;
211            }
212            level -= 1;
213        }
214
215        Some(node)
216    }
217
218    pub(crate) fn count_specific_ancestors<T: crate::ParserTrait>(
219        &self,
220        check: fn(&Node) -> bool,
221        stop: fn(&Node) -> bool,
222    ) -> usize {
223        let mut count = 0;
224        let mut node = *self;
225        while let Some(parent) = node.parent() {
226            if stop(&parent) {
227                break;
228            }
229            if check(&parent) && !T::Checker::is_else_if(&parent) {
230                count += 1;
231            }
232            node = parent;
233        }
234        count
235    }
236
237    /// Returns `true` iff this node's parent satisfies `parent_pred`
238    /// AND that parent's own parent (this node's grandparent)
239    /// satisfies `grand_pred`. Returns `false` as soon as either link
240    /// is absent or its predicate fails, so a misordered predicate
241    /// cannot silently degrade to a single-predicate check.
242    pub(crate) fn parent_grandparent_match(
243        &self,
244        parent_pred: fn(&Node) -> bool,
245        grand_pred: fn(&Node) -> bool,
246    ) -> bool {
247        let Some(parent) = self.parent() else {
248            return false;
249        };
250        if !parent_pred(&parent) {
251            return false;
252        }
253        let Some(grand) = parent.parent() else {
254            return false;
255        };
256        grand_pred(&grand)
257    }
258}
259
260/// An `AST` cursor.
261#[derive(Clone)]
262pub struct Cursor<'a>(TreeCursor<'a>);
263
264impl<'a> Cursor<'a> {
265    pub(crate) fn reset(&mut self, node: &Node<'a>) {
266        self.0.reset(node.0);
267    }
268
269    pub(crate) fn goto_next_sibling(&mut self) -> bool {
270        self.0.goto_next_sibling()
271    }
272
273    pub(crate) fn goto_first_child(&mut self) -> bool {
274        self.0.goto_first_child()
275    }
276
277    pub(crate) fn node(&self) -> Node<'a> {
278        Node(self.0.node())
279    }
280}
281
282impl<'a> Search<'a> for Node<'a> {
283    fn first_occurrence(&self, pred: fn(u16) -> bool) -> Option<Node<'a>> {
284        let mut cursor = self.cursor();
285        let mut stack = Vec::new();
286        let mut children = Vec::new();
287
288        stack.push(*self);
289
290        while let Some(node) = stack.pop() {
291            if pred(node.kind_id()) {
292                return Some(node);
293            }
294            cursor.reset(&node);
295            if cursor.goto_first_child() {
296                loop {
297                    children.push(cursor.node());
298                    if !cursor.goto_next_sibling() {
299                        break;
300                    }
301                }
302                for child in children.drain(..).rev() {
303                    stack.push(child);
304                }
305            }
306        }
307
308        None
309    }
310
311    fn act_on_node(&self, action: &mut dyn FnMut(&Node<'a>)) {
312        let mut cursor = self.cursor();
313        let mut stack = Vec::new();
314        let mut children = Vec::new();
315
316        stack.push(*self);
317
318        while let Some(node) = stack.pop() {
319            action(&node);
320            cursor.reset(&node);
321            if cursor.goto_first_child() {
322                loop {
323                    children.push(cursor.node());
324                    if !cursor.goto_next_sibling() {
325                        break;
326                    }
327                }
328                for child in children.drain(..).rev() {
329                    stack.push(child);
330                }
331            }
332        }
333    }
334
335    fn first_child(&self, pred: fn(u16) -> bool) -> Option<Node<'a>> {
336        self.children().find(|&child| pred(child.kind_id()))
337    }
338
339    fn act_on_child(&self, action: &mut dyn FnMut(&Node<'a>)) {
340        for child in self.children() {
341            action(&child);
342        }
343    }
344}