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 field if is_plugin_field(ctx, field) => Err(anyhow!(
684 "Plugin field '{field}' requires metadata not available in graph backend"
685 )),
686 _ => Ok(false), }
688}
689
690fn is_plugin_field(ctx: &GraphEvalContext, field: &str) -> bool {
692 let is_registered_field = ctx
694 .plugin_manager
695 .plugins()
696 .iter()
697 .flat_map(|plugin| plugin.fields().iter())
698 .any(|descriptor| descriptor.name == field);
699
700 if is_registered_field {
701 return true;
702 }
703
704 matches!(
707 field,
708 "abstract" | "final" | "generic" | "parameters" | "arity"
709 )
710}
711
712fn normalize_kind(kind: &str) -> &str {
721 match kind {
722 "trait" => "interface", "impl" => "implementation",
725 "field" => "property",
727 "namespace" => "module",
729 "element" => "component",
731 "style" => "style_rule",
733 "at_rule" => "style_at_rule",
734 "css_var" | "custom_property" => "style_variable",
735 _ => kind,
737 }
738}
739
740fn match_kind(
741 _ctx: &GraphEvalContext,
742 entry: &NodeEntry,
743 operator: &Operator,
744 value: &Value,
745) -> bool {
746 let actual = entry.kind.as_str();
747
748 match (operator, value) {
749 (Operator::Equal, Value::String(expected)) => {
750 let normalized_expected = normalize_kind(expected);
751 let normalized_actual = normalize_kind(actual);
752 normalized_actual == normalized_expected
753 }
754 (Operator::Regex, Value::Regex(regex_val)) => get_or_compile_regex(
755 ®ex_val.pattern,
756 regex_val.flags.case_insensitive,
757 regex_val.flags.multiline,
758 regex_val.flags.dot_all,
759 )
760 .map(|re| regex_is_match(&re, actual))
761 .unwrap_or(false),
762 _ => false,
763 }
764}
765
766fn match_name(
771 ctx: &GraphEvalContext,
772 entry: &NodeEntry,
773 operator: &Operator,
774 value: &Value,
775) -> bool {
776 match (operator, value) {
777 (Operator::Equal, Value::String(expected)) => {
782 entry_query_texts(ctx.graph, entry).iter().any(|candidate| {
783 language_aware_segments_match(ctx.graph, entry.file, candidate, expected)
784 })
785 }
786 (Operator::Regex, Value::Regex(regex_val)) => get_or_compile_regex(
787 ®ex_val.pattern,
788 regex_val.flags.case_insensitive,
789 regex_val.flags.multiline,
790 regex_val.flags.dot_all,
791 )
792 .map(|re| {
793 entry_query_texts(ctx.graph, entry)
794 .iter()
795 .any(|candidate| regex_is_match(&re, candidate))
796 })
797 .unwrap_or(false),
798 _ => false,
799 }
800}
801
802fn is_relative_pattern(pattern: &str) -> bool {
808 !pattern.starts_with('/')
809}
810
811fn match_path(
812 ctx: &GraphEvalContext,
813 entry: &NodeEntry,
814 operator: &Operator,
815 value: &Value,
816) -> bool {
817 let Some(file_path) = ctx.graph.files().resolve(entry.file) else {
818 return false;
819 };
820
821 match (operator, value) {
822 (Operator::Equal, Value::String(pattern)) => {
823 let match_path = if is_relative_pattern(pattern) {
825 if let Some(root) = ctx.workspace_root {
826 file_path
827 .strip_prefix(root)
828 .map_or_else(|_| file_path.to_path_buf(), std::path::Path::to_path_buf)
829 } else {
830 file_path.to_path_buf()
831 }
832 } else {
833 file_path.to_path_buf()
835 };
836 globset::Glob::new(pattern)
837 .map(|g| g.compile_matcher().is_match(&match_path))
838 .unwrap_or(false)
839 }
840 (Operator::Regex, Value::Regex(regex_val)) => {
841 get_or_compile_regex(
843 ®ex_val.pattern,
844 regex_val.flags.case_insensitive,
845 regex_val.flags.multiline,
846 regex_val.flags.dot_all,
847 )
848 .map(|re| regex_is_match(&re, file_path.to_string_lossy().as_ref()))
849 .unwrap_or(false)
850 }
851 _ => false,
852 }
853}
854
855fn language_to_canonical(lang: crate::graph::node::Language) -> &'static str {
865 use crate::graph::node::Language;
866 match lang {
867 Language::C => "c",
868 Language::Cpp => "cpp",
869 Language::CSharp => "csharp",
870 Language::Css => "css",
871 Language::JavaScript => "javascript",
872 Language::Python => "python",
873 Language::TypeScript => "typescript",
874 Language::Rust => "rust",
875 Language::Go => "go",
876 Language::Java => "java",
877 Language::Ruby => "ruby",
878 Language::Php => "php",
879 Language::Swift => "swift",
880 Language::Kotlin => "kotlin",
881 Language::Scala => "scala",
882 Language::Sql => "sql",
883 Language::Dart => "dart",
884 Language::Lua => "lua",
885 Language::Perl => "perl",
886 Language::Shell => "shell",
887 Language::Groovy => "groovy",
888 Language::Elixir => "elixir",
889 Language::R => "r",
890 Language::Haskell => "haskell",
891 Language::Html => "html",
892 Language::Svelte => "svelte",
893 Language::Vue => "vue",
894 Language::Zig => "zig",
895 Language::Terraform => "terraform",
896 Language::Puppet => "puppet",
897 Language::Pulumi => "pulumi",
898 Language::Http => "http",
899 Language::Plsql => "plsql",
900 Language::Apex => "apex",
901 Language::Abap => "abap",
902 Language::ServiceNow => "servicenow",
903 Language::Json => "json",
904 }
905}
906
907fn match_lang(
908 ctx: &GraphEvalContext,
909 entry: &NodeEntry,
910 operator: &Operator,
911 value: &Value,
912) -> bool {
913 let Some(lang) = ctx.graph.files().language_for_file(entry.file) else {
915 return false;
916 };
917
918 let actual = language_to_canonical(lang);
920
921 match (operator, value) {
923 (Operator::Equal, Value::String(expected)) => actual == expected,
924 (Operator::Regex, Value::Regex(rv)) => get_or_compile_regex(
925 &rv.pattern,
926 rv.flags.case_insensitive,
927 rv.flags.multiline,
928 rv.flags.dot_all,
929 )
930 .map(|re| regex_is_match(&re, actual))
931 .unwrap_or(false),
932 _ => false,
933 }
934}
935
936fn match_visibility(
941 ctx: &GraphEvalContext,
942 entry: &NodeEntry,
943 operator: &Operator,
944 value: &Value,
945) -> bool {
946 let Some(expected) = value.as_string() else {
947 return false;
948 };
949
950 let normalized_expected = if expected == "pub" {
951 "public"
952 } else {
953 expected
954 };
955
956 let Some(vis_id) = entry.visibility else {
957 return match operator {
959 Operator::Equal => normalized_expected == "private",
960 _ => false,
961 };
962 };
963
964 let Some(actual) = ctx.graph.strings().resolve(vis_id) else {
965 return false;
966 };
967 let normalized_actual = if actual.as_ref().starts_with("pub") {
968 "public"
969 } else {
970 actual.as_ref()
971 };
972
973 match operator {
975 Operator::Equal => normalized_actual == normalized_expected,
976 _ => false,
977 }
978}
979
980fn match_returns(
1010 ctx: &GraphEvalContext,
1011 node_id: NodeId,
1012 entry: &NodeEntry,
1013 operator: &Operator,
1014 value: &Value,
1015) -> bool {
1016 let Some(expected) = value.as_string() else {
1017 return false;
1018 };
1019
1020 if !matches!(entry.kind, NodeKind::Function | NodeKind::Method) {
1023 return false;
1024 }
1025
1026 if !matches!(operator, Operator::Equal) {
1027 return false;
1028 }
1029
1030 let nodes = ctx.graph.nodes();
1031 let strings = ctx.graph.strings();
1032 for edge in ctx.graph.edges().edges_from(node_id) {
1033 if !matches!(
1034 edge.kind,
1035 EdgeKind::TypeOf {
1036 context: Some(TypeOfContext::Return),
1037 ..
1038 }
1039 ) {
1040 continue;
1041 }
1042 let Some(target_entry) = nodes.get(edge.target) else {
1043 continue;
1044 };
1045 if let Some(name) = strings.resolve(target_entry.name)
1046 && name.as_ref() == expected
1047 {
1048 return true;
1049 }
1050 }
1051 false
1052}
1053
1054fn match_async(entry: &NodeEntry, operator: &Operator, value: &Value) -> bool {
1063 let expected = value_to_bool(value);
1064 let Some(expected) = expected else {
1065 return false;
1066 };
1067
1068 match operator {
1069 Operator::Equal => entry.is_async == expected,
1070 _ => false,
1071 }
1072}
1073
1074fn match_static(entry: &NodeEntry, operator: &Operator, value: &Value) -> bool {
1079 let expected = value_to_bool(value);
1080 let Some(expected) = expected else {
1081 return false;
1082 };
1083
1084 match operator {
1085 Operator::Equal => entry.is_static == expected,
1086 _ => false,
1087 }
1088}
1089
1090fn value_to_bool(value: &Value) -> Option<bool> {
1098 match value {
1099 Value::Boolean(b) => Some(*b),
1100 Value::String(s) => match s.to_lowercase().as_str() {
1101 "true" | "yes" | "1" => Some(true),
1102 "false" | "no" | "0" => Some(false),
1103 _ => None,
1104 },
1105 _ => None,
1106 }
1107}
1108
1109fn match_callers(ctx: &GraphEvalContext, node_id: NodeId, value: &Value) -> bool {
1123 let Some(target_name) = value.as_string() else {
1124 return false;
1125 };
1126
1127 let method_part = extract_method_name(target_name);
1130
1131 for edge in ctx.graph.edges().edges_from(node_id) {
1133 if let EdgeKind::Calls { .. } = &edge.kind
1134 && let Some(target_entry) = ctx.graph.nodes().get(edge.target)
1135 {
1136 let callee_names = entry_query_texts(ctx.graph, target_entry);
1137
1138 if callee_names.iter().any(|callee_name| {
1139 language_aware_segments_match(
1140 ctx.graph,
1141 target_entry.file,
1142 callee_name,
1143 target_name,
1144 )
1145 }) {
1146 return true;
1147 }
1148
1149 if let Some(method) = &method_part
1153 && callee_names
1154 .iter()
1155 .filter_map(|callee_name| extract_method_name(callee_name))
1156 .any(|callee_method| method == &callee_method)
1157 {
1158 return true;
1159 }
1160 }
1161 }
1162 false
1163}
1164
1165#[must_use]
1169pub fn extract_method_name(qualified: &str) -> Option<String> {
1170 for sep in ["::", ".", "#", ":", "/"] {
1172 if let Some(pos) = qualified.rfind(sep) {
1173 let method = &qualified[pos + sep.len()..];
1174 if !method.is_empty() {
1175 return Some(method.to_string());
1176 }
1177 }
1178 }
1179 None
1180}
1181
1182fn match_callees(ctx: &GraphEvalContext, node_id: NodeId, value: &Value) -> bool {
1186 let Some(caller_name) = value.as_string() else {
1187 return false;
1188 };
1189
1190 for edge in ctx.graph.edges().edges_to(node_id) {
1192 if let EdgeKind::Calls { .. } = &edge.kind
1193 && let Some(source_entry) = ctx.graph.nodes().get(edge.source)
1194 && entry_query_texts(ctx.graph, source_entry)
1195 .iter()
1196 .any(|source_name| {
1197 language_aware_segments_match(
1198 ctx.graph,
1199 source_entry.file,
1200 source_name,
1201 caller_name,
1202 )
1203 })
1204 {
1205 return true;
1206 }
1207 }
1208 false
1209}
1210
1211fn match_imports(ctx: &GraphEvalContext, node_id: NodeId, value: &Value) -> bool {
1227 let Some(target_module) = value.as_string() else {
1228 return false;
1229 };
1230
1231 let Some(entry) = ctx.graph.nodes().get(node_id) else {
1232 return false;
1233 };
1234
1235 if entry.kind == NodeKind::Import && import_entry_matches(ctx.graph, entry, target_module) {
1236 return true;
1237 }
1238
1239 for edge in ctx.graph.edges().edges_from(node_id) {
1240 if import_edge_matches(ctx.graph, &edge, target_module) {
1241 return true;
1242 }
1243 }
1244 false
1245}
1246
1247#[must_use]
1253pub fn import_edge_matches<G: crate::graph::unified::concurrent::GraphAccess>(
1254 graph: &G,
1255 edge: &StoreEdgeRef,
1256 target_module: &str,
1257) -> bool {
1258 let EdgeKind::Imports { alias, is_wildcard } = &edge.kind else {
1259 return false;
1260 };
1261
1262 let target_match = graph
1264 .nodes()
1265 .get(edge.target)
1266 .is_some_and(|entry| import_entry_matches(graph, entry, target_module));
1267
1268 let alias_match = alias
1270 .and_then(|sid| graph.strings().resolve(sid))
1271 .is_some_and(|alias_str| {
1272 graph.nodes().get(edge.source).is_some_and(|entry| {
1273 import_text_matches(graph, entry.file, alias_str.as_ref(), target_module)
1274 })
1275 });
1276
1277 let wildcard_match = *is_wildcard && target_module == "*";
1279
1280 target_match || alias_match || wildcard_match
1281}
1282
1283#[must_use]
1287pub fn import_text_matches<G: crate::graph::unified::concurrent::GraphAccess>(
1288 graph: &G,
1289 file_id: FileId,
1290 candidate: &str,
1291 target_module: &str,
1292) -> bool {
1293 if candidate.contains(target_module) {
1294 return true;
1295 }
1296
1297 graph
1298 .files()
1299 .language_for_file(file_id)
1300 .is_some_and(|language| {
1301 let canonical_target = canonicalize_graph_qualified_name(language, target_module);
1302 canonical_target != target_module && candidate.contains(&canonical_target)
1303 })
1304}
1305
1306#[must_use]
1309pub fn import_entry_matches<G: crate::graph::unified::concurrent::GraphAccess>(
1310 graph: &G,
1311 entry: &NodeEntry,
1312 target_module: &str,
1313) -> bool {
1314 entry_query_texts(graph, entry)
1315 .iter()
1316 .any(|candidate| import_text_matches(graph, entry.file, candidate, target_module))
1317}
1318
1319#[must_use]
1324pub fn language_aware_segments_match<G: crate::graph::unified::concurrent::GraphAccess>(
1325 graph: &G,
1326 file_id: FileId,
1327 candidate: &str,
1328 expected: &str,
1329) -> bool {
1330 if segments_match(candidate, expected) {
1331 return true;
1332 }
1333
1334 graph
1335 .files()
1336 .language_for_file(file_id)
1337 .is_some_and(|language| {
1338 let canonical_expected = canonicalize_graph_qualified_name(language, expected);
1339 canonical_expected != expected && segments_match(candidate, &canonical_expected)
1340 })
1341}
1342
1343fn push_unique_query_text(texts: &mut Vec<String>, candidate: impl Into<String>) {
1344 let candidate = candidate.into();
1345 if !texts.iter().any(|existing| existing == &candidate) {
1346 texts.push(candidate);
1347 }
1348}
1349
1350#[must_use]
1356pub fn entry_query_texts<G: crate::graph::unified::concurrent::GraphAccess>(
1357 graph: &G,
1358 entry: &NodeEntry,
1359) -> Vec<String> {
1360 let mut texts = Vec::with_capacity(3);
1361
1362 if let Some(name) = graph.strings().resolve(entry.name) {
1363 push_unique_query_text(&mut texts, name.to_string());
1364 }
1365
1366 if let Some(qualified) = entry
1367 .qualified_name
1368 .and_then(|qualified_name_id| graph.strings().resolve(qualified_name_id))
1369 {
1370 push_unique_query_text(&mut texts, qualified.to_string());
1371
1372 if let Some(language) = graph.files().language_for_file(entry.file) {
1373 push_unique_query_text(
1374 &mut texts,
1375 display_graph_qualified_name(
1376 language,
1377 qualified.as_ref(),
1378 entry.kind,
1379 entry.is_static,
1380 ),
1381 );
1382 }
1383 }
1384
1385 texts
1386}
1387
1388fn match_exports(ctx: &GraphEvalContext, node_id: NodeId, value: &Value) -> bool {
1394 let Some(target_name) = value.as_string() else {
1395 return false;
1396 };
1397
1398 let Some(entry) = ctx.graph.nodes().get(node_id) else {
1399 return false;
1400 };
1401 let node_file = entry.file;
1402
1403 if !entry_query_texts(ctx.graph, entry).iter().any(|candidate| {
1404 language_aware_segments_match(ctx.graph, entry.file, candidate, target_name)
1405 }) {
1406 return false;
1407 }
1408
1409 let edges = ctx.graph.edges();
1410
1411 for edge in edges.edges_from(node_id) {
1413 if let EdgeKind::Exports { .. } = &edge.kind {
1414 if let Some(target_entry) = ctx.graph.nodes().get(edge.target)
1416 && target_entry.file == node_file
1417 {
1418 return true;
1419 }
1420 }
1421 }
1422
1423 for edge in edges.edges_to(node_id) {
1425 if let EdgeKind::Exports { .. } = &edge.kind {
1426 if let Some(source_entry) = ctx.graph.nodes().get(edge.source)
1428 && source_entry.file == node_file
1429 {
1430 return true;
1431 }
1432 }
1433 }
1434
1435 false
1436}
1437
1438fn match_references(
1442 ctx: &GraphEvalContext,
1443 node_id: NodeId,
1444 operator: &Operator,
1445 value: &Value,
1446) -> bool {
1447 let Some(entry) = ctx.graph.nodes().get(node_id) else {
1449 return false;
1450 };
1451
1452 let name_matches = match (operator, value) {
1453 (Operator::Equal, Value::String(target)) => entry_query_texts(ctx.graph, entry)
1454 .iter()
1455 .any(|candidate| candidate == target || candidate.ends_with(&format!("::{target}"))),
1456 (Operator::Regex, Value::Regex(rv)) => get_or_compile_regex(
1457 &rv.pattern,
1458 rv.flags.case_insensitive,
1459 rv.flags.multiline,
1460 rv.flags.dot_all,
1461 )
1462 .map(|re| {
1463 entry_query_texts(ctx.graph, entry)
1464 .iter()
1465 .any(|candidate| regex_is_match(&re, candidate))
1466 })
1467 .unwrap_or(false),
1468 _ => false,
1469 };
1470
1471 if !name_matches {
1472 return false;
1473 }
1474
1475 for edge in ctx.graph.edges().edges_to(node_id) {
1478 let is_reference = matches!(
1479 &edge.kind,
1480 EdgeKind::References
1481 | EdgeKind::Calls { .. }
1482 | EdgeKind::Imports { .. }
1483 | EdgeKind::FfiCall { .. }
1484 );
1485 if is_reference {
1486 return true;
1487 }
1488 }
1489
1490 false
1491}
1492
1493fn match_implements(ctx: &GraphEvalContext, node_id: NodeId, value: &Value) -> bool {
1495 let Some(trait_name) = value.as_string() else {
1496 return false;
1497 };
1498
1499 for edge in ctx.graph.edges().edges_from(node_id) {
1500 if let EdgeKind::Implements = &edge.kind
1501 && let Some(target_entry) = ctx.graph.nodes().get(edge.target)
1502 && entry_query_texts(ctx.graph, target_entry)
1503 .iter()
1504 .any(|name| {
1505 language_aware_segments_match(ctx.graph, target_entry.file, name, trait_name)
1506 })
1507 {
1508 return true;
1509 }
1510 }
1511 false
1512}
1513
1514fn node_kind_to_scope_type(kind: NodeKind) -> &'static str {
1526 match kind {
1527 NodeKind::Function | NodeKind::Test => "function",
1528 NodeKind::Method => "method",
1529 NodeKind::Class | NodeKind::Service => "class",
1530 NodeKind::Interface | NodeKind::Trait => "interface",
1531 NodeKind::Struct => "struct",
1532 NodeKind::Enum => "enum",
1533 NodeKind::Module => "module",
1534 NodeKind::Macro => "macro",
1535 NodeKind::Component => "component",
1536 NodeKind::Resource | NodeKind::Endpoint => "resource",
1537 NodeKind::Variable => "variable",
1539 NodeKind::Constant => "constant",
1540 NodeKind::Type => "type",
1541 NodeKind::EnumVariant => "enumvariant",
1542 NodeKind::Import => "import",
1543 NodeKind::Export => "export",
1544 NodeKind::CallSite => "callsite",
1545 NodeKind::Parameter => "parameter",
1546 NodeKind::Property => "property",
1547 NodeKind::StyleRule => "style_rule",
1548 NodeKind::StyleAtRule => "style_at_rule",
1549 NodeKind::StyleVariable => "style_variable",
1550 NodeKind::Lifetime => "lifetime",
1551 NodeKind::TypeParameter => "type_parameter",
1552 NodeKind::Annotation => "annotation",
1553 NodeKind::AnnotationValue => "annotation_value",
1554 NodeKind::LambdaTarget => "lambda_target",
1555 NodeKind::JavaModule => "java_module",
1556 NodeKind::EnumConstant => "enum_constant",
1557 NodeKind::Other => "other",
1558 }
1559}
1560
1561fn match_scope(
1562 ctx: &GraphEvalContext,
1563 node_id: NodeId,
1564 field: &str,
1565 operator: &Operator,
1566 value: &Value,
1567) -> bool {
1568 let scope_part = field.strip_prefix("scope.").unwrap_or("");
1569 match scope_part {
1570 "type" => match_scope_type(ctx, node_id, operator, value),
1571 "name" => match_scope_name(ctx, node_id, operator, value),
1572 "parent" => match_scope_parent_name(ctx, node_id, operator, value),
1573 "ancestor" => match_scope_ancestor_name(ctx, node_id, operator, value),
1574 _ => false,
1575 }
1576}
1577
1578fn match_scope_type(
1579 ctx: &GraphEvalContext,
1580 node_id: NodeId,
1581 operator: &Operator,
1582 value: &Value,
1583) -> bool {
1584 for edge in ctx.graph.edges().edges_to(node_id) {
1585 if let EdgeKind::Contains = &edge.kind
1586 && let Some(parent) = ctx.graph.nodes().get(edge.source)
1587 {
1588 let scope_type = node_kind_to_scope_type(parent.kind);
1590 return match (operator, value) {
1591 (Operator::Equal, Value::String(exp)) => scope_type == exp,
1593 (Operator::Regex, Value::Regex(rv)) => get_or_compile_regex(
1594 &rv.pattern,
1595 rv.flags.case_insensitive,
1596 rv.flags.multiline,
1597 rv.flags.dot_all,
1598 )
1599 .map(|re| regex_is_match(&re, scope_type))
1600 .unwrap_or(false),
1601 _ => false,
1602 };
1603 }
1604 }
1605 false
1606}
1607
1608fn match_scope_name(
1609 ctx: &GraphEvalContext,
1610 node_id: NodeId,
1611 operator: &Operator,
1612 value: &Value,
1613) -> bool {
1614 for edge in ctx.graph.edges().edges_to(node_id) {
1615 if let EdgeKind::Contains = &edge.kind
1616 && let Some(parent) = ctx.graph.nodes().get(edge.source)
1617 && let Some(name) = ctx.graph.strings().resolve(parent.name)
1618 {
1619 return match (operator, value) {
1620 (Operator::Equal, Value::String(exp)) => segments_match(&name, exp),
1622 (Operator::Regex, Value::Regex(rv)) => get_or_compile_regex(
1623 &rv.pattern,
1624 rv.flags.case_insensitive,
1625 rv.flags.multiline,
1626 rv.flags.dot_all,
1627 )
1628 .map(|re| regex_is_match(&re, &name))
1629 .unwrap_or(false),
1630 _ => false,
1631 };
1632 }
1633 }
1634 false
1635}
1636
1637fn match_scope_parent_name(
1641 ctx: &GraphEvalContext,
1642 node_id: NodeId,
1643 operator: &Operator,
1644 value: &Value,
1645) -> bool {
1646 for edge in ctx.graph.edges().edges_to(node_id) {
1647 if let EdgeKind::Contains = &edge.kind
1648 && let Some(parent) = ctx.graph.nodes().get(edge.source)
1649 && let Some(name) = ctx.graph.strings().resolve(parent.name)
1650 {
1651 return match (operator, value) {
1652 (Operator::Equal, Value::String(exp)) => segments_match(&name, exp),
1654 (Operator::Regex, Value::Regex(rv)) => get_or_compile_regex(
1655 &rv.pattern,
1656 rv.flags.case_insensitive,
1657 rv.flags.multiline,
1658 rv.flags.dot_all,
1659 )
1660 .map(|re| regex_is_match(&re, &name))
1661 .unwrap_or(false),
1662 _ => false,
1663 };
1664 }
1665 }
1666 false
1667}
1668
1669fn match_scope_ancestor_name(
1674 ctx: &GraphEvalContext,
1675 node_id: NodeId,
1676 operator: &Operator,
1677 value: &Value,
1678) -> bool {
1679 let mut current = node_id;
1680 let mut visited = HashSet::new();
1681 visited.insert(node_id);
1682
1683 loop {
1684 let mut found_parent = false;
1685 for edge in ctx.graph.edges().edges_to(current) {
1686 if let EdgeKind::Contains = &edge.kind {
1687 if visited.contains(&edge.source) {
1689 continue;
1690 }
1691 visited.insert(edge.source);
1692
1693 found_parent = true;
1694 current = edge.source;
1695 if let Some(parent) = ctx.graph.nodes().get(current)
1696 && let Some(name) = ctx.graph.strings().resolve(parent.name)
1697 {
1698 let matches = match (operator, value) {
1699 (Operator::Equal, Value::String(exp)) => segments_match(&name, exp),
1701 (Operator::Regex, Value::Regex(rv)) => get_or_compile_regex(
1702 &rv.pattern,
1703 rv.flags.case_insensitive,
1704 rv.flags.multiline,
1705 rv.flags.dot_all,
1706 )
1707 .map(|re| regex_is_match(&re, &name))
1708 .unwrap_or(false),
1709 _ => false,
1710 };
1711 if matches {
1712 return true;
1713 }
1714 }
1715 break;
1716 }
1717 }
1718 if !found_parent {
1719 break;
1720 }
1721 }
1722 false
1723}
1724
1725pub fn evaluate_subquery(ctx: &GraphEvalContext, expr: &Expr) -> Result<HashSet<NodeId>> {
1737 let recursion_limits = crate::config::RecursionLimits::load_or_default()?;
1738 let expr_depth = recursion_limits.effective_expr_depth()?;
1739 let mut guard = crate::query::security::RecursionGuard::new(expr_depth)?;
1740
1741 let arena = ctx.graph.nodes();
1742 let mut matches = HashSet::new();
1743 for (id, _) in arena.iter() {
1744 if evaluate_node(ctx, id, expr, &mut guard)? {
1745 matches.insert(id);
1746 }
1747 }
1748 Ok(matches)
1749}
1750
1751fn match_callers_subquery(
1757 ctx: &GraphEvalContext,
1758 node_id: NodeId,
1759 subquery_matches: Option<&HashSet<NodeId>>,
1760) -> Result<bool> {
1761 let Some(matches) = subquery_matches else {
1762 return Err(anyhow!(
1763 "subquery cache miss: precompute_subqueries did not populate cache for this relation predicate"
1764 ));
1765 };
1766 for edge in ctx.graph.edges().edges_from(node_id) {
1767 if let EdgeKind::Calls { .. } = &edge.kind
1768 && matches.contains(&edge.target)
1769 {
1770 return Ok(true);
1771 }
1772 }
1773 Ok(false)
1774}
1775
1776fn match_callees_subquery(
1778 ctx: &GraphEvalContext,
1779 node_id: NodeId,
1780 subquery_matches: Option<&HashSet<NodeId>>,
1781) -> Result<bool> {
1782 let Some(matches) = subquery_matches else {
1783 return Err(anyhow!(
1784 "subquery cache miss: precompute_subqueries did not populate cache for this relation predicate"
1785 ));
1786 };
1787 for edge in ctx.graph.edges().edges_to(node_id) {
1788 if let EdgeKind::Calls { .. } = &edge.kind
1789 && matches.contains(&edge.source)
1790 {
1791 return Ok(true);
1792 }
1793 }
1794 Ok(false)
1795}
1796
1797fn match_imports_subquery(
1803 ctx: &GraphEvalContext,
1804 node_id: NodeId,
1805 subquery_matches: Option<&HashSet<NodeId>>,
1806) -> Result<bool> {
1807 let Some(matches) = subquery_matches else {
1808 return Err(anyhow!(
1809 "subquery cache miss: precompute_subqueries did not populate cache for this relation predicate"
1810 ));
1811 };
1812 for edge in ctx.graph.edges().edges_from(node_id) {
1813 if let EdgeKind::Imports { .. } = &edge.kind
1814 && matches.contains(&edge.target)
1815 {
1816 return Ok(true);
1817 }
1818 }
1819 Ok(false)
1820}
1821
1822fn match_exports_subquery(
1824 ctx: &GraphEvalContext,
1825 node_id: NodeId,
1826 subquery_matches: Option<&HashSet<NodeId>>,
1827) -> Result<bool> {
1828 let Some(matches) = subquery_matches else {
1829 return Err(anyhow!(
1830 "subquery cache miss: precompute_subqueries did not populate cache for this relation predicate"
1831 ));
1832 };
1833 for edge in ctx.graph.edges().edges_from(node_id) {
1834 if let EdgeKind::Exports { .. } = &edge.kind
1835 && matches.contains(&edge.target)
1836 {
1837 return Ok(true);
1838 }
1839 }
1840 Ok(false)
1841}
1842
1843fn match_implements_subquery(
1845 ctx: &GraphEvalContext,
1846 node_id: NodeId,
1847 subquery_matches: Option<&HashSet<NodeId>>,
1848) -> Result<bool> {
1849 let Some(matches) = subquery_matches else {
1850 return Err(anyhow!(
1851 "subquery cache miss: precompute_subqueries did not populate cache for this relation predicate"
1852 ));
1853 };
1854 for edge in ctx.graph.edges().edges_from(node_id) {
1855 if let EdgeKind::Implements = &edge.kind
1856 && matches.contains(&edge.target)
1857 {
1858 return Ok(true);
1859 }
1860 }
1861 Ok(false)
1862}
1863
1864fn match_references_subquery(
1866 ctx: &GraphEvalContext,
1867 node_id: NodeId,
1868 subquery_matches: Option<&HashSet<NodeId>>,
1869) -> Result<bool> {
1870 let Some(matches) = subquery_matches else {
1871 return Err(anyhow!(
1872 "subquery cache miss: precompute_subqueries did not populate cache for this relation predicate"
1873 ));
1874 };
1875 for edge in ctx.graph.edges().edges_to(node_id) {
1876 let is_reference = matches!(
1877 &edge.kind,
1878 EdgeKind::References
1879 | EdgeKind::Calls { .. }
1880 | EdgeKind::Imports { .. }
1881 | EdgeKind::FfiCall { .. }
1882 );
1883 if is_reference && matches.contains(&edge.source) {
1884 return Ok(true);
1885 }
1886 }
1887 Ok(false)
1888}
1889
1890pub fn evaluate_join(
1906 ctx: &GraphEvalContext,
1907 join: &JoinExpr,
1908 max_results: Option<usize>,
1909) -> Result<JoinEvalResult> {
1910 let lhs_matches = evaluate_subquery(ctx, &join.left)?;
1911 let rhs_matches = evaluate_subquery(ctx, &join.right)?;
1912 let cap = max_results.unwrap_or(DEFAULT_JOIN_RESULT_CAP);
1913
1914 let mut pairs = Vec::new();
1915 let mut truncated = false;
1916 'outer: for &lhs_id in &lhs_matches {
1917 for edge in ctx.graph.edges().edges_from(lhs_id) {
1918 if edge_matches_join_kind(&edge.kind, &join.edge) && rhs_matches.contains(&edge.target)
1919 {
1920 pairs.push((lhs_id, edge.target));
1921 if pairs.len() >= cap {
1922 truncated = true;
1923 break 'outer;
1924 }
1925 }
1926 }
1927 }
1928 Ok(JoinEvalResult { pairs, truncated })
1929}
1930
1931pub struct JoinEvalResult {
1933 pub pairs: Vec<(NodeId, NodeId)>,
1935 pub truncated: bool,
1937}
1938
1939const DEFAULT_JOIN_RESULT_CAP: usize = 10_000;
1943
1944fn edge_matches_join_kind(edge_kind: &EdgeKind, join_kind: &JoinEdgeKind) -> bool {
1946 match join_kind {
1947 JoinEdgeKind::Calls => matches!(edge_kind, EdgeKind::Calls { .. }),
1948 JoinEdgeKind::Imports => matches!(edge_kind, EdgeKind::Imports { .. }),
1949 JoinEdgeKind::Inherits => matches!(edge_kind, EdgeKind::Inherits),
1950 JoinEdgeKind::Implements => matches!(edge_kind, EdgeKind::Implements),
1951 }
1952}
1953
1954#[cfg(test)]
1955mod tests {
1956 use super::*;
1957 use crate::graph::node::Language;
1958 use crate::query::types::{Condition, Field, Span};
1959 use std::path::Path;
1960
1961 #[test]
1962 fn test_import_text_matches_canonicalized_qualified_imports() {
1963 let mut graph = CodeGraph::new();
1964 let file_id = graph
1965 .files_mut()
1966 .register(Path::new("src/FileProcessor.cs"))
1967 .unwrap();
1968 assert!(graph.files_mut().set_language(file_id, Language::CSharp));
1969
1970 assert!(import_text_matches(
1971 &graph,
1972 file_id,
1973 "System::IO",
1974 "System.IO"
1975 ));
1976 assert!(import_text_matches(
1977 &graph,
1978 file_id,
1979 "System::Collections::Generic",
1980 "System.Collections.Generic"
1981 ));
1982 assert!(!import_text_matches(
1983 &graph,
1984 file_id,
1985 "System::Text",
1986 "System.IO"
1987 ));
1988 }
1989
1990 #[test]
1991 fn test_language_aware_segments_match_supports_ruby_method_separators() {
1992 let mut graph = CodeGraph::new();
1993 let file_id = graph
1994 .files_mut()
1995 .register(Path::new("app/models/user.rb"))
1996 .unwrap();
1997 assert!(graph.files_mut().set_language(file_id, Language::Ruby));
1998
1999 assert!(language_aware_segments_match(
2000 &graph,
2001 file_id,
2002 "Admin::Users::Controller::show",
2003 "Admin::Users::Controller#show"
2004 ));
2005 assert!(language_aware_segments_match(
2006 &graph,
2007 file_id,
2008 "Admin::Users::Controller::show",
2009 "show"
2010 ));
2011 assert!(!language_aware_segments_match(
2012 &graph,
2013 file_id,
2014 "Admin::Users::Controller::index",
2015 "Admin::Users::Controller#show"
2016 ));
2017 }
2018
2019 #[test]
2020 fn test_normalize_kind() {
2021 assert_eq!(normalize_kind("trait"), "interface");
2023 assert_eq!(normalize_kind("TRAIT"), "TRAIT"); assert_eq!(normalize_kind("field"), "property");
2025 assert_eq!(normalize_kind("namespace"), "module");
2026 assert_eq!(normalize_kind("function"), "function"); }
2028
2029 #[test]
2030 fn test_graph_eval_context_builder() {
2031 let graph = CodeGraph::new();
2032 let pm = PluginManager::new();
2033 let ctx = GraphEvalContext::new(&graph, &pm)
2034 .with_workspace_root(Path::new("/test"))
2035 .with_parallel_disabled(true);
2036
2037 assert!(ctx.disable_parallel);
2038 assert_eq!(ctx.workspace_root, Some(Path::new("/test")));
2039 }
2040
2041 fn subquery_condition(field: &str, inner: Expr, start: usize, end: usize) -> Expr {
2047 Expr::Condition(Condition {
2048 field: Field(field.to_string()),
2049 operator: Operator::Equal,
2050 value: Value::Subquery(Box::new(inner)),
2051 span: Span::with_position(start, end, 1, start + 1),
2052 })
2053 }
2054
2055 fn kind_condition(kind: &str) -> Expr {
2057 Expr::Condition(Condition {
2058 field: Field("kind".to_string()),
2059 operator: Operator::Equal,
2060 value: Value::String(kind.to_string()),
2061 span: Span::default(),
2062 })
2063 }
2064
2065 #[test]
2066 fn test_collect_subquery_exprs_post_order_depth_2() {
2067 let inner_subquery = subquery_condition("callees", kind_condition("function"), 20, 40);
2071 let outer_subquery = subquery_condition("callers", inner_subquery, 0, 50);
2072
2073 let mut out = Vec::new();
2074 collect_subquery_exprs(&outer_subquery, &mut out);
2075
2076 assert_eq!(
2078 out.len(),
2079 2,
2080 "should collect both inner and outer subqueries"
2081 );
2082 assert_eq!(out[0].0, (20, 40), "inner subquery span should come first");
2083 assert_eq!(out[1].0, (0, 50), "outer subquery span should come second");
2084 }
2085
2086 #[test]
2087 fn test_collect_subquery_exprs_post_order_depth_3() {
2088 let innermost = subquery_condition("imports", kind_condition("function"), 30, 50);
2090 let middle = subquery_condition("callees", innermost, 15, 55);
2091 let outer = subquery_condition("callers", middle, 0, 60);
2092
2093 let mut out = Vec::new();
2094 collect_subquery_exprs(&outer, &mut out);
2095
2096 assert_eq!(out.len(), 3, "should collect all three nested subqueries");
2097 assert_eq!(out[0].0, (30, 50), "innermost should come first");
2098 assert_eq!(out[1].0, (15, 55), "middle should come second");
2099 assert_eq!(out[2].0, (0, 60), "outer should come last");
2100 }
2101
2102 #[test]
2103 fn test_collect_subquery_exprs_and_or_branches() {
2104 let left = subquery_condition("callers", kind_condition("function"), 0, 25);
2106 let right = subquery_condition("callees", kind_condition("method"), 30, 55);
2107 let expr = Expr::And(vec![left, right]);
2108
2109 let mut out = Vec::new();
2110 collect_subquery_exprs(&expr, &mut out);
2111
2112 assert_eq!(out.len(), 2, "should collect subqueries from both branches");
2113 assert_eq!(out[0].0, (0, 25), "left branch subquery");
2114 assert_eq!(out[1].0, (30, 55), "right branch subquery");
2115 }
2116
2117 #[test]
2118 fn test_collect_subquery_exprs_no_subqueries() {
2119 let expr = kind_condition("function");
2121
2122 let mut out = Vec::new();
2123 collect_subquery_exprs(&expr, &mut out);
2124
2125 assert!(
2126 out.is_empty(),
2127 "should collect nothing for plain conditions"
2128 );
2129 }
2130
2131 use crate::graph::unified::edge::{BidirectionalEdgeStore, FfiConvention};
2136 use crate::graph::unified::storage::{
2137 AuxiliaryIndices, FileRegistry, NodeArena, StringInterner,
2138 };
2139
2140 fn build_ffi_graph() -> (CodeGraph, NodeId, NodeId) {
2142 let mut arena = NodeArena::new();
2143 let edges = BidirectionalEdgeStore::new();
2144 let mut strings = StringInterner::new();
2145 let mut files = FileRegistry::new();
2146 let mut indices = AuxiliaryIndices::new();
2147
2148 let caller_name = strings.intern("caller_fn").unwrap();
2149 let target_name = strings.intern("ffi_target").unwrap();
2150 let file_id = files.register(Path::new("test.r")).unwrap();
2151
2152 let caller_id = arena
2153 .alloc(NodeEntry {
2154 kind: NodeKind::Function,
2155 name: caller_name,
2156 file: file_id,
2157 start_byte: 0,
2158 end_byte: 100,
2159 start_line: 1,
2160 start_column: 0,
2161 end_line: 5,
2162 end_column: 0,
2163 signature: None,
2164 doc: None,
2165 qualified_name: None,
2166 visibility: None,
2167 is_async: false,
2168 is_static: false,
2169 is_unsafe: false,
2170 body_hash: None,
2171 })
2172 .unwrap();
2173
2174 let target_id = arena
2175 .alloc(NodeEntry {
2176 kind: NodeKind::Function,
2177 name: target_name,
2178 file: file_id,
2179 start_byte: 200,
2180 end_byte: 300,
2181 start_line: 10,
2182 start_column: 0,
2183 end_line: 15,
2184 end_column: 0,
2185 signature: None,
2186 doc: None,
2187 qualified_name: None,
2188 visibility: None,
2189 is_async: false,
2190 is_static: false,
2191 is_unsafe: false,
2192 body_hash: None,
2193 })
2194 .unwrap();
2195
2196 indices.add(caller_id, NodeKind::Function, caller_name, None, file_id);
2197 indices.add(target_id, NodeKind::Function, target_name, None, file_id);
2198
2199 edges.add_edge(
2200 caller_id,
2201 target_id,
2202 EdgeKind::FfiCall {
2203 convention: FfiConvention::C,
2204 },
2205 file_id,
2206 );
2207
2208 let graph = CodeGraph::from_components(
2209 arena,
2210 edges,
2211 strings,
2212 files,
2213 indices,
2214 crate::graph::unified::NodeMetadataStore::new(),
2215 );
2216 (graph, caller_id, target_id)
2217 }
2218
2219 #[test]
2220 fn test_ffi_call_edge_in_references_predicate() {
2221 let (graph, _caller_id, target_id) = build_ffi_graph();
2222 let pm = PluginManager::new();
2223 let ctx = GraphEvalContext::new(&graph, &pm);
2224
2225 let result = match_references(
2227 &ctx,
2228 target_id,
2229 &Operator::Equal,
2230 &Value::String("ffi_target".to_string()),
2231 );
2232 assert!(result, "references: predicate should match FfiCall edges");
2233 }
2234
2235 #[test]
2236 fn test_ffi_call_edge_in_references_subquery() {
2237 let (graph, caller_id, target_id) = build_ffi_graph();
2238 let pm = PluginManager::new();
2239 let ctx = GraphEvalContext::new(&graph, &pm);
2240
2241 let mut subquery_results = HashSet::new();
2243 subquery_results.insert(caller_id);
2244
2245 let result = match_references_subquery(&ctx, target_id, Some(&subquery_results)).unwrap();
2248 assert!(
2249 result,
2250 "references subquery should match FfiCall edge sources"
2251 );
2252 }
2253
2254 fn build_returns_graph() -> (CodeGraph, NodeId, NodeId, NodeId) {
2264 let mut arena = NodeArena::new();
2265 let edges = BidirectionalEdgeStore::new();
2266 let mut strings = StringInterner::new();
2267 let mut files = FileRegistry::new();
2268 let mut indices = AuxiliaryIndices::new();
2269
2270 let returner_name = strings.intern("returner_fn").unwrap();
2271 let plain_name = strings.intern("plain_fn").unwrap();
2272 let error_name = strings.intern("error").unwrap();
2273 let file_id = files.register(Path::new("test.go")).unwrap();
2274
2275 let returner_id = arena
2276 .alloc(NodeEntry {
2277 kind: NodeKind::Function,
2278 name: returner_name,
2279 file: file_id,
2280 start_byte: 0,
2281 end_byte: 100,
2282 start_line: 1,
2283 start_column: 0,
2284 end_line: 5,
2285 end_column: 0,
2286 signature: None,
2287 doc: None,
2288 qualified_name: None,
2289 visibility: None,
2290 is_async: false,
2291 is_static: false,
2292 is_unsafe: false,
2293 body_hash: None,
2294 })
2295 .unwrap();
2296
2297 let plain_id = arena
2298 .alloc(NodeEntry {
2299 kind: NodeKind::Function,
2300 name: plain_name,
2301 file: file_id,
2302 start_byte: 200,
2303 end_byte: 300,
2304 start_line: 10,
2305 start_column: 0,
2306 end_line: 15,
2307 end_column: 0,
2308 signature: None,
2309 doc: None,
2310 qualified_name: None,
2311 visibility: None,
2312 is_async: false,
2313 is_static: false,
2314 is_unsafe: false,
2315 body_hash: None,
2316 })
2317 .unwrap();
2318
2319 let error_type_id = arena
2320 .alloc(NodeEntry {
2321 kind: NodeKind::Type,
2322 name: error_name,
2323 file: file_id,
2324 start_byte: 400,
2325 end_byte: 410,
2326 start_line: 20,
2327 start_column: 0,
2328 end_line: 20,
2329 end_column: 10,
2330 signature: None,
2331 doc: None,
2332 qualified_name: None,
2333 visibility: None,
2334 is_async: false,
2335 is_static: false,
2336 is_unsafe: false,
2337 body_hash: None,
2338 })
2339 .unwrap();
2340
2341 indices.add(
2342 returner_id,
2343 NodeKind::Function,
2344 returner_name,
2345 None,
2346 file_id,
2347 );
2348 indices.add(plain_id, NodeKind::Function, plain_name, None, file_id);
2349 indices.add(error_type_id, NodeKind::Type, error_name, None, file_id);
2350
2351 edges.add_edge(
2352 returner_id,
2353 error_type_id,
2354 EdgeKind::TypeOf {
2355 context: Some(TypeOfContext::Return),
2356 index: None,
2357 name: None,
2358 },
2359 file_id,
2360 );
2361
2362 let graph = CodeGraph::from_components(
2363 arena,
2364 edges,
2365 strings,
2366 files,
2367 indices,
2368 crate::graph::unified::NodeMetadataStore::new(),
2369 );
2370 (graph, returner_id, plain_id, error_type_id)
2371 }
2372
2373 #[test]
2374 fn test_match_returns_byte_exact_hit() {
2375 let (graph, returner_id, _plain_id, _error_id) = build_returns_graph();
2376 let pm = PluginManager::new();
2377 let ctx = GraphEvalContext::new(&graph, &pm);
2378 let entry = graph.nodes().get(returner_id).expect("returner exists");
2379
2380 assert!(match_returns(
2383 &ctx,
2384 returner_id,
2385 entry,
2386 &Operator::Equal,
2387 &Value::String("error".to_string()),
2388 ));
2389 }
2390
2391 #[test]
2392 fn test_match_returns_no_edges_misses() {
2393 let (graph, _returner_id, plain_id, _error_id) = build_returns_graph();
2394 let pm = PluginManager::new();
2395 let ctx = GraphEvalContext::new(&graph, &pm);
2396 let entry = graph.nodes().get(plain_id).expect("plain_fn exists");
2397
2398 assert!(!match_returns(
2402 &ctx,
2403 plain_id,
2404 entry,
2405 &Operator::Equal,
2406 &Value::String("error".to_string()),
2407 ));
2408 }
2409
2410 #[test]
2411 fn test_match_returns_byte_exact_miss_on_different_target_name() {
2412 let (graph, returner_id, _plain_id, _error_id) = build_returns_graph();
2413 let pm = PluginManager::new();
2414 let ctx = GraphEvalContext::new(&graph, &pm);
2415 let entry = graph.nodes().get(returner_id).expect("returner exists");
2416
2417 assert!(!match_returns(
2421 &ctx,
2422 returner_id,
2423 entry,
2424 &Operator::Equal,
2425 &Value::String("Error".to_string()),
2426 ));
2427 }
2428
2429 #[test]
2430 fn test_match_returns_rejects_non_callable_kinds() {
2431 let (graph, _returner_id, _plain_id, error_id) = build_returns_graph();
2432 let pm = PluginManager::new();
2433 let ctx = GraphEvalContext::new(&graph, &pm);
2434 let entry = graph.nodes().get(error_id).expect("error type exists");
2437
2438 assert!(!match_returns(
2439 &ctx,
2440 error_id,
2441 entry,
2442 &Operator::Equal,
2443 &Value::String("error".to_string()),
2444 ));
2445 }
2446}