1use crate::{AuditError, IssueSeverity, Result};
10use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::fmt::Write;
14use std::io::Write as IoWrite;
15use std::path::PathBuf;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct AuditReport {
20 pub summary: AuditSummary,
22 pub file_results: Vec<FileAuditResult>,
24 pub issues: Vec<AuditIssue>,
26 pub recommendations: Vec<Recommendation>,
28 pub timestamp: DateTime<Utc>,
30 pub audit_config: AuditReportConfig,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct AuditSummary {
37 pub total_files: usize,
39 pub files_with_issues: usize,
41 pub total_issues: usize,
43 pub critical_issues: usize,
45 pub warning_issues: usize,
47 pub info_issues: usize,
49 pub coverage_percentage: f64,
51 pub average_issues_per_file: f64,
53 pub most_common_issue: Option<IssueCategory>,
55 pub problematic_files: Vec<ProblematicFile>,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct ProblematicFile {
62 pub path: PathBuf,
64 pub issue_count: usize,
66 pub max_severity: IssueSeverity,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct FileAuditResult {
73 pub file_path: PathBuf,
75 pub file_hash: String,
77 pub last_modified: DateTime<Utc>,
79 pub issues_count: usize,
81 pub issues: Vec<AuditIssue>,
83 pub passed: bool,
85 pub audit_duration_ms: u64,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct AuditIssue {
92 pub id: String,
94 pub file_path: PathBuf,
96 pub line_number: Option<usize>,
98 pub column_number: Option<usize>,
100 pub severity: IssueSeverity,
102 pub category: IssueCategory,
104 pub message: String,
106 pub suggestion: Option<String>,
108 pub context: Option<String>,
110 pub code_snippet: Option<String>,
112 pub related_issues: Vec<String>,
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
118pub enum IssueCategory {
119 ApiMismatch,
121 VersionInconsistency,
123 CompilationError,
125 BrokenLink,
127 MissingDocumentation,
129 DeprecatedApi,
131 InvalidImport,
133 ConfigurationError,
135 AsyncPatternError,
137 InvalidFeatureFlag,
139 InvalidCrateName,
141 QualityIssue,
143 ProcessingError,
145 ValidationError,
147}
148
149impl IssueCategory {
150 pub fn description(&self) -> &'static str {
152 match self {
153 IssueCategory::ApiMismatch => "API reference doesn't match implementation",
154 IssueCategory::VersionInconsistency => "Version numbers are inconsistent",
155 IssueCategory::CompilationError => "Code example fails to compile",
156 IssueCategory::BrokenLink => "Internal link is broken",
157 IssueCategory::MissingDocumentation => "Missing documentation for feature",
158 IssueCategory::DeprecatedApi => "References deprecated API",
159 IssueCategory::InvalidImport => "Import statement is invalid",
160 IssueCategory::ConfigurationError => "Configuration parameter error",
161 IssueCategory::AsyncPatternError => "Async/await pattern is incorrect",
162 IssueCategory::InvalidFeatureFlag => "Feature flag reference is invalid",
163 IssueCategory::InvalidCrateName => "Crate name reference is invalid",
164 IssueCategory::QualityIssue => "General documentation quality issue",
165 IssueCategory::ProcessingError => "Error occurred while processing file",
166 IssueCategory::ValidationError => "Error occurred during validation",
167 }
168 }
169
170 pub fn default_severity(&self) -> IssueSeverity {
172 match self {
173 IssueCategory::ApiMismatch => IssueSeverity::Critical,
174 IssueCategory::CompilationError => IssueSeverity::Critical,
175 IssueCategory::VersionInconsistency => IssueSeverity::Warning,
176 IssueCategory::BrokenLink => IssueSeverity::Warning,
177 IssueCategory::DeprecatedApi => IssueSeverity::Warning,
178 IssueCategory::InvalidImport => IssueSeverity::Critical,
179 IssueCategory::ConfigurationError => IssueSeverity::Warning,
180 IssueCategory::AsyncPatternError => IssueSeverity::Warning,
181 IssueCategory::InvalidFeatureFlag => IssueSeverity::Warning,
182 IssueCategory::InvalidCrateName => IssueSeverity::Warning,
183 IssueCategory::MissingDocumentation => IssueSeverity::Info,
184 IssueCategory::QualityIssue => IssueSeverity::Info,
185 IssueCategory::ProcessingError => IssueSeverity::Critical,
186 IssueCategory::ValidationError => IssueSeverity::Warning,
187 }
188 }
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize)]
193pub struct Recommendation {
194 pub id: String,
196 pub recommendation_type: RecommendationType,
198 pub priority: u8,
200 pub title: String,
202 pub description: String,
204 pub affected_files: Vec<PathBuf>,
206 pub estimated_effort_hours: Option<f32>,
208 pub resolves_issues: Vec<String>,
210}
211
212#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
214pub enum RecommendationType {
215 FixIssue,
217 StructuralImprovement,
219 AddDocumentation,
221 UpdateContent,
223 ImproveExamples,
225 EnhanceCrossReferences,
227 ProcessImprovement,
229}
230
231#[derive(Debug, Clone, Serialize, Deserialize)]
233pub struct AuditReportConfig {
234 pub min_severity: IssueSeverity,
236 pub include_suggestions: bool,
238 pub include_code_snippets: bool,
240 pub max_issues_per_file: Option<usize>,
242 pub include_statistics: bool,
244 pub include_recommendations: bool,
246}
247
248impl Default for AuditReportConfig {
249 fn default() -> Self {
250 Self {
251 min_severity: IssueSeverity::Info,
252 include_suggestions: true,
253 include_code_snippets: true,
254 max_issues_per_file: None,
255 include_statistics: true,
256 include_recommendations: true,
257 }
258 }
259}
260
261impl AuditReport {
262 pub fn new(config: AuditReportConfig) -> Self {
264 Self {
265 summary: AuditSummary::default(),
266 file_results: Vec::new(),
267 issues: Vec::new(),
268 recommendations: Vec::new(),
269 timestamp: Utc::now(),
270 audit_config: config,
271 }
272 }
273
274 pub fn add_file_result(&mut self, file_result: FileAuditResult) {
276 self.issues.extend(file_result.issues.clone());
278 self.file_results.push(file_result);
279 }
280
281 pub fn add_issue(&mut self, issue: AuditIssue) {
283 if issue.severity >= self.audit_config.min_severity {
285 self.issues.push(issue);
286 }
287 }
288
289 pub fn add_recommendation(&mut self, recommendation: Recommendation) {
291 if self.audit_config.include_recommendations {
292 self.recommendations.push(recommendation);
293 }
294 }
295
296 pub fn calculate_summary(&mut self) {
298 let total_files = self.file_results.len();
299 let files_with_issues = self.file_results.iter().filter(|f| f.issues_count > 0).count();
300
301 let total_issues = self.issues.len();
302 let critical_issues =
303 self.issues.iter().filter(|i| i.severity == IssueSeverity::Critical).count();
304 let warning_issues =
305 self.issues.iter().filter(|i| i.severity == IssueSeverity::Warning).count();
306 let info_issues = self.issues.iter().filter(|i| i.severity == IssueSeverity::Info).count();
307
308 let files_without_critical = self
310 .file_results
311 .iter()
312 .filter(|f| !f.issues.iter().any(|i| i.severity == IssueSeverity::Critical))
313 .count();
314 let coverage_percentage = if total_files > 0 {
315 (files_without_critical as f64 / total_files as f64) * 100.0
316 } else {
317 100.0
318 };
319
320 let average_issues_per_file =
321 if total_files > 0 { total_issues as f64 / total_files as f64 } else { 0.0 };
322
323 let mut category_counts: HashMap<IssueCategory, usize> = HashMap::new();
325 for issue in &self.issues {
326 *category_counts.entry(issue.category).or_insert(0) += 1;
327 }
328 let most_common_issue = category_counts
329 .into_iter()
330 .max_by_key(|(_, count)| *count)
331 .map(|(category, _)| category);
332
333 let mut file_issue_counts: Vec<_> = self
335 .file_results
336 .iter()
337 .map(|f| ProblematicFile {
338 path: f.file_path.clone(),
339 issue_count: f.issues_count,
340 max_severity: f
341 .issues
342 .iter()
343 .map(|i| i.severity)
344 .max()
345 .unwrap_or(IssueSeverity::Info),
346 })
347 .collect();
348 file_issue_counts.sort_by(|a, b| b.issue_count.cmp(&a.issue_count));
349 file_issue_counts.truncate(5);
350
351 self.summary = AuditSummary {
352 total_files,
353 files_with_issues,
354 total_issues,
355 critical_issues,
356 warning_issues,
357 info_issues,
358 coverage_percentage,
359 average_issues_per_file,
360 most_common_issue,
361 problematic_files: file_issue_counts,
362 };
363 }
364
365 pub fn passed(&self) -> bool {
367 self.summary.critical_issues == 0
368 }
369
370 pub fn issues_by_category(&self) -> HashMap<IssueCategory, Vec<&AuditIssue>> {
372 let mut categorized = HashMap::new();
373 for issue in &self.issues {
374 categorized.entry(issue.category).or_insert_with(Vec::new).push(issue);
375 }
376 categorized
377 }
378
379 pub fn issues_by_severity(&self) -> HashMap<IssueSeverity, Vec<&AuditIssue>> {
381 let mut by_severity = HashMap::new();
382 for issue in &self.issues {
383 by_severity.entry(issue.severity).or_insert_with(Vec::new).push(issue);
384 }
385 by_severity
386 }
387
388 pub fn issues_for_file(&self, file_path: &PathBuf) -> Vec<&AuditIssue> {
390 self.issues.iter().filter(|issue| &issue.file_path == file_path).collect()
391 }
392}
393
394impl Default for AuditSummary {
395 fn default() -> Self {
396 Self {
397 total_files: 0,
398 files_with_issues: 0,
399 total_issues: 0,
400 critical_issues: 0,
401 warning_issues: 0,
402 info_issues: 0,
403 coverage_percentage: 100.0,
404 average_issues_per_file: 0.0,
405 most_common_issue: None,
406 problematic_files: Vec::new(),
407 }
408 }
409}
410
411impl AuditIssue {
412 pub fn new(file_path: PathBuf, category: IssueCategory, message: String) -> Self {
414 let id = uuid::Uuid::new_v4().to_string();
415 let severity = category.default_severity();
416
417 Self {
418 id,
419 file_path,
420 line_number: None,
421 column_number: None,
422 severity,
423 category,
424 message,
425 suggestion: None,
426 context: None,
427 code_snippet: None,
428 related_issues: Vec::new(),
429 }
430 }
431
432 pub fn with_line_number(mut self, line_number: usize) -> Self {
434 self.line_number = Some(line_number);
435 self
436 }
437
438 pub fn with_column_number(mut self, column_number: usize) -> Self {
440 self.column_number = Some(column_number);
441 self
442 }
443
444 pub fn with_severity(mut self, severity: IssueSeverity) -> Self {
446 self.severity = severity;
447 self
448 }
449
450 pub fn with_suggestion(mut self, suggestion: String) -> Self {
452 self.suggestion = Some(suggestion);
453 self
454 }
455
456 pub fn with_context(mut self, context: String) -> Self {
458 self.context = Some(context);
459 self
460 }
461
462 pub fn with_code_snippet(mut self, code_snippet: String) -> Self {
464 self.code_snippet = Some(code_snippet);
465 self
466 }
467
468 pub fn with_related_issue(mut self, issue_id: String) -> Self {
470 self.related_issues.push(issue_id);
471 self
472 }
473}
474
475impl Recommendation {
476 pub fn new(
478 recommendation_type: RecommendationType,
479 title: String,
480 description: String,
481 ) -> Self {
482 let id = uuid::Uuid::new_v4().to_string();
483
484 Self {
485 id,
486 recommendation_type,
487 priority: 3, title,
489 description,
490 affected_files: Vec::new(),
491 estimated_effort_hours: None,
492 resolves_issues: Vec::new(),
493 }
494 }
495
496 pub fn with_priority(mut self, priority: u8) -> Self {
498 self.priority = priority.clamp(1, 5);
499 self
500 }
501
502 pub fn with_affected_file(mut self, file_path: PathBuf) -> Self {
504 self.affected_files.push(file_path);
505 self
506 }
507
508 pub fn with_estimated_effort(mut self, hours: f32) -> Self {
510 self.estimated_effort_hours = Some(hours);
511 self
512 }
513
514 pub fn resolves_issue(mut self, issue_id: String) -> Self {
516 self.resolves_issues.push(issue_id);
517 self
518 }
519}
520
521#[cfg(test)]
522mod tests {
523 use super::*;
524
525 #[test]
526 fn test_audit_report_creation() {
527 let config = AuditReportConfig::default();
528 let report = AuditReport::new(config);
529
530 assert_eq!(report.summary.total_files, 0);
531 assert_eq!(report.issues.len(), 0);
532 assert!(report.passed());
533 }
534
535 #[test]
536 fn test_issue_creation() {
537 let issue = AuditIssue::new(
538 PathBuf::from("test.md"),
539 IssueCategory::ApiMismatch,
540 "Test issue".to_string(),
541 )
542 .with_line_number(42)
543 .with_suggestion("Fix this".to_string());
544
545 assert_eq!(issue.file_path, PathBuf::from("test.md"));
546 assert_eq!(issue.category, IssueCategory::ApiMismatch);
547 assert_eq!(issue.severity, IssueSeverity::Critical);
548 assert_eq!(issue.line_number, Some(42));
549 assert_eq!(issue.suggestion, Some("Fix this".to_string()));
550 }
551
552 #[test]
553 fn test_issue_category_descriptions() {
554 assert_eq!(
555 IssueCategory::ApiMismatch.description(),
556 "API reference doesn't match implementation"
557 );
558 assert_eq!(IssueCategory::CompilationError.description(), "Code example fails to compile");
559 }
560
561 #[test]
562 fn test_issue_category_default_severity() {
563 assert_eq!(IssueCategory::ApiMismatch.default_severity(), IssueSeverity::Critical);
564 assert_eq!(IssueCategory::VersionInconsistency.default_severity(), IssueSeverity::Warning);
565 assert_eq!(IssueCategory::MissingDocumentation.default_severity(), IssueSeverity::Info);
566 }
567
568 #[test]
569 fn test_recommendation_creation() {
570 let rec = Recommendation::new(
571 RecommendationType::FixIssue,
572 "Fix API references".to_string(),
573 "Update all API references to match current implementation".to_string(),
574 )
575 .with_priority(1)
576 .with_estimated_effort(2.5);
577
578 assert_eq!(rec.recommendation_type, RecommendationType::FixIssue);
579 assert_eq!(rec.priority, 1);
580 assert_eq!(rec.estimated_effort_hours, Some(2.5));
581 }
582
583 #[test]
584 fn test_audit_summary_calculation() {
585 let mut report = AuditReport::new(AuditReportConfig::default());
586
587 let file1 = FileAuditResult {
589 file_path: PathBuf::from("file1.md"),
590 file_hash: "hash1".to_string(),
591 last_modified: Utc::now(),
592 issues_count: 2,
593 issues: vec![
594 AuditIssue::new(
595 PathBuf::from("file1.md"),
596 IssueCategory::ApiMismatch,
597 "Issue 1".to_string(),
598 ),
599 AuditIssue::new(
600 PathBuf::from("file1.md"),
601 IssueCategory::VersionInconsistency,
602 "Issue 2".to_string(),
603 ),
604 ],
605 passed: false,
606 audit_duration_ms: 100,
607 };
608
609 let file2 = FileAuditResult {
610 file_path: PathBuf::from("file2.md"),
611 file_hash: "hash2".to_string(),
612 last_modified: Utc::now(),
613 issues_count: 0,
614 issues: vec![],
615 passed: true,
616 audit_duration_ms: 50,
617 };
618
619 report.add_file_result(file1);
620 report.add_file_result(file2);
621 report.calculate_summary();
622
623 assert_eq!(report.summary.total_files, 2);
624 assert_eq!(report.summary.files_with_issues, 1);
625 assert_eq!(report.summary.total_issues, 2);
626 assert_eq!(report.summary.critical_issues, 1);
627 assert_eq!(report.summary.warning_issues, 1);
628 assert_eq!(report.summary.coverage_percentage, 50.0); }
630
631 #[test]
632 fn test_issues_by_category() {
633 let mut report = AuditReport::new(AuditReportConfig::default());
634
635 report.add_issue(AuditIssue::new(
636 PathBuf::from("test.md"),
637 IssueCategory::ApiMismatch,
638 "API issue".to_string(),
639 ));
640
641 report.add_issue(AuditIssue::new(
642 PathBuf::from("test.md"),
643 IssueCategory::ApiMismatch,
644 "Another API issue".to_string(),
645 ));
646
647 report.add_issue(AuditIssue::new(
648 PathBuf::from("test.md"),
649 IssueCategory::CompilationError,
650 "Compilation issue".to_string(),
651 ));
652
653 let by_category = report.issues_by_category();
654 assert_eq!(by_category.get(&IssueCategory::ApiMismatch).unwrap().len(), 2);
655 assert_eq!(by_category.get(&IssueCategory::CompilationError).unwrap().len(), 1);
656 }
657
658 #[test]
659 fn test_report_generator_json() {
660 let mut report = AuditReport::new(AuditReportConfig::default());
661
662 report.add_issue(AuditIssue::new(
663 PathBuf::from("test.md"),
664 IssueCategory::ApiMismatch,
665 "Test issue".to_string(),
666 ));
667
668 report.calculate_summary();
669
670 let generator = ReportGenerator::new(OutputFormat::Json);
671 let json_output = generator.generate_report_string(&report).unwrap();
672
673 let parsed: serde_json::Value = serde_json::from_str(&json_output).unwrap();
675 assert!(parsed.get("summary").is_some());
676 assert!(parsed.get("issues").is_some());
677 assert!(parsed.get("timestamp").is_some());
678 }
679
680 #[test]
681 fn test_report_generator_markdown() {
682 let mut report = AuditReport::new(AuditReportConfig::default());
683
684 report.add_issue(
685 AuditIssue::new(
686 PathBuf::from("test.md"),
687 IssueCategory::CompilationError,
688 "Compilation failed".to_string(),
689 )
690 .with_line_number(42),
691 );
692
693 report.calculate_summary();
694
695 let generator = ReportGenerator::new(OutputFormat::Markdown);
696 let markdown_output = generator.generate_report_string(&report).unwrap();
697
698 assert!(markdown_output.contains("# Documentation Audit Report"));
700 assert!(markdown_output.contains("## Executive Summary"));
701 assert!(markdown_output.contains("test.md"));
702 assert!(markdown_output.contains("line 42"));
703 }
704
705 #[test]
706 fn test_report_generator_console() {
707 let mut report = AuditReport::new(AuditReportConfig::default());
708
709 report.add_issue(AuditIssue::new(
710 PathBuf::from("test.md"),
711 IssueCategory::VersionInconsistency,
712 "Version mismatch".to_string(),
713 ));
714
715 report.calculate_summary();
716
717 let generator = ReportGenerator::new(OutputFormat::Console);
718 let console_output = generator.generate_report_string(&report).unwrap();
719
720 assert!(console_output.contains("DOCUMENTATION AUDIT REPORT"));
722 assert!(console_output.contains("SUMMARY"));
723 assert!(console_output.contains("Total Files:"));
724 assert!(console_output.contains("🟡 WARNING"));
725 }
726
727 #[test]
728 fn test_wrap_text() {
729 use super::wrap_text;
730
731 let text = "This is a very long line that should be wrapped at the specified width";
732 let wrapped = wrap_text(text, 20);
733
734 for line in wrapped.lines() {
735 assert!(line.len() <= 20);
736 }
737
738 let original_words: Vec<&str> = text.split_whitespace().collect();
740 let wrapped_words: Vec<&str> = wrapped.split_whitespace().collect();
741 assert_eq!(original_words, wrapped_words);
742 }
743}
744#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
746pub enum OutputFormat {
747 Json,
749 Markdown,
751 Console,
753}
754
755impl From<crate::config::OutputFormat> for OutputFormat {
756 fn from(config_format: crate::config::OutputFormat) -> Self {
757 match config_format {
758 crate::config::OutputFormat::Console => OutputFormat::Console,
759 crate::config::OutputFormat::Json => OutputFormat::Json,
760 crate::config::OutputFormat::Markdown => OutputFormat::Markdown,
761 }
762 }
763}
764
765pub struct ReportGenerator {
767 output_format: OutputFormat,
768 config: AuditReportConfig,
769}
770
771impl ReportGenerator {
772 pub fn new(output_format: OutputFormat) -> Self {
774 Self { output_format, config: AuditReportConfig::default() }
775 }
776
777 pub fn with_config(output_format: OutputFormat, config: AuditReportConfig) -> Self {
779 Self { output_format, config }
780 }
781
782 pub fn generate_report<W: IoWrite>(&self, report: &AuditReport, writer: &mut W) -> Result<()> {
784 match self.output_format {
785 OutputFormat::Json => self.generate_json_report(report, writer),
786 OutputFormat::Markdown => self.generate_markdown_report(report, writer),
787 OutputFormat::Console => self.generate_console_report(report, writer),
788 }
789 }
790
791 pub fn generate_report_string(&self, report: &AuditReport) -> Result<String> {
793 let mut buffer = Vec::new();
794 self.generate_report(report, &mut buffer)?;
795 String::from_utf8(buffer).map_err(|e| AuditError::ReportGeneration {
796 details: format!("UTF-8 conversion error: {}", e),
797 })
798 }
799
800 fn generate_json_report<W: IoWrite>(&self, report: &AuditReport, writer: &mut W) -> Result<()> {
802 let json = if self.config.include_statistics {
803 serde_json::to_string_pretty(report)
804 } else {
805 let simplified = SimplifiedReport {
807 summary: &report.summary,
808 issues: &report.issues,
809 recommendations: if self.config.include_recommendations {
810 Some(&report.recommendations)
811 } else {
812 None
813 },
814 timestamp: report.timestamp,
815 };
816 serde_json::to_string_pretty(&simplified)
817 };
818
819 let json = json.map_err(|e| AuditError::ReportGeneration {
820 details: format!("JSON serialization error: {}", e),
821 })?;
822 writer
823 .write_all(json.as_bytes())
824 .map_err(|e| AuditError::ReportGeneration { details: format!("Write error: {}", e) })?;
825
826 Ok(())
827 }
828
829 fn generate_markdown_report<W: IoWrite>(
831 &self,
832 report: &AuditReport,
833 writer: &mut W,
834 ) -> Result<()> {
835 let mut output = String::new();
836
837 writeln!(output, "# Documentation Audit Report").unwrap();
839 writeln!(output).unwrap();
840 writeln!(output, "**Generated:** {}", report.timestamp.format("%Y-%m-%d %H:%M:%S UTC"))
841 .unwrap();
842 writeln!(output, "**Status:** {}", if report.passed() { "✅ PASSED" } else { "❌ FAILED" })
843 .unwrap();
844 writeln!(output).unwrap();
845
846 writeln!(output, "## Executive Summary").unwrap();
848 writeln!(output).unwrap();
849 writeln!(output, "- **Total Files Audited:** {}", report.summary.total_files).unwrap();
850 writeln!(output, "- **Files with Issues:** {}", report.summary.files_with_issues).unwrap();
851 writeln!(output, "- **Total Issues:** {}", report.summary.total_issues).unwrap();
852 writeln!(output, "- **Critical Issues:** {}", report.summary.critical_issues).unwrap();
853 writeln!(output, "- **Warning Issues:** {}", report.summary.warning_issues).unwrap();
854 writeln!(output, "- **Info Issues:** {}", report.summary.info_issues).unwrap();
855 writeln!(
856 output,
857 "- **Documentation Coverage:** {:.1}%",
858 report.summary.coverage_percentage
859 )
860 .unwrap();
861 writeln!(output).unwrap();
862
863 if !report.issues.is_empty() {
865 writeln!(output, "## Issues by Category").unwrap();
866 writeln!(output).unwrap();
867
868 let issues_by_category = report.issues_by_category();
869 for (category, issues) in issues_by_category {
870 writeln!(output, "### {} ({} issues)", category.description(), issues.len())
871 .unwrap();
872 writeln!(output).unwrap();
873
874 for issue in
875 issues.iter().take(self.config.max_issues_per_file.unwrap_or(usize::MAX))
876 {
877 let severity_icon = match issue.severity {
878 IssueSeverity::Critical => "🔴",
879 IssueSeverity::Warning => "🟡",
880 IssueSeverity::Info => "🔵",
881 };
882
883 write!(
884 output,
885 "- {} **{}**: {}",
886 severity_icon,
887 issue.file_path.display(),
888 issue.message
889 )
890 .unwrap();
891
892 if let Some(line) = issue.line_number {
893 write!(output, " (line {})", line).unwrap();
894 }
895 writeln!(output).unwrap();
896
897 if self.config.include_suggestions {
898 if let Some(suggestion) = &issue.suggestion {
899 writeln!(output, " - *Suggestion:* {}", suggestion).unwrap();
900 }
901 }
902
903 if self.config.include_code_snippets {
904 if let Some(snippet) = &issue.code_snippet {
905 writeln!(output, " ```").unwrap();
906 writeln!(output, " {}", snippet).unwrap();
907 writeln!(output, " ```").unwrap();
908 }
909 }
910 }
911 writeln!(output).unwrap();
912 }
913 }
914
915 if !report.summary.problematic_files.is_empty() {
917 writeln!(output, "## Most Problematic Files").unwrap();
918 writeln!(output).unwrap();
919
920 for (i, file) in report.summary.problematic_files.iter().enumerate() {
921 let severity_icon = match file.max_severity {
922 IssueSeverity::Critical => "🔴",
923 IssueSeverity::Warning => "🟡",
924 IssueSeverity::Info => "🔵",
925 };
926 writeln!(
927 output,
928 "{}. {} {} ({} issues)",
929 i + 1,
930 severity_icon,
931 file.path.display(),
932 file.issue_count
933 )
934 .unwrap();
935 }
936 writeln!(output).unwrap();
937 }
938
939 if self.config.include_recommendations && !report.recommendations.is_empty() {
941 writeln!(output, "## Recommendations").unwrap();
942 writeln!(output).unwrap();
943
944 let mut sorted_recommendations = report.recommendations.clone();
945 sorted_recommendations.sort_by_key(|r| r.priority);
946
947 for rec in sorted_recommendations {
948 let priority_text = match rec.priority {
949 1 => "🔴 High",
950 2 => "🟡 Medium-High",
951 3 => "🟡 Medium",
952 4 => "🔵 Medium-Low",
953 5 => "🔵 Low",
954 _ => "🔵 Low",
955 };
956
957 writeln!(output, "### {} - {}", priority_text, rec.title).unwrap();
958 writeln!(output).unwrap();
959 writeln!(output, "{}", rec.description).unwrap();
960
961 if let Some(effort) = rec.estimated_effort_hours {
962 writeln!(output, "**Estimated Effort:** {:.1} hours", effort).unwrap();
963 }
964
965 if !rec.affected_files.is_empty() {
966 writeln!(output, "**Affected Files:**").unwrap();
967 for file in &rec.affected_files {
968 writeln!(output, "- {}", file.display()).unwrap();
969 }
970 }
971 writeln!(output).unwrap();
972 }
973 }
974
975 writer
976 .write_all(output.as_bytes())
977 .map_err(|e| AuditError::ReportGeneration { details: format!("Write error: {}", e) })?;
978
979 Ok(())
980 }
981
982 fn generate_console_report<W: IoWrite>(
984 &self,
985 report: &AuditReport,
986 writer: &mut W,
987 ) -> Result<()> {
988 let mut output = String::new();
989
990 writeln!(
992 output,
993 "╔══════════════════════════════════════════════════════════════════════════════╗"
994 )
995 .unwrap();
996 writeln!(
997 output,
998 "║ DOCUMENTATION AUDIT REPORT ║"
999 )
1000 .unwrap();
1001 writeln!(
1002 output,
1003 "╚══════════════════════════════════════════════════════════════════════════════╝"
1004 )
1005 .unwrap();
1006 writeln!(output).unwrap();
1007
1008 let status = if report.passed() { "✅ PASSED" } else { "❌ FAILED" };
1010 writeln!(output, "Status: {}", status).unwrap();
1011 writeln!(output, "Generated: {}", report.timestamp.format("%Y-%m-%d %H:%M:%S UTC"))
1012 .unwrap();
1013 writeln!(output).unwrap();
1014
1015 writeln!(
1017 output,
1018 "┌─ SUMMARY ─────────────────────────────────────────────────────────────────────┐"
1019 )
1020 .unwrap();
1021 writeln!(
1022 output,
1023 "│ Total Files: {:>8} │",
1024 report.summary.total_files
1025 )
1026 .unwrap();
1027 writeln!(
1028 output,
1029 "│ Files with Issues: {:>8} │",
1030 report.summary.files_with_issues
1031 )
1032 .unwrap();
1033 writeln!(
1034 output,
1035 "│ Total Issues: {:>8} │",
1036 report.summary.total_issues
1037 )
1038 .unwrap();
1039 writeln!(
1040 output,
1041 "│ Critical Issues: {:>8} │",
1042 report.summary.critical_issues
1043 )
1044 .unwrap();
1045 writeln!(
1046 output,
1047 "│ Warning Issues: {:>8} │",
1048 report.summary.warning_issues
1049 )
1050 .unwrap();
1051 writeln!(
1052 output,
1053 "│ Info Issues: {:>8} │",
1054 report.summary.info_issues
1055 )
1056 .unwrap();
1057 writeln!(
1058 output,
1059 "│ Coverage: {:>7.1}% │",
1060 report.summary.coverage_percentage
1061 )
1062 .unwrap();
1063 writeln!(
1064 output,
1065 "└───────────────────────────────────────────────────────────────────────────────┘"
1066 )
1067 .unwrap();
1068 writeln!(output).unwrap();
1069
1070 if report.summary.total_issues > 0 {
1072 writeln!(output, "ISSUES BY SEVERITY:").unwrap();
1073 writeln!(
1074 output,
1075 "─────────────────────────────────────────────────────────────────────────────"
1076 )
1077 .unwrap();
1078
1079 let issues_by_severity = report.issues_by_severity();
1080
1081 if let Some(critical_issues) = issues_by_severity.get(&IssueSeverity::Critical) {
1082 writeln!(output, "🔴 CRITICAL ({}):", critical_issues.len()).unwrap();
1083 for issue in critical_issues.iter().take(5) {
1084 writeln!(output, " {} - {}", issue.file_path.display(), issue.message)
1085 .unwrap();
1086 }
1087 if critical_issues.len() > 5 {
1088 writeln!(output, " ... and {} more", critical_issues.len() - 5).unwrap();
1089 }
1090 writeln!(output).unwrap();
1091 }
1092
1093 if let Some(warning_issues) = issues_by_severity.get(&IssueSeverity::Warning) {
1094 writeln!(output, "🟡 WARNING ({}):", warning_issues.len()).unwrap();
1095 for issue in warning_issues.iter().take(3) {
1096 writeln!(output, " {} - {}", issue.file_path.display(), issue.message)
1097 .unwrap();
1098 }
1099 if warning_issues.len() > 3 {
1100 writeln!(output, " ... and {} more", warning_issues.len() - 3).unwrap();
1101 }
1102 writeln!(output).unwrap();
1103 }
1104
1105 if let Some(info_issues) = issues_by_severity.get(&IssueSeverity::Info) {
1106 writeln!(output, "🔵 INFO ({}):", info_issues.len()).unwrap();
1107 for issue in info_issues.iter().take(2) {
1108 writeln!(output, " {} - {}", issue.file_path.display(), issue.message)
1109 .unwrap();
1110 }
1111 if info_issues.len() > 2 {
1112 writeln!(output, " ... and {} more", info_issues.len() - 2).unwrap();
1113 }
1114 writeln!(output).unwrap();
1115 }
1116 }
1117
1118 if !report.summary.problematic_files.is_empty() {
1120 writeln!(output, "MOST PROBLEMATIC FILES:").unwrap();
1121 writeln!(
1122 output,
1123 "─────────────────────────────────────────────────────────────────────────────"
1124 )
1125 .unwrap();
1126
1127 for (i, file) in report.summary.problematic_files.iter().enumerate() {
1128 let severity_icon = match file.max_severity {
1129 IssueSeverity::Critical => "🔴",
1130 IssueSeverity::Warning => "🟡",
1131 IssueSeverity::Info => "🔵",
1132 };
1133 writeln!(
1134 output,
1135 "{}. {} {} ({} issues)",
1136 i + 1,
1137 severity_icon,
1138 file.path.display(),
1139 file.issue_count
1140 )
1141 .unwrap();
1142 }
1143 writeln!(output).unwrap();
1144 }
1145
1146 if self.config.include_recommendations && !report.recommendations.is_empty() {
1148 writeln!(output, "TOP RECOMMENDATIONS:").unwrap();
1149 writeln!(
1150 output,
1151 "─────────────────────────────────────────────────────────────────────────────"
1152 )
1153 .unwrap();
1154
1155 let mut sorted_recommendations = report.recommendations.clone();
1156 sorted_recommendations.sort_by_key(|r| r.priority);
1157
1158 for rec in sorted_recommendations.iter().take(3) {
1159 let priority_text = match rec.priority {
1160 1 => "🔴 HIGH",
1161 2 => "🟡 MED-HIGH",
1162 3 => "🟡 MEDIUM",
1163 4 => "🔵 MED-LOW",
1164 5 => "🔵 LOW",
1165 _ => "🔵 LOW",
1166 };
1167
1168 writeln!(output, "{}: {}", priority_text, rec.title).unwrap();
1169
1170 let wrapped_desc = wrap_text(&rec.description, 75);
1172 for line in wrapped_desc.lines() {
1173 writeln!(output, " {}", line).unwrap();
1174 }
1175 writeln!(output).unwrap();
1176 }
1177
1178 if report.recommendations.len() > 3 {
1179 writeln!(
1180 output,
1181 "... and {} more recommendations",
1182 report.recommendations.len() - 3
1183 )
1184 .unwrap();
1185 writeln!(output).unwrap();
1186 }
1187 }
1188
1189 writeln!(
1191 output,
1192 "─────────────────────────────────────────────────────────────────────────────"
1193 )
1194 .unwrap();
1195 if report.passed() {
1196 writeln!(output, "✅ Audit completed successfully! No critical issues found.").unwrap();
1197 } else {
1198 writeln!(output, "❌ Audit failed. Please address critical issues before proceeding.")
1199 .unwrap();
1200 }
1201
1202 writer
1203 .write_all(output.as_bytes())
1204 .map_err(|e| AuditError::ReportGeneration { details: format!("Write error: {}", e) })?;
1205
1206 Ok(())
1207 }
1208
1209 pub fn save_to_file(&self, report: &AuditReport, file_path: &std::path::Path) -> Result<()> {
1211 use std::fs::File;
1212 use std::io::BufWriter;
1213
1214 let file = File::create(file_path).map_err(|e| AuditError::IoError {
1215 path: file_path.to_path_buf(),
1216 details: format!("Failed to create report file: {}", e),
1217 })?;
1218
1219 let mut writer = BufWriter::new(file);
1220 self.generate_report(report, &mut writer)?;
1221
1222 Ok(())
1223 }
1224}
1225
1226#[derive(Serialize)]
1228struct SimplifiedReport<'a> {
1229 summary: &'a AuditSummary,
1230 issues: &'a [AuditIssue],
1231 #[serde(skip_serializing_if = "Option::is_none")]
1232 recommendations: Option<&'a [Recommendation]>,
1233 timestamp: DateTime<Utc>,
1234}
1235
1236fn wrap_text(text: &str, width: usize) -> String {
1238 let mut result = String::new();
1239 let mut current_line = String::new();
1240
1241 for word in text.split_whitespace() {
1242 if current_line.len() + word.len() + 1 > width && !current_line.is_empty() {
1243 result.push_str(¤t_line);
1244 result.push('\n');
1245 current_line.clear();
1246 }
1247
1248 if !current_line.is_empty() {
1249 current_line.push(' ');
1250 }
1251 current_line.push_str(word);
1252 }
1253
1254 if !current_line.is_empty() {
1255 result.push_str(¤t_line);
1256 }
1257
1258 result
1259}