ripex 0.3.0

Multi-language structural parsing and fact extraction.
Documentation
use ripex::compiler::{
    check_with_compiler, plan_compiler_check, CheckStatus, CompilerCheckOptions,
};
use ripex::Language;
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};

struct TempSource {
    dir: PathBuf,
    path: PathBuf,
}

impl TempSource {
    fn rust(name: &str, source: &str) -> Self {
        let nonce = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_nanos();
        let dir = (0_u32..100)
            .map(|attempt| {
                std::env::temp_dir().join(format!("ripex-check-{nonce:x}-{attempt:02x}"))
            })
            .find(|candidate| fs::create_dir(candidate).is_ok())
            .expect("could not create an exclusive test directory");
        let path = dir.join(format!("{name}.rs"));
        fs::write(&path, source).unwrap();
        Self { dir, path }
    }
}

impl Drop for TempSource {
    fn drop(&mut self) {
        let _ = fs::remove_dir_all(&self.dir);
    }
}

#[test]
fn rustc_accepts_valid_typed_source() {
    let source = TempSource::rust("valid", "pub fn add(a: i32, b: i32) -> i32 { a + b }");
    let report = check_with_compiler(&source.path, None, &Default::default()).unwrap();
    assert_eq!(report.status, CheckStatus::Passed, "{report:#?}");
    assert_eq!(report.stages.len(), 1);
    assert_eq!(report.stages[0].backend, "rustc");
    assert!(report.stages[0]
        .command
        .contains(&"--edition=2021".to_string()));
}

#[test]
fn rustc_rejects_a_real_type_error() {
    let source = TempSource::rust("invalid", "pub fn value() -> i32 { \"not an integer\" }");
    let report = check_with_compiler(&source.path, None, &Default::default()).unwrap();
    assert_eq!(report.status, CheckStatus::Failed, "{report:#?}");
    assert!(
        !report.stages[0].diagnostics.is_empty(),
        "compiler output was not normalized: {report:#?}"
    );
}

#[test]
fn cargo_accepts_the_compiler_conformance_fixture() {
    let project = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/lang-test/rust");
    let options = CompilerCheckOptions {
        trusted_project: true,
        ..Default::default()
    };
    let report = check_with_compiler(&project, None, &options).unwrap();
    assert_eq!(report.status, CheckStatus::Passed, "{report:#?}");
    assert_eq!(report.stages.len(), 1);
    assert_eq!(report.stages[0].backend, "cargo");
}

#[test]
fn missing_explicit_toolchain_is_never_treated_as_success() {
    let source = TempSource::rust("valid", "pub fn value() -> i32 { 1 }");
    let options = CompilerCheckOptions {
        toolchain: Some(PathBuf::from("ripex-toolchain-that-does-not-exist")),
        ..Default::default()
    };
    let report = check_with_compiler(&source.path, Some(Language::Rust), &options).unwrap();
    assert_eq!(report.status, CheckStatus::Unavailable);
    assert_eq!(report.stages[0].status, CheckStatus::Unavailable);
}

#[test]
fn every_language_has_a_compiler_plan() {
    let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/lang-test");
    let cases = [
        (Language::C, root.join("c/main.c")),
        (Language::Cpp, root.join("cpp/main.cpp")),
        (Language::CSharp, root.join("csharp/Program.cs")),
        (Language::Go, root.join("go/main.go")),
        (Language::JavaScript, root.join("javascript/src/index.js")),
        (Language::TypeScript, root.join("javascript/src/types.ts")),
        (Language::Python, root.join("python/src/main.py")),
        (Language::Rust, root.join("rust/src/lib.rs")),
    ];
    for (language, path) in cases {
        let plans = plan_compiler_check(path, Some(language), &Default::default()).unwrap();
        assert!(!plans.is_empty(), "missing {language:?} compiler plan");
        assert!(plans.iter().all(|plan| !plan.candidates.is_empty()));
    }
}

#[test]
fn untrusted_project_checks_fail_closed() {
    let project = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/lang-test/rust");
    let error = plan_compiler_check(&project, Some(Language::Rust), &Default::default())
        .expect_err("project execution must require explicit trust");
    assert_eq!(error.kind(), std::io::ErrorKind::PermissionDenied);
}

#[test]
fn raw_extra_args_require_explicit_unsafe_gate() {
    let source = TempSource::rust("args", "pub fn value() -> i32 { 1 }");
    let options = CompilerCheckOptions {
        extra_args: vec!["--emit=llvm-ir".into()],
        ..Default::default()
    };
    let error = plan_compiler_check(&source.path, Some(Language::Rust), &options)
        .expect_err("raw args must fail closed");
    assert_eq!(error.kind(), std::io::ErrorKind::PermissionDenied);

    let options = CompilerCheckOptions {
        trusted_project: true,
        allow_unsafe_args: true,
        extra_args: vec!["--emit=llvm-ir".into()],
        ..Default::default()
    };
    let plans = plan_compiler_check(&source.path, Some(Language::Rust), &options).unwrap();
    assert!(plans[0].args.contains(&"--emit=llvm-ir".to_string()));
}

#[test]
fn source_discovery_normalizes_extensions_and_is_deterministic() {
    let source = TempSource::rust("discovery", "pub fn value() -> i32 { 1 }");
    let c_source = source.dir.join("UPPER.C");
    fs::write(&c_source, "int value(void) { return 1; }\n").unwrap();
    let options = CompilerCheckOptions {
        trusted_project: true,
        ..Default::default()
    };
    let first = plan_compiler_check(&source.dir, Some(Language::C), &options).unwrap();
    let second = plan_compiler_check(&source.dir, Some(Language::C), &options).unwrap();
    assert_eq!(first, second);
    assert_eq!(first.len(), 1);
    assert!(first[0].args.iter().any(|arg| arg.ends_with("UPPER.C")));
}