Skip to main content

projectatlas_symbols/
rust_binding_source.rs

1//! Typed Rust paths and module ownership retained from the existing native AST.
2
3use crate::SourceByteSpan;
4use std::collections::HashSet;
5use std::sync::Arc;
6use tree_sitter::Node;
7
8/// Explicit anchor of a grammar-owned Rust path.
9#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
10pub enum SourceRustAnchor {
11    /// The selected crate target's root module.
12    Crate,
13    /// The source's actual containing module.
14    SelfModule,
15    /// A bounded number of containing module parents.
16    Super(usize),
17    /// An explicit external-prelude root, without guessed crate ownership.
18    Absolute,
19    /// A bare path whose crate or module ownership needs independent evidence.
20    Unqualified,
21}
22
23/// A complete source path before deciding whether its final segment is an item.
24#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
25pub struct SourceRustPath {
26    /// Grammar-owned root role, independent from path length.
27    pub anchor: SourceRustAnchor,
28    /// Ordered identifiers after the explicit anchor.
29    pub segments: Vec<String>,
30}
31
32impl SourceRustPath {
33    /// Retain components from the bounded canonical path built by the AST walker.
34    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    /// Count owned path identifiers without serializing source facts.
72    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/// Meaning of one Rust use-tree leaf, before value/module lookup.
81#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
82pub enum SourceRustUseRole {
83    /// An identifier path may import a value, a type, a module, or several namespaces.
84    Item,
85    /// Grouped self syntax explicitly imports the prefix's module namespace.
86    SelfNamespace,
87    /// A wildcard retains dependencies without enumerating guessed export bindings.
88    Wildcard,
89}
90
91/// One explicit use binding, paired with its existing import occurrence.
92#[derive(Clone, Debug, Eq, PartialEq)]
93pub struct SourceRustUse {
94    /// Complete AST-owned path, without an inferred module/item split.
95    pub path: SourceRustPath,
96    /// Source leaf role, including grouped self and wildcards.
97    pub role: SourceRustUseRole,
98    /// Actual local alias, absent for a wildcard or unsupported binding name.
99    pub local: Option<String>,
100    /// Inline module path inside the owning source file.
101    pub module: Arc<[String]>,
102    /// Visibility syntax explicitly makes this a re-export as well as an import.
103    pub reexport: bool,
104    /// Attributes or malformed ownership prevent this occurrence from proving a target.
105    pub supported: bool,
106}
107
108impl SourceRustUse {
109    /// Count owned target and alias text; shared module paths are charged separately.
110    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/// Source-proven placement of one Rust module declaration.
118#[derive(Clone, Debug, Eq, PartialEq)]
119pub struct SourceRustModule {
120    /// Inline containing module path inside this source file.
121    pub parent: Arc<[String]>,
122    /// Exact grammar identifier declared by mod syntax.
123    pub name: String,
124    /// Original module declaration extent.
125    pub span: SourceByteSpan,
126    /// Inline body extent; absence means an external module file is required.
127    pub body: Option<SourceByteSpan>,
128    /// Conditional or custom-path ownership is deliberately not guessed.
129    pub supported: bool,
130    /// Visibility checked independently from callable declaration lookup.
131    pub visibility: SourceRustVisibility,
132}
133
134/// Visibility forms that can be established without compiler name resolution.
135#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
136pub enum SourceRustVisibility {
137    /// Visible in the containing module and its descendants.
138    Private,
139    /// Visible throughout this exact crate target.
140    Crate,
141    /// Explicit unrestricted public visibility.
142    Public,
143    /// Restricted or malformed visibility needs additional unsupported proof.
144    Unsupported,
145}
146
147/// One static call path whose source namespace was established lexically.
148#[derive(Clone, Debug, Eq, PartialEq)]
149pub struct SourceRustCall {
150    /// Complete static callee path, without a guessed module/item split.
151    pub path: SourceRustPath,
152    /// Actual inline module owner of the binding's source reference.
153    pub module: Arc<[String]>,
154    /// Distinguish a declared module binding from an explicit anchored path.
155    pub origin: SourceRustCallOrigin,
156}
157
158/// Native syntax evidence that establishes a static Rust call path.
159#[derive(Clone, Copy, Debug, Eq, PartialEq)]
160pub enum SourceRustCallOrigin {
161    /// Lexical lookup selected an actual mod declaration.
162    DeclaredModule,
163    /// The callee explicitly used crate, self or super qualification.
164    AnchoredPath,
165    /// Mixed module-level namespaces need source-target classification before lookup.
166    ModuleLookup,
167}
168
169impl SourceRustCall {
170    /// Retain the existing direct canonical lookup without discarding inline owners.
171    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/// Source-owned meaning of a Rust module's value slot.
207#[derive(Clone, Debug, Eq, PartialEq)]
208pub enum SourceRustBinding {
209    /// Actual callable declaration ordinal in the accompanying graph.
210    Declaration(usize),
211    /// A type namespace owner, which never proves a direct callable value.
212    TypeDeclaration(usize),
213    /// A type namespace owner survived while its exact declaration proof did not.
214    UnresolvedType,
215    /// A lost import may occupy either namespace and must remain a competing blocker.
216    UnresolvedImport,
217    /// A source use binding may forward a value or a namespace.
218    Use(Arc<SourceRustUse>),
219    /// The source name exists but cannot prove a callable target.
220    Unresolved,
221}
222
223/// One Rust module value binding, including non-callable blockers.
224#[derive(Clone, Debug, Eq, PartialEq)]
225pub struct SourceRustDeclaration {
226    /// Actual inline module owner, independent from display parent strings.
227    pub module: Arc<[String]>,
228    /// Identifier declared in this module's value namespace.
229    pub name: String,
230    /// Visibility supplied by this declaration's syntax.
231    pub visibility: SourceRustVisibility,
232    /// Actual source-owned callable or forwarding evidence.
233    pub binding: SourceRustBinding,
234}
235
236/// One inherent method linked to its owning type by actual impl syntax.
237#[derive(Clone, Debug, Eq, PartialEq)]
238pub struct SourceRustAssociated {
239    /// Inline module containing the impl and its local type owner.
240    pub module: Arc<[String]>,
241    /// Actual local type identifier, excluding inferred type flow.
242    pub owner: String,
243    /// Actual method identifier.
244    pub name: String,
245    /// Admitted declaration ordinal; a lost ordinal remains a competing blocker.
246    pub declaration: Option<usize>,
247    /// Method visibility from source syntax.
248    pub visibility: SourceRustVisibility,
249    /// False for conditional, transforming or malformed method/impl syntax.
250    pub supported: bool,
251}
252
253/// One lexical call whose winning scope contains only competing import bindings.
254#[derive(Clone, Debug, Eq, PartialEq)]
255pub struct SourceRustCallImports {
256    /// Exact raw call relation ordinal.
257    pub relation: usize,
258    /// Shared (import evidence, reference) ordinals; every candidate must survive remapping.
259    pub bindings: Arc<[(usize, usize)]>,
260    /// Actual same-scope value declarations competing with those imports.
261    pub declarations: Arc<[usize]>,
262    /// Complete static member suffix in the call's requested namespace.
263    pub member: Option<String>,
264}
265
266/// Rust ownership evidence from the same parsed source as lexical calls.
267#[derive(Clone, Debug, Default, Eq, PartialEq)]
268pub struct SourceRustFacts {
269    /// Inline and external module declarations in source order.
270    pub modules: Vec<SourceRustModule>,
271    /// Module value slots, preserving duplicates and unresolved competitors.
272    pub declarations: Vec<SourceRustDeclaration>,
273    /// Same-source inherent methods with actual local type ownership.
274    pub associated: Vec<SourceRustAssociated>,
275    /// Scope-owned import competitors, including function/block-local imports.
276    pub candidate_calls: Vec<SourceRustCallImports>,
277    /// False when ownership or cardinality bounds discarded competing evidence.
278    pub complete: bool,
279}
280
281/// Cargo target category, retained independently from source filenames.
282#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
283pub enum SourceCargoTargetKind {
284    /// The package's library target.
285    Library,
286    /// An executable target.
287    Binary,
288    /// An example target.
289    Example,
290    /// An integration test target.
291    Test,
292    /// A benchmark target.
293    Benchmark,
294    /// The package's build script.
295    BuildScript,
296}
297
298impl SourceCargoTargetKind {
299    /// Return the conventional directory for independently named automatic targets.
300    #[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    /// Return a fixed conventional root whose absence can be tracked explicitly.
312    #[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
323/// Canonical invalidation cause for one Cargo target-root directory inventory.
324///
325/// This is a dependency key, not proof of a call target or crate membership.
326///
327/// # Errors
328///
329/// Returns a graph contract error for invalid package, manifest or directory identity.
330pub 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
350/// Canonical invalidation cause emitted only by admitted explicit sibling roots.
351///
352/// Its configuration role is distinct from automatic inventory, so an unlisted
353/// helper cannot invalidate explicit roots when automatic discovery is disabled.
354///
355/// # Errors
356///
357/// Returns a graph contract error for invalid package, manifest or directory identity.
358pub 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
378/// Share text admission while preserving distinct automatic/configured root roles.
379fn 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/// Return the inventory directory for a flat potential automatic Cargo root.
416///
417/// This classifies source placement, not actual target membership or discovery policy.
418///
419/// ```
420/// assert_eq!(projectatlas_symbols::cargo_automatic_target_directory("src/bin/app.rs", ""), Some("src/bin"));
421/// assert_eq!(projectatlas_symbols::cargo_automatic_target_directory("src/bin/app/main.rs", ""), None);
422/// ```
423#[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/// One manifest-owned crate root candidate, validated against admitted inventory.
454#[derive(Clone, Debug, Eq, PartialEq)]
455pub struct SourceCargoTarget {
456    /// Actual Cargo target category.
457    pub kind: SourceCargoTargetKind,
458    /// Manifest-relative source path; no source-root basename inference is needed.
459    pub path: String,
460    /// Explicit target name used to avoid restoring an overridden inferred root.
461    pub name: Option<String>,
462}
463
464/// A conventional target category admitted by the manifest's discovery policy.
465#[derive(Clone, Debug, Eq, PartialEq)]
466pub struct SourceCargoAutomaticTarget {
467    /// Category whose conventional source layout may supply roots.
468    pub kind: SourceCargoTargetKind,
469    /// False when an inherited edition leaves the discovery default unproved.
470    pub certain: bool,
471}
472
473/// Target configuration captured at the existing Cargo TOML parse boundary.
474#[derive(Clone, Debug, Default, Eq, PartialEq)]
475pub struct SourceCargoTargets {
476    /// Explicit package edition, or the Cargo 2015 default; inherited/unknown stays absent.
477    pub edition: Option<u16>,
478    /// Explicit or manifest-default source root candidates.
479    pub targets: Vec<SourceCargoTarget>,
480    /// Categories whose conventional directory discovery is enabled.
481    pub automatic: Vec<SourceCargoAutomaticTarget>,
482    /// False when malformed or excessive target settings prevent exact ownership.
483    pub complete: bool,
484}
485
486/// Maximum inline ownership depth retained by a source binding proof.
487const MAX_SOURCE_MODULE_DEPTH: usize = 64;
488
489/// Find actual inline module ancestors, independent from graph display parents.
490pub(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
515/// Charge one shared immutable module path once within its owning projection.
516pub(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
529/// Charge a use target shared by its import occurrence and module declaration slot.
530pub(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    /// Account actual vector capacities, payloads and shared source module/use owners.
545    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    /// Account manifest target arrays and their retained path/name strings.
597    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
611/// Return whether an item belongs to a file or inline module declaration list.
612pub(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
622/// Refuse attributes that may transform an item or select a different source owner.
623pub(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
665/// A tuple constructor is no more visible than its least visible field.
666pub(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
699/// Read visibility tokens from the same native tree without parsing a signature.
700pub(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
711/// Interpret one grammar-owned visibility modifier.
712fn 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
728/// Capture target settings from the already parsed Cargo manifest value.
729pub(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                // These are candidates, not root proof. Inventory and module
863                // membership decide which default path actually owns the source.
864                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    /// Fail a fixture before using its value, keeping assertion tests infallible.
916    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}