syft-semantic 0.2.0

Rust-first semantic indexing and diffing for syft
Documentation
use std::fs;
use std::path::Path;

use tempfile::tempdir;

use crate::{diff_snapshots, extract_rust_symbols};
use syft_objects::capture_directory;
use syft_store::FsObjectStore;
use syft_types::{Snapshot, SnapshotMetadata, SnapshotSource, new_entity_id, now_utc};

#[test]
fn extractor_finds_public_items_and_modules() {
    let symbols = extract_rust_symbols(
        Path::new("src/lib.rs"),
        "pub struct User;\npub fn load(id: u32) {}\nmod inner { pub fn helper() {} }\n",
    )
    .unwrap();

    let ids = symbols
        .iter()
        .map(|symbol| symbol.symbol.id.path.clone())
        .collect::<Vec<_>>();
    assert!(ids.contains(&"User".to_string()));
    assert!(ids.contains(&"load".to_string()));
    assert!(ids.contains(&"inner".to_string()));
    assert!(ids.contains(&"inner::helper".to_string()));
    let user = symbols
        .iter()
        .find(|symbol| symbol.symbol.id.path == "User")
        .unwrap();
    assert!(user.symbol.source.span.start_line > 0);
    assert!(user.symbol.source.span.end_col > 0);
}

#[test]
fn formatting_only_signature_changes_are_ignored() {
    let first = extract_rust_symbols(
        Path::new("src/lib.rs"),
        "pub fn load(id: u32) -> &'static str { \"x\" }\n",
    )
    .unwrap();
    let second = extract_rust_symbols(
        Path::new("src/lib.rs"),
        "pub fn load( id : u32 )-> &'static str { \"x\" }\n",
    )
    .unwrap();

    let first_sig = first[0].attributes.get("signature").unwrap();
    let second_sig = second[0].attributes.get("signature").unwrap();
    assert_eq!(first_sig, second_sig);
}

#[test]
fn diff_detects_signature_change() {
    let base = tempdir().unwrap();
    let next = tempdir().unwrap();
    fs::create_dir_all(base.path().join("src")).unwrap();
    fs::create_dir_all(next.path().join("src")).unwrap();
    fs::write(base.path().join("src/lib.rs"), "pub fn load(id: u32) {}\n").unwrap();
    fs::write(next.path().join("src/lib.rs"), "pub fn load(id: u64) {}\n").unwrap();
    fs::write(
        base.path().join("Cargo.toml"),
        "[package]\nname=\"a\"\nversion=\"0.1.0\"\n",
    )
    .unwrap();
    fs::write(
        next.path().join("Cargo.toml"),
        "[package]\nname=\"a\"\nversion=\"0.1.0\"\n",
    )
    .unwrap();

    let store_dir = tempdir().unwrap();
    let store = FsObjectStore::new(store_dir.path());
    let (base_hash, _) = capture_directory(base.path(), &store, &[]).unwrap();
    let (next_hash, _) = capture_directory(next.path(), &store, &[]).unwrap();

    let base_snapshot = snapshot("repo", base_hash);
    let next_snapshot = snapshot("repo", next_hash);

    let delta = diff_snapshots(&base_snapshot, &next_snapshot, &store).unwrap();
    assert_eq!(delta.touched_symbols.len(), 1);
    assert!(delta.changed_public_api);
    assert!(delta.summary.contains("public API"));
    assert!(delta.summary.contains("load"));
}

#[test]
fn body_only_changes_touch_symbol_without_public_api_change() {
    let base = tempdir().unwrap();
    let next = tempdir().unwrap();
    fs::create_dir_all(base.path().join("src")).unwrap();
    fs::create_dir_all(next.path().join("src")).unwrap();
    fs::write(
        base.path().join("src/lib.rs"),
        "pub fn greet() -> &'static str { \"hello\" }\n",
    )
    .unwrap();
    fs::write(
        next.path().join("src/lib.rs"),
        "pub fn greet() -> &'static str { \"hello, syft\" }\n",
    )
    .unwrap();
    fs::write(
        base.path().join("Cargo.toml"),
        "[package]\nname=\"a\"\nversion=\"0.1.0\"\n",
    )
    .unwrap();
    fs::write(
        next.path().join("Cargo.toml"),
        "[package]\nname=\"a\"\nversion=\"0.1.0\"\n",
    )
    .unwrap();

    let store_dir = tempdir().unwrap();
    let store = FsObjectStore::new(store_dir.path());
    let (base_hash, _) = capture_directory(base.path(), &store, &[]).unwrap();
    let (next_hash, _) = capture_directory(next.path(), &store, &[]).unwrap();

    let base_snapshot = snapshot("repo", base_hash);
    let next_snapshot = snapshot("repo", next_hash);

    let delta = diff_snapshots(&base_snapshot, &next_snapshot, &store).unwrap();
    assert_eq!(delta.touched_symbols[0].id.path, "greet");
    assert!(!delta.changed_public_api);
}

fn snapshot(repo_id: &str, root_tree_hash: String) -> Snapshot {
    Snapshot {
        id: new_entity_id(),
        parent_snapshot_ids: Vec::new(),
        root_tree_hash,
        created_at: now_utc(),
        metadata: SnapshotMetadata {
            repo_id: repo_id.to_string(),
            source: SnapshotSource::MaterializedByHuman,
            labels: Vec::new(),
        },
    }
}