1use crate::ConfiguredModuleResolution;
4use crate::configured_modules::ConfiguredModuleSource;
5use crate::semantic;
6use blake3::Hasher;
7use projectatlas_core::graph::{
8 CanonicalResolutionKey, GraphContractError, GraphIdentityText, GraphLimits, GraphRelationKind,
9 ProjectInstanceId, ResolutionKeyDomain,
10};
11use projectatlas_core::language::{LANGUAGE_CAPABILITIES, SemanticProviderOwner};
12use projectatlas_core::symbols::{RelationKind, SymbolGraph, SymbolKind};
13use std::collections::BTreeMap;
14use std::error::Error;
15use std::fmt;
16
17pub const MAX_RESOLUTION_KEYS_PER_FACT: usize = 64;
19pub const SEMANTIC_RESOLUTION_CONTRACT_VERSION: u32 = 3;
21
22#[must_use]
28pub fn semantic_resolution_contract_digest() -> String {
29 let mut hasher = Hasher::new();
30 hasher.update(&SEMANTIC_RESOLUTION_CONTRACT_VERSION.to_le_bytes());
31 hasher.update(&(MAX_RESOLUTION_KEYS_PER_FACT as u64).to_le_bytes());
32 let mut providers = LANGUAGE_CAPABILITIES
33 .iter()
34 .filter_map(|capability| capability.effective_semantic_provider())
35 .collect::<Vec<_>>();
36 providers.sort_by_key(|provider| provider.as_str());
37 providers.dedup();
38 for provider in providers {
39 hash_contract_value(&mut hasher, provider.as_str());
40 hash_contract_value(
41 &mut hasher,
42 provider.resolution_family().unwrap_or("unavailable"),
43 );
44 if semantic::supports_source_dependencies(provider) {
45 hash_relation_contract(
46 &mut hasher,
47 RelationKind::Imports,
48 ResolutionKeyDomain::Module,
49 );
50 hash_relation_contract(
51 &mut hasher,
52 RelationKind::Calls,
53 ResolutionKeyDomain::Declaration,
54 );
55 }
56 if semantic::supports_package_dependencies(provider) {
57 hash_relation_contract(
58 &mut hasher,
59 RelationKind::DependsOn,
60 ResolutionKeyDomain::Package,
61 );
62 }
63 }
64 for outcome in ["resolved", "ambiguous", "unresolved", "external"] {
65 hash_contract_value(&mut hasher, outcome);
66 }
67 hasher.finalize().to_hex().to_string()
68}
69
70fn hash_relation_contract(
72 hasher: &mut Hasher,
73 relation: RelationKind,
74 domain: ResolutionKeyDomain,
75) {
76 hash_contract_value(hasher, &relation.to_string());
77 hash_contract_value(hasher, domain.as_str());
78}
79
80fn hash_contract_value(hasher: &mut Hasher, value: &str) {
82 hasher.update(&(value.len() as u64).to_le_bytes());
83 hasher.update(value.as_bytes());
84}
85#[derive(Clone, Debug, Eq, PartialEq)]
87pub struct SymbolResolutionKeys {
88 symbol_index: usize,
90 keys: Vec<CanonicalResolutionKey>,
92}
93
94impl SymbolResolutionKeys {
95 #[must_use]
97 pub const fn symbol_index(&self) -> usize {
98 self.symbol_index
99 }
100
101 #[must_use]
103 pub fn keys(&self) -> &[CanonicalResolutionKey] {
104 &self.keys
105 }
106}
107
108#[derive(Clone, Debug, Eq, PartialEq)]
110pub struct RelationResolutionKeys {
111 relation_index: usize,
113 keys: Vec<CanonicalResolutionKey>,
115}
116
117impl RelationResolutionKeys {
118 #[must_use]
120 pub const fn relation_index(&self) -> usize {
121 self.relation_index
122 }
123
124 #[must_use]
126 pub fn keys(&self) -> &[CanonicalResolutionKey] {
127 &self.keys
128 }
129}
130
131#[derive(Clone, Debug, Eq, PartialEq)]
133pub struct ResolutionKeyProjection {
134 source: Vec<CanonicalResolutionKey>,
136 symbols: Vec<SymbolResolutionKeys>,
138 relations: Vec<RelationResolutionKeys>,
140}
141
142#[derive(Clone, Copy, Debug, Default)]
144pub struct ResolutionProjectionContext<'a> {
145 configured_modules: Option<&'a ConfiguredModuleResolution>,
147}
148
149type ImportScopeCache = BTreeMap<String, BTreeMap<String, Vec<String>>>;
151
152struct ContractFailures {
154 failures: Vec<ResolutionProjectionFactFailure>,
156 rejected_count: usize,
158 source_seen: bool,
160 symbol_seen: Vec<bool>,
162 relation_seen: Vec<bool>,
164}
165
166impl ContractFailures {
167 fn new(symbol_count: usize, relation_count: usize) -> Self {
169 Self {
170 failures: Vec::new(),
171 rejected_count: 0,
172 source_seen: false,
173 symbol_seen: vec![false; symbol_count],
174 relation_seen: vec![false; relation_count],
175 }
176 }
177
178 fn remember(&mut self, fact: ResolutionProjectionFact, error: GraphContractError) {
180 let seen = match fact {
181 ResolutionProjectionFact::Source => {
182 let seen = self.source_seen;
183 self.source_seen = true;
184 !seen
185 }
186 ResolutionProjectionFact::Symbol(index) => {
187 self.symbol_seen.get_mut(index).is_some_and(|seen| {
188 let was_seen = *seen;
189 *seen = true;
190 !was_seen
191 })
192 }
193 ResolutionProjectionFact::Relation(index) => {
194 self.relation_seen.get_mut(index).is_some_and(|seen| {
195 let was_seen = *seen;
196 *seen = true;
197 !was_seen
198 })
199 }
200 };
201 if !seen {
202 return;
203 }
204 self.rejected_count = self.rejected_count.saturating_add(1);
205 if self.failures.len() < MAX_RESOLUTION_PROJECTION_FAILURES {
206 self.failures
207 .push(ResolutionProjectionFactFailure { error, fact });
208 }
209 }
210}
211
212impl<'a> ResolutionProjectionContext<'a> {
213 #[must_use]
215 pub const fn with_configured_modules(
216 configured_modules: &'a ConfiguredModuleResolution,
217 ) -> Self {
218 Self {
219 configured_modules: Some(configured_modules),
220 }
221 }
222}
223
224impl ResolutionKeyProjection {
225 #[must_use]
229 pub const fn empty() -> Self {
230 Self {
231 source: Vec::new(),
232 symbols: Vec::new(),
233 relations: Vec::new(),
234 }
235 }
236
237 #[must_use]
239 pub fn source_keys(&self) -> &[CanonicalResolutionKey] {
240 &self.source
241 }
242
243 #[must_use]
245 pub fn symbol_keys(&self) -> &[SymbolResolutionKeys] {
246 &self.symbols
247 }
248
249 #[must_use]
251 pub fn relation_keys(&self) -> &[RelationResolutionKeys] {
252 &self.relations
253 }
254}
255
256#[derive(Clone, Copy, Debug, Eq, PartialEq)]
258pub enum ResolutionProjectionFact {
259 Source,
261 Symbol(usize),
263 Relation(usize),
265}
266
267pub const MAX_RESOLUTION_PROJECTION_FAILURES: usize = GraphLimits::MAX_ROWS as usize;
269
270#[derive(Debug)]
272pub struct ResolutionProjectionFactFailure {
273 error: GraphContractError,
275 fact: ResolutionProjectionFact,
277}
278
279impl ResolutionProjectionFactFailure {
280 #[must_use]
282 pub fn error(&self) -> &GraphContractError {
283 &self.error
284 }
285
286 #[must_use]
288 pub const fn fact(&self) -> ResolutionProjectionFact {
289 self.fact
290 }
291
292 #[must_use]
294 pub fn into_parts(self) -> (ResolutionProjectionFact, GraphContractError) {
295 (self.fact, self.error)
296 }
297}
298
299#[derive(Debug)]
301pub struct ResolutionProjectionFailure {
302 failures: Vec<ResolutionProjectionFactFailure>,
304 rejected_count: usize,
306 projection: ResolutionKeyProjection,
308}
309
310impl ResolutionProjectionFailure {
311 #[must_use]
313 pub fn failures(&self) -> &[ResolutionProjectionFactFailure] {
314 &self.failures
315 }
316
317 #[must_use]
319 pub const fn rejected_count(&self) -> usize {
320 self.rejected_count
321 }
322
323 #[must_use]
325 pub fn projection(&self) -> &ResolutionKeyProjection {
326 &self.projection
327 }
328
329 #[must_use]
331 pub fn into_parts(
332 self,
333 ) -> (
334 Vec<ResolutionProjectionFactFailure>,
335 usize,
336 ResolutionKeyProjection,
337 ) {
338 (self.failures, self.rejected_count, self.projection)
339 }
340}
341
342#[derive(Debug)]
344pub enum ResolutionProjectionError {
345 Contract(Box<ResolutionProjectionFailure>),
347 KeyLimit {
349 fact: &'static str,
351 index: usize,
353 requested: usize,
355 },
356}
357
358impl fmt::Display for ResolutionProjectionError {
359 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
360 match self {
361 Self::Contract(failure) => match failure.failures.first() {
362 Some(failure) => {
363 write!(formatter, "invalid resolution identity: {}", failure.error)
364 }
365 None => formatter.write_str("invalid resolution identity"),
366 },
367 Self::KeyLimit {
368 fact,
369 index,
370 requested,
371 } => write!(
372 formatter,
373 "{fact} fact {index} requires {requested} canonical keys, exceeding the per-fact limit of {MAX_RESOLUTION_KEYS_PER_FACT}"
374 ),
375 }
376 }
377}
378
379impl Error for ResolutionProjectionError {
380 fn source(&self) -> Option<&(dyn Error + 'static)> {
381 match self {
382 Self::Contract(failure) => failure
383 .failures
384 .first()
385 .map(|failure| &failure.error as &(dyn Error + 'static)),
386 Self::KeyLimit { .. } => None,
387 }
388 }
389}
390
391impl From<GraphContractError> for ResolutionProjectionError {
392 fn from(value: GraphContractError) -> Self {
393 Self::Contract(Box::new(ResolutionProjectionFailure {
394 failures: vec![ResolutionProjectionFactFailure {
395 error: value,
396 fact: ResolutionProjectionFact::Source,
397 }],
398 rejected_count: 1,
399 projection: ResolutionKeyProjection::empty(),
400 }))
401 }
402}
403
404#[derive(Clone, Copy, Debug, Eq, PartialEq)]
406pub enum ImportSyntax {
407 Rust,
409 EcmaScript,
411 Python,
413}
414
415#[derive(Clone, Debug, Eq, PartialEq)]
417pub struct ImportReference {
418 syntax: ImportSyntax,
420 module: String,
422 imported: Option<String>,
424 local: String,
426}
427
428impl ImportReference {
429 pub(crate) fn new(
431 syntax: ImportSyntax,
432 module: &str,
433 imported: Option<&str>,
434 local: &str,
435 ) -> Self {
436 Self {
437 syntax,
438 module: module.to_string(),
439 imported: imported.map(ToString::to_string),
440 local: local.to_string(),
441 }
442 }
443
444 #[must_use]
446 pub const fn syntax(&self) -> ImportSyntax {
447 self.syntax
448 }
449
450 #[must_use]
452 pub fn module(&self) -> &str {
453 &self.module
454 }
455
456 #[must_use]
458 pub fn imported(&self) -> Option<&str> {
459 self.imported.as_deref()
460 }
461
462 #[must_use]
464 pub fn local(&self) -> &str {
465 &self.local
466 }
467}
468
469#[must_use]
474pub fn parse_import_references(import_text: &str) -> Vec<ImportReference> {
475 let mut references = semantic::parse_display_import(import_text);
476 references.sort_by(|left, right| {
477 left.module
478 .cmp(&right.module)
479 .then_with(|| left.imported.cmp(&right.imported))
480 .then_with(|| left.local.cmp(&right.local))
481 });
482 references.dedup();
483 references
484}
485
486#[must_use]
488pub fn resolve_relative_import_path(caller_path: &str, module_spec: &str) -> Option<String> {
489 semantic::ecmascript::resolve_relative_import_path(caller_path, module_spec)
490}
491
492#[must_use]
494pub fn module_aliases_for_path(path: &str) -> Vec<String> {
495 let mut aliases = Vec::new();
496 for stem in source_stems_for_path(path) {
497 let mut components = stem
498 .split('/')
499 .filter(|component| !component.is_empty())
500 .collect::<Vec<_>>();
501 if components
502 .first()
503 .is_some_and(|component| *component == "src")
504 {
505 components.remove(0);
506 }
507 if components.last().is_some_and(|component| {
508 matches!(*component, "lib" | "main" | "mod" | "index" | "__init__")
509 }) {
510 components.pop();
511 }
512 if components.is_empty() {
513 continue;
514 }
515 aliases.push(components.join("::"));
516 aliases.push(components.join("."));
517 if let Some(last) = components.last() {
518 aliases.push((*last).to_string());
519 }
520 }
521 aliases.sort();
522 aliases.dedup();
523 aliases
524}
525
526#[must_use]
528pub fn source_stems_for_path(path: &str) -> Vec<String> {
529 let stem = strip_known_source_extension(path);
530 let mut stems = vec![stem.clone()];
531 if let Some((parent, entry_name)) = stem.rsplit_once('/')
532 && matches!(entry_name, "index" | "__init__" | "mod")
533 {
534 stems.push(parent.to_string());
535 }
536 stems.sort();
537 stems.dedup();
538 stems
539}
540
541pub fn derive_resolution_keys(
552 project: ProjectInstanceId,
553 package: Option<&str>,
554 graph: &SymbolGraph,
555) -> Result<ResolutionKeyProjection, ResolutionProjectionError> {
556 derive_resolution_keys_with_context(
557 project,
558 package,
559 graph,
560 ResolutionProjectionContext::default(),
561 )
562}
563
564pub fn derive_resolution_keys_with_context(
571 project: ProjectInstanceId,
572 package: Option<&str>,
573 graph: &SymbolGraph,
574 context: ResolutionProjectionContext<'_>,
575) -> Result<ResolutionKeyProjection, ResolutionProjectionError> {
576 let Some(provider_owner) = semantic::provider_for_graph(graph) else {
577 return Ok(ResolutionKeyProjection {
578 source: Vec::new(),
579 symbols: Vec::new(),
580 relations: Vec::new(),
581 });
582 };
583 let provider = GraphIdentityText::new(provider_owner.as_str())?;
584 let Some(resolution_family) = provider_owner.resolution_family() else {
585 return Ok(ResolutionKeyProjection {
586 source: Vec::new(),
587 symbols: Vec::new(),
588 relations: Vec::new(),
589 });
590 };
591 let mut contract_failures = ContractFailures::new(graph.symbols.len(), graph.relations.len());
592 let language = GraphIdentityText::new(resolution_family)?;
593 let package = package.map(GraphIdentityText::new).transpose()?;
594
595 let source_scopes = canonical_source_scopes(&graph.path);
596 let configured_source_scopes =
597 if provider_owner == SemanticProviderOwner::EcmaScript && package.is_some() {
598 canonical_configured_source_scopes(&graph.path)
599 } else {
600 Vec::new()
601 };
602 let emits_source_module_keys = semantic::emits_source_module_keys(graph);
603 let mut source_keys = Vec::new();
604 if emits_source_module_keys {
605 source_keys.reserve(source_scopes.len() + configured_source_scopes.len());
606 for scope in &source_scopes {
607 if let Some(key) = retain_derived_key(
608 canonical_key(
609 project,
610 ResolutionKeyDomain::Module,
611 &provider,
612 &language,
613 package.as_ref(),
614 None,
615 RelationKind::Imports,
616 scope,
617 ),
618 ResolutionProjectionFact::Source,
619 &mut contract_failures,
620 ) {
621 source_keys.push(key);
622 }
623 }
624 for scope in &configured_source_scopes {
625 if let Some(key) = retain_derived_key(
626 canonical_key(
627 project,
628 ResolutionKeyDomain::Module,
629 &provider,
630 &language,
631 None,
632 None,
633 RelationKind::Imports,
634 scope,
635 ),
636 ResolutionProjectionFact::Source,
637 &mut contract_failures,
638 ) {
639 source_keys.push(key);
640 }
641 }
642 }
643 let source_keys = bounded_keys(source_keys, "source", 0)?;
644
645 let mut symbol_keys = Vec::new();
646 for (symbol_index, symbol) in graph.symbols.iter().enumerate() {
647 let mut keys = Vec::new();
648 match symbol.kind {
649 SymbolKind::Package if provider_owner == SemanticProviderOwner::Cargo => {
650 if let Some(key) = retain_derived_key(
651 canonical_key(
652 project,
653 ResolutionKeyDomain::Package,
654 &provider,
655 &language,
656 None,
657 None,
658 RelationKind::DependsOn,
659 &symbol.name,
660 ),
661 ResolutionProjectionFact::Symbol(symbol_index),
662 &mut contract_failures,
663 ) {
664 keys.push(key);
665 }
666 }
667 SymbolKind::Dependency | SymbolKind::Import | SymbolKind::Workspace => {}
668 _ if emits_source_module_keys
669 && semantic::is_export_candidate(provider_owner, graph, symbol_index) =>
670 {
671 let identity = match GraphIdentityText::new(symbol.name.clone()) {
672 Ok(identity) => identity,
673 Err(error) => {
674 remember_contract_failure(
675 &mut contract_failures,
676 ResolutionProjectionFact::Symbol(symbol_index),
677 error,
678 );
679 continue;
680 }
681 };
682 let mut scopes = source_scopes.clone();
683 if let Some(parent) = symbol.parent.as_deref() {
684 scopes.extend(
685 source_scopes
686 .iter()
687 .map(|scope| format!("{scope}/{parent}")),
688 );
689 scopes.push(parent.to_string());
690 }
691 scopes.push(String::new());
692 scopes.sort();
693 scopes.dedup();
694 for scope in &scopes {
695 let scope = if scope.is_empty() {
696 None
697 } else {
698 match GraphIdentityText::new(scope.clone()) {
699 Ok(scope) => Some(scope),
700 Err(error) => {
701 remember_contract_failure(
702 &mut contract_failures,
703 ResolutionProjectionFact::Symbol(symbol_index),
704 error,
705 );
706 continue;
707 }
708 }
709 };
710 keys.push(CanonicalResolutionKey::new(
711 project,
712 ResolutionKeyDomain::Declaration,
713 &provider,
714 &language,
715 package.as_ref(),
716 scope.as_ref(),
717 Some(GraphRelationKind::from_legacy(RelationKind::Calls)),
718 &identity,
719 ));
720 }
721 let mut configured_scopes = configured_source_scopes.clone();
722 if let Some(parent) = symbol.parent.as_deref() {
723 configured_scopes.extend(
724 configured_source_scopes
725 .iter()
726 .map(|scope| format!("{scope}/{parent}")),
727 );
728 configured_scopes.sort();
729 configured_scopes.dedup();
730 }
731 for scope in configured_scopes {
732 let scope = match GraphIdentityText::new(scope) {
733 Ok(scope) => scope,
734 Err(error) => {
735 remember_contract_failure(
736 &mut contract_failures,
737 ResolutionProjectionFact::Symbol(symbol_index),
738 error,
739 );
740 continue;
741 }
742 };
743 keys.push(CanonicalResolutionKey::new(
744 project,
745 ResolutionKeyDomain::Declaration,
746 &provider,
747 &language,
748 None,
749 Some(&scope),
750 Some(GraphRelationKind::from_legacy(RelationKind::Calls)),
751 &identity,
752 ));
753 }
754 }
755 _ => {}
756 }
757 let keys = bounded_keys(keys, "symbol", symbol_index)?;
758 if !keys.is_empty() {
759 symbol_keys.push(SymbolResolutionKeys { symbol_index, keys });
760 }
761 }
762
763 let parsed_imports = graph
764 .relations
765 .iter()
766 .map(|relation| {
767 if relation.kind == RelationKind::Imports {
768 semantic::parse_imports(provider_owner, &relation.target_name)
769 } else {
770 Vec::new()
771 }
772 })
773 .collect::<Vec<_>>();
774 let mut aliases: BTreeMap<&str, Vec<&ImportReference>> = BTreeMap::new();
775 for references in &parsed_imports {
776 for reference in references {
777 aliases
778 .entry(reference.local())
779 .or_default()
780 .push(reference);
781 }
782 }
783 let configured_source = context
784 .configured_modules
785 .map(|configured| configured.for_source(&graph.path));
786 let import_scopes = build_import_scope_cache(
787 provider_owner,
788 graph,
789 &parsed_imports,
790 configured_source.as_ref(),
791 );
792
793 let mut relation_keys = Vec::new();
794 for (relation_index, relation) in graph.relations.iter().enumerate() {
795 let mut keys = match relation.kind {
796 RelationKind::Imports if semantic::supports_source_dependencies(provider_owner) => {
797 import_dependency_keys(
798 project,
799 provider_owner,
800 &provider,
801 &language,
802 package.as_ref(),
803 &relation.path,
804 &parsed_imports[relation_index],
805 &import_scopes,
806 relation_index,
807 &mut contract_failures,
808 )
809 }
810 RelationKind::Calls if semantic::supports_source_dependencies(provider_owner) => {
811 call_dependency_keys(
812 project,
813 provider_owner,
814 &provider,
815 &language,
816 package.as_ref(),
817 &relation.path,
818 &relation.target_name,
819 &aliases,
820 &import_scopes,
821 relation_index,
822 &mut contract_failures,
823 )
824 }
825 RelationKind::DependsOn if semantic::supports_package_dependencies(provider_owner) => {
826 retain_derived_key(
827 canonical_key(
828 project,
829 ResolutionKeyDomain::Package,
830 &provider,
831 &language,
832 None,
833 None,
834 RelationKind::DependsOn,
835 &relation.target_name,
836 ),
837 ResolutionProjectionFact::Relation(relation_index),
838 &mut contract_failures,
839 )
840 .into_iter()
841 .collect()
842 }
843 RelationKind::Contains => continue,
844 RelationKind::Imports | RelationKind::Calls | RelationKind::DependsOn => Vec::new(),
845 };
846 keys = bounded_keys(keys, "relation", relation_index)?;
847 relation_keys.push(RelationResolutionKeys {
848 relation_index,
849 keys,
850 });
851 }
852
853 let projection = ResolutionKeyProjection {
854 source: source_keys,
855 symbols: symbol_keys,
856 relations: relation_keys,
857 };
858 if contract_failures.failures.is_empty() {
859 Ok(projection)
860 } else {
861 Err(ResolutionProjectionError::Contract(Box::new(
862 ResolutionProjectionFailure {
863 failures: contract_failures.failures,
864 rejected_count: contract_failures.rejected_count,
865 projection,
866 },
867 )))
868 }
869}
870
871fn build_import_scope_cache(
873 provider_owner: SemanticProviderOwner,
874 graph: &SymbolGraph,
875 parsed_imports: &[Vec<ImportReference>],
876 configured_source: Option<&ConfiguredModuleSource<'_>>,
877) -> ImportScopeCache {
878 let mut cache = ImportScopeCache::new();
879 for (relation, references) in graph.relations.iter().zip(parsed_imports) {
880 let caller_path = &relation.path;
881 let modules = cache.entry(caller_path.clone()).or_default();
882 for reference in references {
883 modules
884 .entry(reference.module().to_string())
885 .or_insert_with(|| {
886 semantic::import_scopes(
887 provider_owner,
888 caller_path,
889 reference,
890 configured_source,
891 )
892 });
893 }
894 }
895 cache
896}
897
898fn cached_import_scopes<'a>(
900 cache: &'a ImportScopeCache,
901 caller_path: &str,
902 reference: &ImportReference,
903) -> &'a [String] {
904 cache
905 .get(caller_path)
906 .and_then(|modules| modules.get(reference.module()))
907 .map_or(&[], Vec::as_slice)
908}
909
910fn import_dependency_keys(
912 project: ProjectInstanceId,
913 provider_owner: SemanticProviderOwner,
914 provider: &GraphIdentityText,
915 language: &GraphIdentityText,
916 package: Option<&GraphIdentityText>,
917 caller_path: &str,
918 references: &[ImportReference],
919 import_scopes: &ImportScopeCache,
920 relation_index: usize,
921 contract_failures: &mut ContractFailures,
922) -> Vec<CanonicalResolutionKey> {
923 let mut keys = Vec::new();
924 for reference in references {
925 let scopes = cached_import_scopes(import_scopes, caller_path, reference);
926 let package = dependency_package(provider_owner, reference, package);
927 for scope in scopes {
928 if let Some(key) = retain_derived_key(
929 canonical_key(
930 project,
931 ResolutionKeyDomain::Module,
932 provider,
933 language,
934 package,
935 None,
936 RelationKind::Imports,
937 scope,
938 ),
939 ResolutionProjectionFact::Relation(relation_index),
940 contract_failures,
941 ) {
942 keys.push(key);
943 }
944 }
945 }
946 keys
947}
948
949fn call_dependency_keys(
951 project: ProjectInstanceId,
952 provider_owner: SemanticProviderOwner,
953 provider: &GraphIdentityText,
954 language: &GraphIdentityText,
955 package: Option<&GraphIdentityText>,
956 caller_path: &str,
957 target: &str,
958 aliases: &BTreeMap<&str, Vec<&ImportReference>>,
959 import_scopes: &ImportScopeCache,
960 relation_index: usize,
961 contract_failures: &mut ContractFailures,
962) -> Vec<CanonicalResolutionKey> {
963 let (prefix, remainder) = split_qualified_target(target);
964 let alias = prefix.unwrap_or(target).trim();
965 if let Some(references) = aliases.get(alias) {
966 let mut keys = Vec::new();
967 for reference in references {
968 let identity = remainder.or_else(|| reference.imported());
969 let Some(identity) = identity else {
970 continue;
971 };
972 let scopes = cached_import_scopes(import_scopes, caller_path, reference);
973 let package = dependency_package(provider_owner, reference, package);
974 for scope in scopes {
975 let scoped_parent;
976 let scope = if remainder.is_some()
977 && let Some(imported_parent) = reference.imported()
978 {
979 scoped_parent = format!("{scope}/{imported_parent}");
980 scoped_parent.as_str()
981 } else {
982 scope.as_str()
983 };
984 if let Some(key) = retain_derived_key(
985 canonical_key(
986 project,
987 ResolutionKeyDomain::Declaration,
988 provider,
989 language,
990 package,
991 Some(scope),
992 RelationKind::Calls,
993 identity,
994 ),
995 ResolutionProjectionFact::Relation(relation_index),
996 contract_failures,
997 ) {
998 keys.push(key);
999 }
1000 }
1001 }
1002 return keys;
1003 }
1004
1005 let (scope, identity) = split_qualified_target(target);
1006 if scope.is_some() {
1007 return Vec::new();
1008 }
1009 let Some(identity) = identity.or_else(|| (!target.trim().is_empty()).then_some(target.trim()))
1010 else {
1011 return Vec::new();
1012 };
1013 retain_derived_key(
1014 canonical_key(
1015 project,
1016 ResolutionKeyDomain::Declaration,
1017 provider,
1018 language,
1019 package,
1020 None,
1021 RelationKind::Calls,
1022 identity,
1023 ),
1024 ResolutionProjectionFact::Relation(relation_index),
1025 contract_failures,
1026 )
1027 .into_iter()
1028 .collect()
1029}
1030
1031fn dependency_package<'a>(
1034 provider_owner: SemanticProviderOwner,
1035 reference: &ImportReference,
1036 package: Option<&'a GraphIdentityText>,
1037) -> Option<&'a GraphIdentityText> {
1038 if provider_owner == SemanticProviderOwner::EcmaScript
1039 && !(reference.module().starts_with("./") || reference.module().starts_with("../"))
1040 {
1041 None
1042 } else {
1043 package
1044 }
1045}
1046
1047#[allow(clippy::too_many_arguments)]
1048fn canonical_key(
1050 project: ProjectInstanceId,
1051 domain: ResolutionKeyDomain,
1052 provider: &GraphIdentityText,
1053 language: &GraphIdentityText,
1054 package: Option<&GraphIdentityText>,
1055 scope: Option<&str>,
1056 relation: RelationKind,
1057 identity: &str,
1058) -> Result<CanonicalResolutionKey, GraphContractError> {
1059 let scope = scope.map(GraphIdentityText::new).transpose()?;
1060 let identity = GraphIdentityText::new(identity.to_string())?;
1061 Ok(CanonicalResolutionKey::new(
1062 project,
1063 domain,
1064 provider,
1065 language,
1066 package,
1067 scope.as_ref(),
1068 Some(GraphRelationKind::from_legacy(relation)),
1069 &identity,
1070 ))
1071}
1072
1073fn retain_derived_key(
1075 result: Result<CanonicalResolutionKey, GraphContractError>,
1076 fact: ResolutionProjectionFact,
1077 contract_failures: &mut ContractFailures,
1078) -> Option<CanonicalResolutionKey> {
1079 match result {
1080 Ok(key) => Some(key),
1081 Err(error) => {
1082 remember_contract_failure(contract_failures, fact, error);
1083 None
1084 }
1085 }
1086}
1087
1088fn remember_contract_failure(
1090 contract_failures: &mut ContractFailures,
1091 fact: ResolutionProjectionFact,
1092 error: GraphContractError,
1093) {
1094 contract_failures.remember(fact, error);
1095}
1096
1097fn bounded_keys(
1099 mut keys: Vec<CanonicalResolutionKey>,
1100 fact: &'static str,
1101 index: usize,
1102) -> Result<Vec<CanonicalResolutionKey>, ResolutionProjectionError> {
1103 keys.sort();
1104 keys.dedup();
1105 if keys.len() > MAX_RESOLUTION_KEYS_PER_FACT {
1106 return Err(ResolutionProjectionError::KeyLimit {
1107 fact,
1108 index,
1109 requested: keys.len(),
1110 });
1111 }
1112 Ok(keys)
1113}
1114
1115fn canonical_source_scopes(path: &str) -> Vec<String> {
1117 let mut scopes = source_stems_for_path(path);
1118 scopes.extend(module_aliases_for_path(path));
1119 normalize_scopes(scopes, normalize_repository_scope)
1120}
1121
1122fn canonical_configured_source_scopes(path: &str) -> Vec<String> {
1124 let mut scopes = source_stems_for_path(path)
1125 .into_iter()
1126 .map(|scope| normalize_repository_scope(&scope))
1127 .filter(|scope| !scope.is_empty())
1128 .collect::<Vec<_>>();
1129 scopes.sort();
1130 scopes.dedup();
1131 scopes
1132}
1133
1134fn normalize_scopes(scopes: Vec<String>, normalize: fn(&str) -> String) -> Vec<String> {
1136 let mut normalized = Vec::new();
1137 for scope in scopes {
1138 let scope = normalize(&scope);
1139 if scope.is_empty() {
1140 continue;
1141 }
1142 normalized.push(scope.clone());
1143 if let Some(stripped) = scope.strip_prefix("src/") {
1144 normalized.push(stripped.to_string());
1145 }
1146 if let Some(last) = scope.rsplit('/').next() {
1147 normalized.push(last.to_string());
1148 }
1149 }
1150 normalized.sort();
1151 normalized.dedup();
1152 normalized
1153}
1154
1155fn normalize_repository_scope(value: &str) -> String {
1157 value
1158 .trim()
1159 .trim_start_matches("./")
1160 .replace("::", "/")
1161 .trim_matches('/')
1162 .to_string()
1163}
1164
1165fn split_qualified_target(target: &str) -> (Option<&str>, Option<&str>) {
1167 let target = target.trim();
1168 let rust = target.rsplit_once("::");
1169 let dotted = target.rsplit_once('.');
1170 match (rust, dotted) {
1171 (Some(rust), Some(dotted)) => {
1172 if rust.0.len() >= dotted.0.len() {
1173 (Some(rust.0), Some(rust.1))
1174 } else {
1175 (Some(dotted.0), Some(dotted.1))
1176 }
1177 }
1178 (Some((scope, identity)), None) | (None, Some((scope, identity))) => {
1179 (Some(scope), Some(identity))
1180 }
1181 (None, None) => (None, None),
1182 }
1183}
1184
1185pub(super) fn strip_known_source_extension(path: &str) -> String {
1187 for extension in [
1188 ".d.ts", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".rs",
1189 ] {
1190 if let Some(stem) = path.strip_suffix(extension) {
1191 return stem.to_string();
1192 }
1193 }
1194 path.rsplit_once('.').map_or_else(
1195 || path.to_string(),
1196 |(stem, extension)| {
1197 if extension.contains('/') {
1198 path.to_string()
1199 } else {
1200 stem.to_string()
1201 }
1202 },
1203 )
1204}
1205
1206#[cfg(test)]
1207mod tests {
1208 use super::{
1209 ImportReference, ImportSyntax, ResolutionProjectionContext, ResolutionProjectionError,
1210 SEMANTIC_RESOLUTION_CONTRACT_VERSION, build_import_scope_cache, derive_resolution_keys,
1211 derive_resolution_keys_with_context, parse_import_references, resolve_relative_import_path,
1212 semantic_resolution_contract_digest,
1213 };
1214 use crate::{
1215 ConfiguredModuleResolution, EcmaScriptConfigKind, EcmaScriptModuleConfig,
1216 EcmaScriptPathMapping, extract_symbol_graph,
1217 };
1218 use projectatlas_core::graph::{CanonicalResolutionKey, ProjectInstanceId};
1219 use projectatlas_core::language::SemanticProviderOwner;
1220 use projectatlas_core::symbols::{
1221 CodeSymbol, ParserKind, RelationKind, SymbolGraph, SymbolKind, SymbolRelation,
1222 };
1223 use std::error::Error;
1224 use std::io;
1225
1226 fn reference(
1227 syntax: ImportSyntax,
1228 module: &str,
1229 imported: Option<&str>,
1230 local: &str,
1231 ) -> ImportReference {
1232 ImportReference {
1233 syntax,
1234 module: module.to_string(),
1235 imported: imported.map(ToString::to_string),
1236 local: local.to_string(),
1237 }
1238 }
1239
1240 #[test]
1241 fn semantic_resolution_contract_digest_is_bounded_and_deterministic() {
1242 const PRE_MODULE_CALLBACK_DIGEST: &str =
1243 "487625adf2f9ec76f98034d4ef5667e707960b6b8afd280b213021cb64a0f10f";
1244 let first = semantic_resolution_contract_digest();
1245 assert_eq!(SEMANTIC_RESOLUTION_CONTRACT_VERSION, 3);
1246 assert_eq!(first.len(), 64);
1247 assert!(first.bytes().all(|byte| byte.is_ascii_hexdigit()));
1248 assert_eq!(first, semantic_resolution_contract_digest());
1249 assert_ne!(first, PRE_MODULE_CALLBACK_DIGEST);
1250 }
1251
1252 #[test]
1253 fn parses_language_import_aliases_without_display_statement_identity() {
1254 assert_eq!(
1255 parse_import_references("use crate::worker::{run as execute, stop};"),
1256 vec![
1257 reference(ImportSyntax::Rust, "crate::worker", Some("run"), "execute"),
1258 reference(ImportSyntax::Rust, "crate::worker", Some("stop"), "stop"),
1259 ]
1260 );
1261 assert_eq!(
1262 parse_import_references("use crate::worker::run_function_alias as run_rust_function;"),
1263 vec![reference(
1264 ImportSyntax::Rust,
1265 "crate::worker",
1266 Some("run_function_alias"),
1267 "run_rust_function"
1268 ),]
1269 );
1270 assert_eq!(
1271 parse_import_references("import * as reader from './reader';"),
1272 vec![reference(
1273 ImportSyntax::EcmaScript,
1274 "./reader",
1275 None,
1276 "reader",
1277 )]
1278 );
1279 assert_eq!(
1280 parse_import_references("import { runAlias as runLocal } from './worker';"),
1281 vec![reference(
1282 ImportSyntax::EcmaScript,
1283 "./worker",
1284 Some("runAlias"),
1285 "runLocal",
1286 )]
1287 );
1288 assert_eq!(
1289 parse_import_references("from package.reader import read as load, close"),
1290 vec![
1291 reference(
1292 ImportSyntax::Python,
1293 "package.reader",
1294 Some("close"),
1295 "close"
1296 ),
1297 reference(ImportSyntax::Python, "package.reader", Some("read"), "load"),
1298 ]
1299 );
1300 assert_eq!(
1301 parse_import_references("from package.worker import run_alias as execute"),
1302 vec![reference(
1303 ImportSyntax::Python,
1304 "package.worker",
1305 Some("run_alias"),
1306 "execute",
1307 )]
1308 );
1309 assert!(parse_import_references("import { broken from './reader'").is_empty());
1310 assert!(parse_import_references("use crate::worker::run as ;").is_empty());
1311 assert!(parse_import_references("import { read } from module './reader'").is_empty());
1312 for malformed in [
1313 "use crate::worker::{run, nested::{start, stop}};",
1314 "use crate::worker::{run, broken as };",
1315 "import defaultValue, { read } from './reader';",
1316 "import { read, nested { broken } } from './reader';",
1317 "import { read as load as fetch } from './reader';",
1318 "from package.reader import (read, close)",
1319 "from package.reader import read, broken as ",
1320 "import package.reader as reader as other",
1321 ] {
1322 assert!(
1323 parse_import_references(malformed).is_empty(),
1324 "unsupported syntax fabricated references: {malformed}"
1325 );
1326 }
1327 }
1328
1329 #[test]
1330 fn resolves_relative_imports_without_platform_path_rules() {
1331 assert_eq!(
1332 resolve_relative_import_path("src/features/main.ts", "../shared/reader.ts"),
1333 Some("src/shared/reader".to_string())
1334 );
1335 assert_eq!(
1336 resolve_relative_import_path("src/main.ts", "../../outside"),
1337 None
1338 );
1339 assert_eq!(resolve_relative_import_path("src/main.ts", "reader"), None);
1340 assert_eq!(
1341 resolve_relative_import_path("src/foo.bar/main.ts", "./reader"),
1342 Some("src/foo.bar/reader".to_string())
1343 );
1344 }
1345
1346 #[test]
1347 fn real_language_graphs_share_alias_qualified_keys() -> Result<(), Box<dyn Error>> {
1348 let project = project_id(1)?;
1349
1350 let rust_target = extract_symbol_graph("src/worker.rs", Some("rust"), "pub fn run() {}\n");
1351 let rust_caller = extract_symbol_graph(
1352 "src/main.rs",
1353 Some("rust"),
1354 "use crate::worker::run as execute;\nfn main() { execute(); }\n",
1355 );
1356 assert_alias_resolution(
1357 project,
1358 Some("atlas"),
1359 &rust_target,
1360 "run",
1361 &rust_caller,
1362 &["execute"],
1363 )?;
1364 let rust_module_target = extract_symbol_graph(
1365 "src/config.rs",
1366 Some("rust"),
1367 "pub fn load_timeout_millis() -> u64 { 250 }\n",
1368 );
1369 let rust_module_caller = extract_symbol_graph(
1370 "src/handler.rs",
1371 Some("rust"),
1372 "use crate::config;\npub fn health_response() { config::load_timeout_millis(); }\n",
1373 );
1374 assert_alias_resolution(
1375 project,
1376 Some("atlas"),
1377 &rust_module_target,
1378 "load_timeout_millis",
1379 &rust_module_caller,
1380 &["config::load_timeout_millis"],
1381 )?;
1382 let rust_callback_caller = extract_symbol_graph(
1383 "src/router.rs",
1384 Some("rust"),
1385 "use crate::handler;\nfn dispatch(path: &str) -> Option<()> { (path == \"/health\").then(handler::health_response) }\n",
1386 );
1387 assert_alias_resolution(
1388 project,
1389 Some("atlas"),
1390 &rust_module_caller,
1391 "health_response",
1392 &rust_callback_caller,
1393 &["handler::health_response"],
1394 )?;
1395 let user_defined_then = extract_symbol_graph(
1396 "src/scheduler.rs",
1397 Some("rust"),
1398 "use crate::handler;\nstruct Scheduler;\nimpl Scheduler { fn then(self, callback: fn()) { let _ = callback; } }\nfn schedule(scheduler: Scheduler) { scheduler.then(handler::health_response); }\n",
1399 );
1400 if user_defined_then.relations.iter().any(|relation| {
1401 relation.kind == RelationKind::Calls
1402 && relation.target_name == "handler::health_response"
1403 }) {
1404 return Err(io::Error::other(
1405 "a user-defined then method fabricated a callback call edge",
1406 )
1407 .into());
1408 }
1409
1410 let typescript_target = extract_symbol_graph(
1411 "src/shared/reader.ts",
1412 Some("typescript"),
1413 "export function read() {}\n",
1414 );
1415 let typescript_caller = extract_symbol_graph(
1416 "src/features/main.ts",
1417 Some("typescript"),
1418 "import { read as load } from '../shared/reader';\nimport * as reader from '../shared/reader';\nexport function main() { load(); reader.read(); }\n",
1419 );
1420 assert_alias_resolution(
1421 project,
1422 Some("web"),
1423 &typescript_target,
1424 "read",
1425 &typescript_caller,
1426 &["load", "reader.read"],
1427 )?;
1428
1429 let dotted_directory_target = extract_symbol_graph(
1430 "src/foo.bar/reader.ts",
1431 Some("typescript"),
1432 "export function read() {}\n",
1433 );
1434 let dotted_directory_caller = extract_symbol_graph(
1435 "src/foo.bar/main.ts",
1436 Some("typescript"),
1437 "import { read } from './reader';\nexport function main() { read(); }\n",
1438 );
1439 assert_alias_resolution(
1440 project,
1441 Some("web"),
1442 &dotted_directory_target,
1443 "read",
1444 &dotted_directory_caller,
1445 &["read"],
1446 )?;
1447
1448 let python_target = extract_symbol_graph(
1449 "src/package/reader.py",
1450 Some("python"),
1451 "def read():\n pass\n",
1452 );
1453 let python_caller = extract_symbol_graph(
1454 "src/main.py",
1455 Some("python"),
1456 "import package.reader as reader\nreader.read()\n",
1457 );
1458 assert_alias_resolution(
1459 project,
1460 Some("python-app"),
1461 &python_target,
1462 "read",
1463 &python_caller,
1464 &["reader.read"],
1465 )?;
1466 Ok(())
1467 }
1468
1469 #[test]
1470 fn configured_aliases_share_file_and_declaration_keys_across_ecmascript_hosts()
1471 -> Result<(), Box<dyn Error>> {
1472 let project = project_id(21)?;
1473 let configured = ConfiguredModuleResolution::new(vec![
1474 EcmaScriptModuleConfig::new(
1475 "tsconfig.json",
1476 EcmaScriptConfigKind::TypeScript,
1477 Some("src".to_string()),
1478 vec![
1479 EcmaScriptPathMapping::new("@/*", vec!["src/*".to_string()])?,
1480 EcmaScriptPathMapping::new("@index/*", vec!["src/*/index.ts".to_string()])?,
1481 ],
1482 )?,
1483 EcmaScriptModuleConfig::new(
1484 "jsconfig.json",
1485 EcmaScriptConfigKind::JavaScript,
1486 Some("src".to_string()),
1487 vec![EcmaScriptPathMapping::new(
1488 "@/*",
1489 vec!["src/*".to_string()],
1490 )?],
1491 )?,
1492 ])?;
1493 let context = ResolutionProjectionContext::with_configured_modules(&configured);
1494
1495 for (target_path, language, target_source, caller_path, caller_source) in [
1496 (
1497 "src/controller.ts",
1498 "typescript",
1499 "export function useController(): string { return 'ok'; }\n",
1500 "src/page.ts",
1501 "import { useController } from '@/controller';\nexport const value = useController();\n",
1502 ),
1503 (
1504 "src/controller.ts",
1505 "typescript",
1506 "export function useController(): string { return 'ok'; }\n",
1507 "src/page.tsx",
1508 "import { useController } from '@/controller';\nexport function Page() { useController(); return <div />; }\n",
1509 ),
1510 (
1511 "src/controller.js",
1512 "javascript",
1513 "export function useController() { return 'ok'; }\n",
1514 "src/page.jsx",
1515 "import { useController } from '@/controller';\nexport function Page() { useController(); return <div />; }\n",
1516 ),
1517 (
1518 "src/controller.ts",
1519 "typescript",
1520 "export function useController(): string { return 'ok'; }\n",
1521 "src/Page.vue",
1522 "<script setup lang=\"ts\">\nimport { useController } from '@/controller';\nconst value = useController();\n</script>\n<template><div>{{ value }}</div></template>\n",
1523 ),
1524 ] {
1525 let target = extract_symbol_graph(target_path, Some(language), target_source);
1526 let caller_extension = std::path::Path::new(caller_path)
1527 .extension()
1528 .and_then(std::ffi::OsStr::to_str);
1529 let caller_language = match caller_extension {
1530 Some(extension) if extension.eq_ignore_ascii_case("vue") => "vue",
1531 Some(extension)
1532 if extension.eq_ignore_ascii_case("js")
1533 || extension.eq_ignore_ascii_case("jsx") =>
1534 {
1535 "javascript"
1536 }
1537 Some(extension) if extension.eq_ignore_ascii_case("tsx") => "tsx",
1538 _ => "typescript",
1539 };
1540 let caller = extract_symbol_graph(caller_path, Some(caller_language), caller_source);
1541 let target_projection = derive_resolution_keys(project, Some("shared"), &target)?;
1542 let caller_projection =
1543 derive_resolution_keys_with_context(project, Some("app"), &caller, context)?;
1544 let imports = relation_keys_of_kind(&caller, &caller_projection, RelationKind::Imports);
1545 require(
1546 imports
1547 .iter()
1548 .any(|keys| keys_intersect(keys, target_projection.source_keys())),
1549 "configured import did not share the target file key",
1550 )?;
1551 require(
1552 keys_intersect(
1553 relation_keys(&caller, &caller_projection, "useController"),
1554 symbol_keys(&target, &target_projection, "useController"),
1555 ),
1556 "configured call did not share the target declaration key",
1557 )?;
1558 }
1559
1560 let index_target = extract_symbol_graph(
1561 "src/tools/index.ts",
1562 Some("typescript"),
1563 "export function useTool() { return 'ok'; }\n",
1564 );
1565 let index_caller = extract_symbol_graph(
1566 "src/index-page.ts",
1567 Some("typescript"),
1568 "import { useTool } from '@index/tools';\nexport const value = useTool();\n",
1569 );
1570 let index_target_projection = derive_resolution_keys(project, Some("app"), &index_target)?;
1571 let index_caller_projection =
1572 derive_resolution_keys_with_context(project, Some("app"), &index_caller, context)?;
1573 require(
1574 relation_keys_of_kind(
1575 &index_caller,
1576 &index_caller_projection,
1577 RelationKind::Imports,
1578 )
1579 .iter()
1580 .any(|keys| keys_intersect(keys, index_target_projection.source_keys())),
1581 "configured index target did not reuse package-entry source keys",
1582 )?;
1583 require(
1584 keys_intersect(
1585 relation_keys(&index_caller, &index_caller_projection, "useTool"),
1586 symbol_keys(&index_target, &index_target_projection, "useTool"),
1587 ),
1588 "configured index target did not reuse declaration keys",
1589 )?;
1590
1591 let extension_caller = extract_symbol_graph(
1592 "src/extension-page.ts",
1593 Some("typescript"),
1594 "import { useController } from '@/controller.ts';\nexport const value = useController();\n",
1595 );
1596 let extension_target = extract_symbol_graph(
1597 "src/controller.ts",
1598 Some("typescript"),
1599 "export function useController(): string { return 'ok'; }\n",
1600 );
1601 let extension_caller_projection =
1602 derive_resolution_keys_with_context(project, Some("app"), &extension_caller, context)?;
1603 let extension_target_projection =
1604 derive_resolution_keys(project, Some("app"), &extension_target)?;
1605 require(
1606 relation_keys_of_kind(
1607 &extension_caller,
1608 &extension_caller_projection,
1609 RelationKind::Imports,
1610 )
1611 .iter()
1612 .any(|keys| keys_intersect(keys, extension_target_projection.source_keys())),
1613 "configured extension target did not reuse extensionless source keys",
1614 )?;
1615
1616 let root_configured = ConfiguredModuleResolution::new(vec![EcmaScriptModuleConfig::new(
1617 "tsconfig.json",
1618 EcmaScriptConfigKind::TypeScript,
1619 Some(String::new()),
1620 Vec::new(),
1621 )?])?;
1622 let root_context = ResolutionProjectionContext::with_configured_modules(&root_configured);
1623 let root_target = extract_symbol_graph(
1624 "controller.ts",
1625 Some("typescript"),
1626 "export function useController() { return 'root'; }\n",
1627 );
1628 let nested_same_name = extract_symbol_graph(
1629 "packages/app/src/controller.ts",
1630 Some("typescript"),
1631 "export function useController() { return 'nested'; }\n",
1632 );
1633 let root_caller = extract_symbol_graph(
1634 "page.ts",
1635 Some("typescript"),
1636 "import { useController } from 'controller';\nexport const value = useController();\n",
1637 );
1638 let root_target_projection = derive_resolution_keys(project, Some("shared"), &root_target)?;
1639 let nested_projection = derive_resolution_keys(project, Some("nested"), &nested_same_name)?;
1640 let root_caller_projection =
1641 derive_resolution_keys_with_context(project, Some("app"), &root_caller, root_context)?;
1642 let configured_imports =
1643 relation_keys_of_kind(&root_caller, &root_caller_projection, RelationKind::Imports);
1644 require(
1645 configured_imports
1646 .iter()
1647 .any(|keys| keys_intersect(keys, root_target_projection.source_keys())),
1648 "root baseUrl import did not resolve its repository-exact target",
1649 )?;
1650 require(
1651 configured_imports
1652 .iter()
1653 .all(|keys| !keys_intersect(keys, nested_projection.source_keys())),
1654 "root baseUrl import also matched a nested basename alias",
1655 )?;
1656 let configured_call = relation_keys(&root_caller, &root_caller_projection, "useController");
1657 require(
1658 keys_intersect(
1659 configured_call,
1660 symbol_keys(&root_target, &root_target_projection, "useController"),
1661 ),
1662 "root baseUrl call did not resolve its repository-exact declaration",
1663 )?;
1664 require(
1665 !keys_intersect(
1666 configured_call,
1667 symbol_keys(&nested_same_name, &nested_projection, "useController"),
1668 ),
1669 "root baseUrl call also matched a nested basename alias",
1670 )?;
1671 Ok(())
1672 }
1673
1674 #[test]
1675 fn configured_import_scopes_are_expanded_once_per_source_graph() -> Result<(), Box<dyn Error>> {
1676 let configured = ConfiguredModuleResolution::new(vec![EcmaScriptModuleConfig::new(
1677 "tsconfig.json",
1678 EcmaScriptConfigKind::TypeScript,
1679 Some("src".to_string()),
1680 vec![EcmaScriptPathMapping::new(
1681 "@/*",
1682 vec!["src/*".to_string()],
1683 )?],
1684 )?])?;
1685 let graph = extract_symbol_graph(
1686 "src/page.ts",
1687 Some("typescript"),
1688 "import { useController } from '@/controller';\n\
1689 export const first = useController();\n\
1690 export const second = useController();\n",
1691 );
1692 let parsed_imports = graph
1693 .relations
1694 .iter()
1695 .map(|relation| {
1696 if relation.kind == RelationKind::Imports {
1697 crate::semantic::parse_imports(
1698 SemanticProviderOwner::EcmaScript,
1699 &relation.target_name,
1700 )
1701 } else {
1702 Vec::new()
1703 }
1704 })
1705 .collect::<Vec<_>>();
1706 let configured_source = configured.for_source(&graph.path);
1707 let cache = build_import_scope_cache(
1708 SemanticProviderOwner::EcmaScript,
1709 &graph,
1710 &parsed_imports,
1711 Some(&configured_source),
1712 );
1713 let modules = cache
1714 .get("src/page.ts")
1715 .ok_or_else(|| io::Error::other("source graph omitted its import-scope cache"))?;
1716 if modules.len() != 1 || modules.get("@/controller") != Some(&vec!["src/controller".into()])
1717 {
1718 return Err(io::Error::other(
1719 "repeated imported calls did not reuse one configured scope expansion",
1720 )
1721 .into());
1722 }
1723 Ok(())
1724 }
1725
1726 #[test]
1727 fn imports_target_modules_and_qualified_calls_target_members() -> Result<(), Box<dyn Error>> {
1728 let project = project_id(10)?;
1729 let target = extract_symbol_graph(
1730 "src/shared/reader.ts",
1731 Some("typescript"),
1732 "export function read() {}\nexport function close() {}\nexport function fetch() {}\nexport const client = { fetch() {} };\n",
1733 );
1734 let caller = extract_symbol_graph(
1735 "src/features/main.ts",
1736 Some("typescript"),
1737 "import { read, close, client } from '../shared/reader';\nexport function main() { read(); close(); client.fetch(); }\n",
1738 );
1739 let target_projection = derive_resolution_keys(project, Some("web"), &target)?;
1740 let caller_projection = derive_resolution_keys(project, Some("web"), &caller)?;
1741 let imports = relation_keys_of_kind(&caller, &caller_projection, RelationKind::Imports);
1742 require(
1743 imports.len() == 1,
1744 "one import emitted more than one module target",
1745 )?;
1746 require(
1747 imports[0]
1748 .iter()
1749 .all(|key| key.domain() == projectatlas_core::graph::ResolutionKeyDomain::Module),
1750 "named imports emitted declaration resolution targets",
1751 )?;
1752 require(
1753 keys_intersect(imports[0], target_projection.source_keys()),
1754 "named import did not target its source module",
1755 )?;
1756 for symbol in ["read", "close"] {
1757 require(
1758 !keys_intersect(imports[0], symbol_keys(&target, &target_projection, symbol)),
1759 "module import also targeted a named declaration",
1760 )?;
1761 }
1762 assert_alias_resolution(project, Some("web"), &target, "read", &caller, &["read"])?;
1763 assert_alias_resolution(project, Some("web"), &target, "close", &caller, &["close"])?;
1764 let qualified_call = relation_keys(&caller, &caller_projection, "client.fetch");
1765 let fetch_candidates = target_projection
1766 .symbol_keys()
1767 .iter()
1768 .filter(|entry| target.symbols[entry.symbol_index()].name == "fetch")
1769 .map(|entry| {
1770 (
1771 target.symbols[entry.symbol_index()].parent.as_deref(),
1772 entry.keys(),
1773 )
1774 })
1775 .collect::<Vec<_>>();
1776 require(
1777 fetch_candidates.len() == 2,
1778 "qualified-call fixture lost one fetch declaration",
1779 )?;
1780 require(
1781 fetch_candidates.iter().any(|(parent, keys)| {
1782 *parent == Some("client") && keys_intersect(qualified_call, keys)
1783 }),
1784 "qualified call did not target the imported parent member",
1785 )?;
1786 require(
1787 fetch_candidates
1788 .iter()
1789 .all(|(parent, keys)| parent.is_some() || !keys_intersect(qualified_call, keys)),
1790 "qualified call also targeted a same-module top-level declaration",
1791 )
1792 }
1793
1794 #[test]
1795 fn actual_complex_import_syntax_abstains_instead_of_fabricating_keys()
1796 -> Result<(), Box<dyn Error>> {
1797 let project = project_id(15)?;
1798 for (path, language, source) in [
1799 (
1800 "src/main.rs",
1801 "rust",
1802 "use crate::worker::{run, nested::{start, stop}};\nfn main() { run(); }\n",
1803 ),
1804 (
1805 "src/main.ts",
1806 "typescript",
1807 "import defaultValue, { read } from './reader';\ndefaultValue();\n",
1808 ),
1809 (
1810 "src/main.py",
1811 "python",
1812 "from package.reader import (read, close)\nread()\n",
1813 ),
1814 ] {
1815 let graph = extract_symbol_graph(path, Some(language), source);
1816 let projection = derive_resolution_keys(project, Some("app"), &graph)?;
1817 let imports = relation_keys_of_kind(&graph, &projection, RelationKind::Imports);
1818 require(
1819 !imports.is_empty(),
1820 "complex-import fixture did not reach provider normalization",
1821 )?;
1822 require(
1823 imports.iter().all(|keys| keys.is_empty()),
1824 "unsupported complex import syntax fabricated resolution keys",
1825 )?;
1826 }
1827 Ok(())
1828 }
1829
1830 #[test]
1831 fn provider_scopes_abstain_instead_of_matching_packages_or_wrong_siblings()
1832 -> Result<(), Box<dyn Error>> {
1833 let project = project_id(11)?;
1834 let correct_typescript = extract_symbol_graph(
1835 "src/a/reader.ts",
1836 Some("typescript"),
1837 "export function read() {}\n",
1838 );
1839 let wrong_typescript = extract_symbol_graph(
1840 "src/b/reader.ts",
1841 Some("typescript"),
1842 "export function read() {}\n",
1843 );
1844 let typescript_caller = extract_symbol_graph(
1845 "src/main.ts",
1846 Some("typescript"),
1847 "import { read } from './a/reader';\nread();\n",
1848 );
1849 assert_import_matches_only_source(
1850 project,
1851 Some("web"),
1852 &typescript_caller,
1853 &correct_typescript,
1854 &wrong_typescript,
1855 )?;
1856
1857 let package_caller = extract_symbol_graph(
1858 "src/package.ts",
1859 Some("typescript"),
1860 "import { read } from 'reader-package';\nread();\n",
1861 );
1862 let package_projection = derive_resolution_keys(project, Some("web"), &package_caller)?;
1863 require(
1864 relation_keys_of_kind(&package_caller, &package_projection, RelationKind::Imports)
1865 .iter()
1866 .all(|keys| keys.is_empty()),
1867 "bare ECMAScript package import entered a repository-local module scope",
1868 )?;
1869 require(
1870 relation_keys(&package_caller, &package_projection, "read").is_empty(),
1871 "call through an unresolved package import fell back to a global declaration",
1872 )?;
1873
1874 for (path, language, source, target) in [
1875 (
1876 "src/unimported.ts",
1877 "typescript",
1878 "export function run() { return client.fetch(); }\n",
1879 "client.fetch",
1880 ),
1881 (
1882 "src/unimported.rs",
1883 "rust",
1884 "pub fn run() { worker::execute(); }\n",
1885 "worker::execute",
1886 ),
1887 (
1888 "src/unimported.py",
1889 "python",
1890 "def run():\n return reader.read()\n",
1891 "reader.read",
1892 ),
1893 ] {
1894 let graph = extract_symbol_graph(path, Some(language), source);
1895 let projection = derive_resolution_keys(project, Some("app"), &graph)?;
1896 require(
1897 graph.relations.iter().any(|relation| {
1898 relation.kind == RelationKind::Calls && relation.target_name == target
1899 }),
1900 "unimported qualified-call fixture did not reach semantic normalization",
1901 )?;
1902 require(
1903 relation_keys(&graph, &projection, target).is_empty(),
1904 "unimported qualified call fell back to a project basename scope",
1905 )?;
1906 }
1907
1908 let rust_target =
1909 extract_symbol_graph("src/feature/worker.rs", Some("rust"), "pub fn run() {}\n");
1910 let wrong_rust =
1911 extract_symbol_graph("src/other/worker.rs", Some("rust"), "pub fn run() {}\n");
1912 let rust_caller = extract_symbol_graph(
1913 "src/feature/main.rs",
1914 Some("rust"),
1915 "use super::worker::run;\nfn main() { run(); }\n",
1916 );
1917 let rust_crate_caller = extract_symbol_graph(
1918 "src/main.rs",
1919 Some("rust"),
1920 "use crate::feature::worker::run;\nfn main() { run(); }\n",
1921 );
1922 let rust_self_caller = extract_symbol_graph(
1923 "src/feature/mod.rs",
1924 Some("rust"),
1925 "use self::worker::run;\nfn main() { run(); }\n",
1926 );
1927 for caller in [&rust_caller, &rust_crate_caller, &rust_self_caller] {
1928 assert_import_matches_only_source(
1929 project,
1930 Some("atlas"),
1931 caller,
1932 &rust_target,
1933 &wrong_rust,
1934 )?;
1935 }
1936
1937 let python_target = extract_symbol_graph(
1938 "src/package/reader.py",
1939 Some("python"),
1940 "def read():\n pass\n",
1941 );
1942 let wrong_python = extract_symbol_graph(
1943 "src/other/reader.py",
1944 Some("python"),
1945 "def read():\n pass\n",
1946 );
1947 let python_caller = extract_symbol_graph(
1948 "src/package/main.py",
1949 Some("python"),
1950 "from .reader import read\nread()\n",
1951 );
1952 assert_import_matches_only_source(
1953 project,
1954 Some("python-app"),
1955 &python_caller,
1956 &python_target,
1957 &wrong_python,
1958 )?;
1959 let parent_python_target = extract_symbol_graph(
1960 "src/package/reader.py",
1961 Some("python"),
1962 "def read():\n pass\n",
1963 );
1964 let parent_python_wrong = extract_symbol_graph(
1965 "src/package/sub/reader.py",
1966 Some("python"),
1967 "def read():\n pass\n",
1968 );
1969 let parent_python_caller = extract_symbol_graph(
1970 "src/package/sub/main.py",
1971 Some("python"),
1972 "from ..reader import read\nread()\n",
1973 );
1974 assert_import_matches_only_source(
1975 project,
1976 Some("python-app"),
1977 &parent_python_caller,
1978 &parent_python_target,
1979 &parent_python_wrong,
1980 )
1981 }
1982
1983 #[test]
1984 fn direct_provider_families_retain_duplicate_ambiguity_keys() -> Result<(), Box<dyn Error>> {
1985 let project = project_id(12)?;
1986 for (path, language, target_source, caller_path, caller_source, target_name) in [
1987 (
1988 "src/shared/reader.ts",
1989 "typescript",
1990 "export function read() {}\nexport function read() {}\n",
1991 "src/main.ts",
1992 "import { read } from './shared/reader';\nread();\n",
1993 "read",
1994 ),
1995 (
1996 "src/package/reader.py",
1997 "python",
1998 "def read():\n pass\ndef read():\n pass\n",
1999 "src/main.py",
2000 "from package.reader import read\nread()\n",
2001 "read",
2002 ),
2003 ] {
2004 let target = extract_symbol_graph(path, Some(language), target_source);
2005 let caller = extract_symbol_graph(caller_path, Some(language), caller_source);
2006 let target_projection = derive_resolution_keys(project, Some("app"), &target)?;
2007 let caller_projection = derive_resolution_keys(project, Some("app"), &caller)?;
2008 let duplicate_keys = target_projection
2009 .symbol_keys()
2010 .iter()
2011 .filter(|entry| target.symbols[entry.symbol_index()].name == target_name)
2012 .map(super::SymbolResolutionKeys::keys)
2013 .collect::<Vec<_>>();
2014 require(
2015 duplicate_keys.len() == 2,
2016 "duplicate declarations were not both projected",
2017 )?;
2018 let dependency = relation_keys(&caller, &caller_projection, target_name);
2019 require(
2020 duplicate_keys
2021 .iter()
2022 .all(|keys| keys_intersect(dependency, keys)),
2023 "call dependency did not retain every duplicate declaration candidate",
2024 )?;
2025 }
2026 Ok(())
2027 }
2028
2029 #[test]
2030 fn resolution_identity_separates_provider_from_cross_dialect_family()
2031 -> Result<(), Box<dyn Error>> {
2032 let project = project_id(13)?;
2033 let typescript = extract_symbol_graph(
2034 "src/shared/reader.ts",
2035 Some("typescript"),
2036 "export function read() {}\n",
2037 );
2038 let javascript = extract_symbol_graph(
2039 "src/shared/reader.ts",
2040 Some("javascript"),
2041 "export function read() {}\n",
2042 );
2043 let typescript_projection = derive_resolution_keys(project, Some("web"), &typescript)?;
2044 let javascript_projection = derive_resolution_keys(project, Some("web"), &javascript)?;
2045 require(
2046 typescript_projection.source_keys() == javascript_projection.source_keys(),
2047 "ECMAScript-compatible dialects entered different resolution families",
2048 )?;
2049 let provider = projectatlas_core::graph::GraphIdentityText::new("ecma-script")?;
2050 let family = projectatlas_core::graph::GraphIdentityText::new("ecmascript")?;
2051 let package = projectatlas_core::graph::GraphIdentityText::new("web")?;
2052 let identity = projectatlas_core::graph::GraphIdentityText::new("src/shared/reader")?;
2053 let expected = CanonicalResolutionKey::new(
2054 project,
2055 projectatlas_core::graph::ResolutionKeyDomain::Module,
2056 &provider,
2057 &family,
2058 Some(&package),
2059 None,
2060 Some(projectatlas_core::graph::GraphRelationKind::from_legacy(
2061 RelationKind::Imports,
2062 )),
2063 &identity,
2064 );
2065 require(
2066 typescript_projection.source_keys().contains(&expected),
2067 "provider owner and cross-dialect family were not projected independently",
2068 )
2069 }
2070
2071 #[test]
2072 fn embedded_hosts_publish_module_keys_only_for_admitted_component_facts()
2073 -> Result<(), Box<dyn Error>> {
2074 let project = project_id(14)?;
2075 for (path, language, source) in [
2076 (
2077 "page.html",
2078 "html",
2079 "<script>export function run() {}</script>",
2080 ),
2081 ("empty.vue", "vue", "<template><p>empty</p></template>"),
2082 (
2083 "external.vue",
2084 "vue",
2085 "<script src='./external.js'></script>",
2086 ),
2087 ("empty.svelte", "svelte", "<p>empty</p>"),
2088 (
2089 "external.svelte",
2090 "svelte",
2091 "<script src='./external.js'></script>",
2092 ),
2093 (
2094 "external-inline.vue",
2095 "vue",
2096 "<script>import * as fs from 'node:fs';</script>",
2097 ),
2098 (
2099 "external-inline.svelte",
2100 "svelte",
2101 "<script>import * as fs from 'node:fs';</script>",
2102 ),
2103 ("malformed.vue", "vue", "<script"),
2104 ("malformed.svelte", "svelte", "<script"),
2105 ] {
2106 let graph = extract_symbol_graph(path, Some(language), source);
2107 let projection = derive_resolution_keys(project, Some("web"), &graph)?;
2108 require(
2109 projection.source_keys().is_empty(),
2110 "host without an admitted component module surface emitted source keys",
2111 )?;
2112 if path.starts_with("external-inline") {
2113 require(
2114 relation_keys_of_kind(&graph, &projection, RelationKind::Imports).len() == 1,
2115 "external-only component lost its outbound import relationship",
2116 )?;
2117 }
2118 }
2119 for (path, language) in [("component.vue", "vue"), ("component.svelte", "svelte")] {
2120 let graph = extract_symbol_graph(
2121 path,
2122 Some(language),
2123 "<script>export function run() {}</script>",
2124 );
2125 require(
2126 !derive_resolution_keys(project, Some("web"), &graph)?
2127 .source_keys()
2128 .is_empty(),
2129 "admitted component script facts did not expose their host module",
2130 )?;
2131 }
2132 Ok(())
2133 }
2134
2135 #[test]
2136 fn canonical_projection_is_stable_and_retains_ambiguous_unresolved_and_package_keys()
2137 -> Result<(), Box<dyn Error>> {
2138 let project = project_id(2)?;
2139 let first = extract_symbol_graph("src/worker.rs", Some("rust"), "pub fn run() {}\n");
2140 let moved = extract_symbol_graph(
2141 "src/worker.rs",
2142 Some("rust"),
2143 "\n\n// formatting moved the declaration\npub fn run() {}\n",
2144 );
2145 let first_projection = derive_resolution_keys(project, Some("atlas"), &first)?;
2146 let moved_projection = derive_resolution_keys(project, Some("atlas"), &moved)?;
2147 require(
2148 symbol_keys(&first, &first_projection, "run")
2149 == symbol_keys(&moved, &moved_projection, "run"),
2150 "formatting-only line movement changed canonical symbol identity",
2151 )?;
2152 require(
2153 first_projection.source_keys()
2154 != derive_resolution_keys(project_id(3)?, Some("atlas"), &first)?.source_keys(),
2155 "different projects produced the same canonical source identity",
2156 )?;
2157 require(
2158 symbol_keys(&first, &first_projection, "run")
2159 != symbol_keys(
2160 &first,
2161 &derive_resolution_keys(project, Some("other-package"), &first)?,
2162 "run",
2163 ),
2164 "different packages produced the same canonical declaration identity",
2165 )?;
2166
2167 let duplicate = extract_symbol_graph("src/other.rs", Some("rust"), "pub fn run() {}\n");
2168 let caller = extract_symbol_graph(
2169 "src/main.rs",
2170 Some("rust"),
2171 "fn main() { run(); missing(); }\n",
2172 );
2173 let duplicate_projection = derive_resolution_keys(project, Some("atlas"), &duplicate)?;
2174 let caller_projection = derive_resolution_keys(project, Some("atlas"), &caller)?;
2175 let run_dependencies = relation_keys(&caller, &caller_projection, "run");
2176 require(
2177 keys_intersect(
2178 run_dependencies,
2179 symbol_keys(&first, &first_projection, "run"),
2180 ),
2181 "ambiguous call did not retain the first matching declaration key",
2182 )?;
2183 require(
2184 keys_intersect(
2185 run_dependencies,
2186 symbol_keys(&duplicate, &duplicate_projection, "run"),
2187 ),
2188 "ambiguous call did not retain the duplicate declaration key",
2189 )?;
2190 let missing_dependencies = relation_keys(&caller, &caller_projection, "missing");
2191 require(
2192 !missing_dependencies.is_empty(),
2193 "unresolved call lost its canonical dependency key",
2194 )?;
2195 require(
2196 !keys_intersect(
2197 missing_dependencies,
2198 symbol_keys(&first, &first_projection, "run"),
2199 ),
2200 "unresolved call matched an unrelated declaration key",
2201 )?;
2202
2203 let package = extract_symbol_graph(
2204 "vendor/library/Cargo.toml",
2205 Some("cargo-manifest"),
2206 "[package]\nname = \"library\"\n",
2207 );
2208 let manifest = extract_symbol_graph(
2209 "Cargo.toml",
2210 Some("cargo-manifest"),
2211 "[package]\nname = \"app\"\n[dependencies]\nlibrary = \"1\"\n",
2212 );
2213 let package_projection = derive_resolution_keys(project, None, &package)?;
2214 let manifest_projection = derive_resolution_keys(project, None, &manifest)?;
2215 require(
2216 package_projection.source_keys().is_empty()
2217 && manifest_projection.source_keys().is_empty(),
2218 "Cargo manifests advertised ordinary source-module identities",
2219 )?;
2220 require(
2221 keys_intersect(
2222 relation_keys(&manifest, &manifest_projection, "library"),
2223 symbol_keys(&package, &package_projection, "library"),
2224 ),
2225 "Cargo dependency did not match its package declaration key",
2226 )?;
2227 Ok(())
2228 }
2229
2230 #[test]
2231 fn cargo_provider_abstains_on_malformed_input_and_retains_duplicate_package_candidates()
2232 -> Result<(), Box<dyn Error>> {
2233 let project = project_id(16)?;
2234 let malformed = extract_symbol_graph(
2235 "Cargo.toml",
2236 Some("cargo-manifest"),
2237 "[package\nname = \"broken\"\n",
2238 );
2239 let malformed_projection = derive_resolution_keys(project, None, &malformed)?;
2240 require(
2241 malformed_projection.source_keys().is_empty()
2242 && malformed_projection.symbol_keys().is_empty()
2243 && malformed_projection.relation_keys().is_empty(),
2244 "malformed Cargo input fabricated semantic identities",
2245 )?;
2246
2247 let first = extract_symbol_graph(
2248 "vendor/first/Cargo.toml",
2249 Some("cargo-manifest"),
2250 "[package]\nname = \"shared-package\"\n",
2251 );
2252 let second = extract_symbol_graph(
2253 "vendor/second/Cargo.toml",
2254 Some("cargo-manifest"),
2255 "[package]\nname = \"shared-package\"\n",
2256 );
2257 let caller = extract_symbol_graph(
2258 "Cargo.toml",
2259 Some("cargo-manifest"),
2260 "[package]\nname = \"app\"\n[dependencies]\nshared-package = \"1\"\n",
2261 );
2262 let first_projection = derive_resolution_keys(project, None, &first)?;
2263 let second_projection = derive_resolution_keys(project, None, &second)?;
2264 let caller_projection = derive_resolution_keys(project, None, &caller)?;
2265 let dependency = relation_keys(&caller, &caller_projection, "shared-package");
2266 require(
2267 keys_intersect(
2268 dependency,
2269 symbol_keys(&first, &first_projection, "shared-package"),
2270 ) && keys_intersect(
2271 dependency,
2272 symbol_keys(&second, &second_projection, "shared-package"),
2273 ),
2274 "duplicate Cargo packages did not remain ambiguity candidates",
2275 )
2276 }
2277
2278 #[test]
2279 fn projection_rejects_invalid_identity_and_excessive_key_fanout() -> Result<(), Box<dyn Error>>
2280 {
2281 let graph = extract_symbol_graph("src/lib.rs", Some("rust"), "pub fn run() {}\n");
2282 require(
2283 matches!(
2284 derive_resolution_keys(project_id(4)?, Some("bad\0package"), &graph),
2285 Err(ResolutionProjectionError::Contract(_))
2286 ),
2287 "invalid package identity was accepted",
2288 )?;
2289
2290 let imports = (0..65)
2291 .map(|index| format!("import {{ run as execute }} from './module{index}';"))
2292 .collect::<Vec<_>>()
2293 .join("\n");
2294 let source = format!("{imports}\nexecute();\n");
2295 let graph = extract_symbol_graph("src/main.ts", Some("typescript"), &source);
2296 require(
2297 matches!(
2298 derive_resolution_keys(project_id(4)?, Some("web"), &graph),
2299 Err(ResolutionProjectionError::KeyLimit {
2300 fact: "relation",
2301 ..
2302 })
2303 ),
2304 "excessive relation-key fan-out was not rejected",
2305 )?;
2306 Ok(())
2307 }
2308
2309 #[test]
2310 fn contract_failure_retains_valid_resolution_projection_siblings() -> Result<(), Box<dyn Error>>
2311 {
2312 let path = format!("pkg/{0}/{0}/{0}/{0}/page.rs", "d".repeat(100));
2313 let imported = "i".repeat(3_800);
2314 let mut graph = extract_symbol_graph(
2315 &path,
2316 Some("rust"),
2317 "pub fn caller() { helper(); }\npub fn helper() {}\n",
2318 );
2319 graph
2320 .relations
2321 .retain(|relation| relation.kind == RelationKind::Calls);
2322 graph.relations.push(SymbolRelation {
2323 path,
2324 source_name: "<module>".to_string(),
2325 target_name: format!("use self::{{{imported}}};"),
2326 kind: RelationKind::Imports,
2327 line: 1,
2328 context: "partial projection fixture".to_string(),
2329 parser: ParserKind::TreeSitter,
2330 });
2331 graph.relations.push(SymbolRelation {
2332 path: graph.path.clone(),
2333 source_name: "caller".to_string(),
2334 target_name: format!("{imported}.member"),
2335 kind: RelationKind::Calls,
2336 line: 3,
2337 context: "partial projection fixture".to_string(),
2338 parser: ParserKind::TreeSitter,
2339 });
2340
2341 let result = derive_resolution_keys(project_id(23)?, None, &graph);
2342 let Err(ResolutionProjectionError::Contract(failure)) = result else {
2343 return Err(io::Error::other(
2344 "invalid derived resolution key did not retain a typed contract failure",
2345 )
2346 .into());
2347 };
2348 require(
2349 !failure.projection().source_keys().is_empty(),
2350 "contract failure discarded valid source resolution keys",
2351 )?;
2352 require(
2353 !failure.projection().symbol_keys().is_empty(),
2354 "contract failure discarded valid symbol resolution keys",
2355 )?;
2356 require(
2357 failure
2358 .projection()
2359 .relation_keys()
2360 .iter()
2361 .any(|relation| relation.relation_index() == 0 && !relation.keys().is_empty()),
2362 "contract failure discarded a valid sibling relation key",
2363 )?;
2364 require(
2365 failure
2366 .failures()
2367 .iter()
2368 .all(|failure| !format!("{:?}", failure.error()).contains(&imported)),
2369 "contract failure retained rejected raw identity text",
2370 )?;
2371 Ok(())
2372 }
2373
2374 #[test]
2375 fn contract_failure_deduplicates_repeated_facts_after_detail_ceiling()
2376 -> Result<(), Box<dyn Error>> {
2377 let mut graph = SymbolGraph {
2378 path: "src/main.rs".to_string(),
2379 language: Some("rust".to_string()),
2380 parser: ParserKind::TreeSitter,
2381 symbols: Vec::with_capacity(super::MAX_RESOLUTION_PROJECTION_FAILURES + 1),
2382 relations: Vec::new(),
2383 };
2384 for index in 0..super::MAX_RESOLUTION_PROJECTION_FAILURES {
2385 graph.symbols.push(CodeSymbol {
2386 path: graph.path.clone(),
2387 language: graph.language.clone(),
2388 name: format!("invalid\0{index}"),
2389 kind: SymbolKind::Function,
2390 signature: "fn invalid()".to_string(),
2391 exported: true,
2392 documentation: None,
2393 line_start: index + 1,
2394 line_end: index + 1,
2395 source_selector: None,
2396 parent: None,
2397 parser: ParserKind::TreeSitter,
2398 detail: None,
2399 });
2400 }
2401 graph.symbols.push(CodeSymbol {
2402 path: graph.path.clone(),
2403 language: graph.language.clone(),
2404 name: "valid_tail".to_string(),
2405 kind: SymbolKind::Function,
2406 signature: "fn valid_tail()".to_string(),
2407 exported: true,
2408 documentation: None,
2409 line_start: graph.symbols.len() + 1,
2410 line_end: graph.symbols.len() + 1,
2411 source_selector: None,
2412 parent: Some("\0invalid-parent".to_string()),
2413 parser: ParserKind::TreeSitter,
2414 detail: None,
2415 });
2416
2417 let Err(ResolutionProjectionError::Contract(failure)) =
2418 derive_resolution_keys(project_id(24)?, None, &graph)
2419 else {
2420 return Err(
2421 io::Error::other("repeated post-ceiling failures were not reported").into(),
2422 );
2423 };
2424 require(
2425 failure.failures().len() == super::MAX_RESOLUTION_PROJECTION_FAILURES,
2426 "detail ceiling retained repeated fact attempts",
2427 )?;
2428 require(
2429 failure.rejected_count() == super::MAX_RESOLUTION_PROJECTION_FAILURES + 1,
2430 "repeated post-ceiling fact inflated rejected count",
2431 )?;
2432 Ok(())
2433 }
2434
2435 #[test]
2436 fn supported_typescript_parser_retains_control_bearing_import_identities()
2437 -> Result<(), Box<dyn Error>> {
2438 let graph = extract_symbol_graph(
2439 "src/page.ts",
2440 Some("typescript"),
2441 "import * as first from './bad\0one';\nimport * as second from './bad\0two';\nexport function caller() { first(); second(); }\n",
2442 );
2443 require(
2444 graph.parser == ParserKind::TreeSitter,
2445 "supported TypeScript fixture did not use the semantic parser",
2446 )?;
2447 require(
2448 graph.relations.iter().any(|relation| {
2449 relation.kind == RelationKind::Imports && relation.target_name.contains('\0')
2450 }),
2451 "supported TypeScript parser did not retain the control-bearing import",
2452 )?;
2453 Ok(())
2454 }
2455
2456 #[test]
2457 fn unsupported_and_lockfile_languages_do_not_gain_generic_name_resolution()
2458 -> Result<(), Box<dyn Error>> {
2459 for graph in [
2460 extract_symbol_graph(
2461 "src/Service.java",
2462 Some("java"),
2463 "public class Service { void run() {} }\n",
2464 ),
2465 extract_symbol_graph(
2466 "Cargo.lock",
2467 Some("cargo-lock"),
2468 "[[package]]\nname = \"dependency\"\nversion = \"1.0.0\"\n",
2469 ),
2470 ] {
2471 let projection = derive_resolution_keys(project_id(9)?, Some("app"), &graph)?;
2472 require(
2473 projection.source_keys().is_empty()
2474 && projection.symbol_keys().is_empty()
2475 && projection.relation_keys().is_empty(),
2476 "unsupported language emitted generic resolution keys",
2477 )?;
2478 }
2479 Ok(())
2480 }
2481
2482 fn require(condition: bool, message: &'static str) -> Result<(), Box<dyn Error>> {
2483 if condition {
2484 Ok(())
2485 } else {
2486 Err(io::Error::other(message).into())
2487 }
2488 }
2489
2490 fn project_id(byte: u8) -> Result<ProjectInstanceId, Box<dyn Error>> {
2491 Ok(ProjectInstanceId::from_bytes([byte; 16])?)
2492 }
2493
2494 fn assert_alias_resolution(
2495 project: ProjectInstanceId,
2496 package: Option<&str>,
2497 target: &SymbolGraph,
2498 symbol_name: &str,
2499 caller: &SymbolGraph,
2500 call_targets: &[&str],
2501 ) -> Result<(), Box<dyn Error>> {
2502 let target_projection = derive_resolution_keys(project, package, target)?;
2503 let caller_projection = derive_resolution_keys(project, package, caller)?;
2504 let exports = symbol_keys(target, &target_projection, symbol_name);
2505 for call_target in call_targets {
2506 if !keys_intersect(
2507 relation_keys(caller, &caller_projection, call_target),
2508 exports,
2509 ) {
2510 return Err(io::Error::other(format!(
2511 "{call_target} did not share a canonical key with {symbol_name}"
2512 ))
2513 .into());
2514 }
2515 }
2516 Ok(())
2517 }
2518
2519 fn assert_import_matches_only_source(
2520 project: ProjectInstanceId,
2521 package: Option<&str>,
2522 caller: &SymbolGraph,
2523 expected: &SymbolGraph,
2524 wrong: &SymbolGraph,
2525 ) -> Result<(), Box<dyn Error>> {
2526 let caller_projection = derive_resolution_keys(project, package, caller)?;
2527 let expected_projection = derive_resolution_keys(project, package, expected)?;
2528 let wrong_projection = derive_resolution_keys(project, package, wrong)?;
2529 let imports = relation_keys_of_kind(caller, &caller_projection, RelationKind::Imports);
2530 require(
2531 imports.len() == 1,
2532 "caller did not retain one import relationship",
2533 )?;
2534 require(
2535 keys_intersect(imports[0], expected_projection.source_keys()),
2536 "caller import missed its exact source",
2537 )?;
2538 require(
2539 !keys_intersect(imports[0], wrong_projection.source_keys()),
2540 "caller import matched a wrong-sibling basename",
2541 )
2542 }
2543
2544 fn symbol_keys<'a>(
2545 graph: &SymbolGraph,
2546 projection: &'a super::ResolutionKeyProjection,
2547 name: &str,
2548 ) -> &'a [CanonicalResolutionKey] {
2549 projection
2550 .symbol_keys()
2551 .iter()
2552 .find(|entry| graph.symbols[entry.symbol_index()].name == name)
2553 .map_or(&[], super::SymbolResolutionKeys::keys)
2554 }
2555
2556 fn relation_keys<'a>(
2557 graph: &SymbolGraph,
2558 projection: &'a super::ResolutionKeyProjection,
2559 target: &str,
2560 ) -> &'a [CanonicalResolutionKey] {
2561 projection
2562 .relation_keys()
2563 .iter()
2564 .find(|entry| {
2565 let relation = &graph.relations[entry.relation_index()];
2566 matches!(relation.kind, RelationKind::Calls | RelationKind::DependsOn)
2567 && relation.target_name == target
2568 })
2569 .map_or(&[], super::RelationResolutionKeys::keys)
2570 }
2571
2572 fn relation_keys_of_kind<'a>(
2573 graph: &SymbolGraph,
2574 projection: &'a super::ResolutionKeyProjection,
2575 kind: RelationKind,
2576 ) -> Vec<&'a [CanonicalResolutionKey]> {
2577 projection
2578 .relation_keys()
2579 .iter()
2580 .filter(|entry| graph.relations[entry.relation_index()].kind == kind)
2581 .map(super::RelationResolutionKeys::keys)
2582 .collect()
2583 }
2584
2585 fn keys_intersect(left: &[CanonicalResolutionKey], right: &[CanonicalResolutionKey]) -> bool {
2586 left.iter().any(|key| right.binary_search(key).is_ok())
2587 }
2588}