rust_checker 1.0.0

A modular Rust code validation tool with HTML, JSON, SVG badge, and JUnit XML report export. Includes optional web dashboard and PQC guardrails via plugins.
Documentation
use chrono::Utc;
use colored::*;
use rayon::prelude::*;
use rust_checker::{
    config::Config,
    fixer::auto_fix_unused_imports,
    plugin::{compile_and_run_plugins, generate_plugin_template},
    pqc_hybrid,
    report::{
        badge::export_svg_badge, html::export_to_html, print_json_report, FileValidationResult,
        ValidationSummary,
    },
    rules::RuleConfig,
    scanner::scan_rust_files,
    tooling::{run_clippy_check, run_fmt_check},
    validate_rust_file,
};
use std::env;
use std::fs;
use std::process::Command;
use std::str;

fn main() {
    let args: Vec<String> = env::args().collect();
    // Quick production-style demo for PQC hybrid flow
    if args.iter().any(|a| a == "--run-pqc-hybrid") {
        let len = pqc_hybrid::run_hybrid_demo();
        println!("PQC hybrid demo complete: shared secret length = {len} bytes");
        return;
    }

    // Plugin scaffolding shortcut
    if args.len() == 3 && args[1] == "--gen-plugin" {
        match generate_plugin_template(&args[2]) {
            Ok(_) => {
                println!("Plugin template created at plugins/{}.rs", args[2]);
                return;
            }
            Err(e) => {
                eprintln!("Failed to generate plugin: {}", e);
                std::process::exit(1);
            }
        }
    }

    // PQC plugin scaffolding
    if args.len() == 2 && args[1] == "--gen-pqc-plugins" {
        if let Err(e) = gen_pqc_plugins() {
            eprintln!("Failed to scaffold PQC plugins: {e}");
            std::process::exit(1);
        }
        println!("PQC plugins created in ./plugins: cargo_toml_pqc.rs, code_pqc.rs");
        return;
    }

    if args.len() < 2 {
        eprintln!(
            "{}",
            "Usage: cargo run -- <path_to_rust_project> [flags]".red()
        );
        eprintln!("{}", "\nOptional flags:".blue());
        eprintln!("  --skip-main             Skip `fn main` check");
        eprintln!("  --allow-unused-var      Allow `let _` pattern");
        eprintln!("  --allow-unused-import   Allow unused `use` statements");
        eprintln!("  --json                  Output report as JSON");
        eprintln!("  --check-fmt             Run `cargo fmt --check`");
        eprintln!("  --check-clippy          Run `cargo clippy --quiet -- -Dwarnings`");
        eprintln!("  --fix                   Auto-fix unused imports (heuristic)");
        eprintln!("  --gen-plugin <name>     Scaffold a new plugin in /plugins");
        eprintln!("  --run-pqc-hybrid        Run Kyber→HKDF(ring)→ChaCha20-Poly1305 demo\n");
        std::process::exit(1);
    }

    let project_path = &args[1];
    let file_config = Config::load(".rustchecker.toml");
    let config = RuleConfig::from_args_and_config(&args, file_config.rules);
    let output_json = args.contains(&"--json".to_string());
    let check_fmt = args.contains(&"--check-fmt".to_string());
    let check_clippy = args.contains(&"--check-clippy".to_string());
    let auto_fix = args.contains(&"--fix".to_string());

    println!(
        "{}",
        format!(
            "[{}] Checking Rust project recursively at: {}\n",
            Utc::now(),
            project_path
        )
        .blue()
    );

    if check_fmt {
        if run_fmt_check(project_path) {
            println!("{}", "cargo fmt check passed.".green());
        } else {
            eprintln!(
                "{}",
                "cargo fmt check failed. Please format your code.".red()
            );
        }
    }

    if check_clippy {
        if run_clippy_check(project_path) {
            println!("{}", "cargo clippy check passed.".green());
        } else {
            eprintln!(
                "{}",
                "cargo clippy check failed. Please fix lint issues.".red()
            );
        }
    }

    let output = Command::new("cargo")
        .arg("check")
        .current_dir(project_path)
        .output()
        .expect("Failed to execute cargo check");

    if output.status.success() {
        println!("{}", "No compilation errors found.\n".green());
    } else {
        println!("{}", "Compilation errors detected:".red());
        let stderr = str::from_utf8(&output.stderr).unwrap_or("Unknown error");
        parse_and_display_errors(stderr);
    }

    let rust_files = scan_rust_files(project_path);
    if rust_files.is_empty() {
        println!("{}", "No .rs files found in the directory.".yellow());
        return;
    }

    if auto_fix {
        for file_path in &rust_files {
            match auto_fix_unused_imports(file_path) {
                Ok(_) => println!("Auto-fixed unused imports in {}", file_path),
                Err(e) => eprintln!("Failed to fix {}: {}", file_path, e),
            }
        }
    }

    let results: Vec<_> = rust_files
        .par_iter()
        .map(|file_path| {
            let result = validate_rust_file(file_path, &config);
            (file_path.clone(), result)
        })
        .collect();

    let mut passed = 0;
    let mut failed = 0;
    let mut output_results = Vec::new();

    for (file_path, result) in results {
        match result {
            Ok(_) => {
                println!("{}", format!("{} passed validation.", file_path).green());
                passed += 1;
                output_results.push(FileValidationResult {
                    file: file_path,
                    passed: true,
                    error: None,
                });
            }
            Err(e) => {
                eprintln!(
                    "{}",
                    format!("{} failed validation: {}", file_path, e).red()
                );
                failed += 1;
                output_results.push(FileValidationResult {
                    file: file_path,
                    passed: false,
                    error: Some(e),
                });
            }
        }
    }

    let summary = ValidationSummary {
        total_files: passed + failed,
        passed,
        failed,
        results: output_results,
    };

    if output_json {
        print_json_report(&summary);
    } else {
        println!(
            "\n{}",
            format!(
                "Summary: {} passed | {} failed | {} total files checked",
                summary.passed, summary.failed, summary.total_files
            )
            .bold()
        );
    }

    if let Err(e) = export_to_html(&summary, "target/report.html") {
        eprintln!("Failed to export HTML report: {}", e);
    } else {
        println!("{}", "HTML report saved to target/report.html".blue());
    }

    if let Err(e) = export_svg_badge(&summary, "target/status-badge.svg") {
        eprintln!("Failed to export badge: {}", e);
    } else {
        println!("{}", "Badge saved to target/status-badge.svg".blue());
    }

    let plugin_results = compile_and_run_plugins("plugins");
    if !plugin_results.is_empty() {
        println!("\nPlugin execution results:");
        for (name, result) in plugin_results {
            match result {
                Ok(_) => println!("Plugin {} passed.", name),
                Err(e) => eprintln!("Plugin {} failed: {}", name, e),
            }
        }
    }

    if failed > 0 {
        std::process::exit(1);
    }
}

