allow-policy-legacy 0.1.9

Legacy policy adapters for cargo-allow migrations.
Documentation
use allow_core::{
    AllowEntry, FindingKind, Lifecycle, Selector, normalize_path, normalize_snippet,
    stable_hash_hex,
};
use std::path::PathBuf;

use crate::converter_panic_support::{
    cargo_allow_panic_family, no_panic_macro_name, normalize_selector_kind,
};
use crate::types::LegacyNoPanicBaselineEntry;
use crate::{default_baseline_created, default_baseline_expires};

pub(crate) fn entry_from_no_panic_baseline_entry(rule: &LegacyNoPanicBaselineEntry) -> AllowEntry {
    let path = normalize_path(&rule.path);
    let family = cargo_allow_panic_family(&rule.family);
    let ast_kind = normalize_selector_kind(&rule.selector_kind);
    let snippet_hash = stable_hash_hex(&normalize_snippet(&rule.snippet));
    AllowEntry {
        id: format!("panic-baseline-{:04}", rule.index + 1),
        kind: FindingKind::Panic,
        family: Some(family.clone()),
        path: Some(PathBuf::from(&path)),
        glob: None,
        owner: "unowned".to_string(),
        classification: "baseline_debt".to_string(),
        reason: "Generated from legacy no-panic baseline; requires human review.".to_string(),
        evidence: vec![
            "legacy_policy:no-panic-baseline".to_string(),
            format!("legacy_selector_callee:{}", rule.selector_callee),
            format!("baseline_count:{}", rule.count),
        ],
        links: vec!["legacy-policy:no-panic-baseline".to_string()],
        occurrence_limit: Some(rule.count),
        lifecycle: Lifecycle {
            created: Some(default_baseline_created()),
            review_after: None,
            expires: Some(default_baseline_expires()),
        },
        selector: Selector {
            ast_kind: Some(ast_kind.clone()),
            callee: (ast_kind == "method_call").then(|| family.clone()),
            macro_name: (ast_kind == "macro_call").then(|| no_panic_macro_name(&rule.family)),
            normalized_snippet_hash: Some(snippet_hash),
            glob: Some(path),
            ..Selector::default()
        },
        last_seen: None,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn no_panic_baseline_method_entry_preserves_count_and_selector_identity() {
        let rule = LegacyNoPanicBaselineEntry {
            index: 7,
            path: "src\\parser.rs".to_string(),
            family: "expect".to_string(),
            selector_kind: "method-call".to_string(),
            selector_callee: "core::result::Result::expect".to_string(),
            snippet: " value\n    .expect(\"validated\") ".to_string(),
            count: 3,
        };

        let entry = entry_from_no_panic_baseline_entry(&rule);

        assert_eq!(entry.id, "panic-baseline-0008");
        assert_eq!(entry.kind, FindingKind::Panic);
        assert_eq!(entry.family.as_deref(), Some("expect"));
        assert_eq!(entry.path, Some(PathBuf::from("src/parser.rs")));
        assert_eq!(entry.glob, None);
        assert_eq!(entry.owner, "unowned");
        assert_eq!(entry.classification, "baseline_debt");
        assert_eq!(
            entry.reason,
            "Generated from legacy no-panic baseline; requires human review."
        );
        assert_eq!(
            entry.evidence,
            vec![
                "legacy_policy:no-panic-baseline".to_string(),
                "legacy_selector_callee:core::result::Result::expect".to_string(),
                "baseline_count:3".to_string(),
            ]
        );
        assert_eq!(
            entry.links,
            vec!["legacy-policy:no-panic-baseline".to_string()]
        );
        assert_eq!(entry.occurrence_limit, Some(3));
        assert_eq!(
            entry.lifecycle.created.as_deref(),
            Some(default_baseline_created().as_str())
        );
        assert_eq!(entry.lifecycle.review_after, None);
        assert_eq!(
            entry.lifecycle.expires.as_deref(),
            Some(default_baseline_expires().as_str())
        );
        assert_eq!(entry.selector.ast_kind.as_deref(), Some("method_call"));
        assert_eq!(entry.selector.callee.as_deref(), Some("expect"));
        assert_eq!(entry.selector.macro_name, None);
        assert_eq!(
            entry.selector.normalized_snippet_hash,
            Some(stable_hash_hex(&normalize_snippet(&rule.snippet)))
        );
        assert_eq!(entry.selector.glob.as_deref(), Some("src/parser.rs"));
        assert!(entry.last_seen.is_none());
    }

    #[test]
    fn no_panic_baseline_macro_entry_normalizes_panic_family_and_macro_name() {
        let rule = LegacyNoPanicBaselineEntry {
            index: 0,
            path: "src\\panic_path.rs".to_string(),
            family: "panic".to_string(),
            selector_kind: "macro-call".to_string(),
            selector_callee: "panic".to_string(),
            snippet: "panic!(\"invalid state\")".to_string(),
            count: 1,
        };

        let entry = entry_from_no_panic_baseline_entry(&rule);

        assert_eq!(entry.id, "panic-baseline-0001");
        assert_eq!(entry.kind, FindingKind::Panic);
        assert_eq!(entry.family.as_deref(), Some("panic_macro"));
        assert_eq!(entry.path, Some(PathBuf::from("src/panic_path.rs")));
        assert_eq!(entry.owner, "unowned");
        assert_eq!(entry.classification, "baseline_debt");
        assert_eq!(
            entry.evidence,
            vec![
                "legacy_policy:no-panic-baseline".to_string(),
                "legacy_selector_callee:panic".to_string(),
                "baseline_count:1".to_string(),
            ]
        );
        assert_eq!(entry.occurrence_limit, Some(1));
        assert_eq!(entry.selector.ast_kind.as_deref(), Some("macro_call"));
        assert_eq!(entry.selector.callee, None);
        assert_eq!(entry.selector.macro_name.as_deref(), Some("panic"));
        assert_eq!(
            entry.selector.normalized_snippet_hash,
            Some(stable_hash_hex(&normalize_snippet(&rule.snippet)))
        );
        assert_eq!(entry.selector.glob.as_deref(), Some("src/panic_path.rs"));
        assert!(entry.last_seen.is_none());
    }
}