patholog 1.0.0

Diagnose, explain, clean, and safely repair PATH behavior.
Documentation
use crate::model::{Diagnostic, DoctorReport, IssueKind};

use super::shared::finish_lines;

pub(crate) fn format_doctor(report: &DoctorReport) -> String {
    let mut lines = vec![format!(
        "{} entries: {}",
        report.variable.env_name(),
        report.entries.len()
    )];
    if report.diagnostics.is_empty() {
        lines.push(String::new());
        lines.push("No issues found.".to_owned());
        return finish_lines(lines);
    }

    append_diagnostic_group(
        &mut lines,
        "Duplicates:",
        &report.diagnostics,
        IssueKind::Duplicate,
        format_duplicate,
    );
    append_diagnostic_group(
        &mut lines,
        "Missing directories:",
        &report.diagnostics,
        IssueKind::Missing,
        format_indexed,
    );
    append_diagnostic_group(
        &mut lines,
        "Non-directories:",
        &report.diagnostics,
        IssueKind::NotDirectory,
        format_indexed,
    );
    append_diagnostic_group(
        &mut lines,
        "Unreadable directories:",
        &report.diagnostics,
        IssueKind::Unreadable,
        format_indexed,
    );
    append_diagnostic_group(
        &mut lines,
        "Unwanted entries:",
        &report.diagnostics,
        IssueKind::Unwanted,
        format_indexed,
    );
    append_diagnostic_group(
        &mut lines,
        "Empty entries:",
        &report.diagnostics,
        IssueKind::Empty,
        format_empty,
    );
    append_diagnostic_group(
        &mut lines,
        "Ordering warnings:",
        &report.diagnostics,
        IssueKind::SuspiciousOrder,
        format_message,
    );
    append_diagnostic_group(
        &mut lines,
        "Shadowed commands:",
        &report.diagnostics,
        IssueKind::ShadowedCommand,
        format_message,
    );

    finish_lines(lines)
}

fn append_diagnostic_group(
    lines: &mut Vec<String>,
    heading: &str,
    diagnostics: &[Diagnostic],
    kind: IssueKind,
    formatter: fn(&Diagnostic) -> String,
) {
    let matching = diagnostics
        .iter()
        .filter(|diagnostic| diagnostic.kind == kind)
        .collect::<Vec<_>>();
    if matching.is_empty() {
        return;
    }

    lines.push(String::new());
    lines.push(heading.to_owned());
    lines.extend(matching.iter().map(|diagnostic| formatter(diagnostic)));
}

fn format_duplicate(diagnostic: &Diagnostic) -> String {
    let indexes = diagnostic
        .related_indexes
        .iter()
        .map(usize::to_string)
        .collect::<Vec<_>>()
        .join(", ");
    let value = diagnostic.entry_value.as_deref().unwrap_or("");
    format!(
        "  {value} ({}x; entries {indexes})",
        diagnostic.related_indexes.len()
    )
}

fn format_indexed(diagnostic: &Diagnostic) -> String {
    format!(
        "  {}  {}",
        diagnostic.entry_index.unwrap_or_default(),
        diagnostic.entry_value.as_deref().unwrap_or("")
    )
}

fn format_empty(diagnostic: &Diagnostic) -> String {
    format!("  {}  <empty>", diagnostic.entry_index.unwrap_or_default())
}

fn format_message(diagnostic: &Diagnostic) -> String {
    format!("  {}", diagnostic.message)
}