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 let normalized_value = match &condition.value {
498 Value::Subquery(inner) => Value::Subquery(Box::new(self.normalize_expr(inner)?)),
499 other => other.clone(),
500 };
501
502 let field_name = condition.field.as_str();
511 let canonical = self
512 .registry
513 .resolve_canonical(field_name)
514 .map(std::string::ToString::to_string);
515 if let Some(canonical) = canonical {
516 let mut resolved = condition.clone();
517 if canonical != field_name {
518 resolved.field = Field::new(canonical);
519 }
520 resolved.value = normalized_value;
521 return Ok(resolved);
522 }
523
524 if !self.options.fuzzy_fields {
526 return Err(ValidationError::UnknownField {
527 field: condition.field.as_str().to_string(),
528 suggestion: self.suggest_field(condition.field.as_str()),
529 span: condition.span.clone(),
530 });
531 }
532
533 let suggestions = self.suggest_field_with_threshold(
535 condition.field.as_str(),
536 self.options.fuzzy_field_distance,
537 );
538 match suggestions.len() {
539 1 => {
540 let mut corrected = condition.clone();
541 let candidate = suggestions[0].clone();
542
543 if !SAFE_FUZZY_FIELDS.contains(&candidate.as_str()) {
546 return Err(ValidationError::UnsafeFuzzyCorrection {
547 input: condition.field.as_str().to_string(),
548 suggestion: candidate,
549 span: condition.span.clone(),
550 });
551 }
552
553 corrected.field = Field::new(candidate);
554 corrected.value = normalized_value;
555 Ok(corrected)
556 }
557 n if n > 1 => Err(ValidationError::UnknownField {
558 field: condition.field.as_str().to_string(),
559 suggestion: Some(format!("ambiguous: {}", suggestions.join(", "))),
560 span: condition.span.clone(),
561 }),
562 _ => Err(ValidationError::UnknownField {
563 field: condition.field.as_str().to_string(),
564 suggestion: None,
565 span: condition.span.clone(),
566 }),
567 }
568 }
569}
570
571#[derive(Debug, Clone, PartialEq)]
573pub struct ContradictionWarning {
574 pub message: String,
576 pub span: Span,
578}
579
580#[allow(clippy::needless_range_loop)]
585fn levenshtein_distance(s1: &str, s2: &str) -> usize {
586 let len1 = s1.chars().count();
587 let len2 = s2.chars().count();
588
589 let mut matrix = vec![vec![0; len2 + 1]; len1 + 1];
591
592 for i in 0..=len1 {
594 matrix[i][0] = i;
595 }
596 for j in 0..=len2 {
597 matrix[0][j] = j;
598 }
599
600 let s1_chars: Vec<char> = s1.chars().collect();
602 let s2_chars: Vec<char> = s2.chars().collect();
603
604 for (i, c1) in s1_chars.iter().enumerate() {
605 for (j, c2) in s2_chars.iter().enumerate() {
606 let cost = usize::from(c1 != c2);
607
608 matrix[i + 1][j + 1] = std::cmp::min(
609 std::cmp::min(
610 matrix[i][j + 1] + 1, matrix[i + 1][j] + 1, ),
613 matrix[i][j] + cost, );
615 }
616 }
617
618 matrix[len1][len2]
619}
620
621#[cfg(test)]
622mod tests {
623 use super::*;
624 use crate::query::types::{Field, Span};
625
626 #[test]
627 fn test_levenshtein_distance() {
628 assert_eq!(levenshtein_distance("", ""), 0);
629 assert_eq!(levenshtein_distance("hello", "hello"), 0);
630 assert_eq!(levenshtein_distance("hello", "hallo"), 1);
631 assert_eq!(levenshtein_distance("kind", "knd"), 1);
632 assert_eq!(levenshtein_distance("kind", "kond"), 1);
633 assert_eq!(levenshtein_distance("kind", "king"), 1);
634 assert_eq!(levenshtein_distance("kind", "xyz"), 4);
635 }
636
637 #[test]
638 fn test_validate_valid_condition() {
639 let registry = FieldRegistry::with_core_fields();
640 let validator = Validator::new(registry);
641
642 let condition = Expr::Condition(Condition {
643 field: Field::new("kind"),
644 operator: Operator::Equal,
645 value: Value::String("function".to_string()),
646 span: Span::default(),
647 });
648
649 assert!(validator.validate(&condition).is_ok());
650 }
651
652 #[test]
653 fn test_validate_unknown_field() {
654 let registry = FieldRegistry::with_core_fields();
655 let validator = Validator::new(registry);
656
657 let condition = Expr::Condition(Condition {
658 field: Field::new("unknown"),
659 operator: Operator::Equal,
660 value: Value::String("value".to_string()),
661 span: Span::default(),
662 });
663
664 let result = validator.validate(&condition);
665 assert!(result.is_err());
666 assert!(matches!(
667 result.unwrap_err(),
668 ValidationError::UnknownField { .. }
669 ));
670 }
671
672 #[test]
677 fn test_normalize_resolves_file_alias_to_path() {
678 let registry = FieldRegistry::with_core_fields();
679 let validator = Validator::new(registry);
680
681 let file_expr = Expr::Condition(Condition {
682 field: Field::new("file"),
683 operator: Operator::Equal,
684 value: Value::String("crates/**".to_string()),
685 span: Span::default(),
686 });
687 let normalized = validator.normalize_expr(&file_expr).unwrap();
688 let Expr::Condition(cond) = normalized else {
689 panic!("expected condition");
690 };
691 assert_eq!(cond.field.as_str(), "path");
692 assert!(matches!(cond.value, Value::String(ref s) if s == "crates/**"));
693 assert_eq!(cond.operator, Operator::Equal);
694 }
695
696 #[test]
699 fn test_normalize_file_and_path_are_equivalent() {
700 let registry = FieldRegistry::with_core_fields();
701 let validator = Validator::new(registry);
702
703 let make = |field: &str| {
704 Expr::Condition(Condition {
705 field: Field::new(field),
706 operator: Operator::Equal,
707 value: Value::String("src/**/*.rs".to_string()),
708 span: Span::default(),
709 })
710 };
711
712 let from_file = validator.normalize_expr(&make("file")).unwrap();
713 let from_path = validator.normalize_expr(&make("path")).unwrap();
714
715 let (Expr::Condition(file_cond), Expr::Condition(path_cond)) = (from_file, from_path)
716 else {
717 panic!("expected conditions");
718 };
719 assert_eq!(file_cond.field.as_str(), path_cond.field.as_str());
720 assert_eq!(file_cond.field.as_str(), "path");
721 assert_eq!(file_cond.operator, path_cond.operator);
722 assert!(matches!((&file_cond.value, &path_cond.value),
723 (Value::String(a), Value::String(b)) if a == b));
724
725 assert!(validator.validate(&Expr::Condition(file_cond)).is_ok());
727 assert!(validator.validate(&Expr::Condition(path_cond)).is_ok());
728 }
729
730 #[test]
733 fn test_normalize_language_alias_and_nested() {
734 let registry = FieldRegistry::with_core_fields();
735 let validator = Validator::new(registry);
736
737 let expr = Expr::And(vec![
738 Expr::Condition(Condition {
739 field: Field::new("kind"),
740 operator: Operator::Equal,
741 value: Value::String("struct".to_string()),
742 span: Span::default(),
743 }),
744 Expr::Condition(Condition {
745 field: Field::new("file"),
746 operator: Operator::Equal,
747 value: Value::String("crates/**".to_string()),
748 span: Span::default(),
749 }),
750 Expr::Condition(Condition {
751 field: Field::new("language"),
752 operator: Operator::Equal,
753 value: Value::String("rust".to_string()),
754 span: Span::default(),
755 }),
756 ]);
757
758 let Expr::And(operands) = validator.normalize_expr(&expr).unwrap() else {
759 panic!("expected And");
760 };
761 let fields: Vec<&str> = operands
762 .iter()
763 .map(|op| match op {
764 Expr::Condition(c) => c.field.as_str(),
765 _ => panic!("expected condition"),
766 })
767 .collect();
768 assert_eq!(fields, vec!["kind", "path", "lang"]);
769 }
770
771 #[test]
777 fn test_normalize_resolves_file_alias_inside_subquery() {
778 let registry = FieldRegistry::with_core_fields();
779 let validator = Validator::new(registry);
780
781 let relation_with = |inner_field: &str| {
782 Expr::Condition(Condition {
783 field: Field::new("callers"),
784 operator: Operator::Equal,
785 value: Value::Subquery(Box::new(Expr::Condition(Condition {
786 field: Field::new(inner_field),
787 operator: Operator::Equal,
788 value: Value::String("main.rs".to_string()),
789 span: Span::default(),
790 }))),
791 span: Span::default(),
792 })
793 };
794
795 let from_file = validator.normalize_expr(&relation_with("file")).unwrap();
796 let from_path = validator.normalize_expr(&relation_with("path")).unwrap();
797
798 assert_eq!(from_file, from_path);
801
802 let Expr::Condition(cond) = from_file else {
803 panic!("expected condition");
804 };
805 let Value::Subquery(inner) = cond.value else {
806 panic!("expected subquery value");
807 };
808 let Expr::Condition(inner_cond) = *inner else {
809 panic!("expected inner condition");
810 };
811 assert_eq!(inner_cond.field.as_str(), "path");
812 }
813
814 #[test]
817 fn test_normalize_resolves_file_alias_in_nested_boolean_subquery() {
818 let registry = FieldRegistry::with_core_fields();
819 let validator = Validator::new(registry);
820
821 let make_leaf = |value: &str| {
822 Expr::Condition(Condition {
823 field: Field::new("file"),
824 operator: Operator::Equal,
825 value: Value::String(value.to_string()),
826 span: Span::default(),
827 })
828 };
829 let expr = Expr::Condition(Condition {
830 field: Field::new("callers"),
831 operator: Operator::Equal,
832 value: Value::Subquery(Box::new(Expr::Or(vec![
833 make_leaf("a.rs"),
834 make_leaf("b.rs"),
835 ]))),
836 span: Span::default(),
837 });
838
839 let Expr::Condition(cond) = validator.normalize_expr(&expr).unwrap() else {
840 panic!("expected condition");
841 };
842 let Value::Subquery(inner) = cond.value else {
843 panic!("expected subquery value");
844 };
845 let Expr::Or(operands) = *inner else {
846 panic!("expected Or inside subquery");
847 };
848 for op in operands {
849 let Expr::Condition(leaf) = op else {
850 panic!("expected condition leaf");
851 };
852 assert_eq!(leaf.field.as_str(), "path");
853 }
854 }
855
856 #[test]
857 fn test_suggest_field_typo() {
858 let registry = FieldRegistry::with_core_fields();
859 let validator = Validator::new(registry);
860
861 let suggestion = validator.suggest_field("knd");
862 assert_eq!(suggestion, Some("kind".to_string()));
863
864 let suggestion = validator.suggest_field("kond");
865 assert_eq!(suggestion, Some("kind".to_string()));
866
867 let suggestion = validator.suggest_field("nme");
868 assert_eq!(suggestion, Some("name".to_string()));
869 }
870
871 #[test]
872 fn test_suggest_field_no_match() {
873 let registry = FieldRegistry::with_core_fields();
874 let validator = Validator::new(registry);
875
876 let suggestion = validator.suggest_field("xyz");
877 assert!(suggestion.is_none());
878
879 let suggestion = validator.suggest_field("foobar");
880 assert!(suggestion.is_none());
881 }
882
883 #[test]
884 fn test_fuzzy_field_correction_enabled() {
885 let registry = FieldRegistry::with_core_fields();
886 let options = ValidationOptions {
887 fuzzy_fields: true,
888 fuzzy_field_distance: 2,
889 };
890 let validator = Validator::with_options(registry, options);
891 let cond = Condition {
892 field: Field::new("knd"),
893 operator: Operator::Equal,
894 value: Value::String("function".to_string()),
895 span: Span::default(),
896 };
897 let normalized = validator
898 .normalize_condition(&cond)
899 .expect("should normalize");
900 assert_eq!(normalized.field.as_str(), "kind");
901 }
902
903 #[test]
904 fn test_fuzzy_field_ambiguous_rejected() {
905 let registry = FieldRegistry::with_core_fields();
906 let options = ValidationOptions {
907 fuzzy_fields: true,
908 fuzzy_field_distance: 2,
909 };
910 let validator = Validator::with_options(registry, options);
911 let cond = Condition {
912 field: Field::new("nam"),
913 operator: Operator::Equal,
914 value: Value::String("foo".to_string()),
915 span: Span::default(),
916 };
917 let result = validator.normalize_condition(&cond);
918 assert!(result.is_err(), "ambiguous correction must error");
919 }
920
921 #[test]
922 fn test_fuzzy_field_disabled_rejects() {
923 let registry = FieldRegistry::with_core_fields();
924 let validator = Validator::new(registry);
925 let cond = Condition {
926 field: Field::new("knd"),
927 operator: Operator::Equal,
928 value: Value::String("function".to_string()),
929 span: Span::default(),
930 };
931 let result = validator.normalize_condition(&cond);
932 assert!(result.is_err(), "disabled fuzzy should reject typos");
933 }
934
935 #[test]
936 fn test_fuzzy_field_non_whitelisted_returns_unsafe_error() {
937 let mut registry = FieldRegistry::with_core_fields();
939 registry.add_field(super::super::types::FieldDescriptor {
940 name: "custom",
941 field_type: FieldType::String,
942 operators: &[Operator::Equal],
943 indexed: false,
944 doc: "A custom field for testing",
945 });
946 let options = ValidationOptions {
947 fuzzy_fields: true,
948 fuzzy_field_distance: 2,
949 };
950 let validator = Validator::with_options(registry, options);
951 let cond = Condition {
953 field: Field::new("custm"),
954 operator: Operator::Equal,
955 value: Value::String("test".to_string()),
956 span: Span::default(),
957 };
958 let result = validator.normalize_condition(&cond);
959 assert!(result.is_err(), "non-whitelisted field should error");
960 assert!(
961 matches!(
962 result.unwrap_err(),
963 ValidationError::UnsafeFuzzyCorrection { .. }
964 ),
965 "should return UnsafeFuzzyCorrection, not UnknownField"
966 );
967 }
968
969 #[test]
970 fn test_suggest_field_case_insensitive() {
971 let registry = FieldRegistry::with_core_fields();
972 let validator = Validator::new(registry);
973
974 let suggestion = validator.suggest_field("KIND");
976 assert_eq!(suggestion, Some("kind".to_string()));
977
978 let suggestion = validator.suggest_field("Name");
979 assert_eq!(suggestion, Some("name".to_string()));
980
981 let suggestion = validator.suggest_field("KND");
983 assert_eq!(suggestion, Some("kind".to_string()));
984 }
985
986 #[test]
987 fn test_validate_invalid_operator() {
988 let registry = FieldRegistry::with_core_fields();
989 let validator = Validator::new(registry);
990
991 let condition = Expr::Condition(Condition {
992 field: Field::new("kind"),
993 operator: Operator::Greater,
994 value: Value::String("function".to_string()),
995 span: Span::default(),
996 });
997
998 let result = validator.validate(&condition);
999 assert!(result.is_err());
1000 assert!(matches!(
1001 result.unwrap_err(),
1002 ValidationError::InvalidOperator { .. }
1003 ));
1004 }
1005
1006 #[test]
1007 fn test_validate_type_mismatch() {
1008 let registry = FieldRegistry::with_core_fields();
1009 let _validator = Validator::new(registry);
1010
1011 let mut registry = FieldRegistry::with_core_fields();
1013 registry.add_field(super::super::types::FieldDescriptor {
1014 name: "async",
1015 field_type: FieldType::Bool,
1016 operators: &[Operator::Equal],
1017 indexed: false,
1018 doc: "Whether function is async",
1019 });
1020 let validator = Validator::new(registry);
1021
1022 let condition = Expr::Condition(Condition {
1023 field: Field::new("async"),
1024 operator: Operator::Equal,
1025 value: Value::Number(123),
1026 span: Span::default(),
1027 });
1028
1029 let result = validator.validate(&condition);
1030 assert!(result.is_err());
1031 assert!(matches!(
1032 result.unwrap_err(),
1033 ValidationError::TypeMismatch { .. }
1034 ));
1035 }
1036
1037 #[test]
1038 fn test_validate_invalid_enum_value() {
1039 let registry = FieldRegistry::with_core_fields();
1040 let validator = Validator::new(registry);
1041
1042 let condition = Expr::Condition(Condition {
1043 field: Field::new("kind"),
1044 operator: Operator::Equal,
1045 value: Value::String("invalid_kind".to_string()),
1046 span: Span::default(),
1047 });
1048
1049 let result = validator.validate(&condition);
1050 assert!(result.is_err());
1051 assert!(matches!(
1052 result.unwrap_err(),
1053 ValidationError::InvalidEnumValue { .. }
1054 ));
1055 }
1056
1057 #[test]
1058 fn test_validate_valid_enum_value() {
1059 let registry = FieldRegistry::with_core_fields();
1060 let validator = Validator::new(registry);
1061
1062 let valid_kinds = ["function", "method", "class", "struct", "trait"];
1063
1064 for kind in &valid_kinds {
1065 let condition = Expr::Condition(Condition {
1066 field: Field::new("kind"),
1067 operator: Operator::Equal,
1068 value: Value::String((*kind).to_string()),
1069 span: Span::default(),
1070 });
1071
1072 assert!(validator.validate(&condition).is_ok());
1073 }
1074 }
1075
1076 #[test]
1077 fn test_validate_invalid_regex() {
1078 let registry = FieldRegistry::with_core_fields();
1079 let validator = Validator::new(registry);
1080
1081 let condition = Expr::Condition(Condition {
1082 field: Field::new("name"),
1083 operator: Operator::Regex,
1084 value: Value::Regex(super::super::types::RegexValue {
1085 pattern: "[invalid".to_string(),
1086 flags: super::super::types::RegexFlags::default(),
1087 }),
1088 span: Span::default(),
1089 });
1090
1091 let result = validator.validate(&condition);
1092 assert!(result.is_err());
1093 assert!(matches!(
1094 result.unwrap_err(),
1095 ValidationError::InvalidRegexPattern { .. }
1096 ));
1097 }
1098
1099 #[test]
1100 fn test_validate_valid_regex() {
1101 let registry = FieldRegistry::with_core_fields();
1102 let validator = Validator::new(registry);
1103
1104 let condition = Expr::Condition(Condition {
1105 field: Field::new("name"),
1106 operator: Operator::Regex,
1107 value: Value::Regex(super::super::types::RegexValue {
1108 pattern: "^test_.*".to_string(),
1109 flags: super::super::types::RegexFlags::default(),
1110 }),
1111 span: Span::default(),
1112 });
1113
1114 assert!(validator.validate(&condition).is_ok());
1115 }
1116
1117 #[test]
1118 fn test_detect_contradiction_enum() {
1119 let registry = FieldRegistry::with_core_fields();
1120 let validator = Validator::new(registry);
1121
1122 let expr = 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
1137 let warnings = validator.detect_contradictions(&expr);
1138 assert_eq!(warnings.len(), 1);
1139 assert!(warnings[0].message.contains("kind"));
1140 assert!(warnings[0].message.contains("function"));
1141 assert!(warnings[0].message.contains("class"));
1142 }
1143
1144 #[test]
1145 fn test_detect_contradiction_boolean() {
1146 let mut registry = FieldRegistry::with_core_fields();
1147 registry.add_field(super::super::types::FieldDescriptor {
1148 name: "async",
1149 field_type: FieldType::Bool,
1150 operators: &[Operator::Equal],
1151 indexed: false,
1152 doc: "Whether function is async",
1153 });
1154 let validator = Validator::new(registry);
1155
1156 let expr = Expr::And(vec![
1157 Expr::Condition(Condition {
1158 field: Field::new("async"),
1159 operator: Operator::Equal,
1160 value: Value::Boolean(true),
1161 span: Span::default(),
1162 }),
1163 Expr::Condition(Condition {
1164 field: Field::new("async"),
1165 operator: Operator::Equal,
1166 value: Value::Boolean(false),
1167 span: Span::default(),
1168 }),
1169 ]);
1170
1171 let warnings = validator.detect_contradictions(&expr);
1172 assert_eq!(warnings.len(), 1);
1173 assert!(warnings[0].message.contains("async"));
1174 }
1175
1176 #[test]
1177 fn test_no_contradiction_or() {
1178 let registry = FieldRegistry::with_core_fields();
1179 let validator = Validator::new(registry);
1180
1181 let expr = Expr::Or(vec![
1182 Expr::Condition(Condition {
1183 field: Field::new("kind"),
1184 operator: Operator::Equal,
1185 value: Value::String("function".to_string()),
1186 span: Span::default(),
1187 }),
1188 Expr::Condition(Condition {
1189 field: Field::new("kind"),
1190 operator: Operator::Equal,
1191 value: Value::String("class".to_string()),
1192 span: Span::default(),
1193 }),
1194 ]);
1195
1196 let warnings = validator.detect_contradictions(&expr);
1197 assert_eq!(warnings.len(), 0);
1198 }
1199
1200 #[test]
1201 fn test_no_contradiction_different_fields() {
1202 let mut registry = FieldRegistry::with_core_fields();
1203 registry.add_field(super::super::types::FieldDescriptor {
1204 name: "async",
1205 field_type: FieldType::Bool,
1206 operators: &[Operator::Equal],
1207 indexed: false,
1208 doc: "Whether function is async",
1209 });
1210 let validator = Validator::new(registry);
1211
1212 let expr = Expr::And(vec![
1213 Expr::Condition(Condition {
1214 field: Field::new("kind"),
1215 operator: Operator::Equal,
1216 value: Value::String("function".to_string()),
1217 span: Span::default(),
1218 }),
1219 Expr::Condition(Condition {
1220 field: Field::new("async"),
1221 operator: Operator::Equal,
1222 value: Value::Boolean(true),
1223 span: Span::default(),
1224 }),
1225 ]);
1226
1227 let warnings = validator.detect_contradictions(&expr);
1228 assert_eq!(warnings.len(), 0);
1229 }
1230
1231 #[test]
1232 fn test_validate_and_expression() {
1233 let mut registry = FieldRegistry::with_core_fields();
1234 registry.add_field(super::super::types::FieldDescriptor {
1235 name: "async",
1236 field_type: FieldType::Bool,
1237 operators: &[Operator::Equal],
1238 indexed: false,
1239 doc: "Whether function is async",
1240 });
1241 let validator = Validator::new(registry);
1242
1243 let expr = Expr::And(vec![
1244 Expr::Condition(Condition {
1245 field: Field::new("kind"),
1246 operator: Operator::Equal,
1247 value: Value::String("function".to_string()),
1248 span: Span::default(),
1249 }),
1250 Expr::Condition(Condition {
1251 field: Field::new("async"),
1252 operator: Operator::Equal,
1253 value: Value::Boolean(true),
1254 span: Span::default(),
1255 }),
1256 ]);
1257
1258 assert!(validator.validate(&expr).is_ok());
1259 }
1260
1261 #[test]
1262 fn test_validate_or_expression() {
1263 let registry = FieldRegistry::with_core_fields();
1264 let validator = Validator::new(registry);
1265
1266 let expr = Expr::Or(vec![
1267 Expr::Condition(Condition {
1268 field: Field::new("kind"),
1269 operator: Operator::Equal,
1270 value: Value::String("function".to_string()),
1271 span: Span::default(),
1272 }),
1273 Expr::Condition(Condition {
1274 field: Field::new("kind"),
1275 operator: Operator::Equal,
1276 value: Value::String("class".to_string()),
1277 span: Span::default(),
1278 }),
1279 ]);
1280
1281 assert!(validator.validate(&expr).is_ok());
1282 }
1283
1284 #[test]
1285 fn test_validate_not_expression() {
1286 let registry = FieldRegistry::with_core_fields();
1287 let validator = Validator::new(registry);
1288
1289 let expr = Expr::Not(Box::new(Expr::Condition(Condition {
1290 field: Field::new("kind"),
1291 operator: Operator::Equal,
1292 value: Value::String("function".to_string()),
1293 span: Span::default(),
1294 })));
1295
1296 assert!(validator.validate(&expr).is_ok());
1297 }
1298
1299 #[test]
1300 fn test_validate_nested_expression() {
1301 let mut registry = FieldRegistry::with_core_fields();
1302 registry.add_field(super::super::types::FieldDescriptor {
1303 name: "async",
1304 field_type: FieldType::Bool,
1305 operators: &[Operator::Equal],
1306 indexed: false,
1307 doc: "Whether function is async",
1308 });
1309 let validator = Validator::new(registry);
1310
1311 let expr = Expr::And(vec![
1312 Expr::Or(vec![
1313 Expr::Condition(Condition {
1314 field: Field::new("kind"),
1315 operator: Operator::Equal,
1316 value: Value::String("function".to_string()),
1317 span: Span::default(),
1318 }),
1319 Expr::Condition(Condition {
1320 field: Field::new("kind"),
1321 operator: Operator::Equal,
1322 value: Value::String("method".to_string()),
1323 span: Span::default(),
1324 }),
1325 ]),
1326 Expr::Condition(Condition {
1327 field: Field::new("async"),
1328 operator: Operator::Equal,
1329 value: Value::Boolean(true),
1330 span: Span::default(),
1331 }),
1332 ]);
1333
1334 assert!(validator.validate(&expr).is_ok());
1335 }
1336
1337 #[test]
1338 fn test_detect_nested_contradiction() {
1339 let registry = FieldRegistry::with_core_fields();
1340 let validator = Validator::new(registry);
1341
1342 let expr = Expr::Or(vec![
1345 Expr::And(vec![
1346 Expr::Condition(Condition {
1347 field: Field::new("kind"),
1348 operator: Operator::Equal,
1349 value: Value::String("function".to_string()),
1350 span: Span::default(),
1351 }),
1352 Expr::Condition(Condition {
1353 field: Field::new("kind"),
1354 operator: Operator::Equal,
1355 value: Value::String("class".to_string()),
1356 span: Span::default(),
1357 }),
1358 ]),
1359 Expr::Condition(Condition {
1360 field: Field::new("name"),
1361 operator: Operator::Equal,
1362 value: Value::String("test".to_string()),
1363 span: Span::default(),
1364 }),
1365 ]);
1366
1367 let warnings = validator.detect_contradictions(&expr);
1368 assert_eq!(warnings.len(), 1);
1369 assert!(warnings[0].message.contains("kind"));
1370 assert!(warnings[0].message.contains("function"));
1371 assert!(warnings[0].message.contains("class"));
1372 }
1373
1374 #[test]
1375 fn test_contradiction_warning_has_span() {
1376 let registry = FieldRegistry::with_core_fields();
1377 let validator = Validator::new(registry);
1378
1379 let expr = Expr::And(vec![
1380 Expr::Condition(Condition {
1381 field: Field::new("kind"),
1382 operator: Operator::Equal,
1383 value: Value::String("function".to_string()),
1384 span: Span::with_position(0, 13, 1, 1),
1385 }),
1386 Expr::Condition(Condition {
1387 field: Field::new("kind"),
1388 operator: Operator::Equal,
1389 value: Value::String("class".to_string()),
1390 span: Span::with_position(18, 28, 1, 19),
1391 }),
1392 ]);
1393
1394 let warnings = validator.detect_contradictions(&expr);
1395 assert_eq!(warnings.len(), 1);
1396
1397 assert_eq!(warnings[0].span.start, 0);
1399 assert_eq!(warnings[0].span.end, 28);
1400 }
1401
1402 fn build_nested_subquery(depth: usize) -> Expr {
1412 let mut expr = Expr::Condition(Condition {
1413 field: Field::new("kind"),
1414 operator: Operator::Equal,
1415 value: Value::String("function".to_string()),
1416 span: Span::default(),
1417 });
1418 for _ in 0..depth {
1419 expr = Expr::Condition(Condition {
1420 field: Field::new("callers"),
1421 operator: Operator::Equal,
1422 value: Value::Subquery(Box::new(expr)),
1423 span: Span::default(),
1424 });
1425 }
1426 expr
1427 }
1428
1429 #[test]
1430 fn test_subquery_depth_at_max_succeeds() {
1431 let registry = FieldRegistry::with_core_fields();
1432 let validator = Validator::new(registry);
1433
1434 let expr = build_nested_subquery(crate::query::types::MAX_SUBQUERY_DEPTH);
1436 assert!(
1437 validator.validate(&expr).is_ok(),
1438 "subquery at exactly MAX_SUBQUERY_DEPTH should be valid"
1439 );
1440 }
1441
1442 #[test]
1443 fn test_subquery_depth_exceeds_max_fails() {
1444 let registry = FieldRegistry::with_core_fields();
1445 let validator = Validator::new(registry);
1446
1447 let expr = build_nested_subquery(crate::query::types::MAX_SUBQUERY_DEPTH + 1);
1449 let result = validator.validate(&expr);
1450 assert!(
1451 result.is_err(),
1452 "subquery beyond MAX_SUBQUERY_DEPTH should fail"
1453 );
1454 assert!(
1455 matches!(
1456 result.unwrap_err(),
1457 ValidationError::SubqueryDepthExceeded { .. }
1458 ),
1459 "error should be SubqueryDepthExceeded"
1460 );
1461 }
1462}