1use std::collections::{BTreeMap, BTreeSet};
8use std::path::{Path, PathBuf};
9
10use color_eyre::eyre::eyre;
11use llmy_types::error::LLMYError;
12use serde::{Deserialize, Serialize};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case")]
16pub enum Language {
17 Solidity,
18 Rust,
19 MoveAptos,
20 MoveSui,
21}
22
23impl Language {
24 pub fn render(&self) -> &'static str {
25 match self {
26 Self::Solidity => "Solidity",
27 Self::Rust => "Rust",
28 Self::MoveAptos => "Move (Aptos)",
29 Self::MoveSui => "Move (Sui)",
30 }
31 }
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(rename_all = "snake_case")]
36pub enum ModuleKind {
37 Contract,
38 Interface,
39 Library,
40 Module,
42}
43
44impl ModuleKind {
45 pub fn render(&self) -> &'static str {
46 match self {
47 Self::Contract => "contract",
48 Self::Interface => "interface",
49 Self::Library => "library",
50 Self::Module => "module",
51 }
52 }
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
56#[serde(rename_all = "snake_case")]
57pub enum CallableKind {
58 Function,
59 Constructor,
60 Modifier,
61 Fallback,
62 Receive,
63 Entry,
66}
67
68impl CallableKind {
69 pub fn render(&self) -> &'static str {
70 match self {
71 Self::Function => "function",
72 Self::Constructor => "constructor",
73 Self::Modifier => "modifier",
74 Self::Fallback => "fallback",
75 Self::Receive => "receive",
76 Self::Entry => "entry",
77 }
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(rename_all = "snake_case")]
83pub enum StateKind {
84 StateVariable,
86 AnchorAccount,
88 CwItem,
90 CwMap,
92 MoveResource,
94 SuiObject,
96}
97
98impl StateKind {
99 pub fn render(&self) -> &'static str {
100 match self {
101 Self::StateVariable => "state variable",
102 Self::AnchorAccount => "anchor account",
103 Self::CwItem => "cw item",
104 Self::CwMap => "cw map",
105 Self::MoveResource => "move resource",
106 Self::SuiObject => "sui object",
107 }
108 }
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
112#[serde(rename_all = "snake_case")]
113pub enum AccessKind {
114 Read,
115 Write,
116}
117
118impl AccessKind {
119 pub fn render(&self) -> &'static str {
120 match self {
121 Self::Read => "read",
122 Self::Write => "write",
123 }
124 }
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
129pub struct LineSpan {
130 pub start_line: usize,
131 pub end_line: usize,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct Module {
136 pub id: i64,
137 pub language: Language,
138 pub kind: ModuleKind,
139 pub name: String,
140 pub file: PathBuf,
142 pub span: LineSpan,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct Callable {
147 pub id: i64,
148 pub module_id: i64,
149 pub kind: CallableKind,
150 pub name: String,
151 pub signature: String,
153 pub file: PathBuf,
154 pub span: LineSpan,
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct StateItem {
159 pub id: i64,
160 pub module_id: i64,
161 pub kind: StateKind,
162 pub name: String,
163 pub type_text: String,
165 pub file: PathBuf,
166 pub span: LineSpan,
167}
168
169#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
172#[serde(rename_all = "snake_case")]
173pub enum CalleeRef {
174 Resolved(i64),
175 Ambiguous(Vec<i64>),
176 External(String),
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct CallEdge {
183 pub caller_id: i64,
184 pub callee: CalleeRef,
185 pub callee_text: String,
187 pub line: usize,
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct StateEdge {
192 pub callable_id: i64,
193 pub state_id: i64,
194 pub access: AccessKind,
195 pub line: usize,
196}
197
198#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
199#[serde(rename_all = "snake_case")]
200pub enum ParentRef {
201 Resolved(i64),
202 External(String),
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct InheritEdge {
207 pub module_id: i64,
208 pub parent: ParentRef,
209}
210
211#[derive(Debug, Clone, Default, Serialize, Deserialize)]
213pub struct CodeGraph {
214 pub modules: BTreeMap<i64, Module>,
215 pub callables: BTreeMap<i64, Callable>,
216 pub states: BTreeMap<i64, StateItem>,
217 pub call_edges: Vec<CallEdge>,
218 pub state_edges: Vec<StateEdge>,
219 pub inherit_edges: Vec<InheritEdge>,
220}
221
222impl CodeGraph {
223 pub fn is_empty(&self) -> bool {
224 self.modules.is_empty()
225 }
226
227 pub fn modules_by_name(&self, name: &str) -> Vec<&Module> {
228 self.modules.values().filter(|m| m.name == name).collect()
229 }
230
231 pub fn callables_of_module(&self, module_id: i64) -> Vec<&Callable> {
232 self.callables
233 .values()
234 .filter(|c| c.module_id == module_id)
235 .collect()
236 }
237
238 pub fn states_of_module(&self, module_id: i64) -> Vec<&StateItem> {
239 self.states
240 .values()
241 .filter(|s| s.module_id == module_id)
242 .collect()
243 }
244
245 pub fn find_callables(&self, module: Option<&str>, name: &str) -> Vec<&Callable> {
247 self.callables
248 .values()
249 .filter(|c| c.name == name)
250 .filter(|c| match module {
251 Some(module) => self
252 .modules
253 .get(&c.module_id)
254 .map(|m| m.name == module)
255 .unwrap_or(false),
256 None => true,
257 })
258 .collect()
259 }
260
261 pub fn find_states(&self, module: Option<&str>, name: &str) -> Vec<&StateItem> {
263 self.states
264 .values()
265 .filter(|s| s.name == name)
266 .filter(|s| match module {
267 Some(module) => self
268 .modules
269 .get(&s.module_id)
270 .map(|m| m.name == module)
271 .unwrap_or(false),
272 None => true,
273 })
274 .collect()
275 }
276
277 pub fn outgoing_calls(&self, caller_id: i64) -> Vec<&CallEdge> {
278 self.call_edges
279 .iter()
280 .filter(|e| e.caller_id == caller_id)
281 .collect()
282 }
283
284 pub fn incoming_calls(&self, callee_id: i64) -> Vec<&CallEdge> {
285 self.call_edges
286 .iter()
287 .filter(|e| match &e.callee {
288 CalleeRef::Resolved(id) => *id == callee_id,
289 CalleeRef::Ambiguous(ids) => ids.contains(&callee_id),
290 CalleeRef::External(_) => false,
291 })
292 .collect()
293 }
294
295 pub fn state_accesses_of(&self, callable_id: i64) -> Vec<&StateEdge> {
296 self.state_edges
297 .iter()
298 .filter(|e| e.callable_id == callable_id)
299 .collect()
300 }
301
302 pub fn accessors_of_state(&self, state_id: i64) -> Vec<&StateEdge> {
303 self.state_edges
304 .iter()
305 .filter(|e| e.state_id == state_id)
306 .collect()
307 }
308
309 pub fn ancestors_of(&self, module_id: i64) -> BTreeSet<i64> {
312 let mut out = BTreeSet::new();
313 let mut frontier = vec![module_id];
314 while let Some(current) = frontier.pop() {
315 for edge in self.inherit_edges.iter().filter(|e| e.module_id == current) {
316 if let ParentRef::Resolved(parent) = edge.parent
317 && out.insert(parent)
318 {
319 frontier.push(parent);
320 }
321 }
322 }
323 out
324 }
325
326 pub fn descendants_of(&self, module_id: i64) -> BTreeSet<i64> {
329 let mut out = BTreeSet::new();
330 let mut frontier = vec![module_id];
331 while let Some(current) = frontier.pop() {
332 for edge in self.inherit_edges.iter() {
333 if edge.parent == ParentRef::Resolved(current) && out.insert(edge.module_id) {
334 frontier.push(edge.module_id);
335 }
336 }
337 }
338 out
339 }
340
341 pub fn visible_states(&self, module_id: i64) -> Vec<&StateItem> {
344 let mut scope = self.ancestors_of(module_id);
345 scope.insert(module_id);
346 self.states
347 .values()
348 .filter(|s| scope.contains(&s.module_id))
349 .collect()
350 }
351
352 pub fn render_callee(&self, callee: &CalleeRef) -> String {
353 match callee {
354 CalleeRef::Resolved(id) => self.render_callable_ref(*id),
355 CalleeRef::Ambiguous(ids) => {
356 let rendered = ids
357 .iter()
358 .map(|id| self.render_callable_ref(*id))
359 .collect::<Vec<_>>()
360 .join(" | ");
361 format!("ambiguous({rendered})")
362 }
363 CalleeRef::External(name) => format!("external({name})"),
364 }
365 }
366
367 pub fn render_callable_ref(&self, callable_id: i64) -> String {
368 match self.callables.get(&callable_id) {
369 Some(callable) => {
370 let module = self
371 .modules
372 .get(&callable.module_id)
373 .map(|m| m.name.as_str())
374 .unwrap_or("?");
375 format!("{}.{}", module, callable.name)
376 }
377 None => format!("#{callable_id}"),
378 }
379 }
380
381 pub async fn read_span(
383 &self,
384 root: &Path,
385 file: &Path,
386 span: LineSpan,
387 ) -> Result<String, LLMYError> {
388 let content = tokio::fs::read_to_string(root.join(file)).await?;
389 let start = span.start_line.saturating_sub(1);
390 let count = span.end_line.saturating_sub(start);
391 let selected = content
392 .lines()
393 .skip(start)
394 .take(count)
395 .collect::<Vec<_>>()
396 .join("\n");
397 if selected.is_empty() {
398 return Err(eyre!(
399 "span {}..{} of {} is empty or out of range",
400 span.start_line,
401 span.end_line,
402 file.display()
403 )
404 .into());
405 }
406 Ok(selected)
407 }
408
409 pub fn counts(&self) -> String {
410 format!(
411 "{} modules, {} callables, {} state items, {} call edges, {} state edges, {} inheritance edges",
412 self.modules.len(),
413 self.callables.len(),
414 self.states.len(),
415 self.call_edges.len(),
416 self.state_edges.len(),
417 self.inherit_edges.len()
418 )
419 }
420}
421
422#[cfg(test)]
423mod tests {
424 use super::*;
425
426 fn graph_with_inheritance() -> CodeGraph {
427 let mut graph = CodeGraph::default();
428 for (id, name) in [(1, "Base"), (2, "Mid"), (3, "Leaf")] {
429 graph.modules.insert(
430 id,
431 Module {
432 id,
433 language: Language::Solidity,
434 kind: ModuleKind::Contract,
435 name: name.to_string(),
436 file: PathBuf::from("a.sol"),
437 span: LineSpan {
438 start_line: 1,
439 end_line: 10,
440 },
441 },
442 );
443 }
444 graph.inherit_edges.push(InheritEdge {
445 module_id: 2,
446 parent: ParentRef::Resolved(1),
447 });
448 graph.inherit_edges.push(InheritEdge {
449 module_id: 3,
450 parent: ParentRef::Resolved(2),
451 });
452 graph
453 }
454
455 #[test]
456 fn ancestors_and_descendants_are_transitive() {
457 let graph = graph_with_inheritance();
458 assert_eq!(graph.ancestors_of(3), BTreeSet::from([1, 2]));
459 assert_eq!(graph.descendants_of(1), BTreeSet::from([2, 3]));
460 assert!(graph.ancestors_of(1).is_empty());
461 }
462
463 #[test]
464 fn visible_states_include_inherited_ones() {
465 let mut graph = graph_with_inheritance();
466 graph.states.insert(
467 1,
468 StateItem {
469 id: 1,
470 module_id: 1,
471 kind: StateKind::StateVariable,
472 name: "owner".to_string(),
473 type_text: "address".to_string(),
474 file: PathBuf::from("a.sol"),
475 span: LineSpan {
476 start_line: 2,
477 end_line: 2,
478 },
479 },
480 );
481 let visible = graph.visible_states(3);
482 assert_eq!(visible.len(), 1);
483 assert_eq!(visible[0].name, "owner");
484 assert!(graph.visible_states(1).len() == 1);
485 }
486
487 #[test]
488 fn incoming_calls_match_resolved_and_ambiguous() {
489 let mut graph = graph_with_inheritance();
490 for (id, name) in [(10, "a"), (11, "b"), (12, "c")] {
491 graph.callables.insert(
492 id,
493 Callable {
494 id,
495 module_id: 1,
496 kind: CallableKind::Function,
497 name: name.to_string(),
498 signature: format!("function {name}()"),
499 file: PathBuf::from("a.sol"),
500 span: LineSpan {
501 start_line: 3,
502 end_line: 5,
503 },
504 },
505 );
506 }
507 graph.call_edges.push(CallEdge {
508 caller_id: 10,
509 callee: CalleeRef::Resolved(11),
510 callee_text: "b".to_string(),
511 line: 4,
512 });
513 graph.call_edges.push(CallEdge {
514 caller_id: 12,
515 callee: CalleeRef::Ambiguous(vec![10, 11]),
516 callee_text: "ab".to_string(),
517 line: 4,
518 });
519
520 assert_eq!(graph.incoming_calls(11).len(), 2);
521 assert_eq!(graph.incoming_calls(10).len(), 1);
522 assert_eq!(graph.outgoing_calls(10).len(), 1);
523 }
524}