alef 0.62.6

Opinionated polyglot binding generator for Rust libraries
Documentation
use super::*;
use crate::core::config::{ResolvedCrateConfig, SyncConfig, TextReplacement};

/// A crate config whose ONLY version-sync work is the two catch-all paths, so a
/// rewrite observed here can only have come from them and not from a named-filename
/// branch. ~keep
fn catch_all_config(root: &std::path::Path, sync: SyncConfig) -> ResolvedCrateConfig {
    ResolvedCrateConfig {
        name: "sample_core".to_string(),
        version_from: root.join("Cargo.toml").to_string_lossy().into_owned(),
        sources: vec![root.join("src/lib.rs")],
        workspace_root: Some(root.to_path_buf()),
        sync: Some(sync),
        ..ResolvedCrateConfig::default()
    }
}

fn seed_crate(root: &std::path::Path) {
    std::fs::create_dir_all(root.join("src")).expect("source directory");
    std::fs::write(root.join("src/lib.rs"), "pub fn transform() {}\n").expect("Rust source");
    std::fs::write(
        root.join("Cargo.toml"),
        "[package]\nname = \"sample_core\"\nversion = \"1.2.3\"\nedition = \"2024\"\n",
    )
    .expect("Cargo.toml");
}

fn run_sync(root: &std::path::Path, config: &ResolvedCrateConfig) {
    let config_path = root.join("alef.toml");
    std::fs::write(&config_path, "").expect("alef.toml");
    let original_cwd = std::env::current_dir().expect("current directory");
    std::env::set_current_dir(root).expect("enter temporary directory");
    let result = sync_versions(config, &config_path, None, true, true, None);
    std::env::set_current_dir(&original_cwd).expect("restore current directory");
    result.expect("sync versions");
}

/// THE CANARY. `extra_paths` accepts a glob, so a slightly wide pattern reaches files
/// nobody meant to hand to it, and the catch-all arm then runs a blanket
/// `SEMVER_RE.replace_all` over whatever it matched. Before the guard this rewrote a
/// hand-written file silently; `notes.toml` here is stampable (`.toml` has a comment
/// style) and carries no marker, which is the only combination where absence is real
/// evidence of foreignness.
///
/// This fails against the pre-guard generator, which rewrote 9.9.9 to 1.2.3. ~keep
#[test]
fn catch_all_semver_rewrite_leaves_a_stampable_file_that_carries_no_marker() {
    let _guard = CWD_LOCK.lock().unwrap_or_else(|error| error.into_inner());
    let temporary = tempfile::tempdir().expect("temporary directory");
    let root = temporary.path();
    seed_crate(root);

    let hand_written = "# maintained by hand\npinned = \"9.9.9\"\n";
    std::fs::write(root.join("notes.toml"), hand_written).expect("hand-written file");

    run_sync(
        root,
        &catch_all_config(
            root,
            SyncConfig {
                extra_paths: vec!["notes.toml".to_string()],
                text_replacements: Vec::new(),
            },
        ),
    );

    assert_eq!(
        std::fs::read_to_string(root.join("notes.toml")).expect("read notes.toml"),
        hand_written,
        "a stampable file with no alef marker reads as hand-written and must survive the catch-all"
    );
}

/// The other half of the same predicate: once alef owns the file, the catch-all is
/// free to keep its version coordinates current. Without this, the test above would
/// also pass if the guard simply refused everything. ~keep
#[test]
fn catch_all_semver_rewrite_still_updates_a_stampable_file_alef_owns() {
    let _guard = CWD_LOCK.lock().unwrap_or_else(|error| error.into_inner());
    let temporary = tempfile::tempdir().expect("temporary directory");
    let root = temporary.path();
    seed_crate(root);

    let owned = format!(
        "{}\npinned = \"9.9.9\"\n",
        crate::core::hash::header(crate::core::hash::CommentStyle::Hash)
    );
    std::fs::write(root.join("owned.toml"), &owned).expect("alef-owned file");

    run_sync(
        root,
        &catch_all_config(
            root,
            SyncConfig {
                extra_paths: vec!["owned.toml".to_string()],
                text_replacements: Vec::new(),
            },
        ),
    );

    let after = std::fs::read_to_string(root.join("owned.toml")).expect("read owned.toml");
    assert!(
        after.contains("1.2.3") && !after.contains("9.9.9"),
        "an alef-owned file must still be rewritten by the catch-all, got:\n{after}"
    );
}

/// The exemption `marker_comment_style`'s own `~keep` demands. `.md` has no comment
/// style alef stamps in, so a generated README never carries a marker even when alef
/// wrote every byte — refusing there would freeze legitimate regeneration, which is
/// exactly the failure mode that made this a policy question rather than a bug fix.
/// Guarding on marker-absence alone would break this. ~keep
#[test]
fn catch_all_rewrite_still_touches_an_unstampable_file_because_absence_proves_nothing() {
    let _guard = CWD_LOCK.lock().unwrap_or_else(|error| error.into_inner());
    let temporary = tempfile::tempdir().expect("temporary directory");
    let root = temporary.path();
    seed_crate(root);

    std::fs::write(root.join("NOTES.md"), "docs for 9.9.9\n").expect("markdown file");

    run_sync(
        root,
        &catch_all_config(
            root,
            SyncConfig {
                extra_paths: vec!["NOTES.md".to_string()],
                text_replacements: Vec::new(),
            },
        ),
    );

    assert!(
        std::fs::read_to_string(root.join("NOTES.md"))
            .expect("read NOTES.md")
            .contains("1.2.3"),
        "an extension alef cannot stamp must stay eligible — marker absence is not evidence there"
    );
}

/// `text_replacements` is the second catch-all and runs a consumer-supplied regex, so
/// it can rewrite anything its glob reaches. Same predicate, separate call site — a fix
/// applied to only one of the two would leave this one open. ~keep
#[test]
fn text_replacement_leaves_a_stampable_file_that_carries_no_marker() {
    let _guard = CWD_LOCK.lock().unwrap_or_else(|error| error.into_inner());
    let temporary = tempfile::tempdir().expect("temporary directory");
    let root = temporary.path();
    seed_crate(root);

    let hand_written = "version: 9.9.9\n";
    std::fs::write(root.join("hand.yaml"), hand_written).expect("hand-written yaml");

    run_sync(
        root,
        &catch_all_config(
            root,
            SyncConfig {
                extra_paths: Vec::new(),
                text_replacements: vec![TextReplacement {
                    path: "hand.yaml".to_string(),
                    search: r"version: [0-9.]+".to_string(),
                    replace: "version: {version}".to_string(),
                }],
            },
        ),
    );

    assert_eq!(
        std::fs::read_to_string(root.join("hand.yaml")).expect("read hand.yaml"),
        hand_written,
        "text_replacements must honour the same ownership predicate as the semver catch-all"
    );
}