cargo-allow 0.1.3

Source-tree exception ledger and policy scanner for Rust repositories.
use allow_core::{AllowConfig, CargoAllowError, CargoAllowResult};
use allow_inventory::{InventoryOptions, inventory, resolve_source_tree_root};
use allow_policy::load_policy;
use std::collections::BTreeSet;
use std::env;
use std::path::Path;

#[path = "doctor_args.rs"]
mod doctor_args;
pub(crate) use doctor_args::DoctorArgs;
use doctor_args::DoctorFormat;

use crate::{
    InventoryFacts, SourceTreeReportContext, config_path, emit_text,
    evidence_inventory::{
        PolicyReferenceDiagnostic, current_evidence_source_tree_files,
        policy_reference_diagnostics_for_source_tree,
    },
};

pub(crate) fn cmd_doctor(args: &DoctorArgs) -> CargoAllowResult<()> {
    let cwd =
        env::current_dir().map_err(|e| CargoAllowError::new(format!("failed to read cwd: {e}")))?;
    let root = resolve_source_tree_root(args.root.root.as_deref(), &cwd)?;
    let root_discovery = root_discovery_kind(args.root.root.as_deref(), &root);
    let config = config_path(&root, args.config.as_deref());
    let policy = load_doctor_policy(config.as_deref());
    let opts = doctor_inventory_options(policy.as_ref());
    let inventory = inventory(&root, &opts)?;
    let files_scanned = inventory.files.len();
    let evidence_source_tree_files = current_evidence_source_tree_files(&root, false);
    let source_context = SourceTreeReportContext::new(
        &root,
        InventoryFacts::scanned(inventory.source, files_scanned),
    );
    let config_text = config
        .as_ref()
        .map(|path| allow_report::source_tree_path_text(path));
    let (config_valid, config_diagnostic) =
        config_status(&root, policy.as_ref(), evidence_source_tree_files.as_ref());
    let (broken_evidence_links, weak_evidence_references) =
        doctor_evidence_health(&root, policy.as_ref(), evidence_source_tree_files.as_ref());
    let config_schema_version = policy
        .as_ref()
        .and_then(|result| result.as_ref().ok())
        .map(|cfg| cfg.schema_version.as_str());
    let config_policy = policy
        .as_ref()
        .and_then(|result| result.as_ref().ok())
        .map(|cfg| cfg.policy.as_str());
    let config_owner = policy
        .as_ref()
        .and_then(|result| result.as_ref().ok())
        .and_then(|cfg| cfg.owner.as_deref());
    let config_status = policy
        .as_ref()
        .and_then(|result| result.as_ref().ok())
        .and_then(|cfg| cfg.status.as_deref());
    let report = allow_report::DoctorReport {
        source_tree_root: source_context.source_tree_root(),
        root_discovery,
        config_path: config_text.as_deref(),
        config_schema_version,
        config_policy,
        config_owner,
        config_status,
        config_valid,
        config_diagnostic: config_diagnostic.as_deref(),
        broken_evidence_links,
        weak_evidence_references,
        inventory_source: source_context.inventory_source(),
        files_scanned,
    };
    let text = match args.format {
        DoctorFormat::Human => allow_report::render_doctor_human(report),
        DoctorFormat::Json => allow_report::render_doctor_json(report),
    };
    emit_text(args.output.as_deref(), &text)?;
    Ok(())
}

fn load_doctor_policy(config: Option<&Path>) -> Option<CargoAllowResult<AllowConfig>> {
    config.map(load_policy)
}

fn config_status(
    root: &Path,
    policy: Option<&CargoAllowResult<AllowConfig>>,
    source_tree_files: Option<&BTreeSet<String>>,
) -> (Option<bool>, Option<String>) {
    match policy {
        None => (None, None),
        Some(Ok(cfg)) => match first_broken_evidence_diagnostic(root, cfg, source_tree_files) {
            Some((entry_id, reference)) => (
                Some(false),
                Some(format!(
                    "{} {} `{}`: {}",
                    entry_id,
                    reference.source.label(),
                    reference.diagnostic.raw,
                    reference.source.message(&reference.diagnostic.message)
                )),
            ),
            None => (Some(true), None),
        },
        Some(Err(err)) => (Some(false), Some(err.to_string())),
    }
}

fn doctor_evidence_health(
    root: &Path,
    policy: Option<&CargoAllowResult<AllowConfig>>,
    source_tree_files: Option<&BTreeSet<String>>,
) -> (Option<usize>, Option<usize>) {
    match policy {
        Some(Ok(cfg)) => {
            let diagnostics = evidence_diagnostics(root, cfg, source_tree_files);
            let broken = diagnostics
                .iter()
                .filter(|reference| reference.diagnostic.status.is_broken_local_link())
                .count();
            let weak = diagnostics
                .iter()
                .filter(|reference| reference.diagnostic.status.is_weak_reference())
                .count();
            (Some(broken), Some(weak))
        }
        _ => (None, None),
    }
}

fn first_broken_evidence_diagnostic(
    root: &Path,
    cfg: &AllowConfig,
    source_tree_files: Option<&BTreeSet<String>>,
) -> Option<(String, PolicyReferenceDiagnostic)> {
    cfg.allow.iter().find_map(|entry| {
        policy_reference_diagnostics_for_source_tree(root, entry, source_tree_files)
            .into_iter()
            .find(|reference| reference.diagnostic.status.is_broken_local_link())
            .map(|reference| (entry.id.clone(), reference))
    })
}

fn evidence_diagnostics(
    root: &Path,
    cfg: &AllowConfig,
    source_tree_files: Option<&BTreeSet<String>>,
) -> Vec<PolicyReferenceDiagnostic> {
    cfg.allow
        .iter()
        .flat_map(|entry| {
            policy_reference_diagnostics_for_source_tree(root, entry, source_tree_files)
        })
        .collect()
}

fn doctor_inventory_options(policy: Option<&CargoAllowResult<AllowConfig>>) -> InventoryOptions {
    match policy {
        Some(Ok(cfg)) => InventoryOptions {
            ignored: cfg.workspace.ignored.clone(),
            generated: cfg.workspace.generated.clone(),
            include_untracked: false,
        },
        _ => InventoryOptions::default(),
    }
}

fn root_discovery_kind(explicit_root: Option<&Path>, root: &Path) -> &'static str {
    if explicit_root.is_some() {
        "explicit_root"
    } else if root.join(".git").exists() {
        "nearest_git_root"
    } else {
        "current_directory_fallback"
    }
}

#[cfg(test)]
pub(crate) fn sample_doctor_json_for_contract_test() -> String {
    allow_report::render_doctor_json(allow_report::DoctorReport {
        source_tree_root: "H:/Code/Rust/cargo-allow",
        root_discovery: "nearest_git_root",
        config_path: Some("H:/Code/Rust/cargo-allow/policy/allow.toml"),
        config_schema_version: Some("0.1"),
        config_policy: Some("cargo-allow"),
        config_owner: Some("core/policy"),
        config_status: Some("active"),
        config_valid: Some(true),
        config_diagnostic: None,
        broken_evidence_links: Some(0),
        weak_evidence_references: Some(0),
        inventory_source: "git_tracked",
        files_scanned: 50,
    })
}

#[cfg(test)]
#[path = "doctor_tests.rs"]
mod tests;