1use std::collections::HashMap;
8
9use sinter_core::{
10 Edge, Embed, Evidence, LocalBinding, Node, NodeId, Reference, Relation, SymbolKind, TraitImpl,
11};
12use sinter_extract::{LanguageSpec, ModuleRoot, spec_for_path};
13
14pub struct Binding {
15 pub edge: Edge,
16 pub reference: usize,
18}
19
20#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
21pub struct ResolutionStats {
22 pub scope: usize,
23 pub import: usize,
24 pub scip: usize,
25 pub unresolved_internal: usize,
28 pub unresolved_external: usize,
32 pub scip_agree: usize,
36 pub scip_disagree: usize,
37 pub scip_external: usize,
42}
43
44impl ResolutionStats {
45 pub fn resolved(&self) -> usize {
46 self.scope + self.import + self.scip + self.scip_external
47 }
48
49 pub fn unresolved(&self) -> usize {
50 self.unresolved_internal + self.unresolved_external
51 }
52
53 pub fn unresolved_rate(&self) -> f64 {
54 let total = self.resolved() + self.unresolved();
55 if total == 0 {
56 0.0
57 } else {
58 self.unresolved() as f64 / total as f64
59 }
60 }
61
62 pub fn internal_unresolved_rate(&self) -> f64 {
67 let total = self.scope + self.import + self.scip + self.unresolved_internal;
68 if total == 0 {
69 0.0
70 } else {
71 self.unresolved_internal as f64 / total as f64
72 }
73 }
74}
75
76enum Res {
78 Bound(Binding),
79 Internal,
80 External,
81}
82
83pub fn qualified_of(id: &str) -> &str {
85 match id.split_once('#') {
86 Some((_, rest)) => rest.rsplit_once('@').map_or(rest, |(q, _)| q),
87 None => id,
88 }
89}
90
91fn is_type_kind(kind: SymbolKind) -> bool {
95 matches!(
96 kind,
97 SymbolKind::Struct
98 | SymbolKind::Enum
99 | SymbolKind::Interface
100 | SymbolKind::Trait
101 | SymbolKind::TypeAlias
102 )
103}
104
105fn is_member_scope(kind: SymbolKind) -> bool {
109 is_type_kind(kind) || kind == SymbolKind::Class
110}
111
112fn is_callable(kind: SymbolKind) -> bool {
113 matches!(
114 kind,
115 SymbolKind::Function | SymbolKind::Method | SymbolKind::Macro | SymbolKind::Class
116 )
117}
118
119struct ModuleFiles<'a> {
120 key: Vec<String>,
121 files: Vec<&'a str>,
122}
123
124struct LocalRange<'a> {
125 start: u64,
126 scope_end: u64,
127 type_name: Option<&'a str>,
128}
129
130struct Import {
131 segments: Vec<String>,
132 binding: String,
134 glob: bool,
136}
137
138struct FileDef<'a> {
139 node: &'a Node,
140 prefix: String,
142 functionish: bool,
145}
146
147struct Index<'a> {
148 by_file_name: HashMap<(&'a str, &'a str), Vec<FileDef<'a>>>,
150 by_file_qualified: HashMap<(&'a str, &'a str), &'a Node>,
152 file_nodes: HashMap<&'a str, &'a Node>,
154 defs_by_file: HashMap<&'a str, Vec<&'a Node>>,
156 by_name: HashMap<&'a str, Vec<(Vec<String>, &'a Node)>>,
158 by_module_tail: HashMap<String, Vec<(Vec<String>, &'a Node)>>,
160 files_of_module: HashMap<String, Vec<ModuleFiles<'a>>>,
163 module_defs: HashMap<Vec<String>, HashMap<&'a str, Vec<&'a Node>>>,
165 imports: HashMap<&'a str, Vec<Import>>,
167 locals: HashMap<(&'a str, &'a str), Vec<LocalRange<'a>>>,
169 embeds: HashMap<&'a str, Vec<&'a str>>,
171 roots: Vec<ModuleRoot>,
173}
174
175fn key_of(spec: &LanguageSpec, roots: &[ModuleRoot], file: &str) -> Vec<String> {
181 let Some((manifest, root)) = spec.manifest.zip(root_of(spec, roots, file)) else {
182 return (spec.module_path)(file);
183 };
184 let rel = if root.dir.is_empty() {
185 file
186 } else {
187 &file[root.dir.len() + 1..]
188 };
189 let mut key = (spec.module_path)(rel);
190 match key.first() {
191 Some(head) if manifest.self_names.contains(&head.as_str()) => {
192 key[0] = root.name.clone();
193 }
194 _ => key.insert(0, root.name.clone()),
195 }
196 key
197}
198
199fn root_of<'r>(spec: &LanguageSpec, roots: &'r [ModuleRoot], file: &str) -> Option<&'r ModuleRoot> {
201 roots
202 .iter()
203 .filter(|r| r.language == spec.name)
204 .filter(|r| r.dir.is_empty() || file.starts_with(&format!("{}/", r.dir)))
205 .max_by_key(|r| r.dir.len())
206}
207
208fn expand(
211 spec: &LanguageSpec,
212 roots: &[ModuleRoot],
213 file: &str,
214 mut segments: Vec<String>,
215) -> Vec<String> {
216 if let Some(manifest) = spec.manifest
217 && let Some(head) = segments.first()
218 && manifest.self_names.contains(&head.as_str())
219 && let Some(root) = root_of(spec, roots, file)
220 {
221 segments[0] = root.name.clone();
222 }
223 segments
224}
225
226fn module_of(node: &Node, roots: &[ModuleRoot]) -> Vec<String> {
227 let mut module = spec_for_path(&node.file)
228 .map(|s| key_of(s, roots, &node.file))
229 .unwrap_or_default();
230 let qualified = qualified_of(node.id.as_str());
231 if let Some((prefix, _)) = qualified.rsplit_once("::") {
232 module.extend(prefix.split("::").map(str::to_string));
233 }
234 module
235}
236
237fn build_index<'a>(
238 nodes: &'a [Node],
239 all_imports: &'a [Reference],
240 locals: &'a [LocalBinding],
241 embeds: &'a [Embed],
242 roots: &[ModuleRoot],
243) -> Index<'a> {
244 let mut index = Index {
245 by_file_name: HashMap::new(),
246 by_file_qualified: HashMap::new(),
247 file_nodes: HashMap::new(),
248 defs_by_file: HashMap::new(),
249 by_name: HashMap::new(),
250 by_module_tail: HashMap::new(),
251 files_of_module: HashMap::new(),
252 module_defs: HashMap::new(),
253 imports: HashMap::new(),
254 locals: HashMap::new(),
255 embeds: HashMap::new(),
256 roots: roots.to_vec(),
257 };
258 let mut kind_of: HashMap<(&str, &str), SymbolKind> = HashMap::new();
260 for node in nodes {
261 kind_of.insert(
262 (node.file.as_str(), qualified_of(node.id.as_str())),
263 node.kind,
264 );
265 }
266 for node in nodes {
267 let Some(spec) = spec_for_path(&node.file) else {
268 continue;
269 };
270 let file_module = key_of(spec, roots, &node.file);
271 if node.kind == SymbolKind::File {
272 index.file_nodes.insert(node.file.as_str(), node);
273 if let Some(tail) = file_module.last() {
274 index
275 .by_module_tail
276 .entry(tail.clone())
277 .or_default()
278 .push((file_module.clone(), node));
279 }
280 if let Some(tail) = file_module.last() {
281 let entries = index.files_of_module.entry(tail.clone()).or_default();
282 match entries.iter_mut().find(|m| m.key == file_module) {
283 Some(m) => m.files.push(&node.file),
284 None => entries.push(ModuleFiles {
285 key: file_module.clone(),
286 files: vec![&node.file],
287 }),
288 }
289 }
290 continue;
291 }
292 let qualified = qualified_of(node.id.as_str());
293 let prefix = qualified.rsplit_once("::").map_or("", |(p, _)| p);
294 let functionish =
295 prefix
296 .split("::")
297 .filter(|s| !s.is_empty())
298 .try_fold(String::new(), |acc, seg| {
299 let q = if acc.is_empty() {
300 seg.to_string()
301 } else {
302 format!("{acc}::{seg}")
303 };
304 let kind = kind_of.get(&(node.file.as_str(), q.as_str()));
305 match kind {
306 Some(k) if is_callable(*k) && *k != SymbolKind::Class => Some(q),
307 None => None, Some(_) => None,
309 }
310 });
311 index
312 .by_file_qualified
313 .insert((node.file.as_str(), qualified), node);
314 index
315 .defs_by_file
316 .entry(node.file.as_str())
317 .or_default()
318 .push(node);
319 index
320 .by_file_name
321 .entry((node.file.as_str(), node.name.as_str()))
322 .or_default()
323 .push(FileDef {
324 node,
325 prefix: prefix.to_string(),
326 functionish: prefix.is_empty() || functionish.is_some(),
327 });
328 let mut module = file_module.clone();
329 if !prefix.is_empty() {
330 module.extend(prefix.split("::").map(str::to_string));
331 }
332 index
333 .by_name
334 .entry(node.name.as_str())
335 .or_default()
336 .push((module, node));
337 if prefix.is_empty() {
338 index
339 .module_defs
340 .entry(file_module)
341 .or_default()
342 .entry(node.name.as_str())
343 .or_default()
344 .push(node);
345 }
346 }
347 for r in all_imports {
348 let Some(spec) = spec_for_path(&r.file) else {
349 continue;
350 };
351 let glob = matches!(r.alias.as_deref(), Some("*") | Some("."));
352 let raw = strip_glob(&r.name);
353 let segments = expand(spec, roots, &r.file, (spec.absolutize)(raw, &r.file));
354 let binding = match (&r.alias, glob) {
355 (Some(alias), false) => alias.clone(),
356 _ => segments.last().cloned().unwrap_or_default(),
357 };
358 index
359 .imports
360 .entry(r.file.as_str())
361 .or_default()
362 .push(Import {
363 segments,
364 binding,
365 glob,
366 });
367 }
368 for l in locals {
369 index
370 .locals
371 .entry((l.file.as_str(), l.name.as_str()))
372 .or_default()
373 .push(LocalRange {
374 start: l.span.start,
375 scope_end: l.scope_end,
376 type_name: l.type_name.as_deref(),
377 });
378 }
379 for e in embeds {
380 index
381 .embeds
382 .entry(e.owner.as_str())
383 .or_default()
384 .push(&e.type_name);
385 }
386 index
387}
388
389fn strip_glob(name: &str) -> &str {
390 name.strip_suffix('*')
391 .map(|s| s.trim_end_matches(['.', ':', '/']))
392 .unwrap_or(name)
393}
394
395impl<'a> Index<'a> {
396 fn local_at(&self, file: &str, name: &str, at: u64) -> Option<Option<&'a str>> {
398 self.locals
399 .get(&(file, name))
400 .into_iter()
401 .flatten()
402 .filter(|l| l.start <= at && at < l.scope_end)
403 .map(|l| l.type_name)
404 .next_back()
405 }
406
407 fn type_def(&self, file: &str, module: &[String], name: &str) -> Option<&'a Node> {
409 let same_file: Vec<&Node> = self
410 .by_file_name
411 .get(&(file, name))
412 .into_iter()
413 .flatten()
414 .filter(|d| is_member_scope(d.node.kind))
415 .map(|d| d.node)
416 .collect();
417 if let [node] = same_file.as_slice() {
418 return Some(node);
419 }
420 let in_module: Vec<&Node> = self
421 .module_defs
422 .get(module)
423 .and_then(|m| m.get(name))
424 .into_iter()
425 .flatten()
426 .filter(|n| is_member_scope(n.kind))
427 .copied()
428 .collect();
429 match in_module.as_slice() {
430 [node] => Some(node),
431 _ => None,
432 }
433 }
434
435 fn member_of(&self, ty: &'a Node, name: &str, depth: usize) -> Option<&'a Node> {
437 if depth == 0 {
438 return None;
439 }
440 let mut module = module_of(ty, &self.roots);
441 module.extend(
442 qualified_of(ty.id.as_str())
443 .rsplit("::")
444 .next()
445 .map(str::to_string),
446 );
447 let direct: Vec<&Node> = self
448 .by_name
449 .get(name)
450 .into_iter()
451 .flatten()
452 .filter(|(m, _)| *m == module)
453 .map(|(_, n)| *n)
454 .collect();
455 if let [node] = direct.as_slice() {
456 return Some(node);
457 }
458 let in_type_file: Vec<&Node> = direct
462 .iter()
463 .filter(|n| n.file == ty.file)
464 .copied()
465 .collect();
466 if let [node] = in_type_file.as_slice() {
467 return Some(node);
468 }
469 let spec = spec_for_path(&ty.file)?;
470 let file_module = key_of(spec, &self.roots, &ty.file);
471 for embedded in self.embeds.get(ty.id.as_str()).into_iter().flatten() {
472 if let Some(embedded_ty) = self.type_def(&ty.file, &file_module, embedded)
473 && let Some(node) = self.member_of(embedded_ty, name, depth - 1)
474 {
475 return Some(node);
476 }
477 }
478 None
479 }
480
481 fn anchored(&self, segments: &[String]) -> bool {
484 let module_hit = |segs: &[String]| {
485 segs.last().is_some_and(|tail| {
486 self.files_of_module
487 .get(tail.as_str())
488 .into_iter()
489 .flatten()
490 .any(|m| suffix_len(&m.key, segs).is_some())
491 || self
492 .by_module_tail
493 .get(tail.as_str())
494 .into_iter()
495 .flatten()
496 .any(|(key, _)| suffix_len(key, segs).is_some())
497 })
498 };
499 if module_hit(segments) {
500 return true;
501 }
502 match segments.split_last() {
503 Some((_, module)) if !module.is_empty() => module_hit(module),
504 _ => false,
505 }
506 }
507
508 fn import_file(&self, segments: &[String]) -> Option<&'a Node> {
515 unique_best(
516 self.by_module_tail
517 .get(segments.last()?.as_str())
518 .into_iter()
519 .flatten()
520 .filter_map(|(key, node)| {
521 let len = suffix_len(key, segments).or_else(|| suffix_len(segments, key))?;
522 Some((len, *node))
523 }),
524 )
525 }
526
527 fn resolve_path(&self, segments: &[String], depth: usize) -> Option<&'a Node> {
530 self.resolve_path_defs(segments, depth).or_else(|| {
531 let files = self
535 .by_module_tail
536 .get(segments.last()?.as_str())
537 .into_iter()
538 .flatten()
539 .filter_map(|(key, node)| Some((suffix_len(key, segments)?, *node)));
540 unique_best(files)
541 })
542 }
543
544 fn resolve_path_defs(&self, segments: &[String], depth: usize) -> Option<&'a Node> {
550 if segments.is_empty() || depth == 0 {
551 return None;
552 }
553 if let Some((name, module)) = segments.split_last() {
554 let defs = self
555 .by_name
556 .get(name.as_str())
557 .into_iter()
558 .flatten()
559 .filter_map(|(key, node)| Some((suffix_len(key, module)?, *node)));
560 if let Some(node) = unique_best(defs) {
561 return Some(node);
562 }
563 if !module.is_empty() {
566 let mut chained: Vec<&Node> = Vec::new();
567 let tail = module.last().map(String::as_str).unwrap_or("");
568 for m in self.files_of_module.get(tail).into_iter().flatten() {
569 if suffix_len(&m.key, module).is_none() {
570 continue;
571 }
572 for file in &m.files {
573 for import in self.imports.get(*file).into_iter().flatten() {
574 if import.binding == *name && !import.glob {
575 chained.extend(self.resolve_path(&import.segments, depth - 1));
576 } else if import.glob {
577 let mut deeper = import.segments.clone();
578 deeper.push(name.clone());
579 chained.extend(self.resolve_path(&deeper, depth - 1));
580 }
581 }
582 }
583 }
584 chained.sort_by_key(|n| n.id.as_str().to_string());
585 chained.dedup_by_key(|n| n.id.as_str().to_string());
586 if let [node] = chained.as_slice() {
587 return Some(node);
588 }
589 }
590 }
591 None
592 }
593}
594
595fn namespace_pick(candidates: Vec<&Node>, relation: Relation) -> Option<&Node> {
598 match candidates.as_slice() {
599 [node] => Some(node),
600 [] => None,
601 _ => {
602 let preferred: Vec<&Node> = candidates
603 .iter()
604 .filter(|n| match relation {
605 Relation::Calls => is_callable(n.kind),
606 Relation::Uses => is_type_kind(n.kind),
607 _ => true,
608 })
609 .copied()
610 .collect();
611 match preferred.as_slice() {
612 [node] => Some(node),
613 _ => None,
614 }
615 }
616 }
617}
618
619pub fn resolve(
620 nodes: &[Node],
621 references: &[Reference],
622 locals: &[LocalBinding],
623 all_imports: &[Reference],
624 embeds: &[Embed],
625 roots: &[ModuleRoot],
626) -> (Vec<Binding>, ResolutionStats, Vec<usize>) {
627 let t = std::time::Instant::now();
628 let index = build_index(nodes, all_imports, locals, embeds, roots);
629 if std::env::var_os("SINTER_TIMING").is_some() {
630 eprintln!("index build: {:?}", t.elapsed());
631 }
632 use rayon::prelude::*;
633 let results: Vec<Res> = references
634 .par_iter()
635 .enumerate()
636 .map(|(i, r)| {
637 let Some(spec) = spec_for_path(&r.file) else {
638 return Res::External;
639 };
640 let src = r
641 .enclosing
642 .clone()
643 .unwrap_or_else(|| NodeId::new(r.file.clone()));
644 let file_module = key_of(spec, &index.roots, &r.file);
645 let imports = index.imports.get(r.file.as_str());
646 let (target, evidence, internal) = resolve_one(&index, spec, r, &file_module, imports);
647 match target {
648 Some(node) if node.id != src => {
649 let relation = if r.relation == Relation::Calls && is_type_kind(node.kind) {
652 Relation::Uses
653 } else {
654 r.relation
655 };
656 Res::Bound(Binding {
657 edge: Edge {
658 src,
659 dst: node.id.clone(),
660 relation,
661 evidence,
662 confidence: evidence.confidence(),
663 },
664 reference: i,
665 })
666 }
667 _ if internal => Res::Internal,
668 _ => Res::External,
669 }
670 })
671 .collect();
672 let mut bindings = Vec::new();
673 let mut stats = ResolutionStats::default();
674 let mut internal_indices = Vec::new();
675 for (i, result) in results.into_iter().enumerate() {
676 match result {
677 Res::Bound(binding) => {
678 match binding.edge.evidence {
679 Evidence::Scope => stats.scope += 1,
680 _ => stats.import += 1,
681 }
682 bindings.push(binding);
683 }
684 Res::Internal => {
685 stats.unresolved_internal += 1;
686 internal_indices.push(i);
687 }
688 Res::External => stats.unresolved_external += 1,
689 }
690 }
691 (bindings, stats, internal_indices)
692}
693
694fn resolve_one<'a>(
695 index: &Index<'a>,
696 spec: &sinter_extract::LanguageSpec,
697 r: &Reference,
698 file_module: &[String],
699 imports: Option<&Vec<Import>>,
700) -> (Option<&'a Node>, Evidence, bool) {
701 if r.relation == Relation::Imports {
702 let glob = matches!(r.alias.as_deref(), Some("*") | Some("."));
703 let raw = strip_glob(&r.name);
704 if let Some(node) = index
708 .file_nodes
709 .get(raw.trim().trim_matches(['<', '>', '"']))
710 {
711 return (Some(node), Evidence::Import, true);
712 }
713 let segments = expand(spec, &index.roots, &r.file, (spec.absolutize)(raw, &r.file));
714 let target = if glob {
715 index.import_file(&segments)
716 } else {
717 index.resolve_path(&segments, 4)
718 };
719 let internal = target.is_some() || index.anchored(&segments);
720 return (target, Evidence::Import, internal);
721 }
722
723 if let Some(path) = &r.path {
724 if spec.file_refs {
727 return resolve_file_ref(index, spec, r, path);
728 }
729 let segments = expand(
732 spec,
733 &index.roots,
734 &r.file,
735 (spec.absolutize)(path, &r.file),
736 );
737 let prefix = segments
738 .len()
739 .checked_sub(2)
740 .and_then(|p| segments.get(p))
741 .cloned();
742 let Some(prefix) = prefix else {
743 return (None, Evidence::Import, false);
744 };
745 if spec.receivers.contains(&prefix.as_str())
746 && let Some(enclosing) = &r.enclosing
747 && let Some((type_prefix, _)) = qualified_of(enclosing.as_str()).rsplit_once("::")
748 && let Some(ty) = index.by_file_qualified.get(&(r.file.as_str(), type_prefix))
749 {
750 return (index.member_of(ty, &r.name, 4), Evidence::Scope, true);
752 }
753 match index.local_at(&r.file, &prefix, r.span.start) {
754 Some(Some(type_name)) => {
755 let ty = index.type_def(&r.file, file_module, type_name);
756 let target = ty.and_then(|ty| index.member_of(ty, &r.name, 4));
757 return (target, Evidence::Scope, ty.is_some());
759 }
760 Some(None) => return (None, Evidence::Scope, false), None => {}
762 }
763 if let Some(ty) = index.type_def(&r.file, file_module, &prefix)
765 && let Some(node) = index.member_of(ty, &r.name, 4)
766 {
767 return (Some(node), Evidence::Scope, true);
768 }
769 if let Some(node) = index.resolve_path_defs(&segments, 4) {
770 return (Some(node), Evidence::Import, true);
771 }
772 if let Some((leaf, type_path)) = segments.split_last()
778 && type_path.len() >= 2
779 && let Some(ty) = index.resolve_path_defs(type_path, 4)
780 && let Some(node) = index.member_of(ty, leaf, 4)
781 {
782 return (Some(node), Evidence::Import, true);
783 }
784 let matching: Vec<&Import> = imports
785 .into_iter()
786 .flatten()
787 .filter(|imp| !imp.glob && imp.binding == prefix)
788 .collect();
789 let candidates: Vec<&Node> = matching
790 .iter()
791 .filter_map(|imp| {
792 let mut full = imp.segments.clone();
793 full.push(r.name.clone());
794 index.resolve_path(&full, 4)
795 })
796 .collect();
797 let internal = candidates.len() > 1
798 || index.anchored(&segments)
799 || matching.iter().any(|imp| index.anchored(&imp.segments));
800 return match candidates.as_slice() {
801 [node] => (Some(node), Evidence::Import, true),
802 _ => (None, Evidence::Import, internal),
803 };
804 }
805
806 if index.local_at(&r.file, &r.name, r.span.start).is_some() {
808 return (None, Evidence::Scope, false); }
810 let enclosing_q = r
811 .enclosing
812 .as_ref()
813 .map(|e| qualified_of(e.as_str()))
814 .unwrap_or("");
815 let visible: Vec<&Node> = index
816 .by_file_name
817 .get(&(r.file.as_str(), r.name.as_str()))
818 .into_iter()
819 .flatten()
820 .filter(|d| {
821 d.prefix.is_empty()
822 || (d.functionish
823 && (enclosing_q == d.prefix
824 || enclosing_q.starts_with(&format!("{}::", d.prefix))))
825 })
826 .map(|d| d.node)
827 .collect();
828 if !visible.is_empty() {
829 return (namespace_pick(visible, r.relation), Evidence::Scope, true);
831 }
832 if let Some(defs) = index
833 .module_defs
834 .get(file_module)
835 .and_then(|m| m.get(r.name.as_str()))
836 {
837 return (
838 namespace_pick(defs.clone(), r.relation),
839 Evidence::Scope,
840 true,
841 );
842 }
843 let named: Vec<&Node> = imports
844 .into_iter()
845 .flatten()
846 .filter(|imp| !imp.glob && imp.binding == r.name)
847 .filter_map(|imp| index.resolve_path(&imp.segments, 4))
848 .collect();
849 let (target, internal) = match named.as_slice() {
850 [node] => (Some(*node), true),
851 [] => {
852 let globbed: Vec<&Node> = imports
853 .into_iter()
854 .flatten()
855 .filter(|imp| imp.glob)
856 .filter_map(|imp| {
857 let mut full = imp.segments.clone();
858 full.push(r.name.clone());
859 index.resolve_path(&full, 4).or_else(|| {
860 let file = index.import_file(&imp.segments)?;
863 index
864 .by_file_name
865 .get(&(file.file.as_str(), r.name.as_str()))
866 .into_iter()
867 .flatten()
868 .find(|d| d.prefix.is_empty())
869 .map(|d| d.node)
870 })
871 })
872 .collect();
873 let name_imports_anchored = imports
874 .into_iter()
875 .flatten()
876 .filter(|imp| !imp.glob && imp.binding == r.name)
877 .any(|imp| index.anchored(&imp.segments));
878 match globbed.as_slice() {
879 [node] => (Some(*node), true),
880 [] => (None, name_imports_anchored),
881 _ => (None, true), }
883 }
884 _ => (None, true), };
886 (target, Evidence::Import, internal)
887}
888
889fn resolve_file_ref<'a>(
897 index: &Index<'a>,
898 spec: &LanguageSpec,
899 r: &Reference,
900 path: &str,
901) -> (Option<&'a Node>, Evidence, bool) {
902 let (head, frag) = match path.split_once('#') {
903 Some((h, f)) => (h, Some(f)),
904 None => (path, None),
905 };
906 let file = if head.is_empty() {
907 index.file_nodes.get(r.file.as_str()).copied()
909 } else {
910 let joined = (spec.absolutize)(head, &r.file).join("/");
911 index.file_nodes.get(joined.as_str()).copied().or_else(|| {
912 spec.extensions.iter().find_map(|ext| {
913 index
914 .file_nodes
915 .get(format!("{joined}.{ext}").as_str())
916 .copied()
917 })
918 })
919 };
920 match (file, frag) {
921 (Some(file), None) => (Some(file), Evidence::Import, true),
922 (Some(file), Some(frag)) => {
923 let matching: Vec<&Node> = index
924 .defs_by_file
925 .get(file.file.as_str())
926 .into_iter()
927 .flatten()
928 .filter(|n| slugify(&n.name) == frag)
929 .copied()
930 .collect();
931 match matching.as_slice() {
934 [node] => (Some(node), Evidence::Import, true),
935 _ => (None, Evidence::Import, true),
936 }
937 }
938 (None, _) => (None, Evidence::Import, false),
939 }
940}
941
942fn slugify(name: &str) -> String {
945 name.chars()
946 .filter_map(|c| match c {
947 ' ' => Some('-'),
948 '-' | '_' => Some(c),
949 c if c.is_alphanumeric() => Some(c.to_ascii_lowercase()),
950 _ => None,
951 })
952 .collect()
953}
954
955pub fn dynamic_edges(
963 nodes: &[Node],
964 trait_impls: &[TraitImpl],
965 all_imports: &[Reference],
966 roots: &[ModuleRoot],
967) -> Vec<Edge> {
968 if trait_impls.is_empty() {
969 return Vec::new();
970 }
971 let index = build_index(nodes, all_imports, &[], &[], roots);
972 let mut by_file: HashMap<&str, Vec<&Node>> = HashMap::new();
973 for n in nodes {
974 if is_callable(n.kind) {
975 by_file.entry(n.file.as_str()).or_default().push(n);
976 }
977 }
978 let is_trait = |n: &Node| {
982 matches!(
983 n.kind,
984 SymbolKind::Trait | SymbolKind::Interface | SymbolKind::Class
985 )
986 };
987 let mut edges = Vec::new();
988 for ti in trait_impls {
989 let Some(spec) = spec_for_path(&ti.file) else {
990 continue;
991 };
992 let file_module = key_of(spec, roots, &ti.file);
993 let trait_node = index
994 .type_def(&ti.file, &file_module, &ti.trait_name)
995 .filter(|n| is_trait(n))
996 .or_else(|| {
997 let named: Vec<&Node> = index
999 .imports
1000 .get(ti.file.as_str())
1001 .into_iter()
1002 .flatten()
1003 .filter(|imp| !imp.glob && imp.binding == ti.trait_name)
1004 .filter_map(|imp| index.resolve_path_defs(&imp.segments, 4))
1005 .filter(|n| is_trait(n))
1006 .collect();
1007 match named.as_slice() {
1008 [node] => Some(node),
1009 _ => None,
1010 }
1011 });
1012 let Some(trait_node) = trait_node else {
1013 continue; };
1015 for method in by_file.get(ti.file.as_str()).into_iter().flatten() {
1016 if !(ti.span.start <= method.span.start && method.span.end <= ti.span.end) {
1017 continue;
1018 }
1019 if let Some(trait_method) = index.member_of(trait_node, &method.name, 1)
1020 && trait_method.id != method.id
1021 {
1022 edges.push(Edge {
1023 src: trait_method.id.clone(),
1024 dst: method.id.clone(),
1025 relation: Relation::Calls,
1026 evidence: Evidence::Dynamic,
1027 confidence: Evidence::Dynamic.confidence(),
1028 });
1029 }
1030 }
1031 }
1032 edges.sort();
1033 edges.dedup();
1034 edges
1035}
1036
1037pub fn resolve_boundary(
1044 foreign_nodes: &[Node],
1045 references: &[Reference],
1046 owner_imports: &[Reference],
1047) -> Vec<Binding> {
1048 let index = build_index(foreign_nodes, owner_imports, &[], &[], &[]);
1049 let mut bindings = Vec::new();
1050 for (i, r) in references.iter().enumerate() {
1051 let Some(spec) = spec_for_path(&r.file) else {
1052 continue;
1053 };
1054 let src = r
1055 .enclosing
1056 .clone()
1057 .unwrap_or_else(|| NodeId::new(r.file.clone()));
1058 let imports = index.imports.get(r.file.as_str());
1059 let target = if r.relation == Relation::Imports {
1060 let glob = matches!(r.alias.as_deref(), Some("*") | Some("."));
1061 let segments = (spec.absolutize)(strip_glob(&r.name), &r.file);
1062 if glob {
1063 index.import_file(&segments)
1064 } else {
1065 index.resolve_path(&segments, 4)
1066 }
1067 } else if let Some(path) = &r.path {
1068 let segments = (spec.absolutize)(path, &r.file);
1069 let direct = index.resolve_path(&segments, 4);
1070 direct.or_else(|| {
1071 let prefix = segments
1072 .len()
1073 .checked_sub(2)
1074 .and_then(|p| segments.get(p))?;
1075 let candidates: Vec<&Node> = imports
1076 .into_iter()
1077 .flatten()
1078 .filter(|imp| !imp.glob && imp.binding == *prefix)
1079 .filter_map(|imp| {
1080 let mut full = imp.segments.clone();
1081 full.push(r.name.clone());
1082 index.resolve_path(&full, 4)
1083 })
1084 .collect();
1085 match candidates.as_slice() {
1086 [node] => Some(node),
1087 _ => None,
1088 }
1089 })
1090 } else {
1091 let named: Vec<&Node> = imports
1093 .into_iter()
1094 .flatten()
1095 .filter(|imp| !imp.glob && imp.binding == r.name)
1096 .filter_map(|imp| index.resolve_path(&imp.segments, 4))
1097 .collect();
1098 match named.as_slice() {
1099 [node] => Some(*node),
1100 _ => None,
1101 }
1102 };
1103 if let Some(node) = target
1104 && node.id != src
1105 {
1106 let relation = if r.relation == Relation::Calls && is_type_kind(node.kind) {
1107 Relation::Uses
1108 } else {
1109 r.relation
1110 };
1111 bindings.push(Binding {
1112 edge: Edge {
1113 src,
1114 dst: node.id.clone(),
1115 relation,
1116 evidence: Evidence::Import,
1117 confidence: Evidence::Import.confidence(),
1118 },
1119 reference: i,
1120 });
1121 }
1122 }
1123 bindings
1124}
1125
1126fn suffix_len(key: &[String], path: &[String]) -> Option<usize> {
1128 (!key.is_empty() && path.len() >= key.len() && path[path.len() - key.len()..] == key[..])
1129 .then_some(key.len())
1130}
1131
1132fn unique_best<'a>(candidates: impl Iterator<Item = (usize, &'a Node)>) -> Option<&'a Node> {
1134 let mut best: Option<(usize, Vec<&Node>)> = None;
1135 for (len, node) in candidates {
1136 match &mut best {
1137 Some((best_len, nodes)) if len == *best_len => nodes.push(node),
1138 Some((best_len, nodes)) if len > *best_len => {
1139 *best_len = len;
1140 nodes.clear();
1141 nodes.push(node);
1142 }
1143 None => best = Some((len, vec![node])),
1144 _ => {}
1145 }
1146 }
1147 match best {
1148 Some((_, nodes)) if nodes.len() == 1 => Some(nodes[0]),
1149 _ => None,
1150 }
1151}