Skip to main content

rez_lsp_server/validation/
mod.rs

1//! Syntax validation for Rez package.py files.
2
3pub mod python_validator;
4pub mod rez_validator;
5pub mod validation_engine;
6
7pub use python_validator::PythonValidator;
8pub use rez_validator::RezValidator;
9pub use validation_engine::ValidationEngine;
10
11use crate::core::Result;
12use serde::{Deserialize, Serialize};
13use std::fmt;
14
15/// Represents a validation issue found in a package.py file.
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17pub struct ValidationIssue {
18    /// The severity of the issue
19    pub severity: Severity,
20    /// The line number where the issue occurs (1-based)
21    pub line: u32,
22    /// The column number where the issue occurs (1-based)
23    pub column: u32,
24    /// The length of the problematic text
25    pub length: u32,
26    /// A human-readable message describing the issue
27    pub message: String,
28    /// A unique code identifying the type of issue
29    pub code: String,
30    /// Optional suggestion for fixing the issue
31    pub suggestion: Option<String>,
32}
33
34/// Severity levels for validation issues.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
36pub enum Severity {
37    /// Informational message
38    Info,
39    /// Warning that doesn't prevent functionality
40    Warning,
41    /// Error that may cause issues
42    Error,
43    /// Critical error that will definitely cause problems
44    Critical,
45}
46
47impl fmt::Display for Severity {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        match self {
50            Severity::Info => write!(f, "info"),
51            Severity::Warning => write!(f, "warning"),
52            Severity::Error => write!(f, "error"),
53            Severity::Critical => write!(f, "critical"),
54        }
55    }
56}
57
58impl ValidationIssue {
59    /// Create a new validation issue.
60    pub fn new(
61        severity: Severity,
62        line: u32,
63        column: u32,
64        length: u32,
65        message: impl Into<String>,
66        code: impl Into<String>,
67    ) -> Self {
68        Self {
69            severity,
70            line,
71            column,
72            length,
73            message: message.into(),
74            code: code.into(),
75            suggestion: None,
76        }
77    }
78
79    /// Add a suggestion for fixing this issue.
80    pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
81        self.suggestion = Some(suggestion.into());
82        self
83    }
84}
85
86/// Trait for validators that can check package.py files.
87pub trait Validator {
88    /// Validate the given content and return any issues found.
89    fn validate(&self, content: &str, file_path: &str) -> Result<Vec<ValidationIssue>>;
90
91    /// Get the name of this validator.
92    fn name(&self) -> &str;
93
94    /// Get the version of this validator.
95    fn version(&self) -> &str {
96        "1.0.0"
97    }
98}
99
100/// Result of a validation operation.
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct ValidationResult {
103    /// The file that was validated
104    pub file_path: String,
105    /// All issues found during validation
106    pub issues: Vec<ValidationIssue>,
107    /// Statistics about the validation
108    pub stats: ValidationStats,
109}
110
111/// Statistics about a validation operation.
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct ValidationStats {
114    /// Total number of issues found
115    pub total_issues: usize,
116    /// Number of critical issues
117    pub critical_count: usize,
118    /// Number of errors
119    pub error_count: usize,
120    /// Number of warnings
121    pub warning_count: usize,
122    /// Number of info messages
123    pub info_count: usize,
124    /// Time taken for validation in milliseconds
125    pub validation_time_ms: u64,
126}
127
128impl ValidationResult {
129    /// Create a new validation result.
130    pub fn new(
131        file_path: impl Into<String>,
132        issues: Vec<ValidationIssue>,
133        validation_time_ms: u64,
134    ) -> Self {
135        let stats = ValidationStats::from_issues(&issues, validation_time_ms);
136        Self {
137            file_path: file_path.into(),
138            issues,
139            stats,
140        }
141    }
142
143    /// Check if the validation found any errors or critical issues.
144    pub fn has_errors(&self) -> bool {
145        self.stats.error_count > 0 || self.stats.critical_count > 0
146    }
147
148    /// Check if the validation found any issues at all.
149    pub fn has_issues(&self) -> bool {
150        self.stats.total_issues > 0
151    }
152}
153
154impl ValidationStats {
155    /// Create statistics from a list of issues.
156    pub fn from_issues(issues: &[ValidationIssue], validation_time_ms: u64) -> Self {
157        let mut critical_count = 0;
158        let mut error_count = 0;
159        let mut warning_count = 0;
160        let mut info_count = 0;
161
162        for issue in issues {
163            match issue.severity {
164                Severity::Critical => critical_count += 1,
165                Severity::Error => error_count += 1,
166                Severity::Warning => warning_count += 1,
167                Severity::Info => info_count += 1,
168            }
169        }
170
171        Self {
172            total_issues: issues.len(),
173            critical_count,
174            error_count,
175            warning_count,
176            info_count,
177            validation_time_ms,
178        }
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    #[test]
187    fn test_validation_issue_creation() {
188        let issue = ValidationIssue::new(Severity::Error, 10, 5, 8, "Invalid syntax", "E001");
189
190        assert_eq!(issue.severity, Severity::Error);
191        assert_eq!(issue.line, 10);
192        assert_eq!(issue.column, 5);
193        assert_eq!(issue.length, 8);
194        assert_eq!(issue.message, "Invalid syntax");
195        assert_eq!(issue.code, "E001");
196        assert!(issue.suggestion.is_none());
197    }
198
199    #[test]
200    fn test_validation_issue_with_suggestion() {
201        let issue = ValidationIssue::new(Severity::Warning, 5, 10, 3, "Deprecated field", "W001")
202            .with_suggestion("Use 'new_field' instead");
203
204        assert_eq!(
205            issue.suggestion,
206            Some("Use 'new_field' instead".to_string())
207        );
208    }
209
210    #[test]
211    fn test_validation_stats() {
212        let issues = vec![
213            ValidationIssue::new(Severity::Critical, 1, 1, 1, "Critical", "C001"),
214            ValidationIssue::new(Severity::Error, 2, 1, 1, "Error", "E001"),
215            ValidationIssue::new(Severity::Warning, 3, 1, 1, "Warning", "W001"),
216            ValidationIssue::new(Severity::Info, 4, 1, 1, "Info", "I001"),
217        ];
218
219        let stats = ValidationStats::from_issues(&issues, 100);
220
221        assert_eq!(stats.total_issues, 4);
222        assert_eq!(stats.critical_count, 1);
223        assert_eq!(stats.error_count, 1);
224        assert_eq!(stats.warning_count, 1);
225        assert_eq!(stats.info_count, 1);
226        assert_eq!(stats.validation_time_ms, 100);
227    }
228
229    #[test]
230    fn test_validation_result() {
231        let issues = vec![
232            ValidationIssue::new(Severity::Error, 1, 1, 1, "Error", "E001"),
233            ValidationIssue::new(Severity::Warning, 2, 1, 1, "Warning", "W001"),
234        ];
235
236        let result = ValidationResult::new("test.py", issues, 50);
237
238        assert_eq!(result.file_path, "test.py");
239        assert!(result.has_errors());
240        assert!(result.has_issues());
241        assert_eq!(result.stats.total_issues, 2);
242        assert_eq!(result.stats.validation_time_ms, 50);
243    }
244}