1use crate::graph::unified::concurrent::CodeGraph;
13use crate::graph::unified::node::NodeKind as UnifiedNodeKind;
14use std::path::PathBuf;
15
16pub struct GraphQueryAdapter<'a> {
20 graph: &'a CodeGraph,
21}
22
23#[derive(Debug, Clone)]
31pub struct ScopeInfo {
32 pub node_id: crate::graph::unified::node::NodeId,
34 pub scope_type: String,
36 pub name: String,
38}
39
40#[derive(Debug, Clone)]
42pub struct ReferenceInfo {
43 pub source_node_id: crate::graph::unified::node::NodeId,
45 pub reference_kind: crate::graph::unified::edge::EdgeKind,
47 pub file_path: PathBuf,
49 pub line: usize,
51}
52
53impl<'a> GraphQueryAdapter<'a> {
54 #[must_use]
56 pub fn new(graph: &'a CodeGraph) -> Self {
57 Self { graph }
58 }
59
60 #[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 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 #[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 while let Some(parent) = self.get_parent_scope(current) {
110 ancestors.push(parent);
111 current = parent;
112 }
113
114 ancestors
115 }
116
117 #[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 #[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 #[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 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 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 #[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 #[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 if let Some(string_id) = strings.get(symbol_name) {
264 for &node_id in indices.by_name(string_id) {
266 all_refs.extend(self.get_references_to(node_id));
267 }
268 }
269
270 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 #[must_use]
285 pub fn graph(&self) -> &CodeGraph {
286 self.graph
287 }
288}
289
290fn 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 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
318 | UnifiedNodeKind::TypeParameter
319 | UnifiedNodeKind::Annotation
320 | UnifiedNodeKind::AnnotationValue
321 | UnifiedNodeKind::LambdaTarget
322 | UnifiedNodeKind::JavaModule
323 | UnifiedNodeKind::EnumConstant => kind.as_str().to_lowercase(),
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330 use crate::graph::unified::concurrent::CodeGraph;
331 use crate::graph::unified::edge::BidirectionalEdgeStore;
332 use crate::graph::unified::edge::{EdgeKind, FfiConvention};
333 use crate::graph::unified::node::NodeKind;
334 use crate::graph::unified::storage::{AuxiliaryIndices, FileRegistry};
335 use crate::graph::unified::storage::{NodeArena, NodeEntry, StringInterner};
336 use std::path::Path;
337
338 fn build_graph_with_ffi_edge() -> (
342 CodeGraph,
343 crate::graph::unified::node::NodeId,
344 crate::graph::unified::node::NodeId,
345 ) {
346 let mut arena = NodeArena::new();
347 let edges = BidirectionalEdgeStore::new();
348 let mut strings = StringInterner::new();
349 let mut files = FileRegistry::new();
350 let mut indices = AuxiliaryIndices::new();
351
352 let caller_name = strings.intern("caller_fn").unwrap();
353 let target_name = strings.intern("ffi_target").unwrap();
354 let file_id = files.register(Path::new("test.r")).unwrap();
355
356 let caller_id = arena
357 .alloc(NodeEntry {
358 kind: NodeKind::Function,
359 name: caller_name,
360 file: file_id,
361 start_byte: 0,
362 end_byte: 100,
363 start_line: 1,
364 start_column: 0,
365 end_line: 5,
366 end_column: 0,
367 signature: None,
368 doc: None,
369 qualified_name: None,
370 visibility: None,
371 is_async: false,
372 is_static: false,
373 is_unsafe: false,
374 body_hash: None,
375 })
376 .unwrap();
377
378 let target_id = arena
379 .alloc(NodeEntry {
380 kind: NodeKind::Function,
381 name: target_name,
382 file: file_id,
383 start_byte: 200,
384 end_byte: 300,
385 start_line: 10,
386 start_column: 0,
387 end_line: 15,
388 end_column: 0,
389 signature: None,
390 doc: None,
391 qualified_name: None,
392 visibility: None,
393 is_async: false,
394 is_static: false,
395 is_unsafe: false,
396 body_hash: None,
397 })
398 .unwrap();
399
400 indices.add(caller_id, NodeKind::Function, caller_name, None, file_id);
401 indices.add(target_id, NodeKind::Function, target_name, None, file_id);
402
403 edges.add_edge(
404 caller_id,
405 target_id,
406 EdgeKind::FfiCall {
407 convention: FfiConvention::C,
408 },
409 file_id,
410 );
411
412 let graph = CodeGraph::from_components(
413 arena,
414 edges,
415 strings,
416 files,
417 indices,
418 crate::graph::unified::NodeMetadataStore::new(),
419 );
420 (graph, caller_id, target_id)
421 }
422
423 #[test]
424 fn test_ffi_call_edge_in_get_references_to() {
425 let (graph, caller_id, target_id) = build_graph_with_ffi_edge();
426 let adapter = GraphQueryAdapter::new(&graph);
427
428 let refs = adapter.get_references_to(target_id);
429 assert_eq!(
430 refs.len(),
431 1,
432 "FfiCall edge should be included in references"
433 );
434 assert_eq!(refs[0].source_node_id, caller_id);
435 assert!(
436 matches!(
437 &refs[0].reference_kind,
438 EdgeKind::FfiCall {
439 convention: FfiConvention::C
440 }
441 ),
442 "reference kind should be FfiCall with C convention"
443 );
444 }
445
446 #[test]
447 fn test_ffi_call_edge_in_node_has_references() {
448 let (graph, _caller_id, target_id) = build_graph_with_ffi_edge();
449 let adapter = GraphQueryAdapter::new(&graph);
450
451 assert!(
452 adapter.node_has_references(target_id),
453 "node_has_references should return true for FfiCall target"
454 );
455 }
456}