1use crate::ConfiguredModuleResolution;
4use crate::configured_modules::ConfiguredModuleSource;
5use crate::semantic;
6use blake3::Hasher;
7use projectatlas_core::graph::{
8 CanonicalResolutionKey, GraphContractError, GraphIdentityText, 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
152impl<'a> ResolutionProjectionContext<'a> {
153 #[must_use]
155 pub const fn with_configured_modules(
156 configured_modules: &'a ConfiguredModuleResolution,
157 ) -> Self {
158 Self {
159 configured_modules: Some(configured_modules),
160 }
161 }
162}
163
164impl ResolutionKeyProjection {
165 #[must_use]
167 pub fn source_keys(&self) -> &[CanonicalResolutionKey] {
168 &self.source
169 }
170
171 #[must_use]
173 pub fn symbol_keys(&self) -> &[SymbolResolutionKeys] {
174 &self.symbols
175 }
176
177 #[must_use]
179 pub fn relation_keys(&self) -> &[RelationResolutionKeys] {
180 &self.relations
181 }
182}
183
184#[derive(Debug)]
186pub enum ResolutionProjectionError {
187 Contract(GraphContractError),
189 KeyLimit {
191 fact: &'static str,
193 index: usize,
195 requested: usize,
197 },
198}
199
200impl fmt::Display for ResolutionProjectionError {
201 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
202 match self {
203 Self::Contract(error) => write!(formatter, "invalid resolution identity: {error}"),
204 Self::KeyLimit {
205 fact,
206 index,
207 requested,
208 } => write!(
209 formatter,
210 "{fact} fact {index} requires {requested} canonical keys, exceeding the per-fact limit of {MAX_RESOLUTION_KEYS_PER_FACT}"
211 ),
212 }
213 }
214}
215
216impl Error for ResolutionProjectionError {
217 fn source(&self) -> Option<&(dyn Error + 'static)> {
218 match self {
219 Self::Contract(error) => Some(error),
220 Self::KeyLimit { .. } => None,
221 }
222 }
223}
224
225impl From<GraphContractError> for ResolutionProjectionError {
226 fn from(value: GraphContractError) -> Self {
227 Self::Contract(value)
228 }
229}
230
231#[derive(Clone, Copy, Debug, Eq, PartialEq)]
233pub enum ImportSyntax {
234 Rust,
236 EcmaScript,
238 Python,
240}
241
242#[derive(Clone, Debug, Eq, PartialEq)]
244pub struct ImportReference {
245 syntax: ImportSyntax,
247 module: String,
249 imported: Option<String>,
251 local: String,
253}
254
255impl ImportReference {
256 pub(crate) fn new(
258 syntax: ImportSyntax,
259 module: &str,
260 imported: Option<&str>,
261 local: &str,
262 ) -> Self {
263 Self {
264 syntax,
265 module: module.to_string(),
266 imported: imported.map(ToString::to_string),
267 local: local.to_string(),
268 }
269 }
270
271 #[must_use]
273 pub const fn syntax(&self) -> ImportSyntax {
274 self.syntax
275 }
276
277 #[must_use]
279 pub fn module(&self) -> &str {
280 &self.module
281 }
282
283 #[must_use]
285 pub fn imported(&self) -> Option<&str> {
286 self.imported.as_deref()
287 }
288
289 #[must_use]
291 pub fn local(&self) -> &str {
292 &self.local
293 }
294}
295
296#[must_use]
301pub fn parse_import_references(import_text: &str) -> Vec<ImportReference> {
302 let mut references = semantic::parse_display_import(import_text);
303 references.sort_by(|left, right| {
304 left.module
305 .cmp(&right.module)
306 .then_with(|| left.imported.cmp(&right.imported))
307 .then_with(|| left.local.cmp(&right.local))
308 });
309 references.dedup();
310 references
311}
312
313#[must_use]
315pub fn resolve_relative_import_path(caller_path: &str, module_spec: &str) -> Option<String> {
316 semantic::ecmascript::resolve_relative_import_path(caller_path, module_spec)
317}
318
319#[must_use]
321pub fn module_aliases_for_path(path: &str) -> Vec<String> {
322 let mut aliases = Vec::new();
323 for stem in source_stems_for_path(path) {
324 let mut components = stem
325 .split('/')
326 .filter(|component| !component.is_empty())
327 .collect::<Vec<_>>();
328 if components
329 .first()
330 .is_some_and(|component| *component == "src")
331 {
332 components.remove(0);
333 }
334 if components.last().is_some_and(|component| {
335 matches!(*component, "lib" | "main" | "mod" | "index" | "__init__")
336 }) {
337 components.pop();
338 }
339 if components.is_empty() {
340 continue;
341 }
342 aliases.push(components.join("::"));
343 aliases.push(components.join("."));
344 if let Some(last) = components.last() {
345 aliases.push((*last).to_string());
346 }
347 }
348 aliases.sort();
349 aliases.dedup();
350 aliases
351}
352
353#[must_use]
355pub fn source_stems_for_path(path: &str) -> Vec<String> {
356 let stem = strip_known_source_extension(path);
357 let mut stems = vec![stem.clone()];
358 if let Some((parent, entry_name)) = stem.rsplit_once('/')
359 && matches!(entry_name, "index" | "__init__" | "mod")
360 {
361 stems.push(parent.to_string());
362 }
363 stems.sort();
364 stems.dedup();
365 stems
366}
367
368pub fn derive_resolution_keys(
379 project: ProjectInstanceId,
380 package: Option<&str>,
381 graph: &SymbolGraph,
382) -> Result<ResolutionKeyProjection, ResolutionProjectionError> {
383 derive_resolution_keys_with_context(
384 project,
385 package,
386 graph,
387 ResolutionProjectionContext::default(),
388 )
389}
390
391pub fn derive_resolution_keys_with_context(
398 project: ProjectInstanceId,
399 package: Option<&str>,
400 graph: &SymbolGraph,
401 context: ResolutionProjectionContext<'_>,
402) -> Result<ResolutionKeyProjection, ResolutionProjectionError> {
403 let Some(provider_owner) = semantic::provider_for_graph(graph) else {
404 return Ok(ResolutionKeyProjection {
405 source: Vec::new(),
406 symbols: Vec::new(),
407 relations: Vec::new(),
408 });
409 };
410 let provider = GraphIdentityText::new(provider_owner.as_str())?;
411 let Some(resolution_family) = provider_owner.resolution_family() else {
412 return Ok(ResolutionKeyProjection {
413 source: Vec::new(),
414 symbols: Vec::new(),
415 relations: Vec::new(),
416 });
417 };
418 let language = GraphIdentityText::new(resolution_family)?;
419 let package = package.map(GraphIdentityText::new).transpose()?;
420
421 let source_scopes = canonical_source_scopes(&graph.path);
422 let configured_source_scopes =
423 if provider_owner == SemanticProviderOwner::EcmaScript && package.is_some() {
424 canonical_configured_source_scopes(&graph.path)
425 } else {
426 Vec::new()
427 };
428 let emits_source_module_keys = semantic::emits_source_module_keys(graph);
429 let mut source_keys = Vec::new();
430 if emits_source_module_keys {
431 source_keys.reserve(source_scopes.len() + configured_source_scopes.len());
432 for scope in &source_scopes {
433 source_keys.push(canonical_key(
434 project,
435 ResolutionKeyDomain::Module,
436 &provider,
437 &language,
438 package.as_ref(),
439 None,
440 RelationKind::Imports,
441 scope,
442 )?);
443 }
444 for scope in &configured_source_scopes {
445 source_keys.push(canonical_key(
446 project,
447 ResolutionKeyDomain::Module,
448 &provider,
449 &language,
450 None,
451 None,
452 RelationKind::Imports,
453 scope,
454 )?);
455 }
456 }
457 let source_keys = bounded_keys(source_keys, "source", 0)?;
458
459 let mut symbol_keys = Vec::new();
460 for (symbol_index, symbol) in graph.symbols.iter().enumerate() {
461 let mut keys = Vec::new();
462 match symbol.kind {
463 SymbolKind::Package if provider_owner == SemanticProviderOwner::Cargo => {
464 keys.push(canonical_key(
465 project,
466 ResolutionKeyDomain::Package,
467 &provider,
468 &language,
469 None,
470 None,
471 RelationKind::DependsOn,
472 &symbol.name,
473 )?);
474 }
475 SymbolKind::Dependency | SymbolKind::Import | SymbolKind::Workspace => {}
476 _ if emits_source_module_keys
477 && semantic::is_export_candidate(provider_owner, graph, symbol_index) =>
478 {
479 let identity = GraphIdentityText::new(symbol.name.clone())?;
480 let mut scopes = source_scopes.clone();
481 if let Some(parent) = symbol.parent.as_deref() {
482 scopes.extend(
483 source_scopes
484 .iter()
485 .map(|scope| format!("{scope}/{parent}")),
486 );
487 scopes.push(parent.to_string());
488 }
489 scopes.push(String::new());
490 scopes.sort();
491 scopes.dedup();
492 for scope in &scopes {
493 let scope = (!scope.is_empty())
494 .then(|| GraphIdentityText::new(scope.clone()))
495 .transpose()?;
496 keys.push(CanonicalResolutionKey::new(
497 project,
498 ResolutionKeyDomain::Declaration,
499 &provider,
500 &language,
501 package.as_ref(),
502 scope.as_ref(),
503 Some(GraphRelationKind::from_legacy(RelationKind::Calls)),
504 &identity,
505 ));
506 }
507 let mut configured_scopes = configured_source_scopes.clone();
508 if let Some(parent) = symbol.parent.as_deref() {
509 configured_scopes.extend(
510 configured_source_scopes
511 .iter()
512 .map(|scope| format!("{scope}/{parent}")),
513 );
514 configured_scopes.sort();
515 configured_scopes.dedup();
516 }
517 for scope in configured_scopes {
518 let scope = GraphIdentityText::new(scope)?;
519 keys.push(CanonicalResolutionKey::new(
520 project,
521 ResolutionKeyDomain::Declaration,
522 &provider,
523 &language,
524 None,
525 Some(&scope),
526 Some(GraphRelationKind::from_legacy(RelationKind::Calls)),
527 &identity,
528 ));
529 }
530 }
531 _ => {}
532 }
533 let keys = bounded_keys(keys, "symbol", symbol_index)?;
534 if !keys.is_empty() {
535 symbol_keys.push(SymbolResolutionKeys { symbol_index, keys });
536 }
537 }
538
539 let parsed_imports = graph
540 .relations
541 .iter()
542 .map(|relation| {
543 if relation.kind == RelationKind::Imports {
544 semantic::parse_imports(provider_owner, &relation.target_name)
545 } else {
546 Vec::new()
547 }
548 })
549 .collect::<Vec<_>>();
550 let mut aliases: BTreeMap<&str, Vec<&ImportReference>> = BTreeMap::new();
551 for references in &parsed_imports {
552 for reference in references {
553 aliases
554 .entry(reference.local())
555 .or_default()
556 .push(reference);
557 }
558 }
559 let configured_source = context
560 .configured_modules
561 .map(|configured| configured.for_source(&graph.path));
562 let import_scopes = build_import_scope_cache(
563 provider_owner,
564 graph,
565 &parsed_imports,
566 configured_source.as_ref(),
567 );
568
569 let mut relation_keys = Vec::new();
570 for (relation_index, relation) in graph.relations.iter().enumerate() {
571 let mut keys = match relation.kind {
572 RelationKind::Imports if semantic::supports_source_dependencies(provider_owner) => {
573 import_dependency_keys(
574 project,
575 provider_owner,
576 &provider,
577 &language,
578 package.as_ref(),
579 &relation.path,
580 &parsed_imports[relation_index],
581 &import_scopes,
582 )?
583 }
584 RelationKind::Calls if semantic::supports_source_dependencies(provider_owner) => {
585 call_dependency_keys(
586 project,
587 provider_owner,
588 &provider,
589 &language,
590 package.as_ref(),
591 &relation.path,
592 &relation.target_name,
593 &aliases,
594 &import_scopes,
595 )?
596 }
597 RelationKind::DependsOn if semantic::supports_package_dependencies(provider_owner) => {
598 vec![canonical_key(
599 project,
600 ResolutionKeyDomain::Package,
601 &provider,
602 &language,
603 None,
604 None,
605 RelationKind::DependsOn,
606 &relation.target_name,
607 )?]
608 }
609 RelationKind::Contains => continue,
610 RelationKind::Imports | RelationKind::Calls | RelationKind::DependsOn => Vec::new(),
611 };
612 keys = bounded_keys(keys, "relation", relation_index)?;
613 relation_keys.push(RelationResolutionKeys {
614 relation_index,
615 keys,
616 });
617 }
618
619 Ok(ResolutionKeyProjection {
620 source: source_keys,
621 symbols: symbol_keys,
622 relations: relation_keys,
623 })
624}
625
626fn build_import_scope_cache(
628 provider_owner: SemanticProviderOwner,
629 graph: &SymbolGraph,
630 parsed_imports: &[Vec<ImportReference>],
631 configured_source: Option<&ConfiguredModuleSource<'_>>,
632) -> ImportScopeCache {
633 let mut cache = ImportScopeCache::new();
634 for (relation, references) in graph.relations.iter().zip(parsed_imports) {
635 let caller_path = &relation.path;
636 let modules = cache.entry(caller_path.clone()).or_default();
637 for reference in references {
638 modules
639 .entry(reference.module().to_string())
640 .or_insert_with(|| {
641 semantic::import_scopes(
642 provider_owner,
643 caller_path,
644 reference,
645 configured_source,
646 )
647 });
648 }
649 }
650 cache
651}
652
653fn cached_import_scopes<'a>(
655 cache: &'a ImportScopeCache,
656 caller_path: &str,
657 reference: &ImportReference,
658) -> &'a [String] {
659 cache
660 .get(caller_path)
661 .and_then(|modules| modules.get(reference.module()))
662 .map_or(&[], Vec::as_slice)
663}
664
665fn import_dependency_keys(
667 project: ProjectInstanceId,
668 provider_owner: SemanticProviderOwner,
669 provider: &GraphIdentityText,
670 language: &GraphIdentityText,
671 package: Option<&GraphIdentityText>,
672 caller_path: &str,
673 references: &[ImportReference],
674 import_scopes: &ImportScopeCache,
675) -> Result<Vec<CanonicalResolutionKey>, ResolutionProjectionError> {
676 let mut keys = Vec::new();
677 for reference in references {
678 let scopes = cached_import_scopes(import_scopes, caller_path, reference);
679 let package = dependency_package(provider_owner, reference, package);
680 for scope in scopes {
681 keys.push(canonical_key(
682 project,
683 ResolutionKeyDomain::Module,
684 provider,
685 language,
686 package,
687 None,
688 RelationKind::Imports,
689 scope,
690 )?);
691 }
692 }
693 Ok(keys)
694}
695
696fn call_dependency_keys(
698 project: ProjectInstanceId,
699 provider_owner: SemanticProviderOwner,
700 provider: &GraphIdentityText,
701 language: &GraphIdentityText,
702 package: Option<&GraphIdentityText>,
703 caller_path: &str,
704 target: &str,
705 aliases: &BTreeMap<&str, Vec<&ImportReference>>,
706 import_scopes: &ImportScopeCache,
707) -> Result<Vec<CanonicalResolutionKey>, ResolutionProjectionError> {
708 let (prefix, remainder) = split_qualified_target(target);
709 let alias = prefix.unwrap_or(target).trim();
710 if let Some(references) = aliases.get(alias) {
711 let mut keys = Vec::new();
712 for reference in references {
713 let identity = remainder.or_else(|| reference.imported());
714 let Some(identity) = identity else {
715 continue;
716 };
717 let scopes = cached_import_scopes(import_scopes, caller_path, reference);
718 let package = dependency_package(provider_owner, reference, package);
719 for scope in scopes {
720 let scoped_parent;
721 let scope = if remainder.is_some()
722 && let Some(imported_parent) = reference.imported()
723 {
724 scoped_parent = format!("{scope}/{imported_parent}");
725 scoped_parent.as_str()
726 } else {
727 scope.as_str()
728 };
729 keys.push(canonical_key(
730 project,
731 ResolutionKeyDomain::Declaration,
732 provider,
733 language,
734 package,
735 Some(scope),
736 RelationKind::Calls,
737 identity,
738 )?);
739 }
740 }
741 return Ok(keys);
742 }
743
744 let (scope, identity) = split_qualified_target(target);
745 if scope.is_some() {
746 return Ok(Vec::new());
747 }
748 let Some(identity) = identity.or_else(|| (!target.trim().is_empty()).then_some(target.trim()))
749 else {
750 return Ok(Vec::new());
751 };
752 Ok(vec![canonical_key(
753 project,
754 ResolutionKeyDomain::Declaration,
755 provider,
756 language,
757 package,
758 None,
759 RelationKind::Calls,
760 identity,
761 )?])
762}
763
764fn dependency_package<'a>(
767 provider_owner: SemanticProviderOwner,
768 reference: &ImportReference,
769 package: Option<&'a GraphIdentityText>,
770) -> Option<&'a GraphIdentityText> {
771 if provider_owner == SemanticProviderOwner::EcmaScript
772 && !(reference.module().starts_with("./") || reference.module().starts_with("../"))
773 {
774 None
775 } else {
776 package
777 }
778}
779
780#[allow(clippy::too_many_arguments)]
781fn canonical_key(
783 project: ProjectInstanceId,
784 domain: ResolutionKeyDomain,
785 provider: &GraphIdentityText,
786 language: &GraphIdentityText,
787 package: Option<&GraphIdentityText>,
788 scope: Option<&str>,
789 relation: RelationKind,
790 identity: &str,
791) -> Result<CanonicalResolutionKey, GraphContractError> {
792 let scope = scope.map(GraphIdentityText::new).transpose()?;
793 let identity = GraphIdentityText::new(identity.to_string())?;
794 Ok(CanonicalResolutionKey::new(
795 project,
796 domain,
797 provider,
798 language,
799 package,
800 scope.as_ref(),
801 Some(GraphRelationKind::from_legacy(relation)),
802 &identity,
803 ))
804}
805
806fn bounded_keys(
808 mut keys: Vec<CanonicalResolutionKey>,
809 fact: &'static str,
810 index: usize,
811) -> Result<Vec<CanonicalResolutionKey>, ResolutionProjectionError> {
812 keys.sort();
813 keys.dedup();
814 if keys.len() > MAX_RESOLUTION_KEYS_PER_FACT {
815 return Err(ResolutionProjectionError::KeyLimit {
816 fact,
817 index,
818 requested: keys.len(),
819 });
820 }
821 Ok(keys)
822}
823
824fn canonical_source_scopes(path: &str) -> Vec<String> {
826 let mut scopes = source_stems_for_path(path);
827 scopes.extend(module_aliases_for_path(path));
828 normalize_scopes(scopes, normalize_repository_scope)
829}
830
831fn canonical_configured_source_scopes(path: &str) -> Vec<String> {
833 let mut scopes = source_stems_for_path(path)
834 .into_iter()
835 .map(|scope| normalize_repository_scope(&scope))
836 .filter(|scope| !scope.is_empty())
837 .collect::<Vec<_>>();
838 scopes.sort();
839 scopes.dedup();
840 scopes
841}
842
843fn normalize_scopes(scopes: Vec<String>, normalize: fn(&str) -> String) -> Vec<String> {
845 let mut normalized = Vec::new();
846 for scope in scopes {
847 let scope = normalize(&scope);
848 if scope.is_empty() {
849 continue;
850 }
851 normalized.push(scope.clone());
852 if let Some(stripped) = scope.strip_prefix("src/") {
853 normalized.push(stripped.to_string());
854 }
855 if let Some(last) = scope.rsplit('/').next() {
856 normalized.push(last.to_string());
857 }
858 }
859 normalized.sort();
860 normalized.dedup();
861 normalized
862}
863
864fn normalize_repository_scope(value: &str) -> String {
866 value
867 .trim()
868 .trim_start_matches("./")
869 .replace("::", "/")
870 .trim_matches('/')
871 .to_string()
872}
873
874fn split_qualified_target(target: &str) -> (Option<&str>, Option<&str>) {
876 let target = target.trim();
877 let rust = target.rsplit_once("::");
878 let dotted = target.rsplit_once('.');
879 match (rust, dotted) {
880 (Some(rust), Some(dotted)) => {
881 if rust.0.len() >= dotted.0.len() {
882 (Some(rust.0), Some(rust.1))
883 } else {
884 (Some(dotted.0), Some(dotted.1))
885 }
886 }
887 (Some((scope, identity)), None) | (None, Some((scope, identity))) => {
888 (Some(scope), Some(identity))
889 }
890 (None, None) => (None, None),
891 }
892}
893
894pub(super) fn strip_known_source_extension(path: &str) -> String {
896 for extension in [
897 ".d.ts", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".rs",
898 ] {
899 if let Some(stem) = path.strip_suffix(extension) {
900 return stem.to_string();
901 }
902 }
903 path.rsplit_once('.').map_or_else(
904 || path.to_string(),
905 |(stem, extension)| {
906 if extension.contains('/') {
907 path.to_string()
908 } else {
909 stem.to_string()
910 }
911 },
912 )
913}
914
915#[cfg(test)]
916mod tests {
917 use super::{
918 ImportReference, ImportSyntax, RelationKind, ResolutionProjectionContext,
919 ResolutionProjectionError, SEMANTIC_RESOLUTION_CONTRACT_VERSION, build_import_scope_cache,
920 derive_resolution_keys, derive_resolution_keys_with_context, parse_import_references,
921 resolve_relative_import_path, semantic_resolution_contract_digest,
922 };
923 use crate::{
924 ConfiguredModuleResolution, EcmaScriptConfigKind, EcmaScriptModuleConfig,
925 EcmaScriptPathMapping, extract_symbol_graph,
926 };
927 use projectatlas_core::graph::{CanonicalResolutionKey, ProjectInstanceId};
928 use projectatlas_core::language::SemanticProviderOwner;
929 use projectatlas_core::symbols::SymbolGraph;
930 use std::error::Error;
931 use std::io;
932
933 fn reference(
934 syntax: ImportSyntax,
935 module: &str,
936 imported: Option<&str>,
937 local: &str,
938 ) -> ImportReference {
939 ImportReference {
940 syntax,
941 module: module.to_string(),
942 imported: imported.map(ToString::to_string),
943 local: local.to_string(),
944 }
945 }
946
947 #[test]
948 fn semantic_resolution_contract_digest_is_bounded_and_deterministic() {
949 const PRE_MODULE_CALLBACK_DIGEST: &str =
950 "487625adf2f9ec76f98034d4ef5667e707960b6b8afd280b213021cb64a0f10f";
951 let first = semantic_resolution_contract_digest();
952 assert_eq!(SEMANTIC_RESOLUTION_CONTRACT_VERSION, 3);
953 assert_eq!(first.len(), 64);
954 assert!(first.bytes().all(|byte| byte.is_ascii_hexdigit()));
955 assert_eq!(first, semantic_resolution_contract_digest());
956 assert_ne!(first, PRE_MODULE_CALLBACK_DIGEST);
957 }
958
959 #[test]
960 fn parses_language_import_aliases_without_display_statement_identity() {
961 assert_eq!(
962 parse_import_references("use crate::worker::{run as execute, stop};"),
963 vec![
964 reference(ImportSyntax::Rust, "crate::worker", Some("run"), "execute"),
965 reference(ImportSyntax::Rust, "crate::worker", Some("stop"), "stop"),
966 ]
967 );
968 assert_eq!(
969 parse_import_references("use crate::worker::run_function_alias as run_rust_function;"),
970 vec![reference(
971 ImportSyntax::Rust,
972 "crate::worker",
973 Some("run_function_alias"),
974 "run_rust_function"
975 ),]
976 );
977 assert_eq!(
978 parse_import_references("import * as reader from './reader';"),
979 vec![reference(
980 ImportSyntax::EcmaScript,
981 "./reader",
982 None,
983 "reader",
984 )]
985 );
986 assert_eq!(
987 parse_import_references("import { runAlias as runLocal } from './worker';"),
988 vec![reference(
989 ImportSyntax::EcmaScript,
990 "./worker",
991 Some("runAlias"),
992 "runLocal",
993 )]
994 );
995 assert_eq!(
996 parse_import_references("from package.reader import read as load, close"),
997 vec![
998 reference(
999 ImportSyntax::Python,
1000 "package.reader",
1001 Some("close"),
1002 "close"
1003 ),
1004 reference(ImportSyntax::Python, "package.reader", Some("read"), "load"),
1005 ]
1006 );
1007 assert_eq!(
1008 parse_import_references("from package.worker import run_alias as execute"),
1009 vec![reference(
1010 ImportSyntax::Python,
1011 "package.worker",
1012 Some("run_alias"),
1013 "execute",
1014 )]
1015 );
1016 assert!(parse_import_references("import { broken from './reader'").is_empty());
1017 assert!(parse_import_references("use crate::worker::run as ;").is_empty());
1018 assert!(parse_import_references("import { read } from module './reader'").is_empty());
1019 for malformed in [
1020 "use crate::worker::{run, nested::{start, stop}};",
1021 "use crate::worker::{run, broken as };",
1022 "import defaultValue, { read } from './reader';",
1023 "import { read, nested { broken } } from './reader';",
1024 "import { read as load as fetch } from './reader';",
1025 "from package.reader import (read, close)",
1026 "from package.reader import read, broken as ",
1027 "import package.reader as reader as other",
1028 ] {
1029 assert!(
1030 parse_import_references(malformed).is_empty(),
1031 "unsupported syntax fabricated references: {malformed}"
1032 );
1033 }
1034 }
1035
1036 #[test]
1037 fn resolves_relative_imports_without_platform_path_rules() {
1038 assert_eq!(
1039 resolve_relative_import_path("src/features/main.ts", "../shared/reader.ts"),
1040 Some("src/shared/reader".to_string())
1041 );
1042 assert_eq!(
1043 resolve_relative_import_path("src/main.ts", "../../outside"),
1044 None
1045 );
1046 assert_eq!(resolve_relative_import_path("src/main.ts", "reader"), None);
1047 assert_eq!(
1048 resolve_relative_import_path("src/foo.bar/main.ts", "./reader"),
1049 Some("src/foo.bar/reader".to_string())
1050 );
1051 }
1052
1053 #[test]
1054 fn real_language_graphs_share_alias_qualified_keys() -> Result<(), Box<dyn Error>> {
1055 let project = project_id(1)?;
1056
1057 let rust_target = extract_symbol_graph("src/worker.rs", Some("rust"), "pub fn run() {}\n");
1058 let rust_caller = extract_symbol_graph(
1059 "src/main.rs",
1060 Some("rust"),
1061 "use crate::worker::run as execute;\nfn main() { execute(); }\n",
1062 );
1063 assert_alias_resolution(
1064 project,
1065 Some("atlas"),
1066 &rust_target,
1067 "run",
1068 &rust_caller,
1069 &["execute"],
1070 )?;
1071 let rust_module_target = extract_symbol_graph(
1072 "src/config.rs",
1073 Some("rust"),
1074 "pub fn load_timeout_millis() -> u64 { 250 }\n",
1075 );
1076 let rust_module_caller = extract_symbol_graph(
1077 "src/handler.rs",
1078 Some("rust"),
1079 "use crate::config;\npub fn health_response() { config::load_timeout_millis(); }\n",
1080 );
1081 assert_alias_resolution(
1082 project,
1083 Some("atlas"),
1084 &rust_module_target,
1085 "load_timeout_millis",
1086 &rust_module_caller,
1087 &["config::load_timeout_millis"],
1088 )?;
1089 let rust_callback_caller = extract_symbol_graph(
1090 "src/router.rs",
1091 Some("rust"),
1092 "use crate::handler;\nfn dispatch(path: &str) -> Option<()> { (path == \"/health\").then(handler::health_response) }\n",
1093 );
1094 assert_alias_resolution(
1095 project,
1096 Some("atlas"),
1097 &rust_module_caller,
1098 "health_response",
1099 &rust_callback_caller,
1100 &["handler::health_response"],
1101 )?;
1102 let user_defined_then = extract_symbol_graph(
1103 "src/scheduler.rs",
1104 Some("rust"),
1105 "use crate::handler;\nstruct Scheduler;\nimpl Scheduler { fn then(self, callback: fn()) { let _ = callback; } }\nfn schedule(scheduler: Scheduler) { scheduler.then(handler::health_response); }\n",
1106 );
1107 if user_defined_then.relations.iter().any(|relation| {
1108 relation.kind == RelationKind::Calls
1109 && relation.target_name == "handler::health_response"
1110 }) {
1111 return Err(io::Error::other(
1112 "a user-defined then method fabricated a callback call edge",
1113 )
1114 .into());
1115 }
1116
1117 let typescript_target = extract_symbol_graph(
1118 "src/shared/reader.ts",
1119 Some("typescript"),
1120 "export function read() {}\n",
1121 );
1122 let typescript_caller = extract_symbol_graph(
1123 "src/features/main.ts",
1124 Some("typescript"),
1125 "import { read as load } from '../shared/reader';\nimport * as reader from '../shared/reader';\nexport function main() { load(); reader.read(); }\n",
1126 );
1127 assert_alias_resolution(
1128 project,
1129 Some("web"),
1130 &typescript_target,
1131 "read",
1132 &typescript_caller,
1133 &["load", "reader.read"],
1134 )?;
1135
1136 let dotted_directory_target = extract_symbol_graph(
1137 "src/foo.bar/reader.ts",
1138 Some("typescript"),
1139 "export function read() {}\n",
1140 );
1141 let dotted_directory_caller = extract_symbol_graph(
1142 "src/foo.bar/main.ts",
1143 Some("typescript"),
1144 "import { read } from './reader';\nexport function main() { read(); }\n",
1145 );
1146 assert_alias_resolution(
1147 project,
1148 Some("web"),
1149 &dotted_directory_target,
1150 "read",
1151 &dotted_directory_caller,
1152 &["read"],
1153 )?;
1154
1155 let python_target = extract_symbol_graph(
1156 "src/package/reader.py",
1157 Some("python"),
1158 "def read():\n pass\n",
1159 );
1160 let python_caller = extract_symbol_graph(
1161 "src/main.py",
1162 Some("python"),
1163 "import package.reader as reader\nreader.read()\n",
1164 );
1165 assert_alias_resolution(
1166 project,
1167 Some("python-app"),
1168 &python_target,
1169 "read",
1170 &python_caller,
1171 &["reader.read"],
1172 )?;
1173 Ok(())
1174 }
1175
1176 #[test]
1177 fn configured_aliases_share_file_and_declaration_keys_across_ecmascript_hosts()
1178 -> Result<(), Box<dyn Error>> {
1179 let project = project_id(21)?;
1180 let configured = ConfiguredModuleResolution::new(vec![
1181 EcmaScriptModuleConfig::new(
1182 "tsconfig.json",
1183 EcmaScriptConfigKind::TypeScript,
1184 Some("src".to_string()),
1185 vec![
1186 EcmaScriptPathMapping::new("@/*", vec!["src/*".to_string()])?,
1187 EcmaScriptPathMapping::new("@index/*", vec!["src/*/index.ts".to_string()])?,
1188 ],
1189 )?,
1190 EcmaScriptModuleConfig::new(
1191 "jsconfig.json",
1192 EcmaScriptConfigKind::JavaScript,
1193 Some("src".to_string()),
1194 vec![EcmaScriptPathMapping::new(
1195 "@/*",
1196 vec!["src/*".to_string()],
1197 )?],
1198 )?,
1199 ])?;
1200 let context = ResolutionProjectionContext::with_configured_modules(&configured);
1201
1202 for (target_path, language, target_source, caller_path, caller_source) in [
1203 (
1204 "src/controller.ts",
1205 "typescript",
1206 "export function useController(): string { return 'ok'; }\n",
1207 "src/page.ts",
1208 "import { useController } from '@/controller';\nexport const value = useController();\n",
1209 ),
1210 (
1211 "src/controller.ts",
1212 "typescript",
1213 "export function useController(): string { return 'ok'; }\n",
1214 "src/page.tsx",
1215 "import { useController } from '@/controller';\nexport function Page() { useController(); return <div />; }\n",
1216 ),
1217 (
1218 "src/controller.js",
1219 "javascript",
1220 "export function useController() { return 'ok'; }\n",
1221 "src/page.jsx",
1222 "import { useController } from '@/controller';\nexport function Page() { useController(); return <div />; }\n",
1223 ),
1224 (
1225 "src/controller.ts",
1226 "typescript",
1227 "export function useController(): string { return 'ok'; }\n",
1228 "src/Page.vue",
1229 "<script setup lang=\"ts\">\nimport { useController } from '@/controller';\nconst value = useController();\n</script>\n<template><div>{{ value }}</div></template>\n",
1230 ),
1231 ] {
1232 let target = extract_symbol_graph(target_path, Some(language), target_source);
1233 let caller_extension = std::path::Path::new(caller_path)
1234 .extension()
1235 .and_then(std::ffi::OsStr::to_str);
1236 let caller_language = match caller_extension {
1237 Some(extension) if extension.eq_ignore_ascii_case("vue") => "vue",
1238 Some(extension)
1239 if extension.eq_ignore_ascii_case("js")
1240 || extension.eq_ignore_ascii_case("jsx") =>
1241 {
1242 "javascript"
1243 }
1244 Some(extension) if extension.eq_ignore_ascii_case("tsx") => "tsx",
1245 _ => "typescript",
1246 };
1247 let caller = extract_symbol_graph(caller_path, Some(caller_language), caller_source);
1248 let target_projection = derive_resolution_keys(project, Some("shared"), &target)?;
1249 let caller_projection =
1250 derive_resolution_keys_with_context(project, Some("app"), &caller, context)?;
1251 let imports = relation_keys_of_kind(&caller, &caller_projection, RelationKind::Imports);
1252 require(
1253 imports
1254 .iter()
1255 .any(|keys| keys_intersect(keys, target_projection.source_keys())),
1256 "configured import did not share the target file key",
1257 )?;
1258 require(
1259 keys_intersect(
1260 relation_keys(&caller, &caller_projection, "useController"),
1261 symbol_keys(&target, &target_projection, "useController"),
1262 ),
1263 "configured call did not share the target declaration key",
1264 )?;
1265 }
1266
1267 let index_target = extract_symbol_graph(
1268 "src/tools/index.ts",
1269 Some("typescript"),
1270 "export function useTool() { return 'ok'; }\n",
1271 );
1272 let index_caller = extract_symbol_graph(
1273 "src/index-page.ts",
1274 Some("typescript"),
1275 "import { useTool } from '@index/tools';\nexport const value = useTool();\n",
1276 );
1277 let index_target_projection = derive_resolution_keys(project, Some("app"), &index_target)?;
1278 let index_caller_projection =
1279 derive_resolution_keys_with_context(project, Some("app"), &index_caller, context)?;
1280 require(
1281 relation_keys_of_kind(
1282 &index_caller,
1283 &index_caller_projection,
1284 RelationKind::Imports,
1285 )
1286 .iter()
1287 .any(|keys| keys_intersect(keys, index_target_projection.source_keys())),
1288 "configured index target did not reuse package-entry source keys",
1289 )?;
1290 require(
1291 keys_intersect(
1292 relation_keys(&index_caller, &index_caller_projection, "useTool"),
1293 symbol_keys(&index_target, &index_target_projection, "useTool"),
1294 ),
1295 "configured index target did not reuse declaration keys",
1296 )?;
1297
1298 let extension_caller = extract_symbol_graph(
1299 "src/extension-page.ts",
1300 Some("typescript"),
1301 "import { useController } from '@/controller.ts';\nexport const value = useController();\n",
1302 );
1303 let extension_target = extract_symbol_graph(
1304 "src/controller.ts",
1305 Some("typescript"),
1306 "export function useController(): string { return 'ok'; }\n",
1307 );
1308 let extension_caller_projection =
1309 derive_resolution_keys_with_context(project, Some("app"), &extension_caller, context)?;
1310 let extension_target_projection =
1311 derive_resolution_keys(project, Some("app"), &extension_target)?;
1312 require(
1313 relation_keys_of_kind(
1314 &extension_caller,
1315 &extension_caller_projection,
1316 RelationKind::Imports,
1317 )
1318 .iter()
1319 .any(|keys| keys_intersect(keys, extension_target_projection.source_keys())),
1320 "configured extension target did not reuse extensionless source keys",
1321 )?;
1322
1323 let root_configured = ConfiguredModuleResolution::new(vec![EcmaScriptModuleConfig::new(
1324 "tsconfig.json",
1325 EcmaScriptConfigKind::TypeScript,
1326 Some(String::new()),
1327 Vec::new(),
1328 )?])?;
1329 let root_context = ResolutionProjectionContext::with_configured_modules(&root_configured);
1330 let root_target = extract_symbol_graph(
1331 "controller.ts",
1332 Some("typescript"),
1333 "export function useController() { return 'root'; }\n",
1334 );
1335 let nested_same_name = extract_symbol_graph(
1336 "packages/app/src/controller.ts",
1337 Some("typescript"),
1338 "export function useController() { return 'nested'; }\n",
1339 );
1340 let root_caller = extract_symbol_graph(
1341 "page.ts",
1342 Some("typescript"),
1343 "import { useController } from 'controller';\nexport const value = useController();\n",
1344 );
1345 let root_target_projection = derive_resolution_keys(project, Some("shared"), &root_target)?;
1346 let nested_projection = derive_resolution_keys(project, Some("nested"), &nested_same_name)?;
1347 let root_caller_projection =
1348 derive_resolution_keys_with_context(project, Some("app"), &root_caller, root_context)?;
1349 let configured_imports =
1350 relation_keys_of_kind(&root_caller, &root_caller_projection, RelationKind::Imports);
1351 require(
1352 configured_imports
1353 .iter()
1354 .any(|keys| keys_intersect(keys, root_target_projection.source_keys())),
1355 "root baseUrl import did not resolve its repository-exact target",
1356 )?;
1357 require(
1358 configured_imports
1359 .iter()
1360 .all(|keys| !keys_intersect(keys, nested_projection.source_keys())),
1361 "root baseUrl import also matched a nested basename alias",
1362 )?;
1363 let configured_call = relation_keys(&root_caller, &root_caller_projection, "useController");
1364 require(
1365 keys_intersect(
1366 configured_call,
1367 symbol_keys(&root_target, &root_target_projection, "useController"),
1368 ),
1369 "root baseUrl call did not resolve its repository-exact declaration",
1370 )?;
1371 require(
1372 !keys_intersect(
1373 configured_call,
1374 symbol_keys(&nested_same_name, &nested_projection, "useController"),
1375 ),
1376 "root baseUrl call also matched a nested basename alias",
1377 )?;
1378 Ok(())
1379 }
1380
1381 #[test]
1382 fn configured_import_scopes_are_expanded_once_per_source_graph() -> Result<(), Box<dyn Error>> {
1383 let configured = ConfiguredModuleResolution::new(vec![EcmaScriptModuleConfig::new(
1384 "tsconfig.json",
1385 EcmaScriptConfigKind::TypeScript,
1386 Some("src".to_string()),
1387 vec![EcmaScriptPathMapping::new(
1388 "@/*",
1389 vec!["src/*".to_string()],
1390 )?],
1391 )?])?;
1392 let graph = extract_symbol_graph(
1393 "src/page.ts",
1394 Some("typescript"),
1395 "import { useController } from '@/controller';\n\
1396 export const first = useController();\n\
1397 export const second = useController();\n",
1398 );
1399 let parsed_imports = graph
1400 .relations
1401 .iter()
1402 .map(|relation| {
1403 if relation.kind == RelationKind::Imports {
1404 crate::semantic::parse_imports(
1405 SemanticProviderOwner::EcmaScript,
1406 &relation.target_name,
1407 )
1408 } else {
1409 Vec::new()
1410 }
1411 })
1412 .collect::<Vec<_>>();
1413 let configured_source = configured.for_source(&graph.path);
1414 let cache = build_import_scope_cache(
1415 SemanticProviderOwner::EcmaScript,
1416 &graph,
1417 &parsed_imports,
1418 Some(&configured_source),
1419 );
1420 let modules = cache
1421 .get("src/page.ts")
1422 .ok_or_else(|| io::Error::other("source graph omitted its import-scope cache"))?;
1423 if modules.len() != 1 || modules.get("@/controller") != Some(&vec!["src/controller".into()])
1424 {
1425 return Err(io::Error::other(
1426 "repeated imported calls did not reuse one configured scope expansion",
1427 )
1428 .into());
1429 }
1430 Ok(())
1431 }
1432
1433 #[test]
1434 fn imports_target_modules_and_qualified_calls_target_members() -> Result<(), Box<dyn Error>> {
1435 let project = project_id(10)?;
1436 let target = extract_symbol_graph(
1437 "src/shared/reader.ts",
1438 Some("typescript"),
1439 "export function read() {}\nexport function close() {}\nexport function fetch() {}\nexport const client = { fetch() {} };\n",
1440 );
1441 let caller = extract_symbol_graph(
1442 "src/features/main.ts",
1443 Some("typescript"),
1444 "import { read, close, client } from '../shared/reader';\nexport function main() { read(); close(); client.fetch(); }\n",
1445 );
1446 let target_projection = derive_resolution_keys(project, Some("web"), &target)?;
1447 let caller_projection = derive_resolution_keys(project, Some("web"), &caller)?;
1448 let imports = relation_keys_of_kind(&caller, &caller_projection, RelationKind::Imports);
1449 require(
1450 imports.len() == 1,
1451 "one import emitted more than one module target",
1452 )?;
1453 require(
1454 imports[0]
1455 .iter()
1456 .all(|key| key.domain() == projectatlas_core::graph::ResolutionKeyDomain::Module),
1457 "named imports emitted declaration resolution targets",
1458 )?;
1459 require(
1460 keys_intersect(imports[0], target_projection.source_keys()),
1461 "named import did not target its source module",
1462 )?;
1463 for symbol in ["read", "close"] {
1464 require(
1465 !keys_intersect(imports[0], symbol_keys(&target, &target_projection, symbol)),
1466 "module import also targeted a named declaration",
1467 )?;
1468 }
1469 assert_alias_resolution(project, Some("web"), &target, "read", &caller, &["read"])?;
1470 assert_alias_resolution(project, Some("web"), &target, "close", &caller, &["close"])?;
1471 let qualified_call = relation_keys(&caller, &caller_projection, "client.fetch");
1472 let fetch_candidates = target_projection
1473 .symbol_keys()
1474 .iter()
1475 .filter(|entry| target.symbols[entry.symbol_index()].name == "fetch")
1476 .map(|entry| {
1477 (
1478 target.symbols[entry.symbol_index()].parent.as_deref(),
1479 entry.keys(),
1480 )
1481 })
1482 .collect::<Vec<_>>();
1483 require(
1484 fetch_candidates.len() == 2,
1485 "qualified-call fixture lost one fetch declaration",
1486 )?;
1487 require(
1488 fetch_candidates.iter().any(|(parent, keys)| {
1489 *parent == Some("client") && keys_intersect(qualified_call, keys)
1490 }),
1491 "qualified call did not target the imported parent member",
1492 )?;
1493 require(
1494 fetch_candidates
1495 .iter()
1496 .all(|(parent, keys)| parent.is_some() || !keys_intersect(qualified_call, keys)),
1497 "qualified call also targeted a same-module top-level declaration",
1498 )
1499 }
1500
1501 #[test]
1502 fn actual_complex_import_syntax_abstains_instead_of_fabricating_keys()
1503 -> Result<(), Box<dyn Error>> {
1504 let project = project_id(15)?;
1505 for (path, language, source) in [
1506 (
1507 "src/main.rs",
1508 "rust",
1509 "use crate::worker::{run, nested::{start, stop}};\nfn main() { run(); }\n",
1510 ),
1511 (
1512 "src/main.ts",
1513 "typescript",
1514 "import defaultValue, { read } from './reader';\ndefaultValue();\n",
1515 ),
1516 (
1517 "src/main.py",
1518 "python",
1519 "from package.reader import (read, close)\nread()\n",
1520 ),
1521 ] {
1522 let graph = extract_symbol_graph(path, Some(language), source);
1523 let projection = derive_resolution_keys(project, Some("app"), &graph)?;
1524 let imports = relation_keys_of_kind(&graph, &projection, RelationKind::Imports);
1525 require(
1526 !imports.is_empty(),
1527 "complex-import fixture did not reach provider normalization",
1528 )?;
1529 require(
1530 imports.iter().all(|keys| keys.is_empty()),
1531 "unsupported complex import syntax fabricated resolution keys",
1532 )?;
1533 }
1534 Ok(())
1535 }
1536
1537 #[test]
1538 fn provider_scopes_abstain_instead_of_matching_packages_or_wrong_siblings()
1539 -> Result<(), Box<dyn Error>> {
1540 let project = project_id(11)?;
1541 let correct_typescript = extract_symbol_graph(
1542 "src/a/reader.ts",
1543 Some("typescript"),
1544 "export function read() {}\n",
1545 );
1546 let wrong_typescript = extract_symbol_graph(
1547 "src/b/reader.ts",
1548 Some("typescript"),
1549 "export function read() {}\n",
1550 );
1551 let typescript_caller = extract_symbol_graph(
1552 "src/main.ts",
1553 Some("typescript"),
1554 "import { read } from './a/reader';\nread();\n",
1555 );
1556 assert_import_matches_only_source(
1557 project,
1558 Some("web"),
1559 &typescript_caller,
1560 &correct_typescript,
1561 &wrong_typescript,
1562 )?;
1563
1564 let package_caller = extract_symbol_graph(
1565 "src/package.ts",
1566 Some("typescript"),
1567 "import { read } from 'reader-package';\nread();\n",
1568 );
1569 let package_projection = derive_resolution_keys(project, Some("web"), &package_caller)?;
1570 require(
1571 relation_keys_of_kind(&package_caller, &package_projection, RelationKind::Imports)
1572 .iter()
1573 .all(|keys| keys.is_empty()),
1574 "bare ECMAScript package import entered a repository-local module scope",
1575 )?;
1576 require(
1577 relation_keys(&package_caller, &package_projection, "read").is_empty(),
1578 "call through an unresolved package import fell back to a global declaration",
1579 )?;
1580
1581 for (path, language, source, target) in [
1582 (
1583 "src/unimported.ts",
1584 "typescript",
1585 "export function run() { return client.fetch(); }\n",
1586 "client.fetch",
1587 ),
1588 (
1589 "src/unimported.rs",
1590 "rust",
1591 "pub fn run() { worker::execute(); }\n",
1592 "worker::execute",
1593 ),
1594 (
1595 "src/unimported.py",
1596 "python",
1597 "def run():\n return reader.read()\n",
1598 "reader.read",
1599 ),
1600 ] {
1601 let graph = extract_symbol_graph(path, Some(language), source);
1602 let projection = derive_resolution_keys(project, Some("app"), &graph)?;
1603 require(
1604 graph.relations.iter().any(|relation| {
1605 relation.kind == RelationKind::Calls && relation.target_name == target
1606 }),
1607 "unimported qualified-call fixture did not reach semantic normalization",
1608 )?;
1609 require(
1610 relation_keys(&graph, &projection, target).is_empty(),
1611 "unimported qualified call fell back to a project basename scope",
1612 )?;
1613 }
1614
1615 let rust_target =
1616 extract_symbol_graph("src/feature/worker.rs", Some("rust"), "pub fn run() {}\n");
1617 let wrong_rust =
1618 extract_symbol_graph("src/other/worker.rs", Some("rust"), "pub fn run() {}\n");
1619 let rust_caller = extract_symbol_graph(
1620 "src/feature/main.rs",
1621 Some("rust"),
1622 "use super::worker::run;\nfn main() { run(); }\n",
1623 );
1624 let rust_crate_caller = extract_symbol_graph(
1625 "src/main.rs",
1626 Some("rust"),
1627 "use crate::feature::worker::run;\nfn main() { run(); }\n",
1628 );
1629 let rust_self_caller = extract_symbol_graph(
1630 "src/feature/mod.rs",
1631 Some("rust"),
1632 "use self::worker::run;\nfn main() { run(); }\n",
1633 );
1634 for caller in [&rust_caller, &rust_crate_caller, &rust_self_caller] {
1635 assert_import_matches_only_source(
1636 project,
1637 Some("atlas"),
1638 caller,
1639 &rust_target,
1640 &wrong_rust,
1641 )?;
1642 }
1643
1644 let python_target = extract_symbol_graph(
1645 "src/package/reader.py",
1646 Some("python"),
1647 "def read():\n pass\n",
1648 );
1649 let wrong_python = extract_symbol_graph(
1650 "src/other/reader.py",
1651 Some("python"),
1652 "def read():\n pass\n",
1653 );
1654 let python_caller = extract_symbol_graph(
1655 "src/package/main.py",
1656 Some("python"),
1657 "from .reader import read\nread()\n",
1658 );
1659 assert_import_matches_only_source(
1660 project,
1661 Some("python-app"),
1662 &python_caller,
1663 &python_target,
1664 &wrong_python,
1665 )?;
1666 let parent_python_target = extract_symbol_graph(
1667 "src/package/reader.py",
1668 Some("python"),
1669 "def read():\n pass\n",
1670 );
1671 let parent_python_wrong = extract_symbol_graph(
1672 "src/package/sub/reader.py",
1673 Some("python"),
1674 "def read():\n pass\n",
1675 );
1676 let parent_python_caller = extract_symbol_graph(
1677 "src/package/sub/main.py",
1678 Some("python"),
1679 "from ..reader import read\nread()\n",
1680 );
1681 assert_import_matches_only_source(
1682 project,
1683 Some("python-app"),
1684 &parent_python_caller,
1685 &parent_python_target,
1686 &parent_python_wrong,
1687 )
1688 }
1689
1690 #[test]
1691 fn direct_provider_families_retain_duplicate_ambiguity_keys() -> Result<(), Box<dyn Error>> {
1692 let project = project_id(12)?;
1693 for (path, language, target_source, caller_path, caller_source, target_name) in [
1694 (
1695 "src/shared/reader.ts",
1696 "typescript",
1697 "export function read() {}\nexport function read() {}\n",
1698 "src/main.ts",
1699 "import { read } from './shared/reader';\nread();\n",
1700 "read",
1701 ),
1702 (
1703 "src/package/reader.py",
1704 "python",
1705 "def read():\n pass\ndef read():\n pass\n",
1706 "src/main.py",
1707 "from package.reader import read\nread()\n",
1708 "read",
1709 ),
1710 ] {
1711 let target = extract_symbol_graph(path, Some(language), target_source);
1712 let caller = extract_symbol_graph(caller_path, Some(language), caller_source);
1713 let target_projection = derive_resolution_keys(project, Some("app"), &target)?;
1714 let caller_projection = derive_resolution_keys(project, Some("app"), &caller)?;
1715 let duplicate_keys = target_projection
1716 .symbol_keys()
1717 .iter()
1718 .filter(|entry| target.symbols[entry.symbol_index()].name == target_name)
1719 .map(super::SymbolResolutionKeys::keys)
1720 .collect::<Vec<_>>();
1721 require(
1722 duplicate_keys.len() == 2,
1723 "duplicate declarations were not both projected",
1724 )?;
1725 let dependency = relation_keys(&caller, &caller_projection, target_name);
1726 require(
1727 duplicate_keys
1728 .iter()
1729 .all(|keys| keys_intersect(dependency, keys)),
1730 "call dependency did not retain every duplicate declaration candidate",
1731 )?;
1732 }
1733 Ok(())
1734 }
1735
1736 #[test]
1737 fn resolution_identity_separates_provider_from_cross_dialect_family()
1738 -> Result<(), Box<dyn Error>> {
1739 let project = project_id(13)?;
1740 let typescript = extract_symbol_graph(
1741 "src/shared/reader.ts",
1742 Some("typescript"),
1743 "export function read() {}\n",
1744 );
1745 let javascript = extract_symbol_graph(
1746 "src/shared/reader.ts",
1747 Some("javascript"),
1748 "export function read() {}\n",
1749 );
1750 let typescript_projection = derive_resolution_keys(project, Some("web"), &typescript)?;
1751 let javascript_projection = derive_resolution_keys(project, Some("web"), &javascript)?;
1752 require(
1753 typescript_projection.source_keys() == javascript_projection.source_keys(),
1754 "ECMAScript-compatible dialects entered different resolution families",
1755 )?;
1756 let provider = projectatlas_core::graph::GraphIdentityText::new("ecma-script")?;
1757 let family = projectatlas_core::graph::GraphIdentityText::new("ecmascript")?;
1758 let package = projectatlas_core::graph::GraphIdentityText::new("web")?;
1759 let identity = projectatlas_core::graph::GraphIdentityText::new("src/shared/reader")?;
1760 let expected = CanonicalResolutionKey::new(
1761 project,
1762 projectatlas_core::graph::ResolutionKeyDomain::Module,
1763 &provider,
1764 &family,
1765 Some(&package),
1766 None,
1767 Some(projectatlas_core::graph::GraphRelationKind::from_legacy(
1768 RelationKind::Imports,
1769 )),
1770 &identity,
1771 );
1772 require(
1773 typescript_projection.source_keys().contains(&expected),
1774 "provider owner and cross-dialect family were not projected independently",
1775 )
1776 }
1777
1778 #[test]
1779 fn embedded_hosts_publish_module_keys_only_for_admitted_component_facts()
1780 -> Result<(), Box<dyn Error>> {
1781 let project = project_id(14)?;
1782 for (path, language, source) in [
1783 (
1784 "page.html",
1785 "html",
1786 "<script>export function run() {}</script>",
1787 ),
1788 ("empty.vue", "vue", "<template><p>empty</p></template>"),
1789 (
1790 "external.vue",
1791 "vue",
1792 "<script src='./external.js'></script>",
1793 ),
1794 ("empty.svelte", "svelte", "<p>empty</p>"),
1795 (
1796 "external.svelte",
1797 "svelte",
1798 "<script src='./external.js'></script>",
1799 ),
1800 (
1801 "external-inline.vue",
1802 "vue",
1803 "<script>import * as fs from 'node:fs';</script>",
1804 ),
1805 (
1806 "external-inline.svelte",
1807 "svelte",
1808 "<script>import * as fs from 'node:fs';</script>",
1809 ),
1810 ("malformed.vue", "vue", "<script"),
1811 ("malformed.svelte", "svelte", "<script"),
1812 ] {
1813 let graph = extract_symbol_graph(path, Some(language), source);
1814 let projection = derive_resolution_keys(project, Some("web"), &graph)?;
1815 require(
1816 projection.source_keys().is_empty(),
1817 "host without an admitted component module surface emitted source keys",
1818 )?;
1819 if path.starts_with("external-inline") {
1820 require(
1821 relation_keys_of_kind(&graph, &projection, RelationKind::Imports).len() == 1,
1822 "external-only component lost its outbound import relationship",
1823 )?;
1824 }
1825 }
1826 for (path, language) in [("component.vue", "vue"), ("component.svelte", "svelte")] {
1827 let graph = extract_symbol_graph(
1828 path,
1829 Some(language),
1830 "<script>export function run() {}</script>",
1831 );
1832 require(
1833 !derive_resolution_keys(project, Some("web"), &graph)?
1834 .source_keys()
1835 .is_empty(),
1836 "admitted component script facts did not expose their host module",
1837 )?;
1838 }
1839 Ok(())
1840 }
1841
1842 #[test]
1843 fn canonical_projection_is_stable_and_retains_ambiguous_unresolved_and_package_keys()
1844 -> Result<(), Box<dyn Error>> {
1845 let project = project_id(2)?;
1846 let first = extract_symbol_graph("src/worker.rs", Some("rust"), "pub fn run() {}\n");
1847 let moved = extract_symbol_graph(
1848 "src/worker.rs",
1849 Some("rust"),
1850 "\n\n// formatting moved the declaration\npub fn run() {}\n",
1851 );
1852 let first_projection = derive_resolution_keys(project, Some("atlas"), &first)?;
1853 let moved_projection = derive_resolution_keys(project, Some("atlas"), &moved)?;
1854 require(
1855 symbol_keys(&first, &first_projection, "run")
1856 == symbol_keys(&moved, &moved_projection, "run"),
1857 "formatting-only line movement changed canonical symbol identity",
1858 )?;
1859 require(
1860 first_projection.source_keys()
1861 != derive_resolution_keys(project_id(3)?, Some("atlas"), &first)?.source_keys(),
1862 "different projects produced the same canonical source identity",
1863 )?;
1864 require(
1865 symbol_keys(&first, &first_projection, "run")
1866 != symbol_keys(
1867 &first,
1868 &derive_resolution_keys(project, Some("other-package"), &first)?,
1869 "run",
1870 ),
1871 "different packages produced the same canonical declaration identity",
1872 )?;
1873
1874 let duplicate = extract_symbol_graph("src/other.rs", Some("rust"), "pub fn run() {}\n");
1875 let caller = extract_symbol_graph(
1876 "src/main.rs",
1877 Some("rust"),
1878 "fn main() { run(); missing(); }\n",
1879 );
1880 let duplicate_projection = derive_resolution_keys(project, Some("atlas"), &duplicate)?;
1881 let caller_projection = derive_resolution_keys(project, Some("atlas"), &caller)?;
1882 let run_dependencies = relation_keys(&caller, &caller_projection, "run");
1883 require(
1884 keys_intersect(
1885 run_dependencies,
1886 symbol_keys(&first, &first_projection, "run"),
1887 ),
1888 "ambiguous call did not retain the first matching declaration key",
1889 )?;
1890 require(
1891 keys_intersect(
1892 run_dependencies,
1893 symbol_keys(&duplicate, &duplicate_projection, "run"),
1894 ),
1895 "ambiguous call did not retain the duplicate declaration key",
1896 )?;
1897 let missing_dependencies = relation_keys(&caller, &caller_projection, "missing");
1898 require(
1899 !missing_dependencies.is_empty(),
1900 "unresolved call lost its canonical dependency key",
1901 )?;
1902 require(
1903 !keys_intersect(
1904 missing_dependencies,
1905 symbol_keys(&first, &first_projection, "run"),
1906 ),
1907 "unresolved call matched an unrelated declaration key",
1908 )?;
1909
1910 let package = extract_symbol_graph(
1911 "vendor/library/Cargo.toml",
1912 Some("cargo-manifest"),
1913 "[package]\nname = \"library\"\n",
1914 );
1915 let manifest = extract_symbol_graph(
1916 "Cargo.toml",
1917 Some("cargo-manifest"),
1918 "[package]\nname = \"app\"\n[dependencies]\nlibrary = \"1\"\n",
1919 );
1920 let package_projection = derive_resolution_keys(project, None, &package)?;
1921 let manifest_projection = derive_resolution_keys(project, None, &manifest)?;
1922 require(
1923 package_projection.source_keys().is_empty()
1924 && manifest_projection.source_keys().is_empty(),
1925 "Cargo manifests advertised ordinary source-module identities",
1926 )?;
1927 require(
1928 keys_intersect(
1929 relation_keys(&manifest, &manifest_projection, "library"),
1930 symbol_keys(&package, &package_projection, "library"),
1931 ),
1932 "Cargo dependency did not match its package declaration key",
1933 )?;
1934 Ok(())
1935 }
1936
1937 #[test]
1938 fn cargo_provider_abstains_on_malformed_input_and_retains_duplicate_package_candidates()
1939 -> Result<(), Box<dyn Error>> {
1940 let project = project_id(16)?;
1941 let malformed = extract_symbol_graph(
1942 "Cargo.toml",
1943 Some("cargo-manifest"),
1944 "[package\nname = \"broken\"\n",
1945 );
1946 let malformed_projection = derive_resolution_keys(project, None, &malformed)?;
1947 require(
1948 malformed_projection.source_keys().is_empty()
1949 && malformed_projection.symbol_keys().is_empty()
1950 && malformed_projection.relation_keys().is_empty(),
1951 "malformed Cargo input fabricated semantic identities",
1952 )?;
1953
1954 let first = extract_symbol_graph(
1955 "vendor/first/Cargo.toml",
1956 Some("cargo-manifest"),
1957 "[package]\nname = \"shared-package\"\n",
1958 );
1959 let second = extract_symbol_graph(
1960 "vendor/second/Cargo.toml",
1961 Some("cargo-manifest"),
1962 "[package]\nname = \"shared-package\"\n",
1963 );
1964 let caller = extract_symbol_graph(
1965 "Cargo.toml",
1966 Some("cargo-manifest"),
1967 "[package]\nname = \"app\"\n[dependencies]\nshared-package = \"1\"\n",
1968 );
1969 let first_projection = derive_resolution_keys(project, None, &first)?;
1970 let second_projection = derive_resolution_keys(project, None, &second)?;
1971 let caller_projection = derive_resolution_keys(project, None, &caller)?;
1972 let dependency = relation_keys(&caller, &caller_projection, "shared-package");
1973 require(
1974 keys_intersect(
1975 dependency,
1976 symbol_keys(&first, &first_projection, "shared-package"),
1977 ) && keys_intersect(
1978 dependency,
1979 symbol_keys(&second, &second_projection, "shared-package"),
1980 ),
1981 "duplicate Cargo packages did not remain ambiguity candidates",
1982 )
1983 }
1984
1985 #[test]
1986 fn projection_rejects_invalid_identity_and_excessive_key_fanout() -> Result<(), Box<dyn Error>>
1987 {
1988 let graph = extract_symbol_graph("src/lib.rs", Some("rust"), "pub fn run() {}\n");
1989 require(
1990 matches!(
1991 derive_resolution_keys(project_id(4)?, Some("bad\0package"), &graph),
1992 Err(ResolutionProjectionError::Contract(_))
1993 ),
1994 "invalid package identity was accepted",
1995 )?;
1996
1997 let imports = (0..65)
1998 .map(|index| format!("import {{ run as execute }} from './module{index}';"))
1999 .collect::<Vec<_>>()
2000 .join("\n");
2001 let source = format!("{imports}\nexecute();\n");
2002 let graph = extract_symbol_graph("src/main.ts", Some("typescript"), &source);
2003 require(
2004 matches!(
2005 derive_resolution_keys(project_id(4)?, Some("web"), &graph),
2006 Err(ResolutionProjectionError::KeyLimit {
2007 fact: "relation",
2008 ..
2009 })
2010 ),
2011 "excessive relation-key fan-out was not rejected",
2012 )?;
2013 Ok(())
2014 }
2015
2016 #[test]
2017 fn unsupported_and_lockfile_languages_do_not_gain_generic_name_resolution()
2018 -> Result<(), Box<dyn Error>> {
2019 for graph in [
2020 extract_symbol_graph(
2021 "src/Service.java",
2022 Some("java"),
2023 "public class Service { void run() {} }\n",
2024 ),
2025 extract_symbol_graph(
2026 "Cargo.lock",
2027 Some("cargo-lock"),
2028 "[[package]]\nname = \"dependency\"\nversion = \"1.0.0\"\n",
2029 ),
2030 ] {
2031 let projection = derive_resolution_keys(project_id(9)?, Some("app"), &graph)?;
2032 require(
2033 projection.source_keys().is_empty()
2034 && projection.symbol_keys().is_empty()
2035 && projection.relation_keys().is_empty(),
2036 "unsupported language emitted generic resolution keys",
2037 )?;
2038 }
2039 Ok(())
2040 }
2041
2042 fn require(condition: bool, message: &'static str) -> Result<(), Box<dyn Error>> {
2043 if condition {
2044 Ok(())
2045 } else {
2046 Err(io::Error::other(message).into())
2047 }
2048 }
2049
2050 fn project_id(byte: u8) -> Result<ProjectInstanceId, Box<dyn Error>> {
2051 Ok(ProjectInstanceId::from_bytes([byte; 16])?)
2052 }
2053
2054 fn assert_alias_resolution(
2055 project: ProjectInstanceId,
2056 package: Option<&str>,
2057 target: &SymbolGraph,
2058 symbol_name: &str,
2059 caller: &SymbolGraph,
2060 call_targets: &[&str],
2061 ) -> Result<(), Box<dyn Error>> {
2062 let target_projection = derive_resolution_keys(project, package, target)?;
2063 let caller_projection = derive_resolution_keys(project, package, caller)?;
2064 let exports = symbol_keys(target, &target_projection, symbol_name);
2065 for call_target in call_targets {
2066 if !keys_intersect(
2067 relation_keys(caller, &caller_projection, call_target),
2068 exports,
2069 ) {
2070 return Err(io::Error::other(format!(
2071 "{call_target} did not share a canonical key with {symbol_name}"
2072 ))
2073 .into());
2074 }
2075 }
2076 Ok(())
2077 }
2078
2079 fn assert_import_matches_only_source(
2080 project: ProjectInstanceId,
2081 package: Option<&str>,
2082 caller: &SymbolGraph,
2083 expected: &SymbolGraph,
2084 wrong: &SymbolGraph,
2085 ) -> Result<(), Box<dyn Error>> {
2086 let caller_projection = derive_resolution_keys(project, package, caller)?;
2087 let expected_projection = derive_resolution_keys(project, package, expected)?;
2088 let wrong_projection = derive_resolution_keys(project, package, wrong)?;
2089 let imports = relation_keys_of_kind(caller, &caller_projection, RelationKind::Imports);
2090 require(
2091 imports.len() == 1,
2092 "caller did not retain one import relationship",
2093 )?;
2094 require(
2095 keys_intersect(imports[0], expected_projection.source_keys()),
2096 "caller import missed its exact source",
2097 )?;
2098 require(
2099 !keys_intersect(imports[0], wrong_projection.source_keys()),
2100 "caller import matched a wrong-sibling basename",
2101 )
2102 }
2103
2104 fn symbol_keys<'a>(
2105 graph: &SymbolGraph,
2106 projection: &'a super::ResolutionKeyProjection,
2107 name: &str,
2108 ) -> &'a [CanonicalResolutionKey] {
2109 projection
2110 .symbol_keys()
2111 .iter()
2112 .find(|entry| graph.symbols[entry.symbol_index()].name == name)
2113 .map_or(&[], super::SymbolResolutionKeys::keys)
2114 }
2115
2116 fn relation_keys<'a>(
2117 graph: &SymbolGraph,
2118 projection: &'a super::ResolutionKeyProjection,
2119 target: &str,
2120 ) -> &'a [CanonicalResolutionKey] {
2121 projection
2122 .relation_keys()
2123 .iter()
2124 .find(|entry| {
2125 let relation = &graph.relations[entry.relation_index()];
2126 matches!(relation.kind, RelationKind::Calls | RelationKind::DependsOn)
2127 && relation.target_name == target
2128 })
2129 .map_or(&[], super::RelationResolutionKeys::keys)
2130 }
2131
2132 fn relation_keys_of_kind<'a>(
2133 graph: &SymbolGraph,
2134 projection: &'a super::ResolutionKeyProjection,
2135 kind: RelationKind,
2136 ) -> Vec<&'a [CanonicalResolutionKey]> {
2137 projection
2138 .relation_keys()
2139 .iter()
2140 .filter(|entry| graph.relations[entry.relation_index()].kind == kind)
2141 .map(super::RelationResolutionKeys::keys)
2142 .collect()
2143 }
2144
2145 fn keys_intersect(left: &[CanonicalResolutionKey], right: &[CanonicalResolutionKey]) -> bool {
2146 left.iter().any(|key| right.binary_search(key).is_ok())
2147 }
2148}