use std::sync::Arc;
use sqry_core::graph::unified::concurrent::GraphSnapshot;
use sqry_core::graph::unified::edge::kind::EdgeKind;
use sqry_core::graph::unified::node::id::NodeId;
use sqry_core::query::CircularType;
use crate::QueryDb;
use crate::dependency::record_file_dep;
use crate::query::DerivedQuery;
use super::SccQuery;
pub type CyclesValue = std::sync::Arc<Vec<Vec<sqry_core::graph::unified::node::id::NodeId>>>;
pub type IsInCycleValue = bool;
#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct CycleBounds {
pub min_depth: usize,
pub max_depth: Option<usize>,
pub max_results: usize,
pub should_include_self_loops: bool,
}
impl Default for CycleBounds {
fn default() -> Self {
Self {
min_depth: 2,
max_depth: None,
max_results: 100,
should_include_self_loops: false,
}
}
}
#[derive(Debug, Clone, Hash, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct CyclesKey {
pub circular_type: CircularType,
pub bounds: CycleBounds,
}
#[derive(Debug, Clone, Hash, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct IsInCycleKey {
pub node_id: NodeId,
pub circular_type: CircularType,
pub bounds: CycleBounds,
}
pub struct CyclesQuery;
impl DerivedQuery for CyclesQuery {
type Key = CyclesKey;
type Value = Arc<Vec<Vec<NodeId>>>;
const QUERY_TYPE_ID: u32 = crate::queries::type_ids::CYCLES;
const TRACKS_EDGE_REVISION: bool = true;
fn execute(key: &CyclesKey, db: &QueryDb, snapshot: &GraphSnapshot) -> Arc<Vec<Vec<NodeId>>> {
for (fid, _) in snapshot.file_segments().iter() {
record_file_dep(fid);
}
let edge_probe = edge_probe_for(key.circular_type);
let scc = db.get::<SccQuery>(&edge_probe);
let mut cycles: Vec<Vec<NodeId>> = Vec::new();
for component in &scc.components {
if cycles.len() >= key.bounds.max_results {
break;
}
let size = component.len();
let is_self_loop =
size == 1 && node_has_self_loop(snapshot, component[0], key.circular_type);
if !should_include(size, is_self_loop, &key.bounds) {
continue;
}
cycles.push(component.clone());
}
Arc::new(cycles)
}
}
pub struct IsInCycleQuery;
impl DerivedQuery for IsInCycleQuery {
type Key = IsInCycleKey;
type Value = bool;
const QUERY_TYPE_ID: u32 = crate::queries::type_ids::IS_IN_CYCLE;
const TRACKS_EDGE_REVISION: bool = true;
fn execute(key: &IsInCycleKey, db: &QueryDb, snapshot: &GraphSnapshot) -> bool {
if let Some(entry) = snapshot.nodes().get(key.node_id) {
record_file_dep(entry.file);
}
let self_loop = node_has_self_loop(snapshot, key.node_id, key.circular_type);
if self_loop && key.bounds.should_include_self_loops {
return true;
}
let edge_probe = edge_probe_for(key.circular_type);
let scc = db.get::<SccQuery>(&edge_probe);
let Some(component_idx) = scc.component_of(key.node_id) else {
return false;
};
let Some(component) = scc.components.get(component_idx as usize) else {
return false;
};
let size = component.len();
if size < 2 {
return false;
}
if size < key.bounds.min_depth {
return false;
}
if key.bounds.max_depth.is_some_and(|max| size > max) {
return false;
}
true
}
}
fn edge_probe_for(circular_type: CircularType) -> EdgeKind {
match circular_type {
CircularType::Calls => EdgeKind::Calls {
argument_count: 0,
is_async: false,
},
CircularType::Imports | CircularType::Modules => EdgeKind::Imports {
alias: None,
is_wildcard: false,
},
}
}
fn node_has_self_loop(
snapshot: &GraphSnapshot,
node_id: NodeId,
circular_type: CircularType,
) -> bool {
for edge in &snapshot.edges().edges_from(node_id) {
if edge.target != node_id {
continue;
}
let is_match = match circular_type {
CircularType::Calls => matches!(edge.kind, EdgeKind::Calls { .. }),
CircularType::Imports | CircularType::Modules => {
matches!(edge.kind, EdgeKind::Imports { .. })
}
};
if is_match {
return true;
}
}
false
}
fn should_include(size: usize, is_self_loop: bool, bounds: &CycleBounds) -> bool {
if is_self_loop {
return bounds.should_include_self_loops;
}
if size == 1 {
return false;
}
if size < bounds.min_depth {
return false;
}
if bounds.max_depth.is_some_and(|max| size > max) {
return false;
}
true
}
#[cfg(test)]
mod serde_roundtrip {
use super::*;
use postcard::{from_bytes, to_allocvec};
use sqry_core::graph::unified::node::id::NodeId;
use sqry_core::query::CircularType;
use std::sync::Arc;
#[test]
fn cycle_bounds_roundtrip() {
let original = CycleBounds {
min_depth: 3,
max_depth: Some(10),
max_results: 50,
should_include_self_loops: true,
};
let bytes = to_allocvec(&original).expect("serialize failed");
let decoded: CycleBounds = from_bytes(&bytes).expect("deserialize failed");
assert_eq!(decoded, original);
}
#[test]
fn cycle_bounds_no_max_depth_roundtrip() {
let original = CycleBounds::default();
let bytes = to_allocvec(&original).expect("serialize failed");
let decoded: CycleBounds = from_bytes(&bytes).expect("deserialize failed");
assert_eq!(decoded, original);
}
#[test]
fn cycles_key_roundtrip() {
let original = CyclesKey {
circular_type: CircularType::Calls,
bounds: CycleBounds::default(),
};
let bytes = to_allocvec(&original).expect("serialize failed");
let decoded: CyclesKey = from_bytes(&bytes).expect("deserialize failed");
assert_eq!(decoded, original);
}
#[test]
fn is_in_cycle_key_roundtrip() {
let original = IsInCycleKey {
node_id: NodeId::new(42, 7),
circular_type: CircularType::Imports,
bounds: CycleBounds {
min_depth: 2,
max_depth: None,
max_results: 100,
should_include_self_loops: false,
},
};
let bytes = to_allocvec(&original).expect("serialize failed");
let decoded: IsInCycleKey = from_bytes(&bytes).expect("deserialize failed");
assert_eq!(decoded, original);
}
#[test]
fn cycles_value_roundtrip() {
let original: CyclesValue = Arc::new(vec![vec![NodeId::new(1, 1), NodeId::new(2, 1)]]);
let bytes = to_allocvec(&original).expect("serialize failed");
let decoded: CyclesValue = from_bytes(&bytes).expect("deserialize failed");
assert_eq!(decoded.as_ref(), original.as_ref());
}
#[test]
fn is_in_cycle_value_roundtrip() {
for val in [true, false] {
let bytes = to_allocvec(&val).expect("serialize failed");
let decoded: IsInCycleValue = from_bytes(&bytes).expect("deserialize failed");
assert_eq!(decoded, val);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::QueryDbConfig;
use sqry_core::graph::unified::concurrent::CodeGraph;
use sqry_core::graph::unified::node::kind::NodeKind;
use sqry_core::graph::unified::storage::arena::NodeEntry;
use std::path::Path;
fn alloc_fn(graph: &mut CodeGraph, name: &str) -> NodeId {
let file = graph.files_mut().register(Path::new("x.rs")).unwrap();
let name_id = graph.strings_mut().intern(name).unwrap();
graph
.nodes_mut()
.alloc(NodeEntry::new(NodeKind::Function, name_id, file).with_qualified_name(name_id))
.unwrap()
}
fn add_call(graph: &mut CodeGraph, src: NodeId, tgt: NodeId) {
let file = graph.nodes().get(src).unwrap().file;
graph.edges_mut().add_edge(
src,
tgt,
EdgeKind::Calls {
argument_count: 0,
is_async: false,
},
file,
);
}
fn build_db(graph: CodeGraph) -> QueryDb {
let snapshot = Arc::new(graph.snapshot());
let mut db = QueryDb::new(snapshot, QueryDbConfig::default());
db.register::<SccQuery>();
db.register::<CyclesQuery>();
db.register::<IsInCycleQuery>();
db
}
#[test]
fn cycles_query_detects_two_node_cycle() {
let mut graph = CodeGraph::new();
let a = alloc_fn(&mut graph, "a");
let b = alloc_fn(&mut graph, "b");
add_call(&mut graph, a, b);
add_call(&mut graph, b, a);
let db = build_db(graph);
let key = CyclesKey {
circular_type: CircularType::Calls,
bounds: CycleBounds::default(),
};
let cycles = db.get::<CyclesQuery>(&key);
assert_eq!(cycles.len(), 1);
assert_eq!(cycles[0].len(), 2);
}
#[test]
fn is_in_cycle_self_loop_requires_opt_in() {
let mut graph = CodeGraph::new();
let a = alloc_fn(&mut graph, "a");
add_call(&mut graph, a, a);
let db = build_db(graph);
let default_key = IsInCycleKey {
node_id: a,
circular_type: CircularType::Calls,
bounds: CycleBounds::default(),
};
assert!(
!db.get::<IsInCycleQuery>(&default_key),
"self-loop excluded by default"
);
let opted_key = IsInCycleKey {
node_id: a,
circular_type: CircularType::Calls,
bounds: CycleBounds {
should_include_self_loops: true,
min_depth: 1,
..CycleBounds::default()
},
};
assert!(
db.get::<IsInCycleQuery>(&opted_key),
"self-loop reported when opted-in"
);
}
#[test]
fn cycles_query_respects_max_results() {
let mut graph = CodeGraph::new();
let a = alloc_fn(&mut graph, "a");
let b = alloc_fn(&mut graph, "b");
let c = alloc_fn(&mut graph, "c");
let d = alloc_fn(&mut graph, "d");
add_call(&mut graph, a, b);
add_call(&mut graph, b, a);
add_call(&mut graph, c, d);
add_call(&mut graph, d, c);
let db = build_db(graph);
let key = CyclesKey {
circular_type: CircularType::Calls,
bounds: CycleBounds {
max_results: 1,
..CycleBounds::default()
},
};
let cycles = db.get::<CyclesQuery>(&key);
assert_eq!(cycles.len(), 1);
}
}