1use std::collections::{HashMap, HashSet, VecDeque};
80use std::sync::Arc;
81
82use globset::GlobBuilder;
83
84use sqry_core::graph::unified::bind::scope::arena::ScopeKind;
85use sqry_core::graph::unified::concurrent::GraphSnapshot;
86use sqry_core::graph::unified::edge::kind::{EdgeKind, TypeOfContext};
87use sqry_core::graph::unified::edge::store::StoreEdgeRef;
88use sqry_core::graph::unified::materialize::display_entry_qualified_name;
89use sqry_core::graph::unified::node::id::NodeId;
90use sqry_core::graph::unified::node::kind::NodeKind;
91use sqry_core::graph::unified::storage::arena::NodeEntry;
92use sqry_core::schema::Visibility;
93
94use super::fuse::{FusedPlanBatch, FusionTail};
95use super::ir::{
96 Direction, MatchMode, PathPattern, PlanNode, Predicate, PredicateValue, QueryPlan,
97 SetOperation, StringPattern,
98};
99use crate::QueryDb;
100use crate::dependency::record_file_dep;
101use crate::queries::relation::{RelationKey, RelationKind, relation_matches_node_via_set};
102use crate::queries::{
103 CalleesQuery, CallersQuery, ExportsQuery, ImplementsQuery, ImportsQuery, ReferencesQuery,
104};
105
106#[must_use]
118pub fn execute_plan(plan: &QueryPlan, db: &QueryDb) -> Vec<NodeId> {
119 let mut executor = PlanExecutor::new(db);
120 executor.run(&plan.root, None).as_ref().clone()
121}
122
123#[must_use]
135pub fn execute_batch(batch: &FusedPlanBatch, db: &QueryDb) -> Vec<Vec<NodeId>> {
136 let mut executor = PlanExecutor::new(db);
137 executor.prime_subqueries(batch);
138 executor.prime_shared_nodes(batch);
139
140 let total = batch.total_plans();
141 let mut out: Vec<Vec<NodeId>> = vec![Vec::new(); total];
142
143 for group in batch.groups() {
144 let prefix_result = executor.run(group.prefix(), None);
145 for fused in group.tails() {
146 let tail_result: Arc<Vec<NodeId>> = match &fused.tail {
147 FusionTail::Identity => Arc::clone(&prefix_result),
148 FusionTail::ChainContinuation { remaining_steps } => executor
149 .run_chain_continuation(
150 group.prefix(),
151 Arc::clone(&prefix_result),
152 remaining_steps,
153 ),
154 };
155 if let Some(slot) = out.get_mut(fused.original_index) {
158 slot.clone_from(tail_result.as_ref());
159 }
160 }
161 }
162
163 out
164}
165
166pub struct PlanExecutor<'db> {
178 #[allow(dead_code)]
181 db: &'db QueryDb,
182 snapshot: Arc<GraphSnapshot>,
186 shared_node_cache: HashMap<PlanNode, Arc<Vec<NodeId>>>,
192}
193
194impl<'db> PlanExecutor<'db> {
195 #[must_use]
197 pub fn new(db: &'db QueryDb) -> Self {
198 Self {
199 db,
200 snapshot: db.snapshot_arc(),
201 shared_node_cache: HashMap::new(),
202 }
203 }
204
205 #[must_use]
207 pub fn execute(&mut self, plan: &QueryPlan) -> Vec<NodeId> {
208 self.run(&plan.root, None).as_ref().clone()
209 }
210
211 fn prime_shared_nodes(&mut self, batch: &FusedPlanBatch) {
214 for shared_node in batch.shared_nodes() {
215 let result = self.run(shared_node.canonical_plan(), None);
216 self.shared_node_cache
217 .insert(shared_node.canonical_plan().clone(), result);
218 }
219 }
220
221 fn prime_subqueries(&mut self, batch: &FusedPlanBatch) {
225 let Some(sub_batch) = batch.subquery_batch() else {
226 return;
227 };
228 self.prime_subqueries(sub_batch);
230 self.prime_shared_nodes(sub_batch);
231 for group in sub_batch.groups() {
232 let prefix_result = self.run(group.prefix(), None);
233 for fused in group.tails() {
234 let plan = fused.reconstruct(group.prefix());
235 let result: Arc<Vec<NodeId>> = match &fused.tail {
236 FusionTail::Identity => Arc::clone(&prefix_result),
237 FusionTail::ChainContinuation { remaining_steps } => self
238 .run_chain_continuation(
239 group.prefix(),
240 Arc::clone(&prefix_result),
241 remaining_steps,
242 ),
243 };
244 self.shared_node_cache.insert(plan.root, result);
245 }
246 }
247 }
248
249 fn run(&mut self, node: &PlanNode, input: Option<Arc<Vec<NodeId>>>) -> Arc<Vec<NodeId>> {
253 if input.is_none()
254 && let Some(existing) = self.shared_node_cache.get(node)
255 {
256 return Arc::clone(existing);
257 }
258
259 match node {
260 PlanNode::NodeScan {
261 kind,
262 visibility,
263 name_pattern,
264 } => self.run_scan(*kind, *visibility, name_pattern.as_ref()),
265 PlanNode::EdgeTraversal {
266 direction,
267 edge_kind,
268 max_depth,
269 } => {
270 let input = input.unwrap_or_else(|| Arc::new(Vec::new()));
271 self.run_traversal(input.as_ref(), *direction, edge_kind.as_ref(), *max_depth)
272 }
273 PlanNode::Filter { predicate } => {
274 let input = input.unwrap_or_else(|| Arc::new(Vec::new()));
275 self.run_filter(input.as_ref(), predicate)
276 }
277 PlanNode::SetOp { op, left, right } => self.run_setop(*op, left, right),
278 PlanNode::Chain { steps } => self.run_chain(steps),
279 }
280 }
281
282 fn run_chain(&mut self, steps: &[PlanNode]) -> Arc<Vec<NodeId>> {
283 if steps.is_empty() {
284 return Arc::new(Vec::new());
285 }
286 let prefix = steps[0].clone();
287 let current = self.run(&steps[0], None);
288 self.run_chain_continuation(&prefix, current, &steps[1..])
289 }
290
291 fn run_chain_continuation(
292 &mut self,
293 prefix: &PlanNode,
294 mut current: Arc<Vec<NodeId>>,
295 remaining: &[PlanNode],
296 ) -> Arc<Vec<NodeId>> {
297 let mut current_prefix = prefix.clone();
298 let mut remaining_steps = remaining;
299
300 while !remaining_steps.is_empty() {
301 if let Some((cached_result, consumed, combined_prefix)) =
302 self.lookup_shared_chain_prefix(¤t_prefix, remaining_steps)
303 {
304 current = cached_result;
305 current_prefix = combined_prefix;
306 remaining_steps = &remaining_steps[consumed..];
307 continue;
308 }
309
310 current = self.run(&remaining_steps[0], Some(Arc::clone(¤t)));
311 current_prefix = append_chain_prefix(¤t_prefix, &remaining_steps[..1]);
312 remaining_steps = &remaining_steps[1..];
313 }
314 current
315 }
316
317 fn lookup_shared_chain_prefix(
318 &self,
319 prefix: &PlanNode,
320 remaining: &[PlanNode],
321 ) -> Option<(Arc<Vec<NodeId>>, usize, PlanNode)> {
322 for consumed in (1..=remaining.len()).rev() {
323 let combined_prefix = append_chain_prefix(prefix, &remaining[..consumed]);
324 if let Some(existing) = self.shared_node_cache.get(&combined_prefix) {
325 return Some((Arc::clone(existing), consumed, combined_prefix));
326 }
327 }
328 None
329 }
330
331 fn run_scan(
332 &self,
333 kind: Option<NodeKind>,
334 visibility: Option<Visibility>,
335 name_pattern: Option<&StringPattern>,
336 ) -> Arc<Vec<NodeId>> {
337 let compiled_name = name_pattern.and_then(CompiledStringPattern::compile);
338
339 let mut out: Vec<NodeId> = Vec::new();
340 match kind {
341 Some(k) => {
342 let ids = self.snapshot.indices().by_kind(k);
343 out.reserve(ids.len());
344 for &id in ids {
345 if let Some(entry) = self.snapshot.nodes().get(id) {
346 Self::record_entry_deps(entry);
347 if self.scan_match(id, entry, visibility, compiled_name.as_ref()) {
348 out.push(id);
349 }
350 }
351 }
352 }
353 None => {
354 for (id, entry) in self.snapshot.nodes().iter() {
355 if entry.is_unified_loser() {
359 continue;
360 }
361 Self::record_entry_deps(entry);
362 if self.scan_match(id, entry, visibility, compiled_name.as_ref()) {
363 out.push(id);
364 }
365 }
366 }
367 }
368 dedup_sort(&mut out);
369 Arc::new(out)
370 }
371
372 fn scan_match(
392 &self,
393 node_id: NodeId,
394 entry: &NodeEntry,
395 visibility: Option<Visibility>,
396 compiled_name: Option<&CompiledStringPattern>,
397 ) -> bool {
398 if let Some(required) = visibility
399 && entry_visibility(&self.snapshot, entry) != Some(required)
400 {
401 return false;
402 }
403 if let Some(pattern) = compiled_name {
404 if !entry_name_or_display_matches(&self.snapshot, entry, pattern) {
410 return false;
411 }
412 if self.snapshot.is_node_synthetic(node_id) {
416 return false;
417 }
418 }
419 true
420 }
421
422 fn run_traversal(
423 &self,
424 input: &[NodeId],
425 direction: Direction,
426 edge_kind: Option<&EdgeKind>,
427 max_depth: u32,
428 ) -> Arc<Vec<NodeId>> {
429 if max_depth == 0 || input.is_empty() {
430 return Arc::new(Vec::new());
431 }
432 let target_discriminant = edge_kind.map(std::mem::discriminant);
433
434 let mut visited: HashSet<NodeId> = input.iter().copied().collect();
435 let mut result: Vec<NodeId> = Vec::new();
436 let mut queue: VecDeque<(NodeId, u32)> = input.iter().map(|&id| (id, 0_u32)).collect();
437
438 while let Some((current, depth)) = queue.pop_front() {
439 if depth >= max_depth {
440 continue;
441 }
442 for edge in self.neighbours(current, direction) {
443 if let Some(disc) = target_discriminant
444 && std::mem::discriminant(&edge.kind) != disc
445 {
446 continue;
447 }
448 let next = match direction {
449 Direction::Forward => edge.target,
450 Direction::Reverse => edge.source,
451 Direction::Both => {
452 if edge.source == current {
453 edge.target
454 } else {
455 edge.source
456 }
457 }
458 };
459 if visited.insert(next) {
460 record_file_dep(edge.file);
462 if let Some(entry) = self.snapshot.nodes().get(next) {
463 record_file_dep(entry.file);
464 }
465 result.push(next);
466 queue.push_back((next, depth + 1));
467 }
468 }
469 }
470 dedup_sort(&mut result);
471 Arc::new(result)
472 }
473
474 fn run_filter(&mut self, input: &[NodeId], predicate: &Predicate) -> Arc<Vec<NodeId>> {
475 let compiled = CompiledPredicate::compile(predicate);
476 let mut out: Vec<NodeId> = Vec::with_capacity(input.len());
477 for &node_id in input {
478 if self.check_predicate(node_id, &compiled) {
479 out.push(node_id);
480 }
481 }
482 dedup_sort(&mut out);
483 Arc::new(out)
484 }
485
486 fn run_setop(
487 &mut self,
488 op: SetOperation,
489 left: &PlanNode,
490 right: &PlanNode,
491 ) -> Arc<Vec<NodeId>> {
492 let left_result = self.run(left, None);
493 let right_result = self.run(right, None);
494 let l = left_result.as_ref();
495 let r = right_result.as_ref();
496
497 let mut out: Vec<NodeId> = match op {
498 SetOperation::Union => {
499 let mut v = Vec::with_capacity(l.len() + r.len());
500 v.extend_from_slice(l);
501 v.extend_from_slice(r);
502 v
503 }
504 SetOperation::Intersect => {
505 let rhs: HashSet<NodeId> = r.iter().copied().collect();
506 l.iter().copied().filter(|id| rhs.contains(id)).collect()
507 }
508 SetOperation::Difference => {
509 let rhs: HashSet<NodeId> = r.iter().copied().collect();
510 l.iter().copied().filter(|id| !rhs.contains(id)).collect()
511 }
512 };
513 dedup_sort(&mut out);
514 Arc::new(out)
515 }
516
517 fn check_predicate(&mut self, node_id: NodeId, predicate: &CompiledPredicate) -> bool {
522 let Some(entry) = self.snapshot.nodes().get(node_id) else {
523 return false;
524 };
525 Self::record_entry_deps(entry);
526
527 match predicate {
528 CompiledPredicate::HasCaller => {
529 self.has_kind(node_id, Direction::Reverse, &CALLS_PROBE)
530 }
531 CompiledPredicate::HasCallee => {
532 self.has_kind(node_id, Direction::Forward, &CALLS_PROBE)
533 }
534 CompiledPredicate::IsUnused => !self.has_any_inbound_use(node_id),
535
536 CompiledPredicate::Callers(value) => {
541 self.relation_matches_via_db::<CallersQuery>(node_id, RelationKind::Callers, value)
542 }
543 CompiledPredicate::Callees(value) => {
544 self.relation_matches_via_db::<CalleesQuery>(node_id, RelationKind::Callees, value)
545 }
546 CompiledPredicate::Imports(value) => {
547 self.relation_matches_via_db::<ImportsQuery>(node_id, RelationKind::Imports, value)
548 }
549 CompiledPredicate::Exports(value) => {
550 self.relation_matches_via_db::<ExportsQuery>(node_id, RelationKind::Exports, value)
551 }
552 CompiledPredicate::References(value) => self
553 .relation_matches_via_db::<ReferencesQuery>(
554 node_id,
555 RelationKind::References,
556 value,
557 ),
558 CompiledPredicate::Implements(value) => self
559 .relation_matches_via_db::<ImplementsQuery>(
560 node_id,
561 RelationKind::Implements,
562 value,
563 ),
564
565 CompiledPredicate::InFile(glob) => entry_in_file(&self.snapshot, entry, glob),
566 CompiledPredicate::InScope(kind) => entry_in_scope(&self.snapshot, node_id, *kind),
567 CompiledPredicate::MatchesName(pattern) => {
568 entry_name_matches(&self.snapshot, node_id, entry, pattern)
569 }
570 CompiledPredicate::Returns(type_name) => self.node_returns_type(node_id, type_name),
571
572 CompiledPredicate::And(list) => list
573 .iter()
574 .all(|inner| self.check_predicate(node_id, inner)),
575 CompiledPredicate::Or(list) => list
576 .iter()
577 .any(|inner| self.check_predicate(node_id, inner)),
578 CompiledPredicate::Not(inner) => !self.check_predicate(node_id, inner),
579 }
580 }
581
582 fn relation_matches_via_db<Q>(
599 &mut self,
600 node_id: NodeId,
601 relation: RelationKind,
602 value: &PredicateValue,
603 ) -> bool
604 where
605 Q: crate::query::DerivedQuery<Key = RelationKey, Value = Arc<Vec<NodeId>>>,
606 {
607 match value {
608 PredicateValue::Pattern(pat) => {
609 let key = RelationKey::Pattern(pat.clone());
610 let matches = self.db.get::<Q>(&key);
611 matches.as_ref().binary_search(&node_id).is_ok()
612 }
613 PredicateValue::Regex(re) => {
614 let key = RelationKey::Regex(re.clone());
615 let matches = self.db.get::<Q>(&key);
616 matches.as_ref().binary_search(&node_id).is_ok()
617 }
618 PredicateValue::Subquery(sub_plan) => {
619 let subquery_set = self.subquery_result(sub_plan);
620 let endpoints: HashSet<NodeId> = subquery_set.iter().copied().collect();
621 relation_matches_node_via_set(relation, node_id, &endpoints, &self.snapshot)
622 }
623 }
624 }
625
626 fn subquery_result(&mut self, sub_plan: &PlanNode) -> Arc<Vec<NodeId>> {
628 if let Some(existing) = self.shared_node_cache.get(sub_plan) {
629 return Arc::clone(existing);
630 }
631 let result = self.run(sub_plan, None);
632 self.shared_node_cache
633 .insert(sub_plan.clone(), Arc::clone(&result));
634 result
635 }
636
637 fn neighbours(&self, node_id: NodeId, direction: Direction) -> Vec<StoreEdgeRef> {
642 match direction {
643 Direction::Forward => self.snapshot.edges().edges_from(node_id),
644 Direction::Reverse => self.snapshot.edges().edges_to(node_id),
645 Direction::Both => {
646 let mut out = self.snapshot.edges().edges_from(node_id);
647 out.extend(self.snapshot.edges().edges_to(node_id));
648 out
649 }
650 }
651 }
652
653 fn has_kind(&self, node_id: NodeId, direction: Direction, probe: &EdgeKind) -> bool {
654 let wanted = std::mem::discriminant(probe);
655 self.neighbours(node_id, direction)
656 .iter()
657 .any(|edge| std::mem::discriminant(&edge.kind) == wanted)
658 }
659
660 fn has_any_inbound_use(&self, node_id: NodeId) -> bool {
666 for edge in self.snapshot.edges().edges_to(node_id) {
667 if matches!(
668 edge.kind,
669 EdgeKind::Calls { .. }
670 | EdgeKind::References
671 | EdgeKind::Imports { .. }
672 | EdgeKind::FfiCall { .. }
673 | EdgeKind::GrpcCall { .. }
674 | EdgeKind::HttpRequest { .. }
675 | EdgeKind::WebAssemblyCall
676 | EdgeKind::Implements
677 | EdgeKind::Inherits
678 ) {
679 return true;
680 }
681 }
682 false
683 }
684
685 fn node_returns_type(&self, node_id: NodeId, type_name: &str) -> bool {
701 for edge in self.snapshot.edges().edges_from(node_id) {
702 if !matches!(
703 edge.kind,
704 EdgeKind::TypeOf {
705 context: Some(TypeOfContext::Return),
706 ..
707 }
708 ) {
709 continue;
710 }
711 let Some(target_entry) = self.snapshot.nodes().get(edge.target) else {
712 continue;
713 };
714 record_file_dep(target_entry.file);
717 if let Some(name) = self.snapshot.strings().resolve(target_entry.name)
718 && name.as_ref() == type_name
719 {
720 return true;
721 }
722 }
723 false
724 }
725
726 fn record_entry_deps(entry: &NodeEntry) {
733 record_file_dep(entry.file);
734 }
735}
736
737#[derive(Debug)]
750enum CompiledPredicate {
751 HasCaller,
752 HasCallee,
753 IsUnused,
754 Callers(PredicateValue),
755 Callees(PredicateValue),
756 Imports(PredicateValue),
757 Exports(PredicateValue),
758 References(PredicateValue),
759 Implements(PredicateValue),
760 InFile(CompiledPathPattern),
761 InScope(ScopeKind),
762 MatchesName(CompiledStringPattern),
763 Returns(String),
768 And(Vec<CompiledPredicate>),
769 Or(Vec<CompiledPredicate>),
770 Not(Box<CompiledPredicate>),
771}
772
773impl CompiledPredicate {
774 fn compile(predicate: &Predicate) -> Self {
775 match predicate {
776 Predicate::HasCaller => CompiledPredicate::HasCaller,
777 Predicate::HasCallee => CompiledPredicate::HasCallee,
778 Predicate::IsUnused => CompiledPredicate::IsUnused,
779 Predicate::Callers(v) => CompiledPredicate::Callers(v.clone()),
782 Predicate::Callees(v) => CompiledPredicate::Callees(v.clone()),
783 Predicate::Imports(v) => CompiledPredicate::Imports(v.clone()),
784 Predicate::Exports(v) => CompiledPredicate::Exports(v.clone()),
785 Predicate::References(v) => CompiledPredicate::References(v.clone()),
786 Predicate::Implements(v) => CompiledPredicate::Implements(v.clone()),
787 Predicate::InFile(path) => {
788 CompiledPredicate::InFile(CompiledPathPattern::compile(path))
789 }
790 Predicate::InScope(kind) => CompiledPredicate::InScope(*kind),
791 Predicate::MatchesName(pattern) => CompiledPredicate::MatchesName(
792 CompiledStringPattern::compile(pattern)
793 .unwrap_or(CompiledStringPattern::REJECT_ALL),
794 ),
795 Predicate::Returns(type_name) => CompiledPredicate::Returns(type_name.clone()),
796 Predicate::And(list) => {
797 CompiledPredicate::And(list.iter().map(CompiledPredicate::compile).collect())
798 }
799 Predicate::Or(list) => {
800 CompiledPredicate::Or(list.iter().map(CompiledPredicate::compile).collect())
801 }
802 Predicate::Not(inner) => {
803 CompiledPredicate::Not(Box::new(CompiledPredicate::compile(inner)))
804 }
805 }
806 }
807}
808
809#[derive(Debug)]
811enum CompiledStringPattern {
812 Literal {
813 needle: String,
814 mode: MatchMode,
815 case_insensitive: bool,
816 },
817 Glob {
818 matcher: globset::GlobMatcher,
819 },
820 RejectAll,
823}
824
825impl CompiledStringPattern {
826 const REJECT_ALL: Self = CompiledStringPattern::RejectAll;
827
828 fn compile(pattern: &StringPattern) -> Option<Self> {
829 match pattern.mode {
830 MatchMode::Glob => {
831 let glob = GlobBuilder::new(&pattern.raw)
832 .case_insensitive(pattern.case_insensitive)
833 .literal_separator(false)
834 .build()
835 .ok()?;
836 Some(CompiledStringPattern::Glob {
837 matcher: glob.compile_matcher(),
838 })
839 }
840 MatchMode::Exact | MatchMode::Prefix | MatchMode::Suffix | MatchMode::Contains => {
841 let needle = if pattern.case_insensitive {
842 pattern.raw.to_lowercase()
843 } else {
844 pattern.raw.clone()
845 };
846 Some(CompiledStringPattern::Literal {
847 needle,
848 mode: pattern.mode,
849 case_insensitive: pattern.case_insensitive,
850 })
851 }
852 }
853 }
854
855 fn matches(&self, candidate: &str) -> bool {
856 match self {
857 CompiledStringPattern::Literal {
858 needle,
859 mode,
860 case_insensitive,
861 } => {
862 let haystack_owned: Option<String> = if *case_insensitive {
863 Some(candidate.to_lowercase())
864 } else {
865 None
866 };
867 let haystack = haystack_owned.as_deref().unwrap_or(candidate);
868 match mode {
869 MatchMode::Exact => haystack == needle,
870 MatchMode::Prefix => haystack.starts_with(needle.as_str()),
871 MatchMode::Suffix => haystack.ends_with(needle.as_str()),
872 MatchMode::Contains => haystack.contains(needle.as_str()),
873 MatchMode::Glob => false, }
875 }
876 CompiledStringPattern::Glob { matcher } => matcher.is_match(candidate),
877 CompiledStringPattern::RejectAll => false,
878 }
879 }
880}
881
882#[derive(Debug)]
883enum CompiledPathPattern {
884 Glob(globset::GlobMatcher),
885 RejectAll,
886}
887
888impl CompiledPathPattern {
889 fn compile(path: &PathPattern) -> Self {
890 match globset::Glob::new(&path.glob) {
891 Ok(glob) => CompiledPathPattern::Glob(glob.compile_matcher()),
892 Err(_) => CompiledPathPattern::RejectAll,
893 }
894 }
895
896 fn matches(&self, candidate: &str) -> bool {
897 match self {
898 CompiledPathPattern::Glob(matcher) => matcher.is_match(candidate),
899 CompiledPathPattern::RejectAll => false,
900 }
901 }
902}
903
904const CALLS_PROBE: EdgeKind = EdgeKind::Calls {
914 argument_count: 0,
915 is_async: false,
916};
917
918fn dedup_sort(v: &mut Vec<NodeId>) {
923 v.sort_unstable_by_key(|id| (id.index(), id.generation()));
924 v.dedup();
925}
926
927fn append_chain_prefix(prefix: &PlanNode, appended_steps: &[PlanNode]) -> PlanNode {
928 if appended_steps.is_empty() {
929 return prefix.clone();
930 }
931
932 let mut steps = match prefix {
933 PlanNode::Chain { steps } => steps.clone(),
934 _ => vec![prefix.clone()],
935 };
936 steps.extend(appended_steps.iter().cloned());
937 PlanNode::Chain { steps }
938}
939
940fn entry_visibility(snapshot: &GraphSnapshot, entry: &NodeEntry) -> Option<Visibility> {
941 entry
942 .visibility
943 .and_then(|sid| snapshot.strings().resolve(sid))
944 .and_then(|s| Visibility::parse(s.as_ref()))
945}
946
947fn entry_in_file(snapshot: &GraphSnapshot, entry: &NodeEntry, glob: &CompiledPathPattern) -> bool {
948 let Some(path) = snapshot.files().resolve(entry.file) else {
949 return false;
950 };
951 let path_str = path.to_string_lossy();
952 glob.matches(path_str.as_ref())
953}
954
955fn entry_in_scope(snapshot: &GraphSnapshot, node_id: NodeId, kind: ScopeKind) -> bool {
956 let Some(entry) = snapshot.nodes().get(node_id) else {
957 return false;
958 };
959 for (_, scope) in snapshot.scope_arena().iter() {
960 if scope.kind != kind {
961 continue;
962 }
963 if scope.file != entry.file {
964 continue;
965 }
966 if scope.byte_span.0 <= entry.start_byte && entry.end_byte <= scope.byte_span.1 {
968 return true;
969 }
970 }
971 false
972}
973
974fn entry_name_matches(
985 snapshot: &GraphSnapshot,
986 node_id: NodeId,
987 entry: &NodeEntry,
988 pattern: &CompiledStringPattern,
989) -> bool {
990 if !entry_name_or_display_matches(snapshot, entry, pattern) {
991 return false;
992 }
993 !snapshot.is_node_synthetic(node_id)
997}
998
999fn entry_name_or_display_matches(
1000 snapshot: &GraphSnapshot,
1001 entry: &NodeEntry,
1002 pattern: &CompiledStringPattern,
1003) -> bool {
1004 let simple_name = snapshot.strings().resolve(entry.name);
1005 if simple_name
1006 .as_ref()
1007 .is_some_and(|name| pattern.matches(name.as_ref()))
1008 {
1009 return true;
1010 }
1011
1012 let Some(qualified_name) = entry
1013 .qualified_name
1014 .and_then(|sid| snapshot.strings().resolve(sid))
1015 else {
1016 return false;
1017 };
1018 if pattern.matches(qualified_name.as_ref()) {
1019 return true;
1020 }
1021
1022 let fallback_name = simple_name.as_deref().unwrap_or_default();
1023 let display_name =
1024 display_entry_qualified_name(entry, snapshot.strings(), snapshot.files(), fallback_name);
1025 display_name != qualified_name.as_ref() && pattern.matches(&display_name)
1026}
1027
1028#[cfg(test)]
1035mod tests {
1036 use super::*;
1037 use crate::planner::ir::{MatchMode, StringPattern};
1038
1039 #[test]
1040 fn compiled_string_pattern_exact_case_sensitive() {
1041 let p = CompiledStringPattern::compile(&StringPattern::exact("Foo")).unwrap();
1042 assert!(p.matches("Foo"));
1043 assert!(!p.matches("foo"));
1044 }
1045
1046 #[test]
1047 fn compiled_string_pattern_exact_case_insensitive() {
1048 let p = CompiledStringPattern::compile(&StringPattern::exact("Foo").case_insensitive())
1049 .unwrap();
1050 assert!(p.matches("FOO"));
1051 assert!(p.matches("foo"));
1052 assert!(!p.matches("bar"));
1053 }
1054
1055 #[test]
1056 fn compiled_string_pattern_prefix_suffix_contains() {
1057 let pref = CompiledStringPattern::compile(&StringPattern::prefix("abc")).unwrap();
1058 assert!(pref.matches("abcdef"));
1059 assert!(!pref.matches("zabc"));
1060
1061 let suf = CompiledStringPattern::compile(&StringPattern::suffix("xyz")).unwrap();
1062 assert!(suf.matches("foo_xyz"));
1063 assert!(!suf.matches("xyz_foo"));
1064
1065 let cont = CompiledStringPattern::compile(&StringPattern::contains("mid")).unwrap();
1066 assert!(cont.matches("prefix_mid_suffix"));
1067 assert!(!cont.matches("no"));
1068 }
1069
1070 #[test]
1071 fn compiled_string_pattern_glob() {
1072 let p = CompiledStringPattern::compile(&StringPattern::glob("parse_*")).unwrap();
1073 assert!(p.matches("parse_expr"));
1074 assert!(p.matches("parse_"));
1075 assert!(!p.matches("lexer"));
1076 }
1077
1078 #[test]
1079 fn compiled_string_pattern_malformed_glob_rejects_all() {
1080 let malformed = StringPattern {
1082 raw: "[abc".into(),
1083 mode: MatchMode::Glob,
1084 case_insensitive: false,
1085 };
1086 let p =
1087 CompiledStringPattern::compile(&malformed).unwrap_or(CompiledStringPattern::REJECT_ALL);
1088 assert!(!p.matches("abc"));
1089 assert!(!p.matches("[abc"));
1090 }
1091
1092 #[test]
1098 fn dedup_sort_sorts_and_dedupes_by_index_then_generation() {
1099 let mut v = vec![
1100 NodeId::new(3, 1),
1101 NodeId::new(1, 1),
1102 NodeId::new(3, 1),
1103 NodeId::new(2, 1),
1104 NodeId::new(1, 1),
1105 ];
1106 dedup_sort(&mut v);
1107 assert_eq!(
1108 v,
1109 vec![NodeId::new(1, 1), NodeId::new(2, 1), NodeId::new(3, 1)]
1110 );
1111 }
1112
1113 #[test]
1114 fn compiled_path_pattern_glob_and_reject() {
1115 let good = CompiledPathPattern::compile(&PathPattern::new("src/**/*.rs"));
1116 assert!(good.matches("src/graph/unified/mod.rs"));
1117 assert!(!good.matches("docs/README.md"));
1118
1119 let bad = CompiledPathPattern::compile(&PathPattern::new("[abc"));
1121 assert!(!bad.matches("abc"));
1122 }
1123}