1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
//! Shared traversal state and tree-sitter helpers for language extractors.
//!
//! Most tree-sitter based extractors need the same bookkeeping while walking
//! an AST: accumulators for nodes/edges, a stack of enclosing scopes for
//! qualified names, and small node-search utilities. Extractors with extra
//! per-language state (e.g. C++ access specifiers) keep their own state
//! structs; everything else shares this one.
use std::time::{Instant, SystemTime, UNIX_EPOCH};
use tree_sitter::Node as TsNode;
use crate::types::{Edge, ExtractionResult, Node, UnresolvedRef, Visibility};
/// Internal state used during AST traversal.
pub(crate) struct ExtractionState {
pub(crate) nodes: Vec<Node>,
pub(crate) edges: Vec<Edge>,
pub(crate) unresolved_refs: Vec<UnresolvedRef>,
pub(crate) errors: Vec<String>,
/// Stack of (name, `node_id`) for building qualified names and parent edges.
pub(crate) node_stack: Vec<(String, String)>,
pub(crate) file_path: String,
pub(crate) source: Vec<u8>,
pub(crate) timestamp: u64,
/// Nesting depth of enclosing class-like scopes (used by extractors that
/// treat top-level and member functions differently; others leave it 0).
pub(crate) class_depth: usize,
/// Current Ruby visibility mode inside a class/module body (private/protected/
/// public switches). Other extractors leave it at the default Pub.
pub(crate) visibility_mode: Visibility,
/// Node IDs of Ruby singleton methods (`def self.foo` / `def obj.foo`), so
/// retroactive visibility (`private_class_method :foo` vs `private :foo`) can
/// tell a singleton from a same-named instance method — they share a kind and
/// qualified name. Other extractors leave it empty.
pub(crate) singleton_method_ids: Vec<String>,
}
impl ExtractionState {
pub(crate) fn new(file_path: &str, source: &str) -> Self {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
Self {
nodes: Vec::new(),
edges: Vec::new(),
unresolved_refs: Vec::new(),
errors: Vec::new(),
node_stack: Vec::new(),
file_path: file_path.to_string(),
source: source.as_bytes().to_vec(),
timestamp,
class_depth: 0,
visibility_mode: Visibility::Pub,
singleton_method_ids: Vec::new(),
}
}
/// Returns the current qualified name prefix from the node stack.
pub(crate) fn qualified_prefix(&self) -> String {
let mut parts = vec![self.file_path.clone()];
for (name, _) in &self.node_stack {
parts.push(name.clone());
}
parts.join("::")
}
/// Returns the current parent node ID, or None if at file root level.
pub(crate) fn parent_node_id(&self) -> Option<&str> {
self.node_stack.last().map(|(_, id)| id.as_str())
}
/// Gets the text of a tree-sitter node from the source.
pub(crate) fn node_text(&self, node: TsNode<'_>) -> String {
node.utf8_text(&self.source)
.unwrap_or("<invalid utf8>")
.to_string()
}
/// Consumes the state into an `ExtractionResult`, stamping the duration.
pub(crate) fn build_result(self, start: Instant) -> ExtractionResult {
ExtractionResult {
nodes: self.nodes,
edges: self.edges,
unresolved_refs: self.unresolved_refs,
errors: self.errors,
duration_ms: start.elapsed().as_millis() as u64,
}
}
}
/// Find the first direct child of a node with a given kind.
pub(crate) fn find_child_by_kind<'a>(node: TsNode<'a>, kind: &str) -> Option<TsNode<'a>> {
let mut cursor = node.walk();
if cursor.goto_first_child() {
loop {
let child = cursor.node();
if child.kind() == kind {
return Some(child);
}
if !cursor.goto_next_sibling() {
break;
}
}
}
None
}
/// Find the first descendant of a node with a given kind (recursive DFS).
pub(crate) fn find_descendant_by_kind<'a>(node: TsNode<'a>, kind: &str) -> Option<TsNode<'a>> {
let mut stack = vec![node];
while let Some(current) = stack.pop() {
if current.kind() == kind {
return Some(current);
}
// Push children via cursor (O(N) per node) and reverse so the
// first child pops first. Previous revision used `current.child(i)`
// in a `for i in (0..N).rev()` loop, which is O(N²) per node
// because `child(i)` walks sibling links from index 0.
let start = stack.len();
let mut cursor = current.walk();
if cursor.goto_first_child() {
loop {
stack.push(cursor.node());
if !cursor.goto_next_sibling() {
break;
}
}
}
stack[start..].reverse();
}
None
}
/// Returns true if the node has a direct child of the given kind.
pub(crate) fn has_child_kind(node: TsNode<'_>, kind: &str) -> bool {
find_child_by_kind(node, kind).is_some()
}