Skip to main content

llmy_codegraph/
extract.rs

1//! Shared extraction machinery: the raw per-file representation every
2//! language extractor produces, the tree-sitter language registry (including
3//! the vendored Move grammars), and node helpers used by all extractors.
4
5use std::path::PathBuf;
6
7use color_eyre::eyre::eyre;
8use llmy_types::error::LLMYError;
9use tree_sitter::{Language as TsLanguage, Node, Parser, Tree};
10
11use crate::model::{CallableKind, Language, LineSpan, ModuleKind, StateKind};
12
13unsafe extern "C" {
14    fn tree_sitter_move_on_aptos() -> *const std::ffi::c_void;
15    fn tree_sitter_move() -> *const std::ffi::c_void;
16}
17
18/// A source file handed to an extractor, path kept relative to the root.
19#[derive(Debug, Clone)]
20pub struct SourceFile {
21    pub relative: PathBuf,
22    pub content: String,
23}
24
25/// A call site as written in the source, before resolution.
26#[derive(Debug, Clone)]
27pub struct RawCallSite {
28    /// Full call target text (e.g. `token.transfer`, `coin::withdraw`).
29    pub text: String,
30    /// The final name segment used for resolution.
31    pub name: String,
32    /// The qualifier before the final segment, if any (`token`, `coin`).
33    pub qualifier: Option<String>,
34    pub line: usize,
35}
36
37/// A by-name reference from a callable body to a state item, before
38/// resolution.
39#[derive(Debug, Clone)]
40pub struct RawStateRef {
41    pub name: String,
42    pub write: bool,
43    pub line: usize,
44}
45
46#[derive(Debug, Clone)]
47pub struct RawCallable {
48    pub name: String,
49    pub kind: CallableKind,
50    pub signature: String,
51    pub span: LineSpan,
52    pub calls: Vec<RawCallSite>,
53    pub state_refs: Vec<RawStateRef>,
54}
55
56#[derive(Debug, Clone)]
57pub struct RawState {
58    pub name: String,
59    pub kind: StateKind,
60    pub type_text: String,
61    pub span: LineSpan,
62}
63
64#[derive(Debug, Clone)]
65pub struct RawModule {
66    pub name: String,
67    pub kind: ModuleKind,
68    pub span: LineSpan,
69    /// Direct parents by name (Solidity inheritance).
70    pub parents: Vec<String>,
71    pub callables: Vec<RawCallable>,
72    pub states: Vec<RawState>,
73}
74
75/// Everything extracted from one file.
76#[derive(Debug, Clone)]
77pub struct FileExtraction {
78    pub file: PathBuf,
79    pub language: Language,
80    pub modules: Vec<RawModule>,
81    /// Number of ERROR nodes tree-sitter produced — used for dialect picking
82    /// and reported as a quality signal.
83    pub parse_errors: usize,
84}
85
86/// Parser front-end over the four supported grammars.
87pub struct GrammarSet;
88
89impl GrammarSet {
90    pub fn language_of(language: Language) -> TsLanguage {
91        match language {
92            Language::Solidity => tree_sitter_solidity::LANGUAGE.into(),
93            Language::Rust => tree_sitter_rust::LANGUAGE.into(),
94            Language::MoveAptos => {
95                let ptr = unsafe { tree_sitter_move_on_aptos() };
96                unsafe { TsLanguage::from_raw(ptr.cast()) }
97            }
98            Language::MoveSui => {
99                let ptr = unsafe { tree_sitter_move() };
100                unsafe { TsLanguage::from_raw(ptr.cast()) }
101            }
102        }
103    }
104
105    pub fn parse(language: Language, content: &str) -> Result<Tree, LLMYError> {
106        let mut parser = Parser::new();
107        parser
108            .set_language(&Self::language_of(language))
109            .map_err(|e| eyre!("failed to load {} grammar: {}", language.render(), e))?;
110        parser
111            .parse(content, None)
112            .ok_or_else(|| eyre!("{} parse returned no tree", language.render()).into())
113    }
114
115    pub fn count_errors(node: Node<'_>) -> usize {
116        let mut count = 0;
117        let mut cursor = node.walk();
118        let mut stack = vec![node];
119        while let Some(current) = stack.pop() {
120            if current.is_error() || current.is_missing() {
121                count += 1;
122            }
123            for child in current.children(&mut cursor) {
124                stack.push(child);
125            }
126        }
127        count
128    }
129
130    /// Render a parse tree for grammar debugging (kinds, lines and short
131    /// text), used by tests when adapting to a grammar's node kinds.
132    pub fn dump(node: Node<'_>, source: &str, depth: usize) -> String {
133        let mut out = String::new();
134        if node.is_named() {
135            let text = node.text_of(source);
136            let short: String = text
137                .chars()
138                .take(40)
139                .collect::<String>()
140                .replace('\n', "\\n");
141            out.push_str(&format!(
142                "{}{} [{}..{}] {:?}\n",
143                "  ".repeat(depth),
144                node.kind(),
145                node.start_position().row + 1,
146                node.end_position().row + 1,
147                short
148            ));
149        }
150        let mut cursor = node.walk();
151        for child in node.children(&mut cursor) {
152            out.push_str(&Self::dump(child, source, depth + 1));
153        }
154        out
155    }
156}
157
158/// Convenience methods every extractor needs on tree-sitter nodes.
159pub trait NodeUtil<'tree> {
160    fn text_of(&self, source: &str) -> String;
161    fn line_span(&self) -> LineSpan;
162    fn field_text(&self, field: &str, source: &str) -> Option<String>;
163    fn child_of_kind(&self, kind: &str) -> Option<Node<'tree>>;
164    fn children_of_kind(&self, kind: &str) -> Vec<Node<'tree>>;
165    /// Every descendant (including self) whose kind is in `kinds`, in
166    /// document order. Does not descend *into* matched nodes when
167    /// `enter_matches` is false.
168    fn descendants_of_kinds(&self, kinds: &[&str], enter_matches: bool) -> Vec<Node<'tree>>;
169    /// The first identifier-like descendant's text.
170    fn first_identifier(&self, source: &str) -> Option<String>;
171    /// The declaration's text cut at the start of its body child — the
172    /// signature head as written, whitespace-normalized.
173    fn signature_head(&self, body_kinds: &[&str], source: &str) -> String;
174}
175
176impl<'tree> NodeUtil<'tree> for Node<'tree> {
177    fn text_of(&self, source: &str) -> String {
178        source
179            .get(self.start_byte()..self.end_byte())
180            .unwrap_or_default()
181            .to_string()
182    }
183
184    fn line_span(&self) -> LineSpan {
185        LineSpan {
186            start_line: self.start_position().row + 1,
187            end_line: self.end_position().row + 1,
188        }
189    }
190
191    fn field_text(&self, field: &str, source: &str) -> Option<String> {
192        self.child_by_field_name(field.as_bytes())
193            .map(|node| node.text_of(source))
194    }
195
196    fn child_of_kind(&self, kind: &str) -> Option<Node<'tree>> {
197        let mut cursor = self.walk();
198        self.children(&mut cursor).find(|c| c.kind() == kind)
199    }
200
201    fn children_of_kind(&self, kind: &str) -> Vec<Node<'tree>> {
202        let mut cursor = self.walk();
203        self.children(&mut cursor)
204            .filter(|c| c.kind() == kind)
205            .collect()
206    }
207
208    fn descendants_of_kinds(&self, kinds: &[&str], enter_matches: bool) -> Vec<Node<'tree>> {
209        let mut out = vec![];
210        let mut stack = vec![*self];
211        while let Some(current) = stack.pop() {
212            let matched = kinds.contains(&current.kind());
213            if matched && current.id() != self.id() {
214                out.push(current);
215                if !enter_matches {
216                    continue;
217                }
218            }
219            let mut cursor = current.walk();
220            let mut children: Vec<_> = current.children(&mut cursor).collect();
221            children.reverse();
222            stack.extend(children);
223        }
224        // Depth-first with a stack visits in reverse; restore document order.
225        out.sort_by_key(|node| node.start_byte());
226        out
227    }
228
229    fn first_identifier(&self, source: &str) -> Option<String> {
230        if self.kind().contains("identifier") {
231            return Some(self.text_of(source));
232        }
233        let mut stack = vec![*self];
234        let mut found: Vec<(usize, String)> = vec![];
235        while let Some(current) = stack.pop() {
236            if current.kind().contains("identifier") {
237                found.push((current.start_byte(), current.text_of(source)));
238                continue;
239            }
240            let mut cursor = current.walk();
241            for child in current.children(&mut cursor) {
242                stack.push(child);
243            }
244        }
245        found.sort();
246        found.into_iter().next().map(|(_, text)| text)
247    }
248
249    fn signature_head(&self, body_kinds: &[&str], source: &str) -> String {
250        let end = {
251            let mut cursor = self.walk();
252            self.children(&mut cursor)
253                .find(|c| body_kinds.contains(&c.kind()))
254                .map(|body| body.start_byte())
255                .unwrap_or(self.end_byte())
256        };
257        let head = source.get(self.start_byte()..end).unwrap_or_default();
258        head.split_whitespace().collect::<Vec<_>>().join(" ")
259    }
260}