Skip to main content

lanekeep_nodes/
nodes.rs

1//! Node handles: how a parsed file's nodes cross into a rule.
2//!
3//! Architecture §14 requires nodes to cross the boundary as opaque handles rather than as
4//! materialized objects. Materializing an AST is the cost that makes native tooling with
5//! JavaScript plugins slow, and it is a decision that cannot be walked back once rules
6//! depend on the object shape. Nothing about that argument is specific to one engine, which
7//! is why this type lives in its own crate rather than inside the engine that first needed
8//! it — see the crate-level docs for which engines that is.
9//!
10//! # Why paths rather than stored nodes
11//!
12//! The obvious arena holds `Node<'tree>` and hands out indices. It does not compile against
13//! `lanekeep-js`'s engine: rquickjs's `Function::new` requires `'static` closures, so
14//! nothing captured by a host function may borrow from a tree living on the caller's stack.
15//!
16//! So the arena **owns** the tree and stores, for each handle, the path of child indices
17//! from the root. Resolving a handle walks that path — `O(depth)`, where depth is typically
18//! ten to thirty — and every method does its work internally rather than returning a
19//! borrowed `Node`, which is what keeps the borrow checker satisfied without `unsafe`.
20//!
21//! Two properties this buys, both of which matter more than the lookup cost, and neither of
22//! which is specific to the engine that first needed them:
23//!
24//! **Laziness.** Only nodes actually handed to a rule are interned. A file whose query
25//! matches nothing costs nothing here, which is the whole point of the query gate.
26//!
27//! **Stable identity.** Handles are interned by node id, so the same node reached twice —
28//! a parent lookup from two siblings, say — yields the same number. A rule comparing two
29//! handles for equality relies on that, and it has to mean what it appears to mean.
30
31use lanekeep_query::CompiledQuery;
32use std::collections::HashMap;
33
34use lanekeep_lang::binding::{Binding, BindingResolver};
35use tree_sitter::{Node, Tree};
36
37/// An opaque reference to a node, as seen from rule code.
38pub type Handle = u32;
39
40/// A subtree's structural fingerprint: what the fold covered, and how much of it.
41///
42/// Computed host-side in one walk, so a rule does not pay a per-node boundary crossing to
43/// inspect a tree's shape — the exact cost invariant 3 (`docs/architecture.md` §4) exists
44/// to prevent. See [`NodeArena::structure_fingerprint`] for the normalization contract.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct StructureFingerprint {
47    /// blake3 of the normalized fold, lowercase hex.
48    pub hash: String,
49    /// How many nodes the fold covered. Exact, not approximate: it is the thresholding
50    /// input, so a rule knows whether a match is trivially small without a second
51    /// traversal.
52    pub nodes: u32,
53}
54
55/// Child indices of a node, as `u32`.
56///
57/// tree-sitter reports `child_count` as `usize` but takes `u32` in `child`, so the
58/// conversion lives here once rather than at each of the three call sites.
59fn child_indices(node: Node<'_>) -> std::ops::Range<u32> {
60    0..u32::try_from(node.child_count()).unwrap_or(u32::MAX)
61}
62
63/// Owns a parsed tree and the handles issued against it.
64#[derive(Debug)]
65pub struct NodeArena {
66    tree: Tree,
67    source: String,
68    /// Handle to the path of child indices from the root. The root's path is empty.
69    paths: Vec<Vec<u32>>,
70    /// Node id to handle, so a node reached twice gets the same handle both times.
71    by_id: HashMap<usize, Handle>,
72}
73
74impl NodeArena {
75    /// Take ownership of a tree and its source.
76    ///
77    /// The root is interned as handle `0`, so rule code always has somewhere to start.
78    #[must_use]
79    pub fn new(tree: Tree, source: String) -> Self {
80        let root_id = tree.root_node().id();
81        let mut arena = Self {
82            tree,
83            source,
84            paths: Vec::new(),
85            by_id: HashMap::new(),
86        };
87        arena.paths.push(Vec::new());
88        arena.by_id.insert(root_id, 0);
89        arena
90    }
91
92    /// The root node's handle.
93    ///
94    /// A constant rather than a method: the root is interned first, so it is always zero.
95    pub const ROOT: Handle = 0;
96
97    /// The source this tree was parsed from.
98    #[must_use]
99    pub fn source(&self) -> &str {
100        &self.source
101    }
102
103    /// How many handles have been issued. Interning is lazy, so this reflects how much of
104    /// the tree a rule actually touched.
105    #[must_use]
106    pub fn len(&self) -> usize {
107        self.paths.len()
108    }
109
110    /// Whether only the root has been interned.
111    #[must_use]
112    pub fn is_empty(&self) -> bool {
113        self.paths.len() <= 1
114    }
115
116    /// Walk a path from the root.
117    fn node_at(&self, path: &[u32]) -> Option<Node<'_>> {
118        let mut node = self.tree.root_node();
119        for index in path {
120            node = node.child(*index)?;
121        }
122        Some(node)
123    }
124
125    /// Resolve a handle to its node.
126    fn node(&self, handle: Handle) -> Option<Node<'_>> {
127        let path = self.paths.get(handle as usize)?;
128        self.node_at(path)
129    }
130
131    /// Issue a handle for a path, reusing the existing one when the node is already known.
132    fn intern(&mut self, id: usize, path: Vec<u32>) -> Handle {
133        if let Some(existing) = self.by_id.get(&id) {
134            return *existing;
135        }
136        // A tree large enough to overflow this would have exhausted memory long before.
137        let handle = Handle::try_from(self.paths.len()).unwrap_or(Handle::MAX);
138        self.paths.push(path);
139        self.by_id.insert(id, handle);
140        handle
141    }
142
143    /// Intern a node reached by extending a known path.
144    ///
145    /// Split into "read the node's id, then intern" so the immutable borrow of `self` ends
146    /// before the mutable one begins.
147    fn intern_child(&mut self, parent_path: &[u32], index: u32) -> Option<Handle> {
148        let mut path = parent_path.to_vec();
149        path.push(index);
150        let id = self.node_at(&path)?.id();
151        Some(self.intern(id, path))
152    }
153
154    /// The node's kind, as the grammar names it.
155    #[must_use]
156    pub fn kind(&self, handle: Handle) -> Option<&'static str> {
157        self.node(handle).map(|node| node.kind())
158    }
159
160    /// Whether the node is named, as opposed to an anonymous token such as a bracket.
161    #[must_use]
162    pub fn is_named(&self, handle: Handle) -> Option<bool> {
163        self.node(handle).map(|node| node.is_named())
164    }
165
166    /// The source text the node spans.
167    #[must_use]
168    pub fn text(&self, handle: Handle) -> Option<&str> {
169        let node = self.node(handle)?;
170        self.source.get(node.byte_range())
171    }
172
173    /// One-based line and column of the node's start.
174    #[must_use]
175    pub fn position(&self, handle: Handle) -> Option<(u32, u32)> {
176        let node = self.node(handle)?;
177        let start = node.start_position();
178        Some((
179            u32::try_from(start.row)
180                .unwrap_or(u32::MAX)
181                .saturating_add(1),
182            u32::try_from(start.column)
183                .unwrap_or(u32::MAX)
184                .saturating_add(1),
185        ))
186    }
187
188    /// The node's byte range in the source.
189    #[must_use]
190    pub fn byte_range(&self, handle: Handle) -> Option<(usize, usize)> {
191        self.node(handle)
192            .map(|node| (node.start_byte(), node.end_byte()))
193    }
194
195    /// What the identifier at a handle refers to.
196    ///
197    /// Takes the resolver rather than holding one so the arena stays language-agnostic,
198    /// and does the work internally so the borrowed `Node` never escapes.
199    #[must_use]
200    pub fn resolve_binding(
201        &self,
202        handle: Handle,
203        resolver: &dyn BindingResolver,
204    ) -> Option<Binding> {
205        let node = self.node(handle)?;
206        resolver.resolve(&self.tree, &self.source, node)
207    }
208
209    /// Whether the identifier at a handle shadows an outer binding of the same name.
210    #[must_use]
211    pub fn is_shadowed(&self, handle: Handle, resolver: &dyn BindingResolver) -> bool {
212        self.node(handle)
213            .is_some_and(|node| resolver.is_shadowed(&self.tree, &self.source, node))
214    }
215
216    /// The tree, for running queries against.
217    ///
218    /// Nodes obtained this way borrow the arena immutably, so they cannot be interned
219    /// while still held. Use [`NodeArena::path_of`] to reduce them to paths first, drop
220    /// them, then call [`NodeArena::intern_path`]. The two-phase shape is not incidental:
221    /// it is what lets the arena own the tree, which is what makes the handles `'static`
222    /// enough for the engine to hold.
223    #[must_use]
224    pub const fn tree(&self) -> &Tree {
225        &self.tree
226    }
227
228    /// Reduce a node to a path, so it can be interned after its borrow ends.
229    ///
230    /// Returns `None` for a node belonging to a different tree. Interning one under a path
231    /// that happened to resolve locally would hand a rule a handle pointing at an
232    /// unrelated node — wrong results, no error.
233    #[must_use]
234    pub fn path_of(&self, node: Node<'_>) -> Option<Vec<u32>> {
235        // Walk up to the root, recording each child index.
236        let mut path = Vec::new();
237        let mut current = node;
238        while let Some(parent) = current.parent() {
239            let index = child_indices(parent)
240                .find(|i| parent.child(*i).is_some_and(|c| c.id() == current.id()))?;
241            path.push(index);
242            current = parent;
243        }
244        path.reverse();
245
246        if self
247            .node_at(&path)
248            .is_none_or(|found| found.id() != node.id())
249        {
250            return None;
251        }
252        Some(path)
253    }
254
255    /// Issue a handle for a path produced by [`NodeArena::path_of`].
256    pub fn intern_path(&mut self, path: Vec<u32>) -> Option<Handle> {
257        let id = self.node_at(&path)?.id();
258        Some(self.intern(id, path))
259    }
260
261    /// The node's parent, if it is not the root.
262    pub fn parent(&mut self, handle: Handle) -> Option<Handle> {
263        let path = self.paths.get(handle as usize)?.clone();
264        if path.is_empty() {
265            return None;
266        }
267
268        let parent_path = path[..path.len() - 1].to_vec();
269        let id = self.node_at(&parent_path)?.id();
270        Some(self.intern(id, parent_path))
271    }
272
273    /// Every child, including anonymous tokens.
274    pub fn children(&mut self, handle: Handle) -> Vec<Handle> {
275        self.children_matching(handle, false)
276    }
277
278    /// Named children only, which is what a rule almost always wants.
279    pub fn named_children(&mut self, handle: Handle) -> Vec<Handle> {
280        self.children_matching(handle, true)
281    }
282
283    /// A structural summary of the subtree rooted at `handle`, computed in one host-side
284    /// walk.
285    ///
286    /// The fold that produces it is the whole normalization contract, stated up front:
287    ///
288    /// - **Every non-extra node's kind name contributes**, named and anonymous — anonymous
289    ///   token kinds are the operators, so `a + b` and `a - b` stay distinct. A node is
290    ///   excluded if and only if the grammar marked it `extra` (comments): a doc-comment
291    ///   difference is not an implementation difference, so comments change neither the
292    ///   bytes nor the count.
293    /// - **Token text is erased.** An identifier or literal contributes its kind
294    ///   (`identifier`, `number`, `string`) and nothing else. The normalization is
295    ///   language-agnostic by construction — there is no per-language table of what counts
296    ///   as an identifier to maintain.
297    /// - **The fold is framed to be unambiguous.** Every kind name is length-prefixed and
298    ///   every node writes its fold-child count, so two different shapes cannot
299    ///   concatenate into one preimage, and two trees whose preorder kind sequences agree
300    ///   but whose nesting differs stay distinct.
301    /// - **`ERROR`/`MISSING` nodes fold like any other kind**: a broken parse hashes as
302    ///   its broken shape rather than failing.
303    ///
304    /// The leading byte of the fold is a format version. The fold encoding is host
305    /// behavior a rule's verdict can depend on, and it is not itself in the cache key —
306    /// so a future change to this contract must bump it **and** `lanekeep_js`'s
307    /// `HOST_API_VERSION` together, or cached verdicts computed under the old encoding
308    /// stay valid under the old key.
309    ///
310    /// The hash is blake3 of the fold bytes, lowercase hex. `None` when the handle does
311    /// not resolve — nothing rather than a fabricated shape, the same posture `kind`,
312    /// `text` and `position` take.
313    #[must_use]
314    pub fn structure_fingerprint(&self, handle: Handle) -> Option<StructureFingerprint> {
315        let node = self.node(handle)?;
316        let mut fold = Fold::new();
317        fold.node(node);
318        let nodes = fold.nodes;
319        Some(StructureFingerprint {
320            hash: fold.finish(),
321            nodes,
322        })
323    }
324
325    fn children_matching(&mut self, handle: Handle, named_only: bool) -> Vec<Handle> {
326        let Some(path) = self.paths.get(handle as usize).cloned() else {
327            return Vec::new();
328        };
329
330        let indices: Vec<u32> = {
331            let Some(node) = self.node_at(&path) else {
332                return Vec::new();
333            };
334            child_indices(node)
335                .filter(|i| !named_only || node.child(*i).is_some_and(|c| c.is_named()))
336                .collect()
337        };
338
339        indices
340            .into_iter()
341            .filter_map(|i| self.intern_child(&path, i))
342            .collect()
343    }
344
345    /// Matches of a query, scoped to one node's subtree.
346    ///
347    /// Two-phase like everything else here: capture paths are collected while the tree is
348    /// borrowed, then interned once that borrow has ended. The arena owns the tree, so a
349    /// handle cannot be minted while a `Node` derived from it is alive.
350    #[must_use]
351    pub fn query_subtree(
352        &self,
353        handle: Handle,
354        query: &CompiledQuery,
355    ) -> Vec<Vec<(String, Vec<u32>)>> {
356        let Some(path) = self.paths.get(handle as usize) else {
357            return Vec::new();
358        };
359        let Some(node) = self.node_at(path) else {
360            return Vec::new();
361        };
362
363        let mut found = Vec::new();
364        query.for_each_match_in(node, self.source.as_bytes(), |m| {
365            found.push(
366                m.captures
367                    .iter()
368                    .filter_map(|(name, node)| {
369                        self.path_of(*node).map(|path| ((*name).to_owned(), path))
370                    })
371                    .collect::<Vec<_>>(),
372            );
373        });
374        found
375    }
376
377    /// The nearest ancestor a query matches at, with that match's captures.
378    ///
379    /// "Matches at" rather than "matches within": the query runs rooted at each ancestor in
380    /// turn, and a match counts only if it captured that ancestor. Without that, a query
381    /// matching anything anywhere inside would make the outermost ancestor the answer every
382    /// time, which is never what a rule walking upward wants.
383    #[must_use]
384    pub fn closest_ancestor_paths(
385        &self,
386        handle: Handle,
387        query: &CompiledQuery,
388    ) -> Option<Vec<(String, Vec<u32>)>> {
389        let path = self.paths.get(handle as usize)?.clone();
390
391        // Innermost first, so the closest ancestor wins.
392        for depth in (0..path.len()).rev() {
393            let Some(ancestor) = self.node_at(&path[..depth]) else {
394                continue;
395            };
396
397            let mut matched: Option<Vec<(String, Vec<u32>)>> = None;
398            query.for_each_match_in(ancestor, self.source.as_bytes(), |m| {
399                if matched.is_some() || !m.captures.iter().any(|(_, node)| *node == ancestor) {
400                    return;
401                }
402                matched = Some(
403                    m.captures
404                        .iter()
405                        .filter_map(|(name, node)| {
406                            self.path_of(*node).map(|p| ((*name).to_owned(), p))
407                        })
408                        .collect(),
409                );
410            });
411
412            if matched.is_some() {
413                return matched;
414            }
415        }
416        None
417    }
418
419    /// Ancestors, innermost first, ending at the root.
420    pub fn ancestors(&mut self, handle: Handle) -> Vec<Handle> {
421        let Some(path) = self.paths.get(handle as usize).cloned() else {
422            return Vec::new();
423        };
424
425        let mut out = Vec::with_capacity(path.len());
426        for depth in (0..path.len()).rev() {
427            let ancestor_path = path[..depth].to_vec();
428            let Some(id) = self.node_at(&ancestor_path).map(|n| n.id()) else {
429                break;
430            };
431            out.push(self.intern(id, ancestor_path));
432        }
433        out
434    }
435}
436
437/// One pass of the structure fold: the normalized bytes and the node count, together.
438///
439/// A `blake3::Hasher` plus a counter, named because the two must never be updated apart —
440/// a node that contributes bytes but not a count (or the reverse) would let two trees that
441/// differ hash alike, which is the exact failure this type exists to make unaskable.
442struct Fold {
443    hasher: blake3::Hasher,
444    nodes: u32,
445}
446
447impl Fold {
448    fn new() -> Self {
449        let mut hasher = blake3::Hasher::new();
450        // The fold encoding's format version — see
451        // `NodeArena::structure_fingerprint`'s doc comment for what changing it entails.
452        hasher.update(&[1]);
453        Self { hasher, nodes: 0 }
454    }
455
456    /// Fold one node and its subtree, preorder. `extra` nodes (comments) are skipped
457    /// entirely: they contribute no bytes, no framing and no count.
458    fn node(&mut self, node: Node<'_>) {
459        if node.is_extra() {
460            return;
461        }
462        self.nodes = self.nodes.saturating_add(1);
463
464        let kind = node.kind();
465        let len = u32::try_from(kind.len()).unwrap_or(u32::MAX);
466        self.hasher.update(&len.to_le_bytes());
467        self.hasher.update(kind.as_bytes());
468
469        // The fold-child count, *excluding* extras, written before the children so the
470        // framing is recoverable without a second pass. A comment between two statements
471        // must not renumber anything.
472        let mut count_cursor = node.walk();
473        let mut count: u32 = 0;
474        for child in node.children(&mut count_cursor) {
475            if !child.is_extra() {
476                count = count.saturating_add(1);
477            }
478        }
479        self.hasher.update(&count.to_le_bytes());
480
481        let mut child_cursor = node.walk();
482        for child in node.children(&mut child_cursor) {
483            self.node(child);
484        }
485    }
486
487    fn finish(self) -> String {
488        self.hasher.finalize().to_hex().to_string()
489    }
490}
491
492#[cfg(test)]
493mod tests {
494    use lanekeep_lang::Language;
495    use lanekeep_lang_js::TypeScript;
496
497    use super::*;
498
499    fn arena(source: &str) -> NodeArena {
500        let mut parser = tree_sitter::Parser::new();
501        parser
502            .set_language(&TypeScript.grammar())
503            .expect("grammar loads");
504        let tree = parser.parse(source, None).expect("parses");
505        NodeArena::new(tree, source.to_owned())
506    }
507
508    #[test]
509    fn the_root_is_always_handle_zero() {
510        let arena = arena("const x = 1;");
511        assert_eq!(NodeArena::ROOT, 0);
512        assert_eq!(arena.kind(0), Some("program"));
513    }
514
515    #[test]
516    fn resolves_kind_text_and_position() {
517        let mut arena = arena("const x = 1;\nconst y = 2;");
518        let statements = arena.named_children(NodeArena::ROOT);
519        assert_eq!(statements.len(), 2);
520
521        assert_eq!(arena.kind(statements[0]), Some("lexical_declaration"));
522        assert_eq!(arena.text(statements[0]), Some("const x = 1;"));
523        assert_eq!(arena.position(statements[0]), Some((1, 1)));
524        assert_eq!(arena.position(statements[1]), Some((2, 1)));
525    }
526
527    #[test]
528    fn walks_down_and_back_up() {
529        let mut arena = arena("const x = 1;");
530        let root = NodeArena::ROOT;
531        let declaration = arena.named_children(root)[0];
532        let declarator = arena.named_children(declaration)[0];
533
534        assert_eq!(arena.parent(declarator), Some(declaration));
535        assert_eq!(arena.parent(declaration), Some(root));
536        assert_eq!(arena.parent(root), None, "the root has no parent");
537    }
538
539    #[test]
540    fn handles_are_stable_for_the_same_node() {
541        // Rules compare handles with `===`, so reaching one node by two routes has to
542        // produce the same number. Without interning it would produce two, and a rule
543        // checking whether two captures are the same node would silently always say no.
544        let mut arena = arena("const x = 1;");
545        let root = NodeArena::ROOT;
546        let declaration = arena.named_children(root)[0];
547
548        let again = arena.named_children(root)[0];
549        assert_eq!(
550            declaration, again,
551            "the same child must intern to the same handle"
552        );
553
554        let declarator = arena.named_children(declaration)[0];
555        assert_eq!(
556            arena.parent(declarator),
557            Some(declaration),
558            "reaching a node from below must give the handle it already had"
559        );
560    }
561
562    #[test]
563    fn interning_is_lazy() {
564        // The property that makes this cheap: a file whose query matched nothing must not
565        // have paid to materialize its tree.
566        let arena = arena("const a = 1; const b = 2; function c() { return [1,2,3] }");
567        assert!(
568            arena.is_empty(),
569            "only the root should be interned before any traversal"
570        );
571        assert_eq!(arena.len(), 1);
572    }
573
574    #[test]
575    fn only_touched_nodes_are_interned() {
576        let mut arena = arena("const a = 1; const b = 2; const c = 3;");
577        let before = arena.len();
578        let _ = arena.named_children(NodeArena::ROOT);
579        let after = arena.len();
580
581        assert!(after > before);
582        assert!(
583            after < 20,
584            "should intern three statements, not the whole tree: {after}"
585        );
586    }
587
588    #[test]
589    fn named_children_excludes_anonymous_tokens() {
590        let mut arena = arena("const x = 1;");
591        let declaration = arena.named_children(NodeArena::ROOT)[0];
592
593        let all = arena.children(declaration);
594        let named = arena.named_children(declaration);
595        assert!(all.len() > named.len(), "`const` and `;` are anonymous");
596        assert!(named.iter().all(|h| arena.is_named(*h) == Some(true)));
597    }
598
599    #[test]
600    fn ancestors_run_innermost_first_and_end_at_the_root() {
601        let mut arena = arena("function f() { return 1; }");
602        let root = NodeArena::ROOT;
603        let function = arena.named_children(root)[0];
604        let body = arena
605            .named_children(function)
606            .last()
607            .copied()
608            .expect("has a body");
609        let statement = arena.named_children(body)[0];
610
611        let ancestors = arena.ancestors(statement);
612        assert_eq!(ancestors.first(), Some(&body), "innermost first");
613        assert_eq!(ancestors.last(), Some(&root), "ending at the root");
614        assert!(ancestors.contains(&function));
615    }
616
617    #[test]
618    fn the_root_has_no_ancestors() {
619        let mut arena = arena("const x = 1;");
620        assert!(arena.ancestors(NodeArena::ROOT).is_empty());
621    }
622
623    #[test]
624    fn an_unknown_handle_yields_nothing_rather_than_panicking() {
625        // Rule code is arbitrary and may pass any number at all.
626        let mut arena = arena("const x = 1;");
627        assert_eq!(arena.kind(9999), None);
628        assert_eq!(arena.text(9999), None);
629        assert_eq!(arena.position(9999), None);
630        assert_eq!(arena.parent(9999), None);
631        assert!(arena.children(9999).is_empty());
632        assert!(arena.ancestors(9999).is_empty());
633    }
634
635    #[test]
636    fn interns_a_node_reached_through_the_tree() {
637        // The shape real callers use: find nodes against `tree()`, reduce them to paths
638        // while the borrow is live, then intern once it has ended.
639        let mut arena = arena("const x = 1;");
640
641        let (path, expected_kind) = {
642            let target = arena
643                .tree()
644                .root_node()
645                .child(0)
646                .and_then(|n| n.child(1))
647                .expect("has a declarator");
648            (arena.path_of(target).expect("has a path"), target.kind())
649        };
650
651        let handle = arena.intern_path(path.clone()).expect("interns");
652        assert_eq!(arena.kind(handle), Some(expected_kind));
653        assert_eq!(
654            arena.intern_path(path),
655            Some(handle),
656            "interning the same path twice must give the same handle"
657        );
658    }
659
660    #[test]
661    fn rejects_a_node_from_a_different_tree() {
662        // Reducing a foreign node to a path that happens to resolve locally would hand a
663        // rule a handle pointing at an unrelated node — wrong results with no error.
664        let mut parser = tree_sitter::Parser::new();
665        parser
666            .set_language(&TypeScript.grammar())
667            .expect("grammar loads");
668        let other = parser
669            .parse("function totallyDifferent() { return 42 }", None)
670            .expect("parses");
671        let foreign = other.root_node().child(0).expect("has a child");
672
673        let arena = arena("const x = 1;");
674        assert_eq!(
675            arena.path_of(foreign),
676            None,
677            "a node from another tree must not be reducible to a path here"
678        );
679    }
680
681    #[test]
682    fn text_is_correct_for_multibyte_source() {
683        let mut arena = arena("const emoji = '🎯';\nconst after = 1;");
684        let statements = arena.named_children(NodeArena::ROOT);
685        assert_eq!(arena.text(statements[0]), Some("const emoji = '🎯';"));
686        assert_eq!(
687            arena.position(statements[1]),
688            Some((2, 1)),
689            "a multibyte character must not shift the following line"
690        );
691    }
692
693    // --- structure fingerprint ----------------------------------------------------------
694
695    fn py_arena(source: &str) -> NodeArena {
696        let mut parser = tree_sitter::Parser::new();
697        parser
698            .set_language(&lanekeep_lang_python::Python.grammar())
699            .expect("grammar loads");
700        let tree = parser.parse(source, None).expect("parses");
701        NodeArena::new(tree, source.to_owned())
702    }
703
704    #[test]
705    fn fingerprint_erases_identifier_names() {
706        // Two functions differing only in their names and their local identifiers must
707        // hash identically — that is the whole point of the fold.
708        let a = arena("function f() { return a + b }");
709        let b = arena("function g() { return c + d }");
710        assert_eq!(
711            a.structure_fingerprint(NodeArena::ROOT),
712            b.structure_fingerprint(NodeArena::ROOT)
713        );
714    }
715
716    #[test]
717    fn fingerprint_erases_literal_values_but_not_kinds() {
718        // Identifiers and literals contribute their kind and nothing else, so `1` and `2`
719        // are the same node kind (`number`) and hash alike, while `1` and `'a'` are
720        // different kinds and must not.
721        let one = arena("const x = 1;");
722        let two = arena("const x = 2;");
723        assert_eq!(
724            one.structure_fingerprint(NodeArena::ROOT),
725            two.structure_fingerprint(NodeArena::ROOT)
726        );
727
728        let string = arena("const x = 'a';");
729        assert_ne!(
730            one.structure_fingerprint(NodeArena::ROOT),
731            string.structure_fingerprint(NodeArena::ROOT)
732        );
733    }
734
735    #[test]
736    fn fingerprint_is_operator_sensitive() {
737        // Anonymous token kinds are included, so `+` and `-` — the same shape, different
738        // operators — cannot hash alike.
739        let plus = arena("function f() { return a + b }");
740        let minus = arena("function f() { return a - b }");
741        assert_ne!(
742            plus.structure_fingerprint(NodeArena::ROOT),
743            minus.structure_fingerprint(NodeArena::ROOT)
744        );
745    }
746
747    #[test]
748    fn fingerprint_is_statement_sensitive() {
749        let one = arena("function f() { return a + b }");
750        let two = arena("function f() { return a + b; a() }");
751        assert_ne!(
752            one.structure_fingerprint(NodeArena::ROOT),
753            two.structure_fingerprint(NodeArena::ROOT)
754        );
755    }
756
757    #[test]
758    fn fingerprint_ignores_comments() {
759        // `extra` nodes are excluded from the fold, from the framing and from the count, so
760        // a doc-comment difference is not an implementation difference.
761        let plain = arena("function f() { return a + b }");
762        let commented = arena("// a doc comment\nfunction f() { return a + b }");
763        assert_eq!(
764            plain.structure_fingerprint(NodeArena::ROOT),
765            commented.structure_fingerprint(NodeArena::ROOT)
766        );
767    }
768
769    #[test]
770    fn fingerprint_counts_every_non_extra_node_exactly() {
771        // `const x = 1;` is program, lexical_declaration, the `const` keyword, the
772        // variable_declarator, the `identifier`, the `=`, the `number` and the `;` — eight
773        // nodes, anonymous tokens counted. The count is exact and asserted, not
774        // approximate: it is the thresholding input, so a rule uses it to skip trivially
775        // small matches without a second traversal.
776        let one = arena("const x = 1;");
777        assert_eq!(
778            one.structure_fingerprint(NodeArena::ROOT)
779                .expect("the root resolves")
780                .nodes,
781            8
782        );
783
784        let two = arena("const x = 1;\nconst y = 2;");
785        assert_eq!(
786            two.structure_fingerprint(NodeArena::ROOT)
787                .expect("the root resolves")
788                .nodes,
789            15
790        );
791    }
792
793    #[test]
794    fn fingerprint_is_subtree_scoped() {
795        // The fold covers exactly the subtree rooted at the handle. The same expression
796        // nested under different parents still hashes identically, which is what a rule
797        // fingerprinting a function body relies on.
798        let mut in_function = arena("function f() { return a + b }");
799        let fn_decl = in_function.named_children(NodeArena::ROOT)[0];
800        // `function` keyword, name, parameters, body — the statement block is last.
801        let body = in_function.named_children(fn_decl).last().copied().unwrap();
802        let return_stmt = in_function.named_children(body)[0];
803        let in_function_expr = in_function.named_children(return_stmt)[0];
804
805        let mut in_initializer = arena("const x = a + b;");
806        let declaration = in_initializer.named_children(NodeArena::ROOT)[0];
807        let declarator = in_initializer.named_children(declaration)[0];
808        // name, `=`, value — the expression is last.
809        let in_initializer_expr = in_initializer
810            .named_children(declarator)
811            .last()
812            .copied()
813            .unwrap();
814
815        assert_eq!(
816            in_function.structure_fingerprint(in_function_expr),
817            in_initializer.structure_fingerprint(in_initializer_expr)
818        );
819    }
820
821    #[test]
822    fn fingerprint_is_deterministic_across_calls_and_parses() {
823        let first = arena("function f() { return a + b }");
824        let once = first
825            .structure_fingerprint(NodeArena::ROOT)
826            .expect("the root resolves");
827        let twice = first
828            .structure_fingerprint(NodeArena::ROOT)
829            .expect("the root resolves");
830        assert_eq!(
831            once, twice,
832            "the same arena must answer the same fingerprint twice"
833        );
834
835        let again = arena("function f() { return a + b }");
836        assert_eq!(
837            once,
838            again
839                .structure_fingerprint(NodeArena::ROOT)
840                .expect("the root resolves"),
841            "a fresh parse of the same bytes must hash identically"
842        );
843    }
844
845    #[test]
846    fn fingerprint_of_a_dead_handle_is_none() {
847        let arena = arena("const x = 1;");
848        assert_eq!(arena.structure_fingerprint(9999), None);
849    }
850
851    #[test]
852    fn fingerprint_is_language_agnostic() {
853        // The fold never consults a per-language table — erasure is "kind name and nothing
854        // else" by construction — so the same shape in a non-JS grammar hashes alike too.
855        let a = py_arena("def f():\n    return a + b\n");
856        let b = py_arena("def g():\n    return c + d\n");
857        assert_eq!(
858            a.structure_fingerprint(NodeArena::ROOT),
859            b.structure_fingerprint(NodeArena::ROOT)
860        );
861    }
862
863    #[test]
864    fn fingerprint_normalization_holds_for_python() {
865        // Identifier erasure above is the language-agnosticism claim's primary half; the
866        // sensitivity halves have to hold in a non-JS grammar too, or the fold would be
867        // language-agnostic only where it was convenient. Anonymous operator kinds and
868        // literal kinds are the same mechanism in Python as in TypeScript.
869        let plus = py_arena("def f():\n    return a + b\n");
870        let minus = py_arena("def f():\n    return a - b\n");
871        assert_ne!(
872            plus.structure_fingerprint(NodeArena::ROOT),
873            minus.structure_fingerprint(NodeArena::ROOT),
874            "a `+` and a `-` are different anonymous kinds in Python too"
875        );
876
877        let one = py_arena("def f():\n    return 1\n");
878        let string = py_arena("def f():\n    return 'a'\n");
879        assert_ne!(
880            one.structure_fingerprint(NodeArena::ROOT),
881            string.structure_fingerprint(NodeArena::ROOT),
882            "`integer` and `string` are different kinds in Python too"
883        );
884
885        let single = py_arena("def f():\n    return a + b\n");
886        let two_statements = py_arena("def f():\n    return a + b\n    g()\n");
887        assert_ne!(
888            single.structure_fingerprint(NodeArena::ROOT),
889            two_statements.structure_fingerprint(NodeArena::ROOT),
890            "a second statement changes the shape in Python too"
891        );
892    }
893
894    #[test]
895    fn fingerprint_hashes_a_broken_parse_as_its_broken_shape() {
896        // `ERROR`/`MISSING` nodes fold like any other kind rather than failing: a broken
897        // parse has to produce a defined, stable fingerprint or a rule cannot decide
898        // anything about the file. Assert the source really is a broken parse first, so the
899        // test cannot quietly pass over a source that parses clean.
900        let source = "const x = ;\n";
901        let mut parser = tree_sitter::Parser::new();
902        parser
903            .set_language(&TypeScript.grammar())
904            .expect("grammar loads");
905        let tree = parser.parse(source, None).expect("parses");
906        assert!(
907            tree.root_node().has_error(),
908            "the fixture must really be broken"
909        );
910
911        let arena = NodeArena::new(tree, source.to_owned());
912        let first = arena
913            .structure_fingerprint(NodeArena::ROOT)
914            .expect("a broken parse still folds");
915        let second = arena
916            .structure_fingerprint(NodeArena::ROOT)
917            .expect("a broken parse still folds");
918        assert_eq!(
919            first, second,
920            "the fingerprint of a broken parse must be stable across calls"
921        );
922        assert!(
923            first.nodes >= 2,
924            "the fold covered the erroring shape, not nothing"
925        );
926    }
927}