1use std::cell::RefCell;
8use std::collections::{HashMap, HashSet};
9
10use crate::facts::{FileFacts, Import, ImportItem, RefKind, TypeSource};
11use crate::model::{Edge, EdgeKind, Language, Symbol, SymbolKind};
12use crate::packages::strip_ts_ext;
13
14mod contracts;
15pub use contracts::{contract_id, http_path, CONTRACTS};
16
17type ScipOccs<'a> = HashMap<&'a str, Vec<(u32, &'a str)>>;
19
20const MAX_DEPTH: u32 = 12;
22
23#[derive(Debug, Default)]
24pub struct Resolved {
25 pub edges: Vec<Edge>,
26 pub symbols: Vec<Symbol>,
28 pub stats: ResolveStats,
29 pub unresolved_modules: HashMap<String, usize>,
31 pub unresolved_examples: HashMap<String, Vec<String>>,
33}
34
35#[derive(Debug, Default, Clone, Copy)]
36pub struct ResolveStats {
37 pub resolved_imports: usize,
38 pub unresolved_imports: usize,
40 pub resolved_calls: usize,
42 pub unresolved_calls: usize,
43 pub type_edges: usize,
45 pub scip_answers: usize,
47 pub contract_edges: usize,
49 pub cross_repo_edges: usize,
50}
51
52#[derive(Debug, Clone, PartialEq)]
53enum Value {
54 File(String),
55 Module {
57 fam: Family,
58 name: String,
59 repo: String,
60 },
61 Sym(String),
62 Instance(String),
63}
64
65pub fn resolve(symbols: &[Symbol], edges: &[Edge], facts: &[FileFacts]) -> Resolved {
67 let b = Binder::new(symbols, edges, facts);
68 let mut out = Resolved {
69 edges: edges.to_vec(),
70 ..Default::default()
71 };
72 let mut new_edges: Vec<(String, String, EdgeKind, f32)> = Vec::new();
73
74 for f in facts {
76 for r in &f.refs {
77 if !matches!(r.kind, RefKind::Inherits | RefKind::Implements) {
78 continue;
79 }
80 let scope = b.parent.get(&r.from).cloned().unwrap_or(f.file_id.clone());
81 let name = r.path.last().map_or("", String::as_str);
82 let target = match b.scip(f, name, r.line) {
83 Some(t) => {
84 out.stats.scip_answers += 1;
85 Some(t)
86 }
87 None => match b.resolve_path(&r.path, &scope, f, 0) {
88 Some(Value::Sym(t)) => Some(t),
89 _ => None,
90 },
91 };
92 let Some(t) = target else {
93 continue;
94 };
95 if !b.is_type(&t) {
96 continue;
97 }
98 b.bases
99 .borrow_mut()
100 .entry(r.from.clone())
101 .or_default()
102 .push(t.clone());
103 let kind = if r.kind == RefKind::Inherits {
104 EdgeKind::Inherits
105 } else {
106 EdgeKind::Implements
107 };
108 new_edges.push((r.from.clone(), t, kind, 0.9));
109 out.stats.type_edges += 1;
110 }
111 }
112
113 for f in facts {
114 for imp in &f.imports {
115 let base = b.import_target(f, imp);
116 for item in &imp.items {
117 let target = match item {
118 ImportItem::Name { name, .. } => match b.scip(f, name, imp.line) {
119 Some(t) => {
120 out.stats.scip_answers += 1;
121 Some(Value::Sym(t))
122 }
123 None => base.as_ref().and_then(|v| b.member(v, name, 0)),
124 },
125 ImportItem::Module { .. } | ImportItem::All => base.clone(),
126 };
127 match target.and_then(|v| b.node_of(&v)) {
128 Some(t) if t != f.file_id => {
129 new_edges.push((f.file_id.clone(), t, EdgeKind::Imports, 0.95));
130 out.stats.resolved_imports += 1;
131 }
132 Some(_) => {}
133 None => {
134 out.stats.unresolved_imports += 1;
135 let top = top_module(f, imp);
136 let ex = out.unresolved_examples.entry(top.clone()).or_default();
137 if ex.len() < 3 {
138 ex.push(format!("{}:{} {} {:?}", f.path, imp.line, imp.module, item));
139 }
140 *out.unresolved_modules.entry(top).or_default() += 1;
141 }
142 }
143 }
144 }
145
146 for r in &f.refs {
147 if !matches!(r.kind, RefKind::Call | RefKind::New) {
148 continue;
149 }
150 let name = r.path.last().map_or("", String::as_str);
151 let target = match b.scip(f, name, r.line) {
152 Some(t) => {
153 out.stats.scip_answers += 1;
154 Some(t)
155 }
156 None => match b.resolve_path(&r.path, &r.from, f, 0) {
157 Some(Value::Sym(t)) => Some(t),
158 _ => None,
159 },
160 };
161 let Some(t) = target else {
162 out.stats.unresolved_calls += 1;
163 continue;
164 };
165 let kind = if b.is_type(&t) {
166 EdgeKind::Instantiates
167 } else if b.is_callable(&t) {
168 EdgeKind::Calls
169 } else {
170 out.stats.unresolved_calls += 1;
171 continue;
172 };
173 out.stats.resolved_calls += 1;
174 new_edges.push((r.from.clone(), t, kind, 0.9));
175 }
176 }
177
178 let calls: Vec<(String, String)> = new_edges
179 .iter()
180 .filter(|(_, _, k, _)| *k == EdgeKind::Calls)
181 .map(|(s, d, _, _)| (s.clone(), d.clone()))
182 .collect();
183 let linked = contracts::link(&b, facts, &calls);
184 for (src, dst, kind) in linked.edges {
185 let Some(src_id) = b.sym(&src).map(|s| s.id.clone()).or_else(|| {
187 linked
188 .symbols
189 .iter()
190 .find(|c| c.id.render() == src)
191 .map(|c| c.id.clone())
192 }) else {
193 continue;
194 };
195 let dst_id = linked
196 .symbols
197 .iter()
198 .find(|c| c.id.render() == dst)
199 .map(|c| c.id.clone());
200 if let Some(d) = dst_id {
201 out.stats.contract_edges += 1;
202 out.edges.push(Edge {
203 src: src_id,
204 dst: d,
205 kind,
206 confidence: 0.9,
207 cross_repo: false,
208 });
209 }
210 }
211 out.symbols = linked.symbols;
212
213 let mut seen: HashSet<(String, String, EdgeKind)> = out
214 .edges
215 .iter()
216 .map(|e| (e.src.render(), e.dst.render(), e.kind.clone()))
217 .collect();
218 for (src, dst, kind, confidence) in new_edges {
219 let (Some(s), Some(d)) = (b.sym(&src), b.sym(&dst)) else {
220 continue;
221 };
222 if !seen.insert((src, dst, kind.clone())) {
223 continue;
224 }
225 let cross_repo = s.repo != d.repo;
226 if cross_repo {
227 out.stats.cross_repo_edges += 1;
228 }
229 out.edges.push(Edge {
230 src: s.id.clone(),
231 dst: d.id.clone(),
232 kind,
233 confidence,
234 cross_repo,
235 });
236 }
237 out
238}
239
240#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
242enum Family {
243 Py,
244 Js,
245 Go,
246 Jvm,
247 Rust,
248}
249
250impl Family {
251 fn sub_sep(self) -> Option<&'static str> {
253 match self {
254 Family::Py => Some("."),
255 Family::Rust => Some("::"),
256 _ => None,
257 }
258 }
259}
260
261fn family(lang: &Language) -> Option<Family> {
262 match lang {
263 Language::Python => Some(Family::Py),
264 Language::TypeScript | Language::JavaScript => Some(Family::Js),
265 Language::Go => Some(Family::Go),
266 Language::Java | Language::Kotlin => Some(Family::Jvm),
267 Language::Rust => Some(Family::Rust),
268 _ => None,
269 }
270}
271
272struct Binder<'a> {
273 syms: HashMap<String, &'a Symbol>,
274 parent: HashMap<String, String>,
275 members: HashMap<String, HashMap<&'a str, String>>,
277 facts: HashMap<&'a str, &'a FileFacts>,
278 modules: HashMap<(Family, &'a str), Vec<&'a FileFacts>>,
280 packages: HashSet<(Family, String)>,
282 by_path: HashMap<(&'a str, &'a str), &'a FileFacts>,
283 types: HashMap<(&'a str, &'a str), &'a TypeSource>,
284 values: HashMap<(&'a str, &'a str), &'a crate::facts::ValueExpr>,
286 bases: RefCell<HashMap<String, Vec<String>>>,
287 scip_defs: HashMap<&'a str, &'a str>,
289 scip_refs: HashMap<&'a str, ScipOccs<'a>>,
291}
292
293impl<'a> Binder<'a> {
294 fn new(symbols: &'a [Symbol], edges: &[Edge], facts: &'a [FileFacts]) -> Self {
295 let syms: HashMap<String, &Symbol> = symbols.iter().map(|s| (s.id.render(), s)).collect();
296 let mut parent = HashMap::new();
297 let mut members: HashMap<String, HashMap<&str, String>> = HashMap::new();
298 for e in edges.iter().filter(|e| e.kind == EdgeKind::Contains) {
299 let (p, c) = (e.src.render(), e.dst.render());
300 if let Some(child) = syms.get(&c) {
301 members
302 .entry(p.clone())
303 .or_default()
304 .insert(child.name.as_str(), c.clone());
305 }
306 parent.insert(c, p);
307 }
308 let mut b = Binder {
309 syms,
310 parent,
311 members,
312 facts: HashMap::new(),
313 modules: HashMap::new(),
314 packages: HashSet::new(),
315 by_path: HashMap::new(),
316 types: HashMap::new(),
317 values: HashMap::new(),
318 bases: RefCell::new(HashMap::new()),
319 scip_defs: HashMap::new(),
320 scip_refs: HashMap::new(),
321 };
322 for f in facts {
323 b.facts.insert(f.file_id.as_str(), f);
324 b.by_path.insert((f.repo.as_str(), f.path.as_str()), f);
325 let Some(fam) = family(&f.language) else {
326 continue;
327 };
328 for m in &f.modules {
329 b.modules.entry((fam, m.as_str())).or_default().push(f);
330 if let Some(sep) = fam.sub_sep() {
331 let mut prefix = String::new();
332 for part in m.split(sep) {
333 if !prefix.is_empty() {
334 prefix.push_str(sep);
335 }
336 prefix.push_str(part);
337 b.packages.insert((fam, prefix.clone()));
338 }
339 }
340 }
341 for t in &f.types {
342 b.types
343 .entry((t.scope.as_str(), t.name.as_str()))
344 .or_insert(&t.source);
345 }
346 for v in &f.values {
347 b.values
348 .insert((v.scope.as_str(), v.name.as_str()), &v.value);
349 }
350 if let Some(sf) = &f.scip {
351 for (sym, id) in &sf.defs {
352 b.scip_defs.insert(sym.as_str(), id.as_str());
353 }
354 let by_name = b.scip_refs.entry(f.file_id.as_str()).or_default();
355 for r in &sf.refs {
356 by_name
357 .entry(r.name.as_str())
358 .or_default()
359 .push((r.line, r.symbol.as_str()));
360 }
361 }
362 }
363 b
364 }
365
366 fn scip(&self, f: &FileFacts, name: &str, line: u32) -> Option<String> {
369 let occs = self.scip_refs.get(f.file_id.as_str())?.get(name)?;
370 let (_, sym) = occs
372 .iter()
373 .filter(|(l, _)| *l >= line && *l <= line + 3)
374 .min_by_key(|(l, _)| *l)?;
375 self.scip_defs.get(sym).map(|id| id.to_string())
376 }
377
378 fn sym(&self, id: &str) -> Option<&'a Symbol> {
379 self.syms.get(id).copied()
380 }
381
382 fn is_type(&self, id: &str) -> bool {
383 self.sym(id).is_some_and(|s| {
384 matches!(
385 s.kind,
386 SymbolKind::Class
387 | SymbolKind::Interface
388 | SymbolKind::Struct
389 | SymbolKind::Trait
390 | SymbolKind::Enum
391 )
392 })
393 }
394
395 fn is_callable(&self, id: &str) -> bool {
396 self.sym(id)
397 .is_some_and(|s| matches!(s.kind, SymbolKind::Function | SymbolKind::Method))
398 }
399
400 fn module_files(&self, fam: Family, name: &str, repo: &str) -> Vec<&'a FileFacts> {
402 let Some(cands) = self.modules.get(&(fam, name)) else {
403 return Vec::new();
404 };
405 let local: Vec<_> = cands.iter().filter(|f| f.repo == repo).copied().collect();
406 if !local.is_empty() {
407 return local;
408 }
409 let first = &cands[0].repo;
410 if cands.iter().all(|f| &f.repo == first) {
411 cands.clone()
412 } else {
413 Vec::new()
414 }
415 }
416
417 fn import_target(&self, f: &FileFacts, imp: &Import) -> Option<Value> {
419 let module = |fam, name: String| Value::Module {
420 fam,
421 name,
422 repo: f.repo.clone(),
423 };
424 match family(&f.language)? {
425 Family::Py => {
426 if imp.level == 0 {
427 return Some(module(Family::Py, imp.module.clone()));
428 }
429 let own = f.modules.first()?;
430 let mut parts: Vec<&str> = own.split('.').collect();
431 let is_pkg = f.path.ends_with("__init__.py") || f.path.ends_with("__init__.pyi");
432 let drop = imp.level as usize - usize::from(is_pkg);
433 parts.truncate(parts.len().checked_sub(drop)?);
434 if !imp.module.is_empty() {
435 parts.push(&imp.module);
436 }
437 Some(module(Family::Py, parts.join(".")))
438 }
439 fam @ (Family::Go | Family::Jvm) => Some(module(fam, imp.module.clone())),
440 Family::Rust => {
442 let head = imp.module.split("::").next().unwrap_or_default();
443 let local = f
444 .modules
445 .first()
446 .filter(|own| {
447 self.packages
448 .contains(&(Family::Rust, format!("{own}::{head}")))
449 })
450 .map(|own| format!("{own}::{}", imp.module));
451 Some(module(
452 Family::Rust,
453 local.unwrap_or_else(|| imp.module.clone()),
454 ))
455 }
456 Family::Js if imp.file.is_some() => self
457 .by_path
458 .get(&(f.repo.as_str(), imp.file.as_deref()?))
459 .map(|t| Value::File(t.file_id.clone())),
460 Family::Js if imp.module.starts_with('.') => {
461 let dir = f.path.rsplit_once('/').map_or("", |(d, _)| d);
462 let base = normalize(dir, &imp.module)?;
463 let stem = strip_ts_ext(&base);
464 [
465 ".ts",
466 ".tsx",
467 ".d.ts",
468 ".js",
469 ".jsx",
470 ".mjs",
471 "/index.ts",
472 "/index.tsx",
473 "/index.js",
474 ]
475 .iter()
476 .find_map(|e| {
477 let path = format!("{stem}{e}");
478 self.by_path
479 .get(&(f.repo.as_str(), path.as_str()))
480 .map(|t| Value::File(t.file_id.clone()))
481 })
482 }
483 Family::Js => {
484 let files = self.module_files(Family::Js, &imp.module, &f.repo);
485 (files.len() == 1).then(|| Value::File(files[0].file_id.clone()))
486 }
487 }
488 }
489
490 fn node_of(&self, v: &Value) -> Option<String> {
492 match v {
493 Value::File(id) | Value::Sym(id) => Some(id.clone()),
494 Value::Module { fam, name, repo } => {
495 let files = self.module_files(*fam, name, repo);
496 let last = name.rsplit(['/', '.']).next().unwrap_or(name);
497 files
498 .iter()
499 .find(|f| {
500 f.path.rsplit('/').next().and_then(|n| n.split('.').next()) == Some(last)
501 })
502 .or_else(|| files.iter().min_by_key(|f| &f.path))
503 .map(|f| f.file_id.clone())
504 }
505 Value::Instance(_) => None,
506 }
507 }
508
509 fn member(&self, v: &Value, name: &str, depth: u32) -> Option<Value> {
510 if depth > MAX_DEPTH {
511 return None;
512 }
513 match v {
514 Value::File(f) => self.module_member(f, name, true, depth + 1),
515 Value::Module { fam, name: m, repo } => {
516 let reexports = matches!(fam, Family::Py | Family::Js | Family::Rust);
518 let files = self.module_files(*fam, m, repo);
519 if let Some(x) = files
520 .iter()
521 .find_map(|f| self.module_member(&f.file_id, name, reexports, depth + 1))
522 {
523 return Some(x);
524 }
525 let sep = match fam {
527 Family::Jvm => Some("."),
528 Family::Rust => Some("::"),
529 _ => None,
530 };
531 if let (Some(sep), true) = (sep, files.is_empty()) {
532 let (pkg, outer) = m.rsplit_once(sep)?;
533 let outer = self.member(
534 &Value::Module {
535 fam: *fam,
536 name: pkg.to_string(),
537 repo: repo.clone(),
538 },
539 outer,
540 depth + 1,
541 )?;
542 return self.member(&outer, name, depth + 1);
543 }
544 let sub = format!("{m}{}{name}", fam.sub_sep()?);
545 self.packages
546 .contains(&(*fam, sub.clone()))
547 .then(|| Value::Module {
548 fam: *fam,
549 name: sub,
550 repo: repo.clone(),
551 })
552 }
553 Value::Sym(id) if self.is_type(id) => self.class_member(id, name, depth + 1),
554 Value::Sym(id) => {
556 let var = self
557 .sym(id)
558 .filter(|s| matches!(s.kind, SymbolKind::Variable | SymbolKind::Constant))?;
559 let scope = self.parent.get(id)?;
560 if let Some(crate::facts::ValueExpr::Record(fields)) =
561 self.values.get(&(scope.as_str(), var.name.as_str()))
562 {
563 if let Some((_, v)) = fields.iter().find(|(k, _)| k == name) {
564 return match v {
565 crate::facts::ValueExpr::Name(p) => {
566 let f = self.file_facts_of(scope)?;
567 self.resolve_path(p, scope, f, depth + 1)
568 }
569 _ => None,
570 };
571 }
572 }
573 let src = self.types.get(&(scope.as_str(), var.name.as_str()))?;
574 let inst = self.type_of(src, scope, depth + 1)?;
575 self.member(&inst, name, depth + 1)
576 }
577 Value::Instance(c) => {
578 for recv in ["self", "this"] {
579 let attr = format!("{recv}.{name}");
580 if let Some(src) = self.types.get(&(c.as_str(), attr.as_str())) {
581 if let Some(v) = self.type_of(src, c, depth + 1) {
582 return Some(v);
583 }
584 if let TypeSource::Alias { name: n, scope: s } = src {
586 let f = self.file_facts_of(s)?;
587 if let Some(v @ Value::Sym(_)) = self.resolve_name(n, s, f, depth + 1) {
588 return Some(v);
589 }
590 }
591 return None;
592 }
593 }
594 self.class_member(c, name, depth + 1)
595 }
596 }
597 }
598
599 fn module_member(&self, file: &str, name: &str, imports: bool, depth: u32) -> Option<Value> {
601 if depth > MAX_DEPTH {
602 return None;
603 }
604 if let Some(id) = self.members.get(file).and_then(|m| m.get(name)) {
605 return Some(Value::Sym(id.clone()));
606 }
607 if !imports {
608 return None;
609 }
610 let f = self.facts.get(file)?;
611 for imp in f.imports.iter().rev() {
612 for item in &imp.items {
613 let hit = match item {
614 ImportItem::Name { name: n, alias } => {
615 if alias.as_deref().unwrap_or(n) != name {
616 continue;
617 }
618 self.import_target(f, imp)
619 .and_then(|t| self.member(&t, n, depth + 1))
620 }
621 ImportItem::Module { alias: Some(a) } if a == name => {
622 self.import_target(f, imp)
623 }
624 ImportItem::Module { alias: None }
626 if f.language == Language::Python
627 && imp.module.split('.').next() == Some(name) =>
628 {
629 Some(Value::Module {
630 fam: Family::Py,
631 name: name.to_string(),
632 repo: f.repo.clone(),
633 })
634 }
635 ImportItem::All => self
636 .import_target(f, imp)
637 .and_then(|t| self.member(&t, name, depth + 1)),
638 _ => continue,
639 };
640 if hit.is_some() {
641 return hit;
642 }
643 }
644 }
645 None
646 }
647
648 fn class_member(&self, class: &str, name: &str, depth: u32) -> Option<Value> {
649 if depth > MAX_DEPTH {
650 return None;
651 }
652 if let Some(id) = self.members.get(class).and_then(|m| m.get(name)) {
653 return Some(Value::Sym(id.clone()));
654 }
655 let bases = self.bases.borrow().get(class).cloned().unwrap_or_default();
656 bases
657 .iter()
658 .find_map(|b| self.class_member(b, name, depth + 1))
659 }
660
661 fn type_of(&self, src: &TypeSource, scope: &str, depth: u32) -> Option<Value> {
663 if depth > MAX_DEPTH {
664 return None;
665 }
666 match src {
667 TypeSource::Annotation(p) => {
668 let f = self.file_facts_of(scope)?;
669 match self.resolve_path(p, scope, f, depth + 1)? {
670 Value::Sym(t) if self.is_type(&t) => Some(Value::Instance(t)),
671 _ => None,
672 }
673 }
674 TypeSource::Construct(p) => {
676 let f = self.file_facts_of(scope)?;
677 match self.resolve_path(p, scope, f, depth + 1)? {
678 Value::Sym(t) if self.is_type(&t) => Some(Value::Instance(t)),
679 Value::Sym(t) if self.is_callable(&t) => {
680 let ret = self.types.get(&(t.as_str(), crate::parse::RETURN))?;
681 self.type_of(ret, &t, depth + 1)
682 }
683 _ => None,
684 }
685 }
686 TypeSource::Alias { name, scope: s } => {
687 let f = self.file_facts_of(s)?;
688 match self.resolve_name(name, s, f, depth + 1)? {
689 v @ Value::Instance(_) => Some(v),
690 _ => None,
691 }
692 }
693 }
694 }
695
696 fn file_facts_of(&self, id: &str) -> Option<&'a FileFacts> {
698 if let Some(f) = self.facts.get(id) {
699 return Some(f);
700 }
701 let s = self.sym(id)?;
702 self.by_path
703 .get(&(s.repo.0.as_str(), s.file_path.as_str()))
704 .copied()
705 }
706
707 fn scope_chain(&self, scope: &str) -> Vec<String> {
709 let mut out = vec![scope.to_string()];
710 let mut cur = scope;
711 while let Some(p) = self.parent.get(cur) {
712 out.push(p.clone());
713 cur = p;
714 }
715 out
716 }
717
718 fn enclosing_class(&self, scope: &str) -> Option<String> {
719 self.scope_chain(scope)
720 .into_iter()
721 .find(|s| self.is_type(s))
722 }
723
724 fn resolve_name(&self, head: &str, scope: &str, f: &FileFacts, depth: u32) -> Option<Value> {
725 if head == "self" || head == "this" {
726 return self.enclosing_class(scope).map(Value::Instance);
727 }
728 if f.language == Language::Rust {
729 if let Some(v) = self.rust_head(head, scope, f) {
730 return Some(v);
731 }
732 }
733 for s in self.scope_chain(scope) {
734 if let Some(src) = self.types.get(&(s.as_str(), head)) {
735 if let Some(v) = self.type_of(src, &s, depth + 1) {
736 return Some(v);
737 }
738 if s != f.file_id {
740 return None;
741 }
742 }
743 if self.is_callable(&s) {
745 if let Some(id) = self.members.get(&s).and_then(|m| m.get(head)) {
746 return Some(Value::Sym(id.clone()));
747 }
748 }
749 }
750 let fam = family(&f.language)?;
751 if fam == Family::Jvm {
753 if let Some(c) = self.enclosing_class(scope) {
754 if let Some(v) = self.member(&Value::Instance(c), head, depth + 1) {
755 return Some(v);
756 }
757 }
758 }
759 if let Some(v) = self.module_member(&f.file_id, head, true, depth + 1) {
760 return Some(v);
761 }
762 if fam == Family::Rust {
764 let own = f.modules.first()?;
765 for cand in [format!("{own}::{head}"), head.to_string()] {
766 if self.packages.contains(&(Family::Rust, cand.clone())) {
767 return Some(Value::Module {
768 fam,
769 name: cand,
770 repo: f.repo.clone(),
771 });
772 }
773 }
774 return None;
775 }
776 if !matches!(fam, Family::Go | Family::Jvm) {
778 return None;
779 }
780 let own = f.modules.first()?;
781 self.module_files(fam, own, &f.repo)
782 .iter()
783 .filter(|s| s.file_id != f.file_id)
784 .find_map(|s| self.module_member(&s.file_id, head, false, depth + 1))
785 }
786
787 fn rust_head(&self, head: &str, scope: &str, f: &FileFacts) -> Option<Value> {
789 let own = f.modules.first()?;
790 let name = match head {
791 "Self" => return self.enclosing_class(scope).map(Value::Sym),
792 "crate" => own.split("::").next()?.to_string(),
793 "<self>" => own.clone(),
794 "super" => own.rsplit_once("::")?.0.to_string(),
795 _ => return None,
796 };
797 Some(Value::Module {
798 fam: Family::Rust,
799 name,
800 repo: f.repo.clone(),
801 })
802 }
803
804 fn resolve_path(
805 &self,
806 path: &[String],
807 scope: &str,
808 f: &FileFacts,
809 depth: u32,
810 ) -> Option<Value> {
811 let (head, rest) = path.split_first()?;
812 let mut v = self.resolve_name(head, scope, f, depth)?;
813 for seg in rest {
814 v = self.member(&v, seg, depth)?;
815 }
816 Some(v)
817 }
818}
819
820fn top_module(f: &FileFacts, imp: &Import) -> String {
822 if imp.level > 0 || imp.module.starts_with('.') {
823 return format!("(relative in {})", f.repo);
824 }
825 if matches!(f.language, Language::Java | Language::Kotlin) {
826 return imp
827 .module
828 .splitn(4, '.')
829 .take(3)
830 .collect::<Vec<_>>()
831 .join(".");
832 }
833 if f.language == Language::Go
835 && imp
836 .module
837 .split('/')
838 .next()
839 .is_some_and(|h| h.contains('.'))
840 {
841 return imp
842 .module
843 .splitn(4, '/')
844 .take(3)
845 .collect::<Vec<_>>()
846 .join("/");
847 }
848 let mut parts = imp.module.split(['.', '/']);
849 let first = parts.next().unwrap_or_default();
850 match (first.starts_with('@'), parts.next()) {
851 (true, Some(second)) => format!("{first}/{second}"),
852 _ => first.to_string(),
853 }
854}
855
856fn normalize(dir: &str, spec: &str) -> Option<String> {
858 let mut parts: Vec<&str> = dir.split('/').filter(|p| !p.is_empty()).collect();
859 for seg in spec.split('/') {
860 match seg {
861 "" | "." => {}
862 ".." => {
863 parts.pop()?;
864 }
865 s => parts.push(s),
866 }
867 }
868 Some(parts.join("/"))
869}
870
871#[cfg(test)]
872mod tests {
873 use super::*;
874
875 #[test]
876 fn normalize_relative_specifiers() {
877 assert_eq!(normalize("src/a", "./b").as_deref(), Some("src/a/b"));
878 assert_eq!(normalize("src/a", "../c/d").as_deref(), Some("src/c/d"));
879 assert_eq!(normalize("", "./x").as_deref(), Some("x"));
880 assert_eq!(normalize("", "../x"), None);
881 }
882}