1use crate::SourceByteSpan;
4use std::collections::HashSet;
5use std::sync::Arc;
6use tree_sitter::Node;
7
8#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
10pub enum SourceRustAnchor {
11 Crate,
13 SelfModule,
15 Super(usize),
17 Absolute,
19 Unqualified,
21}
22
23#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
25pub struct SourceRustPath {
26 pub anchor: SourceRustAnchor,
28 pub segments: Vec<String>,
30}
31
32impl SourceRustPath {
33 pub(crate) fn from_ast_path(path: &str) -> Option<Self> {
35 let absolute = path.starts_with("::");
36 let mut components = path
37 .strip_prefix("::")
38 .unwrap_or(path)
39 .split("::")
40 .peekable();
41 let anchor = if absolute {
42 SourceRustAnchor::Absolute
43 } else {
44 match components.peek().copied() {
45 Some("crate") => {
46 components.next();
47 SourceRustAnchor::Crate
48 }
49 Some("self") => {
50 components.next();
51 SourceRustAnchor::SelfModule
52 }
53 Some("super") => {
54 let mut parents = 0;
55 while components.peek() == Some(&"super") {
56 components.next();
57 parents += 1;
58 }
59 SourceRustAnchor::Super(parents)
60 }
61 _ => SourceRustAnchor::Unqualified,
62 }
63 };
64 let segments = components.map(ToString::to_string).collect::<Vec<_>>();
65 if segments.iter().any(String::is_empty) {
66 return None;
67 }
68 Some(Self { anchor, segments })
69 }
70
71 pub(crate) fn retained_bytes(&self) -> u64 {
73 self.segments.iter().fold(
74 (self.segments.capacity() * std::mem::size_of::<String>()) as u64,
75 |bytes, part| bytes.saturating_add(part.capacity() as u64),
76 )
77 }
78}
79
80#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
82pub enum SourceRustUseRole {
83 Item,
85 SelfNamespace,
87 Wildcard,
89}
90
91#[derive(Clone, Debug, Eq, PartialEq)]
93pub struct SourceRustUse {
94 pub path: SourceRustPath,
96 pub role: SourceRustUseRole,
98 pub local: Option<String>,
100 pub module: Arc<[String]>,
102 pub reexport: bool,
104 pub supported: bool,
106}
107
108impl SourceRustUse {
109 pub(crate) fn retained_bytes(&self) -> u64 {
111 self.path
112 .retained_bytes()
113 .saturating_add(self.local.as_ref().map_or(0, String::capacity) as u64)
114 }
115}
116
117#[derive(Clone, Debug, Eq, PartialEq)]
119pub struct SourceRustModule {
120 pub parent: Arc<[String]>,
122 pub name: String,
124 pub span: SourceByteSpan,
126 pub body: Option<SourceByteSpan>,
128 pub supported: bool,
130 pub visibility: SourceRustVisibility,
132}
133
134#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
136pub enum SourceRustVisibility {
137 Private,
139 Crate,
141 Public,
143 Unsupported,
145}
146
147#[derive(Clone, Debug, Eq, PartialEq)]
149pub struct SourceRustCall {
150 pub path: SourceRustPath,
152 pub module: Arc<[String]>,
154 pub origin: SourceRustCallOrigin,
156}
157
158#[derive(Clone, Copy, Debug, Eq, PartialEq)]
160pub enum SourceRustCallOrigin {
161 DeclaredModule,
163 AnchoredPath,
165 ModuleLookup,
167}
168
169impl SourceRustCall {
170 pub(crate) fn reference(&self) -> Option<crate::ImportReference> {
172 if self.origin == SourceRustCallOrigin::ModuleLookup {
173 return None;
174 }
175 let (member, segments) = self.path.segments.split_last()?;
176 let (anchor, inline) = match self.path.anchor {
177 SourceRustAnchor::Crate => ("crate".to_string(), &[][..]),
178 SourceRustAnchor::SelfModule => ("self".to_string(), self.module.as_ref()),
179 SourceRustAnchor::Super(parents) if parents <= self.module.len() => (
180 "self".to_string(),
181 &self.module[..self.module.len() - parents],
182 ),
183 SourceRustAnchor::Super(parents) => (
184 (0..parents - self.module.len())
185 .map(|_| "super")
186 .collect::<Vec<_>>()
187 .join("::"),
188 &[][..],
189 ),
190 SourceRustAnchor::Absolute | SourceRustAnchor::Unqualified => return None,
191 };
192 let mut module = anchor.clone();
193 for segment in inline.iter().chain(segments) {
194 module.push_str("::");
195 module.push_str(segment);
196 }
197 Some(crate::ImportReference::new(
198 crate::ImportSyntax::Rust,
199 &module,
200 Some(member),
201 anchor.split("::").next()?,
202 ))
203 }
204}
205
206#[derive(Clone, Debug, Eq, PartialEq)]
208pub enum SourceRustBinding {
209 Declaration(usize),
211 TypeDeclaration(usize),
213 UnresolvedType,
215 UnresolvedImport,
217 Use(Arc<SourceRustUse>),
219 Unresolved,
221}
222
223#[derive(Clone, Debug, Eq, PartialEq)]
225pub struct SourceRustDeclaration {
226 pub module: Arc<[String]>,
228 pub name: String,
230 pub visibility: SourceRustVisibility,
232 pub binding: SourceRustBinding,
234}
235
236#[derive(Clone, Debug, Eq, PartialEq)]
238pub struct SourceRustAssociated {
239 pub module: Arc<[String]>,
241 pub owner: String,
243 pub name: String,
245 pub declaration: Option<usize>,
247 pub visibility: SourceRustVisibility,
249 pub supported: bool,
251}
252
253#[derive(Clone, Debug, Eq, PartialEq)]
255pub struct SourceRustCallImports {
256 pub relation: usize,
258 pub bindings: Arc<[(usize, usize)]>,
260 pub declarations: Arc<[usize]>,
262 pub member: Option<String>,
264}
265
266#[derive(Clone, Debug, Default, Eq, PartialEq)]
268pub struct SourceRustFacts {
269 pub modules: Vec<SourceRustModule>,
271 pub declarations: Vec<SourceRustDeclaration>,
273 pub associated: Vec<SourceRustAssociated>,
275 pub candidate_calls: Vec<SourceRustCallImports>,
277 pub complete: bool,
279}
280
281#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
283pub enum SourceCargoTargetKind {
284 Library,
286 Binary,
288 Example,
290 Test,
292 Benchmark,
294 BuildScript,
296}
297
298impl SourceCargoTargetKind {
299 #[must_use]
301 pub const fn automatic_directory(self) -> Option<&'static str> {
302 match self {
303 Self::Binary => Some("src/bin"),
304 Self::Example => Some("examples"),
305 Self::Test => Some("tests"),
306 Self::Benchmark => Some("benches"),
307 Self::Library | Self::BuildScript => None,
308 }
309 }
310
311 #[must_use]
313 pub const fn conventional_root(self) -> Option<&'static str> {
314 match self {
315 Self::Library => Some("src/lib.rs"),
316 Self::Binary => Some("src/main.rs"),
317 Self::BuildScript => Some("build.rs"),
318 Self::Example | Self::Test | Self::Benchmark => None,
319 }
320 }
321}
322
323pub fn cargo_target_inventory_key(
331 project: projectatlas_core::graph::ProjectInstanceId,
332 package: &str,
333 manifest: &str,
334 directory: &str,
335) -> Result<
336 projectatlas_core::graph::CanonicalResolutionKey,
337 projectatlas_core::graph::GraphContractError,
338> {
339 cargo_directory_key(
340 project,
341 package,
342 manifest,
343 directory,
344 projectatlas_core::graph::GraphRelationKind::from_legacy(
345 projectatlas_core::symbols::RelationKind::Imports,
346 ),
347 )
348}
349
350pub fn cargo_root_cohort_key(
359 project: projectatlas_core::graph::ProjectInstanceId,
360 package: &str,
361 manifest: &str,
362 directory: &str,
363) -> Result<
364 projectatlas_core::graph::CanonicalResolutionKey,
365 projectatlas_core::graph::GraphContractError,
366> {
367 cargo_directory_key(
368 project,
369 package,
370 manifest,
371 directory,
372 projectatlas_core::graph::GraphRelationKind::Extended(
373 projectatlas_core::graph::ExtendedRelationKind::Configures,
374 ),
375 )
376}
377
378fn cargo_directory_key(
380 project: projectatlas_core::graph::ProjectInstanceId,
381 package: &str,
382 manifest: &str,
383 directory: &str,
384 kind: projectatlas_core::graph::GraphRelationKind,
385) -> Result<
386 projectatlas_core::graph::CanonicalResolutionKey,
387 projectatlas_core::graph::GraphContractError,
388> {
389 use projectatlas_core::graph::{
390 CanonicalResolutionKey, GraphContractError, GraphIdentityText, ResolutionKeyDomain,
391 };
392 use projectatlas_core::language::SemanticProviderOwner;
393 let owner = SemanticProviderOwner::Rust;
394 let provider = GraphIdentityText::new(owner.as_str())?;
395 let language = GraphIdentityText::new(owner.resolution_family().ok_or(
396 GraphContractError::InvalidResolution {
397 reason: "Rust has no registered resolution family",
398 },
399 )?)?;
400 let package = GraphIdentityText::new(package)?;
401 let manifest = GraphIdentityText::new(manifest)?;
402 let directory = GraphIdentityText::new(directory)?;
403 Ok(CanonicalResolutionKey::new(
404 project,
405 ResolutionKeyDomain::Module,
406 &provider,
407 &language,
408 Some(&package),
409 Some(&manifest),
410 Some(kind),
411 &directory,
412 ))
413}
414
415#[must_use]
424pub fn cargo_automatic_target_directory<'a>(path: &'a str, root: &str) -> Option<&'a str> {
425 let relative = if root.is_empty() {
426 path
427 } else {
428 path.strip_prefix(root)?.strip_prefix('/')?
429 };
430 for kind in [
431 SourceCargoTargetKind::Binary,
432 SourceCargoTargetKind::Example,
433 SourceCargoTargetKind::Test,
434 SourceCargoTargetKind::Benchmark,
435 ] {
436 let Some(directory) = kind.automatic_directory() else {
437 continue;
438 };
439 if let Some(file) = relative
440 .strip_prefix(directory)
441 .and_then(|suffix| suffix.strip_prefix('/'))
442 && !file.contains('/')
443 && file
444 .strip_suffix(".rs")
445 .is_some_and(|stem| !stem.is_empty())
446 {
447 return path.rsplit_once('/').map(|(parent, _)| parent);
448 }
449 }
450 None
451}
452
453#[derive(Clone, Debug, Eq, PartialEq)]
455pub struct SourceCargoTarget {
456 pub kind: SourceCargoTargetKind,
458 pub path: String,
460 pub name: Option<String>,
462}
463
464#[derive(Clone, Debug, Eq, PartialEq)]
466pub struct SourceCargoAutomaticTarget {
467 pub kind: SourceCargoTargetKind,
469 pub certain: bool,
471}
472
473#[derive(Clone, Debug, Default, Eq, PartialEq)]
475pub struct SourceCargoTargets {
476 pub edition: Option<u16>,
478 pub targets: Vec<SourceCargoTarget>,
480 pub automatic: Vec<SourceCargoAutomaticTarget>,
482 pub complete: bool,
484}
485
486const MAX_SOURCE_MODULE_DEPTH: usize = 64;
488
489pub(crate) fn module_owner(node: Node<'_>, content: &str) -> Option<Arc<[String]>> {
491 let mut parts = Vec::new();
492 let mut parent = node.parent();
493 let mut visited = 0;
494 while let Some(current) = parent {
495 visited += 1;
496 if visited > MAX_SOURCE_MODULE_DEPTH * 4 {
497 return None;
498 }
499 if current.kind() == "mod_item" {
500 if !is_module_item(current) {
501 return None;
502 }
503 if parts.len() >= MAX_SOURCE_MODULE_DEPTH {
504 return None;
505 }
506 let name = current.child_by_field_name("name")?;
507 parts.push(name.utf8_text(content.as_bytes()).ok()?.to_string());
508 }
509 parent = current.parent();
510 }
511 parts.reverse();
512 Some(parts.into())
513}
514
515pub(crate) fn module_retained_bytes(
517 module: &Arc<[String]>,
518 seen: &mut HashSet<*const [String]>,
519) -> u64 {
520 if !seen.insert(Arc::as_ptr(module)) {
521 return 0;
522 }
523 module.iter().fold(
524 (2 * std::mem::size_of::<usize>() + std::mem::size_of_val(module.as_ref())) as u64,
525 |bytes, part| bytes.saturating_add(part.capacity() as u64),
526 )
527}
528
529pub(crate) fn use_retained_bytes(
531 binding: &Arc<SourceRustUse>,
532 uses: &mut HashSet<*const SourceRustUse>,
533 modules: &mut HashSet<*const [String]>,
534) -> u64 {
535 if !uses.insert(Arc::as_ptr(binding)) {
536 return 0;
537 }
538 (2 * std::mem::size_of::<usize>() + std::mem::size_of::<SourceRustUse>()) as u64
539 + binding.retained_bytes()
540 + module_retained_bytes(&binding.module, modules)
541}
542
543impl SourceRustFacts {
544 pub(crate) fn retained_bytes(
546 &self,
547 uses: &mut HashSet<*const SourceRustUse>,
548 modules: &mut HashSet<*const [String]>,
549 ) -> u64 {
550 let mut bytes = (self.modules.capacity() * std::mem::size_of::<SourceRustModule>()
551 + self.declarations.capacity() * std::mem::size_of::<SourceRustDeclaration>()
552 + self.associated.capacity() * std::mem::size_of::<SourceRustAssociated>()
553 + self.candidate_calls.capacity() * std::mem::size_of::<SourceRustCallImports>())
554 as u64;
555 for module in &self.modules {
556 bytes = bytes
557 .saturating_add(module.name.capacity() as u64)
558 .saturating_add(module_retained_bytes(&module.parent, modules));
559 }
560 for declaration in &self.declarations {
561 bytes = bytes
562 .saturating_add(declaration.name.capacity() as u64)
563 .saturating_add(module_retained_bytes(&declaration.module, modules));
564 if let SourceRustBinding::Use(binding) = &declaration.binding {
565 bytes = bytes.saturating_add(use_retained_bytes(binding, uses, modules));
566 }
567 }
568 for method in &self.associated {
569 bytes = bytes
570 .saturating_add(method.owner.capacity() as u64)
571 .saturating_add(method.name.capacity() as u64)
572 .saturating_add(module_retained_bytes(&method.module, modules));
573 }
574 let mut candidates = HashSet::new();
575 let mut declarations = HashSet::new();
576 for call in &self.candidate_calls {
577 bytes = bytes.saturating_add(call.member.as_ref().map_or(0, String::capacity) as u64);
578 if declarations.insert(Arc::as_ptr(&call.declarations)) {
579 bytes = bytes.saturating_add(
580 (std::mem::size_of_val(call.declarations.as_ref())
581 + 2 * std::mem::size_of::<usize>()) as u64,
582 );
583 }
584 if candidates.insert(Arc::as_ptr(&call.bindings)) {
585 bytes = bytes.saturating_add(
586 (std::mem::size_of_val(call.bindings.as_ref())
587 + 2 * std::mem::size_of::<usize>()) as u64,
588 );
589 }
590 }
591 bytes
592 }
593}
594
595impl SourceCargoTargets {
596 pub(crate) fn retained_bytes(&self) -> u64 {
598 self.targets.iter().fold(
599 (self.targets.capacity() * std::mem::size_of::<SourceCargoTarget>()
600 + self.automatic.capacity() * std::mem::size_of::<SourceCargoAutomaticTarget>())
601 as u64,
602 |bytes, target| {
603 bytes
604 .saturating_add(target.path.capacity() as u64)
605 .saturating_add(target.name.as_ref().map_or(0, String::capacity) as u64)
606 },
607 )
608 }
609}
610
611pub(crate) fn is_module_item(node: Node<'_>) -> bool {
613 node.parent().is_some_and(|parent| {
614 parent.kind() == "source_file"
615 || parent.kind() == "declaration_list"
616 && parent
617 .parent()
618 .is_some_and(|owner| owner.kind() == "mod_item")
619 })
620}
621
622pub(crate) fn has_unsupported_attributes(node: Node<'_>, content: &str) -> bool {
624 let mut sibling = node.prev_named_sibling();
625 while let Some(previous) = sibling {
626 match previous.kind() {
627 "attribute_item" | "inner_attribute_item" => {
628 let mut cursor = previous.walk();
629 let attribute = previous
630 .named_children(&mut cursor)
631 .find(|child| child.kind() == "attribute");
632 let name = attribute
633 .and_then(|attribute| attribute.named_child(0))
634 .and_then(|path| crate::rust_use_source::rust_path(path, content, 0));
635 if !name.as_deref().is_some_and(|name| {
636 matches!(
637 name,
638 "allow"
639 | "warn"
640 | "deny"
641 | "forbid"
642 | "expect"
643 | "doc"
644 | "inline"
645 | "cold"
646 | "must_use"
647 | "deprecated"
648 | "track_caller"
649 | "repr"
650 | "non_exhaustive"
651 | "rustfmt::skip"
652 )
653 }) {
654 return true;
655 }
656 sibling = previous.prev_named_sibling();
657 }
658 "line_comment" | "block_comment" => sibling = previous.prev_named_sibling(),
659 _ => break,
660 }
661 }
662 false
663}
664
665pub(crate) fn constructor_visibility(node: Node<'_>) -> SourceRustVisibility {
667 let declared = visibility(node);
668 let Some(body) = node.child_by_field_name("body") else {
669 return declared;
670 };
671 if body.kind() != "ordered_field_declaration_list" {
672 return declared;
673 }
674 let mut result = declared;
675 let mut field = SourceRustVisibility::Private;
676 let mut cursor = body.walk();
677 for child in body.named_children(&mut cursor) {
678 if child.kind() == "visibility_modifier" {
679 field = visibility_modifier(child);
680 } else if child.kind() != "attribute_item" {
681 result = match (result, field) {
682 (SourceRustVisibility::Unsupported, _) | (_, SourceRustVisibility::Unsupported) => {
683 SourceRustVisibility::Unsupported
684 }
685 (SourceRustVisibility::Private, _) | (_, SourceRustVisibility::Private) => {
686 SourceRustVisibility::Private
687 }
688 (SourceRustVisibility::Crate, _) | (_, SourceRustVisibility::Crate) => {
689 SourceRustVisibility::Crate
690 }
691 _ => SourceRustVisibility::Public,
692 };
693 field = SourceRustVisibility::Private;
694 }
695 }
696 result
697}
698
699pub(crate) fn visibility(node: Node<'_>) -> SourceRustVisibility {
701 let mut cursor = node.walk();
702 let Some(visibility) = node
703 .named_children(&mut cursor)
704 .find(|child| child.kind() == "visibility_modifier")
705 else {
706 return SourceRustVisibility::Private;
707 };
708 visibility_modifier(visibility)
709}
710
711fn visibility_modifier(visibility: Node<'_>) -> SourceRustVisibility {
713 if visibility.child_count() == 1 {
714 SourceRustVisibility::Public
715 } else {
716 let mut cursor = visibility.walk();
717 if visibility
718 .named_children(&mut cursor)
719 .any(|child| child.kind() == "crate")
720 {
721 SourceRustVisibility::Crate
722 } else {
723 SourceRustVisibility::Unsupported
724 }
725 }
726}
727
728pub(crate) fn cargo_targets<E>(
730 root: &toml::map::Map<String, toml::Value>,
731 check: &mut impl FnMut() -> Result<(), E>,
732) -> Result<SourceCargoTargets, E> {
733 use SourceCargoTargetKind as Kind;
734 let mut facts = SourceCargoTargets {
735 complete: true,
736 ..SourceCargoTargets::default()
737 };
738 let Some(package) = root.get("package").and_then(toml::Value::as_table) else {
739 return Ok(facts);
740 };
741 facts.edition = match package.get("edition") {
742 None => Some(2015),
743 Some(toml::Value::String(edition)) => match edition.as_str() {
744 "2015" => Some(2015),
745 "2018" => Some(2018),
746 "2021" => Some(2021),
747 "2024" => Some(2024),
748 _ => None,
749 },
750 Some(_) => None,
751 };
752 let manual = ["lib", "bin", "example", "test", "bench"]
753 .iter()
754 .any(|key| root.contains_key(*key));
755 let default_auto = match package.get("edition") {
756 None => Some(!manual),
757 Some(toml::Value::String(edition)) if edition == "2015" => Some(!manual),
758 Some(toml::Value::String(edition))
759 if matches!(edition.as_str(), "2018" | "2021" | "2024") =>
760 {
761 Some(true)
762 }
763 Some(toml::Value::Table(edition))
764 if edition.get("workspace").and_then(toml::Value::as_bool) == Some(true) =>
765 {
766 (!manual).then_some(true)
767 }
768 Some(_) => {
769 facts.complete = false;
770 None
771 }
772 };
773 for (key, flag, kind, directory) in [
774 ("lib", "autolib", Kind::Library, "src"),
775 ("bin", "autobins", Kind::Binary, "src/bin"),
776 ("example", "autoexamples", Kind::Example, "examples"),
777 ("test", "autotests", Kind::Test, "tests"),
778 ("bench", "autobenches", Kind::Benchmark, "benches"),
779 ] {
780 check()?;
781 let auto = match package.get(flag) {
782 None => default_auto,
783 Some(value) => {
784 let enabled = value.as_bool();
785 if enabled.is_none() {
786 facts.complete = false;
787 }
788 enabled
789 }
790 };
791 if kind == Kind::Library {
792 if let Some(target) = root.get(key) {
793 if let Some(target) = target.as_table() {
794 let path = target
795 .get("path")
796 .map_or(Some("src/lib.rs"), toml::Value::as_str);
797 if let Some(path) = path.filter(|path| !path.is_empty()) {
798 facts.targets.push(SourceCargoTarget {
799 kind,
800 path: path.to_string(),
801 name: None,
802 });
803 } else {
804 facts.complete = false;
805 }
806 } else {
807 facts.complete = false;
808 }
809 } else if auto != Some(false) {
810 facts.automatic.push(SourceCargoAutomaticTarget {
811 kind,
812 certain: auto == Some(true),
813 });
814 }
815 continue;
816 }
817 if auto != Some(false) {
818 facts.automatic.push(SourceCargoAutomaticTarget {
819 kind,
820 certain: auto == Some(true),
821 });
822 }
823 let Some(targets) = root.get(key) else {
824 continue;
825 };
826 let Some(targets) = targets.as_array() else {
827 facts.complete = false;
828 continue;
829 };
830 for target in targets {
831 check()?;
832 if facts.targets.len() >= crate::MAX_RELATIONS_PER_FILE {
833 facts.complete = false;
834 facts.targets.clear();
835 return Ok(facts);
836 }
837 let Some(target) = target.as_table() else {
838 facts.complete = false;
839 continue;
840 };
841 let Some(name) = target
842 .get("name")
843 .and_then(toml::Value::as_str)
844 .filter(|name| {
845 !name.is_empty() && !name.contains(['/', '\\']) && *name != "." && *name != ".."
846 })
847 else {
848 facts.complete = false;
849 continue;
850 };
851 if let Some(path) = target.get("path") {
852 if let Some(path) = path.as_str().filter(|path| !path.is_empty()) {
853 facts.targets.push(SourceCargoTarget {
854 kind,
855 path: path.to_string(),
856 name: Some(name.to_string()),
857 });
858 } else {
859 facts.complete = false;
860 }
861 } else {
862 for path in [
865 format!("{directory}/{name}.rs"),
866 format!("{directory}/{name}/main.rs"),
867 ] {
868 facts.targets.push(SourceCargoTarget {
869 kind,
870 path,
871 name: Some(name.to_string()),
872 });
873 }
874 if kind == Kind::Binary
875 && package.get("name").and_then(toml::Value::as_str) == Some(name)
876 {
877 facts.targets.push(SourceCargoTarget {
878 kind,
879 path: "src/main.rs".to_string(),
880 name: Some(name.to_string()),
881 });
882 }
883 }
884 }
885 }
886 match package.get("build") {
887 Some(toml::Value::Boolean(false)) => {}
888 Some(toml::Value::String(path)) if !path.is_empty() => {
889 facts.targets.push(SourceCargoTarget {
890 kind: Kind::BuildScript,
891 path: path.clone(),
892 name: None,
893 });
894 }
895 None | Some(toml::Value::Boolean(true)) => facts.targets.push(SourceCargoTarget {
896 kind: Kind::BuildScript,
897 path: "build.rs".to_string(),
898 name: None,
899 }),
900 Some(_) => facts.complete = false,
901 }
902 if facts.targets.len() > crate::MAX_RELATIONS_PER_FILE {
903 facts.complete = false;
904 facts.targets.clear();
905 }
906 check()?;
907 Ok(facts)
908}
909
910#[cfg(test)]
911mod tests {
912 use super::*;
913 use crate::{SourceCallBinding, extract_symbol_graph_with_semantics_controlled};
914 use projectatlas_core::{IndexCancellation, IndexWorkControl};
915 macro_rules! fixture_ok {
917 ($value:expr) => {{
918 let value = $value;
919 assert!(value.is_ok(), "{value:?}");
920 let Ok(value) = value else {
921 return;
922 };
923 value
924 }};
925 }
926
927 #[test]
928 fn rust_attributes_preserve_inert_items_and_refuse_conditional_bindings() {
929 let source = "#[allow(dead_code)] pub mod valid {#[inline] pub fn work(){}}\n#[cfg(any())] use std::mem::drop as omitted; #[cfg(any())] use crate::valid::work as hidden; fn caller(){omitted(1); hidden();}\npub struct Public(pub u8); pub struct Private(u8); pub struct Mixed(pub u8, pub(crate) u8);";
930 let result = fixture_ok!(extract_symbol_graph_with_semantics_controlled(
931 "src/lib.rs",
932 Some("rust"),
933 source,
934 &IndexWorkControl::new(IndexCancellation::new(), None)
935 ));
936 let facts = result.semantics();
937 let rust = fixture_ok!(facts.rust().ok_or("missing Rust facts"));
938 assert!(
939 rust.modules
940 .iter()
941 .any(|module| module.name == "valid" && module.supported)
942 );
943 assert!(
944 rust.declarations
945 .iter()
946 .any(|declaration| declaration.name == "work"
947 && matches!(declaration.binding, SourceRustBinding::Declaration(_)))
948 );
949 for (name, visibility) in [
950 ("Public", SourceRustVisibility::Public),
951 ("Private", SourceRustVisibility::Private),
952 ("Mixed", SourceRustVisibility::Crate),
953 ] {
954 assert!(
955 rust.declarations
956 .iter()
957 .any(|declaration| declaration.name == name
958 && declaration.visibility == visibility),
959 "{name}"
960 );
961 }
962 assert!(facts.imports().iter().all(|import| {
963 import
964 .rust
965 .as_ref()
966 .is_some_and(|binding| !binding.supported)
967 && import.rust_toolchain.is_none()
968 }));
969 assert!(facts.calls().iter().all(|call| !matches!(
970 call.binding,
971 SourceCallBinding::Imported { .. } | SourceCallBinding::LocalDeclaration(_)
972 )));
973 }
974
975 #[test]
976 fn rust_partial_imports_cannot_narrow_a_retained_alias() {
977 let oversized = "nested::".repeat(crate::MAX_SNIPPET_CHARS);
978 let source = format!(
979 "pub mod first {{pub fn run(){{}}}} use crate::{{first::run as launch, {oversized}run as launch}}; fn caller(){{launch();}}"
980 );
981 let result = fixture_ok!(extract_symbol_graph_with_semantics_controlled(
982 "src/lib.rs",
983 Some("rust"),
984 &source,
985 &IndexWorkControl::new(IndexCancellation::new(), None)
986 ));
987 let facts = result.semantics();
988 assert!(facts.bindings_incomplete);
989 assert!(facts.rust().is_some_and(|rust| !rust.complete));
990 assert!(facts.imports().iter().any(|import| {
991 import
992 .references
993 .iter()
994 .any(|reference| reference.local() == "launch")
995 }));
996 assert!(!facts.calls().is_empty());
997 assert!(
998 facts
999 .calls()
1000 .iter()
1001 .all(|call| matches!(call.binding, SourceCallBinding::Unresolved))
1002 );
1003 assert!(facts.matches_graph(result.graph()));
1004 }
1005
1006 #[test]
1007 fn rust_module_boundaries_do_not_inherit_bare_outer_bindings_or_local_module_addresses() {
1008 let source = "fn run(){} mod global {pub fn run(){}} mod inner {pub fn inherited(){run();} use crate::run as launch; pub fn explicit(){launch();}} fn outer(){mod global {pub fn wrong(){self::run();}}}";
1009 let result = fixture_ok!(extract_symbol_graph_with_semantics_controlled(
1010 "src/lib.rs",
1011 Some("rust"),
1012 source,
1013 &IndexWorkControl::new(IndexCancellation::new(), None)
1014 ));
1015 let graph = result.graph();
1016 let facts = result.semantics();
1017 for name in ["inherited", "wrong"] {
1018 let ordinal = fixture_ok!(
1019 graph
1020 .symbols
1021 .iter()
1022 .position(|symbol| symbol.name == name)
1023 .ok_or("missing declaration")
1024 );
1025 assert!(
1026 facts.calls().iter().any(|call| call.owner == Some(ordinal)
1027 && matches!(call.binding, SourceCallBinding::Unresolved)),
1028 "{name}"
1029 );
1030 }
1031 let explicit = fixture_ok!(
1032 graph
1033 .symbols
1034 .iter()
1035 .position(|symbol| symbol.name == "explicit")
1036 .ok_or("missing explicit caller")
1037 );
1038 assert!(facts.calls().iter().any(|call| call.owner == Some(explicit)
1039 && matches!(call.binding, SourceCallBinding::Imported { .. })));
1040 assert!(facts.rust().is_some_and(|rust| rust.complete));
1041 }
1042
1043 #[test]
1044 fn automatic_target_inventory_keys_track_only_the_owning_target_directory() {
1045 let project = fixture_ok!(projectatlas_core::graph::ProjectInstanceId::from_bytes(
1046 [39; 16]
1047 ));
1048 let key = fixture_ok!(cargo_target_inventory_key(
1049 project,
1050 "member",
1051 "crates/member/Cargo.toml",
1052 "crates/member/tests"
1053 ));
1054 for (path, expected) in [
1055 ("crates/member/tests/new.rs", true),
1056 ("crates/member/tests/helper/support.rs", false),
1057 ("crates/membership/tests/new.rs", false),
1058 ("crates/member/src/service.rs", false),
1059 ] {
1060 let graph = crate::extract_symbol_graph(path, Some("rust"), "fn main() {}");
1061 let projection = fixture_ok!(crate::derive_resolution_keys_with_context(
1062 project,
1063 Some("member"),
1064 &graph,
1065 crate::ResolutionProjectionContext::default()
1066 .with_rust_package("crates/member/Cargo.toml", "crates/member"),
1067 ));
1068 assert_eq!(projection.source_keys().contains(&key), expected, "{path}");
1069 }
1070 }
1071
1072 #[test]
1073 fn toolchain_import_proof_follows_source_roots_and_binding_shadowing() {
1074 for (source, expected, import_proved) in [
1075 ("mod std {} fn caller(){std::fs::read(\"x\");}", None, false),
1076 (
1077 "mod fake {} use crate::fake as std; fn caller(){std::fs::read(\"x\");}",
1078 None,
1079 false,
1080 ),
1081 (
1082 "use std; fn caller(){std::fs::read(\"x\");}",
1083 Some("std::fs::read"),
1084 true,
1085 ),
1086 (
1087 "use std::mem::drop as dispose; fn caller(){dispose(1);}",
1088 Some("std::mem::drop"),
1089 true,
1090 ),
1091 (
1092 "use core::mem as std; fn caller(){std::drop(1);}",
1093 Some("core::mem::drop"),
1094 true,
1095 ),
1096 (
1097 "use std::mem::drop as dispose; fn caller(dispose:fn(u8)){dispose(1);}",
1098 None,
1099 true,
1100 ),
1101 (
1102 "mod std {} use std::mem::drop as dispose; fn caller(){dispose(1);}",
1103 None,
1104 false,
1105 ),
1106 ] {
1107 let result = fixture_ok!(extract_symbol_graph_with_semantics_controlled(
1108 "src/lib.rs",
1109 Some("rust"),
1110 source,
1111 &IndexWorkControl::new(IndexCancellation::new(), None)
1112 ));
1113 let facts = result.semantics();
1114 let call = fixture_ok!(facts.calls().last().ok_or("missing call"));
1115 let actual = match &call.binding {
1116 SourceCallBinding::Imported { rust_toolchain, .. } => {
1117 rust_toolchain.as_ref().map(|reference| {
1118 reference.imported().map_or_else(
1119 || reference.module().to_string(),
1120 |name| format!("{}::{name}", reference.module()),
1121 )
1122 })
1123 }
1124 _ => None,
1125 };
1126 assert_eq!(actual.as_deref(), expected, "{source}");
1127 assert_eq!(
1128 facts
1129 .imports()
1130 .iter()
1131 .any(|import| import.rust_toolchain.is_some()),
1132 import_proved,
1133 "{source}"
1134 );
1135 }
1136 }
1137
1138 #[test]
1139 fn use_paths_retain_leaf_roles_before_module_or_item_classification() {
1140 let source = "pub fn run() {} mod run {pub fn child() {}}\nuse crate::run as flat; use crate::{run as grouped}; use crate::run::{self as namespace};";
1141 let result = fixture_ok!(extract_symbol_graph_with_semantics_controlled(
1142 "src/lib.rs",
1143 Some("rust"),
1144 source,
1145 &IndexWorkControl::new(IndexCancellation::new(), None),
1146 ));
1147 let uses = result
1148 .semantics()
1149 .imports()
1150 .iter()
1151 .filter_map(|import| import.rust.as_deref())
1152 .collect::<Vec<_>>();
1153 assert_eq!(uses.len(), 3);
1154 for binding in &uses {
1155 assert_eq!(binding.path.anchor, SourceRustAnchor::Crate);
1156 assert_eq!(binding.path.segments, ["run"]);
1157 }
1158 assert_eq!(uses[0].local.as_deref(), Some("flat"));
1159 assert_eq!(uses[0].role, SourceRustUseRole::Item);
1160 assert_eq!(uses[1].role, SourceRustUseRole::Item);
1161 assert_eq!(uses[2].role, SourceRustUseRole::SelfNamespace);
1162 assert_eq!(
1163 SourceRustPath::from_ast_path("::external::item"),
1164 Some(SourceRustPath {
1165 anchor: SourceRustAnchor::Absolute,
1166 segments: vec!["external".into(), "item".into()]
1167 })
1168 );
1169 assert!(SourceRustPath::from_ast_path("crate::::item").is_none());
1170 }
1171
1172 #[test]
1173 fn module_calls_and_reexports_retain_actual_source_owners() {
1174 let source = "pub fn run() {} mod worker; mod inner {pub use crate::run as launch; pub fn caller() {launch(); crate::run();}} fn outer() {worker::run();}";
1175 let result = fixture_ok!(extract_symbol_graph_with_semantics_controlled(
1176 "src/lib.rs",
1177 Some("rust"),
1178 source,
1179 &IndexWorkControl::new(IndexCancellation::new(), None),
1180 ));
1181 let facts = result.semantics();
1182 let rust = fixture_ok!(facts.rust().ok_or("Rust ownership was not captured"));
1183 assert!(rust.complete);
1184 assert_eq!(rust.modules.len(), 2);
1185 assert!(rust.modules.iter().any(|module| module.name == "worker"
1186 && module.body.is_none()
1187 && module.parent.is_empty()
1188 && module.supported));
1189 assert!(rust.modules.iter().any(|module| module.name == "inner"
1190 && module.body.is_some()
1191 && module.parent.is_empty()));
1192 let imported = fixture_ok!(facts.imports()[0].rust.as_ref().ok_or("use path missing"));
1193 assert_eq!(imported.module.as_ref(), ["inner"]);
1194 assert!(imported.reexport);
1195 let exported = fixture_ok!(
1196 rust.declarations
1197 .iter()
1198 .find(|declaration| declaration.name == "launch")
1199 .ok_or("reexport slot missing")
1200 );
1201 assert!(matches!(&exported.binding, SourceRustBinding::Use(_)));
1202 let SourceRustBinding::Use(exported) = &exported.binding else {
1203 return;
1204 };
1205 assert!(Arc::ptr_eq(imported, exported));
1206 assert!(facts.calls().iter().any(|call| matches!(&call.binding, SourceCallBinding::RustPath(path) if path.path.anchor == SourceRustAnchor::Crate && path.module.as_ref() == ["inner"])));
1207 assert!(facts.calls().iter().any(|call| matches!(&call.binding, SourceCallBinding::RustPath(path) if path.path.anchor == SourceRustAnchor::SelfModule && path.module.is_empty() && path.path.segments == ["worker", "run"])));
1208 assert!(facts.matches_graph(result.graph()));
1209 }
1210
1211 #[test]
1212 fn cargo_target_paths_come_from_the_existing_manifest_parse() {
1213 let source = "[package]\nname='fixture'\nversion='0.1.0'\nedition='2024'\nautobins=false\n[lib]\npath='src/api.rs'\n[[bin]]\nname='tool'\npath='program.rs'\n";
1214 let result = fixture_ok!(extract_symbol_graph_with_semantics_controlled(
1215 "Cargo.toml",
1216 Some("cargo-manifest"),
1217 source,
1218 &IndexWorkControl::new(IndexCancellation::new(), None),
1219 ));
1220 let targets = fixture_ok!(
1221 result
1222 .semantics()
1223 .cargo()
1224 .ok_or("Cargo target settings missing")
1225 );
1226 assert!(targets.complete);
1227 assert!(
1228 targets
1229 .targets
1230 .iter()
1231 .any(|target| target.kind == SourceCargoTargetKind::Library
1232 && target.path == "src/api.rs")
1233 );
1234 assert!(
1235 targets
1236 .targets
1237 .iter()
1238 .any(|target| target.kind == SourceCargoTargetKind::Binary
1239 && target.path == "program.rs")
1240 );
1241 assert!(!targets.automatic.iter().any(|target| matches!(
1242 target.kind,
1243 SourceCargoTargetKind::Library | SourceCargoTargetKind::Binary
1244 )));
1245 assert!(
1246 !targets
1247 .targets
1248 .iter()
1249 .any(|target| target.path == "src/lib.rs")
1250 );
1251 assert!(result.semantics().matches_graph(result.graph()));
1252 }
1253}