use super::super::bindings::CanonScope;
use super::super::file_visibility::collect_file_root_visibility;
use super::super::local_symbols::{collect_local_symbols_scoped, FileScope, LocalSymbols};
use super::super::pub_fns_visibility::{collect_visible_type_canonicals_workspace, is_visible};
use super::super::workspace_graph::{
canonical_fn_name, collect_crate_root_modules, resolve_impl_self_type,
};
use crate::adapters::analyzers::architecture::layer_rule::LayerDefinitions;
use crate::adapters::shared::cfg_test::{has_cfg_test, has_test_attr};
use crate::adapters::shared::use_tree::{gather_alias_map_scoped, AliasMap};
use std::collections::{HashMap, HashSet};
use syn::visit::Visit;
const STDLIB_ATTRS: &[&str] = &[
"allow",
"deny",
"warn",
"forbid",
"deprecated",
"inline",
"cold",
"must_use",
"doc",
"cfg",
"cfg_attr",
"test",
"derive",
"repr",
"non_exhaustive",
"no_mangle",
"link",
"automatically_derived",
"track_caller",
"expect",
];
pub(crate) struct PrivateCandidate {
pub canonical: String,
pub file: String,
pub line: usize,
pub fn_name: String,
pub layer: Option<String>,
pub attr_names: Vec<String>,
}
pub(crate) fn collect_private_candidates(
files: &[(&str, &syn::File)],
aliases_per_file: &HashMap<String, AliasMap>,
layers: &LayerDefinitions,
transparent_wrappers: &HashSet<String>,
workspace: &super::super::local_symbols::WorkspaceLookup<'_>,
) -> Vec<PrivateCandidate> {
let crate_root_modules = workspace.crate_root_modules;
let workspace_module_paths = workspace.workspace_module_paths;
let file_root_visibility = collect_file_root_visibility(files);
let visible_canonicals = collect_visible_type_canonicals_workspace(
files,
aliases_per_file,
workspace,
transparent_wrappers,
);
let empty_aliases = HashMap::new();
let mut out = Vec::new();
for (path, ast) in files {
if workspace.cfg_test_files.contains(*path) {
continue;
}
let alias_map = aliases_per_file.get(*path).unwrap_or(&empty_aliases);
let aliases_per_scope = gather_alias_map_scoped(ast);
let LocalSymbols { flat, by_name } = collect_local_symbols_scoped(ast);
let file = FileScope {
path,
alias_map,
aliases_per_scope: &aliases_per_scope,
local_symbols: &flat,
local_decl_scopes: &by_name,
crate_root_modules,
workspace_module_paths: Some(workspace_module_paths),
};
let file_visible = file_root_visibility.get(*path).copied().unwrap_or(true);
let mut collector = CandidateCollector {
file: &file,
layer: layers.layer_for_file(path).map(String::from),
visible_canonicals: &visible_canonicals,
mod_stack: Vec::new(),
impl_stack: Vec::new(),
enclosing_mod_visible: file_visible,
found: &mut out,
};
collector.visit_file(ast);
}
out
}
struct CandidateCollector<'a, 'vis> {
file: &'vis FileScope<'vis>,
layer: Option<String>,
visible_canonicals: &'vis HashSet<String>,
mod_stack: Vec<String>,
impl_stack: Vec<(Vec<String>, bool)>,
enclosing_mod_visible: bool,
found: &'a mut Vec<PrivateCandidate>,
}
impl<'a, 'vis> CandidateCollector<'a, 'vis> {
fn current_self_type(&self) -> Option<&[String]> {
self.impl_stack.last().map(|(segs, _)| segs.as_slice())
}
fn current_impl_visible(&self) -> bool {
self.impl_stack.last().map(|(_, v)| *v).unwrap_or(true)
}
fn record_if_candidate(
&mut self,
sig: &syn::Signature,
vis: &syn::Visibility,
attrs: &[syn::Attribute],
) {
if !matches!(vis, syn::Visibility::Inherited) {
return;
}
if has_cfg_test(attrs) || has_test_attr(attrs) {
return;
}
if !has_non_stdlib_attribute(attrs) {
return;
}
if !self.enclosing_mod_visible {
return;
}
if !self.impl_stack.is_empty() && !self.current_impl_visible() {
return;
}
let fn_name = sig.ident.to_string();
let self_type = self.current_self_type();
let canonical = canonical_fn_name(self.file.path, self_type, &self.mod_stack, &fn_name);
let line = syn::spanned::Spanned::span(&sig.ident).start().line;
self.found.push(PrivateCandidate {
canonical,
file: self.file.path.to_string(),
line,
fn_name,
layer: self.layer.clone(),
attr_names: non_stdlib_attribute_names(attrs),
});
}
}
impl<'ast, 'a, 'vis> Visit<'ast> for CandidateCollector<'a, 'vis> {
fn visit_item_fn(&mut self, node: &'ast syn::ItemFn) {
self.record_if_candidate(&node.sig, &node.vis, &node.attrs);
syn::visit::visit_item_fn(self, node);
}
fn visit_item_impl(&mut self, node: &'ast syn::ItemImpl) {
if has_cfg_test(&node.attrs) {
return;
}
let scope = CanonScope {
file: self.file,
mod_stack: &self.mod_stack,
reexports: None,
};
let canonical_segs = resolve_impl_self_type(&node.self_ty, &scope).unwrap_or_default();
let visible = !canonical_segs.is_empty()
&& self.visible_canonicals.contains(&canonical_segs.join("::"));
self.impl_stack.push((canonical_segs, visible));
syn::visit::visit_item_impl(self, node);
self.impl_stack.pop();
}
fn visit_impl_item_fn(&mut self, node: &'ast syn::ImplItemFn) {
self.record_if_candidate(&node.sig, &node.vis, &node.attrs);
syn::visit::visit_impl_item_fn(self, node);
}
fn visit_item_mod(&mut self, node: &'ast syn::ItemMod) {
if has_cfg_test(&node.attrs) {
return;
}
let parent_visible = self.enclosing_mod_visible;
self.enclosing_mod_visible = parent_visible && is_visible(&node.vis);
self.mod_stack.push(node.ident.to_string());
syn::visit::visit_item_mod(self, node);
self.mod_stack.pop();
self.enclosing_mod_visible = parent_visible;
}
}
fn has_non_stdlib_attribute(attrs: &[syn::Attribute]) -> bool {
attrs.iter().any(|a| {
a.path()
.segments
.last()
.map(|s| s.ident.to_string())
.is_some_and(|name| !STDLIB_ATTRS.contains(&name.as_str()))
})
}
fn non_stdlib_attribute_names(attrs: &[syn::Attribute]) -> Vec<String> {
attrs
.iter()
.filter_map(|a| {
a.path()
.segments
.last()
.map(|s| s.ident.to_string())
.filter(|name| !STDLIB_ATTRS.contains(&name.as_str()))
})
.collect()
}