1use regex::Regex;
28use std::collections::HashMap;
29
30use super::error::ValidationError;
31use super::registry::FieldRegistry;
32use super::types::{Condition, Expr, Field, FieldDescriptor, FieldType, Operator, Span, Value};
33
34const SAFE_FUZZY_FIELDS: &[&str] = &[
35 "kind",
36 "path",
37 "lang",
38 "repo",
39 "parent",
40 "scope.type",
41 "scope.name",
42 "scope.parent",
43 "scope.ancestor",
44 "callers",
45 "callees",
46 "imports",
47 "exports",
48 "returns",
49 "references",
50];
51
52#[derive(Clone, Copy, Debug)]
81pub struct ValidationOptions {
82 pub fuzzy_fields: bool,
84 pub fuzzy_field_distance: usize,
86}
87
88impl Default for ValidationOptions {
89 fn default() -> Self {
90 Self {
91 fuzzy_fields: false,
92 fuzzy_field_distance: 2,
93 }
94 }
95}
96
97pub struct Validator {
99 registry: FieldRegistry,
100 options: ValidationOptions,
101}
102
103impl Validator {
104 #[must_use]
106 pub fn new(registry: FieldRegistry) -> Self {
107 Self {
108 registry,
109 options: ValidationOptions::default(),
110 }
111 }
112
113 #[must_use]
115 pub fn with_options(registry: FieldRegistry, options: ValidationOptions) -> Self {
116 Self { registry, options }
117 }
118
119 pub fn validate(&self, expr: &Expr) -> Result<(), ValidationError> {
127 self.validate_node_with_depth(expr, 0)
128 }
129
130 pub fn normalize_expr(&self, expr: &Expr) -> Result<Expr, ValidationError> {
136 match expr {
137 Expr::And(operands) => Ok(Expr::And(self.normalize_operands(operands)?)),
138 Expr::Or(operands) => Ok(Expr::Or(self.normalize_operands(operands)?)),
139 Expr::Not(op) => Ok(Expr::Not(Box::new(self.normalize_expr(op)?))),
140 Expr::Condition(cond) => Ok(Expr::Condition(self.normalize_condition(cond)?)),
141 Expr::Join(join) => Ok(Expr::Join(crate::query::types::JoinExpr {
142 left: Box::new(self.normalize_expr(&join.left)?),
143 edge: join.edge.clone(),
144 right: Box::new(self.normalize_expr(&join.right)?),
145 span: join.span.clone(),
146 })),
147 }
148 }
149
150 fn validate_node_with_depth(
152 &self,
153 node: &Expr,
154 subquery_depth: usize,
155 ) -> Result<(), ValidationError> {
156 match node {
157 Expr::And(operands) | Expr::Or(operands) => {
158 for operand in operands {
159 self.validate_node_with_depth(operand, subquery_depth)?;
160 }
161 Ok(())
162 }
163 Expr::Not(operand) => self.validate_node_with_depth(operand, subquery_depth),
164 Expr::Condition(condition) => {
165 self.validate_condition(condition)?;
166 if let Value::Subquery(inner) = &condition.value {
169 let new_depth = subquery_depth + 1;
170 if new_depth > crate::query::types::MAX_SUBQUERY_DEPTH {
171 return Err(ValidationError::SubqueryDepthExceeded {
172 depth: new_depth,
173 max_depth: crate::query::types::MAX_SUBQUERY_DEPTH,
174 span: condition.span.clone(),
175 });
176 }
177 self.validate_node_with_depth(inner, new_depth)?;
178 }
179 Ok(())
180 }
181 Expr::Join(join) => {
182 self.validate_node_with_depth(&join.left, subquery_depth)?;
183 self.validate_node_with_depth(&join.right, subquery_depth)?;
184 Ok(())
185 }
186 }
187 }
188
189 fn validate_condition(&self, condition: &Condition) -> Result<(), ValidationError> {
191 let field_name = condition.field.as_str();
192 let field_desc = self.resolve_field_descriptor(condition)?;
193
194 Self::validate_operator(field_name, field_desc, condition)?;
195 Self::validate_value_type(field_name, field_desc, condition)?;
196 Self::validate_enum_value(field_name, field_desc, condition)?;
197 Self::validate_regex_pattern(condition)?;
198
199 Ok(())
200 }
201
202 fn resolve_field_descriptor<'a>(
203 &'a self,
204 condition: &Condition,
205 ) -> Result<&'a FieldDescriptor, ValidationError> {
206 let field_name = condition.field.as_str();
207 self.registry.get(field_name).ok_or_else(|| {
208 let suggestion = self.suggest_field(field_name);
209 ValidationError::UnknownField {
210 field: field_name.to_string(),
211 suggestion,
212 span: condition.span.clone(),
213 }
214 })
215 }
216
217 fn validate_operator(
218 field_name: &str,
219 field_desc: &FieldDescriptor,
220 condition: &Condition,
221 ) -> Result<(), ValidationError> {
222 if field_desc.supports_operator(&condition.operator) {
223 return Ok(());
224 }
225
226 Err(ValidationError::InvalidOperator {
227 field: field_name.to_string(),
228 operator: condition.operator.clone(),
229 valid_operators: field_desc.operators.to_vec(),
230 span: condition.span.clone(),
231 })
232 }
233
234 fn validate_value_type(
235 field_name: &str,
236 field_desc: &FieldDescriptor,
237 condition: &Condition,
238 ) -> Result<(), ValidationError> {
239 let is_value_type_valid = match (&condition.operator, &condition.value) {
240 (Operator::Regex, Value::Regex(_)) => matches!(
242 field_desc.field_type,
243 FieldType::String | FieldType::Enum(_) | FieldType::Path
244 ),
245 _ => field_desc.matches_value_type(&condition.value),
247 };
248
249 if is_value_type_valid {
250 return Ok(());
251 }
252
253 Err(ValidationError::TypeMismatch {
254 field: field_name.to_string(),
255 expected: field_desc.field_type.clone(),
256 got: condition.value.clone(),
257 span: condition.span.clone(),
258 })
259 }
260
261 fn validate_enum_value(
262 field_name: &str,
263 field_desc: &FieldDescriptor,
264 condition: &Condition,
265 ) -> Result<(), ValidationError> {
266 if let FieldType::Enum(allowed_values) = &field_desc.field_type
267 && let Value::String(value) = &condition.value
268 && !allowed_values.contains(&value.as_str())
269 {
270 return Err(ValidationError::InvalidEnumValue {
271 field: field_name.to_string(),
272 value: value.clone(),
273 valid_values: allowed_values.clone(),
274 span: condition.span.clone(),
275 });
276 }
277
278 Ok(())
279 }
280
281 fn validate_regex_pattern(condition: &Condition) -> Result<(), ValidationError> {
282 let Value::Regex(regex_val) = &condition.value else {
283 return Ok(());
284 };
285
286 let has_lookaround = regex_val.pattern.contains("(?=")
288 || regex_val.pattern.contains("(?!")
289 || regex_val.pattern.contains("(?<=")
290 || regex_val.pattern.contains("(?<!");
291
292 if has_lookaround {
293 if let Err(e) = fancy_regex::Regex::new(®ex_val.pattern) {
295 return Err(ValidationError::InvalidRegexPattern {
296 pattern: regex_val.pattern.clone(),
297 error: e.to_string(),
298 span: condition.span.clone(),
299 });
300 }
301 } else {
302 if let Err(e) = Regex::new(®ex_val.pattern) {
304 return Err(ValidationError::InvalidRegexPattern {
305 pattern: regex_val.pattern.clone(),
306 error: e.to_string(),
307 span: condition.span.clone(),
308 });
309 }
310 }
311
312 Ok(())
313 }
314
315 fn normalize_operands(&self, operands: &[Expr]) -> Result<Vec<Expr>, ValidationError> {
316 let mut normalized = Vec::with_capacity(operands.len());
317 for operand in operands {
318 normalized.push(self.normalize_expr(operand)?);
319 }
320 Ok(normalized)
321 }
322
323 #[allow(clippy::only_used_in_recursion)]
329 #[must_use]
330 pub fn detect_contradictions(&self, expr: &Expr) -> Vec<ContradictionWarning> {
331 let mut warnings = Vec::new();
332
333 if let Expr::And(operands) = expr {
334 warnings.extend(Self::detect_exact_match_contradictions(operands));
335 }
336
337 warnings.extend(self.detect_nested_contradictions(expr));
338
339 warnings
340 }
341
342 fn detect_exact_match_contradictions(operands: &[Expr]) -> Vec<ContradictionWarning> {
343 let constraints = Self::collect_exact_constraints(operands);
344 constraints
345 .into_iter()
346 .filter_map(|(field, values)| {
347 Self::contradiction_for_field(operands, field.as_str(), &values)
348 })
349 .collect()
350 }
351
352 fn detect_nested_contradictions(&self, expr: &Expr) -> Vec<ContradictionWarning> {
353 match expr {
354 Expr::And(operands) | Expr::Or(operands) => operands
355 .iter()
356 .flat_map(|operand| self.detect_contradictions(operand))
357 .collect(),
358 Expr::Not(operand) => self.detect_contradictions(operand),
359 Expr::Condition(_) => Vec::new(),
360 Expr::Join(join) => {
361 let mut warnings = self.detect_contradictions(&join.left);
362 warnings.extend(self.detect_contradictions(&join.right));
363 warnings
364 }
365 }
366 }
367
368 fn collect_exact_constraints(operands: &[Expr]) -> HashMap<String, Vec<(String, usize)>> {
369 let mut constraints: HashMap<String, Vec<(String, usize)>> = HashMap::new();
370
371 for (idx, operand) in operands.iter().enumerate() {
372 if let Expr::Condition(condition) = operand
373 && condition.operator == Operator::Equal
374 {
375 if let Some(value) = condition.value.as_string() {
376 constraints
377 .entry(condition.field.as_str().to_string())
378 .or_default()
379 .push((value.to_string(), idx));
380 } else if let Value::Boolean(value) = &condition.value {
381 constraints
382 .entry(condition.field.as_str().to_string())
383 .or_default()
384 .push((value.to_string(), idx));
385 }
386 }
387 }
388
389 constraints
390 }
391
392 fn contradiction_for_field(
393 operands: &[Expr],
394 field: &str,
395 values: &[(String, usize)],
396 ) -> Option<ContradictionWarning> {
397 if values.len() <= 1 {
398 return None;
399 }
400
401 let unique_values: Vec<_> = values
402 .iter()
403 .map(|(v, _)| v.as_str())
404 .collect::<std::collections::HashSet<_>>()
405 .into_iter()
406 .collect();
407
408 if unique_values.len() <= 1 {
409 return None;
410 }
411
412 let merged_span = Self::merge_operand_spans(operands, values);
413 let value_list = unique_values.join("' and '");
414 Some(ContradictionWarning {
415 message: format!("Query is impossible: field '{field}' cannot be both '{value_list}'"),
416 span: merged_span,
417 })
418 }
419
420 fn merge_operand_spans(operands: &[Expr], values: &[(String, usize)]) -> Span {
421 values
422 .iter()
423 .filter_map(|(_, idx)| match &operands[*idx] {
424 Expr::Condition(cond) => Some(cond.span.clone()),
425 _ => None,
426 })
427 .fold(None, |acc: Option<Span>, span| {
428 Some(acc.map_or(span.clone(), |s| s.merge(&span)))
429 })
430 .unwrap_or_default()
431 }
432
433 fn suggest_field(&self, input: &str) -> Option<String> {
438 self.suggest_field_with_threshold(input, 2)
439 .into_iter()
440 .next()
441 }
442
443 fn suggest_field_with_threshold(&self, input: &str, max_distance: usize) -> Vec<String> {
444 let input_lower = input.to_lowercase();
445 let mut best_match: Option<usize> = None;
446 let mut candidates: Vec<String> = Vec::new();
447
448 for field_name in self.registry.field_names() {
449 if field_name.to_lowercase() == input_lower {
451 return vec![field_name.to_string()];
452 }
453
454 let distance = levenshtein_distance(&input_lower, &field_name.to_lowercase());
456
457 if distance <= max_distance {
459 match best_match {
460 Some(best_dist) if distance < best_dist => {
461 best_match = Some(distance);
462 candidates.clear();
463 candidates.push(field_name.to_string());
464 }
465 Some(best_dist) if distance == best_dist => {
466 candidates.push(field_name.to_string());
467 }
468 None => {
469 best_match = Some(distance);
470 candidates.push(field_name.to_string());
471 }
472 _ => {}
473 }
474 }
475 }
476
477 candidates
478 }
479
480 fn normalize_condition(&self, condition: &Condition) -> Result<Condition, ValidationError> {
481 if self.registry.get(condition.field.as_str()).is_some() {
483 return Ok(condition.clone());
484 }
485
486 if !self.options.fuzzy_fields {
488 return Err(ValidationError::UnknownField {
489 field: condition.field.as_str().to_string(),
490 suggestion: self.suggest_field(condition.field.as_str()),
491 span: condition.span.clone(),
492 });
493 }
494
495 let suggestions = self.suggest_field_with_threshold(
497 condition.field.as_str(),
498 self.options.fuzzy_field_distance,
499 );
500 match suggestions.len() {
501 1 => {
502 let mut corrected = condition.clone();
503 let candidate = suggestions[0].clone();
504
505 if !SAFE_FUZZY_FIELDS.contains(&candidate.as_str()) {
508 return Err(ValidationError::UnsafeFuzzyCorrection {
509 input: condition.field.as_str().to_string(),
510 suggestion: candidate,
511 span: condition.span.clone(),
512 });
513 }
514
515 corrected.field = Field::new(candidate);
516 Ok(corrected)
517 }
518 n if n > 1 => Err(ValidationError::UnknownField {
519 field: condition.field.as_str().to_string(),
520 suggestion: Some(format!("ambiguous: {}", suggestions.join(", "))),
521 span: condition.span.clone(),
522 }),
523 _ => Err(ValidationError::UnknownField {
524 field: condition.field.as_str().to_string(),
525 suggestion: None,
526 span: condition.span.clone(),
527 }),
528 }
529 }
530}
531
532#[derive(Debug, Clone, PartialEq)]
534pub struct ContradictionWarning {
535 pub message: String,
537 pub span: Span,
539}
540
541#[allow(clippy::needless_range_loop)]
546fn levenshtein_distance(s1: &str, s2: &str) -> usize {
547 let len1 = s1.chars().count();
548 let len2 = s2.chars().count();
549
550 let mut matrix = vec![vec![0; len2 + 1]; len1 + 1];
552
553 for i in 0..=len1 {
555 matrix[i][0] = i;
556 }
557 for j in 0..=len2 {
558 matrix[0][j] = j;
559 }
560
561 let s1_chars: Vec<char> = s1.chars().collect();
563 let s2_chars: Vec<char> = s2.chars().collect();
564
565 for (i, c1) in s1_chars.iter().enumerate() {
566 for (j, c2) in s2_chars.iter().enumerate() {
567 let cost = usize::from(c1 != c2);
568
569 matrix[i + 1][j + 1] = std::cmp::min(
570 std::cmp::min(
571 matrix[i][j + 1] + 1, matrix[i + 1][j] + 1, ),
574 matrix[i][j] + cost, );
576 }
577 }
578
579 matrix[len1][len2]
580}
581
582#[cfg(test)]
583mod tests {
584 use super::*;
585 use crate::query::types::{Field, Span};
586
587 #[test]
588 fn test_levenshtein_distance() {
589 assert_eq!(levenshtein_distance("", ""), 0);
590 assert_eq!(levenshtein_distance("hello", "hello"), 0);
591 assert_eq!(levenshtein_distance("hello", "hallo"), 1);
592 assert_eq!(levenshtein_distance("kind", "knd"), 1);
593 assert_eq!(levenshtein_distance("kind", "kond"), 1);
594 assert_eq!(levenshtein_distance("kind", "king"), 1);
595 assert_eq!(levenshtein_distance("kind", "xyz"), 4);
596 }
597
598 #[test]
599 fn test_validate_valid_condition() {
600 let registry = FieldRegistry::with_core_fields();
601 let validator = Validator::new(registry);
602
603 let condition = Expr::Condition(Condition {
604 field: Field::new("kind"),
605 operator: Operator::Equal,
606 value: Value::String("function".to_string()),
607 span: Span::default(),
608 });
609
610 assert!(validator.validate(&condition).is_ok());
611 }
612
613 #[test]
614 fn test_validate_unknown_field() {
615 let registry = FieldRegistry::with_core_fields();
616 let validator = Validator::new(registry);
617
618 let condition = Expr::Condition(Condition {
619 field: Field::new("unknown"),
620 operator: Operator::Equal,
621 value: Value::String("value".to_string()),
622 span: Span::default(),
623 });
624
625 let result = validator.validate(&condition);
626 assert!(result.is_err());
627 assert!(matches!(
628 result.unwrap_err(),
629 ValidationError::UnknownField { .. }
630 ));
631 }
632
633 #[test]
634 fn test_suggest_field_typo() {
635 let registry = FieldRegistry::with_core_fields();
636 let validator = Validator::new(registry);
637
638 let suggestion = validator.suggest_field("knd");
639 assert_eq!(suggestion, Some("kind".to_string()));
640
641 let suggestion = validator.suggest_field("kond");
642 assert_eq!(suggestion, Some("kind".to_string()));
643
644 let suggestion = validator.suggest_field("nme");
645 assert_eq!(suggestion, Some("name".to_string()));
646 }
647
648 #[test]
649 fn test_suggest_field_no_match() {
650 let registry = FieldRegistry::with_core_fields();
651 let validator = Validator::new(registry);
652
653 let suggestion = validator.suggest_field("xyz");
654 assert!(suggestion.is_none());
655
656 let suggestion = validator.suggest_field("foobar");
657 assert!(suggestion.is_none());
658 }
659
660 #[test]
661 fn test_fuzzy_field_correction_enabled() {
662 let registry = FieldRegistry::with_core_fields();
663 let options = ValidationOptions {
664 fuzzy_fields: true,
665 fuzzy_field_distance: 2,
666 };
667 let validator = Validator::with_options(registry, options);
668 let cond = Condition {
669 field: Field::new("knd"),
670 operator: Operator::Equal,
671 value: Value::String("function".to_string()),
672 span: Span::default(),
673 };
674 let normalized = validator
675 .normalize_condition(&cond)
676 .expect("should normalize");
677 assert_eq!(normalized.field.as_str(), "kind");
678 }
679
680 #[test]
681 fn test_fuzzy_field_ambiguous_rejected() {
682 let registry = FieldRegistry::with_core_fields();
683 let options = ValidationOptions {
684 fuzzy_fields: true,
685 fuzzy_field_distance: 2,
686 };
687 let validator = Validator::with_options(registry, options);
688 let cond = Condition {
689 field: Field::new("nam"),
690 operator: Operator::Equal,
691 value: Value::String("foo".to_string()),
692 span: Span::default(),
693 };
694 let result = validator.normalize_condition(&cond);
695 assert!(result.is_err(), "ambiguous correction must error");
696 }
697
698 #[test]
699 fn test_fuzzy_field_disabled_rejects() {
700 let registry = FieldRegistry::with_core_fields();
701 let validator = Validator::new(registry);
702 let cond = Condition {
703 field: Field::new("knd"),
704 operator: Operator::Equal,
705 value: Value::String("function".to_string()),
706 span: Span::default(),
707 };
708 let result = validator.normalize_condition(&cond);
709 assert!(result.is_err(), "disabled fuzzy should reject typos");
710 }
711
712 #[test]
713 fn test_fuzzy_field_non_whitelisted_returns_unsafe_error() {
714 let mut registry = FieldRegistry::with_core_fields();
716 registry.add_field(super::super::types::FieldDescriptor {
717 name: "custom",
718 field_type: FieldType::String,
719 operators: &[Operator::Equal],
720 indexed: false,
721 doc: "A custom field for testing",
722 });
723 let options = ValidationOptions {
724 fuzzy_fields: true,
725 fuzzy_field_distance: 2,
726 };
727 let validator = Validator::with_options(registry, options);
728 let cond = Condition {
730 field: Field::new("custm"),
731 operator: Operator::Equal,
732 value: Value::String("test".to_string()),
733 span: Span::default(),
734 };
735 let result = validator.normalize_condition(&cond);
736 assert!(result.is_err(), "non-whitelisted field should error");
737 assert!(
738 matches!(
739 result.unwrap_err(),
740 ValidationError::UnsafeFuzzyCorrection { .. }
741 ),
742 "should return UnsafeFuzzyCorrection, not UnknownField"
743 );
744 }
745
746 #[test]
747 fn test_suggest_field_case_insensitive() {
748 let registry = FieldRegistry::with_core_fields();
749 let validator = Validator::new(registry);
750
751 let suggestion = validator.suggest_field("KIND");
753 assert_eq!(suggestion, Some("kind".to_string()));
754
755 let suggestion = validator.suggest_field("Name");
756 assert_eq!(suggestion, Some("name".to_string()));
757
758 let suggestion = validator.suggest_field("KND");
760 assert_eq!(suggestion, Some("kind".to_string()));
761 }
762
763 #[test]
764 fn test_validate_invalid_operator() {
765 let registry = FieldRegistry::with_core_fields();
766 let validator = Validator::new(registry);
767
768 let condition = Expr::Condition(Condition {
769 field: Field::new("kind"),
770 operator: Operator::Greater,
771 value: Value::String("function".to_string()),
772 span: Span::default(),
773 });
774
775 let result = validator.validate(&condition);
776 assert!(result.is_err());
777 assert!(matches!(
778 result.unwrap_err(),
779 ValidationError::InvalidOperator { .. }
780 ));
781 }
782
783 #[test]
784 fn test_validate_type_mismatch() {
785 let registry = FieldRegistry::with_core_fields();
786 let _validator = Validator::new(registry);
787
788 let mut registry = FieldRegistry::with_core_fields();
790 registry.add_field(super::super::types::FieldDescriptor {
791 name: "async",
792 field_type: FieldType::Bool,
793 operators: &[Operator::Equal],
794 indexed: false,
795 doc: "Whether function is async",
796 });
797 let validator = Validator::new(registry);
798
799 let condition = Expr::Condition(Condition {
800 field: Field::new("async"),
801 operator: Operator::Equal,
802 value: Value::Number(123),
803 span: Span::default(),
804 });
805
806 let result = validator.validate(&condition);
807 assert!(result.is_err());
808 assert!(matches!(
809 result.unwrap_err(),
810 ValidationError::TypeMismatch { .. }
811 ));
812 }
813
814 #[test]
815 fn test_validate_invalid_enum_value() {
816 let registry = FieldRegistry::with_core_fields();
817 let validator = Validator::new(registry);
818
819 let condition = Expr::Condition(Condition {
820 field: Field::new("kind"),
821 operator: Operator::Equal,
822 value: Value::String("invalid_kind".to_string()),
823 span: Span::default(),
824 });
825
826 let result = validator.validate(&condition);
827 assert!(result.is_err());
828 assert!(matches!(
829 result.unwrap_err(),
830 ValidationError::InvalidEnumValue { .. }
831 ));
832 }
833
834 #[test]
835 fn test_validate_valid_enum_value() {
836 let registry = FieldRegistry::with_core_fields();
837 let validator = Validator::new(registry);
838
839 let valid_kinds = ["function", "method", "class", "struct", "trait"];
840
841 for kind in &valid_kinds {
842 let condition = Expr::Condition(Condition {
843 field: Field::new("kind"),
844 operator: Operator::Equal,
845 value: Value::String((*kind).to_string()),
846 span: Span::default(),
847 });
848
849 assert!(validator.validate(&condition).is_ok());
850 }
851 }
852
853 #[test]
854 fn test_validate_invalid_regex() {
855 let registry = FieldRegistry::with_core_fields();
856 let validator = Validator::new(registry);
857
858 let condition = Expr::Condition(Condition {
859 field: Field::new("name"),
860 operator: Operator::Regex,
861 value: Value::Regex(super::super::types::RegexValue {
862 pattern: "[invalid".to_string(),
863 flags: super::super::types::RegexFlags::default(),
864 }),
865 span: Span::default(),
866 });
867
868 let result = validator.validate(&condition);
869 assert!(result.is_err());
870 assert!(matches!(
871 result.unwrap_err(),
872 ValidationError::InvalidRegexPattern { .. }
873 ));
874 }
875
876 #[test]
877 fn test_validate_valid_regex() {
878 let registry = FieldRegistry::with_core_fields();
879 let validator = Validator::new(registry);
880
881 let condition = Expr::Condition(Condition {
882 field: Field::new("name"),
883 operator: Operator::Regex,
884 value: Value::Regex(super::super::types::RegexValue {
885 pattern: "^test_.*".to_string(),
886 flags: super::super::types::RegexFlags::default(),
887 }),
888 span: Span::default(),
889 });
890
891 assert!(validator.validate(&condition).is_ok());
892 }
893
894 #[test]
895 fn test_detect_contradiction_enum() {
896 let registry = FieldRegistry::with_core_fields();
897 let validator = Validator::new(registry);
898
899 let expr = Expr::And(vec![
900 Expr::Condition(Condition {
901 field: Field::new("kind"),
902 operator: Operator::Equal,
903 value: Value::String("function".to_string()),
904 span: Span::default(),
905 }),
906 Expr::Condition(Condition {
907 field: Field::new("kind"),
908 operator: Operator::Equal,
909 value: Value::String("class".to_string()),
910 span: Span::default(),
911 }),
912 ]);
913
914 let warnings = validator.detect_contradictions(&expr);
915 assert_eq!(warnings.len(), 1);
916 assert!(warnings[0].message.contains("kind"));
917 assert!(warnings[0].message.contains("function"));
918 assert!(warnings[0].message.contains("class"));
919 }
920
921 #[test]
922 fn test_detect_contradiction_boolean() {
923 let mut registry = FieldRegistry::with_core_fields();
924 registry.add_field(super::super::types::FieldDescriptor {
925 name: "async",
926 field_type: FieldType::Bool,
927 operators: &[Operator::Equal],
928 indexed: false,
929 doc: "Whether function is async",
930 });
931 let validator = Validator::new(registry);
932
933 let expr = Expr::And(vec![
934 Expr::Condition(Condition {
935 field: Field::new("async"),
936 operator: Operator::Equal,
937 value: Value::Boolean(true),
938 span: Span::default(),
939 }),
940 Expr::Condition(Condition {
941 field: Field::new("async"),
942 operator: Operator::Equal,
943 value: Value::Boolean(false),
944 span: Span::default(),
945 }),
946 ]);
947
948 let warnings = validator.detect_contradictions(&expr);
949 assert_eq!(warnings.len(), 1);
950 assert!(warnings[0].message.contains("async"));
951 }
952
953 #[test]
954 fn test_no_contradiction_or() {
955 let registry = FieldRegistry::with_core_fields();
956 let validator = Validator::new(registry);
957
958 let expr = Expr::Or(vec![
959 Expr::Condition(Condition {
960 field: Field::new("kind"),
961 operator: Operator::Equal,
962 value: Value::String("function".to_string()),
963 span: Span::default(),
964 }),
965 Expr::Condition(Condition {
966 field: Field::new("kind"),
967 operator: Operator::Equal,
968 value: Value::String("class".to_string()),
969 span: Span::default(),
970 }),
971 ]);
972
973 let warnings = validator.detect_contradictions(&expr);
974 assert_eq!(warnings.len(), 0);
975 }
976
977 #[test]
978 fn test_no_contradiction_different_fields() {
979 let mut registry = FieldRegistry::with_core_fields();
980 registry.add_field(super::super::types::FieldDescriptor {
981 name: "async",
982 field_type: FieldType::Bool,
983 operators: &[Operator::Equal],
984 indexed: false,
985 doc: "Whether function is async",
986 });
987 let validator = Validator::new(registry);
988
989 let expr = Expr::And(vec![
990 Expr::Condition(Condition {
991 field: Field::new("kind"),
992 operator: Operator::Equal,
993 value: Value::String("function".to_string()),
994 span: Span::default(),
995 }),
996 Expr::Condition(Condition {
997 field: Field::new("async"),
998 operator: Operator::Equal,
999 value: Value::Boolean(true),
1000 span: Span::default(),
1001 }),
1002 ]);
1003
1004 let warnings = validator.detect_contradictions(&expr);
1005 assert_eq!(warnings.len(), 0);
1006 }
1007
1008 #[test]
1009 fn test_validate_and_expression() {
1010 let mut registry = FieldRegistry::with_core_fields();
1011 registry.add_field(super::super::types::FieldDescriptor {
1012 name: "async",
1013 field_type: FieldType::Bool,
1014 operators: &[Operator::Equal],
1015 indexed: false,
1016 doc: "Whether function is async",
1017 });
1018 let validator = Validator::new(registry);
1019
1020 let expr = Expr::And(vec![
1021 Expr::Condition(Condition {
1022 field: Field::new("kind"),
1023 operator: Operator::Equal,
1024 value: Value::String("function".to_string()),
1025 span: Span::default(),
1026 }),
1027 Expr::Condition(Condition {
1028 field: Field::new("async"),
1029 operator: Operator::Equal,
1030 value: Value::Boolean(true),
1031 span: Span::default(),
1032 }),
1033 ]);
1034
1035 assert!(validator.validate(&expr).is_ok());
1036 }
1037
1038 #[test]
1039 fn test_validate_or_expression() {
1040 let registry = FieldRegistry::with_core_fields();
1041 let validator = Validator::new(registry);
1042
1043 let expr = Expr::Or(vec![
1044 Expr::Condition(Condition {
1045 field: Field::new("kind"),
1046 operator: Operator::Equal,
1047 value: Value::String("function".to_string()),
1048 span: Span::default(),
1049 }),
1050 Expr::Condition(Condition {
1051 field: Field::new("kind"),
1052 operator: Operator::Equal,
1053 value: Value::String("class".to_string()),
1054 span: Span::default(),
1055 }),
1056 ]);
1057
1058 assert!(validator.validate(&expr).is_ok());
1059 }
1060
1061 #[test]
1062 fn test_validate_not_expression() {
1063 let registry = FieldRegistry::with_core_fields();
1064 let validator = Validator::new(registry);
1065
1066 let expr = Expr::Not(Box::new(Expr::Condition(Condition {
1067 field: Field::new("kind"),
1068 operator: Operator::Equal,
1069 value: Value::String("function".to_string()),
1070 span: Span::default(),
1071 })));
1072
1073 assert!(validator.validate(&expr).is_ok());
1074 }
1075
1076 #[test]
1077 fn test_validate_nested_expression() {
1078 let mut registry = FieldRegistry::with_core_fields();
1079 registry.add_field(super::super::types::FieldDescriptor {
1080 name: "async",
1081 field_type: FieldType::Bool,
1082 operators: &[Operator::Equal],
1083 indexed: false,
1084 doc: "Whether function is async",
1085 });
1086 let validator = Validator::new(registry);
1087
1088 let expr = Expr::And(vec![
1089 Expr::Or(vec![
1090 Expr::Condition(Condition {
1091 field: Field::new("kind"),
1092 operator: Operator::Equal,
1093 value: Value::String("function".to_string()),
1094 span: Span::default(),
1095 }),
1096 Expr::Condition(Condition {
1097 field: Field::new("kind"),
1098 operator: Operator::Equal,
1099 value: Value::String("method".to_string()),
1100 span: Span::default(),
1101 }),
1102 ]),
1103 Expr::Condition(Condition {
1104 field: Field::new("async"),
1105 operator: Operator::Equal,
1106 value: Value::Boolean(true),
1107 span: Span::default(),
1108 }),
1109 ]);
1110
1111 assert!(validator.validate(&expr).is_ok());
1112 }
1113
1114 #[test]
1115 fn test_detect_nested_contradiction() {
1116 let registry = FieldRegistry::with_core_fields();
1117 let validator = Validator::new(registry);
1118
1119 let expr = Expr::Or(vec![
1122 Expr::And(vec![
1123 Expr::Condition(Condition {
1124 field: Field::new("kind"),
1125 operator: Operator::Equal,
1126 value: Value::String("function".to_string()),
1127 span: Span::default(),
1128 }),
1129 Expr::Condition(Condition {
1130 field: Field::new("kind"),
1131 operator: Operator::Equal,
1132 value: Value::String("class".to_string()),
1133 span: Span::default(),
1134 }),
1135 ]),
1136 Expr::Condition(Condition {
1137 field: Field::new("name"),
1138 operator: Operator::Equal,
1139 value: Value::String("test".to_string()),
1140 span: Span::default(),
1141 }),
1142 ]);
1143
1144 let warnings = validator.detect_contradictions(&expr);
1145 assert_eq!(warnings.len(), 1);
1146 assert!(warnings[0].message.contains("kind"));
1147 assert!(warnings[0].message.contains("function"));
1148 assert!(warnings[0].message.contains("class"));
1149 }
1150
1151 #[test]
1152 fn test_contradiction_warning_has_span() {
1153 let registry = FieldRegistry::with_core_fields();
1154 let validator = Validator::new(registry);
1155
1156 let expr = Expr::And(vec![
1157 Expr::Condition(Condition {
1158 field: Field::new("kind"),
1159 operator: Operator::Equal,
1160 value: Value::String("function".to_string()),
1161 span: Span::with_position(0, 13, 1, 1),
1162 }),
1163 Expr::Condition(Condition {
1164 field: Field::new("kind"),
1165 operator: Operator::Equal,
1166 value: Value::String("class".to_string()),
1167 span: Span::with_position(18, 28, 1, 19),
1168 }),
1169 ]);
1170
1171 let warnings = validator.detect_contradictions(&expr);
1172 assert_eq!(warnings.len(), 1);
1173
1174 assert_eq!(warnings[0].span.start, 0);
1176 assert_eq!(warnings[0].span.end, 28);
1177 }
1178
1179 fn build_nested_subquery(depth: usize) -> Expr {
1189 let mut expr = Expr::Condition(Condition {
1190 field: Field::new("kind"),
1191 operator: Operator::Equal,
1192 value: Value::String("function".to_string()),
1193 span: Span::default(),
1194 });
1195 for _ in 0..depth {
1196 expr = Expr::Condition(Condition {
1197 field: Field::new("callers"),
1198 operator: Operator::Equal,
1199 value: Value::Subquery(Box::new(expr)),
1200 span: Span::default(),
1201 });
1202 }
1203 expr
1204 }
1205
1206 #[test]
1207 fn test_subquery_depth_at_max_succeeds() {
1208 let registry = FieldRegistry::with_core_fields();
1209 let validator = Validator::new(registry);
1210
1211 let expr = build_nested_subquery(crate::query::types::MAX_SUBQUERY_DEPTH);
1213 assert!(
1214 validator.validate(&expr).is_ok(),
1215 "subquery at exactly MAX_SUBQUERY_DEPTH should be valid"
1216 );
1217 }
1218
1219 #[test]
1220 fn test_subquery_depth_exceeds_max_fails() {
1221 let registry = FieldRegistry::with_core_fields();
1222 let validator = Validator::new(registry);
1223
1224 let expr = build_nested_subquery(crate::query::types::MAX_SUBQUERY_DEPTH + 1);
1226 let result = validator.validate(&expr);
1227 assert!(
1228 result.is_err(),
1229 "subquery beyond MAX_SUBQUERY_DEPTH should fail"
1230 );
1231 assert!(
1232 matches!(
1233 result.unwrap_err(),
1234 ValidationError::SubqueryDepthExceeded { .. }
1235 ),
1236 "error should be SubqueryDepthExceeded"
1237 );
1238 }
1239}