1use std::path::Path;
7
8use crate::graph::node::Language;
9use crate::graph::unified::concurrent::GraphSnapshot;
10use crate::graph::unified::file::id::FileId;
11use crate::graph::unified::node::id::NodeId;
12use crate::graph::unified::node::kind::NodeKind;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum FileScope<'a> {
17 Any,
19 Path(&'a Path),
21 FileId(FileId),
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum ResolutionMode {
28 Strict,
30 AllowSuffixCandidates,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct SymbolQuery<'a> {
37 pub symbol: &'a str,
39 pub file_scope: FileScope<'a>,
41 pub mode: ResolutionMode,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum ResolvedFileScope {
48 Any,
50 File(FileId),
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum FileScopeError {
57 FileNotIndexed,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct NormalizedSymbolQuery {
64 pub symbol: String,
66 pub file_scope: ResolvedFileScope,
68 pub mode: ResolutionMode,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum SymbolCandidateBucket {
81 ExactQualified,
83 ExactSimple,
85 CanonicalSuffix,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct SymbolCandidateWitness {
92 pub node_id: NodeId,
94 pub bucket: SymbolCandidateBucket,
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
100pub enum SymbolResolutionOutcome {
101 Resolved(NodeId),
103 NotFound,
105 FileNotIndexed,
107 Ambiguous(Vec<NodeId>),
109}
110
111#[derive(Debug, Clone, PartialEq, Eq)]
113pub enum SymbolCandidateOutcome {
114 Candidates(Vec<NodeId>),
116 NotFound,
118 FileNotIndexed,
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
128pub struct SymbolResolutionWitness {
129 pub normalized_query: Option<NormalizedSymbolQuery>,
131 pub outcome: SymbolResolutionOutcome,
133 pub selected_bucket: Option<SymbolCandidateBucket>,
135 pub candidates: Vec<SymbolCandidateWitness>,
137}
138
139impl GraphSnapshot {
140 #[must_use]
142 pub fn resolve_symbol(&self, query: &SymbolQuery<'_>) -> SymbolResolutionOutcome {
143 self.resolve_symbol_with_witness(query).outcome
144 }
145
146 #[must_use]
148 pub fn find_symbol_candidates(&self, query: &SymbolQuery<'_>) -> SymbolCandidateOutcome {
149 self.find_symbol_candidates_with_witness(query).outcome
150 }
151
152 #[must_use]
155 pub fn find_symbol_candidates_with_witness(
156 &self,
157 query: &SymbolQuery<'_>,
158 ) -> SymbolCandidateSearchWitness {
159 let resolved_file_scope = match self.resolve_file_scope(&query.file_scope) {
160 Ok(scope) => scope,
161 Err(FileScopeError::FileNotIndexed) => {
162 return SymbolCandidateSearchWitness {
163 normalized_query: None,
164 outcome: SymbolCandidateOutcome::FileNotIndexed,
165 selected_bucket: None,
166 candidates: Vec::new(),
167 };
168 }
169 };
170
171 let normalized_query = self.normalize_symbol_query(query, &resolved_file_scope);
172
173 if let Some((selected_bucket, candidates)) =
174 self.first_candidate_bucket_with_witness(&normalized_query, resolved_file_scope)
175 {
176 return SymbolCandidateSearchWitness {
177 normalized_query: Some(normalized_query),
178 outcome: SymbolCandidateOutcome::Candidates(
179 candidates
180 .iter()
181 .map(|candidate| candidate.node_id)
182 .collect(),
183 ),
184 selected_bucket: Some(selected_bucket),
185 candidates,
186 };
187 }
188
189 SymbolCandidateSearchWitness {
190 normalized_query: Some(normalized_query),
191 outcome: SymbolCandidateOutcome::NotFound,
192 selected_bucket: None,
193 candidates: Vec::new(),
194 }
195 }
196
197 #[must_use]
200 pub fn resolve_symbol_with_witness(&self, query: &SymbolQuery<'_>) -> SymbolResolutionWitness {
201 let candidate_witness = self.find_symbol_candidates_with_witness(query);
202 let outcome = match &candidate_witness.outcome {
203 SymbolCandidateOutcome::Candidates(candidates) => match candidates.as_slice() {
204 [] => SymbolResolutionOutcome::NotFound,
205 [node_id] => SymbolResolutionOutcome::Resolved(*node_id),
206 _ => SymbolResolutionOutcome::Ambiguous(candidates.clone()),
207 },
208 SymbolCandidateOutcome::NotFound => SymbolResolutionOutcome::NotFound,
209 SymbolCandidateOutcome::FileNotIndexed => SymbolResolutionOutcome::FileNotIndexed,
210 };
211
212 SymbolResolutionWitness {
213 normalized_query: candidate_witness.normalized_query,
214 outcome,
215 selected_bucket: candidate_witness.selected_bucket,
216 candidates: candidate_witness.candidates,
217 }
218 }
219
220 pub fn resolve_file_scope(
227 &self,
228 file_scope: &FileScope<'_>,
229 ) -> Result<ResolvedFileScope, FileScopeError> {
230 match *file_scope {
231 FileScope::Any => Ok(ResolvedFileScope::Any),
232 FileScope::Path(path) => self
233 .files()
234 .get(path)
235 .filter(|file_id| !self.indices().by_file(*file_id).is_empty())
236 .map_or(Err(FileScopeError::FileNotIndexed), |file_id| {
237 Ok(ResolvedFileScope::File(file_id))
238 }),
239 FileScope::FileId(file_id) => {
240 let is_indexed = self.files().resolve(file_id).is_some()
241 && !self.indices().by_file(file_id).is_empty();
242 if is_indexed {
243 Ok(ResolvedFileScope::File(file_id))
244 } else {
245 Err(FileScopeError::FileNotIndexed)
246 }
247 }
248 }
249 }
250
251 #[must_use]
253 pub fn normalize_symbol_query(
254 &self,
255 query: &SymbolQuery<'_>,
256 file_scope: &ResolvedFileScope,
257 ) -> NormalizedSymbolQuery {
258 let normalized_symbol = match *file_scope {
259 ResolvedFileScope::Any => query.symbol.to_string(),
260 ResolvedFileScope::File(file_id) => {
261 self.files().language_for_file(file_id).map_or_else(
262 || query.symbol.to_string(),
263 |language| canonicalize_graph_qualified_name(language, query.symbol),
264 )
265 }
266 };
267
268 NormalizedSymbolQuery {
269 symbol: normalized_symbol,
270 file_scope: *file_scope,
271 mode: query.mode,
272 }
273 }
274
275 fn exact_qualified_bucket(&self, query: &NormalizedSymbolQuery) -> Vec<NodeId> {
276 self.strings()
277 .get(&query.symbol)
278 .map_or_else(Vec::new, |string_id| {
279 self.indices().by_qualified_name(string_id).to_vec()
280 })
281 }
282
283 fn exact_simple_bucket(&self, query: &NormalizedSymbolQuery) -> Vec<NodeId> {
284 self.strings()
285 .get(&query.symbol)
286 .map_or_else(Vec::new, |string_id| {
287 self.indices().by_name(string_id).to_vec()
288 })
289 }
290
291 fn bounded_suffix_bucket(&self, query: &NormalizedSymbolQuery) -> Vec<NodeId> {
292 if !query.symbol.contains("::") {
293 return Vec::new();
294 }
295
296 let Some(leaf_symbol) = query.symbol.rsplit("::").next() else {
297 return Vec::new();
298 };
299 let Some(leaf_id) = self.strings().get(leaf_symbol) else {
300 return Vec::new();
301 };
302 let suffix_pattern = format!("::{}", query.symbol);
303
304 self.indices()
305 .by_name(leaf_id)
306 .iter()
307 .copied()
308 .filter(|node_id| {
309 self.get_node(*node_id)
310 .and_then(|entry| entry.qualified_name)
311 .and_then(|qualified_name_id| self.strings().resolve(qualified_name_id))
312 .is_some_and(|qualified_name| {
313 qualified_name.as_ref() == query.symbol
314 || qualified_name.as_ref().ends_with(&suffix_pattern)
315 })
316 })
317 .collect()
318 }
319
320 fn filtered_bucket(
321 &self,
322 mut bucket: Vec<NodeId>,
323 file_scope: ResolvedFileScope,
324 ) -> Vec<NodeId> {
325 if let ResolvedFileScope::File(file_id) = file_scope {
326 let file_nodes = self.indices().by_file(file_id);
327 bucket.retain(|node_id| file_nodes.contains(node_id));
328 }
329
330 bucket.sort_by(|left, right| {
331 self.candidate_sort_key(*left)
332 .cmp(&self.candidate_sort_key(*right))
333 });
334 bucket.dedup();
335 bucket
336 }
337
338 fn first_candidate_bucket_with_witness(
339 &self,
340 query: &NormalizedSymbolQuery,
341 file_scope: ResolvedFileScope,
342 ) -> Option<(SymbolCandidateBucket, Vec<SymbolCandidateWitness>)> {
343 for bucket in [
344 SymbolCandidateBucket::ExactQualified,
345 SymbolCandidateBucket::ExactSimple,
346 SymbolCandidateBucket::CanonicalSuffix,
347 ] {
348 if bucket == SymbolCandidateBucket::CanonicalSuffix
349 && !matches!(query.mode, ResolutionMode::AllowSuffixCandidates)
350 {
351 continue;
352 }
353
354 let candidates = self.bucket_witnesses(query, file_scope, bucket);
355 if !candidates.is_empty() {
356 return Some((bucket, candidates));
357 }
358 }
359
360 None
361 }
362
363 fn bucket_witnesses(
364 &self,
365 query: &NormalizedSymbolQuery,
366 file_scope: ResolvedFileScope,
367 bucket: SymbolCandidateBucket,
368 ) -> Vec<SymbolCandidateWitness> {
369 let raw_bucket = match bucket {
370 SymbolCandidateBucket::ExactQualified => self.exact_qualified_bucket(query),
371 SymbolCandidateBucket::ExactSimple => self.exact_simple_bucket(query),
372 SymbolCandidateBucket::CanonicalSuffix => self.bounded_suffix_bucket(query),
373 };
374
375 self.filtered_bucket(raw_bucket, file_scope)
376 .into_iter()
377 .map(|node_id| SymbolCandidateWitness { node_id, bucket })
378 .collect()
379 }
380
381 fn candidate_sort_key(&self, node_id: NodeId) -> CandidateSortKey {
382 let Some(entry) = self.get_node(node_id) else {
383 return CandidateSortKey::default_for(node_id);
384 };
385
386 let file_path = self
387 .files()
388 .resolve(entry.file)
389 .map_or_else(String::new, |path| path.to_string_lossy().into_owned());
390 let qualified_name = entry
391 .qualified_name
392 .and_then(|string_id| self.strings().resolve(string_id))
393 .map_or_else(String::new, |value| value.to_string());
394 let simple_name = self
395 .strings()
396 .resolve(entry.name)
397 .map_or_else(String::new, |value| value.to_string());
398
399 CandidateSortKey {
400 file_path,
401 start_line: entry.start_line,
402 start_column: entry.start_column,
403 end_line: entry.end_line,
404 end_column: entry.end_column,
405 kind: entry.kind.as_str().to_string(),
406 qualified_name,
407 simple_name,
408 node_id,
409 }
410 }
411}
412
413#[derive(Debug, Clone, PartialEq, Eq)]
415pub struct SymbolCandidateSearchWitness {
416 pub normalized_query: Option<NormalizedSymbolQuery>,
418 pub outcome: SymbolCandidateOutcome,
420 pub selected_bucket: Option<SymbolCandidateBucket>,
422 pub candidates: Vec<SymbolCandidateWitness>,
424}
425
426#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
427struct CandidateSortKey {
428 file_path: String,
429 start_line: u32,
430 start_column: u32,
431 end_line: u32,
432 end_column: u32,
433 kind: String,
434 qualified_name: String,
435 simple_name: String,
436 node_id: NodeId,
437}
438
439impl CandidateSortKey {
440 fn default_for(node_id: NodeId) -> Self {
441 Self {
442 file_path: String::new(),
443 start_line: 0,
444 start_column: 0,
445 end_line: 0,
446 end_column: 0,
447 kind: String::new(),
448 qualified_name: String::new(),
449 simple_name: String::new(),
450 node_id,
451 }
452 }
453}
454
455#[must_use]
457pub(crate) fn canonicalize_graph_qualified_name(language: Language, symbol: &str) -> String {
458 if should_skip_qualified_name_normalization(symbol) {
459 return symbol.to_string();
460 }
461
462 if language == Language::R {
463 return canonicalize_r_qualified_name(symbol);
464 }
465
466 let mut normalized = symbol.to_string();
467 for delimiter in native_delimiters(language) {
468 if normalized.contains(delimiter) {
469 normalized = normalized.replace(delimiter, "::");
470 }
471 }
472 normalized
473}
474
475#[must_use]
477pub(crate) fn is_canonical_graph_qualified_name(language: Language, symbol: &str) -> bool {
478 should_skip_qualified_name_normalization(symbol)
479 || canonicalize_graph_qualified_name(language, symbol) == symbol
480}
481
482fn should_skip_qualified_name_normalization(symbol: &str) -> bool {
483 symbol.starts_with('<')
484 || symbol.contains('/')
485 || symbol.starts_with("wasm::")
486 || symbol.starts_with("ffi::")
487 || symbol.starts_with("extern::")
488 || symbol.starts_with("native::")
489}
490
491fn canonicalize_r_qualified_name(symbol: &str) -> String {
492 let search_start = usize::from(symbol.starts_with('.'));
493 let Some(relative_split_index) = symbol[search_start..].rfind('.') else {
494 return symbol.to_string();
495 };
496
497 let split_index = search_start + relative_split_index;
498 let prefix = &symbol[..split_index];
499 let suffix = &symbol[split_index + 1..];
500 if suffix.is_empty() {
501 return symbol.to_string();
502 }
503
504 format!("{prefix}::{suffix}")
505}
506
507#[must_use]
509pub fn display_graph_qualified_name(
510 language: Language,
511 qualified: &str,
512 kind: NodeKind,
513 is_static: bool,
514) -> String {
515 if should_skip_qualified_name_normalization(qualified) {
516 return qualified.to_string();
517 }
518
519 match language {
520 Language::Ruby => display_ruby_qualified_name(qualified, kind, is_static),
521 Language::Php => display_php_qualified_name(qualified, kind),
522 _ => native_display_separator(language).map_or_else(
523 || qualified.to_string(),
524 |separator| qualified.replace("::", separator),
525 ),
526 }
527}
528
529pub(crate) fn native_delimiters(language: Language) -> &'static [&'static str] {
530 match language {
531 Language::JavaScript
532 | Language::Python
533 | Language::TypeScript
534 | Language::Java
535 | Language::CSharp
536 | Language::Kotlin
537 | Language::Scala
538 | Language::Go
539 | Language::Css
540 | Language::Sql
541 | Language::Dart
542 | Language::Lua
543 | Language::Perl
544 | Language::Groovy
545 | Language::Elixir
546 | Language::R
547 | Language::Haskell
548 | Language::Html
549 | Language::Svelte
550 | Language::Vue
551 | Language::Terraform
552 | Language::Puppet
553 | Language::Pulumi
554 | Language::Http
555 | Language::Plsql
556 | Language::Apex
557 | Language::Abap
558 | Language::ServiceNow
559 | Language::Swift
560 | Language::Zig
561 | Language::Json => &["."],
562 Language::Ruby => &["#", "."],
563 Language::Php => &["\\", "->"],
564 Language::C | Language::Cpp | Language::Rust | Language::Shell => &[],
565 }
566}
567
568fn native_display_separator(language: Language) -> Option<&'static str> {
569 match language {
570 Language::C
571 | Language::Cpp
572 | Language::Rust
573 | Language::Shell
574 | Language::Php
575 | Language::Ruby => None,
576 _ => Some("."),
577 }
578}
579
580fn display_ruby_qualified_name(qualified: &str, kind: NodeKind, is_static: bool) -> String {
581 if qualified.contains('#') || qualified.contains('.') || !qualified.contains("::") {
582 return qualified.to_string();
583 }
584
585 match kind {
586 NodeKind::Method => {
587 replace_last_separator(qualified, if is_static { "." } else { "#" }, false)
588 }
589 NodeKind::Variable if should_display_ruby_member_variable(qualified) => {
590 replace_last_separator(qualified, "#", false)
591 }
592 _ => qualified.to_string(),
593 }
594}
595
596fn should_display_ruby_member_variable(qualified: &str) -> bool {
597 let Some((_, suffix)) = qualified.rsplit_once("::") else {
598 return false;
599 };
600
601 if suffix.starts_with("@@")
602 || suffix
603 .chars()
604 .next()
605 .is_some_and(|character| character.is_ascii_uppercase())
606 {
607 return false;
608 }
609
610 suffix.starts_with('@')
611 || suffix
612 .chars()
613 .next()
614 .is_some_and(|character| character.is_ascii_lowercase() || character == '_')
615}
616
617fn display_php_qualified_name(qualified: &str, kind: NodeKind) -> String {
618 if !qualified.contains("::") {
619 return qualified.to_string();
620 }
621
622 if matches!(kind, NodeKind::Method | NodeKind::Property) {
623 return replace_last_separator(qualified, "::", true);
624 }
625
626 qualified.replace("::", "\\")
627}
628
629fn replace_last_separator(qualified: &str, final_separator: &str, preserve_prefix: bool) -> String {
630 let Some((prefix, suffix)) = qualified.rsplit_once("::") else {
631 return qualified.to_string();
632 };
633
634 let display_prefix = if preserve_prefix {
635 prefix.replace("::", "\\")
636 } else {
637 prefix.to_string()
638 };
639
640 if display_prefix.is_empty() {
641 suffix.to_string()
642 } else {
643 format!("{display_prefix}{final_separator}{suffix}")
644 }
645}
646
647#[cfg(test)]
648mod tests {
649 use std::path::{Path, PathBuf};
650
651 use crate::graph::node::Language;
652 use crate::graph::unified::concurrent::CodeGraph;
653 use crate::graph::unified::node::id::NodeId;
654 use crate::graph::unified::node::kind::NodeKind;
655 use crate::graph::unified::storage::arena::NodeEntry;
656
657 use super::{
658 FileScope, NormalizedSymbolQuery, ResolutionMode, ResolvedFileScope, SymbolCandidateBucket,
659 SymbolCandidateOutcome, SymbolQuery, SymbolResolutionOutcome,
660 canonicalize_graph_qualified_name, display_graph_qualified_name,
661 };
662
663 struct TestNode {
664 node_id: NodeId,
665 }
666
667 #[test]
668 fn test_resolve_symbol_exact_qualified_same_file() {
669 let mut graph = CodeGraph::new();
670 let file_path = abs_path("src/lib.rs");
671 let symbol = add_node(
672 &mut graph,
673 NodeKind::Function,
674 "target",
675 Some("pkg::target"),
676 &file_path,
677 Some(Language::Rust),
678 10,
679 2,
680 );
681
682 let snapshot = graph.snapshot();
683 let query = SymbolQuery {
684 symbol: "pkg::target",
685 file_scope: FileScope::Path(&file_path),
686 mode: ResolutionMode::Strict,
687 };
688
689 assert_eq!(
690 snapshot.resolve_symbol(&query),
691 SymbolResolutionOutcome::Resolved(symbol.node_id)
692 );
693 }
694
695 #[test]
696 fn test_resolve_symbol_exact_simple_same_file_wins() {
697 let mut graph = CodeGraph::new();
698 let requested_path = abs_path("src/requested.rs");
699 let other_path = abs_path("src/other.rs");
700
701 let requested = add_node(
702 &mut graph,
703 NodeKind::Function,
704 "target",
705 Some("requested::target"),
706 &requested_path,
707 Some(Language::Rust),
708 4,
709 0,
710 );
711 let _other = add_node(
712 &mut graph,
713 NodeKind::Function,
714 "target",
715 Some("other::target"),
716 &other_path,
717 Some(Language::Rust),
718 1,
719 0,
720 );
721
722 let snapshot = graph.snapshot();
723 let query = SymbolQuery {
724 symbol: "target",
725 file_scope: FileScope::Path(&requested_path),
726 mode: ResolutionMode::Strict,
727 };
728
729 assert_eq!(
730 snapshot.resolve_symbol(&query),
731 SymbolResolutionOutcome::Resolved(requested.node_id)
732 );
733 }
734
735 #[test]
736 fn test_resolve_symbol_returns_not_found_without_wrong_file_fallback() {
737 let mut graph = CodeGraph::new();
738 let requested_path = abs_path("src/requested.rs");
739 let other_path = abs_path("src/other.rs");
740
741 let _requested_index_anchor = add_node(
742 &mut graph,
743 NodeKind::Function,
744 "anchor",
745 Some("requested::anchor"),
746 &requested_path,
747 Some(Language::Rust),
748 1,
749 0,
750 );
751 let _other = add_node(
752 &mut graph,
753 NodeKind::Function,
754 "target",
755 Some("other::target"),
756 &other_path,
757 Some(Language::Rust),
758 3,
759 0,
760 );
761
762 let snapshot = graph.snapshot();
763 let query = SymbolQuery {
764 symbol: "target",
765 file_scope: FileScope::Path(&requested_path),
766 mode: ResolutionMode::Strict,
767 };
768
769 assert_eq!(
770 snapshot.resolve_symbol(&query),
771 SymbolResolutionOutcome::NotFound
772 );
773 }
774
775 #[test]
776 fn test_resolve_symbol_returns_file_not_indexed_for_valid_unindexed_path() {
777 let mut graph = CodeGraph::new();
778 let indexed_path = abs_path("src/indexed.rs");
779 let unindexed_path = abs_path("src/unindexed.rs");
780
781 add_node(
782 &mut graph,
783 NodeKind::Function,
784 "indexed",
785 Some("pkg::indexed"),
786 &indexed_path,
787 Some(Language::Rust),
788 1,
789 0,
790 );
791 graph
792 .files_mut()
793 .register_with_language(&unindexed_path, Some(Language::Rust))
794 .unwrap();
795
796 let snapshot = graph.snapshot();
797 let query = SymbolQuery {
798 symbol: "indexed",
799 file_scope: FileScope::Path(&unindexed_path),
800 mode: ResolutionMode::Strict,
801 };
802
803 assert_eq!(
804 snapshot.resolve_symbol(&query),
805 SymbolResolutionOutcome::FileNotIndexed
806 );
807 }
808
809 #[test]
810 fn test_resolve_symbol_returns_ambiguous_for_multi_match_bucket() {
811 let mut graph = CodeGraph::new();
812 let file_path = abs_path("src/lib.rs");
813
814 let first = add_node(
815 &mut graph,
816 NodeKind::Function,
817 "dup",
818 Some("pkg::dup"),
819 &file_path,
820 Some(Language::Rust),
821 2,
822 0,
823 );
824 let second = add_node(
825 &mut graph,
826 NodeKind::Method,
827 "dup",
828 Some("pkg::dup_method"),
829 &file_path,
830 Some(Language::Rust),
831 8,
832 0,
833 );
834
835 let snapshot = graph.snapshot();
836 let query = SymbolQuery {
837 symbol: "dup",
838 file_scope: FileScope::Path(&file_path),
839 mode: ResolutionMode::Strict,
840 };
841
842 assert_eq!(
843 snapshot.resolve_symbol(&query),
844 SymbolResolutionOutcome::Ambiguous(vec![first.node_id, second.node_id])
845 );
846 }
847
848 #[test]
849 fn test_find_symbol_candidates_uses_first_non_empty_bucket_only() {
850 let mut graph = CodeGraph::new();
851 let qualified_path = abs_path("src/qualified.rs");
852 let simple_path = abs_path("src/simple.rs");
853
854 let qualified = add_node(
855 &mut graph,
856 NodeKind::Function,
857 "target",
858 Some("pkg::target"),
859 &qualified_path,
860 Some(Language::Rust),
861 1,
862 0,
863 );
864 let simple_only = add_node(
865 &mut graph,
866 NodeKind::Function,
867 "pkg::target",
868 None,
869 &simple_path,
870 Some(Language::Rust),
871 1,
872 0,
873 );
874
875 let snapshot = graph.snapshot();
876 let query = SymbolQuery {
877 symbol: "pkg::target",
878 file_scope: FileScope::Any,
879 mode: ResolutionMode::AllowSuffixCandidates,
880 };
881
882 assert_eq!(
883 snapshot.find_symbol_candidates(&query),
884 SymbolCandidateOutcome::Candidates(vec![qualified.node_id])
885 );
886 assert_ne!(qualified.node_id, simple_only.node_id);
887 }
888
889 #[test]
890 fn test_find_symbol_candidates_with_witness_reports_exact_qualified_bucket() {
891 let mut graph = CodeGraph::new();
892 let qualified_path = abs_path("src/qualified.rs");
893 let simple_path = abs_path("src/simple.rs");
894
895 let qualified = add_node(
896 &mut graph,
897 NodeKind::Function,
898 "target",
899 Some("pkg::target"),
900 &qualified_path,
901 Some(Language::Rust),
902 1,
903 0,
904 );
905 let _simple_only = add_node(
906 &mut graph,
907 NodeKind::Function,
908 "pkg::target",
909 None,
910 &simple_path,
911 Some(Language::Rust),
912 1,
913 0,
914 );
915
916 let snapshot = graph.snapshot();
917 let query = SymbolQuery {
918 symbol: "pkg::target",
919 file_scope: FileScope::Any,
920 mode: ResolutionMode::AllowSuffixCandidates,
921 };
922
923 let witness = snapshot.find_symbol_candidates_with_witness(&query);
924
925 assert_eq!(
926 witness.outcome,
927 SymbolCandidateOutcome::Candidates(vec![qualified.node_id])
928 );
929 assert_eq!(
930 witness.selected_bucket,
931 Some(SymbolCandidateBucket::ExactQualified)
932 );
933 assert_eq!(
934 witness.candidates,
935 vec![super::SymbolCandidateWitness {
936 node_id: qualified.node_id,
937 bucket: SymbolCandidateBucket::ExactQualified,
938 }]
939 );
940 assert_eq!(
941 witness.normalized_query,
942 Some(NormalizedSymbolQuery {
943 symbol: "pkg::target".to_string(),
944 file_scope: ResolvedFileScope::Any,
945 mode: ResolutionMode::AllowSuffixCandidates,
946 })
947 );
948 }
949
950 #[test]
951 fn test_find_symbol_candidates_preserves_file_not_indexed() {
952 let mut graph = CodeGraph::new();
953 let indexed_path = abs_path("src/indexed.rs");
954 let unindexed_path = abs_path("src/unindexed.rs");
955
956 add_node(
957 &mut graph,
958 NodeKind::Function,
959 "target",
960 Some("pkg::target"),
961 &indexed_path,
962 Some(Language::Rust),
963 1,
964 0,
965 );
966 let unindexed_file_id = graph
967 .files_mut()
968 .register_with_language(&unindexed_path, Some(Language::Rust))
969 .unwrap();
970
971 let snapshot = graph.snapshot();
972 let query = SymbolQuery {
973 symbol: "target",
974 file_scope: FileScope::FileId(unindexed_file_id),
975 mode: ResolutionMode::AllowSuffixCandidates,
976 };
977
978 assert_eq!(
979 snapshot.find_symbol_candidates(&query),
980 SymbolCandidateOutcome::FileNotIndexed
981 );
982 }
983
984 #[test]
985 fn test_resolve_symbol_with_witness_reports_ambiguous_bucket_candidates() {
986 let mut graph = CodeGraph::new();
987 let file_path = abs_path("src/lib.rs");
988
989 let first = add_node(
990 &mut graph,
991 NodeKind::Function,
992 "dup",
993 Some("pkg::dup"),
994 &file_path,
995 Some(Language::Rust),
996 2,
997 0,
998 );
999 let second = add_node(
1000 &mut graph,
1001 NodeKind::Method,
1002 "dup",
1003 Some("pkg::dup_method"),
1004 &file_path,
1005 Some(Language::Rust),
1006 8,
1007 0,
1008 );
1009
1010 let snapshot = graph.snapshot();
1011 let query = SymbolQuery {
1012 symbol: "dup",
1013 file_scope: FileScope::Path(&file_path),
1014 mode: ResolutionMode::Strict,
1015 };
1016
1017 let witness = snapshot.resolve_symbol_with_witness(&query);
1018
1019 assert_eq!(
1020 witness.outcome,
1021 SymbolResolutionOutcome::Ambiguous(vec![first.node_id, second.node_id])
1022 );
1023 assert_eq!(
1024 witness.selected_bucket,
1025 Some(SymbolCandidateBucket::ExactSimple)
1026 );
1027 assert_eq!(
1028 witness.candidates,
1029 vec![
1030 super::SymbolCandidateWitness {
1031 node_id: first.node_id,
1032 bucket: SymbolCandidateBucket::ExactSimple,
1033 },
1034 super::SymbolCandidateWitness {
1035 node_id: second.node_id,
1036 bucket: SymbolCandidateBucket::ExactSimple,
1037 },
1038 ]
1039 );
1040 }
1041
1042 #[test]
1043 fn test_suffix_candidates_disabled_in_strict_mode() {
1044 let mut graph = CodeGraph::new();
1045 let file_path = abs_path("src/lib.rs");
1046
1047 let suffix_match = add_node(
1048 &mut graph,
1049 NodeKind::Function,
1050 "target",
1051 Some("outer::pkg::target"),
1052 &file_path,
1053 Some(Language::Rust),
1054 1,
1055 0,
1056 );
1057
1058 let snapshot = graph.snapshot();
1059 let strict_query = SymbolQuery {
1060 symbol: "pkg::target",
1061 file_scope: FileScope::Any,
1062 mode: ResolutionMode::Strict,
1063 };
1064 let suffix_query = SymbolQuery {
1065 mode: ResolutionMode::AllowSuffixCandidates,
1066 ..strict_query
1067 };
1068
1069 assert_eq!(
1070 snapshot.resolve_symbol(&strict_query),
1071 SymbolResolutionOutcome::NotFound
1072 );
1073 assert_eq!(
1074 snapshot.find_symbol_candidates(&suffix_query),
1075 SymbolCandidateOutcome::Candidates(vec![suffix_match.node_id])
1076 );
1077 }
1078
1079 #[test]
1080 fn test_suffix_candidates_require_canonical_qualified_query() {
1081 let mut graph = CodeGraph::new();
1082 let file_path = abs_path("src/mod.py");
1083
1084 add_node(
1085 &mut graph,
1086 NodeKind::Function,
1087 "target",
1088 Some("pkg::target"),
1089 &file_path,
1090 Some(Language::Python),
1091 1,
1092 0,
1093 );
1094
1095 let snapshot = graph.snapshot();
1096 let query = SymbolQuery {
1097 symbol: "pkg.target",
1098 file_scope: FileScope::Any,
1099 mode: ResolutionMode::AllowSuffixCandidates,
1100 };
1101
1102 assert_eq!(
1103 snapshot.find_symbol_candidates(&query),
1104 SymbolCandidateOutcome::NotFound
1105 );
1106 }
1107
1108 #[test]
1109 fn test_suffix_candidates_filter_same_leaf_bucket_only() {
1110 let mut graph = CodeGraph::new();
1111 let file_path = abs_path("src/lib.rs");
1112
1113 let exact_suffix = add_node(
1114 &mut graph,
1115 NodeKind::Function,
1116 "target",
1117 Some("outer::pkg::target"),
1118 &file_path,
1119 Some(Language::Rust),
1120 2,
1121 0,
1122 );
1123 let another_suffix = add_node(
1124 &mut graph,
1125 NodeKind::Method,
1126 "target",
1127 Some("another::pkg::target"),
1128 &file_path,
1129 Some(Language::Rust),
1130 4,
1131 0,
1132 );
1133 let unrelated = add_node(
1134 &mut graph,
1135 NodeKind::Function,
1136 "target",
1137 Some("pkg::different::target"),
1138 &file_path,
1139 Some(Language::Rust),
1140 6,
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![exact_suffix.node_id, another_suffix.node_id])
1154 );
1155 assert_ne!(unrelated.node_id, exact_suffix.node_id);
1156 }
1157
1158 #[test]
1159 fn test_normalize_symbol_query_rewrites_native_delimiter_when_file_scope_language_known() {
1160 let mut graph = CodeGraph::new();
1161 let file_path = abs_path("src/mod.py");
1162 let file_id = graph
1163 .files_mut()
1164 .register_with_language(&file_path, Some(Language::Python))
1165 .unwrap();
1166 let snapshot = graph.snapshot();
1167 let query = SymbolQuery {
1168 symbol: "pkg.mod.fn",
1169 file_scope: FileScope::Path(&file_path),
1170 mode: ResolutionMode::Strict,
1171 };
1172
1173 let normalized = snapshot.normalize_symbol_query(&query, &ResolvedFileScope::File(file_id));
1174
1175 assert_eq!(
1176 normalized,
1177 NormalizedSymbolQuery {
1178 symbol: "pkg::mod::fn".to_string(),
1179 file_scope: ResolvedFileScope::File(file_id),
1180 mode: ResolutionMode::Strict,
1181 }
1182 );
1183 }
1184
1185 #[test]
1186 fn test_normalize_symbol_query_rewrites_native_delimiter_for_csharp() {
1187 let mut graph = CodeGraph::new();
1188 let file_path = abs_path("src/Program.cs");
1189 let file_id = graph
1190 .files_mut()
1191 .register_with_language(&file_path, Some(Language::CSharp))
1192 .unwrap();
1193 let snapshot = graph.snapshot();
1194 let query = SymbolQuery {
1195 symbol: "System.Console.WriteLine",
1196 file_scope: FileScope::Path(&file_path),
1197 mode: ResolutionMode::Strict,
1198 };
1199
1200 let normalized = snapshot.normalize_symbol_query(&query, &ResolvedFileScope::File(file_id));
1201
1202 assert_eq!(normalized.symbol, "System::Console::WriteLine".to_string());
1203 }
1204
1205 #[test]
1206 fn test_normalize_symbol_query_rewrites_native_delimiter_for_zig() {
1207 let mut graph = CodeGraph::new();
1208 let file_path = abs_path("src/main.zig");
1209 let file_id = graph
1210 .files_mut()
1211 .register_with_language(&file_path, Some(Language::Zig))
1212 .unwrap();
1213 let snapshot = graph.snapshot();
1214 let query = SymbolQuery {
1215 symbol: "std.os.linux.exit",
1216 file_scope: FileScope::Path(&file_path),
1217 mode: ResolutionMode::Strict,
1218 };
1219
1220 let normalized = snapshot.normalize_symbol_query(&query, &ResolvedFileScope::File(file_id));
1221
1222 assert_eq!(normalized.symbol, "std::os::linux::exit".to_string());
1223 }
1224
1225 #[test]
1226 fn test_normalize_symbol_query_does_not_rewrite_when_file_scope_any() {
1227 let graph = CodeGraph::new();
1228 let snapshot = graph.snapshot();
1229 let query = SymbolQuery {
1230 symbol: "pkg.mod.fn",
1231 file_scope: FileScope::Any,
1232 mode: ResolutionMode::Strict,
1233 };
1234
1235 let normalized = snapshot.normalize_symbol_query(&query, &ResolvedFileScope::Any);
1236
1237 assert_eq!(
1238 normalized,
1239 NormalizedSymbolQuery {
1240 symbol: "pkg.mod.fn".to_string(),
1241 file_scope: ResolvedFileScope::Any,
1242 mode: ResolutionMode::Strict,
1243 }
1244 );
1245 }
1246
1247 #[test]
1248 fn test_global_qualified_query_with_native_delimiter_is_exact_only_and_not_found() {
1249 let mut graph = CodeGraph::new();
1250 let file_path = abs_path("src/mod.py");
1251
1252 add_node(
1253 &mut graph,
1254 NodeKind::Function,
1255 "fn",
1256 Some("pkg::mod::fn"),
1257 &file_path,
1258 Some(Language::Python),
1259 1,
1260 0,
1261 );
1262
1263 let snapshot = graph.snapshot();
1264 let query = SymbolQuery {
1265 symbol: "pkg.mod.fn",
1266 file_scope: FileScope::Any,
1267 mode: ResolutionMode::AllowSuffixCandidates,
1268 };
1269
1270 assert_eq!(
1271 snapshot.resolve_symbol(&query),
1272 SymbolResolutionOutcome::NotFound
1273 );
1274 }
1275
1276 #[test]
1277 fn test_global_canonical_qualified_query_can_hit_exact_qualified_bucket() {
1278 let mut graph = CodeGraph::new();
1279 let file_path = abs_path("src/lib.rs");
1280 let expected = add_node(
1281 &mut graph,
1282 NodeKind::Function,
1283 "fn",
1284 Some("pkg::mod::fn"),
1285 &file_path,
1286 Some(Language::Rust),
1287 1,
1288 0,
1289 );
1290
1291 let snapshot = graph.snapshot();
1292 let query = SymbolQuery {
1293 symbol: "pkg::mod::fn",
1294 file_scope: FileScope::Any,
1295 mode: ResolutionMode::Strict,
1296 };
1297
1298 assert_eq!(
1299 snapshot.resolve_symbol(&query),
1300 SymbolResolutionOutcome::Resolved(expected.node_id)
1301 );
1302 }
1303
1304 #[test]
1305 fn test_candidate_order_uses_metadata_then_node_id() {
1306 let mut graph = CodeGraph::new();
1307 let file_path = abs_path("src/lib.rs");
1308
1309 let first = add_node(
1310 &mut graph,
1311 NodeKind::Function,
1312 "dup",
1313 Some("pkg::dup_a"),
1314 &file_path,
1315 Some(Language::Rust),
1316 1,
1317 0,
1318 );
1319 let second = add_node(
1320 &mut graph,
1321 NodeKind::Function,
1322 "dup",
1323 Some("pkg::dup_b"),
1324 &file_path,
1325 Some(Language::Rust),
1326 1,
1327 0,
1328 );
1329
1330 let snapshot = graph.snapshot();
1331 let query = SymbolQuery {
1332 symbol: "dup",
1333 file_scope: FileScope::Any,
1334 mode: ResolutionMode::Strict,
1335 };
1336
1337 assert_eq!(
1338 snapshot.find_symbol_candidates(&query),
1339 SymbolCandidateOutcome::Candidates(vec![first.node_id, second.node_id])
1340 );
1341 }
1342
1343 #[test]
1344 fn test_candidate_order_kind_sort_key_uses_node_kind_as_str() {
1345 let mut graph = CodeGraph::new();
1346 let file_path = abs_path("src/lib.rs");
1347
1348 let function_node = add_node(
1349 &mut graph,
1350 NodeKind::Function,
1351 "shared",
1352 Some("pkg::shared_fn"),
1353 &file_path,
1354 Some(Language::Rust),
1355 1,
1356 0,
1357 );
1358 let variable_node = add_node(
1359 &mut graph,
1360 NodeKind::Variable,
1361 "shared",
1362 Some("pkg::shared_var"),
1363 &file_path,
1364 Some(Language::Rust),
1365 1,
1366 0,
1367 );
1368
1369 let snapshot = graph.snapshot();
1370 let query = SymbolQuery {
1371 symbol: "shared",
1372 file_scope: FileScope::Any,
1373 mode: ResolutionMode::Strict,
1374 };
1375
1376 assert_eq!(
1377 snapshot.find_symbol_candidates(&query),
1378 SymbolCandidateOutcome::Candidates(vec![function_node.node_id, variable_node.node_id])
1379 );
1380 }
1381
1382 fn add_node(
1383 graph: &mut CodeGraph,
1384 kind: NodeKind,
1385 name: &str,
1386 qualified_name: Option<&str>,
1387 file_path: &Path,
1388 language: Option<Language>,
1389 start_line: u32,
1390 start_column: u32,
1391 ) -> TestNode {
1392 let name_id = graph.strings_mut().intern(name).unwrap();
1393 let qualified_name_id =
1394 qualified_name.map(|value| graph.strings_mut().intern(value).unwrap());
1395 let file_id = graph
1396 .files_mut()
1397 .register_with_language(file_path, language)
1398 .unwrap();
1399
1400 let entry = NodeEntry::new(kind, name_id, file_id)
1401 .with_qualified_name_opt(qualified_name_id)
1402 .with_location(start_line, start_column, start_line, start_column + 1);
1403
1404 let node_id = graph.nodes_mut().alloc(entry).unwrap();
1405 graph
1406 .indices_mut()
1407 .add(node_id, kind, name_id, qualified_name_id, file_id);
1408
1409 TestNode { node_id }
1410 }
1411
1412 trait NodeEntryExt {
1413 fn with_qualified_name_opt(
1414 self,
1415 qualified_name: Option<crate::graph::unified::string::id::StringId>,
1416 ) -> Self;
1417 }
1418
1419 impl NodeEntryExt for NodeEntry {
1420 fn with_qualified_name_opt(
1421 mut self,
1422 qualified_name: Option<crate::graph::unified::string::id::StringId>,
1423 ) -> Self {
1424 self.qualified_name = qualified_name;
1425 self
1426 }
1427 }
1428
1429 fn abs_path(relative: &str) -> PathBuf {
1430 PathBuf::from("/resolver-tests").join(relative)
1431 }
1432
1433 #[test]
1434 fn test_display_graph_qualified_name_dot_language() {
1435 let display = display_graph_qualified_name(
1436 Language::CSharp,
1437 "MyApp::User::GetName",
1438 NodeKind::Method,
1439 false,
1440 );
1441 assert_eq!(display, "MyApp.User.GetName");
1442 }
1443
1444 #[test]
1445 fn test_canonicalize_graph_qualified_name_r_private_name_preserved() {
1446 assert_eq!(
1447 canonicalize_graph_qualified_name(Language::R, ".private_func"),
1448 ".private_func"
1449 );
1450 }
1451
1452 #[test]
1453 fn test_canonicalize_graph_qualified_name_r_s3_method_uses_last_dot() {
1454 assert_eq!(
1455 canonicalize_graph_qualified_name(Language::R, "as.data.frame.myclass"),
1456 "as.data.frame::myclass"
1457 );
1458 }
1459
1460 #[test]
1461 fn test_canonicalize_graph_qualified_name_r_leading_dot_s3_generic() {
1462 assert_eq!(
1463 canonicalize_graph_qualified_name(Language::R, ".DollarNames.myclass"),
1464 ".DollarNames::myclass"
1465 );
1466 }
1467
1468 #[test]
1469 fn test_display_graph_qualified_name_ruby_instance_method() {
1470 let display = display_graph_qualified_name(
1471 Language::Ruby,
1472 "Admin::Users::Controller::show",
1473 NodeKind::Method,
1474 false,
1475 );
1476 assert_eq!(display, "Admin::Users::Controller#show");
1477 }
1478
1479 #[test]
1480 fn test_display_graph_qualified_name_ruby_singleton_method() {
1481 let display = display_graph_qualified_name(
1482 Language::Ruby,
1483 "Admin::Users::Controller::show",
1484 NodeKind::Method,
1485 true,
1486 );
1487 assert_eq!(display, "Admin::Users::Controller.show");
1488 }
1489
1490 #[test]
1491 fn test_display_graph_qualified_name_ruby_member_variable() {
1492 let display = display_graph_qualified_name(
1493 Language::Ruby,
1494 "Admin::Users::Controller::username",
1495 NodeKind::Variable,
1496 false,
1497 );
1498 assert_eq!(display, "Admin::Users::Controller#username");
1499 }
1500
1501 #[test]
1502 fn test_display_graph_qualified_name_ruby_instance_variable() {
1503 let display = display_graph_qualified_name(
1504 Language::Ruby,
1505 "Admin::Users::Controller::@current_user",
1506 NodeKind::Variable,
1507 false,
1508 );
1509 assert_eq!(display, "Admin::Users::Controller#@current_user");
1510 }
1511
1512 #[test]
1513 fn test_display_graph_qualified_name_ruby_constant_stays_canonical() {
1514 let display = display_graph_qualified_name(
1515 Language::Ruby,
1516 "Admin::Users::Controller::DEFAULT_ROLE",
1517 NodeKind::Variable,
1518 false,
1519 );
1520 assert_eq!(display, "Admin::Users::Controller::DEFAULT_ROLE");
1521 }
1522
1523 #[test]
1524 fn test_display_graph_qualified_name_ruby_class_variable_stays_canonical() {
1525 let display = display_graph_qualified_name(
1526 Language::Ruby,
1527 "Admin::Users::Controller::@@count",
1528 NodeKind::Variable,
1529 false,
1530 );
1531 assert_eq!(display, "Admin::Users::Controller::@@count");
1532 }
1533
1534 #[test]
1535 fn test_display_graph_qualified_name_php_namespace_function() {
1536 let display = display_graph_qualified_name(
1537 Language::Php,
1538 "App::Services::send_mail",
1539 NodeKind::Function,
1540 false,
1541 );
1542 assert_eq!(display, "App\\Services\\send_mail");
1543 }
1544
1545 #[test]
1546 fn test_display_graph_qualified_name_php_method() {
1547 let display = display_graph_qualified_name(
1548 Language::Php,
1549 "App::Services::Mailer::deliver",
1550 NodeKind::Method,
1551 false,
1552 );
1553 assert_eq!(display, "App\\Services\\Mailer::deliver");
1554 }
1555
1556 #[test]
1557 fn test_display_graph_qualified_name_preserves_path_like_symbols() {
1558 let display = display_graph_qualified_name(
1559 Language::Go,
1560 "route::GET::/health",
1561 NodeKind::Endpoint,
1562 false,
1563 );
1564 assert_eq!(display, "route::GET::/health");
1565 }
1566
1567 #[test]
1568 fn test_display_graph_qualified_name_preserves_ffi_symbols() {
1569 let display = display_graph_qualified_name(
1570 Language::Haskell,
1571 "ffi::C::sin",
1572 NodeKind::Function,
1573 false,
1574 );
1575 assert_eq!(display, "ffi::C::sin");
1576 }
1577
1578 #[test]
1579 fn test_display_graph_qualified_name_preserves_native_cffi_symbols() {
1580 let display = display_graph_qualified_name(
1581 Language::Python,
1582 "native::cffi::calculate",
1583 NodeKind::Function,
1584 false,
1585 );
1586 assert_eq!(display, "native::cffi::calculate");
1587 }
1588
1589 #[test]
1590 fn test_display_graph_qualified_name_preserves_native_php_ffi_symbols() {
1591 let display = display_graph_qualified_name(
1592 Language::Php,
1593 "native::ffi::crypto_encrypt",
1594 NodeKind::Function,
1595 false,
1596 );
1597 assert_eq!(display, "native::ffi::crypto_encrypt");
1598 }
1599
1600 #[test]
1601 fn test_display_graph_qualified_name_preserves_native_panama_symbols() {
1602 let display = display_graph_qualified_name(
1603 Language::Java,
1604 "native::panama::nativeLinker",
1605 NodeKind::Function,
1606 false,
1607 );
1608 assert_eq!(display, "native::panama::nativeLinker");
1609 }
1610
1611 #[test]
1612 fn test_canonicalize_graph_qualified_name_preserves_wasm_symbols() {
1613 assert_eq!(
1614 canonicalize_graph_qualified_name(Language::TypeScript, "wasm::module.wasm"),
1615 "wasm::module.wasm"
1616 );
1617 }
1618
1619 #[test]
1620 fn test_canonicalize_graph_qualified_name_preserves_native_symbols() {
1621 assert_eq!(
1622 canonicalize_graph_qualified_name(Language::TypeScript, "native::binding.node"),
1623 "native::binding.node"
1624 );
1625 }
1626
1627 #[test]
1628 fn test_display_graph_qualified_name_preserves_wasm_symbols() {
1629 let display = display_graph_qualified_name(
1630 Language::TypeScript,
1631 "wasm::module.wasm",
1632 NodeKind::Module,
1633 false,
1634 );
1635 assert_eq!(display, "wasm::module.wasm");
1636 }
1637
1638 #[test]
1639 fn test_display_graph_qualified_name_preserves_native_symbols() {
1640 let display = display_graph_qualified_name(
1641 Language::TypeScript,
1642 "native::binding.node",
1643 NodeKind::Module,
1644 false,
1645 );
1646 assert_eq!(display, "native::binding.node");
1647 }
1648
1649 #[test]
1650 fn test_canonicalize_graph_qualified_name_still_normalizes_dot_language_symbols() {
1651 assert_eq!(
1652 canonicalize_graph_qualified_name(Language::TypeScript, "Foo.bar"),
1653 "Foo::bar"
1654 );
1655 }
1656}