Skip to main content

projectatlas_symbols/
declaration_identity.rs

1//! Purpose: Share canonical declaration scope identities with graph consumers.
2
3use projectatlas_core::graph::{
4    GraphContractError, GraphIdentityText, MAX_GRAPH_IDENTITY_BYTES, QUALIFIED_SYMBOL_SCOPE_PREFIX,
5};
6use projectatlas_core::symbols::{SymbolGraph, SymbolKind};
7use std::collections::BTreeMap;
8
9/// Qualify graph-only parent identity from the parser's existing containment rows.
10///
11/// Sorting and active-scope lookup are `O(n log n)`. Identity work is
12/// `O(n * MAX_GRAPH_IDENTITY_BYTES)`: every retained parent is bounded, and an
13/// overflowing candidate is hashed without materializing more than one bounded
14/// parent plus one admitted component.
15///
16/// # Errors
17/// Returns the graph contract error for an invalid or reserved source identity.
18pub fn qualified_symbol_parents(
19    graph: &SymbolGraph,
20) -> Result<Vec<Option<GraphIdentityText>>, GraphContractError> {
21    qualified_symbol_parents_checked(graph, &mut |_| Ok(()))
22}
23
24/// Qualify parents while admitting vector and scope ownership before growth.
25///
26/// # Errors
27/// Returns source contract errors or the caller's resource/cancellation failure.
28/// A refusal never returns a partially qualified parent vector.
29pub fn qualified_symbol_parents_checked<E: From<GraphContractError>>(
30    graph: &SymbolGraph,
31    reserve: &mut impl FnMut(u64) -> Result<(), E>,
32) -> Result<Vec<Option<GraphIdentityText>>, E> {
33    reserve((graph.symbols.len() as u64).saturating_mul(
34        (2 * std::mem::size_of::<Option<GraphIdentityText>>() + std::mem::size_of::<usize>())
35            as u64,
36    ))?;
37    qualified_symbol_parents_selected_checked(graph, (0..graph.symbols.len()).collect(), reserve)
38}
39
40/// Derive only admitted raw ordinals, without retaining rejected scope ancestors.
41pub(crate) fn qualified_symbol_parents_selected(
42    graph: &SymbolGraph,
43    order: Vec<usize>,
44) -> Result<Vec<Option<GraphIdentityText>>, GraphContractError> {
45    qualified_symbol_parents_selected_checked(graph, order, &mut |_| Ok(()))
46}
47
48/// Build one bounded scope candidate before admitting its retained owners.
49fn qualified_symbol_parents_selected_checked<E: From<GraphContractError>>(
50    graph: &SymbolGraph,
51    mut order: Vec<usize>,
52    reserve: &mut impl FnMut(u64) -> Result<(), E>,
53) -> Result<Vec<Option<GraphIdentityText>>, E> {
54    let is_php = graph
55        .language
56        .as_deref()
57        .is_some_and(|language| language.eq_ignore_ascii_case("php"));
58    order.sort_by_key(|&index| (graph.symbols[index].line_start, index));
59    let mut active_by_name = BTreeMap::<&str, Vec<usize>>::new();
60    let mut qualified_names = vec![None::<GraphIdentityText>; graph.symbols.len()];
61    let mut parents = vec![None; graph.symbols.len()];
62    for index in order {
63        let symbol = &graph.symbols[index];
64        let name = source_symbol_identity(symbol.name.clone())?;
65        let mut parent = symbol
66            .parent
67            .clone()
68            .map(source_symbol_identity)
69            .transpose()?;
70        if let Some(immediate_parent) = parent.as_ref()
71            && let Some(candidates) = active_by_name.get_mut(immediate_parent.as_str())
72        {
73            while candidates.last().is_some_and(|&candidate_index| {
74                let candidate = &graph.symbols[candidate_index];
75                // Namespace headers may precede their body; concrete PHP scopes
76                // have exact ends even when the next declaration shares a line.
77                if is_php
78                    && candidate.kind != SymbolKind::Module
79                    && let Some((outer, inner)) =
80                        candidate.source_selector.zip(symbol.source_selector)
81                {
82                    outer.byte_end < inner.byte_end
83                } else {
84                    candidate.line_end < symbol.line_end
85                }
86            }) {
87                candidates.pop();
88            }
89            if let Some(qualified_parent) = candidates
90                .last()
91                .and_then(|&candidate_index| qualified_names[candidate_index].clone())
92            {
93                parent = Some(qualified_parent);
94            }
95        }
96        let qualified = match parent.as_ref() {
97            Some(parent) => qualified_symbol_identity(parent, &name)?,
98            None => name,
99        };
100        reserve(
101            128_u64
102                .saturating_add((4 * std::mem::size_of::<usize>()) as u64)
103                .saturating_add((qualified.as_str().len() as u64).saturating_mul(2))
104                .saturating_add(
105                    parent
106                        .as_ref()
107                        .map_or(0, |parent| parent.as_str().len() as u64)
108                        .saturating_mul(2),
109                ),
110        )?;
111        qualified_names[index] = Some(qualified);
112        parents[index] = parent;
113        active_by_name
114            .entry(symbol.name.as_str())
115            .or_default()
116            .push(index);
117    }
118    Ok(parents)
119}
120
121/// Validate borrowed parser identity text using the publication admission policy.
122///
123/// # Errors
124/// Returns the graph contract error for invalid text or the reserved scope prefix.
125pub fn validate_source_symbol_identity(value: &str) -> Result<(), GraphContractError> {
126    GraphIdentityText::validate(value)?;
127    validate_source_scope_namespace(value)
128}
129
130/// Reject the private derived-scope namespace without changing source spelling.
131fn validate_source_scope_namespace(value: &str) -> Result<(), GraphContractError> {
132    if value.starts_with(QUALIFIED_SYMBOL_SCOPE_PREFIX) {
133        return Err(GraphContractError::InvalidIdentityText {
134            reason: "source symbol identity uses the reserved derived-scope namespace",
135        });
136    }
137    Ok(())
138}
139
140/// Admit one parser-owned symbol identity outside the derived-scope namespace.
141///
142/// # Errors
143/// Returns the graph contract error for invalid text or the reserved scope prefix.
144pub fn source_symbol_identity(value: String) -> Result<GraphIdentityText, GraphContractError> {
145    let identity = GraphIdentityText::new(value)?;
146    validate_source_scope_namespace(identity.as_str())?;
147    Ok(identity)
148}
149
150/// Derive one exact or compact qualified scope from validated components.
151///
152/// # Errors
153/// Returns the graph contract error when the compact identity cannot retain its name.
154pub fn qualified_symbol_identity(
155    parent: &GraphIdentityText,
156    name: &GraphIdentityText,
157) -> Result<GraphIdentityText, GraphContractError> {
158    const SEPARATOR: &str = "::";
159    const DIGEST_DOMAIN: &str = "projectatlas.graph.qualified-symbol-scope.v1";
160    let qualified_len = parent.as_str().len() + SEPARATOR.len() + name.as_str().len();
161    if qualified_len <= MAX_GRAPH_IDENTITY_BYTES {
162        let mut qualified = String::with_capacity(qualified_len);
163        qualified.push_str(parent.as_str());
164        qualified.push_str(SEPARATOR);
165        qualified.push_str(name.as_str());
166        return GraphIdentityText::new(qualified);
167    }
168
169    let compact_len =
170        QUALIFIED_SYMBOL_SCOPE_PREFIX.len() + 64 + SEPARATOR.len() + name.as_str().len();
171    if compact_len > MAX_GRAPH_IDENTITY_BYTES {
172        return Err(GraphContractError::InvalidIdentityText {
173            reason: "derived scope cannot retain its nearest admitted symbol name",
174        });
175    }
176    let mut hasher = blake3::Hasher::new_derive_key(DIGEST_DOMAIN);
177    hasher.update(parent.as_str().as_bytes());
178    hasher.update(SEPARATOR.as_bytes());
179    hasher.update(name.as_str().as_bytes());
180    let digest = hasher.finalize().to_hex();
181    let mut compact = String::with_capacity(compact_len);
182    compact.push_str(QUALIFIED_SYMBOL_SCOPE_PREFIX);
183    compact.push_str(digest.as_str());
184    compact.push_str(SEPARATOR);
185    compact.push_str(name.as_str());
186    GraphIdentityText::new(compact)
187}