Skip to main content

sqry_core/graph/
call_sites.rs

1//! Normalized representation of call edges returned by query helpers.
2//!
3//! The [`CallSite`] struct captures the essential details about a call edge,
4//! including the caller/callee identifiers, source span, and metadata that
5//! records detection provenance plus language-specific extras.
6
7use super::edge::{CodeEdge, DetectionMethod, EdgeId};
8use super::node::{NodeId, Span};
9
10/// Canonical call-site information returned by the query layer.
11#[derive(Debug, Clone, PartialEq)]
12pub struct CallSite {
13    /// Fully-qualified caller symbol.
14    pub caller: NodeId,
15    /// Fully-qualified callee symbol.
16    pub callee: NodeId,
17    /// Stable identifier for the edge that produced this call site.
18    pub edge_id: EdgeId,
19    /// Source span of the call expression, if available.
20    pub span: Option<Span>,
21    /// Metadata describing how the call was detected.
22    pub metadata: CallSiteMetadata,
23}
24
25/// Detection metadata bundled with a [`CallSite`].
26#[derive(Debug, Clone, PartialEq)]
27pub struct CallSiteMetadata {
28    /// Confidence score (0.0-1.0) for this call site.
29    pub confidence: f32,
30    /// Detection strategy that produced this call (`DetectionMethod` mirrors [`EdgeMetadata`](crate::graph::EdgeMetadata)).
31    pub detection_method: DetectionMethod,
32    /// Human readable explanation (e.g. `"direct call"`, `"axios.post detector"`).
33    pub detector_hint: Option<String>,
34    /// Language-specific extras (argument count, template info, async flags, etc.).
35    pub extras: CallSiteExtras,
36}
37
38/// Language-specific payloads attached to [`CallSiteMetadata`].
39#[derive(Debug, Clone, PartialEq)]
40pub enum CallSiteExtras {
41    /// No extra metadata for this call site.
42    None,
43    /// Additional details for C++ call sites.
44    Cpp {
45        /// Number of arguments passed in the call expression.
46        argument_count: usize,
47        /// Whether the call came from a template instantiation.
48        is_template_instantiation: bool,
49    },
50    /// Additional details for JavaScript/TypeScript call sites.
51    JavaScript {
52        /// Whether the call occurs inside an async function.
53        is_async: bool,
54        /// Whether the call expression uses the `await` keyword.
55        uses_await: bool,
56        /// HTTP call metadata (fetch, axios, etc.) for cross-language edges.
57        http_method: Option<String>,
58        /// HTTP endpoint for cross-language edges.
59        http_endpoint: Option<String>,
60    },
61    /// Additional details for Python call sites.
62    Python {
63        /// Whether the function containing the call is declared `async`.
64        is_async: bool,
65        /// Whether the call is to a method (vs a standalone function).
66        is_method: bool,
67        /// Whether the function has decorators applied (e.g., @staticmethod).
68        uses_decorator: bool,
69    },
70    /// Additional details for Rust call sites.
71    Rust {
72        /// Whether the call occurs inside an async function.
73        is_async: bool,
74        /// Whether the call expression uses `.await`.
75        uses_await: bool,
76        /// Whether the containing function is marked `unsafe`.
77        is_unsafe: bool,
78        /// Whether the call is a method call (`receiver.method()` or `self.method()`).
79        is_method: bool,
80        /// Whether the call occurs inside an `unsafe {}` block.
81        is_unsafe_block: bool,
82    },
83    /// Additional details for Java call sites.
84    Java {
85        /// Whether the call occurs inside a static method.
86        is_static: bool,
87    },
88    /// Additional details for Go call sites.
89    Go {
90        /// Whether this call is launched as a goroutine (`go foo()`).
91        is_goroutine: bool,
92        /// Whether this call is deferred (`defer foo()`).
93        is_deferred: bool,
94        /// Whether this call is to a Go builtin function (make, new, len, etc.).
95        is_builtin: bool,
96        /// Whether this call is a method call (`receiver.Method()`).
97        is_method: bool,
98    },
99    /// Additional details for C call sites.
100    C {
101        /// Whether the call is likely through a function pointer.
102        is_function_pointer: bool,
103    },
104    /// Additional details for C# call sites.
105    CSharp {
106        /// Whether the call occurs inside an async method.
107        is_async: bool,
108        /// Whether the call uses the `await` keyword.
109        uses_await: bool,
110        /// Whether the call is to a property (synthetic getter/setter).
111        is_property_access: bool,
112    },
113    /// Additional details for Lua call sites.
114    Lua {
115        /// Whether the call is to a method (colon syntax).
116        is_method: bool,
117    },
118    /// Additional details for Perl call sites.
119    Perl {
120        /// Whether the call is to a method.
121        is_method: bool,
122    },
123    /// Additional details for Shell call sites.
124    Shell {
125        /// Whether the call is a built-in command.
126        is_builtin: bool,
127    },
128    /// Additional details for Groovy call sites.
129    Groovy {
130        /// Whether the call is to a closure.
131        is_closure: bool,
132    },
133}
134
135impl CallSite {
136    /// Construct a [`CallSite`] from a [`CodeEdge`] and the provided extras.
137    #[must_use]
138    pub fn from_edge(edge: &CodeEdge, extras: CallSiteExtras) -> Self {
139        Self {
140            caller: edge.from.clone(),
141            callee: edge.to.clone(),
142            edge_id: edge.id,
143            span: edge.metadata.span,
144            metadata: CallSiteMetadata {
145                confidence: edge.metadata.confidence,
146                detection_method: edge.metadata.detection_method,
147                detector_hint: edge.metadata.reason.clone(),
148                extras,
149            },
150        }
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use crate::graph::edge::{EdgeKind, EdgeMetadata};
158    use crate::graph::node::{Language, Position};
159
160    fn make_test_edge() -> CodeEdge {
161        CodeEdge {
162            id: EdgeId::new(),
163            from: NodeId::new(Language::Rust, "src/main.rs", "main"),
164            to: NodeId::new(Language::Rust, "src/lib.rs", "helper"),
165            kind: EdgeKind::Call {
166                argument_count: 2,
167                is_async: false,
168            },
169            metadata: EdgeMetadata {
170                span: Some(Span::new(Position::new(10, 5), Position::new(10, 20))),
171                confidence: 0.95,
172                detection_method: DetectionMethod::ASTAnalysis,
173                reason: Some("direct call".to_string()),
174                caller_identity: None,
175                callee_identity: None,
176            },
177        }
178    }
179
180    // CallSite tests
181    #[test]
182    fn test_call_site_from_edge() {
183        let edge = make_test_edge();
184        let call_site = CallSite::from_edge(&edge, CallSiteExtras::None);
185
186        assert_eq!(call_site.caller, edge.from);
187        assert_eq!(call_site.callee, edge.to);
188        assert_eq!(call_site.edge_id, edge.id);
189        assert_eq!(call_site.span, edge.metadata.span);
190        assert!((call_site.metadata.confidence - 0.95).abs() < f32::EPSILON);
191        assert_eq!(
192            call_site.metadata.detection_method,
193            DetectionMethod::ASTAnalysis
194        );
195        assert_eq!(
196            call_site.metadata.detector_hint,
197            Some("direct call".to_string())
198        );
199    }
200
201    #[test]
202    fn test_call_site_from_edge_no_span() {
203        let mut edge = make_test_edge();
204        edge.metadata.span = None;
205        edge.metadata.reason = None;
206
207        let call_site = CallSite::from_edge(&edge, CallSiteExtras::None);
208        assert!(call_site.span.is_none());
209        assert!(call_site.metadata.detector_hint.is_none());
210    }
211
212    #[test]
213    fn test_call_site_clone() {
214        let edge = make_test_edge();
215        let call_site = CallSite::from_edge(&edge, CallSiteExtras::None);
216        let cloned = call_site.clone();
217
218        assert_eq!(call_site, cloned);
219    }
220
221    #[test]
222    fn test_call_site_debug() {
223        let edge = make_test_edge();
224        let call_site = CallSite::from_edge(&edge, CallSiteExtras::None);
225        let debug_str = format!("{:?}", call_site);
226
227        assert!(debug_str.contains("CallSite"));
228        assert!(debug_str.contains("caller"));
229        assert!(debug_str.contains("callee"));
230    }
231
232    // CallSiteMetadata tests
233    #[test]
234    fn test_call_site_metadata_eq() {
235        let meta1 = CallSiteMetadata {
236            confidence: 0.9,
237            detection_method: DetectionMethod::ASTAnalysis,
238            detector_hint: None,
239            extras: CallSiteExtras::None,
240        };
241        let meta2 = CallSiteMetadata {
242            confidence: 0.9,
243            detection_method: DetectionMethod::ASTAnalysis,
244            detector_hint: None,
245            extras: CallSiteExtras::None,
246        };
247        assert_eq!(meta1, meta2);
248    }
249
250    #[test]
251    fn test_call_site_metadata_ne() {
252        let meta1 = CallSiteMetadata {
253            confidence: 0.9,
254            detection_method: DetectionMethod::ASTAnalysis,
255            detector_hint: None,
256            extras: CallSiteExtras::None,
257        };
258        let meta2 = CallSiteMetadata {
259            confidence: 0.8,
260            detection_method: DetectionMethod::ASTAnalysis,
261            detector_hint: None,
262            extras: CallSiteExtras::None,
263        };
264        assert_ne!(meta1, meta2);
265    }
266
267    // CallSiteExtras variants tests
268    #[test]
269    fn test_call_site_extras_none() {
270        let extras = CallSiteExtras::None;
271        assert_eq!(extras, CallSiteExtras::None);
272    }
273
274    #[test]
275    fn test_call_site_extras_cpp() {
276        let extras = CallSiteExtras::Cpp {
277            argument_count: 3,
278            is_template_instantiation: true,
279        };
280        if let CallSiteExtras::Cpp {
281            argument_count,
282            is_template_instantiation,
283        } = extras
284        {
285            assert_eq!(argument_count, 3);
286            assert!(is_template_instantiation);
287        } else {
288            panic!("Expected Cpp variant");
289        }
290    }
291
292    #[test]
293    fn test_call_site_extras_javascript() {
294        let extras = CallSiteExtras::JavaScript {
295            is_async: true,
296            uses_await: true,
297            http_method: Some("POST".to_string()),
298            http_endpoint: Some("/api/users".to_string()),
299        };
300        if let CallSiteExtras::JavaScript {
301            is_async,
302            uses_await,
303            http_method,
304            http_endpoint,
305        } = extras
306        {
307            assert!(is_async);
308            assert!(uses_await);
309            assert_eq!(http_method, Some("POST".to_string()));
310            assert_eq!(http_endpoint, Some("/api/users".to_string()));
311        } else {
312            panic!("Expected JavaScript variant");
313        }
314    }
315
316    #[test]
317    fn test_call_site_extras_python() {
318        let extras = CallSiteExtras::Python {
319            is_async: true,
320            is_method: true,
321            uses_decorator: true,
322        };
323        if let CallSiteExtras::Python {
324            is_async,
325            is_method,
326            uses_decorator,
327        } = extras
328        {
329            assert!(is_async);
330            assert!(is_method);
331            assert!(uses_decorator);
332        } else {
333            panic!("Expected Python variant");
334        }
335    }
336
337    #[test]
338    fn test_call_site_extras_rust() {
339        let extras = CallSiteExtras::Rust {
340            is_async: true,
341            uses_await: true,
342            is_unsafe: false,
343            is_method: true,
344            is_unsafe_block: false,
345        };
346        if let CallSiteExtras::Rust {
347            is_async,
348            uses_await,
349            is_unsafe,
350            is_method,
351            is_unsafe_block,
352        } = extras
353        {
354            assert!(is_async);
355            assert!(uses_await);
356            assert!(!is_unsafe);
357            assert!(is_method);
358            assert!(!is_unsafe_block);
359        } else {
360            panic!("Expected Rust variant");
361        }
362    }
363
364    #[test]
365    fn test_call_site_extras_java() {
366        let extras = CallSiteExtras::Java { is_static: true };
367        if let CallSiteExtras::Java { is_static } = extras {
368            assert!(is_static);
369        } else {
370            panic!("Expected Java variant");
371        }
372    }
373
374    #[test]
375    fn test_call_site_extras_go() {
376        let extras = CallSiteExtras::Go {
377            is_goroutine: true,
378            is_deferred: false,
379            is_builtin: false,
380            is_method: true,
381        };
382        if let CallSiteExtras::Go {
383            is_goroutine,
384            is_deferred,
385            is_builtin,
386            is_method,
387        } = extras
388        {
389            assert!(is_goroutine);
390            assert!(!is_deferred);
391            assert!(!is_builtin);
392            assert!(is_method);
393        } else {
394            panic!("Expected Go variant");
395        }
396    }
397
398    #[test]
399    fn test_call_site_extras_c() {
400        let extras = CallSiteExtras::C {
401            is_function_pointer: true,
402        };
403        if let CallSiteExtras::C {
404            is_function_pointer,
405        } = extras
406        {
407            assert!(is_function_pointer);
408        } else {
409            panic!("Expected C variant");
410        }
411    }
412
413    #[test]
414    fn test_call_site_extras_csharp() {
415        let extras = CallSiteExtras::CSharp {
416            is_async: true,
417            uses_await: true,
418            is_property_access: false,
419        };
420        if let CallSiteExtras::CSharp {
421            is_async,
422            uses_await,
423            is_property_access,
424        } = extras
425        {
426            assert!(is_async);
427            assert!(uses_await);
428            assert!(!is_property_access);
429        } else {
430            panic!("Expected CSharp variant");
431        }
432    }
433
434    #[test]
435    fn test_call_site_extras_lua() {
436        let extras = CallSiteExtras::Lua { is_method: true };
437        if let CallSiteExtras::Lua { is_method } = extras {
438            assert!(is_method);
439        } else {
440            panic!("Expected Lua variant");
441        }
442    }
443
444    #[test]
445    fn test_call_site_extras_perl() {
446        let extras = CallSiteExtras::Perl { is_method: true };
447        if let CallSiteExtras::Perl { is_method } = extras {
448            assert!(is_method);
449        } else {
450            panic!("Expected Perl variant");
451        }
452    }
453
454    #[test]
455    fn test_call_site_extras_shell() {
456        let extras = CallSiteExtras::Shell { is_builtin: true };
457        if let CallSiteExtras::Shell { is_builtin } = extras {
458            assert!(is_builtin);
459        } else {
460            panic!("Expected Shell variant");
461        }
462    }
463
464    #[test]
465    fn test_call_site_extras_groovy() {
466        let extras = CallSiteExtras::Groovy { is_closure: true };
467        if let CallSiteExtras::Groovy { is_closure } = extras {
468            assert!(is_closure);
469        } else {
470            panic!("Expected Groovy variant");
471        }
472    }
473
474    #[test]
475    fn test_call_site_extras_clone() {
476        let extras = CallSiteExtras::Rust {
477            is_async: true,
478            uses_await: true,
479            is_unsafe: false,
480            is_method: true,
481            is_unsafe_block: false,
482        };
483        let cloned = extras.clone();
484        assert_eq!(extras, cloned);
485    }
486
487    // Integration test with different detection methods
488    #[test]
489    fn test_call_site_with_heuristic_detection() {
490        let mut edge = make_test_edge();
491        edge.metadata.detection_method = DetectionMethod::Heuristic;
492        edge.metadata.confidence = 0.7;
493
494        let call_site = CallSite::from_edge(&edge, CallSiteExtras::None);
495        assert_eq!(
496            call_site.metadata.detection_method,
497            DetectionMethod::Heuristic
498        );
499        assert!((call_site.metadata.confidence - 0.7).abs() < f32::EPSILON);
500    }
501
502    #[test]
503    fn test_call_site_with_type_inference_detection() {
504        let mut edge = make_test_edge();
505        edge.metadata.detection_method = DetectionMethod::TypeInference;
506
507        let call_site = CallSite::from_edge(&edge, CallSiteExtras::None);
508        assert_eq!(
509            call_site.metadata.detection_method,
510            DetectionMethod::TypeInference
511        );
512    }
513
514    #[test]
515    fn test_call_site_from_edge_with_cpp_extras() {
516        let edge = make_test_edge();
517        let extras = CallSiteExtras::Cpp {
518            argument_count: 5,
519            is_template_instantiation: false,
520        };
521        let call_site = CallSite::from_edge(&edge, extras.clone());
522
523        assert_eq!(call_site.metadata.extras, extras);
524    }
525}