1use crate::builder::Language;
8use crate::model::{
9 CodeGraph, FileSummary, FromScope, Import, Located, Page, RankedSymbol, Ranking, RefKind,
10 Reference, Symbol, SymbolFilter, SymbolKind, ViewOverview,
11};
12use std::collections::{BTreeMap, BTreeSet, VecDeque};
13
14fn language_of(path: &str) -> &'static str {
17 std::path::Path::new(path)
18 .extension()
19 .and_then(|e| e.to_str())
20 .and_then(Language::from_extension)
21 .map_or("unknown", Language::as_str)
22}
23
24pub trait GraphStore: Send + Sync {
26 fn insert(&mut self, path: &str, graph: CodeGraph);
30 fn symbols_in_file(&self, path: &str) -> Vec<Symbol>;
32 fn definitions(&self, name: &str) -> Vec<Located<Symbol>>;
35 fn references_to(&self, name: &str) -> Vec<Located<Reference>>;
37 fn imports_in_file(&self, path: &str) -> Vec<Import>;
43 fn callers_of(&self, name: &str) -> Vec<String>;
45 fn callees(&self, name: &str) -> Vec<String>;
48
49 fn callers_scoped(&self, name: &str) -> Vec<(String, FromScope)> {
61 let mut out: Vec<(String, FromScope)> = self
62 .references_to(name)
63 .into_iter()
64 .filter(|located| located.item.kind != RefKind::Value)
65 .filter_map(|located| {
66 let scope = located.item.from_scope;
67 located.item.from.map(|from| (from, scope))
68 })
69 .collect();
70 out.sort();
71 out.dedup();
72 out
73 }
74 fn all_symbols(&self) -> Vec<Located<Symbol>>;
77 fn all_references(&self) -> Vec<Located<Reference>>;
79
80 fn impact(&self, name: &str) -> Vec<String> {
86 let mut visited = BTreeSet::from([name.to_string()]);
87 let mut queue: VecDeque<String> = VecDeque::new();
88 for caller in self.callers_of(name) {
89 if visited.insert(caller.clone()) {
90 queue.push_back(caller);
91 }
92 }
93 while let Some(current) = queue.pop_front() {
94 for caller in self.callers_of(¤t) {
95 if visited.insert(caller.clone()) {
96 queue.push_back(caller);
97 }
98 }
99 }
100 visited.remove(name);
101 visited.into_iter().collect()
102 }
103
104 fn overview(&self, largest: usize) -> ViewOverview {
111 let symbols = self.all_symbols();
112 let references = self.all_references();
113
114 let mut by_kind: BTreeMap<String, usize> = BTreeMap::new();
115 let mut by_language: BTreeMap<String, usize> = BTreeMap::new();
116 let mut per_file: BTreeMap<&str, usize> = BTreeMap::new();
117 for located in &symbols {
118 *by_kind
119 .entry(located.item.kind.as_str().to_string())
120 .or_default() += 1;
121 *by_language
122 .entry(language_of(&located.path).to_string())
123 .or_default() += 1;
124 *per_file.entry(located.path.as_str()).or_default() += 1;
125 }
126
127 let files: BTreeSet<&str> = symbols
129 .iter()
130 .map(|l| l.path.as_str())
131 .chain(references.iter().map(|l| l.path.as_str()))
132 .collect();
133
134 let mut largest_files: Vec<FileSummary> = per_file
136 .into_iter()
137 .map(|(path, symbols)| FileSummary {
138 path: path.to_string(),
139 symbols,
140 })
141 .collect();
142 largest_files.sort_by(|a, b| b.symbols.cmp(&a.symbols).then_with(|| a.path.cmp(&b.path)));
143 largest_files.truncate(largest);
144
145 ViewOverview {
146 files: files.len(),
147 symbols: symbols.len(),
148 references: references.len(),
149 by_kind,
150 by_language,
151 largest_files,
152 }
153 }
154
155 fn top(&self, ranking: Ranking, limit: usize) -> Page<RankedSymbol> {
161 let mut definitions: BTreeMap<&str, BTreeSet<&str>> = BTreeMap::new();
162 let symbols = self.all_symbols();
163 for located in &symbols {
164 definitions
165 .entry(located.item.name.as_str())
166 .or_default()
167 .insert(located.path.as_str());
168 }
169
170 let references: Vec<Located<Reference>> = self
174 .all_references()
175 .into_iter()
176 .filter(|located| located.item.kind != RefKind::Value)
177 .collect();
178 let mut scores: BTreeMap<&str, usize> = BTreeMap::new();
179 match ranking {
180 Ranking::Definitions => {
181 for (name, paths) in &definitions {
182 scores.insert(name, paths.len());
183 }
184 }
185 Ranking::FanIn => {
186 for located in &references {
187 *scores.entry(located.item.name.as_str()).or_default() += 1;
188 }
189 }
190 Ranking::FanOut => {
191 let mut callees: BTreeMap<&str, BTreeSet<&str>> = BTreeMap::new();
192 for located in &references {
193 if let Some(from) = located.item.from.as_deref() {
194 callees.entry(from).or_default().insert(&located.item.name);
195 }
196 }
197 for (name, called) in &callees {
198 scores.insert(name, called.len());
199 }
200 }
201 }
202
203 let mut ranked: Vec<RankedSymbol> = scores
205 .into_iter()
206 .map(|(name, score)| RankedSymbol {
207 name: name.to_string(),
208 score,
209 paths: definitions
210 .get(name)
211 .map(|paths| paths.iter().map(|p| p.to_string()).collect())
212 .unwrap_or_default(),
213 })
214 .collect();
215 ranked.sort_by(|a, b| b.score.cmp(&a.score).then_with(|| a.name.cmp(&b.name)));
216 Page::new(ranked, limit)
217 }
218
219 fn list(&self, filter: &SymbolFilter, limit: usize) -> Page<Located<Symbol>> {
224 let matched: Vec<Located<Symbol>> = self
225 .all_symbols()
226 .into_iter()
227 .filter(|located| filter.matches(located))
228 .collect();
229 Page::new(matched, limit)
230 }
231
232 fn unreferenced(
257 &self,
258 filter: &SymbolFilter,
259 exclude_tests: bool,
260 limit: usize,
261 ) -> Page<Located<Symbol>> {
262 let referenced: BTreeSet<String> = self
263 .all_references()
264 .into_iter()
265 .map(|located| located.item.name)
266 .collect();
267
268 let symbols = self.all_symbols();
269 let test_scopes: Vec<(String, usize, usize)> = symbols
272 .iter()
273 .filter(|l| l.item.kind == SymbolKind::Module && is_test_scope_name(&l.item.name))
274 .map(|l| (l.path.clone(), l.item.start_line, l.item.end_line))
275 .collect();
276
277 let in_test_scope = |located: &Located<Symbol>| {
278 in_tests_dir(&located.path)
279 || test_scopes.iter().any(|(path, start, end)| {
280 *path == located.path
281 && located.item.start_line >= *start
282 && located.item.end_line <= *end
283 })
284 };
285
286 let mut matched: Vec<Located<Symbol>> = symbols
287 .into_iter()
288 .filter(|located| !referenced.contains(&located.item.name))
289 .filter(|located| filter.matches(located))
290 .filter(|located| !exclude_tests || !in_test_scope(located))
291 .collect();
292
293 matched.sort_by(|a, b| {
294 a.path
295 .cmp(&b.path)
296 .then_with(|| a.item.start_line.cmp(&b.item.start_line))
297 });
298 Page::new(matched, limit)
299 }
300}
301
302fn is_test_scope_name(name: &str) -> bool {
304 name == "tests" || name == "test"
305}
306
307fn in_tests_dir(path: &str) -> bool {
310 path.split('/').any(|part| part == "tests")
311}
312
313#[derive(Debug, Default)]
315pub struct InMemoryGraphStore {
316 slices: BTreeMap<String, CodeGraph>,
317}
318
319impl InMemoryGraphStore {
320 pub fn new() -> Self {
321 Self::default()
322 }
323
324 pub fn slices(&self) -> &BTreeMap<String, CodeGraph> {
326 &self.slices
327 }
328}
329
330impl GraphStore for InMemoryGraphStore {
331 fn insert(&mut self, path: &str, graph: CodeGraph) {
332 self.slices.insert(path.to_string(), graph);
333 }
334
335 fn symbols_in_file(&self, path: &str) -> Vec<Symbol> {
336 self.slices
337 .get(path)
338 .map(|g| g.symbols.clone())
339 .unwrap_or_default()
340 }
341
342 fn imports_in_file(&self, path: &str) -> Vec<Import> {
343 self.slices
344 .get(path)
345 .map(|g| g.imports.clone())
346 .unwrap_or_default()
347 }
348
349 fn definitions(&self, name: &str) -> Vec<Located<Symbol>> {
350 self.slices
351 .iter()
352 .flat_map(|(path, g)| {
353 g.symbols
354 .iter()
355 .filter(|s| s.name == name)
356 .map(move |s| Located {
357 path: path.clone(),
358 item: s.clone(),
359 })
360 })
361 .collect()
362 }
363
364 fn references_to(&self, name: &str) -> Vec<Located<Reference>> {
365 self.slices
366 .iter()
367 .flat_map(|(path, g)| {
368 g.references
369 .iter()
370 .filter(|r| r.name == name)
371 .map(move |r| Located {
372 path: path.clone(),
373 item: r.clone(),
374 })
375 })
376 .collect()
377 }
378
379 fn callers_of(&self, name: &str) -> Vec<String> {
380 let mut callers: Vec<String> = self
381 .slices
382 .values()
383 .flat_map(|g| g.references.iter())
384 .filter(|r| r.name == name && r.kind != RefKind::Value)
385 .filter_map(|r| r.from.clone())
386 .collect();
387 callers.sort();
388 callers.dedup();
389 callers
390 }
391
392 fn callees(&self, name: &str) -> Vec<String> {
393 let mut callees: Vec<String> = self
394 .slices
395 .values()
396 .flat_map(|g| g.references.iter())
397 .filter(|r| r.from.as_deref() == Some(name) && r.kind != RefKind::Value)
398 .map(|r| r.name.clone())
399 .collect();
400 callees.sort();
401 callees.dedup();
402 callees
403 }
404
405 fn all_symbols(&self) -> Vec<Located<Symbol>> {
406 self.slices
407 .iter()
408 .flat_map(|(path, g)| {
409 g.symbols.iter().map(move |s| Located {
410 path: path.clone(),
411 item: s.clone(),
412 })
413 })
414 .collect()
415 }
416
417 fn all_references(&self) -> Vec<Located<Reference>> {
418 self.slices
419 .iter()
420 .flat_map(|(path, g)| {
421 g.references.iter().map(move |r| Located {
422 path: path.clone(),
423 item: r.clone(),
424 })
425 })
426 .collect()
427 }
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433 use crate::builder::{build, build_rust};
434 use crate::model::{RefKind, SymbolKind};
435
436 const SRC: &str = r#"
437fn helper() {}
438fn a() { helper(); }
439fn b() { helper(); }
440"#;
441
442 fn store() -> InMemoryGraphStore {
443 let mut s = InMemoryGraphStore::new();
444 s.insert("lib.rs", build_rust(SRC));
445 s
446 }
447
448 #[test]
449 fn definitions_carry_their_path() {
450 let s = store();
451 let defs = s.definitions("helper");
452 assert_eq!(defs.len(), 1);
453 assert_eq!(defs[0].path, "lib.rs");
454 assert_eq!(defs[0].item.name, "helper");
455 }
456
457 #[test]
458 fn symbols_in_file_filters_by_path() {
459 let s = store();
460 assert!(s.symbols_in_file("lib.rs").iter().any(|sy| sy.name == "a"));
461 assert!(s.symbols_in_file("other.rs").is_empty());
462 }
463
464 #[test]
465 fn definitions_span_multiple_paths() {
466 let mut s = InMemoryGraphStore::new();
467 s.insert("a.rs", build_rust("fn dup() {}"));
468 s.insert("b.rs", build_rust("fn dup() {}"));
469 let mut paths: Vec<String> = s.definitions("dup").into_iter().map(|l| l.path).collect();
470 paths.sort();
471 assert_eq!(paths, vec!["a.rs".to_string(), "b.rs".to_string()]);
472 }
473
474 #[test]
475 fn reinserting_same_path_replaces_not_appends() {
476 let mut s = store();
477 s.insert("lib.rs", build_rust(SRC));
479 assert_eq!(s.definitions("helper").len(), 1);
480 }
481
482 #[test]
483 fn callers_of_dedups_and_sorts_across_slices() {
484 let s = store();
485 assert_eq!(
486 s.callers_of("helper"),
487 vec!["a".to_string(), "b".to_string()]
488 );
489 }
490
491 #[test]
492 fn references_to_counts_all_with_paths() {
493 let s = store();
494 let refs = s.references_to("helper");
495 assert_eq!(refs.len(), 2);
496 assert!(refs.iter().all(|r| r.path == "lib.rs"));
497 }
498
499 const CHAIN: &str = r#"
500fn leaf() {}
501fn mid() { leaf(); }
502fn top() { mid(); }
503fn other() { leaf(); }
504"#;
505
506 fn chain() -> InMemoryGraphStore {
507 let mut s = InMemoryGraphStore::new();
508 s.insert("chain.rs", build_rust(CHAIN));
509 s
510 }
511
512 #[test]
513 fn callees_lists_names_called_from_a_function() {
514 let s = chain();
515 assert_eq!(s.callees("mid"), vec!["leaf".to_string()]);
516 assert_eq!(s.callees("top"), vec!["mid".to_string()]);
517 assert!(s.callees("leaf").is_empty());
518 }
519
520 #[test]
521 fn callees_dedups_repeated_calls() {
522 let mut s = InMemoryGraphStore::new();
523 s.insert("d.rs", build_rust("fn f() { g(); g(); h(); }"));
524 assert_eq!(s.callees("f"), vec!["g".to_string(), "h".to_string()]);
525 }
526
527 #[test]
528 fn impact_is_the_transitive_caller_closure() {
529 let s = chain();
530 assert_eq!(
533 s.impact("leaf"),
534 vec!["mid".to_string(), "other".to_string(), "top".to_string()]
535 );
536 assert_eq!(s.impact("mid"), vec!["top".to_string()]);
537 assert!(s.impact("top").is_empty());
538 }
539
540 #[test]
541 fn impact_excludes_the_seed_and_survives_cycles() {
542 let mut s = InMemoryGraphStore::new();
543 s.insert("cyc.rs", build_rust("fn a() { b(); } fn b() { a(); }"));
544 assert_eq!(s.impact("a"), vec!["b".to_string()]);
546 assert_eq!(s.impact("b"), vec!["a".to_string()]);
547 }
548
549 fn view() -> InMemoryGraphStore {
555 let mut s = InMemoryGraphStore::new();
556 s.insert(
557 "crates/a/src/lib.rs",
558 build_rust(
559 "struct A; struct A2; fn new() -> A { A } \
560 fn run() { new(); helper(); helper(); }",
561 ),
562 );
563 s.insert("crates/b/src/lib.rs", build_rust("fn new() -> u8 { 0 }"));
564 s.insert(
565 "scripts/tool.py",
566 build(Language::Python, "def main():\n pass\n"),
567 );
568 s
569 }
570
571 #[test]
572 fn overview_counts_files_symbols_and_references() {
573 let o = view().overview(10);
574 assert_eq!(o.files, 3);
575 assert_eq!(o.symbols, view().all_symbols().len());
576 assert_eq!(o.references, view().all_references().len());
577 }
578
579 #[test]
580 fn overview_breaks_down_by_kind() {
581 let o = view().overview(10);
582 assert_eq!(o.by_kind.get("struct"), Some(&2));
584 assert!(o.by_kind.get("function").is_some_and(|n| *n >= 3));
585 }
586
587 #[test]
588 fn overview_breaks_down_by_language_from_the_path_extension() {
589 let o = view().overview(10);
590 assert!(o.by_language.contains_key("rust"));
591 assert!(o.by_language.contains_key("python"));
592 assert_eq!(o.by_language.values().sum::<usize>(), o.symbols);
594 }
595
596 #[test]
597 fn overview_buckets_unrecognized_extensions_as_unknown() {
598 let mut s = InMemoryGraphStore::new();
599 s.insert("build.zzz", build_rust("fn f() {}"));
600 let o = s.overview(10);
601 assert_eq!(o.by_language.get("unknown"), Some(&1));
602 assert_eq!(o.by_language.values().sum::<usize>(), o.symbols);
603 }
604
605 #[test]
606 fn overview_ranks_largest_files_by_symbol_count() {
607 let o = view().overview(10);
608 assert_eq!(o.largest_files[0].path, "crates/a/src/lib.rs");
609 assert!(o.largest_files[0].symbols >= o.largest_files[1].symbols);
610 }
611
612 #[test]
613 fn overview_bounds_largest_files_to_the_requested_limit() {
614 let o = view().overview(1);
615 assert_eq!(o.largest_files.len(), 1);
616 assert_eq!(o.files, 3);
618 }
619
620 #[test]
621 fn top_by_definitions_surfaces_names_defined_in_several_paths() {
622 let page = view().top(Ranking::Definitions, 10);
623 let new = page
624 .items
625 .iter()
626 .find(|r| r.name == "new")
627 .expect("`new` is defined twice");
628 assert_eq!(new.score, 2);
629 assert_eq!(
630 new.paths,
631 vec![
632 "crates/a/src/lib.rs".to_string(),
633 "crates/b/src/lib.rs".to_string()
634 ]
635 );
636 }
637
638 #[test]
639 fn top_by_fan_in_ranks_by_reference_count() {
640 let page = view().top(Ranking::FanIn, 10);
641 let helper = page
642 .items
643 .iter()
644 .find(|r| r.name == "helper")
645 .expect("`helper` is called twice");
646 assert_eq!(helper.score, 2);
647 }
648
649 #[test]
650 fn top_by_fan_out_ranks_by_distinct_callees() {
651 let page = view().top(Ranking::FanOut, 10);
652 let run = page
653 .items
654 .iter()
655 .find(|r| r.name == "run")
656 .expect("`run` calls new and helper");
657 assert_eq!(run.score, 2);
658 }
659
660 #[test]
661 fn top_is_ordered_by_descending_score() {
662 let page = view().top(Ranking::FanIn, 10);
663 let scores: Vec<usize> = page.items.iter().map(|r| r.score).collect();
664 let mut sorted = scores.clone();
665 sorted.sort_by(|a, b| b.cmp(a));
666 assert_eq!(scores, sorted);
667 }
668
669 #[test]
670 fn top_bounds_results_and_reports_truncation() {
671 let page = view().top(Ranking::FanIn, 1);
672 assert_eq!(page.items.len(), 1);
673 assert!(page.truncated);
674 assert!(page.total > 1);
675 }
676
677 #[test]
678 fn list_filters_by_path_prefix() {
679 let page = view().list(&SymbolFilter::default().path_prefix("crates/b"), 100);
680 assert!(!page.items.is_empty());
681 assert!(page.items.iter().all(|l| l.path == "crates/b/src/lib.rs"));
682 }
683
684 #[test]
685 fn list_filters_by_kind() {
686 let page = view().list(&SymbolFilter::default().kind(SymbolKind::Struct), 100);
687 assert_eq!(page.items.len(), 2);
688 assert!(page.items.iter().all(|l| l.item.kind == SymbolKind::Struct));
689 }
690
691 #[test]
692 fn list_filters_by_name_substring() {
693 let page = view().list(&SymbolFilter::default().name_contains("ru"), 100);
694 assert!(page.items.iter().any(|l| l.item.name == "run"));
695 assert!(page.items.iter().all(|l| l.item.name.contains("ru")));
696 }
697
698 #[test]
699 fn list_combines_filters_conjunctively() {
700 let page = view().list(
701 &SymbolFilter::default()
702 .path_prefix("crates/a")
703 .kind(SymbolKind::Struct),
704 100,
705 );
706 assert_eq!(page.items.len(), 2);
707 assert!(
708 page.items
709 .iter()
710 .all(|l| l.path.starts_with("crates/a") && l.item.kind == SymbolKind::Struct)
711 );
712 }
713
714 #[test]
715 fn list_bounds_results_and_reports_truncation() {
716 let page = view().list(&SymbolFilter::default(), 1);
717 assert_eq!(page.items.len(), 1);
718 assert!(page.truncated);
719 assert_eq!(page.total, view().all_symbols().len());
720 }
721
722 #[test]
723 fn list_without_filters_returns_everything_untruncated() {
724 let page = view().list(&SymbolFilter::default(), 1000);
725 assert!(!page.truncated);
726 assert_eq!(page.items.len(), view().all_symbols().len());
727 }
728
729 fn dead() -> InMemoryGraphStore {
735 let mut s = InMemoryGraphStore::new();
736 s.insert(
737 "src/lib.rs",
738 build_rust(
739 "fn used() {}\n\
740 fn caller() { used(); }\n\
741 fn orphan() {}\n\
742 #[cfg(test)]\n\
743 mod tests {\n \
744 fn t_helper() {}\n \
745 fn t_only() {}\n\
746 }\n",
747 ),
748 );
749 s
750 }
751
752 fn names(page: &Page<Located<Symbol>>) -> Vec<&str> {
753 page.items.iter().map(|l| l.item.name.as_str()).collect()
754 }
755
756 #[test]
757 fn unreferenced_finds_symbols_with_no_inbound_reference() {
758 let page = dead().unreferenced(&SymbolFilter::default(), true, 100);
759 assert!(names(&page).contains(&"orphan"));
760 assert!(!names(&page).contains(&"used"));
761 }
762
763 #[test]
764 fn unreferenced_excludes_mod_tests_members_by_default() {
765 let page = dead().unreferenced(&SymbolFilter::default(), true, 100);
766 assert!(!names(&page).contains(&"t_helper"));
768 assert!(!names(&page).contains(&"t_only"));
769 assert!(!names(&page).contains(&"tests"));
771 }
772
773 #[test]
774 fn unreferenced_keeps_test_members_when_not_excluding() {
775 let page = dead().unreferenced(&SymbolFilter::default(), false, 100);
776 assert!(names(&page).contains(&"t_only"));
777 assert!(names(&page).contains(&"orphan"));
778 }
779
780 #[test]
781 fn unreferenced_excludes_files_under_a_tests_directory() {
782 let mut s = InMemoryGraphStore::new();
783 s.insert(
784 "tests/it.rs",
785 build_rust("fn only_in_integration_test() {}"),
786 );
787 s.insert("src/lib.rs", build_rust("fn orphan() {}"));
788 let page = s.unreferenced(&SymbolFilter::default(), true, 100);
789 assert_eq!(names(&page), vec!["orphan"]);
790 }
791
792 #[test]
793 fn unreferenced_does_not_treat_a_tests_substring_as_a_directory() {
794 let mut s = InMemoryGraphStore::new();
795 s.insert("contests/entry.rs", build_rust("fn orphan() {}"));
797 let page = s.unreferenced(&SymbolFilter::default(), true, 100);
798 assert_eq!(names(&page), vec!["orphan"]);
799 }
800
801 #[test]
802 fn unreferenced_counts_a_reference_from_anywhere_including_tests() {
803 let mut s = InMemoryGraphStore::new();
804 s.insert(
807 "src/lib.rs",
808 build_rust(
809 "fn prod() {}\n\
810 #[cfg(test)]\n\
811 mod tests {\n \
812 fn t() { prod(); }\n\
813 }\n",
814 ),
815 );
816 let page = s.unreferenced(&SymbolFilter::default(), true, 100);
817 assert!(!names(&page).contains(&"prod"));
818 }
819
820 #[test]
821 fn unreferenced_does_not_flag_symbols_called_inside_macros() {
822 let mut s = InMemoryGraphStore::new();
823 s.insert(
828 "src/lib.rs",
829 build_rust("fn f() -> u8 { 0 }\nfn g() { assert_eq!(f(), 0); }\n"),
830 );
831 let page = s.unreferenced(&SymbolFilter::default(), true, 100);
832 assert!(!names(&page).contains(&"f"), "f is called from g");
833 }
834
835 #[test]
836 fn unreferenced_does_not_flag_recursive_functions() {
837 let mut s = InMemoryGraphStore::new();
838 s.insert("src/lib.rs", build_rust("fn recur() { recur(); }"));
841 let page = s.unreferenced(&SymbolFilter::default(), true, 100);
842 assert!(!names(&page).contains(&"recur"));
843 }
844
845 #[test]
846 fn unreferenced_applies_the_symbol_filter() {
847 let page =
848 dead().unreferenced(&SymbolFilter::default().kind(SymbolKind::Struct), true, 100);
849 assert!(page.items.is_empty());
850 }
851
852 #[test]
853 fn unreferenced_results_carry_their_path() {
854 let page = dead().unreferenced(&SymbolFilter::default(), true, 100);
855 assert!(page.items.iter().all(|l| l.path == "src/lib.rs"));
856 }
857
858 #[test]
859 fn unreferenced_bounds_results_and_reports_truncation() {
860 let mut s = InMemoryGraphStore::new();
861 s.insert("src/lib.rs", build_rust("fn a() {} fn b() {} fn c() {}"));
862 let page = s.unreferenced(&SymbolFilter::default(), true, 2);
863 assert_eq!(page.items.len(), 2);
864 assert!(page.truncated);
865 assert_eq!(page.total, 3);
866 }
867
868 #[test]
869 fn unreferenced_is_ordered_by_path_then_line() {
870 let mut s = InMemoryGraphStore::new();
871 s.insert("src/b.rs", build_rust("fn z() {}"));
872 s.insert("src/a.rs", build_rust("fn y() {}\nfn x() {}"));
873 let page = s.unreferenced(&SymbolFilter::default(), true, 100);
874 let seen: Vec<(&str, &str)> = page
875 .items
876 .iter()
877 .map(|l| (l.path.as_str(), l.item.name.as_str()))
878 .collect();
879 assert_eq!(
880 seen,
881 vec![("src/a.rs", "y"), ("src/a.rs", "x"), ("src/b.rs", "z")]
882 );
883 }
884
885 fn passed_as_a_value() -> InMemoryGraphStore {
889 let mut s = InMemoryGraphStore::new();
890 s.insert(
891 "src/lib.rs",
892 build_rust("fn helper() {}\nfn g() { register(helper); }\n"),
893 );
894 s
895 }
896
897 #[test]
898 fn unreferenced_does_not_flag_a_function_only_passed_as_a_value() {
899 let s = passed_as_a_value();
903 let page = s.unreferenced(&SymbolFilter::default(), true, 100);
904 assert!(
905 !names(&page).contains(&"helper"),
906 "helper is passed to register: {:?}",
907 names(&page)
908 );
909 }
910
911 #[test]
912 fn callers_of_ignores_a_function_passed_as_a_value() {
913 let s = passed_as_a_value();
916 assert!(
917 s.callers_of("helper").is_empty(),
918 "{:?}",
919 s.callers_of("helper")
920 );
921 }
922
923 #[test]
924 fn callees_ignores_a_function_passed_as_a_value() {
925 let s = passed_as_a_value();
926 assert_eq!(s.callees("g"), vec!["register".to_string()]);
927 }
928
929 #[test]
930 fn references_to_still_reports_a_value_reference() {
931 let s = passed_as_a_value();
934 let refs = s.references_to("helper");
935 assert_eq!(refs.len(), 1);
936 assert_eq!(refs[0].item.kind, RefKind::Value);
937 }
938
939 #[test]
940 fn fan_in_and_fan_out_stay_call_graph_metrics() {
941 let mut s = InMemoryGraphStore::new();
945 s.insert(
946 "src/lib.rs",
947 build_rust("fn helper() {}\nfn g(x: u32) { register(helper); take(x); }\n"),
948 );
949
950 let fan_in = s.top(Ranking::FanIn, 50);
951 let helper_score = fan_in
952 .items
953 .iter()
954 .find(|r| r.name == "helper")
955 .map(|r| r.score);
956 assert_eq!(helper_score, None, "helper is never called: {fan_in:?}");
957
958 let fan_out = s.top(Ranking::FanOut, 50);
959 let g_score = fan_out
960 .items
961 .iter()
962 .find(|r| r.name == "g")
963 .map(|r| r.score);
964 assert_eq!(g_score, Some(2), "g calls register and take: {fan_out:?}");
965 }
966}