1use regex::Regex;
11use serde::Serialize;
12use std::collections::{HashMap, HashSet};
13use std::path::{Path, PathBuf};
14
15use crate::graph::{ItemId, KnowledgeGraph};
16
17pub trait Query<R> {
21 fn run(&self, graph: &KnowledgeGraph) -> R;
22}
23
24pub struct TraitImplsQuery {
28 pub trait_name: String,
29}
30
31impl TraitImplsQuery {
32 #[must_use]
34 pub fn new(trait_name: &str) -> Self {
35 Self { trait_name: trait_name.to_string() }
36 }
37}
38
39impl Query<Vec<(PathBuf, String)>> for TraitImplsQuery {
41 fn run(&self, graph: &KnowledgeGraph) -> Vec<(PathBuf, String)> {
42 let mut out: Vec<(PathBuf, String)> = Vec::new();
43 for (path, file) in &graph.files {
44 for it in &file.items {
45 if let crate::graph::ItemType::Impl { trait_name: Some(tn), type_name } =
46 &it.item_type
47 {
48 if tn.as_ref() == self.trait_name {
49 out.push((path.clone(), type_name.to_string()));
50 }
51 }
52 }
53 }
54 out.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(&b.1)));
56 out
57 }
58}
59
60pub struct ConnectedFilesQuery {
63 pub file: PathBuf,
64}
65
66impl ConnectedFilesQuery {
67 pub fn new<P: AsRef<Path>>(file: P) -> Self {
69 Self { file: file.as_ref().to_path_buf() }
70 }
71}
72
73impl Query<Vec<PathBuf>> for ConnectedFilesQuery {
74 fn run(&self, graph: &KnowledgeGraph) -> Vec<PathBuf> {
75 let mut item_to_file: HashMap<ItemId, PathBuf> = HashMap::new();
77 let mut file_to_items: HashMap<PathBuf, Vec<ItemId>> = HashMap::new();
78 for (path, file) in &graph.files {
79 let ids: Vec<ItemId> = file.items.iter().map(|i| i.id.clone()).collect();
80 for id in &ids {
81 item_to_file.insert(id.clone(), path.clone());
82 }
83 file_to_items.insert(path.clone(), ids);
84 }
85
86 let Some((target_path, _node)) = graph.files.iter().find(|(p, _)| p == &&self.file) else {
88 return Vec::new();
89 };
90
91 let Some(target_items) = file_to_items.get(target_path) else {
92 return Vec::new();
93 };
94 let target_set: HashSet<ItemId> = target_items.iter().cloned().collect();
95
96 let mut out: HashSet<PathBuf> = HashSet::new();
97 for rel in &graph.relationships {
98 if target_set.contains(&rel.from_item) {
100 if let Some(fp) = item_to_file.get(&rel.to_item) {
101 if fp != target_path {
102 out.insert(fp.clone());
103 }
104 }
105 }
106 if target_set.contains(&rel.to_item) {
107 if let Some(fp) = item_to_file.get(&rel.from_item) {
108 if fp != target_path {
109 out.insert(fp.clone());
110 }
111 }
112 }
113 }
114
115 let mut v: Vec<PathBuf> = out.into_iter().collect();
116 v.sort();
117 v
118 }
119}
120
121pub enum UsageDirection {
123 Callers,
124 Callees,
125}
126
127pub struct FunctionUsageQuery {
129 pub function: String,
130 pub direction: UsageDirection,
131}
132
133impl FunctionUsageQuery {
134 #[must_use]
136 pub fn callers(function: &str) -> Self {
137 Self { function: function.to_string(), direction: UsageDirection::Callers }
138 }
139 #[must_use]
141 pub fn callees(function: &str) -> Self {
142 Self { function: function.to_string(), direction: UsageDirection::Callees }
143 }
144}
145
146impl Query<Vec<PathBuf>> for FunctionUsageQuery {
147 fn run(&self, graph: &KnowledgeGraph) -> Vec<PathBuf> {
148 let mut item_to_file: HashMap<ItemId, PathBuf> = HashMap::new();
150 let mut func_name_to_ids: HashMap<String, Vec<ItemId>> = HashMap::new();
151 for (path, file) in &graph.files {
152 for item in &file.items {
153 item_to_file.insert(item.id.clone(), path.clone());
154 if let crate::graph::ItemType::Function { .. } = item.item_type {
156 func_name_to_ids
157 .entry(item.name.to_string())
158 .or_default()
159 .push(item.id.clone());
160 }
161 }
162 }
163
164 let Some(target_ids) = func_name_to_ids.get(&self.function) else {
165 return Vec::new();
166 };
167 let target_set: HashSet<ItemId> = target_ids.iter().cloned().collect();
168
169 let mut out: HashSet<PathBuf> = HashSet::new();
170 for rel in &graph.relationships {
171 match self.direction {
172 UsageDirection::Callers => {
173 if target_set.contains(&rel.to_item) {
174 if let Some(fp) = item_to_file.get(&rel.from_item) {
175 out.insert(fp.clone());
176 }
177 }
178 }
179 UsageDirection::Callees => {
180 if target_set.contains(&rel.from_item) {
181 if let Some(fp) = item_to_file.get(&rel.to_item) {
182 out.insert(fp.clone());
183 }
184 }
185 }
186 }
187 }
188
189 let mut v: Vec<PathBuf> = out.into_iter().collect();
190 v.sort();
191 v
192 }
193}
194
195pub struct CycleDetectionQuery;
197
198impl CycleDetectionQuery {
199 #[must_use]
201 pub fn new() -> Self {
202 Self
203 }
204}
205
206impl Default for CycleDetectionQuery {
207 fn default() -> Self {
208 Self
209 }
210}
211
212fn dfs(
214 u: usize,
215 adj: &Vec<Vec<usize>>,
216 visited: &mut [bool],
217 stack: &mut [bool],
218 path: &mut Vec<usize>,
219 out: &mut Vec<Vec<PathBuf>>,
220 names: &Vec<PathBuf>,
221) {
222 visited[u] = true;
223 stack[u] = true;
224 path.push(u);
225 for &v in &adj[u] {
226 if !visited[v] {
227 dfs(v, adj, visited, stack, path, out, names);
228 } else if stack[v] {
229 if let Some(pos) = path.iter().position(|&x| x == v) {
231 let cyc: Vec<PathBuf> = path[pos..].iter().map(|&i| names[i].clone()).collect();
232 out.push(cyc);
233 }
234 }
235 }
236 path.pop();
237 stack[u] = false;
238}
239
240impl Query<Vec<Vec<PathBuf>>> for CycleDetectionQuery {
241 fn run(&self, graph: &KnowledgeGraph) -> Vec<Vec<PathBuf>> {
242 let mut file_ids: Vec<PathBuf> = graph.files.keys().cloned().collect();
244 file_ids.sort();
245 let index: HashMap<PathBuf, usize> =
246 file_ids.iter().cloned().enumerate().map(|(i, p)| (p, i)).collect();
247
248 let mut adj: Vec<Vec<usize>> = vec![Vec::new(); file_ids.len()];
250 let mut item_to_file: HashMap<ItemId, usize> = HashMap::new();
252 for (path, file) in &graph.files {
253 if let Some(&i) = index.get(path) {
254 for it in &file.items {
255 item_to_file.insert(it.id.clone(), i);
256 }
257 }
258 }
259 for rel in &graph.relationships {
260 if !matches!(rel.relationship_type, crate::graph::RelationshipType::Calls { .. }) {
262 continue;
263 }
264 if let (Some(&u), Some(&v)) =
265 (item_to_file.get(&rel.from_item), item_to_file.get(&rel.to_item))
266 {
267 if u != v {
268 adj[u].push(v);
269 }
270 }
271 }
272
273 for neigh in &mut adj {
275 neigh.sort_unstable();
276 neigh.dedup();
277 }
278
279 let mut visited = vec![false; file_ids.len()];
281 let mut stack = vec![false; file_ids.len()];
282 let mut path: Vec<usize> = Vec::new();
283 let mut cycles: Vec<Vec<PathBuf>> = Vec::new();
284
285 for u in 0..file_ids.len() {
286 if !visited[u] {
287 dfs(u, &adj, &mut visited, &mut stack, &mut path, &mut cycles, &file_ids);
288 }
289 }
290
291 cycles
292 }
293}
294
295pub struct ShortestPathQuery {
297 pub from: PathBuf,
298 pub to: PathBuf,
299}
300
301impl ShortestPathQuery {
302 #[must_use]
304 pub fn new<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Self {
305 Self { from: from.as_ref().to_path_buf(), to: to.as_ref().to_path_buf() }
306 }
307}
308
309impl Query<Vec<PathBuf>> for ShortestPathQuery {
310 fn run(&self, graph: &KnowledgeGraph) -> Vec<PathBuf> {
311 let mut files: Vec<PathBuf> = graph.files.keys().cloned().collect();
313 files.sort();
314 let idx: HashMap<PathBuf, usize> =
315 files.iter().cloned().enumerate().map(|(i, p)| (p, i)).collect();
316
317 let (Some(&src), Some(&dst)) = (idx.get(&self.from), idx.get(&self.to)) else {
318 return Vec::new();
319 };
320
321 let mut item_to_file: HashMap<ItemId, usize> = HashMap::new();
323 for (path, file) in &graph.files {
324 if let Some(&i) = idx.get(path) {
325 for it in &file.items {
326 item_to_file.insert(it.id.clone(), i);
327 }
328 }
329 }
330 let mut adj: Vec<Vec<usize>> = vec![Vec::new(); files.len()];
331 for rel in &graph.relationships {
332 if let (Some(&u), Some(&v)) =
333 (item_to_file.get(&rel.from_item), item_to_file.get(&rel.to_item))
334 {
335 if u != v {
336 adj[u].push(v);
337 }
338 }
339 }
340
341 let mut prev: Vec<Option<usize>> = vec![None; files.len()];
343 let mut q: std::collections::VecDeque<usize> = std::collections::VecDeque::new();
344 let mut visited = vec![false; files.len()];
345 visited[src] = true;
346 q.push_back(src);
347 while let Some(u) = q.pop_front() {
348 if u == dst {
349 break;
350 }
351 for &v in &adj[u] {
352 if !visited[v] {
353 visited[v] = true;
354 prev[v] = Some(u);
355 q.push_back(v);
356 }
357 }
358 }
359
360 if !visited[dst] {
361 return Vec::new();
362 }
363
364 let mut path_indices: Vec<usize> = Vec::new();
366 let mut cur = dst;
367 while let Some(p) = prev[cur] {
368 path_indices.push(cur);
369 cur = p;
370 }
371 path_indices.push(src);
372 path_indices.reverse();
373
374 path_indices.into_iter().map(|i| files[i].clone()).collect()
375 }
376}
377
378pub enum CentralityMetric {
380 In,
381 Out,
382 Total,
383}
384
385pub struct HubsQuery {
387 pub metric: CentralityMetric,
388 pub top: usize,
389}
390
391impl HubsQuery {
392 #[must_use]
394 pub fn new(metric: CentralityMetric, top: usize) -> Self {
395 Self { metric, top }
396 }
397}
398
399impl Query<Vec<(PathBuf, usize, usize)>> for HubsQuery {
400 fn run(&self, graph: &KnowledgeGraph) -> Vec<(PathBuf, usize, usize)> {
401 let mut files: Vec<PathBuf> = graph.files.keys().cloned().collect();
403 files.sort();
404 let idx: HashMap<PathBuf, usize> =
405 files.iter().cloned().enumerate().map(|(i, p)| (p, i)).collect();
406
407 let mut item_to_file: HashMap<ItemId, usize> = HashMap::new();
409 for (path, file) in &graph.files {
410 if let Some(&i) = idx.get(path) {
411 for it in &file.items {
412 item_to_file.insert(it.id.clone(), i);
413 }
414 }
415 }
416 let n = files.len();
417 let mut indeg = vec![0usize; n];
418 let mut outdeg = vec![0usize; n];
419
420 for rel in &graph.relationships {
422 if let (Some(&u), Some(&v)) =
423 (item_to_file.get(&rel.from_item), item_to_file.get(&rel.to_item))
424 {
425 if u != v {
426 outdeg[u] += 1;
427 indeg[v] += 1;
428 }
429 }
430 }
431
432 let mut rows: Vec<(PathBuf, usize, usize)> =
433 (0..n).map(|i| (files[i].clone(), indeg[i], outdeg[i])).collect();
434
435 rows.sort_by(|a, b| {
437 let (ai, ao) = (a.1, a.2);
438 let (bi, bo) = (b.1, b.2);
439 let ak = match self.metric {
440 CentralityMetric::In => ai,
441 CentralityMetric::Out => ao,
442 CentralityMetric::Total => ai + ao,
443 };
444 let bk = match self.metric {
445 CentralityMetric::In => bi,
446 CentralityMetric::Out => bo,
447 CentralityMetric::Total => bi + bo,
448 };
449 bk.cmp(&ak).then_with(|| a.0.cmp(&b.0))
450 });
451
452 rows.truncate(self.top);
453 rows
454 }
455}
456
457pub struct UnreferencedItemsQuery {
463 pub include_public: bool,
464 pub exclude: Option<Regex>,
465}
466
467impl UnreferencedItemsQuery {
468 #[must_use]
469 pub fn new(include_public: bool, exclude: Option<Regex>) -> Self {
470 Self { include_public, exclude }
471 }
472}
473
474impl Query<Vec<(PathBuf, String, String, String, String)>> for UnreferencedItemsQuery {
475 fn run(&self, graph: &KnowledgeGraph) -> Vec<(PathBuf, String, String, String, String)> {
476 use crate::graph::{ItemType, RelationshipType, Visibility};
477 let mut used: HashSet<ItemId> = HashSet::new();
478 for rel in &graph.relationships {
479 match rel.relationship_type {
480 RelationshipType::Uses { .. } | RelationshipType::Calls { .. } => {
481 used.insert(rel.to_item.clone());
482 }
483 _ => {}
484 }
485 }
486
487 let mut out: Vec<(PathBuf, String, String, String, String)> = Vec::new();
488 for (path, file) in &graph.files {
489 if let Some(re) = &self.exclude {
490 if re.is_match(&path.display().to_string()) {
491 continue;
492 }
493 }
494 for (idx, item) in file.items.iter().enumerate() {
495 if idx == 0 {
496 continue;
497 }
498 if !self.include_public {
499 if let Visibility::Public = item.visibility {
500 continue;
501 }
502 }
503 if used.contains(&item.id) {
504 continue;
505 }
506
507 let kind = match &item.item_type {
508 ItemType::Module { .. } => "Module",
509 ItemType::Function { .. } => "Function",
510 ItemType::Struct { .. } => "Struct",
511 ItemType::Enum { .. } => "Enum",
512 ItemType::Trait { .. } => "Trait",
513 ItemType::Impl { .. } => "Impl",
514 ItemType::Const => "Const",
515 ItemType::Static { .. } => "Static",
516 ItemType::Type => "Type",
517 ItemType::Macro => "Macro",
518 };
519 let vis = match item.visibility {
520 Visibility::Public => "public",
521 Visibility::Private => "private",
522 Visibility::PubCrate => "pub(crate)",
523 Visibility::PubSuper => "pub(super)",
524 Visibility::PubIn(_) => "pub(in)",
525 };
526 out.push((
527 path.clone(),
528 item.id.0.clone(),
529 item.name.to_string(),
530 kind.to_string(),
531 vis.to_string(),
532 ));
533 }
534 }
535 out
536 }
537}
538
539#[derive(Debug, Serialize)]
541pub struct ItemInfoRelationEntry {
542 pub id: String,
543 pub name: String,
544 pub path: String,
545 pub relation: String,
546 pub context: String,
547}
548
549#[derive(Debug, Serialize)]
550pub struct ItemInfoResult {
551 pub id: String,
552 pub name: String,
553 pub kind: String,
554 pub visibility: String,
555 pub path: String,
556 pub line_start: usize,
557 pub line_end: usize,
558 pub code: Option<String>,
559 pub inbound: Vec<ItemInfoRelationEntry>,
560 pub outbound: Vec<ItemInfoRelationEntry>,
561}
562
563pub struct ItemInfoQuery {
564 pub item_id: crate::graph::ItemId,
565 pub show_code: bool,
566}
567
568impl ItemInfoQuery {
569 #[must_use]
570 pub fn new(item_id: crate::graph::ItemId, show_code: bool) -> Self {
571 Self { item_id, show_code }
572 }
573}
574
575impl Query<Option<ItemInfoResult>> for ItemInfoQuery {
576 fn run(&self, graph: &KnowledgeGraph) -> Option<ItemInfoResult> {
577 use crate::graph::{ItemType, Visibility};
578 let mut idx: HashMap<&crate::graph::ItemId, (&PathBuf, &crate::graph::Item)> =
580 HashMap::new();
581 for (p, f) in &graph.files {
582 for it in &f.items {
583 idx.insert(&it.id, (p, it));
584 }
585 }
586 let (path, item) = idx.get(&self.item_id)?.to_owned();
587
588 let kind = match &item.item_type {
589 ItemType::Module { .. } => "Module",
590 ItemType::Function { .. } => "Function",
591 ItemType::Struct { .. } => "Struct",
592 ItemType::Enum { .. } => "Enum",
593 ItemType::Trait { .. } => "Trait",
594 ItemType::Impl { .. } => "Impl",
595 ItemType::Const => "Const",
596 ItemType::Static { .. } => "Static",
597 ItemType::Type => "Type",
598 ItemType::Macro => "Macro",
599 }
600 .to_string();
601 let visibility = match &item.visibility {
602 Visibility::Public => "public".to_string(),
603 Visibility::Private => "private".to_string(),
604 Visibility::PubCrate => "pub(crate)".to_string(),
605 Visibility::PubSuper => "pub(super)".to_string(),
606 Visibility::PubIn(p) => format!("pub(in {p})"),
607 };
608
609 let mut inbound: Vec<ItemInfoRelationEntry> = Vec::new();
611 let mut outbound: Vec<ItemInfoRelationEntry> = Vec::new();
612 let rel_to_string = |r: &crate::graph::RelationshipType| -> String {
613 match r {
614 crate::graph::RelationshipType::Uses { import_type } => {
615 format!("Uses:{import_type}")
616 }
617 crate::graph::RelationshipType::Implements { trait_name } => {
618 format!("Implements:{trait_name}")
619 }
620 crate::graph::RelationshipType::Contains { containment_type } => {
621 format!("Contains:{containment_type}")
622 }
623 crate::graph::RelationshipType::Extends { extension_type } => {
624 format!("Extends:{extension_type}")
625 }
626 crate::graph::RelationshipType::Calls { call_type } => format!("Calls:{call_type}"),
627 }
628 };
629 for r in &graph.relationships {
630 if r.to_item == item.id {
631 if let Some((pp, it)) = idx.get(&r.from_item) {
632 inbound.push(ItemInfoRelationEntry {
633 id: r.from_item.0.clone(),
634 name: it.name.to_string(),
635 path: pp.display().to_string(),
636 relation: rel_to_string(&r.relationship_type),
637 context: r.context.clone(),
638 });
639 }
640 }
641 if r.from_item == item.id {
642 if let Some((pp, it)) = idx.get(&r.to_item) {
643 outbound.push(ItemInfoRelationEntry {
644 id: r.to_item.0.clone(),
645 name: it.name.to_string(),
646 path: pp.display().to_string(),
647 relation: rel_to_string(&r.relationship_type),
648 context: r.context.clone(),
649 });
650 }
651 }
652 }
653
654 let mut code: Option<String> = None;
656 if self.show_code {
657 if let Ok(content) = std::fs::read_to_string(path) {
658 let lines: Vec<&str> = content.lines().collect();
659 let s = item.location.line_start.saturating_sub(1);
660 let e = item.location.line_end.min(lines.len());
661 if s < e {
662 code = Some(lines[s..e].join("\n"));
663 }
664 }
665 }
666
667 Some(ItemInfoResult {
668 id: item.id.0.clone(),
669 name: item.name.to_string(),
670 kind,
671 visibility,
672 path: path.display().to_string(),
673 line_start: item.location.line_start,
674 line_end: item.location.line_end,
675 code,
676 inbound,
677 outbound,
678 })
679 }
680}
681
682pub struct ModuleCentralityQuery {
684 pub metric: CentralityMetric,
685 pub top: usize,
686}
687
688impl ModuleCentralityQuery {
689 #[must_use]
691 pub fn new(metric: CentralityMetric, top: usize) -> Self {
692 Self { metric, top }
693 }
694}
695
696impl Query<Vec<(PathBuf, usize, usize)>> for ModuleCentralityQuery {
697 fn run(&self, graph: &KnowledgeGraph) -> Vec<(PathBuf, usize, usize)> {
698 let mut modules: HashSet<PathBuf> = HashSet::new();
700 let mut file_to_module: HashMap<PathBuf, PathBuf> = HashMap::new();
701 for p in graph.files.keys() {
702 let m = p.parent().map_or_else(|| PathBuf::from("."), Path::to_path_buf);
703 modules.insert(m.clone());
704 file_to_module.insert(p.clone(), m);
705 }
706
707 let mut mods: Vec<PathBuf> = modules.into_iter().collect();
708 mods.sort();
709 let midx: HashMap<PathBuf, usize> =
710 mods.iter().cloned().enumerate().map(|(i, p)| (p, i)).collect();
711
712 let mut item_to_mod: HashMap<ItemId, usize> = HashMap::new();
714 for (path, file) in &graph.files {
715 let Some(module_path) = file_to_module.get(path) else { continue };
716 if let Some(&mi) = midx.get(module_path) {
717 for it in &file.items {
718 item_to_mod.insert(it.id.clone(), mi);
719 }
720 }
721 }
722
723 let n = mods.len();
724 let mut indeg = vec![0usize; n];
725 let mut outdeg = vec![0usize; n];
726
727 for rel in &graph.relationships {
729 if let (Some(&u), Some(&v)) =
730 (item_to_mod.get(&rel.from_item), item_to_mod.get(&rel.to_item))
731 {
732 if u != v {
733 outdeg[u] += 1;
734 indeg[v] += 1;
735 }
736 }
737 }
738
739 let mut rows: Vec<(PathBuf, usize, usize)> =
740 (0..n).map(|i| (mods[i].clone(), indeg[i], outdeg[i])).collect();
741
742 rows.sort_by(|a, b| {
743 let (ai, ao) = (a.1, a.2);
744 let (bi, bo) = (b.1, b.2);
745 let ak = match self.metric {
746 CentralityMetric::In => ai,
747 CentralityMetric::Out => ao,
748 CentralityMetric::Total => ai + ao,
749 };
750 let bk = match self.metric {
751 CentralityMetric::In => bi,
752 CentralityMetric::Out => bo,
753 CentralityMetric::Total => bi + bo,
754 };
755 bk.cmp(&ak).then_with(|| a.0.cmp(&b.0))
756 });
757
758 rows.truncate(self.top);
759 rows
760 }
761}
762
763#[cfg(test)]
764mod tests {
765 use super::*;
766 use crate::graph::{FileNode, Item, ItemType, Relationship, RelationshipType};
767 use std::sync::Arc;
768
769 fn make_fn(path: &Path, id_prefix: &str, name: &str) -> Item {
770 Item {
771 id: ItemId(format!("fn:{}:{}", name, id_prefix)),
772 item_type: ItemType::Function { is_async: false, is_const: false },
773 name: Arc::from(name),
774 visibility: crate::graph::Visibility::Public,
775 location: crate::graph::Location {
776 file: path.to_path_buf(),
777 line_start: 1,
778 line_end: 1,
779 },
780 attributes: vec![],
781 }
782 }
783
784 fn graph_fixture(with_cycle: bool) -> KnowledgeGraph {
789 let mut g = KnowledgeGraph::default();
790 let a_path = PathBuf::from("src/a.rs");
791 let b_path = PathBuf::from("src/b.rs");
792 let c_path = PathBuf::from("src/c.rs");
793
794 let a_item = make_fn(&a_path, "1", "fa");
795 let b_item = make_fn(&b_path, "2", "fb");
796 let c_item = make_fn(&c_path, "3", "fc");
797
798 g.files.insert(
799 a_path.clone(),
800 FileNode {
801 path: a_path.clone(),
802 items: vec![a_item.clone()],
803 imports: vec![],
804 metrics: Default::default(),
805 },
806 );
807 g.files.insert(
808 b_path.clone(),
809 FileNode {
810 path: b_path.clone(),
811 items: vec![b_item.clone()],
812 imports: vec![],
813 metrics: Default::default(),
814 },
815 );
816 g.files.insert(
817 c_path.clone(),
818 FileNode {
819 path: c_path.clone(),
820 items: vec![c_item.clone()],
821 imports: vec![],
822 metrics: Default::default(),
823 },
824 );
825
826 g.relationships.push(Relationship {
827 from_item: a_item.id.clone(),
828 to_item: b_item.id.clone(),
829 relationship_type: RelationshipType::Calls { call_type: "test".to_string() },
830 strength: 1.0,
831 context: String::new(),
832 });
833 g.relationships.push(Relationship {
834 from_item: b_item.id.clone(),
835 to_item: c_item.id.clone(),
836 relationship_type: RelationshipType::Calls { call_type: "test".to_string() },
837 strength: 1.0,
838 context: String::new(),
839 });
840 if with_cycle {
841 g.relationships.push(Relationship {
842 from_item: c_item.id.clone(),
843 to_item: a_item.id.clone(),
844 relationship_type: RelationshipType::Calls { call_type: "test".to_string() },
845 strength: 1.0,
846 context: String::new(),
847 });
848 }
849 g
850 }
851
852 #[test]
853 fn connected_files_query_basic() {
854 let g = graph_fixture(false);
855 let q = ConnectedFilesQuery::new("src/a.rs");
856 let res = q.run(&g);
857 assert!(res.contains(&PathBuf::from("src/b.rs")));
859 assert!(!res.contains(&PathBuf::from("src/c.rs")));
861 }
862
863 #[test]
864 fn function_usage_callers_and_callees() {
865 let g = graph_fixture(false);
866 let q_callees = FunctionUsageQuery::callees("fa");
868 let callees = q_callees.run(&g);
869 assert!(callees.contains(&PathBuf::from("src/b.rs")));
870
871 let q_callers = FunctionUsageQuery::callers("fb");
873 let callers = q_callers.run(&g);
874 assert!(callers.contains(&PathBuf::from("src/a.rs")));
875 }
876
877 #[test]
878 fn cycle_detection_detects_simple_cycle() {
879 let g = graph_fixture(true);
880 let q = CycleDetectionQuery::new();
881 let cycles = q.run(&g);
882 assert!(cycles.iter().any(|cyc| {
884 let names: std::collections::HashSet<_> = cyc.iter().cloned().collect();
886 names.contains(&PathBuf::from("src/a.rs"))
887 && names.contains(&PathBuf::from("src/b.rs"))
888 && names.contains(&PathBuf::from("src/c.rs"))
889 }));
890 }
891
892 #[test]
893 fn trait_impls_basic() {
894 let mut g = KnowledgeGraph::default();
895 let p = PathBuf::from("src/x.rs");
896 let impl_item = Item {
897 id: ItemId("impl:X:Display".to_string()),
898 item_type: ItemType::Impl {
899 trait_name: Some(Arc::from("Display")),
900 type_name: Arc::from("X"),
901 },
902 name: Arc::from("impl Display for X"),
903 visibility: crate::graph::Visibility::PubCrate,
904 location: crate::graph::Location { file: p.clone(), line_start: 1, line_end: 1 },
905 attributes: vec![],
906 };
907 g.files.insert(
908 p.clone(),
909 FileNode {
910 path: p.clone(),
911 items: vec![impl_item],
912 imports: vec![],
913 metrics: Default::default(),
914 },
915 );
916
917 let q = TraitImplsQuery::new("Display");
918 let rows = q.run(&g);
919 assert_eq!(rows.len(), 1);
920 assert_eq!(rows[0].0, p);
921 assert_eq!(rows[0].1, "X");
922 }
923}