1use crate::ImportReference;
4use projectatlas_core::graph::SourceSpan;
5use projectatlas_core::symbols::{ParserKind, RelationKind, SymbolGraph};
6use std::collections::{HashMap, HashSet};
7use std::sync::Arc;
8
9#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
11pub enum SourceSemanticRemapError {
12 #[error("source semantic evidence does not match the graph dimensions")]
14 GraphMismatch,
15 #[error("source semantic ordinal mapping is not one-to-one and in bounds")]
17 InvalidOrdinalMap,
18 #[error("source semantic occurrence does not match its relation family")]
20 RelationMismatch,
21}
22
23#[derive(Clone, Copy, Debug, Eq, PartialEq)]
25pub struct SourceByteSpan {
26 pub start: usize,
28 pub end: usize,
30}
31
32#[derive(Clone, Debug, Eq, PartialEq)]
34pub enum SourceCallBinding {
35 LocalDeclaration(usize),
37 LocalDeclarations(Arc<[usize]>),
39 Qualified(ImportReference),
41 RustPath(crate::SourceRustCall),
43 Imported {
45 import: usize,
47 reference: usize,
49 member: Option<String>,
51 rust_toolchain: Option<Box<ImportReference>>,
53 },
54 Shadowed,
56 Unresolved,
58 PhpNamespace,
60}
61
62#[derive(Clone, Debug, Eq, PartialEq)]
64pub struct SourceCallFact {
65 pub source_span: Option<SourceSpan>,
67 pub relation: usize,
69 pub span: SourceByteSpan,
71 pub owner: Option<usize>,
73 pub binding: SourceCallBinding,
75}
76
77#[derive(Clone, Debug, Eq, PartialEq)]
79pub struct SourceImportFact {
80 pub source_span: Option<SourceSpan>,
82 pub relation: usize,
84 pub span: SourceByteSpan,
86 pub module: Option<String>,
88 pub references: Vec<ImportReference>,
90 pub rust: Option<Arc<crate::SourceRustUse>>,
92 pub rust_toolchain: Option<Box<ImportReference>>,
94}
95
96#[derive(Clone, Debug, Eq, PartialEq)]
98pub enum SourceExportBinding {
99 Unresolved,
101 LocalDeclaration(usize),
103 AmbiguousDeclarations(Arc<[usize]>),
105 UnresolvedDeclarations(Arc<[usize]>),
110 ReExport(ImportReference),
112 RustReExport(Arc<crate::SourceRustUse>),
114}
115
116#[derive(Clone, Copy, Debug, Eq, PartialEq)]
118pub enum SourceExportNamespace {
119 Value,
121 Type,
123}
124
125#[derive(Clone, Debug, Eq, PartialEq)]
130pub struct SourceExportFact {
131 pub namespace: SourceExportNamespace,
133 pub name: String,
135 pub span: SourceByteSpan,
137 pub line: usize,
139 pub binding: SourceExportBinding,
141}
142
143#[derive(Clone, Copy, Debug, Eq, PartialEq)]
145pub(crate) enum SourceSymbolRemap {
146 Retained(usize),
148 Ambiguous(usize),
150 Omitted,
152}
153
154impl SourceSymbolRemap {
155 pub(crate) const fn retained(self) -> Option<usize> {
157 match self {
158 Self::Retained(index) => Some(index),
159 Self::Ambiguous(_) | Self::Omitted => None,
160 }
161 }
162
163 const fn candidate(self) -> Option<usize> {
165 match self {
166 Self::Retained(index) | Self::Ambiguous(index) => Some(index),
167 Self::Omitted => None,
168 }
169 }
170}
171
172#[derive(Clone, Debug, Default, Eq, PartialEq)]
178pub struct SourceSemanticFacts {
179 pub(crate) path: String,
181 pub(crate) content_hash: String,
183 pub(crate) symbol_count: usize,
185 pub(crate) relation_count: usize,
187 pub(crate) graph_digest: [u8; 32],
189 pub(crate) imports: Vec<SourceImportFact>,
191 pub(crate) bindings_incomplete: bool,
193 pub(crate) calls: Vec<SourceCallFact>,
195 pub(crate) exports: Vec<SourceExportFact>,
197 pub(crate) exports_incomplete: bool,
199 pub(crate) rust: Option<crate::SourceRustFacts>,
201 pub(crate) cargo: Option<crate::SourceCargoTargets>,
203}
204
205impl SourceSemanticFacts {
206 #[must_use]
208 pub fn path(&self) -> &str {
209 &self.path
210 }
211
212 #[must_use]
214 pub fn content_hash(&self) -> &str {
215 &self.content_hash
216 }
217
218 #[must_use]
220 pub fn imports(&self) -> &[SourceImportFact] {
221 &self.imports
222 }
223
224 #[must_use]
226 pub fn calls(&self) -> &[SourceCallFact] {
227 &self.calls
228 }
229
230 #[must_use]
232 pub fn exports(&self) -> &[SourceExportFact] {
233 &self.exports
234 }
235
236 #[must_use]
238 pub const fn rust(&self) -> Option<&crate::SourceRustFacts> {
239 self.rust.as_ref()
240 }
241
242 #[must_use]
244 pub const fn cargo(&self) -> Option<&crate::SourceCargoTargets> {
245 self.cargo.as_ref()
246 }
247
248 #[must_use]
250 pub const fn exports_complete(&self) -> bool {
251 !self.exports_incomplete
252 }
253
254 #[must_use]
259 pub fn retained_bytes(&self) -> u64 {
260 let mut rust_uses = HashSet::new();
261 let mut rust_modules = HashSet::new();
262 let mut import_modules = HashSet::new();
263 let mut bytes = (std::mem::size_of::<Self>() as u64)
264 .saturating_add(self.path.capacity() as u64)
265 .saturating_add(self.content_hash.capacity() as u64)
266 .saturating_add(
267 (self.imports.capacity() as u64)
268 .saturating_mul(std::mem::size_of::<SourceImportFact>() as u64),
269 )
270 .saturating_add(
271 (self.calls.capacity() as u64)
272 .saturating_mul(std::mem::size_of::<SourceCallFact>() as u64),
273 );
274 for import in &self.imports {
275 if let Some(reference) = &import.rust_toolchain {
276 bytes = bytes
277 .saturating_add(std::mem::size_of::<ImportReference>() as u64)
278 .saturating_add(reference.retained_heap_bytes(&mut import_modules));
279 }
280 if let Some(binding) = &import.rust {
281 bytes = bytes.saturating_add(crate::rust_binding_source::use_retained_bytes(
282 binding,
283 &mut rust_uses,
284 &mut rust_modules,
285 ));
286 }
287 bytes = bytes
288 .saturating_add(import.module.as_ref().map_or(0, String::capacity) as u64)
289 .saturating_add(
290 (import.references.capacity() as u64)
291 .saturating_mul(std::mem::size_of::<ImportReference>() as u64),
292 );
293 for reference in &import.references {
294 bytes = bytes.saturating_add(reference.retained_heap_bytes(&mut import_modules));
295 }
296 }
297 let mut shared = HashSet::new();
298 bytes = bytes
299 .saturating_add(call_binding_heap_bytes_with_shared(
300 &self.calls,
301 &mut shared,
302 &mut rust_modules,
303 &mut import_modules,
304 ))
305 .saturating_add(
306 (self.exports.capacity() as u64)
307 .saturating_mul(std::mem::size_of::<SourceExportFact>() as u64),
308 );
309 for export in &self.exports {
310 bytes = bytes
311 .saturating_add(export.name.capacity() as u64)
312 .saturating_add(match &export.binding {
313 SourceExportBinding::ReExport(reference) => {
314 reference.retained_heap_bytes(&mut import_modules)
315 }
316 SourceExportBinding::RustReExport(binding) => {
317 crate::rust_binding_source::use_retained_bytes(
318 binding,
319 &mut rust_uses,
320 &mut rust_modules,
321 )
322 }
323 SourceExportBinding::AmbiguousDeclarations(symbols)
324 | SourceExportBinding::UnresolvedDeclarations(symbols)
325 if shared.insert(Arc::as_ptr(symbols)) =>
326 {
327 ((symbols.len() as u64).saturating_add(2))
328 .saturating_mul(std::mem::size_of::<usize>() as u64)
329 }
330 SourceExportBinding::LocalDeclaration(_)
331 | SourceExportBinding::AmbiguousDeclarations(_)
332 | SourceExportBinding::UnresolvedDeclarations(_)
333 | SourceExportBinding::Unresolved => 0,
334 });
335 }
336 if let Some(rust) = &self.rust {
337 bytes = bytes.saturating_add(rust.retained_bytes(&mut rust_uses, &mut rust_modules));
338 }
339 if let Some(cargo) = &self.cargo {
340 bytes = bytes.saturating_add(cargo.retained_bytes());
341 }
342 bytes
343 }
344
345 #[must_use]
347 pub fn matches_graph(&self, graph: &SymbolGraph) -> bool {
348 let mut checked_declarations = HashSet::new();
349 let mut checked_rust_imports = HashSet::new();
350 self.path == graph.path
351 && self.symbol_count == graph.symbols.len()
352 && self.relation_count == graph.relations.len()
353 && self.graph_digest == graph_ordinal_digest(graph)
354 && self.imports.iter().all(|fact| {
355 graph
356 .relations
357 .get(fact.relation)
358 .is_some_and(|relation| relation.kind == RelationKind::Imports)
359 })
360 && self.calls.iter().all(|fact| {
361 graph
362 .relations
363 .get(fact.relation)
364 .is_some_and(|relation| relation.kind == RelationKind::Calls)
365 && self.binding_is_in_bounds(&fact.binding, &mut checked_declarations)
366 && fact.owner.is_none_or(|owner| owner < self.symbol_count)
367 })
368 && self.exports.iter().all(|fact| match &fact.binding {
369 SourceExportBinding::LocalDeclaration(symbol) => *symbol < self.symbol_count,
370 SourceExportBinding::AmbiguousDeclarations(symbols)
371 | SourceExportBinding::UnresolvedDeclarations(symbols) => {
372 !symbols.is_empty()
373 && (!checked_declarations.insert(Arc::as_ptr(symbols))
374 || symbols.iter().all(|symbol| *symbol < self.symbol_count))
375 }
376 SourceExportBinding::ReExport(_)
377 | SourceExportBinding::RustReExport(_)
378 | SourceExportBinding::Unresolved => true,
379 })
380 && self.rust.as_ref().is_none_or(|rust| {
381 rust.declarations
382 .iter()
383 .all(|declaration| match declaration.binding {
384 crate::SourceRustBinding::Declaration(symbol)
385 | crate::SourceRustBinding::TypeDeclaration(symbol) => {
386 symbol < self.symbol_count
387 }
388 crate::SourceRustBinding::Use(_)
389 | crate::SourceRustBinding::Unresolved
390 | crate::SourceRustBinding::UnresolvedType
391 | crate::SourceRustBinding::UnresolvedImport => true,
392 })
393 && rust.candidate_calls.iter().all(|call| {
394 graph
395 .relations
396 .get(call.relation)
397 .is_some_and(|relation| relation.kind == RelationKind::Calls)
398 && !call.bindings.is_empty()
399 && (!checked_declarations.insert(Arc::as_ptr(&call.declarations))
400 || call
401 .declarations
402 .iter()
403 .all(|symbol| *symbol < self.symbol_count))
404 && (!checked_rust_imports.insert(Arc::as_ptr(&call.bindings))
405 || call.bindings.iter().all(|(import, reference)| {
406 self.imports
407 .get(*import)
408 .is_some_and(|import| *reference < import.references.len())
409 }))
410 })
411 && rust.associated.iter().all(|method| {
412 method
413 .declaration
414 .is_none_or(|ordinal| ordinal < self.symbol_count)
415 })
416 })
417 }
418
419 pub fn remap(
432 &self,
433 original: &SymbolGraph,
434 graph: &SymbolGraph,
435 symbols: &[Option<usize>],
436 relations: &[Option<usize>],
437 ) -> Result<Self, SourceSemanticRemapError> {
438 if !self.matches_graph(original)
439 || self.path != graph.path
440 || self.symbol_count != symbols.len()
441 || self.relation_count != relations.len()
442 {
443 return Err(SourceSemanticRemapError::GraphMismatch);
444 }
445 validate_ordinal_map(symbols, graph.symbols.len())?;
446 validate_ordinal_map(relations, graph.relations.len())?;
447 let (imports, calls) = self.remap_associations(symbols, relations);
448 let exports = self.remap_exports(symbols, None);
449 let result = Self {
450 path: self.path.clone(),
451 content_hash: self.content_hash.clone(),
452 symbol_count: graph.symbols.len(),
453 relation_count: graph.relations.len(),
454 graph_digest: graph_ordinal_digest(graph),
455 imports,
456 bindings_incomplete: self.bindings_incomplete,
457 calls,
458 exports,
459 exports_incomplete: self.exports_incomplete,
460 rust: self.remap_rust(symbols, relations),
461 cargo: self.cargo.clone(),
462 };
463 if !result.matches_graph(graph) {
464 return Err(SourceSemanticRemapError::RelationMismatch);
465 }
466 Ok(result)
467 }
468
469 pub fn retain_graph_facts(
479 &mut self,
480 graph: &mut SymbolGraph,
481 symbols: &[bool],
482 relations: &[bool],
483 ) -> Result<(), SourceSemanticRemapError> {
484 if !self.matches_graph(graph)
485 || symbols.len() != graph.symbols.len()
486 || relations.len() != graph.relations.len()
487 {
488 return Err(SourceSemanticRemapError::GraphMismatch);
489 }
490 let symbol_ordinals = retained_ordinals(symbols);
491 let relation_ordinals = retained_ordinals(relations);
492 let (imports, calls) = self.remap_associations(&symbol_ordinals, &relation_ordinals);
493 let exports = self.remap_exports(&symbol_ordinals, None);
494 let rust = self.remap_rust(&symbol_ordinals, &relation_ordinals);
495 let mut symbol = 0;
496 graph.symbols.retain(|_| {
497 let retained = symbols[symbol];
498 symbol += 1;
499 retained
500 });
501 let mut relation = 0;
502 graph.relations.retain(|_| {
503 let retained = relations[relation];
504 relation += 1;
505 retained
506 });
507 self.symbol_count = graph.symbols.len();
508 self.relation_count = graph.relations.len();
509 self.graph_digest = graph_ordinal_digest(graph);
510 self.imports = imports;
511 self.calls = calls;
512 self.exports = exports;
513 self.rust = rust;
514 Ok(())
515 }
516
517 pub(crate) fn append_mapped(
526 &mut self,
527 source: Self,
528 graph: &SymbolGraph,
529 symbols: &[SourceSymbolRemap],
530 relations: &[Option<usize>],
531 ) -> Result<(), SourceSemanticRemapError> {
532 if source.rust.is_some()
533 || source.cargo.is_some()
534 || source.path != graph.path
535 || (!self.path.is_empty() && self.path != graph.path)
536 || source.symbol_count != symbols.len()
537 || source.relation_count != relations.len()
538 || self.symbol_count > graph.symbols.len()
539 || self.relation_count > graph.relations.len()
540 {
541 return Err(SourceSemanticRemapError::GraphMismatch);
542 }
543 if symbols.iter().any(|symbol| {
544 symbol
545 .candidate()
546 .is_some_and(|index| index >= graph.symbols.len())
547 }) {
548 return Err(SourceSemanticRemapError::InvalidOrdinalMap);
549 }
550 let retained_symbols = symbols
551 .iter()
552 .map(|symbol| symbol.retained())
553 .collect::<Vec<_>>();
554 validate_ordinal_map(&retained_symbols, graph.symbols.len())?;
555 validate_ordinal_map(relations, graph.relations.len())?;
556 let (imports, calls) = source.remap_associations(&retained_symbols, relations);
557 let exports = source.remap_exports(&retained_symbols, Some(symbols));
558 let mut mapped = Self {
559 path: source.path,
560 content_hash: String::new(),
561 symbol_count: graph.symbols.len(),
562 relation_count: graph.relations.len(),
563 graph_digest: graph_ordinal_digest(graph),
564 imports,
565 bindings_incomplete: source.bindings_incomplete,
566 calls,
567 exports,
568 exports_incomplete: source.exports_incomplete,
569 rust: None,
570 cargo: None,
571 };
572 if !mapped.matches_graph(graph) {
573 return Err(SourceSemanticRemapError::RelationMismatch);
574 }
575 for call in &mut mapped.calls {
576 if let SourceCallBinding::Imported { import, .. } = &mut call.binding {
577 *import = import
578 .checked_add(self.imports.len())
579 .ok_or(SourceSemanticRemapError::InvalidOrdinalMap)?;
580 }
581 }
582 self.bindings_incomplete |= mapped.bindings_incomplete;
583 self.imports.extend(mapped.imports);
584 self.calls.extend(mapped.calls);
585 self.exports.extend(mapped.exports);
586 self.exports_incomplete |= mapped.exports_incomplete;
587 self.path = mapped.path;
588 self.symbol_count = mapped.symbol_count;
589 self.relation_count = mapped.relation_count;
590 self.graph_digest = mapped.graph_digest;
591 Ok(())
592 }
593
594 fn remap_associations(
596 &self,
597 symbols: &[Option<usize>],
598 relations: &[Option<usize>],
599 ) -> (Vec<SourceImportFact>, Vec<SourceCallFact>) {
600 let mut imports = Vec::with_capacity(self.imports.len());
601 let mut import_ordinals = vec![None; self.imports.len()];
602 for (ordinal, fact) in self.imports.iter().enumerate() {
603 let Some(relation) = relations.get(fact.relation).copied().flatten() else {
604 continue;
605 };
606 import_ordinals[ordinal] = Some(imports.len());
607 imports.push(SourceImportFact {
608 relation,
609 span: fact.span,
610 source_span: fact.source_span,
611 module: fact.module.clone(),
612 references: fact.references.clone(),
613 rust: fact.rust.clone(),
614 rust_toolchain: fact.rust_toolchain.clone(),
615 });
616 }
617 let mut calls = Vec::with_capacity(self.calls.len());
618 let mut declaration_remaps = HashMap::new();
619 for fact in &self.calls {
620 let Some(relation) = relations.get(fact.relation).copied().flatten() else {
621 continue;
622 };
623 calls.push(SourceCallFact {
624 relation,
625 span: fact.span,
626 source_span: fact.source_span,
627 owner: fact
628 .owner
629 .and_then(|owner| symbols.get(owner).copied().flatten()),
630 binding: remap_call_binding(
631 &fact.binding,
632 symbols,
633 &import_ordinals,
634 &mut declaration_remaps,
635 ),
636 });
637 }
638 (imports, calls)
639 }
640
641 fn remap_rust(
643 &self,
644 symbols: &[Option<usize>],
645 relations: &[Option<usize>],
646 ) -> Option<crate::SourceRustFacts> {
647 self.rust.as_ref().map(|facts| {
648 let surviving_uses = self
649 .imports
650 .iter()
651 .filter(|import| relations.get(import.relation).copied().flatten().is_some())
652 .filter_map(|import| import.rust.as_ref().map(Arc::as_ptr))
653 .collect::<HashSet<_>>();
654 let mut mapped = facts.clone();
655 for declaration in &mut mapped.declarations {
656 declaration.binding = match &declaration.binding {
657 crate::SourceRustBinding::Declaration(symbol) => {
658 symbols.get(*symbol).copied().flatten().map_or(
659 crate::SourceRustBinding::Unresolved,
660 crate::SourceRustBinding::Declaration,
661 )
662 }
663 crate::SourceRustBinding::TypeDeclaration(symbol) => {
664 symbols.get(*symbol).copied().flatten().map_or(
665 crate::SourceRustBinding::UnresolvedType,
666 crate::SourceRustBinding::TypeDeclaration,
667 )
668 }
669 crate::SourceRustBinding::Use(binding)
670 if !surviving_uses.contains(&Arc::as_ptr(binding)) =>
671 {
672 crate::SourceRustBinding::UnresolvedImport
673 }
674 binding => binding.clone(),
675 };
676 }
677 for method in &mut mapped.associated {
678 method.declaration = method
679 .declaration
680 .and_then(|ordinal| symbols.get(ordinal).copied().flatten());
681 }
682 let mut next_import = 0;
683 let import_ordinals = self
684 .imports
685 .iter()
686 .map(|import| {
687 relations.get(import.relation).copied().flatten().map(|_| {
688 let ordinal = next_import;
689 next_import += 1;
690 ordinal
691 })
692 })
693 .collect::<Vec<_>>();
694 let mut candidates =
695 HashMap::<*const [(usize, usize)], Option<Arc<[(usize, usize)]>>>::new();
696 let mut declaration_candidates = HashMap::<*const [usize], Option<Arc<[usize]>>>::new();
697 mapped.candidate_calls = facts
698 .candidate_calls
699 .iter()
700 .filter_map(|call| {
701 let relation = relations.get(call.relation).copied().flatten()?;
702 let bindings = candidates
703 .entry(Arc::as_ptr(&call.bindings))
704 .or_insert_with(|| {
705 call.bindings
706 .iter()
707 .map(|(import, reference)| {
708 import_ordinals
709 .get(*import)
710 .copied()
711 .flatten()
712 .map(|import| (import, *reference))
713 })
714 .collect::<Option<Vec<_>>>()
715 .map(Arc::from)
716 })
717 .as_ref()?;
718 let declarations = declaration_candidates
719 .entry(Arc::as_ptr(&call.declarations))
720 .or_insert_with(|| {
721 call.declarations
722 .iter()
723 .map(|symbol| symbols.get(*symbol).copied().flatten())
724 .collect::<Option<Vec<_>>>()
725 .map(Arc::from)
726 })
727 .as_ref()?;
728 Some(crate::SourceRustCallImports {
729 relation,
730 declarations: Arc::clone(declarations),
731 bindings: Arc::clone(bindings),
732 member: call.member.clone(),
733 })
734 })
735 .collect();
736 mapped
737 })
738 }
739
740 fn remap_exports(
742 &self,
743 symbols: &[Option<usize>],
744 candidates: Option<&[SourceSymbolRemap]>,
745 ) -> Vec<SourceExportFact> {
746 let mut ambiguity = HashMap::new();
747 self.exports
748 .iter()
749 .map(|fact| {
750 let binding = match &fact.binding {
751 SourceExportBinding::LocalDeclaration(symbol) => {
752 if let Some(symbol) = symbols.get(*symbol).copied().flatten() {
753 SourceExportBinding::LocalDeclaration(symbol)
754 } else if let Some(candidate) = candidates
755 .and_then(|candidates| candidates.get(*symbol))
756 .and_then(|candidate| candidate.candidate())
757 {
758 SourceExportBinding::AmbiguousDeclarations(vec![candidate].into())
759 } else {
760 SourceExportBinding::Unresolved
761 }
762 }
763 SourceExportBinding::AmbiguousDeclarations(original)
764 | SourceExportBinding::UnresolvedDeclarations(original) => {
765 let mapped: &Arc<[usize]> =
766 ambiguity.entry(original.as_ptr()).or_insert_with(|| {
767 let mut mapped = original
768 .iter()
769 .filter_map(|symbol| {
770 symbols
771 .get(*symbol)
772 .copied()
773 .flatten()
774 .or_else(|| candidates?.get(*symbol)?.candidate())
775 })
776 .collect::<Vec<_>>();
777 mapped.sort_unstable();
778 mapped.dedup();
779 mapped.into()
780 });
781 if mapped.is_empty() {
782 SourceExportBinding::Unresolved
783 } else if matches!(
784 fact.binding,
785 SourceExportBinding::UnresolvedDeclarations(_)
786 ) {
787 SourceExportBinding::UnresolvedDeclarations(Arc::clone(mapped))
788 } else {
789 SourceExportBinding::AmbiguousDeclarations(Arc::clone(mapped))
790 }
791 }
792 SourceExportBinding::ReExport(reference) => {
793 SourceExportBinding::ReExport(reference.clone())
794 }
795 SourceExportBinding::RustReExport(binding) => {
796 SourceExportBinding::RustReExport(Arc::clone(binding))
797 }
798 SourceExportBinding::Unresolved => SourceExportBinding::Unresolved,
799 };
800 SourceExportFact {
801 namespace: fact.namespace,
802 name: fact.name.clone(),
803 span: fact.span,
804 line: fact.line,
805 binding,
806 }
807 })
808 .collect()
809 }
810
811 fn binding_is_in_bounds(
813 &self,
814 binding: &SourceCallBinding,
815 checked_declarations: &mut HashSet<*const [usize]>,
816 ) -> bool {
817 match binding {
818 SourceCallBinding::LocalDeclaration(symbol) => *symbol < self.symbol_count,
819 SourceCallBinding::LocalDeclarations(symbols) => {
820 !symbols.is_empty()
821 && (!checked_declarations.insert(Arc::as_ptr(symbols))
822 || symbols.iter().all(|symbol| *symbol < self.symbol_count))
823 }
824 SourceCallBinding::Imported {
825 import, reference, ..
826 } => self
827 .imports
828 .get(*import)
829 .is_some_and(|fact| *reference < fact.references.len()),
830 SourceCallBinding::Qualified(_)
831 | SourceCallBinding::RustPath(_)
832 | SourceCallBinding::Shadowed
833 | SourceCallBinding::Unresolved
834 | SourceCallBinding::PhpNamespace => true,
835 }
836 }
837
838 pub(crate) fn bind_source_spans<E>(
844 &mut self,
845 content: &str,
846 check: &mut impl FnMut() -> Result<(), E>,
847 ) -> Result<(), E> {
848 let mut offsets = Vec::with_capacity(
849 self.calls
850 .len()
851 .saturating_add(self.imports.len())
852 .saturating_mul(2),
853 );
854 for span in self
855 .calls
856 .iter()
857 .map(|fact| fact.span)
858 .chain(self.imports.iter().map(|fact| fact.span))
859 {
860 check()?;
861 offsets.push(span.start);
862 offsets.push(span.end);
863 }
864 let positions = crate::source_positions::source_positions(content, offsets, check)?;
865 for (span, target) in self
866 .calls
867 .iter_mut()
868 .map(|fact| (fact.span, &mut fact.source_span))
869 .chain(
870 self.imports
871 .iter_mut()
872 .map(|fact| (fact.span, &mut fact.source_span)),
873 )
874 {
875 check()?;
876 *target = occurrence_source_span(span, &positions);
877 }
878 Ok(())
879 }
880
881 pub(crate) fn bind_source(&mut self, graph: &SymbolGraph, content: &str) {
883 self.path.clone_from(&graph.path);
884 self.content_hash = blake3::hash(content.as_bytes()).to_hex().to_string();
885 self.symbol_count = graph.symbols.len();
886 self.relation_count = graph.relations.len();
887 self.graph_digest = graph_ordinal_digest(graph);
888 }
889}
890
891fn occurrence_source_span(
893 span: SourceByteSpan,
894 positions: &[(usize, usize, usize)],
895) -> Option<SourceSpan> {
896 let start = positions.get(
897 positions
898 .binary_search_by_key(&span.start, |point| point.0)
899 .ok()?,
900 )?;
901 let end = positions.get(
902 positions
903 .binary_search_by_key(&span.end, |point| point.0)
904 .ok()?,
905 )?;
906 SourceSpan::new(
907 u32::try_from(start.1).ok()?,
908 u32::try_from(start.2).ok()?,
909 u32::try_from(end.1).ok()?,
910 u32::try_from(end.2).ok()?,
911 )
912 .ok()
913}
914
915fn graph_ordinal_digest(graph: &SymbolGraph) -> [u8; 32] {
917 let mut hash = blake3::Hasher::new();
918 hash.update(b"source-binding-ordinals");
919 hash_text(&mut hash, &graph.path);
920 hash_optional_text(&mut hash, graph.language.as_deref());
921 hash.update(&[graph.parser as u8]);
922 hash.update(&(graph.symbols.len() as u64).to_le_bytes());
923 for symbol in &graph.symbols {
924 hash_text(&mut hash, &symbol.path);
925 hash_text(&mut hash, &symbol.name);
926 hash_text(&mut hash, &symbol.signature);
927 hash_optional_text(&mut hash, symbol.parent.as_deref());
928 hash.update(&[
929 symbol.kind as u8,
930 symbol.parser as u8,
931 u8::from(symbol.exported),
932 ]);
933 hash.update(&(symbol.line_start as u64).to_le_bytes());
934 hash.update(&(symbol.line_end as u64).to_le_bytes());
935 if let Some(selector) = symbol.source_selector {
936 hash.update(&[1]);
937 for ordinal in [
938 selector.byte_start,
939 selector.byte_end,
940 selector.column_start,
941 selector.column_end,
942 ] {
943 hash.update(&(ordinal as u64).to_le_bytes());
944 }
945 } else {
946 hash.update(&[0]);
947 }
948 }
949 hash.update(&(graph.relations.len() as u64).to_le_bytes());
950 for relation in &graph.relations {
951 hash_text(&mut hash, &relation.path);
952 hash_text(&mut hash, &relation.source_name);
953 hash_text(&mut hash, &relation.target_name);
954 hash_text(&mut hash, &relation.context);
955 hash.update(&[relation.kind as u8, relation.parser as u8]);
956 hash.update(&(relation.line as u64).to_le_bytes());
957 }
958 *hash.finalize().as_bytes()
959}
960
961fn hash_text(hash: &mut blake3::Hasher, text: &str) {
963 hash.update(&(text.len() as u64).to_le_bytes());
964 hash.update(text.as_bytes());
965}
966
967fn hash_optional_text(hash: &mut blake3::Hasher, text: Option<&str>) {
969 if let Some(text) = text {
970 hash.update(&[1]);
971 hash_text(hash, text);
972 } else {
973 hash.update(&[0]);
974 }
975}
976
977fn retained_ordinals(retained: &[bool]) -> Vec<Option<usize>> {
979 let mut next = 0;
980 retained
981 .iter()
982 .map(|retained| {
983 if *retained {
984 let ordinal = next;
985 next += 1;
986 Some(ordinal)
987 } else {
988 None
989 }
990 })
991 .collect()
992}
993
994fn validate_ordinal_map(
996 mapping: &[Option<usize>],
997 output_count: usize,
998) -> Result<(), SourceSemanticRemapError> {
999 let mut seen = vec![false; output_count];
1000 for ordinal in mapping.iter().flatten() {
1001 let Some(slot) = seen.get_mut(*ordinal) else {
1002 return Err(SourceSemanticRemapError::InvalidOrdinalMap);
1003 };
1004 if *slot {
1005 return Err(SourceSemanticRemapError::InvalidOrdinalMap);
1006 }
1007 *slot = true;
1008 }
1009 Ok(())
1010}
1011
1012pub(crate) fn call_binding_heap_bytes(calls: &[SourceCallFact]) -> u64 {
1014 let mut shared = HashSet::new();
1015 call_binding_heap_bytes_with_shared(
1016 calls,
1017 &mut shared,
1018 &mut HashSet::new(),
1019 &mut HashSet::new(),
1020 )
1021}
1022
1023fn call_binding_heap_bytes_with_shared(
1025 calls: &[SourceCallFact],
1026 shared: &mut HashSet<*const [usize]>,
1027 rust_modules: &mut HashSet<*const [String]>,
1028 import_modules: &mut HashSet<*const str>,
1029) -> u64 {
1030 calls.iter().fold(0_u64, |bytes, call| {
1031 bytes.saturating_add(match &call.binding {
1032 SourceCallBinding::Qualified(reference) => {
1033 reference.retained_heap_bytes(import_modules)
1034 }
1035 SourceCallBinding::RustPath(call) => call.path.retained_bytes().saturating_add(
1036 crate::rust_binding_source::module_retained_bytes(&call.module, rust_modules),
1037 ),
1038 SourceCallBinding::Imported {
1039 member,
1040 rust_toolchain,
1041 ..
1042 } => (member.as_ref().map_or(0, String::capacity) as u64).saturating_add(
1043 rust_toolchain.as_ref().map_or(0, |reference| {
1044 (std::mem::size_of::<ImportReference>() as u64)
1045 .saturating_add(reference.retained_heap_bytes(import_modules))
1046 }),
1047 ),
1048 SourceCallBinding::LocalDeclarations(declarations)
1049 if shared.insert(Arc::as_ptr(declarations)) =>
1050 {
1051 ((declarations.len() as u64).saturating_add(2))
1052 .saturating_mul(std::mem::size_of::<usize>() as u64)
1053 }
1054 SourceCallBinding::LocalDeclaration(_)
1055 | SourceCallBinding::LocalDeclarations(_)
1056 | SourceCallBinding::Shadowed
1057 | SourceCallBinding::Unresolved
1058 | SourceCallBinding::PhpNamespace => 0,
1059 })
1060 })
1061}
1062
1063fn remap_call_binding(
1065 binding: &SourceCallBinding,
1066 symbols: &[Option<usize>],
1067 imports: &[Option<usize>],
1068 declaration_remaps: &mut HashMap<*const usize, SourceCallBinding>,
1069) -> SourceCallBinding {
1070 match binding {
1071 SourceCallBinding::LocalDeclaration(symbol) => {
1072 symbols.get(*symbol).copied().flatten().map_or(
1073 SourceCallBinding::Unresolved,
1074 SourceCallBinding::LocalDeclaration,
1075 )
1076 }
1077 SourceCallBinding::LocalDeclarations(original) => declaration_remaps
1078 .entry(original.as_ptr())
1079 .or_insert_with(|| {
1080 original
1081 .iter()
1082 .map(|symbol| symbols.get(*symbol).copied().flatten())
1083 .collect::<Option<Vec<_>>>()
1084 .filter(|mapped| !mapped.is_empty())
1085 .map_or(SourceCallBinding::Unresolved, |mapped| {
1086 SourceCallBinding::LocalDeclarations(mapped.into())
1087 })
1088 })
1089 .clone(),
1090 SourceCallBinding::Imported {
1091 import,
1092 reference,
1093 member,
1094 rust_toolchain,
1095 } => imports.get(*import).copied().flatten().map_or(
1096 SourceCallBinding::Unresolved,
1097 |import| SourceCallBinding::Imported {
1098 import,
1099 reference: *reference,
1100 member: member.clone(),
1101 rust_toolchain: rust_toolchain.clone(),
1102 },
1103 ),
1104 SourceCallBinding::Shadowed
1105 | SourceCallBinding::RustPath(_)
1106 | SourceCallBinding::Qualified(_)
1107 | SourceCallBinding::Unresolved
1108 | SourceCallBinding::PhpNamespace => binding.clone(),
1109 }
1110}
1111
1112#[derive(Debug)]
1114pub struct SourceExtraction {
1115 pub(crate) source_parser: ParserKind,
1117 pub(crate) graph: SymbolGraph,
1119 pub(crate) semantics: SourceSemanticFacts,
1121}
1122
1123impl SourceExtraction {
1124 #[must_use]
1126 pub const fn graph(&self) -> &SymbolGraph {
1127 &self.graph
1128 }
1129
1130 #[must_use]
1132 pub const fn semantics(&self) -> &SourceSemanticFacts {
1133 &self.semantics
1134 }
1135
1136 #[must_use]
1138 pub fn into_parts(self) -> (ParserKind, SymbolGraph, SourceSemanticFacts) {
1139 (self.source_parser, self.graph, self.semantics)
1140 }
1141}
1142
1143#[cfg(test)]
1144mod tests {
1145 use super::*;
1146 use crate::{ImportSyntax, extract_symbol_graph};
1147 use projectatlas_core::symbols::SymbolKind;
1148 use std::fmt::Write as _;
1149
1150 macro_rules! fixture_ok {
1152 ($value:expr) => {{
1153 let outcome = $value;
1154 if let Err(error) = &outcome {
1155 assert!(outcome.is_ok(), "fixture operation failed: {error:?}");
1156 }
1157 let Ok(value) = outcome else {
1158 return;
1159 };
1160 value
1161 }};
1162 }
1163
1164 #[test]
1165 fn native_and_host_occurrences_keep_original_scalar_coordinates_through_filtering() {
1166 for (path, language, source) in [
1167 (
1168 "main.rs",
1169 "rust",
1170 "/* Purpose: 💡π */ fn target(){} fn caller(){let _=\"💡π\";target();target();}",
1171 ),
1172 (
1173 "build.rs",
1174 "rust-build-script",
1175 "fn target(){} fn caller(){let _=\"💡π\";target();target();}",
1176 ),
1177 (
1178 "main.py",
1179 "python",
1180 "def target(): pass\ndef caller(): \"💡π\";target();target()\n",
1181 ),
1182 (
1183 "main.js",
1184 "javascript",
1185 "function target(){} function caller(){const x=\"💡π\";target();target();}",
1186 ),
1187 (
1188 "main.ts",
1189 "typescript",
1190 "function target(){} function caller(){const x=\"💡π\";target();target();}",
1191 ),
1192 (
1193 "main.tsx",
1194 "tsx",
1195 "function target(){} function caller(){const x=\"💡π\";target();target();}",
1196 ),
1197 (
1198 "main.go",
1199 "go",
1200 "package p; func Target(){}; func Caller(){_ = \"💡π\";Target();Target()}",
1201 ),
1202 (
1203 "main.c",
1204 "c",
1205 "void target(){} void caller(){const char*x=\"💡π\";(void)x;target();target();}",
1206 ),
1207 (
1208 "main.cpp",
1209 "cpp",
1210 "void target(){} void caller(){const char*x=\"💡π\";(void)x;target();target();}",
1211 ),
1212 (
1213 "main.m",
1214 "objective-c",
1215 "void target(){} void caller(){const char*x=\"💡π\";(void)x;target();target();}",
1216 ),
1217 (
1218 "Main.java",
1219 "java",
1220 "class Main {void target(){} void caller(){String x=\"💡π\";target();target();}}",
1221 ),
1222 (
1223 "Main.cs",
1224 "csharp",
1225 "class Main {void target(){} void caller(){string x=\"💡π\";target();target();}}",
1226 ),
1227 (
1228 "main.kt",
1229 "kotlin",
1230 "fun target(){}\r\nfun caller(){val x=\"💡π\";target();target()}\r\n",
1231 ),
1232 (
1233 "main.zig",
1234 "zig",
1235 "fn target() void {} fn caller() void {const x=\"💡π\";_=x;target();target();}",
1236 ),
1237 (
1238 "main.php",
1239 "php",
1240 "<?php function target(){} function caller(){$x=\"💡π\";target();target();}",
1241 ),
1242 (
1243 "view.html",
1244 "html",
1245 "<div>💡π</div><script>function target(){} function caller(){target();target();}</script>",
1246 ),
1247 (
1248 "view.vue",
1249 "vue",
1250 "<template>💡π</template><script setup>function target(){} function caller(){target();target();}</script>",
1251 ),
1252 (
1253 "view.svelte",
1254 "svelte",
1255 "<div>💡π</div><script>function target(){} function caller(){target();target();}</script>",
1256 ),
1257 ] {
1258 let result = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
1259 path,
1260 Some(language),
1261 source,
1262 &projectatlas_core::IndexWorkControl::new(
1263 projectatlas_core::IndexCancellation::new(),
1264 None
1265 )
1266 ));
1267 assert_eq!(
1268 result.semantics().calls().len(),
1269 2,
1270 "{language}: {:?}",
1271 result.graph()
1272 );
1273 let original = result.semantics().calls();
1274 assert_ne!(
1275 original[0].source_span, original[1].source_span,
1276 "{language}"
1277 );
1278 for call in original {
1279 let span = fixture_ok!(call.source_span.ok_or("missing captured coordinates"));
1280 let prefix = &source[..call.span.start];
1281 let line = prefix.bytes().filter(|byte| *byte == b'\n').count() + 1;
1282 let column = prefix
1283 .rsplit('\n')
1284 .next()
1285 .map_or(0, |line| line.chars().count());
1286 assert_eq!(
1287 (span.start_line() as usize, span.start_column() as usize),
1288 (line, column),
1289 "{language}"
1290 );
1291 let end = &source[..call.span.end];
1292 let end_line = end.bytes().filter(|byte| *byte == b'\n').count() + 1;
1293 let end_column = end
1294 .rsplit('\n')
1295 .next()
1296 .map_or(0, |line| line.chars().count());
1297 assert_eq!(
1298 (span.end_line() as usize, span.end_column() as usize),
1299 (end_line, end_column),
1300 "{language}"
1301 );
1302 }
1303 let mut graph = result.graph().clone();
1304 let mut facts = result.semantics().clone();
1305 let expected = original[1].source_span;
1306 let mut relations = vec![true; graph.relations.len()];
1307 relations[original[0].relation] = false;
1308 fixture_ok!(facts.retain_graph_facts(
1309 &mut graph,
1310 &vec![true; result.graph().symbols.len()],
1311 &relations
1312 ));
1313 assert_eq!(facts.calls().len(), 1, "{language}");
1314 assert_eq!(facts.calls()[0].source_span, expected, "{language}");
1315 assert!(facts.matches_graph(&graph));
1316 }
1317 }
1318
1319 #[test]
1320 fn multiline_imports_and_refined_callbacks_keep_their_exact_coordinates() {
1321 for (path, language, source) in [
1322 (
1323 "main.rs",
1324 "rust",
1325 "/*Ï€*/ use std::{\r\n mem as aa,\r\n}; use std::mem as ab;",
1326 ),
1327 (
1328 "main.py",
1329 "python",
1330 "from worker import (\r\n target as aa,\r\n)\r\nfrom worker import target as ab\r\n",
1331 ),
1332 (
1333 "main.js",
1334 "javascript",
1335 "/*Ï€*/ import {\r\n target as aa\r\n} from './Ï€worker.js'; import {target as ab} from './Ï€worker.js';",
1336 ),
1337 ] {
1338 let result = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
1339 path,
1340 Some(language),
1341 source,
1342 &projectatlas_core::IndexWorkControl::new(
1343 projectatlas_core::IndexCancellation::new(),
1344 None
1345 )
1346 ));
1347 let imports = result.semantics().imports();
1348 assert_eq!(imports.len(), 2, "{language}");
1349 let first = fixture_ok!(imports[0].source_span.ok_or("missing import coordinates"));
1350 let second = fixture_ok!(imports[1].source_span.ok_or("missing import coordinates"));
1351 assert_ne!(first, second);
1352 assert!(first.end_line() > first.start_line());
1353 }
1354 let result = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
1355 "main.rs",
1356 Some("rust"),
1357 "mod worker {pub fn target(){}} fn caller(){let _=(1 == 1).then(worker::target);let _=(1 == 1).then(worker::target);}",
1358 &projectatlas_core::IndexWorkControl::new(
1359 projectatlas_core::IndexCancellation::new(),
1360 None
1361 )
1362 ));
1363 let callbacks = result
1364 .semantics()
1365 .calls()
1366 .iter()
1367 .filter(|call| result.graph().relations[call.relation].target_name == "worker::target")
1368 .collect::<Vec<_>>();
1369 assert_eq!(callbacks.len(), 2);
1370 assert_ne!(callbacks[0].source_span, callbacks[1].source_span);
1371 for callback in callbacks {
1372 let span = fixture_ok!(callback.source_span.ok_or("missing callback coordinates"));
1373 assert_eq!(
1374 span.end_column() - span.start_column(),
1375 "worker::target".len() as u32
1376 );
1377 }
1378 }
1379
1380 #[test]
1381 fn native_member_declarations_do_not_replace_ordinary_function_values() {
1382 let mut failures = Vec::new();
1383 for (path, language, source, name) in [
1384 (
1385 "source.rs",
1386 "rust",
1387 "fn run(){} struct A; impl A {fn run(){} fn caller(){run();}}",
1388 "run",
1389 ),
1390 (
1391 "trait.rs",
1392 "rust",
1393 "fn run(){} trait A {fn run(){} fn caller(){run();}}",
1394 "run",
1395 ),
1396 (
1397 "source.go",
1398 "go",
1399 "package p; func Run(){}; type A struct{}; func(A) Run(){}; func(A) Caller(){Run()}",
1400 "Run",
1401 ),
1402 (
1403 "source.js",
1404 "javascript",
1405 "function run(){} class A {run(){} caller(){run();}}",
1406 "run",
1407 ),
1408 (
1409 "object.js",
1410 "javascript",
1411 "function run(){} export const obj={run(){},caller(){run();}};",
1412 "run",
1413 ),
1414 (
1415 "source.ts",
1416 "typescript",
1417 "function run(){} class A {run(){} caller(){run();}}",
1418 "run",
1419 ),
1420 (
1421 "source.tsx",
1422 "tsx",
1423 "function run(){} class A {run(){} caller(){run();}}",
1424 "run",
1425 ),
1426 (
1427 "source.m",
1428 "objective-c",
1429 "void run(void){} @interface A -(void)run; -(void)caller; @end @implementation A -(void)run{} -(void)caller{run();} @end",
1430 "run",
1431 ),
1432 (
1433 "source.py",
1434 "python",
1435 "def run(): pass\nclass A:\n def run(self): pass\n def caller(self): run()\n",
1436 "run",
1437 ),
1438 ] {
1439 let result = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
1440 path,
1441 Some(language),
1442 source,
1443 &projectatlas_core::IndexWorkControl::new(
1444 projectatlas_core::IndexCancellation::new(),
1445 None
1446 )
1447 ));
1448 let correct = !result.semantics().calls().is_empty()
1449 && result
1450 .semantics()
1451 .calls()
1452 .iter()
1453 .all(|call| match call.binding {
1454 SourceCallBinding::LocalDeclaration(ordinal) => {
1455 result.graph().symbols.get(ordinal).is_some_and(|symbol| {
1456 symbol.name == name
1457 && symbol.kind == SymbolKind::Function
1458 && symbol.parent.is_none()
1459 })
1460 }
1461 _ => false,
1462 });
1463 if !correct {
1464 failures.push((path, result.semantics().calls().to_vec()));
1465 }
1466 }
1467 assert!(failures.is_empty(), "{failures:?}");
1468 }
1469
1470 #[test]
1471 fn distinct_declarations_keep_implicit_member_lookup_and_real_ambiguity() {
1472 for (path, language, source) in [
1473 (
1474 "source.java",
1475 "java",
1476 "class One {void run(){} void caller(){run();}} class Two {void run(){} void caller(){run();}}",
1477 ),
1478 (
1479 "source.cs",
1480 "csharp",
1481 "class One {void run(){} void caller(){run();}} class Two {void run(){} void caller(){run();}}",
1482 ),
1483 (
1484 "source.cpp",
1485 "cpp",
1486 "class One {public:void run(){} void caller(){run();}}; class Two {public:void run(){} void caller(){run();}};",
1487 ),
1488 (
1489 "source.kt",
1490 "kotlin",
1491 "class One {\n fun run() {}\n fun caller() { run() }\n}\nclass Two {\n fun run() {}\n fun caller() { run() }\n}\n",
1492 ),
1493 ] {
1494 let result = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
1495 path,
1496 Some(language),
1497 source,
1498 &projectatlas_core::IndexWorkControl::new(
1499 projectatlas_core::IndexCancellation::new(),
1500 None
1501 )
1502 ));
1503 assert_eq!(
1504 result.semantics().calls().len(),
1505 2,
1506 "{language}: {:?}",
1507 result.graph()
1508 );
1509 for call in result.semantics().calls() {
1510 let owner = fixture_ok!(
1511 call.owner
1512 .and_then(|ordinal| result.graph().symbols.get(ordinal))
1513 .ok_or("missing exact caller")
1514 );
1515 assert!(
1516 matches!(call.binding, SourceCallBinding::LocalDeclaration(_)),
1517 "{language}: {:?}",
1518 call.binding
1519 );
1520 let SourceCallBinding::LocalDeclaration(ordinal) = call.binding else {
1521 return;
1522 };
1523 let target =
1524 fixture_ok!(result.graph().symbols.get(ordinal).ok_or("missing target"));
1525 assert_eq!(target.name, "run");
1526 assert_eq!(target.parent, owner.parent, "{language}");
1527 }
1528 }
1529 let result = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
1530 "same.rs",
1531 Some("rust"),
1532 "fn run(){} fn run(){} fn caller(){run();}",
1533 &projectatlas_core::IndexWorkControl::new(
1534 projectatlas_core::IndexCancellation::new(),
1535 None
1536 )
1537 ));
1538 assert!(
1539 result
1540 .semantics()
1541 .calls()
1542 .iter()
1543 .all(|call| !matches!(call.binding, SourceCallBinding::LocalDeclaration(_)))
1544 );
1545 }
1546
1547 #[test]
1548 fn rust_scoped_import_candidates_preserve_sharing_and_cannot_narrow_after_filtering() {
1549 let source = "mod m {pub struct Foo{}} fn foo(){} fn caller(){use crate::m::Foo;use crate::foo as Foo; Foo();\nFoo();} fn local(){use crate::m::Foo;fn Foo(){} Foo();}";
1550 let result = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
1551 "src/lib.rs",
1552 Some("rust"),
1553 source,
1554 &projectatlas_core::IndexWorkControl::new(
1555 projectatlas_core::IndexCancellation::new(),
1556 None
1557 )
1558 ));
1559 let graph = result.graph();
1560 let original = result.semantics();
1561 let rust = fixture_ok!(original.rust().ok_or("Rust facts missing"));
1562 assert_eq!(rust.candidate_calls.len(), 3);
1563 assert!(Arc::ptr_eq(
1564 &rust.candidate_calls[0].bindings,
1565 &rust.candidate_calls[1].bindings
1566 ));
1567 assert!(
1568 rust.candidate_calls
1569 .iter()
1570 .any(|call| !call.declarations.is_empty())
1571 );
1572 let mut filtered = graph.clone();
1573 let mut facts = original.clone();
1574 let symbols = graph
1575 .symbols
1576 .iter()
1577 .map(|symbol| symbol.name != "Foo" || symbol.kind != SymbolKind::Function)
1578 .collect::<Vec<_>>();
1579 let discarded = original.imports()[1].relation;
1580 let relations = graph
1581 .relations
1582 .iter()
1583 .enumerate()
1584 .map(|(ordinal, _)| ordinal != discarded)
1585 .collect::<Vec<_>>();
1586 fixture_ok!(facts.retain_graph_facts(&mut filtered, &symbols, &relations));
1587 assert!(facts.matches_graph(&filtered));
1588 assert!(
1589 facts
1590 .rust()
1591 .is_some_and(|rust| rust.candidate_calls.is_empty())
1592 );
1593 assert!(
1594 facts
1595 .calls()
1596 .iter()
1597 .all(|call| matches!(call.binding, SourceCallBinding::Unresolved))
1598 );
1599 assert_eq!(facts.imports().len(), original.imports().len() - 1);
1600 }
1601
1602 #[test]
1603 fn go_receiver_parameters_shadow_file_functions_without_hiding_siblings() {
1604 let result = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
1605 "receiver.go",
1606 Some("go"),
1607 "package p; type T func(); func Run(){}; func(Run T) Caller(){Run()}; func(T) Clean(){Run()}",
1608 &projectatlas_core::IndexWorkControl::new(
1609 projectatlas_core::IndexCancellation::new(),
1610 None
1611 )
1612 ));
1613 assert_eq!(result.semantics().calls().len(), 2);
1614 for call in result.semantics().calls() {
1615 let name = result.graph().relations[call.relation].source_name.as_str();
1616 if name == "Caller" {
1617 assert_eq!(call.binding, SourceCallBinding::Shadowed);
1618 } else {
1619 assert_eq!(name, "Clean");
1620 assert!(matches!(
1621 call.binding,
1622 SourceCallBinding::LocalDeclaration(_)
1623 ));
1624 }
1625 }
1626 }
1627
1628 #[test]
1629 fn omitted_export_dependencies_cannot_retain_forwarding_authority() {
1630 let mut source = "function target(){} function caller(){".to_string();
1631 for _ in 0..crate::MAX_RELATIONS_PER_FILE {
1632 source.push_str("target();");
1633 }
1634 source.push_str("} export {run as launch} from './target.js';");
1635 let result = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
1636 "forward.js",
1637 Some("javascript"),
1638 &source,
1639 &projectatlas_core::IndexWorkControl::new(
1640 projectatlas_core::IndexCancellation::new(),
1641 None
1642 )
1643 ));
1644 assert!(result.semantics().bindings_incomplete);
1645 assert!(result.semantics().exports_incomplete);
1646 assert!(result.semantics().exports().is_empty());
1647 assert!(!result.graph().relations.is_empty());
1648 }
1649
1650 #[test]
1651 fn omitted_native_imports_cannot_leave_exact_call_authority() {
1652 for (path, language, prefix, import) in [
1653 (
1654 "source.py",
1655 "python",
1656 "def target(): pass\ndef caller(): target()\n",
1657 "import module",
1658 ),
1659 (
1660 "source.js",
1661 "javascript",
1662 "function target(){} function caller(){target();}\n",
1663 "import * as module",
1664 ),
1665 ] {
1666 let mut source = prefix.to_string();
1667 for ordinal in 0..=crate::MAX_RELATIONS_PER_FILE {
1668 if language == "python" {
1669 fixture_ok!(writeln!(source, "{import}{ordinal}"));
1670 } else {
1671 fixture_ok!(writeln!(
1672 source,
1673 "{import}{ordinal} from './module{ordinal}';"
1674 ));
1675 }
1676 }
1677 let result = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
1678 path,
1679 Some(language),
1680 &source,
1681 &projectatlas_core::IndexWorkControl::new(
1682 projectatlas_core::IndexCancellation::new(),
1683 None
1684 )
1685 ));
1686 let facts = result.semantics();
1687 assert!(facts.bindings_incomplete, "{language}");
1688 assert!(
1689 !facts.imports().is_empty() && !facts.calls().is_empty(),
1690 "{language}"
1691 );
1692 assert!(
1693 facts
1694 .calls()
1695 .iter()
1696 .all(|call| matches!(call.binding, SourceCallBinding::Unresolved)),
1697 "{language}"
1698 );
1699 let mut graph = result.graph().clone();
1700 let mut remapped = facts.clone();
1701 fixture_ok!(remapped.retain_graph_facts(
1702 &mut graph,
1703 &vec![true; result.graph().symbols.len()],
1704 &vec![true; result.graph().relations.len()]
1705 ));
1706 assert!(remapped.bindings_incomplete && remapped.matches_graph(&graph));
1707 }
1708 }
1709
1710 #[test]
1711 fn rust_remapping_retains_blockers_and_drops_lost_import_authority() {
1712 let source = "pub fn run(){} pub struct Service; impl Service {pub fn work(){}} pub use crate::run as launch; use std::mem::drop as dispose; fn caller(){launch();dispose(1);}";
1713 let result = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
1714 "src/lib.rs",
1715 Some("rust"),
1716 source,
1717 &projectatlas_core::IndexWorkControl::new(
1718 projectatlas_core::IndexCancellation::new(),
1719 None
1720 )
1721 ));
1722 let (graph, original) = (result.graph(), result.semantics());
1723 let mut filtered = graph.clone();
1724 let mut facts = original.clone();
1725 let symbols = graph
1726 .symbols
1727 .iter()
1728 .map(|symbol| symbol.name != "run" && symbol.name != "Service" && symbol.name != "work")
1729 .collect::<Vec<_>>();
1730 let relations = graph
1731 .relations
1732 .iter()
1733 .map(|relation| relation.kind != RelationKind::Imports)
1734 .collect::<Vec<_>>();
1735 fixture_ok!(facts.retain_graph_facts(&mut filtered, &symbols, &relations));
1736 assert!(facts.matches_graph(&filtered));
1737 let rust = fixture_ok!(facts.rust().ok_or("Rust facts missing"));
1738 assert!(
1739 rust.declarations
1740 .iter()
1741 .any(|declaration| declaration.name == "run"
1742 && declaration.binding == crate::SourceRustBinding::Unresolved)
1743 );
1744 assert!(
1745 rust.declarations
1746 .iter()
1747 .any(|declaration| declaration.name == "Service"
1748 && declaration.binding == crate::SourceRustBinding::UnresolvedType)
1749 );
1750 assert!(
1751 rust.declarations
1752 .iter()
1753 .any(|declaration| declaration.name == "launch"
1754 && declaration.binding == crate::SourceRustBinding::UnresolvedImport)
1755 );
1756 assert!(
1757 rust.associated
1758 .iter()
1759 .all(|method| method.declaration.is_none())
1760 );
1761 assert!(
1762 facts
1763 .calls()
1764 .iter()
1765 .all(|call| !matches!(call.binding, SourceCallBinding::Imported { .. }))
1766 );
1767 assert!(
1768 original
1769 .imports()
1770 .iter()
1771 .any(|import| import.rust_toolchain.is_some())
1772 );
1773 assert!(facts.imports().is_empty());
1774 }
1775
1776 #[test]
1777 fn embedded_hosts_keep_binding_ownership_and_original_byte_occurrences() {
1778 let content = "<template>🙂</template>\n<script lang=\"ts\">\nimport launch, { run as named } from './worker';\nfunction caller(launch: () => void) { launch(); }\nfunction clean() { launch(); named(); named(); }\n</script>\n<style>fake { content: 'launch()'; }</style>";
1779 for (path, language) in [
1780 ("view.vue", "vue"),
1781 ("view.svelte", "svelte"),
1782 ("view.html", "html"),
1783 ] {
1784 let content = if language == "html" {
1785 content.replace("<script lang", "<script type=module lang")
1786 } else {
1787 content.to_string()
1788 };
1789 let extraction = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
1790 path,
1791 Some(language),
1792 &content,
1793 &projectatlas_core::IndexWorkControl::new(
1794 projectatlas_core::IndexCancellation::new(),
1795 None
1796 )
1797 ));
1798 let (_, graph, facts) = extraction.into_parts();
1799 assert!(facts.matches_graph(&graph), "{language}");
1800 assert_eq!(
1801 facts.content_hash(),
1802 blake3::hash(content.as_bytes()).to_hex().as_str()
1803 );
1804 assert_eq!(facts.imports().len(), 1, "{language}");
1805 assert_eq!(facts.imports()[0].references.len(), 2, "{language}");
1806 let calls = facts.calls();
1807 assert_eq!(calls.len(), 4, "{language}: {graph:?}");
1808 assert_eq!(calls[0].binding, SourceCallBinding::Shadowed, "{language}");
1809 for call in &calls[1..] {
1810 assert!(
1811 matches!(call.binding, SourceCallBinding::Imported { .. }),
1812 "{language}: {call:?}"
1813 );
1814 assert_eq!(
1815 call.owner.map(|owner| graph.symbols[owner].name.as_str()),
1816 Some("clean")
1817 );
1818 assert!(matches!(
1819 &content[call.span.start..call.span.end],
1820 "launch()" | "named()"
1821 ));
1822 }
1823 assert_ne!(
1824 calls[2].relation, calls[3].relation,
1825 "distinct same-line occurrences"
1826 );
1827 assert_eq!(
1828 graph.parser,
1829 if language == "svelte" {
1830 ParserKind::Fallback
1831 } else {
1832 ParserKind::Structural
1833 },
1834 "{language} host provenance"
1835 );
1836 }
1837 }
1838
1839 #[test]
1840 fn embedded_script_families_keep_independent_import_ordinals() {
1841 let content = "<script type=module>import { run as first } from './first'; function js_caller() { first(); }</script>\n<script type=module lang='ts'>import { run as second } from './second'; function ts_caller() { second(); }</script>";
1842 let extraction = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
1843 "view.html",
1844 Some("html"),
1845 content,
1846 &projectatlas_core::IndexWorkControl::new(
1847 projectatlas_core::IndexCancellation::new(),
1848 None
1849 )
1850 ));
1851 let (_, graph, facts) = extraction.into_parts();
1852 assert!(facts.matches_graph(&graph));
1853 assert_eq!(facts.imports().len(), 2);
1854 assert_eq!(facts.calls().len(), 2);
1855 for fact in facts.calls() {
1856 assert!(
1857 matches!(fact.binding, SourceCallBinding::Imported { .. }),
1858 "missing independently mapped import: {fact:?}"
1859 );
1860 let SourceCallBinding::Imported {
1861 import, reference, ..
1862 } = fact.binding
1863 else {
1864 return;
1865 };
1866 let module = facts.imports()[import].references[reference].module();
1867 assert_eq!(
1868 module,
1869 if graph.relations[fact.relation].target_name == "first" {
1870 "./first"
1871 } else {
1872 "./second"
1873 }
1874 );
1875 }
1876 }
1877
1878 #[test]
1879 fn coalesced_embedded_declarations_cannot_narrow_ambiguous_binding_proof() {
1880 let script = "function run() {} function run() {} function clean() { run(); }";
1881 let control = projectatlas_core::IndexWorkControl::new(
1882 projectatlas_core::IndexCancellation::new(),
1883 None,
1884 );
1885 let native = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
1886 "script.js",
1887 Some("javascript"),
1888 script,
1889 &control
1890 ));
1891 assert_eq!(
1892 native
1893 .graph()
1894 .symbols
1895 .iter()
1896 .filter(|symbol| symbol.name == "run")
1897 .count(),
1898 2
1899 );
1900 let mut merged = native.graph().clone();
1901 merged.symbols.clear();
1902 merged.relations.clear();
1903 let mut mapping = crate::GraphMergeOrdinals {
1904 symbols: vec![crate::SourceSymbolRemap::Omitted; native.graph().symbols.len()],
1905 relations: vec![None; native.graph().relations.len()],
1906 };
1907 fixture_ok!(crate::merge_missing_graph_entries_checked(
1908 &mut merged,
1909 native.graph().clone(),
1910 &std::collections::BTreeMap::new(),
1911 projectatlas_core::language::EmbeddedHostKind::HtmlLike,
1912 &mut || control.check(projectatlas_core::IndexWorkStage::SymbolParsing),
1913 Some(&mut mapping)
1914 ));
1915 for (ordinal, symbol) in native.graph().symbols.iter().enumerate() {
1916 assert_eq!(
1917 mapping.symbols[ordinal].retained().is_none(),
1918 symbol.name == "run",
1919 "all coalesced owners must lose correspondence"
1920 );
1921 }
1922 let content = format!("<script>{script}</script>");
1923 let host = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
1924 "view.html",
1925 Some("html"),
1926 &content,
1927 &control
1928 ));
1929 assert_eq!(
1930 host.graph()
1931 .symbols
1932 .iter()
1933 .filter(|symbol| symbol.name == "run")
1934 .count(),
1935 1
1936 );
1937 assert_eq!(host.semantics().calls().len(), 1);
1938 assert_eq!(
1939 host.semantics().calls()[0].binding,
1940 SourceCallBinding::Unresolved
1941 );
1942 assert!(host.semantics().matches_graph(host.graph()));
1943 }
1944
1945 fn captured_graph() -> Result<(SymbolGraph, SourceSemanticFacts), Box<dyn std::error::Error>> {
1946 let content = "use dep::run as launch; fn run() {} fn caller() { launch(); }";
1947 let graph = extract_symbol_graph("src/main.rs", Some("rust"), content);
1948 let import = graph
1949 .relations
1950 .iter()
1951 .position(|fact| fact.kind == RelationKind::Imports);
1952 let call = graph
1953 .relations
1954 .iter()
1955 .position(|fact| fact.kind == RelationKind::Calls);
1956 let import = import.ok_or_else(|| std::io::Error::other("fixture import missing"))?;
1957 let call = call.ok_or_else(|| std::io::Error::other("fixture call missing"))?;
1958 let call_start = content
1959 .rfind("launch()")
1960 .ok_or_else(|| std::io::Error::other("fixture call span missing"))?;
1961 let mut facts = SourceSemanticFacts {
1962 imports: vec![SourceImportFact {
1963 source_span: None,
1964 relation: import,
1965 span: SourceByteSpan { start: 0, end: 23 },
1966 module: Some("dep".into()),
1967 references: vec![ImportReference::new(
1968 ImportSyntax::Rust,
1969 "dep",
1970 Some("run"),
1971 "launch",
1972 )],
1973 rust: None,
1974 rust_toolchain: None,
1975 }],
1976 calls: vec![SourceCallFact {
1977 source_span: None,
1978 relation: call,
1979 span: SourceByteSpan {
1980 start: call_start,
1981 end: call_start + "launch()".len(),
1982 },
1983 binding: SourceCallBinding::Imported {
1984 import: 0,
1985 reference: 0,
1986 member: None,
1987 rust_toolchain: None,
1988 },
1989 owner: None,
1990 }],
1991 ..SourceSemanticFacts::default()
1992 };
1993 facts.bind_source(&graph, content);
1994 Ok((graph, facts))
1995 }
1996
1997 #[test]
1998 fn same_size_graph_reordering_cannot_reuse_binding_ordinals() {
1999 let (mut graph, facts) = fixture_ok!(captured_graph());
2000 assert!(facts.matches_graph(&graph));
2001 graph.relations.swap(0, 1);
2002 assert!(!facts.matches_graph(&graph));
2003 let symbols = (0..graph.symbols.len()).map(Some).collect::<Vec<_>>();
2004 let relations = (0..graph.relations.len()).map(Some).collect::<Vec<_>>();
2005 assert_eq!(
2006 facts.remap(&graph, &graph, &symbols, &relations),
2007 Err(SourceSemanticRemapError::GraphMismatch)
2008 );
2009 }
2010
2011 #[test]
2012 fn retaining_graph_facts_rebases_targets_and_call_owners_without_source_drift() {
2013 let content = "fn padding() {} fn run() {} fn caller() { run(); }";
2014 let control = projectatlas_core::IndexWorkControl::new(
2015 projectatlas_core::IndexCancellation::new(),
2016 None,
2017 );
2018 let extracted = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
2019 "main.rs",
2020 Some("rust"),
2021 content,
2022 &control
2023 ));
2024 let (_, mut graph, mut facts) = extracted.into_parts();
2025 let hash = facts.content_hash.clone();
2026 assert_eq!(graph.symbols.len(), 3);
2027 assert_eq!(facts.calls[0].owner, Some(2));
2028 let relations = vec![true; graph.relations.len()];
2029 fixture_ok!(facts.retain_graph_facts(&mut graph, &[false, true, true], &relations));
2030 assert!(facts.matches_graph(&graph));
2031 assert_eq!(facts.content_hash, hash);
2032 assert_eq!(facts.calls[0].owner, Some(1));
2033 assert_eq!(
2034 facts.calls[0].binding,
2035 SourceCallBinding::LocalDeclaration(0)
2036 );
2037 let original_graph = graph.clone();
2038 let original_facts = facts.clone();
2039 assert_eq!(
2040 facts.retain_graph_facts(&mut graph, &[true], &relations),
2041 Err(SourceSemanticRemapError::GraphMismatch)
2042 );
2043 assert_eq!(graph, original_graph);
2044 assert_eq!(facts, original_facts);
2045 }
2046
2047 #[test]
2048 fn shared_overload_sets_stay_shared_through_ordinal_remapping() {
2049 let content =
2050 "void run(int value) {}\nvoid run(double value) {}\nvoid caller() { run(0); run(1); }";
2051 let control = projectatlas_core::IndexWorkControl::new(
2052 projectatlas_core::IndexCancellation::new(),
2053 None,
2054 );
2055 let extracted = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
2056 "main.cpp",
2057 Some("cpp"),
2058 content,
2059 &control
2060 ));
2061 let (_, mut graph, mut facts) = extracted.into_parts();
2062 let symbols = vec![true; graph.symbols.len()];
2063 let relations = vec![true; graph.relations.len()];
2064 fixture_ok!(facts.retain_graph_facts(&mut graph, &symbols, &relations));
2065 assert!(facts.matches_graph(&graph));
2066 let (
2067 SourceCallBinding::LocalDeclarations(first),
2068 SourceCallBinding::LocalDeclarations(second),
2069 ) = (&facts.calls[0].binding, &facts.calls[1].binding)
2070 else {
2071 assert!(
2072 facts
2073 .calls
2074 .iter()
2075 .all(|fact| matches!(fact.binding, SourceCallBinding::LocalDeclarations(_))),
2076 "{:?}",
2077 facts.calls
2078 );
2079 return;
2080 };
2081 assert!(Arc::ptr_eq(first, second));
2082 }
2083
2084 #[test]
2085 fn filtering_an_import_does_not_rebind_its_surviving_call() {
2086 let (graph, facts) = fixture_ok!(captured_graph());
2087 let removed = facts.imports[0].relation;
2088 let mut admitted = graph.clone();
2089 admitted.relations.remove(removed);
2090 let symbols = (0..graph.symbols.len()).map(Some).collect::<Vec<_>>();
2091 let relations = (0..graph.relations.len())
2092 .map(|index| {
2093 if index == removed {
2094 None
2095 } else {
2096 Some(index - usize::from(index > removed))
2097 }
2098 })
2099 .collect::<Vec<_>>();
2100 let remapped = fixture_ok!(facts.remap(&graph, &admitted, &symbols, &relations));
2101 assert!(remapped.matches_graph(&admitted));
2102 assert!(remapped.imports.is_empty());
2103 assert_eq!(remapped.calls[0].binding, SourceCallBinding::Unresolved);
2104 assert_eq!(remapped.content_hash, facts.content_hash);
2105 }
2106
2107 #[test]
2108 fn filtering_ambiguous_declarations_does_not_create_an_exact_target() {
2109 let binding = SourceCallBinding::LocalDeclarations(vec![0, 1].into());
2110 assert_eq!(
2111 remap_call_binding(&binding, &[Some(0), None], &[], &mut HashMap::new()),
2112 SourceCallBinding::Unresolved
2113 );
2114 assert_eq!(
2115 remap_call_binding(&binding, &[Some(1), Some(0)], &[], &mut HashMap::new()),
2116 SourceCallBinding::LocalDeclarations(vec![1, 0].into())
2117 );
2118 }
2119
2120 #[test]
2121 fn duplicate_or_outside_output_ordinals_fail_without_changing_evidence() {
2122 let (graph, facts) = fixture_ok!(captured_graph());
2123 let symbols = (0..graph.symbols.len()).map(Some).collect::<Vec<_>>();
2124 let mut relations = (0..graph.relations.len()).map(Some).collect::<Vec<_>>();
2125 relations[1] = relations[0];
2126 assert_eq!(
2127 facts.remap(&graph, &graph, &symbols, &relations),
2128 Err(SourceSemanticRemapError::InvalidOrdinalMap)
2129 );
2130 relations[1] = Some(graph.relations.len());
2131 assert_eq!(
2132 facts.remap(&graph, &graph, &symbols, &relations),
2133 Err(SourceSemanticRemapError::InvalidOrdinalMap)
2134 );
2135 assert!(facts.matches_graph(&graph));
2136 }
2137
2138 #[test]
2139 fn malformed_ecmascript_imports_have_no_admitted_binding_evidence() {
2140 let control = projectatlas_core::IndexWorkControl::new(
2141 projectatlas_core::IndexCancellation::new(),
2142 None,
2143 );
2144 for language in ["javascript", "typescript", "tsx"] {
2145 for declaration in [
2146 "import foo.bar, { run } from './dep';",
2147 "import class, { run } from './dep';",
2148 "import { run as class } from './dep';",
2149 "import * as class from './dep';",
2150 ] {
2151 let extraction =
2152 fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
2153 "src/main.tsx",
2154 Some(language),
2155 declaration,
2156 &control,
2157 ));
2158 assert!(extraction.semantics().matches_graph(extraction.graph()));
2159 assert!(
2160 extraction
2161 .semantics()
2162 .imports()
2163 .iter()
2164 .flat_map(|fact| &fact.references)
2165 .all(|reference| !matches!(reference.local(), "class" | "foo.bar" | "foo")),
2166 "{language}: {declaration}"
2167 );
2168 }
2169 }
2170 }
2171
2172 #[test]
2173 fn valid_mixed_import_evidence_retains_independent_references_and_source() {
2174 let control = projectatlas_core::IndexWorkControl::new(
2175 projectatlas_core::IndexCancellation::new(),
2176 None,
2177 );
2178 let content =
2179 "import primary, { run as launch } from './dep'; function caller() { launch(); }";
2180 let extraction = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
2181 "src/main.js",
2182 Some("javascript"),
2183 content,
2184 &control,
2185 ));
2186 let facts = extraction.semantics();
2187 assert!(facts.matches_graph(extraction.graph()));
2188 assert_eq!(facts.imports().len(), 1);
2189 assert_eq!(facts.imports()[0].references.len(), 2);
2190 assert_eq!(facts.imports()[0].references[0].local(), "primary");
2191 assert_eq!(facts.imports()[0].references[1].local(), "launch");
2192 assert_eq!(facts.calls().len(), 1);
2193 let span = facts.calls()[0].span;
2194 assert_eq!(&content[span.start..span.end], "launch()");
2195 assert_eq!(
2196 facts.content_hash(),
2197 blake3::hash(content.as_bytes()).to_hex().as_str()
2198 );
2199 }
2200
2201 #[test]
2202 fn invalid_binding_leaves_preserve_static_modules_and_valid_siblings() {
2203 let control = projectatlas_core::IndexWorkControl::new(
2204 projectatlas_core::IndexCancellation::new(),
2205 None,
2206 );
2207 for language in ["javascript", "typescript", "tsx"] {
2208 for statement in [
2209 "import class, { run as keep } from './dep';",
2210 "import foo.bar, { run as keep } from './dep';",
2211 "import primary, { run as class, stop as keep } from './dep';",
2212 r"import cl\u0061ss, { run as keep } from './dep';",
2213 r"import { run as ev\u0061l, stop as keep } from './dep';",
2214 "import { run as 🙂, stop as keep } from './dep';",
2215 ] {
2216 let extraction =
2217 fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
2218 "src/main.tsx",
2219 Some(language),
2220 statement,
2221 &control,
2222 ));
2223 let imports = extraction.semantics().imports();
2224 assert!(
2225 imports
2226 .iter()
2227 .any(|fact| fact.module.as_deref() == Some("./dep")),
2228 "lost module: {language} {statement}"
2229 );
2230 let locals = imports
2231 .iter()
2232 .flat_map(|fact| &fact.references)
2233 .map(ImportReference::local)
2234 .collect::<Vec<_>>();
2235 let diagnostic = if locals.contains(&"keep") {
2236 String::new()
2237 } else {
2238 let language = fixture_ok!(
2239 crate::tree_sitter_language(language)
2240 .ok_or_else(|| std::io::Error::other("fixture language missing"))
2241 );
2242 let mut parser = tree_sitter::Parser::new();
2243 fixture_ok!(parser.set_language(&language));
2244 fixture_ok!(
2245 parser
2246 .parse(statement, None)
2247 .ok_or_else(|| std::io::Error::other("fixture parse missing"))
2248 )
2249 .root_node()
2250 .to_sexp()
2251 };
2252 assert!(
2253 locals.contains(&"keep"),
2254 "lost valid sibling: {language} {statement}: {locals:?} {diagnostic}"
2255 );
2256 assert!(
2257 locals
2258 .iter()
2259 .all(|local| matches!(*local, "primary" | "keep")),
2260 "invalid leaf admitted: {language} {statement}: {locals:?}"
2261 );
2262 }
2263 let statement = r"import { class as C, default as D, run as l\u0061unch, stop as undefined } from './dep';";
2264 let extraction = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
2265 "src/main.tsx",
2266 Some(language),
2267 statement,
2268 &control,
2269 ));
2270 let locals = extraction
2271 .semantics()
2272 .imports()
2273 .iter()
2274 .flat_map(|fact| &fact.references)
2275 .map(ImportReference::local)
2276 .collect::<Vec<_>>();
2277 assert_eq!(locals, ["C", "D", "launch", "undefined"], "{language}");
2278 }
2279 }
2280
2281 #[test]
2282 fn recovery_uses_the_ast_separator_and_preserves_quoted_export_data() {
2283 let control = projectatlas_core::IndexWorkControl::new(
2284 projectatlas_core::IndexCancellation::new(),
2285 None,
2286 );
2287 for language in ["javascript", "typescript", "tsx"] {
2288 for (statement, module, imported) in [
2289 (
2290 "import { from as keep, run as class } from './dep';",
2291 "./dep",
2292 "from",
2293 ),
2294 (
2295 "import { run as class, stop as keep } from './dep from space.js';",
2296 "./dep from space.js",
2297 "stop",
2298 ),
2299 (
2300 r#"import { "name from space" as keep, run as class } from './dep from space.js';"#,
2301 "./dep from space.js",
2302 "name from space",
2303 ),
2304 (
2305 r#"import { "a, ghost as forged, b" as keep, run as class } from './dep';"#,
2306 "./dep",
2307 "a, ghost as forged, b",
2308 ),
2309 (
2310 r#"import { "x as y" as keep, run as class } from './dep';"#,
2311 "./dep",
2312 "x as y",
2313 ),
2314 (
2315 "import /*lead*/ primary, {run /*bad*/ as class, from /*good*/ as keep} /*tail*/ from './dep';",
2316 "./dep",
2317 "from",
2318 ),
2319 ] {
2320 let extraction =
2321 fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
2322 "src/main.tsx",
2323 Some(language),
2324 statement,
2325 &control,
2326 ));
2327 let imports = extraction.semantics().imports();
2328 assert!(
2329 imports
2330 .iter()
2331 .any(|fact| fact.module.as_deref() == Some(module))
2332 );
2333 let references = imports
2334 .iter()
2335 .flat_map(|fact| &fact.references)
2336 .collect::<Vec<_>>();
2337 assert!(
2338 references
2339 .iter()
2340 .any(|reference| reference.local() == "keep"
2341 && reference.imported() == Some(imported)),
2342 "{language}: {statement}: {references:?}"
2343 );
2344 assert!(
2345 references
2346 .iter()
2347 .all(|reference| matches!(reference.local(), "keep" | "primary")),
2348 "fabricated binding: {language}: {statement}: {references:?}"
2349 );
2350 }
2351 let extraction = fixture_ok!(crate::extract_symbol_graph_with_semantics_controlled(
2352 "src/main.tsx",
2353 Some(language),
2354 "import { run as keep,, stop as other } from './dep';",
2355 &control,
2356 ));
2357 assert!(
2358 extraction
2359 .semantics()
2360 .imports()
2361 .iter()
2362 .all(|fact| fact.references.is_empty())
2363 );
2364 }
2365 }
2366}