repopilot 0.18.0

Local-first CLI for reviewing Git changes, security boundaries, and blast radius before merge.
Documentation
use super::RULE_ID;
use super::pattern::RustPanicPattern;
use crate::audits::context::{AuditContext, FileRole, RuntimeKind};
use crate::findings::types::{Confidence, Evidence, Finding, FindingCategory, Severity};
use crate::scan::facts::FileFacts;

const RUST_ERROR_HANDLING_DOCS_URL: &str =
    "https://doc.rust-lang.org/book/ch09-00-error-handling.html";

pub(super) fn build_finding(
    file: &FileFacts,
    line_number: usize,
    snippet: &str,
    pattern: RustPanicPattern,
    context: &AuditContext,
    severity: Severity,
) -> Finding {
    let context_label = context_label(context);
    let recommendation = recommendation_for(context, pattern);
    let confidence = confidence_for(context, pattern, severity);
    let confidence_reason = confidence_reason_for(context, pattern);

    Finding {
        id: String::new(),
        rule_id: RULE_ID.to_string(),
        recommendation: recommendation.to_string(),
        title: format!("Risky Rust {} usage in {}", pattern.label(), context_label),
        description: format!(
            "Rust `{}` was found in {}; confidence is {} because {}. Unhandled panic paths can terminate execution or hide recoverable errors in production code.",
            pattern.label(),
            context_label,
            confidence.label(),
            confidence_reason,
        ),
        category: FindingCategory::CodeQuality,
        severity,
        confidence,
        evidence: vec![Evidence {
            path: file.path.clone(),
            line_start: line_number,
            line_end: None,
            snippet: snippet.to_string(),
        }],
        workspace_package: None,
        docs_url: Some(RUST_ERROR_HANDLING_DOCS_URL.to_string()),
        provenance: Default::default(),
        risk: Default::default(),
    }
}

fn context_label(context: &AuditContext) -> &'static str {
    if context.is_test {
        return "Rust test code";
    }

    if context.has_runtime(RuntimeKind::RustCli) {
        return "Rust CLI boundary code";
    }

    if context.has_role(FileRole::Domain) {
        return "Rust domain code";
    }

    if context.has_runtime(RuntimeKind::RustLibrary) {
        return "Rust library code";
    }

    "Rust production code"
}

fn confidence_for(
    context: &AuditContext,
    pattern: RustPanicPattern,
    severity: Severity,
) -> Confidence {
    if context.is_test {
        return Confidence::Low;
    }

    if matches!(
        pattern,
        RustPanicPattern::Todo | RustPanicPattern::Unimplemented
    ) {
        return Confidence::High;
    }

    if context.has_runtime(RuntimeKind::RustCli) {
        return match pattern {
            RustPanicPattern::Unwrap
            | RustPanicPattern::UnwrapErr
            | RustPanicPattern::Expect
            | RustPanicPattern::ExpectErr => Confidence::Low,
            RustPanicPattern::Panic => Confidence::Medium,
            RustPanicPattern::Todo | RustPanicPattern::Unimplemented => Confidence::High,
        };
    }

    if context.has_role(FileRole::Domain) || context.has_runtime(RuntimeKind::RustLibrary) {
        return Confidence::High;
    }

    match severity {
        Severity::High | Severity::Critical => Confidence::High,
        Severity::Info | Severity::Low => Confidence::Low,
        Severity::Medium => Confidence::Medium,
    }
}

fn confidence_reason_for(context: &AuditContext, pattern: RustPanicPattern) -> &'static str {
    if context.is_test {
        return "test code often uses panic-style macros for assertion setup or unfinished test scaffolding";
    }

    if matches!(
        pattern,
        RustPanicPattern::Todo | RustPanicPattern::Unimplemented
    ) {
        return "placeholder macros always panic if this path is reached";
    }

    if context.has_runtime(RuntimeKind::RustCli) {
        return match pattern {
            RustPanicPattern::Unwrap
            | RustPanicPattern::UnwrapErr
            | RustPanicPattern::Expect
            | RustPanicPattern::ExpectErr => {
                "CLI boundary code may intentionally fail fast, but user-facing errors are usually better"
            }
            RustPanicPattern::Panic => {
                "CLI boundary code can terminate the process intentionally, but panic output is rarely a good user-facing error"
            }
            RustPanicPattern::Todo | RustPanicPattern::Unimplemented => {
                "placeholder macros always panic if this path is reached"
            }
        };
    }

    if context.has_role(FileRole::Domain) {
        return "domain code is usually reusable production logic, so callers cannot recover from this panic path";
    }

    if context.has_runtime(RuntimeKind::RustLibrary) {
        return "library code is reused by callers, so panics become part of its public failure behavior";
    }

    "this is production Rust code and the panic path is not locally handled"
}

fn recommendation_for(context: &AuditContext, pattern: RustPanicPattern) -> &'static str {
    if context.is_test {
        return "Panic-style helpers in tests can be acceptable, but keep them out of reusable test utilities when possible.";
    }

    match pattern {
        RustPanicPattern::Unwrap | RustPanicPattern::UnwrapErr => {
            if context.has_runtime(RuntimeKind::RustCli) {
                "At CLI boundaries this may be acceptable for prototype code, but prefer returning a user-friendly error with context."
            } else {
                "Return `Result` or propagate with `?`; convert to `expect()` only when failure is impossible and the message documents the invariant."
            }
        }
        RustPanicPattern::Expect | RustPanicPattern::ExpectErr => {
            if context.has_runtime(RuntimeKind::RustCli) {
                "At CLI boundaries this may be acceptable for prototype code, but prefer returning a user-friendly error with context."
            } else {
                "Prefer `Result`/`?` for recoverable errors. Keep `expect()` only for impossible states, with a message that names the invariant."
            }
        }
        RustPanicPattern::Panic => {
            "Avoid panics in reusable production code. Prefer typed errors, validation, or explicit fallback behavior."
        }
        RustPanicPattern::Todo | RustPanicPattern::Unimplemented => {
            "Replace placeholder macros before release or guard them behind test-only code paths."
        }
    }
}