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    // Phase A C indirect-call precision (U18.1) — fuzzy-correct common typos
51    // like `address_take:`, `resolve_via:`, `callsite_promiscous:` so users on
52    // the `mcp__sqry__semantic_search` surface get the same suggestion
53    // experience the planner-surface (`sqry_query`) already provides via U14.
54    "address_taken",
55    "resolved_via",
56    "callsite_promiscuous",
57];
58
59/// Semantic validator for query ASTs
60///
61/// Validates queries against a field registry to ensure:
62/// - All field names exist
63/// - Operators are compatible with field types
64/// - Values match expected types
65/// - Regex patterns are valid
66///
67/// # Example
68///
69/// ```
70/// use sqry_core::query::registry::FieldRegistry;
71/// use sqry_core::query::validator::Validator;
72/// use sqry_core::query::types::{Expr, Condition, Field, Operator, Value, Span};
73///
74/// let registry = FieldRegistry::with_core_fields();
75/// let validator = Validator::new(registry);
76///
77/// let condition = Expr::Condition(Condition {
78///     field: Field::new("kind"),
79///     operator: Operator::Equal,
80///     value: Value::String("function".to_string()),
81///     span: Span::default(),
82/// });
83///
84/// assert!(validator.validate(&condition).is_ok());
85/// ```
86/// Configuration for validation behaviour.
87#[derive(Clone, Copy, Debug)]
88pub struct ValidationOptions {
89    /// Enable fuzzy field correction (opt-in).
90    pub fuzzy_fields: bool,
91    /// Maximum edit distance allowed for fuzzy field correction.
92    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
104/// Validator for query expressions and field/value semantics.
105pub struct Validator {
106    registry: FieldRegistry,
107    options: ValidationOptions,
108}
109
110impl Validator {
111    /// Create a new validator with the given field registry
112    #[must_use]
113    pub fn new(registry: FieldRegistry) -> Self {
114        Self {
115            registry,
116            options: ValidationOptions::default(),
117        }
118    }
119
120    /// Create a new validator with options
121    #[must_use]
122    pub fn with_options(registry: FieldRegistry, options: ValidationOptions) -> Self {
123        Self { registry, options }
124    }
125
126    /// Validate a query expression
127    ///
128    /// Returns `Ok(())` if the expression is valid, or a `ValidationError` if validation fails.
129    ///
130    /// # Errors
131    ///
132    /// Returns [`ValidationError`] when the expression uses unknown fields, invalid operators, or mismatched value types.
133    pub fn validate(&self, expr: &Expr) -> Result<(), ValidationError> {
134        self.validate_node_with_depth(expr, 0)
135    }
136
137    /// Normalize field names using fuzzy options (when enabled) and return a new expression tree.
138    ///
139    /// # Errors
140    ///
141    /// Returns [`ValidationError`] when normalization fails or fuzzy correction is unsafe.
142    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    /// Validate a single AST node recursively, tracking subquery nesting depth.
158    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 the value is a subquery, validate its inner expression
174                // with incremented depth
175                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    /// Validate a condition
197    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            // Regex values are valid with ~= operator for String/Enum/Path fields
248            (Operator::Regex, Value::Regex(_)) => matches!(
249                field_desc.field_type,
250                FieldType::String | FieldType::Enum(_) | FieldType::Path
251            ),
252            // For all other cases, use standard type matching
253            _ => 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        // Check if pattern contains lookaround assertions
294        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            // Use fancy-regex for lookaround support
301            if let Err(e) = fancy_regex::Regex::new(&regex_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            // Use standard regex for performance
310            if let Err(e) = Regex::new(&regex_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    /// Detect contradictions in the query
331    ///
332    /// Returns warnings for impossible queries, such as:
333    /// - `kind:function AND kind:class` (same field with different values)
334    /// - `async:true AND async:false` (boolean contradiction)
335    #[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    /// Suggest a field name for a typo using Levenshtein distance
441    ///
442    /// Returns the closest matching field name if the edit distance is ≤ 2.
443    /// Matching is case-insensitive to handle case typos like "KIND" → "kind".
444    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            // Check for exact case-insensitive match first
457            if field_name.to_lowercase() == input_lower {
458                return vec![field_name.to_string()];
459            }
460
461            // Otherwise use Levenshtein distance
462            let distance = levenshtein_distance(&input_lower, &field_name.to_lowercase());
463
464            // Only suggest if distance within threshold
465            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        // Fast path: exact match
489        if self.registry.get(condition.field.as_str()).is_some() {
490            return Ok(condition.clone());
491        }
492
493        // Fuzzy disabled: reject unknown fields outright.
494        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        // Try fuzzy suggestion within threshold
503        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                // Do not auto-correct fields that are prone to ambiguity (e.g., "name").
513                // Users must spell these exactly or accept explicit errors.
514                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/// Warning about a potential contradiction in the query
540#[derive(Debug, Clone, PartialEq)]
541pub struct ContradictionWarning {
542    /// Warning message
543    pub message: String,
544    /// Location of the contradiction
545    pub span: Span,
546}
547
548/// Compute Levenshtein distance (edit distance) between two strings
549///
550/// Returns the minimum number of single-character edits (insertions, deletions, substitutions)
551/// required to change one string into the other.
552#[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    // Create matrix
558    let mut matrix = vec![vec![0; len2 + 1]; len1 + 1];
559
560    // Initialize first row and column
561    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    // Fill matrix
569    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, // deletion
579                    matrix[i + 1][j] + 1, // insertion
580                ),
581                matrix[i][j] + cost, // substitution
582            );
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        // Add a custom field that is NOT in SAFE_FUZZY_FIELDS whitelist
722        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        // "custm" is a typo for "custom" (distance 1)
736        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        // Exact case-insensitive match
759        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        // Case-insensitive with typo
766        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        // Add an async field for testing
796        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        // Nested: (kind:function AND kind:class) OR async:true
1127        // Should detect contradiction in left branch
1128        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        // Verify span is present and covers both conditions
1182        assert_eq!(warnings[0].span.start, 0);
1183        assert_eq!(warnings[0].span.end, 28);
1184    }
1185
1186    // ================================================================
1187    // Subquery depth validation tests
1188    // ================================================================
1189
1190    /// Build a subquery chain nested to the given depth.
1191    ///
1192    /// Depth 1: `callers:(kind:function)`
1193    /// Depth 2: `callers:(callers:(kind:function))`
1194    /// etc.
1195    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        // Build subquery nested exactly at MAX_SUBQUERY_DEPTH
1219        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        // Build subquery nested one beyond MAX_SUBQUERY_DEPTH
1232        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}