ripr 0.6.0

Find static mutation-exposure gaps before expensive mutation testing
Documentation
use crate::config::RiprConfig;
use crate::domain::{Finding, LanguageId, LanguageStatus};

use super::evidence_lines::{evidence_path_lines, weakness_lines};

pub(crate) fn render_finding_with_config(finding: &Finding, config: &RiprConfig) -> String {
    let mut out = String::new();
    let severity = config.severity().for_exposure(&finding.class).as_str();
    out.push_str(&format!(
        "{} {}:{}\n",
        severity.to_ascii_uppercase(),
        finding.probe.location.file.display(),
        finding.probe.location.line
    ));

    out.push_str("\nChanged\n");
    if let Some(before) = &finding.probe.before {
        out.push_str(&format!("  before: {before}\n"));
    }
    if let Some(after) = &finding.probe.after {
        out.push_str(&format!("  after:  {after}\n"));
    } else {
        out.push_str(&format!("  expr:   {}\n", finding.probe.expression));
    }

    out.push_str("\nProbe\n");
    out.push_str(&format!(
        "  family: {}\n  delta:  {}\n",
        finding.probe.family.as_str(),
        finding.probe.delta.as_str()
    ));
    if let Some(owner) = &finding.probe.owner {
        out.push_str(&format!("  owner:  {owner}\n"));
    }

    if should_render_language_metadata(finding) {
        out.push_str("\nLanguage\n");
        if let Some(language) = finding.language {
            out.push_str(&format!("  language: {}\n", language.as_str()));
        }
        if let Some(status) = finding.language_status {
            out.push_str(&format!("  status: {}\n", status.as_str()));
        }
        if let Some(owner_kind) = finding.owner_kind {
            out.push_str(&format!("  owner kind: {}\n", owner_kind.as_str()));
        }
    }

    out.push_str("\nStatic exposure\n");
    out.push_str(&format!(
        "  {} ({}, confidence {:.2})\n",
        finding.class.as_str(),
        severity,
        finding.confidence
    ));

    out.push_str("\nEvidence\n");
    for line in evidence_path_lines(finding) {
        out.push_str(&format!("  - {line}\n"));
    }

    let weakness = weakness_lines(finding);
    if !weakness.is_empty() {
        out.push_str("\nWeakness\n");
        for line in weakness {
            out.push_str(&format!("  - {line}\n"));
        }
    }

    let stop_reasons = finding.effective_stop_reasons();
    if !stop_reasons.is_empty() {
        out.push_str("\nStop reasons:\n");
        for reason in &stop_reasons {
            out.push_str(&format!("  - {}\n", reason.as_str()));
        }
    }

    if let Some(step) = &finding.recommended_next_step {
        out.push_str("\nNext step\n");
        out.push_str(&format!("  {step}\n"));
    }

    out
}

fn should_render_language_metadata(finding: &Finding) -> bool {
    finding
        .language
        .is_some_and(|language| language != LanguageId::Rust)
        || finding
            .language_status
            .is_some_and(|status| status != LanguageStatus::Stable)
        || finding.owner_kind.is_some()
}