rust_mir2_core 0.1.0

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 package_name.is_none() && !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<_>>>()?;
        expected_package_names.sort();
        return Ok(TargetProject {
            input_kind: InputKind::Workspace,
            project_root: manifest_dir.to_path_buf(),
            manifest_path,
            expected_package_names,
        });
    }

    let package_name = package_name.context("failed to resolve target package name")?;
    Ok(TargetProject {
        input_kind: InputKind::SingleCrate,
        project_root: manifest_dir.to_path_buf(),
        manifest_path,
        expected_package_names: vec![package_name],
    })
}