rust_mir2_core 0.1.3

Shared Rust MIR extraction model and helpers for rust_mir2
Documentation
use std::path::Path;

use anyhow::{Context, bail};
use toml::Value;

use crate::model::{InputKind, RustMir2Error, TargetProject};

fn read_manifest(manifest_path: &Path) -> anyhow::Result<Value> {
    let content = std::fs::read_to_string(manifest_path)
        .with_context(|| format!("failed to read manifest `{}`", manifest_path.display()))?;
    toml::from_str(&content)
        .with_context(|| format!("failed to parse manifest `{}`", manifest_path.display()))
}

fn package_name_from_manifest(manifest: &Value) -> Option<String> {
    manifest
        .get("package")
        .and_then(Value::as_table)
        .and_then(|package| package.get("name"))
        .and_then(Value::as_str)
        .map(ToOwned::to_owned)
}

fn workspace_members_from_manifest(manifest: &Value) -> Vec<String> {
    manifest
        .get("workspace")
        .and_then(Value::as_table)
        .and_then(|workspace| workspace.get("members"))
        .and_then(Value::as_array)
        .into_iter()
        .flatten()
        .filter_map(Value::as_str)
        .map(ToOwned::to_owned)
        .collect()
}

pub fn resolve_target_project(input_path: &Path) -> Result<TargetProject, RustMir2Error> {
    try_resolve_target_project(input_path).map_err(Into::into)
}

pub(crate) fn try_resolve_target_project(input_path: &Path) -> anyhow::Result<TargetProject> {
    let canonical = input_path.canonicalize().with_context(|| {
        format!(
            "failed to canonicalize input path `{}`",
            input_path.display()
        )
    })?;

    let manifest_path = if canonical.is_file() {
        if canonical
            .file_name()
            .is_some_and(|name| name == "Cargo.toml")
        {
            canonical.clone()
        } else {
            bail!("input file `{}` is not a Cargo.toml", canonical.display());
        }
    } else {
        canonical.join("Cargo.toml")
    };

    if !manifest_path.is_file() {
        bail!("no Cargo.toml found at `{}`", manifest_path.display());
    }

    let manifest = read_manifest(&manifest_path)?;
    let manifest_dir = manifest_path
        .parent()
        .context("manifest path had no parent directory")?;
    let package_name = package_name_from_manifest(&manifest);
    let workspace_members = workspace_members_from_manifest(&manifest);

    if let Some(package_name) = package_name.clone() {
        return Ok(TargetProject {
            input_kind: InputKind::SingleCrate,
            project_root: manifest_dir.to_path_buf(),
            manifest_path,
            expected_package_names: vec![package_name],
        });
    }

    if !workspace_members.is_empty() {
        let mut expected_package_names = workspace_members
            .into_iter()
            .map(|member| {
                let member_manifest = manifest_dir.join(member).join("Cargo.toml");
                let member_value = read_manifest(&member_manifest)?;
                package_name_from_manifest(&member_value).with_context(|| {
                    format!(
                        "workspace member manifest `{}` has no package.name",
                        member_manifest.display()
                    )
                })
            })
            .collect::<anyhow::Result<Vec<_>>>()?;
        if let Some(package_name) = package_name {
            expected_package_names.push(package_name);
        }
        expected_package_names.sort();
        expected_package_names.dedup();
        return Ok(TargetProject {
            input_kind: InputKind::Workspace,
            project_root: manifest_dir.to_path_buf(),
            manifest_path,
            expected_package_names,
        });
    }

    bail!("failed to resolve target package name")
}

#[cfg(test)]
mod tests {
    use tempfile::TempDir;

    use super::*;

    fn write(path: &Path, text: &str) {
        if let Some(parent) = path.parent() {
            std::fs::create_dir_all(parent).unwrap();
        }
        std::fs::write(path, text).unwrap();
    }

    #[test]
    fn package_manifest_with_workspace_members_resolves_as_workspace() {
        let temp = TempDir::new().unwrap();
        write(
            &temp.path().join("Cargo.toml"),
            r#"
[package]
name = "root_pkg"
version = "0.1.0"
edition = "2024"

[workspace]
members = ["adder", "add-one", "test3"]
"#,
        );
        for (dir, name) in [
            ("adder", "adder"),
            ("add-one", "add-one"),
            ("test3", "test3"),
        ] {
            write(
                &temp.path().join(dir).join("Cargo.toml"),
                &format!(
                    r#"
[package]
name = "{name}"
version = "0.1.0"
edition = "2024"
"#
                ),
            );
        }

        let project = try_resolve_target_project(temp.path()).unwrap();

        assert_eq!(project.input_kind, InputKind::SingleCrate);
        assert_eq!(project.expected_package_names, vec!["root_pkg"]);
    }

    #[test]
    fn root_package_name_wins_before_reading_workspace_members() {
        let temp = TempDir::new().unwrap();
        write(
            &temp.path().join("Cargo.toml"),
            r#"
[package]
name = "rustdesk"
version = "0.1.0"
edition = "2024"

[workspace]
members = ["rustdesk"]
"#,
        );

        let project = try_resolve_target_project(temp.path()).unwrap();

        assert_eq!(project.input_kind, InputKind::SingleCrate);
        assert_eq!(project.expected_package_names, vec!["rustdesk"]);
    }
}