cargo-license 0.4.2

Cargo subcommand to see license of dependencies
Documentation
use serde_derive::Serialize;
use std::collections::{HashMap, HashSet};

pub type Result<T> = std::result::Result<T, anyhow::Error>;

fn normalize(license_string: &str) -> String {
    let mut list: Vec<&str> = license_string
        .split('/')
        .flat_map(|e| e.split(" OR "))
        .map(str::trim)
        .collect();
    list.sort_unstable();
    list.dedup();
    list.join(" OR ")
}

#[derive(Debug, Serialize, Clone, Hash, Ord, PartialOrd, Eq, PartialEq)]
pub struct DependencyDetails {
    pub name: String,
    pub version: semver::Version,
    pub authors: Option<String>,
    pub repository: Option<String>,
    pub license: Option<String>,
    pub license_file: Option<String>,
    pub description: Option<String>,
}

impl DependencyDetails {
    #[must_use]
    pub fn new(package: &cargo_metadata::Package) -> Self {
        let authors = if package.authors.is_empty() {
            None
        } else {
            Some(package.authors.to_owned().join("|"))
        };
        Self {
            name: package.name.to_owned(),
            version: package.version.to_owned(),
            authors,
            repository: package.repository.to_owned(),
            license: package.license.as_ref().map(|s| normalize(s)),
            license_file: package.license_file.to_owned().map(|f| f.into_string()),
            description: package
                .description
                .to_owned()
                .map(|s| s.trim().replace("\n", " ")),
        }
    }
}

pub fn get_dependencies_from_cargo_lock(
    metadata_command: cargo_metadata::MetadataCommand,
    avoid_dev_deps: bool,
    avoid_build_deps: bool,
) -> Result<Vec<DependencyDetails>> {
    let metadata = metadata_command.exec()?;

    let connected = {
        let resolve = metadata.resolve.as_ref().expect("missing `resolve`");

        let deps = resolve
            .nodes
            .iter()
            .map(|cargo_metadata::Node { id, deps, .. }| (id, deps))
            .collect::<HashMap<_, _>>();

        let missing_dep_kinds = deps
            .values()
            .flat_map(|d| d.iter())
            .any(|cargo_metadata::NodeDep { dep_kinds, .. }| dep_kinds.is_empty());

        if missing_dep_kinds && avoid_dev_deps {
            eprintln!("warning: Cargo 1.41+ is required for `--avoid-dev-deps`");
        }
        if missing_dep_kinds && avoid_build_deps {
            eprintln!("warning: Cargo 1.41+ is required for `--avoid-build-deps`");
        }

        let neighbors = |package_id: &cargo_metadata::PackageId| {
            deps[package_id]
                .iter()
                .filter(|cargo_metadata::NodeDep { dep_kinds, .. }| {
                    missing_dep_kinds
                        || dep_kinds
                            .iter()
                            .any(|cargo_metadata::DepKindInfo { kind, .. }| {
                                *kind == cargo_metadata::DependencyKind::Normal
                                    || !avoid_dev_deps
                                        && *kind == cargo_metadata::DependencyKind::Development
                                    || !avoid_build_deps
                                        && *kind == cargo_metadata::DependencyKind::Build
                            })
                })
                .map(|cargo_metadata::NodeDep { pkg, .. }| pkg)
        };

        let mut connected = HashSet::new();
        let stack = &mut if let Some(root) = &resolve.root {
            vec![root]
        } else {
            metadata.workspace_members.iter().collect()
        };
        while let Some(package_id) = stack.pop() {
            if connected.insert(package_id) {
                stack.extend(neighbors(package_id));
            }
        }
        connected
    };

    let mut detailed_dependencies = metadata
        .packages
        .iter()
        .filter(|p| connected.contains(&p.id))
        .map(DependencyDetails::new)
        .collect::<Vec<_>>();
    detailed_dependencies.sort_unstable();
    Ok(detailed_dependencies)
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_detailed() {
        let cmd = cargo_metadata::MetadataCommand::new();
        let detailed_dependencies = get_dependencies_from_cargo_lock(cmd, false, false).unwrap();
        assert!(!detailed_dependencies.is_empty());
        for detailed_dependency in detailed_dependencies.iter() {
            assert!(
                detailed_dependency.license.is_some() || detailed_dependency.license_file.is_some()
            );
        }
    }
}