cargo-allow 0.1.3

Source-tree exception ledger and policy scanner for Rust repositories.
use super::worklist_actions::suggested_actions_for_context;
use super::worklist_item_kind::{BROKEN_EVIDENCE_LINK, WEAK_EVIDENCE_REFERENCE};
use super::worklist_priority::DIFFICULTY_SMALL;
use super::worklist_scoring::work_item_risk;
use super::{WorkItem, WorkItemEvidenceReference, proof_commands};
use crate::evidence_inventory::{
    DEFAULT_SOURCE_TREE_INVENTORY_EVIDENCE_MESSAGE, ReferenceSource,
    policy_reference_diagnostics_for_source_tree,
};
use crate::evidence_render::evidence_reference_target_text;
use allow_core::{AllowConfig, AllowEntry, MatchStatus};
use allow_diff::selector_precision_score;
use allow_policy::EvidenceReferenceDiagnostic;
use std::collections::BTreeSet;
use std::path::Path;

#[cfg(test)]
pub(super) fn work_items_from_evidence_diagnostics(
    root: &Path,
    cfg: &AllowConfig,
    start_index: usize,
) -> Vec<WorkItem> {
    work_items_from_evidence_diagnostics_with_source_tree_files(root, cfg, start_index, None)
}

pub(super) fn work_items_from_evidence_diagnostics_with_source_tree_files(
    root: &Path,
    cfg: &AllowConfig,
    start_index: usize,
    evidence_source_tree_files: Option<&BTreeSet<String>>,
) -> Vec<WorkItem> {
    let mut items = Vec::new();
    for entry in &cfg.allow {
        for reference in
            policy_reference_diagnostics_for_source_tree(root, entry, evidence_source_tree_files)
                .into_iter()
                .filter(|reference| {
                    reference.diagnostic.status.is_broken_local_link()
                        || reference.diagnostic.status.is_weak_reference()
                })
        {
            let mut diagnostic = reference.diagnostic;
            diagnostic.message = reference.source.message(&diagnostic.message);
            let item_index = start_index + items.len();
            items.push(work_item_from_evidence_diagnostic(
                entry,
                diagnostic,
                item_index,
                reference.source,
            ));
        }
    }
    items
}

fn work_item_from_evidence_diagnostic(
    entry: &AllowEntry,
    diagnostic: EvidenceReferenceDiagnostic,
    item_index: usize,
    source: ReferenceSource,
) -> WorkItem {
    let kind = if diagnostic.status.is_weak_reference() {
        WEAK_EVIDENCE_REFERENCE
    } else {
        debug_assert!(diagnostic.status.is_broken_local_link());
        BROKEN_EVIDENCE_LINK
    };
    let proof_commands = evidence_proof_commands(kind, entry, &diagnostic);
    let target = evidence_reference_target_text(&diagnostic);
    let path = if kind == BROKEN_EVIDENCE_LINK {
        target.clone()
    } else {
        None
    };
    let evidence_reference = WorkItemEvidenceReference {
        raw: diagnostic.raw.clone(),
        prefix: diagnostic.prefix.clone(),
        target,
        status: diagnostic.status.as_str().to_string(),
        category: diagnostic.category.as_str().to_string(),
        message: diagnostic.message.clone(),
    };
    WorkItem {
        id: format!("work-{}-{item_index:04}", kind.replace('_', "-")),
        kind: kind.to_string(),
        exception_kind: Some(entry.kind.as_str().to_string()),
        family: entry.family.clone(),
        owner: Some(entry.owner.clone()),
        classification: Some(entry.classification.clone()),
        reason: Some(entry.reason.clone()),
        created: entry.lifecycle.created.clone(),
        review_after: entry.lifecycle.review_after.clone(),
        expires: entry.lifecycle.expires.clone(),
        evidence_count: Some(entry.evidence.len()),
        selector_precision: Some(selector_precision_score(entry)),
        risk: work_item_risk(kind, MatchStatus::EvidenceMissing, None, Some(entry)),
        difficulty: DIFFICULTY_SMALL,
        status: MatchStatus::EvidenceMissing,
        allow_id: Some(entry.id.clone()),
        finding_index: None,
        path,
        evidence_reference: Some(evidence_reference),
        source_package: None,
        message: format!(
            "{} {} `{}`: {}",
            entry.id,
            source.label(),
            diagnostic.raw,
            diagnostic.message
        ),
        suggested_actions: evidence_suggested_actions(kind, entry, &diagnostic, source),
        proof_commands,
    }
}

fn evidence_suggested_actions(
    kind: &str,
    entry: &AllowEntry,
    diagnostic: &EvidenceReferenceDiagnostic,
    source: ReferenceSource,
) -> Vec<String> {
    if evidence_exists_outside_default_inventory(diagnostic) {
        return outside_inventory_actions(source);
    }
    if source == ReferenceSource::Link {
        return super::worklist_actions::suggested_link_actions_for_context(
            kind,
            None,
            Some(entry),
        );
    }
    suggested_actions_for_context(kind, None, Some(entry))
}

fn evidence_proof_commands(
    kind: &str,
    entry: &AllowEntry,
    diagnostic: &EvidenceReferenceDiagnostic,
) -> Vec<String> {
    if !evidence_exists_outside_default_inventory(diagnostic) {
        return proof_commands(kind, None, Some(entry));
    }

    let mut commands = vec![
        format!("cargo-allow explain {}", entry.id),
        format!("cargo-allow explain {} --include-untracked", entry.id),
        format!("cargo-allow worklist --allow-id {} --format json", entry.id),
        "cargo-allow check --include-untracked --mode no-new".to_string(),
    ];
    for command in proof_commands(kind, None, Some(entry)) {
        if !commands.iter().any(|existing| existing == &command) {
            commands.push(command);
        }
    }
    commands
}

fn evidence_exists_outside_default_inventory(diagnostic: &EvidenceReferenceDiagnostic) -> bool {
    diagnostic.message == DEFAULT_SOURCE_TREE_INVENTORY_EVIDENCE_MESSAGE
        || diagnostic.message
            == ReferenceSource::Link.message(DEFAULT_SOURCE_TREE_INVENTORY_EVIDENCE_MESSAGE)
}

fn outside_inventory_actions(source: ReferenceSource) -> Vec<String> {
    match source {
        ReferenceSource::Evidence => vec![
            "commit the referenced evidence file if it should support repository policy"
                .to_string(),
            "or rerun with --include-untracked when intentionally reviewing local receipt artifacts"
                .to_string(),
        ],
        ReferenceSource::Link => vec![
            "commit the referenced traceability file if it should support repository policy"
                .to_string(),
            "or rerun with --include-untracked when intentionally reviewing local traceability files"
                .to_string(),
        ],
    }
}