1use crate::query::RepoFilter;
22use crate::query::types::{Expr, JoinExpr, Operator, Query as QueryAST, Value};
23use anyhow::Result;
24use std::sync::Arc;
25
26#[derive(Debug, Clone)]
49pub struct ParsedQuery {
50 pub ast: Arc<QueryAST>,
58
59 pub repo_filter: RepoFilter,
67
68 pub normalized: Arc<str>,
79}
80
81impl ParsedQuery {
82 #[must_use]
90 pub fn new(ast: Arc<QueryAST>, repo_filter: RepoFilter, normalized: String) -> Self {
91 Self {
92 ast,
93 repo_filter,
94 normalized: Arc::from(normalized),
95 }
96 }
97
98 pub fn from_ast(ast: Arc<QueryAST>) -> Result<Self> {
128 let repo_filter = extract_repo_filter(&ast)?;
130
131 let normalized_ast = strip_repo_predicates(&ast);
133
134 let normalized = if let Some(norm_ast) = normalized_ast {
136 serialize_query(&norm_ast)
137 } else {
138 String::new()
140 };
141
142 Ok(Self {
143 ast,
144 repo_filter,
145 normalized: Arc::from(normalized),
146 })
147 }
148
149 #[inline]
151 #[must_use]
152 pub fn ast(&self) -> &Arc<QueryAST> {
153 &self.ast
154 }
155
156 #[inline]
158 #[must_use]
159 pub fn repo_filter(&self) -> &RepoFilter {
160 &self.repo_filter
161 }
162
163 #[inline]
165 #[must_use]
166 pub fn normalized(&self) -> &str {
167 &self.normalized
168 }
169}
170
171pub fn extract_repo_filter(ast: &QueryAST) -> Result<RepoFilter> {
202 let mut patterns = Vec::new();
203 let has_negated_repo = collect_repo_patterns(&ast.root, &mut patterns, false);
204
205 if has_negated_repo {
206 RepoFilter::new(vec![])
209 } else {
210 RepoFilter::new(patterns)
211 }
212}
213
214fn collect_repo_patterns(expr: &Expr, patterns: &mut Vec<String>, is_negated: bool) -> bool {
224 match expr {
225 Expr::And(operands) | Expr::Or(operands) => {
226 let mut found_negated = false;
227 for operand in operands {
228 if collect_repo_patterns(operand, patterns, is_negated) {
229 found_negated = true;
230 }
231 }
232 found_negated
233 }
234 Expr::Not(operand) => {
235 collect_repo_patterns(operand, patterns, !is_negated)
237 }
238 Expr::Condition(condition) => {
239 if condition.field.as_str() == "repo" {
240 if is_negated {
241 return true;
243 }
244
245 if let (Operator::Equal, Value::String(pattern)) =
247 (&condition.operator, &condition.value)
248 {
249 patterns.push(pattern.clone());
250 }
251 }
252 false
253 }
254 Expr::Join(join) => {
255 let left = collect_repo_patterns(&join.left, patterns, is_negated);
256 let right = collect_repo_patterns(&join.right, patterns, is_negated);
257 left || right
258 }
259 }
260}
261
262#[must_use]
283pub fn strip_repo_predicates(ast: &QueryAST) -> Option<QueryAST> {
284 strip_repo_from_expr(&ast.root).map(|root| QueryAST {
285 root,
286 span: ast.span.clone(),
287 })
288}
289
290fn strip_repo_from_expr(expr: &Expr) -> Option<Expr> {
292 match expr {
293 Expr::And(operands) => {
294 let filtered: Vec<Expr> = operands.iter().filter_map(strip_repo_from_expr).collect();
295
296 match filtered.len() {
297 0 => None, 1 => Some(filtered.into_iter().next().unwrap()), _ => Some(Expr::And(filtered)),
300 }
301 }
302 Expr::Or(operands) => {
303 let filtered: Vec<Expr> = operands.iter().filter_map(strip_repo_from_expr).collect();
304
305 match filtered.len() {
306 0 => None, 1 => Some(filtered.into_iter().next().unwrap()), _ => Some(Expr::Or(filtered)),
309 }
310 }
311 Expr::Not(operand) => {
312 strip_repo_from_expr(operand).map(|expr| Expr::Not(Box::new(expr)))
314 }
315 Expr::Condition(condition) => {
316 if condition.field.as_str() == "repo" {
318 None
319 } else {
320 Some(Expr::Condition(condition.clone()))
321 }
322 }
323 Expr::Join(join) => {
324 let left = strip_repo_from_expr(&join.left)?;
325 let right = strip_repo_from_expr(&join.right)?;
326 Some(Expr::Join(JoinExpr {
327 left: Box::new(left),
328 edge: join.edge.clone(),
329 right: Box::new(right),
330 span: join.span.clone(),
331 }))
332 }
333 }
334}
335
336#[must_use]
349pub fn serialize_query(ast: &QueryAST) -> String {
350 serialize_expr(&ast.root)
351}
352
353fn serialize_expr(expr: &Expr) -> String {
355 match expr {
356 Expr::And(operands) => {
357 let parts: Vec<String> = operands.iter().map(serialize_expr).collect();
358 parts.join(" AND ")
359 }
360 Expr::Or(operands) => {
361 let parts: Vec<String> = operands.iter().map(serialize_expr).collect();
362 format!("({})", parts.join(" OR "))
363 }
364 Expr::Not(operand) => {
365 format!("NOT {}", serialize_expr(operand))
366 }
367 Expr::Condition(condition) => {
368 let op_str = match condition.operator {
369 Operator::Equal => ":",
370 Operator::Regex => "~=",
371 Operator::Greater => ">",
372 Operator::GreaterEq => ">=",
373 Operator::Less => "<",
374 Operator::LessEq => "<=",
375 };
376
377 let value_str = match &condition.value {
378 Value::String(s) => s.clone(),
379 Value::Number(n) => n.to_string(),
380 Value::Boolean(b) => b.to_string(),
381 Value::Regex(r) => {
382 let mut flags = String::new();
385 if r.flags.case_insensitive {
386 flags.push('i');
387 }
388 if r.flags.multiline {
389 flags.push('m');
390 }
391 if r.flags.dot_all {
392 flags.push('s');
393 }
394 format!("/{}/{}", r.pattern, flags)
395 }
396 Value::Variable(name) => format!("${name}"),
397 Value::Subquery(expr) => format!("({})", serialize_expr(expr)),
398 };
399
400 format!("{}{}{}", condition.field.as_str(), op_str, value_str)
401 }
402 Expr::Join(join) => {
403 format!(
404 "({}) {} ({})",
405 serialize_expr(&join.left),
406 join.edge,
407 serialize_expr(&join.right)
408 )
409 }
410 }
411}
412
413#[cfg(test)]
414mod tests {
415 use super::*;
416 use crate::query::types::{
417 Condition, Expr, Field, Operator, RegexFlags, RegexValue, Span, Value,
418 };
419
420 fn build_regex_ast(flags: RegexFlags) -> QueryAST {
421 QueryAST {
422 root: Expr::Condition(Condition {
423 field: Field::new("name"),
424 operator: Operator::Regex,
425 value: Value::Regex(RegexValue {
426 pattern: "foo".to_string(),
427 flags,
428 }),
429 span: Span::new(0, 10),
430 }),
431 span: Span::new(0, 10),
432 }
433 }
434
435 fn assert_regex_serialization(flags: RegexFlags, expected: &str) {
436 let ast = build_regex_ast(flags);
437 let serialized = serialize_query(&ast);
438 assert_eq!(serialized, expected);
439 }
440
441 #[test]
442 fn test_parsed_query_creation() {
443 let ast = QueryAST {
445 root: Expr::Condition(Condition {
446 field: Field::new("kind"),
447 operator: Operator::Equal,
448 value: Value::String("function".to_string()),
449 span: Span::new(0, 13),
450 }),
451 span: Span::new(0, 13),
452 };
453
454 let repo_filter = RepoFilter::new(vec![]).unwrap();
455 let normalized = "kind:function".to_string();
456
457 let parsed = ParsedQuery::new(Arc::new(ast), repo_filter, normalized);
458
459 assert_eq!(parsed.normalized(), "kind:function");
460 assert!(parsed.repo_filter().patterns().is_empty());
461 }
462
463 #[test]
464 fn test_parsed_query_with_repo_filter() {
465 let ast = QueryAST {
466 root: Expr::Condition(Condition {
467 field: Field::new("kind"),
468 operator: Operator::Equal,
469 value: Value::String("function".to_string()),
470 span: Span::new(0, 13),
471 }),
472 span: Span::new(0, 13),
473 };
474
475 let repo_filter = RepoFilter::new(vec!["backend-*".to_string()]).unwrap();
476 let normalized = "kind:function".to_string();
477
478 let parsed = ParsedQuery::new(Arc::new(ast), repo_filter, normalized.clone());
479
480 assert_eq!(parsed.normalized(), "kind:function");
481 assert_eq!(parsed.repo_filter().patterns().len(), 1);
482 assert_eq!(parsed.repo_filter().patterns()[0], "backend-*");
483 }
484
485 #[test]
486 fn test_parsed_query_arc_sharing() {
487 let ast = QueryAST {
488 root: Expr::Condition(Condition {
489 field: Field::new("name"),
490 operator: Operator::Equal,
491 value: Value::String("test".to_string()),
492 span: Span::new(0, 10),
493 }),
494 span: Span::new(0, 10),
495 };
496
497 let repo_filter = RepoFilter::new(vec![]).unwrap();
498 let parsed = ParsedQuery::new(Arc::new(ast), repo_filter, "name:test".to_string());
499
500 let parsed_clone = parsed.clone();
502 assert!(Arc::ptr_eq(&parsed.ast, &parsed_clone.ast));
503 assert!(Arc::ptr_eq(&parsed.normalized, &parsed_clone.normalized));
504 }
505
506 #[test]
507 fn test_extract_repo_filter_empty() {
508 let ast = QueryAST {
509 root: Expr::Condition(Condition {
510 field: Field::new("kind"),
511 operator: Operator::Equal,
512 value: Value::String("function".to_string()),
513 span: Span::new(0, 13),
514 }),
515 span: Span::new(0, 13),
516 };
517
518 let repo_filter = extract_repo_filter(&ast).unwrap();
519 assert_eq!(repo_filter.patterns().len(), 0);
520 }
521
522 #[test]
523 fn test_extract_repo_filter_single() {
524 let ast = QueryAST {
525 root: Expr::And(vec![
526 Expr::Condition(Condition {
527 field: Field::new("repo"),
528 operator: Operator::Equal,
529 value: Value::String("backend-*".to_string()),
530 span: Span::new(0, 16),
531 }),
532 Expr::Condition(Condition {
533 field: Field::new("kind"),
534 operator: Operator::Equal,
535 value: Value::String("function".to_string()),
536 span: Span::new(21, 34),
537 }),
538 ]),
539 span: Span::new(0, 34),
540 };
541
542 let repo_filter = extract_repo_filter(&ast).unwrap();
543 assert_eq!(repo_filter.patterns().len(), 1);
544 assert_eq!(repo_filter.patterns()[0], "backend-*");
545 }
546
547 #[test]
548 fn test_extract_repo_filter_multiple() {
549 let ast = QueryAST {
550 root: Expr::And(vec![
551 Expr::Condition(Condition {
552 field: Field::new("repo"),
553 operator: Operator::Equal,
554 value: Value::String("backend-*".to_string()),
555 span: Span::new(0, 16),
556 }),
557 Expr::Condition(Condition {
558 field: Field::new("repo"),
559 operator: Operator::Equal,
560 value: Value::String("frontend-*".to_string()),
561 span: Span::new(21, 38),
562 }),
563 ]),
564 span: Span::new(0, 38),
565 };
566
567 let repo_filter = extract_repo_filter(&ast).unwrap();
568 assert_eq!(repo_filter.patterns().len(), 2);
569 assert!(repo_filter.patterns().contains(&"backend-*".to_string()));
570 assert!(repo_filter.patterns().contains(&"frontend-*".to_string()));
571 }
572
573 #[test]
574 fn test_extract_repo_filter_negated_aborts() {
575 let ast = QueryAST {
578 root: Expr::And(vec![
579 Expr::Not(Box::new(Expr::Condition(Condition {
580 field: Field::new("repo"),
581 operator: Operator::Equal,
582 value: Value::String("test-*".to_string()),
583 span: Span::new(4, 16),
584 }))),
585 Expr::Condition(Condition {
586 field: Field::new("kind"),
587 operator: Operator::Equal,
588 value: Value::String("function".to_string()),
589 span: Span::new(21, 34),
590 }),
591 ]),
592 span: Span::new(0, 34),
593 };
594
595 let repo_filter = extract_repo_filter(&ast).unwrap();
596 assert_eq!(
598 repo_filter.patterns().len(),
599 0,
600 "Negated repo predicates should abort pre-filtering"
601 );
602 }
603
604 #[test]
605 fn test_extract_repo_filter_mixed_positive_and_negated() {
606 let ast = QueryAST {
610 root: Expr::And(vec![
611 Expr::Condition(Condition {
612 field: Field::new("repo"),
613 operator: Operator::Equal,
614 value: Value::String("backend-*".to_string()),
615 span: Span::new(0, 16),
616 }),
617 Expr::Not(Box::new(Expr::Condition(Condition {
618 field: Field::new("repo"),
619 operator: Operator::Equal,
620 value: Value::String("test-*".to_string()),
621 span: Span::new(25, 37),
622 }))),
623 Expr::Condition(Condition {
624 field: Field::new("kind"),
625 operator: Operator::Equal,
626 value: Value::String("function".to_string()),
627 span: Span::new(42, 55),
628 }),
629 ]),
630 span: Span::new(0, 55),
631 };
632
633 let repo_filter = extract_repo_filter(&ast).unwrap();
634 assert_eq!(
636 repo_filter.patterns().len(),
637 0,
638 "Mixed positive and negated repo predicates should abort pre-filtering"
639 );
640 }
641
642 #[test]
643 fn test_extract_repo_filter_double_negation() {
644 let ast = QueryAST {
648 root: Expr::And(vec![
649 Expr::Not(Box::new(Expr::Not(Box::new(Expr::Condition(Condition {
650 field: Field::new("repo"),
651 operator: Operator::Equal,
652 value: Value::String("backend-*".to_string()),
653 span: Span::new(9, 25),
654 }))))),
655 Expr::Condition(Condition {
656 field: Field::new("kind"),
657 operator: Operator::Equal,
658 value: Value::String("function".to_string()),
659 span: Span::new(30, 43),
660 }),
661 ]),
662 span: Span::new(0, 43),
663 };
664
665 let repo_filter = extract_repo_filter(&ast).unwrap();
666 assert_eq!(
668 repo_filter.patterns().len(),
669 1,
670 "Double negation should be treated as positive context"
671 );
672 assert_eq!(repo_filter.patterns()[0], "backend-*");
673 }
674
675 #[test]
676 fn test_extract_repo_filter_or_with_negation() {
677 let ast = QueryAST {
680 root: Expr::And(vec![
681 Expr::Or(vec![
682 Expr::Condition(Condition {
683 field: Field::new("repo"),
684 operator: Operator::Equal,
685 value: Value::String("A".to_string()),
686 span: Span::new(1, 7),
687 }),
688 Expr::Not(Box::new(Expr::Condition(Condition {
689 field: Field::new("repo"),
690 operator: Operator::Equal,
691 value: Value::String("B".to_string()),
692 span: Span::new(16, 22),
693 }))),
694 ]),
695 Expr::Condition(Condition {
696 field: Field::new("kind"),
697 operator: Operator::Equal,
698 value: Value::String("function".to_string()),
699 span: Span::new(28, 41),
700 }),
701 ]),
702 span: Span::new(0, 41),
703 };
704
705 let repo_filter = extract_repo_filter(&ast).unwrap();
706 assert_eq!(
707 repo_filter.patterns().len(),
708 0,
709 "OR with negated repo should abort pre-filtering"
710 );
711 }
712
713 #[test]
714 fn test_strip_repo_predicates_none() {
715 let ast = QueryAST {
716 root: Expr::Condition(Condition {
717 field: Field::new("kind"),
718 operator: Operator::Equal,
719 value: Value::String("function".to_string()),
720 span: Span::new(0, 13),
721 }),
722 span: Span::new(0, 13),
723 };
724
725 let stripped = strip_repo_predicates(&ast).unwrap();
726 if let Expr::Condition(cond) = &stripped.root {
728 assert_eq!(cond.field.as_str(), "kind");
729 } else {
730 panic!("Expected Condition, got {:?}", stripped.root);
731 }
732 }
733
734 #[test]
735 fn test_strip_repo_predicates_and() {
736 let ast = QueryAST {
737 root: Expr::And(vec![
738 Expr::Condition(Condition {
739 field: Field::new("repo"),
740 operator: Operator::Equal,
741 value: Value::String("backend-*".to_string()),
742 span: Span::new(0, 16),
743 }),
744 Expr::Condition(Condition {
745 field: Field::new("kind"),
746 operator: Operator::Equal,
747 value: Value::String("function".to_string()),
748 span: Span::new(21, 34),
749 }),
750 ]),
751 span: Span::new(0, 34),
752 };
753
754 let stripped = strip_repo_predicates(&ast).unwrap();
755 if let Expr::Condition(cond) = &stripped.root {
757 assert_eq!(cond.field.as_str(), "kind");
758 } else {
759 panic!("Expected simplified Condition, got {:?}", stripped.root);
760 }
761 }
762
763 #[test]
764 fn test_strip_repo_predicates_all_repo() {
765 let ast = QueryAST {
766 root: Expr::Condition(Condition {
767 field: Field::new("repo"),
768 operator: Operator::Equal,
769 value: Value::String("backend-*".to_string()),
770 span: Span::new(0, 16),
771 }),
772 span: Span::new(0, 16),
773 };
774
775 let stripped = strip_repo_predicates(&ast);
776 assert!(stripped.is_none());
778 }
779
780 #[test]
781 fn test_serialize_simple_condition() {
782 let ast = QueryAST {
783 root: Expr::Condition(Condition {
784 field: Field::new("kind"),
785 operator: Operator::Equal,
786 value: Value::String("function".to_string()),
787 span: Span::new(0, 13),
788 }),
789 span: Span::new(0, 13),
790 };
791
792 let serialized = serialize_query(&ast);
793 assert_eq!(serialized, "kind:function");
794 }
795
796 #[test]
797 fn test_serialize_and_expression() {
798 let ast = QueryAST {
799 root: Expr::And(vec![
800 Expr::Condition(Condition {
801 field: Field::new("kind"),
802 operator: Operator::Equal,
803 value: Value::String("function".to_string()),
804 span: Span::new(0, 13),
805 }),
806 Expr::Condition(Condition {
807 field: Field::new("name"),
808 operator: Operator::Equal,
809 value: Value::String("test".to_string()),
810 span: Span::new(18, 28),
811 }),
812 ]),
813 span: Span::new(0, 28),
814 };
815
816 let serialized = serialize_query(&ast);
817 assert_eq!(serialized, "kind:function AND name:test");
818 }
819
820 #[test]
821 fn test_serialize_or_expression() {
822 let ast = QueryAST {
823 root: Expr::Or(vec![
824 Expr::Condition(Condition {
825 field: Field::new("kind"),
826 operator: Operator::Equal,
827 value: Value::String("function".to_string()),
828 span: Span::new(0, 13),
829 }),
830 Expr::Condition(Condition {
831 field: Field::new("kind"),
832 operator: Operator::Equal,
833 value: Value::String("method".to_string()),
834 span: Span::new(17, 28),
835 }),
836 ]),
837 span: Span::new(0, 28),
838 };
839
840 let serialized = serialize_query(&ast);
841 assert_eq!(serialized, "(kind:function OR kind:method)");
842 }
843
844 #[test]
845 fn test_serialize_regex_no_flags() {
846 assert_regex_serialization(
847 RegexFlags {
848 case_insensitive: false,
849 multiline: false,
850 dot_all: false,
851 },
852 "name~=/foo/",
853 );
854 }
855
856 #[test]
857 fn test_serialize_regex_case_insensitive() {
858 assert_regex_serialization(
859 RegexFlags {
860 case_insensitive: true,
861 multiline: false,
862 dot_all: false,
863 },
864 "name~=/foo/i",
865 );
866 }
867
868 #[test]
869 fn test_serialize_regex_multiline() {
870 assert_regex_serialization(
871 RegexFlags {
872 case_insensitive: false,
873 multiline: true,
874 dot_all: false,
875 },
876 "name~=/foo/m",
877 );
878 }
879
880 #[test]
881 fn test_serialize_regex_dot_all() {
882 assert_regex_serialization(
883 RegexFlags {
884 case_insensitive: false,
885 multiline: false,
886 dot_all: true,
887 },
888 "name~=/foo/s",
889 );
890 }
891
892 #[test]
893 fn test_serialize_regex_all_flags() {
894 assert_regex_serialization(
895 RegexFlags {
896 case_insensitive: true,
897 multiline: true,
898 dot_all: true,
899 },
900 "name~=/foo/ims",
901 );
902 }
903
904 #[test]
905 fn test_serialize_regex_flag_combinations_distinct() {
906 let no_flags = serialize_query(&build_regex_ast(RegexFlags {
907 case_insensitive: false,
908 multiline: false,
909 dot_all: false,
910 }));
911 let with_i = serialize_query(&build_regex_ast(RegexFlags {
912 case_insensitive: true,
913 multiline: false,
914 dot_all: false,
915 }));
916 let with_m = serialize_query(&build_regex_ast(RegexFlags {
917 case_insensitive: false,
918 multiline: true,
919 dot_all: false,
920 }));
921 let with_s = serialize_query(&build_regex_ast(RegexFlags {
922 case_insensitive: false,
923 multiline: false,
924 dot_all: true,
925 }));
926 let with_all = serialize_query(&build_regex_ast(RegexFlags {
927 case_insensitive: true,
928 multiline: true,
929 dot_all: true,
930 }));
931
932 assert_ne!(
933 no_flags, with_i,
934 "Cache collision: no flags vs case_insensitive"
935 );
936 assert_ne!(no_flags, with_m, "Cache collision: no flags vs multiline");
937 assert_ne!(no_flags, with_s, "Cache collision: no flags vs dot_all");
938 assert_ne!(
939 with_i, with_m,
940 "Cache collision: case_insensitive vs multiline"
941 );
942 assert_ne!(
943 with_i, with_all,
944 "Cache collision: case_insensitive vs all flags"
945 );
946 }
947
948 #[test]
951 fn test_serialize_variable() {
952 let ast = QueryAST {
953 root: Expr::Condition(Condition {
954 field: Field::new("kind"),
955 operator: Operator::Equal,
956 value: Value::Variable("type".to_string()),
957 span: Span::new(0, 10),
958 }),
959 span: Span::new(0, 10),
960 };
961 let serialized = serialize_query(&ast);
962 assert_eq!(serialized, "kind:$type");
963 }
964
965 #[test]
966 fn test_serialize_join() {
967 use crate::query::types::JoinEdgeKind;
968
969 let ast = QueryAST {
970 root: Expr::Join(crate::query::types::JoinExpr {
971 left: Box::new(Expr::Condition(Condition {
972 field: Field::new("kind"),
973 operator: Operator::Equal,
974 value: Value::String("function".to_string()),
975 span: Span::new(0, 13),
976 })),
977 edge: JoinEdgeKind::Calls,
978 right: Box::new(Expr::Condition(Condition {
979 field: Field::new("kind"),
980 operator: Operator::Equal,
981 value: Value::String("function".to_string()),
982 span: Span::new(20, 33),
983 })),
984 span: Span::new(0, 33),
985 }),
986 span: Span::new(0, 33),
987 };
988 let serialized = serialize_query(&ast);
989 assert_eq!(serialized, "(kind:function) CALLS (kind:function)");
990 }
991}