xbp-analysis 10.57.0

Language-agnostic static analysis (error-handling / Aspirator-style) for XBP
Documentation
//! Positive / negative Rust fixtures through the full engine.

use std::fs;
use std::io::Write;
use tempfile::tempdir;
use xbp_analysis::adapters::output::render_report;
use xbp_analysis::application::engine::AnalysisEngine;
use xbp_analysis::domain::request::{AnalysisRequest, OutputFormat};

fn write_rs(dir: &std::path::Path, name: &str, body: &str) {
    let p = dir.join(name);
    if let Some(parent) = p.parent() {
        fs::create_dir_all(parent).unwrap();
    }
    let mut f = fs::File::create(p).unwrap();
    write!(f, "{body}").unwrap();
}

#[test]
fn positive_unwrap_and_panic() {
    let dir = tempdir().unwrap();
    write_rs(
        dir.path(),
        "bad.rs",
        r#"
fn boom() {
    let x = Ok::<i32, ()>(1).unwrap();
    panic!("nope");
    let _ = x;
}
"#,
    );
    let engine = AnalysisEngine::new();
    let mut req = AnalysisRequest::new(dir.path());
    req.language_filter = Some(xbp_analysis::LanguageId::rust());
    let report = engine.analyze(&req).unwrap();
    assert!(
        report.findings.iter().any(|f| f.rule_id == "unsafe-termination"),
        "expected unsafe-termination, got {:?}",
        report.findings.iter().map(|f| &f.rule_id).collect::<Vec<_>>()
    );
}

#[test]
fn negative_proper_propagation() {
    let dir = tempdir().unwrap();
    write_rs(
        dir.path(),
        "good.rs",
        r#"
fn ok() -> Result<(), std::io::Error> {
    let data = std::fs::read("x")?;
    let _ = data.len();
    Ok(())
}
"#,
    );
    let engine = AnalysisEngine::new();
    let mut req = AnalysisRequest::new(dir.path());
    req.language_filter = Some(xbp_analysis::LanguageId::rust());
    // Disable noisy recovery rules for this negative test
    req.config.rules.disabled = vec![
        "error-context-loss".into(),
        "missing-cleanup".into(),
    ];
    let report = engine.analyze(&req).unwrap();
    assert!(
        !report
            .findings
            .iter()
            .any(|f| f.rule_id == "ignored-failure" || f.rule_id == "unsafe-termination"),
        "unexpected {:?}",
        report.findings
    );
}

#[test]
fn json_and_sarif_render() {
    let dir = tempdir().unwrap();
    write_rs(dir.path(), "u.rs", "fn f() { let _ = Ok::<(),()>(()).unwrap(); }\n");
    let engine = AnalysisEngine::new();
    let req = AnalysisRequest::new(dir.path());
    let report = engine.analyze(&req).unwrap();
    let json = render_report(&report, OutputFormat::Json).unwrap();
    assert!(json.contains("findings"));
    let sarif = render_report(&report, OutputFormat::Sarif).unwrap();
    assert!(sarif.contains("2.1.0"));
    let term = render_report(&report, OutputFormat::Terminal).unwrap();
    assert!(term.contains("xbp audit"));
}

#[test]
fn determinism_two_runs() {
    let dir = tempdir().unwrap();
    write_rs(
        dir.path(),
        "a.rs",
        "fn a() { Ok::<(),()>(()).unwrap(); }\nfn b() { todo!(); }\n",
    );
    let engine = AnalysisEngine::new();
    let req = AnalysisRequest::new(dir.path());
    let r1 = engine.analyze(&req).unwrap();
    let r2 = engine.analyze(&req).unwrap();
    let ids1: Vec<_> = r1.findings.iter().map(|f| f.id.clone()).collect();
    let ids2: Vec<_> = r2.findings.iter().map(|f| f.id.clone()).collect();
    assert_eq!(ids1, ids2);
}

#[test]
fn performance_many_files() {
    let dir = tempdir().unwrap();
    for i in 0..50 {
        write_rs(
            dir.path(),
            &format!("f{i}.rs"),
            &format!("fn f{i}() -> Result<(),()> {{ Ok(()) }}\n"),
        );
    }
    let engine = AnalysisEngine::new();
    let req = AnalysisRequest::new(dir.path());
    let started = std::time::Instant::now();
    let report = engine.analyze(&req).unwrap();
    let ms = started.elapsed().as_millis();
    assert_eq!(report.files_analyzed, 50);
    assert!(ms < 30_000, "analysis took {ms}ms");
}