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}
100
101impl<'a> GraphEvalContext<'a> {
102 #[must_use]
104 pub fn new(graph: &'a CodeGraph, plugin_manager: &'a PluginManager) -> Self {
105 Self {
106 graph,
107 plugin_manager,
108 workspace_root: None,
109 disable_parallel: false,
110 subquery_cache: HashMap::new(),
111 }
112 }
113
114 #[must_use]
116 pub fn with_workspace_root(mut self, root: &'a Path) -> Self {
117 self.workspace_root = Some(root);
118 self
119 }
120
121 #[must_use]
123 pub fn with_parallel_disabled(mut self, disabled: bool) -> Self {
124 self.disable_parallel = disabled;
125 self
126 }
127
128 pub fn precompute_subqueries(&mut self, expr: &Expr) -> Result<()> {
137 let mut subquery_exprs = Vec::new();
138 collect_subquery_exprs(expr, &mut subquery_exprs);
139
140 for (span_key, inner_expr) in subquery_exprs {
141 if !self.subquery_cache.contains_key(&span_key) {
142 let result_set = evaluate_subquery(self, inner_expr)?;
143 self.subquery_cache.insert(span_key, Arc::new(result_set));
144 }
145 }
146 Ok(())
147 }
148}
149
150fn collect_subquery_exprs<'a>(expr: &'a Expr, out: &mut Vec<((usize, usize), &'a Expr)>) {
158 match expr {
159 Expr::Condition(cond) => {
160 if let Value::Subquery(inner) = &cond.value {
161 collect_subquery_exprs(inner, out);
163 out.push(((cond.span.start, cond.span.end), inner));
165 }
166 }
167 Expr::And(operands) | Expr::Or(operands) => {
168 for op in operands {
169 collect_subquery_exprs(op, out);
170 }
171 }
172 Expr::Not(inner) => collect_subquery_exprs(inner, out),
173 Expr::Join(join) => {
174 collect_subquery_exprs(&join.left, out);
175 collect_subquery_exprs(&join.right, out);
176 }
177 }
178}
179
180pub fn evaluate_all(ctx: &mut GraphEvalContext, expr: &Expr) -> Result<Vec<NodeId>> {
186 ctx.precompute_subqueries(expr)?;
190
191 let arena = ctx.graph.nodes();
192
193 let recursion_limits = crate::config::RecursionLimits::load_or_default()?;
195 let expr_depth = recursion_limits.effective_expr_depth()?;
196 let mut guard = crate::query::security::RecursionGuard::new(expr_depth)?;
197
198 if ctx.disable_parallel {
199 let mut matches = Vec::new();
201 for (id, entry) in arena.iter() {
202 if entry.is_unified_loser() {
208 continue;
209 }
210 if evaluate_node(ctx, id, expr, &mut guard)? {
211 matches.push(id);
212 }
213 }
214 Ok(matches)
215 } else {
216 use rayon::prelude::*;
218
219 let node_ids: Vec<_> = arena
220 .iter()
221 .filter(|(_id, entry)| !entry.is_unified_loser())
222 .map(|(id, _)| id)
223 .collect();
224 let results: Vec<Result<Option<NodeId>>> = node_ids
225 .into_par_iter()
226 .map(|id| {
227 let mut thread_guard = crate::query::security::RecursionGuard::new(expr_depth)?;
228 evaluate_node(ctx, id, expr, &mut thread_guard)
229 .map(|m| if m { Some(id) } else { None })
230 })
231 .collect();
232
233 let mut matches = Vec::new();
235 for result in results {
236 if let Some(id) = result? {
237 matches.push(id);
238 }
239 }
240 Ok(matches)
241 }
242}
243
244pub fn evaluate_node(
250 ctx: &GraphEvalContext,
251 node_id: NodeId,
252 expr: &Expr,
253 guard: &mut crate::query::security::RecursionGuard,
254) -> Result<bool> {
255 guard.enter()?;
256
257 let result = match expr {
258 Expr::Condition(cond) => evaluate_condition(ctx, node_id, cond),
259 Expr::And(operands) => {
260 for operand in operands {
261 if !evaluate_node(ctx, node_id, operand, guard)? {
262 guard.exit();
263 return Ok(false);
264 }
265 }
266 Ok(true)
267 }
268 Expr::Or(operands) => {
269 for operand in operands {
270 if evaluate_node(ctx, node_id, operand, guard)? {
271 guard.exit();
272 return Ok(true);
273 }
274 }
275 Ok(false)
276 }
277 Expr::Not(inner) => Ok(!evaluate_node(ctx, node_id, inner, guard)?),
278 Expr::Join(_) => {
279 Err(anyhow::anyhow!(
282 "Join expressions cannot be evaluated per-node; use execute_join instead"
283 ))
284 }
285 };
286
287 guard.exit();
288 result
289}
290
291fn evaluate_condition(ctx: &GraphEvalContext, node_id: NodeId, cond: &Condition) -> Result<bool> {
292 let Some(entry) = ctx.graph.nodes().get(node_id) else {
293 return Ok(false);
294 };
295
296 match cond.field.as_str() {
297 "kind" => Ok(match_kind(ctx, entry, &cond.operator, &cond.value)),
298 "name" => Ok(match_name(ctx, entry, &cond.operator, &cond.value)),
299 "path" => Ok(match_path(ctx, entry, &cond.operator, &cond.value)),
300 "lang" | "language" => Ok(match_lang(ctx, entry, &cond.operator, &cond.value)),
301 "visibility" => Ok(match_visibility(ctx, entry, &cond.operator, &cond.value)),
302 "async" => Ok(match_async(entry, &cond.operator, &cond.value)),
303 "static" => Ok(match_static(entry, &cond.operator, &cond.value)),
304 "callers" => {
305 if matches!(cond.value, Value::Subquery(_)) {
306 let key = (cond.span.start, cond.span.end);
307 let cached = ctx.subquery_cache.get(&key).cloned();
308 match_callers_subquery(ctx, node_id, cached.as_deref())
309 } else {
310 Ok(match_callers(ctx, node_id, &cond.value))
311 }
312 }
313 "callees" => {
314 if matches!(cond.value, Value::Subquery(_)) {
315 let key = (cond.span.start, cond.span.end);
316 let cached = ctx.subquery_cache.get(&key).cloned();
317 match_callees_subquery(ctx, node_id, cached.as_deref())
318 } else {
319 Ok(match_callees(ctx, node_id, &cond.value))
320 }
321 }
322 "imports" => {
323 if matches!(cond.value, Value::Subquery(_)) {
324 let key = (cond.span.start, cond.span.end);
325 let cached = ctx.subquery_cache.get(&key).cloned();
326 match_imports_subquery(ctx, node_id, cached.as_deref())
327 } else {
328 Ok(match_imports(ctx, node_id, &cond.value))
329 }
330 }
331 "exports" => {
332 if matches!(cond.value, Value::Subquery(_)) {
333 let key = (cond.span.start, cond.span.end);
334 let cached = ctx.subquery_cache.get(&key).cloned();
335 match_exports_subquery(ctx, node_id, cached.as_deref())
336 } else {
337 Ok(match_exports(ctx, node_id, &cond.value))
338 }
339 }
340 "references" => {
341 if matches!(cond.value, Value::Subquery(_)) {
342 let key = (cond.span.start, cond.span.end);
343 let cached = ctx.subquery_cache.get(&key).cloned();
344 match_references_subquery(ctx, node_id, cached.as_deref())
345 } else {
346 Ok(match_references(ctx, node_id, &cond.operator, &cond.value))
347 }
348 }
349 "impl" | "implements" => {
350 if matches!(cond.value, Value::Subquery(_)) {
351 let key = (cond.span.start, cond.span.end);
352 let cached = ctx.subquery_cache.get(&key).cloned();
353 match_implements_subquery(ctx, node_id, cached.as_deref())
354 } else {
355 Ok(match_implements(ctx, node_id, &cond.value))
356 }
357 }
358 field if field.starts_with("scope.") => Ok(match_scope(
359 ctx,
360 node_id,
361 field,
362 &cond.operator,
363 &cond.value,
364 )),
365 "returns" => Ok(match_returns(
366 ctx,
367 node_id,
368 entry,
369 &cond.operator,
370 &cond.value,
371 )),
372 field if is_plugin_field(ctx, field) => Err(anyhow!(
373 "Plugin field '{field}' requires metadata not available in graph backend"
374 )),
375 _ => Ok(false), }
377}
378
379fn is_plugin_field(ctx: &GraphEvalContext, field: &str) -> bool {
381 let is_registered_field = ctx
383 .plugin_manager
384 .plugins()
385 .iter()
386 .flat_map(|plugin| plugin.fields().iter())
387 .any(|descriptor| descriptor.name == field);
388
389 if is_registered_field {
390 return true;
391 }
392
393 matches!(
396 field,
397 "abstract" | "final" | "generic" | "parameters" | "arity"
398 )
399}
400
401fn normalize_kind(kind: &str) -> &str {
410 match kind {
411 "trait" => "interface", "impl" => "implementation",
414 "field" => "property",
416 "namespace" => "module",
418 "element" => "component",
420 "style" => "style_rule",
422 "at_rule" => "style_at_rule",
423 "css_var" | "custom_property" => "style_variable",
424 _ => kind,
426 }
427}
428
429fn match_kind(
430 _ctx: &GraphEvalContext,
431 entry: &NodeEntry,
432 operator: &Operator,
433 value: &Value,
434) -> bool {
435 let actual = entry.kind.as_str();
436
437 match (operator, value) {
438 (Operator::Equal, Value::String(expected)) => {
439 let normalized_expected = normalize_kind(expected);
440 let normalized_actual = normalize_kind(actual);
441 normalized_actual == normalized_expected
442 }
443 (Operator::Regex, Value::Regex(regex_val)) => get_or_compile_regex(
444 ®ex_val.pattern,
445 regex_val.flags.case_insensitive,
446 regex_val.flags.multiline,
447 regex_val.flags.dot_all,
448 )
449 .map(|re| regex_is_match(&re, actual))
450 .unwrap_or(false),
451 _ => false,
452 }
453}
454
455fn match_name(
460 ctx: &GraphEvalContext,
461 entry: &NodeEntry,
462 operator: &Operator,
463 value: &Value,
464) -> bool {
465 match (operator, value) {
466 (Operator::Equal, Value::String(expected)) => {
471 entry_query_texts(ctx.graph, entry).iter().any(|candidate| {
472 language_aware_segments_match(ctx.graph, entry.file, candidate, expected)
473 })
474 }
475 (Operator::Regex, Value::Regex(regex_val)) => get_or_compile_regex(
476 ®ex_val.pattern,
477 regex_val.flags.case_insensitive,
478 regex_val.flags.multiline,
479 regex_val.flags.dot_all,
480 )
481 .map(|re| {
482 entry_query_texts(ctx.graph, entry)
483 .iter()
484 .any(|candidate| regex_is_match(&re, candidate))
485 })
486 .unwrap_or(false),
487 _ => false,
488 }
489}
490
491fn is_relative_pattern(pattern: &str) -> bool {
497 !pattern.starts_with('/')
498}
499
500fn match_path(
501 ctx: &GraphEvalContext,
502 entry: &NodeEntry,
503 operator: &Operator,
504 value: &Value,
505) -> bool {
506 let Some(file_path) = ctx.graph.files().resolve(entry.file) else {
507 return false;
508 };
509
510 match (operator, value) {
511 (Operator::Equal, Value::String(pattern)) => {
512 let match_path = if is_relative_pattern(pattern) {
514 if let Some(root) = ctx.workspace_root {
515 file_path
516 .strip_prefix(root)
517 .map_or_else(|_| file_path.to_path_buf(), std::path::Path::to_path_buf)
518 } else {
519 file_path.to_path_buf()
520 }
521 } else {
522 file_path.to_path_buf()
524 };
525 globset::Glob::new(pattern)
526 .map(|g| g.compile_matcher().is_match(&match_path))
527 .unwrap_or(false)
528 }
529 (Operator::Regex, Value::Regex(regex_val)) => {
530 get_or_compile_regex(
532 ®ex_val.pattern,
533 regex_val.flags.case_insensitive,
534 regex_val.flags.multiline,
535 regex_val.flags.dot_all,
536 )
537 .map(|re| regex_is_match(&re, file_path.to_string_lossy().as_ref()))
538 .unwrap_or(false)
539 }
540 _ => false,
541 }
542}
543
544fn language_to_canonical(lang: crate::graph::node::Language) -> &'static str {
554 use crate::graph::node::Language;
555 match lang {
556 Language::C => "c",
557 Language::Cpp => "cpp",
558 Language::CSharp => "csharp",
559 Language::Css => "css",
560 Language::JavaScript => "javascript",
561 Language::Python => "python",
562 Language::TypeScript => "typescript",
563 Language::Rust => "rust",
564 Language::Go => "go",
565 Language::Java => "java",
566 Language::Ruby => "ruby",
567 Language::Php => "php",
568 Language::Swift => "swift",
569 Language::Kotlin => "kotlin",
570 Language::Scala => "scala",
571 Language::Sql => "sql",
572 Language::Dart => "dart",
573 Language::Lua => "lua",
574 Language::Perl => "perl",
575 Language::Shell => "shell",
576 Language::Groovy => "groovy",
577 Language::Elixir => "elixir",
578 Language::R => "r",
579 Language::Haskell => "haskell",
580 Language::Html => "html",
581 Language::Svelte => "svelte",
582 Language::Vue => "vue",
583 Language::Zig => "zig",
584 Language::Terraform => "terraform",
585 Language::Puppet => "puppet",
586 Language::Pulumi => "pulumi",
587 Language::Http => "http",
588 Language::Plsql => "plsql",
589 Language::Apex => "apex",
590 Language::Abap => "abap",
591 Language::ServiceNow => "servicenow",
592 Language::Json => "json",
593 }
594}
595
596fn match_lang(
597 ctx: &GraphEvalContext,
598 entry: &NodeEntry,
599 operator: &Operator,
600 value: &Value,
601) -> bool {
602 let Some(lang) = ctx.graph.files().language_for_file(entry.file) else {
604 return false;
605 };
606
607 let actual = language_to_canonical(lang);
609
610 match (operator, value) {
612 (Operator::Equal, Value::String(expected)) => actual == expected,
613 (Operator::Regex, Value::Regex(rv)) => get_or_compile_regex(
614 &rv.pattern,
615 rv.flags.case_insensitive,
616 rv.flags.multiline,
617 rv.flags.dot_all,
618 )
619 .map(|re| regex_is_match(&re, actual))
620 .unwrap_or(false),
621 _ => false,
622 }
623}
624
625fn match_visibility(
630 ctx: &GraphEvalContext,
631 entry: &NodeEntry,
632 operator: &Operator,
633 value: &Value,
634) -> bool {
635 let Some(expected) = value.as_string() else {
636 return false;
637 };
638
639 let normalized_expected = if expected == "pub" {
640 "public"
641 } else {
642 expected
643 };
644
645 let Some(vis_id) = entry.visibility else {
646 return match operator {
648 Operator::Equal => normalized_expected == "private",
649 _ => false,
650 };
651 };
652
653 let Some(actual) = ctx.graph.strings().resolve(vis_id) else {
654 return false;
655 };
656 let normalized_actual = if actual.as_ref().starts_with("pub") {
657 "public"
658 } else {
659 actual.as_ref()
660 };
661
662 match operator {
664 Operator::Equal => normalized_actual == normalized_expected,
665 _ => false,
666 }
667}
668
669fn match_returns(
699 ctx: &GraphEvalContext,
700 node_id: NodeId,
701 entry: &NodeEntry,
702 operator: &Operator,
703 value: &Value,
704) -> bool {
705 let Some(expected) = value.as_string() else {
706 return false;
707 };
708
709 if !matches!(entry.kind, NodeKind::Function | NodeKind::Method) {
712 return false;
713 }
714
715 if !matches!(operator, Operator::Equal) {
716 return false;
717 }
718
719 let nodes = ctx.graph.nodes();
720 let strings = ctx.graph.strings();
721 for edge in ctx.graph.edges().edges_from(node_id) {
722 if !matches!(
723 edge.kind,
724 EdgeKind::TypeOf {
725 context: Some(TypeOfContext::Return),
726 ..
727 }
728 ) {
729 continue;
730 }
731 let Some(target_entry) = nodes.get(edge.target) else {
732 continue;
733 };
734 if let Some(name) = strings.resolve(target_entry.name)
735 && name.as_ref() == expected
736 {
737 return true;
738 }
739 }
740 false
741}
742
743fn match_async(entry: &NodeEntry, operator: &Operator, value: &Value) -> bool {
752 let expected = value_to_bool(value);
753 let Some(expected) = expected else {
754 return false;
755 };
756
757 match operator {
758 Operator::Equal => entry.is_async == expected,
759 _ => false,
760 }
761}
762
763fn match_static(entry: &NodeEntry, operator: &Operator, value: &Value) -> bool {
768 let expected = value_to_bool(value);
769 let Some(expected) = expected else {
770 return false;
771 };
772
773 match operator {
774 Operator::Equal => entry.is_static == expected,
775 _ => false,
776 }
777}
778
779fn value_to_bool(value: &Value) -> Option<bool> {
787 match value {
788 Value::Boolean(b) => Some(*b),
789 Value::String(s) => match s.to_lowercase().as_str() {
790 "true" | "yes" | "1" => Some(true),
791 "false" | "no" | "0" => Some(false),
792 _ => None,
793 },
794 _ => None,
795 }
796}
797
798fn match_callers(ctx: &GraphEvalContext, node_id: NodeId, value: &Value) -> bool {
812 let Some(target_name) = value.as_string() else {
813 return false;
814 };
815
816 let method_part = extract_method_name(target_name);
819
820 for edge in ctx.graph.edges().edges_from(node_id) {
822 if let EdgeKind::Calls { .. } = &edge.kind
823 && let Some(target_entry) = ctx.graph.nodes().get(edge.target)
824 {
825 let callee_names = entry_query_texts(ctx.graph, target_entry);
826
827 if callee_names.iter().any(|callee_name| {
828 language_aware_segments_match(
829 ctx.graph,
830 target_entry.file,
831 callee_name,
832 target_name,
833 )
834 }) {
835 return true;
836 }
837
838 if let Some(method) = &method_part
842 && callee_names
843 .iter()
844 .filter_map(|callee_name| extract_method_name(callee_name))
845 .any(|callee_method| method == &callee_method)
846 {
847 return true;
848 }
849 }
850 }
851 false
852}
853
854#[must_use]
858pub fn extract_method_name(qualified: &str) -> Option<String> {
859 for sep in ["::", ".", "#", ":", "/"] {
861 if let Some(pos) = qualified.rfind(sep) {
862 let method = &qualified[pos + sep.len()..];
863 if !method.is_empty() {
864 return Some(method.to_string());
865 }
866 }
867 }
868 None
869}
870
871fn match_callees(ctx: &GraphEvalContext, node_id: NodeId, value: &Value) -> bool {
875 let Some(caller_name) = value.as_string() else {
876 return false;
877 };
878
879 for edge in ctx.graph.edges().edges_to(node_id) {
881 if let EdgeKind::Calls { .. } = &edge.kind
882 && let Some(source_entry) = ctx.graph.nodes().get(edge.source)
883 && entry_query_texts(ctx.graph, source_entry)
884 .iter()
885 .any(|source_name| {
886 language_aware_segments_match(
887 ctx.graph,
888 source_entry.file,
889 source_name,
890 caller_name,
891 )
892 })
893 {
894 return true;
895 }
896 }
897 false
898}
899
900fn match_imports(ctx: &GraphEvalContext, node_id: NodeId, value: &Value) -> bool {
916 let Some(target_module) = value.as_string() else {
917 return false;
918 };
919
920 let Some(entry) = ctx.graph.nodes().get(node_id) else {
921 return false;
922 };
923
924 if entry.kind == NodeKind::Import && import_entry_matches(ctx.graph, entry, target_module) {
925 return true;
926 }
927
928 for edge in ctx.graph.edges().edges_from(node_id) {
929 if import_edge_matches(ctx.graph, &edge, target_module) {
930 return true;
931 }
932 }
933 false
934}
935
936#[must_use]
942pub fn import_edge_matches<G: crate::graph::unified::concurrent::GraphAccess>(
943 graph: &G,
944 edge: &StoreEdgeRef,
945 target_module: &str,
946) -> bool {
947 let EdgeKind::Imports { alias, is_wildcard } = &edge.kind else {
948 return false;
949 };
950
951 let target_match = graph
953 .nodes()
954 .get(edge.target)
955 .is_some_and(|entry| import_entry_matches(graph, entry, target_module));
956
957 let alias_match = alias
959 .and_then(|sid| graph.strings().resolve(sid))
960 .is_some_and(|alias_str| {
961 graph.nodes().get(edge.source).is_some_and(|entry| {
962 import_text_matches(graph, entry.file, alias_str.as_ref(), target_module)
963 })
964 });
965
966 let wildcard_match = *is_wildcard && target_module == "*";
968
969 target_match || alias_match || wildcard_match
970}
971
972#[must_use]
976pub fn import_text_matches<G: crate::graph::unified::concurrent::GraphAccess>(
977 graph: &G,
978 file_id: FileId,
979 candidate: &str,
980 target_module: &str,
981) -> bool {
982 if candidate.contains(target_module) {
983 return true;
984 }
985
986 graph
987 .files()
988 .language_for_file(file_id)
989 .is_some_and(|language| {
990 let canonical_target = canonicalize_graph_qualified_name(language, target_module);
991 canonical_target != target_module && candidate.contains(&canonical_target)
992 })
993}
994
995#[must_use]
998pub fn import_entry_matches<G: crate::graph::unified::concurrent::GraphAccess>(
999 graph: &G,
1000 entry: &NodeEntry,
1001 target_module: &str,
1002) -> bool {
1003 entry_query_texts(graph, entry)
1004 .iter()
1005 .any(|candidate| import_text_matches(graph, entry.file, candidate, target_module))
1006}
1007
1008#[must_use]
1013pub fn language_aware_segments_match<G: crate::graph::unified::concurrent::GraphAccess>(
1014 graph: &G,
1015 file_id: FileId,
1016 candidate: &str,
1017 expected: &str,
1018) -> bool {
1019 if segments_match(candidate, expected) {
1020 return true;
1021 }
1022
1023 graph
1024 .files()
1025 .language_for_file(file_id)
1026 .is_some_and(|language| {
1027 let canonical_expected = canonicalize_graph_qualified_name(language, expected);
1028 canonical_expected != expected && segments_match(candidate, &canonical_expected)
1029 })
1030}
1031
1032fn push_unique_query_text(texts: &mut Vec<String>, candidate: impl Into<String>) {
1033 let candidate = candidate.into();
1034 if !texts.iter().any(|existing| existing == &candidate) {
1035 texts.push(candidate);
1036 }
1037}
1038
1039#[must_use]
1045pub fn entry_query_texts<G: crate::graph::unified::concurrent::GraphAccess>(
1046 graph: &G,
1047 entry: &NodeEntry,
1048) -> Vec<String> {
1049 let mut texts = Vec::with_capacity(3);
1050
1051 if let Some(name) = graph.strings().resolve(entry.name) {
1052 push_unique_query_text(&mut texts, name.to_string());
1053 }
1054
1055 if let Some(qualified) = entry
1056 .qualified_name
1057 .and_then(|qualified_name_id| graph.strings().resolve(qualified_name_id))
1058 {
1059 push_unique_query_text(&mut texts, qualified.to_string());
1060
1061 if let Some(language) = graph.files().language_for_file(entry.file) {
1062 push_unique_query_text(
1063 &mut texts,
1064 display_graph_qualified_name(
1065 language,
1066 qualified.as_ref(),
1067 entry.kind,
1068 entry.is_static,
1069 ),
1070 );
1071 }
1072 }
1073
1074 texts
1075}
1076
1077fn match_exports(ctx: &GraphEvalContext, node_id: NodeId, value: &Value) -> bool {
1083 let Some(target_name) = value.as_string() else {
1084 return false;
1085 };
1086
1087 let Some(entry) = ctx.graph.nodes().get(node_id) else {
1088 return false;
1089 };
1090 let node_file = entry.file;
1091
1092 if !entry_query_texts(ctx.graph, entry).iter().any(|candidate| {
1093 language_aware_segments_match(ctx.graph, entry.file, candidate, target_name)
1094 }) {
1095 return false;
1096 }
1097
1098 let edges = ctx.graph.edges();
1099
1100 for edge in edges.edges_from(node_id) {
1102 if let EdgeKind::Exports { .. } = &edge.kind {
1103 if let Some(target_entry) = ctx.graph.nodes().get(edge.target)
1105 && target_entry.file == node_file
1106 {
1107 return true;
1108 }
1109 }
1110 }
1111
1112 for edge in edges.edges_to(node_id) {
1114 if let EdgeKind::Exports { .. } = &edge.kind {
1115 if let Some(source_entry) = ctx.graph.nodes().get(edge.source)
1117 && source_entry.file == node_file
1118 {
1119 return true;
1120 }
1121 }
1122 }
1123
1124 false
1125}
1126
1127fn match_references(
1131 ctx: &GraphEvalContext,
1132 node_id: NodeId,
1133 operator: &Operator,
1134 value: &Value,
1135) -> bool {
1136 let Some(entry) = ctx.graph.nodes().get(node_id) else {
1138 return false;
1139 };
1140
1141 let name_matches = match (operator, value) {
1142 (Operator::Equal, Value::String(target)) => entry_query_texts(ctx.graph, entry)
1143 .iter()
1144 .any(|candidate| candidate == target || candidate.ends_with(&format!("::{target}"))),
1145 (Operator::Regex, Value::Regex(rv)) => get_or_compile_regex(
1146 &rv.pattern,
1147 rv.flags.case_insensitive,
1148 rv.flags.multiline,
1149 rv.flags.dot_all,
1150 )
1151 .map(|re| {
1152 entry_query_texts(ctx.graph, entry)
1153 .iter()
1154 .any(|candidate| regex_is_match(&re, candidate))
1155 })
1156 .unwrap_or(false),
1157 _ => false,
1158 };
1159
1160 if !name_matches {
1161 return false;
1162 }
1163
1164 for edge in ctx.graph.edges().edges_to(node_id) {
1167 let is_reference = matches!(
1168 &edge.kind,
1169 EdgeKind::References
1170 | EdgeKind::Calls { .. }
1171 | EdgeKind::Imports { .. }
1172 | EdgeKind::FfiCall { .. }
1173 );
1174 if is_reference {
1175 return true;
1176 }
1177 }
1178
1179 false
1180}
1181
1182fn match_implements(ctx: &GraphEvalContext, node_id: NodeId, value: &Value) -> bool {
1184 let Some(trait_name) = value.as_string() else {
1185 return false;
1186 };
1187
1188 for edge in ctx.graph.edges().edges_from(node_id) {
1189 if let EdgeKind::Implements = &edge.kind
1190 && let Some(target_entry) = ctx.graph.nodes().get(edge.target)
1191 && entry_query_texts(ctx.graph, target_entry)
1192 .iter()
1193 .any(|name| {
1194 language_aware_segments_match(ctx.graph, target_entry.file, name, trait_name)
1195 })
1196 {
1197 return true;
1198 }
1199 }
1200 false
1201}
1202
1203fn node_kind_to_scope_type(kind: NodeKind) -> &'static str {
1215 match kind {
1216 NodeKind::Function | NodeKind::Test => "function",
1217 NodeKind::Method => "method",
1218 NodeKind::Class | NodeKind::Service => "class",
1219 NodeKind::Interface | NodeKind::Trait => "interface",
1220 NodeKind::Struct => "struct",
1221 NodeKind::Enum => "enum",
1222 NodeKind::Module => "module",
1223 NodeKind::Macro => "macro",
1224 NodeKind::Component => "component",
1225 NodeKind::Resource | NodeKind::Endpoint => "resource",
1226 NodeKind::Variable => "variable",
1228 NodeKind::Constant => "constant",
1229 NodeKind::Type => "type",
1230 NodeKind::EnumVariant => "enumvariant",
1231 NodeKind::Import => "import",
1232 NodeKind::Export => "export",
1233 NodeKind::CallSite => "callsite",
1234 NodeKind::Parameter => "parameter",
1235 NodeKind::Property => "property",
1236 NodeKind::StyleRule => "style_rule",
1237 NodeKind::StyleAtRule => "style_at_rule",
1238 NodeKind::StyleVariable => "style_variable",
1239 NodeKind::Lifetime => "lifetime",
1240 NodeKind::TypeParameter => "type_parameter",
1241 NodeKind::Annotation => "annotation",
1242 NodeKind::AnnotationValue => "annotation_value",
1243 NodeKind::LambdaTarget => "lambda_target",
1244 NodeKind::JavaModule => "java_module",
1245 NodeKind::EnumConstant => "enum_constant",
1246 NodeKind::Other => "other",
1247 }
1248}
1249
1250fn match_scope(
1251 ctx: &GraphEvalContext,
1252 node_id: NodeId,
1253 field: &str,
1254 operator: &Operator,
1255 value: &Value,
1256) -> bool {
1257 let scope_part = field.strip_prefix("scope.").unwrap_or("");
1258 match scope_part {
1259 "type" => match_scope_type(ctx, node_id, operator, value),
1260 "name" => match_scope_name(ctx, node_id, operator, value),
1261 "parent" => match_scope_parent_name(ctx, node_id, operator, value),
1262 "ancestor" => match_scope_ancestor_name(ctx, node_id, operator, value),
1263 _ => false,
1264 }
1265}
1266
1267fn match_scope_type(
1268 ctx: &GraphEvalContext,
1269 node_id: NodeId,
1270 operator: &Operator,
1271 value: &Value,
1272) -> bool {
1273 for edge in ctx.graph.edges().edges_to(node_id) {
1274 if let EdgeKind::Contains = &edge.kind
1275 && let Some(parent) = ctx.graph.nodes().get(edge.source)
1276 {
1277 let scope_type = node_kind_to_scope_type(parent.kind);
1279 return match (operator, value) {
1280 (Operator::Equal, Value::String(exp)) => scope_type == exp,
1282 (Operator::Regex, Value::Regex(rv)) => get_or_compile_regex(
1283 &rv.pattern,
1284 rv.flags.case_insensitive,
1285 rv.flags.multiline,
1286 rv.flags.dot_all,
1287 )
1288 .map(|re| regex_is_match(&re, scope_type))
1289 .unwrap_or(false),
1290 _ => false,
1291 };
1292 }
1293 }
1294 false
1295}
1296
1297fn match_scope_name(
1298 ctx: &GraphEvalContext,
1299 node_id: NodeId,
1300 operator: &Operator,
1301 value: &Value,
1302) -> bool {
1303 for edge in ctx.graph.edges().edges_to(node_id) {
1304 if let EdgeKind::Contains = &edge.kind
1305 && let Some(parent) = ctx.graph.nodes().get(edge.source)
1306 && let Some(name) = ctx.graph.strings().resolve(parent.name)
1307 {
1308 return match (operator, value) {
1309 (Operator::Equal, Value::String(exp)) => segments_match(&name, exp),
1311 (Operator::Regex, Value::Regex(rv)) => get_or_compile_regex(
1312 &rv.pattern,
1313 rv.flags.case_insensitive,
1314 rv.flags.multiline,
1315 rv.flags.dot_all,
1316 )
1317 .map(|re| regex_is_match(&re, &name))
1318 .unwrap_or(false),
1319 _ => false,
1320 };
1321 }
1322 }
1323 false
1324}
1325
1326fn match_scope_parent_name(
1330 ctx: &GraphEvalContext,
1331 node_id: NodeId,
1332 operator: &Operator,
1333 value: &Value,
1334) -> bool {
1335 for edge in ctx.graph.edges().edges_to(node_id) {
1336 if let EdgeKind::Contains = &edge.kind
1337 && let Some(parent) = ctx.graph.nodes().get(edge.source)
1338 && let Some(name) = ctx.graph.strings().resolve(parent.name)
1339 {
1340 return match (operator, value) {
1341 (Operator::Equal, Value::String(exp)) => segments_match(&name, exp),
1343 (Operator::Regex, Value::Regex(rv)) => get_or_compile_regex(
1344 &rv.pattern,
1345 rv.flags.case_insensitive,
1346 rv.flags.multiline,
1347 rv.flags.dot_all,
1348 )
1349 .map(|re| regex_is_match(&re, &name))
1350 .unwrap_or(false),
1351 _ => false,
1352 };
1353 }
1354 }
1355 false
1356}
1357
1358fn match_scope_ancestor_name(
1363 ctx: &GraphEvalContext,
1364 node_id: NodeId,
1365 operator: &Operator,
1366 value: &Value,
1367) -> bool {
1368 let mut current = node_id;
1369 let mut visited = HashSet::new();
1370 visited.insert(node_id);
1371
1372 loop {
1373 let mut found_parent = false;
1374 for edge in ctx.graph.edges().edges_to(current) {
1375 if let EdgeKind::Contains = &edge.kind {
1376 if visited.contains(&edge.source) {
1378 continue;
1379 }
1380 visited.insert(edge.source);
1381
1382 found_parent = true;
1383 current = edge.source;
1384 if let Some(parent) = ctx.graph.nodes().get(current)
1385 && let Some(name) = ctx.graph.strings().resolve(parent.name)
1386 {
1387 let matches = match (operator, value) {
1388 (Operator::Equal, Value::String(exp)) => segments_match(&name, exp),
1390 (Operator::Regex, Value::Regex(rv)) => get_or_compile_regex(
1391 &rv.pattern,
1392 rv.flags.case_insensitive,
1393 rv.flags.multiline,
1394 rv.flags.dot_all,
1395 )
1396 .map(|re| regex_is_match(&re, &name))
1397 .unwrap_or(false),
1398 _ => false,
1399 };
1400 if matches {
1401 return true;
1402 }
1403 }
1404 break;
1405 }
1406 }
1407 if !found_parent {
1408 break;
1409 }
1410 }
1411 false
1412}
1413
1414pub fn evaluate_subquery(ctx: &GraphEvalContext, expr: &Expr) -> Result<HashSet<NodeId>> {
1426 let recursion_limits = crate::config::RecursionLimits::load_or_default()?;
1427 let expr_depth = recursion_limits.effective_expr_depth()?;
1428 let mut guard = crate::query::security::RecursionGuard::new(expr_depth)?;
1429
1430 let arena = ctx.graph.nodes();
1431 let mut matches = HashSet::new();
1432 for (id, _) in arena.iter() {
1433 if evaluate_node(ctx, id, expr, &mut guard)? {
1434 matches.insert(id);
1435 }
1436 }
1437 Ok(matches)
1438}
1439
1440fn match_callers_subquery(
1446 ctx: &GraphEvalContext,
1447 node_id: NodeId,
1448 subquery_matches: Option<&HashSet<NodeId>>,
1449) -> Result<bool> {
1450 let Some(matches) = subquery_matches else {
1451 return Err(anyhow!(
1452 "subquery cache miss: precompute_subqueries did not populate cache for this relation predicate"
1453 ));
1454 };
1455 for edge in ctx.graph.edges().edges_from(node_id) {
1456 if let EdgeKind::Calls { .. } = &edge.kind
1457 && matches.contains(&edge.target)
1458 {
1459 return Ok(true);
1460 }
1461 }
1462 Ok(false)
1463}
1464
1465fn match_callees_subquery(
1467 ctx: &GraphEvalContext,
1468 node_id: NodeId,
1469 subquery_matches: Option<&HashSet<NodeId>>,
1470) -> Result<bool> {
1471 let Some(matches) = subquery_matches else {
1472 return Err(anyhow!(
1473 "subquery cache miss: precompute_subqueries did not populate cache for this relation predicate"
1474 ));
1475 };
1476 for edge in ctx.graph.edges().edges_to(node_id) {
1477 if let EdgeKind::Calls { .. } = &edge.kind
1478 && matches.contains(&edge.source)
1479 {
1480 return Ok(true);
1481 }
1482 }
1483 Ok(false)
1484}
1485
1486fn match_imports_subquery(
1492 ctx: &GraphEvalContext,
1493 node_id: NodeId,
1494 subquery_matches: Option<&HashSet<NodeId>>,
1495) -> Result<bool> {
1496 let Some(matches) = subquery_matches else {
1497 return Err(anyhow!(
1498 "subquery cache miss: precompute_subqueries did not populate cache for this relation predicate"
1499 ));
1500 };
1501 for edge in ctx.graph.edges().edges_from(node_id) {
1502 if let EdgeKind::Imports { .. } = &edge.kind
1503 && matches.contains(&edge.target)
1504 {
1505 return Ok(true);
1506 }
1507 }
1508 Ok(false)
1509}
1510
1511fn match_exports_subquery(
1513 ctx: &GraphEvalContext,
1514 node_id: NodeId,
1515 subquery_matches: Option<&HashSet<NodeId>>,
1516) -> Result<bool> {
1517 let Some(matches) = subquery_matches else {
1518 return Err(anyhow!(
1519 "subquery cache miss: precompute_subqueries did not populate cache for this relation predicate"
1520 ));
1521 };
1522 for edge in ctx.graph.edges().edges_from(node_id) {
1523 if let EdgeKind::Exports { .. } = &edge.kind
1524 && matches.contains(&edge.target)
1525 {
1526 return Ok(true);
1527 }
1528 }
1529 Ok(false)
1530}
1531
1532fn match_implements_subquery(
1534 ctx: &GraphEvalContext,
1535 node_id: NodeId,
1536 subquery_matches: Option<&HashSet<NodeId>>,
1537) -> Result<bool> {
1538 let Some(matches) = subquery_matches else {
1539 return Err(anyhow!(
1540 "subquery cache miss: precompute_subqueries did not populate cache for this relation predicate"
1541 ));
1542 };
1543 for edge in ctx.graph.edges().edges_from(node_id) {
1544 if let EdgeKind::Implements = &edge.kind
1545 && matches.contains(&edge.target)
1546 {
1547 return Ok(true);
1548 }
1549 }
1550 Ok(false)
1551}
1552
1553fn match_references_subquery(
1555 ctx: &GraphEvalContext,
1556 node_id: NodeId,
1557 subquery_matches: Option<&HashSet<NodeId>>,
1558) -> Result<bool> {
1559 let Some(matches) = subquery_matches else {
1560 return Err(anyhow!(
1561 "subquery cache miss: precompute_subqueries did not populate cache for this relation predicate"
1562 ));
1563 };
1564 for edge in ctx.graph.edges().edges_to(node_id) {
1565 let is_reference = matches!(
1566 &edge.kind,
1567 EdgeKind::References
1568 | EdgeKind::Calls { .. }
1569 | EdgeKind::Imports { .. }
1570 | EdgeKind::FfiCall { .. }
1571 );
1572 if is_reference && matches.contains(&edge.source) {
1573 return Ok(true);
1574 }
1575 }
1576 Ok(false)
1577}
1578
1579pub fn evaluate_join(
1595 ctx: &GraphEvalContext,
1596 join: &JoinExpr,
1597 max_results: Option<usize>,
1598) -> Result<JoinEvalResult> {
1599 let lhs_matches = evaluate_subquery(ctx, &join.left)?;
1600 let rhs_matches = evaluate_subquery(ctx, &join.right)?;
1601 let cap = max_results.unwrap_or(DEFAULT_JOIN_RESULT_CAP);
1602
1603 let mut pairs = Vec::new();
1604 let mut truncated = false;
1605 'outer: for &lhs_id in &lhs_matches {
1606 for edge in ctx.graph.edges().edges_from(lhs_id) {
1607 if edge_matches_join_kind(&edge.kind, &join.edge) && rhs_matches.contains(&edge.target)
1608 {
1609 pairs.push((lhs_id, edge.target));
1610 if pairs.len() >= cap {
1611 truncated = true;
1612 break 'outer;
1613 }
1614 }
1615 }
1616 }
1617 Ok(JoinEvalResult { pairs, truncated })
1618}
1619
1620pub struct JoinEvalResult {
1622 pub pairs: Vec<(NodeId, NodeId)>,
1624 pub truncated: bool,
1626}
1627
1628const DEFAULT_JOIN_RESULT_CAP: usize = 10_000;
1632
1633fn edge_matches_join_kind(edge_kind: &EdgeKind, join_kind: &JoinEdgeKind) -> bool {
1635 match join_kind {
1636 JoinEdgeKind::Calls => matches!(edge_kind, EdgeKind::Calls { .. }),
1637 JoinEdgeKind::Imports => matches!(edge_kind, EdgeKind::Imports { .. }),
1638 JoinEdgeKind::Inherits => matches!(edge_kind, EdgeKind::Inherits),
1639 JoinEdgeKind::Implements => matches!(edge_kind, EdgeKind::Implements),
1640 }
1641}
1642
1643#[cfg(test)]
1644mod tests {
1645 use super::*;
1646 use crate::graph::node::Language;
1647 use crate::query::types::{Condition, Field, Span};
1648 use std::path::Path;
1649
1650 #[test]
1651 fn test_import_text_matches_canonicalized_qualified_imports() {
1652 let mut graph = CodeGraph::new();
1653 let file_id = graph
1654 .files_mut()
1655 .register(Path::new("src/FileProcessor.cs"))
1656 .unwrap();
1657 assert!(graph.files_mut().set_language(file_id, Language::CSharp));
1658
1659 assert!(import_text_matches(
1660 &graph,
1661 file_id,
1662 "System::IO",
1663 "System.IO"
1664 ));
1665 assert!(import_text_matches(
1666 &graph,
1667 file_id,
1668 "System::Collections::Generic",
1669 "System.Collections.Generic"
1670 ));
1671 assert!(!import_text_matches(
1672 &graph,
1673 file_id,
1674 "System::Text",
1675 "System.IO"
1676 ));
1677 }
1678
1679 #[test]
1680 fn test_language_aware_segments_match_supports_ruby_method_separators() {
1681 let mut graph = CodeGraph::new();
1682 let file_id = graph
1683 .files_mut()
1684 .register(Path::new("app/models/user.rb"))
1685 .unwrap();
1686 assert!(graph.files_mut().set_language(file_id, Language::Ruby));
1687
1688 assert!(language_aware_segments_match(
1689 &graph,
1690 file_id,
1691 "Admin::Users::Controller::show",
1692 "Admin::Users::Controller#show"
1693 ));
1694 assert!(language_aware_segments_match(
1695 &graph,
1696 file_id,
1697 "Admin::Users::Controller::show",
1698 "show"
1699 ));
1700 assert!(!language_aware_segments_match(
1701 &graph,
1702 file_id,
1703 "Admin::Users::Controller::index",
1704 "Admin::Users::Controller#show"
1705 ));
1706 }
1707
1708 #[test]
1709 fn test_normalize_kind() {
1710 assert_eq!(normalize_kind("trait"), "interface");
1712 assert_eq!(normalize_kind("TRAIT"), "TRAIT"); assert_eq!(normalize_kind("field"), "property");
1714 assert_eq!(normalize_kind("namespace"), "module");
1715 assert_eq!(normalize_kind("function"), "function"); }
1717
1718 #[test]
1719 fn test_graph_eval_context_builder() {
1720 let graph = CodeGraph::new();
1721 let pm = PluginManager::new();
1722 let ctx = GraphEvalContext::new(&graph, &pm)
1723 .with_workspace_root(Path::new("/test"))
1724 .with_parallel_disabled(true);
1725
1726 assert!(ctx.disable_parallel);
1727 assert_eq!(ctx.workspace_root, Some(Path::new("/test")));
1728 }
1729
1730 fn subquery_condition(field: &str, inner: Expr, start: usize, end: usize) -> Expr {
1736 Expr::Condition(Condition {
1737 field: Field(field.to_string()),
1738 operator: Operator::Equal,
1739 value: Value::Subquery(Box::new(inner)),
1740 span: Span::with_position(start, end, 1, start + 1),
1741 })
1742 }
1743
1744 fn kind_condition(kind: &str) -> Expr {
1746 Expr::Condition(Condition {
1747 field: Field("kind".to_string()),
1748 operator: Operator::Equal,
1749 value: Value::String(kind.to_string()),
1750 span: Span::default(),
1751 })
1752 }
1753
1754 #[test]
1755 fn test_collect_subquery_exprs_post_order_depth_2() {
1756 let inner_subquery = subquery_condition("callees", kind_condition("function"), 20, 40);
1760 let outer_subquery = subquery_condition("callers", inner_subquery, 0, 50);
1761
1762 let mut out = Vec::new();
1763 collect_subquery_exprs(&outer_subquery, &mut out);
1764
1765 assert_eq!(
1767 out.len(),
1768 2,
1769 "should collect both inner and outer subqueries"
1770 );
1771 assert_eq!(out[0].0, (20, 40), "inner subquery span should come first");
1772 assert_eq!(out[1].0, (0, 50), "outer subquery span should come second");
1773 }
1774
1775 #[test]
1776 fn test_collect_subquery_exprs_post_order_depth_3() {
1777 let innermost = subquery_condition("imports", kind_condition("function"), 30, 50);
1779 let middle = subquery_condition("callees", innermost, 15, 55);
1780 let outer = subquery_condition("callers", middle, 0, 60);
1781
1782 let mut out = Vec::new();
1783 collect_subquery_exprs(&outer, &mut out);
1784
1785 assert_eq!(out.len(), 3, "should collect all three nested subqueries");
1786 assert_eq!(out[0].0, (30, 50), "innermost should come first");
1787 assert_eq!(out[1].0, (15, 55), "middle should come second");
1788 assert_eq!(out[2].0, (0, 60), "outer should come last");
1789 }
1790
1791 #[test]
1792 fn test_collect_subquery_exprs_and_or_branches() {
1793 let left = subquery_condition("callers", kind_condition("function"), 0, 25);
1795 let right = subquery_condition("callees", kind_condition("method"), 30, 55);
1796 let expr = Expr::And(vec![left, right]);
1797
1798 let mut out = Vec::new();
1799 collect_subquery_exprs(&expr, &mut out);
1800
1801 assert_eq!(out.len(), 2, "should collect subqueries from both branches");
1802 assert_eq!(out[0].0, (0, 25), "left branch subquery");
1803 assert_eq!(out[1].0, (30, 55), "right branch subquery");
1804 }
1805
1806 #[test]
1807 fn test_collect_subquery_exprs_no_subqueries() {
1808 let expr = kind_condition("function");
1810
1811 let mut out = Vec::new();
1812 collect_subquery_exprs(&expr, &mut out);
1813
1814 assert!(
1815 out.is_empty(),
1816 "should collect nothing for plain conditions"
1817 );
1818 }
1819
1820 use crate::graph::unified::edge::{BidirectionalEdgeStore, FfiConvention};
1825 use crate::graph::unified::storage::{
1826 AuxiliaryIndices, FileRegistry, NodeArena, StringInterner,
1827 };
1828
1829 fn build_ffi_graph() -> (CodeGraph, NodeId, NodeId) {
1831 let mut arena = NodeArena::new();
1832 let edges = BidirectionalEdgeStore::new();
1833 let mut strings = StringInterner::new();
1834 let mut files = FileRegistry::new();
1835 let mut indices = AuxiliaryIndices::new();
1836
1837 let caller_name = strings.intern("caller_fn").unwrap();
1838 let target_name = strings.intern("ffi_target").unwrap();
1839 let file_id = files.register(Path::new("test.r")).unwrap();
1840
1841 let caller_id = arena
1842 .alloc(NodeEntry {
1843 kind: NodeKind::Function,
1844 name: caller_name,
1845 file: file_id,
1846 start_byte: 0,
1847 end_byte: 100,
1848 start_line: 1,
1849 start_column: 0,
1850 end_line: 5,
1851 end_column: 0,
1852 signature: None,
1853 doc: None,
1854 qualified_name: None,
1855 visibility: None,
1856 is_async: false,
1857 is_static: false,
1858 is_unsafe: false,
1859 body_hash: None,
1860 })
1861 .unwrap();
1862
1863 let target_id = arena
1864 .alloc(NodeEntry {
1865 kind: NodeKind::Function,
1866 name: target_name,
1867 file: file_id,
1868 start_byte: 200,
1869 end_byte: 300,
1870 start_line: 10,
1871 start_column: 0,
1872 end_line: 15,
1873 end_column: 0,
1874 signature: None,
1875 doc: None,
1876 qualified_name: None,
1877 visibility: None,
1878 is_async: false,
1879 is_static: false,
1880 is_unsafe: false,
1881 body_hash: None,
1882 })
1883 .unwrap();
1884
1885 indices.add(caller_id, NodeKind::Function, caller_name, None, file_id);
1886 indices.add(target_id, NodeKind::Function, target_name, None, file_id);
1887
1888 edges.add_edge(
1889 caller_id,
1890 target_id,
1891 EdgeKind::FfiCall {
1892 convention: FfiConvention::C,
1893 },
1894 file_id,
1895 );
1896
1897 let graph = CodeGraph::from_components(
1898 arena,
1899 edges,
1900 strings,
1901 files,
1902 indices,
1903 crate::graph::unified::NodeMetadataStore::new(),
1904 );
1905 (graph, caller_id, target_id)
1906 }
1907
1908 #[test]
1909 fn test_ffi_call_edge_in_references_predicate() {
1910 let (graph, _caller_id, target_id) = build_ffi_graph();
1911 let pm = PluginManager::new();
1912 let ctx = GraphEvalContext::new(&graph, &pm);
1913
1914 let result = match_references(
1916 &ctx,
1917 target_id,
1918 &Operator::Equal,
1919 &Value::String("ffi_target".to_string()),
1920 );
1921 assert!(result, "references: predicate should match FfiCall edges");
1922 }
1923
1924 #[test]
1925 fn test_ffi_call_edge_in_references_subquery() {
1926 let (graph, caller_id, target_id) = build_ffi_graph();
1927 let pm = PluginManager::new();
1928 let ctx = GraphEvalContext::new(&graph, &pm);
1929
1930 let mut subquery_results = HashSet::new();
1932 subquery_results.insert(caller_id);
1933
1934 let result = match_references_subquery(&ctx, target_id, Some(&subquery_results)).unwrap();
1937 assert!(
1938 result,
1939 "references subquery should match FfiCall edge sources"
1940 );
1941 }
1942
1943 fn build_returns_graph() -> (CodeGraph, NodeId, NodeId, NodeId) {
1953 let mut arena = NodeArena::new();
1954 let edges = BidirectionalEdgeStore::new();
1955 let mut strings = StringInterner::new();
1956 let mut files = FileRegistry::new();
1957 let mut indices = AuxiliaryIndices::new();
1958
1959 let returner_name = strings.intern("returner_fn").unwrap();
1960 let plain_name = strings.intern("plain_fn").unwrap();
1961 let error_name = strings.intern("error").unwrap();
1962 let file_id = files.register(Path::new("test.go")).unwrap();
1963
1964 let returner_id = arena
1965 .alloc(NodeEntry {
1966 kind: NodeKind::Function,
1967 name: returner_name,
1968 file: file_id,
1969 start_byte: 0,
1970 end_byte: 100,
1971 start_line: 1,
1972 start_column: 0,
1973 end_line: 5,
1974 end_column: 0,
1975 signature: None,
1976 doc: None,
1977 qualified_name: None,
1978 visibility: None,
1979 is_async: false,
1980 is_static: false,
1981 is_unsafe: false,
1982 body_hash: None,
1983 })
1984 .unwrap();
1985
1986 let plain_id = arena
1987 .alloc(NodeEntry {
1988 kind: NodeKind::Function,
1989 name: plain_name,
1990 file: file_id,
1991 start_byte: 200,
1992 end_byte: 300,
1993 start_line: 10,
1994 start_column: 0,
1995 end_line: 15,
1996 end_column: 0,
1997 signature: None,
1998 doc: None,
1999 qualified_name: None,
2000 visibility: None,
2001 is_async: false,
2002 is_static: false,
2003 is_unsafe: false,
2004 body_hash: None,
2005 })
2006 .unwrap();
2007
2008 let error_type_id = arena
2009 .alloc(NodeEntry {
2010 kind: NodeKind::Type,
2011 name: error_name,
2012 file: file_id,
2013 start_byte: 400,
2014 end_byte: 410,
2015 start_line: 20,
2016 start_column: 0,
2017 end_line: 20,
2018 end_column: 10,
2019 signature: None,
2020 doc: None,
2021 qualified_name: None,
2022 visibility: None,
2023 is_async: false,
2024 is_static: false,
2025 is_unsafe: false,
2026 body_hash: None,
2027 })
2028 .unwrap();
2029
2030 indices.add(
2031 returner_id,
2032 NodeKind::Function,
2033 returner_name,
2034 None,
2035 file_id,
2036 );
2037 indices.add(plain_id, NodeKind::Function, plain_name, None, file_id);
2038 indices.add(error_type_id, NodeKind::Type, error_name, None, file_id);
2039
2040 edges.add_edge(
2041 returner_id,
2042 error_type_id,
2043 EdgeKind::TypeOf {
2044 context: Some(TypeOfContext::Return),
2045 index: None,
2046 name: None,
2047 },
2048 file_id,
2049 );
2050
2051 let graph = CodeGraph::from_components(
2052 arena,
2053 edges,
2054 strings,
2055 files,
2056 indices,
2057 crate::graph::unified::NodeMetadataStore::new(),
2058 );
2059 (graph, returner_id, plain_id, error_type_id)
2060 }
2061
2062 #[test]
2063 fn test_match_returns_byte_exact_hit() {
2064 let (graph, returner_id, _plain_id, _error_id) = build_returns_graph();
2065 let pm = PluginManager::new();
2066 let ctx = GraphEvalContext::new(&graph, &pm);
2067 let entry = graph.nodes().get(returner_id).expect("returner exists");
2068
2069 assert!(match_returns(
2072 &ctx,
2073 returner_id,
2074 entry,
2075 &Operator::Equal,
2076 &Value::String("error".to_string()),
2077 ));
2078 }
2079
2080 #[test]
2081 fn test_match_returns_no_edges_misses() {
2082 let (graph, _returner_id, plain_id, _error_id) = build_returns_graph();
2083 let pm = PluginManager::new();
2084 let ctx = GraphEvalContext::new(&graph, &pm);
2085 let entry = graph.nodes().get(plain_id).expect("plain_fn exists");
2086
2087 assert!(!match_returns(
2091 &ctx,
2092 plain_id,
2093 entry,
2094 &Operator::Equal,
2095 &Value::String("error".to_string()),
2096 ));
2097 }
2098
2099 #[test]
2100 fn test_match_returns_byte_exact_miss_on_different_target_name() {
2101 let (graph, returner_id, _plain_id, _error_id) = build_returns_graph();
2102 let pm = PluginManager::new();
2103 let ctx = GraphEvalContext::new(&graph, &pm);
2104 let entry = graph.nodes().get(returner_id).expect("returner exists");
2105
2106 assert!(!match_returns(
2110 &ctx,
2111 returner_id,
2112 entry,
2113 &Operator::Equal,
2114 &Value::String("Error".to_string()),
2115 ));
2116 }
2117
2118 #[test]
2119 fn test_match_returns_rejects_non_callable_kinds() {
2120 let (graph, _returner_id, _plain_id, error_id) = build_returns_graph();
2121 let pm = PluginManager::new();
2122 let ctx = GraphEvalContext::new(&graph, &pm);
2123 let entry = graph.nodes().get(error_id).expect("error type exists");
2126
2127 assert!(!match_returns(
2128 &ctx,
2129 error_id,
2130 entry,
2131 &Operator::Equal,
2132 &Value::String("error".to_string()),
2133 ));
2134 }
2135}