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 => 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 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}