Skip to main content

sqry_core/graph/unified/bind/
mod.rs

1//! Binding query facade with declaration/reference classification.
2//!
3//! Re-exports the full alias derivation API from the sub-module so that
4//! callers can reach `AliasEntry`, `AliasEntryId`, and `AliasTable` via the
5//! `bind` module path without navigating to `bind::alias` directly.
6//!
7//! Provides `BindingQuery`, a builder-pattern API for resolving symbols and
8//! classifying them as declarations, references, imports, or ambiguous. This
9//! module builds on the witness-bearing resolution API in `resolution.rs`.
10
11pub mod alias;
12pub mod plane;
13pub mod query;
14pub mod shadow;
15
16pub use alias::{AliasEntry, AliasEntryId, AliasTable};
17pub use plane::{BindingPlane, BindingResolution};
18pub use shadow::{ShadowEntry, ShadowEntryId, ShadowTable};
19pub use witness::WitnessRendering;
20pub mod scope;
21pub mod witness;
22
23pub use query::BindingQuery;
24pub use witness::{
25    RejectionReason, ResolutionStep, TieBreakReason, UnresolvedReason, VisibilityReason,
26};
27
28use serde::{Deserialize, Serialize};
29
30use super::concurrent::GraphSnapshot;
31use super::edge::kind::{EdgeKind, ExportKind};
32use super::node::id::NodeId;
33use super::node::kind::NodeKind;
34use super::resolution::{NormalizedSymbolQuery, SymbolCandidateBucket, SymbolResolutionOutcome};
35
36/// How a resolved symbol relates to its definition site.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
38pub enum SymbolClassification {
39    /// The node IS the declaration (target of `Defines`/`Contains` edges,
40    /// `NodeKind` is not `CallSite`/`Import`/`Export`).
41    Declaration,
42    /// The node is a reference/use of a declaration elsewhere.
43    Reference,
44    /// The node is an import statement (has `Defines` edge but is not
45    /// the source-of-truth declaration).
46    Import,
47    /// Multiple interpretations possible (e.g., re-export that is both
48    /// a reference and a local declaration).
49    Ambiguous,
50    /// Classification could not be determined from graph structure.
51    Unknown,
52}
53
54/// A single resolved binding with classification and provenance.
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56pub struct ResolvedBinding {
57    /// The resolved node ID.
58    pub node_id: NodeId,
59    /// Declaration vs reference vs import classification.
60    pub classification: SymbolClassification,
61    /// Which resolution bucket produced this candidate.
62    pub bucket: SymbolCandidateBucket,
63    /// The node's kind (saves consumers a `get_node()` round-trip).
64    pub kind: NodeKind,
65}
66
67/// Result of a binding query.
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69pub struct BindingResult {
70    /// Normalized query (`None` if file scope resolution failed).
71    pub query: Option<NormalizedSymbolQuery>,
72    /// Resolved bindings with classification and provenance.
73    pub bindings: Vec<ResolvedBinding>,
74    /// Resolution outcome (same semantics as `resolve_symbol`).
75    pub outcome: SymbolResolutionOutcome,
76}
77
78/// Classify a node as declaration, reference, import, or ambiguous.
79///
80/// Classification logic:
81/// 1. `NodeKind::Import` → `Import`
82/// 2. `NodeKind::Export` → check for re-export edge → `Ambiguous` if re-export, else `Import`
83/// 3. `NodeKind::CallSite` → `Reference`
84/// 4. Has incoming `Defines` or `Contains` edge → `Declaration`
85/// 5. No structural incoming edges → `Reference`
86/// 6. Fallback → `Unknown`
87pub(crate) fn classify_node(
88    snapshot: &GraphSnapshot,
89    node_id: NodeId,
90    kind: NodeKind,
91) -> SymbolClassification {
92    // Step 1: Check NodeKind first
93    if kind == NodeKind::Import {
94        return SymbolClassification::Import;
95    }
96
97    if kind == NodeKind::Export {
98        // Check if this export has a re-export edge
99        let incoming = snapshot.edges().edges_to(node_id);
100        let has_reexport = incoming.iter().any(|e| {
101            matches!(
102                &e.kind,
103                EdgeKind::Exports {
104                    kind: ExportKind::Reexport | ExportKind::Namespace,
105                    ..
106                }
107            )
108        });
109        // Also check outgoing exports for re-export classification
110        let outgoing = snapshot.edges().edges_from(node_id);
111        let has_reexport_outgoing = outgoing.iter().any(|e| {
112            matches!(
113                &e.kind,
114                EdgeKind::Exports {
115                    kind: ExportKind::Reexport | ExportKind::Namespace,
116                    ..
117                }
118            )
119        });
120
121        return if has_reexport || has_reexport_outgoing {
122            SymbolClassification::Ambiguous
123        } else {
124            SymbolClassification::Import
125        };
126    }
127
128    if kind == NodeKind::CallSite {
129        return SymbolClassification::Reference;
130    }
131
132    // Step 2: Check incoming edges for structural (Defines/Contains)
133    let incoming = snapshot.edges().edges_to(node_id);
134    let has_structural = incoming
135        .iter()
136        .any(|e| matches!(&e.kind, EdgeKind::Defines | EdgeKind::Contains));
137
138    if has_structural {
139        return SymbolClassification::Declaration;
140    }
141
142    // Step 3: No structural incoming edges → Reference
143    if !incoming.is_empty() {
144        return SymbolClassification::Reference;
145    }
146
147    // Step 4: No incoming edges at all — could be a root declaration or orphan
148    // If the node has outgoing Defines/Contains edges, it's likely a root module/declaration
149    let outgoing = snapshot.edges().edges_from(node_id);
150    let has_outgoing_structural = outgoing
151        .iter()
152        .any(|e| matches!(&e.kind, EdgeKind::Defines | EdgeKind::Contains));
153
154    if has_outgoing_structural {
155        return SymbolClassification::Declaration;
156    }
157
158    SymbolClassification::Unknown
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164    use crate::graph::node::Language;
165    use crate::graph::unified::concurrent::CodeGraph;
166    use crate::graph::unified::file::FileId;
167    use crate::graph::unified::storage::arena::NodeEntry;
168
169    struct TestGraph {
170        graph: CodeGraph,
171        file_id: Option<FileId>,
172    }
173
174    impl TestGraph {
175        fn new() -> Self {
176            Self {
177                graph: CodeGraph::new(),
178                file_id: None,
179            }
180        }
181
182        fn ensure_file_id(&mut self) -> FileId {
183            if let Some(fid) = self.file_id {
184                return fid;
185            }
186            let file_path = std::path::PathBuf::from("/bind-tests/test.rs");
187            let fid = self
188                .graph
189                .files_mut()
190                .register_with_language(&file_path, Some(Language::Rust))
191                .unwrap();
192            self.file_id = Some(fid);
193            fid
194        }
195
196        fn add_node(&mut self, name: &str, kind: NodeKind) -> NodeId {
197            let file_id = self.ensure_file_id();
198            let name_id = self.graph.strings_mut().intern(name).unwrap();
199            let qn_id = self
200                .graph
201                .strings_mut()
202                .intern(&format!("test::{name}"))
203                .unwrap();
204
205            let entry = NodeEntry::new(kind, name_id, file_id)
206                .with_qualified_name(qn_id)
207                .with_location(1, 0, 10, 0);
208
209            let node_id = self.graph.nodes_mut().alloc(entry).unwrap();
210            self.graph
211                .indices_mut()
212                .add(node_id, kind, name_id, Some(qn_id), file_id);
213            node_id
214        }
215
216        fn add_edge(&mut self, source: NodeId, target: NodeId, kind: EdgeKind) {
217            let file_id = self.ensure_file_id();
218            self.graph
219                .edges_mut()
220                .add_edge(source, target, kind, file_id);
221        }
222
223        fn snapshot(&self) -> GraphSnapshot {
224            self.graph.snapshot()
225        }
226    }
227
228    #[test]
229    fn declaration_classification() {
230        let mut tg = TestGraph::new();
231        let module_node = tg.add_node("my_module", NodeKind::Module);
232        let func_node = tg.add_node("my_func", NodeKind::Function);
233        tg.add_edge(module_node, func_node, EdgeKind::Defines);
234
235        let snapshot = tg.snapshot();
236        let result = BindingQuery::new("my_func").resolve(&snapshot);
237
238        assert!(!result.bindings.is_empty(), "expected at least one binding");
239        let binding = result
240            .bindings
241            .iter()
242            .find(|b| b.node_id == func_node)
243            .expect("expected binding for func_node");
244        assert_eq!(binding.classification, SymbolClassification::Declaration);
245        assert_eq!(binding.kind, NodeKind::Function);
246    }
247
248    #[test]
249    fn reference_classification_callsite() {
250        let mut tg = TestGraph::new();
251        let _call_node = tg.add_node("some_call", NodeKind::CallSite);
252
253        let snapshot = tg.snapshot();
254        let result = BindingQuery::new("some_call").resolve(&snapshot);
255
256        assert!(!result.bindings.is_empty());
257        assert_eq!(
258            result.bindings[0].classification,
259            SymbolClassification::Reference
260        );
261    }
262
263    #[test]
264    fn import_classification() {
265        let mut tg = TestGraph::new();
266        let _import_node = tg.add_node("imported_sym", NodeKind::Import);
267
268        let snapshot = tg.snapshot();
269        let result = BindingQuery::new("imported_sym").resolve(&snapshot);
270
271        assert!(!result.bindings.is_empty());
272        assert_eq!(
273            result.bindings[0].classification,
274            SymbolClassification::Import
275        );
276    }
277
278    #[test]
279    fn export_direct_classification() {
280        let mut tg = TestGraph::new();
281        let _export_node = tg.add_node("exported_sym", NodeKind::Export);
282
283        let snapshot = tg.snapshot();
284        let result = BindingQuery::new("exported_sym").resolve(&snapshot);
285
286        assert!(!result.bindings.is_empty());
287        // Direct export without re-export edges → Import classification
288        assert_eq!(
289            result.bindings[0].classification,
290            SymbolClassification::Import
291        );
292    }
293
294    #[test]
295    fn export_reexport_ambiguous() {
296        let mut tg = TestGraph::new();
297        let source = tg.add_node("source_mod", NodeKind::Module);
298        let export_node = tg.add_node("reexported", NodeKind::Export);
299        tg.add_edge(
300            source,
301            export_node,
302            EdgeKind::Exports {
303                kind: ExportKind::Reexport,
304                alias: None,
305            },
306        );
307
308        let snapshot = tg.snapshot();
309        let result = BindingQuery::new("reexported").resolve(&snapshot);
310
311        assert!(!result.bindings.is_empty());
312        let binding = result
313            .bindings
314            .iter()
315            .find(|b| b.node_id == export_node)
316            .expect("expected binding for export_node");
317        assert_eq!(binding.classification, SymbolClassification::Ambiguous);
318    }
319
320    #[test]
321    fn not_found_result() {
322        let tg = TestGraph::new();
323        let snapshot = tg.snapshot();
324        let result = BindingQuery::new("nonexistent_symbol_xyz").resolve(&snapshot);
325
326        assert!(result.bindings.is_empty());
327        assert_eq!(result.outcome, SymbolResolutionOutcome::NotFound);
328    }
329
330    #[test]
331    fn declaration_via_contains_edge() {
332        let mut tg = TestGraph::new();
333        let class_node = tg.add_node("MyClass", NodeKind::Class);
334        let method_node = tg.add_node("my_method", NodeKind::Method);
335        tg.add_edge(class_node, method_node, EdgeKind::Contains);
336
337        let snapshot = tg.snapshot();
338        let result = BindingQuery::new("my_method").resolve(&snapshot);
339
340        assert!(!result.bindings.is_empty());
341        let binding = result
342            .bindings
343            .iter()
344            .find(|b| b.node_id == method_node)
345            .expect("expected binding for method_node");
346        assert_eq!(binding.classification, SymbolClassification::Declaration);
347    }
348
349    #[test]
350    fn root_declaration_with_outgoing_defines() {
351        let mut tg = TestGraph::new();
352        let module_node = tg.add_node("root_mod", NodeKind::Module);
353        let child = tg.add_node("child_func", NodeKind::Function);
354        tg.add_edge(module_node, child, EdgeKind::Defines);
355
356        let snapshot = tg.snapshot();
357        let result = BindingQuery::new("root_mod").resolve(&snapshot);
358
359        assert!(!result.bindings.is_empty());
360        let binding = result
361            .bindings
362            .iter()
363            .find(|b| b.node_id == module_node)
364            .expect("expected binding for module_node");
365        // Root module has no incoming edges but has outgoing Defines → Declaration
366        assert_eq!(binding.classification, SymbolClassification::Declaration);
367    }
368}