1use crate::builder::Language;
8use crate::model::{
9 CodeGraph, FileSummary, Located, Page, RankedSymbol, Ranking, Reference, Symbol, SymbolFilter,
10 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 callers_of(&self, name: &str) -> Vec<String>;
39 fn callees(&self, name: &str) -> Vec<String>;
42 fn all_symbols(&self) -> Vec<Located<Symbol>>;
45 fn all_references(&self) -> Vec<Located<Reference>>;
47
48 fn impact(&self, name: &str) -> Vec<String> {
54 let mut visited = BTreeSet::from([name.to_string()]);
55 let mut queue: VecDeque<String> = VecDeque::new();
56 for caller in self.callers_of(name) {
57 if visited.insert(caller.clone()) {
58 queue.push_back(caller);
59 }
60 }
61 while let Some(current) = queue.pop_front() {
62 for caller in self.callers_of(¤t) {
63 if visited.insert(caller.clone()) {
64 queue.push_back(caller);
65 }
66 }
67 }
68 visited.remove(name);
69 visited.into_iter().collect()
70 }
71
72 fn overview(&self, largest: usize) -> ViewOverview {
79 let symbols = self.all_symbols();
80 let references = self.all_references();
81
82 let mut by_kind: BTreeMap<String, usize> = BTreeMap::new();
83 let mut by_language: BTreeMap<String, usize> = BTreeMap::new();
84 let mut per_file: BTreeMap<&str, usize> = BTreeMap::new();
85 for located in &symbols {
86 *by_kind
87 .entry(located.item.kind.as_str().to_string())
88 .or_default() += 1;
89 *by_language
90 .entry(language_of(&located.path).to_string())
91 .or_default() += 1;
92 *per_file.entry(located.path.as_str()).or_default() += 1;
93 }
94
95 let files: BTreeSet<&str> = symbols
97 .iter()
98 .map(|l| l.path.as_str())
99 .chain(references.iter().map(|l| l.path.as_str()))
100 .collect();
101
102 let mut largest_files: Vec<FileSummary> = per_file
104 .into_iter()
105 .map(|(path, symbols)| FileSummary {
106 path: path.to_string(),
107 symbols,
108 })
109 .collect();
110 largest_files.sort_by(|a, b| b.symbols.cmp(&a.symbols).then_with(|| a.path.cmp(&b.path)));
111 largest_files.truncate(largest);
112
113 ViewOverview {
114 files: files.len(),
115 symbols: symbols.len(),
116 references: references.len(),
117 by_kind,
118 by_language,
119 largest_files,
120 }
121 }
122
123 fn top(&self, ranking: Ranking, limit: usize) -> Page<RankedSymbol> {
129 let mut definitions: BTreeMap<&str, BTreeSet<&str>> = BTreeMap::new();
130 let symbols = self.all_symbols();
131 for located in &symbols {
132 definitions
133 .entry(located.item.name.as_str())
134 .or_default()
135 .insert(located.path.as_str());
136 }
137
138 let references = self.all_references();
139 let mut scores: BTreeMap<&str, usize> = BTreeMap::new();
140 match ranking {
141 Ranking::Definitions => {
142 for (name, paths) in &definitions {
143 scores.insert(name, paths.len());
144 }
145 }
146 Ranking::FanIn => {
147 for located in &references {
148 *scores.entry(located.item.name.as_str()).or_default() += 1;
149 }
150 }
151 Ranking::FanOut => {
152 let mut callees: BTreeMap<&str, BTreeSet<&str>> = BTreeMap::new();
153 for located in &references {
154 if let Some(from) = located.item.from.as_deref() {
155 callees.entry(from).or_default().insert(&located.item.name);
156 }
157 }
158 for (name, called) in &callees {
159 scores.insert(name, called.len());
160 }
161 }
162 }
163
164 let mut ranked: Vec<RankedSymbol> = scores
166 .into_iter()
167 .map(|(name, score)| RankedSymbol {
168 name: name.to_string(),
169 score,
170 paths: definitions
171 .get(name)
172 .map(|paths| paths.iter().map(|p| p.to_string()).collect())
173 .unwrap_or_default(),
174 })
175 .collect();
176 ranked.sort_by(|a, b| b.score.cmp(&a.score).then_with(|| a.name.cmp(&b.name)));
177 Page::new(ranked, limit)
178 }
179
180 fn list(&self, filter: &SymbolFilter, limit: usize) -> Page<Located<Symbol>> {
185 let matched: Vec<Located<Symbol>> = self
186 .all_symbols()
187 .into_iter()
188 .filter(|located| filter.matches(located))
189 .collect();
190 Page::new(matched, limit)
191 }
192
193 fn unreferenced(
218 &self,
219 filter: &SymbolFilter,
220 exclude_tests: bool,
221 limit: usize,
222 ) -> Page<Located<Symbol>> {
223 let referenced: BTreeSet<String> = self
224 .all_references()
225 .into_iter()
226 .map(|located| located.item.name)
227 .collect();
228
229 let symbols = self.all_symbols();
230 let test_scopes: Vec<(String, usize, usize)> = symbols
233 .iter()
234 .filter(|l| l.item.kind == SymbolKind::Module && is_test_scope_name(&l.item.name))
235 .map(|l| (l.path.clone(), l.item.start_line, l.item.end_line))
236 .collect();
237
238 let in_test_scope = |located: &Located<Symbol>| {
239 in_tests_dir(&located.path)
240 || test_scopes.iter().any(|(path, start, end)| {
241 *path == located.path
242 && located.item.start_line >= *start
243 && located.item.end_line <= *end
244 })
245 };
246
247 let mut matched: Vec<Located<Symbol>> = symbols
248 .into_iter()
249 .filter(|located| !referenced.contains(&located.item.name))
250 .filter(|located| filter.matches(located))
251 .filter(|located| !exclude_tests || !in_test_scope(located))
252 .collect();
253
254 matched.sort_by(|a, b| {
255 a.path
256 .cmp(&b.path)
257 .then_with(|| a.item.start_line.cmp(&b.item.start_line))
258 });
259 Page::new(matched, limit)
260 }
261}
262
263fn is_test_scope_name(name: &str) -> bool {
265 name == "tests" || name == "test"
266}
267
268fn in_tests_dir(path: &str) -> bool {
271 path.split('/').any(|part| part == "tests")
272}
273
274#[derive(Debug, Default)]
276pub struct InMemoryGraphStore {
277 slices: BTreeMap<String, CodeGraph>,
278}
279
280impl InMemoryGraphStore {
281 pub fn new() -> Self {
282 Self::default()
283 }
284
285 pub fn slices(&self) -> &BTreeMap<String, CodeGraph> {
287 &self.slices
288 }
289}
290
291impl GraphStore for InMemoryGraphStore {
292 fn insert(&mut self, path: &str, graph: CodeGraph) {
293 self.slices.insert(path.to_string(), graph);
294 }
295
296 fn symbols_in_file(&self, path: &str) -> Vec<Symbol> {
297 self.slices
298 .get(path)
299 .map(|g| g.symbols.clone())
300 .unwrap_or_default()
301 }
302
303 fn definitions(&self, name: &str) -> Vec<Located<Symbol>> {
304 self.slices
305 .iter()
306 .flat_map(|(path, g)| {
307 g.symbols
308 .iter()
309 .filter(|s| s.name == name)
310 .map(move |s| Located {
311 path: path.clone(),
312 item: s.clone(),
313 })
314 })
315 .collect()
316 }
317
318 fn references_to(&self, name: &str) -> Vec<Located<Reference>> {
319 self.slices
320 .iter()
321 .flat_map(|(path, g)| {
322 g.references
323 .iter()
324 .filter(|r| r.name == name)
325 .map(move |r| Located {
326 path: path.clone(),
327 item: r.clone(),
328 })
329 })
330 .collect()
331 }
332
333 fn callers_of(&self, name: &str) -> Vec<String> {
334 let mut callers: Vec<String> = self
335 .slices
336 .values()
337 .flat_map(|g| g.references.iter())
338 .filter(|r| r.name == name)
339 .filter_map(|r| r.from.clone())
340 .collect();
341 callers.sort();
342 callers.dedup();
343 callers
344 }
345
346 fn callees(&self, name: &str) -> Vec<String> {
347 let mut callees: Vec<String> = self
348 .slices
349 .values()
350 .flat_map(|g| g.references.iter())
351 .filter(|r| r.from.as_deref() == Some(name))
352 .map(|r| r.name.clone())
353 .collect();
354 callees.sort();
355 callees.dedup();
356 callees
357 }
358
359 fn all_symbols(&self) -> Vec<Located<Symbol>> {
360 self.slices
361 .iter()
362 .flat_map(|(path, g)| {
363 g.symbols.iter().map(move |s| Located {
364 path: path.clone(),
365 item: s.clone(),
366 })
367 })
368 .collect()
369 }
370
371 fn all_references(&self) -> Vec<Located<Reference>> {
372 self.slices
373 .iter()
374 .flat_map(|(path, g)| {
375 g.references.iter().map(move |r| Located {
376 path: path.clone(),
377 item: r.clone(),
378 })
379 })
380 .collect()
381 }
382}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387 use crate::builder::{build, build_rust};
388 use crate::model::SymbolKind;
389
390 const SRC: &str = r#"
391fn helper() {}
392fn a() { helper(); }
393fn b() { helper(); }
394"#;
395
396 fn store() -> InMemoryGraphStore {
397 let mut s = InMemoryGraphStore::new();
398 s.insert("lib.rs", build_rust(SRC));
399 s
400 }
401
402 #[test]
403 fn definitions_carry_their_path() {
404 let s = store();
405 let defs = s.definitions("helper");
406 assert_eq!(defs.len(), 1);
407 assert_eq!(defs[0].path, "lib.rs");
408 assert_eq!(defs[0].item.name, "helper");
409 }
410
411 #[test]
412 fn symbols_in_file_filters_by_path() {
413 let s = store();
414 assert!(s.symbols_in_file("lib.rs").iter().any(|sy| sy.name == "a"));
415 assert!(s.symbols_in_file("other.rs").is_empty());
416 }
417
418 #[test]
419 fn definitions_span_multiple_paths() {
420 let mut s = InMemoryGraphStore::new();
421 s.insert("a.rs", build_rust("fn dup() {}"));
422 s.insert("b.rs", build_rust("fn dup() {}"));
423 let mut paths: Vec<String> = s.definitions("dup").into_iter().map(|l| l.path).collect();
424 paths.sort();
425 assert_eq!(paths, vec!["a.rs".to_string(), "b.rs".to_string()]);
426 }
427
428 #[test]
429 fn reinserting_same_path_replaces_not_appends() {
430 let mut s = store();
431 s.insert("lib.rs", build_rust(SRC));
433 assert_eq!(s.definitions("helper").len(), 1);
434 }
435
436 #[test]
437 fn callers_of_dedups_and_sorts_across_slices() {
438 let s = store();
439 assert_eq!(
440 s.callers_of("helper"),
441 vec!["a".to_string(), "b".to_string()]
442 );
443 }
444
445 #[test]
446 fn references_to_counts_all_with_paths() {
447 let s = store();
448 let refs = s.references_to("helper");
449 assert_eq!(refs.len(), 2);
450 assert!(refs.iter().all(|r| r.path == "lib.rs"));
451 }
452
453 const CHAIN: &str = r#"
454fn leaf() {}
455fn mid() { leaf(); }
456fn top() { mid(); }
457fn other() { leaf(); }
458"#;
459
460 fn chain() -> InMemoryGraphStore {
461 let mut s = InMemoryGraphStore::new();
462 s.insert("chain.rs", build_rust(CHAIN));
463 s
464 }
465
466 #[test]
467 fn callees_lists_names_called_from_a_function() {
468 let s = chain();
469 assert_eq!(s.callees("mid"), vec!["leaf".to_string()]);
470 assert_eq!(s.callees("top"), vec!["mid".to_string()]);
471 assert!(s.callees("leaf").is_empty());
472 }
473
474 #[test]
475 fn callees_dedups_repeated_calls() {
476 let mut s = InMemoryGraphStore::new();
477 s.insert("d.rs", build_rust("fn f() { g(); g(); h(); }"));
478 assert_eq!(s.callees("f"), vec!["g".to_string(), "h".to_string()]);
479 }
480
481 #[test]
482 fn impact_is_the_transitive_caller_closure() {
483 let s = chain();
484 assert_eq!(
487 s.impact("leaf"),
488 vec!["mid".to_string(), "other".to_string(), "top".to_string()]
489 );
490 assert_eq!(s.impact("mid"), vec!["top".to_string()]);
491 assert!(s.impact("top").is_empty());
492 }
493
494 #[test]
495 fn impact_excludes_the_seed_and_survives_cycles() {
496 let mut s = InMemoryGraphStore::new();
497 s.insert("cyc.rs", build_rust("fn a() { b(); } fn b() { a(); }"));
498 assert_eq!(s.impact("a"), vec!["b".to_string()]);
500 assert_eq!(s.impact("b"), vec!["a".to_string()]);
501 }
502
503 fn view() -> InMemoryGraphStore {
509 let mut s = InMemoryGraphStore::new();
510 s.insert(
511 "crates/a/src/lib.rs",
512 build_rust(
513 "struct A; struct A2; fn new() -> A { A } \
514 fn run() { new(); helper(); helper(); }",
515 ),
516 );
517 s.insert("crates/b/src/lib.rs", build_rust("fn new() -> u8 { 0 }"));
518 s.insert(
519 "scripts/tool.py",
520 build(Language::Python, "def main():\n pass\n"),
521 );
522 s
523 }
524
525 #[test]
526 fn overview_counts_files_symbols_and_references() {
527 let o = view().overview(10);
528 assert_eq!(o.files, 3);
529 assert_eq!(o.symbols, view().all_symbols().len());
530 assert_eq!(o.references, view().all_references().len());
531 }
532
533 #[test]
534 fn overview_breaks_down_by_kind() {
535 let o = view().overview(10);
536 assert_eq!(o.by_kind.get("struct"), Some(&2));
538 assert!(o.by_kind.get("function").is_some_and(|n| *n >= 3));
539 }
540
541 #[test]
542 fn overview_breaks_down_by_language_from_the_path_extension() {
543 let o = view().overview(10);
544 assert!(o.by_language.contains_key("rust"));
545 assert!(o.by_language.contains_key("python"));
546 assert_eq!(o.by_language.values().sum::<usize>(), o.symbols);
548 }
549
550 #[test]
551 fn overview_buckets_unrecognized_extensions_as_unknown() {
552 let mut s = InMemoryGraphStore::new();
553 s.insert("build.zzz", build_rust("fn f() {}"));
554 let o = s.overview(10);
555 assert_eq!(o.by_language.get("unknown"), Some(&1));
556 assert_eq!(o.by_language.values().sum::<usize>(), o.symbols);
557 }
558
559 #[test]
560 fn overview_ranks_largest_files_by_symbol_count() {
561 let o = view().overview(10);
562 assert_eq!(o.largest_files[0].path, "crates/a/src/lib.rs");
563 assert!(o.largest_files[0].symbols >= o.largest_files[1].symbols);
564 }
565
566 #[test]
567 fn overview_bounds_largest_files_to_the_requested_limit() {
568 let o = view().overview(1);
569 assert_eq!(o.largest_files.len(), 1);
570 assert_eq!(o.files, 3);
572 }
573
574 #[test]
575 fn top_by_definitions_surfaces_names_defined_in_several_paths() {
576 let page = view().top(Ranking::Definitions, 10);
577 let new = page
578 .items
579 .iter()
580 .find(|r| r.name == "new")
581 .expect("`new` is defined twice");
582 assert_eq!(new.score, 2);
583 assert_eq!(
584 new.paths,
585 vec![
586 "crates/a/src/lib.rs".to_string(),
587 "crates/b/src/lib.rs".to_string()
588 ]
589 );
590 }
591
592 #[test]
593 fn top_by_fan_in_ranks_by_reference_count() {
594 let page = view().top(Ranking::FanIn, 10);
595 let helper = page
596 .items
597 .iter()
598 .find(|r| r.name == "helper")
599 .expect("`helper` is called twice");
600 assert_eq!(helper.score, 2);
601 }
602
603 #[test]
604 fn top_by_fan_out_ranks_by_distinct_callees() {
605 let page = view().top(Ranking::FanOut, 10);
606 let run = page
607 .items
608 .iter()
609 .find(|r| r.name == "run")
610 .expect("`run` calls new and helper");
611 assert_eq!(run.score, 2);
612 }
613
614 #[test]
615 fn top_is_ordered_by_descending_score() {
616 let page = view().top(Ranking::FanIn, 10);
617 let scores: Vec<usize> = page.items.iter().map(|r| r.score).collect();
618 let mut sorted = scores.clone();
619 sorted.sort_by(|a, b| b.cmp(a));
620 assert_eq!(scores, sorted);
621 }
622
623 #[test]
624 fn top_bounds_results_and_reports_truncation() {
625 let page = view().top(Ranking::FanIn, 1);
626 assert_eq!(page.items.len(), 1);
627 assert!(page.truncated);
628 assert!(page.total > 1);
629 }
630
631 #[test]
632 fn list_filters_by_path_prefix() {
633 let page = view().list(&SymbolFilter::default().path_prefix("crates/b"), 100);
634 assert!(!page.items.is_empty());
635 assert!(page.items.iter().all(|l| l.path == "crates/b/src/lib.rs"));
636 }
637
638 #[test]
639 fn list_filters_by_kind() {
640 let page = view().list(&SymbolFilter::default().kind(SymbolKind::Struct), 100);
641 assert_eq!(page.items.len(), 2);
642 assert!(page.items.iter().all(|l| l.item.kind == SymbolKind::Struct));
643 }
644
645 #[test]
646 fn list_filters_by_name_substring() {
647 let page = view().list(&SymbolFilter::default().name_contains("ru"), 100);
648 assert!(page.items.iter().any(|l| l.item.name == "run"));
649 assert!(page.items.iter().all(|l| l.item.name.contains("ru")));
650 }
651
652 #[test]
653 fn list_combines_filters_conjunctively() {
654 let page = view().list(
655 &SymbolFilter::default()
656 .path_prefix("crates/a")
657 .kind(SymbolKind::Struct),
658 100,
659 );
660 assert_eq!(page.items.len(), 2);
661 assert!(
662 page.items
663 .iter()
664 .all(|l| l.path.starts_with("crates/a") && l.item.kind == SymbolKind::Struct)
665 );
666 }
667
668 #[test]
669 fn list_bounds_results_and_reports_truncation() {
670 let page = view().list(&SymbolFilter::default(), 1);
671 assert_eq!(page.items.len(), 1);
672 assert!(page.truncated);
673 assert_eq!(page.total, view().all_symbols().len());
674 }
675
676 #[test]
677 fn list_without_filters_returns_everything_untruncated() {
678 let page = view().list(&SymbolFilter::default(), 1000);
679 assert!(!page.truncated);
680 assert_eq!(page.items.len(), view().all_symbols().len());
681 }
682
683 fn dead() -> InMemoryGraphStore {
689 let mut s = InMemoryGraphStore::new();
690 s.insert(
691 "src/lib.rs",
692 build_rust(
693 "fn used() {}\n\
694 fn caller() { used(); }\n\
695 fn orphan() {}\n\
696 #[cfg(test)]\n\
697 mod tests {\n \
698 fn t_helper() {}\n \
699 fn t_only() {}\n\
700 }\n",
701 ),
702 );
703 s
704 }
705
706 fn names(page: &Page<Located<Symbol>>) -> Vec<&str> {
707 page.items.iter().map(|l| l.item.name.as_str()).collect()
708 }
709
710 #[test]
711 fn unreferenced_finds_symbols_with_no_inbound_reference() {
712 let page = dead().unreferenced(&SymbolFilter::default(), true, 100);
713 assert!(names(&page).contains(&"orphan"));
714 assert!(!names(&page).contains(&"used"));
715 }
716
717 #[test]
718 fn unreferenced_excludes_mod_tests_members_by_default() {
719 let page = dead().unreferenced(&SymbolFilter::default(), true, 100);
720 assert!(!names(&page).contains(&"t_helper"));
722 assert!(!names(&page).contains(&"t_only"));
723 assert!(!names(&page).contains(&"tests"));
725 }
726
727 #[test]
728 fn unreferenced_keeps_test_members_when_not_excluding() {
729 let page = dead().unreferenced(&SymbolFilter::default(), false, 100);
730 assert!(names(&page).contains(&"t_only"));
731 assert!(names(&page).contains(&"orphan"));
732 }
733
734 #[test]
735 fn unreferenced_excludes_files_under_a_tests_directory() {
736 let mut s = InMemoryGraphStore::new();
737 s.insert(
738 "tests/it.rs",
739 build_rust("fn only_in_integration_test() {}"),
740 );
741 s.insert("src/lib.rs", build_rust("fn orphan() {}"));
742 let page = s.unreferenced(&SymbolFilter::default(), true, 100);
743 assert_eq!(names(&page), vec!["orphan"]);
744 }
745
746 #[test]
747 fn unreferenced_does_not_treat_a_tests_substring_as_a_directory() {
748 let mut s = InMemoryGraphStore::new();
749 s.insert("contests/entry.rs", build_rust("fn orphan() {}"));
751 let page = s.unreferenced(&SymbolFilter::default(), true, 100);
752 assert_eq!(names(&page), vec!["orphan"]);
753 }
754
755 #[test]
756 fn unreferenced_counts_a_reference_from_anywhere_including_tests() {
757 let mut s = InMemoryGraphStore::new();
758 s.insert(
761 "src/lib.rs",
762 build_rust(
763 "fn prod() {}\n\
764 #[cfg(test)]\n\
765 mod tests {\n \
766 fn t() { prod(); }\n\
767 }\n",
768 ),
769 );
770 let page = s.unreferenced(&SymbolFilter::default(), true, 100);
771 assert!(!names(&page).contains(&"prod"));
772 }
773
774 #[test]
775 fn unreferenced_does_not_flag_symbols_called_inside_macros() {
776 let mut s = InMemoryGraphStore::new();
777 s.insert(
782 "src/lib.rs",
783 build_rust("fn f() -> u8 { 0 }\nfn g() { assert_eq!(f(), 0); }\n"),
784 );
785 let page = s.unreferenced(&SymbolFilter::default(), true, 100);
786 assert!(!names(&page).contains(&"f"), "f is called from g");
787 }
788
789 #[test]
790 fn unreferenced_does_not_flag_recursive_functions() {
791 let mut s = InMemoryGraphStore::new();
792 s.insert("src/lib.rs", build_rust("fn recur() { recur(); }"));
795 let page = s.unreferenced(&SymbolFilter::default(), true, 100);
796 assert!(!names(&page).contains(&"recur"));
797 }
798
799 #[test]
800 fn unreferenced_applies_the_symbol_filter() {
801 let page =
802 dead().unreferenced(&SymbolFilter::default().kind(SymbolKind::Struct), true, 100);
803 assert!(page.items.is_empty());
804 }
805
806 #[test]
807 fn unreferenced_results_carry_their_path() {
808 let page = dead().unreferenced(&SymbolFilter::default(), true, 100);
809 assert!(page.items.iter().all(|l| l.path == "src/lib.rs"));
810 }
811
812 #[test]
813 fn unreferenced_bounds_results_and_reports_truncation() {
814 let mut s = InMemoryGraphStore::new();
815 s.insert("src/lib.rs", build_rust("fn a() {} fn b() {} fn c() {}"));
816 let page = s.unreferenced(&SymbolFilter::default(), true, 2);
817 assert_eq!(page.items.len(), 2);
818 assert!(page.truncated);
819 assert_eq!(page.total, 3);
820 }
821
822 #[test]
823 fn unreferenced_is_ordered_by_path_then_line() {
824 let mut s = InMemoryGraphStore::new();
825 s.insert("src/b.rs", build_rust("fn z() {}"));
826 s.insert("src/a.rs", build_rust("fn y() {}\nfn x() {}"));
827 let page = s.unreferenced(&SymbolFilter::default(), true, 100);
828 let seen: Vec<(&str, &str)> = page
829 .items
830 .iter()
831 .map(|l| (l.path.as_str(), l.item.name.as_str()))
832 .collect();
833 assert_eq!(
834 seen,
835 vec![("src/a.rs", "y"), ("src/a.rs", "x"), ("src/b.rs", "z")]
836 );
837 }
838}