Skip to main content

codemod_core/pattern/
validator.rs

1//! Pattern validation.
2//!
3//! After a [`Pattern`](super::Pattern) is inferred, it should be validated
4//! before being applied to a codebase. This module checks for common issues
5//! such as empty templates, unused variables, and low confidence scores.
6
7use super::Pattern;
8
9// ---------------------------------------------------------------------------
10// Public types
11// ---------------------------------------------------------------------------
12
13/// Result of validating a [`Pattern`].
14#[derive(Debug, Clone)]
15pub struct ValidationResult {
16    /// `true` if the pattern passes all hard checks.
17    pub is_valid: bool,
18    /// Non-fatal issues that the user should review.
19    pub warnings: Vec<String>,
20    /// Fatal issues that prevent the pattern from being used.
21    pub errors: Vec<String>,
22}
23
24impl ValidationResult {
25    /// Creates a passing result with no warnings or errors.
26    fn ok() -> Self {
27        Self {
28            is_valid: true,
29            warnings: Vec::new(),
30            errors: Vec::new(),
31        }
32    }
33}
34
35// ---------------------------------------------------------------------------
36// PatternValidator
37// ---------------------------------------------------------------------------
38
39/// Validates that a [`Pattern`] is well-formed and likely to produce correct
40/// transformations.
41pub struct PatternValidator;
42
43impl PatternValidator {
44    /// Validate a pattern and return a [`ValidationResult`].
45    ///
46    /// ## Checks performed
47    ///
48    /// | Check | Severity |
49    /// |---|---|
50    /// | `before_template` is non-empty | error |
51    /// | `after_template` is non-empty | error |
52    /// | `language` is non-empty | error |
53    /// | All variables appear in `before_template` | error |
54    /// | All variables appear in `after_template` | warning |
55    /// | Confidence >= 0.3 | warning |
56    /// | Confidence >= 0.1 | error |
57    /// | `before_template` != `after_template` | warning |
58    pub fn validate(pattern: &Pattern) -> crate::Result<ValidationResult> {
59        let mut result = ValidationResult::ok();
60
61        // --- Hard errors ---
62
63        if pattern.before_template.trim().is_empty() {
64            result.errors.push("before_template is empty".into());
65        }
66        if pattern.after_template.trim().is_empty() {
67            result.errors.push("after_template is empty".into());
68        }
69        if pattern.language.trim().is_empty() {
70            result.errors.push("language is not specified".into());
71        }
72
73        // Every variable must appear in the before template.
74        for var in &pattern.variables {
75            if !pattern.before_template.contains(&var.name) {
76                result.errors.push(format!(
77                    "Variable '{}' does not appear in before_template",
78                    var.name
79                ));
80            }
81        }
82
83        // Confidence floor.
84        if pattern.confidence < 0.1 {
85            result.errors.push(format!(
86                "Confidence score ({:.2}) is below the minimum threshold (0.1)",
87                pattern.confidence
88            ));
89        }
90
91        // --- Warnings ---
92
93        // Variables missing from after_template may indicate a deletion-only
94        // transform, which is valid but worth flagging.
95        for var in &pattern.variables {
96            if !pattern.after_template.contains(&var.name) {
97                result.warnings.push(format!(
98                    "Variable '{}' does not appear in after_template — captured value will be dropped",
99                    var.name
100                ));
101            }
102        }
103
104        if pattern.confidence < 0.3 {
105            result.warnings.push(format!(
106                "Low confidence score ({:.2}); consider providing more examples",
107                pattern.confidence
108            ));
109        }
110
111        if pattern.before_template == pattern.after_template {
112            result.warnings.push(
113                "before_template and after_template are identical — no transformation will occur"
114                    .into(),
115            );
116        }
117
118        // Check for duplicate variable names.
119        let mut seen = std::collections::HashSet::new();
120        for var in &pattern.variables {
121            if !seen.insert(&var.name) {
122                result
123                    .warnings
124                    .push(format!("Duplicate variable name '{}'", var.name));
125            }
126        }
127
128        result.is_valid = result.errors.is_empty();
129        Ok(result)
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use crate::pattern::{Pattern, PatternVar};
137
138    #[test]
139    fn test_valid_pattern() {
140        let p = Pattern::new(
141            "foo($var1)".into(),
142            "bar($var1)".into(),
143            vec![PatternVar {
144                name: "$var1".into(),
145                node_type: None,
146            }],
147            "rust".into(),
148            0.9,
149        );
150        let res = PatternValidator::validate(&p).unwrap();
151        assert!(res.is_valid);
152        assert!(res.errors.is_empty());
153    }
154
155    #[test]
156    fn test_empty_before_template() {
157        let p = Pattern::new(
158            "".into(),
159            "bar($var1)".into(),
160            vec![PatternVar {
161                name: "$var1".into(),
162                node_type: None,
163            }],
164            "rust".into(),
165            0.9,
166        );
167        let res = PatternValidator::validate(&p).unwrap();
168        assert!(!res.is_valid);
169        assert!(res.errors.iter().any(|e| e.contains("before_template")));
170    }
171
172    #[test]
173    fn test_missing_variable_in_before() {
174        let p = Pattern::new(
175            "foo(x)".into(),
176            "bar($var1)".into(),
177            vec![PatternVar {
178                name: "$var1".into(),
179                node_type: None,
180            }],
181            "rust".into(),
182            0.9,
183        );
184        let res = PatternValidator::validate(&p).unwrap();
185        assert!(!res.is_valid);
186        assert!(res.errors.iter().any(|e| e.contains("$var1")));
187    }
188
189    #[test]
190    fn test_low_confidence_warning() {
191        let p = Pattern::new(
192            "foo($var1)".into(),
193            "bar($var1)".into(),
194            vec![PatternVar {
195                name: "$var1".into(),
196                node_type: None,
197            }],
198            "rust".into(),
199            0.2,
200        );
201        let res = PatternValidator::validate(&p).unwrap();
202        assert!(res.is_valid);
203        assert!(res.warnings.iter().any(|w| w.contains("confidence")));
204    }
205
206    #[test]
207    fn test_identical_templates_warning() {
208        let p = Pattern::new(
209            "foo($var1)".into(),
210            "foo($var1)".into(),
211            vec![PatternVar {
212                name: "$var1".into(),
213                node_type: None,
214            }],
215            "rust".into(),
216            0.9,
217        );
218        let res = PatternValidator::validate(&p).unwrap();
219        assert!(res.is_valid);
220        assert!(res.warnings.iter().any(|w| w.contains("identical")));
221    }
222}