cargo-port 0.1.4

A TUI for inspecting and managing Rust projects
use super::AbsolutePath;
use super::CARGO_TOML;
use super::Component;
use super::HashMap;
use super::HashSet;
use super::Path;
use super::PathBuf;
use super::Table;
use super::Value;

pub(super) fn workspace_path_dependencies(workspace_path: &Path) -> HashMap<String, AbsolutePath> {
    let Some(table) = manifest_table(&workspace_path.join(CARGO_TOML)) else {
        return HashMap::new();
    };
    let Some(dependencies) = table
        .get("workspace")
        .and_then(Value::as_table)
        .and_then(|workspace| workspace.get("dependencies"))
        .and_then(Value::as_table)
    else {
        return HashMap::new();
    };

    dependencies
        .iter()
        .filter_map(|(name, value)| {
            dependency_path(value, workspace_path, name, &HashMap::new())
                .map(|path| (name.clone(), path))
        })
        .collect()
}

pub(super) fn package_path_dependencies(
    package_path: &Path,
    workspace_dependencies: &HashMap<String, AbsolutePath>,
) -> HashSet<AbsolutePath> {
    let Some(table) = manifest_table(&package_path.join(CARGO_TOML)) else {
        return HashSet::new();
    };
    let mut paths = HashSet::new();
    collect_dependency_paths(&table, package_path, workspace_dependencies, &mut paths);
    if let Some(targets) = table.get("target").and_then(Value::as_table) {
        for target in targets.values().filter_map(Value::as_table) {
            collect_dependency_paths(target, package_path, workspace_dependencies, &mut paths);
        }
    }
    paths
}

fn manifest_table(manifest_path: &Path) -> Option<Table> {
    let contents = std::fs::read_to_string(manifest_path).ok()?;
    contents.parse().ok()
}

fn collect_dependency_paths(
    table: &Table,
    package_path: &Path,
    workspace_dependencies: &HashMap<String, AbsolutePath>,
    paths: &mut HashSet<AbsolutePath>,
) {
    for section in ["dependencies", "dev-dependencies", "build-dependencies"] {
        let Some(dependencies) = table.get(section).and_then(Value::as_table) else {
            continue;
        };
        for (name, value) in dependencies {
            if let Some(path) = dependency_path(value, package_path, name, workspace_dependencies) {
                paths.insert(path);
            }
        }
    }
}

fn dependency_path(
    value: &Value,
    base_path: &Path,
    name: &str,
    workspace_dependencies: &HashMap<String, AbsolutePath>,
) -> Option<AbsolutePath> {
    let table = value.as_table()?;
    if let Some(path) = table.get("path").and_then(Value::as_str) {
        return Some(resolve_dependency_path(path, base_path));
    }
    if table.get("workspace").and_then(Value::as_bool) == Some(true) {
        return workspace_dependencies.get(name).cloned();
    }
    None
}

fn resolve_dependency_path(path: &str, base_path: &Path) -> AbsolutePath {
    let raw_path = Path::new(path);
    let resolved = if raw_path.is_absolute() {
        raw_path.to_path_buf()
    } else {
        base_path.join(raw_path)
    };
    AbsolutePath::from(normalize_path_components(&resolved))
}

fn normalize_path_components(path: &Path) -> PathBuf {
    let mut normalized = PathBuf::new();
    for component in path.components() {
        match component {
            Component::CurDir => {},
            Component::ParentDir => {
                normalized.pop();
            },
            Component::Prefix(_) | Component::RootDir | Component::Normal(_) => {
                normalized.push(component.as_os_str());
            },
        }
    }
    normalized
}