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 "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::Other => "other",
1671 }
1672}
1673
1674fn match_scope(
1675 ctx: &GraphEvalContext,
1676 node_id: NodeId,
1677 field: &str,
1678 operator: &Operator,
1679 value: &Value,
1680) -> bool {
1681 let scope_part = field.strip_prefix("scope.").unwrap_or("");
1682 match scope_part {
1683 "type" => match_scope_type(ctx, node_id, operator, value),
1684 "name" => match_scope_name(ctx, node_id, operator, value),
1685 "parent" => match_scope_parent_name(ctx, node_id, operator, value),
1686 "ancestor" => match_scope_ancestor_name(ctx, node_id, operator, value),
1687 _ => false,
1688 }
1689}
1690
1691fn match_scope_type(
1692 ctx: &GraphEvalContext,
1693 node_id: NodeId,
1694 operator: &Operator,
1695 value: &Value,
1696) -> bool {
1697 for edge in ctx.graph.edges().edges_to(node_id) {
1698 if let EdgeKind::Contains = &edge.kind
1699 && let Some(parent) = ctx.graph.nodes().get(edge.source)
1700 {
1701 let scope_type = node_kind_to_scope_type(parent.kind);
1703 return match (operator, value) {
1704 (Operator::Equal, Value::String(exp)) => scope_type == exp,
1706 (Operator::Regex, Value::Regex(rv)) => get_or_compile_regex(
1707 &rv.pattern,
1708 rv.flags.case_insensitive,
1709 rv.flags.multiline,
1710 rv.flags.dot_all,
1711 )
1712 .map(|re| regex_is_match(&re, scope_type))
1713 .unwrap_or(false),
1714 _ => false,
1715 };
1716 }
1717 }
1718 false
1719}
1720
1721fn match_scope_name(
1722 ctx: &GraphEvalContext,
1723 node_id: NodeId,
1724 operator: &Operator,
1725 value: &Value,
1726) -> bool {
1727 for edge in ctx.graph.edges().edges_to(node_id) {
1728 if let EdgeKind::Contains = &edge.kind
1729 && let Some(parent) = ctx.graph.nodes().get(edge.source)
1730 && let Some(name) = ctx.graph.strings().resolve(parent.name)
1731 {
1732 return match (operator, value) {
1733 (Operator::Equal, Value::String(exp)) => segments_match(&name, exp),
1735 (Operator::Regex, Value::Regex(rv)) => get_or_compile_regex(
1736 &rv.pattern,
1737 rv.flags.case_insensitive,
1738 rv.flags.multiline,
1739 rv.flags.dot_all,
1740 )
1741 .map(|re| regex_is_match(&re, &name))
1742 .unwrap_or(false),
1743 _ => false,
1744 };
1745 }
1746 }
1747 false
1748}
1749
1750fn match_scope_parent_name(
1754 ctx: &GraphEvalContext,
1755 node_id: NodeId,
1756 operator: &Operator,
1757 value: &Value,
1758) -> bool {
1759 for edge in ctx.graph.edges().edges_to(node_id) {
1760 if let EdgeKind::Contains = &edge.kind
1761 && let Some(parent) = ctx.graph.nodes().get(edge.source)
1762 && let Some(name) = ctx.graph.strings().resolve(parent.name)
1763 {
1764 return match (operator, value) {
1765 (Operator::Equal, Value::String(exp)) => segments_match(&name, exp),
1767 (Operator::Regex, Value::Regex(rv)) => get_or_compile_regex(
1768 &rv.pattern,
1769 rv.flags.case_insensitive,
1770 rv.flags.multiline,
1771 rv.flags.dot_all,
1772 )
1773 .map(|re| regex_is_match(&re, &name))
1774 .unwrap_or(false),
1775 _ => false,
1776 };
1777 }
1778 }
1779 false
1780}
1781
1782fn match_scope_ancestor_name(
1787 ctx: &GraphEvalContext,
1788 node_id: NodeId,
1789 operator: &Operator,
1790 value: &Value,
1791) -> bool {
1792 let mut current = node_id;
1793 let mut visited = HashSet::new();
1794 visited.insert(node_id);
1795
1796 loop {
1797 let mut found_parent = false;
1798 for edge in ctx.graph.edges().edges_to(current) {
1799 if let EdgeKind::Contains = &edge.kind {
1800 if visited.contains(&edge.source) {
1802 continue;
1803 }
1804 visited.insert(edge.source);
1805
1806 found_parent = true;
1807 current = edge.source;
1808 if let Some(parent) = ctx.graph.nodes().get(current)
1809 && let Some(name) = ctx.graph.strings().resolve(parent.name)
1810 {
1811 let matches = match (operator, value) {
1812 (Operator::Equal, Value::String(exp)) => segments_match(&name, exp),
1814 (Operator::Regex, Value::Regex(rv)) => get_or_compile_regex(
1815 &rv.pattern,
1816 rv.flags.case_insensitive,
1817 rv.flags.multiline,
1818 rv.flags.dot_all,
1819 )
1820 .map(|re| regex_is_match(&re, &name))
1821 .unwrap_or(false),
1822 _ => false,
1823 };
1824 if matches {
1825 return true;
1826 }
1827 }
1828 break;
1829 }
1830 }
1831 if !found_parent {
1832 break;
1833 }
1834 }
1835 false
1836}
1837
1838pub fn evaluate_subquery(ctx: &GraphEvalContext, expr: &Expr) -> Result<HashSet<NodeId>> {
1850 let recursion_limits = crate::config::RecursionLimits::load_or_default()?;
1851 let expr_depth = recursion_limits.effective_expr_depth()?;
1852 let mut guard = crate::query::security::RecursionGuard::new(expr_depth)?;
1853
1854 let arena = ctx.graph.nodes();
1855 let mut matches = HashSet::new();
1856 for (id, _) in arena.iter() {
1857 if evaluate_node(ctx, id, expr, &mut guard)? {
1858 matches.insert(id);
1859 }
1860 }
1861 Ok(matches)
1862}
1863
1864fn match_callers_subquery(
1870 ctx: &GraphEvalContext,
1871 node_id: NodeId,
1872 subquery_matches: Option<&HashSet<NodeId>>,
1873) -> Result<bool> {
1874 let Some(matches) = subquery_matches else {
1875 return Err(anyhow!(
1876 "subquery cache miss: precompute_subqueries did not populate cache for this relation predicate"
1877 ));
1878 };
1879 for edge in ctx.graph.edges().edges_from(node_id) {
1880 if let EdgeKind::Calls { .. } = &edge.kind
1881 && matches.contains(&edge.target)
1882 {
1883 return Ok(true);
1884 }
1885 }
1886 Ok(false)
1887}
1888
1889fn match_callees_subquery(
1891 ctx: &GraphEvalContext,
1892 node_id: NodeId,
1893 subquery_matches: Option<&HashSet<NodeId>>,
1894) -> Result<bool> {
1895 let Some(matches) = subquery_matches else {
1896 return Err(anyhow!(
1897 "subquery cache miss: precompute_subqueries did not populate cache for this relation predicate"
1898 ));
1899 };
1900 for edge in ctx.graph.edges().edges_to(node_id) {
1901 if let EdgeKind::Calls { .. } = &edge.kind
1902 && matches.contains(&edge.source)
1903 {
1904 return Ok(true);
1905 }
1906 }
1907 Ok(false)
1908}
1909
1910fn match_imports_subquery(
1916 ctx: &GraphEvalContext,
1917 node_id: NodeId,
1918 subquery_matches: Option<&HashSet<NodeId>>,
1919) -> Result<bool> {
1920 let Some(matches) = subquery_matches else {
1921 return Err(anyhow!(
1922 "subquery cache miss: precompute_subqueries did not populate cache for this relation predicate"
1923 ));
1924 };
1925 for edge in ctx.graph.edges().edges_from(node_id) {
1926 if let EdgeKind::Imports { .. } = &edge.kind
1927 && matches.contains(&edge.target)
1928 {
1929 return Ok(true);
1930 }
1931 }
1932 Ok(false)
1933}
1934
1935fn match_exports_subquery(
1937 ctx: &GraphEvalContext,
1938 node_id: NodeId,
1939 subquery_matches: Option<&HashSet<NodeId>>,
1940) -> Result<bool> {
1941 let Some(matches) = subquery_matches else {
1942 return Err(anyhow!(
1943 "subquery cache miss: precompute_subqueries did not populate cache for this relation predicate"
1944 ));
1945 };
1946 for edge in ctx.graph.edges().edges_from(node_id) {
1947 if let EdgeKind::Exports { .. } = &edge.kind
1948 && matches.contains(&edge.target)
1949 {
1950 return Ok(true);
1951 }
1952 }
1953 Ok(false)
1954}
1955
1956fn match_implements_subquery(
1958 ctx: &GraphEvalContext,
1959 node_id: NodeId,
1960 subquery_matches: Option<&HashSet<NodeId>>,
1961) -> Result<bool> {
1962 let Some(matches) = subquery_matches else {
1963 return Err(anyhow!(
1964 "subquery cache miss: precompute_subqueries did not populate cache for this relation predicate"
1965 ));
1966 };
1967 for edge in ctx.graph.edges().edges_from(node_id) {
1968 if let EdgeKind::Implements = &edge.kind
1969 && matches.contains(&edge.target)
1970 {
1971 return Ok(true);
1972 }
1973 }
1974 Ok(false)
1975}
1976
1977fn match_references_subquery(
1979 ctx: &GraphEvalContext,
1980 node_id: NodeId,
1981 subquery_matches: Option<&HashSet<NodeId>>,
1982) -> Result<bool> {
1983 let Some(matches) = subquery_matches else {
1984 return Err(anyhow!(
1985 "subquery cache miss: precompute_subqueries did not populate cache for this relation predicate"
1986 ));
1987 };
1988 for edge in ctx.graph.edges().edges_to(node_id) {
1989 let is_reference = matches!(
1990 &edge.kind,
1991 EdgeKind::References
1992 | EdgeKind::Calls { .. }
1993 | EdgeKind::Imports { .. }
1994 | EdgeKind::FfiCall { .. }
1995 );
1996 if is_reference && matches.contains(&edge.source) {
1997 return Ok(true);
1998 }
1999 }
2000 Ok(false)
2001}
2002
2003pub fn evaluate_join(
2019 ctx: &GraphEvalContext,
2020 join: &JoinExpr,
2021 max_results: Option<usize>,
2022) -> Result<JoinEvalResult> {
2023 let lhs_matches = evaluate_subquery(ctx, &join.left)?;
2024 let rhs_matches = evaluate_subquery(ctx, &join.right)?;
2025 let cap = max_results.unwrap_or(DEFAULT_JOIN_RESULT_CAP);
2026
2027 let mut pairs = Vec::new();
2028 let mut truncated = false;
2029 'outer: for &lhs_id in &lhs_matches {
2030 for edge in ctx.graph.edges().edges_from(lhs_id) {
2031 if edge_matches_join_kind(&edge.kind, &join.edge) && rhs_matches.contains(&edge.target)
2032 {
2033 pairs.push((lhs_id, edge.target));
2034 if pairs.len() >= cap {
2035 truncated = true;
2036 break 'outer;
2037 }
2038 }
2039 }
2040 }
2041 Ok(JoinEvalResult { pairs, truncated })
2042}
2043
2044pub struct JoinEvalResult {
2046 pub pairs: Vec<(NodeId, NodeId)>,
2048 pub truncated: bool,
2050}
2051
2052const DEFAULT_JOIN_RESULT_CAP: usize = 10_000;
2056
2057fn edge_matches_join_kind(edge_kind: &EdgeKind, join_kind: &JoinEdgeKind) -> bool {
2059 match join_kind {
2060 JoinEdgeKind::Calls => matches!(edge_kind, EdgeKind::Calls { .. }),
2061 JoinEdgeKind::Imports => matches!(edge_kind, EdgeKind::Imports { .. }),
2062 JoinEdgeKind::Inherits => matches!(edge_kind, EdgeKind::Inherits),
2063 JoinEdgeKind::Implements => matches!(edge_kind, EdgeKind::Implements),
2064 }
2065}
2066
2067#[cfg(test)]
2068mod tests {
2069 use super::*;
2070 use crate::graph::node::Language;
2071 use crate::query::types::{Condition, Field, Span};
2072 use std::path::Path;
2073
2074 #[test]
2075 fn test_import_text_matches_canonicalized_qualified_imports() {
2076 let mut graph = CodeGraph::new();
2077 let file_id = graph
2078 .files_mut()
2079 .register(Path::new("src/FileProcessor.cs"))
2080 .unwrap();
2081 assert!(graph.files_mut().set_language(file_id, Language::CSharp));
2082
2083 assert!(import_text_matches(
2084 &graph,
2085 file_id,
2086 "System::IO",
2087 "System.IO"
2088 ));
2089 assert!(import_text_matches(
2090 &graph,
2091 file_id,
2092 "System::Collections::Generic",
2093 "System.Collections.Generic"
2094 ));
2095 assert!(!import_text_matches(
2096 &graph,
2097 file_id,
2098 "System::Text",
2099 "System.IO"
2100 ));
2101 }
2102
2103 #[test]
2104 fn test_language_aware_segments_match_supports_ruby_method_separators() {
2105 let mut graph = CodeGraph::new();
2106 let file_id = graph
2107 .files_mut()
2108 .register(Path::new("app/models/user.rb"))
2109 .unwrap();
2110 assert!(graph.files_mut().set_language(file_id, Language::Ruby));
2111
2112 assert!(language_aware_segments_match(
2113 &graph,
2114 file_id,
2115 "Admin::Users::Controller::show",
2116 "Admin::Users::Controller#show"
2117 ));
2118 assert!(language_aware_segments_match(
2119 &graph,
2120 file_id,
2121 "Admin::Users::Controller::show",
2122 "show"
2123 ));
2124 assert!(!language_aware_segments_match(
2125 &graph,
2126 file_id,
2127 "Admin::Users::Controller::index",
2128 "Admin::Users::Controller#show"
2129 ));
2130 }
2131
2132 #[test]
2133 fn test_normalize_kind() {
2134 assert_eq!(normalize_kind("trait"), "interface");
2136 assert_eq!(normalize_kind("TRAIT"), "TRAIT"); assert_eq!(normalize_kind("field"), "property");
2138 assert_eq!(normalize_kind("namespace"), "module");
2139 assert_eq!(normalize_kind("function"), "function"); }
2141
2142 #[test]
2143 fn test_graph_eval_context_builder() {
2144 let graph = CodeGraph::new();
2145 let pm = PluginManager::new();
2146 let ctx = GraphEvalContext::new(&graph, &pm)
2147 .with_workspace_root(Path::new("/test"))
2148 .with_parallel_disabled(true);
2149
2150 assert!(ctx.disable_parallel);
2151 assert_eq!(ctx.workspace_root, Some(Path::new("/test")));
2152 }
2153
2154 fn subquery_condition(field: &str, inner: Expr, start: usize, end: usize) -> Expr {
2160 Expr::Condition(Condition {
2161 field: Field(field.to_string()),
2162 operator: Operator::Equal,
2163 value: Value::Subquery(Box::new(inner)),
2164 span: Span::with_position(start, end, 1, start + 1),
2165 })
2166 }
2167
2168 fn kind_condition(kind: &str) -> Expr {
2170 Expr::Condition(Condition {
2171 field: Field("kind".to_string()),
2172 operator: Operator::Equal,
2173 value: Value::String(kind.to_string()),
2174 span: Span::default(),
2175 })
2176 }
2177
2178 #[test]
2179 fn test_collect_subquery_exprs_post_order_depth_2() {
2180 let inner_subquery = subquery_condition("callees", kind_condition("function"), 20, 40);
2184 let outer_subquery = subquery_condition("callers", inner_subquery, 0, 50);
2185
2186 let mut out = Vec::new();
2187 collect_subquery_exprs(&outer_subquery, &mut out);
2188
2189 assert_eq!(
2191 out.len(),
2192 2,
2193 "should collect both inner and outer subqueries"
2194 );
2195 assert_eq!(out[0].0, (20, 40), "inner subquery span should come first");
2196 assert_eq!(out[1].0, (0, 50), "outer subquery span should come second");
2197 }
2198
2199 #[test]
2200 fn test_collect_subquery_exprs_post_order_depth_3() {
2201 let innermost = subquery_condition("imports", kind_condition("function"), 30, 50);
2203 let middle = subquery_condition("callees", innermost, 15, 55);
2204 let outer = subquery_condition("callers", middle, 0, 60);
2205
2206 let mut out = Vec::new();
2207 collect_subquery_exprs(&outer, &mut out);
2208
2209 assert_eq!(out.len(), 3, "should collect all three nested subqueries");
2210 assert_eq!(out[0].0, (30, 50), "innermost should come first");
2211 assert_eq!(out[1].0, (15, 55), "middle should come second");
2212 assert_eq!(out[2].0, (0, 60), "outer should come last");
2213 }
2214
2215 #[test]
2216 fn test_collect_subquery_exprs_and_or_branches() {
2217 let left = subquery_condition("callers", kind_condition("function"), 0, 25);
2219 let right = subquery_condition("callees", kind_condition("method"), 30, 55);
2220 let expr = Expr::And(vec![left, right]);
2221
2222 let mut out = Vec::new();
2223 collect_subquery_exprs(&expr, &mut out);
2224
2225 assert_eq!(out.len(), 2, "should collect subqueries from both branches");
2226 assert_eq!(out[0].0, (0, 25), "left branch subquery");
2227 assert_eq!(out[1].0, (30, 55), "right branch subquery");
2228 }
2229
2230 #[test]
2231 fn test_collect_subquery_exprs_no_subqueries() {
2232 let expr = kind_condition("function");
2234
2235 let mut out = Vec::new();
2236 collect_subquery_exprs(&expr, &mut out);
2237
2238 assert!(
2239 out.is_empty(),
2240 "should collect nothing for plain conditions"
2241 );
2242 }
2243
2244 use crate::graph::unified::edge::{BidirectionalEdgeStore, FfiConvention};
2249 use crate::graph::unified::storage::{
2250 AuxiliaryIndices, FileRegistry, NodeArena, StringInterner,
2251 };
2252
2253 fn build_ffi_graph() -> (CodeGraph, NodeId, NodeId) {
2255 let mut arena = NodeArena::new();
2256 let edges = BidirectionalEdgeStore::new();
2257 let mut strings = StringInterner::new();
2258 let mut files = FileRegistry::new();
2259 let mut indices = AuxiliaryIndices::new();
2260
2261 let caller_name = strings.intern("caller_fn").unwrap();
2262 let target_name = strings.intern("ffi_target").unwrap();
2263 let file_id = files.register(Path::new("test.r")).unwrap();
2264
2265 let caller_id = arena
2266 .alloc(NodeEntry {
2267 kind: NodeKind::Function,
2268 name: caller_name,
2269 file: file_id,
2270 start_byte: 0,
2271 end_byte: 100,
2272 start_line: 1,
2273 start_column: 0,
2274 end_line: 5,
2275 end_column: 0,
2276 signature: None,
2277 doc: None,
2278 qualified_name: None,
2279 visibility: None,
2280 is_async: false,
2281 is_static: false,
2282 is_unsafe: false,
2283 body_hash: None,
2284 })
2285 .unwrap();
2286
2287 let target_id = arena
2288 .alloc(NodeEntry {
2289 kind: NodeKind::Function,
2290 name: target_name,
2291 file: file_id,
2292 start_byte: 200,
2293 end_byte: 300,
2294 start_line: 10,
2295 start_column: 0,
2296 end_line: 15,
2297 end_column: 0,
2298 signature: None,
2299 doc: None,
2300 qualified_name: None,
2301 visibility: None,
2302 is_async: false,
2303 is_static: false,
2304 is_unsafe: false,
2305 body_hash: None,
2306 })
2307 .unwrap();
2308
2309 indices.add(caller_id, NodeKind::Function, caller_name, None, file_id);
2310 indices.add(target_id, NodeKind::Function, target_name, None, file_id);
2311
2312 edges.add_edge(
2313 caller_id,
2314 target_id,
2315 EdgeKind::FfiCall {
2316 convention: FfiConvention::C,
2317 },
2318 file_id,
2319 );
2320
2321 let graph = CodeGraph::from_components(
2322 arena,
2323 edges,
2324 strings,
2325 files,
2326 indices,
2327 crate::graph::unified::NodeMetadataStore::new(),
2328 );
2329 (graph, caller_id, target_id)
2330 }
2331
2332 #[test]
2333 fn test_ffi_call_edge_in_references_predicate() {
2334 let (graph, _caller_id, target_id) = build_ffi_graph();
2335 let pm = PluginManager::new();
2336 let ctx = GraphEvalContext::new(&graph, &pm);
2337
2338 let result = match_references(
2340 &ctx,
2341 target_id,
2342 &Operator::Equal,
2343 &Value::String("ffi_target".to_string()),
2344 );
2345 assert!(result, "references: predicate should match FfiCall edges");
2346 }
2347
2348 #[test]
2349 fn test_ffi_call_edge_in_references_subquery() {
2350 let (graph, caller_id, target_id) = build_ffi_graph();
2351 let pm = PluginManager::new();
2352 let ctx = GraphEvalContext::new(&graph, &pm);
2353
2354 let mut subquery_results = HashSet::new();
2356 subquery_results.insert(caller_id);
2357
2358 let result = match_references_subquery(&ctx, target_id, Some(&subquery_results)).unwrap();
2361 assert!(
2362 result,
2363 "references subquery should match FfiCall edge sources"
2364 );
2365 }
2366
2367 fn build_returns_graph() -> (CodeGraph, NodeId, NodeId, NodeId) {
2377 let mut arena = NodeArena::new();
2378 let edges = BidirectionalEdgeStore::new();
2379 let mut strings = StringInterner::new();
2380 let mut files = FileRegistry::new();
2381 let mut indices = AuxiliaryIndices::new();
2382
2383 let returner_name = strings.intern("returner_fn").unwrap();
2384 let plain_name = strings.intern("plain_fn").unwrap();
2385 let error_name = strings.intern("error").unwrap();
2386 let file_id = files.register(Path::new("test.go")).unwrap();
2387
2388 let returner_id = arena
2389 .alloc(NodeEntry {
2390 kind: NodeKind::Function,
2391 name: returner_name,
2392 file: file_id,
2393 start_byte: 0,
2394 end_byte: 100,
2395 start_line: 1,
2396 start_column: 0,
2397 end_line: 5,
2398 end_column: 0,
2399 signature: None,
2400 doc: None,
2401 qualified_name: None,
2402 visibility: None,
2403 is_async: false,
2404 is_static: false,
2405 is_unsafe: false,
2406 body_hash: None,
2407 })
2408 .unwrap();
2409
2410 let plain_id = arena
2411 .alloc(NodeEntry {
2412 kind: NodeKind::Function,
2413 name: plain_name,
2414 file: file_id,
2415 start_byte: 200,
2416 end_byte: 300,
2417 start_line: 10,
2418 start_column: 0,
2419 end_line: 15,
2420 end_column: 0,
2421 signature: None,
2422 doc: None,
2423 qualified_name: None,
2424 visibility: None,
2425 is_async: false,
2426 is_static: false,
2427 is_unsafe: false,
2428 body_hash: None,
2429 })
2430 .unwrap();
2431
2432 let error_type_id = arena
2433 .alloc(NodeEntry {
2434 kind: NodeKind::Type,
2435 name: error_name,
2436 file: file_id,
2437 start_byte: 400,
2438 end_byte: 410,
2439 start_line: 20,
2440 start_column: 0,
2441 end_line: 20,
2442 end_column: 10,
2443 signature: None,
2444 doc: None,
2445 qualified_name: None,
2446 visibility: None,
2447 is_async: false,
2448 is_static: false,
2449 is_unsafe: false,
2450 body_hash: None,
2451 })
2452 .unwrap();
2453
2454 indices.add(
2455 returner_id,
2456 NodeKind::Function,
2457 returner_name,
2458 None,
2459 file_id,
2460 );
2461 indices.add(plain_id, NodeKind::Function, plain_name, None, file_id);
2462 indices.add(error_type_id, NodeKind::Type, error_name, None, file_id);
2463
2464 edges.add_edge(
2465 returner_id,
2466 error_type_id,
2467 EdgeKind::TypeOf {
2468 context: Some(TypeOfContext::Return),
2469 index: None,
2470 name: None,
2471 },
2472 file_id,
2473 );
2474
2475 let graph = CodeGraph::from_components(
2476 arena,
2477 edges,
2478 strings,
2479 files,
2480 indices,
2481 crate::graph::unified::NodeMetadataStore::new(),
2482 );
2483 (graph, returner_id, plain_id, error_type_id)
2484 }
2485
2486 #[test]
2487 fn test_match_returns_byte_exact_hit() {
2488 let (graph, returner_id, _plain_id, _error_id) = build_returns_graph();
2489 let pm = PluginManager::new();
2490 let ctx = GraphEvalContext::new(&graph, &pm);
2491 let entry = graph.nodes().get(returner_id).expect("returner exists");
2492
2493 assert!(match_returns(
2496 &ctx,
2497 returner_id,
2498 entry,
2499 &Operator::Equal,
2500 &Value::String("error".to_string()),
2501 ));
2502 }
2503
2504 #[test]
2505 fn test_match_returns_no_edges_misses() {
2506 let (graph, _returner_id, plain_id, _error_id) = build_returns_graph();
2507 let pm = PluginManager::new();
2508 let ctx = GraphEvalContext::new(&graph, &pm);
2509 let entry = graph.nodes().get(plain_id).expect("plain_fn exists");
2510
2511 assert!(!match_returns(
2515 &ctx,
2516 plain_id,
2517 entry,
2518 &Operator::Equal,
2519 &Value::String("error".to_string()),
2520 ));
2521 }
2522
2523 #[test]
2524 fn test_match_returns_byte_exact_miss_on_different_target_name() {
2525 let (graph, returner_id, _plain_id, _error_id) = build_returns_graph();
2526 let pm = PluginManager::new();
2527 let ctx = GraphEvalContext::new(&graph, &pm);
2528 let entry = graph.nodes().get(returner_id).expect("returner exists");
2529
2530 assert!(!match_returns(
2534 &ctx,
2535 returner_id,
2536 entry,
2537 &Operator::Equal,
2538 &Value::String("Error".to_string()),
2539 ));
2540 }
2541
2542 #[test]
2543 fn test_match_returns_rejects_non_callable_kinds() {
2544 let (graph, _returner_id, _plain_id, error_id) = build_returns_graph();
2545 let pm = PluginManager::new();
2546 let ctx = GraphEvalContext::new(&graph, &pm);
2547 let entry = graph.nodes().get(error_id).expect("error type exists");
2550
2551 assert!(!match_returns(
2552 &ctx,
2553 error_id,
2554 entry,
2555 &Operator::Equal,
2556 &Value::String("error".to_string()),
2557 ));
2558 }
2559
2560 use crate::graph::unified::NodeMetadataStore;
2571 use crate::graph::unified::edge::ResolvedVia;
2572
2573 fn build_two_function_graph() -> (CodeGraph, NodeId, NodeId) {
2577 let mut arena = NodeArena::new();
2578 let edges = BidirectionalEdgeStore::new();
2579 let mut strings = StringInterner::new();
2580 let mut files = FileRegistry::new();
2581 let mut indices = AuxiliaryIndices::new();
2582
2583 let flagged_name = strings.intern("flagged").unwrap();
2584 let plain_name = strings.intern("plain").unwrap();
2585 let file_id = files.register(Path::new("u18_1.c")).unwrap();
2586
2587 let mk_fn = |arena: &mut NodeArena, name, start: u32, end: u32, line: u32| -> NodeId {
2588 arena
2589 .alloc(NodeEntry {
2590 kind: NodeKind::Function,
2591 name,
2592 file: file_id,
2593 start_byte: start,
2594 end_byte: end,
2595 start_line: line,
2596 start_column: 0,
2597 end_line: line + 2,
2598 end_column: 0,
2599 signature: None,
2600 doc: None,
2601 qualified_name: None,
2602 visibility: None,
2603 is_async: false,
2604 is_static: false,
2605 is_unsafe: false,
2606 body_hash: None,
2607 })
2608 .unwrap()
2609 };
2610
2611 let flagged_id = mk_fn(&mut arena, flagged_name, 0, 50, 1);
2612 let plain_id = mk_fn(&mut arena, plain_name, 100, 150, 10);
2613
2614 indices.add(flagged_id, NodeKind::Function, flagged_name, None, file_id);
2615 indices.add(plain_id, NodeKind::Function, plain_name, None, file_id);
2616
2617 let graph = CodeGraph::from_components(
2618 arena,
2619 edges,
2620 strings,
2621 files,
2622 indices,
2623 NodeMetadataStore::new(),
2624 );
2625 (graph, flagged_id, plain_id)
2626 }
2627
2628 #[test]
2632 fn match_address_taken_returns_true_when_flag_set() {
2633 let (mut graph, flagged_id, plain_id) = build_two_function_graph();
2634 graph.macro_metadata_mut().mark_address_taken(flagged_id);
2635 let pm = PluginManager::new();
2636 let ctx = GraphEvalContext::new(&graph, &pm);
2637
2638 assert!(match_address_taken(
2639 &ctx,
2640 flagged_id,
2641 &Operator::Equal,
2642 &Value::Boolean(true)
2643 ));
2644 assert!(!match_address_taken(
2645 &ctx,
2646 plain_id,
2647 &Operator::Equal,
2648 &Value::Boolean(true)
2649 ));
2650
2651 assert!(match_address_taken(
2653 &ctx,
2654 plain_id,
2655 &Operator::Equal,
2656 &Value::Boolean(false)
2657 ));
2658 assert!(!match_address_taken(
2659 &ctx,
2660 flagged_id,
2661 &Operator::Equal,
2662 &Value::Boolean(false)
2663 ));
2664
2665 assert!(match_address_taken(
2668 &ctx,
2669 flagged_id,
2670 &Operator::Equal,
2671 &Value::String("true".to_string())
2672 ));
2673 }
2674
2675 #[test]
2678 fn match_callsite_promiscuous_returns_true_when_flag_set() {
2679 let (mut graph, flagged_id, plain_id) = build_two_function_graph();
2680 graph
2681 .macro_metadata_mut()
2682 .mark_callsite_promiscuous(flagged_id);
2683 let pm = PluginManager::new();
2684 let ctx = GraphEvalContext::new(&graph, &pm);
2685
2686 assert!(match_callsite_promiscuous(
2687 &ctx,
2688 flagged_id,
2689 &Operator::Equal,
2690 &Value::Boolean(true)
2691 ));
2692 assert!(!match_callsite_promiscuous(
2693 &ctx,
2694 plain_id,
2695 &Operator::Equal,
2696 &Value::Boolean(true)
2697 ));
2698
2699 graph.macro_metadata_mut().mark_address_taken(flagged_id);
2703 let ctx = GraphEvalContext::new(&graph, &pm);
2704 assert!(match_callsite_promiscuous(
2705 &ctx,
2706 flagged_id,
2707 &Operator::Equal,
2708 &Value::Boolean(true)
2709 ));
2710 assert!(match_address_taken(
2711 &ctx,
2712 flagged_id,
2713 &Operator::Equal,
2714 &Value::Boolean(true)
2715 ));
2716 }
2717
2718 #[test]
2724 fn match_resolved_via_filters_calls_edges_by_resolution() {
2725 let mut arena = NodeArena::new();
2726 let edges = BidirectionalEdgeStore::new();
2727 let mut strings = StringInterner::new();
2728 let mut files = FileRegistry::new();
2729 let mut indices = AuxiliaryIndices::new();
2730
2731 let caller_name = strings.intern("caller").unwrap();
2732 let target_a = strings.intern("target_direct").unwrap();
2733 let target_b = strings.intern("target_binding").unwrap();
2734 let file_id = files.register(Path::new("u18_1_resolved_via.c")).unwrap();
2735
2736 let mk_fn = |arena: &mut NodeArena, name, start: u32, end: u32, line: u32| {
2737 arena
2738 .alloc(NodeEntry {
2739 kind: NodeKind::Function,
2740 name,
2741 file: file_id,
2742 start_byte: start,
2743 end_byte: end,
2744 start_line: line,
2745 start_column: 0,
2746 end_line: line + 2,
2747 end_column: 0,
2748 signature: None,
2749 doc: None,
2750 qualified_name: None,
2751 visibility: None,
2752 is_async: false,
2753 is_static: false,
2754 is_unsafe: false,
2755 body_hash: None,
2756 })
2757 .unwrap()
2758 };
2759 let caller_id = mk_fn(&mut arena, caller_name, 0, 50, 1);
2760 let target_a_id = mk_fn(&mut arena, target_a, 100, 150, 10);
2761 let target_b_id = mk_fn(&mut arena, target_b, 200, 250, 20);
2762
2763 indices.add(caller_id, NodeKind::Function, caller_name, None, file_id);
2764 indices.add(target_a_id, NodeKind::Function, target_a, None, file_id);
2765 indices.add(target_b_id, NodeKind::Function, target_b, None, file_id);
2766
2767 edges.add_edge(
2769 caller_id,
2770 target_a_id,
2771 EdgeKind::Calls {
2772 argument_count: 0,
2773 is_async: false,
2774 resolved_via: ResolvedVia::Direct,
2775 },
2776 file_id,
2777 );
2778 edges.add_edge(
2779 caller_id,
2780 target_b_id,
2781 EdgeKind::Calls {
2782 argument_count: 0,
2783 is_async: false,
2784 resolved_via: ResolvedVia::BindingPlane,
2785 },
2786 file_id,
2787 );
2788
2789 let graph = CodeGraph::from_components(
2790 arena,
2791 edges,
2792 strings,
2793 files,
2794 indices,
2795 NodeMetadataStore::new(),
2796 );
2797 let pm = PluginManager::new();
2798 let ctx = GraphEvalContext::new(&graph, &pm);
2799
2800 assert!(match_resolved_via(
2802 &ctx,
2803 caller_id,
2804 &Value::String("direct".to_string())
2805 ));
2806 assert!(match_resolved_via(
2808 &ctx,
2809 caller_id,
2810 &Value::String("binding_plane".to_string())
2811 ));
2812 assert!(!match_resolved_via(
2814 &ctx,
2815 caller_id,
2816 &Value::String("type_match".to_string())
2817 ));
2818 assert!(!match_resolved_via(
2820 &ctx,
2821 target_a_id,
2822 &Value::String("direct".to_string())
2823 ));
2824 assert!(!match_resolved_via(
2826 &ctx,
2827 caller_id,
2828 &Value::String("not_a_real_variant".to_string())
2829 ));
2830 assert!(!match_resolved_via(&ctx, caller_id, &Value::Boolean(true)));
2832 }
2833}