feature-manifest 0.6.0

Document, validate, and render Cargo feature metadata.
Documentation
use std::collections::BTreeMap;

use anyhow::{Result, bail};

use crate::cli::output::{check_json, github, sarif};
use crate::{FeatureManifest, LintLevel, LintPreset, ValidationReport, WorkspaceManifest};
use crate::{ValidateOptions, parse_lint_override, validate_with_options};

use super::super::CheckFormat;

pub type PackageReport<'a> = (&'a FeatureManifest, ValidationReport);

#[derive(Debug, Clone, Copy)]
pub struct Summary {
    pub packages: usize,
    pub features: usize,
    pub groups: usize,
    pub errors: usize,
    pub warnings: usize,
}

pub fn run(
    workspace: &WorkspaceManifest,
    format: CheckFormat,
    lint_overrides: &[String],
    preset: Option<LintPreset>,
) -> Result<()> {
    let package_reports = collect_reports(workspace, lint_overrides, preset)?;
    let summary = workspace_summary(workspace, &package_reports);

    match format {
        CheckFormat::Text => emit_text(workspace, &package_reports, &summary),
        CheckFormat::Json => println!(
            "{}",
            check_json::render(workspace, &package_reports, &summary)?
        ),
        CheckFormat::Github => github::emit(workspace, &package_reports),
        CheckFormat::Sarif => println!(
            "{}",
            serde_json::to_string_pretty(&sarif::render(workspace, &package_reports))?
        ),
    }

    if summary.errors > 0 {
        bail!("validation failed");
    }

    Ok(())
}

pub fn collect_reports<'a>(
    workspace: &'a WorkspaceManifest,
    lint_overrides: &[String],
    preset: Option<LintPreset>,
) -> Result<Vec<PackageReport<'a>>> {
    let mut cli_lints = BTreeMap::<String, LintLevel>::new();
    for override_value in lint_overrides {
        let (code, level) = parse_lint_override(override_value)?;
        cli_lints.insert(code, level);
    }

    let validate_options =
        ValidateOptions::with_cli_lint_overrides(cli_lints).with_cli_preset(preset);

    Ok(workspace
        .packages
        .iter()
        .map(|package| (package, validate_with_options(package, &validate_options)))
        .collect())
}

pub fn workspace_summary(
    workspace: &WorkspaceManifest,
    package_reports: &[PackageReport<'_>],
) -> Summary {
    Summary {
        packages: workspace.packages.len(),
        features: workspace
            .packages
            .iter()
            .map(|package| package.features.len())
            .sum(),
        groups: workspace
            .packages
            .iter()
            .map(|package| package.groups.len())
            .sum(),
        errors: package_reports
            .iter()
            .map(|(_, report)| report.error_count())
            .sum(),
        warnings: package_reports
            .iter()
            .map(|(_, report)| report.warning_count())
            .sum(),
    }
}

fn emit_text(
    workspace: &WorkspaceManifest,
    package_reports: &[PackageReport<'_>],
    summary: &Summary,
) {
    for (package, report) in package_reports {
        if workspace.is_single_package() {
            emit_package_report(None, package, report);
            continue;
        }

        emit_package_report(package.package_name.as_deref(), package, report);
    }

    if !workspace.is_single_package() {
        eprintln!(
            "workspace summary: validated {} package(s), {} feature(s), {} group(s): {} error(s), {} warning(s)",
            summary.packages, summary.features, summary.groups, summary.errors, summary.warnings
        );
    }
}

fn emit_package_report(
    package_name: Option<&str>,
    package: &FeatureManifest,
    report: &ValidationReport,
) {
    let summary = report.summary(package.features.len(), package.groups.len());

    if report.issues.is_empty() {
        if package_name.is_some() {
            println!("package `{}`", package_name.unwrap_or("unknown-package"));
            println!("  {summary}");
        } else {
            println!("{summary}");
        }
        return;
    }

    if let Some(package_name) = package_name {
        eprintln!("package `{package_name}`");
    }

    for issue in &report.issues {
        if package_name.is_some() {
            eprintln!("  {issue}");
        } else {
            eprintln!("{issue}");
        }
    }

    if package_name.is_some() {
        eprintln!("  {summary}");
    } else {
        eprintln!("{summary}");
    }
}