cargo-features 1.0.0

Power tools for working with (conditional) features
use anyhow::{Context, Result};
use cargo_metadata::MetadataCommand;
use colored::Colorize;
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;

pub fn run(enabled_only: bool, manifest_path: Option<PathBuf>) -> Result<()> {
    let mut cmd = MetadataCommand::new();

    if let Some(path) = manifest_path {
        cmd.manifest_path(path);
    }

    let metadata = cmd.exec().context("Failed to get cargo metadata")?;
    let root_package = metadata.root_package().context("No root package found")?;

    println!(
        "{} {} ({})\n",
        "Analyzing".bright_blue().bold(),
        root_package.name.bright_white().bold(),
        root_package.version
    );

    let enabled_features = get_enabled_features(&metadata)?;

    println!("{}", "Root package features:\n".bright_green().bold());

    let mut root_features: Vec<_> = root_package.features.keys().collect();
    root_features.sort();

    for feature in root_features {
        let deps = &root_package.features[feature];
        let is_enabled = enabled_features
            .get(&root_package.id)
            .map(|f| f.contains(feature))
            .unwrap_or(false);

        if enabled_only && !is_enabled {
            continue;
        }

        let (marker, style) = if is_enabled {
            ("", feature.bright_green())
        } else {
            ("", feature.dimmed())
        };

        println!(
            "  {} {} {}",
            marker,
            style,
            if deps.is_empty() {
                String::new()
            } else {
                format!("{}", deps.join(", ")).dimmed().to_string()
            }
        );
    }

    println!("\n{}", "Features by dependency:".bright_green().bold());

    let mut dep_features: Vec<_> = metadata
        .packages
        .iter()
        .filter(|p| p.id != root_package.id && !p.features.is_empty())
        .collect();

    dep_features.sort_by(|a, b| a.name.cmp(&b.name));

    for package in dep_features.as_slice() {
        let package_enabled = enabled_features
            .get(&package.id)
            .cloned()
            .unwrap_or_default();

        let mut features: Vec<_> = package.features.keys().collect();
        features.sort();

        let features_to_show: Vec<_> = if enabled_only {
            features
                .iter()
                .filter(|f| package_enabled.contains(f.as_str()))
                .copied()
                .collect()
        } else {
            features
        };

        if enabled_only && features_to_show.is_empty() {
            continue;
        }

        println!(
            "\n{} ({})",
            package.name.bright_cyan().bold(),
            package.version
        );

        let enabled_count = features_to_show
            .iter()
            .filter(|f| package_enabled.contains(f.as_str()))
            .count();

        if !enabled_only && enabled_count > 0 {
            println!(
                "  {} {}/{}",
                "Enabled:".bright_white(),
                enabled_count.to_string().bright_green(),
                features_to_show.len()
            );
        }

        for feature in features_to_show {
            let is_enabled = package_enabled.contains(feature);
            let (marker, style) = if is_enabled {
                ("", feature.bright_green())
            } else {
                ("", feature.dimmed())
            };

            println!("  {} {}", marker, style);
        }
    }

    if dep_features.is_empty() {
        println!("  {}", "No dependency features found".dimmed());
    }

    let total_enabled: usize = enabled_features.values().map(|f| f.len()).sum();

    println!(
        "\n{} {} features enabled across {} crates",
        "Summary:".bright_blue().bold(),
        total_enabled.to_string().bright_green(),
        enabled_features.len()
    );

    Ok(())
}

fn get_enabled_features(
    metadata: &cargo_metadata::Metadata,
) -> Result<HashMap<cargo_metadata::PackageId, HashSet<String>>> {
    let mut enabled = HashMap::new();

    if let Some(resolve) = &metadata.resolve {
        for node in &resolve.nodes {
            let mut features = HashSet::new();

            for feature in &node.features {
                features.insert(feature.to_string());
            }

            enabled.insert(node.id.clone(), features);
        }
    }

    Ok(enabled)
}