semantic/parser/comment_walk.rs
1// SPDX-License-Identifier: Apache-2.0
2//! The single source of truth for "which tree-sitter nodes are comments" and
3//! the non-comment leaf walk built on it.
4//!
5//! Both change-classification (`strip_comments`) and the semantic-index token
6//! stream (`hd-sem-sym-v1`) need to walk an AST in document order while
7//! skipping comment subtrees. Keeping the predicate and the walk here — in the
8//! parser crate — means there is exactly one definition of "comment" and one
9//! traversal shape; the consumers only differ in what they do with each leaf.
10
11use tree_sitter::Node;
12
13/// Whether a tree-sitter node kind names a comment. Comment subtrees are
14/// skipped wholesale by [`walk_non_comment_leaves`], so doc-comments and
15/// ordinary comments alike are excluded from semantic fingerprints.
16pub fn is_comment_node(kind: &str) -> bool {
17 matches!(
18 kind,
19 "comment" | "line_comment" | "block_comment" | "doc_comment" | "string_comment"
20 )
21}
22
23/// DFS in document order over `node`'s subtree, invoking `on_leaf` for each
24/// non-comment leaf (a node with no children). Comment nodes — and their
25/// entire subtrees — are skipped.
26///
27/// The stack pushes children in reverse so they pop in source order, giving a
28/// stable pre-order document-order traversal. Iterative (not recursive) so
29/// deeply-nested-but-valid input drives heap, not call depth.
30pub fn walk_non_comment_leaves<'tree>(node: Node<'tree>, mut on_leaf: impl FnMut(Node<'tree>)) {
31 let mut stack = vec![node];
32 while let Some(current) = stack.pop() {
33 if is_comment_node(current.kind()) {
34 continue;
35 }
36 if current.child_count() == 0 {
37 on_leaf(current);
38 continue;
39 }
40 let child_count = current.child_count();
41 for index in (0..child_count).rev() {
42 if let Some(child) = current.child(index as u32) {
43 stack.push(child);
44 }
45 }
46 }
47}