1use std::collections::{HashMap, HashSet, VecDeque};
8use std::path::PathBuf;
9
10use rayon::prelude::*;
11
12use crate::error::{Diagnostic, Severity};
13use crate::graph::edge::{
14 CONFIDENCE_CROSS_LANGUAGE, CONFIDENCE_OWN_OR_DIRECT, CONFIDENCE_TRANSITIVE,
15};
16use crate::language::LangId;
17use crate::model::{FileExtraction, FileId, SourceRange, SymbolId, Visibility};
18
19pub type ScopeMap = HashMap<String, Vec<(SymbolId, f32)>>;
20pub(crate) type SymbolIndexEntry = (SymbolId, String, LangId, Option<Visibility>);
21pub(crate) type SymbolIndex = HashMap<FileId, Vec<SymbolIndexEntry>>;
22
23struct Candidate {
25 symbol: SymbolId,
26 confidence: f32,
27 rank: u8,
28 path: PathBuf,
29 name: String,
30}
31
32pub struct ResolutionContext {
34 pub symbol_index: SymbolIndex,
35 pub import_adjacency: HashMap<FileId, Vec<FileId>>,
36 pub file_languages: HashMap<FileId, LangId>,
37 pub file_paths: HashMap<FileId, PathBuf>,
38}
39
40impl ResolutionContext {
41 pub fn from_extractions<F>(
43 extractions: &[F],
44 path_to_file_id: &HashMap<PathBuf, FileId>,
45 import_adjacency: HashMap<FileId, Vec<FileId>>,
46 ) -> Self
47 where
48 F: std::borrow::Borrow<FileExtraction>,
49 {
50 let symbol_index = build_symbol_index(extractions, path_to_file_id);
51 let file_languages: HashMap<_, _> = extractions
52 .iter()
53 .filter_map(|f| {
54 let f = f.borrow();
55 Some((path_to_file_id.get(&f.path)?.to_owned(), f.lang))
56 })
57 .collect();
58 let file_paths: HashMap<_, _> = path_to_file_id
59 .iter()
60 .map(|(path, &fid)| (fid, path.clone()))
61 .collect();
62
63 Self {
64 symbol_index,
65 import_adjacency,
66 file_languages,
67 file_paths,
68 }
69 }
70}
71
72#[derive(Debug, Clone, Default)]
77pub struct FlattenedScopeCache {
78 scopes: HashMap<FileId, ScopeMap>,
79}
80
81impl FlattenedScopeCache {
82 pub fn build(ctx: &ResolutionContext, diagnostics: &mut Vec<Diagnostic>) -> Self {
90 let mut results: Vec<(FileId, ScopeMap, Vec<Diagnostic>)> = ctx
91 .symbol_index
92 .par_iter()
93 .map(|(&file_id, _)| {
94 let (scope, diags) = Self::compute_scope(file_id, ctx);
95 (file_id, scope, diags)
96 })
97 .collect();
98
99 results.sort_by(|a, b| ctx.file_paths.get(&a.0).cmp(&ctx.file_paths.get(&b.0)));
102
103 let mut scopes = HashMap::with_capacity(results.len());
104 for (file_id, scope, diags) in results {
105 scopes.insert(file_id, scope);
106 diagnostics.extend(diags);
107 }
108
109 Self { scopes }
110 }
111
112 fn compute_scope(file_id: FileId, ctx: &ResolutionContext) -> (ScopeMap, Vec<Diagnostic>) {
113 let mut diagnostics = Vec::new();
114 let source_lang = ctx.file_languages.get(&file_id).copied();
115 let mut scope: ScopeMap = HashMap::new();
116 let mut candidates: Vec<Candidate> = Vec::new();
117 let mut visited: HashSet<FileId> = HashSet::new();
118 let mut queue: VecDeque<(FileId, usize)> = VecDeque::new();
119
120 queue.push_back((file_id, 0));
121
122 while let Some((current, distance)) = queue.pop_front() {
123 if !visited.insert(current) {
124 continue;
125 }
126
127 if let Some(symbols) = ctx.symbol_index.get(¤t) {
128 let default_vis = ctx
129 .file_languages
130 .get(¤t)
131 .map(|lang| lang.spec().default_visibility)
132 .unwrap_or(crate::language::DefaultVisibility::PublicByDefault);
133
134 for (sym_id, name, sym_lang, visibility) in symbols {
135 let is_public = match visibility {
136 Some(Visibility::Public) => true,
137 Some(Visibility::Private) => current == file_id,
138 None => {
139 matches!(
140 default_vis,
141 crate::language::DefaultVisibility::PublicByDefault
142 ) || current == file_id
143 }
144 };
145
146 if !is_public {
147 continue;
148 }
149
150 let same_lang = source_lang.is_some() && source_lang == Some(*sym_lang);
151 let diff_lang = source_lang.is_some() && source_lang != Some(*sym_lang);
152
153 let confidence = if distance == 0 || (distance == 1 && same_lang) {
154 CONFIDENCE_OWN_OR_DIRECT
155 } else if diff_lang {
156 CONFIDENCE_CROSS_LANGUAGE
157 } else {
158 CONFIDENCE_TRANSITIVE
159 };
160
161 let rank = if distance == 0 {
165 0u8
166 } else if distance == 1 && same_lang {
167 1
168 } else if same_lang {
169 2
170 } else {
171 3
172 };
173
174 candidates.push(Candidate {
175 symbol: *sym_id,
176 name: name.clone(),
177 confidence,
178 rank,
179 path: ctx.file_paths.get(¤t).cloned().unwrap_or_default(),
180 });
181 }
182 }
183
184 if let Some(neighbors) = ctx.import_adjacency.get(¤t) {
185 for &neighbor in neighbors {
186 if !visited.contains(&neighbor) {
187 queue.push_back((neighbor, distance + 1));
188 } else if neighbor == file_id {
189 let path = ctx
190 .file_paths
191 .get(¤t)
192 .cloned()
193 .unwrap_or_else(|| PathBuf::from("<unknown>"));
194 let root_path = ctx
195 .file_paths
196 .get(&file_id)
197 .map(|p| p.display().to_string())
198 .unwrap_or_else(|| "<unknown>".to_string());
199 diagnostics.push(Diagnostic {
200 path,
201 severity: Severity::Warning,
202 message: format!(
203 "circular import: {} -> {}",
204 current.to_raw(),
205 root_path
206 ),
207 source_range: None,
208 });
209 }
210 }
211 }
212 }
213
214 let mut grouped: HashMap<String, Vec<Candidate>> = HashMap::new();
218 for candidate in candidates {
219 grouped
220 .entry(candidate.name.clone())
221 .or_default()
222 .push(candidate);
223 }
224 for (name, mut group) in grouped {
225 let best = group.iter().map(|candidate| candidate.rank).min();
226 if let Some(best) = best {
227 group.retain(|candidate| candidate.rank == best);
228 }
229 group.sort_by(|a, b| {
230 a.rank
231 .cmp(&b.rank)
232 .then(b.confidence.total_cmp(&a.confidence))
233 .then(a.path.cmp(&b.path))
234 .then(a.symbol.to_raw().cmp(&b.symbol.to_raw()))
235 });
236 scope.insert(
237 name,
238 group
239 .into_iter()
240 .map(|candidate| (candidate.symbol, candidate.confidence))
241 .collect(),
242 );
243 }
244
245 (scope, diagnostics)
246 }
247
248 pub fn resolve(&self, file_id: FileId, name: &str) -> Option<&[(SymbolId, f32)]> {
252 self.scopes
253 .get(&file_id)
254 .and_then(|s| s.get(name).map(|v| v.as_slice()))
255 }
256
257 pub fn scope(&self, file_id: FileId) -> Option<&ScopeMap> {
259 self.scopes.get(&file_id)
260 }
261
262 pub fn iter_scopes(&self) -> impl Iterator<Item = (FileId, &ScopeMap)> {
264 self.scopes.iter().map(|(&file_id, scope)| (file_id, scope))
265 }
266
267 pub fn len(&self) -> usize {
269 self.scopes.len()
270 }
271
272 pub fn is_empty(&self) -> bool {
274 self.scopes.is_empty()
275 }
276}
277
278#[derive(Debug, Clone, PartialEq)]
284pub struct ResolvedReference {
285 pub file_path: PathBuf,
287 pub range: SourceRange,
289 pub source: SymbolId,
291 pub target: SymbolId,
293 pub confidence: f32,
295}
296
297pub fn resolve_references_detailed<F>(
310 extractions: &[F],
311 path_to_file_id: &HashMap<PathBuf, FileId>,
312 scope_cache: &FlattenedScopeCache,
313 diagnostics: &mut Vec<Diagnostic>,
314) -> Vec<ResolvedReference>
315where
316 F: std::borrow::Borrow<FileExtraction> + Sync,
317{
318 #[allow(clippy::type_complexity)]
319 let results: Vec<(Vec<ResolvedReference>, Vec<Diagnostic>)> = extractions
320 .par_iter()
321 .map(|file_ext| {
322 let file_ext = file_ext.borrow();
323 let mut local_refs = Vec::new();
324 let mut local_diags = Vec::new();
325
326 let file_id = match path_to_file_id.get(&file_ext.path) {
327 Some(&id) => id,
328 None => return (local_refs, local_diags),
329 };
330
331 let file_path = &file_ext.path;
332 for ref_ in &file_ext.references {
333 if let Some(matches) = scope_cache.resolve(file_id, &ref_.name) {
334 let source_sym = file_ext
337 .symbols
338 .iter()
339 .filter(|s| {
340 s.source_range.byte_start <= ref_.range.byte_start
341 && s.source_range.byte_end >= ref_.range.byte_end
342 })
343 .min_by_key(|s| s.source_range.byte_end - s.source_range.byte_start);
344
345 if let Some(source) = source_sym {
346 for &(target_id, confidence) in matches {
347 local_refs.push(ResolvedReference {
348 file_path: file_path.clone(),
349 range: ref_.range.clone(),
350 source: source.id,
351 target: target_id,
352 confidence,
353 });
354 }
355 }
356 } else {
357 local_diags.push(Diagnostic {
358 path: file_path.clone(),
359 severity: Severity::Warning,
360 message: format!("unresolved reference: '{}'", ref_.name),
361 source_range: Some(ref_.range.clone()),
362 });
363 }
364 }
365 (local_refs, local_diags)
366 })
367 .collect();
368
369 let mut resolved = Vec::new();
370 for (mut local_refs, local_diags) in results {
371 resolved.append(&mut local_refs);
372 diagnostics.extend(local_diags);
373 }
374 resolved
375}
376
377pub fn reference_edges(resolved: &[ResolvedReference]) -> Vec<(SymbolId, SymbolId, f32)> {
384 let mut seen: HashMap<(SymbolId, SymbolId), f32> = HashMap::with_capacity(resolved.len());
385 for reference in resolved {
386 seen.entry((reference.source, reference.target))
387 .and_modify(|confidence| *confidence = confidence.max(reference.confidence))
388 .or_insert(reference.confidence);
389 }
390 let mut edges: Vec<_> = seen
391 .into_iter()
392 .map(|((source, target), confidence)| (source, target, confidence))
393 .collect();
394 edges.sort_by_key(|(source, target, _)| (source.to_raw(), target.to_raw()));
395 edges
396}
397
398pub fn resolve_all_references<F>(
405 extractions: &[F],
406 path_to_file_id: &HashMap<PathBuf, FileId>,
407 scope_cache: &FlattenedScopeCache,
408 diagnostics: &mut Vec<Diagnostic>,
409) -> Vec<(SymbolId, SymbolId, f32)>
410where
411 F: std::borrow::Borrow<FileExtraction> + Sync,
412{
413 let resolved =
414 resolve_references_detailed(extractions, path_to_file_id, scope_cache, diagnostics);
415 reference_edges(&resolved)
416}
417
418pub fn build_symbol_index<F>(
422 extractions: &[F],
423 path_to_file_id: &HashMap<PathBuf, FileId>,
424) -> SymbolIndex
425where
426 F: std::borrow::Borrow<FileExtraction>,
427{
428 let mut index: SymbolIndex = HashMap::new();
429
430 for file_ext in extractions {
431 let file_ext = file_ext.borrow();
432 if let Some(&file_id) = path_to_file_id.get(&file_ext.path) {
433 let entries: Vec<_> = file_ext
434 .symbols
435 .iter()
436 .map(|s| (s.id, s.name.clone(), s.language, s.visibility))
437 .collect();
438 index.entry(file_id).or_default().extend(entries);
439 }
440 }
441
442 index
443}
444
445#[cfg(test)]
446mod tests {
447 use super::*;
448 use std::path::PathBuf;
449
450 #[test]
451 fn empty_cache() {
452 let cache = FlattenedScopeCache {
453 scopes: HashMap::new(),
454 };
455 assert!(cache.is_empty());
456 assert_eq!(cache.len(), 0);
457 assert!(cache.resolve(FileId::new(1).unwrap(), "foo").is_none());
458 }
459
460 #[test]
461 fn detailed_resolution_keeps_each_use_site_and_the_triples_are_unchanged() {
462 use crate::model::{LineColumn, Symbol, SymbolKind, UnresolvedReference};
463
464 fn range(start: usize, end: usize) -> SourceRange {
465 SourceRange {
466 byte_start: start,
467 byte_end: end,
468 start: LineColumn {
469 line: 0,
470 column: start,
471 },
472 end: LineColumn {
473 line: 0,
474 column: end,
475 },
476 }
477 }
478
479 fn symbol(id: u32, name: &str, start: usize, end: usize, path: &std::path::Path) -> Symbol {
480 Symbol {
481 id: SymbolId::new(id).unwrap(),
482 name: name.to_string(),
483 kind: SymbolKind::Function,
484 language: LangId::Python,
485 file_path: path.to_path_buf(),
486 source_range: range(start, end),
487 name_range: None,
488 visibility: None,
489 signature: None,
490 docstring: None,
491 is_async: false,
492 }
493 }
494
495 let path = PathBuf::from("a.py");
496 let file_id = FileId::new(1).unwrap();
497 let mut file = FileExtraction::empty(path.clone(), LangId::Python);
498 file.symbols = vec![
499 symbol(1, "caller", 0, 100, &path),
500 symbol(2, "helper", 200, 210, &path),
501 ];
502 file.references = vec![
503 UnresolvedReference {
504 name: "helper".into(),
505 range: range(10, 16),
506 },
507 UnresolvedReference {
508 name: "helper".into(),
509 range: range(30, 36),
510 },
511 ];
512 let mut symbol_index: SymbolIndex = HashMap::new();
513 symbol_index.insert(
514 file_id,
515 vec![
516 (
517 SymbolId::new(1).unwrap(),
518 "caller".into(),
519 LangId::Python,
520 None,
521 ),
522 (
523 SymbolId::new(2).unwrap(),
524 "helper".into(),
525 LangId::Python,
526 None,
527 ),
528 ],
529 );
530 let ctx = ResolutionContext {
531 symbol_index,
532 import_adjacency: HashMap::new(),
533 file_languages: HashMap::from([(file_id, LangId::Python)]),
534 file_paths: HashMap::from([(file_id, path.clone())]),
535 };
536 let cache = FlattenedScopeCache::build(&ctx, &mut Vec::new());
537 let extractions = vec![file];
538 let paths = HashMap::from([(path, file_id)]);
539
540 let mut diagnostics = Vec::new();
541 let resolved = resolve_references_detailed(&extractions, &paths, &cache, &mut diagnostics);
542
543 assert!(diagnostics.is_empty());
544 assert_eq!(resolved.len(), 2, "one record per use site");
545 assert_eq!(resolved[0].range.byte_start, 10);
546 assert_eq!(resolved[1].range.byte_start, 30);
547 assert!(
548 resolved
549 .iter()
550 .all(|record| record.source == SymbolId::new(1).unwrap())
551 );
552 assert!(
553 resolved.iter().all(
554 |record| record.target == SymbolId::new(2).unwrap() && record.confidence == 1.0
555 )
556 );
557
558 let mut wrapper_diagnostics = Vec::new();
559 let triples =
560 resolve_all_references(&extractions, &paths, &cache, &mut wrapper_diagnostics);
561 assert!(wrapper_diagnostics.is_empty());
562 assert_eq!(
563 triples,
564 vec![(SymbolId::new(1).unwrap(), SymbolId::new(2).unwrap(), 1.0)],
565 "the wrapper max-merges the two use sites into one edge"
566 );
567 }
568
569 #[test]
570 fn scope_cache_resolve_own_file() {
571 let mut symbol_index: SymbolIndex = HashMap::new();
572 symbol_index.insert(
573 FileId::new(1).unwrap(),
574 vec![(
575 SymbolId::new(10).unwrap(),
576 "main".into(),
577 LangId::Python,
578 None,
579 )],
580 );
581
582 let ctx = ResolutionContext {
583 symbol_index,
584 import_adjacency: HashMap::new(),
585 file_languages: HashMap::from([(FileId::new(1).unwrap(), LangId::Python)]),
586 file_paths: HashMap::new(),
587 };
588
589 let cache = FlattenedScopeCache::build(&ctx, &mut Vec::new());
590 let result = cache.resolve(FileId::new(1).unwrap(), "main");
591 assert!(result.is_some());
592 let matches = result.unwrap();
593 assert_eq!(matches.len(), 1);
594 assert_eq!(matches[0].0, SymbolId::new(10).unwrap());
595 assert_eq!(matches[0].1, 1.0);
596 }
597
598 #[test]
599 fn scope_cache_resolve_imported_symbol() {
600 let mut symbol_index = HashMap::new();
601 symbol_index.insert(FileId::new(1).unwrap(), vec![]);
602 symbol_index.insert(
603 FileId::new(2).unwrap(),
604 vec![(
605 SymbolId::new(20).unwrap(),
606 "helper".into(),
607 LangId::Python,
608 Some(Visibility::Public),
609 )],
610 );
611
612 let ctx = ResolutionContext {
613 symbol_index,
614 import_adjacency: HashMap::from([(
615 FileId::new(1).unwrap(),
616 vec![FileId::new(2).unwrap()],
617 )]),
618 file_languages: HashMap::from([
619 (FileId::new(1).unwrap(), LangId::Python),
620 (FileId::new(2).unwrap(), LangId::Python),
621 ]),
622 file_paths: HashMap::new(),
623 };
624
625 let cache = FlattenedScopeCache::build(&ctx, &mut Vec::new());
626 let result = cache.resolve(FileId::new(1).unwrap(), "helper");
627 assert!(result.is_some());
628 let matches = result.unwrap();
629 assert_eq!(matches.len(), 1);
630 assert_eq!(matches[0].0, SymbolId::new(20).unwrap());
631 assert_eq!(matches[0].1, 1.0);
632 }
633
634 #[test]
635 fn scope_cache_missing_symbol() {
636 let mut symbol_index = HashMap::new();
637 symbol_index.insert(
638 FileId::new(1).unwrap(),
639 vec![(
640 SymbolId::new(10).unwrap(),
641 "foo".into(),
642 LangId::Python,
643 None,
644 )],
645 );
646
647 let ctx = ResolutionContext {
648 symbol_index,
649 import_adjacency: HashMap::new(),
650 file_languages: HashMap::from([(FileId::new(1).unwrap(), LangId::Python)]),
651 file_paths: HashMap::new(),
652 };
653
654 let cache = FlattenedScopeCache::build(&ctx, &mut Vec::new());
655 assert!(cache.resolve(FileId::new(1).unwrap(), "bar").is_none());
656 }
657
658 #[test]
659 fn scope_cache_cycle_safe() {
660 let mut symbol_index = HashMap::new();
661 symbol_index.insert(
662 FileId::new(1).unwrap(),
663 vec![(
664 SymbolId::new(10).unwrap(),
665 "a".into(),
666 LangId::Python,
667 Some(Visibility::Public),
668 )],
669 );
670 symbol_index.insert(
671 FileId::new(2).unwrap(),
672 vec![(
673 SymbolId::new(20).unwrap(),
674 "b".into(),
675 LangId::Python,
676 Some(Visibility::Public),
677 )],
678 );
679
680 let ctx = ResolutionContext {
682 symbol_index,
683 import_adjacency: HashMap::from([
684 (FileId::new(1).unwrap(), vec![FileId::new(2).unwrap()]),
685 (FileId::new(2).unwrap(), vec![FileId::new(1).unwrap()]),
686 ]),
687 file_languages: HashMap::from([
688 (FileId::new(1).unwrap(), LangId::Python),
689 (FileId::new(2).unwrap(), LangId::Python),
690 ]),
691 file_paths: HashMap::new(),
692 };
693
694 let cache = FlattenedScopeCache::build(&ctx, &mut Vec::new());
695 assert!(cache.resolve(FileId::new(1).unwrap(), "b").is_some());
697 assert!(cache.resolve(FileId::new(2).unwrap(), "a").is_some());
698 }
699
700 #[test]
701 fn scope_cache_cross_language_confidence() {
702 let mut symbol_index = HashMap::new();
703 symbol_index.insert(FileId::new(1).unwrap(), vec![]);
704 symbol_index.insert(
705 FileId::new(2).unwrap(),
706 vec![(
707 SymbolId::new(20).unwrap(),
708 "util".into(),
709 LangId::Rust,
710 Some(Visibility::Public),
711 )],
712 );
713
714 let ctx = ResolutionContext {
715 symbol_index,
716 import_adjacency: HashMap::from([(
717 FileId::new(1).unwrap(),
718 vec![FileId::new(2).unwrap()],
719 )]),
720 file_languages: HashMap::from([
721 (FileId::new(1).unwrap(), LangId::Python),
722 (FileId::new(2).unwrap(), LangId::Rust),
723 ]),
724 file_paths: HashMap::new(),
725 };
726
727 let cache = FlattenedScopeCache::build(&ctx, &mut Vec::new());
728 let result = cache.resolve(FileId::new(1).unwrap(), "util");
729 assert!(result.is_some());
730 assert_eq!(result.unwrap()[0].1, 0.6);
731 }
732
733 #[test]
734 fn resolve_references_creates_edges() {
735 use crate::model::{LineColumn, SourceRange, Symbol, SymbolKind, UnresolvedReference};
736
737 let sym_a = Symbol {
738 id: SymbolId::new(1).unwrap(),
739 name: "caller".into(),
740 kind: SymbolKind::Function,
741 language: LangId::Python,
742 file_path: PathBuf::from("/proj/a.py"),
743 source_range: SourceRange {
744 byte_start: 0,
745 byte_end: 50,
746 start: LineColumn { line: 0, column: 0 },
747 end: LineColumn { line: 2, column: 0 },
748 },
749 name_range: None,
750 visibility: None,
751 signature: None,
752 docstring: None,
753 is_async: false,
754 };
755
756 let mut file = FileExtraction::empty(PathBuf::from("/proj/a.py"), LangId::Python);
757 file.symbols = vec![sym_a];
758 file.references = vec![UnresolvedReference {
759 name: "helper".into(),
760 range: SourceRange {
761 byte_start: 20,
762 byte_end: 26,
763 start: LineColumn { line: 1, column: 4 },
764 end: LineColumn {
765 line: 1,
766 column: 10,
767 },
768 },
769 }];
770
771 let mut path_to_file_id = HashMap::new();
772 path_to_file_id.insert(PathBuf::from("/proj/a.py"), FileId::new(1).unwrap());
773
774 let mut scopes: HashMap<FileId, ScopeMap> = HashMap::new();
775 let mut scope = HashMap::new();
776 scope.insert("helper".into(), vec![(SymbolId::new(99).unwrap(), 1.0)]);
777 scopes.insert(FileId::new(1).unwrap(), scope);
778
779 let cache = FlattenedScopeCache { scopes };
780
781 let edges = resolve_all_references(&[file], &path_to_file_id, &cache, &mut Vec::new());
782 assert_eq!(edges.len(), 1);
783 assert_eq!(edges[0].0, SymbolId::new(1).unwrap());
784 assert_eq!(edges[0].1, SymbolId::new(99).unwrap());
785 assert_eq!(edges[0].2, 1.0);
786 }
787
788 #[test]
789 fn resolve_references_selects_innermost_enclosing_symbol_regardless_of_vector_order() {
790 use crate::model::{LineColumn, SourceRange, Symbol, SymbolKind, UnresolvedReference};
791
792 let inner_method = Symbol {
794 id: SymbolId::new(1).unwrap(),
795 name: "inner_method".into(),
796 kind: SymbolKind::Method,
797 language: LangId::Python,
798 file_path: PathBuf::from("/proj/a.py"),
799 source_range: SourceRange {
800 byte_start: 10,
801 byte_end: 50,
802 start: LineColumn { line: 1, column: 0 },
803 end: LineColumn { line: 3, column: 0 },
804 },
805 name_range: None,
806 visibility: None,
807 signature: None,
808 docstring: None,
809 is_async: false,
810 };
811
812 let outer_class = Symbol {
814 id: SymbolId::new(2).unwrap(),
815 name: "OuterClass".into(),
816 kind: SymbolKind::Class,
817 language: LangId::Python,
818 file_path: PathBuf::from("/proj/a.py"),
819 source_range: SourceRange {
820 byte_start: 0,
821 byte_end: 100,
822 start: LineColumn { line: 0, column: 0 },
823 end: LineColumn { line: 5, column: 0 },
824 },
825 name_range: None,
826 visibility: None,
827 signature: None,
828 docstring: None,
829 is_async: false,
830 };
831
832 let mut file = FileExtraction::empty(PathBuf::from("/proj/a.py"), LangId::Python);
833 file.symbols = vec![inner_method, outer_class]; file.references = vec![UnresolvedReference {
835 name: "helper".into(),
836 range: SourceRange {
837 byte_start: 20,
838 byte_end: 26,
839 start: LineColumn { line: 2, column: 4 },
840 end: LineColumn {
841 line: 2,
842 column: 10,
843 },
844 },
845 }];
846
847 let mut path_to_file_id = HashMap::new();
848 path_to_file_id.insert(PathBuf::from("/proj/a.py"), FileId::new(1).unwrap());
849
850 let mut scopes: HashMap<FileId, ScopeMap> = HashMap::new();
851 let mut scope = HashMap::new();
852 scope.insert("helper".into(), vec![(SymbolId::new(99).unwrap(), 1.0)]);
853 scopes.insert(FileId::new(1).unwrap(), scope);
854
855 let cache = FlattenedScopeCache { scopes };
856
857 let edges = resolve_all_references(&[file], &path_to_file_id, &cache, &mut Vec::new());
858 assert_eq!(edges.len(), 1);
859 assert_eq!(
861 edges[0].0,
862 SymbolId::new(1).unwrap(),
863 "Reference should attach to innermost symbol SymbolId(1), but attached to SymbolId({})",
864 edges[0].0.to_raw()
865 );
866 assert_eq!(edges[0].1, SymbolId::new(99).unwrap());
867 }
868
869 #[test]
870 fn local_symbol_shadows_the_imported_symbol() {
871 let mut symbol_index: SymbolIndex = HashMap::new();
872 symbol_index.insert(
873 FileId::new(1).unwrap(),
874 vec![(
875 SymbolId::new(10).unwrap(),
876 "helper".into(),
877 LangId::Python,
878 Some(Visibility::Public),
879 )],
880 );
881 symbol_index.insert(
882 FileId::new(2).unwrap(),
883 vec![(
884 SymbolId::new(20).unwrap(),
885 "helper".into(),
886 LangId::Python,
887 Some(Visibility::Public),
888 )],
889 );
890
891 let ctx = ResolutionContext {
892 symbol_index,
893 import_adjacency: HashMap::from([(
894 FileId::new(1).unwrap(),
895 vec![FileId::new(2).unwrap()],
896 )]),
897 file_languages: HashMap::from([
898 (FileId::new(1).unwrap(), LangId::Python),
899 (FileId::new(2).unwrap(), LangId::Python),
900 ]),
901 file_paths: HashMap::from([
902 (FileId::new(1).unwrap(), PathBuf::from("/proj/main.py")),
903 (FileId::new(2).unwrap(), PathBuf::from("/proj/lib.py")),
904 ]),
905 };
906
907 let cache = FlattenedScopeCache::build(&ctx, &mut Vec::new());
908 let matches = cache.resolve(FileId::new(1).unwrap(), "helper").unwrap();
909 assert_eq!(
910 matches.len(),
911 1,
912 "the local definition must shadow the imported one"
913 );
914 assert_eq!(matches[0].0, SymbolId::new(10).unwrap());
915 assert_eq!(matches[0].1, 1.0);
916 }
917
918 #[test]
919 fn imported_candidates_are_ranked_by_path_not_by_id() {
920 let mut symbol_index: SymbolIndex = HashMap::new();
921 symbol_index.insert(FileId::new(1).unwrap(), vec![]);
922 symbol_index.insert(
924 FileId::new(2).unwrap(),
925 vec![(
926 SymbolId::new(30).unwrap(),
927 "util".into(),
928 LangId::Python,
929 Some(Visibility::Public),
930 )],
931 );
932 symbol_index.insert(
933 FileId::new(3).unwrap(),
934 vec![(
935 SymbolId::new(20).unwrap(),
936 "util".into(),
937 LangId::Python,
938 Some(Visibility::Public),
939 )],
940 );
941
942 let ctx = ResolutionContext {
943 symbol_index,
944 import_adjacency: HashMap::from([(
945 FileId::new(1).unwrap(),
946 vec![FileId::new(2).unwrap(), FileId::new(3).unwrap()],
947 )]),
948 file_languages: HashMap::from([
949 (FileId::new(1).unwrap(), LangId::Python),
950 (FileId::new(2).unwrap(), LangId::Python),
951 (FileId::new(3).unwrap(), LangId::Python),
952 ]),
953 file_paths: HashMap::from([
954 (FileId::new(1).unwrap(), PathBuf::from("/proj/app.py")),
955 (FileId::new(2).unwrap(), PathBuf::from("/proj/a_util.py")),
956 (FileId::new(3).unwrap(), PathBuf::from("/proj/z_util.py")),
957 ]),
958 };
959
960 let cache = FlattenedScopeCache::build(&ctx, &mut Vec::new());
961 let matches = cache.resolve(FileId::new(1).unwrap(), "util").unwrap();
962 assert_eq!(
963 matches.len(),
964 2,
965 "an ambiguous import keeps both candidates"
966 );
967 assert_eq!(
968 matches[0].0,
969 SymbolId::new(30).unwrap(),
970 "candidate order must follow the file path, not the raw symbol id"
971 );
972 }
973}