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 )
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 proto_rpcs: crate::proto_service_bindings::ProtoRpcs<'a>,
195}
196
197fn key_of(spec: &LanguageSpec, roots: &[ModuleRoot], file: &str) -> Vec<String> {
203 let Some((manifest, root)) = spec.manifest.zip(root_of(spec, roots, file)) else {
204 return (spec.module_path)(file);
205 };
206 let rel = if root.dir.is_empty() {
207 file
208 } else {
209 &file[root.dir.len() + 1..]
210 };
211 let mut key = (spec.module_path)(rel);
212 let mut name_segments = vec![root.name.clone()];
216 for sep in spec.path_separators {
217 name_segments = name_segments
218 .iter()
219 .flat_map(|s| s.split(sep).map(str::to_string))
220 .collect();
221 }
222 name_segments.retain(|s| !s.is_empty());
223 match key.first() {
224 Some(head) if manifest.self_names.contains(&head.as_str()) => {
225 key.splice(0..1, name_segments);
226 }
227 _ => {
228 key.splice(0..0, name_segments);
229 }
230 }
231 key
232}
233
234fn root_of<'r>(spec: &LanguageSpec, roots: &'r [ModuleRoot], file: &str) -> Option<&'r ModuleRoot> {
236 roots
237 .iter()
238 .filter(|r| r.language == spec.name)
239 .filter(|r| r.dir.is_empty() || file.starts_with(&format!("{}/", r.dir)))
240 .max_by_key(|r| r.dir.len())
241}
242
243fn expand(
246 spec: &LanguageSpec,
247 roots: &[ModuleRoot],
248 file: &str,
249 mut segments: Vec<String>,
250) -> Vec<String> {
251 if let Some(manifest) = spec.manifest
252 && let Some(head) = segments.first()
253 && manifest.self_names.contains(&head.as_str())
254 && let Some(root) = root_of(spec, roots, file)
255 {
256 segments[0] = root.name.clone();
257 }
258 segments
259}
260
261fn module_of(node: &Node, roots: &[ModuleRoot]) -> Vec<String> {
262 let mut module = spec_for_path(&node.file)
263 .map(|s| key_of(s, roots, &node.file))
264 .unwrap_or_default();
265 let qualified = qualified_of(node.id.as_str());
266 if let Some((prefix, _)) = qualified.rsplit_once("::") {
267 module.extend(prefix.split("::").map(str::to_string));
268 }
269 module
270}
271
272struct Prep<'a> {
275 file_module: Vec<String>,
276 qualified: &'a str,
277 prefix: &'a str,
278 functionish: bool,
279 module: Vec<String>,
281}
282
283fn build_index<'a>(
284 nodes: &'a [Node],
285 all_imports: &'a [Reference],
286 locals: &'a [LocalBinding],
287 fields: &'a [FieldBinding],
288 embeds: &'a [Embed],
289 roots: &[ModuleRoot],
290) -> Index<'a> {
291 use rayon::prelude::*;
292 let mut index = Index {
293 by_file_name: HashMap::new(),
294 by_file_qualified: HashMap::new(),
295 file_nodes: HashMap::new(),
296 defs_by_file: HashMap::new(),
297 by_name: HashMap::new(),
298 by_module_tail: HashMap::new(),
299 files_of_module: HashMap::new(),
300 module_defs: HashMap::new(),
301 imports: HashMap::new(),
302 locals: HashMap::new(),
303 fields: HashMap::new(),
304 embeds: HashMap::new(),
305 roots: roots.to_vec(),
306 proto_rpcs: crate::proto_service_bindings::ProtoRpcs::build(nodes),
307 };
308 let mut kind_of: HashMap<(&str, &str), SymbolKind> = HashMap::new();
310 for node in nodes {
311 kind_of.insert(
312 (node.file.as_str(), qualified_of(node.id.as_str())),
313 node.kind,
314 );
315 }
316 let preps: Vec<Option<Prep<'a>>> = nodes
322 .par_iter()
323 .map(|node| {
324 let spec = spec_for_path(&node.file)?;
325 let file_module = key_of(spec, roots, &node.file);
326 if node.kind == SymbolKind::File {
327 return Some(Prep {
328 file_module,
329 qualified: "",
330 prefix: "",
331 functionish: false,
332 module: Vec::new(),
333 });
334 }
335 let qualified = qualified_of(node.id.as_str());
336 let prefix = qualified.rsplit_once("::").map_or("", |(p, _)| p);
337 let functionish =
338 prefix
339 .split("::")
340 .filter(|s| !s.is_empty())
341 .try_fold(String::new(), |acc, seg| {
342 let q = if acc.is_empty() {
343 seg.to_string()
344 } else {
345 format!("{acc}::{seg}")
346 };
347 let kind = kind_of.get(&(node.file.as_str(), q.as_str()));
348 match kind {
349 Some(k) if is_callable(*k) && *k != SymbolKind::Class => Some(q),
350 None => None, Some(_) => None,
352 }
353 });
354 let mut module = file_module.clone();
355 if !prefix.is_empty() {
356 module.extend(prefix.split("::").map(str::to_string));
357 }
358 Some(Prep {
359 file_module,
360 qualified,
361 prefix,
362 functionish: prefix.is_empty() || functionish.is_some(),
363 module,
364 })
365 })
366 .collect();
367 for (node, prep) in nodes.iter().zip(preps) {
369 let Some(prep) = prep else {
370 continue;
371 };
372 let file_module = prep.file_module;
373 if node.kind == SymbolKind::File {
374 index.file_nodes.insert(node.file.as_str(), node);
375 if let Some(tail) = file_module.last() {
376 index
377 .by_module_tail
378 .entry(tail.clone())
379 .or_default()
380 .push((file_module.clone(), node));
381 let entries = index.files_of_module.entry(tail.clone()).or_default();
382 match entries.iter_mut().find(|m| m.key == file_module) {
383 Some(m) => m.files.push(&node.file),
384 None => entries.push(ModuleFiles {
385 key: file_module,
386 files: vec![&node.file],
387 }),
388 }
389 }
390 continue;
391 }
392 index
393 .by_file_qualified
394 .insert((node.file.as_str(), prep.qualified), node);
395 index
396 .defs_by_file
397 .entry(node.file.as_str())
398 .or_default()
399 .push(node);
400 index
401 .by_file_name
402 .entry((node.file.as_str(), node.name.as_str()))
403 .or_default()
404 .push(FileDef {
405 node,
406 prefix: prep.prefix,
407 functionish: prep.functionish,
408 });
409 index
410 .by_name
411 .entry(node.name.as_str())
412 .or_default()
413 .push((prep.module, node));
414 if prep.prefix.is_empty() {
415 index
416 .module_defs
417 .entry(file_module)
418 .or_default()
419 .entry(node.name.as_str())
420 .or_default()
421 .push(node);
422 }
423 }
424 for r in all_imports {
425 let Some(spec) = spec_for_path(&r.file) else {
426 continue;
427 };
428 let glob = matches!(r.alias.as_deref(), Some("*") | Some("."));
429 let raw = strip_glob(&r.name);
430 let segments = expand(spec, roots, &r.file, (spec.absolutize)(raw, &r.file));
431 let binding = match (&r.alias, glob) {
432 (Some(alias), false) => alias.clone(),
433 _ => segments.last().cloned().unwrap_or_default(),
434 };
435 index
436 .imports
437 .entry(r.file.as_str())
438 .or_default()
439 .push(Import {
440 segments,
441 binding,
442 glob,
443 });
444 }
445 for l in locals {
446 index
447 .locals
448 .entry((l.file.as_str(), l.name.as_str()))
449 .or_default()
450 .push(LocalRange {
451 start: l.span.start,
452 scope_end: l.scope_end,
453 type_name: l.type_name.as_deref(),
454 });
455 }
456 for field in fields {
457 index
458 .fields
459 .entry(field.owner.as_str())
460 .or_default()
461 .push(field);
462 }
463 for e in embeds {
464 index
465 .embeds
466 .entry(e.owner.as_str())
467 .or_default()
468 .push(&e.type_name);
469 }
470 index
471}
472
473fn strip_glob(name: &str) -> &str {
474 name.strip_suffix('*')
475 .map(|s| s.trim_end_matches(['.', ':', '/']))
476 .unwrap_or(name)
477}
478
479impl<'a> Index<'a> {
480 pub fn build(
483 nodes: &'a [Node],
484 all_imports: &'a [Reference],
485 locals: &'a [LocalBinding],
486 fields: &'a [FieldBinding],
487 embeds: &'a [Embed],
488 roots: &[ModuleRoot],
489 ) -> Index<'a> {
490 let t = std::time::Instant::now();
491 let index = build_index(nodes, all_imports, locals, fields, embeds, roots);
492 if std::env::var_os("SINTER_TIMING").is_some() {
493 eprintln!("index build: {:?}", t.elapsed());
494 }
495 index
496 }
497
498 fn local_at(&self, file: &str, name: &str, at: u64) -> Option<Option<&'a str>> {
500 self.locals
501 .get(&(file, name))
502 .into_iter()
503 .flatten()
504 .filter(|l| l.start <= at && at < l.scope_end)
505 .map(|l| l.type_name)
506 .next_back()
507 }
508
509 fn type_def(&self, file: &str, module: &[String], name: &str) -> Option<&'a Node> {
511 let same_file: Vec<&Node> = self
512 .by_file_name
513 .get(&(file, name))
514 .into_iter()
515 .flatten()
516 .filter(|d| is_member_scope(d.node.kind))
517 .map(|d| d.node)
518 .collect();
519 if let [node] = same_file.as_slice() {
520 return Some(node);
521 }
522 let in_module: Vec<&Node> = self
523 .module_defs
524 .get(module)
525 .and_then(|m| m.get(name))
526 .into_iter()
527 .flatten()
528 .filter(|n| is_member_scope(n.kind))
529 .copied()
530 .collect();
531 match in_module.as_slice() {
532 [node] => Some(node),
533 _ => None,
534 }
535 }
536
537 fn visible_types(&self, file: &str, module: &[String], written: &str) -> Vec<&'a Node> {
542 let mut found = Vec::new();
543 for candidate in type_candidates(written) {
544 if let Some(node) = self.type_def(file, module, candidate) {
545 found.push(node);
546 continue;
547 }
548 let imported: Vec<&Node> = self
549 .imports
550 .get(file)
551 .into_iter()
552 .flatten()
553 .filter(|imp| !imp.glob && imp.binding == candidate)
554 .filter_map(|imp| self.resolve_path_defs(&imp.segments, 4))
555 .filter(|n| is_member_scope(n.kind))
556 .collect();
557 if let [node] = imported.as_slice() {
558 found.push(*node);
559 }
560 }
561 found.sort_by_key(|node| node.id.as_str());
562 found.dedup_by_key(|node| node.id.as_str());
563 found
564 }
565
566 fn member_of_written_type(
570 &self,
571 file: &str,
572 module: &[String],
573 written: &str,
574 member: &str,
575 ) -> (Option<&'a Node>, bool) {
576 let types = self.visible_types(file, module, written);
577 let mut members: Vec<&Node> = types
578 .iter()
579 .filter_map(|ty| self.member_of(ty, member, 4))
580 .collect();
581 members.sort_by_key(|node| node.id.as_str());
582 members.dedup_by_key(|node| node.id.as_str());
583 let target = match members.as_slice() {
584 [member] => Some(*member),
585 _ => None,
586 };
587 (target, !types.is_empty())
588 }
589
590 fn field(&self, owner: &Node, name: &str) -> Option<&'a FieldBinding> {
591 let matching: Vec<&FieldBinding> = self
592 .fields
593 .get(owner.id.as_str())
594 .into_iter()
595 .flatten()
596 .filter(|f| f.name == name)
597 .copied()
598 .collect();
599 match matching.as_slice() {
600 [field] => Some(*field),
601 _ => None,
602 }
603 }
604
605 fn member_of(&self, ty: &'a Node, name: &str, depth: usize) -> Option<&'a Node> {
607 if depth == 0 {
608 return None;
609 }
610 let mut module = module_of(ty, &self.roots);
611 module.extend(
612 qualified_of(ty.id.as_str())
613 .rsplit("::")
614 .next()
615 .map(str::to_string),
616 );
617 let direct: Vec<&Node> = self
618 .by_name
619 .get(name)
620 .into_iter()
621 .flatten()
622 .filter(|(m, _)| *m == module)
623 .map(|(_, n)| *n)
624 .collect();
625 if let [node] = direct.as_slice() {
626 return Some(node);
627 }
628 let in_type_file: Vec<&Node> = direct
632 .iter()
633 .filter(|n| n.file == ty.file)
634 .copied()
635 .collect();
636 if let [node] = in_type_file.as_slice() {
637 return Some(node);
638 }
639 let spec = spec_for_path(&ty.file)?;
640 let file_module = key_of(spec, &self.roots, &ty.file);
641 for embedded in self.embeds.get(ty.id.as_str()).into_iter().flatten() {
642 if let Some(embedded_ty) = self.type_def(&ty.file, &file_module, embedded)
643 && let Some(node) = self.member_of(embedded_ty, name, depth - 1)
644 {
645 return Some(node);
646 }
647 }
648 None
649 }
650
651 fn anchored(&self, segments: &[String]) -> bool {
654 let module_hit = |segs: &[String]| {
655 segs.last().is_some_and(|tail| {
656 self.files_of_module
657 .get(tail.as_str())
658 .into_iter()
659 .flatten()
660 .any(|m| suffix_len(&m.key, segs).is_some())
661 || self
662 .by_module_tail
663 .get(tail.as_str())
664 .into_iter()
665 .flatten()
666 .any(|(key, _)| suffix_len(key, segs).is_some())
667 })
668 };
669 if module_hit(segments) {
670 return true;
671 }
672 match segments.split_last() {
673 Some((_, module)) if !module.is_empty() => module_hit(module),
674 _ => false,
675 }
676 }
677
678 fn import_file(&self, segments: &[String]) -> Option<&'a Node> {
685 unique_best(
686 self.by_module_tail
687 .get(segments.last()?.as_str())
688 .into_iter()
689 .flatten()
690 .filter_map(|(key, node)| {
691 let len = suffix_len(key, segments).or_else(|| suffix_len(segments, key))?;
692 Some((len, *node))
693 }),
694 )
695 }
696
697 fn resolve_path(&self, segments: &[String], depth: usize) -> Option<&'a Node> {
700 self.resolve_path_defs(segments, depth).or_else(|| {
701 let files = self
705 .by_module_tail
706 .get(segments.last()?.as_str())
707 .into_iter()
708 .flatten()
709 .filter_map(|(key, node)| Some((suffix_len(key, segments)?, *node)));
710 unique_best(files)
711 })
712 }
713
714 fn resolve_path_defs(&self, segments: &[String], depth: usize) -> Option<&'a Node> {
720 if segments.is_empty() || depth == 0 {
721 return None;
722 }
723 if let Some((name, module)) = segments.split_last() {
724 let defs = self
725 .by_name
726 .get(name.as_str())
727 .into_iter()
728 .flatten()
729 .filter_map(|(key, node)| Some((suffix_len(key, module)?, *node)));
730 if let Some(node) = unique_best(defs) {
731 return Some(node);
732 }
733 if !module.is_empty() {
736 let mut chained: Vec<&Node> = Vec::new();
737 let tail = module.last().map(String::as_str).unwrap_or("");
738 for m in self.files_of_module.get(tail).into_iter().flatten() {
739 if suffix_len(&m.key, module).is_none() {
740 continue;
741 }
742 for file in &m.files {
743 for import in self.imports.get(*file).into_iter().flatten() {
744 if import.binding == *name && !import.glob {
745 chained.extend(self.resolve_path(&import.segments, depth - 1));
746 } else if import.glob {
747 let mut deeper = import.segments.clone();
748 deeper.push(name.clone());
749 chained.extend(self.resolve_path(&deeper, depth - 1));
750 }
751 }
752 }
753 }
754 chained.sort_by_key(|n| n.id.as_str().to_string());
755 chained.dedup_by_key(|n| n.id.as_str().to_string());
756 if let [node] = chained.as_slice() {
757 return Some(node);
758 }
759 }
760 }
761 None
762 }
763}
764
765const TYPE_KEYWORDS: &[&str] = &[
770 "dyn", "impl", "mut", "const", "ref", "crate", "self", "super", "std", "core", "alloc",
771];
772
773fn type_tokens(text: &str) -> impl DoubleEndedIterator<Item = &str> {
774 text.split(|c: char| !(c.is_alphanumeric() || c == '_'))
775 .filter(|token| {
776 !token.is_empty()
777 && !token.chars().next().is_some_and(char::is_numeric)
778 && !TYPE_KEYWORDS.contains(token)
779 })
780}
781
782fn type_candidates(written: &str) -> Vec<&str> {
783 const DEREF_WRAPPERS: &[&str] = &["Box", "Arc", "Rc", "Pin", "Cow"];
787 let (head_text, arguments) = written
788 .split_once('<')
789 .map_or((written, None), |(head, rest)| (head, Some(rest)));
790 let head = type_tokens(head_text).next_back();
791 if let Some(head) = head
792 && !DEREF_WRAPPERS.contains(&head)
793 {
794 return vec![head];
795 }
796 let mut out = Vec::new();
797 for token in type_tokens(arguments.unwrap_or(written)).rev() {
798 if DEREF_WRAPPERS.contains(&token) {
799 continue;
800 }
801 if !out.contains(&token) {
802 out.push(token);
803 }
804 }
805 out
806}
807
808fn namespace_pick(candidates: Vec<&Node>, relation: Relation) -> Option<&Node> {
811 match candidates.as_slice() {
812 [node] => Some(node),
813 [] => None,
814 _ => {
815 let preferred: Vec<&Node> = candidates
816 .iter()
817 .filter(|n| match relation {
818 Relation::Calls => is_callable(n.kind),
819 Relation::Uses => is_type_kind(n.kind),
820 _ => true,
821 })
822 .copied()
823 .collect();
824 match preferred.as_slice() {
825 [node] => Some(node),
826 _ => None,
827 }
828 }
829 }
830}
831
832pub fn resolve(
833 index: &Index<'_>,
834 references: &[Reference],
835) -> (Vec<Binding>, ResolutionStats, Vec<usize>) {
836 use rayon::prelude::*;
837 let results: Vec<Res> = references
838 .par_iter()
839 .enumerate()
840 .map(|(i, r)| {
841 let Some(spec) = spec_for_path(&r.file) else {
842 return Res::External;
843 };
844 let src = r
845 .enclosing
846 .clone()
847 .unwrap_or_else(|| NodeId::new(r.file.clone()));
848 let file_module = key_of(spec, &index.roots, &r.file);
849 let imports = index.imports.get(r.file.as_str());
850 let (target, evidence, internal) = resolve_one(index, spec, r, &file_module, imports);
851 match target {
852 Some(node) if node.id != src => {
853 let relation = if r.relation == Relation::Calls && is_type_kind(node.kind) {
856 Relation::Uses
857 } else {
858 r.relation
859 };
860 Res::Bound(Binding {
861 edge: Edge {
862 src,
863 dst: node.id.clone(),
864 relation,
865 evidence,
866 confidence: if evidence == Evidence::Declared {
869 Confidence::Inferred
870 } else {
871 evidence.confidence()
872 },
873 site: Some(r.span),
874 },
875 reference: i,
876 })
877 }
878 _ if internal => Res::Internal,
879 _ => Res::External,
880 }
881 })
882 .collect();
883 let mut bindings = Vec::new();
884 let mut stats = ResolutionStats::default();
885 let mut internal_indices = Vec::new();
886 for (i, result) in results.into_iter().enumerate() {
887 match result {
888 Res::Bound(binding) => {
889 match binding.edge.evidence {
890 Evidence::Scope => stats.scope += 1,
891 _ => stats.import += 1,
892 }
893 bindings.push(binding);
894 }
895 Res::Internal => {
896 stats.unresolved_internal += 1;
897 internal_indices.push(i);
898 }
899 Res::External => stats.unresolved_external += 1,
900 }
901 }
902 (bindings, stats, internal_indices)
903}
904
905fn resolve_one<'a>(
906 index: &Index<'a>,
907 spec: &sinter_extract::LanguageSpec,
908 r: &Reference,
909 file_module: &[String],
910 imports: Option<&Vec<Import>>,
911) -> (Option<&'a Node>, Evidence, bool) {
912 if r.relation == Relation::Imports {
913 return resolve_import_reference(index, spec, r);
914 }
915
916 if let Some(path) = &r.path {
917 let (target, evidence, internal) =
918 resolve_qualified_reference(index, spec, r, file_module, imports, path);
919 if target.is_none()
920 && r.relation == Relation::Calls
921 && !index.proto_rpcs.is_empty()
922 && let Some(rpc) = proto_client_call(index, spec, r, file_module, imports, path)
923 {
924 return (Some(rpc), Evidence::Declared, true);
925 }
926 return (target, evidence, internal);
927 }
928
929 resolve_bare_reference(index, r, file_module, imports)
930}
931
932fn proto_client_call<'a>(
937 index: &Index<'a>,
938 spec: &LanguageSpec,
939 r: &Reference,
940 file_module: &[String],
941 imports: Option<&Vec<Import>>,
942 path: &str,
943) -> Option<&'a Node> {
944 let segments = expand(
945 spec,
946 &index.roots,
947 &r.file,
948 (spec.absolutize)(path, &r.file),
949 );
950 let prefix = segments.get(segments.len().checked_sub(2)?)?;
951 let field_type = || {
952 let enclosing = r.enclosing.as_ref()?;
953 let (type_prefix, _) = qualified_of(enclosing.as_str()).rsplit_once("::")?;
954 let name = type_prefix.rsplit("::").next().unwrap_or(type_prefix);
955 let owner = index
956 .by_file_qualified
957 .get(&(r.file.as_str(), type_prefix))
958 .copied()
959 .or_else(|| index.type_def(&r.file, file_module, name))?;
960 Some(index.field(owner, prefix)?.type_name.as_str())
961 };
962 let receiver_type = if segments.len() >= 3
963 && spec
964 .receivers
965 .contains(&segments[segments.len() - 3].as_str())
966 {
967 field_type()
968 } else {
969 index.local_at(&r.file, prefix, r.span.start).flatten()
970 };
971 let tokens = imports.into_iter().flatten().flat_map(|imp| {
972 imp.segments
973 .iter()
974 .map(String::as_str)
975 .chain([imp.binding.as_str()])
976 });
977 index.proto_rpcs.client_call(&r.name, receiver_type, tokens)
978}
979
980fn resolve_import_reference<'a>(
983 index: &Index<'a>,
984 spec: &LanguageSpec,
985 r: &Reference,
986) -> (Option<&'a Node>, Evidence, bool) {
987 let glob = matches!(r.alias.as_deref(), Some("*") | Some("."));
988 let raw = strip_glob(&r.name);
989 if let Some(node) = index
993 .file_nodes
994 .get(raw.trim().trim_matches(['<', '>', '"']))
995 {
996 return (Some(node), Evidence::Import, true);
997 }
998 let segments = expand(spec, &index.roots, &r.file, (spec.absolutize)(raw, &r.file));
999 let target = if glob {
1000 index.import_file(&segments)
1001 } else {
1002 index.resolve_path(&segments, 4)
1003 };
1004 let internal = target.is_some() || index.anchored(&segments);
1005 (target, Evidence::Import, internal)
1006}
1007
1008fn resolve_qualified_reference<'a>(
1011 index: &Index<'a>,
1012 spec: &LanguageSpec,
1013 r: &Reference,
1014 file_module: &[String],
1015 imports: Option<&Vec<Import>>,
1016 path: &str,
1017) -> (Option<&'a Node>, Evidence, bool) {
1018 if spec.file_refs {
1021 return resolve_file_ref(index, spec, r, path);
1022 }
1023 let segments = expand(
1026 spec,
1027 &index.roots,
1028 &r.file,
1029 (spec.absolutize)(path, &r.file),
1030 );
1031 let prefix = segments
1032 .len()
1033 .checked_sub(2)
1034 .and_then(|p| segments.get(p))
1035 .cloned();
1036 let Some(prefix) = prefix else {
1037 return (None, Evidence::Import, false);
1038 };
1039 if segments.len() >= 3
1043 && spec
1044 .receivers
1045 .contains(&segments[segments.len() - 3].as_str())
1046 && let Some(enclosing) = &r.enclosing
1047 && let Some((type_prefix, _)) = qualified_of(enclosing.as_str()).rsplit_once("::")
1048 {
1049 let owner = index
1050 .by_file_qualified
1051 .get(&(r.file.as_str(), type_prefix))
1052 .copied()
1053 .or_else(|| {
1054 let name = type_prefix.rsplit("::").next().unwrap_or(type_prefix);
1055 index.type_def(&r.file, file_module, name)
1056 });
1057 if let Some(owner) = owner
1058 && let Some(field) = index.field(owner, &segments[segments.len() - 2])
1059 {
1060 let field_spec = spec_for_path(&owner.file).unwrap_or(spec);
1061 let field_module = key_of(field_spec, &index.roots, &owner.file);
1062 let (target, anchored) =
1063 index.member_of_written_type(&owner.file, &field_module, &field.type_name, &r.name);
1064 return (target, Evidence::Scope, anchored);
1065 }
1066 }
1067 if spec.receivers.contains(&prefix.as_str())
1068 && let Some(enclosing) = &r.enclosing
1069 && let Some((type_prefix, _)) = qualified_of(enclosing.as_str()).rsplit_once("::")
1070 {
1071 let sibling = format!("{type_prefix}::{}", r.name);
1075 if let Some(node) = index
1076 .by_file_qualified
1077 .get(&(r.file.as_str(), sibling.as_str()))
1078 {
1079 return (Some(node), Evidence::Scope, true);
1080 }
1081 if let Some(ty) = index.by_file_qualified.get(&(r.file.as_str(), type_prefix)) {
1082 return (index.member_of(ty, &r.name, 4), Evidence::Scope, true);
1084 }
1085 }
1086 match index.local_at(&r.file, &prefix, r.span.start) {
1087 Some(Some(type_name)) => {
1088 let (target, anchored) =
1089 index.member_of_written_type(&r.file, file_module, type_name, &r.name);
1090 return (target, Evidence::Scope, anchored);
1092 }
1093 Some(None) => return (None, Evidence::Scope, false), None => {}
1095 }
1096 if let Some(ty) = index.type_def(&r.file, file_module, &prefix)
1098 && let Some(node) = index.member_of(ty, &r.name, 4)
1099 {
1100 return (Some(node), Evidence::Scope, true);
1101 }
1102 if let Some(node) = index.resolve_path_defs(&segments, 4) {
1103 return (Some(node), Evidence::Import, true);
1104 }
1105 if let Some((leaf, type_path)) = segments.split_last()
1111 && type_path.len() >= 2
1112 && let Some(ty) = index.resolve_path_defs(type_path, 4)
1113 && let Some(node) = index.member_of(ty, leaf, 4)
1114 {
1115 return (Some(node), Evidence::Import, true);
1116 }
1117 let matching: Vec<&Import> = imports
1118 .into_iter()
1119 .flatten()
1120 .filter(|imp| !imp.glob && imp.binding == prefix)
1121 .collect();
1122 let candidates: Vec<&Node> = matching
1123 .iter()
1124 .filter_map(|imp| {
1125 let mut full = imp.segments.clone();
1126 full.push(r.name.clone());
1127 index.resolve_path(&full, 4)
1128 })
1129 .collect();
1130 let internal = candidates.len() > 1
1131 || index.anchored(&segments)
1132 || matching.iter().any(|imp| index.anchored(&imp.segments));
1133 match candidates.as_slice() {
1134 [node] => (Some(node), Evidence::Import, true),
1135 _ => (None, Evidence::Import, internal),
1136 }
1137}
1138
1139fn resolve_bare_reference<'a>(
1142 index: &Index<'a>,
1143 r: &Reference,
1144 file_module: &[String],
1145 imports: Option<&Vec<Import>>,
1146) -> (Option<&'a Node>, Evidence, bool) {
1147 if index.local_at(&r.file, &r.name, r.span.start).is_some() {
1148 return (None, Evidence::Scope, false); }
1150 let enclosing_q = r
1151 .enclosing
1152 .as_ref()
1153 .map(|e| qualified_of(e.as_str()))
1154 .unwrap_or("");
1155 let visible: Vec<&Node> = index
1156 .by_file_name
1157 .get(&(r.file.as_str(), r.name.as_str()))
1158 .into_iter()
1159 .flatten()
1160 .filter(|d| {
1161 d.prefix.is_empty()
1162 || (d.functionish
1163 && (enclosing_q == d.prefix
1164 || enclosing_q.starts_with(&format!("{}::", d.prefix))))
1165 })
1166 .map(|d| d.node)
1167 .collect();
1168 if !visible.is_empty() {
1169 return (namespace_pick(visible, r.relation), Evidence::Scope, true);
1171 }
1172 if let Some(defs) = index
1173 .module_defs
1174 .get(file_module)
1175 .and_then(|m| m.get(r.name.as_str()))
1176 {
1177 return (
1178 namespace_pick(defs.clone(), r.relation),
1179 Evidence::Scope,
1180 true,
1181 );
1182 }
1183 let named: Vec<&Node> = imports
1184 .into_iter()
1185 .flatten()
1186 .filter(|imp| !imp.glob && imp.binding == r.name)
1187 .filter_map(|imp| index.resolve_path(&imp.segments, 4))
1188 .collect();
1189 let (target, internal) = match named.as_slice() {
1190 [node] => (Some(*node), true),
1191 [] => {
1192 let globbed: Vec<&Node> = imports
1193 .into_iter()
1194 .flatten()
1195 .filter(|imp| imp.glob)
1196 .filter_map(|imp| {
1197 let mut full = imp.segments.clone();
1198 full.push(r.name.clone());
1199 index.resolve_path(&full, 4).or_else(|| {
1200 let file = index.import_file(&imp.segments)?;
1203 index
1204 .by_file_name
1205 .get(&(file.file.as_str(), r.name.as_str()))
1206 .into_iter()
1207 .flatten()
1208 .find(|d| d.prefix.is_empty())
1209 .map(|d| d.node)
1210 })
1211 })
1212 .collect();
1213 let name_imports_anchored = imports
1214 .into_iter()
1215 .flatten()
1216 .filter(|imp| !imp.glob && imp.binding == r.name)
1217 .any(|imp| index.anchored(&imp.segments));
1218 match globbed.as_slice() {
1219 [node] => (Some(*node), true),
1220 [] => (None, name_imports_anchored),
1221 _ => (None, true), }
1223 }
1224 _ => (None, true), };
1226 (target, Evidence::Import, internal)
1227}
1228
1229fn resolve_file_ref<'a>(
1237 index: &Index<'a>,
1238 spec: &LanguageSpec,
1239 r: &Reference,
1240 path: &str,
1241) -> (Option<&'a Node>, Evidence, bool) {
1242 let (head, frag) = match path.split_once('#') {
1243 Some((h, f)) => (h, Some(f)),
1244 None => (path, None),
1245 };
1246 let file = if head.is_empty() {
1247 index.file_nodes.get(r.file.as_str()).copied()
1249 } else {
1250 let joined = (spec.absolutize)(head, &r.file).join("/");
1251 index.file_nodes.get(joined.as_str()).copied().or_else(|| {
1252 spec.extensions.iter().find_map(|ext| {
1253 index
1254 .file_nodes
1255 .get(format!("{joined}.{ext}").as_str())
1256 .copied()
1257 })
1258 })
1259 };
1260 match (file, frag) {
1261 (Some(file), None) => (Some(file), Evidence::Import, true),
1262 (Some(file), Some(frag)) => {
1263 let matching: Vec<&Node> = index
1264 .defs_by_file
1265 .get(file.file.as_str())
1266 .into_iter()
1267 .flatten()
1268 .filter(|n| slugify(&n.name) == frag)
1269 .copied()
1270 .collect();
1271 match matching.as_slice() {
1274 [node] => (Some(node), Evidence::Import, true),
1275 _ => (None, Evidence::Import, true),
1276 }
1277 }
1278 (None, _) => (None, Evidence::Import, false),
1279 }
1280}
1281
1282fn slugify(name: &str) -> String {
1285 name.chars()
1286 .filter_map(|c| match c {
1287 ' ' => Some('-'),
1288 '-' | '_' => Some(c),
1289 c if c.is_alphanumeric() => Some(c.to_ascii_lowercase()),
1290 _ => None,
1291 })
1292 .collect()
1293}
1294
1295pub fn dynamic_edges(index: &Index<'_>, nodes: &[Node], trait_impls: &[TraitImpl]) -> Vec<Edge> {
1303 let mut edges = crate::proto_service_bindings::proto_service_edges(nodes, trait_impls);
1306 let implicit = nodes
1307 .iter()
1308 .any(|n| spec_for_path(&n.file).is_some_and(|s| s.implicit_interfaces));
1309 if trait_impls.is_empty() && !implicit {
1310 return edges;
1311 }
1312 let roots = &index.roots;
1313 let mut by_file: HashMap<&str, Vec<&Node>> = HashMap::new();
1314 let mut types_by_file: HashMap<&str, Vec<&Node>> = HashMap::new();
1315 for n in nodes {
1316 if is_callable(n.kind) {
1317 by_file.entry(n.file.as_str()).or_default().push(n);
1318 }
1319 if is_member_scope(n.kind) {
1320 types_by_file.entry(n.file.as_str()).or_default().push(n);
1321 }
1322 }
1323 let is_trait = |n: &Node| {
1327 matches!(
1328 n.kind,
1329 SymbolKind::Trait | SymbolKind::Interface | SymbolKind::Class
1330 )
1331 };
1332 for ti in trait_impls {
1333 let Some(spec) = spec_for_path(&ti.file) else {
1334 continue;
1335 };
1336 let file_module = key_of(spec, roots, &ti.file);
1337 let trait_node = index
1338 .type_def(&ti.file, &file_module, &ti.trait_name)
1339 .filter(|n| is_trait(n))
1340 .map(|n| (n, Evidence::Scope))
1341 .or_else(|| {
1342 let named: Vec<&Node> = index
1344 .imports
1345 .get(ti.file.as_str())
1346 .into_iter()
1347 .flatten()
1348 .filter(|imp| !imp.glob && imp.binding == ti.trait_name)
1349 .filter_map(|imp| index.resolve_path_defs(&imp.segments, 4))
1350 .filter(|n| is_trait(n))
1351 .collect();
1352 match named.as_slice() {
1353 [node] => Some((node, Evidence::Import)),
1354 _ => None,
1355 }
1356 })
1357 .or_else(|| {
1358 let globbed: Vec<&Node> = index
1361 .imports
1362 .get(ti.file.as_str())
1363 .into_iter()
1364 .flatten()
1365 .filter(|imp| imp.glob)
1366 .filter_map(|imp| {
1367 let mut full = imp.segments.clone();
1368 full.push(ti.trait_name.clone());
1369 index.resolve_path_defs(&full, 4)
1370 })
1371 .filter(|n| is_trait(n))
1372 .collect();
1373 match globbed.as_slice() {
1374 [node] => Some((node, Evidence::Import)),
1375 _ => None,
1376 }
1377 });
1378 let Some((trait_node, pair_evidence)) = trait_node else {
1379 continue; };
1381 let mut impl_methods: Vec<&Node> = Vec::new();
1382 for method in by_file.get(ti.file.as_str()).into_iter().flatten() {
1383 if !(ti.span.start <= method.span.start && method.span.end <= ti.span.end) {
1384 continue;
1385 }
1386 impl_methods.push(method);
1387 if let Some(trait_method) = index.member_of(trait_node, &method.name, 1)
1388 && trait_method.id != method.id
1389 {
1390 edges.push(Edge {
1391 src: trait_method.id.clone(),
1392 dst: method.id.clone(),
1393 relation: Relation::Calls,
1394 evidence: Evidence::Dynamic,
1395 confidence: Evidence::Dynamic.confidence(),
1396 site: None,
1398 });
1399 }
1400 }
1401 let impl_type = types_by_file
1408 .get(ti.file.as_str())
1409 .into_iter()
1410 .flatten()
1411 .find(|n| n.span == ti.span)
1412 .copied()
1413 .or_else(|| {
1414 let prefix = impl_methods.iter().find_map(|m| {
1415 let q = qualified_of(m.id.as_str());
1416 q.rsplit_once("::")
1417 .map(|(p, _)| p.rsplit("::").next().unwrap_or(p))
1418 })?;
1419 index.type_def(&ti.file, &file_module, prefix)
1420 });
1421 if let Some(impl_type) = impl_type
1422 && impl_type.id != trait_node.id
1423 {
1424 let relation = if impl_type.kind == trait_node.kind {
1425 Relation::Extends
1426 } else {
1427 Relation::Implements
1428 };
1429 edges.push(Edge {
1430 src: impl_type.id.clone(),
1431 dst: trait_node.id.clone(),
1432 relation,
1433 evidence: pair_evidence,
1434 confidence: pair_evidence.confidence(),
1435 site: None,
1439 });
1440 }
1441 }
1442 if implicit {
1443 edges.extend(implicit_interface_edges(nodes, roots));
1444 }
1445 edges.sort();
1446 edges.dedup();
1447 edges
1448}
1449
1450fn implicit_interface_edges(nodes: &[Node], roots: &[ModuleRoot]) -> Vec<Edge> {
1460 let mut types: HashMap<(Vec<String>, &str), Vec<&Node>> = HashMap::new();
1463 let mut types_by_key: HashMap<Vec<String>, Vec<&Node>> = HashMap::new();
1464 let mut methods: HashMap<(Vec<String>, &str), Vec<&Node>> = HashMap::new();
1465 for n in nodes {
1466 let Some(spec) = spec_for_path(&n.file) else {
1467 continue;
1468 };
1469 if !spec.implicit_interfaces {
1470 continue;
1471 }
1472 let key = key_of(spec, roots, &n.file);
1473 match n.kind {
1474 SymbolKind::Interface | SymbolKind::Struct | SymbolKind::TypeAlias => {
1475 types
1476 .entry((key.clone(), n.name.as_str()))
1477 .or_default()
1478 .push(n);
1479 types_by_key.entry(key).or_default().push(n);
1480 }
1481 SymbolKind::Method => {
1482 let q = qualified_of(n.id.as_str());
1483 if let Some((owner, _)) = q.rsplit_once("::")
1484 && !owner.contains("::")
1485 {
1486 methods.entry((key, owner)).or_default().push(n);
1487 }
1488 }
1489 _ => {}
1490 }
1491 }
1492 let mut edges = Vec::new();
1493 for ((key, name), candidates) in &types {
1494 let [iface] = candidates.as_slice() else {
1496 continue;
1497 };
1498 if iface.kind != SymbolKind::Interface {
1499 continue;
1500 }
1501 let Some(iface_methods) = methods.get(&(key.clone(), *name)) else {
1502 continue; };
1504 for ty in types_by_key.get(key).into_iter().flatten() {
1505 if ty.kind == SymbolKind::Interface {
1506 continue;
1507 }
1508 let ty_methods = methods.get(&(key.clone(), ty.name.as_str()));
1509 let covers = |m: &Node| ty_methods.into_iter().flatten().any(|tm| tm.name == m.name);
1510 if !iface_methods.iter().all(|m| covers(m)) {
1511 continue;
1512 }
1513 for im in iface_methods {
1514 for tm in ty_methods.into_iter().flatten() {
1515 if tm.name == im.name {
1516 edges.push(Edge {
1517 src: im.id.clone(),
1518 dst: tm.id.clone(),
1519 relation: Relation::Calls,
1520 evidence: Evidence::Dynamic,
1521 confidence: Evidence::Dynamic.confidence(),
1522 site: None,
1523 });
1524 }
1525 }
1526 }
1527 edges.push(Edge {
1528 src: ty.id.clone(),
1529 dst: iface.id.clone(),
1530 relation: Relation::Implements,
1531 evidence: Evidence::Dynamic,
1532 confidence: Evidence::Dynamic.confidence(),
1533 site: None,
1534 });
1535 }
1536 }
1537 edges
1538}
1539
1540pub fn resolve_boundary(
1547 foreign_nodes: &[Node],
1548 references: &[Reference],
1549 owner_imports: &[Reference],
1550) -> Vec<Binding> {
1551 let index = build_index(foreign_nodes, owner_imports, &[], &[], &[], &[]);
1552 let mut bindings = Vec::new();
1553 for (i, r) in references.iter().enumerate() {
1554 let Some(spec) = spec_for_path(&r.file) else {
1555 continue;
1556 };
1557 let src = r
1558 .enclosing
1559 .clone()
1560 .unwrap_or_else(|| NodeId::new(r.file.clone()));
1561 let imports = index.imports.get(r.file.as_str());
1562 let target = if r.relation == Relation::Imports {
1563 let glob = matches!(r.alias.as_deref(), Some("*") | Some("."));
1564 let segments = (spec.absolutize)(strip_glob(&r.name), &r.file);
1565 if glob {
1566 index.import_file(&segments)
1567 } else {
1568 index.resolve_path(&segments, 4)
1569 }
1570 } else if let Some(path) = &r.path {
1571 let segments = (spec.absolutize)(path, &r.file);
1572 let direct = index.resolve_path(&segments, 4);
1573 direct.or_else(|| {
1574 let prefix = segments
1575 .len()
1576 .checked_sub(2)
1577 .and_then(|p| segments.get(p))?;
1578 let candidates: Vec<&Node> = imports
1579 .into_iter()
1580 .flatten()
1581 .filter(|imp| !imp.glob && imp.binding == *prefix)
1582 .filter_map(|imp| {
1583 let mut full = imp.segments.clone();
1584 full.push(r.name.clone());
1585 index.resolve_path(&full, 4)
1586 })
1587 .collect();
1588 match candidates.as_slice() {
1589 [node] => Some(node),
1590 _ => None,
1591 }
1592 })
1593 } else {
1594 let named: Vec<&Node> = imports
1596 .into_iter()
1597 .flatten()
1598 .filter(|imp| !imp.glob && imp.binding == r.name)
1599 .filter_map(|imp| index.resolve_path(&imp.segments, 4))
1600 .collect();
1601 match named.as_slice() {
1602 [node] => Some(*node),
1603 _ => None,
1604 }
1605 };
1606 if let Some(node) = target
1607 && node.id != src
1608 {
1609 let relation = if r.relation == Relation::Calls && is_type_kind(node.kind) {
1610 Relation::Uses
1611 } else {
1612 r.relation
1613 };
1614 bindings.push(Binding {
1615 edge: Edge {
1616 src,
1617 dst: node.id.clone(),
1618 relation,
1619 evidence: Evidence::Import,
1620 confidence: Evidence::Import.confidence(),
1621 site: Some(r.span),
1622 },
1623 reference: i,
1624 });
1625 }
1626 }
1627 bindings
1628}
1629
1630fn suffix_len(key: &[String], path: &[String]) -> Option<usize> {
1632 (!key.is_empty() && path.len() >= key.len() && path[path.len() - key.len()..] == key[..])
1633 .then_some(key.len())
1634}
1635
1636fn unique_best<'a>(candidates: impl Iterator<Item = (usize, &'a Node)>) -> Option<&'a Node> {
1638 let mut best: Option<(usize, Vec<&Node>)> = None;
1639 for (len, node) in candidates {
1640 match &mut best {
1641 Some((best_len, nodes)) if len == *best_len => nodes.push(node),
1642 Some((best_len, nodes)) if len > *best_len => {
1643 *best_len = len;
1644 nodes.clear();
1645 nodes.push(node);
1646 }
1647 None => best = Some((len, vec![node])),
1648 _ => {}
1649 }
1650 }
1651 match best {
1652 Some((_, nodes)) if nodes.len() == 1 => Some(nodes[0]),
1653 _ => None,
1654 }
1655}
1656
1657#[cfg(test)]
1658mod resolution_stats_tests {
1659 use super::{ResolutionStats, type_candidates};
1660
1661 #[test]
1662 fn anchored_rate_is_absent_when_the_pass_measured_nothing() {
1663 assert_eq!(ResolutionStats::default().anchored_unresolved_rate(), None);
1664 }
1665
1666 #[test]
1667 fn anchored_rate_excludes_external_references() {
1668 let stats = ResolutionStats {
1669 scope: 4,
1670 import: 3,
1671 scip: 42,
1672 compiler_rescued_internal: 2,
1673 unresolved_internal: 1,
1674 unresolved_external: 90,
1675 ..ResolutionStats::default()
1676 };
1677
1678 assert_eq!(stats.anchored_unresolved_rate(), Some(0.1));
1679 }
1680
1681 #[test]
1682 fn written_type_unwraps_only_receiver_transparent_wrappers() {
1683 assert_eq!(type_candidates("&Dog"), ["Dog"]);
1684 assert_eq!(type_candidates("std::sync::Arc<dyn Harness>"), ["Harness"]);
1685 assert_eq!(type_candidates("Option<Dog>"), ["Option"]);
1686 assert_eq!(type_candidates("Result<Dog, Error>"), ["Result"]);
1687 }
1688}