ferrous_forge/commands/
validate.rs

1//! Validate command implementation
2
3use crate::{
4    doc_coverage, formatting, security,
5    validation::{RustValidator, Violation},
6    Result,
7};
8use console::style;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::path::PathBuf;
12use tokio::fs;
13
14/// AI-friendly compliance report structure
15#[derive(Serialize, Deserialize)]
16struct AIReport {
17    metadata: AIMetadata,
18    summary: AISummary,
19    violations: Vec<AIViolation>,
20    fix_instructions: Vec<FixInstruction>,
21}
22
23#[derive(Serialize, Deserialize)]
24struct AIMetadata {
25    timestamp: String,
26    project_path: String,
27    ferrous_forge_version: String,
28    total_violations: usize,
29    report_version: String,
30}
31
32#[derive(Serialize, Deserialize)]
33struct AISummary {
34    compliance_percentage: f64,
35    files_analyzed: usize,
36    most_critical_issues: Vec<String>,
37    estimated_fix_time_hours: f64,
38}
39
40#[derive(Serialize, Deserialize)]
41struct AIViolation {
42    violation_type: String,
43    file: String,
44    line: usize,
45    message: String,
46    code_snippet: String,
47    suggested_fix: String,
48    auto_fixable: bool,
49    priority: u8,
50}
51
52#[derive(Serialize, Deserialize)]
53struct FixInstruction {
54    violation_type: String,
55    count: usize,
56    fix_strategy: String,
57    example_fix: String,
58    effort_level: String,
59}
60
61/// Execute the validate command
62pub async fn execute(
63    path: Option<PathBuf>,
64    ai_report: bool,
65    _compare_previous: bool,
66) -> Result<()> {
67    let project_path = path.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
68
69    println!(
70        "{}",
71        style("šŸ¦€ Running Ferrous Forge validation...")
72            .bold()
73            .cyan()
74    );
75    println!("šŸ“ Project: {}", project_path.display());
76    println!();
77
78    // Create validator
79    let validator = RustValidator::new(project_path.clone())?;
80
81    // Run validation
82    let violations = validator.validate_project().await?;
83
84    // Generate and display report
85    let report = validator.generate_report(&violations);
86    println!("{}", report);
87
88    // Generate AI-friendly report if requested
89    if ai_report {
90        println!("\nšŸ¤– Generating AI-friendly compliance report...");
91        generate_ai_report(&project_path, &violations).await?;
92    }
93
94    // Run clippy with our strict configuration
95    println!(
96        "{}",
97        style("šŸ”§ Running Clippy with strict configuration...")
98            .bold()
99            .yellow()
100    );
101    let clippy_result = validator.run_clippy().await?;
102
103    if !clippy_result.success {
104        println!("{}", style("āŒ Clippy found issues:").red());
105        println!("{}", clippy_result.output);
106    } else {
107        println!("{}", style("āœ… Clippy validation passed!").green());
108    }
109
110    // Check documentation coverage
111    println!();
112    println!(
113        "{}",
114        style("šŸ“š Checking documentation coverage...")
115            .bold()
116            .yellow()
117    );
118    match doc_coverage::check_documentation_coverage(&project_path).await {
119        Ok(coverage) => {
120            println!("{}", coverage.report());
121            if coverage.coverage_percent < 80.0 {
122                println!("{}", style("āš ļø  Documentation coverage below 80%").yellow());
123            }
124        }
125        Err(e) => {
126            println!(
127                "{}",
128                style(format!("āš ļø  Could not check documentation: {}", e)).yellow()
129            );
130        }
131    }
132
133    // Check formatting
134    println!();
135    println!(
136        "{}",
137        style("šŸ“ Checking code formatting...").bold().yellow()
138    );
139    match formatting::check_formatting(&project_path).await {
140        Ok(format_result) => {
141            println!("{}", format_result.report());
142        }
143        Err(e) => {
144            println!(
145                "{}",
146                style(format!("āš ļø  Could not check formatting: {}", e)).yellow()
147            );
148        }
149    }
150
151    // Run security audit
152    println!();
153    println!("{}", style("šŸ”’ Running security audit...").bold().yellow());
154    match security::run_security_audit(&project_path).await {
155        Ok(audit_report) => {
156            println!("{}", audit_report.report());
157        }
158        Err(e) => {
159            println!(
160                "{}",
161                style(format!("āš ļø  Could not run security audit: {}", e)).yellow()
162            );
163        }
164    }
165
166    // Exit with error code if violations found
167    if !violations.is_empty() || !clippy_result.success {
168        std::process::exit(1);
169    } else {
170        println!();
171        println!(
172            "{}",
173            style("šŸŽ‰ All validations passed! Code meets Ferrous Forge standards.")
174                .bold()
175                .green()
176        );
177    }
178
179    Ok(())
180}
181
182/// Generate AI-friendly compliance report
183async fn generate_ai_report(project_path: &PathBuf, violations: &[Violation]) -> Result<()> {
184    use chrono::Utc;
185
186    // Create reports directory
187    let reports_dir = project_path.join(".ferrous-forge").join("reports");
188    fs::create_dir_all(&reports_dir).await?;
189
190    // Generate timestamp
191    let timestamp = Utc::now();
192    let timestamp_str = timestamp.format("%Y%m%d_%H%M%S").to_string();
193
194    // Count violations by type
195    let mut violation_counts = HashMap::new();
196    for violation in violations {
197        *violation_counts
198            .entry(format!("{:?}", violation.violation_type))
199            .or_insert(0) += 1;
200    }
201
202    // Create AI violations with context
203    let mut ai_violations = Vec::new();
204    for violation in violations.iter().take(50) {
205        // Limit to 50 for AI processing
206        let code_snippet = get_code_snippet(&violation.file, violation.line)
207            .await
208            .unwrap_or_else(|_| "Could not read file".to_string());
209
210        let (suggested_fix, auto_fixable, priority) = match violation.violation_type {
211            crate::validation::ViolationType::UnderscoreBandaid => {
212                if violation.message.contains("parameter") {
213                    (
214                        "Remove unused parameter or implement missing functionality".to_string(),
215                        false,
216                        2,
217                    )
218                } else {
219                    (
220                        "Replace `let _ =` with proper error handling using `?`".to_string(),
221                        true,
222                        1,
223                    )
224                }
225            }
226            crate::validation::ViolationType::UnwrapInProduction => (
227                "Replace `.unwrap()` with `?` or proper error handling".to_string(),
228                true,
229                1,
230            ),
231            crate::validation::ViolationType::FileTooLarge => (
232                "Split file into smaller modules following single responsibility principle"
233                    .to_string(),
234                false,
235                4,
236            ),
237            crate::validation::ViolationType::FunctionTooLarge => (
238                "Extract helper functions or split into smaller, focused functions".to_string(),
239                false,
240                3,
241            ),
242            _ => (
243                "Review and fix according to Ferrous Forge standards".to_string(),
244                false,
245                3,
246            ),
247        };
248
249        ai_violations.push(AIViolation {
250            violation_type: format!("{:?}", violation.violation_type),
251            file: violation.file.display().to_string(),
252            line: violation.line + 1, // Convert to 1-based
253            message: violation.message.clone(),
254            code_snippet,
255            suggested_fix,
256            auto_fixable,
257            priority,
258        });
259    }
260
261    // Generate fix instructions
262    let mut fix_instructions = Vec::new();
263    for (vtype, count) in violation_counts {
264        let (strategy, example, effort) = match vtype.as_str() {
265            "UnderscoreBandaid" => (
266                "1. Identify what functionality the parameter should provide\n2. Either implement the functionality or remove the parameter\n3. Update function signature and callers".to_string(),
267                "// Before: fn process(_unused: String, data: Data)\n// After: fn process(data: Data) or implement the unused parameter".to_string(),
268                "Moderate".to_string(),
269            ),
270            "UnwrapInProduction" => (
271                "1. Change function to return Result<T, Error>\n2. Replace .unwrap() with ?\n3. Handle errors at call sites".to_string(),
272                "// Before: value.unwrap()\n// After: value?".to_string(),
273                "Easy".to_string(),
274            ),
275            "FileTooLarge" => (
276                "1. Identify logical boundaries in the file\n2. Create new module directory\n3. Split into focused modules\n4. Update imports".to_string(),
277                "// Split validation.rs into validation/mod.rs, validation/core.rs, validation/types.rs".to_string(),
278                "Hard".to_string(),
279            ),
280            _ => ("Review and fix manually".to_string(), "".to_string(), "Moderate".to_string()),
281        };
282
283        fix_instructions.push(FixInstruction {
284            violation_type: vtype,
285            count,
286            fix_strategy: strategy,
287            example_fix: example,
288            effort_level: effort,
289        });
290    }
291
292    // Calculate compliance
293    let total_files = count_rust_files(project_path).await?;
294    let files_with_violations = violations
295        .iter()
296        .map(|v| &v.file)
297        .collect::<std::collections::HashSet<_>>()
298        .len();
299
300    let compliance_percentage = if total_files > 0 && files_with_violations <= total_files {
301        ((total_files - files_with_violations) as f64 / total_files as f64) * 100.0
302    } else {
303        0.0 // If we have more violations than files, compliance is 0%
304    };
305
306    // Create report
307    let report = AIReport {
308        metadata: AIMetadata {
309            timestamp: timestamp.to_rfc3339(),
310            project_path: project_path.display().to_string(),
311            ferrous_forge_version: env!("CARGO_PKG_VERSION").to_string(),
312            total_violations: violations.len(),
313            report_version: "1.0.0".to_string(),
314        },
315        summary: AISummary {
316            compliance_percentage,
317            files_analyzed: total_files,
318            most_critical_issues: vec![
319                "UnderscoreBandaid violations (implement missing functionality)".to_string(),
320                "Large files need splitting (validation.rs: 1133 lines)".to_string(),
321                "Large functions need refactoring".to_string(),
322            ],
323            estimated_fix_time_hours: violations.len() as f64 * 0.25, // 15 minutes per violation average
324        },
325        violations: ai_violations,
326        fix_instructions,
327    };
328
329    // Save JSON report
330    let json_path = reports_dir.join(format!("ai_compliance_{}.json", timestamp_str));
331    let json_content = serde_json::to_string_pretty(&report)
332        .map_err(|e| crate::Error::config(format!("Failed to serialize AI report: {}", e)))?;
333    fs::write(&json_path, json_content).await?;
334
335    // Save human-readable markdown
336    let md_path = reports_dir.join(format!("ai_compliance_{}.md", timestamp_str));
337    let md_content = generate_markdown_report(&report);
338    fs::write(&md_path, md_content).await?;
339
340    // Create latest links
341    let latest_json = reports_dir.join("latest_ai_report.json");
342    let latest_md = reports_dir.join("latest_ai_report.md");
343    fs::copy(&json_path, &latest_json).await?;
344    fs::copy(&md_path, &latest_md).await?;
345
346    println!("šŸ“Š AI Compliance Report Generated:");
347    println!("  šŸ“„ JSON: {}", json_path.display());
348    println!("  šŸ“ Markdown: {}", md_path.display());
349    println!("  šŸ”— Latest JSON: {}", latest_json.display());
350    println!("  šŸ”— Latest MD: {}", latest_md.display());
351    println!("\nšŸ¤– This report is optimized for AI assistant consumption");
352    println!("   Use the JSON file for automated processing and fix suggestions");
353
354    Ok(())
355}
356
357/// Get code snippet around a violation
358async fn get_code_snippet(file_path: &PathBuf, line: usize) -> Result<String> {
359    if !file_path.exists() {
360        return Ok("File not found".to_string());
361    }
362
363    let contents = fs::read_to_string(file_path).await?;
364    let lines: Vec<&str> = contents.lines().collect();
365
366    if line < lines.len() {
367        Ok(lines[line].to_string())
368    } else {
369        Ok("Line not found".to_string())
370    }
371}
372
373/// Count Rust files in project
374async fn count_rust_files(project_path: &PathBuf) -> Result<usize> {
375    let mut count = 0;
376
377    // Simple count for now - would need recursive implementation for full accuracy
378    let mut entries = fs::read_dir(project_path).await?;
379    while let Some(entry) = entries.next_entry().await? {
380        if let Some(ext) = entry.path().extension() {
381            if ext == "rs" {
382                count += 1;
383            }
384        }
385    }
386
387    Ok(count.max(1))
388}
389
390/// Generate human-readable markdown from AI report
391fn generate_markdown_report(report: &AIReport) -> String {
392    let mut md = String::new();
393
394    md.push_str("# šŸ¤– AI-Friendly Compliance Report\n\n");
395    md.push_str(&format!("**Generated**: {}\n", report.metadata.timestamp));
396    md.push_str(&format!("**Project**: {}\n", report.metadata.project_path));
397    md.push_str(&format!(
398        "**Total Violations**: {}\n",
399        report.metadata.total_violations
400    ));
401    md.push_str(&format!(
402        "**Compliance**: {:.1}%\n\n",
403        report.summary.compliance_percentage
404    ));
405
406    md.push_str("## šŸŽÆ Fix Priority Order\n\n");
407    md.push_str("1. **UnwrapInProduction** - Critical for safety\n");
408    md.push_str("2. **UnderscoreBandaid** - Implement missing functionality\n");
409    md.push_str("3. **FunctionTooLarge** - Refactor for maintainability\n");
410    md.push_str("4. **FileTooLarge** - Split into modules\n\n");
411
412    md.push_str("## šŸ”§ Automated Fix Commands\n\n");
413    md.push_str("```bash\n");
414    md.push_str("# Generate this report\n");
415    md.push_str("ferrous-forge validate . --ai-report\n\n");
416    md.push_str("# Use AI assistant with the JSON report to implement fixes\n");
417    md.push_str("# The JSON contains structured data for automated processing\n");
418    md.push_str("```\n\n");
419
420    md.push_str("## šŸ“Š Violation Summary\n\n");
421    for instruction in &report.fix_instructions {
422        md.push_str(&format!(
423            "### {} ({} violations)\n",
424            instruction.violation_type, instruction.count
425        ));
426        md.push_str(&format!("**Strategy**: {}\n\n", instruction.fix_strategy));
427        md.push_str(&format!(
428            "**Example**: \n```rust\n{}\n```\n\n",
429            instruction.example_fix
430        ));
431    }
432
433    md
434}