cargo-crap4rust 0.6.1

Cargo subcommand for computing CRAP scores across Rust crates
Documentation
// Copyright 2025 Umberto Gotti <umberto.gotti@umbertogotti.dev>
// Licensed under the MIT License or Apache License, Version 2.0
// SPDX-License-Identifier: MIT OR Apache-2.0

use crate::source_root_collector::SourceRootCollector;
use std::path::{Path, PathBuf};

use anyhow::{Context, Result, bail};
use cargo_metadata::{Metadata, MetadataCommand, Package};

use crate::model::{Config, PackageContext};

pub fn resolve_packages(config: &Config) -> Result<Vec<PackageContext>> {
    let mut command = MetadataCommand::new();
    command.no_deps();
    if let Some(manifest_path) = &config.manifest_path {
        command.manifest_path(manifest_path);
    }

    let metadata = command.exec().context("failed to read Cargo metadata")?;
    let workspace_root = metadata.workspace_root.clone().into_std_path_buf();
    let packages = select_packages(&metadata, &config.packages)?;

    packages
        .into_iter()
        .map(|package| {
            build_package_context(
                package,
                &workspace_root,
                config.include_test_targets,
                config.exclude_paths.clone(),
            )
        })
        .collect()
}

fn build_package_context(
    package: &Package,
    workspace_root: &Path,
    include_test_targets: bool,
    exclude_paths: Vec<String>,
) -> Result<PackageContext> {
    let manifest_dir = package
        .manifest_path
        .clone()
        .into_std_path_buf()
        .parent()
        .map(PathBuf::from)
        .context("package manifest has no parent directory")?;

    let mut collector = SourceRootCollector::new(include_test_targets, &manifest_dir);
    collector.collect(&package.targets);
    let source_roots = collector.finalize();

    Ok(PackageContext {
        name: package.name.to_string(),
        manifest_dir,
        workspace_root: workspace_root.to_path_buf(),
        source_roots,
        include_test_targets,
        exclude_paths,
    })
}

fn select_packages<'a>(metadata: &'a Metadata, requested: &[String]) -> Result<Vec<&'a Package>> {
    if !requested.is_empty() {
        let mut selected = Vec::new();
        for package_name in requested {
            let package = metadata
                .packages
                .iter()
                .find(|package| package.name == package_name)
                .with_context(|| format!("package {package_name} was not found in the manifest"))?;
            selected.push(package);
        }
        return Ok(selected);
    }

    if let Some(root) = metadata.root_package()
        && metadata.workspace_members.len() <= 1
    {
        return Ok(vec![root]);
    }

    let workspace_member_ids = metadata
        .workspace_members
        .iter()
        .cloned()
        .collect::<std::collections::BTreeSet<_>>();
    let selected = metadata
        .packages
        .iter()
        .filter(|package| workspace_member_ids.contains(&package.id))
        .collect::<Vec<_>>();
    if selected.is_empty() {
        bail!("manifest contains no workspace member packages");
    }

    Ok(selected)
}