cargo-allow 0.1.3

Source-tree exception ledger and policy scanner for Rust repositories.
use super::worklist_item_kind::{BASELINE_DEBT, BROAD_SCOPE};
use super::worklist_priority::{
    DIFFICULTY_MEDIUM, DIFFICULTY_SMALL, RISK_HIGH, RISK_LOW, RISK_MEDIUM,
};
use super::{WorkItem, WorklistFilters};
use crate::parse_kind_filter;
use allow_core::{FindingKind, MatchStatus, source_tree_path_matches_filter};

pub(super) fn filter_work_items(
    items: Vec<WorkItem>,
    filters: WorklistFilters<'_>,
) -> Vec<WorkItem> {
    items
        .into_iter()
        .filter(|item| {
            kind_matches(item, filters.kind)
                && filters
                    .family
                    .map(|family| item.family.as_deref() == Some(family))
                    .unwrap_or(true)
                && filters
                    .item_kind
                    .map(|item_kind| item_kind_matches(item.kind.as_str(), item_kind))
                    .unwrap_or(true)
                && filters
                    .status
                    .map(|status| item.status.as_str() == status)
                    .unwrap_or(true)
                && filters
                    .allow_id
                    .map(|allow_id| item.allow_id.as_deref() == Some(allow_id))
                    .unwrap_or(true)
                && filters
                    .path
                    .map(|path| work_item_path_matches_filter(item, path))
                    .unwrap_or(true)
                && filters
                    .source_package
                    .map(|source_package| item.source_package.as_deref() == Some(source_package))
                    .unwrap_or(true)
                && filters
                    .owner
                    .map(|owner| item.owner.as_deref() == Some(owner))
                    .unwrap_or(true)
                && filters
                    .classification
                    .map(|classification| item.classification.as_deref() == Some(classification))
                    .unwrap_or(true)
                && (!filters.baseline_debt
                    || item.kind == BASELINE_DEBT
                    || item.classification.as_deref() == Some("baseline_debt")
                    || item.status == MatchStatus::BaselineDebt)
                && (!filters.broad_scope || item.kind == BROAD_SCOPE)
                && filters.risk.map(|risk| item.risk == risk).unwrap_or(true)
                && filters
                    .difficulty
                    .map(|difficulty| item.difficulty == difficulty)
                    .unwrap_or(true)
                && (!filters.missing_evidence || item.evidence_count == Some(0))
        })
        .collect()
}

fn work_item_path_matches_filter(item: &WorkItem, filter: &str) -> bool {
    let Some(item_path) = item.path.as_deref() else {
        return false;
    };
    if item
        .evidence_reference
        .as_ref()
        .is_some_and(|reference| reference.status == "invalid_local_path")
    {
        return item_path.replace('\\', "/") == filter.replace('\\', "/");
    }
    source_tree_path_matches_filter(item_path, filter)
}

fn item_kind_matches(item_kind: &str, filter: &str) -> bool {
    item_kind == filter || item_kind == filter.replace('-', "_")
}

fn kind_matches(item: &WorkItem, kind: Option<&str>) -> bool {
    let Some(kind) = kind else {
        return true;
    };
    let Ok(parsed) = parse_kind_filter(kind) else {
        return false;
    };
    item.exception_kind
        .as_deref()
        .and_then(|kind| kind.parse::<FindingKind>().ok())
        .is_some_and(|item_kind| item_kind == parsed.kind)
        && parsed.family.matches(item.family.as_deref())
}

pub(super) fn sort_work_items(items: &mut [WorkItem]) {
    items.sort_by(|left, right| {
        work_item_risk_rank(left.risk)
            .cmp(&work_item_risk_rank(right.risk))
            .then_with(|| {
                work_item_difficulty_rank(left.difficulty)
                    .cmp(&work_item_difficulty_rank(right.difficulty))
            })
            .then_with(|| left.kind.cmp(&right.kind))
            .then_with(|| selector_precision_rank(left).cmp(&selector_precision_rank(right)))
            .then_with(|| left.path.cmp(&right.path))
            .then_with(|| left.allow_id.cmp(&right.allow_id))
            .then_with(|| left.id.cmp(&right.id))
    });
}

fn work_item_risk_rank(risk: &str) -> u8 {
    match risk {
        RISK_HIGH => 0,
        RISK_MEDIUM => 1,
        RISK_LOW => 2,
        _ => 3,
    }
}

fn work_item_difficulty_rank(difficulty: &str) -> u8 {
    match difficulty {
        DIFFICULTY_SMALL => 0,
        DIFFICULTY_MEDIUM => 1,
        _ => 2,
    }
}

fn selector_precision_rank(item: &WorkItem) -> u32 {
    item.selector_precision.unwrap_or(u32::MAX)
}

pub(super) fn renumber_work_items(items: &mut [WorkItem]) {
    for (index, item) in items.iter_mut().enumerate() {
        item.id = format!("work-{}-{:04}", item.kind.replace('_', "-"), index + 1);
    }
}