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