alef 0.58.3

Opinionated polyglot binding generator for Rust libraries
Documentation
//! Resolve which `Result` type alias a module has in scope.
//!
//! A crate may declare several `Result` aliases — a canonical one next to the crate error plus
//! private ones inside individual subsystems. Which alias a given module means when it writes
//! `Result<T>` is decided by that module's `use` statements, so error-type resolution has to read
//! them rather than assume a single crate-wide alias.

use ahash::AHashSet;

use crate::extract::type_resolver::ResultAliasScope;

/// Find which `Result` alias the module's `use` statements bring into scope.
///
/// Returns `None` when the module imports no `Result` at all; the caller then falls back to the
/// crate's canonical alias. A `use` of a foreign crate's `Result` (`anyhow::Result`, say) yields
/// [`ResultAliasScope::Foreign`] so that no crate-local error type is substituted.
pub(crate) fn resolve_result_alias_scope(items: &[syn::Item], module_path: &str) -> Option<ResultAliasScope> {
    let local_modules = collect_local_module_names(items);
    let mut prefix: Vec<String> = Vec::new();
    items.iter().find_map(|item| match item {
        syn::Item::Use(item_use) => scope_from_tree(&item_use.tree, module_path, &local_modules, &mut prefix),
        _ => None,
    })
}

/// Names of the child modules declared in `items`.
///
/// Rust 2018 uniform paths let a `use` start at a module of the current module
/// (`pub use error::Result;` in `lib.rs`), which is otherwise indistinguishable from a foreign
/// crate name. Knowing the local module names is what tells the two apart. ~keep
fn collect_local_module_names(items: &[syn::Item]) -> AHashSet<String> {
    items
        .iter()
        .filter_map(|item| match item {
            syn::Item::Mod(item_mod) => Some(item_mod.ident.to_string()),
            _ => None,
        })
        .collect()
}

/// Walk a use tree, accumulating the module path segments that precede each leaf.
fn scope_from_tree(
    tree: &syn::UseTree,
    module_path: &str,
    local_modules: &AHashSet<String>,
    prefix: &mut Vec<String>,
) -> Option<ResultAliasScope> {
    match tree {
        syn::UseTree::Path(use_path) => {
            prefix.push(use_path.ident.to_string());
            let found = scope_from_tree(&use_path.tree, module_path, local_modules, prefix);
            prefix.pop();
            found
        }
        syn::UseTree::Group(group) => group
            .items
            .iter()
            .find_map(|item| scope_from_tree(item, module_path, local_modules, prefix)),
        // The name the module actually writes is the imported ident, or the rename when there is
        // one — `use anyhow::Result as AnyResult` does not put `Result` in scope. ~keep
        syn::UseTree::Name(use_name) if use_name.ident == "Result" => {
            Some(scope_for_prefix(prefix, module_path, local_modules))
        }
        syn::UseTree::Rename(use_rename) if use_rename.rename == "Result" => {
            Some(scope_for_prefix(prefix, module_path, local_modules))
        }
        _ => None,
    }
}

/// Turn the path segments preceding an imported `Result` into the module that declares it.
///
/// `crate`/`self`/`super` prefixes and uniform paths into a locally declared module resolve
/// against `module_path`; anything else names another crate, whose `Result` carries its own error
/// type and must not borrow this crate's.
fn scope_for_prefix(prefix: &[String], module_path: &str, local_modules: &AHashSet<String>) -> ResultAliasScope {
    let segments: Vec<&str> = prefix.iter().map(String::as_str).collect();
    let mut index = 0;
    let mut base: Vec<&str> = Vec::new();

    match segments.first().copied() {
        Some("crate") => index = 1,
        Some("self") => {
            index = 1;
            base = split_module_path(module_path);
        }
        Some("super") => {
            base = split_module_path(module_path);
            while segments.get(index).copied() == Some("super") {
                index += 1;
                base.pop();
            }
        }
        Some(first) if local_modules.contains(first) => base = split_module_path(module_path),
        _ => return ResultAliasScope::Foreign,
    }

    base.extend(segments[index..].iter().copied());
    ResultAliasScope::Crate(base.join("::"))
}

/// Split a module path into segments (the crate root is the empty path).
fn split_module_path(module_path: &str) -> Vec<&str> {
    if module_path.is_empty() {
        Vec::new()
    } else {
        module_path.split("::").collect()
    }
}

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

    fn items(source: &str) -> Vec<syn::Item> {
        syn::parse_str::<syn::File>(source)
            .expect("test source must parse")
            .items
    }

    #[test]
    fn crate_root_import_resolves_to_the_root_module() {
        let parsed = items("use crate::Result;");
        assert_eq!(
            resolve_result_alias_scope(&parsed, "plugins::embedding"),
            Some(ResultAliasScope::Crate(String::new()))
        );
    }

    #[test]
    fn qualified_crate_import_resolves_to_the_declaring_module() {
        let parsed = items("use crate::error::Result;");
        assert_eq!(
            resolve_result_alias_scope(&parsed, "plugins"),
            Some(ResultAliasScope::Crate("error".to_string()))
        );
    }

    #[test]
    fn super_import_resolves_against_the_parent_module() {
        let parsed = items("use super::error::Result;");
        assert_eq!(
            resolve_result_alias_scope(&parsed, "extraction::binary::model"),
            Some(ResultAliasScope::Crate("extraction::binary::error".to_string()))
        );
    }

    #[test]
    fn grouped_super_import_resolves_to_the_parent_module() {
        let parsed = items("use super::{ConversionResult, Result};");
        assert_eq!(
            resolve_result_alias_scope(&parsed, "convert_api"),
            Some(ResultAliasScope::Crate(String::new()))
        );
    }

    #[test]
    fn uniform_path_into_a_locally_declared_module_is_crate_local() {
        let parsed = items("pub mod error;\npub use error::{Result, SampleCrateError};");
        assert_eq!(
            resolve_result_alias_scope(&parsed, ""),
            Some(ResultAliasScope::Crate("error".to_string()))
        );
    }

    #[test]
    fn foreign_crate_import_is_not_a_crate_local_alias() {
        let parsed = items("use anyhow::Result;");
        assert_eq!(
            resolve_result_alias_scope(&parsed, "plugins"),
            Some(ResultAliasScope::Foreign)
        );
    }

    #[test]
    fn renamed_foreign_import_does_not_bring_result_into_scope() {
        let parsed = items("use anyhow::Result as AnyResult;");
        assert_eq!(resolve_result_alias_scope(&parsed, "plugins"), None);
    }

    #[test]
    fn module_without_a_result_import_has_no_scope() {
        let parsed = items("use std::sync::Arc;");
        assert_eq!(resolve_result_alias_scope(&parsed, "plugins"), None);
    }
}