Skip to main content

sqry_core/query/
validator.rs

1//! Semantic validation for query ASTs
2//!
3//! This module provides validation for parsed query ASTs, checking:
4//! - Field names against the field registry
5//! - Operator compatibility with field types
6//! - Value type matching
7//! - Regex pattern validity
8//! - Contradiction detection
9//!
10//! The validator provides helpful error messages with suggestions for typos
11//! and clear explanations of validation failures.
12//!
13//! # Regex Support (FT-C.1)
14//!
15//! The validator supports both standard regex patterns (via the `regex` crate)
16//! and advanced patterns with lookaround assertions (via the `fancy-regex` crate).
17//!
18//! Lookaround support includes:
19//! - Positive lookahead: `(?=...)`
20//! - Negative lookahead: `(?!...)`
21//! - Positive lookbehind: `(?<=...)`
22//! - Negative lookbehind: `(?<!...)`
23//!
24//! The validator automatically detects lookaround patterns and uses the
25//! appropriate regex engine.
26
27use 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/// Semantic validator for query ASTs
53///
54/// Validates queries against a field registry to ensure:
55/// - All field names exist
56/// - Operators are compatible with field types
57/// - Values match expected types
58/// - Regex patterns are valid
59///
60/// # Example
61///
62/// ```
63/// use sqry_core::query::registry::FieldRegistry;
64/// use sqry_core::query::validator::Validator;
65/// use sqry_core::query::types::{Expr, Condition, Field, Operator, Value, Span};
66///
67/// let registry = FieldRegistry::with_core_fields();
68/// let validator = Validator::new(registry);
69///
70/// let condition = Expr::Condition(Condition {
71///     field: Field::new("kind"),
72///     operator: Operator::Equal,
73///     value: Value::String("function".to_string()),
74///     span: Span::default(),
75/// });
76///
77/// assert!(validator.validate(&condition).is_ok());
78/// ```
79/// Configuration for validation behaviour.
80#[derive(Clone, Copy, Debug)]
81pub struct ValidationOptions {
82    /// Enable fuzzy field correction (opt-in).
83    pub fuzzy_fields: bool,
84    /// Maximum edit distance allowed for fuzzy field correction.
85    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
97/// Validator for query expressions and field/value semantics.
98pub struct Validator {
99    registry: FieldRegistry,
100    options: ValidationOptions,
101}
102
103impl Validator {
104    /// Create a new validator with the given field registry
105    #[must_use]
106    pub fn new(registry: FieldRegistry) -> Self {
107        Self {
108            registry,
109            options: ValidationOptions::default(),
110        }
111    }
112
113    /// Create a new validator with options
114    #[must_use]
115    pub fn with_options(registry: FieldRegistry, options: ValidationOptions) -> Self {
116        Self { registry, options }
117    }
118
119    /// Validate a query expression
120    ///
121    /// Returns `Ok(())` if the expression is valid, or a `ValidationError` if validation fails.
122    ///
123    /// # Errors
124    ///
125    /// Returns [`ValidationError`] when the expression uses unknown fields, invalid operators, or mismatched value types.
126    pub fn validate(&self, expr: &Expr) -> Result<(), ValidationError> {
127        self.validate_node_with_depth(expr, 0)
128    }
129
130    /// Normalize field names using fuzzy options (when enabled) and return a new expression tree.
131    ///
132    /// # Errors
133    ///
134    /// Returns [`ValidationError`] when normalization fails or fuzzy correction is unsafe.
135    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    /// Validate a single AST node recursively, tracking subquery nesting depth.
151    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 the value is a subquery, validate its inner expression
167                // with incremented depth
168                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    /// Validate a condition
190    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            // Regex values are valid with ~= operator for String/Enum/Path fields
241            (Operator::Regex, Value::Regex(_)) => matches!(
242                field_desc.field_type,
243                FieldType::String | FieldType::Enum(_) | FieldType::Path
244            ),
245            // For all other cases, use standard type matching
246            _ => 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        // Check if pattern contains lookaround assertions
287        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            // Use fancy-regex for lookaround support
294            if let Err(e) = fancy_regex::Regex::new(&regex_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            // Use standard regex for performance
303            if let Err(e) = Regex::new(&regex_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    /// Detect contradictions in the query
324    ///
325    /// Returns warnings for impossible queries, such as:
326    /// - `kind:function AND kind:class` (same field with different values)
327    /// - `async:true AND async:false` (boolean contradiction)
328    #[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    /// Suggest a field name for a typo using Levenshtein distance
434    ///
435    /// Returns the closest matching field name if the edit distance is ≤ 2.
436    /// Matching is case-insensitive to handle case typos like "KIND" → "kind".
437    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            // Check for exact case-insensitive match first
450            if field_name.to_lowercase() == input_lower {
451                return vec![field_name.to_string()];
452            }
453
454            // Otherwise use Levenshtein distance
455            let distance = levenshtein_distance(&input_lower, &field_name.to_lowercase());
456
457            // Only suggest if distance within threshold
458            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        // Fast path: exact match
482        if self.registry.get(condition.field.as_str()).is_some() {
483            return Ok(condition.clone());
484        }
485
486        // Fuzzy disabled: reject unknown fields outright.
487        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        // Try fuzzy suggestion within threshold
496        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                // Do not auto-correct fields that are prone to ambiguity (e.g., "name").
506                // Users must spell these exactly or accept explicit errors.
507                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/// Warning about a potential contradiction in the query
533#[derive(Debug, Clone, PartialEq)]
534pub struct ContradictionWarning {
535    /// Warning message
536    pub message: String,
537    /// Location of the contradiction
538    pub span: Span,
539}
540
541/// Compute Levenshtein distance (edit distance) between two strings
542///
543/// Returns the minimum number of single-character edits (insertions, deletions, substitutions)
544/// required to change one string into the other.
545#[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    // Create matrix
551    let mut matrix = vec![vec![0; len2 + 1]; len1 + 1];
552
553    // Initialize first row and column
554    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    // Fill matrix
562    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, // deletion
572                    matrix[i + 1][j] + 1, // insertion
573                ),
574                matrix[i][j] + cost, // substitution
575            );
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        // Add a custom field that is NOT in SAFE_FUZZY_FIELDS whitelist
715        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        // "custm" is a typo for "custom" (distance 1)
729        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        // Exact case-insensitive match
752        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        // Case-insensitive with typo
759        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        // Add an async field for testing
789        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        // Nested: (kind:function AND kind:class) OR async:true
1120        // Should detect contradiction in left branch
1121        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        // Verify span is present and covers both conditions
1175        assert_eq!(warnings[0].span.start, 0);
1176        assert_eq!(warnings[0].span.end, 28);
1177    }
1178
1179    // ================================================================
1180    // Subquery depth validation tests
1181    // ================================================================
1182
1183    /// Build a subquery chain nested to the given depth.
1184    ///
1185    /// Depth 1: `callers:(kind:function)`
1186    /// Depth 2: `callers:(callers:(kind:function))`
1187    /// etc.
1188    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        // Build subquery nested exactly at MAX_SUBQUERY_DEPTH
1212        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        // Build subquery nested one beyond MAX_SUBQUERY_DEPTH
1225        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}