use std::collections::HashSet;
use std::path::{Path, PathBuf};
use syn::parse::Parser;
use crate::errors::missing_module_file_error;
use crate::module_resolve::{locate_module_file, read_parse};
use crate::resolve::{
AliasMap, BareFallback, ExternRenameMap, ReexportMap, UseMap, alias_nominal_target,
collect_reexports, collect_uses, extern_verbatim_renamed, resolve_path, strip_raw,
};
use crate::syn_util::has_path_attr;
use crate::{child_module_names, local_type_namespace_names};
pub(crate) struct ImplSite {
pub(crate) module: String,
pub(crate) trait_path: syn::Path,
pub(crate) self_ty: syn::Type,
pub(crate) uses: UseMap,
}
pub(crate) struct TypeDef {
pub(crate) canonical: String,
pub(crate) module: String,
pub(crate) derives: Vec<syn::Path>,
}
pub(crate) struct CrateScan {
pub(crate) reexports: ReexportMap,
pub(crate) aliases: AliasMap,
pub(crate) extern_renames: ExternRenameMap,
pub(crate) trait_defs: HashSet<String>,
pub(crate) impls: Vec<ImplSite>,
pub(crate) type_defs: Vec<TypeDef>,
}
fn collect_crate_root_extern_renames(items: &[syn::Item], out: &mut ExternRenameMap) {
for item in items {
if let syn::Item::ExternCrate(ec) = item {
if let Some((_, rename)) = &ec.rename {
let alias = strip_raw(&rename.to_string());
let real = strip_raw(&ec.ident.to_string());
if alias != "_" && alias != real && real != "self" {
out.insert(alias, real);
}
}
}
}
}
fn bare_local_alias_target(
target: &syn::Path,
module: &str,
local_alias_names: &HashSet<String>,
) -> Option<String> {
if target.leading_colon.is_some() || target.segments.len() != 1 {
return None;
}
let seg = &target.segments[0];
if !matches!(seg.arguments, syn::PathArguments::None) {
return None;
}
let name = strip_raw(&seg.ident.to_string());
local_alias_names
.contains(&name)
.then(|| format!("{module}::{name}"))
}
pub(crate) fn scan_crate(
src_dir: &Path,
root_file: &Path,
crate_package: &str,
externs: &HashSet<String>,
) -> Result<CrateScan, String> {
let root = read_parse(root_file)?;
let mut scan = CrateScan {
reexports: ReexportMap::new(),
aliases: AliasMap::new(),
extern_renames: ExternRenameMap::new(),
trait_defs: HashSet::new(),
impls: Vec::new(),
type_defs: Vec::new(),
};
collect_crate_root_extern_renames(&root.items, &mut scan.extern_renames);
walk_module(
root.items,
"crate".to_string(),
src_dir.to_path_buf(),
crate_package,
externs,
&mut scan,
)?;
Ok(scan)
}
fn walk_module(
items: Vec<syn::Item>,
module: String,
child_dir: PathBuf,
crate_package: &str,
externs: &HashSet<String>,
scan: &mut CrateScan,
) -> Result<(), String> {
let uses = collect_uses(&items);
let child_mods = child_module_names(&items);
collect_reexports(
&items,
&module,
externs,
&child_mods,
&scan.extern_renames,
&mut scan.reexports,
);
let externs_type: HashSet<String> = externs
.difference(&local_type_namespace_names(&items))
.cloned()
.collect();
let local_alias_names: HashSet<String> = items
.iter()
.filter_map(|it| match it {
syn::Item::Type(t) if t.generics.params.is_empty() => {
Some(strip_raw(&t.ident.to_string()))
}
_ => None,
})
.collect();
for item in &items {
match item {
syn::Item::Trait(trait_item) => {
scan.trait_defs.insert(format!(
"{module}::{}",
strip_raw(&trait_item.ident.to_string())
));
}
syn::Item::Impl(impl_item) if impl_item.trait_.is_some() => {
let (_, trait_path, _) = impl_item.trait_.as_ref().expect("trait_ is Some");
scan.impls.push(ImplSite {
module: module.clone(),
trait_path: trait_path.clone(),
self_ty: (*impl_item.self_ty).clone(),
uses: uses.clone(),
});
}
syn::Item::Struct(i) => {
push_type_def(&i.attrs, &i.ident, &module, scan)?;
}
syn::Item::Enum(i) => {
push_type_def(&i.attrs, &i.ident, &module, scan)?;
}
syn::Item::Union(i) => {
push_type_def(&i.attrs, &i.ident, &module, scan)?;
}
syn::Item::Type(type_item) if type_item.generics.params.is_empty() => {
if let Some(target) = alias_nominal_target(&type_item.ty) {
let alias = format!("{module}::{}", strip_raw(&type_item.ident.to_string()));
let resolved = if target.leading_colon.is_some() {
extern_verbatim_renamed(target, externs, &scan.extern_renames)
} else {
resolve_path(target, &uses, &module, BareFallback::Ignore)
.or_else(|| {
bare_local_alias_target(target, &module, &local_alias_names)
})
.or_else(|| {
extern_verbatim_renamed(target, &externs_type, &scan.extern_renames)
})
};
if let Some(resolved) = resolved {
if resolved != alias {
scan.aliases.insert(alias, resolved);
}
}
}
}
_ => {}
}
}
for item in items {
if let syn::Item::Mod(module_item) = item {
if has_path_attr(&module_item.attrs) {
continue;
}
let name = strip_raw(&module_item.ident.to_string());
let child_module = format!("{module}::{name}");
match module_item.content {
Some((_, inner)) => {
walk_module(
inner,
child_module,
child_dir.join(&name),
crate_package,
externs,
scan,
)?;
}
None => match locate_module_file(&child_dir, &name) {
Some(file) => {
let parsed = read_parse(&file)?;
walk_module(
parsed.items,
child_module,
child_dir.join(&name),
crate_package,
externs,
scan,
)?;
}
None => {
if !has_cfg_attr(&module_item.attrs) {
return Err(missing_module_file_error(&child_module, crate_package));
}
}
},
}
}
}
Ok(())
}
fn push_type_def(
attrs: &[syn::Attribute],
ident: &syn::Ident,
module: &str,
scan: &mut CrateScan,
) -> Result<(), String> {
let name = strip_raw(&ident.to_string());
let derives = extract_derives(attrs)?;
scan.type_defs.push(TypeDef {
canonical: format!("{module}::{name}"),
module: module.to_string(),
derives,
});
Ok(())
}
fn extract_derives(attrs: &[syn::Attribute]) -> Result<Vec<syn::Path>, String> {
let mut out = Vec::new();
for attr in attrs {
if attr.path().is_ident("derive") {
out.extend(parse_derive_paths(&attr.meta)?);
} else if attr.path().is_ident("cfg_attr") {
let metas = attr
.parse_args_with(meta_list_parser())
.map_err(|e| format!("cannot parse #[cfg_attr(...)]: {e}"))?;
extract_derives_from_cfg_metas(&metas, &mut out)?;
}
}
Ok(out)
}
fn meta_list_parser() -> impl Parser<Output = syn::punctuated::Punctuated<syn::Meta, syn::Token![,]>>
{
syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated
}
fn parse_derive_paths(meta: &syn::Meta) -> Result<Vec<syn::Path>, String> {
let parser = syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::parse_terminated;
match meta {
syn::Meta::List(list) => Ok(list
.parse_args_with(parser)
.map_err(|e| format!("cannot parse derive(...): {e}"))?
.into_iter()
.collect()),
_ => Ok(Vec::new()),
}
}
fn extract_derives_from_cfg_metas(
metas: &syn::punctuated::Punctuated<syn::Meta, syn::Token![,]>,
out: &mut Vec<syn::Path>,
) -> Result<(), String> {
for meta in metas.iter().skip(1) {
if let syn::Meta::List(list) = meta {
if list.path.is_ident("derive") {
out.extend(parse_derive_paths(meta)?);
} else if list.path.is_ident("cfg_attr") {
let inner = list
.parse_args_with(meta_list_parser())
.map_err(|e| format!("cannot parse nested #[cfg_attr(...)]: {e}"))?;
extract_derives_from_cfg_metas(&inner, out)?;
}
}
}
Ok(())
}
fn has_cfg_attr(attrs: &[syn::Attribute]) -> bool {
attrs.iter().any(|attr| attr.path().is_ident("cfg"))
}