use std::path::{Path, PathBuf};
use alef_core::ir::ApiSurface;
use anyhow::Result;
use syn;
use super::helpers::ReexportKind;
pub(crate) fn resolve_use_tree(
tree: &syn::UseTree,
crate_name: &str,
surface: &mut ApiSurface,
workspace_root: Option<&Path>,
visited: &mut Vec<PathBuf>,
) -> Result<()> {
match tree {
syn::UseTree::Path(use_path) => {
let root_ident = use_path.ident.to_string();
if root_ident == "self" || root_ident == "super" || root_ident == "crate" {
return Ok(());
}
resolve_external_use(
&root_ident,
&use_path.tree,
crate_name,
surface,
workspace_root,
visited,
)
}
syn::UseTree::Group(group) => {
for tree in &group.items {
resolve_use_tree(tree, crate_name, surface, workspace_root, visited)?;
}
Ok(())
}
_ => Ok(()),
}
}
fn resolve_external_use(
ext_crate_name: &str,
subtree: &syn::UseTree,
crate_name: &str,
surface: &mut ApiSurface,
workspace_root: Option<&Path>,
visited: &mut Vec<PathBuf>,
) -> Result<()> {
let Some(crate_source) = find_crate_source(ext_crate_name, workspace_root) else {
return Ok(());
};
let canonical = std::fs::canonicalize(&crate_source).unwrap_or_else(|_| crate_source.clone());
if visited.contains(&canonical) {
return Ok(());
}
visited.push(canonical.clone());
let content = match std::fs::read_to_string(&crate_source) {
Ok(c) => c,
Err(_) => return Ok(()),
};
let file = match syn::parse_file(&content) {
Ok(f) => f,
Err(_) => return Ok(()),
};
let mut ext_surface = ApiSurface {
crate_name: crate_name.to_string(), version: String::new(),
types: vec![],
functions: vec![],
enums: vec![],
errors: vec![],
};
super::extract_items(
&file.items,
&canonical,
crate_name,
"",
&mut ext_surface,
workspace_root,
visited,
)?;
let filter = collect_use_names(subtree);
match filter {
UseFilter::All => {
merge_surface(surface, ext_surface);
}
UseFilter::Names(names) => {
merge_surface_filtered(surface, ext_surface, &names);
}
}
Ok(())
}
pub(crate) enum UseFilter {
All,
Names(Vec<String>),
}
pub(crate) fn collect_use_names(tree: &syn::UseTree) -> UseFilter {
match tree {
syn::UseTree::Glob(_) => UseFilter::All,
syn::UseTree::Name(name) => UseFilter::Names(vec![name.ident.to_string()]),
syn::UseTree::Rename(rename) => UseFilter::Names(vec![rename.rename.to_string()]),
syn::UseTree::Path(path) => collect_use_names(&path.tree),
syn::UseTree::Group(group) => {
let mut names = Vec::new();
for item in &group.items {
match collect_use_names(item) {
UseFilter::All => return UseFilter::All,
UseFilter::Names(n) => names.extend(n),
}
}
UseFilter::Names(names)
}
}
}
pub(crate) fn merge_surface(dst: &mut ApiSurface, src: ApiSurface) {
for ty in src.types {
if !dst.types.iter().any(|t| t.name == ty.name) {
dst.types.push(ty);
}
}
for func in src.functions {
if !dst.functions.iter().any(|f| f.name == func.name) {
dst.functions.push(func);
}
}
for en in src.enums {
if !dst.enums.iter().any(|e| e.name == en.name) {
dst.enums.push(en);
}
}
}
pub(crate) fn merge_surface_filtered(dst: &mut ApiSurface, src: ApiSurface, names: &[String]) {
for ty in src.types {
if names.contains(&ty.name) && !dst.types.iter().any(|t| t.name == ty.name) {
dst.types.push(ty);
}
}
for func in src.functions {
if names.contains(&func.name) && !dst.functions.iter().any(|f| f.name == func.name) {
dst.functions.push(func);
}
}
for en in src.enums {
if names.contains(&en.name) && !dst.enums.iter().any(|e| e.name == en.name) {
dst.enums.push(en);
}
}
}
pub(crate) fn find_crate_source(dep_crate_name: &str, workspace_root: Option<&Path>) -> Option<PathBuf> {
let root = workspace_root?;
let cargo_toml = std::fs::read_to_string(root.join("Cargo.toml")).ok()?;
let value: toml::Value = toml::from_str(&cargo_toml).ok()?;
if let Some(deps) = value.get("dependencies").and_then(|d| d.as_table()) {
if let Some(path) = resolve_dep_path(deps, dep_crate_name, root) {
return Some(path);
}
}
if let Some(deps) = value
.get("workspace")
.and_then(|w| w.get("dependencies"))
.and_then(|d| d.as_table())
{
if let Some(path) = resolve_dep_path(deps, dep_crate_name, root) {
return Some(path);
}
}
let heuristic = root.join("crates").join(dep_crate_name).join("src/lib.rs");
if heuristic.exists() {
return Some(heuristic);
}
let alt_name = if dep_crate_name.contains('-') {
dep_crate_name.replace('-', "_")
} else {
dep_crate_name.replace('_', "-")
};
let alt = root.join("crates").join(&alt_name).join("src/lib.rs");
if alt.exists() {
return Some(alt);
}
None
}
fn resolve_dep_path(deps: &toml::map::Map<String, toml::Value>, dep_name: &str, root: &Path) -> Option<PathBuf> {
let dep = deps.get(dep_name)?;
let path = dep.get("path").and_then(|p| p.as_str())?;
let crate_dir = root.join(path);
let lib_rs = crate_dir.join("src/lib.rs");
if lib_rs.exists() { Some(lib_rs) } else { None }
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn extract_module(
item_mod: &syn::ItemMod,
source_path: &Path,
crate_name: &str,
module_path: &str,
reexport_map: &ahash::AHashMap<String, ReexportKind>,
surface: &mut ApiSurface,
workspace_root: Option<&Path>,
visited: &mut Vec<PathBuf>,
) -> Result<()> {
let mod_name = item_mod.ident.to_string();
let reexport_kind = reexport_map.get(&mod_name);
let has_glob_reexport = matches!(reexport_kind, Some(ReexportKind::Glob));
let new_module_path = if has_glob_reexport {
module_path.to_string()
} else if module_path.is_empty() {
mod_name.clone()
} else {
format!("{module_path}::{mod_name}")
};
let named_reexports = match reexport_kind {
Some(ReexportKind::Names(names)) => Some(names),
_ => None,
};
let (types_before, enums_before, fns_before) = if named_reexports.is_some() {
(surface.types.len(), surface.enums.len(), surface.functions.len())
} else {
(0, 0, 0)
};
if let Some((_, items)) = &item_mod.content {
super::extract_items(
items,
source_path,
crate_name,
&new_module_path,
surface,
workspace_root,
visited,
)?;
} else {
let parent_dir = source_path.parent().unwrap_or_else(|| Path::new("."));
let candidates = [
parent_dir.join(format!("{mod_name}.rs")),
parent_dir.join(&mod_name).join("mod.rs"),
];
let mut found = false;
for candidate in &candidates {
if candidate.exists() {
let canonical_candidate = std::fs::canonicalize(candidate).unwrap_or_else(|_| candidate.to_path_buf());
if visited.contains(&canonical_candidate) {
found = true;
break;
}
visited.push(canonical_candidate);
let content = std::fs::read_to_string(candidate)
.with_context(|| format!("Failed to read module file: {}", candidate.display()))?;
let file = syn::parse_file(&content)
.with_context(|| format!("Failed to parse module file: {}", candidate.display()))?;
super::extract_items(
&file.items,
candidate,
crate_name,
&new_module_path,
surface,
workspace_root,
visited,
)?;
found = true;
break;
}
}
if !found {
return Ok(());
}
}
if let Some(names) = named_reexports {
let parent_prefix = if module_path.is_empty() {
crate_name.to_string()
} else {
format!("{crate_name}::{module_path}")
};
for ty in &mut surface.types[types_before..] {
if names.contains(&ty.name) {
ty.rust_path = format!("{parent_prefix}::{}", ty.name);
}
}
for en in &mut surface.enums[enums_before..] {
if names.contains(&en.name) {
en.rust_path = format!("{parent_prefix}::{}", en.name);
}
}
for func in &mut surface.functions[fns_before..] {
if names.contains(&func.name) {
func.rust_path = format!("{parent_prefix}::{}", func.name);
}
}
}
Ok(())
}
use anyhow::Context;