1use std::collections::HashSet;
65use std::sync::Arc;
66
67use regex::RegexBuilder;
68use sqry_core::graph::unified::concurrent::GraphSnapshot;
69use sqry_core::graph::unified::edge::kind::EdgeKind;
70use sqry_core::graph::unified::node::id::NodeId;
71use sqry_core::graph::unified::storage::arena::NodeEntry;
72use sqry_core::query::executor::graph_eval::{
73 entry_query_texts, extract_method_name, language_aware_segments_match,
74};
75
76use crate::QueryDb;
77use crate::dependency::record_file_dep;
78use crate::planner::ir::{MatchMode, RegexPattern, StringPattern};
79use crate::query::DerivedQuery;
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
95pub enum RelationKind {
96 Callers,
99 Callees,
102 Imports,
105 Exports,
108 References,
111 Implements,
114}
115
116#[derive(Debug, Clone, Hash, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
126pub enum RelationKey {
127 Pattern(StringPattern),
129 Regex(RegexPattern),
131}
132
133impl RelationKey {
134 #[must_use]
136 pub fn exact(raw: impl Into<String>) -> Self {
137 RelationKey::Pattern(StringPattern::exact(raw))
138 }
139
140 #[must_use]
142 pub fn regex(pattern: impl Into<String>) -> Self {
143 RelationKey::Regex(RegexPattern::new(pattern))
144 }
145}
146
147#[derive(Debug, Clone, Copy)]
153pub(super) enum Direction {
154 Forward,
156 Reverse,
158 Both,
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
164pub(super) enum EndpointRole {
165 Source,
167 Target,
169 Either,
173}
174
175#[must_use]
183#[allow(clippy::match_same_arms)]
184pub(super) fn traversal_for(relation: RelationKind) -> (Direction, EndpointRole) {
185 match relation {
186 RelationKind::Callers => (Direction::Reverse, EndpointRole::Source),
187 RelationKind::Callees => (Direction::Forward, EndpointRole::Target),
188 RelationKind::Imports => (Direction::Forward, EndpointRole::Target),
189 RelationKind::Exports => (Direction::Both, EndpointRole::Either),
190 RelationKind::References => (Direction::Reverse, EndpointRole::Source),
191 RelationKind::Implements => (Direction::Forward, EndpointRole::Target),
192 }
193}
194
195#[must_use]
197pub(super) fn edge_kind_matches(relation: RelationKind, kind: &EdgeKind) -> bool {
198 match relation {
199 RelationKind::Callers | RelationKind::Callees => matches!(kind, EdgeKind::Calls { .. }),
200 RelationKind::Imports => matches!(kind, EdgeKind::Imports { .. }),
201 RelationKind::Exports => matches!(kind, EdgeKind::Exports { .. }),
202 RelationKind::References => matches!(
203 kind,
204 EdgeKind::Calls { .. }
205 | EdgeKind::References
206 | EdgeKind::Imports { .. }
207 | EdgeKind::FfiCall { .. }
208 ),
209 RelationKind::Implements => matches!(kind, EdgeKind::Implements),
210 }
211}
212
213#[must_use]
217pub fn compute_relation_source_set(
218 relation: RelationKind,
219 key: &RelationKey,
220 snapshot: &GraphSnapshot,
221) -> Arc<Vec<NodeId>> {
222 for (fid, _) in snapshot.file_segments().iter() {
223 record_file_dep(fid);
224 }
225
226 let matcher = KeyMatcher::build(key);
227 let (direction, role) = traversal_for(relation);
228 let mut results: Vec<NodeId> = Vec::new();
229
230 for (node_id, entry) in snapshot.nodes().iter() {
231 if entry.is_unified_loser() {
234 continue;
235 }
236 if node_has_matching_endpoint(snapshot, node_id, relation, direction, role, &matcher) {
237 results.push(node_id);
238 }
239 }
240
241 results.sort_unstable_by_key(|id| (id.index(), id.generation()));
242 results.dedup();
243 Arc::new(results)
244}
245
246#[must_use]
254pub fn relation_matches_node_via_set<S: std::hash::BuildHasher>(
255 relation: RelationKind,
256 node_id: NodeId,
257 endpoints: &HashSet<NodeId, S>,
258 snapshot: &GraphSnapshot,
259) -> bool {
260 let (direction, role) = traversal_for(relation);
261 for edge in neighbours(snapshot, node_id, direction) {
262 if !edge_kind_matches(relation, &edge.kind) {
263 continue;
264 }
265 let endpoint = match role {
266 EndpointRole::Source => edge.source,
267 EndpointRole::Target => edge.target,
268 EndpointRole::Either => {
269 if edge.source == node_id {
270 edge.target
271 } else {
272 edge.source
273 }
274 }
275 };
276 if role == EndpointRole::Either && endpoint == node_id {
277 continue;
280 }
281 if endpoints.contains(&endpoint) {
282 return true;
283 }
284 }
285 false
286}
287
288fn node_has_matching_endpoint(
293 snapshot: &GraphSnapshot,
294 node_id: NodeId,
295 relation: RelationKind,
296 direction: Direction,
297 role: EndpointRole,
298 matcher: &KeyMatcher,
299) -> bool {
300 if relation == RelationKind::Imports
307 && let Some(entry) = snapshot.nodes().get(node_id)
308 && entry.kind == sqry_core::graph::unified::node::NodeKind::Import
309 && match_endpoint_name(snapshot, entry, matcher)
310 {
311 return true;
312 }
313
314 for edge in neighbours(snapshot, node_id, direction) {
315 if !edge_kind_matches(relation, &edge.kind) {
316 continue;
317 }
318 let endpoint_id = match role {
319 EndpointRole::Source => edge.source,
320 EndpointRole::Target => edge.target,
321 EndpointRole::Either => {
322 if edge.source == node_id {
323 edge.target
324 } else {
325 edge.source
326 }
327 }
328 };
329 if role == EndpointRole::Either && endpoint_id == node_id {
330 continue;
331 }
332 let Some(endpoint_entry) = snapshot.nodes().get(endpoint_id) else {
333 continue;
334 };
335 if match_endpoint_name(snapshot, endpoint_entry, matcher) {
336 return true;
337 }
338 if matches!(relation, RelationKind::Callers | RelationKind::Callees)
344 && method_segment_matches(snapshot, endpoint_entry, matcher)
345 {
346 return true;
347 }
348 if relation == RelationKind::Imports
356 && imports_alias_or_wildcard_matches(snapshot, &edge, matcher)
357 {
358 return true;
359 }
360 }
361 false
362}
363
364fn imports_alias_or_wildcard_matches(
374 snapshot: &GraphSnapshot,
375 edge: &sqry_core::graph::unified::edge::store::StoreEdgeRef,
376 matcher: &KeyMatcher,
377) -> bool {
378 let EdgeKind::Imports { alias, is_wildcard } = &edge.kind else {
379 return false;
380 };
381 let KeyMatcher::Pattern(pattern) = matcher else {
382 return false;
383 };
384 if !matches!(pattern.mode, MatchMode::Exact) {
385 return false;
386 }
387 if *is_wildcard && pattern.raw == "*" {
388 return true;
389 }
390 let Some(alias_id) = alias else {
391 return false;
392 };
393 let Some(alias_str) = snapshot.strings().resolve(*alias_id) else {
394 return false;
395 };
396 alias_str.as_ref().contains(pattern.raw.as_str())
402}
403
404fn neighbours(
405 snapshot: &GraphSnapshot,
406 node_id: NodeId,
407 direction: Direction,
408) -> Vec<sqry_core::graph::unified::edge::store::StoreEdgeRef> {
409 match direction {
410 Direction::Forward => snapshot.edges().edges_from(node_id),
411 Direction::Reverse => snapshot.edges().edges_to(node_id),
412 Direction::Both => {
413 let mut out = snapshot.edges().edges_from(node_id);
414 out.extend(snapshot.edges().edges_to(node_id));
415 out
416 }
417 }
418}
419
420enum KeyMatcher {
428 Pattern(StringPattern),
429 Regex(regex::Regex),
430 RegexReject,
431}
432
433impl KeyMatcher {
434 fn build(key: &RelationKey) -> Self {
435 match key {
436 RelationKey::Pattern(pattern) => KeyMatcher::Pattern(pattern.clone()),
437 RelationKey::Regex(pattern) => {
438 let mut builder = RegexBuilder::new(&pattern.pattern);
439 builder
440 .case_insensitive(pattern.flags.case_insensitive)
441 .multi_line(pattern.flags.multiline)
442 .dot_matches_new_line(pattern.flags.dot_all)
443 .size_limit(1 << 20)
444 .dfa_size_limit(1 << 20);
445 builder
446 .build()
447 .map(KeyMatcher::Regex)
448 .unwrap_or(KeyMatcher::RegexReject)
449 }
450 }
451 }
452}
453
454fn match_endpoint_name(snapshot: &GraphSnapshot, entry: &NodeEntry, matcher: &KeyMatcher) -> bool {
460 let texts = entry_query_texts(snapshot, entry);
461 if texts.is_empty() {
462 return false;
463 }
464 match matcher {
465 KeyMatcher::Pattern(pattern) => match pattern.mode {
466 MatchMode::Exact => texts.iter().any(|candidate| {
467 language_aware_segments_match(snapshot, entry.file, candidate, &pattern.raw)
468 }),
469 MatchMode::Glob => globset::Glob::new(&pattern.raw).is_ok_and(|g| {
470 let m = g.compile_matcher();
471 texts.iter().any(|candidate| m.is_match(candidate))
472 }),
473 MatchMode::Prefix => {
474 let needle = case_adjust(&pattern.raw, pattern.case_insensitive);
475 texts.iter().any(|candidate| {
476 case_adjust(candidate, pattern.case_insensitive).starts_with(&needle)
477 })
478 }
479 MatchMode::Suffix => {
480 let needle = case_adjust(&pattern.raw, pattern.case_insensitive);
481 texts.iter().any(|candidate| {
482 case_adjust(candidate, pattern.case_insensitive).ends_with(&needle)
483 })
484 }
485 MatchMode::Contains => {
486 let needle = case_adjust(&pattern.raw, pattern.case_insensitive);
487 texts.iter().any(|candidate| {
488 case_adjust(candidate, pattern.case_insensitive).contains(&needle)
489 })
490 }
491 },
492 KeyMatcher::Regex(re) => texts.iter().any(|candidate| re.is_match(candidate)),
493 KeyMatcher::RegexReject => false,
494 }
495}
496
497fn case_adjust(s: &str, case_insensitive: bool) -> String {
498 if case_insensitive {
499 s.to_lowercase()
500 } else {
501 s.to_string()
502 }
503}
504
505fn method_segment_matches(
506 snapshot: &GraphSnapshot,
507 entry: &NodeEntry,
508 matcher: &KeyMatcher,
509) -> bool {
510 let KeyMatcher::Pattern(pattern) = matcher else {
511 return false;
512 };
513 if !matches!(pattern.mode, MatchMode::Exact) {
514 return false;
515 }
516 let Some(method) = extract_method_name(&pattern.raw) else {
517 return false;
518 };
519 entry_query_texts(snapshot, entry)
520 .iter()
521 .filter_map(|candidate| extract_method_name(candidate))
522 .any(|candidate_method| candidate_method == method)
523}
524
525pub struct ImportsQuery;
533
534impl DerivedQuery for ImportsQuery {
535 type Key = RelationKey;
536 type Value = Arc<Vec<NodeId>>;
537 const QUERY_TYPE_ID: u32 = crate::queries::type_ids::IMPORTS;
538 const TRACKS_EDGE_REVISION: bool = true;
539
540 fn execute(key: &RelationKey, _db: &QueryDb, snapshot: &GraphSnapshot) -> Arc<Vec<NodeId>> {
541 compute_relation_source_set(RelationKind::Imports, key, snapshot)
542 }
543}
544
545pub struct ExportsQuery;
548
549impl DerivedQuery for ExportsQuery {
550 type Key = RelationKey;
551 type Value = Arc<Vec<NodeId>>;
552 const QUERY_TYPE_ID: u32 = crate::queries::type_ids::EXPORTS;
553 const TRACKS_EDGE_REVISION: bool = true;
554
555 fn execute(key: &RelationKey, _db: &QueryDb, snapshot: &GraphSnapshot) -> Arc<Vec<NodeId>> {
556 compute_relation_source_set(RelationKind::Exports, key, snapshot)
557 }
558}
559
560pub struct ReferencesQuery;
563
564impl DerivedQuery for ReferencesQuery {
565 type Key = RelationKey;
566 type Value = Arc<Vec<NodeId>>;
567 const QUERY_TYPE_ID: u32 = crate::queries::type_ids::REFERENCES;
568 const TRACKS_EDGE_REVISION: bool = true;
569
570 fn execute(key: &RelationKey, _db: &QueryDb, snapshot: &GraphSnapshot) -> Arc<Vec<NodeId>> {
571 compute_relation_source_set(RelationKind::References, key, snapshot)
572 }
573}
574
575pub struct ImplementsQuery;
578
579impl DerivedQuery for ImplementsQuery {
580 type Key = RelationKey;
581 type Value = Arc<Vec<NodeId>>;
582 const QUERY_TYPE_ID: u32 = crate::queries::type_ids::IMPLEMENTS;
583 const TRACKS_EDGE_REVISION: bool = true;
584
585 fn execute(key: &RelationKey, _db: &QueryDb, snapshot: &GraphSnapshot) -> Arc<Vec<NodeId>> {
586 compute_relation_source_set(RelationKind::Implements, key, snapshot)
587 }
588}
589
590#[cfg(test)]
595mod tests {
596 use super::*;
597 use crate::QueryDbConfig;
598 use sqry_core::graph::unified::concurrent::CodeGraph;
599 use sqry_core::graph::unified::edge::kind::ExportKind;
600 use sqry_core::graph::unified::node::kind::NodeKind;
601 use std::path::Path;
602 use std::sync::Arc;
603
604 fn alloc_function(graph: &mut CodeGraph, name: &str, file: &Path) -> NodeId {
605 let name_id = graph.strings_mut().intern(name).unwrap();
606 let file_id = graph.files_mut().register(file).unwrap();
607 let entry =
608 NodeEntry::new(NodeKind::Function, name_id, file_id).with_qualified_name(name_id);
609 graph.nodes_mut().alloc(entry).unwrap()
610 }
611
612 fn add_calls_edge(graph: &mut CodeGraph, src: NodeId, tgt: NodeId) {
613 let file_id = graph.nodes().get(src).unwrap().file;
614 graph.edges_mut().add_edge(
615 src,
616 tgt,
617 EdgeKind::Calls {
618 argument_count: 0,
619 is_async: false,
620 },
621 file_id,
622 );
623 }
624
625 fn build_db_for_graph(graph: CodeGraph) -> QueryDb {
626 let snapshot = Arc::new(graph.snapshot());
627 let mut db = QueryDb::new(snapshot, QueryDbConfig::default());
628 db.register::<ImportsQuery>();
629 db.register::<ExportsQuery>();
630 db.register::<ReferencesQuery>();
631 db.register::<ImplementsQuery>();
632 db
633 }
634
635 #[test]
636 fn callers_relation_returns_nodes_called_by_matching_source() {
637 let mut graph = CodeGraph::new();
640 let path = Path::new("main.rs");
641 let main_fn = alloc_function(&mut graph, "main", path);
642 let a = alloc_function(&mut graph, "a", path);
643 let b = alloc_function(&mut graph, "b", path);
644 add_calls_edge(&mut graph, main_fn, a);
645 add_calls_edge(&mut graph, main_fn, b);
646
647 let snapshot = Arc::new(graph.snapshot());
648 let out = compute_relation_source_set(
649 RelationKind::Callers,
650 &RelationKey::exact("main"),
651 &snapshot,
652 );
653 let ids: Vec<NodeId> = out.as_ref().clone();
654 assert!(ids.contains(&a));
655 assert!(ids.contains(&b));
656 assert!(!ids.contains(&main_fn));
657 }
658
659 #[test]
660 fn callees_relation_returns_nodes_that_call_matching_target() {
661 let mut graph = CodeGraph::new();
663 let path = Path::new("main.rs");
664 let main_fn = alloc_function(&mut graph, "main", path);
665 let parse_expr = alloc_function(&mut graph, "parse_expr", path);
666 add_calls_edge(&mut graph, main_fn, parse_expr);
667
668 let snapshot = Arc::new(graph.snapshot());
669 let out = compute_relation_source_set(
670 RelationKind::Callees,
671 &RelationKey::exact("parse_expr"),
672 &snapshot,
673 );
674 assert_eq!(out.as_ref(), &vec![main_fn]);
675 }
676
677 #[test]
678 fn references_relation_matches_incoming_reference_edges() {
679 let mut graph = CodeGraph::new();
681 let path = Path::new("lib.rs");
682 let file_id = graph.files_mut().register(path).unwrap();
683 let caller_name = graph.strings_mut().intern("caller").unwrap();
684 let target_name = graph.strings_mut().intern("target").unwrap();
685 let unrelated_name = graph.strings_mut().intern("unrelated").unwrap();
686
687 let caller = graph
688 .nodes_mut()
689 .alloc(
690 NodeEntry::new(NodeKind::Function, caller_name, file_id)
691 .with_qualified_name(caller_name),
692 )
693 .unwrap();
694 let target = graph
695 .nodes_mut()
696 .alloc(
697 NodeEntry::new(NodeKind::Function, target_name, file_id)
698 .with_qualified_name(target_name),
699 )
700 .unwrap();
701 let unrelated = graph
702 .nodes_mut()
703 .alloc(
704 NodeEntry::new(NodeKind::Function, unrelated_name, file_id)
705 .with_qualified_name(unrelated_name),
706 )
707 .unwrap();
708
709 graph
710 .edges_mut()
711 .add_edge(caller, target, EdgeKind::References, file_id);
712
713 let snapshot = Arc::new(graph.snapshot());
714 let out = compute_relation_source_set(
715 RelationKind::References,
716 &RelationKey::exact("caller"),
717 &snapshot,
718 );
719 assert_eq!(out.as_ref(), &vec![target]);
720 assert!(!out.contains(&unrelated));
721 }
722
723 #[test]
724 fn implements_relation_matches_outgoing_implements_edge() {
725 let mut graph = CodeGraph::new();
726 let path = Path::new("lib.rs");
727 let name_id = graph.strings_mut().intern("MyStruct").unwrap();
728 let trait_name = graph.strings_mut().intern("Drawable").unwrap();
729 let file_id = graph.files_mut().register(path).unwrap();
730 let s = graph
731 .nodes_mut()
732 .alloc(NodeEntry::new(NodeKind::Struct, name_id, file_id).with_qualified_name(name_id))
733 .unwrap();
734 let t = graph
735 .nodes_mut()
736 .alloc(
737 NodeEntry::new(NodeKind::Trait, trait_name, file_id)
738 .with_qualified_name(trait_name),
739 )
740 .unwrap();
741 graph
742 .edges_mut()
743 .add_edge(s, t, EdgeKind::Implements, file_id);
744
745 let snapshot = Arc::new(graph.snapshot());
746 let out = compute_relation_source_set(
747 RelationKind::Implements,
748 &RelationKey::exact("Drawable"),
749 &snapshot,
750 );
751 assert_eq!(out.as_ref(), &vec![s]);
752 }
753
754 #[test]
755 fn exports_relation_matches_either_direction() {
756 let mut graph = CodeGraph::new();
761 let path = Path::new("lib.rs");
762 let file_id = graph.files_mut().register(path).unwrap();
763 let run_name = graph.strings_mut().intern("run_fn").unwrap();
764 let main_name = graph.strings_mut().intern("main").unwrap();
765 let run_fn = graph
766 .nodes_mut()
767 .alloc(
768 NodeEntry::new(NodeKind::Function, run_name, file_id).with_qualified_name(run_name),
769 )
770 .unwrap();
771 let main_fn = graph
772 .nodes_mut()
773 .alloc(
774 NodeEntry::new(NodeKind::Function, main_name, file_id)
775 .with_qualified_name(main_name),
776 )
777 .unwrap();
778 graph.edges_mut().add_edge(
779 run_fn,
780 main_fn,
781 EdgeKind::Exports {
782 kind: ExportKind::Direct,
783 alias: None,
784 },
785 file_id,
786 );
787
788 let snapshot = Arc::new(graph.snapshot());
789 let out_main = compute_relation_source_set(
790 RelationKind::Exports,
791 &RelationKey::exact("main"),
792 &snapshot,
793 );
794 assert_eq!(out_main.as_ref(), &vec![run_fn]);
795 let out_run = compute_relation_source_set(
796 RelationKind::Exports,
797 &RelationKey::exact("run_fn"),
798 &snapshot,
799 );
800 assert_eq!(out_run.as_ref(), &vec![main_fn]);
801 }
802
803 #[test]
804 fn imports_relation_matches_outgoing_import_target() {
805 let mut graph = CodeGraph::new();
806 let path = Path::new("lib.rs");
807 let file_id = graph.files_mut().register(path).unwrap();
808 let importer_name = graph.strings_mut().intern("importer").unwrap();
809 let target_name = graph.strings_mut().intern("serde").unwrap();
810 let importer = graph
811 .nodes_mut()
812 .alloc(
813 NodeEntry::new(NodeKind::Function, importer_name, file_id)
814 .with_qualified_name(importer_name),
815 )
816 .unwrap();
817 let target = graph
818 .nodes_mut()
819 .alloc(
820 NodeEntry::new(NodeKind::Module, target_name, file_id)
821 .with_qualified_name(target_name),
822 )
823 .unwrap();
824 graph.edges_mut().add_edge(
825 importer,
826 target,
827 EdgeKind::Imports {
828 alias: None,
829 is_wildcard: false,
830 },
831 file_id,
832 );
833
834 let snapshot = Arc::new(graph.snapshot());
835 let out = compute_relation_source_set(
836 RelationKind::Imports,
837 &RelationKey::exact("serde"),
838 &snapshot,
839 );
840 assert_eq!(out.as_ref(), &vec![importer]);
841 }
842
843 #[test]
844 fn derived_query_is_cached_and_invalidated_on_edge_bump() {
845 let mut graph = CodeGraph::new();
846 let path = Path::new("main.rs");
847 let a = alloc_function(&mut graph, "a", path);
848 let b = alloc_function(&mut graph, "b", path);
849 add_calls_edge(&mut graph, a, b);
850
851 let db = build_db_for_graph(graph);
852 let first = db.get::<ImplementsQuery>(&RelationKey::exact("nothing"));
853 assert!(first.as_ref().is_empty());
854 let second = db.get::<ImplementsQuery>(&RelationKey::exact("nothing"));
855 assert!(Arc::ptr_eq(&first, &second));
856
857 db.bump_edge_revision();
858 let third = db.get::<ImplementsQuery>(&RelationKey::exact("nothing"));
859 assert!(!Arc::ptr_eq(&first, &third));
860 }
861
862 #[test]
863 fn relation_matches_node_via_set_honours_direction() {
864 let mut graph = CodeGraph::new();
865 let path = Path::new("main.rs");
866 let a = alloc_function(&mut graph, "a", path);
867 let b = alloc_function(&mut graph, "b", path);
868 add_calls_edge(&mut graph, a, b);
869
870 let snapshot = graph.snapshot();
871 let mut endpoints = HashSet::new();
874 endpoints.insert(a);
875 assert!(relation_matches_node_via_set(
876 RelationKind::Callers,
877 b,
878 &endpoints,
879 &snapshot
880 ));
881 assert!(!relation_matches_node_via_set(
883 RelationKind::Callers,
884 a,
885 &endpoints,
886 &snapshot
887 ));
888 }
889
890 #[test]
900 fn exports_self_loop_is_rejected_in_keymatcher_path() {
901 let mut graph = CodeGraph::new();
902 let path = Path::new("lib.rs");
903 let file_id = graph.files_mut().register(path).unwrap();
904 let sym_name = graph.strings_mut().intern("self_export").unwrap();
905 let sym = graph
906 .nodes_mut()
907 .alloc(
908 NodeEntry::new(NodeKind::Function, sym_name, file_id).with_qualified_name(sym_name),
909 )
910 .unwrap();
911 graph.edges_mut().add_edge(
912 sym,
913 sym,
914 EdgeKind::Exports {
915 kind: ExportKind::Direct,
916 alias: None,
917 },
918 file_id,
919 );
920
921 let snapshot = Arc::new(graph.snapshot());
922 let out = compute_relation_source_set(
923 RelationKind::Exports,
924 &RelationKey::exact("self_export"),
925 &snapshot,
926 );
927 assert!(
928 out.as_ref().is_empty(),
929 "self-export must not satisfy exports:<own name> (regression \
930 for the Either-role self-loop skip)"
931 );
932 }
933
934 #[test]
935 fn exports_self_loop_is_rejected_in_subquery_set_path() {
936 let mut graph = CodeGraph::new();
937 let path = Path::new("lib.rs");
938 let file_id = graph.files_mut().register(path).unwrap();
939 let sym_name = graph.strings_mut().intern("self_export").unwrap();
940 let sym = graph
941 .nodes_mut()
942 .alloc(
943 NodeEntry::new(NodeKind::Function, sym_name, file_id).with_qualified_name(sym_name),
944 )
945 .unwrap();
946 graph.edges_mut().add_edge(
947 sym,
948 sym,
949 EdgeKind::Exports {
950 kind: ExportKind::Direct,
951 alias: None,
952 },
953 file_id,
954 );
955
956 let snapshot = graph.snapshot();
957 let mut endpoints = HashSet::new();
958 endpoints.insert(sym);
959 assert!(
960 !relation_matches_node_via_set(RelationKind::Exports, sym, &endpoints, &snapshot,),
961 "subquery-set path must also skip the Either-role self-loop"
962 );
963 }
964
965 #[test]
986 fn imports_per_node_semantic_aligned_across_engines() {
987 use sqry_core::query::executor::graph_eval::{import_edge_matches, import_entry_matches};
988
989 let mut graph = CodeGraph::new();
990 let path = Path::new("lib.rs");
991 let file_id = graph.files_mut().register(path).unwrap();
992 let importer_name = graph.strings_mut().intern("importer").unwrap();
993 let alias_importer_name = graph.strings_mut().intern("alias_importer").unwrap();
994 let wildcard_importer_name = graph.strings_mut().intern("wildcard_importer").unwrap();
995 let import_node_name = graph.strings_mut().intern("serde").unwrap();
996 let unrelated_name = graph.strings_mut().intern("unrelated").unwrap();
997 let target_name = graph.strings_mut().intern("serde").unwrap();
998 let aliased_target_name = graph.strings_mut().intern("private_target").unwrap();
999 let alias_text = graph.strings_mut().intern("serde").unwrap();
1000 let wildcard_target_name = graph.strings_mut().intern("anything").unwrap();
1001
1002 let importer = graph
1004 .nodes_mut()
1005 .alloc(
1006 NodeEntry::new(NodeKind::Function, importer_name, file_id)
1007 .with_qualified_name(importer_name),
1008 )
1009 .unwrap();
1010 let alias_importer = graph
1013 .nodes_mut()
1014 .alloc(
1015 NodeEntry::new(NodeKind::Function, alias_importer_name, file_id)
1016 .with_qualified_name(alias_importer_name),
1017 )
1018 .unwrap();
1019 let wildcard_importer = graph
1021 .nodes_mut()
1022 .alloc(
1023 NodeEntry::new(NodeKind::Function, wildcard_importer_name, file_id)
1024 .with_qualified_name(wildcard_importer_name),
1025 )
1026 .unwrap();
1027 let import_node = graph
1030 .nodes_mut()
1031 .alloc(
1032 NodeEntry::new(NodeKind::Import, import_node_name, file_id)
1033 .with_qualified_name(import_node_name),
1034 )
1035 .unwrap();
1036 let unrelated = graph
1039 .nodes_mut()
1040 .alloc(
1041 NodeEntry::new(NodeKind::Function, unrelated_name, file_id)
1042 .with_qualified_name(unrelated_name),
1043 )
1044 .unwrap();
1045 let target = graph
1046 .nodes_mut()
1047 .alloc(
1048 NodeEntry::new(NodeKind::Module, target_name, file_id)
1049 .with_qualified_name(target_name),
1050 )
1051 .unwrap();
1052 let aliased_target = graph
1053 .nodes_mut()
1054 .alloc(
1055 NodeEntry::new(NodeKind::Module, aliased_target_name, file_id)
1056 .with_qualified_name(aliased_target_name),
1057 )
1058 .unwrap();
1059 let wildcard_target = graph
1060 .nodes_mut()
1061 .alloc(
1062 NodeEntry::new(NodeKind::Module, wildcard_target_name, file_id)
1063 .with_qualified_name(wildcard_target_name),
1064 )
1065 .unwrap();
1066
1067 graph.edges_mut().add_edge(
1069 importer,
1070 target,
1071 EdgeKind::Imports {
1072 alias: None,
1073 is_wildcard: false,
1074 },
1075 file_id,
1076 );
1077 graph.edges_mut().add_edge(
1079 alias_importer,
1080 aliased_target,
1081 EdgeKind::Imports {
1082 alias: Some(alias_text),
1083 is_wildcard: false,
1084 },
1085 file_id,
1086 );
1087 graph.edges_mut().add_edge(
1089 wildcard_importer,
1090 wildcard_target,
1091 EdgeKind::Imports {
1092 alias: None,
1093 is_wildcard: true,
1094 },
1095 file_id,
1096 );
1097
1098 let snapshot = Arc::new(graph.snapshot());
1099
1100 let sqry_db_matches_for = |key: &str| -> Arc<Vec<NodeId>> {
1101 compute_relation_source_set(RelationKind::Imports, &RelationKey::exact(key), &snapshot)
1102 };
1103
1104 let snapshot_ref: &GraphSnapshot = snapshot.as_ref();
1107 let graph_eval_matches = |node_id: NodeId, key: &str| -> bool {
1108 let entry = match snapshot_ref.nodes().get(node_id) {
1109 Some(e) => e,
1110 None => return false,
1111 };
1112 if entry.kind == NodeKind::Import && import_entry_matches(snapshot_ref, entry, key) {
1113 return true;
1114 }
1115 snapshot_ref
1116 .edges()
1117 .edges_from(node_id)
1118 .iter()
1119 .any(|edge| import_edge_matches(snapshot_ref, edge, key))
1120 };
1121
1122 let cases: &[(&str, NodeId, &str, bool)] = &[
1124 ("serde", importer, "importer (target match)", true),
1125 (
1126 "serde",
1127 alias_importer,
1128 "alias_importer (alias match)",
1129 true,
1130 ),
1131 (
1132 "*",
1133 wildcard_importer,
1134 "wildcard_importer (wildcard *)",
1135 true,
1136 ),
1137 ("serde", import_node, "import_node (Import fast-path)", true),
1138 ("serde", unrelated, "unrelated (no outgoing Imports)", false),
1139 ("serde", target, "target (no outgoing Imports)", false),
1140 ];
1141
1142 for &(key, node, label, expected) in cases {
1143 let db_set = sqry_db_matches_for(key);
1144 let db_hit = db_set.contains(&node);
1145 let ge_hit = graph_eval_matches(node, key);
1146 assert_eq!(
1147 db_hit, ge_hit,
1148 "{label}: sqry-db ImportsQuery and graph_eval::match_imports \
1149 must agree under per-node semantics for key={key:?} \
1150 (db_hit={db_hit}, ge_hit={ge_hit})"
1151 );
1152 assert_eq!(
1153 db_hit, expected,
1154 "{label}: expected match={expected} for key={key:?}"
1155 );
1156 }
1157 }
1158}
1159
1160#[cfg(test)]
1165mod serde_roundtrip {
1166 use super::*;
1167 use crate::planner::ir::{MatchMode, RegexFlags, RegexPattern, StringPattern};
1168 use postcard::{from_bytes, to_allocvec};
1169
1170 #[test]
1171 fn relation_key_pattern_roundtrip() {
1172 let original = RelationKey::Pattern(StringPattern {
1173 raw: "my_function".to_string(),
1174 mode: MatchMode::Exact,
1175 case_insensitive: false,
1176 });
1177 let bytes = to_allocvec(&original).expect("serialize failed");
1178 let decoded: RelationKey = from_bytes(&bytes).expect("deserialize failed");
1179 assert_eq!(decoded, original);
1180 }
1181
1182 #[test]
1183 fn relation_key_regex_roundtrip() {
1184 let original = RelationKey::Regex(RegexPattern {
1185 pattern: "fn_.*".to_string(),
1186 flags: RegexFlags {
1187 case_insensitive: true,
1188 multiline: false,
1189 dot_all: false,
1190 },
1191 });
1192 let bytes = to_allocvec(&original).expect("serialize failed");
1193 let decoded: RelationKey = from_bytes(&bytes).expect("deserialize failed");
1194 assert_eq!(decoded, original);
1195 }
1196}