1pub mod analyzer;
9pub mod cache;
10pub mod config;
11pub mod domain;
12pub mod patterns;
13pub mod report;
14
15pub use domain::violations::{
17 GuardianError, GuardianResult, Severity, ValidationReport, ValidationSummary, Violation,
18};
19
20pub use config::{GuardianConfig, PatternCategory, PatternRule, RuleType};
21
22pub use analyzer::{AnalysisOptions, Analyzer, PatternStats};
23
24pub use report::{OutputFormat, ReportFormatter, ReportOptions};
25
26pub use cache::{CacheStatistics, FileCache};
27
28use std::path::{Path, PathBuf};
29
30pub struct GuardianValidator {
32 analyzer: Analyzer,
33 cache: Option<FileCache>,
34 report_formatter: ReportFormatter,
35}
36
37#[derive(Debug, Clone)]
39pub struct ValidationOptions {
40 pub use_cache: bool,
42 pub cache_path: Option<PathBuf>,
44 pub continue_on_error: bool,
46 pub output_format: OutputFormat,
48 pub report_options: ReportOptions,
50 pub analysis_options: AnalysisOptions,
52}
53
54impl Default for ValidationOptions {
55 fn default() -> Self {
56 Self {
57 use_cache: true,
58 cache_path: None,
59 continue_on_error: true,
60 output_format: OutputFormat::Human,
61 report_options: ReportOptions::default(),
62 analysis_options: AnalysisOptions::default(),
63 }
64 }
65}
66
67impl GuardianValidator {
68 pub fn new_with_config(config: GuardianConfig) -> GuardianResult<Self> {
70 let analyzer = Analyzer::new(config)?;
71 let report_formatter = ReportFormatter::default();
72
73 Ok(Self {
74 analyzer,
75 cache: None,
76 report_formatter,
77 })
78 }
79
80 pub fn new() -> GuardianResult<Self> {
82 Self::new_with_config(GuardianConfig::default())
83 }
84
85 pub fn from_config_file<P: AsRef<Path>>(path: P) -> GuardianResult<Self> {
87 let config = GuardianConfig::load_from_file(path)?;
88 Self::new_with_config(config)
89 }
90
91 pub fn with_cache<P: AsRef<Path>>(mut self, cache_path: P) -> GuardianResult<Self> {
93 let mut cache = FileCache::new(cache_path);
94 cache.load()?;
95 cache.set_config_fingerprint(self.analyzer.config_fingerprint());
96 self.cache = Some(cache);
97 Ok(self)
98 }
99
100 pub fn with_report_formatter(mut self, formatter: ReportFormatter) -> Self {
102 self.report_formatter = formatter;
103 self
104 }
105
106 pub async fn validate_for_agent<P: AsRef<Path>>(
108 &mut self,
109 paths: Vec<P>,
110 ) -> GuardianResult<ValidationReport> {
111 self.validate_with_options(paths, &ValidationOptions::default())
112 .await
113 }
114
115 pub async fn validate_with_options<P: AsRef<Path>>(
117 &mut self,
118 paths: Vec<P>,
119 options: &ValidationOptions,
120 ) -> GuardianResult<ValidationReport> {
121 let paths: Vec<PathBuf> = paths.iter().map(|p| p.as_ref().to_path_buf()).collect();
123
124 let report = if options.use_cache && self.cache.is_some() {
126 self.analyze_with_cache(&paths, &options.analysis_options)
127 .await?
128 } else {
129 self.analyzer.analyze_paths(
130 &paths.iter().map(|p| p.as_path()).collect::<Vec<_>>(),
131 &options.analysis_options,
132 )?
133 };
134
135 Ok(report)
136 }
137
138 pub fn validate_file<P: AsRef<Path>>(&self, file_path: P) -> GuardianResult<ValidationReport> {
140 let violations = self.analyzer.analyze_file(file_path)?;
141
142 let mut report = ValidationReport::new();
143 for violation in violations {
144 report.add_violation(violation);
145 }
146 report.set_files_analyzed(1);
147
148 Ok(report)
149 }
150
151 pub fn validate_directory<P: AsRef<Path>>(
153 &self,
154 root: P,
155 options: &AnalysisOptions,
156 ) -> GuardianResult<ValidationReport> {
157 self.analyzer.analyze_directory(root, options)
158 }
159
160 pub fn format_report(
162 &self,
163 report: &ValidationReport,
164 format: OutputFormat,
165 ) -> GuardianResult<String> {
166 self.report_formatter.format_report(report, format)
167 }
168
169 pub fn pattern_statistics(&self) -> PatternStats {
171 self.analyzer.pattern_stats()
172 }
173
174 pub fn cache_statistics(&self) -> Option<CacheStatistics> {
176 self.cache.as_ref().map(|c| c.statistics())
177 }
178
179 pub fn clear_cache(&mut self) -> GuardianResult<()> {
181 if let Some(cache) = &mut self.cache {
182 cache.clear()?;
183 }
184 Ok(())
185 }
186
187 pub fn save_cache(&mut self) -> GuardianResult<()> {
189 if let Some(cache) = &mut self.cache {
190 cache.save()?;
191 }
192 Ok(())
193 }
194
195 pub fn cleanup_cache(&mut self) -> GuardianResult<Option<usize>> {
197 if let Some(cache) = &mut self.cache {
198 Ok(Some(cache.cleanup()?))
199 } else {
200 Ok(None)
201 }
202 }
203
204 async fn analyze_with_cache(
206 &mut self,
207 paths: &[PathBuf],
208 options: &AnalysisOptions,
209 ) -> GuardianResult<ValidationReport> {
210 let mut all_violations = Vec::new();
211 let files_analyzed: usize;
212 let start_time = std::time::Instant::now();
213
214 let config_fingerprint = self.analyzer.config_fingerprint();
216
217 let mut all_files = Vec::new();
219 for path in paths {
220 if path.is_file() {
221 all_files.push(path.clone());
222 } else if path.is_dir() {
223 let temp_report = self.analyzer.analyze_directory(path, options)?;
225 let discovered_files: std::collections::HashSet<PathBuf> = temp_report
227 .violations
228 .iter()
229 .map(|v| v.file_path.clone())
230 .collect();
231 all_files.extend(discovered_files);
232 }
233 }
234
235 if let Some(cache) = &mut self.cache {
236 let mut files_to_analyze = Vec::new();
238 let mut _cached_violation_count = 0;
239
240 for file_path in &all_files {
241 match cache.needs_analysis(file_path, &config_fingerprint) {
242 Ok(needs_analysis) => {
243 if needs_analysis {
244 files_to_analyze.push(file_path.clone());
245 } else {
246 _cached_violation_count += 1; }
251 }
252 Err(e) => {
253 tracing::warn!("Cache check failed for {}: {}", file_path.display(), e);
255 files_to_analyze.push(file_path.clone());
256 }
257 }
258 }
259
260 if !files_to_analyze.is_empty() {
262 let fresh_report = self.analyzer.analyze_paths(
263 &files_to_analyze
264 .iter()
265 .map(|p| p.as_path())
266 .collect::<Vec<_>>(),
267 options,
268 )?;
269
270 all_violations.extend(fresh_report.violations);
271 for file_path in &files_to_analyze {
275 let file_violations: Vec<_> = all_violations
276 .iter()
277 .filter(|v| v.file_path == *file_path)
278 .collect();
279
280 if let Err(e) =
281 cache.update_entry(file_path, file_violations.len(), &config_fingerprint)
282 {
283 tracing::warn!("Failed to update cache for {}: {}", file_path.display(), e);
284 }
285 }
286 }
287
288 files_analyzed = all_files.len(); } else {
290 let report = self.analyzer.analyze_paths(
292 &all_files.iter().map(|p| p.as_path()).collect::<Vec<_>>(),
293 options,
294 )?;
295
296 all_violations.extend(report.violations);
297 files_analyzed = report.summary.total_files;
298 }
299
300 let mut report = ValidationReport::new();
302 for violation in all_violations {
303 report.add_violation(violation);
304 }
305
306 report.set_files_analyzed(files_analyzed);
307 report.set_execution_time(start_time.elapsed().as_millis() as u64);
308 report.set_config_fingerprint(config_fingerprint);
309 report.sort_violations();
310
311 Ok(report)
312 }
313}
314
315pub fn create_validator() -> GuardianResult<GuardianValidator> {
317 GuardianValidator::new()
318}
319
320pub async fn validate_files<P: AsRef<Path>>(files: Vec<P>) -> GuardianResult<ValidationReport> {
322 let mut validator = GuardianValidator::new()?;
323 validator.validate_for_agent(files).await
324}
325
326pub fn validate_directory<P: AsRef<Path>>(directory: P) -> GuardianResult<ValidationReport> {
328 let validator = GuardianValidator::new()?;
329 validator.validate_directory(directory, &AnalysisOptions::default())
330}
331
332pub mod agent {
334 use super::*;
335
336 pub async fn pre_commit_check<P: AsRef<Path>>(modified_files: Vec<P>) -> GuardianResult<()> {
342 let mut validator = GuardianValidator::new()?;
343 let report = validator.validate_for_agent(modified_files).await?;
344
345 if report.has_errors() {
346 let error_count = report.summary.violations_by_severity.error;
347 return Err(GuardianError::config(format!(
348 "Pre-commit check failed: {} blocking violation{} found",
349 error_count,
350 if error_count == 1 { "" } else { "s" }
351 )));
352 }
353
354 Ok(())
355 }
356
357 pub async fn development_check<P: AsRef<Path>>(
362 files: Vec<P>,
363 ) -> GuardianResult<ValidationReport> {
364 let options = ValidationOptions {
365 analysis_options: AnalysisOptions {
366 fail_fast: false,
367 parallel: true,
368 ..Default::default()
369 },
370 report_options: ReportOptions {
371 min_severity: Some(Severity::Warning),
372 ..Default::default()
373 },
374 ..Default::default()
375 };
376
377 let mut validator = GuardianValidator::new()?;
378 validator.validate_with_options(files, &options).await
379 }
380
381 pub async fn production_check<P: AsRef<Path>>(
386 files: Vec<P>,
387 ) -> GuardianResult<ValidationReport> {
388 let options = ValidationOptions {
389 analysis_options: AnalysisOptions {
390 fail_fast: true,
391 parallel: true,
392 ..Default::default()
393 },
394 report_options: ReportOptions {
395 min_severity: Some(Severity::Warning),
396 show_suggestions: true,
397 ..Default::default()
398 },
399 ..Default::default()
400 };
401
402 let mut validator = GuardianValidator::new()?;
403 let report = validator.validate_with_options(files, &options).await?;
404
405 if report.has_violations() {
407 return Err(GuardianError::config(format!(
408 "Production validation failed: {} violations found",
409 report.violations.len()
410 )));
411 }
412
413 Ok(report)
414 }
415}
416
417#[cfg(test)]
418mod tests {
419 use super::*;
420 use std::fs;
421 use tempfile::TempDir;
422
423 #[tokio::test]
424 async fn test_validator_creation() {
425 let validator = GuardianValidator::new().unwrap();
426 let stats = validator.pattern_statistics();
427
428 assert!(stats.enabled_rules > 0);
430 }
431
432 #[tokio::test]
433 async fn test_validate_for_agent() {
434 let temp_dir = TempDir::new().unwrap();
435 let test_file = temp_dir.path().join("test.rs");
436
437 fs::write(&test_file, "// TODO: implement this\nfn main() {}").unwrap();
439
440 let mut validator = GuardianValidator::new().unwrap();
441 let report = validator.validate_for_agent(vec![test_file]).await.unwrap();
442
443 assert!(report.has_violations());
445 assert!(report.violations.iter().any(|v| v.rule_id.contains("todo")));
446 }
447
448 #[test]
449 fn test_single_file_validation() {
450 let temp_dir = TempDir::new().unwrap();
451 let test_file = temp_dir.path().join("test.rs");
452
453 fs::write(&test_file, "fn main() { unimplemented!() }").unwrap();
454
455 let validator = GuardianValidator::new().unwrap();
456 let report = validator.validate_file(&test_file).unwrap();
457
458 assert!(report.has_violations());
459 assert_eq!(report.summary.total_files, 1);
460 }
461
462 #[test]
463 fn test_directory_validation() {
464 let temp_dir = TempDir::new().unwrap();
465 let root = temp_dir.path();
466
467 fs::create_dir_all(root.join("src")).unwrap();
469 fs::write(root.join("src/lib.rs"), "// TODO: implement").unwrap();
470 fs::write(root.join("src/main.rs"), "fn main() {}").unwrap();
471
472 let validator = GuardianValidator::new().unwrap();
473 let report = validator
474 .validate_directory(root, &AnalysisOptions::default())
475 .unwrap();
476
477 assert!(report.has_violations());
478 assert!(report.summary.total_files > 0);
479 }
480
481 #[test]
482 fn test_report_formatting() {
483 let temp_dir = TempDir::new().unwrap();
484 let test_file = temp_dir.path().join("test.rs");
485
486 fs::write(&test_file, "// TODO: test").unwrap();
487
488 let validator = GuardianValidator::new().unwrap();
489 let report = validator.validate_file(&test_file).unwrap();
490
491 let human = validator
493 .format_report(&report, OutputFormat::Human)
494 .unwrap();
495 assert!(human.contains("Code Quality Violations Found"));
496
497 let json = validator
498 .format_report(&report, OutputFormat::Json)
499 .unwrap();
500 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
501 assert!(parsed["violations"].is_array());
502 }
503
504 #[tokio::test]
505 async fn test_agent_pre_commit_check() {
506 let temp_dir = TempDir::new().unwrap();
507 let clean_file = temp_dir.path().join("clean.rs");
508 let dirty_file = temp_dir.path().join("dirty.rs");
509
510 fs::write(&clean_file, "fn main() { println!(\"Hello\"); }").unwrap();
511 fs::write(&dirty_file, "fn main() { TODO: implement }").unwrap();
512
513 assert!(agent::pre_commit_check(vec![clean_file]).await.is_ok());
515
516 assert!(agent::pre_commit_check(vec![dirty_file]).await.is_err());
518 }
519
520 #[tokio::test]
521 async fn test_development_vs_production_checks() {
522 let temp_dir = TempDir::new().unwrap();
523 let test_file = temp_dir.path().join("test.rs");
524
525 fs::write(&test_file, "fn main() { /* temporary implementation */ }").unwrap();
527
528 let dev_result = agent::development_check(vec![&test_file]).await;
530 assert!(dev_result.is_ok());
531
532 let prod_result = agent::production_check(vec![&test_file]).await;
534 let _ = prod_result; }
537
538 #[test]
539 fn test_convenience_functions() {
540 let temp_dir = TempDir::new().unwrap();
541 let test_file = temp_dir.path().join("test.rs");
542
543 fs::write(&test_file, "fn main() {}").unwrap();
544
545 let validator = create_validator().unwrap();
547 assert!(validator.pattern_statistics().enabled_rules > 0);
548
549 let report = validate_directory(temp_dir.path()).unwrap();
551 assert_eq!(report.summary.total_files, 1);
552 }
553}