pedant 0.14.0

An opinionated Rust linter, with special focus on AI-generated code
use std::io::Write;
use std::sync::Arc;

use pedant_types::{
    AnalysisTier, AttestationContent, CapabilityDiff, CapabilityFinding, CapabilityProfile,
    SourceLocation,
};
use tempfile::NamedTempFile;

mod common;

fn write_json_file(value: &impl serde::Serialize) -> NamedTempFile {
    let mut f = NamedTempFile::new().expect("failed to create temp file");
    serde_json::to_writer(&mut f, value).expect("failed to write json");
    f
}

fn make_profile(findings: Vec<CapabilityFinding>) -> CapabilityProfile {
    CapabilityProfile {
        findings: findings.into_boxed_slice(),
    }
}

fn make_attestation(profile: CapabilityProfile) -> AttestationContent {
    AttestationContent {
        spec_version: Box::from("0.1.0"),
        source_hash: Box::from("deadbeef"),
        crate_name: Box::from("test"),
        crate_version: Box::from("0.0.1"),
        analysis_tier: AnalysisTier::Syntactic,
        timestamp: 0,
        profile,
    }
}

fn net_finding() -> CapabilityFinding {
    CapabilityFinding {
        capability: pedant_types::Capability::Network,
        location: SourceLocation {
            file: Arc::from("lib.rs"),
            line: 1,
            column: 0,
        },
        evidence: Arc::from("use std::net::TcpStream"),
        build_script: false,
    }
}

fn fs_finding() -> CapabilityFinding {
    CapabilityFinding {
        capability: pedant_types::Capability::FileRead,
        location: SourceLocation {
            file: Arc::from("lib.rs"),
            line: 1,
            column: 0,
        },
        evidence: Arc::from("use std::fs::read"),
        build_script: false,
    }
}

#[test]
fn identical_profiles_exit_0() {
    let profile = make_profile(vec![net_finding()]);
    let old = write_json_file(&profile);
    let new = write_json_file(&profile);

    let output = common::run_pedant(
        &[
            "--diff",
            old.path().to_str().unwrap(),
            new.path().to_str().unwrap(),
        ],
        None,
    );

    assert_eq!(output.status.code(), Some(0));

    let diff: CapabilityDiff = serde_json::from_slice(&output.stdout).unwrap();
    assert!(diff.added.is_empty());
    assert!(diff.removed.is_empty());
    assert!(diff.new_capabilities.is_empty());
    assert!(diff.dropped_capabilities.is_empty());
}

#[test]
fn different_profiles_exit_1() {
    let old = write_json_file(&make_profile(vec![net_finding()]));
    let new = write_json_file(&make_profile(vec![fs_finding()]));

    let output = common::run_pedant(
        &[
            "--diff",
            old.path().to_str().unwrap(),
            new.path().to_str().unwrap(),
        ],
        None,
    );

    assert_eq!(output.status.code(), Some(1));

    let diff: CapabilityDiff = serde_json::from_slice(&output.stdout).unwrap();
    assert!(!diff.added.is_empty());
    assert!(!diff.removed.is_empty());
    assert!(!diff.new_capabilities.is_empty());
    assert!(!diff.dropped_capabilities.is_empty());
}

#[test]
fn attestation_format_input() {
    let old_att = make_attestation(make_profile(vec![net_finding()]));
    let new_att = make_attestation(make_profile(vec![fs_finding()]));
    let old = write_json_file(&old_att);
    let new = write_json_file(&new_att);

    let output = common::run_pedant(
        &[
            "--diff",
            old.path().to_str().unwrap(),
            new.path().to_str().unwrap(),
        ],
        None,
    );

    assert_eq!(output.status.code(), Some(1));

    let diff: CapabilityDiff = serde_json::from_slice(&output.stdout).unwrap();
    assert!(!diff.added.is_empty());
}

#[test]
fn mixed_attestation_and_bare_profile() {
    let old_att = make_attestation(make_profile(vec![net_finding()]));
    let new_profile = make_profile(vec![fs_finding()]);
    let old = write_json_file(&old_att);
    let new = write_json_file(&new_profile);

    let output = common::run_pedant(
        &[
            "--diff",
            old.path().to_str().unwrap(),
            new.path().to_str().unwrap(),
        ],
        None,
    );

    assert_eq!(output.status.code(), Some(1));

    let diff: CapabilityDiff = serde_json::from_slice(&output.stdout).unwrap();
    assert!(!diff.added.is_empty());
    assert!(!diff.removed.is_empty());
}

#[test]
fn missing_file_exit_2() {
    let existing = write_json_file(&make_profile(vec![]));

    let output = common::run_pedant(
        &[
            "--diff",
            existing.path().to_str().unwrap(),
            "/nonexistent/file.json",
        ],
        None,
    );

    assert_eq!(output.status.code(), Some(2));
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.contains("failed to read diff input"));
}

#[test]
fn invalid_json_exit_2() {
    let mut bad = NamedTempFile::new().unwrap();
    bad.write_all(b"not json at all").unwrap();
    let good = write_json_file(&make_profile(vec![]));

    let output = common::run_pedant(
        &[
            "--diff",
            good.path().to_str().unwrap(),
            bad.path().to_str().unwrap(),
        ],
        None,
    );

    assert_eq!(output.status.code(), Some(2));
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.contains("failed to parse diff input"));
}

#[test]
fn no_args_after_diff_flag() {
    let output = common::run_pedant(&["--diff"], None);
    assert!(!output.status.success());
}