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 { analyzer, cache: None, report_formatter })
74 }
75
76 pub fn new() -> GuardianResult<Self> {
78 Self::new_with_config(GuardianConfig::default())
79 }
80
81 pub fn from_config_file<P: AsRef<Path>>(path: P) -> GuardianResult<Self> {
83 let config = GuardianConfig::load_from_file(path)?;
84 Self::new_with_config(config)
85 }
86
87 pub fn with_cache<P: AsRef<Path>>(mut self, cache_path: P) -> GuardianResult<Self> {
89 let mut cache = FileCache::new(cache_path);
90 cache.load()?;
91 cache.set_config_fingerprint(self.analyzer.config_fingerprint());
92 self.cache = Some(cache);
93 Ok(self)
94 }
95
96 pub fn with_report_formatter(mut self, formatter: ReportFormatter) -> Self {
98 self.report_formatter = formatter;
99 self
100 }
101
102 pub async fn validate_for_agent<P: AsRef<Path>>(
104 &mut self,
105 paths: Vec<P>,
106 ) -> GuardianResult<ValidationReport> {
107 self.validate_with_options(paths, &ValidationOptions::default()).await
108 }
109
110 pub async fn validate_with_options<P: AsRef<Path>>(
112 &mut self,
113 paths: Vec<P>,
114 options: &ValidationOptions,
115 ) -> GuardianResult<ValidationReport> {
116 let paths: Vec<PathBuf> = paths.iter().map(|p| p.as_ref().to_path_buf()).collect();
118
119 let report = if options.use_cache && self.cache.is_some() {
121 self.analyze_with_cache(&paths, &options.analysis_options).await?
122 } else {
123 self.analyzer.analyze_paths(
124 &paths.iter().map(|p| p.as_path()).collect::<Vec<_>>(),
125 &options.analysis_options,
126 )?
127 };
128
129 Ok(report)
130 }
131
132 pub fn validate_file<P: AsRef<Path>>(&self, file_path: P) -> GuardianResult<ValidationReport> {
134 let violations = self.analyzer.analyze_file(file_path)?;
135
136 let mut report = ValidationReport::new();
137 for violation in violations {
138 report.add_violation(violation);
139 }
140 report.set_files_analyzed(1);
141
142 Ok(report)
143 }
144
145 pub fn validate_directory<P: AsRef<Path>>(
147 &self,
148 root: P,
149 options: &AnalysisOptions,
150 ) -> GuardianResult<ValidationReport> {
151 self.analyzer.analyze_directory(root, options)
152 }
153
154 pub fn format_report(
156 &self,
157 report: &ValidationReport,
158 format: OutputFormat,
159 ) -> GuardianResult<String> {
160 self.report_formatter.format_report(report, format)
161 }
162
163 pub fn pattern_statistics(&self) -> PatternStats {
165 self.analyzer.pattern_stats()
166 }
167
168 pub fn cache_statistics(&self) -> Option<CacheStatistics> {
170 self.cache.as_ref().map(|c| c.statistics())
171 }
172
173 pub fn clear_cache(&mut self) -> GuardianResult<()> {
175 if let Some(cache) = &mut self.cache {
176 cache.clear()?;
177 }
178 Ok(())
179 }
180
181 pub fn save_cache(&mut self) -> GuardianResult<()> {
183 if let Some(cache) = &mut self.cache {
184 cache.save()?;
185 }
186 Ok(())
187 }
188
189 pub fn cleanup_cache(&mut self) -> GuardianResult<Option<usize>> {
191 if let Some(cache) = &mut self.cache { Ok(Some(cache.cleanup()?)) } else { Ok(None) }
192 }
193
194 async fn analyze_with_cache(
196 &mut self,
197 paths: &[PathBuf],
198 options: &AnalysisOptions,
199 ) -> GuardianResult<ValidationReport> {
200 let mut all_violations = Vec::new();
201 let files_analyzed: usize;
202 let start_time = std::time::Instant::now();
203
204 let config_fingerprint = self.analyzer.config_fingerprint();
206
207 let mut all_files = Vec::new();
209 for path in paths {
210 if path.is_file() {
211 all_files.push(path.clone());
212 } else if path.is_dir() {
213 let temp_report = self.analyzer.analyze_directory(path, options)?;
215 let discovered_files: std::collections::HashSet<PathBuf> =
217 temp_report.violations.iter().map(|v| v.file_path.clone()).collect();
218 all_files.extend(discovered_files);
219 }
220 }
221
222 if let Some(cache) = &mut self.cache {
223 let mut files_to_analyze = Vec::new();
225 let mut _cached_violation_count = 0;
226
227 for file_path in &all_files {
228 match cache.needs_analysis(file_path, &config_fingerprint) {
229 Ok(needs_analysis) => {
230 if needs_analysis {
231 files_to_analyze.push(file_path.clone());
232 } else {
233 _cached_violation_count += 1; }
238 }
239 Err(e) => {
240 tracing::warn!("Cache check failed for {}: {}", file_path.display(), e);
242 files_to_analyze.push(file_path.clone());
243 }
244 }
245 }
246
247 if !files_to_analyze.is_empty() {
249 let fresh_report = self.analyzer.analyze_paths(
250 &files_to_analyze.iter().map(|p| p.as_path()).collect::<Vec<_>>(),
251 options,
252 )?;
253
254 all_violations.extend(fresh_report.violations);
255 for file_path in &files_to_analyze {
259 let file_violations: Vec<_> =
260 all_violations.iter().filter(|v| v.file_path == *file_path).collect();
261
262 if let Err(e) =
263 cache.update_entry(file_path, file_violations.len(), &config_fingerprint)
264 {
265 tracing::warn!("Failed to update cache for {}: {}", file_path.display(), e);
266 }
267 }
268 }
269
270 files_analyzed = all_files.len(); } else {
272 let report = self.analyzer.analyze_paths(
274 &all_files.iter().map(|p| p.as_path()).collect::<Vec<_>>(),
275 options,
276 )?;
277
278 all_violations.extend(report.violations);
279 files_analyzed = report.summary.total_files;
280 }
281
282 let mut report = ValidationReport::new();
284 for violation in all_violations {
285 report.add_violation(violation);
286 }
287
288 report.set_files_analyzed(files_analyzed);
289 report.set_execution_time(start_time.elapsed().as_millis() as u64);
290 report.set_config_fingerprint(config_fingerprint);
291 report.sort_violations();
292
293 Ok(report)
294 }
295}
296
297pub fn create_validator() -> GuardianResult<GuardianValidator> {
299 GuardianValidator::new()
300}
301
302pub async fn validate_files<P: AsRef<Path>>(files: Vec<P>) -> GuardianResult<ValidationReport> {
304 let mut validator = GuardianValidator::new()?;
305 validator.validate_for_agent(files).await
306}
307
308pub fn validate_directory<P: AsRef<Path>>(directory: P) -> GuardianResult<ValidationReport> {
310 let validator = GuardianValidator::new()?;
311 validator.validate_directory(directory, &AnalysisOptions::default())
312}
313
314pub mod agent {
316 use super::*;
317
318 pub async fn pre_commit_check<P: AsRef<Path>>(modified_files: Vec<P>) -> GuardianResult<()> {
324 let mut validator = GuardianValidator::new()?;
325 let report = validator.validate_for_agent(modified_files).await?;
326
327 if report.has_errors() {
328 let error_count = report.summary.violations_by_severity.error;
329 return Err(GuardianError::config(format!(
330 "Pre-commit check failed: {} blocking violation{} found",
331 error_count,
332 if error_count == 1 { "" } else { "s" }
333 )));
334 }
335
336 Ok(())
337 }
338
339 pub async fn development_check<P: AsRef<Path>>(
344 files: Vec<P>,
345 ) -> GuardianResult<ValidationReport> {
346 let options = ValidationOptions {
347 analysis_options: AnalysisOptions {
348 fail_fast: false,
349 parallel: true,
350 ..Default::default()
351 },
352 report_options: ReportOptions {
353 min_severity: Some(Severity::Warning),
354 ..Default::default()
355 },
356 ..Default::default()
357 };
358
359 let mut validator = GuardianValidator::new()?;
360 validator.validate_with_options(files, &options).await
361 }
362
363 pub async fn production_check<P: AsRef<Path>>(
368 files: Vec<P>,
369 ) -> GuardianResult<ValidationReport> {
370 let options = ValidationOptions {
371 analysis_options: AnalysisOptions {
372 fail_fast: true,
373 parallel: true,
374 ..Default::default()
375 },
376 report_options: ReportOptions {
377 min_severity: Some(Severity::Warning),
378 show_suggestions: true,
379 ..Default::default()
380 },
381 ..Default::default()
382 };
383
384 let mut validator = GuardianValidator::new()?;
385 let report = validator.validate_with_options(files, &options).await?;
386
387 if report.has_violations() {
389 return Err(GuardianError::config(format!(
390 "Production validation failed: {} violations found",
391 report.violations.len()
392 )));
393 }
394
395 Ok(report)
396 }
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402 use std::fs;
403 use tempfile::TempDir;
404
405 #[tokio::test]
406 async fn test_validator_creation() {
407 let validator = GuardianValidator::new().unwrap();
408 let stats = validator.pattern_statistics();
409
410 assert!(stats.enabled_rules > 0);
412 }
413
414 #[tokio::test]
415 async fn test_validate_for_agent() {
416 let temp_dir = TempDir::new().unwrap();
417 let test_file = temp_dir.path().join("test.rs");
418
419 fs::write(&test_file, "// TODO: implement this\nfn main() {}").unwrap();
421
422 let mut validator = GuardianValidator::new().unwrap();
423 let report = validator.validate_for_agent(vec![test_file]).await.unwrap();
424
425 assert!(report.has_violations());
427 assert!(report.violations.iter().any(|v| v.rule_id.contains("todo")));
428 }
429
430 #[test]
431 fn test_single_file_validation() {
432 let temp_dir = TempDir::new().unwrap();
433 let test_file = temp_dir.path().join("test.rs");
434
435 fs::write(&test_file, "fn main() { unimplemented!() }").unwrap();
436
437 let validator = GuardianValidator::new().unwrap();
438 let report = validator.validate_file(&test_file).unwrap();
439
440 assert!(report.has_violations());
441 assert_eq!(report.summary.total_files, 1);
442 }
443
444 #[test]
445 fn test_directory_validation() {
446 let temp_dir = TempDir::new().unwrap();
447 let root = temp_dir.path();
448
449 fs::create_dir_all(root.join("src")).unwrap();
451 fs::write(root.join("src/lib.rs"), "// TODO: implement").unwrap();
452 fs::write(root.join("src/main.rs"), "fn main() {}").unwrap();
453
454 let validator = GuardianValidator::new().unwrap();
455 let report = validator.validate_directory(root, &AnalysisOptions::default()).unwrap();
456
457 assert!(report.has_violations());
458 assert!(report.summary.total_files > 0);
459 }
460
461 #[test]
462 fn test_report_formatting() {
463 let temp_dir = TempDir::new().unwrap();
464 let test_file = temp_dir.path().join("test.rs");
465
466 fs::write(&test_file, "// TODO: test").unwrap();
467
468 let validator = GuardianValidator::new().unwrap();
469 let report = validator.validate_file(&test_file).unwrap();
470
471 let human = validator.format_report(&report, OutputFormat::Human).unwrap();
473 assert!(human.contains("Code Quality Violations Found"));
474
475 let json = validator.format_report(&report, OutputFormat::Json).unwrap();
476 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
477 assert!(parsed["violations"].is_array());
478 }
479
480 #[tokio::test]
481 async fn test_agent_pre_commit_check() {
482 let temp_dir = TempDir::new().unwrap();
483 let clean_file = temp_dir.path().join("clean.rs");
484 let dirty_file = temp_dir.path().join("dirty.rs");
485
486 fs::write(&clean_file, "fn main() { println!(\"Hello\"); }").unwrap();
487 fs::write(&dirty_file, "fn main() { TODO: implement }").unwrap();
488
489 assert!(agent::pre_commit_check(vec![clean_file]).await.is_ok());
491
492 assert!(agent::pre_commit_check(vec![dirty_file]).await.is_err());
494 }
495
496 #[tokio::test]
497 async fn test_development_vs_production_checks() {
498 let temp_dir = TempDir::new().unwrap();
499 let test_file = temp_dir.path().join("test.rs");
500
501 fs::write(&test_file, "fn main() { /* temporary implementation */ }").unwrap();
503
504 let dev_result = agent::development_check(vec![&test_file]).await;
506 assert!(dev_result.is_ok());
507
508 let prod_result = agent::production_check(vec![&test_file]).await;
510 let _ = prod_result; }
513
514 #[test]
515 fn test_convenience_functions() {
516 let temp_dir = TempDir::new().unwrap();
517 let test_file = temp_dir.path().join("test.rs");
518
519 fs::write(&test_file, "fn main() {}").unwrap();
520
521 let validator = create_validator().unwrap();
523 assert!(validator.pattern_statistics().enabled_rules > 0);
524
525 let report = validate_directory(temp_dir.path()).unwrap();
527 assert_eq!(report.summary.total_files, 1);
528 }
529}