mod defaults;
mod functions;
mod helpers;
mod reexports;
mod types;
use std::path::{Path, PathBuf};
use ahash::AHashMap;
use alef_core::ir::{ApiSurface, MethodDef, TypeDef, TypeRef};
use anyhow::{Context, Result};
use crate::type_resolver;
use self::functions::{detect_receiver, extract_function, extract_impl_block, extract_params, resolve_return_type};
use self::helpers::{build_rust_path, collect_reexport_map, extract_doc_comments, is_pub, is_thiserror_enum};
use self::reexports::{extract_module, resolve_use_tree};
use self::types::{extract_enum, extract_error_enum, extract_struct};
pub fn extract(
sources: &[&Path],
crate_name: &str,
version: &str,
workspace_root: Option<&Path>,
) -> Result<ApiSurface> {
let mut surface = ApiSurface {
crate_name: crate_name.to_string(),
version: version.to_string(),
types: vec![],
functions: vec![],
enums: vec![],
errors: vec![],
};
let mut visited = Vec::<PathBuf>::new();
let crate_src_dir = sources.first().and_then(|s| s.parent()).map(|p| p.to_path_buf());
for source in sources {
let canonical = std::fs::canonicalize(source).unwrap_or_else(|_| source.to_path_buf());
if visited.contains(&canonical) {
continue;
}
visited.push(canonical);
let content = std::fs::read_to_string(source)
.with_context(|| format!("Failed to read source file: {}", source.display()))?;
let file =
syn::parse_file(&content).with_context(|| format!("Failed to parse source file: {}", source.display()))?;
let module_path = derive_module_path(source, crate_src_dir.as_deref());
let types_before = surface.types.len();
let enums_before = surface.enums.len();
let fns_before = surface.functions.len();
extract_items(
&file.items,
source,
crate_name,
&module_path,
&mut surface,
workspace_root,
&mut visited,
)?;
if !module_path.is_empty() {
apply_parent_reexport_shortening(
source,
crate_name,
&module_path,
&mut surface,
types_before,
enums_before,
fns_before,
);
}
}
resolve_trait_sources(&mut surface);
resolve_newtypes(&mut surface);
let return_type_names: ahash::AHashSet<String> = surface
.functions
.iter()
.filter_map(|f| match &f.return_type {
TypeRef::Named(name) => Some(name.clone()),
_ => None,
})
.collect();
for typ in &mut surface.types {
if return_type_names.contains(&typ.name) {
typ.is_return_type = true;
}
}
Ok(surface)
}
fn apply_parent_reexport_shortening(
source: &Path,
crate_name: &str,
module_path: &str,
surface: &mut ApiSurface,
types_before: usize,
enums_before: usize,
fns_before: usize,
) {
use self::helpers::collect_reexport_map;
use self::reexports::collect_use_names;
let parent_dir = match source.parent() {
Some(p) => p,
None => return,
};
let parent_mod = parent_dir.join("mod.rs");
let parent_lib = parent_dir.join("lib.rs");
let parent_content = if parent_mod.exists() && parent_mod != source {
std::fs::read_to_string(&parent_mod).ok()
} else if parent_lib.exists() && parent_lib != source {
std::fs::read_to_string(&parent_lib).ok()
} else {
None
};
let Some(content) = parent_content else {
return;
};
let Ok(parent_file) = syn::parse_file(&content) else {
return;
};
let mod_name = source.file_stem().and_then(|s| s.to_str()).unwrap_or("");
if mod_name.is_empty() || mod_name == "mod" {
return;
}
let reexport_map = collect_reexport_map(&parent_file.items);
let mut reexported_names = std::collections::HashSet::new();
for item in &parent_file.items {
if let syn::Item::Use(item_use) = item {
if helpers::is_pub(&item_use.vis) {
if let syn::UseTree::Path(use_path) = &item_use.tree {
if use_path.ident == mod_name {
match collect_use_names(&use_path.tree) {
reexports::UseFilter::All => {
let parent_module_path = module_path.rsplit_once("::").map(|(p, _)| p).unwrap_or("");
let parent_prefix = if parent_module_path.is_empty() {
crate_name.to_string()
} else {
format!("{crate_name}::{parent_module_path}")
};
for ty in &mut surface.types[types_before..] {
ty.rust_path = format!("{parent_prefix}::{}", ty.name);
}
for en in &mut surface.enums[enums_before..] {
en.rust_path = format!("{parent_prefix}::{}", en.name);
}
for func in &mut surface.functions[fns_before..] {
func.rust_path = format!("{parent_prefix}::{}", func.name);
}
return;
}
reexports::UseFilter::Names(names) => {
reexported_names.extend(names);
}
}
}
}
}
}
}
if let Some(helpers::ReexportKind::Names(names)) = reexport_map.get(mod_name) {
reexported_names.extend(names.iter().cloned());
} else if matches!(reexport_map.get(mod_name), Some(helpers::ReexportKind::Glob)) {
let parent_module_path = module_path.rsplit_once("::").map(|(p, _)| p).unwrap_or("");
let parent_prefix = if parent_module_path.is_empty() {
crate_name.to_string()
} else {
format!("{crate_name}::{parent_module_path}")
};
for ty in &mut surface.types[types_before..] {
ty.rust_path = format!("{parent_prefix}::{}", ty.name);
}
for en in &mut surface.enums[enums_before..] {
en.rust_path = format!("{parent_prefix}::{}", en.name);
}
for func in &mut surface.functions[fns_before..] {
func.rust_path = format!("{parent_prefix}::{}", func.name);
}
return;
}
if reexported_names.is_empty() {
return;
}
let parent_module_path = module_path.rsplit_once("::").map(|(p, _)| p).unwrap_or("");
let parent_prefix = if parent_module_path.is_empty() {
crate_name.to_string()
} else {
format!("{crate_name}::{parent_module_path}")
};
for ty in &mut surface.types[types_before..] {
if reexported_names.contains(&ty.name) {
ty.rust_path = format!("{parent_prefix}::{}", ty.name);
}
}
for en in &mut surface.enums[enums_before..] {
if reexported_names.contains(&en.name) {
en.rust_path = format!("{parent_prefix}::{}", en.name);
}
}
for func in &mut surface.functions[fns_before..] {
if reexported_names.contains(&func.name) {
func.rust_path = format!("{parent_prefix}::{}", func.name);
}
}
}
fn derive_module_path(source: &Path, crate_src_dir: Option<&Path>) -> String {
let Some(root) = crate_src_dir else {
return String::new();
};
let root_canonical = std::fs::canonicalize(root).unwrap_or_else(|_| root.to_path_buf());
let source_canonical = std::fs::canonicalize(source).unwrap_or_else(|_| source.to_path_buf());
let Ok(relative) = source_canonical.strip_prefix(&root_canonical) else {
return String::new();
};
let mut segments = Vec::new();
for component in relative.iter() {
let s = component.to_string_lossy();
if s == "lib.rs" || s == "main.rs" {
return String::new();
} else if s == "mod.rs" {
continue;
} else if let Some(stem) = s.strip_suffix(".rs") {
segments.push(stem.to_string());
} else {
segments.push(s.to_string());
}
}
segments.join("::")
}
fn is_simple_type(ty: &TypeRef) -> bool {
matches!(
ty,
TypeRef::Primitive(_)
| TypeRef::String
| TypeRef::Bytes
| TypeRef::Path
| TypeRef::Unit
| TypeRef::Duration
| TypeRef::Json
)
}
fn resolve_newtypes(surface: &mut ApiSurface) {
let newtype_map: AHashMap<String, TypeRef> = surface
.types
.iter()
.filter(|t| t.fields.len() == 1 && t.fields[0].name == "_0" && is_simple_type(&t.fields[0].ty))
.map(|t| (t.name.clone(), t.fields[0].ty.clone()))
.collect();
if newtype_map.is_empty() {
return;
}
let newtype_rust_paths: AHashMap<String, String> = surface
.types
.iter()
.filter(|t| newtype_map.contains_key(&t.name))
.map(|t| (t.name.clone(), t.rust_path.replace('-', "_")))
.collect();
surface.types.retain(|t| !newtype_map.contains_key(&t.name));
for typ in &mut surface.types {
for field in &mut typ.fields {
if let alef_core::ir::TypeRef::Named(name) = &field.ty {
if let Some(rust_path) = newtype_rust_paths.get(name.as_str()) {
field.newtype_wrapper = Some(rust_path.clone());
}
}
if let alef_core::ir::TypeRef::Optional(inner) = &field.ty {
if let alef_core::ir::TypeRef::Named(name) = inner.as_ref() {
if let Some(rust_path) = newtype_rust_paths.get(name.as_str()) {
field.newtype_wrapper = Some(rust_path.clone());
}
}
}
if let alef_core::ir::TypeRef::Vec(inner) = &field.ty {
if let alef_core::ir::TypeRef::Named(name) = inner.as_ref() {
if let Some(rust_path) = newtype_rust_paths.get(name.as_str()) {
field.newtype_wrapper = Some(rust_path.clone());
}
}
}
resolve_typeref(&newtype_map, &mut field.ty);
}
for method in &mut typ.methods {
for param in &mut method.params {
if let alef_core::ir::TypeRef::Named(name) = ¶m.ty {
if let Some(rust_path) = newtype_rust_paths.get(name.as_str()) {
param.newtype_wrapper = Some(rust_path.clone());
}
}
resolve_typeref(&newtype_map, &mut param.ty);
}
if let alef_core::ir::TypeRef::Named(name) = &method.return_type {
if let Some(rust_path) = newtype_rust_paths.get(name.as_str()) {
method.return_newtype_wrapper = Some(rust_path.clone());
}
}
resolve_typeref(&newtype_map, &mut method.return_type);
}
}
for func in &mut surface.functions {
for param in &mut func.params {
if let alef_core::ir::TypeRef::Named(name) = ¶m.ty {
if let Some(rust_path) = newtype_rust_paths.get(name.as_str()) {
param.newtype_wrapper = Some(rust_path.clone());
}
}
resolve_typeref(&newtype_map, &mut param.ty);
}
if let alef_core::ir::TypeRef::Named(name) = &func.return_type {
if let Some(rust_path) = newtype_rust_paths.get(name.as_str()) {
func.return_newtype_wrapper = Some(rust_path.clone());
}
}
resolve_typeref(&newtype_map, &mut func.return_type);
}
for enum_def in &mut surface.enums {
for variant in &mut enum_def.variants {
for field in &mut variant.fields {
resolve_typeref(&newtype_map, &mut field.ty);
}
}
}
}
fn resolve_typeref(newtype_map: &AHashMap<String, TypeRef>, ty: &mut TypeRef) {
match ty {
TypeRef::Named(name) => {
if let Some(inner) = newtype_map.get(name.as_str()) {
*ty = inner.clone();
}
}
TypeRef::Optional(inner) => resolve_typeref(newtype_map, inner),
TypeRef::Vec(inner) => resolve_typeref(newtype_map, inner),
TypeRef::Map(k, v) => {
resolve_typeref(newtype_map, k);
resolve_typeref(newtype_map, v);
}
_ => {}
}
}
fn resolve_trait_sources(surface: &mut ApiSurface) {
let mut trait_method_map: AHashMap<String, Vec<(String, String)>> = AHashMap::new();
let mut trait_methods_set: AHashMap<String, Vec<String>> = AHashMap::new();
for typ in &surface.types {
if !typ.is_trait {
continue;
}
let method_names: Vec<String> = typ.methods.iter().map(|m| m.name.clone()).collect();
trait_methods_set.insert(typ.name.clone(), method_names.clone());
for method_name in &method_names {
trait_method_map
.entry(method_name.clone())
.or_default()
.push((typ.name.clone(), typ.rust_path.replace('-', "_")));
}
}
if trait_method_map.is_empty() {
return;
}
for typ in &mut surface.types {
if typ.is_trait {
continue;
}
let unresolved_names: Vec<String> = typ
.methods
.iter()
.filter(|m| m.trait_source.is_none())
.map(|m| m.name.clone())
.collect();
for method in &mut typ.methods {
if method.trait_source.is_some() {
continue;
}
let Some(candidates) = trait_method_map.get(&method.name) else {
continue;
};
if candidates.len() == 1 {
method.trait_source = Some(candidates[0].1.clone());
} else {
let best = candidates.iter().max_by_key(|(trait_name, _)| {
trait_methods_set
.get(trait_name)
.map(|trait_ms| trait_ms.iter().filter(|m| unresolved_names.contains(m)).count())
.unwrap_or(0)
});
if let Some((_, rust_path)) = best {
method.trait_source = Some(rust_path.clone());
}
}
}
}
}
fn extract_items(
items: &[syn::Item],
source_path: &Path,
crate_name: &str,
module_path: &str,
surface: &mut ApiSurface,
workspace_root: Option<&Path>,
visited: &mut Vec<PathBuf>,
) -> Result<()> {
let reexport_map = collect_reexport_map(items);
for item in items {
match item {
syn::Item::Struct(item_struct) => {
if is_pub(&item_struct.vis) {
if let Some(td) = extract_struct(item_struct, crate_name, module_path) {
surface.types.push(td);
}
}
}
syn::Item::Enum(item_enum) => {
if is_pub(&item_enum.vis) {
if is_thiserror_enum(&item_enum.attrs) {
if let Some(ed) = extract_error_enum(item_enum, crate_name, module_path) {
surface.errors.push(ed);
}
} else if let Some(ed) = extract_enum(item_enum, crate_name, module_path) {
surface.enums.push(ed);
}
}
}
syn::Item::Fn(item_fn) => {
if is_pub(&item_fn.vis) {
if let Some(fd) = extract_function(item_fn, crate_name, module_path) {
surface.functions.push(fd);
}
}
}
syn::Item::Type(item_type) => {
if is_pub(&item_type.vis) && item_type.generics.params.is_empty() {
let name = item_type.ident.to_string();
let _ty = type_resolver::resolve_type(&item_type.ty);
let rust_path = build_rust_path(crate_name, module_path, &name);
let doc = extract_doc_comments(&item_type.attrs);
surface.types.push(TypeDef {
name,
rust_path,
fields: vec![],
methods: vec![],
is_opaque: true, is_clone: false,
is_trait: false,
has_default: false,
has_stripped_cfg_fields: false,
is_return_type: false,
doc,
cfg: None,
serde_rename_all: None,
has_serde: false,
});
}
}
syn::Item::Trait(item_trait) => {
if is_pub(&item_trait.vis) && item_trait.generics.params.is_empty() {
let name = item_trait.ident.to_string();
let rust_path = build_rust_path(crate_name, module_path, &name);
let doc = extract_doc_comments(&item_trait.attrs);
let methods: Vec<MethodDef> = item_trait
.items
.iter()
.filter_map(|item| {
if let syn::TraitItem::Fn(method) = item {
let method_name = method.sig.ident.to_string();
let method_doc = extract_doc_comments(&method.attrs);
let mut is_async = method.sig.asyncness.is_some();
let (mut return_type, error_type, returns_ref) =
resolve_return_type(&method.sig.output);
if !is_async {
if let Some(inner) = functions::unwrap_future_return(&method.sig.output) {
is_async = true;
return_type = inner;
}
}
if !method.sig.generics.params.is_empty() {
return None;
}
let (receiver, is_static) = detect_receiver(&method.sig.inputs);
let params = extract_params(&method.sig.inputs);
Some(MethodDef {
name: method_name,
params,
return_type,
is_async,
is_static,
error_type,
doc: method_doc,
receiver,
sanitized: false,
trait_source: None,
returns_ref,
returns_cow: false,
return_newtype_wrapper: None,
})
} else {
None
}
})
.collect();
surface.types.push(TypeDef {
name,
rust_path,
fields: vec![],
methods,
is_opaque: true,
is_clone: false,
is_trait: true,
has_default: false,
has_stripped_cfg_fields: false,
is_return_type: false,
doc,
cfg: None,
serde_rename_all: None,
has_serde: false,
});
}
}
syn::Item::Mod(item_mod) => {
let mod_name = item_mod.ident.to_string();
let is_reexported = reexport_map.contains_key(&mod_name);
if is_pub(&item_mod.vis) || is_reexported {
extract_module(
item_mod,
source_path,
crate_name,
module_path,
&reexport_map,
surface,
workspace_root,
visited,
)?;
}
}
syn::Item::Use(item_use) if is_pub(&item_use.vis) => {
resolve_use_tree(&item_use.tree, crate_name, surface, workspace_root, visited)?;
}
_ => {}
}
}
let type_index: AHashMap<String, usize> = surface
.types
.iter()
.enumerate()
.map(|(idx, typ)| (typ.name.clone(), idx))
.collect();
for item in items {
if let syn::Item::Impl(item_impl) = item {
extract_impl_block(item_impl, crate_name, module_path, surface, &type_index);
}
}
Ok(())
}
#[cfg(test)]
mod tests;