Skip to main content

sqry_core/graph/unified/
query_adapter.rs

1//! Query adapter for unified graph.
2//!
3//! This module provides query helpers for the unified graph (Arena+CSR storage)
4//! used by graph-native query evaluation.
5//!
6//! # Design
7//!
8//! The adapter provides:
9//! - `GraphQueryAdapter`: A thin wrapper around `CodeGraph`
10//! - Scope and reference helpers for graph-native queries
11
12use crate::graph::unified::concurrent::CodeGraph;
13use crate::graph::unified::node::NodeKind as UnifiedNodeKind;
14use std::path::PathBuf;
15
16/// Query adapter for the unified graph.
17///
18/// Wraps a `CodeGraph` and exposes query-focused helpers.
19pub struct GraphQueryAdapter<'a> {
20    graph: &'a CodeGraph,
21}
22
23// ============================================================================
24// Scope and Reference Info Types (v2.0.0+)
25// ============================================================================
26
27/// Information about a scope/container in the graph.
28///
29/// Used by scope predicates (`scope.type`, `scope.name`, `scope.parent`, `scope.ancestor`).
30#[derive(Debug, Clone)]
31pub struct ScopeInfo {
32    /// The node ID of this scope
33    pub node_id: crate::graph::unified::node::NodeId,
34    /// Scope type (derived from `NodeKind`, e.g., "class", "function", "module")
35    pub scope_type: String,
36    /// Scope name
37    pub name: String,
38}
39
40/// Information about a reference to a symbol.
41#[derive(Debug, Clone)]
42pub struct ReferenceInfo {
43    /// The node ID of the referencing symbol
44    pub source_node_id: crate::graph::unified::node::NodeId,
45    /// The kind of reference (Calls, References, Imports, etc.)
46    pub reference_kind: crate::graph::unified::edge::EdgeKind,
47    /// File path where the reference occurs
48    pub file_path: PathBuf,
49    /// Line number of the reference
50    pub line: usize,
51}
52
53impl<'a> GraphQueryAdapter<'a> {
54    /// Creates a new adapter for the given graph.
55    #[must_use]
56    pub fn new(graph: &'a CodeGraph) -> Self {
57        Self { graph }
58    }
59
60    // ========================================================================
61    // Scope Query Methods (v2.0.0+)
62    // ========================================================================
63
64    /// Get the parent scope (container) of a node.
65    ///
66    /// Finds the container by looking for an incoming `Contains` edge.
67    /// For example, a method's parent scope is its containing class.
68    ///
69    /// # Returns
70    ///
71    /// `Some(parent_node_id)` if the node has a parent container, `None` otherwise.
72    #[must_use]
73    pub fn get_parent_scope(
74        &self,
75        node_id: crate::graph::unified::node::NodeId,
76    ) -> Option<crate::graph::unified::node::NodeId> {
77        use crate::graph::unified::edge::EdgeKind;
78
79        let edges = self.graph.edges();
80
81        // Find incoming Contains edge (parent -> this node)
82        for edge in edges.edges_to(node_id) {
83            if matches!(edge.kind, EdgeKind::Contains) {
84                return Some(edge.source);
85            }
86        }
87
88        None
89    }
90
91    /// Get all ancestor scopes of a node (parent chain up to root).
92    ///
93    /// Returns the chain of containing scopes from immediate parent to top-level.
94    /// For example, for a method in a nested class:
95    /// `[immediate_class, outer_class, module]`
96    ///
97    /// # Returns
98    ///
99    /// Vector of ancestor node IDs, from immediate parent to root.
100    #[must_use]
101    pub fn get_ancestor_scopes(
102        &self,
103        node_id: crate::graph::unified::node::NodeId,
104    ) -> Vec<crate::graph::unified::node::NodeId> {
105        let mut ancestors = Vec::new();
106        let mut current = node_id;
107
108        // Walk up the parent chain
109        while let Some(parent) = self.get_parent_scope(current) {
110            ancestors.push(parent);
111            current = parent;
112        }
113
114        ancestors
115    }
116
117    /// Get scope information for a node.
118    ///
119    /// Returns the scope type (derived from `NodeKind`) and name.
120    ///
121    /// # Returns
122    ///
123    /// `Some(ScopeInfo)` if the node exists, `None` otherwise.
124    #[must_use]
125    pub fn get_scope_info(
126        &self,
127        node_id: crate::graph::unified::node::NodeId,
128    ) -> Option<ScopeInfo> {
129        let arena = self.graph.nodes();
130        let strings = self.graph.strings();
131
132        let entry = arena.get(node_id)?;
133        let name = strings.resolve(entry.name)?.to_string();
134        let scope_type = node_kind_to_scope_type(entry.kind);
135
136        Some(ScopeInfo {
137            node_id,
138            scope_type,
139            name,
140        })
141    }
142
143    /// Get the containing scope info for a node (its parent's scope info).
144    ///
145    /// This is a convenience method that combines `get_parent_scope` and `get_scope_info`.
146    ///
147    /// # Returns
148    ///
149    /// `Some(ScopeInfo)` of the parent container, `None` if no parent exists.
150    #[must_use]
151    pub fn get_containing_scope_info(
152        &self,
153        node_id: crate::graph::unified::node::NodeId,
154    ) -> Option<ScopeInfo> {
155        let parent_id = self.get_parent_scope(node_id)?;
156        self.get_scope_info(parent_id)
157    }
158
159    // ========================================================================
160    // Reference Query Methods (v2.0.0+)
161    // ========================================================================
162
163    /// Get all references to a node.
164    ///
165    /// Finds incoming `References`, `Calls`, `Imports`, and `FfiCall` edges.
166    ///
167    /// # Returns
168    ///
169    /// Vector of `ReferenceInfo` for all references to this node.
170    #[must_use]
171    pub fn get_references_to(
172        &self,
173        node_id: crate::graph::unified::node::NodeId,
174    ) -> Vec<ReferenceInfo> {
175        use crate::graph::unified::edge::EdgeKind;
176
177        let edges = self.graph.edges();
178        let arena = self.graph.nodes();
179        let files = self.graph.files();
180
181        let mut refs = Vec::new();
182
183        for edge in edges.edges_to(node_id) {
184            // Include References, Calls, Imports, and FfiCall edges
185            let is_reference = matches!(
186                &edge.kind,
187                EdgeKind::References
188                    | EdgeKind::Calls { .. }
189                    | EdgeKind::Imports { .. }
190                    | EdgeKind::FfiCall { .. }
191            );
192
193            if is_reference {
194                // Get source node info for location
195                let (file_path, line) = arena
196                    .get(edge.source)
197                    .map(|entry| {
198                        let path = files
199                            .resolve(entry.file)
200                            .map(|s| PathBuf::from(s.as_ref()))
201                            .unwrap_or_default();
202                        (path, entry.start_line as usize)
203                    })
204                    .unwrap_or_default();
205
206                refs.push(ReferenceInfo {
207                    source_node_id: edge.source,
208                    reference_kind: edge.kind.clone(),
209                    file_path,
210                    line,
211                });
212            }
213        }
214
215        refs
216    }
217
218    /// Check if a node has any references (is referenced by other symbols).
219    ///
220    /// This is more efficient than `get_references_to` when you only need
221    /// to check existence, not enumerate all references.
222    ///
223    /// # Returns
224    ///
225    /// `true` if the node has at least one incoming reference edge.
226    #[must_use]
227    pub fn node_has_references(&self, node_id: crate::graph::unified::node::NodeId) -> bool {
228        use crate::graph::unified::edge::EdgeKind;
229
230        let edges = self.graph.edges();
231
232        edges.edges_to(node_id).iter().any(|edge| {
233            matches!(
234                &edge.kind,
235                EdgeKind::References
236                    | EdgeKind::Calls { .. }
237                    | EdgeKind::Imports { .. }
238                    | EdgeKind::FfiCall { .. }
239            )
240        })
241    }
242
243    /// Find nodes by name and return their references.
244    ///
245    /// This is useful for implementing `references:symbol_name` predicate.
246    ///
247    /// # Arguments
248    ///
249    /// * `symbol_name` - The name to search for (exact match or suffix match with `::`)
250    ///
251    /// # Returns
252    ///
253    /// Vector of `ReferenceInfo` for all references to matching symbols.
254    #[must_use]
255    pub fn find_references_to_symbol(&self, symbol_name: &str) -> Vec<ReferenceInfo> {
256        let indices = self.graph.indices();
257        let arena = self.graph.nodes();
258        let strings = self.graph.strings();
259
260        let mut all_refs = Vec::new();
261
262        // Try to find the exact StringId for this symbol name
263        if let Some(string_id) = strings.get(symbol_name) {
264            // Look up nodes by the interned name
265            for &node_id in indices.by_name(string_id) {
266                all_refs.extend(self.get_references_to(node_id));
267            }
268        }
269
270        // Also check for qualified names ending with ::symbol_name
271        let suffix = format!("::{symbol_name}");
272        for (node_id, entry) in arena.iter() {
273            if let Some(name) = strings.resolve(entry.name)
274                && name.ends_with(&suffix)
275            {
276                all_refs.extend(self.get_references_to(node_id));
277            }
278        }
279
280        all_refs
281    }
282
283    /// Returns a reference to the underlying graph.
284    #[must_use]
285    pub fn graph(&self) -> &CodeGraph {
286        self.graph
287    }
288}
289
290/// Convert `NodeKind` to scope type string for predicate matching.
291fn node_kind_to_scope_type(kind: UnifiedNodeKind) -> String {
292    match kind {
293        UnifiedNodeKind::Function | UnifiedNodeKind::Test => "function".to_string(),
294        UnifiedNodeKind::Method => "method".to_string(),
295        UnifiedNodeKind::Class | UnifiedNodeKind::Service => "class".to_string(),
296        UnifiedNodeKind::Interface | UnifiedNodeKind::Trait => "interface".to_string(),
297        UnifiedNodeKind::Struct => "struct".to_string(),
298        UnifiedNodeKind::Enum => "enum".to_string(),
299        UnifiedNodeKind::Module => "module".to_string(),
300        UnifiedNodeKind::Macro => "macro".to_string(),
301        UnifiedNodeKind::Component => "component".to_string(),
302        UnifiedNodeKind::Resource | UnifiedNodeKind::Endpoint => "resource".to_string(),
303        // Non-container types
304        UnifiedNodeKind::Variable
305        | UnifiedNodeKind::Constant
306        | UnifiedNodeKind::Parameter
307        | UnifiedNodeKind::Property
308        | UnifiedNodeKind::EnumVariant
309        | UnifiedNodeKind::Type
310        | UnifiedNodeKind::Import
311        | UnifiedNodeKind::Export
312        | UnifiedNodeKind::CallSite
313        | UnifiedNodeKind::Other
314        | UnifiedNodeKind::Lifetime
315        | UnifiedNodeKind::StyleRule
316        | UnifiedNodeKind::StyleAtRule
317        | UnifiedNodeKind::StyleVariable => kind.as_str().to_lowercase(),
318    }
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324    use crate::graph::unified::concurrent::CodeGraph;
325    use crate::graph::unified::edge::BidirectionalEdgeStore;
326    use crate::graph::unified::edge::{EdgeKind, FfiConvention};
327    use crate::graph::unified::node::NodeKind;
328    use crate::graph::unified::storage::{AuxiliaryIndices, FileRegistry};
329    use crate::graph::unified::storage::{NodeArena, NodeEntry, StringInterner};
330    use std::path::Path;
331
332    /// Build a minimal `CodeGraph` with two function nodes connected by an `FfiCall` edge.
333    ///
334    /// Graph: `caller --FfiCall(C)--> ffi_target`
335    fn build_graph_with_ffi_edge() -> (
336        CodeGraph,
337        crate::graph::unified::node::NodeId,
338        crate::graph::unified::node::NodeId,
339    ) {
340        let mut arena = NodeArena::new();
341        let edges = BidirectionalEdgeStore::new();
342        let mut strings = StringInterner::new();
343        let mut files = FileRegistry::new();
344        let mut indices = AuxiliaryIndices::new();
345
346        let caller_name = strings.intern("caller_fn").unwrap();
347        let target_name = strings.intern("ffi_target").unwrap();
348        let file_id = files.register(Path::new("test.r")).unwrap();
349
350        let caller_id = arena
351            .alloc(NodeEntry {
352                kind: NodeKind::Function,
353                name: caller_name,
354                file: file_id,
355                start_byte: 0,
356                end_byte: 100,
357                start_line: 1,
358                start_column: 0,
359                end_line: 5,
360                end_column: 0,
361                signature: None,
362                doc: None,
363                qualified_name: None,
364                visibility: None,
365                is_async: false,
366                is_static: false,
367                is_unsafe: false,
368                body_hash: None,
369            })
370            .unwrap();
371
372        let target_id = arena
373            .alloc(NodeEntry {
374                kind: NodeKind::Function,
375                name: target_name,
376                file: file_id,
377                start_byte: 200,
378                end_byte: 300,
379                start_line: 10,
380                start_column: 0,
381                end_line: 15,
382                end_column: 0,
383                signature: None,
384                doc: None,
385                qualified_name: None,
386                visibility: None,
387                is_async: false,
388                is_static: false,
389                is_unsafe: false,
390                body_hash: None,
391            })
392            .unwrap();
393
394        indices.add(caller_id, NodeKind::Function, caller_name, None, file_id);
395        indices.add(target_id, NodeKind::Function, target_name, None, file_id);
396
397        edges.add_edge(
398            caller_id,
399            target_id,
400            EdgeKind::FfiCall {
401                convention: FfiConvention::C,
402            },
403            file_id,
404        );
405
406        let graph = CodeGraph::from_components(arena, edges, strings, files, indices);
407        (graph, caller_id, target_id)
408    }
409
410    #[test]
411    fn test_ffi_call_edge_in_get_references_to() {
412        let (graph, caller_id, target_id) = build_graph_with_ffi_edge();
413        let adapter = GraphQueryAdapter::new(&graph);
414
415        let refs = adapter.get_references_to(target_id);
416        assert_eq!(
417            refs.len(),
418            1,
419            "FfiCall edge should be included in references"
420        );
421        assert_eq!(refs[0].source_node_id, caller_id);
422        assert!(
423            matches!(
424                &refs[0].reference_kind,
425                EdgeKind::FfiCall {
426                    convention: FfiConvention::C
427                }
428            ),
429            "reference kind should be FfiCall with C convention"
430        );
431    }
432
433    #[test]
434    fn test_ffi_call_edge_in_node_has_references() {
435        let (graph, _caller_id, target_id) = build_graph_with_ffi_edge();
436        let adapter = GraphQueryAdapter::new(&graph);
437
438        assert!(
439            adapter.node_has_references(target_id),
440            "node_has_references should return true for FfiCall target"
441        );
442    }
443}