1use crate::graph::unified::FileId;
45use crate::graph::unified::concurrent::CodeGraph;
46use crate::graph::unified::edge::kind::TypeOfContext;
47use crate::graph::unified::edge::{EdgeKind, StoreEdgeRef};
48use crate::graph::unified::node::{NodeId, NodeKind};
49use crate::graph::unified::resolution::{
50 canonicalize_graph_qualified_name, display_graph_qualified_name,
51};
52use crate::graph::unified::storage::arena::NodeEntry;
53use crate::plugin::PluginManager;
54use crate::query::name_matching::segments_match;
55use crate::query::regex_cache::{CompiledRegex, get_or_compile_regex};
56use crate::query::types::{Condition, Expr, JoinEdgeKind, JoinExpr, Operator, Value};
57use anyhow::{Result, anyhow};
58use std::collections::{HashMap, HashSet};
59use std::path::Path;
60
61fn regex_is_match(re: &CompiledRegex, text: &str) -> bool {
65 match re.is_match(text) {
66 Ok(b) => b,
67 Err(e) => {
68 log::warn!("regex match aborted (backtrack limit?): {e}");
69 false
70 }
71 }
72}
73use std::sync::Arc;
74
75type SubqueryCache = HashMap<(usize, usize), Arc<HashSet<NodeId>>>;
82
83pub struct GraphEvalContext<'a> {
88 pub graph: &'a CodeGraph,
90 pub plugin_manager: &'a PluginManager,
92 pub workspace_root: Option<&'a Path>,
94 pub disable_parallel: bool,
96 pub subquery_cache: SubqueryCache,
99 pub cancellation: crate::query::cancellation::CancellationToken,
113 pub budget: crate::query::budget::QueryBudget,
128}
129
130impl<'a> GraphEvalContext<'a> {
131 #[must_use]
133 pub fn new(graph: &'a CodeGraph, plugin_manager: &'a PluginManager) -> Self {
134 let cancellation = crate::query::cancellation::CancellationToken::new();
138 let budget = crate::query::budget::QueryBudget::unbounded(cancellation.clone());
139 Self {
140 graph,
141 plugin_manager,
142 workspace_root: None,
143 disable_parallel: false,
144 subquery_cache: HashMap::new(),
145 cancellation,
146 budget,
147 }
148 }
149
150 #[must_use]
152 pub fn with_workspace_root(mut self, root: &'a Path) -> Self {
153 self.workspace_root = Some(root);
154 self
155 }
156
157 #[must_use]
159 pub fn with_parallel_disabled(mut self, disabled: bool) -> Self {
160 self.disable_parallel = disabled;
161 self
162 }
163
164 #[must_use]
172 pub fn with_cancellation(
173 mut self,
174 token: crate::query::cancellation::CancellationToken,
175 ) -> Self {
176 self.cancellation = token.clone();
182 let max_rows = self.budget.max_rows;
185 self.budget = crate::query::budget::QueryBudget::new(max_rows, token);
186 self
187 }
188
189 #[must_use]
197 pub fn with_budget(mut self, budget: crate::query::budget::QueryBudget) -> Self {
198 self.cancellation = budget.cancel.clone();
199 self.budget = budget;
200 self
201 }
202
203 pub fn precompute_subqueries(&mut self, expr: &Expr) -> Result<()> {
212 let mut subquery_exprs = Vec::new();
213 collect_subquery_exprs(expr, &mut subquery_exprs);
214
215 for (span_key, inner_expr) in subquery_exprs {
216 if !self.subquery_cache.contains_key(&span_key) {
217 let result_set = evaluate_subquery(self, inner_expr)?;
218 self.subquery_cache.insert(span_key, Arc::new(result_set));
219 }
220 }
221 Ok(())
222 }
223}
224
225fn collect_subquery_exprs<'a>(expr: &'a Expr, out: &mut Vec<((usize, usize), &'a Expr)>) {
233 match expr {
234 Expr::Condition(cond) => {
235 if let Value::Subquery(inner) = &cond.value {
236 collect_subquery_exprs(inner, out);
238 out.push(((cond.span.start, cond.span.end), inner));
240 }
241 }
242 Expr::And(operands) | Expr::Or(operands) => {
243 for op in operands {
244 collect_subquery_exprs(op, out);
245 }
246 }
247 Expr::Not(inner) => collect_subquery_exprs(inner, out),
248 Expr::Join(join) => {
249 collect_subquery_exprs(&join.left, out);
250 collect_subquery_exprs(&join.right, out);
251 }
252 }
253}
254
255pub const CANCELLATION_POLL_BATCH: usize = 1024;
280
281pub fn evaluate_all(ctx: &mut GraphEvalContext, expr: &Expr) -> Result<Vec<NodeId>> {
291 if ctx.budget.cancel.is_cancelled() {
303 ctx.budget.mark_external_cancel();
304 return Err(crate::query::QueryError::Cancelled.into());
305 }
306 ctx.precompute_subqueries(expr)?;
310
311 let arena = ctx.graph.nodes();
312
313 let recursion_limits = crate::config::RecursionLimits::load_or_default()?;
315 let expr_depth = recursion_limits.effective_expr_depth()?;
316 let mut guard = crate::query::security::RecursionGuard::new(expr_depth)?;
317
318 let _span = tracing::info_span!(
323 "graph_eval.evaluate_all",
324 budget_rows = ctx.budget.max_rows,
325 examined = tracing::field::Empty,
326 matched = tracing::field::Empty,
327 budget_exceeded = tracing::field::Empty,
328 )
329 .entered();
330
331 if ctx.cancellation.is_cancelled() {
342 ctx.budget.mark_external_cancel();
343 return finalize_span_and_return(ctx, Err(classify_cancel(&ctx.budget)), expr);
344 }
345
346 let result: Result<Vec<NodeId>> = if ctx.disable_parallel {
347 let mut matches = Vec::new();
353 let mut since_check: usize = 0;
354 let mut bail: Option<anyhow::Error> = None;
355 for (id, entry) in arena.iter() {
356 if entry.is_unified_loser() {
362 continue;
363 }
364 since_check += 1;
365 if since_check >= CANCELLATION_POLL_BATCH {
366 since_check = 0;
367 if ctx.cancellation.is_cancelled() {
368 ctx.budget.mark_external_cancel();
374 bail = Some(classify_cancel(&ctx.budget));
375 break;
376 }
377 }
378 if ctx.budget.tick().is_err() {
383 bail = Some(classify_cancel(&ctx.budget));
384 break;
385 }
386 match evaluate_node(ctx, id, expr, &mut guard) {
387 Ok(true) => matches.push(id),
388 Ok(false) => {}
389 Err(e) => {
390 bail = Some(e);
391 break;
392 }
393 }
394 }
395 if let Some(e) = bail {
396 Err(e)
397 } else {
398 Ok(matches)
399 }
400 } else {
401 use rayon::prelude::*;
403
404 let node_ids: Vec<_> = arena
405 .iter()
406 .filter(|(_id, entry)| !entry.is_unified_loser())
407 .map(|(id, _)| id)
408 .collect();
409
410 let budget = ctx.budget.clone();
414 let results: Vec<Result<Option<NodeId>>> = node_ids
415 .into_par_iter()
416 .map(|id| {
417 if budget.cancel.is_cancelled() {
422 budget.mark_external_cancel();
428 return Err(classify_cancel(&budget));
429 }
430 if budget.tick().is_err() {
431 return Err(classify_cancel(&budget));
432 }
433 let mut thread_guard = crate::query::security::RecursionGuard::new(expr_depth)?;
434 evaluate_node(ctx, id, expr, &mut thread_guard)
435 .map(|m| if m { Some(id) } else { None })
436 })
437 .collect();
438
439 let mut matches = Vec::new();
444 let mut first_err: Option<anyhow::Error> = None;
445 for result in results {
446 match result {
447 Ok(Some(id)) => matches.push(id),
448 Ok(None) => {}
449 Err(e) => {
450 if first_err.is_none() {
451 first_err = Some(e);
452 }
453 }
454 }
455 }
456 if let Some(e) = first_err {
457 Err(e)
458 } else {
459 Ok(matches)
460 }
461 };
462
463 finalize_span_and_return(ctx, result, expr)
464}
465
466fn classify_cancel(budget: &crate::query::budget::QueryBudget) -> anyhow::Error {
482 match budget.source() {
483 crate::query::budget::CancellationSource::Budget => {
484 anyhow::Error::from(crate::query::budget::BudgetExceeded {
485 examined: budget.examined.load(std::sync::atomic::Ordering::Relaxed),
486 limit: budget.max_rows,
487 predicate_shape: None,
488 })
489 }
490 crate::query::budget::CancellationSource::External
491 | crate::query::budget::CancellationSource::None => {
492 anyhow::Error::from(crate::query::error::QueryError::Cancelled)
493 }
494 }
495}
496
497fn finalize_span_and_return(
507 ctx: &GraphEvalContext,
508 result: Result<Vec<NodeId>>,
509 expr: &Expr,
510) -> Result<Vec<NodeId>> {
511 let examined = ctx
512 .budget
513 .examined
514 .load(std::sync::atomic::Ordering::Relaxed);
515 let span = tracing::Span::current();
516 span.record("examined", examined);
517 match result {
518 Ok(m) => {
519 span.record("matched", m.len() as u64);
520 span.record("budget_exceeded", false);
521 Ok(m)
522 }
523 Err(e)
524 if e.downcast_ref::<crate::query::budget::BudgetExceeded>()
525 .is_some() =>
526 {
527 span.record("matched", 0u64);
528 span.record("budget_exceeded", true);
529 let shape = expr.shape_summary();
530 tracing::warn!(
531 examined,
532 budget = ctx.budget.max_rows,
533 predicate = %shape,
534 "query exceeded row budget — cancellation triggered"
535 );
536 let mut budget_err = e
542 .downcast::<crate::query::budget::BudgetExceeded>()
543 .expect("matched arm guarantees downcast");
544 budget_err.predicate_shape = Some(shape);
545 Err(anyhow::Error::from(budget_err))
546 }
547 Err(other) => {
548 span.record("matched", 0u64);
549 span.record("budget_exceeded", false);
550 Err(other)
551 }
552 }
553}
554
555pub fn evaluate_node(
561 ctx: &GraphEvalContext,
562 node_id: NodeId,
563 expr: &Expr,
564 guard: &mut crate::query::security::RecursionGuard,
565) -> Result<bool> {
566 guard.enter()?;
567
568 let result = match expr {
569 Expr::Condition(cond) => evaluate_condition(ctx, node_id, cond),
570 Expr::And(operands) => {
571 for operand in operands {
572 if !evaluate_node(ctx, node_id, operand, guard)? {
573 guard.exit();
574 return Ok(false);
575 }
576 }
577 Ok(true)
578 }
579 Expr::Or(operands) => {
580 for operand in operands {
581 if evaluate_node(ctx, node_id, operand, guard)? {
582 guard.exit();
583 return Ok(true);
584 }
585 }
586 Ok(false)
587 }
588 Expr::Not(inner) => Ok(!evaluate_node(ctx, node_id, inner, guard)?),
589 Expr::Join(_) => {
590 Err(anyhow::anyhow!(
593 "Join expressions cannot be evaluated per-node; use execute_join instead"
594 ))
595 }
596 };
597
598 guard.exit();
599 result
600}
601
602fn evaluate_condition(ctx: &GraphEvalContext, node_id: NodeId, cond: &Condition) -> Result<bool> {
603 let Some(entry) = ctx.graph.nodes().get(node_id) else {
604 return Ok(false);
605 };
606
607 match cond.field.as_str() {
608 "kind" => Ok(match_kind(ctx, entry, &cond.operator, &cond.value)),
609 "name" => Ok(match_name(ctx, entry, &cond.operator, &cond.value)),
610 "path" => Ok(match_path(ctx, entry, &cond.operator, &cond.value)),
611 "lang" | "language" => Ok(match_lang(ctx, entry, &cond.operator, &cond.value)),
612 "visibility" => Ok(match_visibility(ctx, entry, &cond.operator, &cond.value)),
613 "async" => Ok(match_async(entry, &cond.operator, &cond.value)),
614 "static" => Ok(match_static(entry, &cond.operator, &cond.value)),
615 "callers" => {
616 if matches!(cond.value, Value::Subquery(_)) {
617 let key = (cond.span.start, cond.span.end);
618 let cached = ctx.subquery_cache.get(&key).cloned();
619 match_callers_subquery(ctx, node_id, cached.as_deref())
620 } else {
621 Ok(match_callers(ctx, node_id, &cond.value))
622 }
623 }
624 "callees" => {
625 if matches!(cond.value, Value::Subquery(_)) {
626 let key = (cond.span.start, cond.span.end);
627 let cached = ctx.subquery_cache.get(&key).cloned();
628 match_callees_subquery(ctx, node_id, cached.as_deref())
629 } else {
630 Ok(match_callees(ctx, node_id, &cond.value))
631 }
632 }
633 "imports" => {
634 if matches!(cond.value, Value::Subquery(_)) {
635 let key = (cond.span.start, cond.span.end);
636 let cached = ctx.subquery_cache.get(&key).cloned();
637 match_imports_subquery(ctx, node_id, cached.as_deref())
638 } else {
639 Ok(match_imports(ctx, node_id, &cond.value))
640 }
641 }
642 "exports" => {
643 if matches!(cond.value, Value::Subquery(_)) {
644 let key = (cond.span.start, cond.span.end);
645 let cached = ctx.subquery_cache.get(&key).cloned();
646 match_exports_subquery(ctx, node_id, cached.as_deref())
647 } else {
648 Ok(match_exports(ctx, node_id, &cond.value))
649 }
650 }
651 "references" => {
652 if matches!(cond.value, Value::Subquery(_)) {
653 let key = (cond.span.start, cond.span.end);
654 let cached = ctx.subquery_cache.get(&key).cloned();
655 match_references_subquery(ctx, node_id, cached.as_deref())
656 } else {
657 Ok(match_references(ctx, node_id, &cond.operator, &cond.value))
658 }
659 }
660 "impl" | "implements" => {
661 if matches!(cond.value, Value::Subquery(_)) {
662 let key = (cond.span.start, cond.span.end);
663 let cached = ctx.subquery_cache.get(&key).cloned();
664 match_implements_subquery(ctx, node_id, cached.as_deref())
665 } else {
666 Ok(match_implements(ctx, node_id, &cond.value))
667 }
668 }
669 field if field.starts_with("scope.") => Ok(match_scope(
670 ctx,
671 node_id,
672 field,
673 &cond.operator,
674 &cond.value,
675 )),
676 "returns" => Ok(match_returns(
677 ctx,
678 node_id,
679 entry,
680 &cond.operator,
681 &cond.value,
682 )),
683 "address_taken" => Ok(match_address_taken(
691 ctx,
692 node_id,
693 &cond.operator,
694 &cond.value,
695 )),
696 "callsite_promiscuous" => Ok(match_callsite_promiscuous(
697 ctx,
698 node_id,
699 &cond.operator,
700 &cond.value,
701 )),
702 "resolved_via" => Ok(match_resolved_via(ctx, node_id, &cond.value)),
703 field if is_plugin_field(ctx, field) => Err(anyhow!(
704 "Plugin field '{field}' requires metadata not available in graph backend"
705 )),
706 _ => Ok(false), }
708}
709
710fn is_plugin_field(ctx: &GraphEvalContext, field: &str) -> bool {
712 let is_registered_field = ctx
714 .plugin_manager
715 .plugins()
716 .iter()
717 .flat_map(|plugin| plugin.fields().iter())
718 .any(|descriptor| descriptor.name == field);
719
720 if is_registered_field {
721 return true;
722 }
723
724 matches!(
727 field,
728 "abstract" | "final" | "generic" | "parameters" | "arity"
729 )
730}
731
732fn normalize_kind(kind: &str) -> &str {
741 match kind {
742 "trait" => "interface", "impl" => "implementation",
745 "field" => "property",
747 "namespace" => "module",
749 "element" => "component",
751 "style" => "style_rule",
753 "at_rule" => "style_at_rule",
754 "css_var" | "custom_property" => "style_variable",
755 _ => kind,
757 }
758}
759
760fn match_kind(
761 _ctx: &GraphEvalContext,
762 entry: &NodeEntry,
763 operator: &Operator,
764 value: &Value,
765) -> bool {
766 let actual = entry.kind.as_str();
767
768 match (operator, value) {
769 (Operator::Equal, Value::String(expected)) => {
770 let normalized_expected = normalize_kind(expected);
771 let normalized_actual = normalize_kind(actual);
772 normalized_actual == normalized_expected
773 }
774 (Operator::Regex, Value::Regex(regex_val)) => get_or_compile_regex(
775 ®ex_val.pattern,
776 regex_val.flags.case_insensitive,
777 regex_val.flags.multiline,
778 regex_val.flags.dot_all,
779 )
780 .map(|re| regex_is_match(&re, actual))
781 .unwrap_or(false),
782 _ => false,
783 }
784}
785
786fn match_name(
791 ctx: &GraphEvalContext,
792 entry: &NodeEntry,
793 operator: &Operator,
794 value: &Value,
795) -> bool {
796 match (operator, value) {
797 (Operator::Equal, Value::String(expected)) => {
802 entry_query_texts(ctx.graph, entry).iter().any(|candidate| {
803 language_aware_segments_match(ctx.graph, entry.file, candidate, expected)
804 })
805 }
806 (Operator::Regex, Value::Regex(regex_val)) => get_or_compile_regex(
807 ®ex_val.pattern,
808 regex_val.flags.case_insensitive,
809 regex_val.flags.multiline,
810 regex_val.flags.dot_all,
811 )
812 .map(|re| {
813 entry_query_texts(ctx.graph, entry)
814 .iter()
815 .any(|candidate| regex_is_match(&re, candidate))
816 })
817 .unwrap_or(false),
818 _ => false,
819 }
820}
821
822fn is_relative_pattern(pattern: &str) -> bool {
828 !pattern.starts_with('/')
829}
830
831fn match_path(
832 ctx: &GraphEvalContext,
833 entry: &NodeEntry,
834 operator: &Operator,
835 value: &Value,
836) -> bool {
837 let Some(file_path) = ctx.graph.files().resolve(entry.file) else {
838 return false;
839 };
840
841 match (operator, value) {
842 (Operator::Equal, Value::String(pattern)) => {
843 let match_path = if is_relative_pattern(pattern) {
845 if let Some(root) = ctx.workspace_root {
846 file_path
847 .strip_prefix(root)
848 .map_or_else(|_| file_path.to_path_buf(), std::path::Path::to_path_buf)
849 } else {
850 file_path.to_path_buf()
851 }
852 } else {
853 file_path.to_path_buf()
855 };
856 globset::Glob::new(pattern)
857 .map(|g| g.compile_matcher().is_match(&match_path))
858 .unwrap_or(false)
859 }
860 (Operator::Regex, Value::Regex(regex_val)) => {
861 get_or_compile_regex(
863 ®ex_val.pattern,
864 regex_val.flags.case_insensitive,
865 regex_val.flags.multiline,
866 regex_val.flags.dot_all,
867 )
868 .map(|re| regex_is_match(&re, file_path.to_string_lossy().as_ref()))
869 .unwrap_or(false)
870 }
871 _ => false,
872 }
873}
874
875fn language_to_canonical(lang: crate::graph::node::Language) -> &'static str {
885 use crate::graph::node::Language;
886 match lang {
887 Language::C => "c",
888 Language::Cpp => "cpp",
889 Language::CSharp => "csharp",
890 Language::Css => "css",
891 Language::JavaScript => "javascript",
892 Language::Python => "python",
893 Language::TypeScript => "typescript",
894 Language::Rust => "rust",
895 Language::Go => "go",
896 Language::Java => "java",
897 Language::Ruby => "ruby",
898 Language::Php => "php",
899 Language::Swift => "swift",
900 Language::Kotlin => "kotlin",
901 Language::Scala => "scala",
902 Language::Sql => "sql",
903 Language::Dart => "dart",
904 Language::Lua => "lua",
905 Language::Perl => "perl",
906 Language::Shell => "shell",
907 Language::Groovy => "groovy",
908 Language::Elixir => "elixir",
909 Language::R => "r",
910 Language::Haskell => "haskell",
911 Language::Html => "html",
912 Language::Svelte => "svelte",
913 Language::Vue => "vue",
914 Language::Zig => "zig",
915 Language::Terraform => "terraform",
916 Language::Puppet => "puppet",
917 Language::Pulumi => "pulumi",
918 Language::Http => "http",
919 Language::Plsql => "plsql",
920 Language::Apex => "apex",
921 Language::Abap => "abap",
922 Language::ServiceNow => "servicenow",
923 Language::Json => "json",
924 }
925}
926
927fn match_lang(
928 ctx: &GraphEvalContext,
929 entry: &NodeEntry,
930 operator: &Operator,
931 value: &Value,
932) -> bool {
933 let Some(lang) = ctx.graph.files().language_for_file(entry.file) else {
935 return false;
936 };
937
938 let actual = language_to_canonical(lang);
940
941 match (operator, value) {
943 (Operator::Equal, Value::String(expected)) => actual == expected,
944 (Operator::Regex, Value::Regex(rv)) => get_or_compile_regex(
945 &rv.pattern,
946 rv.flags.case_insensitive,
947 rv.flags.multiline,
948 rv.flags.dot_all,
949 )
950 .map(|re| regex_is_match(&re, actual))
951 .unwrap_or(false),
952 _ => false,
953 }
954}
955
956fn match_visibility(
961 ctx: &GraphEvalContext,
962 entry: &NodeEntry,
963 operator: &Operator,
964 value: &Value,
965) -> bool {
966 let Some(expected) = value.as_string() else {
967 return false;
968 };
969
970 let normalized_expected = if expected == "pub" {
971 "public"
972 } else {
973 expected
974 };
975
976 let Some(vis_id) = entry.visibility else {
977 return match operator {
979 Operator::Equal => normalized_expected == "private",
980 _ => false,
981 };
982 };
983
984 let Some(actual) = ctx.graph.strings().resolve(vis_id) else {
985 return false;
986 };
987 let normalized_actual = if actual.as_ref().starts_with("pub") {
988 "public"
989 } else {
990 actual.as_ref()
991 };
992
993 match operator {
995 Operator::Equal => normalized_actual == normalized_expected,
996 _ => false,
997 }
998}
999
1000fn match_returns(
1030 ctx: &GraphEvalContext,
1031 node_id: NodeId,
1032 entry: &NodeEntry,
1033 operator: &Operator,
1034 value: &Value,
1035) -> bool {
1036 let Some(expected) = value.as_string() else {
1037 return false;
1038 };
1039
1040 if !matches!(entry.kind, NodeKind::Function | NodeKind::Method) {
1043 return false;
1044 }
1045
1046 if !matches!(operator, Operator::Equal) {
1047 return false;
1048 }
1049
1050 let nodes = ctx.graph.nodes();
1051 let strings = ctx.graph.strings();
1052 for edge in ctx.graph.edges().edges_from(node_id) {
1053 if !matches!(
1054 edge.kind,
1055 EdgeKind::TypeOf {
1056 context: Some(TypeOfContext::Return),
1057 ..
1058 }
1059 ) {
1060 continue;
1061 }
1062 let Some(target_entry) = nodes.get(edge.target) else {
1063 continue;
1064 };
1065 if let Some(name) = strings.resolve(target_entry.name)
1066 && name.as_ref() == expected
1067 {
1068 return true;
1069 }
1070 }
1071 false
1072}
1073
1074fn match_async(entry: &NodeEntry, operator: &Operator, value: &Value) -> bool {
1083 let expected = value_to_bool(value);
1084 let Some(expected) = expected else {
1085 return false;
1086 };
1087
1088 match operator {
1089 Operator::Equal => entry.is_async == expected,
1090 _ => false,
1091 }
1092}
1093
1094fn match_static(entry: &NodeEntry, operator: &Operator, value: &Value) -> bool {
1099 let expected = value_to_bool(value);
1100 let Some(expected) = expected else {
1101 return false;
1102 };
1103
1104 match operator {
1105 Operator::Equal => entry.is_static == expected,
1106 _ => false,
1107 }
1108}
1109
1110fn value_to_bool(value: &Value) -> Option<bool> {
1118 match value {
1119 Value::Boolean(b) => Some(*b),
1120 Value::String(s) => match s.to_lowercase().as_str() {
1121 "true" | "yes" | "1" => Some(true),
1122 "false" | "no" | "0" => Some(false),
1123 _ => None,
1124 },
1125 _ => None,
1126 }
1127}
1128
1129fn match_address_taken(
1147 ctx: &GraphEvalContext,
1148 node_id: NodeId,
1149 operator: &Operator,
1150 value: &Value,
1151) -> bool {
1152 let Some(expected) = value_to_bool(value) else {
1153 return false;
1154 };
1155 match operator {
1156 Operator::Equal => ctx.graph.macro_metadata().is_address_taken(node_id) == expected,
1157 _ => false,
1158 }
1159}
1160
1161fn match_callsite_promiscuous(
1169 ctx: &GraphEvalContext,
1170 node_id: NodeId,
1171 operator: &Operator,
1172 value: &Value,
1173) -> bool {
1174 let Some(expected) = value_to_bool(value) else {
1175 return false;
1176 };
1177 match operator {
1178 Operator::Equal => ctx.graph.macro_metadata().is_callsite_promiscuous(node_id) == expected,
1179 _ => false,
1180 }
1181}
1182
1183fn match_resolved_via(ctx: &GraphEvalContext, node_id: NodeId, value: &Value) -> bool {
1198 let Some(want_str) = value.as_string() else {
1199 return false;
1200 };
1201 let want = match want_str {
1202 "direct" => crate::graph::unified::edge::ResolvedVia::Direct,
1203 "type_match" => crate::graph::unified::edge::ResolvedVia::TypeMatch,
1204 "binding_plane" => crate::graph::unified::edge::ResolvedVia::BindingPlane,
1205 _ => return false, };
1207 for edge in ctx.graph.edges().edges_from(node_id) {
1208 if let EdgeKind::Calls { resolved_via, .. } = &edge.kind
1209 && *resolved_via == want
1210 {
1211 return true;
1212 }
1213 }
1214 false
1215}
1216
1217fn match_callers(ctx: &GraphEvalContext, node_id: NodeId, value: &Value) -> bool {
1231 let Some(target_name) = value.as_string() else {
1232 return false;
1233 };
1234
1235 let method_part = extract_method_name(target_name);
1238
1239 for edge in ctx.graph.edges().edges_from(node_id) {
1241 if let EdgeKind::Calls { .. } = &edge.kind
1242 && let Some(target_entry) = ctx.graph.nodes().get(edge.target)
1243 {
1244 let callee_names = entry_query_texts(ctx.graph, target_entry);
1245
1246 if callee_names.iter().any(|callee_name| {
1247 language_aware_segments_match(
1248 ctx.graph,
1249 target_entry.file,
1250 callee_name,
1251 target_name,
1252 )
1253 }) {
1254 return true;
1255 }
1256
1257 if let Some(method) = &method_part
1261 && callee_names
1262 .iter()
1263 .filter_map(|callee_name| extract_method_name(callee_name))
1264 .any(|callee_method| method == &callee_method)
1265 {
1266 return true;
1267 }
1268 }
1269 }
1270 false
1271}
1272
1273#[must_use]
1277pub fn extract_method_name(qualified: &str) -> Option<String> {
1278 for sep in ["::", ".", "#", ":", "/"] {
1280 if let Some(pos) = qualified.rfind(sep) {
1281 let method = &qualified[pos + sep.len()..];
1282 if !method.is_empty() {
1283 return Some(method.to_string());
1284 }
1285 }
1286 }
1287 None
1288}
1289
1290fn match_callees(ctx: &GraphEvalContext, node_id: NodeId, value: &Value) -> bool {
1294 let Some(caller_name) = value.as_string() else {
1295 return false;
1296 };
1297
1298 for edge in ctx.graph.edges().edges_to(node_id) {
1300 if let EdgeKind::Calls { .. } = &edge.kind
1301 && let Some(source_entry) = ctx.graph.nodes().get(edge.source)
1302 && entry_query_texts(ctx.graph, source_entry)
1303 .iter()
1304 .any(|source_name| {
1305 language_aware_segments_match(
1306 ctx.graph,
1307 source_entry.file,
1308 source_name,
1309 caller_name,
1310 )
1311 })
1312 {
1313 return true;
1314 }
1315 }
1316 false
1317}
1318
1319fn match_imports(ctx: &GraphEvalContext, node_id: NodeId, value: &Value) -> bool {
1335 let Some(target_module) = value.as_string() else {
1336 return false;
1337 };
1338
1339 let Some(entry) = ctx.graph.nodes().get(node_id) else {
1340 return false;
1341 };
1342
1343 if entry.kind == NodeKind::Import && import_entry_matches(ctx.graph, entry, target_module) {
1344 return true;
1345 }
1346
1347 for edge in ctx.graph.edges().edges_from(node_id) {
1348 if import_edge_matches(ctx.graph, &edge, target_module) {
1349 return true;
1350 }
1351 }
1352 false
1353}
1354
1355#[must_use]
1361pub fn import_edge_matches<G: crate::graph::unified::concurrent::GraphAccess>(
1362 graph: &G,
1363 edge: &StoreEdgeRef,
1364 target_module: &str,
1365) -> bool {
1366 let EdgeKind::Imports { alias, is_wildcard } = &edge.kind else {
1367 return false;
1368 };
1369
1370 let target_match = graph
1372 .nodes()
1373 .get(edge.target)
1374 .is_some_and(|entry| import_entry_matches(graph, entry, target_module));
1375
1376 let alias_match = alias
1378 .and_then(|sid| graph.strings().resolve(sid))
1379 .is_some_and(|alias_str| {
1380 graph.nodes().get(edge.source).is_some_and(|entry| {
1381 import_text_matches(graph, entry.file, alias_str.as_ref(), target_module)
1382 })
1383 });
1384
1385 let wildcard_match = *is_wildcard && target_module == "*";
1387
1388 target_match || alias_match || wildcard_match
1389}
1390
1391#[must_use]
1395pub fn import_text_matches<G: crate::graph::unified::concurrent::GraphAccess>(
1396 graph: &G,
1397 file_id: FileId,
1398 candidate: &str,
1399 target_module: &str,
1400) -> bool {
1401 if candidate.contains(target_module) {
1402 return true;
1403 }
1404
1405 graph
1406 .files()
1407 .language_for_file(file_id)
1408 .is_some_and(|language| {
1409 let canonical_target = canonicalize_graph_qualified_name(language, target_module);
1410 canonical_target != target_module && candidate.contains(&canonical_target)
1411 })
1412}
1413
1414#[must_use]
1417pub fn import_entry_matches<G: crate::graph::unified::concurrent::GraphAccess>(
1418 graph: &G,
1419 entry: &NodeEntry,
1420 target_module: &str,
1421) -> bool {
1422 entry_query_texts(graph, entry)
1423 .iter()
1424 .any(|candidate| import_text_matches(graph, entry.file, candidate, target_module))
1425}
1426
1427#[must_use]
1432pub fn language_aware_segments_match<G: crate::graph::unified::concurrent::GraphAccess>(
1433 graph: &G,
1434 file_id: FileId,
1435 candidate: &str,
1436 expected: &str,
1437) -> bool {
1438 if segments_match(candidate, expected) {
1439 return true;
1440 }
1441
1442 graph
1443 .files()
1444 .language_for_file(file_id)
1445 .is_some_and(|language| {
1446 let canonical_expected = canonicalize_graph_qualified_name(language, expected);
1447 canonical_expected != expected && segments_match(candidate, &canonical_expected)
1448 })
1449}
1450
1451fn push_unique_query_text(texts: &mut Vec<String>, candidate: impl Into<String>) {
1452 let candidate = candidate.into();
1453 if !texts.iter().any(|existing| existing == &candidate) {
1454 texts.push(candidate);
1455 }
1456}
1457
1458#[must_use]
1464pub fn entry_query_texts<G: crate::graph::unified::concurrent::GraphAccess>(
1465 graph: &G,
1466 entry: &NodeEntry,
1467) -> Vec<String> {
1468 let mut texts = Vec::with_capacity(3);
1469
1470 if let Some(name) = graph.strings().resolve(entry.name) {
1471 push_unique_query_text(&mut texts, name.to_string());
1472 }
1473
1474 if let Some(qualified) = entry
1475 .qualified_name
1476 .and_then(|qualified_name_id| graph.strings().resolve(qualified_name_id))
1477 {
1478 push_unique_query_text(&mut texts, qualified.to_string());
1479
1480 if let Some(language) = graph.files().language_for_file(entry.file) {
1481 push_unique_query_text(
1482 &mut texts,
1483 display_graph_qualified_name(
1484 language,
1485 qualified.as_ref(),
1486 entry.kind,
1487 entry.is_static,
1488 ),
1489 );
1490 }
1491 }
1492
1493 texts
1494}
1495
1496fn match_exports(ctx: &GraphEvalContext, node_id: NodeId, value: &Value) -> bool {
1502 let Some(target_name) = value.as_string() else {
1503 return false;
1504 };
1505
1506 let Some(entry) = ctx.graph.nodes().get(node_id) else {
1507 return false;
1508 };
1509 let node_file = entry.file;
1510
1511 if !entry_query_texts(ctx.graph, entry).iter().any(|candidate| {
1512 language_aware_segments_match(ctx.graph, entry.file, candidate, target_name)
1513 }) {
1514 return false;
1515 }
1516
1517 let edges = ctx.graph.edges();
1518
1519 for edge in edges.edges_from(node_id) {
1521 if let EdgeKind::Exports { .. } = &edge.kind {
1522 if let Some(target_entry) = ctx.graph.nodes().get(edge.target)
1524 && target_entry.file == node_file
1525 {
1526 return true;
1527 }
1528 }
1529 }
1530
1531 for edge in edges.edges_to(node_id) {
1533 if let EdgeKind::Exports { .. } = &edge.kind {
1534 if let Some(source_entry) = ctx.graph.nodes().get(edge.source)
1536 && source_entry.file == node_file
1537 {
1538 return true;
1539 }
1540 }
1541 }
1542
1543 false
1544}
1545
1546fn match_references(
1550 ctx: &GraphEvalContext,
1551 node_id: NodeId,
1552 operator: &Operator,
1553 value: &Value,
1554) -> bool {
1555 let Some(entry) = ctx.graph.nodes().get(node_id) else {
1557 return false;
1558 };
1559
1560 let name_matches = match (operator, value) {
1561 (Operator::Equal, Value::String(target)) => entry_query_texts(ctx.graph, entry)
1562 .iter()
1563 .any(|candidate| candidate == target || candidate.ends_with(&format!("::{target}"))),
1564 (Operator::Regex, Value::Regex(rv)) => get_or_compile_regex(
1565 &rv.pattern,
1566 rv.flags.case_insensitive,
1567 rv.flags.multiline,
1568 rv.flags.dot_all,
1569 )
1570 .map(|re| {
1571 entry_query_texts(ctx.graph, entry)
1572 .iter()
1573 .any(|candidate| regex_is_match(&re, candidate))
1574 })
1575 .unwrap_or(false),
1576 _ => false,
1577 };
1578
1579 if !name_matches {
1580 return false;
1581 }
1582
1583 for edge in ctx.graph.edges().edges_to(node_id) {
1586 let is_reference = matches!(
1587 &edge.kind,
1588 EdgeKind::References
1589 | EdgeKind::Calls { .. }
1590 | EdgeKind::Imports { .. }
1591 | EdgeKind::FfiCall { .. }
1592 );
1593 if is_reference {
1594 return true;
1595 }
1596 }
1597
1598 false
1599}
1600
1601fn match_implements(ctx: &GraphEvalContext, node_id: NodeId, value: &Value) -> bool {
1603 let Some(trait_name) = value.as_string() else {
1604 return false;
1605 };
1606
1607 for edge in ctx.graph.edges().edges_from(node_id) {
1608 if let EdgeKind::Implements = &edge.kind
1609 && let Some(target_entry) = ctx.graph.nodes().get(edge.target)
1610 && entry_query_texts(ctx.graph, target_entry)
1611 .iter()
1612 .any(|name| {
1613 language_aware_segments_match(ctx.graph, target_entry.file, name, trait_name)
1614 })
1615 {
1616 return true;
1617 }
1618 }
1619 false
1620}
1621
1622fn node_kind_to_scope_type(kind: NodeKind) -> &'static str {
1634 match kind {
1635 NodeKind::Function | NodeKind::Test => "function",
1636 NodeKind::Method => "method",
1637 NodeKind::Class | NodeKind::Service => "class",
1638 NodeKind::Interface | NodeKind::Trait => "interface",
1639 NodeKind::Struct => "struct",
1640 NodeKind::Enum => "enum",
1641 NodeKind::Module => "module",
1642 NodeKind::Macro => "macro",
1643 NodeKind::Component => "component",
1644 NodeKind::Resource | NodeKind::Endpoint => "resource",
1645 NodeKind::Variable => "variable",
1647 NodeKind::Constant => "constant",
1648 NodeKind::Type => "type",
1649 NodeKind::EnumVariant => "enumvariant",
1650 NodeKind::Import => "import",
1651 NodeKind::Export => "export",
1652 NodeKind::CallSite => "callsite",
1653 NodeKind::Parameter => "parameter",
1654 NodeKind::Property => "property",
1655 NodeKind::StyleRule => "style_rule",
1656 NodeKind::StyleAtRule => "style_at_rule",
1657 NodeKind::StyleVariable => "style_variable",
1658 NodeKind::Lifetime => "lifetime",
1659 NodeKind::TypeParameter => "type_parameter",
1660 NodeKind::Annotation => "annotation",
1661 NodeKind::AnnotationValue => "annotation_value",
1662 NodeKind::LambdaTarget => "lambda_target",
1663 NodeKind::JavaModule => "java_module",
1664 NodeKind::EnumConstant => "enum_constant",
1665 NodeKind::Other => "other",
1666 }
1667}
1668
1669fn match_scope(
1670 ctx: &GraphEvalContext,
1671 node_id: NodeId,
1672 field: &str,
1673 operator: &Operator,
1674 value: &Value,
1675) -> bool {
1676 let scope_part = field.strip_prefix("scope.").unwrap_or("");
1677 match scope_part {
1678 "type" => match_scope_type(ctx, node_id, operator, value),
1679 "name" => match_scope_name(ctx, node_id, operator, value),
1680 "parent" => match_scope_parent_name(ctx, node_id, operator, value),
1681 "ancestor" => match_scope_ancestor_name(ctx, node_id, operator, value),
1682 _ => false,
1683 }
1684}
1685
1686fn match_scope_type(
1687 ctx: &GraphEvalContext,
1688 node_id: NodeId,
1689 operator: &Operator,
1690 value: &Value,
1691) -> bool {
1692 for edge in ctx.graph.edges().edges_to(node_id) {
1693 if let EdgeKind::Contains = &edge.kind
1694 && let Some(parent) = ctx.graph.nodes().get(edge.source)
1695 {
1696 let scope_type = node_kind_to_scope_type(parent.kind);
1698 return match (operator, value) {
1699 (Operator::Equal, Value::String(exp)) => scope_type == exp,
1701 (Operator::Regex, Value::Regex(rv)) => get_or_compile_regex(
1702 &rv.pattern,
1703 rv.flags.case_insensitive,
1704 rv.flags.multiline,
1705 rv.flags.dot_all,
1706 )
1707 .map(|re| regex_is_match(&re, scope_type))
1708 .unwrap_or(false),
1709 _ => false,
1710 };
1711 }
1712 }
1713 false
1714}
1715
1716fn match_scope_name(
1717 ctx: &GraphEvalContext,
1718 node_id: NodeId,
1719 operator: &Operator,
1720 value: &Value,
1721) -> bool {
1722 for edge in ctx.graph.edges().edges_to(node_id) {
1723 if let EdgeKind::Contains = &edge.kind
1724 && let Some(parent) = ctx.graph.nodes().get(edge.source)
1725 && let Some(name) = ctx.graph.strings().resolve(parent.name)
1726 {
1727 return match (operator, value) {
1728 (Operator::Equal, Value::String(exp)) => segments_match(&name, exp),
1730 (Operator::Regex, Value::Regex(rv)) => get_or_compile_regex(
1731 &rv.pattern,
1732 rv.flags.case_insensitive,
1733 rv.flags.multiline,
1734 rv.flags.dot_all,
1735 )
1736 .map(|re| regex_is_match(&re, &name))
1737 .unwrap_or(false),
1738 _ => false,
1739 };
1740 }
1741 }
1742 false
1743}
1744
1745fn match_scope_parent_name(
1749 ctx: &GraphEvalContext,
1750 node_id: NodeId,
1751 operator: &Operator,
1752 value: &Value,
1753) -> bool {
1754 for edge in ctx.graph.edges().edges_to(node_id) {
1755 if let EdgeKind::Contains = &edge.kind
1756 && let Some(parent) = ctx.graph.nodes().get(edge.source)
1757 && let Some(name) = ctx.graph.strings().resolve(parent.name)
1758 {
1759 return match (operator, value) {
1760 (Operator::Equal, Value::String(exp)) => segments_match(&name, exp),
1762 (Operator::Regex, Value::Regex(rv)) => get_or_compile_regex(
1763 &rv.pattern,
1764 rv.flags.case_insensitive,
1765 rv.flags.multiline,
1766 rv.flags.dot_all,
1767 )
1768 .map(|re| regex_is_match(&re, &name))
1769 .unwrap_or(false),
1770 _ => false,
1771 };
1772 }
1773 }
1774 false
1775}
1776
1777fn match_scope_ancestor_name(
1782 ctx: &GraphEvalContext,
1783 node_id: NodeId,
1784 operator: &Operator,
1785 value: &Value,
1786) -> bool {
1787 let mut current = node_id;
1788 let mut visited = HashSet::new();
1789 visited.insert(node_id);
1790
1791 loop {
1792 let mut found_parent = false;
1793 for edge in ctx.graph.edges().edges_to(current) {
1794 if let EdgeKind::Contains = &edge.kind {
1795 if visited.contains(&edge.source) {
1797 continue;
1798 }
1799 visited.insert(edge.source);
1800
1801 found_parent = true;
1802 current = edge.source;
1803 if let Some(parent) = ctx.graph.nodes().get(current)
1804 && let Some(name) = ctx.graph.strings().resolve(parent.name)
1805 {
1806 let matches = match (operator, value) {
1807 (Operator::Equal, Value::String(exp)) => segments_match(&name, exp),
1809 (Operator::Regex, Value::Regex(rv)) => get_or_compile_regex(
1810 &rv.pattern,
1811 rv.flags.case_insensitive,
1812 rv.flags.multiline,
1813 rv.flags.dot_all,
1814 )
1815 .map(|re| regex_is_match(&re, &name))
1816 .unwrap_or(false),
1817 _ => false,
1818 };
1819 if matches {
1820 return true;
1821 }
1822 }
1823 break;
1824 }
1825 }
1826 if !found_parent {
1827 break;
1828 }
1829 }
1830 false
1831}
1832
1833pub fn evaluate_subquery(ctx: &GraphEvalContext, expr: &Expr) -> Result<HashSet<NodeId>> {
1845 let recursion_limits = crate::config::RecursionLimits::load_or_default()?;
1846 let expr_depth = recursion_limits.effective_expr_depth()?;
1847 let mut guard = crate::query::security::RecursionGuard::new(expr_depth)?;
1848
1849 let arena = ctx.graph.nodes();
1850 let mut matches = HashSet::new();
1851 for (id, _) in arena.iter() {
1852 if evaluate_node(ctx, id, expr, &mut guard)? {
1853 matches.insert(id);
1854 }
1855 }
1856 Ok(matches)
1857}
1858
1859fn match_callers_subquery(
1865 ctx: &GraphEvalContext,
1866 node_id: NodeId,
1867 subquery_matches: Option<&HashSet<NodeId>>,
1868) -> Result<bool> {
1869 let Some(matches) = subquery_matches else {
1870 return Err(anyhow!(
1871 "subquery cache miss: precompute_subqueries did not populate cache for this relation predicate"
1872 ));
1873 };
1874 for edge in ctx.graph.edges().edges_from(node_id) {
1875 if let EdgeKind::Calls { .. } = &edge.kind
1876 && matches.contains(&edge.target)
1877 {
1878 return Ok(true);
1879 }
1880 }
1881 Ok(false)
1882}
1883
1884fn match_callees_subquery(
1886 ctx: &GraphEvalContext,
1887 node_id: NodeId,
1888 subquery_matches: Option<&HashSet<NodeId>>,
1889) -> Result<bool> {
1890 let Some(matches) = subquery_matches else {
1891 return Err(anyhow!(
1892 "subquery cache miss: precompute_subqueries did not populate cache for this relation predicate"
1893 ));
1894 };
1895 for edge in ctx.graph.edges().edges_to(node_id) {
1896 if let EdgeKind::Calls { .. } = &edge.kind
1897 && matches.contains(&edge.source)
1898 {
1899 return Ok(true);
1900 }
1901 }
1902 Ok(false)
1903}
1904
1905fn match_imports_subquery(
1911 ctx: &GraphEvalContext,
1912 node_id: NodeId,
1913 subquery_matches: Option<&HashSet<NodeId>>,
1914) -> Result<bool> {
1915 let Some(matches) = subquery_matches else {
1916 return Err(anyhow!(
1917 "subquery cache miss: precompute_subqueries did not populate cache for this relation predicate"
1918 ));
1919 };
1920 for edge in ctx.graph.edges().edges_from(node_id) {
1921 if let EdgeKind::Imports { .. } = &edge.kind
1922 && matches.contains(&edge.target)
1923 {
1924 return Ok(true);
1925 }
1926 }
1927 Ok(false)
1928}
1929
1930fn match_exports_subquery(
1932 ctx: &GraphEvalContext,
1933 node_id: NodeId,
1934 subquery_matches: Option<&HashSet<NodeId>>,
1935) -> Result<bool> {
1936 let Some(matches) = subquery_matches else {
1937 return Err(anyhow!(
1938 "subquery cache miss: precompute_subqueries did not populate cache for this relation predicate"
1939 ));
1940 };
1941 for edge in ctx.graph.edges().edges_from(node_id) {
1942 if let EdgeKind::Exports { .. } = &edge.kind
1943 && matches.contains(&edge.target)
1944 {
1945 return Ok(true);
1946 }
1947 }
1948 Ok(false)
1949}
1950
1951fn match_implements_subquery(
1953 ctx: &GraphEvalContext,
1954 node_id: NodeId,
1955 subquery_matches: Option<&HashSet<NodeId>>,
1956) -> Result<bool> {
1957 let Some(matches) = subquery_matches else {
1958 return Err(anyhow!(
1959 "subquery cache miss: precompute_subqueries did not populate cache for this relation predicate"
1960 ));
1961 };
1962 for edge in ctx.graph.edges().edges_from(node_id) {
1963 if let EdgeKind::Implements = &edge.kind
1964 && matches.contains(&edge.target)
1965 {
1966 return Ok(true);
1967 }
1968 }
1969 Ok(false)
1970}
1971
1972fn match_references_subquery(
1974 ctx: &GraphEvalContext,
1975 node_id: NodeId,
1976 subquery_matches: Option<&HashSet<NodeId>>,
1977) -> Result<bool> {
1978 let Some(matches) = subquery_matches else {
1979 return Err(anyhow!(
1980 "subquery cache miss: precompute_subqueries did not populate cache for this relation predicate"
1981 ));
1982 };
1983 for edge in ctx.graph.edges().edges_to(node_id) {
1984 let is_reference = matches!(
1985 &edge.kind,
1986 EdgeKind::References
1987 | EdgeKind::Calls { .. }
1988 | EdgeKind::Imports { .. }
1989 | EdgeKind::FfiCall { .. }
1990 );
1991 if is_reference && matches.contains(&edge.source) {
1992 return Ok(true);
1993 }
1994 }
1995 Ok(false)
1996}
1997
1998pub fn evaluate_join(
2014 ctx: &GraphEvalContext,
2015 join: &JoinExpr,
2016 max_results: Option<usize>,
2017) -> Result<JoinEvalResult> {
2018 let lhs_matches = evaluate_subquery(ctx, &join.left)?;
2019 let rhs_matches = evaluate_subquery(ctx, &join.right)?;
2020 let cap = max_results.unwrap_or(DEFAULT_JOIN_RESULT_CAP);
2021
2022 let mut pairs = Vec::new();
2023 let mut truncated = false;
2024 'outer: for &lhs_id in &lhs_matches {
2025 for edge in ctx.graph.edges().edges_from(lhs_id) {
2026 if edge_matches_join_kind(&edge.kind, &join.edge) && rhs_matches.contains(&edge.target)
2027 {
2028 pairs.push((lhs_id, edge.target));
2029 if pairs.len() >= cap {
2030 truncated = true;
2031 break 'outer;
2032 }
2033 }
2034 }
2035 }
2036 Ok(JoinEvalResult { pairs, truncated })
2037}
2038
2039pub struct JoinEvalResult {
2041 pub pairs: Vec<(NodeId, NodeId)>,
2043 pub truncated: bool,
2045}
2046
2047const DEFAULT_JOIN_RESULT_CAP: usize = 10_000;
2051
2052fn edge_matches_join_kind(edge_kind: &EdgeKind, join_kind: &JoinEdgeKind) -> bool {
2054 match join_kind {
2055 JoinEdgeKind::Calls => matches!(edge_kind, EdgeKind::Calls { .. }),
2056 JoinEdgeKind::Imports => matches!(edge_kind, EdgeKind::Imports { .. }),
2057 JoinEdgeKind::Inherits => matches!(edge_kind, EdgeKind::Inherits),
2058 JoinEdgeKind::Implements => matches!(edge_kind, EdgeKind::Implements),
2059 }
2060}
2061
2062#[cfg(test)]
2063mod tests {
2064 use super::*;
2065 use crate::graph::node::Language;
2066 use crate::query::types::{Condition, Field, Span};
2067 use std::path::Path;
2068
2069 #[test]
2070 fn test_import_text_matches_canonicalized_qualified_imports() {
2071 let mut graph = CodeGraph::new();
2072 let file_id = graph
2073 .files_mut()
2074 .register(Path::new("src/FileProcessor.cs"))
2075 .unwrap();
2076 assert!(graph.files_mut().set_language(file_id, Language::CSharp));
2077
2078 assert!(import_text_matches(
2079 &graph,
2080 file_id,
2081 "System::IO",
2082 "System.IO"
2083 ));
2084 assert!(import_text_matches(
2085 &graph,
2086 file_id,
2087 "System::Collections::Generic",
2088 "System.Collections.Generic"
2089 ));
2090 assert!(!import_text_matches(
2091 &graph,
2092 file_id,
2093 "System::Text",
2094 "System.IO"
2095 ));
2096 }
2097
2098 #[test]
2099 fn test_language_aware_segments_match_supports_ruby_method_separators() {
2100 let mut graph = CodeGraph::new();
2101 let file_id = graph
2102 .files_mut()
2103 .register(Path::new("app/models/user.rb"))
2104 .unwrap();
2105 assert!(graph.files_mut().set_language(file_id, Language::Ruby));
2106
2107 assert!(language_aware_segments_match(
2108 &graph,
2109 file_id,
2110 "Admin::Users::Controller::show",
2111 "Admin::Users::Controller#show"
2112 ));
2113 assert!(language_aware_segments_match(
2114 &graph,
2115 file_id,
2116 "Admin::Users::Controller::show",
2117 "show"
2118 ));
2119 assert!(!language_aware_segments_match(
2120 &graph,
2121 file_id,
2122 "Admin::Users::Controller::index",
2123 "Admin::Users::Controller#show"
2124 ));
2125 }
2126
2127 #[test]
2128 fn test_normalize_kind() {
2129 assert_eq!(normalize_kind("trait"), "interface");
2131 assert_eq!(normalize_kind("TRAIT"), "TRAIT"); assert_eq!(normalize_kind("field"), "property");
2133 assert_eq!(normalize_kind("namespace"), "module");
2134 assert_eq!(normalize_kind("function"), "function"); }
2136
2137 #[test]
2138 fn test_graph_eval_context_builder() {
2139 let graph = CodeGraph::new();
2140 let pm = PluginManager::new();
2141 let ctx = GraphEvalContext::new(&graph, &pm)
2142 .with_workspace_root(Path::new("/test"))
2143 .with_parallel_disabled(true);
2144
2145 assert!(ctx.disable_parallel);
2146 assert_eq!(ctx.workspace_root, Some(Path::new("/test")));
2147 }
2148
2149 fn subquery_condition(field: &str, inner: Expr, start: usize, end: usize) -> Expr {
2155 Expr::Condition(Condition {
2156 field: Field(field.to_string()),
2157 operator: Operator::Equal,
2158 value: Value::Subquery(Box::new(inner)),
2159 span: Span::with_position(start, end, 1, start + 1),
2160 })
2161 }
2162
2163 fn kind_condition(kind: &str) -> Expr {
2165 Expr::Condition(Condition {
2166 field: Field("kind".to_string()),
2167 operator: Operator::Equal,
2168 value: Value::String(kind.to_string()),
2169 span: Span::default(),
2170 })
2171 }
2172
2173 #[test]
2174 fn test_collect_subquery_exprs_post_order_depth_2() {
2175 let inner_subquery = subquery_condition("callees", kind_condition("function"), 20, 40);
2179 let outer_subquery = subquery_condition("callers", inner_subquery, 0, 50);
2180
2181 let mut out = Vec::new();
2182 collect_subquery_exprs(&outer_subquery, &mut out);
2183
2184 assert_eq!(
2186 out.len(),
2187 2,
2188 "should collect both inner and outer subqueries"
2189 );
2190 assert_eq!(out[0].0, (20, 40), "inner subquery span should come first");
2191 assert_eq!(out[1].0, (0, 50), "outer subquery span should come second");
2192 }
2193
2194 #[test]
2195 fn test_collect_subquery_exprs_post_order_depth_3() {
2196 let innermost = subquery_condition("imports", kind_condition("function"), 30, 50);
2198 let middle = subquery_condition("callees", innermost, 15, 55);
2199 let outer = subquery_condition("callers", middle, 0, 60);
2200
2201 let mut out = Vec::new();
2202 collect_subquery_exprs(&outer, &mut out);
2203
2204 assert_eq!(out.len(), 3, "should collect all three nested subqueries");
2205 assert_eq!(out[0].0, (30, 50), "innermost should come first");
2206 assert_eq!(out[1].0, (15, 55), "middle should come second");
2207 assert_eq!(out[2].0, (0, 60), "outer should come last");
2208 }
2209
2210 #[test]
2211 fn test_collect_subquery_exprs_and_or_branches() {
2212 let left = subquery_condition("callers", kind_condition("function"), 0, 25);
2214 let right = subquery_condition("callees", kind_condition("method"), 30, 55);
2215 let expr = Expr::And(vec![left, right]);
2216
2217 let mut out = Vec::new();
2218 collect_subquery_exprs(&expr, &mut out);
2219
2220 assert_eq!(out.len(), 2, "should collect subqueries from both branches");
2221 assert_eq!(out[0].0, (0, 25), "left branch subquery");
2222 assert_eq!(out[1].0, (30, 55), "right branch subquery");
2223 }
2224
2225 #[test]
2226 fn test_collect_subquery_exprs_no_subqueries() {
2227 let expr = kind_condition("function");
2229
2230 let mut out = Vec::new();
2231 collect_subquery_exprs(&expr, &mut out);
2232
2233 assert!(
2234 out.is_empty(),
2235 "should collect nothing for plain conditions"
2236 );
2237 }
2238
2239 use crate::graph::unified::edge::{BidirectionalEdgeStore, FfiConvention};
2244 use crate::graph::unified::storage::{
2245 AuxiliaryIndices, FileRegistry, NodeArena, StringInterner,
2246 };
2247
2248 fn build_ffi_graph() -> (CodeGraph, NodeId, NodeId) {
2250 let mut arena = NodeArena::new();
2251 let edges = BidirectionalEdgeStore::new();
2252 let mut strings = StringInterner::new();
2253 let mut files = FileRegistry::new();
2254 let mut indices = AuxiliaryIndices::new();
2255
2256 let caller_name = strings.intern("caller_fn").unwrap();
2257 let target_name = strings.intern("ffi_target").unwrap();
2258 let file_id = files.register(Path::new("test.r")).unwrap();
2259
2260 let caller_id = arena
2261 .alloc(NodeEntry {
2262 kind: NodeKind::Function,
2263 name: caller_name,
2264 file: file_id,
2265 start_byte: 0,
2266 end_byte: 100,
2267 start_line: 1,
2268 start_column: 0,
2269 end_line: 5,
2270 end_column: 0,
2271 signature: None,
2272 doc: None,
2273 qualified_name: None,
2274 visibility: None,
2275 is_async: false,
2276 is_static: false,
2277 is_unsafe: false,
2278 body_hash: None,
2279 })
2280 .unwrap();
2281
2282 let target_id = arena
2283 .alloc(NodeEntry {
2284 kind: NodeKind::Function,
2285 name: target_name,
2286 file: file_id,
2287 start_byte: 200,
2288 end_byte: 300,
2289 start_line: 10,
2290 start_column: 0,
2291 end_line: 15,
2292 end_column: 0,
2293 signature: None,
2294 doc: None,
2295 qualified_name: None,
2296 visibility: None,
2297 is_async: false,
2298 is_static: false,
2299 is_unsafe: false,
2300 body_hash: None,
2301 })
2302 .unwrap();
2303
2304 indices.add(caller_id, NodeKind::Function, caller_name, None, file_id);
2305 indices.add(target_id, NodeKind::Function, target_name, None, file_id);
2306
2307 edges.add_edge(
2308 caller_id,
2309 target_id,
2310 EdgeKind::FfiCall {
2311 convention: FfiConvention::C,
2312 },
2313 file_id,
2314 );
2315
2316 let graph = CodeGraph::from_components(
2317 arena,
2318 edges,
2319 strings,
2320 files,
2321 indices,
2322 crate::graph::unified::NodeMetadataStore::new(),
2323 );
2324 (graph, caller_id, target_id)
2325 }
2326
2327 #[test]
2328 fn test_ffi_call_edge_in_references_predicate() {
2329 let (graph, _caller_id, target_id) = build_ffi_graph();
2330 let pm = PluginManager::new();
2331 let ctx = GraphEvalContext::new(&graph, &pm);
2332
2333 let result = match_references(
2335 &ctx,
2336 target_id,
2337 &Operator::Equal,
2338 &Value::String("ffi_target".to_string()),
2339 );
2340 assert!(result, "references: predicate should match FfiCall edges");
2341 }
2342
2343 #[test]
2344 fn test_ffi_call_edge_in_references_subquery() {
2345 let (graph, caller_id, target_id) = build_ffi_graph();
2346 let pm = PluginManager::new();
2347 let ctx = GraphEvalContext::new(&graph, &pm);
2348
2349 let mut subquery_results = HashSet::new();
2351 subquery_results.insert(caller_id);
2352
2353 let result = match_references_subquery(&ctx, target_id, Some(&subquery_results)).unwrap();
2356 assert!(
2357 result,
2358 "references subquery should match FfiCall edge sources"
2359 );
2360 }
2361
2362 fn build_returns_graph() -> (CodeGraph, NodeId, NodeId, NodeId) {
2372 let mut arena = NodeArena::new();
2373 let edges = BidirectionalEdgeStore::new();
2374 let mut strings = StringInterner::new();
2375 let mut files = FileRegistry::new();
2376 let mut indices = AuxiliaryIndices::new();
2377
2378 let returner_name = strings.intern("returner_fn").unwrap();
2379 let plain_name = strings.intern("plain_fn").unwrap();
2380 let error_name = strings.intern("error").unwrap();
2381 let file_id = files.register(Path::new("test.go")).unwrap();
2382
2383 let returner_id = arena
2384 .alloc(NodeEntry {
2385 kind: NodeKind::Function,
2386 name: returner_name,
2387 file: file_id,
2388 start_byte: 0,
2389 end_byte: 100,
2390 start_line: 1,
2391 start_column: 0,
2392 end_line: 5,
2393 end_column: 0,
2394 signature: None,
2395 doc: None,
2396 qualified_name: None,
2397 visibility: None,
2398 is_async: false,
2399 is_static: false,
2400 is_unsafe: false,
2401 body_hash: None,
2402 })
2403 .unwrap();
2404
2405 let plain_id = arena
2406 .alloc(NodeEntry {
2407 kind: NodeKind::Function,
2408 name: plain_name,
2409 file: file_id,
2410 start_byte: 200,
2411 end_byte: 300,
2412 start_line: 10,
2413 start_column: 0,
2414 end_line: 15,
2415 end_column: 0,
2416 signature: None,
2417 doc: None,
2418 qualified_name: None,
2419 visibility: None,
2420 is_async: false,
2421 is_static: false,
2422 is_unsafe: false,
2423 body_hash: None,
2424 })
2425 .unwrap();
2426
2427 let error_type_id = arena
2428 .alloc(NodeEntry {
2429 kind: NodeKind::Type,
2430 name: error_name,
2431 file: file_id,
2432 start_byte: 400,
2433 end_byte: 410,
2434 start_line: 20,
2435 start_column: 0,
2436 end_line: 20,
2437 end_column: 10,
2438 signature: None,
2439 doc: None,
2440 qualified_name: None,
2441 visibility: None,
2442 is_async: false,
2443 is_static: false,
2444 is_unsafe: false,
2445 body_hash: None,
2446 })
2447 .unwrap();
2448
2449 indices.add(
2450 returner_id,
2451 NodeKind::Function,
2452 returner_name,
2453 None,
2454 file_id,
2455 );
2456 indices.add(plain_id, NodeKind::Function, plain_name, None, file_id);
2457 indices.add(error_type_id, NodeKind::Type, error_name, None, file_id);
2458
2459 edges.add_edge(
2460 returner_id,
2461 error_type_id,
2462 EdgeKind::TypeOf {
2463 context: Some(TypeOfContext::Return),
2464 index: None,
2465 name: None,
2466 },
2467 file_id,
2468 );
2469
2470 let graph = CodeGraph::from_components(
2471 arena,
2472 edges,
2473 strings,
2474 files,
2475 indices,
2476 crate::graph::unified::NodeMetadataStore::new(),
2477 );
2478 (graph, returner_id, plain_id, error_type_id)
2479 }
2480
2481 #[test]
2482 fn test_match_returns_byte_exact_hit() {
2483 let (graph, returner_id, _plain_id, _error_id) = build_returns_graph();
2484 let pm = PluginManager::new();
2485 let ctx = GraphEvalContext::new(&graph, &pm);
2486 let entry = graph.nodes().get(returner_id).expect("returner exists");
2487
2488 assert!(match_returns(
2491 &ctx,
2492 returner_id,
2493 entry,
2494 &Operator::Equal,
2495 &Value::String("error".to_string()),
2496 ));
2497 }
2498
2499 #[test]
2500 fn test_match_returns_no_edges_misses() {
2501 let (graph, _returner_id, plain_id, _error_id) = build_returns_graph();
2502 let pm = PluginManager::new();
2503 let ctx = GraphEvalContext::new(&graph, &pm);
2504 let entry = graph.nodes().get(plain_id).expect("plain_fn exists");
2505
2506 assert!(!match_returns(
2510 &ctx,
2511 plain_id,
2512 entry,
2513 &Operator::Equal,
2514 &Value::String("error".to_string()),
2515 ));
2516 }
2517
2518 #[test]
2519 fn test_match_returns_byte_exact_miss_on_different_target_name() {
2520 let (graph, returner_id, _plain_id, _error_id) = build_returns_graph();
2521 let pm = PluginManager::new();
2522 let ctx = GraphEvalContext::new(&graph, &pm);
2523 let entry = graph.nodes().get(returner_id).expect("returner exists");
2524
2525 assert!(!match_returns(
2529 &ctx,
2530 returner_id,
2531 entry,
2532 &Operator::Equal,
2533 &Value::String("Error".to_string()),
2534 ));
2535 }
2536
2537 #[test]
2538 fn test_match_returns_rejects_non_callable_kinds() {
2539 let (graph, _returner_id, _plain_id, error_id) = build_returns_graph();
2540 let pm = PluginManager::new();
2541 let ctx = GraphEvalContext::new(&graph, &pm);
2542 let entry = graph.nodes().get(error_id).expect("error type exists");
2545
2546 assert!(!match_returns(
2547 &ctx,
2548 error_id,
2549 entry,
2550 &Operator::Equal,
2551 &Value::String("error".to_string()),
2552 ));
2553 }
2554
2555 use crate::graph::unified::NodeMetadataStore;
2566 use crate::graph::unified::edge::ResolvedVia;
2567
2568 fn build_two_function_graph() -> (CodeGraph, NodeId, NodeId) {
2572 let mut arena = NodeArena::new();
2573 let edges = BidirectionalEdgeStore::new();
2574 let mut strings = StringInterner::new();
2575 let mut files = FileRegistry::new();
2576 let mut indices = AuxiliaryIndices::new();
2577
2578 let flagged_name = strings.intern("flagged").unwrap();
2579 let plain_name = strings.intern("plain").unwrap();
2580 let file_id = files.register(Path::new("u18_1.c")).unwrap();
2581
2582 let mk_fn = |arena: &mut NodeArena, name, start: u32, end: u32, line: u32| -> NodeId {
2583 arena
2584 .alloc(NodeEntry {
2585 kind: NodeKind::Function,
2586 name,
2587 file: file_id,
2588 start_byte: start,
2589 end_byte: end,
2590 start_line: line,
2591 start_column: 0,
2592 end_line: line + 2,
2593 end_column: 0,
2594 signature: None,
2595 doc: None,
2596 qualified_name: None,
2597 visibility: None,
2598 is_async: false,
2599 is_static: false,
2600 is_unsafe: false,
2601 body_hash: None,
2602 })
2603 .unwrap()
2604 };
2605
2606 let flagged_id = mk_fn(&mut arena, flagged_name, 0, 50, 1);
2607 let plain_id = mk_fn(&mut arena, plain_name, 100, 150, 10);
2608
2609 indices.add(flagged_id, NodeKind::Function, flagged_name, None, file_id);
2610 indices.add(plain_id, NodeKind::Function, plain_name, None, file_id);
2611
2612 let graph = CodeGraph::from_components(
2613 arena,
2614 edges,
2615 strings,
2616 files,
2617 indices,
2618 NodeMetadataStore::new(),
2619 );
2620 (graph, flagged_id, plain_id)
2621 }
2622
2623 #[test]
2627 fn match_address_taken_returns_true_when_flag_set() {
2628 let (mut graph, flagged_id, plain_id) = build_two_function_graph();
2629 graph.macro_metadata_mut().mark_address_taken(flagged_id);
2630 let pm = PluginManager::new();
2631 let ctx = GraphEvalContext::new(&graph, &pm);
2632
2633 assert!(match_address_taken(
2634 &ctx,
2635 flagged_id,
2636 &Operator::Equal,
2637 &Value::Boolean(true)
2638 ));
2639 assert!(!match_address_taken(
2640 &ctx,
2641 plain_id,
2642 &Operator::Equal,
2643 &Value::Boolean(true)
2644 ));
2645
2646 assert!(match_address_taken(
2648 &ctx,
2649 plain_id,
2650 &Operator::Equal,
2651 &Value::Boolean(false)
2652 ));
2653 assert!(!match_address_taken(
2654 &ctx,
2655 flagged_id,
2656 &Operator::Equal,
2657 &Value::Boolean(false)
2658 ));
2659
2660 assert!(match_address_taken(
2663 &ctx,
2664 flagged_id,
2665 &Operator::Equal,
2666 &Value::String("true".to_string())
2667 ));
2668 }
2669
2670 #[test]
2673 fn match_callsite_promiscuous_returns_true_when_flag_set() {
2674 let (mut graph, flagged_id, plain_id) = build_two_function_graph();
2675 graph
2676 .macro_metadata_mut()
2677 .mark_callsite_promiscuous(flagged_id);
2678 let pm = PluginManager::new();
2679 let ctx = GraphEvalContext::new(&graph, &pm);
2680
2681 assert!(match_callsite_promiscuous(
2682 &ctx,
2683 flagged_id,
2684 &Operator::Equal,
2685 &Value::Boolean(true)
2686 ));
2687 assert!(!match_callsite_promiscuous(
2688 &ctx,
2689 plain_id,
2690 &Operator::Equal,
2691 &Value::Boolean(true)
2692 ));
2693
2694 graph.macro_metadata_mut().mark_address_taken(flagged_id);
2698 let ctx = GraphEvalContext::new(&graph, &pm);
2699 assert!(match_callsite_promiscuous(
2700 &ctx,
2701 flagged_id,
2702 &Operator::Equal,
2703 &Value::Boolean(true)
2704 ));
2705 assert!(match_address_taken(
2706 &ctx,
2707 flagged_id,
2708 &Operator::Equal,
2709 &Value::Boolean(true)
2710 ));
2711 }
2712
2713 #[test]
2719 fn match_resolved_via_filters_calls_edges_by_resolution() {
2720 let mut arena = NodeArena::new();
2721 let edges = BidirectionalEdgeStore::new();
2722 let mut strings = StringInterner::new();
2723 let mut files = FileRegistry::new();
2724 let mut indices = AuxiliaryIndices::new();
2725
2726 let caller_name = strings.intern("caller").unwrap();
2727 let target_a = strings.intern("target_direct").unwrap();
2728 let target_b = strings.intern("target_binding").unwrap();
2729 let file_id = files.register(Path::new("u18_1_resolved_via.c")).unwrap();
2730
2731 let mk_fn = |arena: &mut NodeArena, name, start: u32, end: u32, line: u32| {
2732 arena
2733 .alloc(NodeEntry {
2734 kind: NodeKind::Function,
2735 name,
2736 file: file_id,
2737 start_byte: start,
2738 end_byte: end,
2739 start_line: line,
2740 start_column: 0,
2741 end_line: line + 2,
2742 end_column: 0,
2743 signature: None,
2744 doc: None,
2745 qualified_name: None,
2746 visibility: None,
2747 is_async: false,
2748 is_static: false,
2749 is_unsafe: false,
2750 body_hash: None,
2751 })
2752 .unwrap()
2753 };
2754 let caller_id = mk_fn(&mut arena, caller_name, 0, 50, 1);
2755 let target_a_id = mk_fn(&mut arena, target_a, 100, 150, 10);
2756 let target_b_id = mk_fn(&mut arena, target_b, 200, 250, 20);
2757
2758 indices.add(caller_id, NodeKind::Function, caller_name, None, file_id);
2759 indices.add(target_a_id, NodeKind::Function, target_a, None, file_id);
2760 indices.add(target_b_id, NodeKind::Function, target_b, None, file_id);
2761
2762 edges.add_edge(
2764 caller_id,
2765 target_a_id,
2766 EdgeKind::Calls {
2767 argument_count: 0,
2768 is_async: false,
2769 resolved_via: ResolvedVia::Direct,
2770 },
2771 file_id,
2772 );
2773 edges.add_edge(
2774 caller_id,
2775 target_b_id,
2776 EdgeKind::Calls {
2777 argument_count: 0,
2778 is_async: false,
2779 resolved_via: ResolvedVia::BindingPlane,
2780 },
2781 file_id,
2782 );
2783
2784 let graph = CodeGraph::from_components(
2785 arena,
2786 edges,
2787 strings,
2788 files,
2789 indices,
2790 NodeMetadataStore::new(),
2791 );
2792 let pm = PluginManager::new();
2793 let ctx = GraphEvalContext::new(&graph, &pm);
2794
2795 assert!(match_resolved_via(
2797 &ctx,
2798 caller_id,
2799 &Value::String("direct".to_string())
2800 ));
2801 assert!(match_resolved_via(
2803 &ctx,
2804 caller_id,
2805 &Value::String("binding_plane".to_string())
2806 ));
2807 assert!(!match_resolved_via(
2809 &ctx,
2810 caller_id,
2811 &Value::String("type_match".to_string())
2812 ));
2813 assert!(!match_resolved_via(
2815 &ctx,
2816 target_a_id,
2817 &Value::String("direct".to_string())
2818 ));
2819 assert!(!match_resolved_via(
2821 &ctx,
2822 caller_id,
2823 &Value::String("not_a_real_variant".to_string())
2824 ));
2825 assert!(!match_resolved_via(&ctx, caller_id, &Value::Boolean(true)));
2827 }
2828}