1use std::path::Path;
2
3use crate::{
4 error::Result,
5 graph::{Edge, GraphDiff, Node},
6 schema::{EdgeConfidence, EdgeKind, NodeKind, Visibility},
7};
8
9#[derive(Debug, Default, Clone)]
12pub struct AttributeFilter {
13 pub kind: Option<NodeKind>,
14 pub is_async: Option<bool>,
15 pub visibility: Option<Visibility>,
16 pub min_complexity: Option<u32>,
19 pub max_complexity: Option<u32>,
21 pub name_contains: Option<String>,
23 pub annotation: Option<String>,
27}
28
29impl AttributeFilter {
30 pub fn matches(&self, node: &Node) -> bool {
32 if let Some(k) = &self.kind {
33 if &node.kind != k {
34 return false;
35 }
36 }
37 if let Some(a) = self.is_async {
38 if node.metadata.is_async != a {
39 return false;
40 }
41 }
42 if let Some(v) = &self.visibility {
43 if &node.metadata.visibility != v {
44 return false;
45 }
46 }
47 if let Some(min) = self.min_complexity {
48 match node.metadata.lld.complexity {
49 Some(c) if c >= min => {}
50 _ => return false,
51 }
52 }
53 if let Some(max) = self.max_complexity {
54 match node.metadata.lld.complexity {
55 Some(c) if c <= max => {}
56 _ => return false,
57 }
58 }
59 if let Some(sub) = &self.name_contains {
60 if !node
61 .name
62 .to_ascii_lowercase()
63 .contains(&sub.to_ascii_lowercase())
64 {
65 return false;
66 }
67 }
68 if let Some(ann) = &self.annotation {
69 let needle = ann.to_ascii_lowercase();
70 if !node
71 .metadata
72 .annotations
73 .iter()
74 .any(|a| a.to_ascii_lowercase().contains(&needle))
75 {
76 return false;
77 }
78 }
79 true
80 }
81
82 pub fn is_empty(&self) -> bool {
84 self.kind.is_none()
85 && self.is_async.is_none()
86 && self.visibility.is_none()
87 && self.min_complexity.is_none()
88 && self.max_complexity.is_none()
89 && self.name_contains.is_none()
90 && self.annotation.is_none()
91 }
92}
93
94pub struct SubGraph {
96 pub nodes: Vec<Node>,
97 pub edges: Vec<Edge>,
98}
99
100pub struct CallersDeep {
102 pub hops: Vec<Vec<Node>>,
104 pub risk_level: &'static str,
106}
107
108pub struct GraphStats {
112 pub total_nodes: u64,
113 pub total_edges: u64,
114 pub nodes_by_kind: Vec<(String, u64)>,
116 pub edges_by_kind: Vec<(String, u64)>,
118}
119
120pub struct CallSite {
122 pub caller: Node,
123 pub line: Option<u32>,
125}
126
127pub struct TypeHierarchy {
129 pub supertypes: Vec<Node>,
131 pub subtypes: Vec<Node>,
133}
134
135pub struct SymbolContext {
137 pub definition: Node,
139 pub callers: Vec<Node>,
141 pub callees: Vec<Node>,
143 pub used_by: Vec<Node>,
145}
146
147pub trait GraphStore: Send + Sync {
153 fn apply_diff(&mut self, branch: &str, diff: &GraphDiff) -> Result<()>;
157
158 fn lookup_symbol(&self, branch: &str, name: &str, fuzzy: bool) -> Result<Vec<Node>>;
164
165 fn find_callers(&self, branch: &str, function_name: &str) -> Result<Vec<Node>>;
168
169 fn find_callers_with_confidence(
173 &self,
174 branch: &str,
175 function_name: &str,
176 ) -> Result<Vec<(Node, EdgeConfidence)>> {
177 Ok(self
178 .find_callers(branch, function_name)?
179 .into_iter()
180 .map(|n| (n, EdgeConfidence::Inferred))
181 .collect())
182 }
183
184 fn find_callers_by_id_with_confidence(
191 &self,
192 branch: &str,
193 target_id: &str,
194 ) -> Result<Vec<(Node, EdgeConfidence)>> {
195 let callers: std::collections::HashMap<String, Node> = self
196 .list_all_nodes(branch)?
197 .into_iter()
198 .map(|n| (n.id.as_str(), n))
199 .collect();
200 let mut out = Vec::new();
201 for edge in self.list_all_edges(branch)? {
202 if edge.kind == EdgeKind::Calls && edge.dst.as_str() == target_id {
203 if let Some(node) = callers.get(&edge.src.as_str()) {
204 out.push((node.clone(), edge.confidence));
205 }
206 }
207 }
208 Ok(out)
209 }
210
211 fn find_callers_deep(
215 &self,
216 branch: &str,
217 function_name: &str,
218 depth: u8,
219 ) -> Result<CallersDeep>;
220
221 fn symbol_context(&self, branch: &str, name: &str) -> Result<SymbolContext>;
224
225 fn list_definitions(&self, branch: &str, file: &Path) -> Result<Vec<Node>>;
227
228 fn list_all_nodes(&self, branch: &str) -> Result<Vec<Node>>;
230
231 fn list_all_edges(&self, branch: &str) -> Result<Vec<Edge>>;
233
234 fn list_nodes_page(&self, branch: &str, offset: usize, limit: usize) -> Result<Vec<Node>> {
240 let mut nodes = self.list_all_nodes(branch)?;
241 nodes.sort_by_key(|node| node.id.as_str());
242 Ok(nodes.into_iter().skip(offset).take(limit).collect())
243 }
244
245 fn list_edges_page(&self, branch: &str, offset: usize, limit: usize) -> Result<Vec<Edge>> {
248 let mut edges = self.list_all_edges(branch)?;
249 edges.sort_by_key(|edge| {
250 (
251 edge.src.as_str(),
252 edge.dst.as_str(),
253 edge.kind.to_string(),
254 edge.line,
255 )
256 });
257 Ok(edges.into_iter().skip(offset).take(limit).collect())
258 }
259
260 fn list_edges_by_kind(&self, branch: &str, kind: EdgeKind) -> Result<Vec<Edge>> {
264 Ok(self
265 .list_all_edges(branch)?
266 .into_iter()
267 .filter(|e| e.kind == kind)
268 .collect())
269 }
270
271 fn search_by_attributes(
277 &self,
278 branch: &str,
279 filter: &AttributeFilter,
280 limit: usize,
281 ) -> Result<Vec<Node>> {
282 let mut nodes: Vec<Node> = self
283 .list_all_nodes(branch)?
284 .into_iter()
285 .filter(|n| filter.matches(n))
286 .collect();
287 nodes.truncate(limit);
288 Ok(nodes)
289 }
290
291 fn graph_stats(&self, branch: &str) -> Result<GraphStats> {
296 use std::collections::HashMap;
297
298 fn tally<T, F>(items: &[T], key: F) -> Vec<(String, u64)>
299 where
300 F: Fn(&T) -> String,
301 {
302 let mut counts: HashMap<String, u64> = HashMap::new();
303 for item in items {
304 *counts.entry(key(item)).or_insert(0) += 1;
305 }
306 let mut pairs: Vec<(String, u64)> = counts.into_iter().collect();
307 pairs.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
309 pairs
310 }
311
312 let nodes = self.list_all_nodes(branch)?;
313 let edges = self.list_all_edges(branch)?;
314 Ok(GraphStats {
315 total_nodes: nodes.len() as u64,
316 total_edges: edges.len() as u64,
317 nodes_by_kind: tally(&nodes, |n| n.kind.to_string()),
318 edges_by_kind: tally(&edges, |e| e.kind.to_string()),
319 })
320 }
321
322 fn search_nodes(&self, branch: &str, query: &str, limit: usize) -> Result<Vec<Node>> {
329 let q = query.to_ascii_lowercase();
330 let mut nodes: Vec<Node> = self
331 .list_all_nodes(branch)?
332 .into_iter()
333 .filter(|n| {
334 n.name.to_ascii_lowercase().contains(&q)
335 || n.qualified_name.to_ascii_lowercase().contains(&q)
336 })
337 .collect();
338 nodes.truncate(limit);
339 Ok(nodes)
340 }
341
342 fn get_nodes_by_ids(&self, branch: &str, ids: &[String]) -> Result<Vec<Node>> {
348 let idset: std::collections::HashSet<&str> = ids.iter().map(String::as_str).collect();
349 Ok(self
350 .list_all_nodes(branch)?
351 .into_iter()
352 .filter(|n| idset.contains(n.id.as_str().as_str()))
353 .collect())
354 }
355
356 fn branch_diff(&self, from: &str, to: &str) -> Result<GraphDiff>;
360
361 fn find_callees(&self, branch: &str, function_name: &str, depth: u8) -> Result<CallersDeep>;
366
367 fn find_implementors(&self, branch: &str, trait_or_interface_name: &str) -> Result<Vec<Node>>;
369
370 fn module_dependencies(&self, branch: &str, module_name: &str) -> Result<Vec<Node>> {
378 use crate::schema::{EdgeKind, NodeKind};
379 use std::collections::{HashMap, HashSet};
380
381 let nodes = self.list_all_nodes(branch)?;
382 let edges = self.list_all_edges(branch)?;
383
384 let src_ids: HashSet<String> = nodes
386 .iter()
387 .filter(|n| n.kind == NodeKind::Module && n.name == module_name)
388 .map(|n| n.id.as_str())
389 .collect();
390 if src_ids.is_empty() {
391 return Ok(Vec::new());
392 }
393
394 let id_to_file: HashMap<String, String> = nodes
396 .iter()
397 .map(|n| (n.id.as_str(), n.file.to_string_lossy().into_owned()))
398 .collect();
399 let file_to_module: HashMap<String, &Node> = nodes
400 .iter()
401 .filter(|n| n.kind == NodeKind::Module)
402 .map(|n| (n.file.to_string_lossy().into_owned(), n))
403 .collect();
404
405 let src_files: HashSet<&String> =
406 src_ids.iter().filter_map(|id| id_to_file.get(id)).collect();
407
408 let mut seen: HashSet<String> = HashSet::new();
409 let mut deps: Vec<Node> = Vec::new();
410 for e in &edges {
411 if !matches!(e.kind, EdgeKind::Imports) {
412 continue;
413 }
414 if !src_ids.contains(&e.src.as_str()) {
415 continue;
416 }
417 let Some(sym_file) = id_to_file.get(&e.dst.as_str()) else {
419 continue;
420 };
421 if src_files.contains(sym_file) {
423 continue;
424 }
425 if let Some(dst_mod) = file_to_module.get(sym_file) {
426 if seen.insert(dst_mod.id.as_str()) {
427 deps.push((*dst_mod).clone());
428 }
429 }
430 }
431 Ok(deps)
432 }
433
434 fn find_type_usages(&self, branch: &str, type_name: &str) -> Result<Vec<Node>> {
441 use crate::schema::EdgeKind;
442 use std::collections::HashSet;
443
444 let nodes = self.list_all_nodes(branch)?;
445 let edges = self.list_all_edges(branch)?;
446
447 let target_ids: HashSet<String> = nodes
448 .iter()
449 .filter(|n| n.name == type_name)
450 .map(|n| n.id.as_str())
451 .collect();
452 if target_ids.is_empty() {
453 return Ok(Vec::new());
454 }
455
456 let user_ids: Vec<String> = edges
457 .iter()
458 .filter(|e| matches!(e.kind, EdgeKind::Uses) && target_ids.contains(&e.dst.as_str()))
459 .map(|e| e.src.as_str())
460 .collect();
461 self.get_nodes_by_ids(branch, &user_ids)
462 }
463
464 fn find_call_sites(&self, branch: &str, function_name: &str) -> Result<Vec<CallSite>> {
472 use crate::schema::EdgeKind;
473 use std::collections::HashMap;
474
475 let nodes = self.list_all_nodes(branch)?;
476 let edges = self.list_all_edges(branch)?;
477
478 let target_ids: std::collections::HashSet<String> = nodes
479 .iter()
480 .filter(|n| n.name == function_name)
481 .map(|n| n.id.as_str())
482 .collect();
483 if target_ids.is_empty() {
484 return Ok(Vec::new());
485 }
486
487 let by_id: HashMap<String, &Node> = nodes.iter().map(|n| (n.id.as_str(), n)).collect();
488
489 let mut sites = Vec::new();
490 for e in &edges {
491 if matches!(e.kind, EdgeKind::Calls) && target_ids.contains(&e.dst.as_str()) {
492 if let Some(caller) = by_id.get(&e.src.as_str()) {
493 sites.push(CallSite {
494 caller: (*caller).clone(),
495 line: e.line,
496 });
497 }
498 }
499 }
500 Ok(sites)
501 }
502
503 fn find_importers(&self, branch: &str, symbol_name: &str) -> Result<Vec<Node>> {
509 use crate::schema::EdgeKind;
510 use std::collections::HashSet;
511
512 let nodes = self.list_all_nodes(branch)?;
513 let edges = self.list_all_edges(branch)?;
514
515 let target_ids: HashSet<String> = nodes
516 .iter()
517 .filter(|n| n.name == symbol_name)
518 .map(|n| n.id.as_str())
519 .collect();
520 if target_ids.is_empty() {
521 return Ok(Vec::new());
522 }
523
524 let importer_ids: Vec<String> = edges
525 .iter()
526 .filter(|e| matches!(e.kind, EdgeKind::Imports) && target_ids.contains(&e.dst.as_str()))
527 .map(|e| e.src.as_str())
528 .collect();
529 self.get_nodes_by_ids(branch, &importer_ids)
530 }
531
532 fn type_hierarchy(&self, branch: &str, name: &str) -> Result<TypeHierarchy> {
539 use crate::schema::EdgeKind;
540 use std::collections::HashSet;
541
542 let nodes = self.list_all_nodes(branch)?;
543 let edges = self.list_all_edges(branch)?;
544
545 let self_ids: HashSet<String> = nodes
546 .iter()
547 .filter(|n| n.name == name)
548 .map(|n| n.id.as_str())
549 .collect();
550 if self_ids.is_empty() {
551 return Ok(TypeHierarchy {
552 supertypes: Vec::new(),
553 subtypes: Vec::new(),
554 });
555 }
556
557 let is_hierarchy = |k: &EdgeKind| matches!(k, EdgeKind::Implements | EdgeKind::Inherits);
558 let mut super_ids: Vec<String> = Vec::new();
559 let mut sub_ids: Vec<String> = Vec::new();
560 for e in &edges {
561 if !is_hierarchy(&e.kind) {
562 continue;
563 }
564 if self_ids.contains(&e.src.as_str()) {
566 super_ids.push(e.dst.as_str());
567 }
568 if self_ids.contains(&e.dst.as_str()) {
570 sub_ids.push(e.src.as_str());
571 }
572 }
573
574 Ok(TypeHierarchy {
575 supertypes: self.get_nodes_by_ids(branch, &super_ids)?,
576 subtypes: self.get_nodes_by_ids(branch, &sub_ids)?,
577 })
578 }
579
580 fn trace_path(&self, branch: &str, from: &str, to: &str) -> Result<Vec<Node>>;
583
584 fn list_symbols_in_range(
586 &self,
587 branch: &str,
588 file: &Path,
589 start_line: u32,
590 end_line: u32,
591 ) -> Result<Vec<Node>>;
592
593 fn find_unused_symbols(&self, branch: &str, kind: Option<NodeKind>) -> Result<Vec<Node>>;
596
597 fn get_subgraph(
600 &self,
601 branch: &str,
602 seed_name: &str,
603 depth: u8,
604 direction: &str,
605 ) -> Result<SubGraph>;
606
607 fn get_subgraph_by_id(
614 &self,
615 branch: &str,
616 seed_id: &str,
617 depth: u8,
618 direction: &str,
619 ) -> Result<SubGraph> {
620 let nodes = self.list_all_nodes(branch)?;
621 let edges = self.list_all_edges(branch)?;
622 let by_id: std::collections::HashMap<String, Node> = nodes
623 .into_iter()
624 .map(|node| (node.id.as_str(), node))
625 .collect();
626 if !by_id.contains_key(seed_id) {
627 return Ok(SubGraph {
628 nodes: Vec::new(),
629 edges: Vec::new(),
630 });
631 }
632
633 let mut selected: std::collections::HashSet<String> =
634 [seed_id.to_owned()].into_iter().collect();
635 let mut frontier = vec![seed_id.to_owned()];
636 for _ in 0..depth.min(5) {
637 let mut next = Vec::new();
638 for edge in &edges {
639 let src = edge.src.as_str();
640 let dst = edge.dst.as_str();
641 let neighbour = if (direction == "out" || direction == "both")
642 && frontier.contains(&src)
643 {
644 Some(dst)
645 } else if (direction == "in" || direction == "both") && frontier.contains(&dst) {
646 Some(src)
647 } else {
648 None
649 };
650 if let Some(id) = neighbour {
651 if selected.insert(id.clone()) {
652 next.push(id);
653 }
654 }
655 }
656 if next.is_empty() {
657 break;
658 }
659 frontier = next;
660 }
661
662 let selected_edges = edges
663 .into_iter()
664 .filter(|edge| {
665 selected.contains(&edge.src.as_str()) && selected.contains(&edge.dst.as_str())
666 })
667 .collect();
668 let selected_nodes = selected
669 .into_iter()
670 .filter_map(|id| by_id.get(&id).cloned())
671 .collect();
672 Ok(SubGraph {
673 nodes: selected_nodes,
674 edges: selected_edges,
675 })
676 }
677
678 fn get_neighborhood_by_id(
682 &self,
683 branch: &str,
684 seed_id: &str,
685 direction: &str,
686 limit: usize,
687 ) -> Result<SubGraph> {
688 let all_nodes = self.list_all_nodes(branch)?;
689 let by_id: std::collections::HashMap<String, Node> = all_nodes
690 .into_iter()
691 .map(|node| (node.id.as_str(), node))
692 .collect();
693 if !by_id.contains_key(seed_id) {
694 return Ok(SubGraph {
695 nodes: Vec::new(),
696 edges: Vec::new(),
697 });
698 }
699 let mut edges = Vec::new();
700 for edge in self.list_all_edges(branch)? {
701 let incoming = edge.dst.as_str() == seed_id;
702 let outgoing = edge.src.as_str() == seed_id;
703 if ((direction == "in" && incoming)
704 || (direction == "out" && outgoing)
705 || (direction == "both" && (incoming || outgoing)))
706 && edges.len() < limit
707 {
708 edges.push(edge);
709 }
710 }
711 let mut ids = std::collections::HashSet::from([seed_id.to_owned()]);
712 for edge in &edges {
713 ids.insert(edge.src.as_str());
714 ids.insert(edge.dst.as_str());
715 }
716 let nodes = ids
717 .into_iter()
718 .filter_map(|id| by_id.get(&id).cloned())
719 .collect();
720 Ok(SubGraph { nodes, edges })
721 }
722
723 fn last_indexed_sha(&self, branch: &str) -> Result<Option<String>>;
728
729 fn set_last_indexed_sha(&mut self, branch: &str, sha: &str) -> Result<()>;
731}