use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use syn::parse::Parser;
use syn::visit::{self, Visit};
use crate::crate_scope::{child_module_names, local_type_namespace_names};
use crate::errors::missing_module_file_error;
use crate::module_resolve::{locate_module_file, read_parse, resolve_module_root};
use crate::resolve::{
AliasMap, BareFallback, ExternRenameMap, ReexportMap, UseMap, alias_nominal_target,
collect_reexports, collect_uses, extern_verbatim_renamed, resolve_path, strip_raw,
type_to_string,
};
use crate::syn_util::has_path_attr;
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) uses: UseMap,
}
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>,
pub(crate) alias_targets: HashMap<String, String>,
}
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(),
alias_targets: HashMap::new(),
};
collect_crate_root_extern_renames(&root.items, &mut scan.extern_renames);
let mut visited: HashSet<PathBuf> = HashSet::new();
visited.insert(canonicalize_source(root_file)?);
walk_module(
root.items,
"crate".to_string(),
src_dir.to_path_buf(),
crate_package,
externs,
&mut visited,
&mut scan,
)?;
Ok(scan)
}
fn canonicalize_source(file: &Path) -> Result<PathBuf, String> {
std::fs::canonicalize(file).map_err(|err| {
format!(
"cannot canonicalize source file '{}': {err}",
file.display()
)
})
}
fn resolve_child_modules(
items: &[syn::Item],
module: &str,
child_dir: &Path,
crate_package: &str,
visited: &mut HashSet<PathBuf>,
) -> Result<Vec<(Vec<syn::Item>, String, PathBuf)>, String> {
let mut children = Vec::new();
for item in items {
let syn::Item::Mod(module_item) = item else {
continue;
};
if has_path_attr(&module_item.attrs) {
continue;
}
let name = strip_raw(&module_item.ident.to_string());
let child_module = format!("{module}::{name}");
let sub_dir = child_dir.join(&name);
match &module_item.content {
Some((_, inner)) => children.push((inner.clone(), child_module, sub_dir)),
None => match locate_module_file(child_dir, &name) {
Some(file) => {
if !visited.insert(canonicalize_source(&file)?) {
return Err(format!(
"cannot judge module '{child_module}' in package '{crate_package}': \
its source file '{}' forms a module cycle (a symlink loop)",
file.display()
));
}
let parsed = read_parse(&file)?;
children.push((parsed.items, child_module, sub_dir));
}
None => {
if !has_cfg_attr(&module_item.attrs) {
return Err(missing_module_file_error(&child_module, crate_package));
}
}
},
}
}
Ok(children)
}
fn walk_module(
items: Vec<syn::Item>,
module: String,
child_dir: PathBuf,
crate_package: &str,
externs: &HashSet<String>,
visited: &mut HashSet<PathBuf>,
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, &uses, scan)?;
}
syn::Item::Enum(i) => {
push_type_def(&i.attrs, &i.ident, &module, &uses, scan)?;
}
syn::Item::Union(i) => {
push_type_def(&i.attrs, &i.ident, &module, &uses, scan)?;
}
syn::Item::Type(type_item) if type_item.generics.params.is_empty() => {
if let syn::Type::Path(tp) = &*type_item.ty {
if let Some(landing) =
resolve_path(&tp.path, &uses, &module, BareFallback::CurrentModule)
{
let alias =
format!("{module}::{}", strip_raw(&type_item.ident.to_string()));
scan.alias_targets.insert(alias, landing);
}
}
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 (child_items, child_module, sub_dir) in
resolve_child_modules(&items, &module, &child_dir, crate_package, visited)?
{
walk_module(
child_items,
child_module,
sub_dir,
crate_package,
externs,
visited,
scan,
)?;
}
Ok(())
}
pub(crate) fn walk_subtree_modules(
src_dir: &Path,
root_file: &Path,
module: &str,
crate_package: &str,
) -> Result<Vec<(String, Vec<syn::Item>)>, String> {
let (items, file, child_dir) = resolve_module_root(src_dir, root_file, module, crate_package)?;
let mut visited: HashSet<PathBuf> = HashSet::new();
visited.insert(canonicalize_source(&file)?);
let mut out: Vec<(String, Vec<syn::Item>)> = Vec::new();
collect_subtree(
items,
module.to_string(),
child_dir,
crate_package,
&mut visited,
&mut out,
)?;
Ok(out)
}
fn collect_subtree(
items: Vec<syn::Item>,
module: String,
child_dir: PathBuf,
crate_package: &str,
visited: &mut HashSet<PathBuf>,
out: &mut Vec<(String, Vec<syn::Item>)>,
) -> Result<(), String> {
for (child_items, child_module, sub_dir) in
resolve_child_modules(&items, &module, &child_dir, crate_package, visited)?
{
collect_subtree(
child_items,
child_module,
sub_dir,
crate_package,
visited,
out,
)?;
}
out.push((module, items));
Ok(())
}
fn push_type_def(
attrs: &[syn::Attribute],
ident: &syn::Ident,
module: &str,
uses: &UseMap,
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,
uses: uses.clone(),
});
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"))
}
pub(crate) struct UnsafeSite {
pub(crate) module: String,
pub(crate) label: String,
}
#[derive(Default)]
struct UnsafeSiteCollector {
labels: Vec<String>,
unsafe_impl_ordinal: usize,
}
fn render_trait_path(path: &syn::Path) -> String {
let lead = if path.leading_colon.is_some() {
"::"
} else {
""
};
let segs: Vec<String> = path
.segments
.iter()
.map(|s| strip_raw(&s.ident.to_string()))
.collect();
format!("{lead}{}", segs.join("::"))
}
impl<'ast> Visit<'ast> for UnsafeSiteCollector {
fn visit_expr_unsafe(&mut self, node: &'ast syn::ExprUnsafe) {
self.labels.push("unsafe block".to_string());
visit::visit_expr_unsafe(self, node);
}
fn visit_item_fn(&mut self, node: &'ast syn::ItemFn) {
if node.sig.unsafety.is_some() {
self.labels.push(format!(
"unsafe fn {}",
strip_raw(&node.sig.ident.to_string())
));
}
visit::visit_item_fn(self, node);
}
fn visit_impl_item_fn(&mut self, node: &'ast syn::ImplItemFn) {
if node.sig.unsafety.is_some() {
self.labels.push(format!(
"unsafe fn {}",
strip_raw(&node.sig.ident.to_string())
));
}
visit::visit_impl_item_fn(self, node);
}
fn visit_trait_item_fn(&mut self, node: &'ast syn::TraitItemFn) {
if node.sig.unsafety.is_some() {
self.labels.push(format!(
"unsafe fn {}",
strip_raw(&node.sig.ident.to_string())
));
}
visit::visit_trait_item_fn(self, node);
}
fn visit_item_impl(&mut self, node: &'ast syn::ItemImpl) {
if node.unsafety.is_some() {
let owner = type_to_string(&node.self_ty)
.unwrap_or_else(|| format!("_#{}", self.unsafe_impl_ordinal));
self.unsafe_impl_ordinal += 1;
let label = match &node.trait_ {
Some((_, path, _)) => {
format!("unsafe impl {} for {}", render_trait_path(path), owner)
}
None => format!("unsafe impl {owner}"),
};
self.labels.push(label);
}
visit::visit_item_impl(self, node);
}
fn visit_item_trait(&mut self, node: &'ast syn::ItemTrait) {
if node.unsafety.is_some() {
self.labels.push(format!(
"unsafe trait {}",
strip_raw(&node.ident.to_string())
));
}
visit::visit_item_trait(self, node);
}
fn visit_item_foreign_mod(&mut self, node: &'ast syn::ItemForeignMod) {
if node.unsafety.is_some() {
self.labels.push("unsafe extern block".to_string());
}
visit::visit_item_foreign_mod(self, node);
}
}
pub(crate) fn scan_unsafe_sites(
src_dir: &Path,
root_file: &Path,
crate_package: &str,
) -> Result<Vec<UnsafeSite>, String> {
let root = read_parse(root_file)?;
let mut sites = Vec::new();
let mut visited: HashSet<PathBuf> = HashSet::new();
visited.insert(canonicalize_source(root_file)?);
walk_unsafe(
root.items,
"crate".to_string(),
src_dir.to_path_buf(),
crate_package,
&mut visited,
&mut sites,
)?;
Ok(sites)
}
fn walk_unsafe(
items: Vec<syn::Item>,
module: String,
child_dir: PathBuf,
crate_package: &str,
visited: &mut HashSet<PathBuf>,
sites: &mut Vec<UnsafeSite>,
) -> Result<(), String> {
let mut collector = UnsafeSiteCollector::default();
for item in &items {
if matches!(item, syn::Item::Mod(_)) {
continue;
}
collector.visit_item(item);
}
for label in collector.labels {
sites.push(UnsafeSite {
module: module.clone(),
label,
});
}
for (child_items, child_module, sub_dir) in
resolve_child_modules(&items, &module, &child_dir, crate_package, visited)?
{
walk_unsafe(
child_items,
child_module,
sub_dir,
crate_package,
visited,
sites,
)?;
}
Ok(())
}