1use std::collections::HashMap;
8
9use sinter_core::{
10 Confidence, Edge, Embed, Evidence, FieldBinding, LocalBinding, Node, NodeId, Reference,
11 Relation, SymbolKind, TraitImpl,
12};
13use sinter_extract::{LanguageSpec, ModuleRoot, spec_for_path};
14
15pub struct Binding {
16 pub edge: Edge,
17 pub reference: usize,
19}
20
21#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
22pub struct ResolutionStats {
23 pub scope: usize,
24 pub import: usize,
25 pub scip: usize,
26 pub compiler_rescued_internal: usize,
31 pub unresolved_internal: usize,
36 pub unresolved_external: usize,
40 pub scip_agree: usize,
44 pub scip_disagree: usize,
45 pub scip_external: usize,
50 pub scip_unanchored: usize,
53}
54
55impl ResolutionStats {
56 pub fn resolved(&self) -> usize {
57 self.scope + self.import + self.scip + self.scip_external
58 }
59
60 pub fn unresolved(&self) -> usize {
61 self.unresolved_internal + self.unresolved_external
62 }
63
64 pub fn unresolved_rate(&self) -> f64 {
65 let total = self.resolved() + self.unresolved();
66 if total == 0 {
67 0.0
68 } else {
69 self.unresolved() as f64 / total as f64
70 }
71 }
72
73 pub fn anchored_unresolved_rate(&self) -> Option<f64> {
81 let total =
82 self.scope + self.import + self.compiler_rescued_internal + self.unresolved_internal;
83 if total == 0 {
84 None
85 } else {
86 Some(self.unresolved_internal as f64 / total as f64)
87 }
88 }
89}
90
91enum Res {
93 Bound(Binding),
94 Internal,
95 External,
96}
97
98pub fn qualified_of(id: &str) -> &str {
100 match id.split_once('#') {
101 Some((_, rest)) => rest.rsplit_once('@').map_or(rest, |(q, _)| q),
102 None => id,
103 }
104}
105
106fn is_type_kind(kind: SymbolKind) -> bool {
110 matches!(
111 kind,
112 SymbolKind::Struct
113 | SymbolKind::Enum
114 | SymbolKind::Interface
115 | SymbolKind::Trait
116 | SymbolKind::TypeAlias
117 | SymbolKind::Table
118 | SymbolKind::View
119 )
120}
121
122fn is_member_scope(kind: SymbolKind) -> bool {
126 is_type_kind(kind) || kind == SymbolKind::Class
127}
128
129fn is_callable(kind: SymbolKind) -> bool {
130 matches!(
131 kind,
132 SymbolKind::Function | SymbolKind::Method | SymbolKind::Macro | SymbolKind::Class
133 )
134}
135
136struct ModuleFiles<'a> {
137 key: Vec<String>,
138 files: Vec<&'a str>,
139}
140
141struct LocalRange<'a> {
142 start: u64,
143 scope_end: u64,
144 type_name: Option<&'a str>,
145}
146
147struct Import {
148 segments: Vec<String>,
149 binding: String,
151 glob: bool,
153}
154
155struct FileDef<'a> {
156 node: &'a Node,
157 prefix: &'a str,
159 functionish: bool,
162}
163
164pub struct Index<'a> {
168 by_file_name: HashMap<(&'a str, &'a str), Vec<FileDef<'a>>>,
170 by_file_qualified: HashMap<(&'a str, &'a str), &'a Node>,
172 file_nodes: HashMap<&'a str, &'a Node>,
174 defs_by_file: HashMap<&'a str, Vec<&'a Node>>,
176 by_name: HashMap<&'a str, Vec<(Vec<String>, &'a Node)>>,
178 by_module_tail: HashMap<String, Vec<(Vec<String>, &'a Node)>>,
180 files_of_module: HashMap<String, Vec<ModuleFiles<'a>>>,
183 module_defs: HashMap<Vec<String>, HashMap<&'a str, Vec<&'a Node>>>,
185 imports: HashMap<&'a str, Vec<Import>>,
187 locals: HashMap<(&'a str, &'a str), Vec<LocalRange<'a>>>,
189 fields: HashMap<&'a str, Vec<&'a FieldBinding>>,
191 embeds: HashMap<&'a str, Vec<&'a str>>,
193 roots: Vec<ModuleRoot>,
195 proto_rpcs: crate::proto_service_bindings::ProtoRpcs<'a>,
197}
198
199fn key_of(spec: &LanguageSpec, roots: &[ModuleRoot], file: &str) -> Vec<String> {
205 let Some((manifest, root)) = spec.manifest.zip(root_of(spec, roots, file)) else {
206 return (spec.module_path)(file);
207 };
208 let rel = if root.dir.is_empty() {
209 file
210 } else {
211 &file[root.dir.len() + 1..]
212 };
213 let mut key = (spec.module_path)(rel);
214 let mut name_segments = vec![root.name.clone()];
218 for sep in spec.path_separators {
219 name_segments = name_segments
220 .iter()
221 .flat_map(|s| s.split(sep).map(str::to_string))
222 .collect();
223 }
224 name_segments.retain(|s| !s.is_empty());
225 match key.first() {
226 Some(head) if manifest.self_names.contains(&head.as_str()) => {
227 key.splice(0..1, name_segments);
228 }
229 _ => {
230 key.splice(0..0, name_segments);
231 }
232 }
233 key
234}
235
236fn root_of<'r>(spec: &LanguageSpec, roots: &'r [ModuleRoot], file: &str) -> Option<&'r ModuleRoot> {
238 roots
239 .iter()
240 .filter(|r| r.language == spec.name)
241 .filter(|r| r.dir.is_empty() || file.starts_with(&format!("{}/", r.dir)))
242 .max_by_key(|r| r.dir.len())
243}
244
245fn expand(
248 spec: &LanguageSpec,
249 roots: &[ModuleRoot],
250 file: &str,
251 mut segments: Vec<String>,
252) -> Vec<String> {
253 if let Some(manifest) = spec.manifest
254 && let Some(head) = segments.first()
255 && manifest.self_names.contains(&head.as_str())
256 && let Some(root) = root_of(spec, roots, file)
257 {
258 segments[0] = root.name.clone();
259 }
260 segments
261}
262
263fn module_of(node: &Node, roots: &[ModuleRoot]) -> Vec<String> {
264 let mut module = spec_for_path(&node.file)
265 .map(|s| key_of(s, roots, &node.file))
266 .unwrap_or_default();
267 let qualified = qualified_of(node.id.as_str());
268 if let Some((prefix, _)) = qualified.rsplit_once("::") {
269 module.extend(prefix.split("::").map(str::to_string));
270 }
271 module
272}
273
274struct Prep<'a> {
277 file_module: Vec<String>,
278 qualified: &'a str,
279 prefix: &'a str,
280 functionish: bool,
281 module: Vec<String>,
283}
284
285fn build_index<'a>(
286 nodes: &'a [Node],
287 all_imports: &'a [Reference],
288 locals: &'a [LocalBinding],
289 fields: &'a [FieldBinding],
290 embeds: &'a [Embed],
291 roots: &[ModuleRoot],
292) -> Index<'a> {
293 use rayon::prelude::*;
294 let mut index = Index {
295 by_file_name: HashMap::new(),
296 by_file_qualified: HashMap::new(),
297 file_nodes: HashMap::new(),
298 defs_by_file: HashMap::new(),
299 by_name: HashMap::new(),
300 by_module_tail: HashMap::new(),
301 files_of_module: HashMap::new(),
302 module_defs: HashMap::new(),
303 imports: HashMap::new(),
304 locals: HashMap::new(),
305 fields: HashMap::new(),
306 embeds: HashMap::new(),
307 roots: roots.to_vec(),
308 proto_rpcs: crate::proto_service_bindings::ProtoRpcs::build(nodes),
309 };
310 let mut kind_of: HashMap<(&str, &str), SymbolKind> = HashMap::new();
312 for node in nodes {
313 kind_of.insert(
314 (node.file.as_str(), qualified_of(node.id.as_str())),
315 node.kind,
316 );
317 }
318 let preps: Vec<Option<Prep<'a>>> = nodes
324 .par_iter()
325 .map(|node| {
326 let spec = spec_for_path(&node.file)?;
327 let file_module = key_of(spec, roots, &node.file);
328 if node.kind == SymbolKind::File {
329 return Some(Prep {
330 file_module,
331 qualified: "",
332 prefix: "",
333 functionish: false,
334 module: Vec::new(),
335 });
336 }
337 let qualified = qualified_of(node.id.as_str());
338 let prefix = qualified.rsplit_once("::").map_or("", |(p, _)| p);
339 let functionish =
340 prefix
341 .split("::")
342 .filter(|s| !s.is_empty())
343 .try_fold(String::new(), |acc, seg| {
344 let q = if acc.is_empty() {
345 seg.to_string()
346 } else {
347 format!("{acc}::{seg}")
348 };
349 let kind = kind_of.get(&(node.file.as_str(), q.as_str()));
350 match kind {
351 Some(k) if is_callable(*k) && *k != SymbolKind::Class => Some(q),
352 None => None, Some(_) => None,
354 }
355 });
356 let mut module = file_module.clone();
357 if !prefix.is_empty() {
358 module.extend(prefix.split("::").map(str::to_string));
359 }
360 Some(Prep {
361 file_module,
362 qualified,
363 prefix,
364 functionish: prefix.is_empty() || functionish.is_some(),
365 module,
366 })
367 })
368 .collect();
369 for (node, prep) in nodes.iter().zip(preps) {
371 let Some(prep) = prep else {
372 continue;
373 };
374 let file_module = prep.file_module;
375 if node.kind == SymbolKind::File {
376 index.file_nodes.insert(node.file.as_str(), node);
377 if let Some(tail) = file_module.last() {
378 index
379 .by_module_tail
380 .entry(tail.clone())
381 .or_default()
382 .push((file_module.clone(), node));
383 let entries = index.files_of_module.entry(tail.clone()).or_default();
384 match entries.iter_mut().find(|m| m.key == file_module) {
385 Some(m) => m.files.push(&node.file),
386 None => entries.push(ModuleFiles {
387 key: file_module,
388 files: vec![&node.file],
389 }),
390 }
391 }
392 continue;
393 }
394 index
395 .by_file_qualified
396 .insert((node.file.as_str(), prep.qualified), node);
397 index
398 .defs_by_file
399 .entry(node.file.as_str())
400 .or_default()
401 .push(node);
402 index
403 .by_file_name
404 .entry((node.file.as_str(), node.name.as_str()))
405 .or_default()
406 .push(FileDef {
407 node,
408 prefix: prep.prefix,
409 functionish: prep.functionish,
410 });
411 index
412 .by_name
413 .entry(node.name.as_str())
414 .or_default()
415 .push((prep.module, node));
416 if prep.prefix.is_empty() {
417 index
418 .module_defs
419 .entry(file_module)
420 .or_default()
421 .entry(node.name.as_str())
422 .or_default()
423 .push(node);
424 }
425 }
426 for r in all_imports {
427 let Some(spec) = spec_for_path(&r.file) else {
428 continue;
429 };
430 let glob = matches!(r.alias.as_deref(), Some("*") | Some("."));
431 let raw = strip_glob(&r.name);
432 let segments = expand(spec, roots, &r.file, (spec.absolutize)(raw, &r.file));
433 let binding = match (&r.alias, glob) {
434 (Some(alias), false) => alias.clone(),
435 _ => segments.last().cloned().unwrap_or_default(),
436 };
437 index
438 .imports
439 .entry(r.file.as_str())
440 .or_default()
441 .push(Import {
442 segments,
443 binding,
444 glob,
445 });
446 }
447 for l in locals {
448 index
449 .locals
450 .entry((l.file.as_str(), l.name.as_str()))
451 .or_default()
452 .push(LocalRange {
453 start: l.span.start,
454 scope_end: l.scope_end,
455 type_name: l.type_name.as_deref(),
456 });
457 }
458 for field in fields {
459 index
460 .fields
461 .entry(field.owner.as_str())
462 .or_default()
463 .push(field);
464 }
465 for e in embeds {
466 index
467 .embeds
468 .entry(e.owner.as_str())
469 .or_default()
470 .push(&e.type_name);
471 }
472 index
473}
474
475fn strip_glob(name: &str) -> &str {
476 name.strip_suffix('*')
477 .map(|s| s.trim_end_matches(['.', ':', '/']))
478 .unwrap_or(name)
479}
480
481impl<'a> Index<'a> {
482 pub fn build(
485 nodes: &'a [Node],
486 all_imports: &'a [Reference],
487 locals: &'a [LocalBinding],
488 fields: &'a [FieldBinding],
489 embeds: &'a [Embed],
490 roots: &[ModuleRoot],
491 ) -> Index<'a> {
492 let t = std::time::Instant::now();
493 let index = build_index(nodes, all_imports, locals, fields, embeds, roots);
494 if std::env::var_os("SINTER_TIMING").is_some() {
495 eprintln!("index build: {:?}", t.elapsed());
496 }
497 index
498 }
499
500 fn local_at(&self, file: &str, name: &str, at: u64) -> Option<Option<&'a str>> {
502 self.locals
503 .get(&(file, name))
504 .into_iter()
505 .flatten()
506 .filter(|l| l.start <= at && at < l.scope_end)
507 .map(|l| l.type_name)
508 .next_back()
509 }
510
511 fn type_def(&self, file: &str, module: &[String], name: &str) -> Option<&'a Node> {
513 let same_file: Vec<&Node> = self
514 .by_file_name
515 .get(&(file, name))
516 .into_iter()
517 .flatten()
518 .filter(|d| is_member_scope(d.node.kind))
519 .map(|d| d.node)
520 .collect();
521 if let [node] = same_file.as_slice() {
522 return Some(node);
523 }
524 let in_module: Vec<&Node> = self
525 .module_defs
526 .get(module)
527 .and_then(|m| m.get(name))
528 .into_iter()
529 .flatten()
530 .filter(|n| is_member_scope(n.kind))
531 .copied()
532 .collect();
533 match in_module.as_slice() {
534 [node] => Some(node),
535 _ => None,
536 }
537 }
538
539 fn visible_types(&self, file: &str, module: &[String], written: &str) -> Vec<&'a Node> {
544 let mut found = Vec::new();
545 for candidate in type_candidates(written) {
546 if let Some(node) = self.type_def(file, module, candidate) {
547 found.push(node);
548 continue;
549 }
550 let imported: Vec<&Node> = self
551 .imports
552 .get(file)
553 .into_iter()
554 .flatten()
555 .filter(|imp| !imp.glob && imp.binding == candidate)
556 .filter_map(|imp| self.resolve_path_defs(&imp.segments, 4))
557 .filter(|n| is_member_scope(n.kind))
558 .collect();
559 if let [node] = imported.as_slice() {
560 found.push(*node);
561 }
562 }
563 found.sort_by_key(|node| node.id.as_str());
564 found.dedup_by_key(|node| node.id.as_str());
565 found
566 }
567
568 fn member_of_written_type(
572 &self,
573 file: &str,
574 module: &[String],
575 written: &str,
576 member: &str,
577 ) -> (Option<&'a Node>, bool) {
578 let types = self.visible_types(file, module, written);
579 let mut members: Vec<&Node> = types
580 .iter()
581 .filter_map(|ty| self.member_of(ty, member, 4))
582 .collect();
583 members.sort_by_key(|node| node.id.as_str());
584 members.dedup_by_key(|node| node.id.as_str());
585 let target = match members.as_slice() {
586 [member] => Some(*member),
587 _ => None,
588 };
589 (target, !types.is_empty())
590 }
591
592 fn field(&self, owner: &Node, name: &str) -> Option<&'a FieldBinding> {
593 let matching: Vec<&FieldBinding> = self
594 .fields
595 .get(owner.id.as_str())
596 .into_iter()
597 .flatten()
598 .filter(|f| f.name == name)
599 .copied()
600 .collect();
601 match matching.as_slice() {
602 [field] => Some(*field),
603 _ => None,
604 }
605 }
606
607 fn member_of(&self, ty: &'a Node, name: &str, depth: usize) -> Option<&'a Node> {
609 if depth == 0 {
610 return None;
611 }
612 let mut module = module_of(ty, &self.roots);
613 module.extend(
614 qualified_of(ty.id.as_str())
615 .rsplit("::")
616 .next()
617 .map(str::to_string),
618 );
619 let direct: Vec<&Node> = self
620 .by_name
621 .get(name)
622 .into_iter()
623 .flatten()
624 .filter(|(m, _)| *m == module)
625 .map(|(_, n)| *n)
626 .collect();
627 if let [node] = direct.as_slice() {
628 return Some(node);
629 }
630 let in_type_file: Vec<&Node> = direct
634 .iter()
635 .filter(|n| n.file == ty.file)
636 .copied()
637 .collect();
638 if let [node] = in_type_file.as_slice() {
639 return Some(node);
640 }
641 let spec = spec_for_path(&ty.file)?;
642 let file_module = key_of(spec, &self.roots, &ty.file);
643 for embedded in self.embeds.get(ty.id.as_str()).into_iter().flatten() {
644 if let Some(embedded_ty) = self.type_def(&ty.file, &file_module, embedded)
645 && let Some(node) = self.member_of(embedded_ty, name, depth - 1)
646 {
647 return Some(node);
648 }
649 }
650 None
651 }
652
653 fn anchored(&self, segments: &[String]) -> bool {
656 let module_hit = |segs: &[String]| {
657 segs.last().is_some_and(|tail| {
658 self.files_of_module
659 .get(tail.as_str())
660 .into_iter()
661 .flatten()
662 .any(|m| suffix_len(&m.key, segs).is_some())
663 || self
664 .by_module_tail
665 .get(tail.as_str())
666 .into_iter()
667 .flatten()
668 .any(|(key, _)| suffix_len(key, segs).is_some())
669 })
670 };
671 if module_hit(segments) {
672 return true;
673 }
674 match segments.split_last() {
675 Some((_, module)) if !module.is_empty() => module_hit(module),
676 _ => false,
677 }
678 }
679
680 fn import_file(&self, segments: &[String]) -> Option<&'a Node> {
687 unique_best(
688 self.by_module_tail
689 .get(segments.last()?.as_str())
690 .into_iter()
691 .flatten()
692 .filter_map(|(key, node)| {
693 let len = suffix_len(key, segments).or_else(|| suffix_len(segments, key))?;
694 Some((len, *node))
695 }),
696 )
697 }
698
699 fn resolve_path(&self, segments: &[String], depth: usize) -> Option<&'a Node> {
702 self.resolve_path_defs(segments, depth).or_else(|| {
703 let files = self
707 .by_module_tail
708 .get(segments.last()?.as_str())
709 .into_iter()
710 .flatten()
711 .filter_map(|(key, node)| Some((suffix_len(key, segments)?, *node)));
712 unique_best(files)
713 })
714 }
715
716 fn resolve_path_defs(&self, segments: &[String], depth: usize) -> Option<&'a Node> {
722 if segments.is_empty() || depth == 0 {
723 return None;
724 }
725 if let Some((name, module)) = segments.split_last() {
726 let defs = self
727 .by_name
728 .get(name.as_str())
729 .into_iter()
730 .flatten()
731 .filter_map(|(key, node)| Some((suffix_len(key, module)?, *node)));
732 if let Some(node) = unique_best(defs) {
733 return Some(node);
734 }
735 if !module.is_empty() {
738 let mut chained: Vec<&Node> = Vec::new();
739 let tail = module.last().map(String::as_str).unwrap_or("");
740 for m in self.files_of_module.get(tail).into_iter().flatten() {
741 if suffix_len(&m.key, module).is_none() {
742 continue;
743 }
744 for file in &m.files {
745 for import in self.imports.get(*file).into_iter().flatten() {
746 if import.binding == *name && !import.glob {
747 chained.extend(self.resolve_path(&import.segments, depth - 1));
748 } else if import.glob {
749 let mut deeper = import.segments.clone();
750 deeper.push(name.clone());
751 chained.extend(self.resolve_path(&deeper, depth - 1));
752 }
753 }
754 }
755 }
756 chained.sort_by_key(|n| n.id.as_str().to_string());
757 chained.dedup_by_key(|n| n.id.as_str().to_string());
758 if let [node] = chained.as_slice() {
759 return Some(node);
760 }
761 }
762 }
763 None
764 }
765}
766
767const TYPE_KEYWORDS: &[&str] = &[
772 "dyn", "impl", "mut", "const", "ref", "crate", "self", "super", "std", "core", "alloc",
773];
774
775fn type_tokens(text: &str) -> impl DoubleEndedIterator<Item = &str> {
776 text.split(|c: char| !(c.is_alphanumeric() || c == '_'))
777 .filter(|token| {
778 !token.is_empty()
779 && !token.chars().next().is_some_and(char::is_numeric)
780 && !TYPE_KEYWORDS.contains(token)
781 })
782}
783
784fn type_candidates(written: &str) -> Vec<&str> {
785 const DEREF_WRAPPERS: &[&str] = &["Box", "Arc", "Rc", "Pin", "Cow"];
789 let (head_text, arguments) = written
790 .split_once('<')
791 .map_or((written, None), |(head, rest)| (head, Some(rest)));
792 let head = type_tokens(head_text).next_back();
793 if let Some(head) = head
794 && !DEREF_WRAPPERS.contains(&head)
795 {
796 return vec![head];
797 }
798 let mut out = Vec::new();
799 for token in type_tokens(arguments.unwrap_or(written)).rev() {
800 if DEREF_WRAPPERS.contains(&token) {
801 continue;
802 }
803 if !out.contains(&token) {
804 out.push(token);
805 }
806 }
807 out
808}
809
810fn namespace_pick(candidates: Vec<&Node>, relation: Relation) -> Option<&Node> {
813 match candidates.as_slice() {
814 [node] => Some(node),
815 [] => None,
816 _ => {
817 let preferred: Vec<&Node> = candidates
818 .iter()
819 .filter(|n| match relation {
820 Relation::Calls => is_callable(n.kind),
821 Relation::Uses => is_type_kind(n.kind),
822 Relation::Reads | Relation::Writes => {
823 matches!(n.kind, SymbolKind::Table | SymbolKind::View)
824 }
825 Relation::Creates | Relation::Alters | Relation::Drops => matches!(
826 n.kind,
827 SymbolKind::Table | SymbolKind::View | SymbolKind::Index
828 ),
829 _ => true,
830 })
831 .copied()
832 .collect();
833 match preferred.as_slice() {
834 [node] => Some(node),
835 _ => None,
836 }
837 }
838 }
839}
840
841pub fn resolve(
842 index: &Index<'_>,
843 references: &[Reference],
844) -> (Vec<Binding>, ResolutionStats, Vec<usize>) {
845 use rayon::prelude::*;
846 let results: Vec<Res> = references
847 .par_iter()
848 .enumerate()
849 .map(|(i, r)| {
850 let Some(spec) = spec_for_path(&r.file) else {
851 return Res::External;
852 };
853 let src = r
854 .enclosing
855 .clone()
856 .unwrap_or_else(|| NodeId::new(r.file.clone()));
857 let file_module = key_of(spec, &index.roots, &r.file);
858 let imports = index.imports.get(r.file.as_str());
859 let (target, evidence, internal) = resolve_one(index, spec, r, &file_module, imports);
860 match target {
861 Some(node) if node.id != src => {
862 let relation = if r.relation == Relation::Calls && is_type_kind(node.kind) {
865 Relation::Uses
866 } else {
867 r.relation
868 };
869 Res::Bound(Binding {
870 edge: Edge {
871 src,
872 dst: node.id.clone(),
873 relation,
874 evidence,
875 confidence: if evidence == Evidence::Declared {
878 Confidence::Inferred
879 } else {
880 evidence.confidence()
881 },
882 site: Some(r.span),
883 },
884 reference: i,
885 })
886 }
887 _ if internal => Res::Internal,
888 _ => Res::External,
889 }
890 })
891 .collect();
892 let mut bindings = Vec::new();
893 let mut stats = ResolutionStats::default();
894 let mut internal_indices = Vec::new();
895 for (i, result) in results.into_iter().enumerate() {
896 match result {
897 Res::Bound(binding) => {
898 match binding.edge.evidence {
899 Evidence::Scope => stats.scope += 1,
900 _ => stats.import += 1,
901 }
902 bindings.push(binding);
903 }
904 Res::Internal => {
905 stats.unresolved_internal += 1;
906 internal_indices.push(i);
907 }
908 Res::External => stats.unresolved_external += 1,
909 }
910 }
911 (bindings, stats, internal_indices)
912}
913
914fn resolve_one<'a>(
915 index: &Index<'a>,
916 spec: &sinter_extract::LanguageSpec,
917 r: &Reference,
918 file_module: &[String],
919 imports: Option<&Vec<Import>>,
920) -> (Option<&'a Node>, Evidence, bool) {
921 if r.relation == Relation::Imports {
922 return resolve_import_reference(index, spec, r);
923 }
924
925 if let Some(path) = &r.path {
926 let (target, evidence, internal) =
927 resolve_qualified_reference(index, spec, r, file_module, imports, path);
928 if target.is_none()
929 && r.relation == Relation::Calls
930 && !index.proto_rpcs.is_empty()
931 && let Some(rpc) = proto_client_call(index, spec, r, file_module, imports, path)
932 {
933 return (Some(rpc), Evidence::Declared, true);
934 }
935 return (target, evidence, internal);
936 }
937
938 resolve_bare_reference(index, spec, r, file_module, imports)
939}
940
941fn proto_client_call<'a>(
946 index: &Index<'a>,
947 spec: &LanguageSpec,
948 r: &Reference,
949 file_module: &[String],
950 imports: Option<&Vec<Import>>,
951 path: &str,
952) -> Option<&'a Node> {
953 let segments = expand(
954 spec,
955 &index.roots,
956 &r.file,
957 (spec.absolutize)(path, &r.file),
958 );
959 let prefix = segments.get(segments.len().checked_sub(2)?)?;
960 let field_type = || {
961 let enclosing = r.enclosing.as_ref()?;
962 let (type_prefix, _) = qualified_of(enclosing.as_str()).rsplit_once("::")?;
963 let name = type_prefix.rsplit("::").next().unwrap_or(type_prefix);
964 let owner = index
965 .by_file_qualified
966 .get(&(r.file.as_str(), type_prefix))
967 .copied()
968 .or_else(|| index.type_def(&r.file, file_module, name))?;
969 Some(index.field(owner, prefix)?.type_name.as_str())
970 };
971 let receiver_type = if segments.len() >= 3
972 && spec
973 .receivers
974 .contains(&segments[segments.len() - 3].as_str())
975 {
976 field_type()
977 } else {
978 index.local_at(&r.file, prefix, r.span.start).flatten()
979 };
980 let tokens = imports.into_iter().flatten().flat_map(|imp| {
981 imp.segments
982 .iter()
983 .map(String::as_str)
984 .chain([imp.binding.as_str()])
985 });
986 index.proto_rpcs.client_call(&r.name, receiver_type, tokens)
987}
988
989fn resolve_import_reference<'a>(
992 index: &Index<'a>,
993 spec: &LanguageSpec,
994 r: &Reference,
995) -> (Option<&'a Node>, Evidence, bool) {
996 let glob = matches!(r.alias.as_deref(), Some("*") | Some("."));
997 let raw = strip_glob(&r.name);
998 if let Some(node) = index
1002 .file_nodes
1003 .get(raw.trim().trim_matches(['<', '>', '"']))
1004 {
1005 return (Some(node), Evidence::Import, true);
1006 }
1007 let segments = expand(spec, &index.roots, &r.file, (spec.absolutize)(raw, &r.file));
1008 let target = if glob {
1009 index.import_file(&segments)
1010 } else {
1011 index.resolve_path(&segments, 4)
1012 };
1013 let internal = target.is_some() || index.anchored(&segments);
1014 (target, Evidence::Import, internal)
1015}
1016
1017fn resolve_qualified_reference<'a>(
1020 index: &Index<'a>,
1021 spec: &LanguageSpec,
1022 r: &Reference,
1023 file_module: &[String],
1024 imports: Option<&Vec<Import>>,
1025 path: &str,
1026) -> (Option<&'a Node>, Evidence, bool) {
1027 if spec.file_refs {
1030 return resolve_file_ref(index, spec, r, path);
1031 }
1032 let segments = expand(
1035 spec,
1036 &index.roots,
1037 &r.file,
1038 (spec.absolutize)(path, &r.file),
1039 );
1040 let prefix = segments
1041 .len()
1042 .checked_sub(2)
1043 .and_then(|p| segments.get(p))
1044 .cloned();
1045 let Some(prefix) = prefix else {
1046 return (None, Evidence::Import, false);
1047 };
1048 if segments.len() >= 3
1052 && spec
1053 .receivers
1054 .contains(&segments[segments.len() - 3].as_str())
1055 && let Some(enclosing) = &r.enclosing
1056 && let Some((type_prefix, _)) = qualified_of(enclosing.as_str()).rsplit_once("::")
1057 {
1058 let owner = index
1059 .by_file_qualified
1060 .get(&(r.file.as_str(), type_prefix))
1061 .copied()
1062 .or_else(|| {
1063 let name = type_prefix.rsplit("::").next().unwrap_or(type_prefix);
1064 index.type_def(&r.file, file_module, name)
1065 });
1066 if let Some(owner) = owner
1067 && let Some(field) = index.field(owner, &segments[segments.len() - 2])
1068 {
1069 let field_spec = spec_for_path(&owner.file).unwrap_or(spec);
1070 let field_module = key_of(field_spec, &index.roots, &owner.file);
1071 let (target, anchored) =
1072 index.member_of_written_type(&owner.file, &field_module, &field.type_name, &r.name);
1073 return (target, Evidence::Scope, anchored);
1074 }
1075 }
1076 if spec.receivers.contains(&prefix.as_str())
1077 && let Some(enclosing) = &r.enclosing
1078 && let Some((type_prefix, _)) = qualified_of(enclosing.as_str()).rsplit_once("::")
1079 {
1080 let sibling = format!("{type_prefix}::{}", r.name);
1084 if let Some(node) = index
1085 .by_file_qualified
1086 .get(&(r.file.as_str(), sibling.as_str()))
1087 {
1088 return (Some(node), Evidence::Scope, true);
1089 }
1090 if let Some(ty) = index.by_file_qualified.get(&(r.file.as_str(), type_prefix)) {
1091 return (index.member_of(ty, &r.name, 4), Evidence::Scope, true);
1093 }
1094 }
1095 match index.local_at(&r.file, &prefix, r.span.start) {
1096 Some(Some(type_name)) => {
1097 let (target, anchored) =
1098 index.member_of_written_type(&r.file, file_module, type_name, &r.name);
1099 return (target, Evidence::Scope, anchored);
1101 }
1102 Some(None) => return (None, Evidence::Scope, false), None => {}
1104 }
1105 if let Some(ty) = index.type_def(&r.file, file_module, &prefix)
1107 && let Some(node) = index.member_of(ty, &r.name, 4)
1108 {
1109 return (Some(node), Evidence::Scope, true);
1110 }
1111 if let Some(node) = index.resolve_path_defs(&segments, 4) {
1112 return (Some(node), Evidence::Import, true);
1113 }
1114 if let Some((leaf, type_path)) = segments.split_last()
1120 && type_path.len() >= 2
1121 && let Some(ty) = index.resolve_path_defs(type_path, 4)
1122 && let Some(node) = index.member_of(ty, leaf, 4)
1123 {
1124 return (Some(node), Evidence::Import, true);
1125 }
1126 let matching: Vec<&Import> = imports
1127 .into_iter()
1128 .flatten()
1129 .filter(|imp| !imp.glob && imp.binding == prefix)
1130 .collect();
1131 let candidates: Vec<&Node> = matching
1132 .iter()
1133 .filter_map(|imp| {
1134 let mut full = imp.segments.clone();
1135 full.push(r.name.clone());
1136 index.resolve_path(&full, 4)
1137 })
1138 .collect();
1139 let internal = candidates.len() > 1
1140 || index.anchored(&segments)
1141 || matching.iter().any(|imp| index.anchored(&imp.segments));
1142 match candidates.as_slice() {
1143 [node] => (Some(node), Evidence::Import, true),
1144 _ => (None, Evidence::Import, internal),
1145 }
1146}
1147
1148fn resolve_bare_reference<'a>(
1151 index: &Index<'a>,
1152 spec: &LanguageSpec,
1153 r: &Reference,
1154 file_module: &[String],
1155 imports: Option<&Vec<Import>>,
1156) -> (Option<&'a Node>, Evidence, bool) {
1157 if index.local_at(&r.file, &r.name, r.span.start).is_some() {
1158 return (None, Evidence::Scope, false); }
1160 let enclosing_q = r
1161 .enclosing
1162 .as_ref()
1163 .map(|e| qualified_of(e.as_str()))
1164 .unwrap_or("");
1165 let visible: Vec<&Node> = index
1166 .by_file_name
1167 .get(&(r.file.as_str(), r.name.as_str()))
1168 .into_iter()
1169 .flatten()
1170 .filter(|d| {
1171 d.prefix.is_empty()
1172 || (d.functionish
1173 && (enclosing_q == d.prefix
1174 || enclosing_q.starts_with(&format!("{}::", d.prefix))))
1175 })
1176 .map(|d| d.node)
1177 .collect();
1178 if !visible.is_empty() {
1179 return (namespace_pick(visible, r.relation), Evidence::Scope, true);
1181 }
1182 if let Some(defs) = index
1183 .module_defs
1184 .get(file_module)
1185 .and_then(|m| m.get(r.name.as_str()))
1186 {
1187 return (
1188 namespace_pick(defs.clone(), r.relation),
1189 Evidence::Scope,
1190 true,
1191 );
1192 }
1193 let named: Vec<&Node> = imports
1194 .into_iter()
1195 .flatten()
1196 .filter(|imp| !imp.glob && imp.binding == r.name)
1197 .filter_map(|imp| index.resolve_path(&imp.segments, 4))
1198 .collect();
1199 let (target, internal) = match named.as_slice() {
1200 [node] => (Some(*node), true),
1201 [] => {
1202 let globbed: Vec<&Node> = imports
1203 .into_iter()
1204 .flatten()
1205 .filter(|imp| imp.glob)
1206 .filter_map(|imp| {
1207 let mut full = imp.segments.clone();
1208 full.push(r.name.clone());
1209 index.resolve_path(&full, 4).or_else(|| {
1210 let file = index.import_file(&imp.segments)?;
1213 index
1214 .by_file_name
1215 .get(&(file.file.as_str(), r.name.as_str()))
1216 .into_iter()
1217 .flatten()
1218 .find(|d| d.prefix.is_empty())
1219 .map(|d| d.node)
1220 })
1221 })
1222 .collect();
1223 let name_imports_anchored = imports
1224 .into_iter()
1225 .flatten()
1226 .filter(|imp| !imp.glob && imp.binding == r.name)
1227 .any(|imp| index.anchored(&imp.segments));
1228 match globbed.as_slice() {
1229 [node] => (Some(*node), true),
1230 [] => (None, name_imports_anchored),
1231 _ => (None, true), }
1233 }
1234 _ => (None, true), };
1236 if target.is_none() && (spec.name == "sql" || is_data_relation(r.relation)) {
1237 return sql_repo_fallback(index, r);
1242 }
1243 (target, Evidence::Import, internal)
1244}
1245
1246fn is_data_relation(relation: Relation) -> bool {
1249 matches!(
1250 relation,
1251 Relation::Reads | Relation::Writes | Relation::Creates | Relation::Alters | Relation::Drops
1252 )
1253}
1254
1255fn sql_repo_fallback<'a>(index: &Index<'a>, r: &Reference) -> (Option<&'a Node>, Evidence, bool) {
1260 let candidates: Vec<&Node> = index
1261 .by_name
1262 .get(r.name.as_str())
1263 .into_iter()
1264 .flatten()
1265 .map(|(_, node)| *node)
1266 .filter(|n| matches!(n.kind, SymbolKind::Table | SymbolKind::View))
1267 .collect();
1268 match candidates.as_slice() {
1269 [node] => (Some(node), Evidence::Scope, true),
1270 [] => (None, Evidence::Scope, false),
1271 _ => (None, Evidence::Scope, true), }
1273}
1274
1275fn resolve_file_ref<'a>(
1283 index: &Index<'a>,
1284 spec: &LanguageSpec,
1285 r: &Reference,
1286 path: &str,
1287) -> (Option<&'a Node>, Evidence, bool) {
1288 let (head, frag) = match path.split_once('#') {
1289 Some((h, f)) => (h, Some(f)),
1290 None => (path, None),
1291 };
1292 let file = if head.is_empty() {
1293 index.file_nodes.get(r.file.as_str()).copied()
1295 } else {
1296 let joined = (spec.absolutize)(head, &r.file).join("/");
1297 index.file_nodes.get(joined.as_str()).copied().or_else(|| {
1298 spec.extensions.iter().find_map(|ext| {
1299 index
1300 .file_nodes
1301 .get(format!("{joined}.{ext}").as_str())
1302 .copied()
1303 })
1304 })
1305 };
1306 match (file, frag) {
1307 (Some(file), None) => (Some(file), Evidence::Import, true),
1308 (Some(file), Some(frag)) => {
1309 let matching: Vec<&Node> = index
1310 .defs_by_file
1311 .get(file.file.as_str())
1312 .into_iter()
1313 .flatten()
1314 .filter(|n| slugify(&n.name) == frag)
1315 .copied()
1316 .collect();
1317 match matching.as_slice() {
1320 [node] => (Some(node), Evidence::Import, true),
1321 _ => (None, Evidence::Import, true),
1322 }
1323 }
1324 (None, _) => (None, Evidence::Import, false),
1325 }
1326}
1327
1328fn slugify(name: &str) -> String {
1331 name.chars()
1332 .filter_map(|c| match c {
1333 ' ' => Some('-'),
1334 '-' | '_' => Some(c),
1335 c if c.is_alphanumeric() => Some(c.to_ascii_lowercase()),
1336 _ => None,
1337 })
1338 .collect()
1339}
1340
1341pub fn dynamic_edges(index: &Index<'_>, nodes: &[Node], trait_impls: &[TraitImpl]) -> Vec<Edge> {
1349 let mut edges = crate::proto_service_bindings::proto_service_edges(nodes, trait_impls);
1352 let implicit = nodes
1353 .iter()
1354 .any(|n| spec_for_path(&n.file).is_some_and(|s| s.implicit_interfaces));
1355 if trait_impls.is_empty() && !implicit {
1356 return edges;
1357 }
1358 let roots = &index.roots;
1359 let mut by_file: HashMap<&str, Vec<&Node>> = HashMap::new();
1360 let mut types_by_file: HashMap<&str, Vec<&Node>> = HashMap::new();
1361 for n in nodes {
1362 if is_callable(n.kind) {
1363 by_file.entry(n.file.as_str()).or_default().push(n);
1364 }
1365 if is_member_scope(n.kind) {
1366 types_by_file.entry(n.file.as_str()).or_default().push(n);
1367 }
1368 }
1369 let is_trait = |n: &Node| {
1373 matches!(
1374 n.kind,
1375 SymbolKind::Trait | SymbolKind::Interface | SymbolKind::Class
1376 )
1377 };
1378 for ti in trait_impls {
1379 let Some(spec) = spec_for_path(&ti.file) else {
1380 continue;
1381 };
1382 let file_module = key_of(spec, roots, &ti.file);
1383 let trait_node = index
1384 .type_def(&ti.file, &file_module, &ti.trait_name)
1385 .filter(|n| is_trait(n))
1386 .map(|n| (n, Evidence::Scope))
1387 .or_else(|| {
1388 let named: Vec<&Node> = index
1390 .imports
1391 .get(ti.file.as_str())
1392 .into_iter()
1393 .flatten()
1394 .filter(|imp| !imp.glob && imp.binding == ti.trait_name)
1395 .filter_map(|imp| index.resolve_path_defs(&imp.segments, 4))
1396 .filter(|n| is_trait(n))
1397 .collect();
1398 match named.as_slice() {
1399 [node] => Some((node, Evidence::Import)),
1400 _ => None,
1401 }
1402 })
1403 .or_else(|| {
1404 let globbed: Vec<&Node> = index
1407 .imports
1408 .get(ti.file.as_str())
1409 .into_iter()
1410 .flatten()
1411 .filter(|imp| imp.glob)
1412 .filter_map(|imp| {
1413 let mut full = imp.segments.clone();
1414 full.push(ti.trait_name.clone());
1415 index.resolve_path_defs(&full, 4)
1416 })
1417 .filter(|n| is_trait(n))
1418 .collect();
1419 match globbed.as_slice() {
1420 [node] => Some((node, Evidence::Import)),
1421 _ => None,
1422 }
1423 });
1424 let Some((trait_node, pair_evidence)) = trait_node else {
1425 continue; };
1427 let mut impl_methods: Vec<&Node> = Vec::new();
1428 for method in by_file.get(ti.file.as_str()).into_iter().flatten() {
1429 if !(ti.span.start <= method.span.start && method.span.end <= ti.span.end) {
1430 continue;
1431 }
1432 impl_methods.push(method);
1433 if let Some(trait_method) = index.member_of(trait_node, &method.name, 1)
1434 && trait_method.id != method.id
1435 {
1436 edges.push(Edge {
1437 src: trait_method.id.clone(),
1438 dst: method.id.clone(),
1439 relation: Relation::Calls,
1440 evidence: Evidence::Dynamic,
1441 confidence: Evidence::Dynamic.confidence(),
1442 site: None,
1444 });
1445 }
1446 }
1447 let impl_type = types_by_file
1454 .get(ti.file.as_str())
1455 .into_iter()
1456 .flatten()
1457 .find(|n| n.span == ti.span)
1458 .copied()
1459 .or_else(|| {
1460 let prefix = impl_methods.iter().find_map(|m| {
1461 let q = qualified_of(m.id.as_str());
1462 q.rsplit_once("::")
1463 .map(|(p, _)| p.rsplit("::").next().unwrap_or(p))
1464 })?;
1465 index.type_def(&ti.file, &file_module, prefix)
1466 });
1467 if let Some(impl_type) = impl_type
1468 && impl_type.id != trait_node.id
1469 {
1470 let relation = if impl_type.kind == trait_node.kind {
1471 Relation::Extends
1472 } else {
1473 Relation::Implements
1474 };
1475 edges.push(Edge {
1476 src: impl_type.id.clone(),
1477 dst: trait_node.id.clone(),
1478 relation,
1479 evidence: pair_evidence,
1480 confidence: pair_evidence.confidence(),
1481 site: None,
1485 });
1486 }
1487 }
1488 if implicit {
1489 edges.extend(implicit_interface_edges(nodes, roots));
1490 }
1491 edges.sort();
1492 edges.dedup();
1493 edges
1494}
1495
1496fn implicit_interface_edges(nodes: &[Node], roots: &[ModuleRoot]) -> Vec<Edge> {
1506 let mut types: HashMap<(Vec<String>, &str), Vec<&Node>> = HashMap::new();
1509 let mut types_by_key: HashMap<Vec<String>, Vec<&Node>> = HashMap::new();
1510 let mut methods: HashMap<(Vec<String>, &str), Vec<&Node>> = HashMap::new();
1511 for n in nodes {
1512 let Some(spec) = spec_for_path(&n.file) else {
1513 continue;
1514 };
1515 if !spec.implicit_interfaces {
1516 continue;
1517 }
1518 let key = key_of(spec, roots, &n.file);
1519 match n.kind {
1520 SymbolKind::Interface | SymbolKind::Struct | SymbolKind::TypeAlias => {
1521 types
1522 .entry((key.clone(), n.name.as_str()))
1523 .or_default()
1524 .push(n);
1525 types_by_key.entry(key).or_default().push(n);
1526 }
1527 SymbolKind::Method => {
1528 let q = qualified_of(n.id.as_str());
1529 if let Some((owner, _)) = q.rsplit_once("::")
1530 && !owner.contains("::")
1531 {
1532 methods.entry((key, owner)).or_default().push(n);
1533 }
1534 }
1535 _ => {}
1536 }
1537 }
1538 let mut edges = Vec::new();
1539 for ((key, name), candidates) in &types {
1540 let [iface] = candidates.as_slice() else {
1542 continue;
1543 };
1544 if iface.kind != SymbolKind::Interface {
1545 continue;
1546 }
1547 let Some(iface_methods) = methods.get(&(key.clone(), *name)) else {
1548 continue; };
1550 for ty in types_by_key.get(key).into_iter().flatten() {
1551 if ty.kind == SymbolKind::Interface {
1552 continue;
1553 }
1554 let ty_methods = methods.get(&(key.clone(), ty.name.as_str()));
1555 let covers = |m: &Node| ty_methods.into_iter().flatten().any(|tm| tm.name == m.name);
1556 if !iface_methods.iter().all(|m| covers(m)) {
1557 continue;
1558 }
1559 for im in iface_methods {
1560 for tm in ty_methods.into_iter().flatten() {
1561 if tm.name == im.name {
1562 edges.push(Edge {
1563 src: im.id.clone(),
1564 dst: tm.id.clone(),
1565 relation: Relation::Calls,
1566 evidence: Evidence::Dynamic,
1567 confidence: Evidence::Dynamic.confidence(),
1568 site: None,
1569 });
1570 }
1571 }
1572 }
1573 edges.push(Edge {
1574 src: ty.id.clone(),
1575 dst: iface.id.clone(),
1576 relation: Relation::Implements,
1577 evidence: Evidence::Dynamic,
1578 confidence: Evidence::Dynamic.confidence(),
1579 site: None,
1580 });
1581 }
1582 }
1583 edges
1584}
1585
1586pub fn resolve_boundary(
1593 foreign_nodes: &[Node],
1594 references: &[Reference],
1595 owner_imports: &[Reference],
1596) -> Vec<Binding> {
1597 let index = build_index(foreign_nodes, owner_imports, &[], &[], &[], &[]);
1598 let mut bindings = Vec::new();
1599 for (i, r) in references.iter().enumerate() {
1600 let Some(spec) = spec_for_path(&r.file) else {
1601 continue;
1602 };
1603 let src = r
1604 .enclosing
1605 .clone()
1606 .unwrap_or_else(|| NodeId::new(r.file.clone()));
1607 let imports = index.imports.get(r.file.as_str());
1608 let target = if r.relation == Relation::Imports {
1609 let glob = matches!(r.alias.as_deref(), Some("*") | Some("."));
1610 let segments = (spec.absolutize)(strip_glob(&r.name), &r.file);
1611 if glob {
1612 index.import_file(&segments)
1613 } else {
1614 index.resolve_path(&segments, 4)
1615 }
1616 } else if let Some(path) = &r.path {
1617 let segments = (spec.absolutize)(path, &r.file);
1618 let direct = index.resolve_path(&segments, 4);
1619 direct.or_else(|| {
1620 let prefix = segments
1621 .len()
1622 .checked_sub(2)
1623 .and_then(|p| segments.get(p))?;
1624 let candidates: Vec<&Node> = imports
1625 .into_iter()
1626 .flatten()
1627 .filter(|imp| !imp.glob && imp.binding == *prefix)
1628 .filter_map(|imp| {
1629 let mut full = imp.segments.clone();
1630 full.push(r.name.clone());
1631 index.resolve_path(&full, 4)
1632 })
1633 .collect();
1634 match candidates.as_slice() {
1635 [node] => Some(node),
1636 _ => None,
1637 }
1638 })
1639 } else {
1640 let named: Vec<&Node> = imports
1642 .into_iter()
1643 .flatten()
1644 .filter(|imp| !imp.glob && imp.binding == r.name)
1645 .filter_map(|imp| index.resolve_path(&imp.segments, 4))
1646 .collect();
1647 match named.as_slice() {
1648 [node] => Some(*node),
1649 _ => None,
1650 }
1651 };
1652 if let Some(node) = target
1653 && node.id != src
1654 {
1655 let relation = if r.relation == Relation::Calls && is_type_kind(node.kind) {
1656 Relation::Uses
1657 } else {
1658 r.relation
1659 };
1660 bindings.push(Binding {
1661 edge: Edge {
1662 src,
1663 dst: node.id.clone(),
1664 relation,
1665 evidence: Evidence::Import,
1666 confidence: Evidence::Import.confidence(),
1667 site: Some(r.span),
1668 },
1669 reference: i,
1670 });
1671 }
1672 }
1673 bindings
1674}
1675
1676fn suffix_len(key: &[String], path: &[String]) -> Option<usize> {
1678 (!key.is_empty() && path.len() >= key.len() && path[path.len() - key.len()..] == key[..])
1679 .then_some(key.len())
1680}
1681
1682fn unique_best<'a>(candidates: impl Iterator<Item = (usize, &'a Node)>) -> Option<&'a Node> {
1684 let mut best: Option<(usize, Vec<&Node>)> = None;
1685 for (len, node) in candidates {
1686 match &mut best {
1687 Some((best_len, nodes)) if len == *best_len => nodes.push(node),
1688 Some((best_len, nodes)) if len > *best_len => {
1689 *best_len = len;
1690 nodes.clear();
1691 nodes.push(node);
1692 }
1693 None => best = Some((len, vec![node])),
1694 _ => {}
1695 }
1696 }
1697 match best {
1698 Some((_, nodes)) if nodes.len() == 1 => Some(nodes[0]),
1699 _ => None,
1700 }
1701}
1702
1703#[cfg(test)]
1704mod resolution_stats_tests {
1705 use super::{ResolutionStats, type_candidates};
1706
1707 #[test]
1708 fn anchored_rate_is_absent_when_the_pass_measured_nothing() {
1709 assert_eq!(ResolutionStats::default().anchored_unresolved_rate(), None);
1710 }
1711
1712 #[test]
1713 fn anchored_rate_excludes_external_references() {
1714 let stats = ResolutionStats {
1715 scope: 4,
1716 import: 3,
1717 scip: 42,
1718 compiler_rescued_internal: 2,
1719 unresolved_internal: 1,
1720 unresolved_external: 90,
1721 ..ResolutionStats::default()
1722 };
1723
1724 assert_eq!(stats.anchored_unresolved_rate(), Some(0.1));
1725 }
1726
1727 #[test]
1728 fn written_type_unwraps_only_receiver_transparent_wrappers() {
1729 assert_eq!(type_candidates("&Dog"), ["Dog"]);
1730 assert_eq!(type_candidates("std::sync::Arc<dyn Harness>"), ["Harness"]);
1731 assert_eq!(type_candidates("Option<Dog>"), ["Option"]);
1732 assert_eq!(type_candidates("Result<Dog, Error>"), ["Result"]);
1733 }
1734}