1use crate::utils::cache;
10use rayon::prelude::*;
11use regex::Regex;
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use std::path::PathBuf;
15use std::sync::{Arc, Mutex};
16
17pub mod resolver;
18
19type Segments = Vec<Arc<str>>;
21type ImportSegments = Vec<(Segments, Option<Arc<str>>)>;
22type ParsedEntry = (PathBuf, FileNode, Vec<Relationship>, cache::CacheEntry);
23
24#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash)]
25pub struct ItemId(pub String);
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct Location {
29 pub file: PathBuf,
30 pub line_start: usize,
31 pub line_end: usize,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub enum ItemType {
36 Module { is_inline: bool },
37 Function { is_async: bool, is_const: bool },
38 Struct { is_tuple: bool },
39 Enum { variant_count: usize },
40 Trait { is_object_safe: bool },
41 Impl { trait_name: Option<Arc<str>>, type_name: Arc<str> },
42 Const,
43 Static { is_mut: bool },
44 Type,
45 Macro,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub enum Visibility {
50 Public,
51 Private,
52 PubCrate,
53 PubSuper,
54 PubIn(Arc<str>),
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct Item {
59 pub id: ItemId,
60 pub item_type: ItemType,
61 pub name: Arc<str>,
62 pub visibility: Visibility,
63 pub location: Location,
64 pub attributes: Vec<String>,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct Import {
69 pub path: Arc<str>,
70 pub alias: Option<Arc<str>>,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub enum RelationshipType {
75 Uses { import_type: String },
76 Implements { trait_name: String },
77 Contains { containment_type: String },
78 Extends { extension_type: String },
79 Calls { call_type: String },
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct Relationship {
84 pub from_item: ItemId,
85 pub to_item: ItemId,
86 pub relationship_type: RelationshipType,
87 pub strength: f64,
88 pub context: String,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize, Default)]
92pub struct FileMetrics {
93 pub item_count: usize,
94 pub import_count: usize,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize, Default)]
98pub struct FileNode {
99 pub path: PathBuf,
100 pub items: Vec<Item>,
101 pub imports: Vec<Import>,
102 pub metrics: FileMetrics,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize, Default)]
106pub struct GraphMetadata {
107 pub generated_at: String,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize, Default)]
111pub struct KnowledgeGraph {
112 pub files: HashMap<PathBuf, FileNode>,
113 pub relationships: Vec<Relationship>,
114 pub metadata: GraphMetadata,
115 pub module_parent: HashMap<PathBuf, PathBuf>,
117 pub module_children: HashMap<PathBuf, Vec<PathBuf>>,
118 pub module_segments: HashMap<PathBuf, Vec<String>>,
120 #[serde(skip, default)]
122 pub import_segments: HashMap<PathBuf, ImportSegments>,
123 #[serde(skip, default)]
125 pub string_pool: std::sync::Arc<Mutex<HashMap<String, Arc<str>>>>,
126}
127
128impl KnowledgeGraph {
129 #[allow(clippy::too_many_lines)]
142 pub fn build_from_directory_with_cache_opts(
143 path: &std::path::Path,
144 mode: cache::CacheMode,
145 no_ignore: bool,
146 ) -> Result<Self, crate::errors::KnowledgeGraphError> {
147 use crate::errors::KnowledgeGraphError;
148 use crate::parser::RustParser;
149 use crate::utils::file_walker;
150 use std::fs;
151
152 let files =
153 file_walker::rust_files_with_options(path.to_string_lossy().as_ref(), no_ignore);
154
155 let root_dir = path.to_path_buf();
157 let mut cache_state = match mode {
158 cache::CacheMode::Use => cache::load_cache(&root_dir).unwrap_or_default(),
159 cache::CacheMode::Ignore | cache::CacheMode::Rebuild => cache::Cache::default(),
160 };
161
162 let infos: Vec<(String, cache::CacheEntryMeta)> = files
164 .iter()
165 .map(|f| {
166 let p = std::path::Path::new(f);
167 let meta = fs::metadata(p).ok();
168 let len = meta.as_ref().map_or(0u64, std::fs::Metadata::len);
169 let mtime = meta
170 .and_then(|m| m.modified().ok())
171 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
172 .map_or(0u64, |d| d.as_secs());
173 (f.clone(), cache::CacheEntryMeta { mtime, len })
174 })
175 .collect();
176
177 if matches!(mode, cache::CacheMode::Use) {
179 use std::collections::HashSet;
180 let present: HashSet<PathBuf> =
181 files.iter().map(|f| std::path::Path::new(f).to_path_buf()).collect();
182 cache_state.entries.retain(|k, _| present.contains(k));
183 }
184
185 let mut reused: Vec<(PathBuf, FileNode, Vec<Relationship>)> = Vec::new();
187 let mut to_parse: Vec<(String, cache::CacheEntryMeta)> = Vec::new();
188 for (file, meta) in &infos {
189 let key = std::path::Path::new(file).to_path_buf();
190 if matches!(mode, cache::CacheMode::Use) {
191 if let Some(entry) = cache_state.entries.get(&key) {
192 if entry.meta == *meta {
193 let node = entry.node.clone();
194 let file_id = ItemId(format!("file:{}", node.path.display()));
196 let mut edges = Vec::new();
197 for it in node.items.iter().skip(1) {
198 edges.push(Relationship {
199 from_item: file_id.clone(),
200 to_item: it.id.clone(),
201 relationship_type: RelationshipType::Contains {
202 containment_type: "file_contains".to_string(),
203 },
204 strength: 1.0,
205 context: "auto".to_string(),
206 });
207 }
208 reused.push((node.path.clone(), node, edges));
209 continue;
210 }
211 }
212 }
213 to_parse.push((file.clone(), meta.clone()));
214 }
215
216 let parsed: Result<Vec<ParsedEntry>, KnowledgeGraphError> = to_parse
218 .into_par_iter()
219 .map(|(file, meta)| {
220 let p = std::path::Path::new(&file);
221 let content = fs::read_to_string(p)?;
222 let p = std::path::Path::new(&file).to_path_buf();
223 let mut node = RustParser::new().parse_file(&content, &p).map_err(|source| {
224 KnowledgeGraphError::ParseError { file: p.clone(), source }
225 })?;
226
227 let file_id = ItemId(format!("file:{}", node.path.display()));
229 let file_item = Item {
230 id: file_id.clone(),
231 item_type: ItemType::Module { is_inline: false },
232 name: Arc::from(
233 node.path.file_stem().and_then(|s| s.to_str()).unwrap_or("(file)"),
234 ),
235 visibility: Visibility::PubCrate,
236 location: Location { file: node.path.clone(), line_start: 1, line_end: 1 },
237 attributes: vec![],
238 };
239
240 let mut items_with_file = Vec::with_capacity(node.items.len() + 1);
242 items_with_file.push(file_item);
243 items_with_file.extend(node.items);
244 node.metrics.item_count = items_with_file.len();
245 node.items = items_with_file;
246
247 let mut contains_edges: Vec<Relationship> = Vec::new();
249 for it in node.items.iter().skip(1) {
250 contains_edges.push(Relationship {
251 from_item: file_id.clone(),
252 to_item: it.id.clone(),
253 relationship_type: RelationshipType::Contains {
254 containment_type: "file_contains".to_string(),
255 },
256 strength: 1.0,
257 context: "auto".to_string(),
258 });
259 }
260
261 let cache_entry = cache::CacheEntry { meta, node: node.clone() };
262 Ok::<_, KnowledgeGraphError>((node.path.clone(), node, contains_edges, cache_entry))
263 })
264 .collect();
265
266 let mut graph = KnowledgeGraph::default();
267 for (path, node, edges) in reused {
269 graph.files.insert(path, node);
270 graph.relationships.extend(edges);
271 }
272 for (path, node, edges, cache_entry) in parsed? {
274 graph.files.insert(path, node);
275 graph.relationships.extend(edges);
276 cache_state.entries.insert(cache_entry.node.path.clone(), cache_entry);
277 }
278
279 graph.module_segments = {
282 let mut map: HashMap<PathBuf, Vec<String>> = HashMap::with_capacity(graph.files.len());
283 for p in graph.files.keys() {
284 let comps: Vec<_> = p.components().collect();
286 let mut src_idx: Option<usize> = None;
287 for (i, c) in comps.iter().enumerate() {
288 if let std::path::Component::Normal(os) = c {
289 if os.to_str() == Some("src") {
290 src_idx = Some(i);
291 break;
292 }
293 }
294 }
295 let mut segs: Vec<String> = Vec::new();
296 if let Some(i) = src_idx {
297 for c in &comps[i + 1..comps.len().saturating_sub(1)] {
299 if let std::path::Component::Normal(os) = c {
300 if let Some(s) = os.to_str() {
301 segs.push(s.to_string());
302 }
303 }
304 }
305 if let Some(file_os) = p.file_name() {
307 let file = file_os.to_string_lossy();
308 if file != "mod.rs" && file != "lib.rs" {
309 if let Some(stem) = p.file_stem().and_then(|s| s.to_str()) {
310 segs.push(stem.to_string());
311 }
312 }
313 }
314 }
315 map.insert(p.clone(), segs);
316 }
317 map
318 };
319
320 graph.import_segments = {
322 let mut pool: HashMap<String, Arc<str>> = HashMap::new();
323 let mut intern = |s: &str| -> Arc<str> {
324 if let Some(a) = pool.get(s) {
325 return a.clone();
326 }
327 let a: Arc<str> = Arc::from(s);
328 pool.insert(s.to_string(), a.clone());
329 a
330 };
331 let mut map: HashMap<PathBuf, ImportSegments> =
332 HashMap::with_capacity(graph.files.len());
333 for (p, f) in &graph.files {
334 if f.imports.is_empty() {
335 continue;
336 }
337 let mut vecs: ImportSegments = Vec::with_capacity(f.imports.len());
338 for imp in &f.imports {
339 let parts: Vec<Arc<str>> =
340 imp.path.split("::").filter(|s| !s.is_empty()).map(&mut intern).collect();
341 let alias_arc: Option<Arc<str>> = imp
342 .alias
343 .as_deref()
344 .filter(|a| !a.is_empty() && *a != "_")
345 .map(&mut intern);
346 vecs.push((parts, alias_arc));
347 }
348 map.insert(p.clone(), vecs);
349 }
350 map
351 };
352
353 graph.metadata.generated_at =
355 match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
356 Ok(d) => format!("{}", d.as_secs()),
357 Err(_) => "0".to_string(),
358 };
359 graph.analyze_relationships();
361
362 cache::save_cache(&root_dir, &cache_state);
364 Ok(graph)
365 }
366
367 #[must_use]
369 pub fn get_module_parent(&self, file: &PathBuf) -> Option<&PathBuf> {
370 self.module_parent.get(file)
371 }
372
373 #[must_use]
374 pub fn get_module_children(&self, file: &PathBuf) -> &[PathBuf] {
375 match self.module_children.get(file) {
376 Some(v) => v.as_slice(),
377 None => &[],
378 }
379 }
380 pub fn build_from_directory_with_cache(
385 path: &std::path::Path,
386 mode: cache::CacheMode,
387 ) -> Result<Self, crate::errors::KnowledgeGraphError> {
388 let no_ignore = std::env::var("KNOWLEDGE_RS_NO_IGNORE")
389 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
390 .unwrap_or(false);
391 Self::build_from_directory_with_cache_opts(path, mode, no_ignore)
392 }
393
394 pub fn build_from_directory(
399 path: &std::path::Path,
400 ) -> Result<Self, crate::errors::KnowledgeGraphError> {
401 let no_ignore = std::env::var("KNOWLEDGE_RS_NO_IGNORE")
402 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
403 .unwrap_or(false);
404 Self::build_from_directory_with_cache_opts(path, cache::CacheMode::Use, no_ignore)
405 }
406
407 pub fn build_from_directory_opts(
412 path: &std::path::Path,
413 no_ignore: bool,
414 ) -> Result<Self, crate::errors::KnowledgeGraphError> {
415 Self::build_from_directory_with_cache_opts(path, cache::CacheMode::Use, no_ignore)
416 }
417
418 pub fn save_json(
423 &self,
424 path: &std::path::Path,
425 ) -> Result<(), crate::errors::KnowledgeGraphError> {
426 let data = serde_json::to_string_pretty(self).map_err(|e| {
427 crate::errors::KnowledgeGraphError::Io(std::io::Error::other(e.to_string()))
428 })?;
429 std::fs::write(path, data)?;
430 Ok(())
431 }
432
433 pub fn load_json(path: &std::path::Path) -> Result<Self, crate::errors::KnowledgeGraphError> {
438 let data = std::fs::read_to_string(path)?;
439 let graph: KnowledgeGraph = serde_json::from_str(&data).map_err(|e| {
440 crate::errors::KnowledgeGraphError::Io(std::io::Error::other(e.to_string()))
441 })?;
442 Ok(graph)
443 }
444}
445
446impl KnowledgeGraph {
447 fn analyze_relationships(&mut self) {
448 self.analyze_module_hierarchy();
449 self.analyze_import_uses();
450 self.analyze_calls_heuristic();
451 }
452
453 fn analyze_module_hierarchy(&mut self) {
456 self.module_parent.clear();
458 self.module_children.clear();
459 let mut file_level_id: HashMap<PathBuf, ItemId> = HashMap::with_capacity(self.files.len());
461 for (p, f) in &self.files {
462 if let Some(it) = f.items.first() {
463 file_level_id.insert(p.clone(), it.id.clone());
464 }
465 }
466 let mut id_to_path: HashMap<ItemId, PathBuf> = HashMap::with_capacity(file_level_id.len());
468 for (p, id) in &file_level_id {
469 id_to_path.insert(id.clone(), p.clone());
470 }
471
472 let all_paths: Vec<PathBuf> = self.files.keys().cloned().collect();
474 for path in all_paths {
475 let Some(child_id) = file_level_id.get(&path) else {
477 continue;
478 };
479
480 let parent_dir = match path.parent() {
482 Some(d) => d.to_path_buf(),
483 None => continue,
484 };
485 let file_name = path.file_name().and_then(|s| s.to_str()).unwrap_or("");
486
487 let mut parent_id_opt: Option<ItemId> = None;
489 if file_name == "mod.rs" || file_name == "lib.rs" {
490 if let Some(g) = parent_dir.parent() {
491 let p1 = g.join("mod.rs");
492 if let Some(pid) = file_level_id.get(&p1) {
493 parent_id_opt = Some(pid.clone());
494 }
495 if parent_id_opt.is_none() {
496 let p2 = g.join("lib.rs");
497 if let Some(pid) = file_level_id.get(&p2) {
498 parent_id_opt = Some(pid.clone());
499 }
500 }
501 }
502 } else {
503 let p1 = parent_dir.join("mod.rs");
504 if let Some(pid) = file_level_id.get(&p1) {
505 parent_id_opt = Some(pid.clone());
506 }
507 if parent_id_opt.is_none() {
508 let p2 = parent_dir.join("lib.rs");
509 if let Some(pid) = file_level_id.get(&p2) {
510 parent_id_opt = Some(pid.clone());
511 }
512 }
513 }
514
515 if let Some(parent_id) = parent_id_opt {
516 if parent_id != *child_id {
517 self.relationships.push(Relationship {
519 from_item: parent_id.clone(),
520 to_item: child_id.clone(),
521 relationship_type: RelationshipType::Contains {
522 containment_type: "module_contains".to_string(),
523 },
524 strength: 1.0,
525 context: "fs".to_string(),
526 });
527 if let Some(pp) = id_to_path.get(&parent_id).cloned() {
529 self.module_parent.insert(path.clone(), pp.clone());
530 self.module_children.entry(pp).or_default().push(path.clone());
531 }
532 }
533 }
534 }
535 }
536
537 fn analyze_import_uses(&mut self) {
538 let res = resolver::Resolver::new(self);
540 let produced: Vec<Relationship> = self
541 .files
542 .par_iter()
543 .map(|(path, file)| {
544 let mut edges: Vec<Relationship> = Vec::with_capacity(file.imports.len());
545 if file.items.is_empty() {
546 return edges;
547 }
548 let file_id = file.items[0].id.clone();
549 for imp in &file.imports {
550 let targets = res.resolve_import(path, &imp.path);
551 if targets.is_empty() {
552 continue;
553 }
554 for to in targets {
555 if to == file_id {
556 continue;
557 }
558 let import_type = if res.is_file_level_module(&to) {
559 "import-module"
560 } else {
561 "import-item"
562 };
563 edges.push(Relationship {
564 from_item: file_id.clone(),
565 to_item: to,
566 relationship_type: RelationshipType::Uses {
567 import_type: import_type.to_string(),
568 },
569 strength: if import_type == "import-item" { 1.0 } else { 0.8 },
570 context: imp.path.to_string(),
571 });
572 }
573 }
574 edges
575 })
576 .reduce(Vec::new, |mut a, mut b| {
577 a.append(&mut b);
578 a
579 });
580 self.relationships.extend(produced);
581 }
582
583 fn analyze_calls_heuristic(&mut self) {
584 let path_call_re =
586 Regex::new(r"\b([A-Za-z_][A-Za-z0-9_]*(?:::[A-Za-z_][A-Za-z0-9_]*)+)\s*\(").unwrap();
587 let simple_call_re = Regex::new(r"\b([A-Za-z_][A-Za-z0-9_]*)\s*\(").unwrap();
589
590 let mut func_count = 0usize;
593 for file in self.files.values() {
594 for item in &file.items {
595 if matches!(item.item_type, ItemType::Function { .. }) {
596 func_count += 1;
597 }
598 }
599 }
600 let mut func_index: HashMap<String, Vec<ItemId>> = HashMap::with_capacity(func_count);
601 for file in self.files.values() {
602 for item in &file.items {
603 if let ItemType::Function { .. } = item.item_type {
604 func_index.entry(item.name.to_string()).or_default().push(item.id.clone());
605 }
606 }
607 }
608
609 let res = resolver::Resolver::new(self);
611 let produced: Vec<Relationship> = self
612 .files
613 .par_iter()
614 .map(|(path, file)| {
615 let mut seen_local: std::collections::HashSet<(String, String)> =
616 std::collections::HashSet::new();
617 let mut edges: Vec<Relationship> = Vec::with_capacity(16);
618 if file.items.is_empty() {
619 return edges;
620 }
621 let file_id = file.items[0].id.clone();
622 if let Ok(content) = std::fs::read_to_string(path) {
623 for cap in path_call_re.captures_iter(&content) {
625 let full = cap.get(1).map_or("", |m| m.as_str());
626 let mut targets = res.resolve_import(path, full);
627 if targets.is_empty() {
628 if let Some(last) = full.rsplit("::").next() {
629 if let Some(funcs) = func_index.get(last) {
630 targets.clone_from(funcs);
631 }
632 }
633 }
634 for to in targets {
635 let key = (file_id.0.clone(), to.0.clone());
636 if seen_local.insert(key) {
637 edges.push(Relationship {
638 from_item: file_id.clone(),
639 to_item: to.clone(),
640 relationship_type: RelationshipType::Calls {
641 call_type: "path".to_string(),
642 },
643 strength: 0.7,
644 context: full.to_string(),
645 });
646 }
647 }
648 }
649
650 for cap in simple_call_re.captures_iter(&content) {
652 let Some(m) = cap.get(0) else { continue };
653 let name = cap.get(1).map_or("", |m| m.as_str());
654 let start = m.start();
655 let prefix = &content[start.saturating_sub(8)..start];
656 if prefix.contains("fn ")
657 || prefix.contains("struct ")
658 || prefix.contains("enum ")
659 || prefix.contains("trait ")
660 {
661 continue;
662 }
663 if start > 0 {
664 let prev = content[..start].chars().rev().find(|c| !c.is_whitespace());
665 if let Some('!') = prev {
666 continue;
667 }
668 }
669 if let Some(targets) = func_index.get(name) {
670 for to in targets {
671 let key = (file_id.0.clone(), to.0.clone());
672 if seen_local.insert(key) {
673 edges.push(Relationship {
674 from_item: file_id.clone(),
675 to_item: to.clone(),
676 relationship_type: RelationshipType::Calls {
677 call_type: "heuristic".to_string(),
678 },
679 strength: 0.5,
680 context: name.to_string(),
681 });
682 }
683 }
684 }
685 }
686 }
687 edges
688 })
689 .reduce(Vec::new, |mut a, mut b| {
690 a.append(&mut b);
691 a
692 });
693 self.relationships.extend(produced);
694 }
695}
696
697#[cfg(test)]
698mod tests {
699 use super::*;
700 use std::fs;
701 use tempfile::tempdir;
702
703 #[test]
704 fn module_hierarchy_basic() {
705 let mut g = KnowledgeGraph::default();
707 let lib = PathBuf::from("src/lib.rs");
708 let a_mod = PathBuf::from("src/a/mod.rs");
709 let a_foo = PathBuf::from("src/a/foo.rs");
710
711 let make_file_item = |p: &PathBuf| Item {
712 id: ItemId(format!("file:{}", p.display())),
713 item_type: ItemType::Module { is_inline: false },
714 name: Arc::from(p.file_stem().and_then(|s| s.to_str()).unwrap_or("(file)")),
715 visibility: Visibility::PubCrate,
716 location: Location { file: p.clone(), line_start: 1, line_end: 1 },
717 attributes: vec![],
718 };
719
720 g.files.insert(
721 lib.clone(),
722 FileNode {
723 path: lib.clone(),
724 items: vec![make_file_item(&lib)],
725 imports: vec![],
726 metrics: Default::default(),
727 },
728 );
729 g.files.insert(
730 a_mod.clone(),
731 FileNode {
732 path: a_mod.clone(),
733 items: vec![make_file_item(&a_mod)],
734 imports: vec![],
735 metrics: Default::default(),
736 },
737 );
738 g.files.insert(
739 a_foo.clone(),
740 FileNode {
741 path: a_foo.clone(),
742 items: vec![make_file_item(&a_foo)],
743 imports: vec![],
744 metrics: Default::default(),
745 },
746 );
747
748 g.analyze_module_hierarchy();
750
751 assert_eq!(g.get_module_parent(&a_mod), Some(&lib));
753 assert_eq!(g.get_module_parent(&a_foo), Some(&a_mod));
754 assert!(g.get_module_parent(&lib).is_none());
755
756 let lib_children = g.get_module_children(&lib);
758 assert!(lib_children.contains(&a_mod));
759 let a_mod_children = g.get_module_children(&a_mod);
760 assert!(a_mod_children.contains(&a_foo));
761 }
762
763 #[test]
764 fn import_uses_edges_item_vs_module() {
765 let td = tempdir().unwrap();
767 let f1 = td.path().join("a.rs");
768 let f2 = td.path().join("modx.rs");
769 let f3 = td.path().join("b.rs");
770 fs::write(&f1, "// a.rs\n").unwrap();
772 fs::write(&f2, "// modx.rs\n").unwrap();
773 fs::write(&f3, "// b.rs\n").unwrap();
774
775 let mk_file_item = |p: &PathBuf| Item {
777 id: ItemId(format!("file:{}", p.display())),
778 item_type: ItemType::Module { is_inline: false },
779 name: Arc::from(p.file_stem().and_then(|s| s.to_str()).unwrap_or("(file)")),
780 visibility: Visibility::PubCrate,
781 location: Location { file: p.clone(), line_start: 1, line_end: 1 },
782 attributes: vec![],
783 };
784 let mk_fn_item = |p: &PathBuf, name: &str| Item {
785 id: ItemId(format!("fn:{}:1", name)),
786 item_type: ItemType::Function { is_async: false, is_const: false },
787 name: Arc::from(name),
788 visibility: Visibility::Public,
789 location: Location { file: p.clone(), line_start: 1, line_end: 1 },
790 attributes: vec![],
791 };
792
793 let mut g = KnowledgeGraph::default();
794
795 let a_node = FileNode {
797 path: f1.clone(),
798 items: vec![mk_file_item(&f1)],
799 imports: vec![
800 Import { path: "foo".into(), alias: None },
801 Import { path: "modx".into(), alias: None },
802 ],
803 ..Default::default()
804 };
805
806 let modx_node =
808 FileNode { path: f2.clone(), items: vec![mk_file_item(&f2)], ..Default::default() };
809
810 let b_node = FileNode {
812 path: f3.clone(),
813 items: vec![mk_file_item(&f3), mk_fn_item(&f3, "foo")],
814 ..Default::default()
815 };
816
817 g.files.insert(f1.clone(), a_node);
818 g.files.insert(f2.clone(), modx_node);
819 g.files.insert(f3.clone(), b_node);
820
821 g.analyze_import_uses();
823
824 let a_file_id = ItemId(format!("file:{}", f1.display()));
826 let mut saw_item = false;
828 let mut saw_module = false;
829 for r in &g.relationships {
830 if r.from_item == a_file_id {
831 if let RelationshipType::Uses { import_type } = &r.relationship_type {
832 if import_type == "import-item" {
833 saw_item = true;
834 }
835 if import_type == "import-module" {
836 saw_module = true;
837 }
838 }
839 }
840 }
841 assert!(saw_item, "expected import-item edge");
842 assert!(saw_module, "expected import-module edge");
843 }
844
845 #[test]
846 fn calls_heuristic_and_path_and_macro_exclusion() {
847 let td = tempdir().unwrap();
849 let caller = td.path().join("caller.rs");
850 let callee_foo = td.path().join("callee.rs");
851 let dir_a = td.path().join("a");
852 let dir_b = dir_a.join("b");
853 fs::create_dir_all(&dir_b).unwrap();
854 let baz = dir_b.join("baz.rs");
855
856 fs::write(&caller, "fn main(){ foo(); a::b::baz(); my_macro!(x); }\n").unwrap();
858 fs::write(&callee_foo, "pub fn foo(){}\n").unwrap();
859 fs::write(&baz, "pub fn baz(){}\n").unwrap();
860
861 let mk_file_item = |p: &PathBuf| Item {
863 id: ItemId(format!("file:{}", p.display())),
864 item_type: ItemType::Module { is_inline: false },
865 name: Arc::from(p.file_stem().and_then(|s| s.to_str()).unwrap_or("(file)")),
866 visibility: Visibility::PubCrate,
867 location: Location { file: p.clone(), line_start: 1, line_end: 1 },
868 attributes: vec![],
869 };
870 let mk_fn_item = |p: &PathBuf, name: &str| Item {
871 id: ItemId(format!("fn:{}:X", name)),
872 item_type: ItemType::Function { is_async: false, is_const: false },
873 name: Arc::from(name),
874 visibility: Visibility::Public,
875 location: Location { file: p.clone(), line_start: 1, line_end: 1 },
876 attributes: vec![],
877 };
878
879 let mut g = KnowledgeGraph::default();
880 let caller_node = FileNode {
882 path: caller.clone(),
883 items: vec![mk_file_item(&caller)],
884 ..Default::default()
885 };
886 let callee_node = FileNode {
888 path: callee_foo.clone(),
889 items: vec![mk_file_item(&callee_foo), mk_fn_item(&callee_foo, "foo")],
890 ..Default::default()
891 };
892 let baz_node = FileNode {
894 path: baz.clone(),
895 items: vec![mk_file_item(&baz), mk_fn_item(&baz, "baz")],
896 ..Default::default()
897 };
898
899 g.files.insert(caller.clone(), caller_node);
900 g.files.insert(callee_foo.clone(), callee_node);
901 g.files.insert(baz.clone(), baz_node);
902
903 g.analyze_calls_heuristic();
905
906 let caller_file_id = ItemId(format!("file:{}", caller.display()));
907 let mut saw_foo = false;
908 let mut saw_baz = false;
909 let saw_macro = false;
910 for r in &g.relationships {
911 if r.from_item != caller_file_id {
912 continue;
913 }
914 if let RelationshipType::Calls { call_type } = &r.relationship_type {
915 if r.to_item.0.starts_with("fn:foo:") {
917 saw_foo = true;
918 assert_eq!(call_type, "heuristic");
919 }
920 if r.to_item.0.starts_with("fn:baz:") {
921 saw_baz = true;
922 }
924 }
925 if let RelationshipType::Uses { .. } = r.relationship_type { }
926 }
927 assert!(saw_foo, "expected call edge to foo()");
928 assert!(saw_baz, "expected call edge to a::b::baz()");
929 assert!(!saw_macro, "macro invocations must not create call edges");
930 }
931}