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("::") {
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 _ => qualified.to_string(),
862 }
863}
864
865fn should_display_ruby_member_variable(qualified: &str) -> bool {
866 let Some((_, suffix)) = qualified.rsplit_once("::") else {
867 return false;
868 };
869
870 if suffix.starts_with("@@")
871 || suffix
872 .chars()
873 .next()
874 .is_some_and(|character| character.is_ascii_uppercase())
875 {
876 return false;
877 }
878
879 suffix.starts_with('@')
880 || suffix
881 .chars()
882 .next()
883 .is_some_and(|character| character.is_ascii_lowercase() || character == '_')
884}
885
886fn display_php_qualified_name(qualified: &str, kind: NodeKind) -> String {
887 if !qualified.contains("::") {
888 return qualified.to_string();
889 }
890
891 if matches!(kind, NodeKind::Method | NodeKind::Property) {
892 return replace_last_separator(qualified, "::", true);
893 }
894
895 qualified.replace("::", "\\")
896}
897
898fn replace_last_separator(qualified: &str, final_separator: &str, preserve_prefix: bool) -> String {
899 let Some((prefix, suffix)) = qualified.rsplit_once("::") else {
900 return qualified.to_string();
901 };
902
903 let display_prefix = if preserve_prefix {
904 prefix.replace("::", "\\")
905 } else {
906 prefix.to_string()
907 };
908
909 if display_prefix.is_empty() {
910 suffix.to_string()
911 } else {
912 format!("{display_prefix}{final_separator}{suffix}")
913 }
914}
915
916#[cfg(test)]
917mod tests {
918 use std::path::{Path, PathBuf};
919
920 use crate::graph::node::Language;
921 use crate::graph::unified::concurrent::CodeGraph;
922 use crate::graph::unified::node::id::NodeId;
923 use crate::graph::unified::node::kind::NodeKind;
924 use crate::graph::unified::storage::arena::NodeEntry;
925
926 use super::{
927 FileScope, NormalizedSymbolQuery, ResolutionMode, ResolvedFileScope, SymbolCandidateBucket,
928 SymbolCandidateOutcome, SymbolQuery, SymbolResolutionOutcome,
929 canonicalize_graph_qualified_name, display_graph_qualified_name,
930 };
931
932 struct TestNode {
933 node_id: NodeId,
934 }
935
936 #[test]
937 fn test_resolve_symbol_exact_qualified_same_file() {
938 let mut graph = CodeGraph::new();
939 let file_path = abs_path("src/lib.rs");
940 let symbol = add_node(
941 &mut graph,
942 NodeKind::Function,
943 "target",
944 Some("pkg::target"),
945 &file_path,
946 Some(Language::Rust),
947 10,
948 2,
949 );
950
951 let snapshot = graph.snapshot();
952 let query = SymbolQuery {
953 symbol: "pkg::target",
954 file_scope: FileScope::Path(&file_path),
955 mode: ResolutionMode::Strict,
956 };
957
958 assert_eq!(
959 snapshot.resolve_symbol(&query),
960 SymbolResolutionOutcome::Resolved(symbol.node_id)
961 );
962 }
963
964 #[test]
965 fn test_resolve_symbol_exact_simple_same_file_wins() {
966 let mut graph = CodeGraph::new();
967 let requested_path = abs_path("src/requested.rs");
968 let other_path = abs_path("src/other.rs");
969
970 let requested = add_node(
971 &mut graph,
972 NodeKind::Function,
973 "target",
974 Some("requested::target"),
975 &requested_path,
976 Some(Language::Rust),
977 4,
978 0,
979 );
980 let _other = add_node(
981 &mut graph,
982 NodeKind::Function,
983 "target",
984 Some("other::target"),
985 &other_path,
986 Some(Language::Rust),
987 1,
988 0,
989 );
990
991 let snapshot = graph.snapshot();
992 let query = SymbolQuery {
993 symbol: "target",
994 file_scope: FileScope::Path(&requested_path),
995 mode: ResolutionMode::Strict,
996 };
997
998 assert_eq!(
999 snapshot.resolve_symbol(&query),
1000 SymbolResolutionOutcome::Resolved(requested.node_id)
1001 );
1002 }
1003
1004 #[test]
1005 fn test_resolve_symbol_returns_not_found_without_wrong_file_fallback() {
1006 let mut graph = CodeGraph::new();
1007 let requested_path = abs_path("src/requested.rs");
1008 let other_path = abs_path("src/other.rs");
1009
1010 let _requested_index_anchor = add_node(
1011 &mut graph,
1012 NodeKind::Function,
1013 "anchor",
1014 Some("requested::anchor"),
1015 &requested_path,
1016 Some(Language::Rust),
1017 1,
1018 0,
1019 );
1020 let _other = add_node(
1021 &mut graph,
1022 NodeKind::Function,
1023 "target",
1024 Some("other::target"),
1025 &other_path,
1026 Some(Language::Rust),
1027 3,
1028 0,
1029 );
1030
1031 let snapshot = graph.snapshot();
1032 let query = SymbolQuery {
1033 symbol: "target",
1034 file_scope: FileScope::Path(&requested_path),
1035 mode: ResolutionMode::Strict,
1036 };
1037
1038 assert_eq!(
1039 snapshot.resolve_symbol(&query),
1040 SymbolResolutionOutcome::NotFound
1041 );
1042 }
1043
1044 #[test]
1045 fn test_resolve_symbol_returns_file_not_indexed_for_valid_unindexed_path() {
1046 let mut graph = CodeGraph::new();
1047 let indexed_path = abs_path("src/indexed.rs");
1048 let unindexed_path = abs_path("src/unindexed.rs");
1049
1050 add_node(
1051 &mut graph,
1052 NodeKind::Function,
1053 "indexed",
1054 Some("pkg::indexed"),
1055 &indexed_path,
1056 Some(Language::Rust),
1057 1,
1058 0,
1059 );
1060 graph
1061 .files_mut()
1062 .register_with_language(&unindexed_path, Some(Language::Rust))
1063 .unwrap();
1064
1065 let snapshot = graph.snapshot();
1066 let query = SymbolQuery {
1067 symbol: "indexed",
1068 file_scope: FileScope::Path(&unindexed_path),
1069 mode: ResolutionMode::Strict,
1070 };
1071
1072 assert_eq!(
1073 snapshot.resolve_symbol(&query),
1074 SymbolResolutionOutcome::FileNotIndexed
1075 );
1076 }
1077
1078 #[test]
1079 fn test_resolve_symbol_returns_ambiguous_for_multi_match_bucket() {
1080 let mut graph = CodeGraph::new();
1081 let file_path = abs_path("src/lib.rs");
1082
1083 let first = add_node(
1084 &mut graph,
1085 NodeKind::Function,
1086 "dup",
1087 Some("pkg::dup"),
1088 &file_path,
1089 Some(Language::Rust),
1090 2,
1091 0,
1092 );
1093 let second = add_node(
1094 &mut graph,
1095 NodeKind::Method,
1096 "dup",
1097 Some("pkg::dup_method"),
1098 &file_path,
1099 Some(Language::Rust),
1100 8,
1101 0,
1102 );
1103
1104 let snapshot = graph.snapshot();
1105 let query = SymbolQuery {
1106 symbol: "dup",
1107 file_scope: FileScope::Path(&file_path),
1108 mode: ResolutionMode::Strict,
1109 };
1110
1111 assert_eq!(
1112 snapshot.resolve_symbol(&query),
1113 SymbolResolutionOutcome::Ambiguous(vec![first.node_id, second.node_id])
1114 );
1115 }
1116
1117 #[test]
1118 fn test_find_symbol_candidates_uses_first_non_empty_bucket_only() {
1119 let mut graph = CodeGraph::new();
1120 let qualified_path = abs_path("src/qualified.rs");
1121 let simple_path = abs_path("src/simple.rs");
1122
1123 let qualified = add_node(
1124 &mut graph,
1125 NodeKind::Function,
1126 "target",
1127 Some("pkg::target"),
1128 &qualified_path,
1129 Some(Language::Rust),
1130 1,
1131 0,
1132 );
1133 let simple_only = add_node(
1134 &mut graph,
1135 NodeKind::Function,
1136 "pkg::target",
1137 None,
1138 &simple_path,
1139 Some(Language::Rust),
1140 1,
1141 0,
1142 );
1143
1144 let snapshot = graph.snapshot();
1145 let query = SymbolQuery {
1146 symbol: "pkg::target",
1147 file_scope: FileScope::Any,
1148 mode: ResolutionMode::AllowSuffixCandidates,
1149 };
1150
1151 assert_eq!(
1152 snapshot.find_symbol_candidates(&query),
1153 SymbolCandidateOutcome::Candidates(vec![qualified.node_id])
1154 );
1155 assert_ne!(qualified.node_id, simple_only.node_id);
1156 }
1157
1158 #[test]
1159 fn test_find_symbol_candidates_with_witness_reports_exact_qualified_bucket() {
1160 let mut graph = CodeGraph::new();
1161 let qualified_path = abs_path("src/qualified.rs");
1162 let simple_path = abs_path("src/simple.rs");
1163
1164 let qualified = add_node(
1165 &mut graph,
1166 NodeKind::Function,
1167 "target",
1168 Some("pkg::target"),
1169 &qualified_path,
1170 Some(Language::Rust),
1171 1,
1172 0,
1173 );
1174 let _simple_only = add_node(
1175 &mut graph,
1176 NodeKind::Function,
1177 "pkg::target",
1178 None,
1179 &simple_path,
1180 Some(Language::Rust),
1181 1,
1182 0,
1183 );
1184
1185 let snapshot = graph.snapshot();
1186 let query = SymbolQuery {
1187 symbol: "pkg::target",
1188 file_scope: FileScope::Any,
1189 mode: ResolutionMode::AllowSuffixCandidates,
1190 };
1191
1192 let witness = snapshot.find_symbol_candidates_with_witness(&query);
1193
1194 assert_eq!(
1195 witness.outcome,
1196 SymbolCandidateOutcome::Candidates(vec![qualified.node_id])
1197 );
1198 assert_eq!(
1199 witness.selected_bucket,
1200 Some(SymbolCandidateBucket::ExactQualified)
1201 );
1202 assert_eq!(
1203 witness.candidates,
1204 vec![super::SymbolCandidateWitness {
1205 node_id: qualified.node_id,
1206 bucket: SymbolCandidateBucket::ExactQualified,
1207 }]
1208 );
1209 assert_eq!(
1210 witness.normalized_query,
1211 Some(NormalizedSymbolQuery {
1212 symbol: "pkg::target".to_string(),
1213 file_scope: ResolvedFileScope::Any,
1214 mode: ResolutionMode::AllowSuffixCandidates,
1215 })
1216 );
1217 }
1218
1219 #[test]
1220 fn test_find_symbol_candidates_preserves_file_not_indexed() {
1221 let mut graph = CodeGraph::new();
1222 let indexed_path = abs_path("src/indexed.rs");
1223 let unindexed_path = abs_path("src/unindexed.rs");
1224
1225 add_node(
1226 &mut graph,
1227 NodeKind::Function,
1228 "target",
1229 Some("pkg::target"),
1230 &indexed_path,
1231 Some(Language::Rust),
1232 1,
1233 0,
1234 );
1235 let unindexed_file_id = graph
1236 .files_mut()
1237 .register_with_language(&unindexed_path, Some(Language::Rust))
1238 .unwrap();
1239
1240 let snapshot = graph.snapshot();
1241 let query = SymbolQuery {
1242 symbol: "target",
1243 file_scope: FileScope::FileId(unindexed_file_id),
1244 mode: ResolutionMode::AllowSuffixCandidates,
1245 };
1246
1247 assert_eq!(
1248 snapshot.find_symbol_candidates(&query),
1249 SymbolCandidateOutcome::FileNotIndexed
1250 );
1251 }
1252
1253 #[test]
1254 fn test_resolve_symbol_with_witness_reports_ambiguous_bucket_candidates() {
1255 let mut graph = CodeGraph::new();
1256 let file_path = abs_path("src/lib.rs");
1257
1258 let first = add_node(
1259 &mut graph,
1260 NodeKind::Function,
1261 "dup",
1262 Some("pkg::dup"),
1263 &file_path,
1264 Some(Language::Rust),
1265 2,
1266 0,
1267 );
1268 let second = add_node(
1269 &mut graph,
1270 NodeKind::Method,
1271 "dup",
1272 Some("pkg::dup_method"),
1273 &file_path,
1274 Some(Language::Rust),
1275 8,
1276 0,
1277 );
1278
1279 let snapshot = graph.snapshot();
1280 let query = SymbolQuery {
1281 symbol: "dup",
1282 file_scope: FileScope::Path(&file_path),
1283 mode: ResolutionMode::Strict,
1284 };
1285
1286 let witness = snapshot.resolve_symbol_with_witness(&query);
1287
1288 assert_eq!(
1289 witness.outcome,
1290 SymbolResolutionOutcome::Ambiguous(vec![first.node_id, second.node_id])
1291 );
1292 assert_eq!(
1293 witness.selected_bucket,
1294 Some(SymbolCandidateBucket::ExactSimple)
1295 );
1296 assert_eq!(
1297 witness.candidates,
1298 vec![
1299 super::SymbolCandidateWitness {
1300 node_id: first.node_id,
1301 bucket: SymbolCandidateBucket::ExactSimple,
1302 },
1303 super::SymbolCandidateWitness {
1304 node_id: second.node_id,
1305 bucket: SymbolCandidateBucket::ExactSimple,
1306 },
1307 ]
1308 );
1309 }
1310
1311 #[test]
1312 fn test_suffix_candidates_disabled_in_strict_mode() {
1313 let mut graph = CodeGraph::new();
1314 let file_path = abs_path("src/lib.rs");
1315
1316 let suffix_match = add_node(
1317 &mut graph,
1318 NodeKind::Function,
1319 "target",
1320 Some("outer::pkg::target"),
1321 &file_path,
1322 Some(Language::Rust),
1323 1,
1324 0,
1325 );
1326
1327 let snapshot = graph.snapshot();
1328 let strict_query = SymbolQuery {
1329 symbol: "pkg::target",
1330 file_scope: FileScope::Any,
1331 mode: ResolutionMode::Strict,
1332 };
1333 let suffix_query = SymbolQuery {
1334 mode: ResolutionMode::AllowSuffixCandidates,
1335 ..strict_query
1336 };
1337
1338 assert_eq!(
1339 snapshot.resolve_symbol(&strict_query),
1340 SymbolResolutionOutcome::NotFound
1341 );
1342 assert_eq!(
1343 snapshot.find_symbol_candidates(&suffix_query),
1344 SymbolCandidateOutcome::Candidates(vec![suffix_match.node_id])
1345 );
1346 }
1347
1348 #[test]
1349 fn test_suffix_candidates_require_canonical_qualified_query() {
1350 let mut graph = CodeGraph::new();
1351 let file_path = abs_path("src/mod.py");
1352
1353 add_node(
1354 &mut graph,
1355 NodeKind::Function,
1356 "target",
1357 Some("pkg::target"),
1358 &file_path,
1359 Some(Language::Python),
1360 1,
1361 0,
1362 );
1363
1364 let snapshot = graph.snapshot();
1365 let query = SymbolQuery {
1366 symbol: "pkg.target",
1367 file_scope: FileScope::Any,
1368 mode: ResolutionMode::AllowSuffixCandidates,
1369 };
1370
1371 assert_eq!(
1372 snapshot.find_symbol_candidates(&query),
1373 SymbolCandidateOutcome::NotFound
1374 );
1375 }
1376
1377 #[test]
1378 fn test_suffix_candidates_filter_same_leaf_bucket_only() {
1379 let mut graph = CodeGraph::new();
1380 let file_path = abs_path("src/lib.rs");
1381
1382 let exact_suffix = add_node(
1383 &mut graph,
1384 NodeKind::Function,
1385 "target",
1386 Some("outer::pkg::target"),
1387 &file_path,
1388 Some(Language::Rust),
1389 2,
1390 0,
1391 );
1392 let another_suffix = add_node(
1393 &mut graph,
1394 NodeKind::Method,
1395 "target",
1396 Some("another::pkg::target"),
1397 &file_path,
1398 Some(Language::Rust),
1399 4,
1400 0,
1401 );
1402 let unrelated = add_node(
1403 &mut graph,
1404 NodeKind::Function,
1405 "target",
1406 Some("pkg::different::target"),
1407 &file_path,
1408 Some(Language::Rust),
1409 6,
1410 0,
1411 );
1412
1413 let snapshot = graph.snapshot();
1414 let query = SymbolQuery {
1415 symbol: "pkg::target",
1416 file_scope: FileScope::Any,
1417 mode: ResolutionMode::AllowSuffixCandidates,
1418 };
1419
1420 assert_eq!(
1421 snapshot.find_symbol_candidates(&query),
1422 SymbolCandidateOutcome::Candidates(vec![exact_suffix.node_id, another_suffix.node_id])
1423 );
1424 assert_ne!(unrelated.node_id, exact_suffix.node_id);
1425 }
1426
1427 #[test]
1428 fn test_normalize_symbol_query_rewrites_native_delimiter_when_file_scope_language_known() {
1429 let mut graph = CodeGraph::new();
1430 let file_path = abs_path("src/mod.py");
1431 let file_id = graph
1432 .files_mut()
1433 .register_with_language(&file_path, Some(Language::Python))
1434 .unwrap();
1435 let snapshot = graph.snapshot();
1436 let query = SymbolQuery {
1437 symbol: "pkg.mod.fn",
1438 file_scope: FileScope::Path(&file_path),
1439 mode: ResolutionMode::Strict,
1440 };
1441
1442 let normalized = snapshot.normalize_symbol_query(&query, &ResolvedFileScope::File(file_id));
1443
1444 assert_eq!(
1445 normalized,
1446 NormalizedSymbolQuery {
1447 symbol: "pkg::mod::fn".to_string(),
1448 file_scope: ResolvedFileScope::File(file_id),
1449 mode: ResolutionMode::Strict,
1450 }
1451 );
1452 }
1453
1454 #[test]
1455 fn test_normalize_symbol_query_rewrites_native_delimiter_for_csharp() {
1456 let mut graph = CodeGraph::new();
1457 let file_path = abs_path("src/Program.cs");
1458 let file_id = graph
1459 .files_mut()
1460 .register_with_language(&file_path, Some(Language::CSharp))
1461 .unwrap();
1462 let snapshot = graph.snapshot();
1463 let query = SymbolQuery {
1464 symbol: "System.Console.WriteLine",
1465 file_scope: FileScope::Path(&file_path),
1466 mode: ResolutionMode::Strict,
1467 };
1468
1469 let normalized = snapshot.normalize_symbol_query(&query, &ResolvedFileScope::File(file_id));
1470
1471 assert_eq!(normalized.symbol, "System::Console::WriteLine".to_string());
1472 }
1473
1474 #[test]
1475 fn test_normalize_symbol_query_rewrites_native_delimiter_for_zig() {
1476 let mut graph = CodeGraph::new();
1477 let file_path = abs_path("src/main.zig");
1478 let file_id = graph
1479 .files_mut()
1480 .register_with_language(&file_path, Some(Language::Zig))
1481 .unwrap();
1482 let snapshot = graph.snapshot();
1483 let query = SymbolQuery {
1484 symbol: "std.os.linux.exit",
1485 file_scope: FileScope::Path(&file_path),
1486 mode: ResolutionMode::Strict,
1487 };
1488
1489 let normalized = snapshot.normalize_symbol_query(&query, &ResolvedFileScope::File(file_id));
1490
1491 assert_eq!(normalized.symbol, "std::os::linux::exit".to_string());
1492 }
1493
1494 #[test]
1495 fn test_normalize_symbol_query_does_not_rewrite_when_file_scope_any() {
1496 let graph = CodeGraph::new();
1497 let snapshot = graph.snapshot();
1498 let query = SymbolQuery {
1499 symbol: "pkg.mod.fn",
1500 file_scope: FileScope::Any,
1501 mode: ResolutionMode::Strict,
1502 };
1503
1504 let normalized = snapshot.normalize_symbol_query(&query, &ResolvedFileScope::Any);
1505
1506 assert_eq!(
1507 normalized,
1508 NormalizedSymbolQuery {
1509 symbol: "pkg.mod.fn".to_string(),
1510 file_scope: ResolvedFileScope::Any,
1511 mode: ResolutionMode::Strict,
1512 }
1513 );
1514 }
1515
1516 #[test]
1517 fn test_global_qualified_query_with_native_delimiter_is_exact_only_and_not_found() {
1518 let mut graph = CodeGraph::new();
1519 let file_path = abs_path("src/mod.py");
1520
1521 add_node(
1522 &mut graph,
1523 NodeKind::Function,
1524 "fn",
1525 Some("pkg::mod::fn"),
1526 &file_path,
1527 Some(Language::Python),
1528 1,
1529 0,
1530 );
1531
1532 let snapshot = graph.snapshot();
1533 let query = SymbolQuery {
1534 symbol: "pkg.mod.fn",
1535 file_scope: FileScope::Any,
1536 mode: ResolutionMode::AllowSuffixCandidates,
1537 };
1538
1539 assert_eq!(
1540 snapshot.resolve_symbol(&query),
1541 SymbolResolutionOutcome::NotFound
1542 );
1543 }
1544
1545 #[test]
1546 fn test_global_canonical_qualified_query_can_hit_exact_qualified_bucket() {
1547 let mut graph = CodeGraph::new();
1548 let file_path = abs_path("src/lib.rs");
1549 let expected = add_node(
1550 &mut graph,
1551 NodeKind::Function,
1552 "fn",
1553 Some("pkg::mod::fn"),
1554 &file_path,
1555 Some(Language::Rust),
1556 1,
1557 0,
1558 );
1559
1560 let snapshot = graph.snapshot();
1561 let query = SymbolQuery {
1562 symbol: "pkg::mod::fn",
1563 file_scope: FileScope::Any,
1564 mode: ResolutionMode::Strict,
1565 };
1566
1567 assert_eq!(
1568 snapshot.resolve_symbol(&query),
1569 SymbolResolutionOutcome::Resolved(expected.node_id)
1570 );
1571 }
1572
1573 #[test]
1574 fn test_candidate_order_uses_metadata_then_node_id() {
1575 let mut graph = CodeGraph::new();
1576 let file_path = abs_path("src/lib.rs");
1577
1578 let first = add_node(
1579 &mut graph,
1580 NodeKind::Function,
1581 "dup",
1582 Some("pkg::dup_a"),
1583 &file_path,
1584 Some(Language::Rust),
1585 1,
1586 0,
1587 );
1588 let second = add_node(
1589 &mut graph,
1590 NodeKind::Function,
1591 "dup",
1592 Some("pkg::dup_b"),
1593 &file_path,
1594 Some(Language::Rust),
1595 1,
1596 0,
1597 );
1598
1599 let snapshot = graph.snapshot();
1600 let query = SymbolQuery {
1601 symbol: "dup",
1602 file_scope: FileScope::Any,
1603 mode: ResolutionMode::Strict,
1604 };
1605
1606 assert_eq!(
1607 snapshot.find_symbol_candidates(&query),
1608 SymbolCandidateOutcome::Candidates(vec![first.node_id, second.node_id])
1609 );
1610 }
1611
1612 #[test]
1613 fn test_candidate_order_kind_sort_key_uses_node_kind_as_str() {
1614 let mut graph = CodeGraph::new();
1615 let file_path = abs_path("src/lib.rs");
1616
1617 let function_node = add_node(
1618 &mut graph,
1619 NodeKind::Function,
1620 "shared",
1621 Some("pkg::shared_fn"),
1622 &file_path,
1623 Some(Language::Rust),
1624 1,
1625 0,
1626 );
1627 let variable_node = add_node(
1628 &mut graph,
1629 NodeKind::Variable,
1630 "shared",
1631 Some("pkg::shared_var"),
1632 &file_path,
1633 Some(Language::Rust),
1634 1,
1635 0,
1636 );
1637
1638 let snapshot = graph.snapshot();
1639 let query = SymbolQuery {
1640 symbol: "shared",
1641 file_scope: FileScope::Any,
1642 mode: ResolutionMode::Strict,
1643 };
1644
1645 assert_eq!(
1646 snapshot.find_symbol_candidates(&query),
1647 SymbolCandidateOutcome::Candidates(vec![function_node.node_id, variable_node.node_id])
1648 );
1649 }
1650
1651 fn add_node(
1652 graph: &mut CodeGraph,
1653 kind: NodeKind,
1654 name: &str,
1655 qualified_name: Option<&str>,
1656 file_path: &Path,
1657 language: Option<Language>,
1658 start_line: u32,
1659 start_column: u32,
1660 ) -> TestNode {
1661 let name_id = graph.strings_mut().intern(name).unwrap();
1662 let qualified_name_id =
1663 qualified_name.map(|value| graph.strings_mut().intern(value).unwrap());
1664 let file_id = graph
1665 .files_mut()
1666 .register_with_language(file_path, language)
1667 .unwrap();
1668
1669 let entry = NodeEntry::new(kind, name_id, file_id)
1670 .with_qualified_name_opt(qualified_name_id)
1671 .with_location(start_line, start_column, start_line, start_column + 1);
1672
1673 let node_id = graph.nodes_mut().alloc(entry).unwrap();
1674 graph
1675 .indices_mut()
1676 .add(node_id, kind, name_id, qualified_name_id, file_id);
1677
1678 TestNode { node_id }
1679 }
1680
1681 trait NodeEntryExt {
1682 fn with_qualified_name_opt(
1683 self,
1684 qualified_name: Option<crate::graph::unified::string::id::StringId>,
1685 ) -> Self;
1686 }
1687
1688 impl NodeEntryExt for NodeEntry {
1689 fn with_qualified_name_opt(
1690 mut self,
1691 qualified_name: Option<crate::graph::unified::string::id::StringId>,
1692 ) -> Self {
1693 self.qualified_name = qualified_name;
1694 self
1695 }
1696 }
1697
1698 fn abs_path(relative: &str) -> PathBuf {
1699 PathBuf::from("/resolver-tests").join(relative)
1700 }
1701
1702 #[test]
1703 fn test_display_graph_qualified_name_dot_language() {
1704 let display = display_graph_qualified_name(
1705 Language::CSharp,
1706 "MyApp::User::GetName",
1707 NodeKind::Method,
1708 false,
1709 );
1710 assert_eq!(display, "MyApp.User.GetName");
1711 }
1712
1713 #[test]
1714 fn test_canonicalize_graph_qualified_name_r_private_name_preserved() {
1715 assert_eq!(
1716 canonicalize_graph_qualified_name(Language::R, ".private_func"),
1717 ".private_func"
1718 );
1719 }
1720
1721 #[test]
1722 fn test_canonicalize_graph_qualified_name_r_s3_method_uses_last_dot() {
1723 assert_eq!(
1724 canonicalize_graph_qualified_name(Language::R, "as.data.frame.myclass"),
1725 "as.data.frame::myclass"
1726 );
1727 }
1728
1729 #[test]
1730 fn test_canonicalize_graph_qualified_name_r_leading_dot_s3_generic() {
1731 assert_eq!(
1732 canonicalize_graph_qualified_name(Language::R, ".DollarNames.myclass"),
1733 ".DollarNames::myclass"
1734 );
1735 }
1736
1737 #[test]
1738 fn test_display_graph_qualified_name_ruby_instance_method() {
1739 let display = display_graph_qualified_name(
1740 Language::Ruby,
1741 "Admin::Users::Controller::show",
1742 NodeKind::Method,
1743 false,
1744 );
1745 assert_eq!(display, "Admin::Users::Controller#show");
1746 }
1747
1748 #[test]
1749 fn test_display_graph_qualified_name_ruby_singleton_method() {
1750 let display = display_graph_qualified_name(
1751 Language::Ruby,
1752 "Admin::Users::Controller::show",
1753 NodeKind::Method,
1754 true,
1755 );
1756 assert_eq!(display, "Admin::Users::Controller.show");
1757 }
1758
1759 #[test]
1760 fn test_display_graph_qualified_name_ruby_member_variable() {
1761 let display = display_graph_qualified_name(
1762 Language::Ruby,
1763 "Admin::Users::Controller::username",
1764 NodeKind::Variable,
1765 false,
1766 );
1767 assert_eq!(display, "Admin::Users::Controller#username");
1768 }
1769
1770 #[test]
1771 fn test_display_graph_qualified_name_ruby_instance_variable() {
1772 let display = display_graph_qualified_name(
1773 Language::Ruby,
1774 "Admin::Users::Controller::@current_user",
1775 NodeKind::Variable,
1776 false,
1777 );
1778 assert_eq!(display, "Admin::Users::Controller#@current_user");
1779 }
1780
1781 #[test]
1782 fn test_display_graph_qualified_name_ruby_constant_stays_canonical() {
1783 let display = display_graph_qualified_name(
1784 Language::Ruby,
1785 "Admin::Users::Controller::DEFAULT_ROLE",
1786 NodeKind::Variable,
1787 false,
1788 );
1789 assert_eq!(display, "Admin::Users::Controller::DEFAULT_ROLE");
1790 }
1791
1792 #[test]
1793 fn test_display_graph_qualified_name_ruby_class_variable_stays_canonical() {
1794 let display = display_graph_qualified_name(
1795 Language::Ruby,
1796 "Admin::Users::Controller::@@count",
1797 NodeKind::Variable,
1798 false,
1799 );
1800 assert_eq!(display, "Admin::Users::Controller::@@count");
1801 }
1802
1803 #[test]
1804 fn test_display_graph_qualified_name_php_namespace_function() {
1805 let display = display_graph_qualified_name(
1806 Language::Php,
1807 "App::Services::send_mail",
1808 NodeKind::Function,
1809 false,
1810 );
1811 assert_eq!(display, "App\\Services\\send_mail");
1812 }
1813
1814 #[test]
1815 fn test_display_graph_qualified_name_php_method() {
1816 let display = display_graph_qualified_name(
1817 Language::Php,
1818 "App::Services::Mailer::deliver",
1819 NodeKind::Method,
1820 false,
1821 );
1822 assert_eq!(display, "App\\Services\\Mailer::deliver");
1823 }
1824
1825 #[test]
1826 fn test_display_graph_qualified_name_preserves_path_like_symbols() {
1827 let display = display_graph_qualified_name(
1828 Language::Go,
1829 "route::GET::/health",
1830 NodeKind::Endpoint,
1831 false,
1832 );
1833 assert_eq!(display, "route::GET::/health");
1834 }
1835
1836 #[test]
1837 fn test_display_graph_qualified_name_preserves_ffi_symbols() {
1838 let display = display_graph_qualified_name(
1839 Language::Haskell,
1840 "ffi::C::sin",
1841 NodeKind::Function,
1842 false,
1843 );
1844 assert_eq!(display, "ffi::C::sin");
1845 }
1846
1847 #[test]
1848 fn test_display_graph_qualified_name_preserves_native_cffi_symbols() {
1849 let display = display_graph_qualified_name(
1850 Language::Python,
1851 "native::cffi::calculate",
1852 NodeKind::Function,
1853 false,
1854 );
1855 assert_eq!(display, "native::cffi::calculate");
1856 }
1857
1858 #[test]
1859 fn test_display_graph_qualified_name_preserves_native_php_ffi_symbols() {
1860 let display = display_graph_qualified_name(
1861 Language::Php,
1862 "native::ffi::crypto_encrypt",
1863 NodeKind::Function,
1864 false,
1865 );
1866 assert_eq!(display, "native::ffi::crypto_encrypt");
1867 }
1868
1869 #[test]
1870 fn test_display_graph_qualified_name_preserves_native_panama_symbols() {
1871 let display = display_graph_qualified_name(
1872 Language::Java,
1873 "native::panama::nativeLinker",
1874 NodeKind::Function,
1875 false,
1876 );
1877 assert_eq!(display, "native::panama::nativeLinker");
1878 }
1879
1880 #[test]
1881 fn test_canonicalize_graph_qualified_name_preserves_wasm_symbols() {
1882 assert_eq!(
1883 canonicalize_graph_qualified_name(Language::TypeScript, "wasm::module.wasm"),
1884 "wasm::module.wasm"
1885 );
1886 }
1887
1888 #[test]
1889 fn test_canonicalize_graph_qualified_name_preserves_native_symbols() {
1890 assert_eq!(
1891 canonicalize_graph_qualified_name(Language::TypeScript, "native::binding.node"),
1892 "native::binding.node"
1893 );
1894 }
1895
1896 #[test]
1897 fn test_display_graph_qualified_name_preserves_wasm_symbols() {
1898 let display = display_graph_qualified_name(
1899 Language::TypeScript,
1900 "wasm::module.wasm",
1901 NodeKind::Module,
1902 false,
1903 );
1904 assert_eq!(display, "wasm::module.wasm");
1905 }
1906
1907 #[test]
1908 fn test_display_graph_qualified_name_preserves_native_symbols() {
1909 let display = display_graph_qualified_name(
1910 Language::TypeScript,
1911 "native::binding.node",
1912 NodeKind::Module,
1913 false,
1914 );
1915 assert_eq!(display, "native::binding.node");
1916 }
1917
1918 #[test]
1919 fn test_canonicalize_graph_qualified_name_still_normalizes_dot_language_symbols() {
1920 assert_eq!(
1921 canonicalize_graph_qualified_name(Language::TypeScript, "Foo.bar"),
1922 "Foo::bar"
1923 );
1924 }
1925
1926 #[test]
1931 fn p2u06_witness_steps_field_defaults_to_empty() {
1932 let mut graph = CodeGraph::new();
1933 let file_path = abs_path("src/lib.rs");
1934
1935 let symbol = add_node(
1936 &mut graph,
1937 NodeKind::Function,
1938 "my_fn",
1939 Some("pkg::my_fn"),
1940 &file_path,
1941 Some(Language::Rust),
1942 1,
1943 0,
1944 );
1945
1946 let snapshot = graph.snapshot();
1947 let query = SymbolQuery {
1948 symbol: "pkg::my_fn",
1949 file_scope: FileScope::Any,
1950 mode: ResolutionMode::Strict,
1951 };
1952
1953 let witness = snapshot.resolve_symbol_with_witness(&query);
1954 assert_eq!(
1955 witness.outcome,
1956 SymbolResolutionOutcome::Resolved(symbol.node_id)
1957 );
1958 assert!(
1959 witness.steps.is_empty(),
1960 "P2U06 initialises steps to Vec::new(); emission is deferred to P2U07"
1961 );
1962 }
1963
1964 #[test]
1967 fn p2u06_witness_steps_field_is_eq_compatible() {
1968 use crate::graph::unified::bind::witness::step::ResolutionStep;
1969 use crate::graph::unified::file::id::FileId;
1970
1971 let step = ResolutionStep::EnterFileScope {
1972 file: FileId::new(0),
1973 };
1974
1975 let witness = super::SymbolResolutionWitness {
1976 normalized_query: None,
1977 outcome: super::SymbolResolutionOutcome::NotFound,
1978 selected_bucket: None,
1979 candidates: Vec::new(),
1980 symbol: None,
1981 steps: vec![step.clone()],
1982 };
1983 let expected = super::SymbolResolutionWitness {
1984 normalized_query: None,
1985 outcome: super::SymbolResolutionOutcome::NotFound,
1986 selected_bucket: None,
1987 candidates: Vec::new(),
1988 symbol: None,
1989 steps: vec![step],
1990 };
1991 assert_eq!(witness, expected);
1992 }
1993
1994 #[test]
1996 fn p2u06_witness_steps_field_clones_correctly() {
1997 use crate::graph::unified::bind::witness::step::ResolutionStep;
1998 use crate::graph::unified::node::id::NodeId;
1999
2000 let step = ResolutionStep::Chose {
2001 node: NodeId::new(99, 2),
2002 };
2003 let witness = super::SymbolResolutionWitness {
2004 normalized_query: None,
2005 outcome: super::SymbolResolutionOutcome::NotFound,
2006 selected_bucket: None,
2007 candidates: Vec::new(),
2008 symbol: None,
2009 steps: vec![step],
2010 };
2011 let cloned = witness.clone();
2012 assert_eq!(witness.steps.len(), 1);
2013 assert_eq!(cloned.steps.len(), 1);
2014 assert_eq!(witness, cloned);
2015 }
2016
2017 #[test]
2023 fn resolve_global_symbol_ambiguity_aware_returns_ambiguous_for_simple_name_collision() {
2024 let mut graph = CodeGraph::new();
2025 let file_path = abs_path("src/main.go");
2026
2027 let property_node = add_node(
2028 &mut graph,
2029 NodeKind::Property,
2030 "NeedTags",
2031 Some("main::SelectorSource::NeedTags"),
2032 &file_path,
2033 Some(Language::Go),
2034 4,
2035 6,
2036 );
2037 let variable_node = add_node(
2038 &mut graph,
2039 NodeKind::Variable,
2040 "NeedTags",
2041 Some("main::unrelated::NeedTags"),
2042 &file_path,
2043 Some(Language::Go),
2044 25,
2045 6,
2046 );
2047
2048 let snapshot = graph.snapshot();
2049 let result =
2050 snapshot.resolve_global_symbol_ambiguity_aware("NeedTags", super::FileScope::Any);
2051
2052 let err = result.expect_err("two same-name nodes must produce Ambiguous");
2053 let super::SymbolResolveError::Ambiguous(payload) = err else {
2054 panic!("expected Ambiguous variant, got {err:?}");
2055 };
2056 assert_eq!(payload.name, "NeedTags");
2057 assert!(!payload.truncated);
2058 assert_eq!(payload.candidates.len(), 2);
2059
2060 assert_eq!(
2062 payload.candidates[0].qualified_name,
2063 "main::SelectorSource::NeedTags"
2064 );
2065 assert_eq!(payload.candidates[0].kind, "property");
2066 assert_eq!(payload.candidates[0].start_line, 4);
2067 assert_eq!(payload.candidates[0].start_column, 6);
2068
2069 assert_eq!(
2070 payload.candidates[1].qualified_name,
2071 "main::unrelated::NeedTags"
2072 );
2073 assert_eq!(payload.candidates[1].kind, "variable");
2074 assert_eq!(payload.candidates[1].start_line, 25);
2075
2076 assert_ne!(property_node.node_id, variable_node.node_id);
2077 }
2078
2079 #[test]
2082 fn resolve_global_symbol_ambiguity_aware_resolves_qualified_name_uniquely() {
2083 let mut graph = CodeGraph::new();
2084 let file_path = abs_path("src/main.go");
2085
2086 let property_node = add_node(
2087 &mut graph,
2088 NodeKind::Property,
2089 "NeedTags",
2090 Some("main::SelectorSource::NeedTags"),
2091 &file_path,
2092 Some(Language::Go),
2093 4,
2094 6,
2095 );
2096 let _variable_node = add_node(
2097 &mut graph,
2098 NodeKind::Variable,
2099 "NeedTags",
2100 Some("main::unrelated::NeedTags"),
2101 &file_path,
2102 Some(Language::Go),
2103 25,
2104 6,
2105 );
2106
2107 let snapshot = graph.snapshot();
2108 let result = snapshot.resolve_global_symbol_ambiguity_aware(
2109 "main::SelectorSource::NeedTags",
2110 super::FileScope::Any,
2111 );
2112 assert_eq!(result, Ok(property_node.node_id));
2113 }
2114
2115 #[test]
2119 fn resolve_global_symbol_ambiguity_aware_normalizes_dot_delimiter() {
2120 let mut graph = CodeGraph::new();
2121 let file_path = abs_path("src/main.go");
2122
2123 let property_node = add_node(
2124 &mut graph,
2125 NodeKind::Property,
2126 "NeedTags",
2127 Some("main::SelectorSource::NeedTags"),
2128 &file_path,
2129 Some(Language::Go),
2130 4,
2131 6,
2132 );
2133 let _variable_node = add_node(
2134 &mut graph,
2135 NodeKind::Variable,
2136 "NeedTags",
2137 Some("main::unrelated::NeedTags"),
2138 &file_path,
2139 Some(Language::Go),
2140 25,
2141 6,
2142 );
2143
2144 let snapshot = graph.snapshot();
2145 let result = snapshot.resolve_global_symbol_ambiguity_aware(
2146 "main.SelectorSource.NeedTags",
2147 super::FileScope::Any,
2148 );
2149 assert_eq!(result, Ok(property_node.node_id));
2150 }
2151
2152 #[test]
2155 fn resolve_global_symbol_ambiguity_aware_returns_not_found_for_missing_symbol() {
2156 let graph = CodeGraph::new();
2157 let snapshot = graph.snapshot();
2158 let result =
2159 snapshot.resolve_global_symbol_ambiguity_aware("does_not_exist", super::FileScope::Any);
2160 assert_eq!(
2161 result,
2162 Err(super::SymbolResolveError::NotFound {
2163 name: "does_not_exist".to_string(),
2164 })
2165 );
2166 }
2167
2168 #[test]
2171 fn resolve_global_symbol_ambiguity_aware_caps_candidates_at_truncation_limit() {
2172 let mut graph = CodeGraph::new();
2173 let file_path = abs_path("src/main.go");
2174 let total = super::AMBIGUOUS_SYMBOL_CANDIDATE_CAP + 5;
2175
2176 for index in 0..total {
2177 let qualified = format!("pkg::module_{index:03}::collide");
2180 add_node(
2181 &mut graph,
2182 NodeKind::Function,
2183 "collide",
2184 Some(qualified.as_str()),
2185 &file_path,
2186 Some(Language::Go),
2187 u32::try_from(index + 1).unwrap_or(1),
2188 0,
2189 );
2190 }
2191
2192 let snapshot = graph.snapshot();
2193 let err = snapshot
2194 .resolve_global_symbol_ambiguity_aware("collide", super::FileScope::Any)
2195 .expect_err("collisions across many nodes must surface as Ambiguous");
2196 let super::SymbolResolveError::Ambiguous(payload) = err else {
2197 panic!("expected Ambiguous variant");
2198 };
2199
2200 assert!(payload.truncated, "truncated flag must be set above cap");
2201 assert_eq!(
2202 payload.candidates.len(),
2203 super::AMBIGUOUS_SYMBOL_CANDIDATE_CAP
2204 );
2205 assert_eq!(
2207 payload.candidates[0].qualified_name,
2208 "pkg::module_000::collide"
2209 );
2210 }
2211
2212 #[test]
2215 fn resolve_global_symbol_ambiguity_aware_respects_file_scope() {
2216 let mut graph = CodeGraph::new();
2217 let scope_file = abs_path("src/in_scope.go");
2218 let other_file = abs_path("src/other.go");
2219
2220 let scoped_property = add_node(
2221 &mut graph,
2222 NodeKind::Property,
2223 "Same",
2224 Some("main::Owner::Same"),
2225 &scope_file,
2226 Some(Language::Go),
2227 10,
2228 6,
2229 );
2230 let _outside = add_node(
2231 &mut graph,
2232 NodeKind::Property,
2233 "Same",
2234 Some("main::Other::Same"),
2235 &other_file,
2236 Some(Language::Go),
2237 10,
2238 6,
2239 );
2240
2241 let snapshot = graph.snapshot();
2242 let result = snapshot
2243 .resolve_global_symbol_ambiguity_aware("Same", super::FileScope::Path(&scope_file));
2244 assert_eq!(result, Ok(scoped_property.node_id));
2245 }
2246}