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 let _ = 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 let _ = 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 let _ = 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 let _ = 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 "virtual_dispatch" => crate::graph::unified::edge::ResolvedVia::VirtualDispatch,
1206 "interface_dispatch" => crate::graph::unified::edge::ResolvedVia::InterfaceDispatch,
1207 "duck_typed" => crate::graph::unified::edge::ResolvedVia::DuckTyped,
1208 "structural" => crate::graph::unified::edge::ResolvedVia::Structural,
1209 "promiscuous_elided" => crate::graph::unified::edge::ResolvedVia::PromiscuousElided,
1210 _ => return false, };
1212 for edge in ctx.graph.edges().edges_from(node_id) {
1213 if let EdgeKind::Calls { resolved_via, .. } = &edge.kind
1214 && *resolved_via == want
1215 {
1216 return true;
1217 }
1218 }
1219 false
1220}
1221
1222fn match_callers(ctx: &GraphEvalContext, node_id: NodeId, value: &Value) -> bool {
1236 let Some(target_name) = value.as_string() else {
1237 return false;
1238 };
1239
1240 let method_part = extract_method_name(target_name);
1243
1244 for edge in ctx.graph.edges().edges_from(node_id) {
1246 if let EdgeKind::Calls { .. } = &edge.kind
1247 && let Some(target_entry) = ctx.graph.nodes().get(edge.target)
1248 {
1249 let callee_names = entry_query_texts(ctx.graph, target_entry);
1250
1251 if callee_names.iter().any(|callee_name| {
1252 language_aware_segments_match(
1253 ctx.graph,
1254 target_entry.file,
1255 callee_name,
1256 target_name,
1257 )
1258 }) {
1259 return true;
1260 }
1261
1262 if let Some(method) = &method_part
1266 && callee_names
1267 .iter()
1268 .filter_map(|callee_name| extract_method_name(callee_name))
1269 .any(|callee_method| method == &callee_method)
1270 {
1271 return true;
1272 }
1273 }
1274 }
1275 false
1276}
1277
1278#[must_use]
1282pub fn extract_method_name(qualified: &str) -> Option<String> {
1283 for sep in ["::", ".", "#", ":", "/"] {
1285 if let Some(pos) = qualified.rfind(sep) {
1286 let method = &qualified[pos + sep.len()..];
1287 if !method.is_empty() {
1288 return Some(method.to_string());
1289 }
1290 }
1291 }
1292 None
1293}
1294
1295fn match_callees(ctx: &GraphEvalContext, node_id: NodeId, value: &Value) -> bool {
1299 let Some(caller_name) = value.as_string() else {
1300 return false;
1301 };
1302
1303 for edge in ctx.graph.edges().edges_to(node_id) {
1305 if let EdgeKind::Calls { .. } = &edge.kind
1306 && let Some(source_entry) = ctx.graph.nodes().get(edge.source)
1307 && entry_query_texts(ctx.graph, source_entry)
1308 .iter()
1309 .any(|source_name| {
1310 language_aware_segments_match(
1311 ctx.graph,
1312 source_entry.file,
1313 source_name,
1314 caller_name,
1315 )
1316 })
1317 {
1318 return true;
1319 }
1320 }
1321 false
1322}
1323
1324fn match_imports(ctx: &GraphEvalContext, node_id: NodeId, value: &Value) -> bool {
1340 let Some(target_module) = value.as_string() else {
1341 return false;
1342 };
1343
1344 let Some(entry) = ctx.graph.nodes().get(node_id) else {
1345 return false;
1346 };
1347
1348 if entry.kind == NodeKind::Import && import_entry_matches(ctx.graph, entry, target_module) {
1349 return true;
1350 }
1351
1352 for edge in ctx.graph.edges().edges_from(node_id) {
1353 if import_edge_matches(ctx.graph, &edge, target_module) {
1354 return true;
1355 }
1356 }
1357 false
1358}
1359
1360#[must_use]
1366pub fn import_edge_matches<G: crate::graph::unified::concurrent::GraphAccess>(
1367 graph: &G,
1368 edge: &StoreEdgeRef,
1369 target_module: &str,
1370) -> bool {
1371 let EdgeKind::Imports { alias, is_wildcard } = &edge.kind else {
1372 return false;
1373 };
1374
1375 let target_match = graph
1377 .nodes()
1378 .get(edge.target)
1379 .is_some_and(|entry| import_entry_matches(graph, entry, target_module));
1380
1381 let alias_match = alias
1383 .and_then(|sid| graph.strings().resolve(sid))
1384 .is_some_and(|alias_str| {
1385 graph.nodes().get(edge.source).is_some_and(|entry| {
1386 import_text_matches(graph, entry.file, alias_str.as_ref(), target_module)
1387 })
1388 });
1389
1390 let wildcard_match = *is_wildcard && target_module == "*";
1392
1393 target_match || alias_match || wildcard_match
1394}
1395
1396#[must_use]
1400pub fn import_text_matches<G: crate::graph::unified::concurrent::GraphAccess>(
1401 graph: &G,
1402 file_id: FileId,
1403 candidate: &str,
1404 target_module: &str,
1405) -> bool {
1406 if candidate.contains(target_module) {
1407 return true;
1408 }
1409
1410 graph
1411 .files()
1412 .language_for_file(file_id)
1413 .is_some_and(|language| {
1414 let canonical_target = canonicalize_graph_qualified_name(language, target_module);
1415 canonical_target != target_module && candidate.contains(&canonical_target)
1416 })
1417}
1418
1419#[must_use]
1422pub fn import_entry_matches<G: crate::graph::unified::concurrent::GraphAccess>(
1423 graph: &G,
1424 entry: &NodeEntry,
1425 target_module: &str,
1426) -> bool {
1427 entry_query_texts(graph, entry)
1428 .iter()
1429 .any(|candidate| import_text_matches(graph, entry.file, candidate, target_module))
1430}
1431
1432#[must_use]
1437pub fn language_aware_segments_match<G: crate::graph::unified::concurrent::GraphAccess>(
1438 graph: &G,
1439 file_id: FileId,
1440 candidate: &str,
1441 expected: &str,
1442) -> bool {
1443 if segments_match(candidate, expected) {
1444 return true;
1445 }
1446
1447 graph
1448 .files()
1449 .language_for_file(file_id)
1450 .is_some_and(|language| {
1451 let canonical_expected = canonicalize_graph_qualified_name(language, expected);
1452 canonical_expected != expected && segments_match(candidate, &canonical_expected)
1453 })
1454}
1455
1456fn push_unique_query_text(texts: &mut Vec<String>, candidate: impl Into<String>) {
1457 let candidate = candidate.into();
1458 if !texts.iter().any(|existing| existing == &candidate) {
1459 texts.push(candidate);
1460 }
1461}
1462
1463#[must_use]
1469pub fn entry_query_texts<G: crate::graph::unified::concurrent::GraphAccess>(
1470 graph: &G,
1471 entry: &NodeEntry,
1472) -> Vec<String> {
1473 let mut texts = Vec::with_capacity(3);
1474
1475 if let Some(name) = graph.strings().resolve(entry.name) {
1476 push_unique_query_text(&mut texts, name.to_string());
1477 }
1478
1479 if let Some(qualified) = entry
1480 .qualified_name
1481 .and_then(|qualified_name_id| graph.strings().resolve(qualified_name_id))
1482 {
1483 push_unique_query_text(&mut texts, qualified.to_string());
1484
1485 if let Some(language) = graph.files().language_for_file(entry.file) {
1486 push_unique_query_text(
1487 &mut texts,
1488 display_graph_qualified_name(
1489 language,
1490 qualified.as_ref(),
1491 entry.kind,
1492 entry.is_static,
1493 ),
1494 );
1495 }
1496 }
1497
1498 texts
1499}
1500
1501fn match_exports(ctx: &GraphEvalContext, node_id: NodeId, value: &Value) -> bool {
1507 let Some(target_name) = value.as_string() else {
1508 return false;
1509 };
1510
1511 let Some(entry) = ctx.graph.nodes().get(node_id) else {
1512 return false;
1513 };
1514 let node_file = entry.file;
1515
1516 if !entry_query_texts(ctx.graph, entry).iter().any(|candidate| {
1517 language_aware_segments_match(ctx.graph, entry.file, candidate, target_name)
1518 }) {
1519 return false;
1520 }
1521
1522 let edges = ctx.graph.edges();
1523
1524 for edge in edges.edges_from(node_id) {
1526 if let EdgeKind::Exports { .. } = &edge.kind {
1527 if let Some(target_entry) = ctx.graph.nodes().get(edge.target)
1529 && target_entry.file == node_file
1530 {
1531 return true;
1532 }
1533 }
1534 }
1535
1536 for edge in edges.edges_to(node_id) {
1538 if let EdgeKind::Exports { .. } = &edge.kind {
1539 if let Some(source_entry) = ctx.graph.nodes().get(edge.source)
1541 && source_entry.file == node_file
1542 {
1543 return true;
1544 }
1545 }
1546 }
1547
1548 false
1549}
1550
1551fn match_references(
1555 ctx: &GraphEvalContext,
1556 node_id: NodeId,
1557 operator: &Operator,
1558 value: &Value,
1559) -> bool {
1560 let Some(entry) = ctx.graph.nodes().get(node_id) else {
1562 return false;
1563 };
1564
1565 let name_matches = match (operator, value) {
1566 (Operator::Equal, Value::String(target)) => entry_query_texts(ctx.graph, entry)
1567 .iter()
1568 .any(|candidate| candidate == target || candidate.ends_with(&format!("::{target}"))),
1569 (Operator::Regex, Value::Regex(rv)) => get_or_compile_regex(
1570 &rv.pattern,
1571 rv.flags.case_insensitive,
1572 rv.flags.multiline,
1573 rv.flags.dot_all,
1574 )
1575 .map(|re| {
1576 entry_query_texts(ctx.graph, entry)
1577 .iter()
1578 .any(|candidate| regex_is_match(&re, candidate))
1579 })
1580 .unwrap_or(false),
1581 _ => false,
1582 };
1583
1584 if !name_matches {
1585 return false;
1586 }
1587
1588 for edge in ctx.graph.edges().edges_to(node_id) {
1591 let is_reference = matches!(
1592 &edge.kind,
1593 EdgeKind::References
1594 | EdgeKind::Calls { .. }
1595 | EdgeKind::Imports { .. }
1596 | EdgeKind::FfiCall { .. }
1597 );
1598 if is_reference {
1599 return true;
1600 }
1601 }
1602
1603 false
1604}
1605
1606fn match_implements(ctx: &GraphEvalContext, node_id: NodeId, value: &Value) -> bool {
1608 let Some(trait_name) = value.as_string() else {
1609 return false;
1610 };
1611
1612 for edge in ctx.graph.edges().edges_from(node_id) {
1613 if let EdgeKind::Implements = &edge.kind
1614 && let Some(target_entry) = ctx.graph.nodes().get(edge.target)
1615 && entry_query_texts(ctx.graph, target_entry)
1616 .iter()
1617 .any(|name| {
1618 language_aware_segments_match(ctx.graph, target_entry.file, name, trait_name)
1619 })
1620 {
1621 return true;
1622 }
1623 }
1624 false
1625}
1626
1627fn node_kind_to_scope_type(kind: NodeKind) -> &'static str {
1639 match kind {
1640 NodeKind::Function | NodeKind::Test => "function",
1641 NodeKind::Method => "method",
1642 NodeKind::Class | NodeKind::Service => "class",
1643 NodeKind::Interface | NodeKind::Trait => "interface",
1644 NodeKind::Struct => "struct",
1645 NodeKind::Enum => "enum",
1646 NodeKind::Module => "module",
1647 NodeKind::Macro => "macro",
1648 NodeKind::Component => "component",
1649 NodeKind::Resource | NodeKind::Endpoint => "resource",
1650 NodeKind::Variable => "variable",
1652 NodeKind::Constant => "constant",
1653 NodeKind::Type => "type",
1654 NodeKind::EnumVariant => "enumvariant",
1655 NodeKind::Import => "import",
1656 NodeKind::Export => "export",
1657 NodeKind::CallSite => "callsite",
1658 NodeKind::Parameter => "parameter",
1659 NodeKind::Property => "property",
1660 NodeKind::StyleRule => "style_rule",
1661 NodeKind::StyleAtRule => "style_at_rule",
1662 NodeKind::StyleVariable => "style_variable",
1663 NodeKind::Lifetime => "lifetime",
1664 NodeKind::TypeParameter => "type_parameter",
1665 NodeKind::Annotation => "annotation",
1666 NodeKind::AnnotationValue => "annotation_value",
1667 NodeKind::LambdaTarget => "lambda_target",
1668 NodeKind::JavaModule => "java_module",
1669 NodeKind::EnumConstant => "enum_constant",
1670 NodeKind::Channel => "channel",
1671 NodeKind::Other => "other",
1672 }
1673}
1674
1675fn match_scope(
1676 ctx: &GraphEvalContext,
1677 node_id: NodeId,
1678 field: &str,
1679 operator: &Operator,
1680 value: &Value,
1681) -> bool {
1682 let scope_part = field.strip_prefix("scope.").unwrap_or("");
1683 match scope_part {
1684 "type" => match_scope_type(ctx, node_id, operator, value),
1685 "name" => match_scope_name(ctx, node_id, operator, value),
1686 "parent" => match_scope_parent_name(ctx, node_id, operator, value),
1687 "ancestor" => match_scope_ancestor_name(ctx, node_id, operator, value),
1688 _ => false,
1689 }
1690}
1691
1692fn match_scope_type(
1693 ctx: &GraphEvalContext,
1694 node_id: NodeId,
1695 operator: &Operator,
1696 value: &Value,
1697) -> bool {
1698 for edge in ctx.graph.edges().edges_to(node_id) {
1699 if let EdgeKind::Contains = &edge.kind
1700 && let Some(parent) = ctx.graph.nodes().get(edge.source)
1701 {
1702 let scope_type = node_kind_to_scope_type(parent.kind);
1704 return match (operator, value) {
1705 (Operator::Equal, Value::String(exp)) => scope_type == exp,
1707 (Operator::Regex, Value::Regex(rv)) => get_or_compile_regex(
1708 &rv.pattern,
1709 rv.flags.case_insensitive,
1710 rv.flags.multiline,
1711 rv.flags.dot_all,
1712 )
1713 .map(|re| regex_is_match(&re, scope_type))
1714 .unwrap_or(false),
1715 _ => false,
1716 };
1717 }
1718 }
1719 false
1720}
1721
1722fn match_scope_name(
1723 ctx: &GraphEvalContext,
1724 node_id: NodeId,
1725 operator: &Operator,
1726 value: &Value,
1727) -> bool {
1728 for edge in ctx.graph.edges().edges_to(node_id) {
1729 if let EdgeKind::Contains = &edge.kind
1730 && let Some(parent) = ctx.graph.nodes().get(edge.source)
1731 && let Some(name) = ctx.graph.strings().resolve(parent.name)
1732 {
1733 return match (operator, value) {
1734 (Operator::Equal, Value::String(exp)) => segments_match(&name, exp),
1736 (Operator::Regex, Value::Regex(rv)) => get_or_compile_regex(
1737 &rv.pattern,
1738 rv.flags.case_insensitive,
1739 rv.flags.multiline,
1740 rv.flags.dot_all,
1741 )
1742 .map(|re| regex_is_match(&re, &name))
1743 .unwrap_or(false),
1744 _ => false,
1745 };
1746 }
1747 }
1748 false
1749}
1750
1751fn match_scope_parent_name(
1755 ctx: &GraphEvalContext,
1756 node_id: NodeId,
1757 operator: &Operator,
1758 value: &Value,
1759) -> bool {
1760 for edge in ctx.graph.edges().edges_to(node_id) {
1761 if let EdgeKind::Contains = &edge.kind
1762 && let Some(parent) = ctx.graph.nodes().get(edge.source)
1763 && let Some(name) = ctx.graph.strings().resolve(parent.name)
1764 {
1765 return match (operator, value) {
1766 (Operator::Equal, Value::String(exp)) => segments_match(&name, exp),
1768 (Operator::Regex, Value::Regex(rv)) => get_or_compile_regex(
1769 &rv.pattern,
1770 rv.flags.case_insensitive,
1771 rv.flags.multiline,
1772 rv.flags.dot_all,
1773 )
1774 .map(|re| regex_is_match(&re, &name))
1775 .unwrap_or(false),
1776 _ => false,
1777 };
1778 }
1779 }
1780 false
1781}
1782
1783fn match_scope_ancestor_name(
1788 ctx: &GraphEvalContext,
1789 node_id: NodeId,
1790 operator: &Operator,
1791 value: &Value,
1792) -> bool {
1793 let mut current = node_id;
1794 let mut visited = HashSet::new();
1795 visited.insert(node_id);
1796
1797 loop {
1798 let mut found_parent = false;
1799 for edge in ctx.graph.edges().edges_to(current) {
1800 if let EdgeKind::Contains = &edge.kind {
1801 if visited.contains(&edge.source) {
1803 continue;
1804 }
1805 visited.insert(edge.source);
1806
1807 found_parent = true;
1808 current = edge.source;
1809 if let Some(parent) = ctx.graph.nodes().get(current)
1810 && let Some(name) = ctx.graph.strings().resolve(parent.name)
1811 {
1812 let matches = match (operator, value) {
1813 (Operator::Equal, Value::String(exp)) => segments_match(&name, exp),
1815 (Operator::Regex, Value::Regex(rv)) => get_or_compile_regex(
1816 &rv.pattern,
1817 rv.flags.case_insensitive,
1818 rv.flags.multiline,
1819 rv.flags.dot_all,
1820 )
1821 .map(|re| regex_is_match(&re, &name))
1822 .unwrap_or(false),
1823 _ => false,
1824 };
1825 if matches {
1826 return true;
1827 }
1828 }
1829 break;
1830 }
1831 }
1832 if !found_parent {
1833 break;
1834 }
1835 }
1836 false
1837}
1838
1839pub fn evaluate_subquery(ctx: &GraphEvalContext, expr: &Expr) -> Result<HashSet<NodeId>> {
1851 let recursion_limits = crate::config::RecursionLimits::load_or_default()?;
1852 let expr_depth = recursion_limits.effective_expr_depth()?;
1853 let mut guard = crate::query::security::RecursionGuard::new(expr_depth)?;
1854
1855 let arena = ctx.graph.nodes();
1856 let mut matches = HashSet::new();
1857 for (id, _) in arena.iter() {
1858 if evaluate_node(ctx, id, expr, &mut guard)? {
1859 matches.insert(id);
1860 }
1861 }
1862 Ok(matches)
1863}
1864
1865fn match_callers_subquery(
1871 ctx: &GraphEvalContext,
1872 node_id: NodeId,
1873 subquery_matches: Option<&HashSet<NodeId>>,
1874) -> Result<bool> {
1875 let Some(matches) = subquery_matches else {
1876 return Err(anyhow!(
1877 "subquery cache miss: precompute_subqueries did not populate cache for this relation predicate"
1878 ));
1879 };
1880 for edge in ctx.graph.edges().edges_from(node_id) {
1881 if let EdgeKind::Calls { .. } = &edge.kind
1882 && matches.contains(&edge.target)
1883 {
1884 return Ok(true);
1885 }
1886 }
1887 Ok(false)
1888}
1889
1890fn match_callees_subquery(
1892 ctx: &GraphEvalContext,
1893 node_id: NodeId,
1894 subquery_matches: Option<&HashSet<NodeId>>,
1895) -> Result<bool> {
1896 let Some(matches) = subquery_matches else {
1897 return Err(anyhow!(
1898 "subquery cache miss: precompute_subqueries did not populate cache for this relation predicate"
1899 ));
1900 };
1901 for edge in ctx.graph.edges().edges_to(node_id) {
1902 if let EdgeKind::Calls { .. } = &edge.kind
1903 && matches.contains(&edge.source)
1904 {
1905 return Ok(true);
1906 }
1907 }
1908 Ok(false)
1909}
1910
1911fn match_imports_subquery(
1917 ctx: &GraphEvalContext,
1918 node_id: NodeId,
1919 subquery_matches: Option<&HashSet<NodeId>>,
1920) -> Result<bool> {
1921 let Some(matches) = subquery_matches else {
1922 return Err(anyhow!(
1923 "subquery cache miss: precompute_subqueries did not populate cache for this relation predicate"
1924 ));
1925 };
1926 for edge in ctx.graph.edges().edges_from(node_id) {
1927 if let EdgeKind::Imports { .. } = &edge.kind
1928 && matches.contains(&edge.target)
1929 {
1930 return Ok(true);
1931 }
1932 }
1933 Ok(false)
1934}
1935
1936fn match_exports_subquery(
1938 ctx: &GraphEvalContext,
1939 node_id: NodeId,
1940 subquery_matches: Option<&HashSet<NodeId>>,
1941) -> Result<bool> {
1942 let Some(matches) = subquery_matches else {
1943 return Err(anyhow!(
1944 "subquery cache miss: precompute_subqueries did not populate cache for this relation predicate"
1945 ));
1946 };
1947 for edge in ctx.graph.edges().edges_from(node_id) {
1948 if let EdgeKind::Exports { .. } = &edge.kind
1949 && matches.contains(&edge.target)
1950 {
1951 return Ok(true);
1952 }
1953 }
1954 Ok(false)
1955}
1956
1957fn match_implements_subquery(
1959 ctx: &GraphEvalContext,
1960 node_id: NodeId,
1961 subquery_matches: Option<&HashSet<NodeId>>,
1962) -> Result<bool> {
1963 let Some(matches) = subquery_matches else {
1964 return Err(anyhow!(
1965 "subquery cache miss: precompute_subqueries did not populate cache for this relation predicate"
1966 ));
1967 };
1968 for edge in ctx.graph.edges().edges_from(node_id) {
1969 if let EdgeKind::Implements = &edge.kind
1970 && matches.contains(&edge.target)
1971 {
1972 return Ok(true);
1973 }
1974 }
1975 Ok(false)
1976}
1977
1978fn match_references_subquery(
1980 ctx: &GraphEvalContext,
1981 node_id: NodeId,
1982 subquery_matches: Option<&HashSet<NodeId>>,
1983) -> Result<bool> {
1984 let Some(matches) = subquery_matches else {
1985 return Err(anyhow!(
1986 "subquery cache miss: precompute_subqueries did not populate cache for this relation predicate"
1987 ));
1988 };
1989 for edge in ctx.graph.edges().edges_to(node_id) {
1990 let is_reference = matches!(
1991 &edge.kind,
1992 EdgeKind::References
1993 | EdgeKind::Calls { .. }
1994 | EdgeKind::Imports { .. }
1995 | EdgeKind::FfiCall { .. }
1996 );
1997 if is_reference && matches.contains(&edge.source) {
1998 return Ok(true);
1999 }
2000 }
2001 Ok(false)
2002}
2003
2004pub fn evaluate_join(
2020 ctx: &GraphEvalContext,
2021 join: &JoinExpr,
2022 max_results: Option<usize>,
2023) -> Result<JoinEvalResult> {
2024 let lhs_matches = evaluate_subquery(ctx, &join.left)?;
2025 let rhs_matches = evaluate_subquery(ctx, &join.right)?;
2026 let cap = max_results.unwrap_or(DEFAULT_JOIN_RESULT_CAP);
2027
2028 let mut pairs = Vec::new();
2029 let mut truncated = false;
2030 'outer: for &lhs_id in &lhs_matches {
2031 for edge in ctx.graph.edges().edges_from(lhs_id) {
2032 if edge_matches_join_kind(&edge.kind, &join.edge) && rhs_matches.contains(&edge.target)
2033 {
2034 pairs.push((lhs_id, edge.target));
2035 if pairs.len() >= cap {
2036 truncated = true;
2037 break 'outer;
2038 }
2039 }
2040 }
2041 }
2042 Ok(JoinEvalResult { pairs, truncated })
2043}
2044
2045pub struct JoinEvalResult {
2047 pub pairs: Vec<(NodeId, NodeId)>,
2049 pub truncated: bool,
2051}
2052
2053const DEFAULT_JOIN_RESULT_CAP: usize = 10_000;
2057
2058fn edge_matches_join_kind(edge_kind: &EdgeKind, join_kind: &JoinEdgeKind) -> bool {
2060 match join_kind {
2061 JoinEdgeKind::Calls => matches!(edge_kind, EdgeKind::Calls { .. }),
2062 JoinEdgeKind::Imports => matches!(edge_kind, EdgeKind::Imports { .. }),
2063 JoinEdgeKind::Inherits => matches!(edge_kind, EdgeKind::Inherits),
2064 JoinEdgeKind::Implements => matches!(edge_kind, EdgeKind::Implements),
2065 }
2066}
2067
2068#[cfg(test)]
2069mod tests {
2070 use super::*;
2071 use crate::graph::node::Language;
2072 use crate::query::types::{Condition, Field, Span};
2073 use std::path::Path;
2074
2075 #[test]
2076 fn test_import_text_matches_canonicalized_qualified_imports() {
2077 let mut graph = CodeGraph::new();
2078 let file_id = graph
2079 .files_mut()
2080 .register(Path::new("src/FileProcessor.cs"))
2081 .unwrap();
2082 assert!(graph.files_mut().set_language(file_id, Language::CSharp));
2083
2084 assert!(import_text_matches(
2085 &graph,
2086 file_id,
2087 "System::IO",
2088 "System.IO"
2089 ));
2090 assert!(import_text_matches(
2091 &graph,
2092 file_id,
2093 "System::Collections::Generic",
2094 "System.Collections.Generic"
2095 ));
2096 assert!(!import_text_matches(
2097 &graph,
2098 file_id,
2099 "System::Text",
2100 "System.IO"
2101 ));
2102 }
2103
2104 #[test]
2105 fn test_language_aware_segments_match_supports_ruby_method_separators() {
2106 let mut graph = CodeGraph::new();
2107 let file_id = graph
2108 .files_mut()
2109 .register(Path::new("app/models/user.rb"))
2110 .unwrap();
2111 assert!(graph.files_mut().set_language(file_id, Language::Ruby));
2112
2113 assert!(language_aware_segments_match(
2114 &graph,
2115 file_id,
2116 "Admin::Users::Controller::show",
2117 "Admin::Users::Controller#show"
2118 ));
2119 assert!(language_aware_segments_match(
2120 &graph,
2121 file_id,
2122 "Admin::Users::Controller::show",
2123 "show"
2124 ));
2125 assert!(!language_aware_segments_match(
2126 &graph,
2127 file_id,
2128 "Admin::Users::Controller::index",
2129 "Admin::Users::Controller#show"
2130 ));
2131 }
2132
2133 #[test]
2134 fn test_normalize_kind() {
2135 assert_eq!(normalize_kind("trait"), "interface");
2137 assert_eq!(normalize_kind("TRAIT"), "TRAIT"); assert_eq!(normalize_kind("field"), "property");
2139 assert_eq!(normalize_kind("namespace"), "module");
2140 assert_eq!(normalize_kind("function"), "function"); }
2142
2143 #[test]
2144 fn test_graph_eval_context_builder() {
2145 let graph = CodeGraph::new();
2146 let pm = PluginManager::new();
2147 let ctx = GraphEvalContext::new(&graph, &pm)
2148 .with_workspace_root(Path::new("/test"))
2149 .with_parallel_disabled(true);
2150
2151 assert!(ctx.disable_parallel);
2152 assert_eq!(ctx.workspace_root, Some(Path::new("/test")));
2153 }
2154
2155 fn subquery_condition(field: &str, inner: Expr, start: usize, end: usize) -> Expr {
2161 Expr::Condition(Condition {
2162 field: Field(field.to_string()),
2163 operator: Operator::Equal,
2164 value: Value::Subquery(Box::new(inner)),
2165 span: Span::with_position(start, end, 1, start + 1),
2166 })
2167 }
2168
2169 fn kind_condition(kind: &str) -> Expr {
2171 Expr::Condition(Condition {
2172 field: Field("kind".to_string()),
2173 operator: Operator::Equal,
2174 value: Value::String(kind.to_string()),
2175 span: Span::default(),
2176 })
2177 }
2178
2179 #[test]
2180 fn test_collect_subquery_exprs_post_order_depth_2() {
2181 let inner_subquery = subquery_condition("callees", kind_condition("function"), 20, 40);
2185 let outer_subquery = subquery_condition("callers", inner_subquery, 0, 50);
2186
2187 let mut out = Vec::new();
2188 collect_subquery_exprs(&outer_subquery, &mut out);
2189
2190 assert_eq!(
2192 out.len(),
2193 2,
2194 "should collect both inner and outer subqueries"
2195 );
2196 assert_eq!(out[0].0, (20, 40), "inner subquery span should come first");
2197 assert_eq!(out[1].0, (0, 50), "outer subquery span should come second");
2198 }
2199
2200 #[test]
2201 fn test_collect_subquery_exprs_post_order_depth_3() {
2202 let innermost = subquery_condition("imports", kind_condition("function"), 30, 50);
2204 let middle = subquery_condition("callees", innermost, 15, 55);
2205 let outer = subquery_condition("callers", middle, 0, 60);
2206
2207 let mut out = Vec::new();
2208 collect_subquery_exprs(&outer, &mut out);
2209
2210 assert_eq!(out.len(), 3, "should collect all three nested subqueries");
2211 assert_eq!(out[0].0, (30, 50), "innermost should come first");
2212 assert_eq!(out[1].0, (15, 55), "middle should come second");
2213 assert_eq!(out[2].0, (0, 60), "outer should come last");
2214 }
2215
2216 #[test]
2217 fn test_collect_subquery_exprs_and_or_branches() {
2218 let left = subquery_condition("callers", kind_condition("function"), 0, 25);
2220 let right = subquery_condition("callees", kind_condition("method"), 30, 55);
2221 let expr = Expr::And(vec![left, right]);
2222
2223 let mut out = Vec::new();
2224 collect_subquery_exprs(&expr, &mut out);
2225
2226 assert_eq!(out.len(), 2, "should collect subqueries from both branches");
2227 assert_eq!(out[0].0, (0, 25), "left branch subquery");
2228 assert_eq!(out[1].0, (30, 55), "right branch subquery");
2229 }
2230
2231 #[test]
2232 fn test_collect_subquery_exprs_no_subqueries() {
2233 let expr = kind_condition("function");
2235
2236 let mut out = Vec::new();
2237 collect_subquery_exprs(&expr, &mut out);
2238
2239 assert!(
2240 out.is_empty(),
2241 "should collect nothing for plain conditions"
2242 );
2243 }
2244
2245 use crate::graph::unified::edge::{BidirectionalEdgeStore, FfiConvention};
2250 use crate::graph::unified::storage::{
2251 AuxiliaryIndices, FileRegistry, NodeArena, StringInterner,
2252 };
2253
2254 fn build_ffi_graph() -> (CodeGraph, NodeId, NodeId) {
2256 let mut arena = NodeArena::new();
2257 let edges = BidirectionalEdgeStore::new();
2258 let mut strings = StringInterner::new();
2259 let mut files = FileRegistry::new();
2260 let mut indices = AuxiliaryIndices::new();
2261
2262 let caller_name = strings.intern("caller_fn").unwrap();
2263 let target_name = strings.intern("ffi_target").unwrap();
2264 let file_id = files.register(Path::new("test.r")).unwrap();
2265
2266 let caller_id = arena
2267 .alloc(NodeEntry {
2268 kind: NodeKind::Function,
2269 name: caller_name,
2270 file: file_id,
2271 start_byte: 0,
2272 end_byte: 100,
2273 start_line: 1,
2274 start_column: 0,
2275 end_line: 5,
2276 end_column: 0,
2277 signature: None,
2278 doc: None,
2279 qualified_name: None,
2280 visibility: None,
2281 is_async: false,
2282 is_static: false,
2283 is_unsafe: false,
2284 body_hash: None,
2285 })
2286 .unwrap();
2287
2288 let target_id = arena
2289 .alloc(NodeEntry {
2290 kind: NodeKind::Function,
2291 name: target_name,
2292 file: file_id,
2293 start_byte: 200,
2294 end_byte: 300,
2295 start_line: 10,
2296 start_column: 0,
2297 end_line: 15,
2298 end_column: 0,
2299 signature: None,
2300 doc: None,
2301 qualified_name: None,
2302 visibility: None,
2303 is_async: false,
2304 is_static: false,
2305 is_unsafe: false,
2306 body_hash: None,
2307 })
2308 .unwrap();
2309
2310 indices.add(caller_id, NodeKind::Function, caller_name, None, file_id);
2311 indices.add(target_id, NodeKind::Function, target_name, None, file_id);
2312
2313 edges.add_edge(
2314 caller_id,
2315 target_id,
2316 EdgeKind::FfiCall {
2317 convention: FfiConvention::C,
2318 },
2319 file_id,
2320 );
2321
2322 let graph = CodeGraph::from_components(
2323 arena,
2324 edges,
2325 strings,
2326 files,
2327 indices,
2328 crate::graph::unified::NodeMetadataStore::new(),
2329 );
2330 (graph, caller_id, target_id)
2331 }
2332
2333 #[test]
2334 fn test_ffi_call_edge_in_references_predicate() {
2335 let (graph, _caller_id, target_id) = build_ffi_graph();
2336 let pm = PluginManager::new();
2337 let ctx = GraphEvalContext::new(&graph, &pm);
2338
2339 let result = match_references(
2341 &ctx,
2342 target_id,
2343 &Operator::Equal,
2344 &Value::String("ffi_target".to_string()),
2345 );
2346 assert!(result, "references: predicate should match FfiCall edges");
2347 }
2348
2349 #[test]
2350 fn test_ffi_call_edge_in_references_subquery() {
2351 let (graph, caller_id, target_id) = build_ffi_graph();
2352 let pm = PluginManager::new();
2353 let ctx = GraphEvalContext::new(&graph, &pm);
2354
2355 let mut subquery_results = HashSet::new();
2357 subquery_results.insert(caller_id);
2358
2359 let result = match_references_subquery(&ctx, target_id, Some(&subquery_results)).unwrap();
2362 assert!(
2363 result,
2364 "references subquery should match FfiCall edge sources"
2365 );
2366 }
2367
2368 fn build_returns_graph() -> (CodeGraph, NodeId, NodeId, NodeId) {
2378 let mut arena = NodeArena::new();
2379 let edges = BidirectionalEdgeStore::new();
2380 let mut strings = StringInterner::new();
2381 let mut files = FileRegistry::new();
2382 let mut indices = AuxiliaryIndices::new();
2383
2384 let returner_name = strings.intern("returner_fn").unwrap();
2385 let plain_name = strings.intern("plain_fn").unwrap();
2386 let error_name = strings.intern("error").unwrap();
2387 let file_id = files.register(Path::new("test.go")).unwrap();
2388
2389 let returner_id = arena
2390 .alloc(NodeEntry {
2391 kind: NodeKind::Function,
2392 name: returner_name,
2393 file: file_id,
2394 start_byte: 0,
2395 end_byte: 100,
2396 start_line: 1,
2397 start_column: 0,
2398 end_line: 5,
2399 end_column: 0,
2400 signature: None,
2401 doc: None,
2402 qualified_name: None,
2403 visibility: None,
2404 is_async: false,
2405 is_static: false,
2406 is_unsafe: false,
2407 body_hash: None,
2408 })
2409 .unwrap();
2410
2411 let plain_id = arena
2412 .alloc(NodeEntry {
2413 kind: NodeKind::Function,
2414 name: plain_name,
2415 file: file_id,
2416 start_byte: 200,
2417 end_byte: 300,
2418 start_line: 10,
2419 start_column: 0,
2420 end_line: 15,
2421 end_column: 0,
2422 signature: None,
2423 doc: None,
2424 qualified_name: None,
2425 visibility: None,
2426 is_async: false,
2427 is_static: false,
2428 is_unsafe: false,
2429 body_hash: None,
2430 })
2431 .unwrap();
2432
2433 let error_type_id = arena
2434 .alloc(NodeEntry {
2435 kind: NodeKind::Type,
2436 name: error_name,
2437 file: file_id,
2438 start_byte: 400,
2439 end_byte: 410,
2440 start_line: 20,
2441 start_column: 0,
2442 end_line: 20,
2443 end_column: 10,
2444 signature: None,
2445 doc: None,
2446 qualified_name: None,
2447 visibility: None,
2448 is_async: false,
2449 is_static: false,
2450 is_unsafe: false,
2451 body_hash: None,
2452 })
2453 .unwrap();
2454
2455 indices.add(
2456 returner_id,
2457 NodeKind::Function,
2458 returner_name,
2459 None,
2460 file_id,
2461 );
2462 indices.add(plain_id, NodeKind::Function, plain_name, None, file_id);
2463 indices.add(error_type_id, NodeKind::Type, error_name, None, file_id);
2464
2465 edges.add_edge(
2466 returner_id,
2467 error_type_id,
2468 EdgeKind::TypeOf {
2469 context: Some(TypeOfContext::Return),
2470 index: None,
2471 name: None,
2472 },
2473 file_id,
2474 );
2475
2476 let graph = CodeGraph::from_components(
2477 arena,
2478 edges,
2479 strings,
2480 files,
2481 indices,
2482 crate::graph::unified::NodeMetadataStore::new(),
2483 );
2484 (graph, returner_id, plain_id, error_type_id)
2485 }
2486
2487 #[test]
2488 fn test_match_returns_byte_exact_hit() {
2489 let (graph, returner_id, _plain_id, _error_id) = build_returns_graph();
2490 let pm = PluginManager::new();
2491 let ctx = GraphEvalContext::new(&graph, &pm);
2492 let entry = graph.nodes().get(returner_id).expect("returner exists");
2493
2494 assert!(match_returns(
2497 &ctx,
2498 returner_id,
2499 entry,
2500 &Operator::Equal,
2501 &Value::String("error".to_string()),
2502 ));
2503 }
2504
2505 #[test]
2506 fn test_match_returns_no_edges_misses() {
2507 let (graph, _returner_id, plain_id, _error_id) = build_returns_graph();
2508 let pm = PluginManager::new();
2509 let ctx = GraphEvalContext::new(&graph, &pm);
2510 let entry = graph.nodes().get(plain_id).expect("plain_fn exists");
2511
2512 assert!(!match_returns(
2516 &ctx,
2517 plain_id,
2518 entry,
2519 &Operator::Equal,
2520 &Value::String("error".to_string()),
2521 ));
2522 }
2523
2524 #[test]
2525 fn test_match_returns_byte_exact_miss_on_different_target_name() {
2526 let (graph, returner_id, _plain_id, _error_id) = build_returns_graph();
2527 let pm = PluginManager::new();
2528 let ctx = GraphEvalContext::new(&graph, &pm);
2529 let entry = graph.nodes().get(returner_id).expect("returner exists");
2530
2531 assert!(!match_returns(
2535 &ctx,
2536 returner_id,
2537 entry,
2538 &Operator::Equal,
2539 &Value::String("Error".to_string()),
2540 ));
2541 }
2542
2543 #[test]
2544 fn test_match_returns_rejects_non_callable_kinds() {
2545 let (graph, _returner_id, _plain_id, error_id) = build_returns_graph();
2546 let pm = PluginManager::new();
2547 let ctx = GraphEvalContext::new(&graph, &pm);
2548 let entry = graph.nodes().get(error_id).expect("error type exists");
2551
2552 assert!(!match_returns(
2553 &ctx,
2554 error_id,
2555 entry,
2556 &Operator::Equal,
2557 &Value::String("error".to_string()),
2558 ));
2559 }
2560
2561 use crate::graph::unified::NodeMetadataStore;
2572 use crate::graph::unified::edge::ResolvedVia;
2573
2574 fn build_two_function_graph() -> (CodeGraph, NodeId, NodeId) {
2578 let mut arena = NodeArena::new();
2579 let edges = BidirectionalEdgeStore::new();
2580 let mut strings = StringInterner::new();
2581 let mut files = FileRegistry::new();
2582 let mut indices = AuxiliaryIndices::new();
2583
2584 let flagged_name = strings.intern("flagged").unwrap();
2585 let plain_name = strings.intern("plain").unwrap();
2586 let file_id = files.register(Path::new("u18_1.c")).unwrap();
2587
2588 let mk_fn = |arena: &mut NodeArena, name, start: u32, end: u32, line: u32| -> NodeId {
2589 arena
2590 .alloc(NodeEntry {
2591 kind: NodeKind::Function,
2592 name,
2593 file: file_id,
2594 start_byte: start,
2595 end_byte: end,
2596 start_line: line,
2597 start_column: 0,
2598 end_line: line + 2,
2599 end_column: 0,
2600 signature: None,
2601 doc: None,
2602 qualified_name: None,
2603 visibility: None,
2604 is_async: false,
2605 is_static: false,
2606 is_unsafe: false,
2607 body_hash: None,
2608 })
2609 .unwrap()
2610 };
2611
2612 let flagged_id = mk_fn(&mut arena, flagged_name, 0, 50, 1);
2613 let plain_id = mk_fn(&mut arena, plain_name, 100, 150, 10);
2614
2615 indices.add(flagged_id, NodeKind::Function, flagged_name, None, file_id);
2616 indices.add(plain_id, NodeKind::Function, plain_name, None, file_id);
2617
2618 let graph = CodeGraph::from_components(
2619 arena,
2620 edges,
2621 strings,
2622 files,
2623 indices,
2624 NodeMetadataStore::new(),
2625 );
2626 (graph, flagged_id, plain_id)
2627 }
2628
2629 #[test]
2633 fn match_address_taken_returns_true_when_flag_set() {
2634 let (mut graph, flagged_id, plain_id) = build_two_function_graph();
2635 graph.macro_metadata_mut().mark_address_taken(flagged_id);
2636 let pm = PluginManager::new();
2637 let ctx = GraphEvalContext::new(&graph, &pm);
2638
2639 assert!(match_address_taken(
2640 &ctx,
2641 flagged_id,
2642 &Operator::Equal,
2643 &Value::Boolean(true)
2644 ));
2645 assert!(!match_address_taken(
2646 &ctx,
2647 plain_id,
2648 &Operator::Equal,
2649 &Value::Boolean(true)
2650 ));
2651
2652 assert!(match_address_taken(
2654 &ctx,
2655 plain_id,
2656 &Operator::Equal,
2657 &Value::Boolean(false)
2658 ));
2659 assert!(!match_address_taken(
2660 &ctx,
2661 flagged_id,
2662 &Operator::Equal,
2663 &Value::Boolean(false)
2664 ));
2665
2666 assert!(match_address_taken(
2669 &ctx,
2670 flagged_id,
2671 &Operator::Equal,
2672 &Value::String("true".to_string())
2673 ));
2674 }
2675
2676 #[test]
2679 fn match_callsite_promiscuous_returns_true_when_flag_set() {
2680 let (mut graph, flagged_id, plain_id) = build_two_function_graph();
2681 graph
2682 .macro_metadata_mut()
2683 .mark_callsite_promiscuous(flagged_id);
2684 let pm = PluginManager::new();
2685 let ctx = GraphEvalContext::new(&graph, &pm);
2686
2687 assert!(match_callsite_promiscuous(
2688 &ctx,
2689 flagged_id,
2690 &Operator::Equal,
2691 &Value::Boolean(true)
2692 ));
2693 assert!(!match_callsite_promiscuous(
2694 &ctx,
2695 plain_id,
2696 &Operator::Equal,
2697 &Value::Boolean(true)
2698 ));
2699
2700 graph.macro_metadata_mut().mark_address_taken(flagged_id);
2704 let ctx = GraphEvalContext::new(&graph, &pm);
2705 assert!(match_callsite_promiscuous(
2706 &ctx,
2707 flagged_id,
2708 &Operator::Equal,
2709 &Value::Boolean(true)
2710 ));
2711 assert!(match_address_taken(
2712 &ctx,
2713 flagged_id,
2714 &Operator::Equal,
2715 &Value::Boolean(true)
2716 ));
2717 }
2718
2719 #[test]
2725 fn match_resolved_via_filters_calls_edges_by_resolution() {
2726 let mut arena = NodeArena::new();
2727 let edges = BidirectionalEdgeStore::new();
2728 let mut strings = StringInterner::new();
2729 let mut files = FileRegistry::new();
2730 let mut indices = AuxiliaryIndices::new();
2731
2732 let caller_name = strings.intern("caller").unwrap();
2733 let target_a = strings.intern("target_direct").unwrap();
2734 let target_b = strings.intern("target_binding").unwrap();
2735 let file_id = files.register(Path::new("u18_1_resolved_via.c")).unwrap();
2736
2737 let mk_fn = |arena: &mut NodeArena, name, start: u32, end: u32, line: u32| {
2738 arena
2739 .alloc(NodeEntry {
2740 kind: NodeKind::Function,
2741 name,
2742 file: file_id,
2743 start_byte: start,
2744 end_byte: end,
2745 start_line: line,
2746 start_column: 0,
2747 end_line: line + 2,
2748 end_column: 0,
2749 signature: None,
2750 doc: None,
2751 qualified_name: None,
2752 visibility: None,
2753 is_async: false,
2754 is_static: false,
2755 is_unsafe: false,
2756 body_hash: None,
2757 })
2758 .unwrap()
2759 };
2760 let caller_id = mk_fn(&mut arena, caller_name, 0, 50, 1);
2761 let target_a_id = mk_fn(&mut arena, target_a, 100, 150, 10);
2762 let target_b_id = mk_fn(&mut arena, target_b, 200, 250, 20);
2763
2764 indices.add(caller_id, NodeKind::Function, caller_name, None, file_id);
2765 indices.add(target_a_id, NodeKind::Function, target_a, None, file_id);
2766 indices.add(target_b_id, NodeKind::Function, target_b, None, file_id);
2767
2768 edges.add_edge(
2770 caller_id,
2771 target_a_id,
2772 EdgeKind::Calls {
2773 argument_count: 0,
2774 is_async: false,
2775 resolved_via: ResolvedVia::Direct,
2776 },
2777 file_id,
2778 );
2779 edges.add_edge(
2780 caller_id,
2781 target_b_id,
2782 EdgeKind::Calls {
2783 argument_count: 0,
2784 is_async: false,
2785 resolved_via: ResolvedVia::BindingPlane,
2786 },
2787 file_id,
2788 );
2789
2790 let graph = CodeGraph::from_components(
2791 arena,
2792 edges,
2793 strings,
2794 files,
2795 indices,
2796 NodeMetadataStore::new(),
2797 );
2798 let pm = PluginManager::new();
2799 let ctx = GraphEvalContext::new(&graph, &pm);
2800
2801 assert!(match_resolved_via(
2803 &ctx,
2804 caller_id,
2805 &Value::String("direct".to_string())
2806 ));
2807 assert!(match_resolved_via(
2809 &ctx,
2810 caller_id,
2811 &Value::String("binding_plane".to_string())
2812 ));
2813 assert!(!match_resolved_via(
2815 &ctx,
2816 caller_id,
2817 &Value::String("type_match".to_string())
2818 ));
2819 assert!(!match_resolved_via(
2821 &ctx,
2822 target_a_id,
2823 &Value::String("direct".to_string())
2824 ));
2825 assert!(!match_resolved_via(
2827 &ctx,
2828 caller_id,
2829 &Value::String("not_a_real_variant".to_string())
2830 ));
2831 assert!(!match_resolved_via(&ctx, caller_id, &Value::Boolean(true)));
2833 }
2834}