cuke-dedup 0.6.0

Static analysis for duplicate and reusable Cucumber step definitions
Documentation
use super::shared::{bounded_findings, count_label, terminal_safe, MAX_REPORTED_CLUSTER_MEMBERS};
use super::ReportContext;
use anyhow::Result;
use std::collections::BTreeMap;
use std::io::Write;

/// Writes a plain-text terminal report to `writer`.
pub fn write_terminal(context: &ReportContext<'_>, writer: &mut dyn Write) -> Result<()> {
    write_terminal_with_color(context, writer, false)
}

pub(super) fn write_terminal_with_color(
    context: &ReportContext<'_>,
    writer: &mut dyn Write,
    color: bool,
) -> Result<()> {
    let result = context.result;
    let root = context.root;
    let summary = &context.summary;
    writeln!(writer, "CukeDedup v{}", context.tool_version)?;
    writeln!(writer)?;
    let mut wrote_rule = false;
    let mut findings_by_rule: BTreeMap<_, Vec<_>> = BTreeMap::new();
    let selected = bounded_findings(result, true);
    let findings_truncated = selected.truncated;
    for finding in selected.findings {
        findings_by_rule
            .entry(finding.rule)
            .or_default()
            .push(finding);
    }
    for (rule, findings) in findings_by_rule {
        if wrote_rule {
            writeln!(writer)?;
        }
        writeln!(writer, "{}", colorize(rule.to_string(), "1;36", color))?;
        writeln!(writer)?;
        wrote_rule = true;
        for (index, finding) in findings.into_iter().enumerate() {
            if index > 0 {
                writeln!(writer)?;
            }
            // The active-finding selection excludes Off.
            let severity_color = if finding.severity == crate::model::Severity::Error {
                "31"
            } else {
                "33"
            };
            let severity = colorize(format!("[{}]", finding.severity), severity_color, color);
            writeln!(writer, "  {severity} {}", terminal_safe(&finding.message))?;
            writeln!(
                writer,
                "    --> {}",
                colorize(terminal_safe(&finding.primary.display(root)), "94", color)
            )?;
            let retained_related = MAX_REPORTED_CLUSTER_MEMBERS.saturating_sub(1);
            for related in finding.related.iter().take(retained_related) {
                writeln!(
                    writer,
                    "    related: {}",
                    colorize(terminal_safe(&related.display(root)), "94", color)
                )?;
            }
            if finding.related.len() > retained_related {
                writeln!(
                    writer,
                    "    related: ... {} more locations omitted",
                    finding.related.len() - retained_related
                )?;
            }
            if !finding.evidence.matcher_difference.is_empty() {
                writeln!(
                    writer,
                    "    evidence: {}",
                    terminal_safe(&finding.evidence.matcher_difference)
                )?;
            }
            if !finding.evidence.handler_evidence.is_empty() {
                writeln!(
                    writer,
                    "    handler: {}",
                    terminal_safe(&finding.evidence.handler_evidence)
                )?;
            }
            writeln!(
                writer,
                "    action: {}",
                terminal_safe(&finding.suggested_action)
            )?;
        }
    }
    if summary.findings > 0 {
        writeln!(writer)?;
    }
    if findings_truncated > 0 {
        writeln!(
            writer,
            "Showing the highest-severity findings; {} additional active findings omitted",
            findings_truncated
        )?;
    }
    writeln!(
        writer,
        "Analyzed {} and {}: {}, {}, {} suppressed",
        count_label(summary.definitions_analyzed, "definition"),
        count_label(summary.feature_steps_analyzed, "feature step"),
        count_label(summary.errors, "error"),
        count_label(summary.warnings, "warning"),
        summary.suppressed
    )?;
    writeln!(
        writer,
        "Duplication: {} of {} definitions ({:.2}%), threshold {:.2}% — {}",
        summary.duplication.duplicated_definitions,
        summary.duplication.total_definitions,
        summary.duplication.percentage,
        summary.duplication.threshold,
        if summary.duplication.passed {
            "PASS"
        } else {
            "FAIL"
        }
    )?;
    if !summary.duplication.rules.is_empty() {
        writeln!(
            writer,
            "Contributing rules: {}",
            summary
                .duplication
                .rules
                .iter()
                .map(ToString::to_string)
                .collect::<Vec<_>>()
                .join(", ")
        )?;
    }
    writeln!(writer)?;
    writeln!(
        writer,
        "{}",
        colorize(
            format!("Found {}.", count_label(summary.findings, "finding")),
            "90",
            color
        )
    )?;
    if let Some(metrics) = context.metrics {
        writeln!(
            writer,
            "{}",
            colorize(
                format!(
                    "Detection time: {:.1} ms",
                    metrics.discovery_ms + metrics.parsing_ms + metrics.analysis_ms
                ),
                "90",
                color
            )
        )?;
    }
    Ok(())
}

fn colorize(text: String, code: &str, enabled: bool) -> String {
    if enabled {
        format!("\u{1b}[{code}m{text}\u{1b}[0m")
    } else {
        text
    }
}