cargo-mend 0.2.2

Opinionated visibility auditing for Rust crates and workspaces
#![allow(clippy::expect_used)]
#![allow(clippy::needless_raw_string_hashes)]
#![allow(clippy::struct_field_names)]

pub use std::collections::BTreeSet;
pub use std::fs;
pub use std::path::PathBuf;
use std::process::Command;

use regex::Regex;
use serde::Deserialize;
pub use tempfile::tempdir;

use self::cargo_mend_tests_support::FixSummaryBucket;
pub use self::cargo_mend_tests_support::FixSupport;
pub use self::cargo_mend_tests_support::diagnostic_specs;

pub fn clear_wrappers(command: &mut Command) -> &mut Command {
    command
        .env_remove("RUSTC")
        .env("RUSTC_WRAPPER", "")
        .env("CARGO_BUILD_RUSTC_WRAPPER", "")
        .env_remove("RUSTC_WORKSPACE_WRAPPER")
}

pub fn cargo_command() -> Command {
    let mut command = Command::new("cargo");
    clear_wrappers(&mut command);
    command
}

pub fn mend_command() -> Command {
    let mut command = Command::new(mend_bin());
    clear_wrappers(&mut command);
    command
}

pub fn mend_bin() -> PathBuf { PathBuf::from(env!("CARGO_BIN_EXE_cargo-mend")) }

pub fn strip_ansi(input: &str) -> String {
    let ansi = Regex::new(r"\x1b\[[0-9;]*m").expect("compile ansi regex");
    ansi.replace_all(input, "").into_owned()
}

#[derive(Debug, Deserialize)]
pub struct Finding {
    pub code:        String,
    #[serde(default)]
    pub path:        String,
    #[serde(default)]
    pub item:        Option<String>,
    #[serde(default)]
    pub fix_support: FixSupport,
}

#[derive(Debug, Deserialize)]
pub struct Report {
    pub summary:  Summary,
    pub findings: Vec<Finding>,
}

#[derive(Debug, Deserialize)]
pub struct Summary {
    pub error_count:                    usize,
    pub warning_count:                  usize,
    pub fixable_with_fix_count:         usize,
    pub fixable_with_fix_pub_use_count: usize,
}

#[derive(Clone, Copy)]
pub struct ExpectedFinding<'a> {
    pub code:        &'a str,
    pub fix_support: FixSupport,
}

pub fn severity_for_code(code: &str) -> &'static str {
    match code {
        "forbidden_pub_crate" | "forbidden_pub_in_crate" | "review_pub_mod" => "error",
        _ => "warning",
    }
}

pub fn expected_summary(report: &Report) -> Summary {
    let mut summary = Summary {
        error_count:                    0,
        warning_count:                  0,
        fixable_with_fix_count:         0,
        fixable_with_fix_pub_use_count: 0,
    };

    for finding in &report.findings {
        match severity_for_code(&finding.code) {
            "error" => summary.error_count += 1,
            _ => summary.warning_count += 1,
        }

        let fix_support = if matches!(finding.fix_support, FixSupport::None) {
            diagnostic_specs()
                .iter()
                .find(|spec| spec.code == finding.code)
                .expect("known diagnostic code")
                .fix_support
        } else {
            finding.fix_support
        };
        match fix_support.summary_bucket() {
            Some(FixSummaryBucket::Fix) => summary.fixable_with_fix_count += 1,
            Some(FixSummaryBucket::FixPubUse) => summary.fixable_with_fix_pub_use_count += 1,
            None => {},
        }
    }

    summary
}

pub fn assert_summary_matches_findings(report: &Report) {
    let expected = expected_summary(report);
    assert_eq!(report.summary.error_count, expected.error_count);
    assert_eq!(report.summary.warning_count, expected.warning_count);
    assert_eq!(
        report.summary.fixable_with_fix_count,
        expected.fixable_with_fix_count
    );
    assert_eq!(
        report.summary.fixable_with_fix_pub_use_count,
        expected.fixable_with_fix_pub_use_count
    );
}

pub fn fix_support_for(code: &str, fix_support: FixSupport) -> FixSupport {
    if matches!(fix_support, FixSupport::None) {
        diagnostic_specs()
            .iter()
            .find(|spec| spec.code == code)
            .expect("known diagnostic code")
            .fix_support
    } else {
        fix_support
    }
}

pub fn expected_summary_from_findings(expected_findings: &[ExpectedFinding<'_>]) -> Summary {
    let mut summary = Summary {
        error_count:                    0,
        warning_count:                  0,
        fixable_with_fix_count:         0,
        fixable_with_fix_pub_use_count: 0,
    };

    for finding in expected_findings {
        match severity_for_code(finding.code) {
            "error" => summary.error_count += 1,
            _ => summary.warning_count += 1,
        }

        let fix_support = fix_support_for(finding.code, finding.fix_support);

        match fix_support.summary_bucket() {
            Some(FixSummaryBucket::Fix) => summary.fixable_with_fix_count += 1,
            Some(FixSummaryBucket::FixPubUse) => summary.fixable_with_fix_pub_use_count += 1,
            None => {},
        }
    }

    summary
}

pub fn expected_summary_text(report: &Report) -> String {
    let mut parts = vec![
        format!("{} error(s)", report.summary.error_count),
        format!("{} warning(s)", report.summary.warning_count),
    ];

    if report.summary.fixable_with_fix_count > 0 {
        parts.push(format!(
            "{} fixable with `--fix`",
            report.summary.fixable_with_fix_count
        ));
    }

    if report.summary.fixable_with_fix_pub_use_count > 0 {
        parts.push(format!(
            "{} fixable with `--fix-pub-use`",
            report.summary.fixable_with_fix_pub_use_count
        ));
    }

    format!("summary: {}", parts.join(", "))
}

pub fn run_mend_json(manifest_path: &std::path::Path) -> Report {
    let output = mend_command()
        .arg("--manifest-path")
        .arg(manifest_path)
        .arg("--json")
        .output()
        .expect("run cargo-mend --json");
    assert!(
        matches!(output.status.code(), Some(0..=2)),
        "cargo-mend returned unexpected status {:?}: {}",
        output.status.code(),
        String::from_utf8_lossy(&output.stderr)
    );
    serde_json::from_slice(&output.stdout).expect("parse mend json report")
}

pub mod cargo_mend_tests_support {
    #![allow(
        dead_code,
        reason = "include!() pulls in entire source files; only a subset is re-exported"
    )]

    mod fix_support {
        include!("../../src/fix_support.rs");
    }

    mod diagnostics_impl {
        include!("../../src/diagnostics.rs");
    }

    pub use diagnostics_impl::*;
    pub use fix_support::FixSummaryBucket;
    pub use fix_support::FixSupport;

    pub const fn diagnostic_specs() -> &'static [DiagnosticSpec] { DIAGNOSTICS }
}