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::Ast::from_tree_sitter`], which accepts a
24/// caller-built `tree_sitter::Tree` directly; this wrapper stays
25/// internal so the metric walker is the only thing that observes it.
26#[derive(Clone, Debug)]
27pub(crate) struct Tree(OtherTree);
28
29impl Tree {
30    pub(crate) fn new<T: LanguageInfo>(code: &[u8]) -> Self {
31        let mut parser = Parser::new();
32        // `Tree::new::<T>` is only reachable from the `mk_action!`
33        // dispatchers, which themselves cfg-gate each `LANG::*` arm
34        // behind the matching per-language feature (see #252). When
35        // the feature is off the dispatcher returns
36        // `Err(LanguageDisabled)` before we get here, so
37        // `get_ts_language` is provably `Ok` at this call site.
38        let language = T::lang().get_ts_language().expect(
39            "invariant: dispatcher cfg-gates this call behind the per-language Cargo feature",
40        );
41        parser
42            .set_language(&language)
43            .expect("invariant: grammar version is pinned and compatible with bundled tree-sitter");
44
45        Self(
46            parser
47                .parse(code, None)
48                .expect("invariant: parser has a language set and no cancellation flag"),
49        )
50    }
51
52    pub(crate) fn from_ts_tree(tree: OtherTree) -> Self {
53        Self(tree)
54    }
55
56    pub(crate) fn get_root(&self) -> Node<'_> {
57        Node(self.0.root_node())
58    }
59
60    pub(crate) fn as_ts_tree(&self) -> &OtherTree {
61        &self.0
62    }
63}
64
65/// An `AST` node.
66///
67/// The inner [`tree_sitter::Node`] is reached through
68/// [`Node::as_tree_sitter`] for advanced use cases that need direct
69/// access to the underlying tree-sitter API; the field itself is
70/// private so a `tree-sitter` version bump cannot silently reshape
71/// this struct's public layout.
72#[derive(Clone, Copy, Debug)]
73pub struct Node<'a>(OtherNode<'a>);
74
75impl<'a> Node<'a> {
76    /// Returns the underlying [`tree_sitter::Node`] for callers that
77    /// want to drive their own traversal alongside the metric walker.
78    ///
79    /// `tree_sitter::Node` is [`Copy`], so the node is returned by
80    /// value. The returned node borrows the same source-tree lifetime
81    /// as `self`.
82    ///
83    /// The `tree-sitter` re-export this exposes is *value-not-stable*:
84    /// the underlying pin may bump in any minor release, so node shape
85    /// and node-kind ids are not part of this crate's stability
86    /// contract (see the [`tree_sitter`](crate::tree_sitter) re-export
87    /// note in the crate root).
88    #[must_use]
89    #[inline]
90    pub fn as_tree_sitter(&self) -> OtherNode<'a> {
91        self.0
92    }
93
94    /// Checks if a node represents a syntax error or contains any syntax errors
95    /// anywhere within it.
96    #[must_use]
97    pub fn has_error(&self) -> bool {
98        self.0.has_error()
99    }
100
101    pub(crate) fn id(&self) -> usize {
102        self.0.id()
103    }
104
105    pub(crate) fn kind(&self) -> &'static str {
106        self.0.kind()
107    }
108
109    pub(crate) fn kind_id(&self) -> u16 {
110        self.0.kind_id()
111    }
112
113    pub(crate) fn utf8_text(&self, data: &'a [u8]) -> Option<&'a str> {
114        self.0.utf8_text(data).ok()
115    }
116
117    pub(crate) fn start_byte(&self) -> usize {
118        self.0.start_byte()
119    }
120
121    pub(crate) fn end_byte(&self) -> usize {
122        self.0.end_byte()
123    }
124
125    pub(crate) fn start_position(&self) -> (usize, usize) {
126        let temp = self.0.start_position();
127        (temp.row, temp.column)
128    }
129
130    pub(crate) fn end_position(&self) -> (usize, usize) {
131        let temp = self.0.end_position();
132        (temp.row, temp.column)
133    }
134
135    pub(crate) fn start_row(&self) -> usize {
136        self.0.start_position().row
137    }
138
139    pub(crate) fn end_row(&self) -> usize {
140        self.0.end_position().row
141    }
142
143    pub(crate) fn parent(&self) -> Option<Node<'a>> {
144        self.0.parent().map(Node)
145    }
146
147    /// Returns `true` if this node's parent has any direct child with
148    /// the given grammar `kind_id` (the parent's children include this
149    /// node itself, so a self-match counts). Delegates to [`wraps_any`]
150    /// on the parent so the scan reuses the allocation-free `child(0)` +
151    /// `next_sibling()` walk rather than `children(&mut parent.walk())`,
152    /// which heap-allocates a `TreeCursor` per call. This sits on the
153    /// JS/TS arrow-function closure-classification hot path
154    /// (`check_if_arrow_func!`), the same path #217 optimized for
155    /// `wraps_any` / `is_child` but missed here. See #521.
156    ///
157    /// [`wraps_any`]: Self::wraps_any
158    #[inline]
159    pub(crate) fn has_sibling(&self, id: u16) -> bool {
160        self.0
161            .parent()
162            .is_some_and(|parent| Node(parent).is_child(id))
163    }
164
165    pub(crate) fn previous_sibling(&self) -> Option<Node<'a>> {
166        self.0.prev_sibling().map(Node)
167    }
168
169    /// Returns `true` if any direct child has the given grammar
170    /// `kind_id`. Walks via `child(0)` + `next_sibling()` instead of
171    /// `children(&mut self.0.walk())` so the implementation avoids
172    /// the per-call `TreeCursor` heap allocation that the iterator
173    /// form requires. Each `next_sibling()` is O(1) (tree-sitter
174    /// stores siblings as a linked list), so total cost is O(n)
175    /// without cursor overhead. See #217 for the motivating perf
176    /// finding from the JS/TS template-literal hot path.
177    #[inline]
178    pub(crate) fn is_child(&self, id: u16) -> bool {
179        self.wraps_any(&[id])
180    }
181
182    /// Returns `true` if any direct child matches one of the given
183    /// grammar `kind_id`s. The single-id [`is_child`] delegates here, so
184    /// both share one allocation-free sibling walk (the `#[inline]` makes
185    /// the single-element `contains` collapse to an equality check — the
186    /// #217 hot-path optimization is preserved). Generalizing the check to
187    /// a set lets the shared string-interpolation operand skip declare its
188    /// rule once (issue #420).
189    ///
190    /// [`is_child`]: Self::is_child
191    #[inline]
192    pub(crate) fn wraps_any(&self, ids: &[u16]) -> bool {
193        let mut cur = self.0.child(0);
194        while let Some(c) = cur {
195            if ids.contains(&c.kind_id()) {
196                return true;
197            }
198            cur = c.next_sibling();
199        }
200        false
201    }
202
203    pub(crate) fn child_count(&self) -> usize {
204        self.0.child_count()
205    }
206
207    // Returns `true` if this node is a named grammar production
208    // (as opposed to an anonymous token such as a punctuation or
209    // keyword literal). Used to skip anonymous tokens like the
210    // leading `|` in an or-pattern.
211    pub(crate) fn is_named(&self) -> bool {
212        self.0.is_named()
213    }
214
215    /// Returns the direct child reached through the grammar `field_name`,
216    /// if any. The child carries the underlying tree lifetime `'a` (the
217    /// `tree_sitter::Node` it wraps is [`Copy`] and valid for the whole
218    /// tree), so callers may hold it past the borrow of `&self` — matching
219    /// the sibling accessors ([`child`], [`parent`], [`children`], …) rather
220    /// than over-narrowing to the method-call borrow (see issue #786).
221    ///
222    /// [`child`]: Self::child
223    /// [`parent`]: Self::parent
224    /// [`children`]: Self::children
225    pub(crate) fn child_by_field_name(&self, name: &str) -> Option<Node<'a>> {
226        self.0.child_by_field_name(name).map(Node)
227    }
228
229    pub(crate) fn child(&self, pos: usize) -> Option<Node<'a>> {
230        self.0.child(pos as u32).map(Node)
231    }
232
233    /// Returns the tree-sitter grammar field name through which this
234    /// node reaches the child at `child_index`, if any. Used by the
235    /// AST builder to thread the parent's `field_name` into each child
236    /// without a parallel cursor walk.
237    pub(crate) fn field_name_for_child(&self, child_index: u32) -> Option<&'static str> {
238        self.0.field_name_for_child(child_index)
239    }
240
241    pub(crate) fn children(&self) -> Children<'a> {
242        let mut cursor = self.cursor();
243        // `goto_first_child` returns false when the node has no
244        // children, in which case the iterator is empty from the
245        // outset. Termination is then driven entirely by the cursor
246        // (see `Children::next`), so the iterator stops exactly when
247        // the tree reports no further siblings — it can never pad the
248        // sequence with duplicate nodes if `child_count` and the
249        // cursor walk ever disagree.
250        let done = !cursor.goto_first_child();
251        Children {
252            cursor,
253            done,
254            // `child_count` is the authoritative length for the
255            // `ExactSizeIterator` contract; for well-formed trees it
256            // equals the cursor sibling walk, so the reported length
257            // and the emitted data agree. A childless node (`done`
258            // already set) reports `0` so the empty iterator's length
259            // matches its (lack of) data.
260            remaining: if done { 0 } else { self.child_count() },
261        }
262    }
263
264    pub(crate) fn cursor(&self) -> Cursor<'a> {
265        Cursor(self.0.walk())
266    }
267
268    #[allow(dead_code)]
269    pub(crate) fn get_parent(&self, level: usize) -> Option<Node<'a>> {
270        let mut level = level;
271        let mut node = *self;
272        while level != 0 {
273            if let Some(parent) = node.parent() {
274                node = parent;
275            } else {
276                return None;
277            }
278            level -= 1;
279        }
280
281        Some(node)
282    }
283
284    pub(crate) fn count_specific_ancestors<C: Checker>(
285        &self,
286        check: fn(&Node) -> bool,
287        stop: fn(&Node) -> bool,
288    ) -> usize {
289        let mut count = 0;
290        let mut node = *self;
291        while let Some(parent) = node.parent() {
292            if stop(&parent) {
293                break;
294            }
295            if check(&parent) && !C::is_else_if(&parent) {
296                count += 1;
297            }
298            node = parent;
299        }
300        count
301    }
302
303    /// Returns `true` iff this node's parent satisfies `parent_pred`
304    /// AND that parent's own parent (this node's grandparent)
305    /// satisfies `grand_pred`. Returns `false` as soon as either link
306    /// is absent or its predicate fails, so a misordered predicate
307    /// cannot silently degrade to a single-predicate check.
308    pub(crate) fn parent_grandparent_match(
309        &self,
310        parent_pred: fn(&Node) -> bool,
311        grand_pred: fn(&Node) -> bool,
312    ) -> bool {
313        let Some(parent) = self.parent() else {
314            return false;
315        };
316        if !parent_pred(&parent) {
317            return false;
318        }
319        let Some(grand) = parent.parent() else {
320            return false;
321        };
322        grand_pred(&grand)
323    }
324
325    /// Returns a pre-order iterator over this node and all of its
326    /// descendants (this node first, then each child subtree left to
327    /// right).
328    ///
329    /// The traversal is allocation-light: it reuses one work stack and
330    /// visits each node exactly once, so a full walk is O(n) in the
331    /// subtree size. Every yielded [`Node`] carries the underlying tree
332    /// lifetime `'a`, so callers may collect or retain the handles.
333    ///
334    /// This is the Rust counterpart of the Python `Node.walk()` binding
335    /// (issue #728): the binding wraps each yielded node, so Rust and
336    /// Python share one traversal order.
337    #[must_use]
338    pub fn preorder(&self) -> Preorder<'a> {
339        Preorder { stack: vec![*self] }
340    }
341
342    /// Collects every node in this subtree (this node included) whose
343    /// [`kind`](tree_sitter::Node::kind) is listed in `kinds`, in
344    /// pre-order.
345    ///
346    /// Membership is an exact match against the raw grammar kind — the
347    /// same unaltered vocabulary [`crate::Ast::root_node`] exposes, not
348    /// the `Alterator`-curated kinds [`crate::Ast::dump`] emits. This is
349    /// the Rust counterpart of the Python `Node.descendants_by_kind()`
350    /// binding (issue #728).
351    #[must_use]
352    pub fn descendants_by_kind(&self, kinds: &[&str]) -> Vec<Node<'a>> {
353        self.preorder()
354            .filter(|node| kinds.contains(&node.kind()))
355            .collect()
356    }
357}
358
359/// Pre-order iterator over a node and its descendants, returned by
360/// [`Node::preorder`].
361///
362/// Holds a single work stack of not-yet-visited nodes. Each step pops the
363/// next node, pushes its children so the leftmost is visited first, and
364/// yields the popped node — so the sequence is the node, then each child
365/// subtree in order. The stack is reused across steps (children are pushed
366/// then the freshly-pushed slice is reversed in place), so the walk
367/// allocates only the stack's growth, not a fresh buffer per node.
368pub struct Preorder<'a> {
369    stack: Vec<Node<'a>>,
370}
371
372impl<'a> Iterator for Preorder<'a> {
373    type Item = Node<'a>;
374
375    fn next(&mut self) -> Option<Self::Item> {
376        let node = self.stack.pop()?;
377        // Push children in document order, then reverse just the slice we
378        // appended so the leftmost child ends up on top of the stack and
379        // is visited next — pre-order without a per-node temporary.
380        let first_child = self.stack.len();
381        self.stack.extend(node.children());
382        self.stack[first_child..].reverse();
383        Some(node)
384    }
385}
386
387/// An `AST` cursor.
388#[derive(Clone)]
389pub(crate) struct Cursor<'a>(TreeCursor<'a>);
390
391impl<'a> Cursor<'a> {
392    pub(crate) fn reset(&mut self, node: &Node<'a>) {
393        self.0.reset(node.0);
394    }
395
396    pub(crate) fn goto_next_sibling(&mut self) -> bool {
397        self.0.goto_next_sibling()
398    }
399
400    pub(crate) fn goto_first_child(&mut self) -> bool {
401        self.0.goto_first_child()
402    }
403
404    pub(crate) fn node(&self) -> Node<'a> {
405        Node(self.0.node())
406    }
407}
408
409/// Iterator over a node's direct children, returned by
410/// [`Node::children`].
411///
412/// Termination is driven by the cursor alone: each step yields the
413/// cursor's current node, then advances with `goto_next_sibling`,
414/// stopping the moment that returns false. This makes the cursor the
415/// single source of truth for both the emitted data and when to stop,
416/// so the sequence can never be padded with duplicates if
417/// `child_count` and the actual sibling walk disagree.
418///
419/// The `ExactSizeIterator` length is reported from `child_count`
420/// (tracked in `remaining`). For well-formed trees the cursor walk and
421/// `child_count` agree, so the advertised length matches the data
422/// exactly.
423pub(crate) struct Children<'a> {
424    cursor: Cursor<'a>,
425    done: bool,
426    remaining: usize,
427}
428
429impl<'a> Iterator for Children<'a> {
430    type Item = Node<'a>;
431
432    fn next(&mut self) -> Option<Self::Item> {
433        if self.done {
434            return None;
435        }
436        let result = self.cursor.node();
437        // The cursor is the single source of truth for termination:
438        // once there is no next sibling this yield is the last one.
439        self.done = !self.cursor.goto_next_sibling();
440        // Keep the advertised length consistent with termination: when
441        // the cursor stops, nothing remains. For well-formed trees this
442        // equals `child_count - emitted`; if the cursor walk and
443        // `child_count` ever disagree, this still honors the
444        // `ExactSizeIterator` contract (`len() == 0` exactly at
445        // exhaustion) rather than reporting a phantom remainder.
446        self.remaining = if self.done {
447            0
448        } else {
449            self.remaining.saturating_sub(1)
450        };
451        Some(result)
452    }
453
454    fn size_hint(&self) -> (usize, Option<usize>) {
455        (self.remaining, Some(self.remaining))
456    }
457}
458
459impl ExactSizeIterator for Children<'_> {}
460
461impl<'a> Search<'a> for Node<'a> {
462    fn first_occurrence(&self, pred: fn(u16) -> bool) -> Option<Node<'a>> {
463        let mut cursor = self.cursor();
464        let mut stack = Vec::new();
465        let mut children = Vec::new();
466
467        stack.push(*self);
468
469        while let Some(node) = stack.pop() {
470            if pred(node.kind_id()) {
471                return Some(node);
472            }
473            cursor.reset(&node);
474            if cursor.goto_first_child() {
475                loop {
476                    children.push(cursor.node());
477                    if !cursor.goto_next_sibling() {
478                        break;
479                    }
480                }
481                for child in children.drain(..).rev() {
482                    stack.push(child);
483                }
484            }
485        }
486
487        None
488    }
489
490    fn act_on_node(&self, action: &mut dyn FnMut(&Node<'a>)) {
491        let mut cursor = self.cursor();
492        let mut stack = Vec::new();
493        let mut children = Vec::new();
494
495        stack.push(*self);
496
497        while let Some(node) = stack.pop() {
498            action(&node);
499            cursor.reset(&node);
500            if cursor.goto_first_child() {
501                loop {
502                    children.push(cursor.node());
503                    if !cursor.goto_next_sibling() {
504                        break;
505                    }
506                }
507                for child in children.drain(..).rev() {
508                    stack.push(child);
509                }
510            }
511        }
512    }
513
514    fn first_child(&self, pred: fn(u16) -> bool) -> Option<Node<'a>> {
515        self.children().find(|&child| pred(child.kind_id()))
516    }
517
518    fn act_on_child(&self, action: &mut dyn FnMut(&Node<'a>)) {
519        for child in self.children() {
520            action(&child);
521        }
522    }
523}
524
525#[cfg(test)]
526mod tests {
527    use super::*;
528    use crate::langs::MozjsCode;
529
530    /// The cursor-free [`Node::has_sibling`] (issue #521) must yield the
531    /// exact same result as the original `parent.children(&mut
532    /// parent.walk()).any(...)` form for every node and every kind in a
533    /// real tree: same child set (named + anonymous), same order, same
534    /// short-circuit. Comparing against the literal old logic node-by-node
535    /// proves equivalence without hardcoding grammar `kind_id`s.
536    fn old_has_sibling(node: OtherNode, id: u16) -> bool {
537        node.parent().is_some_and(|parent| {
538            parent
539                .children(&mut parent.walk())
540                .any(|child| child.kind_id() == id)
541        })
542    }
543
544    #[test]
545    fn has_sibling_matches_cursor_iterator_form() {
546        // Arrow functions exercise the `check_if_arrow_func!` call site
547        // that motivated #521 (PropertyIdentifier siblings on the JS/TS
548        // closure-classification hot path).
549        let code = b"const o = { m: (a) => a + 1, n: function () {} }; foo.bar();";
550        let tree = Tree::new::<MozjsCode>(code);
551        let ts_tree = tree.as_ts_tree();
552
553        // Collect the grammar kinds that actually occur, so the
554        // equivalence check covers present-sibling (true) cases.
555        let mut kinds = std::collections::BTreeSet::new();
556        let mut stack = vec![ts_tree.root_node()];
557        while let Some(n) = stack.pop() {
558            kinds.insert(n.kind_id());
559            let mut child = n.child(0);
560            while let Some(c) = child {
561                stack.push(c);
562                child = c.next_sibling();
563            }
564        }
565        // Include an id that does not occur anywhere for absent-sibling
566        // (false) coverage.
567        let absent_id = u16::MAX;
568
569        let mut stack = vec![ts_tree.root_node()];
570        while let Some(n) = stack.pop() {
571            let wrapped = Node(n);
572            for &id in kinds.iter().chain(std::iter::once(&absent_id)) {
573                assert_eq!(
574                    wrapped.has_sibling(id),
575                    old_has_sibling(n, id),
576                    "has_sibling diverged from cursor-iterator form at node kind {} for id {id}",
577                    n.kind(),
578                );
579            }
580            let mut child = n.child(0);
581            while let Some(c) = child {
582                stack.push(c);
583                child = c.next_sibling();
584            }
585        }
586
587        // No-parent node (root) always reports no sibling.
588        let root = Node(ts_tree.root_node());
589        assert!(!root.has_sibling(absent_id));
590        for &id in &kinds {
591            assert!(
592                !root.has_sibling(id),
593                "root node has no parent → no sibling"
594            );
595        }
596    }
597
598    /// `children()` must yield exactly the node's direct children, in
599    /// order, for every node in a real tree — including the empty
600    /// (leaf) and single-child cases. Termination is cursor-driven, so
601    /// the emitted set is compared node-by-node against the raw
602    /// tree-sitter `child(i)` walk (the ground truth for both order and
603    /// count). This pins the no-duplicate-padding property: a desync
604    /// between `child_count` and the cursor walk would surface here as
605    /// extra trailing duplicates or a length mismatch.
606    #[test]
607    fn children_matches_tree_sitter_child_walk() {
608        // Mix of leaf nodes (no children), single-child wrappers, and
609        // multi-child constructs to cover all arities.
610        let code = b"const o = { m: (a) => a + 1 }; foo(); ;";
611        let tree = Tree::new::<MozjsCode>(code);
612        let ts_tree = tree.as_ts_tree();
613
614        let mut stack = vec![ts_tree.root_node()];
615        while let Some(n) = stack.pop() {
616            let wrapped = Node(n);
617
618            // Ground truth: walk children by index off the raw node.
619            let expected: Vec<_> = (0..n.child_count() as u32)
620                .filter_map(|i| n.child(i))
621                .map(|c| (c.id(), c.kind_id()))
622                .collect();
623
624            let mut iter = wrapped.children();
625            // ExactSizeIterator length must equal the child count up
626            // front and stay exact as the iterator is consumed.
627            assert_eq!(
628                iter.len(),
629                expected.len(),
630                "children().len() disagreed with child_count at kind {}",
631                n.kind(),
632            );
633
634            let mut actual = Vec::new();
635            let mut remaining = expected.len();
636            while let Some(child) = iter.next() {
637                remaining -= 1;
638                assert_eq!(
639                    iter.len(),
640                    remaining,
641                    "size_hint drifted mid-iteration at kind {}",
642                    n.kind(),
643                );
644                actual.push((child.id(), child.kind_id()));
645            }
646            assert_eq!(iter.len(), 0, "iterator not drained to zero len");
647            assert_eq!(
648                actual,
649                expected,
650                "children() diverged from child(i) walk at kind {}",
651                n.kind(),
652            );
653
654            for i in 0..n.child_count() as u32 {
655                if let Some(c) = n.child(i) {
656                    stack.push(c);
657                }
658            }
659        }
660    }
661
662    /// `child_by_field_name` (issue #786) must return the child at the
663    /// underlying tree lifetime `'a`, not the method-call borrow of
664    /// `&self`. The proof is a helper whose return type *requires* the
665    /// child to outlive an intermediate `&Node` borrow: under the old
666    /// `Option<Node<'_>>` signature the returned node would be tied to
667    /// `parent`'s borrow and this would fail to compile. Binding the
668    /// child to a variable that outlives the `&parent` reborrow inside
669    /// the helper exercises the widened lifetime.
670    #[test]
671    fn child_by_field_name_outlives_self_borrow() {
672        // `find_named_child` takes the parent by value, reborrows it
673        // through a `&` reference to call `child_by_field_name`, and
674        // returns the child. The returned `Node<'a>` must survive past
675        // that inner `&parent` borrow — only possible because the child
676        // carries the tree lifetime, not the borrow of `&parent`.
677        fn find_named_child<'a>(parent: Node<'a>) -> Option<Node<'a>> {
678            let borrowed: &Node<'a> = &parent;
679            borrowed.child_by_field_name("declarator")
680        }
681
682        let code = b"int answer = 42;";
683        let tree = Tree::new::<crate::langs::CppCode>(code);
684        let root = tree.get_root();
685
686        // Walk to the `declaration` node, then pull its `declarator`
687        // child out and hold it after the producing borrow has ended.
688        let mut held: Option<Node> = None;
689        let mut stack = vec![root];
690        while let Some(n) = stack.pop() {
691            if n.kind() == "declaration" {
692                // `find_named_child` consumes a copy of `n`; the result
693                // must remain valid here, well past the inner borrow.
694                held = find_named_child(n);
695                break;
696            }
697            for child in n.children() {
698                stack.push(child);
699            }
700        }
701
702        let declarator = held.expect("C declaration has a `declarator` field");
703        // The held node is still usable: it kept its tree linkage rather
704        // than dangling at the end of the producing borrow.
705        assert_eq!(declarator.kind(), "init_declarator");
706    }
707
708    /// `Node::as_tree_sitter` (issue #556) must hand back the *same*
709    /// underlying `tree_sitter::Node` the wrapper holds: identical
710    /// `kind()` / `kind_id()` and a usable tree-sitter API. Obtaining
711    /// the wrapper through the public `CppParser` + `ParserTrait::root`
712    /// path (rather than the in-module `Tree::new`) proves the accessor
713    /// is the public seam that replaced the former `pub` `.0` field.
714    #[test]
715    fn as_tree_sitter_round_trips_wrapper_kind() {
716        use crate::{CppParser, ParserTrait};
717        use std::path::Path;
718
719        let source = b"int main() { return 0; }";
720        let parser = CppParser::new(source.to_vec(), Path::new("example.cpp"), None);
721        let root = parser.root();
722
723        let ts_root = root.as_tree_sitter();
724
725        // A well-formed C++ translation unit roots at `translation_unit`.
726        assert_eq!(ts_root.kind(), "translation_unit");
727        // The accessor must agree with the wrapper's own kind views.
728        assert_eq!(ts_root.kind(), root.kind());
729        assert_eq!(ts_root.kind_id(), root.kind_id());
730        // The returned node is usable as a tree-sitter node, not a copy
731        // that has lost its tree linkage: the parse is error-free and
732        // the root has children.
733        assert!(!ts_root.has_error());
734        assert!(ts_root.child_count() > 0);
735    }
736
737    /// Ground-truth pre-order walk over the raw tree-sitter node, by
738    /// document order (`child(0..child_count)`). [`Node::preorder`] must
739    /// emit exactly this sequence of node ids — node first, then each
740    /// child subtree left to right.
741    fn ground_truth_preorder(node: OtherNode) -> Vec<usize> {
742        let mut out = vec![node.id()];
743        for i in 0..node.child_count() as u32 {
744            if let Some(child) = node.child(i) {
745                out.extend(ground_truth_preorder(child));
746            }
747        }
748        out
749    }
750
751    #[test]
752    fn preorder_matches_recursive_document_order() {
753        // A nested construct (function holding a declaration and a call)
754        // gives the walk real depth and sibling fan-out to order.
755        let code = b"int main() { int x = 1; foo(x); return 0; }";
756        let tree = Tree::new::<crate::langs::CppCode>(code);
757        let root = tree.get_root();
758
759        let actual: Vec<usize> = root.preorder().map(|n| n.id()).collect();
760        let expected = ground_truth_preorder(root.as_tree_sitter());
761
762        assert_eq!(
763            actual, expected,
764            "preorder diverged from recursive child(0..n) document order"
765        );
766        // Sanity: a non-trivial tree, and the root is visited first.
767        assert!(actual.len() > 5, "expected a multi-node tree");
768        assert_eq!(actual[0], root.id(), "root must be yielded first");
769    }
770
771    #[test]
772    fn descendants_by_kind_collects_matching_subtree_nodes() {
773        // `x` is declared once and used twice, so three `identifier`
774        // nodes exist under the function; `main` is an identifier too.
775        let code = b"int main() { int x = 1; return x + x; }";
776        let tree = Tree::new::<crate::langs::CppCode>(code);
777        let root = tree.get_root();
778
779        let found = root.descendants_by_kind(&["identifier"]);
780        // Cross-check against an independent pre-order count so the helper
781        // cannot pass by matching everything or nothing.
782        let expected: Vec<usize> = root
783            .preorder()
784            .filter(|n| n.kind() == "identifier")
785            .map(|n| n.id())
786            .collect();
787        let actual: Vec<usize> = found.iter().map(Node::id).collect();
788        assert_eq!(actual, expected);
789        assert!(
790            found.len() >= 3,
791            "expected at least the `main`, `x` decl, and `x` uses"
792        );
793        assert!(
794            found.iter().all(|n| n.kind() == "identifier"),
795            "every collected node must match the requested kind"
796        );
797
798        // An absent kind yields nothing; a multi-kind filter unions.
799        assert!(root.descendants_by_kind(&["no_such_kind"]).is_empty());
800        assert!(
801            root.descendants_by_kind(&["identifier", "number_literal"])
802                .len()
803                > found.len(),
804            "adding `number_literal` must widen the match set"
805        );
806    }
807}