use std::collections::HashSet;
use std::sync::Arc;
use regex::RegexBuilder;
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::graph::unified::storage::arena::NodeEntry;
use sqry_core::query::executor::graph_eval::{
entry_query_texts, extract_method_name, language_aware_segments_match,
};
use crate::QueryDb;
use crate::dependency::record_file_dep;
use crate::planner::ir::{MatchMode, RegexPattern, StringPattern};
use crate::query::DerivedQuery;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RelationKind {
Callers,
Callees,
Imports,
Exports,
References,
Implements,
}
#[derive(Debug, Clone, Hash, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum RelationKey {
Pattern(StringPattern),
Regex(RegexPattern),
}
impl RelationKey {
#[must_use]
pub fn exact(raw: impl Into<String>) -> Self {
RelationKey::Pattern(StringPattern::exact(raw))
}
#[must_use]
pub fn regex(pattern: impl Into<String>) -> Self {
RelationKey::Regex(RegexPattern::new(pattern))
}
}
#[derive(Debug, Clone, Copy)]
pub(super) enum Direction {
Forward,
Reverse,
Both,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum EndpointRole {
Source,
Target,
Either,
}
#[must_use]
#[allow(clippy::match_same_arms)]
pub(super) fn traversal_for(relation: RelationKind) -> (Direction, EndpointRole) {
match relation {
RelationKind::Callers => (Direction::Reverse, EndpointRole::Source),
RelationKind::Callees => (Direction::Forward, EndpointRole::Target),
RelationKind::Imports => (Direction::Forward, EndpointRole::Target),
RelationKind::Exports => (Direction::Both, EndpointRole::Either),
RelationKind::References => (Direction::Reverse, EndpointRole::Source),
RelationKind::Implements => (Direction::Forward, EndpointRole::Target),
}
}
#[must_use]
pub(super) fn edge_kind_matches(relation: RelationKind, kind: &EdgeKind) -> bool {
match relation {
RelationKind::Callers | RelationKind::Callees => matches!(kind, EdgeKind::Calls { .. }),
RelationKind::Imports => matches!(kind, EdgeKind::Imports { .. }),
RelationKind::Exports => matches!(kind, EdgeKind::Exports { .. }),
RelationKind::References => matches!(
kind,
EdgeKind::Calls { .. }
| EdgeKind::References
| EdgeKind::Imports { .. }
| EdgeKind::FfiCall { .. }
),
RelationKind::Implements => matches!(kind, EdgeKind::Implements),
}
}
#[must_use]
pub fn compute_relation_source_set(
relation: RelationKind,
key: &RelationKey,
snapshot: &GraphSnapshot,
) -> Arc<Vec<NodeId>> {
for (fid, _) in snapshot.file_segments().iter() {
record_file_dep(fid);
}
let matcher = KeyMatcher::build(key);
let (direction, role) = traversal_for(relation);
let mut results: Vec<NodeId> = Vec::new();
for (node_id, entry) in snapshot.nodes().iter() {
if entry.is_unified_loser() {
continue;
}
if node_has_matching_endpoint(snapshot, node_id, relation, direction, role, &matcher) {
results.push(node_id);
}
}
results.sort_unstable_by_key(|id| (id.index(), id.generation()));
results.dedup();
Arc::new(results)
}
#[must_use]
pub fn relation_matches_node_via_set<S: std::hash::BuildHasher>(
relation: RelationKind,
node_id: NodeId,
endpoints: &HashSet<NodeId, S>,
snapshot: &GraphSnapshot,
) -> bool {
let (direction, role) = traversal_for(relation);
for edge in neighbours(snapshot, node_id, direction) {
if !edge_kind_matches(relation, &edge.kind) {
continue;
}
let endpoint = match role {
EndpointRole::Source => edge.source,
EndpointRole::Target => edge.target,
EndpointRole::Either => {
if edge.source == node_id {
edge.target
} else {
edge.source
}
}
};
if role == EndpointRole::Either && endpoint == node_id {
continue;
}
if endpoints.contains(&endpoint) {
return true;
}
}
false
}
fn node_has_matching_endpoint(
snapshot: &GraphSnapshot,
node_id: NodeId,
relation: RelationKind,
direction: Direction,
role: EndpointRole,
matcher: &KeyMatcher,
) -> bool {
if relation == RelationKind::Imports
&& let Some(entry) = snapshot.nodes().get(node_id)
&& entry.kind == sqry_core::graph::unified::node::NodeKind::Import
&& match_endpoint_name(snapshot, entry, matcher)
{
return true;
}
for edge in neighbours(snapshot, node_id, direction) {
if !edge_kind_matches(relation, &edge.kind) {
continue;
}
let endpoint_id = match role {
EndpointRole::Source => edge.source,
EndpointRole::Target => edge.target,
EndpointRole::Either => {
if edge.source == node_id {
edge.target
} else {
edge.source
}
}
};
if role == EndpointRole::Either && endpoint_id == node_id {
continue;
}
let Some(endpoint_entry) = snapshot.nodes().get(endpoint_id) else {
continue;
};
if match_endpoint_name(snapshot, endpoint_entry, matcher) {
return true;
}
if matches!(relation, RelationKind::Callers | RelationKind::Callees)
&& method_segment_matches(snapshot, endpoint_entry, matcher)
{
return true;
}
if relation == RelationKind::Imports
&& imports_alias_or_wildcard_matches(snapshot, &edge, matcher)
{
return true;
}
}
false
}
fn imports_alias_or_wildcard_matches(
snapshot: &GraphSnapshot,
edge: &sqry_core::graph::unified::edge::store::StoreEdgeRef,
matcher: &KeyMatcher,
) -> bool {
let EdgeKind::Imports { alias, is_wildcard } = &edge.kind else {
return false;
};
let KeyMatcher::Pattern(pattern) = matcher else {
return false;
};
if !matches!(pattern.mode, MatchMode::Exact) {
return false;
}
if *is_wildcard && pattern.raw == "*" {
return true;
}
let Some(alias_id) = alias else {
return false;
};
let Some(alias_str) = snapshot.strings().resolve(*alias_id) else {
return false;
};
alias_str.as_ref().contains(pattern.raw.as_str())
}
fn neighbours(
snapshot: &GraphSnapshot,
node_id: NodeId,
direction: Direction,
) -> Vec<sqry_core::graph::unified::edge::store::StoreEdgeRef> {
match direction {
Direction::Forward => snapshot.edges().edges_from(node_id),
Direction::Reverse => snapshot.edges().edges_to(node_id),
Direction::Both => {
let mut out = snapshot.edges().edges_from(node_id);
out.extend(snapshot.edges().edges_to(node_id));
out
}
}
}
enum KeyMatcher {
Pattern(StringPattern),
Regex(regex::Regex),
RegexReject,
}
impl KeyMatcher {
fn build(key: &RelationKey) -> Self {
match key {
RelationKey::Pattern(pattern) => KeyMatcher::Pattern(pattern.clone()),
RelationKey::Regex(pattern) => {
let mut builder = RegexBuilder::new(&pattern.pattern);
builder
.case_insensitive(pattern.flags.case_insensitive)
.multi_line(pattern.flags.multiline)
.dot_matches_new_line(pattern.flags.dot_all)
.size_limit(1 << 20)
.dfa_size_limit(1 << 20);
builder
.build()
.map(KeyMatcher::Regex)
.unwrap_or(KeyMatcher::RegexReject)
}
}
}
}
fn match_endpoint_name(snapshot: &GraphSnapshot, entry: &NodeEntry, matcher: &KeyMatcher) -> bool {
let texts = entry_query_texts(snapshot, entry);
if texts.is_empty() {
return false;
}
match matcher {
KeyMatcher::Pattern(pattern) => match pattern.mode {
MatchMode::Exact => texts.iter().any(|candidate| {
language_aware_segments_match(snapshot, entry.file, candidate, &pattern.raw)
}),
MatchMode::Glob => globset::Glob::new(&pattern.raw).is_ok_and(|g| {
let m = g.compile_matcher();
texts.iter().any(|candidate| m.is_match(candidate))
}),
MatchMode::Prefix => {
let needle = case_adjust(&pattern.raw, pattern.case_insensitive);
texts.iter().any(|candidate| {
case_adjust(candidate, pattern.case_insensitive).starts_with(&needle)
})
}
MatchMode::Suffix => {
let needle = case_adjust(&pattern.raw, pattern.case_insensitive);
texts.iter().any(|candidate| {
case_adjust(candidate, pattern.case_insensitive).ends_with(&needle)
})
}
MatchMode::Contains => {
let needle = case_adjust(&pattern.raw, pattern.case_insensitive);
texts.iter().any(|candidate| {
case_adjust(candidate, pattern.case_insensitive).contains(&needle)
})
}
},
KeyMatcher::Regex(re) => texts.iter().any(|candidate| re.is_match(candidate)),
KeyMatcher::RegexReject => false,
}
}
fn case_adjust(s: &str, case_insensitive: bool) -> String {
if case_insensitive {
s.to_lowercase()
} else {
s.to_string()
}
}
fn method_segment_matches(
snapshot: &GraphSnapshot,
entry: &NodeEntry,
matcher: &KeyMatcher,
) -> bool {
let KeyMatcher::Pattern(pattern) = matcher else {
return false;
};
if !matches!(pattern.mode, MatchMode::Exact) {
return false;
}
let Some(method) = extract_method_name(&pattern.raw) else {
return false;
};
entry_query_texts(snapshot, entry)
.iter()
.filter_map(|candidate| extract_method_name(candidate))
.any(|candidate_method| candidate_method == method)
}
pub struct ImportsQuery;
impl DerivedQuery for ImportsQuery {
type Key = RelationKey;
type Value = Arc<Vec<NodeId>>;
const QUERY_TYPE_ID: u32 = crate::queries::type_ids::IMPORTS;
const TRACKS_EDGE_REVISION: bool = true;
fn execute(key: &RelationKey, _db: &QueryDb, snapshot: &GraphSnapshot) -> Arc<Vec<NodeId>> {
compute_relation_source_set(RelationKind::Imports, key, snapshot)
}
}
pub struct ExportsQuery;
impl DerivedQuery for ExportsQuery {
type Key = RelationKey;
type Value = Arc<Vec<NodeId>>;
const QUERY_TYPE_ID: u32 = crate::queries::type_ids::EXPORTS;
const TRACKS_EDGE_REVISION: bool = true;
fn execute(key: &RelationKey, _db: &QueryDb, snapshot: &GraphSnapshot) -> Arc<Vec<NodeId>> {
compute_relation_source_set(RelationKind::Exports, key, snapshot)
}
}
pub struct ReferencesQuery;
impl DerivedQuery for ReferencesQuery {
type Key = RelationKey;
type Value = Arc<Vec<NodeId>>;
const QUERY_TYPE_ID: u32 = crate::queries::type_ids::REFERENCES;
const TRACKS_EDGE_REVISION: bool = true;
fn execute(key: &RelationKey, _db: &QueryDb, snapshot: &GraphSnapshot) -> Arc<Vec<NodeId>> {
compute_relation_source_set(RelationKind::References, key, snapshot)
}
}
pub struct ImplementsQuery;
impl DerivedQuery for ImplementsQuery {
type Key = RelationKey;
type Value = Arc<Vec<NodeId>>;
const QUERY_TYPE_ID: u32 = crate::queries::type_ids::IMPLEMENTS;
const TRACKS_EDGE_REVISION: bool = true;
fn execute(key: &RelationKey, _db: &QueryDb, snapshot: &GraphSnapshot) -> Arc<Vec<NodeId>> {
compute_relation_source_set(RelationKind::Implements, key, snapshot)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::QueryDbConfig;
use sqry_core::graph::unified::concurrent::CodeGraph;
use sqry_core::graph::unified::edge::kind::ExportKind;
use sqry_core::graph::unified::node::kind::NodeKind;
use std::path::Path;
use std::sync::Arc;
fn alloc_function(graph: &mut CodeGraph, name: &str, file: &Path) -> NodeId {
let name_id = graph.strings_mut().intern(name).unwrap();
let file_id = graph.files_mut().register(file).unwrap();
let entry =
NodeEntry::new(NodeKind::Function, name_id, file_id).with_qualified_name(name_id);
graph.nodes_mut().alloc(entry).unwrap()
}
fn add_calls_edge(graph: &mut CodeGraph, src: NodeId, tgt: NodeId) {
let file_id = graph.nodes().get(src).unwrap().file;
graph.edges_mut().add_edge(
src,
tgt,
EdgeKind::Calls {
argument_count: 0,
is_async: false,
},
file_id,
);
}
fn build_db_for_graph(graph: CodeGraph) -> QueryDb {
let snapshot = Arc::new(graph.snapshot());
let mut db = QueryDb::new(snapshot, QueryDbConfig::default());
db.register::<ImportsQuery>();
db.register::<ExportsQuery>();
db.register::<ReferencesQuery>();
db.register::<ImplementsQuery>();
db
}
#[test]
fn callers_relation_returns_nodes_called_by_matching_source() {
let mut graph = CodeGraph::new();
let path = Path::new("main.rs");
let main_fn = alloc_function(&mut graph, "main", path);
let a = alloc_function(&mut graph, "a", path);
let b = alloc_function(&mut graph, "b", path);
add_calls_edge(&mut graph, main_fn, a);
add_calls_edge(&mut graph, main_fn, b);
let snapshot = Arc::new(graph.snapshot());
let out = compute_relation_source_set(
RelationKind::Callers,
&RelationKey::exact("main"),
&snapshot,
);
let ids: Vec<NodeId> = out.as_ref().clone();
assert!(ids.contains(&a));
assert!(ids.contains(&b));
assert!(!ids.contains(&main_fn));
}
#[test]
fn callees_relation_returns_nodes_that_call_matching_target() {
let mut graph = CodeGraph::new();
let path = Path::new("main.rs");
let main_fn = alloc_function(&mut graph, "main", path);
let parse_expr = alloc_function(&mut graph, "parse_expr", path);
add_calls_edge(&mut graph, main_fn, parse_expr);
let snapshot = Arc::new(graph.snapshot());
let out = compute_relation_source_set(
RelationKind::Callees,
&RelationKey::exact("parse_expr"),
&snapshot,
);
assert_eq!(out.as_ref(), &vec![main_fn]);
}
#[test]
fn references_relation_matches_incoming_reference_edges() {
let mut graph = CodeGraph::new();
let path = Path::new("lib.rs");
let file_id = graph.files_mut().register(path).unwrap();
let caller_name = graph.strings_mut().intern("caller").unwrap();
let target_name = graph.strings_mut().intern("target").unwrap();
let unrelated_name = graph.strings_mut().intern("unrelated").unwrap();
let caller = graph
.nodes_mut()
.alloc(
NodeEntry::new(NodeKind::Function, caller_name, file_id)
.with_qualified_name(caller_name),
)
.unwrap();
let target = graph
.nodes_mut()
.alloc(
NodeEntry::new(NodeKind::Function, target_name, file_id)
.with_qualified_name(target_name),
)
.unwrap();
let unrelated = graph
.nodes_mut()
.alloc(
NodeEntry::new(NodeKind::Function, unrelated_name, file_id)
.with_qualified_name(unrelated_name),
)
.unwrap();
graph
.edges_mut()
.add_edge(caller, target, EdgeKind::References, file_id);
let snapshot = Arc::new(graph.snapshot());
let out = compute_relation_source_set(
RelationKind::References,
&RelationKey::exact("caller"),
&snapshot,
);
assert_eq!(out.as_ref(), &vec![target]);
assert!(!out.contains(&unrelated));
}
#[test]
fn implements_relation_matches_outgoing_implements_edge() {
let mut graph = CodeGraph::new();
let path = Path::new("lib.rs");
let name_id = graph.strings_mut().intern("MyStruct").unwrap();
let trait_name = graph.strings_mut().intern("Drawable").unwrap();
let file_id = graph.files_mut().register(path).unwrap();
let s = graph
.nodes_mut()
.alloc(NodeEntry::new(NodeKind::Struct, name_id, file_id).with_qualified_name(name_id))
.unwrap();
let t = graph
.nodes_mut()
.alloc(
NodeEntry::new(NodeKind::Trait, trait_name, file_id)
.with_qualified_name(trait_name),
)
.unwrap();
graph
.edges_mut()
.add_edge(s, t, EdgeKind::Implements, file_id);
let snapshot = Arc::new(graph.snapshot());
let out = compute_relation_source_set(
RelationKind::Implements,
&RelationKey::exact("Drawable"),
&snapshot,
);
assert_eq!(out.as_ref(), &vec![s]);
}
#[test]
fn exports_relation_matches_either_direction() {
let mut graph = CodeGraph::new();
let path = Path::new("lib.rs");
let file_id = graph.files_mut().register(path).unwrap();
let run_name = graph.strings_mut().intern("run_fn").unwrap();
let main_name = graph.strings_mut().intern("main").unwrap();
let run_fn = graph
.nodes_mut()
.alloc(
NodeEntry::new(NodeKind::Function, run_name, file_id).with_qualified_name(run_name),
)
.unwrap();
let main_fn = graph
.nodes_mut()
.alloc(
NodeEntry::new(NodeKind::Function, main_name, file_id)
.with_qualified_name(main_name),
)
.unwrap();
graph.edges_mut().add_edge(
run_fn,
main_fn,
EdgeKind::Exports {
kind: ExportKind::Direct,
alias: None,
},
file_id,
);
let snapshot = Arc::new(graph.snapshot());
let out_main = compute_relation_source_set(
RelationKind::Exports,
&RelationKey::exact("main"),
&snapshot,
);
assert_eq!(out_main.as_ref(), &vec![run_fn]);
let out_run = compute_relation_source_set(
RelationKind::Exports,
&RelationKey::exact("run_fn"),
&snapshot,
);
assert_eq!(out_run.as_ref(), &vec![main_fn]);
}
#[test]
fn imports_relation_matches_outgoing_import_target() {
let mut graph = CodeGraph::new();
let path = Path::new("lib.rs");
let file_id = graph.files_mut().register(path).unwrap();
let importer_name = graph.strings_mut().intern("importer").unwrap();
let target_name = graph.strings_mut().intern("serde").unwrap();
let importer = graph
.nodes_mut()
.alloc(
NodeEntry::new(NodeKind::Function, importer_name, file_id)
.with_qualified_name(importer_name),
)
.unwrap();
let target = graph
.nodes_mut()
.alloc(
NodeEntry::new(NodeKind::Module, target_name, file_id)
.with_qualified_name(target_name),
)
.unwrap();
graph.edges_mut().add_edge(
importer,
target,
EdgeKind::Imports {
alias: None,
is_wildcard: false,
},
file_id,
);
let snapshot = Arc::new(graph.snapshot());
let out = compute_relation_source_set(
RelationKind::Imports,
&RelationKey::exact("serde"),
&snapshot,
);
assert_eq!(out.as_ref(), &vec![importer]);
}
#[test]
fn derived_query_is_cached_and_invalidated_on_edge_bump() {
let mut graph = CodeGraph::new();
let path = Path::new("main.rs");
let a = alloc_function(&mut graph, "a", path);
let b = alloc_function(&mut graph, "b", path);
add_calls_edge(&mut graph, a, b);
let db = build_db_for_graph(graph);
let first = db.get::<ImplementsQuery>(&RelationKey::exact("nothing"));
assert!(first.as_ref().is_empty());
let second = db.get::<ImplementsQuery>(&RelationKey::exact("nothing"));
assert!(Arc::ptr_eq(&first, &second));
db.bump_edge_revision();
let third = db.get::<ImplementsQuery>(&RelationKey::exact("nothing"));
assert!(!Arc::ptr_eq(&first, &third));
}
#[test]
fn relation_matches_node_via_set_honours_direction() {
let mut graph = CodeGraph::new();
let path = Path::new("main.rs");
let a = alloc_function(&mut graph, "a", path);
let b = alloc_function(&mut graph, "b", path);
add_calls_edge(&mut graph, a, b);
let snapshot = graph.snapshot();
let mut endpoints = HashSet::new();
endpoints.insert(a);
assert!(relation_matches_node_via_set(
RelationKind::Callers,
b,
&endpoints,
&snapshot
));
assert!(!relation_matches_node_via_set(
RelationKind::Callers,
a,
&endpoints,
&snapshot
));
}
#[test]
fn exports_self_loop_is_rejected_in_keymatcher_path() {
let mut graph = CodeGraph::new();
let path = Path::new("lib.rs");
let file_id = graph.files_mut().register(path).unwrap();
let sym_name = graph.strings_mut().intern("self_export").unwrap();
let sym = graph
.nodes_mut()
.alloc(
NodeEntry::new(NodeKind::Function, sym_name, file_id).with_qualified_name(sym_name),
)
.unwrap();
graph.edges_mut().add_edge(
sym,
sym,
EdgeKind::Exports {
kind: ExportKind::Direct,
alias: None,
},
file_id,
);
let snapshot = Arc::new(graph.snapshot());
let out = compute_relation_source_set(
RelationKind::Exports,
&RelationKey::exact("self_export"),
&snapshot,
);
assert!(
out.as_ref().is_empty(),
"self-export must not satisfy exports:<own name> (regression \
for the Either-role self-loop skip)"
);
}
#[test]
fn exports_self_loop_is_rejected_in_subquery_set_path() {
let mut graph = CodeGraph::new();
let path = Path::new("lib.rs");
let file_id = graph.files_mut().register(path).unwrap();
let sym_name = graph.strings_mut().intern("self_export").unwrap();
let sym = graph
.nodes_mut()
.alloc(
NodeEntry::new(NodeKind::Function, sym_name, file_id).with_qualified_name(sym_name),
)
.unwrap();
graph.edges_mut().add_edge(
sym,
sym,
EdgeKind::Exports {
kind: ExportKind::Direct,
alias: None,
},
file_id,
);
let snapshot = graph.snapshot();
let mut endpoints = HashSet::new();
endpoints.insert(sym);
assert!(
!relation_matches_node_via_set(RelationKind::Exports, sym, &endpoints, &snapshot,),
"subquery-set path must also skip the Either-role self-loop"
);
}
#[test]
fn imports_per_node_semantic_aligned_across_engines() {
use sqry_core::query::executor::graph_eval::{import_edge_matches, import_entry_matches};
let mut graph = CodeGraph::new();
let path = Path::new("lib.rs");
let file_id = graph.files_mut().register(path).unwrap();
let importer_name = graph.strings_mut().intern("importer").unwrap();
let alias_importer_name = graph.strings_mut().intern("alias_importer").unwrap();
let wildcard_importer_name = graph.strings_mut().intern("wildcard_importer").unwrap();
let import_node_name = graph.strings_mut().intern("serde").unwrap();
let unrelated_name = graph.strings_mut().intern("unrelated").unwrap();
let target_name = graph.strings_mut().intern("serde").unwrap();
let aliased_target_name = graph.strings_mut().intern("private_target").unwrap();
let alias_text = graph.strings_mut().intern("serde").unwrap();
let wildcard_target_name = graph.strings_mut().intern("anything").unwrap();
let importer = graph
.nodes_mut()
.alloc(
NodeEntry::new(NodeKind::Function, importer_name, file_id)
.with_qualified_name(importer_name),
)
.unwrap();
let alias_importer = graph
.nodes_mut()
.alloc(
NodeEntry::new(NodeKind::Function, alias_importer_name, file_id)
.with_qualified_name(alias_importer_name),
)
.unwrap();
let wildcard_importer = graph
.nodes_mut()
.alloc(
NodeEntry::new(NodeKind::Function, wildcard_importer_name, file_id)
.with_qualified_name(wildcard_importer_name),
)
.unwrap();
let import_node = graph
.nodes_mut()
.alloc(
NodeEntry::new(NodeKind::Import, import_node_name, file_id)
.with_qualified_name(import_node_name),
)
.unwrap();
let unrelated = graph
.nodes_mut()
.alloc(
NodeEntry::new(NodeKind::Function, unrelated_name, file_id)
.with_qualified_name(unrelated_name),
)
.unwrap();
let target = graph
.nodes_mut()
.alloc(
NodeEntry::new(NodeKind::Module, target_name, file_id)
.with_qualified_name(target_name),
)
.unwrap();
let aliased_target = graph
.nodes_mut()
.alloc(
NodeEntry::new(NodeKind::Module, aliased_target_name, file_id)
.with_qualified_name(aliased_target_name),
)
.unwrap();
let wildcard_target = graph
.nodes_mut()
.alloc(
NodeEntry::new(NodeKind::Module, wildcard_target_name, file_id)
.with_qualified_name(wildcard_target_name),
)
.unwrap();
graph.edges_mut().add_edge(
importer,
target,
EdgeKind::Imports {
alias: None,
is_wildcard: false,
},
file_id,
);
graph.edges_mut().add_edge(
alias_importer,
aliased_target,
EdgeKind::Imports {
alias: Some(alias_text),
is_wildcard: false,
},
file_id,
);
graph.edges_mut().add_edge(
wildcard_importer,
wildcard_target,
EdgeKind::Imports {
alias: None,
is_wildcard: true,
},
file_id,
);
let snapshot = Arc::new(graph.snapshot());
let sqry_db_matches_for = |key: &str| -> Arc<Vec<NodeId>> {
compute_relation_source_set(RelationKind::Imports, &RelationKey::exact(key), &snapshot)
};
let snapshot_ref: &GraphSnapshot = snapshot.as_ref();
let graph_eval_matches = |node_id: NodeId, key: &str| -> bool {
let entry = match snapshot_ref.nodes().get(node_id) {
Some(e) => e,
None => return false,
};
if entry.kind == NodeKind::Import && import_entry_matches(snapshot_ref, entry, key) {
return true;
}
snapshot_ref
.edges()
.edges_from(node_id)
.iter()
.any(|edge| import_edge_matches(snapshot_ref, edge, key))
};
let cases: &[(&str, NodeId, &str, bool)] = &[
("serde", importer, "importer (target match)", true),
(
"serde",
alias_importer,
"alias_importer (alias match)",
true,
),
(
"*",
wildcard_importer,
"wildcard_importer (wildcard *)",
true,
),
("serde", import_node, "import_node (Import fast-path)", true),
("serde", unrelated, "unrelated (no outgoing Imports)", false),
("serde", target, "target (no outgoing Imports)", false),
];
for &(key, node, label, expected) in cases {
let db_set = sqry_db_matches_for(key);
let db_hit = db_set.contains(&node);
let ge_hit = graph_eval_matches(node, key);
assert_eq!(
db_hit, ge_hit,
"{label}: sqry-db ImportsQuery and graph_eval::match_imports \
must agree under per-node semantics for key={key:?} \
(db_hit={db_hit}, ge_hit={ge_hit})"
);
assert_eq!(
db_hit, expected,
"{label}: expected match={expected} for key={key:?}"
);
}
}
}
#[cfg(test)]
mod serde_roundtrip {
use super::*;
use crate::planner::ir::{MatchMode, RegexFlags, RegexPattern, StringPattern};
use postcard::{from_bytes, to_allocvec};
#[test]
fn relation_key_pattern_roundtrip() {
let original = RelationKey::Pattern(StringPattern {
raw: "my_function".to_string(),
mode: MatchMode::Exact,
case_insensitive: false,
});
let bytes = to_allocvec(&original).expect("serialize failed");
let decoded: RelationKey = from_bytes(&bytes).expect("deserialize failed");
assert_eq!(decoded, original);
}
#[test]
fn relation_key_regex_roundtrip() {
let original = RelationKey::Regex(RegexPattern {
pattern: "fn_.*".to_string(),
flags: RegexFlags {
case_insensitive: true,
multiline: false,
dot_all: false,
},
});
let bytes = to_allocvec(&original).expect("serialize failed");
let decoded: RelationKey = from_bytes(&bytes).expect("deserialize failed");
assert_eq!(decoded, original);
}
}