rust_guardian/domain/
violations.rs

1//! Core domain models for code quality violations and validation results
2//!
3//! Architecture: Rich Domain Models - Violations are entities with behavior, not just data
4//! - Violations can classify themselves, suggest fixes, and maintain context
5//! - ValidationReport acts as an aggregate root managing collections of violations
6//! - Domain events can be generated when patterns are detected or when validation completes
7
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10use std::path::PathBuf;
11
12/// Severity levels for code quality violations
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Hash)]
14#[serde(rename_all = "lowercase")]
15pub enum Severity {
16    /// Informational messages and suggestions
17    Info,
18    /// Warnings that should be addressed but don't block builds
19    Warning,
20    /// Errors that block commits and fail CI/CD builds
21    Error,
22}
23
24impl Severity {
25    /// Whether this severity level should cause validation to fail
26    pub fn is_blocking(self) -> bool {
27        matches!(self, Self::Error)
28    }
29
30    /// Convert to string for display
31    pub fn as_str(self) -> &'static str {
32        match self {
33            Self::Info => "info",
34            Self::Warning => "warning",
35            Self::Error => "error",
36        }
37    }
38}
39
40/// A code quality violation detected during analysis
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct Violation {
43    /// Unique identifier for the rule that detected this violation
44    pub rule_id: String,
45    /// Severity level of this violation
46    pub severity: Severity,
47    /// File path where the violation was found
48    pub file_path: PathBuf,
49    /// Line number (1-indexed) where the violation occurs
50    pub line_number: Option<u32>,
51    /// Column number (1-indexed) where the violation starts
52    pub column_number: Option<u32>,
53    /// Human-readable description of the violation
54    pub message: String,
55    /// Source code context around the violation
56    pub context: Option<String>,
57    /// Suggested fix for the violation (if available)
58    pub suggested_fix: Option<String>,
59    /// When this violation was detected
60    pub detected_at: DateTime<Utc>,
61}
62
63impl Violation {
64    /// Create a new violation
65    pub fn new(
66        rule_id: impl Into<String>,
67        severity: Severity,
68        file_path: PathBuf,
69        message: impl Into<String>,
70    ) -> Self {
71        Self {
72            rule_id: rule_id.into(),
73            severity,
74            file_path,
75            line_number: None,
76            column_number: None,
77            message: message.into(),
78            context: None,
79            suggested_fix: None,
80            detected_at: Utc::now(),
81        }
82    }
83
84    /// Set line and column position
85    pub fn with_position(mut self, line: u32, column: u32) -> Self {
86        self.line_number = Some(line);
87        self.column_number = Some(column);
88        self
89    }
90
91    /// Add source code context
92    pub fn with_context(mut self, context: impl Into<String>) -> Self {
93        self.context = Some(context.into());
94        self
95    }
96
97    /// Add a suggested fix
98    pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
99        self.suggested_fix = Some(suggestion.into());
100        self
101    }
102
103    /// Whether this violation is blocking (prevents commits/builds)
104    pub fn is_blocking(&self) -> bool {
105        self.severity.is_blocking()
106    }
107
108    /// Format violation for display
109    pub fn format_display(&self) -> String {
110        let location = match (self.line_number, self.column_number) {
111            (Some(line), Some(col)) => format!(":{line}:{col}"),
112            (Some(line), None) => format!(":{line}"),
113            _ => String::new(),
114        };
115
116        format!(
117            "{}{} [{}] {}",
118            self.file_path.display(),
119            location,
120            self.severity.as_str(),
121            self.message
122        )
123    }
124}
125
126/// Summary statistics for a validation report
127#[derive(Debug, Clone, Default, Serialize, Deserialize)]
128pub struct ValidationSummary {
129    /// Total number of files analyzed
130    pub total_files: usize,
131    /// Number of violations by severity level
132    pub violations_by_severity: ViolationCounts,
133    /// Total execution time in milliseconds
134    pub execution_time_ms: u64,
135    /// Timestamp when validation was performed
136    pub validated_at: DateTime<Utc>,
137}
138
139/// Count of violations by severity level
140#[derive(Debug, Clone, Default, Serialize, Deserialize)]
141pub struct ViolationCounts {
142    pub error: usize,
143    pub warning: usize,
144    pub info: usize,
145}
146
147impl ViolationCounts {
148    /// Total number of violations across all severities
149    pub fn total(&self) -> usize {
150        self.error + self.warning + self.info
151    }
152
153    /// Whether there are any blocking violations
154    pub fn has_blocking(&self) -> bool {
155        self.error > 0
156    }
157
158    /// Add a violation to the counts
159    pub fn add(&mut self, severity: Severity) {
160        match severity {
161            Severity::Error => self.error += 1,
162            Severity::Warning => self.warning += 1,
163            Severity::Info => self.info += 1,
164        }
165    }
166}
167
168/// Complete validation report containing all violations and metadata
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct ValidationReport {
171    /// All violations found during validation
172    pub violations: Vec<Violation>,
173    /// Summary statistics
174    pub summary: ValidationSummary,
175    /// Configuration used for this validation
176    pub config_fingerprint: Option<String>,
177}
178
179impl ValidationReport {
180    /// Create a new empty validation report
181    pub fn new() -> Self {
182        Self {
183            violations: Vec::new(),
184            summary: ValidationSummary { validated_at: Utc::now(), ..Default::default() },
185            config_fingerprint: None,
186        }
187    }
188
189    /// Add a violation to the report
190    pub fn add_violation(&mut self, violation: Violation) {
191        self.summary.violations_by_severity.add(violation.severity);
192        self.violations.push(violation);
193    }
194
195    /// Whether the report contains any violations
196    pub fn has_violations(&self) -> bool {
197        !self.violations.is_empty()
198    }
199
200    /// Whether the report contains blocking violations (errors)
201    pub fn has_errors(&self) -> bool {
202        self.summary.violations_by_severity.has_blocking()
203    }
204
205    /// Get violations of a specific severity
206    pub fn violations_by_severity(&self, severity: Severity) -> impl Iterator<Item = &Violation> {
207        self.violations.iter().filter(move |v| v.severity == severity)
208    }
209
210    /// Set the number of files analyzed
211    pub fn set_files_analyzed(&mut self, count: usize) {
212        self.summary.total_files = count;
213    }
214
215    /// Set the execution time
216    pub fn set_execution_time(&mut self, duration_ms: u64) {
217        self.summary.execution_time_ms = duration_ms;
218    }
219
220    /// Set the configuration fingerprint
221    pub fn set_config_fingerprint(&mut self, fingerprint: impl Into<String>) {
222        self.config_fingerprint = Some(fingerprint.into());
223    }
224
225    /// Merge another report into this one
226    pub fn merge(&mut self, other: ValidationReport) {
227        for violation in other.violations {
228            self.add_violation(violation);
229        }
230        self.summary.total_files += other.summary.total_files;
231    }
232
233    /// Sort violations by file path and line number for consistent output
234    pub fn sort_violations(&mut self) {
235        self.violations.sort_by(|a, b| {
236            a.file_path
237                .cmp(&b.file_path)
238                .then_with(|| a.line_number.unwrap_or(0).cmp(&b.line_number.unwrap_or(0)))
239                .then_with(|| a.severity.cmp(&b.severity))
240        });
241    }
242}
243
244impl Default for ValidationReport {
245    fn default() -> Self {
246        Self::new()
247    }
248}
249
250/// Error types that can occur during validation
251#[derive(Debug, thiserror::Error)]
252pub enum GuardianError {
253    /// Configuration file could not be loaded or parsed
254    #[error("Configuration error: {message}")]
255    Configuration { message: String },
256
257    /// File could not be read or accessed
258    #[error("IO error: {source}")]
259    Io {
260        #[from]
261        source: std::io::Error,
262    },
263
264    /// Pattern compilation failed
265    #[error("Pattern error: {message}")]
266    Pattern { message: String },
267
268    /// Analysis failed for a specific file
269    #[error("Analysis error in {file}: {message}")]
270    Analysis { file: String, message: String },
271
272    /// Cache operation failed
273    #[error("Cache error: {message}")]
274    Cache { message: String },
275
276    /// Validation operation failed
277    #[error("Validation error: {message}")]
278    Validation { message: String },
279}
280
281impl GuardianError {
282    /// Create a configuration error
283    pub fn config(message: impl Into<String>) -> Self {
284        Self::Configuration { message: message.into() }
285    }
286
287    /// Create a pattern error
288    pub fn pattern(message: impl Into<String>) -> Self {
289        Self::Pattern { message: message.into() }
290    }
291
292    /// Create an analysis error
293    pub fn analysis(file: impl Into<String>, message: impl Into<String>) -> Self {
294        Self::Analysis { file: file.into(), message: message.into() }
295    }
296
297    /// Create a cache error
298    pub fn cache(message: impl Into<String>) -> Self {
299        Self::Cache { message: message.into() }
300    }
301
302    /// Create a validation error
303    pub fn validation(message: impl Into<String>) -> Self {
304        Self::Validation { message: message.into() }
305    }
306}
307
308/// Result type for Guardian operations
309pub type GuardianResult<T> = Result<T, GuardianError>;
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314    use std::path::Path;
315
316    #[test]
317    fn test_violation_creation() {
318        let violation = Violation::new(
319            "test_rule",
320            Severity::Error,
321            PathBuf::from("src/lib.rs"),
322            "Test message",
323        );
324
325        assert_eq!(violation.rule_id, "test_rule");
326        assert_eq!(violation.severity, Severity::Error);
327        assert_eq!(violation.file_path, Path::new("src/lib.rs"));
328        assert_eq!(violation.message, "Test message");
329        assert!(violation.is_blocking());
330    }
331
332    #[test]
333    fn test_violation_with_position() {
334        let violation = Violation::new(
335            "test_rule",
336            Severity::Warning,
337            PathBuf::from("src/lib.rs"),
338            "Test message",
339        )
340        .with_position(42, 15)
341        .with_context("let x = unimplemented!();");
342
343        assert_eq!(violation.line_number, Some(42));
344        assert_eq!(violation.column_number, Some(15));
345        assert_eq!(violation.context, Some("let x = unimplemented!();".to_string()));
346        assert!(!violation.is_blocking());
347    }
348
349    #[test]
350    fn test_validation_report() {
351        let mut report = ValidationReport::new();
352
353        report.add_violation(Violation::new(
354            "rule1",
355            Severity::Error,
356            PathBuf::from("src/main.rs"),
357            "Error message",
358        ));
359
360        report.add_violation(Violation::new(
361            "rule2",
362            Severity::Warning,
363            PathBuf::from("src/lib.rs"),
364            "Warning message",
365        ));
366
367        assert!(report.has_violations());
368        assert!(report.has_errors());
369        assert_eq!(report.summary.violations_by_severity.total(), 2);
370        assert_eq!(report.summary.violations_by_severity.error, 1);
371        assert_eq!(report.summary.violations_by_severity.warning, 1);
372    }
373
374    #[test]
375    fn test_severity_ordering() {
376        assert!(Severity::Error > Severity::Warning);
377        assert!(Severity::Warning > Severity::Info);
378        assert!(Severity::Error.is_blocking());
379        assert!(!Severity::Warning.is_blocking());
380    }
381}