use std::collections::HashSet;
use std::path::{Path, PathBuf};
use syn::parse::Parser;
use super::types::*;
use crate::collect::type_param_names;
use crate::crate_scope::local_type_namespace_names;
use crate::errors::{dual_backed_module_error, missing_module_file_error};
use crate::module_resolve::{ModuleFile, 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,
resolve_path_all, strip_raw,
};
use crate::syn_util::{
FlatItem, cfg_attr_path_values, child_module_decls, direct_path_value,
flatten_transparent_macro_items, flatten_with_body_nested_impls, has_cfg_attr,
};
fn collect_crate_root_extern_renames(items: &[syn::Item], out: &mut ExternRenameMap) {
for item in flatten_transparent_macro_items(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: AliasMap::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,
0,
&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()
)
}
const MAX_MODULE_DEPTH: usize = 32;
pub(super) fn check_module_depth(
depth: usize,
module: &str,
crate_package: &str,
) -> Result<(), String> {
if depth >= MAX_MODULE_DEPTH {
return Err(format!(
"cannot judge module '{module}' in package '{crate_package}': module nesting exceeds \
the depth bound ({MAX_MODULE_DEPTH}) this scanner supports without risking a native \
stack overflow"
));
}
Ok(())
}
pub(super) fn flatten_for_walk(items: &[syn::Item]) -> (Vec<syn::Item>, Vec<FlatItem>) {
let (flat, nested_impls) = flatten_with_body_nested_impls(items);
let mut plain: Vec<syn::Item> = flat.iter().map(|f| f.item.clone()).collect();
plain.extend(nested_impls);
(plain, flat)
}
type ChildEntry = (
Vec<syn::Item>,
String,
PathBuf,
PathBuf,
Option<PathBuf>,
PathBuf,
);
fn load_child_file(
file: &Path,
child_module: &str,
crate_package: &str,
ancestors: &HashSet<PathBuf>,
seen_files: &mut HashSet<(String, PathBuf)>,
name: &str,
) -> Result<Option<(Vec<syn::Item>, PathBuf)>, String> {
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.to_string(), canon.clone())) {
return Ok(None);
}
let parsed = read_parse(file)?;
Ok(Some((parsed.items, canon)))
}
#[allow(clippy::too_many_arguments)]
fn resolve_direct_path_child(
rel: &str,
module_item: &syn::ItemMod,
name: &str,
child_module: &str,
file_dir: &Path,
current_file: &Path,
crate_package: &str,
cfg_conditional: bool,
ancestors: &HashSet<PathBuf>,
seen_files: &mut HashSet<(String, PathBuf)>,
children: &mut Vec<ChildEntry>,
) -> Result<(), String> {
match &module_item.content {
Some((_, inner)) => {
let relocated = file_dir.join(rel);
children.push((
inner.clone(),
child_module.to_string(),
relocated.clone(),
relocated,
None,
current_file.to_path_buf(),
));
}
None => {
let file = file_dir.join(rel);
if !file.is_file() {
if cfg_conditional {
return Ok(());
}
return Err(missing_module_file_error(child_module, crate_package));
}
let Some((items, canon)) = load_child_file(
&file,
child_module,
crate_package,
ancestors,
seen_files,
name,
)?
else {
return Ok(());
};
let own_dir = file
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| file_dir.to_path_buf());
children.push((
items,
child_module.to_string(),
own_dir.clone(),
own_dir,
Some(canon),
file,
));
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn resolve_conventional_child(
module_item: &syn::ItemMod,
name: &str,
child_module: &str,
child_dir: &Path,
file_dir: &Path,
current_file: &Path,
crate_package: &str,
cfg_conditional: bool,
ancestors: &HashSet<PathBuf>,
seen_files: &mut HashSet<(String, PathBuf)>,
children: &mut Vec<ChildEntry>,
) -> Result<(), String> {
let cfg_attr_targets = cfg_attr_path_values(&module_item.attrs);
let sub_dir = child_dir.join(name);
match &module_item.content {
Some((_, inner)) => {
let mut bases: Vec<PathBuf> = cfg_attr_targets
.iter()
.map(|rel| file_dir.join(rel))
.chain(std::iter::once(sub_dir.clone()))
.filter(|base| base.is_dir())
.collect();
bases.sort();
bases.dedup();
if bases.is_empty() {
bases.push(sub_dir);
}
for base in bases {
children.push((
inner.clone(),
child_module.to_string(),
base.clone(),
base,
None,
current_file.to_path_buf(),
));
}
}
None => {
let mut has_backing_source = false;
for rel in &cfg_attr_targets {
let file = file_dir.join(rel);
if file.is_file() {
has_backing_source = true;
if let Some((items, canon)) = load_child_file(
&file,
child_module,
crate_package,
ancestors,
seen_files,
name,
)? {
let own_dir = file
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| file_dir.to_path_buf());
children.push((
items,
child_module.to_string(),
own_dir.clone(),
own_dir,
Some(canon),
file,
));
}
}
}
match locate_module_file(child_dir, name) {
ModuleFile::One(file) => {
if let Some((items, canon)) = load_child_file(
&file,
child_module,
crate_package,
ancestors,
seen_files,
name,
)? {
let own_dir = file
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| sub_dir.clone());
children.push((
items,
child_module.to_string(),
sub_dir,
own_dir,
Some(canon),
file,
));
}
}
ModuleFile::Ambiguous { flat, nested } => {
return Err(dual_backed_module_error(
child_module,
name,
crate_package,
&flat,
&nested,
));
}
ModuleFile::Absent => {
if !has_backing_source && !cfg_conditional {
return Err(missing_module_file_error(child_module, crate_package));
}
}
}
}
}
Ok(())
}
pub(super) fn resolve_child_modules(
items: &[FlatItem],
module: &str,
child_dir: &Path,
file_dir: &Path,
current_file: &Path,
crate_package: &str,
ancestors: &HashSet<PathBuf>,
) -> Result<Vec<ChildEntry>, String> {
let mut children = Vec::new();
let mut seen_files: HashSet<(String, PathBuf)> = HashSet::new();
for flat in items {
let syn::Item::Mod(module_item) = &flat.item else {
continue;
};
let name = strip_raw(&module_item.ident.to_string());
let child_module = format!("{module}::{name}");
let cfg_conditional = flat.in_transparent_arm || has_cfg_attr(&module_item.attrs);
if let Some(rel) = direct_path_value(&module_item.attrs) {
resolve_direct_path_child(
&rel,
module_item,
&name,
&child_module,
file_dir,
current_file,
crate_package,
cfg_conditional,
ancestors,
&mut seen_files,
&mut children,
)?;
continue;
}
resolve_conventional_child(
module_item,
&name,
&child_module,
child_dir,
file_dir,
current_file,
crate_package,
cfg_conditional,
ancestors,
&mut seen_files,
&mut children,
)?;
}
Ok(children)
}
#[allow(clippy::too_many_arguments)]
fn record_module_facts(
items: &[syn::Item],
module: &str,
current_file: &Path,
uses: &UseMap,
externs: &HashSet<String>,
externs_type: &HashSet<String>,
local_alias_names: &HashSet<String>,
scan: &mut CrateScan,
) -> Result<(), String> {
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.to_string(),
file: current_file.to_path_buf(),
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, current_file, uses, scan)?;
}
syn::Item::Enum(i) => {
push_type_def(&i.attrs, &i.ident, module, current_file, uses, scan)?;
}
syn::Item::Union(i) => {
push_type_def(&i.attrs, &i.ident, module, current_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 {
let landings =
resolve_path_all(&tp.path, uses, module, BareFallback::CurrentModule);
if !landings.is_empty() {
let alias =
format!("{module}::{}", strip_raw(&type_item.ident.to_string()));
let entry = scan.alias_targets.entry(alias).or_default();
for landing in landings {
if !entry.contains(&landing) {
entry.push(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_list: Vec<String> = if target.leading_colon.is_some() {
extern_verbatim_renamed(target, externs, &scan.extern_renames)
.into_iter()
.collect()
} else {
let use_candidates =
resolve_path_all(target, uses, module, BareFallback::Ignore);
if !use_candidates.is_empty() {
use_candidates
} else {
bare_local_alias_target(target, module, local_alias_names)
.or_else(|| {
extern_verbatim_renamed(
target,
externs_type,
&scan.extern_renames,
)
})
.into_iter()
.collect()
}
};
for resolved in resolved_list {
if resolved != alias {
let entry = scan.aliases.entry(alias.clone()).or_default();
if !entry.contains(&resolved) {
entry.push(resolved);
}
}
}
}
}
_ => {}
}
}
Ok(())
}
#[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>,
depth: usize,
scan: &mut CrateScan,
) -> Result<(), String> {
check_module_depth(depth, &module, crate_package)?;
let (items, flat) = flatten_for_walk(&items);
let uses = collect_uses(&items);
let child_mod_decls = child_module_decls(&flat);
collect_reexports(
&flat,
&module,
externs,
&child_mod_decls,
&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();
record_module_facts(
&items,
&module,
¤t_file,
&uses,
externs,
&externs_type,
&local_alias_names,
scan,
)?;
for (child_items, child_module, sub_dir, sub_file_dir, opened, child_file) in
resolve_child_modules(
&flat,
&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,
depth + 1,
scan,
)?;
}
None => walk_module(
child_items,
child_module,
sub_dir,
sub_file_dir,
child_file,
crate_package,
externs,
ancestors,
depth + 1,
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,
0,
&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>,
depth: usize,
out: &mut Vec<(String, Vec<syn::Item>, PathBuf)>,
) -> Result<(), String> {
check_module_depth(depth, &module, crate_package)?;
let (items, flat) = flatten_for_walk(&items);
for (child_items, child_module, sub_dir, sub_file_dir, opened, child_file) in
resolve_child_modules(
&flat,
&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,
depth + 1,
out,
)?;
}
None => collect_subtree(
child_items,
child_module,
sub_dir,
sub_file_dir,
child_file,
crate_package,
ancestors,
depth + 1,
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(())
}