1use std::path::Path;
7
8use serde::{Deserialize, Serialize};
9
10use crate::graph::unified::string::id::StringId;
11
12use crate::graph::node::Language;
13use crate::graph::unified::concurrent::GraphSnapshot;
14use crate::graph::unified::file::id::FileId;
15use crate::graph::unified::node::id::NodeId;
16use crate::graph::unified::node::kind::NodeKind;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum FileScope<'a> {
21 Any,
23 Path(&'a Path),
25 FileId(FileId),
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
31pub enum ResolutionMode {
32 Strict,
34 AllowSuffixCandidates,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub struct SymbolQuery<'a> {
41 pub symbol: &'a str,
43 pub file_scope: FileScope<'a>,
45 pub mode: ResolutionMode,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
51pub enum ResolvedFileScope {
52 Any,
54 File(FileId),
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum FileScopeError {
61 FileNotIndexed,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67pub struct NormalizedSymbolQuery {
68 pub symbol: String,
70 pub file_scope: ResolvedFileScope,
72 pub mode: ResolutionMode,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
84pub enum SymbolCandidateBucket {
85 ExactQualified,
87 ExactSimple,
89 CanonicalSuffix,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct SymbolCandidateWitness {
96 pub node_id: NodeId,
98 pub bucket: SymbolCandidateBucket,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
104pub enum SymbolResolutionOutcome {
105 Resolved(NodeId),
107 NotFound,
109 FileNotIndexed,
111 Ambiguous(Vec<NodeId>),
113}
114
115#[derive(Debug, Clone, PartialEq, Eq)]
117pub enum SymbolCandidateOutcome {
118 Candidates(Vec<NodeId>),
120 NotFound,
122 FileNotIndexed,
124}
125
126#[derive(Debug, Clone, PartialEq, Eq)]
132pub struct SymbolResolutionWitness {
133 pub normalized_query: Option<NormalizedSymbolQuery>,
135 pub outcome: SymbolResolutionOutcome,
137 pub selected_bucket: Option<SymbolCandidateBucket>,
139 pub candidates: Vec<SymbolCandidateWitness>,
141 pub symbol: Option<StringId>,
151 pub steps: Vec<crate::graph::unified::bind::witness::step::ResolutionStep>,
160}
161
162impl GraphSnapshot {
163 #[must_use]
165 pub fn resolve_symbol(&self, query: &SymbolQuery<'_>) -> SymbolResolutionOutcome {
166 self.resolve_symbol_with_witness(query).outcome
167 }
168
169 #[must_use]
171 pub fn find_symbol_candidates(&self, query: &SymbolQuery<'_>) -> SymbolCandidateOutcome {
172 self.find_symbol_candidates_with_witness(query).outcome
173 }
174
175 #[must_use]
178 pub fn find_symbol_candidates_with_witness(
179 &self,
180 query: &SymbolQuery<'_>,
181 ) -> SymbolCandidateSearchWitness {
182 let resolved_file_scope = match self.resolve_file_scope(&query.file_scope) {
183 Ok(scope) => scope,
184 Err(FileScopeError::FileNotIndexed) => {
185 return SymbolCandidateSearchWitness {
186 normalized_query: None,
187 outcome: SymbolCandidateOutcome::FileNotIndexed,
188 selected_bucket: None,
189 candidates: Vec::new(),
190 };
191 }
192 };
193
194 let normalized_query = self.normalize_symbol_query(query, &resolved_file_scope);
195
196 if let Some((selected_bucket, candidates)) =
197 self.first_candidate_bucket_with_witness(&normalized_query, resolved_file_scope)
198 {
199 return SymbolCandidateSearchWitness {
200 normalized_query: Some(normalized_query),
201 outcome: SymbolCandidateOutcome::Candidates(
202 candidates
203 .iter()
204 .map(|candidate| candidate.node_id)
205 .collect(),
206 ),
207 selected_bucket: Some(selected_bucket),
208 candidates,
209 };
210 }
211
212 SymbolCandidateSearchWitness {
213 normalized_query: Some(normalized_query),
214 outcome: SymbolCandidateOutcome::NotFound,
215 selected_bucket: None,
216 candidates: Vec::new(),
217 }
218 }
219
220 #[must_use]
223 pub fn resolve_symbol_with_witness(&self, query: &SymbolQuery<'_>) -> SymbolResolutionWitness {
224 let candidate_witness = self.find_symbol_candidates_with_witness(query);
225 let outcome = match &candidate_witness.outcome {
226 SymbolCandidateOutcome::Candidates(candidates) => match candidates.as_slice() {
227 [] => SymbolResolutionOutcome::NotFound,
228 [node_id] => SymbolResolutionOutcome::Resolved(*node_id),
229 _ => SymbolResolutionOutcome::Ambiguous(candidates.clone()),
230 },
231 SymbolCandidateOutcome::NotFound => SymbolResolutionOutcome::NotFound,
232 SymbolCandidateOutcome::FileNotIndexed => SymbolResolutionOutcome::FileNotIndexed,
233 };
234
235 let symbol = candidate_witness
237 .normalized_query
238 .as_ref()
239 .and_then(|nq| self.strings().get(&nq.symbol));
240
241 SymbolResolutionWitness {
242 normalized_query: candidate_witness.normalized_query,
243 outcome,
244 selected_bucket: candidate_witness.selected_bucket,
245 candidates: candidate_witness.candidates,
246 symbol,
247 steps: Vec::new(),
248 }
249 }
250
251 pub fn resolve_file_scope(
258 &self,
259 file_scope: &FileScope<'_>,
260 ) -> Result<ResolvedFileScope, FileScopeError> {
261 match *file_scope {
262 FileScope::Any => Ok(ResolvedFileScope::Any),
263 FileScope::Path(path) => self
264 .files()
265 .get(path)
266 .filter(|file_id| !self.indices().by_file(*file_id).is_empty())
267 .map_or(Err(FileScopeError::FileNotIndexed), |file_id| {
268 Ok(ResolvedFileScope::File(file_id))
269 }),
270 FileScope::FileId(file_id) => {
271 let is_indexed = self.files().resolve(file_id).is_some()
272 && !self.indices().by_file(file_id).is_empty();
273 if is_indexed {
274 Ok(ResolvedFileScope::File(file_id))
275 } else {
276 Err(FileScopeError::FileNotIndexed)
277 }
278 }
279 }
280 }
281
282 #[must_use]
284 pub fn normalize_symbol_query(
285 &self,
286 query: &SymbolQuery<'_>,
287 file_scope: &ResolvedFileScope,
288 ) -> NormalizedSymbolQuery {
289 let normalized_symbol = match *file_scope {
290 ResolvedFileScope::Any => query.symbol.to_string(),
291 ResolvedFileScope::File(file_id) => {
292 self.files().language_for_file(file_id).map_or_else(
293 || query.symbol.to_string(),
294 |language| canonicalize_graph_qualified_name(language, query.symbol),
295 )
296 }
297 };
298
299 NormalizedSymbolQuery {
300 symbol: normalized_symbol,
301 file_scope: *file_scope,
302 mode: query.mode,
303 }
304 }
305
306 fn exact_qualified_bucket(&self, query: &NormalizedSymbolQuery) -> Vec<NodeId> {
307 self.strings()
308 .get(&query.symbol)
309 .map_or_else(Vec::new, |string_id| {
310 self.indices().by_qualified_name(string_id).to_vec()
311 })
312 }
313
314 fn exact_simple_bucket(&self, query: &NormalizedSymbolQuery) -> Vec<NodeId> {
315 self.strings()
316 .get(&query.symbol)
317 .map_or_else(Vec::new, |string_id| {
318 self.indices().by_name(string_id).to_vec()
319 })
320 }
321
322 fn bounded_suffix_bucket(&self, query: &NormalizedSymbolQuery) -> Vec<NodeId> {
323 if !query.symbol.contains("::") {
324 return Vec::new();
325 }
326
327 let Some(leaf_symbol) = query.symbol.rsplit("::").next() else {
328 return Vec::new();
329 };
330 let Some(leaf_id) = self.strings().get(leaf_symbol) else {
331 return Vec::new();
332 };
333 let suffix_pattern = format!("::{}", query.symbol);
334
335 self.indices()
336 .by_name(leaf_id)
337 .iter()
338 .copied()
339 .filter(|node_id| {
340 self.get_node(*node_id)
341 .and_then(|entry| entry.qualified_name)
342 .and_then(|qualified_name_id| self.strings().resolve(qualified_name_id))
343 .is_some_and(|qualified_name| {
344 qualified_name.as_ref() == query.symbol
345 || qualified_name.as_ref().ends_with(&suffix_pattern)
346 })
347 })
348 .collect()
349 }
350
351 fn filtered_bucket(
352 &self,
353 mut bucket: Vec<NodeId>,
354 file_scope: ResolvedFileScope,
355 ) -> Vec<NodeId> {
356 if let ResolvedFileScope::File(file_id) = file_scope {
357 let file_nodes = self.indices().by_file(file_id);
358 bucket.retain(|node_id| file_nodes.contains(node_id));
359 }
360
361 bucket.sort_by(|left, right| {
362 self.candidate_sort_key(*left)
363 .cmp(&self.candidate_sort_key(*right))
364 });
365 bucket.dedup();
366 bucket
367 }
368
369 fn first_candidate_bucket_with_witness(
370 &self,
371 query: &NormalizedSymbolQuery,
372 file_scope: ResolvedFileScope,
373 ) -> Option<(SymbolCandidateBucket, Vec<SymbolCandidateWitness>)> {
374 for bucket in [
375 SymbolCandidateBucket::ExactQualified,
376 SymbolCandidateBucket::ExactSimple,
377 SymbolCandidateBucket::CanonicalSuffix,
378 ] {
379 if bucket == SymbolCandidateBucket::CanonicalSuffix
380 && !matches!(query.mode, ResolutionMode::AllowSuffixCandidates)
381 {
382 continue;
383 }
384
385 let candidates = self.bucket_witnesses(query, file_scope, bucket);
386 if !candidates.is_empty() {
387 return Some((bucket, candidates));
388 }
389 }
390
391 None
392 }
393
394 fn bucket_witnesses(
395 &self,
396 query: &NormalizedSymbolQuery,
397 file_scope: ResolvedFileScope,
398 bucket: SymbolCandidateBucket,
399 ) -> Vec<SymbolCandidateWitness> {
400 let raw_bucket = match bucket {
401 SymbolCandidateBucket::ExactQualified => self.exact_qualified_bucket(query),
402 SymbolCandidateBucket::ExactSimple => self.exact_simple_bucket(query),
403 SymbolCandidateBucket::CanonicalSuffix => self.bounded_suffix_bucket(query),
404 };
405
406 self.filtered_bucket(raw_bucket, file_scope)
407 .into_iter()
408 .map(|node_id| SymbolCandidateWitness { node_id, bucket })
409 .collect()
410 }
411
412 pub fn resolve_global_symbol_ambiguity_aware(
453 &self,
454 symbol: &str,
455 file_scope: FileScope<'_>,
456 ) -> Result<NodeId, SymbolResolveError> {
457 let primary = self.resolve_symbol(&SymbolQuery {
462 symbol,
463 file_scope,
464 mode: ResolutionMode::Strict,
465 });
466
467 let outcome = match primary {
468 SymbolResolutionOutcome::Resolved(_) | SymbolResolutionOutcome::Ambiguous(_) => primary,
470 SymbolResolutionOutcome::NotFound | SymbolResolutionOutcome::FileNotIndexed => {
476 if !symbol.contains("::") && (symbol.contains('.') || symbol.contains('#')) {
477 let normalized = symbol.replace(['.', '#'], "::");
478 self.resolve_symbol(&SymbolQuery {
479 symbol: &normalized,
480 file_scope,
481 mode: ResolutionMode::Strict,
482 })
483 } else {
484 primary
485 }
486 }
487 };
488
489 match outcome {
490 SymbolResolutionOutcome::Resolved(node_id) => Ok(node_id),
491 SymbolResolutionOutcome::NotFound | SymbolResolutionOutcome::FileNotIndexed => {
492 Err(SymbolResolveError::NotFound {
493 name: symbol.to_string(),
494 })
495 }
496 SymbolResolutionOutcome::Ambiguous(candidates) => Err(SymbolResolveError::Ambiguous(
497 self.build_ambiguous_symbol_error(symbol, &candidates),
498 )),
499 }
500 }
501
502 fn build_ambiguous_symbol_error(
505 &self,
506 symbol: &str,
507 candidates: &[NodeId],
508 ) -> AmbiguousSymbolError {
509 let mut materialized: Vec<AmbiguousSymbolCandidate> = candidates
510 .iter()
511 .filter_map(|node_id| self.materialize_ambiguous_candidate(*node_id))
512 .collect();
513
514 materialized.sort_by(|left, right| {
519 left.qualified_name
520 .cmp(&right.qualified_name)
521 .then(left.file_path.cmp(&right.file_path))
522 .then(left.start_line.cmp(&right.start_line))
523 .then(left.start_column.cmp(&right.start_column))
524 });
525
526 let truncated = materialized.len() > AMBIGUOUS_SYMBOL_CANDIDATE_CAP;
527 materialized.truncate(AMBIGUOUS_SYMBOL_CANDIDATE_CAP);
528
529 AmbiguousSymbolError {
530 name: symbol.to_string(),
531 candidates: materialized,
532 truncated,
533 }
534 }
535
536 fn materialize_ambiguous_candidate(&self, node_id: NodeId) -> Option<AmbiguousSymbolCandidate> {
537 let entry = self.get_node(node_id)?;
538 let strings = self.strings();
539 let files = self.files();
540
541 let simple_name = strings
542 .resolve(entry.name)
543 .map_or_else(String::new, |s| s.to_string());
544 let qualified_name = entry
545 .qualified_name
546 .and_then(|id| strings.resolve(id))
547 .map_or_else(|| simple_name.clone(), |s| s.to_string());
548 let file_path = files
549 .resolve(entry.file)
550 .map_or_else(String::new, |p| p.display().to_string());
551
552 Some(AmbiguousSymbolCandidate {
553 qualified_name,
554 kind: entry.kind.as_str().to_string(),
555 file_path,
556 start_line: entry.start_line,
557 start_column: entry.start_column,
558 })
559 }
560
561 fn candidate_sort_key(&self, node_id: NodeId) -> CandidateSortKey {
562 let Some(entry) = self.get_node(node_id) else {
563 return CandidateSortKey::default_for(node_id);
564 };
565
566 let file_path = self
567 .files()
568 .resolve(entry.file)
569 .map_or_else(String::new, |path| path.to_string_lossy().into_owned());
570 let qualified_name = entry
571 .qualified_name
572 .and_then(|string_id| self.strings().resolve(string_id))
573 .map_or_else(String::new, |value| value.to_string());
574 let simple_name = self
575 .strings()
576 .resolve(entry.name)
577 .map_or_else(String::new, |value| value.to_string());
578
579 CandidateSortKey {
580 file_path,
581 start_line: entry.start_line,
582 start_column: entry.start_column,
583 end_line: entry.end_line,
584 end_column: entry.end_column,
585 kind: entry.kind.as_str().to_string(),
586 qualified_name,
587 simple_name,
588 node_id,
589 }
590 }
591}
592
593#[derive(Debug, Clone, PartialEq, Eq)]
595pub struct SymbolCandidateSearchWitness {
596 pub normalized_query: Option<NormalizedSymbolQuery>,
598 pub outcome: SymbolCandidateOutcome,
600 pub selected_bucket: Option<SymbolCandidateBucket>,
602 pub candidates: Vec<SymbolCandidateWitness>,
604}
605
606pub const AMBIGUOUS_SYMBOL_CANDIDATE_CAP: usize = 20;
613
614#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
621pub struct AmbiguousSymbolCandidate {
622 pub qualified_name: String,
625 pub kind: String,
627 pub file_path: String,
629 pub start_line: u32,
631 pub start_column: u32,
633}
634
635#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
647pub struct AmbiguousSymbolError {
648 pub name: String,
650 pub candidates: Vec<AmbiguousSymbolCandidate>,
652 pub truncated: bool,
655}
656
657impl std::fmt::Display for AmbiguousSymbolError {
658 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
659 write!(
660 f,
661 "Symbol '{}' is ambiguous; specify the qualified name",
662 self.name
663 )
664 }
665}
666
667#[derive(Debug, Clone, PartialEq, Eq)]
674pub enum SymbolResolveError {
675 NotFound {
677 name: String,
679 },
680 Ambiguous(AmbiguousSymbolError),
682}
683
684impl std::fmt::Display for SymbolResolveError {
685 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
686 match self {
687 Self::NotFound { name } => write!(f, "Symbol '{name}' not found in graph"),
688 Self::Ambiguous(err) => write!(f, "{err}"),
689 }
690 }
691}
692
693impl std::error::Error for SymbolResolveError {}
694
695#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
696struct CandidateSortKey {
697 file_path: String,
698 start_line: u32,
699 start_column: u32,
700 end_line: u32,
701 end_column: u32,
702 kind: String,
703 qualified_name: String,
704 simple_name: String,
705 node_id: NodeId,
706}
707
708impl CandidateSortKey {
709 fn default_for(node_id: NodeId) -> Self {
710 Self {
711 file_path: String::new(),
712 start_line: 0,
713 start_column: 0,
714 end_line: 0,
715 end_column: 0,
716 kind: String::new(),
717 qualified_name: String::new(),
718 simple_name: String::new(),
719 node_id,
720 }
721 }
722}
723
724#[must_use]
726pub fn canonicalize_graph_qualified_name(language: Language, symbol: &str) -> String {
727 if should_skip_qualified_name_normalization(symbol) {
728 return symbol.to_string();
729 }
730
731 if language == Language::R {
732 return canonicalize_r_qualified_name(symbol);
733 }
734
735 let mut normalized = symbol.to_string();
736 for delimiter in native_delimiters(language) {
737 if normalized.contains(delimiter) {
738 normalized = normalized.replace(delimiter, "::");
739 }
740 }
741 normalized
742}
743
744#[must_use]
746pub(crate) fn is_canonical_graph_qualified_name(language: Language, symbol: &str) -> bool {
747 should_skip_qualified_name_normalization(symbol)
748 || canonicalize_graph_qualified_name(language, symbol) == symbol
749}
750
751fn should_skip_qualified_name_normalization(symbol: &str) -> bool {
752 symbol.starts_with('<')
753 || symbol.contains('/')
754 || symbol.starts_with("wasm::")
755 || symbol.starts_with("ffi::")
756 || symbol.starts_with("extern::")
757 || symbol.starts_with("native::")
758}
759
760fn canonicalize_r_qualified_name(symbol: &str) -> String {
761 let search_start = usize::from(symbol.starts_with('.'));
762 let Some(relative_split_index) = symbol[search_start..].rfind('.') else {
763 return symbol.to_string();
764 };
765
766 let split_index = search_start + relative_split_index;
767 let prefix = &symbol[..split_index];
768 let suffix = &symbol[split_index + 1..];
769 if suffix.is_empty() {
770 return symbol.to_string();
771 }
772
773 format!("{prefix}::{suffix}")
774}
775
776#[must_use]
778pub fn display_graph_qualified_name(
779 language: Language,
780 qualified: &str,
781 kind: NodeKind,
782 is_static: bool,
783) -> String {
784 if should_skip_qualified_name_normalization(qualified) {
785 return qualified.to_string();
786 }
787
788 match language {
789 Language::Ruby => display_ruby_qualified_name(qualified, kind, is_static),
790 Language::Php => display_php_qualified_name(qualified, kind),
791 _ => native_display_separator(language).map_or_else(
792 || qualified.to_string(),
793 |separator| qualified.replace("::", separator),
794 ),
795 }
796}
797
798pub(crate) fn native_delimiters(language: Language) -> &'static [&'static str] {
799 match language {
800 Language::JavaScript
801 | Language::Python
802 | Language::TypeScript
803 | Language::Java
804 | Language::CSharp
805 | Language::Kotlin
806 | Language::Scala
807 | Language::Go
808 | Language::Css
809 | Language::Sql
810 | Language::Dart
811 | Language::Lua
812 | Language::Perl
813 | Language::Groovy
814 | Language::Elixir
815 | Language::R
816 | Language::Haskell
817 | Language::Html
818 | Language::Svelte
819 | Language::Vue
820 | Language::Terraform
821 | Language::Puppet
822 | Language::Pulumi
823 | Language::Http
824 | Language::Plsql
825 | Language::Apex
826 | Language::Abap
827 | Language::ServiceNow
828 | Language::Swift
829 | Language::Zig
830 | Language::Json => &["."],
831 Language::Ruby => &["#", "."],
832 Language::Php => &["\\", "->"],
833 Language::C | Language::Cpp | Language::Rust | Language::Shell => &[],
834 }
835}
836
837fn native_display_separator(language: Language) -> Option<&'static str> {
838 match language {
839 Language::C
840 | Language::Cpp
841 | Language::Rust
842 | Language::Shell
843 | Language::Php
844 | Language::Ruby => None,
845 _ => Some("."),
846 }
847}
848
849fn display_ruby_qualified_name(qualified: &str, kind: NodeKind, is_static: bool) -> String {
850 if qualified.contains('#') || qualified.contains('.') || !qualified.contains("::") {
851 return qualified.to_string();
852 }
853
854 match kind {
855 NodeKind::Method => {
856 replace_last_separator(qualified, if is_static { "." } else { "#" }, false)
857 }
858 NodeKind::Variable if should_display_ruby_member_variable(qualified) => {
859 replace_last_separator(qualified, "#", false)
860 }
861 NodeKind::Property | NodeKind::Constant
868 if !is_static && should_display_ruby_member_variable(qualified) =>
869 {
870 replace_last_separator(qualified, "#", false)
871 }
872 _ => qualified.to_string(),
873 }
874}
875
876fn should_display_ruby_member_variable(qualified: &str) -> bool {
877 let Some((_, suffix)) = qualified.rsplit_once("::") else {
878 return false;
879 };
880
881 if suffix.starts_with("@@")
882 || suffix
883 .chars()
884 .next()
885 .is_some_and(|character| character.is_ascii_uppercase())
886 {
887 return false;
888 }
889
890 suffix.starts_with('@')
891 || suffix
892 .chars()
893 .next()
894 .is_some_and(|character| character.is_ascii_lowercase() || character == '_')
895}
896
897fn display_php_qualified_name(qualified: &str, kind: NodeKind) -> String {
898 if !qualified.contains("::") {
899 return qualified.to_string();
900 }
901
902 if matches!(kind, NodeKind::Method | NodeKind::Property) {
903 return replace_last_separator(qualified, "::", true);
904 }
905
906 qualified.replace("::", "\\")
907}
908
909fn replace_last_separator(qualified: &str, final_separator: &str, preserve_prefix: bool) -> String {
910 let Some((prefix, suffix)) = qualified.rsplit_once("::") else {
911 return qualified.to_string();
912 };
913
914 let display_prefix = if preserve_prefix {
915 prefix.replace("::", "\\")
916 } else {
917 prefix.to_string()
918 };
919
920 if display_prefix.is_empty() {
921 suffix.to_string()
922 } else {
923 format!("{display_prefix}{final_separator}{suffix}")
924 }
925}
926
927#[cfg(test)]
928mod tests {
929 use std::path::{Path, PathBuf};
930
931 use crate::graph::node::Language;
932 use crate::graph::unified::concurrent::CodeGraph;
933 use crate::graph::unified::node::id::NodeId;
934 use crate::graph::unified::node::kind::NodeKind;
935 use crate::graph::unified::storage::arena::NodeEntry;
936
937 use super::{
938 FileScope, NormalizedSymbolQuery, ResolutionMode, ResolvedFileScope, SymbolCandidateBucket,
939 SymbolCandidateOutcome, SymbolQuery, SymbolResolutionOutcome,
940 canonicalize_graph_qualified_name, display_graph_qualified_name,
941 };
942
943 struct TestNode {
944 node_id: NodeId,
945 }
946
947 #[test]
948 fn test_resolve_symbol_exact_qualified_same_file() {
949 let mut graph = CodeGraph::new();
950 let file_path = abs_path("src/lib.rs");
951 let symbol = add_node(
952 &mut graph,
953 NodeKind::Function,
954 "target",
955 Some("pkg::target"),
956 &file_path,
957 Some(Language::Rust),
958 10,
959 2,
960 );
961
962 let snapshot = graph.snapshot();
963 let query = SymbolQuery {
964 symbol: "pkg::target",
965 file_scope: FileScope::Path(&file_path),
966 mode: ResolutionMode::Strict,
967 };
968
969 assert_eq!(
970 snapshot.resolve_symbol(&query),
971 SymbolResolutionOutcome::Resolved(symbol.node_id)
972 );
973 }
974
975 #[test]
976 fn test_resolve_symbol_exact_simple_same_file_wins() {
977 let mut graph = CodeGraph::new();
978 let requested_path = abs_path("src/requested.rs");
979 let other_path = abs_path("src/other.rs");
980
981 let requested = add_node(
982 &mut graph,
983 NodeKind::Function,
984 "target",
985 Some("requested::target"),
986 &requested_path,
987 Some(Language::Rust),
988 4,
989 0,
990 );
991 let _other = add_node(
992 &mut graph,
993 NodeKind::Function,
994 "target",
995 Some("other::target"),
996 &other_path,
997 Some(Language::Rust),
998 1,
999 0,
1000 );
1001
1002 let snapshot = graph.snapshot();
1003 let query = SymbolQuery {
1004 symbol: "target",
1005 file_scope: FileScope::Path(&requested_path),
1006 mode: ResolutionMode::Strict,
1007 };
1008
1009 assert_eq!(
1010 snapshot.resolve_symbol(&query),
1011 SymbolResolutionOutcome::Resolved(requested.node_id)
1012 );
1013 }
1014
1015 #[test]
1016 fn test_resolve_symbol_returns_not_found_without_wrong_file_fallback() {
1017 let mut graph = CodeGraph::new();
1018 let requested_path = abs_path("src/requested.rs");
1019 let other_path = abs_path("src/other.rs");
1020
1021 let _requested_index_anchor = add_node(
1022 &mut graph,
1023 NodeKind::Function,
1024 "anchor",
1025 Some("requested::anchor"),
1026 &requested_path,
1027 Some(Language::Rust),
1028 1,
1029 0,
1030 );
1031 let _other = add_node(
1032 &mut graph,
1033 NodeKind::Function,
1034 "target",
1035 Some("other::target"),
1036 &other_path,
1037 Some(Language::Rust),
1038 3,
1039 0,
1040 );
1041
1042 let snapshot = graph.snapshot();
1043 let query = SymbolQuery {
1044 symbol: "target",
1045 file_scope: FileScope::Path(&requested_path),
1046 mode: ResolutionMode::Strict,
1047 };
1048
1049 assert_eq!(
1050 snapshot.resolve_symbol(&query),
1051 SymbolResolutionOutcome::NotFound
1052 );
1053 }
1054
1055 #[test]
1056 fn test_resolve_symbol_returns_file_not_indexed_for_valid_unindexed_path() {
1057 let mut graph = CodeGraph::new();
1058 let indexed_path = abs_path("src/indexed.rs");
1059 let unindexed_path = abs_path("src/unindexed.rs");
1060
1061 add_node(
1062 &mut graph,
1063 NodeKind::Function,
1064 "indexed",
1065 Some("pkg::indexed"),
1066 &indexed_path,
1067 Some(Language::Rust),
1068 1,
1069 0,
1070 );
1071 graph
1072 .files_mut()
1073 .register_with_language(&unindexed_path, Some(Language::Rust))
1074 .unwrap();
1075
1076 let snapshot = graph.snapshot();
1077 let query = SymbolQuery {
1078 symbol: "indexed",
1079 file_scope: FileScope::Path(&unindexed_path),
1080 mode: ResolutionMode::Strict,
1081 };
1082
1083 assert_eq!(
1084 snapshot.resolve_symbol(&query),
1085 SymbolResolutionOutcome::FileNotIndexed
1086 );
1087 }
1088
1089 #[test]
1090 fn test_resolve_symbol_returns_ambiguous_for_multi_match_bucket() {
1091 let mut graph = CodeGraph::new();
1092 let file_path = abs_path("src/lib.rs");
1093
1094 let first = add_node(
1095 &mut graph,
1096 NodeKind::Function,
1097 "dup",
1098 Some("pkg::dup"),
1099 &file_path,
1100 Some(Language::Rust),
1101 2,
1102 0,
1103 );
1104 let second = add_node(
1105 &mut graph,
1106 NodeKind::Method,
1107 "dup",
1108 Some("pkg::dup_method"),
1109 &file_path,
1110 Some(Language::Rust),
1111 8,
1112 0,
1113 );
1114
1115 let snapshot = graph.snapshot();
1116 let query = SymbolQuery {
1117 symbol: "dup",
1118 file_scope: FileScope::Path(&file_path),
1119 mode: ResolutionMode::Strict,
1120 };
1121
1122 assert_eq!(
1123 snapshot.resolve_symbol(&query),
1124 SymbolResolutionOutcome::Ambiguous(vec![first.node_id, second.node_id])
1125 );
1126 }
1127
1128 #[test]
1129 fn test_find_symbol_candidates_uses_first_non_empty_bucket_only() {
1130 let mut graph = CodeGraph::new();
1131 let qualified_path = abs_path("src/qualified.rs");
1132 let simple_path = abs_path("src/simple.rs");
1133
1134 let qualified = add_node(
1135 &mut graph,
1136 NodeKind::Function,
1137 "target",
1138 Some("pkg::target"),
1139 &qualified_path,
1140 Some(Language::Rust),
1141 1,
1142 0,
1143 );
1144 let simple_only = add_node(
1145 &mut graph,
1146 NodeKind::Function,
1147 "pkg::target",
1148 None,
1149 &simple_path,
1150 Some(Language::Rust),
1151 1,
1152 0,
1153 );
1154
1155 let snapshot = graph.snapshot();
1156 let query = SymbolQuery {
1157 symbol: "pkg::target",
1158 file_scope: FileScope::Any,
1159 mode: ResolutionMode::AllowSuffixCandidates,
1160 };
1161
1162 assert_eq!(
1163 snapshot.find_symbol_candidates(&query),
1164 SymbolCandidateOutcome::Candidates(vec![qualified.node_id])
1165 );
1166 assert_ne!(qualified.node_id, simple_only.node_id);
1167 }
1168
1169 #[test]
1170 fn test_find_symbol_candidates_with_witness_reports_exact_qualified_bucket() {
1171 let mut graph = CodeGraph::new();
1172 let qualified_path = abs_path("src/qualified.rs");
1173 let simple_path = abs_path("src/simple.rs");
1174
1175 let qualified = add_node(
1176 &mut graph,
1177 NodeKind::Function,
1178 "target",
1179 Some("pkg::target"),
1180 &qualified_path,
1181 Some(Language::Rust),
1182 1,
1183 0,
1184 );
1185 let _simple_only = add_node(
1186 &mut graph,
1187 NodeKind::Function,
1188 "pkg::target",
1189 None,
1190 &simple_path,
1191 Some(Language::Rust),
1192 1,
1193 0,
1194 );
1195
1196 let snapshot = graph.snapshot();
1197 let query = SymbolQuery {
1198 symbol: "pkg::target",
1199 file_scope: FileScope::Any,
1200 mode: ResolutionMode::AllowSuffixCandidates,
1201 };
1202
1203 let witness = snapshot.find_symbol_candidates_with_witness(&query);
1204
1205 assert_eq!(
1206 witness.outcome,
1207 SymbolCandidateOutcome::Candidates(vec![qualified.node_id])
1208 );
1209 assert_eq!(
1210 witness.selected_bucket,
1211 Some(SymbolCandidateBucket::ExactQualified)
1212 );
1213 assert_eq!(
1214 witness.candidates,
1215 vec![super::SymbolCandidateWitness {
1216 node_id: qualified.node_id,
1217 bucket: SymbolCandidateBucket::ExactQualified,
1218 }]
1219 );
1220 assert_eq!(
1221 witness.normalized_query,
1222 Some(NormalizedSymbolQuery {
1223 symbol: "pkg::target".to_string(),
1224 file_scope: ResolvedFileScope::Any,
1225 mode: ResolutionMode::AllowSuffixCandidates,
1226 })
1227 );
1228 }
1229
1230 #[test]
1231 fn test_find_symbol_candidates_preserves_file_not_indexed() {
1232 let mut graph = CodeGraph::new();
1233 let indexed_path = abs_path("src/indexed.rs");
1234 let unindexed_path = abs_path("src/unindexed.rs");
1235
1236 add_node(
1237 &mut graph,
1238 NodeKind::Function,
1239 "target",
1240 Some("pkg::target"),
1241 &indexed_path,
1242 Some(Language::Rust),
1243 1,
1244 0,
1245 );
1246 let unindexed_file_id = graph
1247 .files_mut()
1248 .register_with_language(&unindexed_path, Some(Language::Rust))
1249 .unwrap();
1250
1251 let snapshot = graph.snapshot();
1252 let query = SymbolQuery {
1253 symbol: "target",
1254 file_scope: FileScope::FileId(unindexed_file_id),
1255 mode: ResolutionMode::AllowSuffixCandidates,
1256 };
1257
1258 assert_eq!(
1259 snapshot.find_symbol_candidates(&query),
1260 SymbolCandidateOutcome::FileNotIndexed
1261 );
1262 }
1263
1264 #[test]
1265 fn test_resolve_symbol_with_witness_reports_ambiguous_bucket_candidates() {
1266 let mut graph = CodeGraph::new();
1267 let file_path = abs_path("src/lib.rs");
1268
1269 let first = add_node(
1270 &mut graph,
1271 NodeKind::Function,
1272 "dup",
1273 Some("pkg::dup"),
1274 &file_path,
1275 Some(Language::Rust),
1276 2,
1277 0,
1278 );
1279 let second = add_node(
1280 &mut graph,
1281 NodeKind::Method,
1282 "dup",
1283 Some("pkg::dup_method"),
1284 &file_path,
1285 Some(Language::Rust),
1286 8,
1287 0,
1288 );
1289
1290 let snapshot = graph.snapshot();
1291 let query = SymbolQuery {
1292 symbol: "dup",
1293 file_scope: FileScope::Path(&file_path),
1294 mode: ResolutionMode::Strict,
1295 };
1296
1297 let witness = snapshot.resolve_symbol_with_witness(&query);
1298
1299 assert_eq!(
1300 witness.outcome,
1301 SymbolResolutionOutcome::Ambiguous(vec![first.node_id, second.node_id])
1302 );
1303 assert_eq!(
1304 witness.selected_bucket,
1305 Some(SymbolCandidateBucket::ExactSimple)
1306 );
1307 assert_eq!(
1308 witness.candidates,
1309 vec![
1310 super::SymbolCandidateWitness {
1311 node_id: first.node_id,
1312 bucket: SymbolCandidateBucket::ExactSimple,
1313 },
1314 super::SymbolCandidateWitness {
1315 node_id: second.node_id,
1316 bucket: SymbolCandidateBucket::ExactSimple,
1317 },
1318 ]
1319 );
1320 }
1321
1322 #[test]
1323 fn test_suffix_candidates_disabled_in_strict_mode() {
1324 let mut graph = CodeGraph::new();
1325 let file_path = abs_path("src/lib.rs");
1326
1327 let suffix_match = add_node(
1328 &mut graph,
1329 NodeKind::Function,
1330 "target",
1331 Some("outer::pkg::target"),
1332 &file_path,
1333 Some(Language::Rust),
1334 1,
1335 0,
1336 );
1337
1338 let snapshot = graph.snapshot();
1339 let strict_query = SymbolQuery {
1340 symbol: "pkg::target",
1341 file_scope: FileScope::Any,
1342 mode: ResolutionMode::Strict,
1343 };
1344 let suffix_query = SymbolQuery {
1345 mode: ResolutionMode::AllowSuffixCandidates,
1346 ..strict_query
1347 };
1348
1349 assert_eq!(
1350 snapshot.resolve_symbol(&strict_query),
1351 SymbolResolutionOutcome::NotFound
1352 );
1353 assert_eq!(
1354 snapshot.find_symbol_candidates(&suffix_query),
1355 SymbolCandidateOutcome::Candidates(vec![suffix_match.node_id])
1356 );
1357 }
1358
1359 #[test]
1360 fn test_suffix_candidates_require_canonical_qualified_query() {
1361 let mut graph = CodeGraph::new();
1362 let file_path = abs_path("src/mod.py");
1363
1364 add_node(
1365 &mut graph,
1366 NodeKind::Function,
1367 "target",
1368 Some("pkg::target"),
1369 &file_path,
1370 Some(Language::Python),
1371 1,
1372 0,
1373 );
1374
1375 let snapshot = graph.snapshot();
1376 let query = SymbolQuery {
1377 symbol: "pkg.target",
1378 file_scope: FileScope::Any,
1379 mode: ResolutionMode::AllowSuffixCandidates,
1380 };
1381
1382 assert_eq!(
1383 snapshot.find_symbol_candidates(&query),
1384 SymbolCandidateOutcome::NotFound
1385 );
1386 }
1387
1388 #[test]
1389 fn test_suffix_candidates_filter_same_leaf_bucket_only() {
1390 let mut graph = CodeGraph::new();
1391 let file_path = abs_path("src/lib.rs");
1392
1393 let exact_suffix = add_node(
1394 &mut graph,
1395 NodeKind::Function,
1396 "target",
1397 Some("outer::pkg::target"),
1398 &file_path,
1399 Some(Language::Rust),
1400 2,
1401 0,
1402 );
1403 let another_suffix = add_node(
1404 &mut graph,
1405 NodeKind::Method,
1406 "target",
1407 Some("another::pkg::target"),
1408 &file_path,
1409 Some(Language::Rust),
1410 4,
1411 0,
1412 );
1413 let unrelated = add_node(
1414 &mut graph,
1415 NodeKind::Function,
1416 "target",
1417 Some("pkg::different::target"),
1418 &file_path,
1419 Some(Language::Rust),
1420 6,
1421 0,
1422 );
1423
1424 let snapshot = graph.snapshot();
1425 let query = SymbolQuery {
1426 symbol: "pkg::target",
1427 file_scope: FileScope::Any,
1428 mode: ResolutionMode::AllowSuffixCandidates,
1429 };
1430
1431 assert_eq!(
1432 snapshot.find_symbol_candidates(&query),
1433 SymbolCandidateOutcome::Candidates(vec![exact_suffix.node_id, another_suffix.node_id])
1434 );
1435 assert_ne!(unrelated.node_id, exact_suffix.node_id);
1436 }
1437
1438 #[test]
1439 fn test_normalize_symbol_query_rewrites_native_delimiter_when_file_scope_language_known() {
1440 let mut graph = CodeGraph::new();
1441 let file_path = abs_path("src/mod.py");
1442 let file_id = graph
1443 .files_mut()
1444 .register_with_language(&file_path, Some(Language::Python))
1445 .unwrap();
1446 let snapshot = graph.snapshot();
1447 let query = SymbolQuery {
1448 symbol: "pkg.mod.fn",
1449 file_scope: FileScope::Path(&file_path),
1450 mode: ResolutionMode::Strict,
1451 };
1452
1453 let normalized = snapshot.normalize_symbol_query(&query, &ResolvedFileScope::File(file_id));
1454
1455 assert_eq!(
1456 normalized,
1457 NormalizedSymbolQuery {
1458 symbol: "pkg::mod::fn".to_string(),
1459 file_scope: ResolvedFileScope::File(file_id),
1460 mode: ResolutionMode::Strict,
1461 }
1462 );
1463 }
1464
1465 #[test]
1466 fn test_normalize_symbol_query_rewrites_native_delimiter_for_csharp() {
1467 let mut graph = CodeGraph::new();
1468 let file_path = abs_path("src/Program.cs");
1469 let file_id = graph
1470 .files_mut()
1471 .register_with_language(&file_path, Some(Language::CSharp))
1472 .unwrap();
1473 let snapshot = graph.snapshot();
1474 let query = SymbolQuery {
1475 symbol: "System.Console.WriteLine",
1476 file_scope: FileScope::Path(&file_path),
1477 mode: ResolutionMode::Strict,
1478 };
1479
1480 let normalized = snapshot.normalize_symbol_query(&query, &ResolvedFileScope::File(file_id));
1481
1482 assert_eq!(normalized.symbol, "System::Console::WriteLine".to_string());
1483 }
1484
1485 #[test]
1486 fn test_normalize_symbol_query_rewrites_native_delimiter_for_zig() {
1487 let mut graph = CodeGraph::new();
1488 let file_path = abs_path("src/main.zig");
1489 let file_id = graph
1490 .files_mut()
1491 .register_with_language(&file_path, Some(Language::Zig))
1492 .unwrap();
1493 let snapshot = graph.snapshot();
1494 let query = SymbolQuery {
1495 symbol: "std.os.linux.exit",
1496 file_scope: FileScope::Path(&file_path),
1497 mode: ResolutionMode::Strict,
1498 };
1499
1500 let normalized = snapshot.normalize_symbol_query(&query, &ResolvedFileScope::File(file_id));
1501
1502 assert_eq!(normalized.symbol, "std::os::linux::exit".to_string());
1503 }
1504
1505 #[test]
1506 fn test_normalize_symbol_query_does_not_rewrite_when_file_scope_any() {
1507 let graph = CodeGraph::new();
1508 let snapshot = graph.snapshot();
1509 let query = SymbolQuery {
1510 symbol: "pkg.mod.fn",
1511 file_scope: FileScope::Any,
1512 mode: ResolutionMode::Strict,
1513 };
1514
1515 let normalized = snapshot.normalize_symbol_query(&query, &ResolvedFileScope::Any);
1516
1517 assert_eq!(
1518 normalized,
1519 NormalizedSymbolQuery {
1520 symbol: "pkg.mod.fn".to_string(),
1521 file_scope: ResolvedFileScope::Any,
1522 mode: ResolutionMode::Strict,
1523 }
1524 );
1525 }
1526
1527 #[test]
1528 fn test_global_qualified_query_with_native_delimiter_is_exact_only_and_not_found() {
1529 let mut graph = CodeGraph::new();
1530 let file_path = abs_path("src/mod.py");
1531
1532 add_node(
1533 &mut graph,
1534 NodeKind::Function,
1535 "fn",
1536 Some("pkg::mod::fn"),
1537 &file_path,
1538 Some(Language::Python),
1539 1,
1540 0,
1541 );
1542
1543 let snapshot = graph.snapshot();
1544 let query = SymbolQuery {
1545 symbol: "pkg.mod.fn",
1546 file_scope: FileScope::Any,
1547 mode: ResolutionMode::AllowSuffixCandidates,
1548 };
1549
1550 assert_eq!(
1551 snapshot.resolve_symbol(&query),
1552 SymbolResolutionOutcome::NotFound
1553 );
1554 }
1555
1556 #[test]
1557 fn test_global_canonical_qualified_query_can_hit_exact_qualified_bucket() {
1558 let mut graph = CodeGraph::new();
1559 let file_path = abs_path("src/lib.rs");
1560 let expected = add_node(
1561 &mut graph,
1562 NodeKind::Function,
1563 "fn",
1564 Some("pkg::mod::fn"),
1565 &file_path,
1566 Some(Language::Rust),
1567 1,
1568 0,
1569 );
1570
1571 let snapshot = graph.snapshot();
1572 let query = SymbolQuery {
1573 symbol: "pkg::mod::fn",
1574 file_scope: FileScope::Any,
1575 mode: ResolutionMode::Strict,
1576 };
1577
1578 assert_eq!(
1579 snapshot.resolve_symbol(&query),
1580 SymbolResolutionOutcome::Resolved(expected.node_id)
1581 );
1582 }
1583
1584 #[test]
1585 fn test_candidate_order_uses_metadata_then_node_id() {
1586 let mut graph = CodeGraph::new();
1587 let file_path = abs_path("src/lib.rs");
1588
1589 let first = add_node(
1590 &mut graph,
1591 NodeKind::Function,
1592 "dup",
1593 Some("pkg::dup_a"),
1594 &file_path,
1595 Some(Language::Rust),
1596 1,
1597 0,
1598 );
1599 let second = add_node(
1600 &mut graph,
1601 NodeKind::Function,
1602 "dup",
1603 Some("pkg::dup_b"),
1604 &file_path,
1605 Some(Language::Rust),
1606 1,
1607 0,
1608 );
1609
1610 let snapshot = graph.snapshot();
1611 let query = SymbolQuery {
1612 symbol: "dup",
1613 file_scope: FileScope::Any,
1614 mode: ResolutionMode::Strict,
1615 };
1616
1617 assert_eq!(
1618 snapshot.find_symbol_candidates(&query),
1619 SymbolCandidateOutcome::Candidates(vec![first.node_id, second.node_id])
1620 );
1621 }
1622
1623 #[test]
1624 fn test_candidate_order_kind_sort_key_uses_node_kind_as_str() {
1625 let mut graph = CodeGraph::new();
1626 let file_path = abs_path("src/lib.rs");
1627
1628 let function_node = add_node(
1629 &mut graph,
1630 NodeKind::Function,
1631 "shared",
1632 Some("pkg::shared_fn"),
1633 &file_path,
1634 Some(Language::Rust),
1635 1,
1636 0,
1637 );
1638 let variable_node = add_node(
1639 &mut graph,
1640 NodeKind::Variable,
1641 "shared",
1642 Some("pkg::shared_var"),
1643 &file_path,
1644 Some(Language::Rust),
1645 1,
1646 0,
1647 );
1648
1649 let snapshot = graph.snapshot();
1650 let query = SymbolQuery {
1651 symbol: "shared",
1652 file_scope: FileScope::Any,
1653 mode: ResolutionMode::Strict,
1654 };
1655
1656 assert_eq!(
1657 snapshot.find_symbol_candidates(&query),
1658 SymbolCandidateOutcome::Candidates(vec![function_node.node_id, variable_node.node_id])
1659 );
1660 }
1661
1662 fn add_node(
1663 graph: &mut CodeGraph,
1664 kind: NodeKind,
1665 name: &str,
1666 qualified_name: Option<&str>,
1667 file_path: &Path,
1668 language: Option<Language>,
1669 start_line: u32,
1670 start_column: u32,
1671 ) -> TestNode {
1672 let name_id = graph.strings_mut().intern(name).unwrap();
1673 let qualified_name_id =
1674 qualified_name.map(|value| graph.strings_mut().intern(value).unwrap());
1675 let file_id = graph
1676 .files_mut()
1677 .register_with_language(file_path, language)
1678 .unwrap();
1679
1680 let entry = NodeEntry::new(kind, name_id, file_id)
1681 .with_qualified_name_opt(qualified_name_id)
1682 .with_location(start_line, start_column, start_line, start_column + 1);
1683
1684 let node_id = graph.nodes_mut().alloc(entry).unwrap();
1685 graph
1686 .indices_mut()
1687 .add(node_id, kind, name_id, qualified_name_id, file_id);
1688
1689 TestNode { node_id }
1690 }
1691
1692 trait NodeEntryExt {
1693 fn with_qualified_name_opt(
1694 self,
1695 qualified_name: Option<crate::graph::unified::string::id::StringId>,
1696 ) -> Self;
1697 }
1698
1699 impl NodeEntryExt for NodeEntry {
1700 fn with_qualified_name_opt(
1701 mut self,
1702 qualified_name: Option<crate::graph::unified::string::id::StringId>,
1703 ) -> Self {
1704 self.qualified_name = qualified_name;
1705 self
1706 }
1707 }
1708
1709 fn abs_path(relative: &str) -> PathBuf {
1710 PathBuf::from("/resolver-tests").join(relative)
1711 }
1712
1713 #[test]
1714 fn test_display_graph_qualified_name_dot_language() {
1715 let display = display_graph_qualified_name(
1716 Language::CSharp,
1717 "MyApp::User::GetName",
1718 NodeKind::Method,
1719 false,
1720 );
1721 assert_eq!(display, "MyApp.User.GetName");
1722 }
1723
1724 #[test]
1725 fn test_canonicalize_graph_qualified_name_r_private_name_preserved() {
1726 assert_eq!(
1727 canonicalize_graph_qualified_name(Language::R, ".private_func"),
1728 ".private_func"
1729 );
1730 }
1731
1732 #[test]
1733 fn test_canonicalize_graph_qualified_name_r_s3_method_uses_last_dot() {
1734 assert_eq!(
1735 canonicalize_graph_qualified_name(Language::R, "as.data.frame.myclass"),
1736 "as.data.frame::myclass"
1737 );
1738 }
1739
1740 #[test]
1741 fn test_canonicalize_graph_qualified_name_r_leading_dot_s3_generic() {
1742 assert_eq!(
1743 canonicalize_graph_qualified_name(Language::R, ".DollarNames.myclass"),
1744 ".DollarNames::myclass"
1745 );
1746 }
1747
1748 #[test]
1749 fn test_display_graph_qualified_name_ruby_instance_method() {
1750 let display = display_graph_qualified_name(
1751 Language::Ruby,
1752 "Admin::Users::Controller::show",
1753 NodeKind::Method,
1754 false,
1755 );
1756 assert_eq!(display, "Admin::Users::Controller#show");
1757 }
1758
1759 #[test]
1760 fn test_display_graph_qualified_name_ruby_singleton_method() {
1761 let display = display_graph_qualified_name(
1762 Language::Ruby,
1763 "Admin::Users::Controller::show",
1764 NodeKind::Method,
1765 true,
1766 );
1767 assert_eq!(display, "Admin::Users::Controller.show");
1768 }
1769
1770 #[test]
1771 fn test_display_graph_qualified_name_ruby_member_variable() {
1772 let display = display_graph_qualified_name(
1773 Language::Ruby,
1774 "Admin::Users::Controller::username",
1775 NodeKind::Variable,
1776 false,
1777 );
1778 assert_eq!(display, "Admin::Users::Controller#username");
1779 }
1780
1781 #[test]
1782 fn test_display_graph_qualified_name_ruby_instance_variable() {
1783 let display = display_graph_qualified_name(
1784 Language::Ruby,
1785 "Admin::Users::Controller::@current_user",
1786 NodeKind::Variable,
1787 false,
1788 );
1789 assert_eq!(display, "Admin::Users::Controller#@current_user");
1790 }
1791
1792 #[test]
1793 fn test_display_graph_qualified_name_ruby_constant_stays_canonical() {
1794 let display = display_graph_qualified_name(
1795 Language::Ruby,
1796 "Admin::Users::Controller::DEFAULT_ROLE",
1797 NodeKind::Variable,
1798 false,
1799 );
1800 assert_eq!(display, "Admin::Users::Controller::DEFAULT_ROLE");
1801 }
1802
1803 #[test]
1804 fn test_display_graph_qualified_name_ruby_class_variable_stays_canonical() {
1805 let display = display_graph_qualified_name(
1806 Language::Ruby,
1807 "Admin::Users::Controller::@@count",
1808 NodeKind::Variable,
1809 false,
1810 );
1811 assert_eq!(display, "Admin::Users::Controller::@@count");
1812 }
1813
1814 #[test]
1820 fn test_display_graph_qualified_name_ruby_property_renders_as_instance_member() {
1821 let display = display_graph_qualified_name(
1822 Language::Ruby,
1823 "Counter::name",
1824 NodeKind::Property,
1825 false,
1826 );
1827 assert_eq!(display, "Counter#name");
1828 }
1829
1830 #[test]
1838 fn test_display_graph_qualified_name_ruby_constant_lowercase_renders_as_instance_member() {
1839 let display = display_graph_qualified_name(
1840 Language::Ruby,
1841 "Counter::name",
1842 NodeKind::Constant,
1843 false,
1844 );
1845 assert_eq!(display, "Counter#name");
1846 }
1847
1848 #[test]
1853 fn test_display_graph_qualified_name_ruby_constant_uppercase_stays_canonical() {
1854 let display = display_graph_qualified_name(
1855 Language::Ruby,
1856 "Counter::CONSTANT",
1857 NodeKind::Constant,
1858 false,
1859 );
1860 assert_eq!(display, "Counter::CONSTANT");
1861 }
1862
1863 #[test]
1867 fn test_display_graph_qualified_name_ruby_static_property_stays_canonical() {
1868 let display =
1869 display_graph_qualified_name(Language::Ruby, "Counter::name", NodeKind::Property, true);
1870 assert_eq!(display, "Counter::name");
1871 }
1872
1873 #[test]
1874 fn test_display_graph_qualified_name_php_namespace_function() {
1875 let display = display_graph_qualified_name(
1876 Language::Php,
1877 "App::Services::send_mail",
1878 NodeKind::Function,
1879 false,
1880 );
1881 assert_eq!(display, "App\\Services\\send_mail");
1882 }
1883
1884 #[test]
1885 fn test_display_graph_qualified_name_php_method() {
1886 let display = display_graph_qualified_name(
1887 Language::Php,
1888 "App::Services::Mailer::deliver",
1889 NodeKind::Method,
1890 false,
1891 );
1892 assert_eq!(display, "App\\Services\\Mailer::deliver");
1893 }
1894
1895 #[test]
1896 fn test_display_graph_qualified_name_preserves_path_like_symbols() {
1897 let display = display_graph_qualified_name(
1898 Language::Go,
1899 "route::GET::/health",
1900 NodeKind::Endpoint,
1901 false,
1902 );
1903 assert_eq!(display, "route::GET::/health");
1904 }
1905
1906 #[test]
1907 fn test_display_graph_qualified_name_preserves_ffi_symbols() {
1908 let display = display_graph_qualified_name(
1909 Language::Haskell,
1910 "ffi::C::sin",
1911 NodeKind::Function,
1912 false,
1913 );
1914 assert_eq!(display, "ffi::C::sin");
1915 }
1916
1917 #[test]
1918 fn test_display_graph_qualified_name_preserves_native_cffi_symbols() {
1919 let display = display_graph_qualified_name(
1920 Language::Python,
1921 "native::cffi::calculate",
1922 NodeKind::Function,
1923 false,
1924 );
1925 assert_eq!(display, "native::cffi::calculate");
1926 }
1927
1928 #[test]
1929 fn test_display_graph_qualified_name_preserves_native_php_ffi_symbols() {
1930 let display = display_graph_qualified_name(
1931 Language::Php,
1932 "native::ffi::crypto_encrypt",
1933 NodeKind::Function,
1934 false,
1935 );
1936 assert_eq!(display, "native::ffi::crypto_encrypt");
1937 }
1938
1939 #[test]
1940 fn test_display_graph_qualified_name_preserves_native_panama_symbols() {
1941 let display = display_graph_qualified_name(
1942 Language::Java,
1943 "native::panama::nativeLinker",
1944 NodeKind::Function,
1945 false,
1946 );
1947 assert_eq!(display, "native::panama::nativeLinker");
1948 }
1949
1950 #[test]
1951 fn test_canonicalize_graph_qualified_name_preserves_wasm_symbols() {
1952 assert_eq!(
1953 canonicalize_graph_qualified_name(Language::TypeScript, "wasm::module.wasm"),
1954 "wasm::module.wasm"
1955 );
1956 }
1957
1958 #[test]
1959 fn test_canonicalize_graph_qualified_name_preserves_native_symbols() {
1960 assert_eq!(
1961 canonicalize_graph_qualified_name(Language::TypeScript, "native::binding.node"),
1962 "native::binding.node"
1963 );
1964 }
1965
1966 #[test]
1967 fn test_display_graph_qualified_name_preserves_wasm_symbols() {
1968 let display = display_graph_qualified_name(
1969 Language::TypeScript,
1970 "wasm::module.wasm",
1971 NodeKind::Module,
1972 false,
1973 );
1974 assert_eq!(display, "wasm::module.wasm");
1975 }
1976
1977 #[test]
1978 fn test_display_graph_qualified_name_preserves_native_symbols() {
1979 let display = display_graph_qualified_name(
1980 Language::TypeScript,
1981 "native::binding.node",
1982 NodeKind::Module,
1983 false,
1984 );
1985 assert_eq!(display, "native::binding.node");
1986 }
1987
1988 #[test]
1989 fn test_canonicalize_graph_qualified_name_still_normalizes_dot_language_symbols() {
1990 assert_eq!(
1991 canonicalize_graph_qualified_name(Language::TypeScript, "Foo.bar"),
1992 "Foo::bar"
1993 );
1994 }
1995
1996 #[test]
2001 fn p2u06_witness_steps_field_defaults_to_empty() {
2002 let mut graph = CodeGraph::new();
2003 let file_path = abs_path("src/lib.rs");
2004
2005 let symbol = add_node(
2006 &mut graph,
2007 NodeKind::Function,
2008 "my_fn",
2009 Some("pkg::my_fn"),
2010 &file_path,
2011 Some(Language::Rust),
2012 1,
2013 0,
2014 );
2015
2016 let snapshot = graph.snapshot();
2017 let query = SymbolQuery {
2018 symbol: "pkg::my_fn",
2019 file_scope: FileScope::Any,
2020 mode: ResolutionMode::Strict,
2021 };
2022
2023 let witness = snapshot.resolve_symbol_with_witness(&query);
2024 assert_eq!(
2025 witness.outcome,
2026 SymbolResolutionOutcome::Resolved(symbol.node_id)
2027 );
2028 assert!(
2029 witness.steps.is_empty(),
2030 "P2U06 initialises steps to Vec::new(); emission is deferred to P2U07"
2031 );
2032 }
2033
2034 #[test]
2037 fn p2u06_witness_steps_field_is_eq_compatible() {
2038 use crate::graph::unified::bind::witness::step::ResolutionStep;
2039 use crate::graph::unified::file::id::FileId;
2040
2041 let step = ResolutionStep::EnterFileScope {
2042 file: FileId::new(0),
2043 };
2044
2045 let witness = super::SymbolResolutionWitness {
2046 normalized_query: None,
2047 outcome: super::SymbolResolutionOutcome::NotFound,
2048 selected_bucket: None,
2049 candidates: Vec::new(),
2050 symbol: None,
2051 steps: vec![step.clone()],
2052 };
2053 let expected = super::SymbolResolutionWitness {
2054 normalized_query: None,
2055 outcome: super::SymbolResolutionOutcome::NotFound,
2056 selected_bucket: None,
2057 candidates: Vec::new(),
2058 symbol: None,
2059 steps: vec![step],
2060 };
2061 assert_eq!(witness, expected);
2062 }
2063
2064 #[test]
2066 fn p2u06_witness_steps_field_clones_correctly() {
2067 use crate::graph::unified::bind::witness::step::ResolutionStep;
2068 use crate::graph::unified::node::id::NodeId;
2069
2070 let step = ResolutionStep::Chose {
2071 node: NodeId::new(99, 2),
2072 };
2073 let witness = super::SymbolResolutionWitness {
2074 normalized_query: None,
2075 outcome: super::SymbolResolutionOutcome::NotFound,
2076 selected_bucket: None,
2077 candidates: Vec::new(),
2078 symbol: None,
2079 steps: vec![step],
2080 };
2081 let cloned = witness.clone();
2082 assert_eq!(witness.steps.len(), 1);
2083 assert_eq!(cloned.steps.len(), 1);
2084 assert_eq!(witness, cloned);
2085 }
2086
2087 #[test]
2093 fn resolve_global_symbol_ambiguity_aware_returns_ambiguous_for_simple_name_collision() {
2094 let mut graph = CodeGraph::new();
2095 let file_path = abs_path("src/main.go");
2096
2097 let property_node = add_node(
2098 &mut graph,
2099 NodeKind::Property,
2100 "NeedTags",
2101 Some("main::SelectorSource::NeedTags"),
2102 &file_path,
2103 Some(Language::Go),
2104 4,
2105 6,
2106 );
2107 let variable_node = add_node(
2108 &mut graph,
2109 NodeKind::Variable,
2110 "NeedTags",
2111 Some("main::unrelated::NeedTags"),
2112 &file_path,
2113 Some(Language::Go),
2114 25,
2115 6,
2116 );
2117
2118 let snapshot = graph.snapshot();
2119 let result =
2120 snapshot.resolve_global_symbol_ambiguity_aware("NeedTags", super::FileScope::Any);
2121
2122 let err = result.expect_err("two same-name nodes must produce Ambiguous");
2123 let super::SymbolResolveError::Ambiguous(payload) = err else {
2124 panic!("expected Ambiguous variant, got {err:?}");
2125 };
2126 assert_eq!(payload.name, "NeedTags");
2127 assert!(!payload.truncated);
2128 assert_eq!(payload.candidates.len(), 2);
2129
2130 assert_eq!(
2132 payload.candidates[0].qualified_name,
2133 "main::SelectorSource::NeedTags"
2134 );
2135 assert_eq!(payload.candidates[0].kind, "property");
2136 assert_eq!(payload.candidates[0].start_line, 4);
2137 assert_eq!(payload.candidates[0].start_column, 6);
2138
2139 assert_eq!(
2140 payload.candidates[1].qualified_name,
2141 "main::unrelated::NeedTags"
2142 );
2143 assert_eq!(payload.candidates[1].kind, "variable");
2144 assert_eq!(payload.candidates[1].start_line, 25);
2145
2146 assert_ne!(property_node.node_id, variable_node.node_id);
2147 }
2148
2149 #[test]
2152 fn resolve_global_symbol_ambiguity_aware_resolves_qualified_name_uniquely() {
2153 let mut graph = CodeGraph::new();
2154 let file_path = abs_path("src/main.go");
2155
2156 let property_node = add_node(
2157 &mut graph,
2158 NodeKind::Property,
2159 "NeedTags",
2160 Some("main::SelectorSource::NeedTags"),
2161 &file_path,
2162 Some(Language::Go),
2163 4,
2164 6,
2165 );
2166 let _variable_node = add_node(
2167 &mut graph,
2168 NodeKind::Variable,
2169 "NeedTags",
2170 Some("main::unrelated::NeedTags"),
2171 &file_path,
2172 Some(Language::Go),
2173 25,
2174 6,
2175 );
2176
2177 let snapshot = graph.snapshot();
2178 let result = snapshot.resolve_global_symbol_ambiguity_aware(
2179 "main::SelectorSource::NeedTags",
2180 super::FileScope::Any,
2181 );
2182 assert_eq!(result, Ok(property_node.node_id));
2183 }
2184
2185 #[test]
2189 fn resolve_global_symbol_ambiguity_aware_normalizes_dot_delimiter() {
2190 let mut graph = CodeGraph::new();
2191 let file_path = abs_path("src/main.go");
2192
2193 let property_node = add_node(
2194 &mut graph,
2195 NodeKind::Property,
2196 "NeedTags",
2197 Some("main::SelectorSource::NeedTags"),
2198 &file_path,
2199 Some(Language::Go),
2200 4,
2201 6,
2202 );
2203 let _variable_node = add_node(
2204 &mut graph,
2205 NodeKind::Variable,
2206 "NeedTags",
2207 Some("main::unrelated::NeedTags"),
2208 &file_path,
2209 Some(Language::Go),
2210 25,
2211 6,
2212 );
2213
2214 let snapshot = graph.snapshot();
2215 let result = snapshot.resolve_global_symbol_ambiguity_aware(
2216 "main.SelectorSource.NeedTags",
2217 super::FileScope::Any,
2218 );
2219 assert_eq!(result, Ok(property_node.node_id));
2220 }
2221
2222 #[test]
2225 fn resolve_global_symbol_ambiguity_aware_returns_not_found_for_missing_symbol() {
2226 let graph = CodeGraph::new();
2227 let snapshot = graph.snapshot();
2228 let result =
2229 snapshot.resolve_global_symbol_ambiguity_aware("does_not_exist", super::FileScope::Any);
2230 assert_eq!(
2231 result,
2232 Err(super::SymbolResolveError::NotFound {
2233 name: "does_not_exist".to_string(),
2234 })
2235 );
2236 }
2237
2238 #[test]
2241 fn resolve_global_symbol_ambiguity_aware_caps_candidates_at_truncation_limit() {
2242 let mut graph = CodeGraph::new();
2243 let file_path = abs_path("src/main.go");
2244 let total = super::AMBIGUOUS_SYMBOL_CANDIDATE_CAP + 5;
2245
2246 for index in 0..total {
2247 let qualified = format!("pkg::module_{index:03}::collide");
2250 add_node(
2251 &mut graph,
2252 NodeKind::Function,
2253 "collide",
2254 Some(qualified.as_str()),
2255 &file_path,
2256 Some(Language::Go),
2257 u32::try_from(index + 1).unwrap_or(1),
2258 0,
2259 );
2260 }
2261
2262 let snapshot = graph.snapshot();
2263 let err = snapshot
2264 .resolve_global_symbol_ambiguity_aware("collide", super::FileScope::Any)
2265 .expect_err("collisions across many nodes must surface as Ambiguous");
2266 let super::SymbolResolveError::Ambiguous(payload) = err else {
2267 panic!("expected Ambiguous variant");
2268 };
2269
2270 assert!(payload.truncated, "truncated flag must be set above cap");
2271 assert_eq!(
2272 payload.candidates.len(),
2273 super::AMBIGUOUS_SYMBOL_CANDIDATE_CAP
2274 );
2275 assert_eq!(
2277 payload.candidates[0].qualified_name,
2278 "pkg::module_000::collide"
2279 );
2280 }
2281
2282 #[test]
2285 fn resolve_global_symbol_ambiguity_aware_respects_file_scope() {
2286 let mut graph = CodeGraph::new();
2287 let scope_file = abs_path("src/in_scope.go");
2288 let other_file = abs_path("src/other.go");
2289
2290 let scoped_property = add_node(
2291 &mut graph,
2292 NodeKind::Property,
2293 "Same",
2294 Some("main::Owner::Same"),
2295 &scope_file,
2296 Some(Language::Go),
2297 10,
2298 6,
2299 );
2300 let _outside = add_node(
2301 &mut graph,
2302 NodeKind::Property,
2303 "Same",
2304 Some("main::Other::Same"),
2305 &other_file,
2306 Some(Language::Go),
2307 10,
2308 6,
2309 );
2310
2311 let snapshot = graph.snapshot();
2312 let result = snapshot
2313 .resolve_global_symbol_ambiguity_aware("Same", super::FileScope::Path(&scope_file));
2314 assert_eq!(result, Ok(scoped_property.node_id));
2315 }
2316}