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 fn candidate_sort_key(&self, node_id: NodeId) -> CandidateSortKey {
413 let Some(entry) = self.get_node(node_id) else {
414 return CandidateSortKey::default_for(node_id);
415 };
416
417 let file_path = self
418 .files()
419 .resolve(entry.file)
420 .map_or_else(String::new, |path| path.to_string_lossy().into_owned());
421 let qualified_name = entry
422 .qualified_name
423 .and_then(|string_id| self.strings().resolve(string_id))
424 .map_or_else(String::new, |value| value.to_string());
425 let simple_name = self
426 .strings()
427 .resolve(entry.name)
428 .map_or_else(String::new, |value| value.to_string());
429
430 CandidateSortKey {
431 file_path,
432 start_line: entry.start_line,
433 start_column: entry.start_column,
434 end_line: entry.end_line,
435 end_column: entry.end_column,
436 kind: entry.kind.as_str().to_string(),
437 qualified_name,
438 simple_name,
439 node_id,
440 }
441 }
442}
443
444#[derive(Debug, Clone, PartialEq, Eq)]
446pub struct SymbolCandidateSearchWitness {
447 pub normalized_query: Option<NormalizedSymbolQuery>,
449 pub outcome: SymbolCandidateOutcome,
451 pub selected_bucket: Option<SymbolCandidateBucket>,
453 pub candidates: Vec<SymbolCandidateWitness>,
455}
456
457#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
458struct CandidateSortKey {
459 file_path: String,
460 start_line: u32,
461 start_column: u32,
462 end_line: u32,
463 end_column: u32,
464 kind: String,
465 qualified_name: String,
466 simple_name: String,
467 node_id: NodeId,
468}
469
470impl CandidateSortKey {
471 fn default_for(node_id: NodeId) -> Self {
472 Self {
473 file_path: String::new(),
474 start_line: 0,
475 start_column: 0,
476 end_line: 0,
477 end_column: 0,
478 kind: String::new(),
479 qualified_name: String::new(),
480 simple_name: String::new(),
481 node_id,
482 }
483 }
484}
485
486#[must_use]
488pub fn canonicalize_graph_qualified_name(language: Language, symbol: &str) -> String {
489 if should_skip_qualified_name_normalization(symbol) {
490 return symbol.to_string();
491 }
492
493 if language == Language::R {
494 return canonicalize_r_qualified_name(symbol);
495 }
496
497 let mut normalized = symbol.to_string();
498 for delimiter in native_delimiters(language) {
499 if normalized.contains(delimiter) {
500 normalized = normalized.replace(delimiter, "::");
501 }
502 }
503 normalized
504}
505
506#[must_use]
508pub(crate) fn is_canonical_graph_qualified_name(language: Language, symbol: &str) -> bool {
509 should_skip_qualified_name_normalization(symbol)
510 || canonicalize_graph_qualified_name(language, symbol) == symbol
511}
512
513fn should_skip_qualified_name_normalization(symbol: &str) -> bool {
514 symbol.starts_with('<')
515 || symbol.contains('/')
516 || symbol.starts_with("wasm::")
517 || symbol.starts_with("ffi::")
518 || symbol.starts_with("extern::")
519 || symbol.starts_with("native::")
520}
521
522fn canonicalize_r_qualified_name(symbol: &str) -> String {
523 let search_start = usize::from(symbol.starts_with('.'));
524 let Some(relative_split_index) = symbol[search_start..].rfind('.') else {
525 return symbol.to_string();
526 };
527
528 let split_index = search_start + relative_split_index;
529 let prefix = &symbol[..split_index];
530 let suffix = &symbol[split_index + 1..];
531 if suffix.is_empty() {
532 return symbol.to_string();
533 }
534
535 format!("{prefix}::{suffix}")
536}
537
538#[must_use]
540pub fn display_graph_qualified_name(
541 language: Language,
542 qualified: &str,
543 kind: NodeKind,
544 is_static: bool,
545) -> String {
546 if should_skip_qualified_name_normalization(qualified) {
547 return qualified.to_string();
548 }
549
550 match language {
551 Language::Ruby => display_ruby_qualified_name(qualified, kind, is_static),
552 Language::Php => display_php_qualified_name(qualified, kind),
553 _ => native_display_separator(language).map_or_else(
554 || qualified.to_string(),
555 |separator| qualified.replace("::", separator),
556 ),
557 }
558}
559
560pub(crate) fn native_delimiters(language: Language) -> &'static [&'static str] {
561 match language {
562 Language::JavaScript
563 | Language::Python
564 | Language::TypeScript
565 | Language::Java
566 | Language::CSharp
567 | Language::Kotlin
568 | Language::Scala
569 | Language::Go
570 | Language::Css
571 | Language::Sql
572 | Language::Dart
573 | Language::Lua
574 | Language::Perl
575 | Language::Groovy
576 | Language::Elixir
577 | Language::R
578 | Language::Haskell
579 | Language::Html
580 | Language::Svelte
581 | Language::Vue
582 | Language::Terraform
583 | Language::Puppet
584 | Language::Pulumi
585 | Language::Http
586 | Language::Plsql
587 | Language::Apex
588 | Language::Abap
589 | Language::ServiceNow
590 | Language::Swift
591 | Language::Zig
592 | Language::Json => &["."],
593 Language::Ruby => &["#", "."],
594 Language::Php => &["\\", "->"],
595 Language::C | Language::Cpp | Language::Rust | Language::Shell => &[],
596 }
597}
598
599fn native_display_separator(language: Language) -> Option<&'static str> {
600 match language {
601 Language::C
602 | Language::Cpp
603 | Language::Rust
604 | Language::Shell
605 | Language::Php
606 | Language::Ruby => None,
607 _ => Some("."),
608 }
609}
610
611fn display_ruby_qualified_name(qualified: &str, kind: NodeKind, is_static: bool) -> String {
612 if qualified.contains('#') || qualified.contains('.') || !qualified.contains("::") {
613 return qualified.to_string();
614 }
615
616 match kind {
617 NodeKind::Method => {
618 replace_last_separator(qualified, if is_static { "." } else { "#" }, false)
619 }
620 NodeKind::Variable if should_display_ruby_member_variable(qualified) => {
621 replace_last_separator(qualified, "#", false)
622 }
623 _ => qualified.to_string(),
624 }
625}
626
627fn should_display_ruby_member_variable(qualified: &str) -> bool {
628 let Some((_, suffix)) = qualified.rsplit_once("::") else {
629 return false;
630 };
631
632 if suffix.starts_with("@@")
633 || suffix
634 .chars()
635 .next()
636 .is_some_and(|character| character.is_ascii_uppercase())
637 {
638 return false;
639 }
640
641 suffix.starts_with('@')
642 || suffix
643 .chars()
644 .next()
645 .is_some_and(|character| character.is_ascii_lowercase() || character == '_')
646}
647
648fn display_php_qualified_name(qualified: &str, kind: NodeKind) -> String {
649 if !qualified.contains("::") {
650 return qualified.to_string();
651 }
652
653 if matches!(kind, NodeKind::Method | NodeKind::Property) {
654 return replace_last_separator(qualified, "::", true);
655 }
656
657 qualified.replace("::", "\\")
658}
659
660fn replace_last_separator(qualified: &str, final_separator: &str, preserve_prefix: bool) -> String {
661 let Some((prefix, suffix)) = qualified.rsplit_once("::") else {
662 return qualified.to_string();
663 };
664
665 let display_prefix = if preserve_prefix {
666 prefix.replace("::", "\\")
667 } else {
668 prefix.to_string()
669 };
670
671 if display_prefix.is_empty() {
672 suffix.to_string()
673 } else {
674 format!("{display_prefix}{final_separator}{suffix}")
675 }
676}
677
678#[cfg(test)]
679mod tests {
680 use std::path::{Path, PathBuf};
681
682 use crate::graph::node::Language;
683 use crate::graph::unified::concurrent::CodeGraph;
684 use crate::graph::unified::node::id::NodeId;
685 use crate::graph::unified::node::kind::NodeKind;
686 use crate::graph::unified::storage::arena::NodeEntry;
687
688 use super::{
689 FileScope, NormalizedSymbolQuery, ResolutionMode, ResolvedFileScope, SymbolCandidateBucket,
690 SymbolCandidateOutcome, SymbolQuery, SymbolResolutionOutcome,
691 canonicalize_graph_qualified_name, display_graph_qualified_name,
692 };
693
694 struct TestNode {
695 node_id: NodeId,
696 }
697
698 #[test]
699 fn test_resolve_symbol_exact_qualified_same_file() {
700 let mut graph = CodeGraph::new();
701 let file_path = abs_path("src/lib.rs");
702 let symbol = add_node(
703 &mut graph,
704 NodeKind::Function,
705 "target",
706 Some("pkg::target"),
707 &file_path,
708 Some(Language::Rust),
709 10,
710 2,
711 );
712
713 let snapshot = graph.snapshot();
714 let query = SymbolQuery {
715 symbol: "pkg::target",
716 file_scope: FileScope::Path(&file_path),
717 mode: ResolutionMode::Strict,
718 };
719
720 assert_eq!(
721 snapshot.resolve_symbol(&query),
722 SymbolResolutionOutcome::Resolved(symbol.node_id)
723 );
724 }
725
726 #[test]
727 fn test_resolve_symbol_exact_simple_same_file_wins() {
728 let mut graph = CodeGraph::new();
729 let requested_path = abs_path("src/requested.rs");
730 let other_path = abs_path("src/other.rs");
731
732 let requested = add_node(
733 &mut graph,
734 NodeKind::Function,
735 "target",
736 Some("requested::target"),
737 &requested_path,
738 Some(Language::Rust),
739 4,
740 0,
741 );
742 let _other = add_node(
743 &mut graph,
744 NodeKind::Function,
745 "target",
746 Some("other::target"),
747 &other_path,
748 Some(Language::Rust),
749 1,
750 0,
751 );
752
753 let snapshot = graph.snapshot();
754 let query = SymbolQuery {
755 symbol: "target",
756 file_scope: FileScope::Path(&requested_path),
757 mode: ResolutionMode::Strict,
758 };
759
760 assert_eq!(
761 snapshot.resolve_symbol(&query),
762 SymbolResolutionOutcome::Resolved(requested.node_id)
763 );
764 }
765
766 #[test]
767 fn test_resolve_symbol_returns_not_found_without_wrong_file_fallback() {
768 let mut graph = CodeGraph::new();
769 let requested_path = abs_path("src/requested.rs");
770 let other_path = abs_path("src/other.rs");
771
772 let _requested_index_anchor = add_node(
773 &mut graph,
774 NodeKind::Function,
775 "anchor",
776 Some("requested::anchor"),
777 &requested_path,
778 Some(Language::Rust),
779 1,
780 0,
781 );
782 let _other = add_node(
783 &mut graph,
784 NodeKind::Function,
785 "target",
786 Some("other::target"),
787 &other_path,
788 Some(Language::Rust),
789 3,
790 0,
791 );
792
793 let snapshot = graph.snapshot();
794 let query = SymbolQuery {
795 symbol: "target",
796 file_scope: FileScope::Path(&requested_path),
797 mode: ResolutionMode::Strict,
798 };
799
800 assert_eq!(
801 snapshot.resolve_symbol(&query),
802 SymbolResolutionOutcome::NotFound
803 );
804 }
805
806 #[test]
807 fn test_resolve_symbol_returns_file_not_indexed_for_valid_unindexed_path() {
808 let mut graph = CodeGraph::new();
809 let indexed_path = abs_path("src/indexed.rs");
810 let unindexed_path = abs_path("src/unindexed.rs");
811
812 add_node(
813 &mut graph,
814 NodeKind::Function,
815 "indexed",
816 Some("pkg::indexed"),
817 &indexed_path,
818 Some(Language::Rust),
819 1,
820 0,
821 );
822 graph
823 .files_mut()
824 .register_with_language(&unindexed_path, Some(Language::Rust))
825 .unwrap();
826
827 let snapshot = graph.snapshot();
828 let query = SymbolQuery {
829 symbol: "indexed",
830 file_scope: FileScope::Path(&unindexed_path),
831 mode: ResolutionMode::Strict,
832 };
833
834 assert_eq!(
835 snapshot.resolve_symbol(&query),
836 SymbolResolutionOutcome::FileNotIndexed
837 );
838 }
839
840 #[test]
841 fn test_resolve_symbol_returns_ambiguous_for_multi_match_bucket() {
842 let mut graph = CodeGraph::new();
843 let file_path = abs_path("src/lib.rs");
844
845 let first = add_node(
846 &mut graph,
847 NodeKind::Function,
848 "dup",
849 Some("pkg::dup"),
850 &file_path,
851 Some(Language::Rust),
852 2,
853 0,
854 );
855 let second = add_node(
856 &mut graph,
857 NodeKind::Method,
858 "dup",
859 Some("pkg::dup_method"),
860 &file_path,
861 Some(Language::Rust),
862 8,
863 0,
864 );
865
866 let snapshot = graph.snapshot();
867 let query = SymbolQuery {
868 symbol: "dup",
869 file_scope: FileScope::Path(&file_path),
870 mode: ResolutionMode::Strict,
871 };
872
873 assert_eq!(
874 snapshot.resolve_symbol(&query),
875 SymbolResolutionOutcome::Ambiguous(vec![first.node_id, second.node_id])
876 );
877 }
878
879 #[test]
880 fn test_find_symbol_candidates_uses_first_non_empty_bucket_only() {
881 let mut graph = CodeGraph::new();
882 let qualified_path = abs_path("src/qualified.rs");
883 let simple_path = abs_path("src/simple.rs");
884
885 let qualified = add_node(
886 &mut graph,
887 NodeKind::Function,
888 "target",
889 Some("pkg::target"),
890 &qualified_path,
891 Some(Language::Rust),
892 1,
893 0,
894 );
895 let simple_only = add_node(
896 &mut graph,
897 NodeKind::Function,
898 "pkg::target",
899 None,
900 &simple_path,
901 Some(Language::Rust),
902 1,
903 0,
904 );
905
906 let snapshot = graph.snapshot();
907 let query = SymbolQuery {
908 symbol: "pkg::target",
909 file_scope: FileScope::Any,
910 mode: ResolutionMode::AllowSuffixCandidates,
911 };
912
913 assert_eq!(
914 snapshot.find_symbol_candidates(&query),
915 SymbolCandidateOutcome::Candidates(vec![qualified.node_id])
916 );
917 assert_ne!(qualified.node_id, simple_only.node_id);
918 }
919
920 #[test]
921 fn test_find_symbol_candidates_with_witness_reports_exact_qualified_bucket() {
922 let mut graph = CodeGraph::new();
923 let qualified_path = abs_path("src/qualified.rs");
924 let simple_path = abs_path("src/simple.rs");
925
926 let qualified = add_node(
927 &mut graph,
928 NodeKind::Function,
929 "target",
930 Some("pkg::target"),
931 &qualified_path,
932 Some(Language::Rust),
933 1,
934 0,
935 );
936 let _simple_only = add_node(
937 &mut graph,
938 NodeKind::Function,
939 "pkg::target",
940 None,
941 &simple_path,
942 Some(Language::Rust),
943 1,
944 0,
945 );
946
947 let snapshot = graph.snapshot();
948 let query = SymbolQuery {
949 symbol: "pkg::target",
950 file_scope: FileScope::Any,
951 mode: ResolutionMode::AllowSuffixCandidates,
952 };
953
954 let witness = snapshot.find_symbol_candidates_with_witness(&query);
955
956 assert_eq!(
957 witness.outcome,
958 SymbolCandidateOutcome::Candidates(vec![qualified.node_id])
959 );
960 assert_eq!(
961 witness.selected_bucket,
962 Some(SymbolCandidateBucket::ExactQualified)
963 );
964 assert_eq!(
965 witness.candidates,
966 vec![super::SymbolCandidateWitness {
967 node_id: qualified.node_id,
968 bucket: SymbolCandidateBucket::ExactQualified,
969 }]
970 );
971 assert_eq!(
972 witness.normalized_query,
973 Some(NormalizedSymbolQuery {
974 symbol: "pkg::target".to_string(),
975 file_scope: ResolvedFileScope::Any,
976 mode: ResolutionMode::AllowSuffixCandidates,
977 })
978 );
979 }
980
981 #[test]
982 fn test_find_symbol_candidates_preserves_file_not_indexed() {
983 let mut graph = CodeGraph::new();
984 let indexed_path = abs_path("src/indexed.rs");
985 let unindexed_path = abs_path("src/unindexed.rs");
986
987 add_node(
988 &mut graph,
989 NodeKind::Function,
990 "target",
991 Some("pkg::target"),
992 &indexed_path,
993 Some(Language::Rust),
994 1,
995 0,
996 );
997 let unindexed_file_id = graph
998 .files_mut()
999 .register_with_language(&unindexed_path, Some(Language::Rust))
1000 .unwrap();
1001
1002 let snapshot = graph.snapshot();
1003 let query = SymbolQuery {
1004 symbol: "target",
1005 file_scope: FileScope::FileId(unindexed_file_id),
1006 mode: ResolutionMode::AllowSuffixCandidates,
1007 };
1008
1009 assert_eq!(
1010 snapshot.find_symbol_candidates(&query),
1011 SymbolCandidateOutcome::FileNotIndexed
1012 );
1013 }
1014
1015 #[test]
1016 fn test_resolve_symbol_with_witness_reports_ambiguous_bucket_candidates() {
1017 let mut graph = CodeGraph::new();
1018 let file_path = abs_path("src/lib.rs");
1019
1020 let first = add_node(
1021 &mut graph,
1022 NodeKind::Function,
1023 "dup",
1024 Some("pkg::dup"),
1025 &file_path,
1026 Some(Language::Rust),
1027 2,
1028 0,
1029 );
1030 let second = add_node(
1031 &mut graph,
1032 NodeKind::Method,
1033 "dup",
1034 Some("pkg::dup_method"),
1035 &file_path,
1036 Some(Language::Rust),
1037 8,
1038 0,
1039 );
1040
1041 let snapshot = graph.snapshot();
1042 let query = SymbolQuery {
1043 symbol: "dup",
1044 file_scope: FileScope::Path(&file_path),
1045 mode: ResolutionMode::Strict,
1046 };
1047
1048 let witness = snapshot.resolve_symbol_with_witness(&query);
1049
1050 assert_eq!(
1051 witness.outcome,
1052 SymbolResolutionOutcome::Ambiguous(vec![first.node_id, second.node_id])
1053 );
1054 assert_eq!(
1055 witness.selected_bucket,
1056 Some(SymbolCandidateBucket::ExactSimple)
1057 );
1058 assert_eq!(
1059 witness.candidates,
1060 vec![
1061 super::SymbolCandidateWitness {
1062 node_id: first.node_id,
1063 bucket: SymbolCandidateBucket::ExactSimple,
1064 },
1065 super::SymbolCandidateWitness {
1066 node_id: second.node_id,
1067 bucket: SymbolCandidateBucket::ExactSimple,
1068 },
1069 ]
1070 );
1071 }
1072
1073 #[test]
1074 fn test_suffix_candidates_disabled_in_strict_mode() {
1075 let mut graph = CodeGraph::new();
1076 let file_path = abs_path("src/lib.rs");
1077
1078 let suffix_match = add_node(
1079 &mut graph,
1080 NodeKind::Function,
1081 "target",
1082 Some("outer::pkg::target"),
1083 &file_path,
1084 Some(Language::Rust),
1085 1,
1086 0,
1087 );
1088
1089 let snapshot = graph.snapshot();
1090 let strict_query = SymbolQuery {
1091 symbol: "pkg::target",
1092 file_scope: FileScope::Any,
1093 mode: ResolutionMode::Strict,
1094 };
1095 let suffix_query = SymbolQuery {
1096 mode: ResolutionMode::AllowSuffixCandidates,
1097 ..strict_query
1098 };
1099
1100 assert_eq!(
1101 snapshot.resolve_symbol(&strict_query),
1102 SymbolResolutionOutcome::NotFound
1103 );
1104 assert_eq!(
1105 snapshot.find_symbol_candidates(&suffix_query),
1106 SymbolCandidateOutcome::Candidates(vec![suffix_match.node_id])
1107 );
1108 }
1109
1110 #[test]
1111 fn test_suffix_candidates_require_canonical_qualified_query() {
1112 let mut graph = CodeGraph::new();
1113 let file_path = abs_path("src/mod.py");
1114
1115 add_node(
1116 &mut graph,
1117 NodeKind::Function,
1118 "target",
1119 Some("pkg::target"),
1120 &file_path,
1121 Some(Language::Python),
1122 1,
1123 0,
1124 );
1125
1126 let snapshot = graph.snapshot();
1127 let query = SymbolQuery {
1128 symbol: "pkg.target",
1129 file_scope: FileScope::Any,
1130 mode: ResolutionMode::AllowSuffixCandidates,
1131 };
1132
1133 assert_eq!(
1134 snapshot.find_symbol_candidates(&query),
1135 SymbolCandidateOutcome::NotFound
1136 );
1137 }
1138
1139 #[test]
1140 fn test_suffix_candidates_filter_same_leaf_bucket_only() {
1141 let mut graph = CodeGraph::new();
1142 let file_path = abs_path("src/lib.rs");
1143
1144 let exact_suffix = add_node(
1145 &mut graph,
1146 NodeKind::Function,
1147 "target",
1148 Some("outer::pkg::target"),
1149 &file_path,
1150 Some(Language::Rust),
1151 2,
1152 0,
1153 );
1154 let another_suffix = add_node(
1155 &mut graph,
1156 NodeKind::Method,
1157 "target",
1158 Some("another::pkg::target"),
1159 &file_path,
1160 Some(Language::Rust),
1161 4,
1162 0,
1163 );
1164 let unrelated = add_node(
1165 &mut graph,
1166 NodeKind::Function,
1167 "target",
1168 Some("pkg::different::target"),
1169 &file_path,
1170 Some(Language::Rust),
1171 6,
1172 0,
1173 );
1174
1175 let snapshot = graph.snapshot();
1176 let query = SymbolQuery {
1177 symbol: "pkg::target",
1178 file_scope: FileScope::Any,
1179 mode: ResolutionMode::AllowSuffixCandidates,
1180 };
1181
1182 assert_eq!(
1183 snapshot.find_symbol_candidates(&query),
1184 SymbolCandidateOutcome::Candidates(vec![exact_suffix.node_id, another_suffix.node_id])
1185 );
1186 assert_ne!(unrelated.node_id, exact_suffix.node_id);
1187 }
1188
1189 #[test]
1190 fn test_normalize_symbol_query_rewrites_native_delimiter_when_file_scope_language_known() {
1191 let mut graph = CodeGraph::new();
1192 let file_path = abs_path("src/mod.py");
1193 let file_id = graph
1194 .files_mut()
1195 .register_with_language(&file_path, Some(Language::Python))
1196 .unwrap();
1197 let snapshot = graph.snapshot();
1198 let query = SymbolQuery {
1199 symbol: "pkg.mod.fn",
1200 file_scope: FileScope::Path(&file_path),
1201 mode: ResolutionMode::Strict,
1202 };
1203
1204 let normalized = snapshot.normalize_symbol_query(&query, &ResolvedFileScope::File(file_id));
1205
1206 assert_eq!(
1207 normalized,
1208 NormalizedSymbolQuery {
1209 symbol: "pkg::mod::fn".to_string(),
1210 file_scope: ResolvedFileScope::File(file_id),
1211 mode: ResolutionMode::Strict,
1212 }
1213 );
1214 }
1215
1216 #[test]
1217 fn test_normalize_symbol_query_rewrites_native_delimiter_for_csharp() {
1218 let mut graph = CodeGraph::new();
1219 let file_path = abs_path("src/Program.cs");
1220 let file_id = graph
1221 .files_mut()
1222 .register_with_language(&file_path, Some(Language::CSharp))
1223 .unwrap();
1224 let snapshot = graph.snapshot();
1225 let query = SymbolQuery {
1226 symbol: "System.Console.WriteLine",
1227 file_scope: FileScope::Path(&file_path),
1228 mode: ResolutionMode::Strict,
1229 };
1230
1231 let normalized = snapshot.normalize_symbol_query(&query, &ResolvedFileScope::File(file_id));
1232
1233 assert_eq!(normalized.symbol, "System::Console::WriteLine".to_string());
1234 }
1235
1236 #[test]
1237 fn test_normalize_symbol_query_rewrites_native_delimiter_for_zig() {
1238 let mut graph = CodeGraph::new();
1239 let file_path = abs_path("src/main.zig");
1240 let file_id = graph
1241 .files_mut()
1242 .register_with_language(&file_path, Some(Language::Zig))
1243 .unwrap();
1244 let snapshot = graph.snapshot();
1245 let query = SymbolQuery {
1246 symbol: "std.os.linux.exit",
1247 file_scope: FileScope::Path(&file_path),
1248 mode: ResolutionMode::Strict,
1249 };
1250
1251 let normalized = snapshot.normalize_symbol_query(&query, &ResolvedFileScope::File(file_id));
1252
1253 assert_eq!(normalized.symbol, "std::os::linux::exit".to_string());
1254 }
1255
1256 #[test]
1257 fn test_normalize_symbol_query_does_not_rewrite_when_file_scope_any() {
1258 let graph = CodeGraph::new();
1259 let snapshot = graph.snapshot();
1260 let query = SymbolQuery {
1261 symbol: "pkg.mod.fn",
1262 file_scope: FileScope::Any,
1263 mode: ResolutionMode::Strict,
1264 };
1265
1266 let normalized = snapshot.normalize_symbol_query(&query, &ResolvedFileScope::Any);
1267
1268 assert_eq!(
1269 normalized,
1270 NormalizedSymbolQuery {
1271 symbol: "pkg.mod.fn".to_string(),
1272 file_scope: ResolvedFileScope::Any,
1273 mode: ResolutionMode::Strict,
1274 }
1275 );
1276 }
1277
1278 #[test]
1279 fn test_global_qualified_query_with_native_delimiter_is_exact_only_and_not_found() {
1280 let mut graph = CodeGraph::new();
1281 let file_path = abs_path("src/mod.py");
1282
1283 add_node(
1284 &mut graph,
1285 NodeKind::Function,
1286 "fn",
1287 Some("pkg::mod::fn"),
1288 &file_path,
1289 Some(Language::Python),
1290 1,
1291 0,
1292 );
1293
1294 let snapshot = graph.snapshot();
1295 let query = SymbolQuery {
1296 symbol: "pkg.mod.fn",
1297 file_scope: FileScope::Any,
1298 mode: ResolutionMode::AllowSuffixCandidates,
1299 };
1300
1301 assert_eq!(
1302 snapshot.resolve_symbol(&query),
1303 SymbolResolutionOutcome::NotFound
1304 );
1305 }
1306
1307 #[test]
1308 fn test_global_canonical_qualified_query_can_hit_exact_qualified_bucket() {
1309 let mut graph = CodeGraph::new();
1310 let file_path = abs_path("src/lib.rs");
1311 let expected = add_node(
1312 &mut graph,
1313 NodeKind::Function,
1314 "fn",
1315 Some("pkg::mod::fn"),
1316 &file_path,
1317 Some(Language::Rust),
1318 1,
1319 0,
1320 );
1321
1322 let snapshot = graph.snapshot();
1323 let query = SymbolQuery {
1324 symbol: "pkg::mod::fn",
1325 file_scope: FileScope::Any,
1326 mode: ResolutionMode::Strict,
1327 };
1328
1329 assert_eq!(
1330 snapshot.resolve_symbol(&query),
1331 SymbolResolutionOutcome::Resolved(expected.node_id)
1332 );
1333 }
1334
1335 #[test]
1336 fn test_candidate_order_uses_metadata_then_node_id() {
1337 let mut graph = CodeGraph::new();
1338 let file_path = abs_path("src/lib.rs");
1339
1340 let first = add_node(
1341 &mut graph,
1342 NodeKind::Function,
1343 "dup",
1344 Some("pkg::dup_a"),
1345 &file_path,
1346 Some(Language::Rust),
1347 1,
1348 0,
1349 );
1350 let second = add_node(
1351 &mut graph,
1352 NodeKind::Function,
1353 "dup",
1354 Some("pkg::dup_b"),
1355 &file_path,
1356 Some(Language::Rust),
1357 1,
1358 0,
1359 );
1360
1361 let snapshot = graph.snapshot();
1362 let query = SymbolQuery {
1363 symbol: "dup",
1364 file_scope: FileScope::Any,
1365 mode: ResolutionMode::Strict,
1366 };
1367
1368 assert_eq!(
1369 snapshot.find_symbol_candidates(&query),
1370 SymbolCandidateOutcome::Candidates(vec![first.node_id, second.node_id])
1371 );
1372 }
1373
1374 #[test]
1375 fn test_candidate_order_kind_sort_key_uses_node_kind_as_str() {
1376 let mut graph = CodeGraph::new();
1377 let file_path = abs_path("src/lib.rs");
1378
1379 let function_node = add_node(
1380 &mut graph,
1381 NodeKind::Function,
1382 "shared",
1383 Some("pkg::shared_fn"),
1384 &file_path,
1385 Some(Language::Rust),
1386 1,
1387 0,
1388 );
1389 let variable_node = add_node(
1390 &mut graph,
1391 NodeKind::Variable,
1392 "shared",
1393 Some("pkg::shared_var"),
1394 &file_path,
1395 Some(Language::Rust),
1396 1,
1397 0,
1398 );
1399
1400 let snapshot = graph.snapshot();
1401 let query = SymbolQuery {
1402 symbol: "shared",
1403 file_scope: FileScope::Any,
1404 mode: ResolutionMode::Strict,
1405 };
1406
1407 assert_eq!(
1408 snapshot.find_symbol_candidates(&query),
1409 SymbolCandidateOutcome::Candidates(vec![function_node.node_id, variable_node.node_id])
1410 );
1411 }
1412
1413 fn add_node(
1414 graph: &mut CodeGraph,
1415 kind: NodeKind,
1416 name: &str,
1417 qualified_name: Option<&str>,
1418 file_path: &Path,
1419 language: Option<Language>,
1420 start_line: u32,
1421 start_column: u32,
1422 ) -> TestNode {
1423 let name_id = graph.strings_mut().intern(name).unwrap();
1424 let qualified_name_id =
1425 qualified_name.map(|value| graph.strings_mut().intern(value).unwrap());
1426 let file_id = graph
1427 .files_mut()
1428 .register_with_language(file_path, language)
1429 .unwrap();
1430
1431 let entry = NodeEntry::new(kind, name_id, file_id)
1432 .with_qualified_name_opt(qualified_name_id)
1433 .with_location(start_line, start_column, start_line, start_column + 1);
1434
1435 let node_id = graph.nodes_mut().alloc(entry).unwrap();
1436 graph
1437 .indices_mut()
1438 .add(node_id, kind, name_id, qualified_name_id, file_id);
1439
1440 TestNode { node_id }
1441 }
1442
1443 trait NodeEntryExt {
1444 fn with_qualified_name_opt(
1445 self,
1446 qualified_name: Option<crate::graph::unified::string::id::StringId>,
1447 ) -> Self;
1448 }
1449
1450 impl NodeEntryExt for NodeEntry {
1451 fn with_qualified_name_opt(
1452 mut self,
1453 qualified_name: Option<crate::graph::unified::string::id::StringId>,
1454 ) -> Self {
1455 self.qualified_name = qualified_name;
1456 self
1457 }
1458 }
1459
1460 fn abs_path(relative: &str) -> PathBuf {
1461 PathBuf::from("/resolver-tests").join(relative)
1462 }
1463
1464 #[test]
1465 fn test_display_graph_qualified_name_dot_language() {
1466 let display = display_graph_qualified_name(
1467 Language::CSharp,
1468 "MyApp::User::GetName",
1469 NodeKind::Method,
1470 false,
1471 );
1472 assert_eq!(display, "MyApp.User.GetName");
1473 }
1474
1475 #[test]
1476 fn test_canonicalize_graph_qualified_name_r_private_name_preserved() {
1477 assert_eq!(
1478 canonicalize_graph_qualified_name(Language::R, ".private_func"),
1479 ".private_func"
1480 );
1481 }
1482
1483 #[test]
1484 fn test_canonicalize_graph_qualified_name_r_s3_method_uses_last_dot() {
1485 assert_eq!(
1486 canonicalize_graph_qualified_name(Language::R, "as.data.frame.myclass"),
1487 "as.data.frame::myclass"
1488 );
1489 }
1490
1491 #[test]
1492 fn test_canonicalize_graph_qualified_name_r_leading_dot_s3_generic() {
1493 assert_eq!(
1494 canonicalize_graph_qualified_name(Language::R, ".DollarNames.myclass"),
1495 ".DollarNames::myclass"
1496 );
1497 }
1498
1499 #[test]
1500 fn test_display_graph_qualified_name_ruby_instance_method() {
1501 let display = display_graph_qualified_name(
1502 Language::Ruby,
1503 "Admin::Users::Controller::show",
1504 NodeKind::Method,
1505 false,
1506 );
1507 assert_eq!(display, "Admin::Users::Controller#show");
1508 }
1509
1510 #[test]
1511 fn test_display_graph_qualified_name_ruby_singleton_method() {
1512 let display = display_graph_qualified_name(
1513 Language::Ruby,
1514 "Admin::Users::Controller::show",
1515 NodeKind::Method,
1516 true,
1517 );
1518 assert_eq!(display, "Admin::Users::Controller.show");
1519 }
1520
1521 #[test]
1522 fn test_display_graph_qualified_name_ruby_member_variable() {
1523 let display = display_graph_qualified_name(
1524 Language::Ruby,
1525 "Admin::Users::Controller::username",
1526 NodeKind::Variable,
1527 false,
1528 );
1529 assert_eq!(display, "Admin::Users::Controller#username");
1530 }
1531
1532 #[test]
1533 fn test_display_graph_qualified_name_ruby_instance_variable() {
1534 let display = display_graph_qualified_name(
1535 Language::Ruby,
1536 "Admin::Users::Controller::@current_user",
1537 NodeKind::Variable,
1538 false,
1539 );
1540 assert_eq!(display, "Admin::Users::Controller#@current_user");
1541 }
1542
1543 #[test]
1544 fn test_display_graph_qualified_name_ruby_constant_stays_canonical() {
1545 let display = display_graph_qualified_name(
1546 Language::Ruby,
1547 "Admin::Users::Controller::DEFAULT_ROLE",
1548 NodeKind::Variable,
1549 false,
1550 );
1551 assert_eq!(display, "Admin::Users::Controller::DEFAULT_ROLE");
1552 }
1553
1554 #[test]
1555 fn test_display_graph_qualified_name_ruby_class_variable_stays_canonical() {
1556 let display = display_graph_qualified_name(
1557 Language::Ruby,
1558 "Admin::Users::Controller::@@count",
1559 NodeKind::Variable,
1560 false,
1561 );
1562 assert_eq!(display, "Admin::Users::Controller::@@count");
1563 }
1564
1565 #[test]
1566 fn test_display_graph_qualified_name_php_namespace_function() {
1567 let display = display_graph_qualified_name(
1568 Language::Php,
1569 "App::Services::send_mail",
1570 NodeKind::Function,
1571 false,
1572 );
1573 assert_eq!(display, "App\\Services\\send_mail");
1574 }
1575
1576 #[test]
1577 fn test_display_graph_qualified_name_php_method() {
1578 let display = display_graph_qualified_name(
1579 Language::Php,
1580 "App::Services::Mailer::deliver",
1581 NodeKind::Method,
1582 false,
1583 );
1584 assert_eq!(display, "App\\Services\\Mailer::deliver");
1585 }
1586
1587 #[test]
1588 fn test_display_graph_qualified_name_preserves_path_like_symbols() {
1589 let display = display_graph_qualified_name(
1590 Language::Go,
1591 "route::GET::/health",
1592 NodeKind::Endpoint,
1593 false,
1594 );
1595 assert_eq!(display, "route::GET::/health");
1596 }
1597
1598 #[test]
1599 fn test_display_graph_qualified_name_preserves_ffi_symbols() {
1600 let display = display_graph_qualified_name(
1601 Language::Haskell,
1602 "ffi::C::sin",
1603 NodeKind::Function,
1604 false,
1605 );
1606 assert_eq!(display, "ffi::C::sin");
1607 }
1608
1609 #[test]
1610 fn test_display_graph_qualified_name_preserves_native_cffi_symbols() {
1611 let display = display_graph_qualified_name(
1612 Language::Python,
1613 "native::cffi::calculate",
1614 NodeKind::Function,
1615 false,
1616 );
1617 assert_eq!(display, "native::cffi::calculate");
1618 }
1619
1620 #[test]
1621 fn test_display_graph_qualified_name_preserves_native_php_ffi_symbols() {
1622 let display = display_graph_qualified_name(
1623 Language::Php,
1624 "native::ffi::crypto_encrypt",
1625 NodeKind::Function,
1626 false,
1627 );
1628 assert_eq!(display, "native::ffi::crypto_encrypt");
1629 }
1630
1631 #[test]
1632 fn test_display_graph_qualified_name_preserves_native_panama_symbols() {
1633 let display = display_graph_qualified_name(
1634 Language::Java,
1635 "native::panama::nativeLinker",
1636 NodeKind::Function,
1637 false,
1638 );
1639 assert_eq!(display, "native::panama::nativeLinker");
1640 }
1641
1642 #[test]
1643 fn test_canonicalize_graph_qualified_name_preserves_wasm_symbols() {
1644 assert_eq!(
1645 canonicalize_graph_qualified_name(Language::TypeScript, "wasm::module.wasm"),
1646 "wasm::module.wasm"
1647 );
1648 }
1649
1650 #[test]
1651 fn test_canonicalize_graph_qualified_name_preserves_native_symbols() {
1652 assert_eq!(
1653 canonicalize_graph_qualified_name(Language::TypeScript, "native::binding.node"),
1654 "native::binding.node"
1655 );
1656 }
1657
1658 #[test]
1659 fn test_display_graph_qualified_name_preserves_wasm_symbols() {
1660 let display = display_graph_qualified_name(
1661 Language::TypeScript,
1662 "wasm::module.wasm",
1663 NodeKind::Module,
1664 false,
1665 );
1666 assert_eq!(display, "wasm::module.wasm");
1667 }
1668
1669 #[test]
1670 fn test_display_graph_qualified_name_preserves_native_symbols() {
1671 let display = display_graph_qualified_name(
1672 Language::TypeScript,
1673 "native::binding.node",
1674 NodeKind::Module,
1675 false,
1676 );
1677 assert_eq!(display, "native::binding.node");
1678 }
1679
1680 #[test]
1681 fn test_canonicalize_graph_qualified_name_still_normalizes_dot_language_symbols() {
1682 assert_eq!(
1683 canonicalize_graph_qualified_name(Language::TypeScript, "Foo.bar"),
1684 "Foo::bar"
1685 );
1686 }
1687
1688 #[test]
1693 fn p2u06_witness_steps_field_defaults_to_empty() {
1694 let mut graph = CodeGraph::new();
1695 let file_path = abs_path("src/lib.rs");
1696
1697 let symbol = add_node(
1698 &mut graph,
1699 NodeKind::Function,
1700 "my_fn",
1701 Some("pkg::my_fn"),
1702 &file_path,
1703 Some(Language::Rust),
1704 1,
1705 0,
1706 );
1707
1708 let snapshot = graph.snapshot();
1709 let query = SymbolQuery {
1710 symbol: "pkg::my_fn",
1711 file_scope: FileScope::Any,
1712 mode: ResolutionMode::Strict,
1713 };
1714
1715 let witness = snapshot.resolve_symbol_with_witness(&query);
1716 assert_eq!(
1717 witness.outcome,
1718 SymbolResolutionOutcome::Resolved(symbol.node_id)
1719 );
1720 assert!(
1721 witness.steps.is_empty(),
1722 "P2U06 initialises steps to Vec::new(); emission is deferred to P2U07"
1723 );
1724 }
1725
1726 #[test]
1729 fn p2u06_witness_steps_field_is_eq_compatible() {
1730 use crate::graph::unified::bind::witness::step::ResolutionStep;
1731 use crate::graph::unified::file::id::FileId;
1732
1733 let step = ResolutionStep::EnterFileScope {
1734 file: FileId::new(0),
1735 };
1736
1737 let witness = super::SymbolResolutionWitness {
1738 normalized_query: None,
1739 outcome: super::SymbolResolutionOutcome::NotFound,
1740 selected_bucket: None,
1741 candidates: Vec::new(),
1742 symbol: None,
1743 steps: vec![step.clone()],
1744 };
1745 let expected = super::SymbolResolutionWitness {
1746 normalized_query: None,
1747 outcome: super::SymbolResolutionOutcome::NotFound,
1748 selected_bucket: None,
1749 candidates: Vec::new(),
1750 symbol: None,
1751 steps: vec![step],
1752 };
1753 assert_eq!(witness, expected);
1754 }
1755
1756 #[test]
1758 fn p2u06_witness_steps_field_clones_correctly() {
1759 use crate::graph::unified::bind::witness::step::ResolutionStep;
1760 use crate::graph::unified::node::id::NodeId;
1761
1762 let step = ResolutionStep::Chose {
1763 node: NodeId::new(99, 2),
1764 };
1765 let witness = super::SymbolResolutionWitness {
1766 normalized_query: None,
1767 outcome: super::SymbolResolutionOutcome::NotFound,
1768 selected_bucket: None,
1769 candidates: Vec::new(),
1770 symbol: None,
1771 steps: vec![step],
1772 };
1773 let cloned = witness.clone();
1774 assert_eq!(witness.steps.len(), 1);
1775 assert_eq!(cloned.steps.len(), 1);
1776 assert_eq!(witness, cloned);
1777 }
1778}