cargo-features-manager 0.12.0

A tui tool to enable/disable & prune dependency features
use color_eyre::Result;

use crate::io::util::{get_item_from_doc, toml_document_from_path};
use crate::project::dependency::Dependency;
use crate::project::document::Document;
use crate::prune::FeaturesMap;
use color_eyre::eyre::{ContextCompat, eyre};
use std::collections::HashMap;
use std::ops::Not;
use std::path::Path;

pub fn get_features_to_test(
    document: &Document,
    only_dependency_features: bool,
) -> Result<FeaturesMap> {
    let base_ignored_features =
        get_ignored_features("./", "workspace.cargo-features-manager.keep")?;

    let mut enabled_features = get_enabled_features(document);

    if only_dependency_features {
        remove_non_dependency_features(document, &mut enabled_features)?;
    }

    remove_ignored_features(document, &base_ignored_features, &mut enabled_features)?;

    Ok(enabled_features)
}

fn get_enabled_features(document: &Document) -> FeaturesMap {
    let mut data = HashMap::new();

    for package in document.get_packages() {
        let mut package_data = HashMap::new();

        for dependency in package.get_deps() {
            let enabled_features = dependency
                .features
                .iter()
                .filter(|(_name, data)| data.is_toggleable() && data.is_enabled())
                .map(|(name, _data)| name)
                .cloned()
                .collect::<Vec<String>>();

            if enabled_features.is_empty().not() {
                package_data.insert(dependency.get_name().clone(), enabled_features);
            }
        }

        if package_data.is_empty().not() {
            data.insert(package.name.clone(), package_data);
        }
    }

    data
}

fn get_ignored_features<P: AsRef<Path>>(
    file_path: P,
    item_path: &str,
) -> Result<HashMap<String, Vec<String>>> {
    let result = toml_document_from_path(file_path.as_ref().join("Cargo.toml"));

    match result {
        Ok(document) => {
            let item = get_item_from_doc(item_path, &document);

            let Ok(item) = item else {
                return Ok(HashMap::new());
            };

            let table = item.as_table_like().context(format!(
                "could not parse {} in {:?}",
                item_path,
                file_path.as_ref()
            ))?;

            let mut map = HashMap::new();

            for (key, value) in table.iter() {
                map.insert(
                    key.to_string(),
                    value
                        .as_array()
                        .ok_or(eyre!("Invalid format to keep features"))?
                        .iter()
                        .filter_map(|value| value.as_str())
                        .map(|value| value.to_string())
                        .collect(),
                );
            }

            Ok(map)
        }
        Err(_) => Ok(HashMap::new()),
    }
}

fn remove_non_dependency_features(
    document: &Document,
    enabled_features: &mut FeaturesMap,
) -> Result<()> {
    for (package_name, dependencies) in enabled_features {
        let package = document.get_package(package_name)?;

        for (dependency_name, features) in dependencies {
            let dependency = package.get_dep(dependency_name)?;

            for feature_name in &features.clone() {
                let Some(feature) = dependency.get_feature(feature_name) else {
                    continue;
                };

                if feature.has_dependency_features().not() {
                    remove_feature(feature_name, features, dependency);
                }
            }
        }
    }

    Ok(())
}

fn remove_ignored_features(
    document: &Document,
    base_ignored: &HashMap<String, Vec<String>>,
    enabled_features: &mut FeaturesMap,
) -> Result<()> {
    for (package_name, dependencies) in enabled_features {
        let package = document.get_package(package_name)?;

        let ignored_features = get_ignored_features(
            package.manifest_path.trim_end_matches("/Cargo.toml"),
            "cargo-features-manager.keep",
        )?;

        for (dependency_name, features) in dependencies {
            let dependency = package.get_dep(dependency_name)?;

            if dependency.can_use_default() {
                features.push("default".to_string());
            }

            for feature in ignored_features.get(&dependency.name).unwrap_or(&vec![]) {
                remove_feature(feature, features, dependency);
            }
            for feature in base_ignored.get(&dependency.name).unwrap_or(&vec![]) {
                remove_feature(feature, features, dependency);
            }

            if let Some(index) = features.iter().position(|name| name == "default") {
                features.remove(index);
            }
        }
    }

    Ok(())
}

fn remove_feature(feature: &String, features: &mut Vec<String>, dependency: &Dependency) {
    let index = features.iter().position(|name| name == feature);

    let Some(index) = index else {
        return;
    };

    features.remove(index);

    if let Some(feature) = dependency.get_feature(feature) {
        for sub_feature in &feature.sub_features {
            remove_feature(&sub_feature.name, features, dependency);
        }
    }
}