Skip to main content

sqry_db/queries/
callers.rs

1//! `callers:X` derived query.
2//!
3//! Under the planner's relation convention (shared with the DB12 inline
4//! `relation_matches` path), `callers:X` filters a node set to those nodes
5//! whose **incoming** `Calls` edges carry a source whose name matches `X`.
6//! Reading it right-to-left: `X` appears in each result node's `callers`
7//! list — i.e. the nodes returned are ones that `X` calls.
8//!
9//! The real computation lives in
10//! [`super::relation::compute_relation_source_set`]. This module only pins
11//! the [`DerivedQuery`] identity so the sharded cache routes the result to
12//! its own slot.
13
14use std::sync::Arc;
15
16use sqry_core::graph::unified::concurrent::GraphSnapshot;
17use sqry_core::graph::unified::node::id::NodeId;
18
19use crate::QueryDb;
20use crate::query::DerivedQuery;
21
22use super::relation::{RelationKey, RelationKind, compute_relation_source_set};
23
24/// `callers:X` — filter to nodes where `X` is one of the callers.
25///
26/// # Invalidation
27///
28/// `TRACKS_EDGE_REVISION = true`: any change in the global `Calls`
29/// topology can introduce or remove callers of a given name.
30pub struct CallersQuery;
31
32impl DerivedQuery for CallersQuery {
33    type Key = RelationKey;
34    type Value = Arc<Vec<NodeId>>;
35    const QUERY_TYPE_ID: u32 = crate::queries::type_ids::CALLERS;
36    const TRACKS_EDGE_REVISION: bool = true;
37
38    fn execute(key: &RelationKey, _db: &QueryDb, snapshot: &GraphSnapshot) -> Arc<Vec<NodeId>> {
39        compute_relation_source_set(RelationKind::Callers, key, snapshot)
40    }
41}
42
43#[cfg(test)]
44mod tests {
45    use super::*;
46    use crate::QueryDbConfig;
47    use sqry_core::graph::unified::concurrent::CodeGraph;
48    use sqry_core::graph::unified::edge::kind::EdgeKind;
49    use sqry_core::graph::unified::node::kind::NodeKind;
50    use sqry_core::graph::unified::storage::arena::NodeEntry;
51    use std::path::Path;
52    use std::sync::Arc;
53
54    #[test]
55    fn callers_query_matches_planner_semantics() {
56        // main --Calls--> target, unrelated has no edges.
57        // `callers:main` = {target} — target's callers list includes main.
58        let mut graph = CodeGraph::new();
59        let file = graph.files_mut().register(Path::new("lib.rs")).unwrap();
60        let main_name = graph.strings_mut().intern("main").unwrap();
61        let target_name = graph.strings_mut().intern("target").unwrap();
62        let unrelated_name = graph.strings_mut().intern("unrelated").unwrap();
63
64        let main_fn = graph
65            .nodes_mut()
66            .alloc(
67                NodeEntry::new(NodeKind::Function, main_name, file).with_qualified_name(main_name),
68            )
69            .unwrap();
70        let target = graph
71            .nodes_mut()
72            .alloc(
73                NodeEntry::new(NodeKind::Function, target_name, file)
74                    .with_qualified_name(target_name),
75            )
76            .unwrap();
77        let unrelated = graph
78            .nodes_mut()
79            .alloc(
80                NodeEntry::new(NodeKind::Function, unrelated_name, file)
81                    .with_qualified_name(unrelated_name),
82            )
83            .unwrap();
84
85        graph.edges_mut().add_edge(
86            main_fn,
87            target,
88            EdgeKind::Calls {
89                argument_count: 0,
90                is_async: false,
91            },
92            file,
93        );
94
95        let snapshot = Arc::new(graph.snapshot());
96        let mut db = QueryDb::new(Arc::clone(&snapshot), QueryDbConfig::default());
97        db.register::<CallersQuery>();
98
99        let matches = db.get::<CallersQuery>(&RelationKey::exact("main"));
100        assert!(matches.contains(&target));
101        assert!(!matches.contains(&main_fn));
102        assert!(!matches.contains(&unrelated));
103    }
104
105    #[test]
106    fn callers_query_dynamic_language_method_segment_fallback() {
107        // Dynamic-language fallback: the pattern `Player::takeDamage` has
108        // `takeDamage` as its method segment. When the actual callee is
109        // `Enemy::takeDamage`, the method-segment fallback keeps the match
110        // alive so cross-receiver Ruby/Python dispatch stays covered.
111        let mut graph = CodeGraph::new();
112        let file = graph.files_mut().register(Path::new("game.rb")).unwrap();
113        assert!(
114            graph
115                .files_mut()
116                .set_language(file, sqry_core::graph::node::Language::Ruby)
117        );
118
119        let caller_name = graph.strings_mut().intern("Game::update").unwrap();
120        let callee_name = graph.strings_mut().intern("Enemy::takeDamage").unwrap();
121
122        let caller = graph
123            .nodes_mut()
124            .alloc(
125                NodeEntry::new(NodeKind::Method, caller_name, file)
126                    .with_qualified_name(caller_name),
127            )
128            .unwrap();
129        let callee = graph
130            .nodes_mut()
131            .alloc(
132                NodeEntry::new(NodeKind::Method, callee_name, file)
133                    .with_qualified_name(callee_name),
134            )
135            .unwrap();
136        graph.edges_mut().add_edge(
137            caller,
138            callee,
139            EdgeKind::Calls {
140                argument_count: 0,
141                is_async: false,
142            },
143            file,
144        );
145
146        let snapshot = Arc::new(graph.snapshot());
147        // `callers:Enemy::takeDamage` — the set of nodes whose callers
148        // include `Enemy::takeDamage`. Under Callers semantics this is the
149        // set of nodes that `Enemy::takeDamage` calls. (Our fixture only
150        // has one Calls edge going *into* Enemy::takeDamage, so that set is
151        // empty.) The interesting assertion is that the trailing-segment
152        // fallback is what makes the *graph_eval* convention test
153        // (`callers:Player::takeDamage` matching Game::update as a caller
154        // of Enemy::takeDamage) still work when it's wired in that
155        // direction — the helper lives in
156        // [`super::relation::method_segment_matches`] and is exercised here
157        // by the parallel `callees:Player::takeDamage` assertion below.
158        let matches = compute_relation_source_set(
159            RelationKind::Callees,
160            &RelationKey::exact("Player::takeDamage"),
161            &snapshot,
162        );
163        assert!(
164            matches.contains(&caller),
165            "Game::update calls Enemy::takeDamage, whose trailing segment \
166             `takeDamage` matches `Player::takeDamage` under the dynamic \
167             fallback."
168        );
169    }
170}