1use std::collections::{HashMap, HashSet};
2
3use crate::extract::ExtractionResult;
4use crate::graph::{EdgeKind, Graph, NodeKind};
5
6struct NameEntry {
7 id: String,
8 module: Option<String>,
9 file: String,
10}
11
12struct ResolveContext<'a> {
13 raw_target: &'a str,
14 source_module: Option<&'a str>,
15 source_imports: Option<&'a HashSet<String>>,
16 prefix_hint: Option<&'a str>,
17 source_owner_names: &'a [String],
18 candidate_to_owner_names: &'a HashMap<String, Vec<String>>,
19 candidate_file_stems: &'a HashMap<String, String>,
20}
21
22fn looks_like_file_path(segment: &str) -> bool {
23 segment.contains('/') || segment.ends_with(".rs") || segment.ends_with(".swift")
24}
25
26fn target_segments(target: &str) -> Vec<&str> {
27 let mut colon_parts: Vec<_> = target.split("::").collect();
28 if !colon_parts.is_empty() && looks_like_file_path(colon_parts[0]) {
29 colon_parts.remove(0);
30 }
31
32 if colon_parts.len() > 1 {
33 return colon_parts;
34 }
35
36 let mut segments = Vec::new();
37 if let Some(single_part) = colon_parts.into_iter().next() {
38 segments.extend(single_part.split('.').filter(|segment| !segment.is_empty()));
39 }
40 if segments.is_empty() {
41 segments.push(target);
42 }
43 segments
44}
45
46fn target_symbol_name(target: &str) -> &str {
47 let segments = target_segments(target);
48 segments.last().copied().unwrap_or(target)
49}
50
51fn target_prefix_hint(target: &str) -> Option<String> {
52 let segments = target_segments(target);
53 if segments.len() < 2 {
54 return None;
55 }
56
57 let hint = segments[segments.len() - 2];
58 if matches!(hint, "crate" | "super") {
59 None
60 } else {
61 Some(hint.to_string())
62 }
63}
64
65fn should_enforce_hint(target: &str, hint: &str) -> bool {
66 hint.eq_ignore_ascii_case("self")
67 || target.contains('.')
68 || hint.chars().any(|ch| ch.is_ascii_uppercase())
69}
70
71pub fn merge(results: Vec<ExtractionResult>) -> Graph {
72 let mut graph = Graph::new();
73
74 let mut file_imports: HashMap<String, HashSet<String>> = HashMap::new();
75 for result in &results {
76 for import in &result.imports {
77 if let Some(first_node) = result.nodes.first() {
78 let file_key = first_node.file.to_string_lossy().to_string();
79 let module_name = import
80 .path
81 .strip_prefix("import ")
82 .unwrap_or(&import.path)
83 .to_string();
84 file_imports
85 .entry(file_key)
86 .or_default()
87 .insert(module_name);
88 }
89 }
90 }
91
92 for result in &results {
93 graph.nodes.extend(result.nodes.iter().cloned());
94 }
95
96 let node_ids: HashSet<&str> = graph.nodes.iter().map(|node| node.id.as_str()).collect();
97
98 let mut name_to_entries: HashMap<&str, Vec<NameEntry>> = HashMap::new();
99 for node in &graph.nodes {
100 if matches!(node.kind, NodeKind::View | NodeKind::Branch) {
101 continue;
102 }
103 name_to_entries
104 .entry(node.name.as_str())
105 .or_default()
106 .push(NameEntry {
107 id: node.id.clone(),
108 module: node.module.clone(),
109 file: node.file.to_string_lossy().to_string(),
110 });
111 }
112
113 let id_to_info: HashMap<&str, (Option<&str>, &str)> = graph
114 .nodes
115 .iter()
116 .map(|node| {
117 (
118 node.id.as_str(),
119 (node.module.as_deref(), node.file.to_str().unwrap_or("")),
120 )
121 })
122 .collect();
123
124 let id_to_name: HashMap<&str, &str> = graph
125 .nodes
126 .iter()
127 .map(|node| (node.id.as_str(), node.name.as_str()))
128 .collect();
129 let candidate_file_stems: HashMap<String, String> = graph
130 .nodes
131 .iter()
132 .filter_map(|node| {
133 node.file
134 .file_stem()
135 .and_then(|stem| stem.to_str())
136 .map(|stem| (node.id.clone(), stem.to_ascii_lowercase()))
137 })
138 .collect();
139 let mut candidate_to_owner_names: HashMap<String, Vec<String>> = HashMap::new();
140 for result in &results {
141 for edge in &result.edges {
142 if edge.kind == EdgeKind::Contains
143 && let Some(parent_name) = id_to_name.get(edge.source.as_str())
144 {
145 candidate_to_owner_names
146 .entry(edge.target.clone())
147 .or_default()
148 .push(parent_name.to_string());
149 } else if edge.kind == EdgeKind::Implements
150 && let Some(owner_name) = id_to_name.get(edge.target.as_str())
151 {
152 candidate_to_owner_names
153 .entry(edge.source.clone())
154 .or_default()
155 .push(owner_name.to_string());
156 }
157 }
158 }
159
160 let all_edges: Vec<_> = results
161 .into_iter()
162 .flat_map(|result| result.edges)
163 .collect();
164
165 let mut child_to_parents: HashMap<String, Vec<String>> = HashMap::new();
169 for edge in &all_edges {
170 if edge.kind == EdgeKind::Contains
171 && node_ids.contains(edge.target.as_str())
172 && node_ids.contains(edge.source.as_str())
173 {
174 child_to_parents
175 .entry(edge.target.clone())
176 .or_default()
177 .push(edge.source.clone());
178 }
179 }
180
181 for mut edge in all_edges {
182 let is_external_usr_call = edge.target.starts_with("s:")
183 && edge.kind == EdgeKind::Calls
184 && !node_ids.contains(edge.target.as_str());
185
186 if node_ids.contains(edge.target.as_str())
187 || edge.kind == EdgeKind::Uses
188 || edge.kind == EdgeKind::Implements
189 || (edge.kind == EdgeKind::Calls
190 && (edge.direction.is_some() || edge.operation.is_some()))
191 || is_external_usr_call
192 {
193 graph.edges.push(edge);
194 continue;
195 }
196
197 let target_name = target_symbol_name(&edge.target);
198 let Some(candidates) = name_to_entries.get(target_name) else {
199 continue;
200 };
201 if candidates.is_empty() {
202 continue;
203 }
204
205 let (source_module, source_file) = id_to_info
206 .get(edge.source.as_str())
207 .copied()
208 .unwrap_or((None, ""));
209 let source_imports = file_imports.get(source_file);
210 let source_owner_names = candidate_to_owner_names
211 .get(&edge.source)
212 .cloned()
213 .unwrap_or_default();
214 let owned_hint = target_prefix_hint(&edge.target);
215 let prefix_hint = edge.operation.as_deref().or(owned_hint.as_deref());
216
217 if candidates.len() == 1 {
218 let candidate = &candidates[0];
219 let same_module = modules_match(source_module, candidate.module.as_deref());
220 if same_module {
221 if let Some(hint) = prefix_hint
222 && !candidate_matches_hint(
223 candidate,
224 hint,
225 &source_owner_names,
226 &candidate_to_owner_names,
227 &candidate_file_stems,
228 )
229 && should_enforce_hint(&edge.target, hint)
230 {
231 continue;
232 }
233 edge.target = candidate.id.clone();
234 edge.confidence *= 0.9;
235 graph.edges.push(edge);
236 } else {
237 let imported = source_imports
238 .and_then(|imports| {
239 candidate
240 .module
241 .as_deref()
242 .map(|module| imports.contains(module))
243 })
244 .unwrap_or(false);
245 if imported {
246 edge.target = candidate.id.clone();
247 edge.confidence *= 0.7;
248 graph.edges.push(edge);
249 }
250 }
251 continue;
252 }
253
254 if edge.kind == EdgeKind::Reads && candidates.len() > 1 {
262 let usr_prefix = if edge.source.starts_with("s:") {
264 usr_type_prefix(&edge.source)
265 } else {
266 None
267 };
268
269 if let Some(prefix) = usr_prefix {
270 let siblings: Vec<&NameEntry> = candidates
271 .iter()
272 .filter(|c| c.id.starts_with(&prefix))
273 .collect();
274 if siblings.len() == 1 {
275 edge.target = siblings[0].id.clone();
276 edge.confidence *= 0.9;
277 graph.edges.push(edge);
278 continue;
279 }
280 if !siblings.is_empty() {
281 let same_file: Vec<&&NameEntry> = siblings
283 .iter()
284 .filter(|c| {
285 id_to_info
286 .get(c.id.as_str())
287 .is_some_and(|(_, f)| *f == source_file)
288 })
289 .collect();
290 if same_file.len() == 1 {
291 edge.target = same_file[0].id.clone();
292 edge.confidence *= 0.9;
293 graph.edges.push(edge);
294 continue;
295 }
296 }
297 continue;
301 }
302
303 if let Some(source_owners) = child_to_parents.get(&edge.source) {
305 let sibling_candidates: Vec<&NameEntry> = candidates
306 .iter()
307 .filter(|c| {
308 candidate_to_owner_names.get(&c.id).is_some_and(|owners| {
309 owners.iter().any(|owner| {
310 source_owners.iter().any(|so| {
311 id_to_name.get(so.as_str()).is_some_and(|n| *n == owner)
312 })
313 })
314 })
315 })
316 .collect();
317 if sibling_candidates.len() == 1 {
318 edge.target = sibling_candidates[0].id.clone();
319 edge.confidence *= 0.9;
320 graph.edges.push(edge);
321 continue;
322 }
323 }
324 }
325
326 let resolve_context = ResolveContext {
327 raw_target: &edge.target,
328 source_module,
329 source_imports,
330 prefix_hint,
331 source_owner_names: &source_owner_names,
332 candidate_to_owner_names: &candidate_to_owner_names,
333 candidate_file_stems: &candidate_file_stems,
334 };
335 let resolved = resolve_candidates(candidates, &resolve_context);
336 for (candidate_id, factor) in resolved {
337 let mut resolved_edge = edge.clone();
338 resolved_edge.target = candidate_id;
339 resolved_edge.confidence *= factor;
340 graph.edges.push(resolved_edge);
341 }
342 }
343
344 graph
345}
346
347fn usr_type_prefix(usr: &str) -> Option<String> {
351 let bytes = usr.as_bytes();
355 let mut last_v_pos = None;
356 for i in (2..bytes.len()).rev() {
357 if bytes[i] == b'V'
358 && i + 1 < bytes.len()
359 && (bytes[i + 1].is_ascii_digit() || bytes[i + 1].is_ascii_lowercase())
360 {
361 last_v_pos = Some(i + 1);
362 break;
363 }
364 }
365 last_v_pos.map(|pos| usr[..pos].to_string())
366}
367
368fn candidate_matches_hint(
369 candidate: &NameEntry,
370 hint: &str,
371 source_owner_names: &[String],
372 candidate_to_owner_names: &HashMap<String, Vec<String>>,
373 candidate_file_stems: &HashMap<String, String>,
374) -> bool {
375 let normalized_hint = hint.to_ascii_lowercase();
376
377 if normalized_hint == "self" {
378 return source_owner_names.iter().any(|source_owner| {
379 candidate_to_owner_names
380 .get(&candidate.id)
381 .is_some_and(|owners| {
382 owners
383 .iter()
384 .any(|owner| owner.eq_ignore_ascii_case(source_owner))
385 })
386 });
387 }
388
389 if candidate_to_owner_names
390 .get(&candidate.id)
391 .is_some_and(|owners| {
392 owners.iter().any(|owner| {
393 owner.eq_ignore_ascii_case(hint)
394 || owner
395 .to_ascii_lowercase()
396 .starts_with(normalized_hint.as_str())
397 })
398 })
399 {
400 return true;
401 }
402
403 candidate_file_stems
404 .get(&candidate.id)
405 .is_some_and(|stem| stem == &normalized_hint)
406}
407
408fn resolve_candidates(
409 candidates: &[NameEntry],
410 context: &ResolveContext<'_>,
411) -> Vec<(String, f64)> {
412 let same_module: Vec<&NameEntry> = candidates
413 .iter()
414 .filter(|candidate| modules_match(context.source_module, candidate.module.as_deref()))
415 .collect();
416 if same_module.len() == 1 {
417 if let Some(hint) = context.prefix_hint
418 && !candidate_matches_hint(
419 same_module[0],
420 hint,
421 context.source_owner_names,
422 context.candidate_to_owner_names,
423 context.candidate_file_stems,
424 )
425 && should_enforce_hint(context.raw_target, hint)
426 {
427 return Vec::new();
428 }
429 return vec![(same_module[0].id.clone(), 0.9)];
430 }
431
432 if same_module.len() > 1 {
433 if let Some(hint) = context.prefix_hint {
434 let narrowed: Vec<&&NameEntry> = same_module
435 .iter()
436 .filter(|candidate| {
437 candidate_matches_hint(
438 candidate,
439 hint,
440 context.source_owner_names,
441 context.candidate_to_owner_names,
442 context.candidate_file_stems,
443 )
444 })
445 .collect();
446 if narrowed.len() == 1 {
447 return vec![(narrowed[0].id.clone(), 0.85)];
448 }
449 if narrowed.is_empty() && should_enforce_hint(context.raw_target, hint) {
450 return Vec::new();
451 }
452 }
453
454 let unique_files: HashSet<&str> = same_module.iter().map(|c| c.file.as_str()).collect();
460 if unique_files.len() > 3 {
461 return Vec::new();
462 }
463 return same_module
464 .iter()
465 .map(|candidate| (candidate.id.clone(), 0.4))
466 .collect();
467 }
468
469 if let Some(imports) = context.source_imports {
470 let imported: Vec<&NameEntry> = candidates
471 .iter()
472 .filter(|candidate| {
473 candidate
474 .module
475 .as_deref()
476 .is_some_and(|module| imports.contains(module))
477 })
478 .collect();
479 if imported.len() == 1 {
480 return vec![(imported[0].id.clone(), 0.8)];
481 }
482 if imported.len() > 1 {
483 let unique_files: HashSet<&str> = imported.iter().map(|c| c.file.as_str()).collect();
484 if unique_files.len() > 3 {
485 return Vec::new();
486 }
487 return imported
488 .iter()
489 .map(|candidate| (candidate.id.clone(), 0.3))
490 .collect();
491 }
492 }
493
494 Vec::new()
495}
496
497fn modules_match(left: Option<&str>, right: Option<&str>) -> bool {
498 match (left, right) {
499 (Some(left), Some(right)) => left == right,
500 (None, None) => true,
501 _ => false,
502 }
503}
504
505#[cfg(test)]
506mod tests {
507 use super::*;
508 use crate::graph::*;
509 use std::collections::HashMap;
510 use std::path::PathBuf;
511
512 fn make_node(id: &str, name: &str, kind: NodeKind) -> Node {
513 Node {
514 id: id.to_string(),
515 kind,
516 name: name.to_string(),
517 file: PathBuf::from("test.rs"),
518 span: Span {
519 start: [0, 0],
520 end: [0, 0],
521 },
522 visibility: Visibility::Public,
523 metadata: HashMap::new(),
524 role: None,
525 signature: None,
526 doc_comment: None,
527 module: None,
528 snippet: None,
529 }
530 }
531
532 #[test]
533 fn merges_nodes_from_multiple_results() {
534 let left = ExtractionResult {
535 nodes: vec![make_node("a::Foo", "Foo", NodeKind::Struct)],
536 edges: vec![],
537 imports: vec![],
538 };
539 let right = ExtractionResult {
540 nodes: vec![make_node("b::Bar", "Bar", NodeKind::Struct)],
541 edges: vec![],
542 imports: vec![],
543 };
544
545 let graph = merge(vec![left, right]);
546 assert_eq!(graph.nodes.len(), 2);
547 }
548
549 #[test]
550 fn drops_edges_with_unresolved_targets() {
551 let result = ExtractionResult {
552 nodes: vec![make_node("a::main", "main", NodeKind::Function)],
553 edges: vec![Edge {
554 source: "a::main".to_string(),
555 target: "nonexistent::foo".to_string(),
556 kind: EdgeKind::Calls,
557 confidence: 1.0,
558 direction: None,
559 operation: None,
560 condition: None,
561 async_boundary: None,
562 provenance: Vec::new(),
563 }],
564 imports: vec![],
565 };
566
567 let graph = merge(vec![result]);
568 assert_eq!(graph.edges.len(), 0);
569 }
570
571 #[test]
572 fn keeps_uses_edges_even_if_target_unresolved() {
573 let result = ExtractionResult {
574 nodes: vec![],
575 edges: vec![Edge {
576 source: "a.rs".to_string(),
577 target: "use std::collections::HashMap;".to_string(),
578 kind: EdgeKind::Uses,
579 confidence: 1.0,
580 direction: None,
581 operation: None,
582 condition: None,
583 async_boundary: None,
584 provenance: Vec::new(),
585 }],
586 imports: vec![],
587 };
588
589 let graph = merge(vec![result]);
590 assert_eq!(graph.edges.len(), 1);
591 }
592
593 #[test]
594 fn same_module_candidate_wins() {
595 let mut helper = make_node("mod_a::helper", "helper", NodeKind::Function);
596 helper.module = Some("mod_a".to_string());
597 let mut caller = make_node("mod_a::main", "main", NodeKind::Function);
598 caller.module = Some("mod_a".to_string());
599
600 let graph = merge(vec![
601 ExtractionResult {
602 nodes: vec![caller],
603 edges: vec![Edge {
604 source: "mod_a::main".to_string(),
605 target: "unknown::helper".to_string(),
606 kind: EdgeKind::Calls,
607 confidence: 1.0,
608 direction: None,
609 operation: None,
610 condition: None,
611 async_boundary: None,
612 provenance: Vec::new(),
613 }],
614 imports: vec![],
615 },
616 ExtractionResult {
617 nodes: vec![helper],
618 edges: vec![],
619 imports: vec![],
620 },
621 ]);
622
623 let call_edge = graph
624 .edges
625 .iter()
626 .find(|edge| edge.kind == EdgeKind::Calls)
627 .unwrap();
628 assert_eq!(call_edge.target, "mod_a::helper");
629 assert!((call_edge.confidence - 0.9).abs() < 0.001);
630 }
631
632 #[test]
633 fn owner_hint_disambiguates_same_module_candidates() {
634 let mut room_page_ext = make_node("room::RoomPageExt", "RoomPage", NodeKind::Extension);
635 room_page_ext.module = Some("Room".to_string());
636 let mut kroom_page_ext = make_node("room::KRoomPageExt", "KRoomPage", NodeKind::Extension);
637 kroom_page_ext.module = Some("Room".to_string());
638
639 let mut room_helper = make_node(
640 "room::RoomPage::chatRoomFragViewPanel",
641 "chatRoomFragViewPanel",
642 NodeKind::Property,
643 );
644 room_helper.module = Some("Room".to_string());
645 let mut kroom_helper = make_node(
646 "room::KRoomPage::chatRoomFragViewPanel",
647 "chatRoomFragViewPanel",
648 NodeKind::Property,
649 );
650 kroom_helper.module = Some("Room".to_string());
651
652 let mut body_view = make_node(
653 "room::RoomPage::body::view:chatRoomFragViewPanel",
654 "chatRoomFragViewPanel",
655 NodeKind::View,
656 );
657 body_view.module = Some("Room".to_string());
658
659 let source = body_view.id.clone();
660 let graph = merge(vec![
661 ExtractionResult {
662 nodes: vec![body_view],
663 edges: vec![Edge {
664 source: source.clone(),
665 target: "room::RoomPage.swift::chatRoomFragViewPanel".to_string(),
666 kind: EdgeKind::TypeRef,
667 confidence: 1.0,
668 direction: None,
669 operation: Some("RoomPage".to_string()),
670 condition: None,
671 async_boundary: None,
672 provenance: Vec::new(),
673 }],
674 imports: vec![],
675 },
676 ExtractionResult {
677 nodes: vec![room_page_ext, kroom_page_ext, room_helper, kroom_helper],
678 edges: vec![
679 Edge {
680 source: "room::RoomPage::chatRoomFragViewPanel".to_string(),
681 target: "room::RoomPageExt".to_string(),
682 kind: EdgeKind::Implements,
683 confidence: 1.0,
684 direction: None,
685 operation: None,
686 condition: None,
687 async_boundary: None,
688 provenance: Vec::new(),
689 },
690 Edge {
691 source: "room::KRoomPage::chatRoomFragViewPanel".to_string(),
692 target: "room::KRoomPageExt".to_string(),
693 kind: EdgeKind::Implements,
694 confidence: 1.0,
695 direction: None,
696 operation: None,
697 condition: None,
698 async_boundary: None,
699 provenance: Vec::new(),
700 },
701 ],
702 imports: vec![],
703 },
704 ]);
705
706 let type_refs: Vec<_> = graph
707 .edges
708 .iter()
709 .filter(|edge| edge.source == source && edge.kind == EdgeKind::TypeRef)
710 .collect();
711
712 assert_eq!(type_refs.len(), 1);
713 assert_eq!(type_refs[0].target, "room::RoomPage::chatRoomFragViewPanel");
714 assert!((type_refs[0].confidence - 0.85).abs() < 0.001);
715 }
716
717 #[test]
718 fn qualified_call_hint_drops_false_local_resolution() {
719 let caller = make_node("sqlite.rs::open", "open", NodeKind::Function);
720 let callee = make_node("sqlite.rs::helper", "open", NodeKind::Function);
721
722 let graph = merge(vec![ExtractionResult {
723 nodes: vec![caller, callee],
724 edges: vec![Edge {
725 source: "sqlite.rs::open".to_string(),
726 target: "Connection::open".to_string(),
727 kind: EdgeKind::Calls,
728 confidence: 1.0,
729 direction: None,
730 operation: None,
731 condition: None,
732 async_boundary: None,
733 provenance: Vec::new(),
734 }],
735 imports: vec![],
736 }]);
737
738 assert!(graph.edges.is_empty());
739 }
740
741 #[test]
742 fn module_hint_resolves_call_by_file_stem() {
743 let caller = Node {
744 file: PathBuf::from("lib.rs"),
745 ..make_node("lib.rs::run", "run", NodeKind::Function)
746 };
747 let callee = Node {
748 file: PathBuf::from("utils.rs"),
749 ..make_node("utils.rs::helper", "helper", NodeKind::Function)
750 };
751
752 let graph = merge(vec![ExtractionResult {
753 nodes: vec![caller, callee],
754 edges: vec![Edge {
755 source: "lib.rs::run".to_string(),
756 target: "utils::helper".to_string(),
757 kind: EdgeKind::Calls,
758 confidence: 1.0,
759 direction: None,
760 operation: None,
761 condition: None,
762 async_boundary: None,
763 provenance: Vec::new(),
764 }],
765 imports: vec![],
766 }]);
767
768 assert_eq!(graph.edges.len(), 1);
769 assert_eq!(graph.edges[0].target, "utils.rs::helper");
770 }
771
772 #[test]
773 fn self_field_read_resolves_to_matching_field() {
774 let struct_node = make_node("sqlite.rs::SqliteStore", "SqliteStore", NodeKind::Struct);
775 let field_node = make_node("sqlite.rs::SqliteStore.path", "path", NodeKind::Field);
776 let impl_node = make_node("sqlite.rs::impl_SqliteStore", "SqliteStore", NodeKind::Impl);
777 let fn_node = make_node(
778 "sqlite.rs::impl_SqliteStore::open",
779 "open",
780 NodeKind::Function,
781 );
782
783 let graph = merge(vec![ExtractionResult {
784 nodes: vec![struct_node, field_node, impl_node, fn_node],
785 edges: vec![
786 Edge {
787 source: "sqlite.rs::SqliteStore".to_string(),
788 target: "sqlite.rs::SqliteStore.path".to_string(),
789 kind: EdgeKind::Contains,
790 confidence: 1.0,
791 direction: None,
792 operation: None,
793 condition: None,
794 async_boundary: None,
795 provenance: Vec::new(),
796 },
797 Edge {
798 source: "sqlite.rs::impl_SqliteStore".to_string(),
799 target: "sqlite.rs::impl_SqliteStore::open".to_string(),
800 kind: EdgeKind::Contains,
801 confidence: 1.0,
802 direction: None,
803 operation: None,
804 condition: None,
805 async_boundary: None,
806 provenance: Vec::new(),
807 },
808 Edge {
809 source: "sqlite.rs::impl_SqliteStore::open".to_string(),
810 target: "self.path".to_string(),
811 kind: EdgeKind::Reads,
812 confidence: 0.8,
813 direction: None,
814 operation: None,
815 condition: None,
816 async_boundary: None,
817 provenance: Vec::new(),
818 },
819 ],
820 imports: vec![],
821 }]);
822
823 let read_edge = graph
824 .edges
825 .iter()
826 .find(|edge| edge.kind == EdgeKind::Reads)
827 .unwrap();
828 assert_eq!(read_edge.target, "sqlite.rs::SqliteStore.path");
829 }
830}