fn parse_and_display_errors(output: &str) {
    for line in output.lines() {
        if line.contains("error:") {
            println!("{}", format!("\n{}", line).red());
        } else if line.contains("--> ") {
            println!("{}", line.yellow());
        } else {
            println!("  {}", line);
        }
    }
}

/// Scaffold two std-only PQC plugins under ./plugins
fn gen_pqc_plugins() -> Result<(), String> {
    fs::create_dir_all("plugins").map_err(|e| format!("mkdir plugins: {e}"))?;

    // Cargo.toml heuristic checker (std-only)
    let cargo_toml_pqc = r#"
use std::fs;

fn main() {
    let Ok(s) = fs::read_to_string("Cargo.toml") else {
        println!("err: cannot read Cargo.toml");
        return;
    };

    // Lightweight heuristics (no deps)
    let has_pq = s.contains("pqcrypto") || s.contains("oqs");
    let has_zeroize = s.contains("zeroize");
    let has_subtle = s.contains("subtle");

    if !has_pq {
        println!("err: no PQC crate detected (add pqcrypto-* or oqs)");
        return;
    }
    if !has_zeroize {
        println!("err: missing `zeroize` dependency for secret wiping");
        return;
    }
    if !has_subtle {
        println!("err: missing `subtle` for constant-time equality");
        return;
    }

    println!("ok");
}
"#;

    // Source code heuristic checker (std-only)
    let code_pqc = r#"
use std::{fs, path::Path};

fn main() {
    let mut uses_thread_rng_for_secret = false;
    let mut saw_osrng = false;
    let mut saw_const_time_eq = false;
    let mut saw_zeroize = false;

    let files = collect_rs_files("src");
    for f in files {
        let Ok(src) = fs::read_to_string(&f) else { continue; };

        if src.contains("thread_rng()") && looks_like_secret_context(&src) {
            uses_thread_rng_for_secret = true;
        }
        if src.contains("rand::rngs::OsRng") || src.contains("rand_core::OsRng") {
            saw_osrng = true;
        }
        if src.contains("subtle::ConstantTimeEq")
            || src.contains(".ct_eq(") || src.contains("ct_eq(") {
            saw_const_time_eq = true;
        }
        if src.contains("derive(Zeroize") || src.contains("zeroize::Zeroize") {
            saw_zeroize = true;
        }
    }

    if uses_thread_rng_for_secret {
        println!("err: avoid `thread_rng()` for key/nonce; use OsRng");
        return;
    }
    if !saw_osrng {
        println!("err: no OsRng usage detected for secrets");
        return;
    }
    if !saw_const_time_eq {
        println!("err: no constant-time equality found (use subtle::ConstantTimeEq / ct_eq)");
        return;
    }
    if !saw_zeroize {
        println!("err: no `zeroize` usage detected for secret types");
        return;
    }

    println!("ok");
}

fn collect_rs_files(root: &str) -> Vec<String> {
    let mut out = Vec::new();
    visit(Path::new(root), &mut out);
    out
}
fn visit(p: &Path, out: &mut Vec<String>) {
    if p.file_name().and_then(|s| s.to_str()) == Some("target") { return; }
    let Ok(read) = fs::read_dir(p) else { return };
    for entry in read.flatten() {
        let path = entry.path();
        if path.is_dir() {
            visit(&path, out);
        } else if path.extension().and_then(|e| e.to_str()) == Some("rs") {
            if let Some(s) = path.to_str() { out.push(s.to_string()); }
        }
    }
}
fn looks_like_secret_context(src: &str) -> bool {
    src.contains("keygen") || src.contains("generate_key") ||
    src.contains("seal") || src.contains("sign(") || src.contains("nonce")
}
"#;

    fs::write("plugins/cargo_toml_pqc.rs", cargo_toml_pqc)
        .map_err(|e| format!("write cargo_toml_pqc.rs: {e}"))?;
    fs::write("plugins/code_pqc.rs", code_pqc).map_err(|e| format!("write code_pqc.rs: {e}"))?;
    Ok(())
}