use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use syn::parse::Parser;
use syn::visit::{self, Visit};
use crate::collect::type_param_names;
use crate::crate_scope::{child_module_names, local_type_namespace_names};
use crate::errors::missing_module_file_error;
use crate::finding::UnsafeSiteFact;
use crate::module_resolve::{locate_module_file, read_parse, resolve_module_branches};
use crate::resolve::{
AliasMap, BareFallback, ExternRenameMap, ReexportMap, UseMap, alias_nominal_targets,
bare_single_segment_ident, collect_reexports, collect_uses, extern_verbatim_renamed,
is_shadowed_param_path, path_to_string, render_last_segment_args, resolve_path, strip_raw,
type_to_string,
};
use crate::syn_util::{direct_path_value, has_cfg_attr, has_path_attr};
pub(crate) struct ImplSite {
pub(crate) module: String,
pub(crate) file: PathBuf,
pub(crate) trait_path: syn::Path,
pub(crate) self_ty: syn::Type,
pub(crate) uses: UseMap,
pub(crate) type_params: HashSet<String>,
}
pub(crate) struct TypeDef {
pub(crate) canonical: String,
pub(crate) module: String,
pub(crate) file: PathBuf,
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> {
bare_single_segment_ident(target)
.filter(|name| local_alias_names.contains(name))
.map(|name| 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 ancestors: HashSet<PathBuf> = HashSet::new();
ancestors.insert(xingbiao::canonicalize_or_fail(root_file)?);
walk_module(
root.items,
"crate".to_string(),
src_dir.to_path_buf(),
src_dir.to_path_buf(),
root_file.to_path_buf(),
crate_package,
externs,
&ancestors,
&mut scan,
)?;
Ok(scan)
}
fn module_cycle_error(module: &str, crate_package: &str, file: &Path) -> String {
format!(
"cannot judge module '{module}' in package '{crate_package}': its source file '{}' forms a \
module cycle (a symlink loop or a circular `#[path]`)",
file.display()
)
}
#[allow(clippy::type_complexity)]
fn resolve_child_modules(
items: &[syn::Item],
module: &str,
child_dir: &Path,
file_dir: &Path,
current_file: &Path,
crate_package: &str,
ancestors: &HashSet<PathBuf>,
) -> Result<
Vec<(
Vec<syn::Item>,
String,
PathBuf,
PathBuf,
Option<PathBuf>,
PathBuf,
)>,
String,
> {
let mut children = Vec::new();
let mut seen_files: HashSet<(String, PathBuf)> = HashSet::new();
for item in items {
let syn::Item::Mod(module_item) = item else {
continue;
};
let name = strip_raw(&module_item.ident.to_string());
let child_module = format!("{module}::{name}");
if let Some(rel) = direct_path_value(&module_item.attrs) {
match &module_item.content {
Some((_, inner)) => {
let relocated = file_dir.join(&rel);
children.push((
inner.clone(),
child_module,
relocated.clone(),
relocated,
None,
current_file.to_path_buf(),
))
}
None => {
let file = file_dir.join(&rel);
if !file.is_file() {
if has_cfg_attr(&module_item.attrs) {
continue;
}
return Err(missing_module_file_error(&child_module, crate_package));
}
let canon = xingbiao::canonicalize_or_fail(&file)?;
if ancestors.contains(&canon) {
return Err(module_cycle_error(&child_module, crate_package, &file));
}
if !seen_files.insert((name.clone(), canon.clone())) {
continue;
}
let parsed = read_parse(&file)?;
let own_dir = file
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| file_dir.to_path_buf());
children.push((
parsed.items,
child_module,
own_dir.clone(),
own_dir,
Some(canon),
file,
));
}
}
continue;
}
if has_path_attr(&module_item.attrs) {
continue;
}
let sub_dir = child_dir.join(&name);
match &module_item.content {
Some((_, inner)) => children.push((
inner.clone(),
child_module,
sub_dir.clone(),
sub_dir,
None,
current_file.to_path_buf(),
)),
None => match locate_module_file(child_dir, &name) {
Some(file) => {
let canon = xingbiao::canonicalize_or_fail(&file)?;
if ancestors.contains(&canon) {
return Err(module_cycle_error(&child_module, crate_package, &file));
}
if !seen_files.insert((name.clone(), canon.clone())) {
continue;
}
let parsed = read_parse(&file)?;
let own_dir = file
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| sub_dir.clone());
children.push((
parsed.items,
child_module,
sub_dir,
own_dir,
Some(canon),
file,
));
}
None => {
if !has_cfg_attr(&module_item.attrs) {
return Err(missing_module_file_error(&child_module, crate_package));
}
}
},
}
}
Ok(children)
}
#[allow(clippy::too_many_arguments)]
fn walk_module(
items: Vec<syn::Item>,
module: String,
child_dir: PathBuf,
file_dir: PathBuf,
current_file: PathBuf,
crate_package: &str,
externs: &HashSet<String>,
ancestors: &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(),
file: current_file.clone(),
trait_path: trait_path.clone(),
self_ty: (*impl_item.self_ty).clone(),
uses: uses.clone(),
type_params: type_param_names(&impl_item.generics),
});
}
syn::Item::Struct(i) => {
push_type_def(&i.attrs, &i.ident, &module, ¤t_file, &uses, scan)?;
}
syn::Item::Enum(i) => {
push_type_def(&i.attrs, &i.ident, &module, ¤t_file, &uses, scan)?;
}
syn::Item::Union(i) => {
push_type_def(&i.attrs, &i.ident, &module, ¤t_file, &uses, scan)?;
}
syn::Item::Type(type_item) => {
if !type_item.generics.params.is_empty() {
continue;
}
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);
}
}
let mut targets = Vec::new();
alias_nominal_targets(&type_item.ty, &mut targets);
for target in targets {
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 {
let entry = scan.aliases.entry(alias).or_default();
if !entry.contains(&resolved) {
entry.push(resolved);
}
}
}
}
}
_ => {}
}
}
for (child_items, child_module, sub_dir, sub_file_dir, opened, child_file) in
resolve_child_modules(
&items,
&module,
&child_dir,
&file_dir,
¤t_file,
crate_package,
ancestors,
)?
{
match opened {
Some(canon) => {
let mut child_ancestors = ancestors.clone();
child_ancestors.insert(canon);
walk_module(
child_items,
child_module,
sub_dir,
sub_file_dir,
child_file,
crate_package,
externs,
&child_ancestors,
scan,
)?;
}
None => walk_module(
child_items,
child_module,
sub_dir,
sub_file_dir,
child_file,
crate_package,
externs,
ancestors,
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>, PathBuf)>, String> {
let branches = resolve_module_branches(src_dir, root_file, module, crate_package)?;
let mut out: Vec<(String, Vec<syn::Item>, PathBuf)> = Vec::new();
for (items, file, child_dir, file_dir) in branches {
let mut ancestors: HashSet<PathBuf> = HashSet::new();
ancestors.insert(xingbiao::canonicalize_or_fail(&file)?);
collect_subtree(
items,
module.to_string(),
child_dir,
file_dir,
file,
crate_package,
&ancestors,
&mut out,
)?;
}
Ok(out)
}
#[allow(clippy::too_many_arguments)]
fn collect_subtree(
items: Vec<syn::Item>,
module: String,
child_dir: PathBuf,
file_dir: PathBuf,
current_file: PathBuf,
crate_package: &str,
ancestors: &HashSet<PathBuf>,
out: &mut Vec<(String, Vec<syn::Item>, PathBuf)>,
) -> Result<(), String> {
for (child_items, child_module, sub_dir, sub_file_dir, opened, child_file) in
resolve_child_modules(
&items,
&module,
&child_dir,
&file_dir,
¤t_file,
crate_package,
ancestors,
)?
{
match opened {
Some(canon) => {
let mut child_ancestors = ancestors.clone();
child_ancestors.insert(canon);
collect_subtree(
child_items,
child_module,
sub_dir,
sub_file_dir,
child_file,
crate_package,
&child_ancestors,
out,
)?;
}
None => collect_subtree(
child_items,
child_module,
sub_dir,
sub_file_dir,
child_file,
crate_package,
ancestors,
out,
)?,
}
}
out.push((module, items, current_file));
Ok(())
}
fn push_type_def(
attrs: &[syn::Attribute],
ident: &syn::Ident,
module: &str,
file: &Path,
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(),
file: file.to_path_buf(),
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(())
}
pub(crate) struct UnsafeSite {
pub(crate) module: String,
pub(crate) file: PathBuf,
pub(crate) site: UnsafeSiteFact,
}
struct UnsafeSiteCollector<'a> {
sites: Vec<UnsafeSiteFact>,
error: Option<String>,
module: &'a str,
uses: &'a UseMap,
local_types: &'a HashSet<String>,
current_owner: Option<String>,
current_trait: Option<String>,
current_impl_trait: Option<String>,
current_impl_is_trait: bool,
}
impl<'a> UnsafeSiteCollector<'a> {
fn new(module: &'a str, uses: &'a UseMap, local_types: &'a HashSet<String>) -> Self {
Self {
sites: Vec::new(),
error: None,
module,
uses,
local_types,
current_owner: None,
current_trait: None,
current_impl_trait: None,
current_impl_is_trait: false,
}
}
fn unsupported(&mut self, role: &str) {
if self.error.is_none() {
self.error = Some(format!(
"cannot identify unsafe {role} in {} without a positional fallback",
self.module
));
}
}
}
fn canonical_unsafe_owner(
self_ty: &syn::Type,
uses: &UseMap,
local_types: &HashSet<String>,
module: &str,
impl_type_params: &HashSet<String>,
) -> Option<String> {
if let syn::Type::Path(tp) = self_ty {
if tp.qself.is_none() && !is_shadowed_param_path(&tp.path, impl_type_params) {
let head = tp
.path
.segments
.first()
.map(|segment| strip_raw(&segment.ident.to_string()));
let should_resolve = tp.path.leading_colon.is_some()
|| matches!(head.as_deref(), Some("crate" | "self" | "super"))
|| head
.as_ref()
.is_some_and(|head| uses.contains_key(head) || local_types.contains(head));
if should_resolve {
let base = resolve_path(&tp.path, uses, module, BareFallback::CurrentModule)?;
return Some(format!("{base}{}", render_last_segment_args(&tp.path)?));
}
}
}
type_to_string(self_ty)
}
impl<'ast> Visit<'ast> for UnsafeSiteCollector<'_> {
fn visit_expr_unsafe(&mut self, node: &'ast syn::ExprUnsafe) {
self.sites.push(UnsafeSiteFact::Block);
visit::visit_expr_unsafe(self, node);
}
fn visit_item_fn(&mut self, node: &'ast syn::ItemFn) {
if node.sig.unsafety.is_some() {
self.sites.push(UnsafeSiteFact::FreeFn {
name: 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() {
let name = strip_raw(&node.sig.ident.to_string());
match (
self.current_impl_is_trait,
&self.current_impl_trait,
&self.current_owner,
) {
(true, Some(trait_ref), Some(owner)) => {
self.sites.push(UnsafeSiteFact::TraitImplMethod {
trait_ref: trait_ref.clone(),
owner: owner.clone(),
name,
});
}
(false, _, Some(owner)) => self.sites.push(UnsafeSiteFact::InherentMethod {
owner: owner.clone(),
name,
}),
_ => self.unsupported("method owner"),
}
}
visit::visit_impl_item_fn(self, node);
}
fn visit_trait_item_fn(&mut self, node: &'ast syn::TraitItemFn) {
if node.sig.unsafety.is_some() {
let name = strip_raw(&node.sig.ident.to_string());
match &self.current_trait {
Some(owner) => self.sites.push(UnsafeSiteFact::TraitMethod {
owner: owner.clone(),
name,
}),
None => self.unsupported("trait-method owner"),
}
}
visit::visit_trait_item_fn(self, node);
}
fn visit_item_impl(&mut self, node: &'ast syn::ItemImpl) {
let params = type_param_names(&node.generics);
let owner = canonical_unsafe_owner(
&node.self_ty,
self.uses,
self.local_types,
self.module,
¶ms,
);
let impl_trait = node
.trait_
.as_ref()
.and_then(|(_, path, _)| path_to_string(path));
if node.unsafety.is_some() {
match (&impl_trait, &owner, node.trait_.is_some()) {
(Some(trait_ref), Some(owner), true) => {
self.sites.push(UnsafeSiteFact::TraitImpl {
trait_ref: trait_ref.clone(),
owner: owner.clone(),
});
}
(None, Some(owner), false) => self.sites.push(UnsafeSiteFact::InherentImpl {
owner: owner.clone(),
}),
(None, _, true) => self.unsupported("impl trait"),
(_, None, _) => self.unsupported("impl self type"),
_ => unreachable!("trait presence and rendered trait stay aligned"),
}
}
let prev_owner = std::mem::replace(&mut self.current_owner, owner);
let prev_trait = self.current_impl_trait.take();
let prev_is_trait = self.current_impl_is_trait;
self.current_impl_is_trait = node.trait_.is_some();
self.current_impl_trait = impl_trait;
visit::visit_item_impl(self, node);
self.current_owner = prev_owner;
self.current_impl_trait = prev_trait;
self.current_impl_is_trait = prev_is_trait;
}
fn visit_item_trait(&mut self, node: &'ast syn::ItemTrait) {
let name = strip_raw(&node.ident.to_string());
if node.unsafety.is_some() {
self.sites
.push(UnsafeSiteFact::Trait { name: name.clone() });
}
let prev = self
.current_trait
.replace(format!("{}::{name}", self.module));
visit::visit_item_trait(self, node);
self.current_trait = prev;
}
fn visit_item_foreign_mod(&mut self, node: &'ast syn::ItemForeignMod) {
if node.unsafety.is_some() {
self.sites.push(UnsafeSiteFact::ExternBlock);
}
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 ancestors: HashSet<PathBuf> = HashSet::new();
ancestors.insert(xingbiao::canonicalize_or_fail(root_file)?);
walk_unsafe(
root.items,
"crate".to_string(),
src_dir.to_path_buf(),
src_dir.to_path_buf(),
root_file.to_path_buf(),
crate_package,
&ancestors,
&mut sites,
)?;
Ok(sites)
}
#[allow(clippy::too_many_arguments)]
fn walk_unsafe(
items: Vec<syn::Item>,
module: String,
child_dir: PathBuf,
file_dir: PathBuf,
current_file: PathBuf,
crate_package: &str,
ancestors: &HashSet<PathBuf>,
sites: &mut Vec<UnsafeSite>,
) -> Result<(), String> {
let uses = collect_uses(&items);
let local_types = local_type_namespace_names(&items);
let mut collector = UnsafeSiteCollector::new(&module, &uses, &local_types);
for item in &items {
if matches!(item, syn::Item::Mod(_)) {
continue;
}
collector.visit_item(item);
}
if let Some(error) = collector.error {
return Err(error);
}
for site in collector.sites {
sites.push(UnsafeSite {
module: module.clone(),
file: current_file.clone(),
site,
});
}
for (child_items, child_module, sub_dir, sub_file_dir, opened, child_file) in
resolve_child_modules(
&items,
&module,
&child_dir,
&file_dir,
¤t_file,
crate_package,
ancestors,
)?
{
match opened {
Some(canon) => {
let mut child_ancestors = ancestors.clone();
child_ancestors.insert(canon);
walk_unsafe(
child_items,
child_module,
sub_dir,
sub_file_dir,
child_file,
crate_package,
&child_ancestors,
sites,
)?;
}
None => walk_unsafe(
child_items,
child_module,
sub_dir,
sub_file_dir,
child_file,
crate_package,
ancestors,
sites,
)?,
}
}
Ok(())
}