1use std::collections::HashMap;
8
9use sinter_core::{
10 Edge, Embed, Evidence, FieldBinding, LocalBinding, Node, NodeId, Reference, Relation,
11 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 )
118}
119
120fn is_member_scope(kind: SymbolKind) -> bool {
124 is_type_kind(kind) || kind == SymbolKind::Class
125}
126
127fn is_callable(kind: SymbolKind) -> bool {
128 matches!(
129 kind,
130 SymbolKind::Function | SymbolKind::Method | SymbolKind::Macro | SymbolKind::Class
131 )
132}
133
134struct ModuleFiles<'a> {
135 key: Vec<String>,
136 files: Vec<&'a str>,
137}
138
139struct LocalRange<'a> {
140 start: u64,
141 scope_end: u64,
142 type_name: Option<&'a str>,
143}
144
145struct Import {
146 segments: Vec<String>,
147 binding: String,
149 glob: bool,
151}
152
153struct FileDef<'a> {
154 node: &'a Node,
155 prefix: &'a str,
157 functionish: bool,
160}
161
162pub struct Index<'a> {
166 by_file_name: HashMap<(&'a str, &'a str), Vec<FileDef<'a>>>,
168 by_file_qualified: HashMap<(&'a str, &'a str), &'a Node>,
170 file_nodes: HashMap<&'a str, &'a Node>,
172 defs_by_file: HashMap<&'a str, Vec<&'a Node>>,
174 by_name: HashMap<&'a str, Vec<(Vec<String>, &'a Node)>>,
176 by_module_tail: HashMap<String, Vec<(Vec<String>, &'a Node)>>,
178 files_of_module: HashMap<String, Vec<ModuleFiles<'a>>>,
181 module_defs: HashMap<Vec<String>, HashMap<&'a str, Vec<&'a Node>>>,
183 imports: HashMap<&'a str, Vec<Import>>,
185 locals: HashMap<(&'a str, &'a str), Vec<LocalRange<'a>>>,
187 fields: HashMap<&'a str, Vec<&'a FieldBinding>>,
189 embeds: HashMap<&'a str, Vec<&'a str>>,
191 roots: Vec<ModuleRoot>,
193}
194
195fn key_of(spec: &LanguageSpec, roots: &[ModuleRoot], file: &str) -> Vec<String> {
201 let Some((manifest, root)) = spec.manifest.zip(root_of(spec, roots, file)) else {
202 return (spec.module_path)(file);
203 };
204 let rel = if root.dir.is_empty() {
205 file
206 } else {
207 &file[root.dir.len() + 1..]
208 };
209 let mut key = (spec.module_path)(rel);
210 let mut name_segments = vec![root.name.clone()];
214 for sep in spec.path_separators {
215 name_segments = name_segments
216 .iter()
217 .flat_map(|s| s.split(sep).map(str::to_string))
218 .collect();
219 }
220 name_segments.retain(|s| !s.is_empty());
221 match key.first() {
222 Some(head) if manifest.self_names.contains(&head.as_str()) => {
223 key.splice(0..1, name_segments);
224 }
225 _ => {
226 key.splice(0..0, name_segments);
227 }
228 }
229 key
230}
231
232fn root_of<'r>(spec: &LanguageSpec, roots: &'r [ModuleRoot], file: &str) -> Option<&'r ModuleRoot> {
234 roots
235 .iter()
236 .filter(|r| r.language == spec.name)
237 .filter(|r| r.dir.is_empty() || file.starts_with(&format!("{}/", r.dir)))
238 .max_by_key(|r| r.dir.len())
239}
240
241fn expand(
244 spec: &LanguageSpec,
245 roots: &[ModuleRoot],
246 file: &str,
247 mut segments: Vec<String>,
248) -> Vec<String> {
249 if let Some(manifest) = spec.manifest
250 && let Some(head) = segments.first()
251 && manifest.self_names.contains(&head.as_str())
252 && let Some(root) = root_of(spec, roots, file)
253 {
254 segments[0] = root.name.clone();
255 }
256 segments
257}
258
259fn module_of(node: &Node, roots: &[ModuleRoot]) -> Vec<String> {
260 let mut module = spec_for_path(&node.file)
261 .map(|s| key_of(s, roots, &node.file))
262 .unwrap_or_default();
263 let qualified = qualified_of(node.id.as_str());
264 if let Some((prefix, _)) = qualified.rsplit_once("::") {
265 module.extend(prefix.split("::").map(str::to_string));
266 }
267 module
268}
269
270struct Prep<'a> {
273 file_module: Vec<String>,
274 qualified: &'a str,
275 prefix: &'a str,
276 functionish: bool,
277 module: Vec<String>,
279}
280
281fn build_index<'a>(
282 nodes: &'a [Node],
283 all_imports: &'a [Reference],
284 locals: &'a [LocalBinding],
285 fields: &'a [FieldBinding],
286 embeds: &'a [Embed],
287 roots: &[ModuleRoot],
288) -> Index<'a> {
289 use rayon::prelude::*;
290 let mut index = Index {
291 by_file_name: HashMap::new(),
292 by_file_qualified: HashMap::new(),
293 file_nodes: HashMap::new(),
294 defs_by_file: HashMap::new(),
295 by_name: HashMap::new(),
296 by_module_tail: HashMap::new(),
297 files_of_module: HashMap::new(),
298 module_defs: HashMap::new(),
299 imports: HashMap::new(),
300 locals: HashMap::new(),
301 fields: HashMap::new(),
302 embeds: HashMap::new(),
303 roots: roots.to_vec(),
304 };
305 let mut kind_of: HashMap<(&str, &str), SymbolKind> = HashMap::new();
307 for node in nodes {
308 kind_of.insert(
309 (node.file.as_str(), qualified_of(node.id.as_str())),
310 node.kind,
311 );
312 }
313 let preps: Vec<Option<Prep<'a>>> = nodes
319 .par_iter()
320 .map(|node| {
321 let spec = spec_for_path(&node.file)?;
322 let file_module = key_of(spec, roots, &node.file);
323 if node.kind == SymbolKind::File {
324 return Some(Prep {
325 file_module,
326 qualified: "",
327 prefix: "",
328 functionish: false,
329 module: Vec::new(),
330 });
331 }
332 let qualified = qualified_of(node.id.as_str());
333 let prefix = qualified.rsplit_once("::").map_or("", |(p, _)| p);
334 let functionish =
335 prefix
336 .split("::")
337 .filter(|s| !s.is_empty())
338 .try_fold(String::new(), |acc, seg| {
339 let q = if acc.is_empty() {
340 seg.to_string()
341 } else {
342 format!("{acc}::{seg}")
343 };
344 let kind = kind_of.get(&(node.file.as_str(), q.as_str()));
345 match kind {
346 Some(k) if is_callable(*k) && *k != SymbolKind::Class => Some(q),
347 None => None, Some(_) => None,
349 }
350 });
351 let mut module = file_module.clone();
352 if !prefix.is_empty() {
353 module.extend(prefix.split("::").map(str::to_string));
354 }
355 Some(Prep {
356 file_module,
357 qualified,
358 prefix,
359 functionish: prefix.is_empty() || functionish.is_some(),
360 module,
361 })
362 })
363 .collect();
364 for (node, prep) in nodes.iter().zip(preps) {
366 let Some(prep) = prep else {
367 continue;
368 };
369 let file_module = prep.file_module;
370 if node.kind == SymbolKind::File {
371 index.file_nodes.insert(node.file.as_str(), node);
372 if let Some(tail) = file_module.last() {
373 index
374 .by_module_tail
375 .entry(tail.clone())
376 .or_default()
377 .push((file_module.clone(), node));
378 let entries = index.files_of_module.entry(tail.clone()).or_default();
379 match entries.iter_mut().find(|m| m.key == file_module) {
380 Some(m) => m.files.push(&node.file),
381 None => entries.push(ModuleFiles {
382 key: file_module,
383 files: vec![&node.file],
384 }),
385 }
386 }
387 continue;
388 }
389 index
390 .by_file_qualified
391 .insert((node.file.as_str(), prep.qualified), node);
392 index
393 .defs_by_file
394 .entry(node.file.as_str())
395 .or_default()
396 .push(node);
397 index
398 .by_file_name
399 .entry((node.file.as_str(), node.name.as_str()))
400 .or_default()
401 .push(FileDef {
402 node,
403 prefix: prep.prefix,
404 functionish: prep.functionish,
405 });
406 index
407 .by_name
408 .entry(node.name.as_str())
409 .or_default()
410 .push((prep.module, node));
411 if prep.prefix.is_empty() {
412 index
413 .module_defs
414 .entry(file_module)
415 .or_default()
416 .entry(node.name.as_str())
417 .or_default()
418 .push(node);
419 }
420 }
421 for r in all_imports {
422 let Some(spec) = spec_for_path(&r.file) else {
423 continue;
424 };
425 let glob = matches!(r.alias.as_deref(), Some("*") | Some("."));
426 let raw = strip_glob(&r.name);
427 let segments = expand(spec, roots, &r.file, (spec.absolutize)(raw, &r.file));
428 let binding = match (&r.alias, glob) {
429 (Some(alias), false) => alias.clone(),
430 _ => segments.last().cloned().unwrap_or_default(),
431 };
432 index
433 .imports
434 .entry(r.file.as_str())
435 .or_default()
436 .push(Import {
437 segments,
438 binding,
439 glob,
440 });
441 }
442 for l in locals {
443 index
444 .locals
445 .entry((l.file.as_str(), l.name.as_str()))
446 .or_default()
447 .push(LocalRange {
448 start: l.span.start,
449 scope_end: l.scope_end,
450 type_name: l.type_name.as_deref(),
451 });
452 }
453 for field in fields {
454 index
455 .fields
456 .entry(field.owner.as_str())
457 .or_default()
458 .push(field);
459 }
460 for e in embeds {
461 index
462 .embeds
463 .entry(e.owner.as_str())
464 .or_default()
465 .push(&e.type_name);
466 }
467 index
468}
469
470fn strip_glob(name: &str) -> &str {
471 name.strip_suffix('*')
472 .map(|s| s.trim_end_matches(['.', ':', '/']))
473 .unwrap_or(name)
474}
475
476impl<'a> Index<'a> {
477 pub fn build(
480 nodes: &'a [Node],
481 all_imports: &'a [Reference],
482 locals: &'a [LocalBinding],
483 fields: &'a [FieldBinding],
484 embeds: &'a [Embed],
485 roots: &[ModuleRoot],
486 ) -> Index<'a> {
487 let t = std::time::Instant::now();
488 let index = build_index(nodes, all_imports, locals, fields, embeds, roots);
489 if std::env::var_os("SINTER_TIMING").is_some() {
490 eprintln!("index build: {:?}", t.elapsed());
491 }
492 index
493 }
494
495 fn local_at(&self, file: &str, name: &str, at: u64) -> Option<Option<&'a str>> {
497 self.locals
498 .get(&(file, name))
499 .into_iter()
500 .flatten()
501 .filter(|l| l.start <= at && at < l.scope_end)
502 .map(|l| l.type_name)
503 .next_back()
504 }
505
506 fn type_def(&self, file: &str, module: &[String], name: &str) -> Option<&'a Node> {
508 let same_file: Vec<&Node> = self
509 .by_file_name
510 .get(&(file, name))
511 .into_iter()
512 .flatten()
513 .filter(|d| is_member_scope(d.node.kind))
514 .map(|d| d.node)
515 .collect();
516 if let [node] = same_file.as_slice() {
517 return Some(node);
518 }
519 let in_module: Vec<&Node> = self
520 .module_defs
521 .get(module)
522 .and_then(|m| m.get(name))
523 .into_iter()
524 .flatten()
525 .filter(|n| is_member_scope(n.kind))
526 .copied()
527 .collect();
528 match in_module.as_slice() {
529 [node] => Some(node),
530 _ => None,
531 }
532 }
533
534 fn visible_types(&self, file: &str, module: &[String], written: &str) -> Vec<&'a Node> {
539 let mut found = Vec::new();
540 for candidate in type_candidates(written) {
541 if let Some(node) = self.type_def(file, module, candidate) {
542 found.push(node);
543 continue;
544 }
545 let imported: Vec<&Node> = self
546 .imports
547 .get(file)
548 .into_iter()
549 .flatten()
550 .filter(|imp| !imp.glob && imp.binding == candidate)
551 .filter_map(|imp| self.resolve_path_defs(&imp.segments, 4))
552 .filter(|n| is_member_scope(n.kind))
553 .collect();
554 if let [node] = imported.as_slice() {
555 found.push(*node);
556 }
557 }
558 found.sort_by_key(|node| node.id.as_str());
559 found.dedup_by_key(|node| node.id.as_str());
560 found
561 }
562
563 fn member_of_written_type(
567 &self,
568 file: &str,
569 module: &[String],
570 written: &str,
571 member: &str,
572 ) -> (Option<&'a Node>, bool) {
573 let types = self.visible_types(file, module, written);
574 let mut members: Vec<&Node> = types
575 .iter()
576 .filter_map(|ty| self.member_of(ty, member, 4))
577 .collect();
578 members.sort_by_key(|node| node.id.as_str());
579 members.dedup_by_key(|node| node.id.as_str());
580 let target = match members.as_slice() {
581 [member] => Some(*member),
582 _ => None,
583 };
584 (target, !types.is_empty())
585 }
586
587 fn field(&self, owner: &Node, name: &str) -> Option<&'a FieldBinding> {
588 let matching: Vec<&FieldBinding> = self
589 .fields
590 .get(owner.id.as_str())
591 .into_iter()
592 .flatten()
593 .filter(|f| f.name == name)
594 .copied()
595 .collect();
596 match matching.as_slice() {
597 [field] => Some(*field),
598 _ => None,
599 }
600 }
601
602 fn member_of(&self, ty: &'a Node, name: &str, depth: usize) -> Option<&'a Node> {
604 if depth == 0 {
605 return None;
606 }
607 let mut module = module_of(ty, &self.roots);
608 module.extend(
609 qualified_of(ty.id.as_str())
610 .rsplit("::")
611 .next()
612 .map(str::to_string),
613 );
614 let direct: Vec<&Node> = self
615 .by_name
616 .get(name)
617 .into_iter()
618 .flatten()
619 .filter(|(m, _)| *m == module)
620 .map(|(_, n)| *n)
621 .collect();
622 if let [node] = direct.as_slice() {
623 return Some(node);
624 }
625 let in_type_file: Vec<&Node> = direct
629 .iter()
630 .filter(|n| n.file == ty.file)
631 .copied()
632 .collect();
633 if let [node] = in_type_file.as_slice() {
634 return Some(node);
635 }
636 let spec = spec_for_path(&ty.file)?;
637 let file_module = key_of(spec, &self.roots, &ty.file);
638 for embedded in self.embeds.get(ty.id.as_str()).into_iter().flatten() {
639 if let Some(embedded_ty) = self.type_def(&ty.file, &file_module, embedded)
640 && let Some(node) = self.member_of(embedded_ty, name, depth - 1)
641 {
642 return Some(node);
643 }
644 }
645 None
646 }
647
648 fn anchored(&self, segments: &[String]) -> bool {
651 let module_hit = |segs: &[String]| {
652 segs.last().is_some_and(|tail| {
653 self.files_of_module
654 .get(tail.as_str())
655 .into_iter()
656 .flatten()
657 .any(|m| suffix_len(&m.key, segs).is_some())
658 || self
659 .by_module_tail
660 .get(tail.as_str())
661 .into_iter()
662 .flatten()
663 .any(|(key, _)| suffix_len(key, segs).is_some())
664 })
665 };
666 if module_hit(segments) {
667 return true;
668 }
669 match segments.split_last() {
670 Some((_, module)) if !module.is_empty() => module_hit(module),
671 _ => false,
672 }
673 }
674
675 fn import_file(&self, segments: &[String]) -> Option<&'a Node> {
682 unique_best(
683 self.by_module_tail
684 .get(segments.last()?.as_str())
685 .into_iter()
686 .flatten()
687 .filter_map(|(key, node)| {
688 let len = suffix_len(key, segments).or_else(|| suffix_len(segments, key))?;
689 Some((len, *node))
690 }),
691 )
692 }
693
694 fn resolve_path(&self, segments: &[String], depth: usize) -> Option<&'a Node> {
697 self.resolve_path_defs(segments, depth).or_else(|| {
698 let files = self
702 .by_module_tail
703 .get(segments.last()?.as_str())
704 .into_iter()
705 .flatten()
706 .filter_map(|(key, node)| Some((suffix_len(key, segments)?, *node)));
707 unique_best(files)
708 })
709 }
710
711 fn resolve_path_defs(&self, segments: &[String], depth: usize) -> Option<&'a Node> {
717 if segments.is_empty() || depth == 0 {
718 return None;
719 }
720 if let Some((name, module)) = segments.split_last() {
721 let defs = self
722 .by_name
723 .get(name.as_str())
724 .into_iter()
725 .flatten()
726 .filter_map(|(key, node)| Some((suffix_len(key, module)?, *node)));
727 if let Some(node) = unique_best(defs) {
728 return Some(node);
729 }
730 if !module.is_empty() {
733 let mut chained: Vec<&Node> = Vec::new();
734 let tail = module.last().map(String::as_str).unwrap_or("");
735 for m in self.files_of_module.get(tail).into_iter().flatten() {
736 if suffix_len(&m.key, module).is_none() {
737 continue;
738 }
739 for file in &m.files {
740 for import in self.imports.get(*file).into_iter().flatten() {
741 if import.binding == *name && !import.glob {
742 chained.extend(self.resolve_path(&import.segments, depth - 1));
743 } else if import.glob {
744 let mut deeper = import.segments.clone();
745 deeper.push(name.clone());
746 chained.extend(self.resolve_path(&deeper, depth - 1));
747 }
748 }
749 }
750 }
751 chained.sort_by_key(|n| n.id.as_str().to_string());
752 chained.dedup_by_key(|n| n.id.as_str().to_string());
753 if let [node] = chained.as_slice() {
754 return Some(node);
755 }
756 }
757 }
758 None
759 }
760}
761
762const TYPE_KEYWORDS: &[&str] = &[
767 "dyn", "impl", "mut", "const", "ref", "crate", "self", "super", "std", "core", "alloc",
768];
769
770fn type_tokens(text: &str) -> impl DoubleEndedIterator<Item = &str> {
771 text.split(|c: char| !(c.is_alphanumeric() || c == '_'))
772 .filter(|token| {
773 !token.is_empty()
774 && !token.chars().next().is_some_and(char::is_numeric)
775 && !TYPE_KEYWORDS.contains(token)
776 })
777}
778
779fn type_candidates(written: &str) -> Vec<&str> {
780 const DEREF_WRAPPERS: &[&str] = &["Box", "Arc", "Rc", "Pin", "Cow"];
784 let (head_text, arguments) = written
785 .split_once('<')
786 .map_or((written, None), |(head, rest)| (head, Some(rest)));
787 let head = type_tokens(head_text).next_back();
788 if let Some(head) = head
789 && !DEREF_WRAPPERS.contains(&head)
790 {
791 return vec![head];
792 }
793 let mut out = Vec::new();
794 for token in type_tokens(arguments.unwrap_or(written)).rev() {
795 if DEREF_WRAPPERS.contains(&token) {
796 continue;
797 }
798 if !out.contains(&token) {
799 out.push(token);
800 }
801 }
802 out
803}
804
805fn namespace_pick(candidates: Vec<&Node>, relation: Relation) -> Option<&Node> {
808 match candidates.as_slice() {
809 [node] => Some(node),
810 [] => None,
811 _ => {
812 let preferred: Vec<&Node> = candidates
813 .iter()
814 .filter(|n| match relation {
815 Relation::Calls => is_callable(n.kind),
816 Relation::Uses => is_type_kind(n.kind),
817 _ => true,
818 })
819 .copied()
820 .collect();
821 match preferred.as_slice() {
822 [node] => Some(node),
823 _ => None,
824 }
825 }
826 }
827}
828
829pub fn resolve(
830 index: &Index<'_>,
831 references: &[Reference],
832) -> (Vec<Binding>, ResolutionStats, Vec<usize>) {
833 use rayon::prelude::*;
834 let results: Vec<Res> = references
835 .par_iter()
836 .enumerate()
837 .map(|(i, r)| {
838 let Some(spec) = spec_for_path(&r.file) else {
839 return Res::External;
840 };
841 let src = r
842 .enclosing
843 .clone()
844 .unwrap_or_else(|| NodeId::new(r.file.clone()));
845 let file_module = key_of(spec, &index.roots, &r.file);
846 let imports = index.imports.get(r.file.as_str());
847 let (target, evidence, internal) = resolve_one(index, spec, r, &file_module, imports);
848 match target {
849 Some(node) if node.id != src => {
850 let relation = if r.relation == Relation::Calls && is_type_kind(node.kind) {
853 Relation::Uses
854 } else {
855 r.relation
856 };
857 Res::Bound(Binding {
858 edge: Edge {
859 src,
860 dst: node.id.clone(),
861 relation,
862 evidence,
863 confidence: evidence.confidence(),
864 site: Some(r.span),
865 },
866 reference: i,
867 })
868 }
869 _ if internal => Res::Internal,
870 _ => Res::External,
871 }
872 })
873 .collect();
874 let mut bindings = Vec::new();
875 let mut stats = ResolutionStats::default();
876 let mut internal_indices = Vec::new();
877 for (i, result) in results.into_iter().enumerate() {
878 match result {
879 Res::Bound(binding) => {
880 match binding.edge.evidence {
881 Evidence::Scope => stats.scope += 1,
882 _ => stats.import += 1,
883 }
884 bindings.push(binding);
885 }
886 Res::Internal => {
887 stats.unresolved_internal += 1;
888 internal_indices.push(i);
889 }
890 Res::External => stats.unresolved_external += 1,
891 }
892 }
893 (bindings, stats, internal_indices)
894}
895
896fn resolve_one<'a>(
897 index: &Index<'a>,
898 spec: &sinter_extract::LanguageSpec,
899 r: &Reference,
900 file_module: &[String],
901 imports: Option<&Vec<Import>>,
902) -> (Option<&'a Node>, Evidence, bool) {
903 if r.relation == Relation::Imports {
904 return resolve_import_reference(index, spec, r);
905 }
906
907 if let Some(path) = &r.path {
908 return resolve_qualified_reference(index, spec, r, file_module, imports, path);
909 }
910
911 resolve_bare_reference(index, r, file_module, imports)
912}
913
914fn resolve_import_reference<'a>(
917 index: &Index<'a>,
918 spec: &LanguageSpec,
919 r: &Reference,
920) -> (Option<&'a Node>, Evidence, bool) {
921 let glob = matches!(r.alias.as_deref(), Some("*") | Some("."));
922 let raw = strip_glob(&r.name);
923 if let Some(node) = index
927 .file_nodes
928 .get(raw.trim().trim_matches(['<', '>', '"']))
929 {
930 return (Some(node), Evidence::Import, true);
931 }
932 let segments = expand(spec, &index.roots, &r.file, (spec.absolutize)(raw, &r.file));
933 let target = if glob {
934 index.import_file(&segments)
935 } else {
936 index.resolve_path(&segments, 4)
937 };
938 let internal = target.is_some() || index.anchored(&segments);
939 (target, Evidence::Import, internal)
940}
941
942fn resolve_qualified_reference<'a>(
945 index: &Index<'a>,
946 spec: &LanguageSpec,
947 r: &Reference,
948 file_module: &[String],
949 imports: Option<&Vec<Import>>,
950 path: &str,
951) -> (Option<&'a Node>, Evidence, bool) {
952 if spec.file_refs {
955 return resolve_file_ref(index, spec, r, path);
956 }
957 let segments = expand(
960 spec,
961 &index.roots,
962 &r.file,
963 (spec.absolutize)(path, &r.file),
964 );
965 let prefix = segments
966 .len()
967 .checked_sub(2)
968 .and_then(|p| segments.get(p))
969 .cloned();
970 let Some(prefix) = prefix else {
971 return (None, Evidence::Import, false);
972 };
973 if segments.len() >= 3
977 && spec
978 .receivers
979 .contains(&segments[segments.len() - 3].as_str())
980 && let Some(enclosing) = &r.enclosing
981 && let Some((type_prefix, _)) = qualified_of(enclosing.as_str()).rsplit_once("::")
982 {
983 let owner = index
984 .by_file_qualified
985 .get(&(r.file.as_str(), type_prefix))
986 .copied()
987 .or_else(|| {
988 let name = type_prefix.rsplit("::").next().unwrap_or(type_prefix);
989 index.type_def(&r.file, file_module, name)
990 });
991 if let Some(owner) = owner
992 && let Some(field) = index.field(owner, &segments[segments.len() - 2])
993 {
994 let field_spec = spec_for_path(&owner.file).unwrap_or(spec);
995 let field_module = key_of(field_spec, &index.roots, &owner.file);
996 let (target, anchored) =
997 index.member_of_written_type(&owner.file, &field_module, &field.type_name, &r.name);
998 return (target, Evidence::Scope, anchored);
999 }
1000 }
1001 if spec.receivers.contains(&prefix.as_str())
1002 && let Some(enclosing) = &r.enclosing
1003 && let Some((type_prefix, _)) = qualified_of(enclosing.as_str()).rsplit_once("::")
1004 {
1005 let sibling = format!("{type_prefix}::{}", r.name);
1009 if let Some(node) = index
1010 .by_file_qualified
1011 .get(&(r.file.as_str(), sibling.as_str()))
1012 {
1013 return (Some(node), Evidence::Scope, true);
1014 }
1015 if let Some(ty) = index.by_file_qualified.get(&(r.file.as_str(), type_prefix)) {
1016 return (index.member_of(ty, &r.name, 4), Evidence::Scope, true);
1018 }
1019 }
1020 match index.local_at(&r.file, &prefix, r.span.start) {
1021 Some(Some(type_name)) => {
1022 let (target, anchored) =
1023 index.member_of_written_type(&r.file, file_module, type_name, &r.name);
1024 return (target, Evidence::Scope, anchored);
1026 }
1027 Some(None) => return (None, Evidence::Scope, false), None => {}
1029 }
1030 if let Some(ty) = index.type_def(&r.file, file_module, &prefix)
1032 && let Some(node) = index.member_of(ty, &r.name, 4)
1033 {
1034 return (Some(node), Evidence::Scope, true);
1035 }
1036 if let Some(node) = index.resolve_path_defs(&segments, 4) {
1037 return (Some(node), Evidence::Import, true);
1038 }
1039 if let Some((leaf, type_path)) = segments.split_last()
1045 && type_path.len() >= 2
1046 && let Some(ty) = index.resolve_path_defs(type_path, 4)
1047 && let Some(node) = index.member_of(ty, leaf, 4)
1048 {
1049 return (Some(node), Evidence::Import, true);
1050 }
1051 let matching: Vec<&Import> = imports
1052 .into_iter()
1053 .flatten()
1054 .filter(|imp| !imp.glob && imp.binding == prefix)
1055 .collect();
1056 let candidates: Vec<&Node> = matching
1057 .iter()
1058 .filter_map(|imp| {
1059 let mut full = imp.segments.clone();
1060 full.push(r.name.clone());
1061 index.resolve_path(&full, 4)
1062 })
1063 .collect();
1064 let internal = candidates.len() > 1
1065 || index.anchored(&segments)
1066 || matching.iter().any(|imp| index.anchored(&imp.segments));
1067 match candidates.as_slice() {
1068 [node] => (Some(node), Evidence::Import, true),
1069 _ => (None, Evidence::Import, internal),
1070 }
1071}
1072
1073fn resolve_bare_reference<'a>(
1076 index: &Index<'a>,
1077 r: &Reference,
1078 file_module: &[String],
1079 imports: Option<&Vec<Import>>,
1080) -> (Option<&'a Node>, Evidence, bool) {
1081 if index.local_at(&r.file, &r.name, r.span.start).is_some() {
1082 return (None, Evidence::Scope, false); }
1084 let enclosing_q = r
1085 .enclosing
1086 .as_ref()
1087 .map(|e| qualified_of(e.as_str()))
1088 .unwrap_or("");
1089 let visible: Vec<&Node> = index
1090 .by_file_name
1091 .get(&(r.file.as_str(), r.name.as_str()))
1092 .into_iter()
1093 .flatten()
1094 .filter(|d| {
1095 d.prefix.is_empty()
1096 || (d.functionish
1097 && (enclosing_q == d.prefix
1098 || enclosing_q.starts_with(&format!("{}::", d.prefix))))
1099 })
1100 .map(|d| d.node)
1101 .collect();
1102 if !visible.is_empty() {
1103 return (namespace_pick(visible, r.relation), Evidence::Scope, true);
1105 }
1106 if let Some(defs) = index
1107 .module_defs
1108 .get(file_module)
1109 .and_then(|m| m.get(r.name.as_str()))
1110 {
1111 return (
1112 namespace_pick(defs.clone(), r.relation),
1113 Evidence::Scope,
1114 true,
1115 );
1116 }
1117 let named: Vec<&Node> = imports
1118 .into_iter()
1119 .flatten()
1120 .filter(|imp| !imp.glob && imp.binding == r.name)
1121 .filter_map(|imp| index.resolve_path(&imp.segments, 4))
1122 .collect();
1123 let (target, internal) = match named.as_slice() {
1124 [node] => (Some(*node), true),
1125 [] => {
1126 let globbed: Vec<&Node> = imports
1127 .into_iter()
1128 .flatten()
1129 .filter(|imp| imp.glob)
1130 .filter_map(|imp| {
1131 let mut full = imp.segments.clone();
1132 full.push(r.name.clone());
1133 index.resolve_path(&full, 4).or_else(|| {
1134 let file = index.import_file(&imp.segments)?;
1137 index
1138 .by_file_name
1139 .get(&(file.file.as_str(), r.name.as_str()))
1140 .into_iter()
1141 .flatten()
1142 .find(|d| d.prefix.is_empty())
1143 .map(|d| d.node)
1144 })
1145 })
1146 .collect();
1147 let name_imports_anchored = imports
1148 .into_iter()
1149 .flatten()
1150 .filter(|imp| !imp.glob && imp.binding == r.name)
1151 .any(|imp| index.anchored(&imp.segments));
1152 match globbed.as_slice() {
1153 [node] => (Some(*node), true),
1154 [] => (None, name_imports_anchored),
1155 _ => (None, true), }
1157 }
1158 _ => (None, true), };
1160 (target, Evidence::Import, internal)
1161}
1162
1163fn resolve_file_ref<'a>(
1171 index: &Index<'a>,
1172 spec: &LanguageSpec,
1173 r: &Reference,
1174 path: &str,
1175) -> (Option<&'a Node>, Evidence, bool) {
1176 let (head, frag) = match path.split_once('#') {
1177 Some((h, f)) => (h, Some(f)),
1178 None => (path, None),
1179 };
1180 let file = if head.is_empty() {
1181 index.file_nodes.get(r.file.as_str()).copied()
1183 } else {
1184 let joined = (spec.absolutize)(head, &r.file).join("/");
1185 index.file_nodes.get(joined.as_str()).copied().or_else(|| {
1186 spec.extensions.iter().find_map(|ext| {
1187 index
1188 .file_nodes
1189 .get(format!("{joined}.{ext}").as_str())
1190 .copied()
1191 })
1192 })
1193 };
1194 match (file, frag) {
1195 (Some(file), None) => (Some(file), Evidence::Import, true),
1196 (Some(file), Some(frag)) => {
1197 let matching: Vec<&Node> = index
1198 .defs_by_file
1199 .get(file.file.as_str())
1200 .into_iter()
1201 .flatten()
1202 .filter(|n| slugify(&n.name) == frag)
1203 .copied()
1204 .collect();
1205 match matching.as_slice() {
1208 [node] => (Some(node), Evidence::Import, true),
1209 _ => (None, Evidence::Import, true),
1210 }
1211 }
1212 (None, _) => (None, Evidence::Import, false),
1213 }
1214}
1215
1216fn slugify(name: &str) -> String {
1219 name.chars()
1220 .filter_map(|c| match c {
1221 ' ' => Some('-'),
1222 '-' | '_' => Some(c),
1223 c if c.is_alphanumeric() => Some(c.to_ascii_lowercase()),
1224 _ => None,
1225 })
1226 .collect()
1227}
1228
1229pub fn dynamic_edges(index: &Index<'_>, nodes: &[Node], trait_impls: &[TraitImpl]) -> Vec<Edge> {
1237 let mut edges = crate::proto_service_bindings::proto_service_edges(nodes, trait_impls);
1240 let implicit = nodes
1241 .iter()
1242 .any(|n| spec_for_path(&n.file).is_some_and(|s| s.implicit_interfaces));
1243 if trait_impls.is_empty() && !implicit {
1244 return edges;
1245 }
1246 let roots = &index.roots;
1247 let mut by_file: HashMap<&str, Vec<&Node>> = HashMap::new();
1248 let mut types_by_file: HashMap<&str, Vec<&Node>> = HashMap::new();
1249 for n in nodes {
1250 if is_callable(n.kind) {
1251 by_file.entry(n.file.as_str()).or_default().push(n);
1252 }
1253 if is_member_scope(n.kind) {
1254 types_by_file.entry(n.file.as_str()).or_default().push(n);
1255 }
1256 }
1257 let is_trait = |n: &Node| {
1261 matches!(
1262 n.kind,
1263 SymbolKind::Trait | SymbolKind::Interface | SymbolKind::Class
1264 )
1265 };
1266 for ti in trait_impls {
1267 let Some(spec) = spec_for_path(&ti.file) else {
1268 continue;
1269 };
1270 let file_module = key_of(spec, roots, &ti.file);
1271 let trait_node = index
1272 .type_def(&ti.file, &file_module, &ti.trait_name)
1273 .filter(|n| is_trait(n))
1274 .map(|n| (n, Evidence::Scope))
1275 .or_else(|| {
1276 let named: Vec<&Node> = index
1278 .imports
1279 .get(ti.file.as_str())
1280 .into_iter()
1281 .flatten()
1282 .filter(|imp| !imp.glob && imp.binding == ti.trait_name)
1283 .filter_map(|imp| index.resolve_path_defs(&imp.segments, 4))
1284 .filter(|n| is_trait(n))
1285 .collect();
1286 match named.as_slice() {
1287 [node] => Some((node, Evidence::Import)),
1288 _ => None,
1289 }
1290 })
1291 .or_else(|| {
1292 let globbed: Vec<&Node> = index
1295 .imports
1296 .get(ti.file.as_str())
1297 .into_iter()
1298 .flatten()
1299 .filter(|imp| imp.glob)
1300 .filter_map(|imp| {
1301 let mut full = imp.segments.clone();
1302 full.push(ti.trait_name.clone());
1303 index.resolve_path_defs(&full, 4)
1304 })
1305 .filter(|n| is_trait(n))
1306 .collect();
1307 match globbed.as_slice() {
1308 [node] => Some((node, Evidence::Import)),
1309 _ => None,
1310 }
1311 });
1312 let Some((trait_node, pair_evidence)) = trait_node else {
1313 continue; };
1315 let mut impl_methods: Vec<&Node> = Vec::new();
1316 for method in by_file.get(ti.file.as_str()).into_iter().flatten() {
1317 if !(ti.span.start <= method.span.start && method.span.end <= ti.span.end) {
1318 continue;
1319 }
1320 impl_methods.push(method);
1321 if let Some(trait_method) = index.member_of(trait_node, &method.name, 1)
1322 && trait_method.id != method.id
1323 {
1324 edges.push(Edge {
1325 src: trait_method.id.clone(),
1326 dst: method.id.clone(),
1327 relation: Relation::Calls,
1328 evidence: Evidence::Dynamic,
1329 confidence: Evidence::Dynamic.confidence(),
1330 site: None,
1332 });
1333 }
1334 }
1335 let impl_type = types_by_file
1342 .get(ti.file.as_str())
1343 .into_iter()
1344 .flatten()
1345 .find(|n| n.span == ti.span)
1346 .copied()
1347 .or_else(|| {
1348 let prefix = impl_methods.iter().find_map(|m| {
1349 let q = qualified_of(m.id.as_str());
1350 q.rsplit_once("::")
1351 .map(|(p, _)| p.rsplit("::").next().unwrap_or(p))
1352 })?;
1353 index.type_def(&ti.file, &file_module, prefix)
1354 });
1355 if let Some(impl_type) = impl_type
1356 && impl_type.id != trait_node.id
1357 {
1358 let relation = if impl_type.kind == trait_node.kind {
1359 Relation::Extends
1360 } else {
1361 Relation::Implements
1362 };
1363 edges.push(Edge {
1364 src: impl_type.id.clone(),
1365 dst: trait_node.id.clone(),
1366 relation,
1367 evidence: pair_evidence,
1368 confidence: pair_evidence.confidence(),
1369 site: None,
1373 });
1374 }
1375 }
1376 if implicit {
1377 edges.extend(implicit_interface_edges(nodes, roots));
1378 }
1379 edges.sort();
1380 edges.dedup();
1381 edges
1382}
1383
1384fn implicit_interface_edges(nodes: &[Node], roots: &[ModuleRoot]) -> Vec<Edge> {
1394 let mut types: HashMap<(Vec<String>, &str), Vec<&Node>> = HashMap::new();
1397 let mut types_by_key: HashMap<Vec<String>, Vec<&Node>> = HashMap::new();
1398 let mut methods: HashMap<(Vec<String>, &str), Vec<&Node>> = HashMap::new();
1399 for n in nodes {
1400 let Some(spec) = spec_for_path(&n.file) else {
1401 continue;
1402 };
1403 if !spec.implicit_interfaces {
1404 continue;
1405 }
1406 let key = key_of(spec, roots, &n.file);
1407 match n.kind {
1408 SymbolKind::Interface | SymbolKind::Struct | SymbolKind::TypeAlias => {
1409 types
1410 .entry((key.clone(), n.name.as_str()))
1411 .or_default()
1412 .push(n);
1413 types_by_key.entry(key).or_default().push(n);
1414 }
1415 SymbolKind::Method => {
1416 let q = qualified_of(n.id.as_str());
1417 if let Some((owner, _)) = q.rsplit_once("::")
1418 && !owner.contains("::")
1419 {
1420 methods.entry((key, owner)).or_default().push(n);
1421 }
1422 }
1423 _ => {}
1424 }
1425 }
1426 let mut edges = Vec::new();
1427 for ((key, name), candidates) in &types {
1428 let [iface] = candidates.as_slice() else {
1430 continue;
1431 };
1432 if iface.kind != SymbolKind::Interface {
1433 continue;
1434 }
1435 let Some(iface_methods) = methods.get(&(key.clone(), *name)) else {
1436 continue; };
1438 for ty in types_by_key.get(key).into_iter().flatten() {
1439 if ty.kind == SymbolKind::Interface {
1440 continue;
1441 }
1442 let ty_methods = methods.get(&(key.clone(), ty.name.as_str()));
1443 let covers = |m: &Node| ty_methods.into_iter().flatten().any(|tm| tm.name == m.name);
1444 if !iface_methods.iter().all(|m| covers(m)) {
1445 continue;
1446 }
1447 for im in iface_methods {
1448 for tm in ty_methods.into_iter().flatten() {
1449 if tm.name == im.name {
1450 edges.push(Edge {
1451 src: im.id.clone(),
1452 dst: tm.id.clone(),
1453 relation: Relation::Calls,
1454 evidence: Evidence::Dynamic,
1455 confidence: Evidence::Dynamic.confidence(),
1456 site: None,
1457 });
1458 }
1459 }
1460 }
1461 edges.push(Edge {
1462 src: ty.id.clone(),
1463 dst: iface.id.clone(),
1464 relation: Relation::Implements,
1465 evidence: Evidence::Dynamic,
1466 confidence: Evidence::Dynamic.confidence(),
1467 site: None,
1468 });
1469 }
1470 }
1471 edges
1472}
1473
1474pub fn resolve_boundary(
1481 foreign_nodes: &[Node],
1482 references: &[Reference],
1483 owner_imports: &[Reference],
1484) -> Vec<Binding> {
1485 let index = build_index(foreign_nodes, owner_imports, &[], &[], &[], &[]);
1486 let mut bindings = Vec::new();
1487 for (i, r) in references.iter().enumerate() {
1488 let Some(spec) = spec_for_path(&r.file) else {
1489 continue;
1490 };
1491 let src = r
1492 .enclosing
1493 .clone()
1494 .unwrap_or_else(|| NodeId::new(r.file.clone()));
1495 let imports = index.imports.get(r.file.as_str());
1496 let target = if r.relation == Relation::Imports {
1497 let glob = matches!(r.alias.as_deref(), Some("*") | Some("."));
1498 let segments = (spec.absolutize)(strip_glob(&r.name), &r.file);
1499 if glob {
1500 index.import_file(&segments)
1501 } else {
1502 index.resolve_path(&segments, 4)
1503 }
1504 } else if let Some(path) = &r.path {
1505 let segments = (spec.absolutize)(path, &r.file);
1506 let direct = index.resolve_path(&segments, 4);
1507 direct.or_else(|| {
1508 let prefix = segments
1509 .len()
1510 .checked_sub(2)
1511 .and_then(|p| segments.get(p))?;
1512 let candidates: Vec<&Node> = imports
1513 .into_iter()
1514 .flatten()
1515 .filter(|imp| !imp.glob && imp.binding == *prefix)
1516 .filter_map(|imp| {
1517 let mut full = imp.segments.clone();
1518 full.push(r.name.clone());
1519 index.resolve_path(&full, 4)
1520 })
1521 .collect();
1522 match candidates.as_slice() {
1523 [node] => Some(node),
1524 _ => None,
1525 }
1526 })
1527 } else {
1528 let named: Vec<&Node> = imports
1530 .into_iter()
1531 .flatten()
1532 .filter(|imp| !imp.glob && imp.binding == r.name)
1533 .filter_map(|imp| index.resolve_path(&imp.segments, 4))
1534 .collect();
1535 match named.as_slice() {
1536 [node] => Some(*node),
1537 _ => None,
1538 }
1539 };
1540 if let Some(node) = target
1541 && node.id != src
1542 {
1543 let relation = if r.relation == Relation::Calls && is_type_kind(node.kind) {
1544 Relation::Uses
1545 } else {
1546 r.relation
1547 };
1548 bindings.push(Binding {
1549 edge: Edge {
1550 src,
1551 dst: node.id.clone(),
1552 relation,
1553 evidence: Evidence::Import,
1554 confidence: Evidence::Import.confidence(),
1555 site: Some(r.span),
1556 },
1557 reference: i,
1558 });
1559 }
1560 }
1561 bindings
1562}
1563
1564fn suffix_len(key: &[String], path: &[String]) -> Option<usize> {
1566 (!key.is_empty() && path.len() >= key.len() && path[path.len() - key.len()..] == key[..])
1567 .then_some(key.len())
1568}
1569
1570fn unique_best<'a>(candidates: impl Iterator<Item = (usize, &'a Node)>) -> Option<&'a Node> {
1572 let mut best: Option<(usize, Vec<&Node>)> = None;
1573 for (len, node) in candidates {
1574 match &mut best {
1575 Some((best_len, nodes)) if len == *best_len => nodes.push(node),
1576 Some((best_len, nodes)) if len > *best_len => {
1577 *best_len = len;
1578 nodes.clear();
1579 nodes.push(node);
1580 }
1581 None => best = Some((len, vec![node])),
1582 _ => {}
1583 }
1584 }
1585 match best {
1586 Some((_, nodes)) if nodes.len() == 1 => Some(nodes[0]),
1587 _ => None,
1588 }
1589}
1590
1591#[cfg(test)]
1592mod resolution_stats_tests {
1593 use super::{ResolutionStats, type_candidates};
1594
1595 #[test]
1596 fn anchored_rate_is_absent_when_the_pass_measured_nothing() {
1597 assert_eq!(ResolutionStats::default().anchored_unresolved_rate(), None);
1598 }
1599
1600 #[test]
1601 fn anchored_rate_excludes_external_references() {
1602 let stats = ResolutionStats {
1603 scope: 4,
1604 import: 3,
1605 scip: 42,
1606 compiler_rescued_internal: 2,
1607 unresolved_internal: 1,
1608 unresolved_external: 90,
1609 ..ResolutionStats::default()
1610 };
1611
1612 assert_eq!(stats.anchored_unresolved_rate(), Some(0.1));
1613 }
1614
1615 #[test]
1616 fn written_type_unwraps_only_receiver_transparent_wrappers() {
1617 assert_eq!(type_candidates("&Dog"), ["Dog"]);
1618 assert_eq!(type_candidates("std::sync::Arc<dyn Harness>"), ["Harness"]);
1619 assert_eq!(type_candidates("Option<Dog>"), ["Option"]);
1620 assert_eq!(type_candidates("Result<Dog, Error>"), ["Result"]);
1621 }
1622}