use crate::cache::ParseCache;
use crate::discover::CrateInfo;
use crate::reference::{PathPrefix, TypeReference};
use crate::utils::read_source_file;
use std::path::Path;
use std::rc::Rc;
use syn::{Item, UseTree};
use tracing::debug;
#[must_use]
pub(crate) fn extract_public_items(
file_path: &Path,
inline_module: &[&str],
target_module: &str,
caller_module: &str,
cache: &mut ParseCache,
) -> Option<Vec<String>> {
let file = if let Some(cached) = cache.get(file_path) {
cached
} else {
let content = read_source_file(file_path).ok()?;
let parsed = syn::parse_file(&content).ok()?;
let rc = Rc::new(parsed);
cache.insert(file_path.to_path_buf(), Rc::clone(&rc));
rc
};
let items = if inline_module.is_empty() {
&file.items
} else {
return extract_from_inline_module(
&file.items,
inline_module,
target_module,
caller_module,
);
};
Some(collect_public_items(items, target_module, caller_module))
}
fn extract_from_inline_module(
items: &[Item],
module_path: &[&str],
target_module: &str,
caller_module: &str,
) -> Option<Vec<String>> {
if module_path.is_empty() {
return Some(collect_public_items(items, target_module, caller_module));
}
let target = module_path[0];
for item in items {
if let Item::Mod(item_mod) = item
&& item_mod.ident == target
&& let Some((_, nested_items)) = &item_mod.content
{
return extract_from_inline_module(
nested_items,
&module_path[1..],
target_module,
caller_module,
);
}
}
None
}
const fn item_vis_and_ident(item: &Item) -> Option<(&syn::Visibility, &proc_macro2::Ident)> {
match item {
Item::Const(i) => Some((&i.vis, &i.ident)),
Item::Enum(i) => Some((&i.vis, &i.ident)),
Item::Fn(i) => Some((&i.vis, &i.sig.ident)),
Item::ExternCrate(i) => Some((&i.vis, &i.ident)),
Item::Mod(i) => Some((&i.vis, &i.ident)),
Item::Static(i) => Some((&i.vis, &i.ident)),
Item::Struct(i) => Some((&i.vis, &i.ident)),
Item::Trait(i) => Some((&i.vis, &i.ident)),
Item::TraitAlias(i) => Some((&i.vis, &i.ident)),
Item::Type(i) => Some((&i.vis, &i.ident)),
Item::Union(i) => Some((&i.vis, &i.ident)),
_ => None,
}
}
fn is_visible_from(vis: &syn::Visibility, target_module: &str, caller_module: &str) -> bool {
match vis {
syn::Visibility::Public(_) => true,
syn::Visibility::Restricted(r) => {
if r.path.is_ident("crate") {
true
} else if r.path.is_ident("super") {
let parent = parent_module(target_module);
is_in_subtree(caller_module, parent)
} else if r.path.is_ident("self") {
false
} else {
let ancestor = normalize_in_path(&r.path, target_module);
is_in_subtree(caller_module, &ancestor)
}
}
syn::Visibility::Inherited => false,
}
}
fn normalize_in_path(path: &syn::Path, target_module: &str) -> String {
let mut segs = path.segments.iter().map(|s| s.ident.to_string());
match segs.next().as_deref() {
Some("crate") => segs.collect::<Vec<_>>().join("::"),
Some("super") => {
let parent = parent_module(target_module);
let rest: Vec<_> = segs.collect();
if rest.is_empty() {
parent.to_owned()
} else if parent.is_empty() {
rest.join("::")
} else {
format!("{parent}::{}", rest.join("::"))
}
}
Some("self") => {
let rest: Vec<_> = segs.collect();
if rest.is_empty() {
target_module.to_owned()
} else if target_module.is_empty() {
rest.join("::")
} else {
format!("{target_module}::{}", rest.join("::"))
}
}
Some(first) => {
let rest: Vec<_> = segs.collect();
if rest.is_empty() {
first.to_owned()
} else {
format!("{first}::{}", rest.join("::"))
}
}
None => String::new(),
}
}
fn parent_module(module: &str) -> &str {
module.rsplit_once("::").map_or("", |(parent, _)| parent)
}
fn is_in_subtree(module: &str, ancestor: &str) -> bool {
if ancestor.is_empty() {
true
} else {
module == ancestor || module.starts_with(&format!("{ancestor}::"))
}
}
fn collect_public_items(items: &[Item], target_module: &str, caller_module: &str) -> Vec<String> {
let mut public_items = Vec::new();
for item in items {
if let Item::Use(use_item) = item {
if is_visible_from(&use_item.vis, target_module, caller_module) {
extract_use_names(&use_item.tree, &mut public_items);
}
} else if let Some((vis, ident)) = item_vis_and_ident(item) {
if is_visible_from(vis, target_module, caller_module) {
public_items.push(ident.to_string());
}
}
}
public_items
}
fn extract_use_names(tree: &UseTree, items: &mut Vec<String>) {
match tree {
UseTree::Name(name) => {
items.push(name.ident.to_string());
}
UseTree::Rename(rename) => {
items.push(rename.rename.to_string());
}
UseTree::Path(path) => {
extract_use_names(&path.tree, items);
}
UseTree::Group(group) => {
for item in &group.items {
extract_use_names(item, items);
}
}
UseTree::Glob(_) => {
}
}
}
pub(crate) fn resolve_glob(
reference: &TypeReference,
caller_module: &str,
crate_info: &CrateInfo,
cache: &mut ParseCache,
) -> Vec<TypeReference> {
let is_crate_prefix = reference.prefix() == PathPrefix::Crate;
let is_crate_name_prefix = reference.prefix() == PathPrefix::None
&& reference
.segments()
.first()
.is_some_and(|s| s == crate_info.root_package_name());
if !is_crate_prefix && !is_crate_name_prefix {
return vec![reference.clone()];
}
let module_path = reference.segments().join("::");
if module_path.is_empty() {
return vec![reference.clone()];
}
let file_path = match crate_info.resolve_module_path_to_file(&module_path) {
Ok(path) => path,
Err(e) => {
debug!(
"Cannot resolve glob for '{}': {e}",
reference.to_path_string()
);
return vec![reference.clone()];
}
};
let inline_path = detect_inline_path(reference, &file_path, crate_info);
let inline_refs: Vec<&str> = inline_path.iter().map(String::as_str).collect();
let Some(public_items) =
extract_public_items(&file_path, &inline_refs, &module_path, caller_module, cache)
else {
debug!("Cannot parse '{}' for glob resolution", file_path.display());
return vec![reference.clone()];
};
if public_items.is_empty() {
return vec![];
}
public_items
.into_iter()
.map(|item| {
let mut segments = reference.segments().to_vec();
segments.push(item);
TypeReference::new(segments).with_prefix(reference.prefix())
})
.collect()
}
fn detect_inline_path(
reference: &TypeReference,
resolved_file: &Path,
crate_info: &CrateInfo,
) -> Vec<String> {
let segments = reference.segments();
for split in (1..segments.len()).rev() {
let prefix_path = segments[..split].join("::");
match crate_info.resolve_module_path_to_file(&prefix_path) {
Ok(ref parent_file) if parent_file == resolved_file => {
return segments[split..].to_vec();
}
_ => {
}
}
}
vec![]
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
fn extract(path: &Path, inline: &[&str]) -> Option<Vec<String>> {
extract_public_items(path, inline, "", "", &mut ParseCache::new())
}
fn extract_with(
path: &Path,
inline: &[&str],
target: &str,
caller: &str,
) -> Option<Vec<String>> {
extract_public_items(path, inline, target, caller, &mut ParseCache::new())
}
#[test]
fn extracts_all_public_item_kinds() {
let mut f = NamedTempFile::new().unwrap();
writeln!(
f,
r"
pub struct PublicStruct;
pub(crate) struct CrateStruct;
struct PrivateStruct;
pub fn public_function() {{}}
pub(crate) fn crate_function() {{}}
fn private_function() {{}}
pub const PUBLIC_CONST: i32 = 42;
pub(crate) const CRATE_CONST: i32 = 43;
const PRIVATE_CONST: i32 = 42;
pub enum PublicEnum {{ A, B }}
pub(crate) enum CrateEnum {{ X, Y }}
enum PrivateEnum {{ X, Y }}
pub type PublicType = String;
pub(crate) type CrateType = String;
type PrivateType = String;
pub mod public_module {{}}
pub(crate) mod crate_module {{}}
mod private_module {{}}
pub trait PublicTrait {{}}
pub(crate) trait CrateTrait {{}}
trait PrivateTrait {{}}
pub use std::collections::HashMap;
pub(crate) use std::collections::BTreeMap;
"
)
.unwrap();
let items = extract(f.path(), &[]).unwrap();
assert!(items.contains(&"PublicStruct".to_owned()));
assert!(items.contains(&"public_function".to_owned()));
assert!(items.contains(&"PUBLIC_CONST".to_owned()));
assert!(items.contains(&"PublicEnum".to_owned()));
assert!(items.contains(&"PublicType".to_owned()));
assert!(items.contains(&"public_module".to_owned()));
assert!(items.contains(&"PublicTrait".to_owned()));
assert!(items.contains(&"HashMap".to_owned()));
assert!(items.contains(&"CrateStruct".to_owned()));
assert!(items.contains(&"crate_function".to_owned()));
assert!(items.contains(&"CRATE_CONST".to_owned()));
assert!(items.contains(&"CrateEnum".to_owned()));
assert!(items.contains(&"CrateType".to_owned()));
assert!(items.contains(&"crate_module".to_owned()));
assert!(items.contains(&"CrateTrait".to_owned()));
assert!(items.contains(&"BTreeMap".to_owned()));
assert!(!items.contains(&"PrivateStruct".to_owned()));
assert!(!items.contains(&"private_function".to_owned()));
assert!(!items.contains(&"PRIVATE_CONST".to_owned()));
assert!(!items.contains(&"PrivateEnum".to_owned()));
assert!(!items.contains(&"PrivateType".to_owned()));
assert!(!items.contains(&"private_module".to_owned()));
assert!(!items.contains(&"PrivateTrait".to_owned()));
}
#[test]
fn extracts_pub_static() {
let mut f = NamedTempFile::new().unwrap();
writeln!(
f,
r"
pub static PUB_STATIC: u32 = 1;
static PRIV_STATIC: u32 = 2;
"
)
.unwrap();
let items = extract(f.path(), &[]).unwrap();
assert!(items.contains(&"PUB_STATIC".to_owned()));
assert!(!items.contains(&"PRIV_STATIC".to_owned()));
}
#[test]
fn extracts_pub_use_rename() {
let mut f = NamedTempFile::new().unwrap();
writeln!(
f,
r"
pub use std::collections::HashMap as Map;
"
)
.unwrap();
let items = extract(f.path(), &[]).unwrap();
assert!(items.contains(&"Map".to_owned()));
assert!(!items.contains(&"HashMap".to_owned()));
}
#[test]
fn extracts_pub_use_group() {
let mut f = NamedTempFile::new().unwrap();
writeln!(
f,
r"
pub use std::collections::{{HashMap, HashSet}};
"
)
.unwrap();
let items = extract(f.path(), &[]).unwrap();
assert!(items.contains(&"HashMap".to_owned()));
assert!(items.contains(&"HashSet".to_owned()));
}
#[test]
fn returns_none_for_nonexistent_file() {
let result = extract(Path::new("/nonexistent/file.rs"), &[]);
assert!(result.is_none());
}
#[test]
fn returns_empty_for_no_public_items() {
let mut f = NamedTempFile::new().unwrap();
writeln!(
f,
r"
struct Private;
fn helper() {{}}
"
)
.unwrap();
let items = extract(f.path(), &[]).unwrap();
assert!(items.is_empty());
}
#[test]
fn extracts_items_from_inline_module() {
let mut f = NamedTempFile::new().unwrap();
writeln!(
f,
r"
pub fn top_level() {{}}
pub mod inner {{
pub fn inner_func() {{}}
pub struct InnerStruct;
fn private_in_inner() {{}}
}}
"
)
.unwrap();
let root_items = extract(f.path(), &[]).unwrap();
assert!(root_items.contains(&"top_level".to_owned()));
assert!(root_items.contains(&"inner".to_owned()));
let inner_items = extract(f.path(), &["inner"]).unwrap();
assert!(inner_items.contains(&"inner_func".to_owned()));
assert!(inner_items.contains(&"InnerStruct".to_owned()));
assert!(!inner_items.contains(&"private_in_inner".to_owned()));
assert!(!inner_items.contains(&"top_level".to_owned()));
}
#[test]
fn extracts_items_from_nested_inline_module() {
let mut f = NamedTempFile::new().unwrap();
writeln!(
f,
r"
pub mod outer {{
pub mod inner {{
pub const DEEP: u32 = 42;
pub fn deep_fn() {{}}
}}
pub fn outer_fn() {{}}
}}
"
)
.unwrap();
let items = extract(f.path(), &["outer", "inner"]).unwrap();
assert!(items.contains(&"DEEP".to_owned()));
assert!(items.contains(&"deep_fn".to_owned()));
assert!(!items.contains(&"outer_fn".to_owned()));
}
#[test]
fn returns_none_for_missing_inline_module() {
let mut f = NamedTempFile::new().unwrap();
writeln!(
f,
r"
pub fn top_level() {{}}
"
)
.unwrap();
let result = extract(f.path(), &["nonexistent"]);
assert!(result.is_none());
}
#[test]
fn pub_super_visible_from_parent_and_siblings() {
let mut f = NamedTempFile::new().unwrap();
writeln!(
f,
r"
pub mod bar {{
pub(super) fn helper() {{}}
pub fn public_fn() {{}}
}}
"
)
.unwrap();
let items = extract_with(f.path(), &["bar"], "foo::bar", "foo").unwrap();
assert!(items.contains(&"helper".to_owned()));
assert!(items.contains(&"public_fn".to_owned()));
let items = extract_with(f.path(), &["bar"], "foo::bar", "foo::other").unwrap();
assert!(items.contains(&"helper".to_owned()));
let items = extract_with(f.path(), &["bar"], "foo::bar", "foo::other::deep").unwrap();
assert!(items.contains(&"helper".to_owned()));
let items = extract_with(f.path(), &["bar"], "foo::bar", "baz").unwrap();
assert!(!items.contains(&"helper".to_owned()));
assert!(items.contains(&"public_fn".to_owned()));
let items = extract_with(f.path(), &["bar"], "foo::bar", "").unwrap();
assert!(!items.contains(&"helper".to_owned()));
}
#[test]
fn pub_super_in_top_level_module_behaves_like_pub_crate() {
let mut f = NamedTempFile::new().unwrap();
writeln!(f, "pub(super) fn helper() {{}}").unwrap();
for caller in ["", "bar", "baz::deep", "foo"] {
let items = extract_with(f.path(), &[], "foo", caller).unwrap();
assert!(items.contains(&"helper".to_owned()), "caller = {caller:?}");
}
}
#[test]
fn pub_in_path_visible_from_within_scope() {
let mut f = NamedTempFile::new().unwrap();
writeln!(
f,
r"
pub(in crate::visibility) fn restricted() {{}}
pub fn public_fn() {{}}
"
)
.unwrap();
let items = extract_with(f.path(), &[], "visibility", "visibility").unwrap();
assert!(items.contains(&"restricted".to_owned()));
assert!(items.contains(&"public_fn".to_owned()));
let items = extract_with(f.path(), &[], "visibility", "visibility::inner").unwrap();
assert!(items.contains(&"restricted".to_owned()));
}
#[test]
fn pub_in_path_hidden_from_outside_scope() {
let mut f = NamedTempFile::new().unwrap();
writeln!(
f,
r"
pub(in crate::visibility) fn restricted() {{}}
pub fn public_fn() {{}}
"
)
.unwrap();
let items = extract_with(f.path(), &[], "visibility", "other_mod").unwrap();
assert!(!items.contains(&"restricted".to_owned()));
assert!(items.contains(&"public_fn".to_owned()));
let items = extract_with(f.path(), &[], "visibility", "").unwrap();
assert!(!items.contains(&"restricted".to_owned()));
}
#[test]
fn normalize_in_path_crate_prefix() {
let path: syn::Path = syn::parse_str("crate::foo::bar").unwrap();
assert_eq!(normalize_in_path(&path, "foo::bar"), "foo::bar");
let path: syn::Path = syn::parse_str("crate::visibility").unwrap();
assert_eq!(normalize_in_path(&path, "visibility::inner"), "visibility");
let path: syn::Path = syn::parse_str("crate").unwrap();
assert_eq!(normalize_in_path(&path, "foo"), "");
}
#[test]
fn normalize_in_path_super_prefix() {
let path: syn::Path = syn::parse_str("super").unwrap();
assert_eq!(normalize_in_path(&path, "foo::bar"), "foo");
let path: syn::Path = syn::parse_str("super::sibling").unwrap();
assert_eq!(normalize_in_path(&path, "foo::bar"), "foo::sibling");
let path: syn::Path = syn::parse_str("super").unwrap();
assert_eq!(normalize_in_path(&path, "foo"), "");
}
#[test]
fn normalize_in_path_self_prefix() {
let path: syn::Path = syn::parse_str("self").unwrap();
assert_eq!(normalize_in_path(&path, "foo::bar"), "foo::bar");
let path: syn::Path = syn::parse_str("self::inner").unwrap();
assert_eq!(normalize_in_path(&path, "foo::bar"), "foo::bar::inner");
}
#[test]
fn parent_module_handles_root_and_nested() {
assert_eq!(parent_module("foo::bar"), "foo");
assert_eq!(parent_module("foo::bar::baz"), "foo::bar");
assert_eq!(parent_module("foo"), "");
assert_eq!(parent_module(""), "");
}
#[test]
fn is_in_subtree_semantics() {
assert!(is_in_subtree("", ""));
assert!(is_in_subtree("foo", ""));
assert!(is_in_subtree("foo::bar", ""));
assert!(is_in_subtree("foo", "foo"));
assert!(is_in_subtree("foo::bar", "foo"));
assert!(is_in_subtree("foo::bar::baz", "foo"));
assert!(!is_in_subtree("foobar", "foo"));
assert!(!is_in_subtree("baz", "foo"));
assert!(!is_in_subtree("", "foo"));
}
}