use syn::visit::Visit;
const STD_CONSTRUCTORS: &[&str] = &[
"Box", "Arc", "Rc", "String", "Vec", "HashMap", "HashSet", "Option", "Result", "Ok", "Err",
"Some", "None", "Ordering", "Duration", "Path", "PathBuf", "CString", "CStr", "OsString",
"OsStr", "Default", "Clone",
];
const STD_MODULE_CALLS: &[&str] = &[
"fs::read",
"fs::write",
"fs::read_to_string",
"fs::read_dir",
"fs::create_dir",
"fs::create_dir_all",
"fs::remove_file",
"fs::remove_dir",
"fs::remove_dir_all",
"fs::copy",
"fs::rename",
"fs::metadata",
"env::var",
"env::args",
"env::temp_dir",
"env::current_dir",
"env::current_exe",
"env::set_var",
"process::exit",
"process::abort",
"process::id",
"thread::sleep",
"thread::spawn",
"net::TcpStream",
"net::TcpListener",
"net::UdpSocket",
];
fn dep_weight(label: &str) -> f64 {
if label.starts_with("println")
|| label.starts_with("eprintln")
|| label.starts_with("print!")
|| label.starts_with("eprint!")
{
0.2
} else if label.starts_with("Instant")
|| label.starts_with("SystemTime")
|| label.starts_with("Utc")
|| label.starts_with("Local")
|| label.contains("elapsed")
{
0.3
} else if label.starts_with("env::") || label.starts_with("process::") {
0.4
} else if label.starts_with("unsafe") {
0.5
} else {
0.6
}
}
fn path_label(path: &syn::Path) -> String {
path.segments
.iter()
.map(|s| s.ident.to_string())
.collect::<Vec<_>>()
.join("::")
}
pub(crate) struct HiddenDepFinder {
pub(crate) count: usize,
pub(crate) weight: f64,
pub(crate) labels: Vec<String>,
concrete_fields: Vec<String>,
}
impl HiddenDepFinder {
pub(crate) fn new() -> Self {
Self {
count: 0,
weight: 0.0,
labels: Vec::new(),
concrete_fields: Vec::new(),
}
}
pub(crate) fn set_concrete_fields(&mut self, fields: Vec<String>) {
self.concrete_fields = fields;
}
fn add_dep(&mut self, label: &str) {
self.count += 1;
let w = dep_weight(label);
self.weight += w;
self.labels.push(label.to_string());
}
fn check_path(&mut self, path: &syn::Path) {
let segments: Vec<_> = path.segments.iter().map(|s| s.ident.to_string()).collect();
if segments.is_empty() {
return;
}
if segments[0] == "Self" || segments[0] == "self" {
return;
}
let tail_start = segments.len().saturating_sub(2);
let tail = segments[tail_start..].join("::");
if STD_MODULE_CALLS.contains(&tail.as_str())
&& (segments.len() <= 2 || segments[0] == "std" || segments[0] == "core")
{
self.add_dep(&tail);
return;
}
let first = &segments[0];
if first.starts_with(|c: char| c.is_ascii_uppercase())
&& !STD_CONSTRUCTORS.contains(&first.as_str())
{
let label = segments.join("::");
self.add_dep(&label);
}
}
}
fn is_print_macro(path: &syn::Path) -> bool {
if let Some(name) = path.get_ident() {
let n = name.to_string();
return n == "println" || n == "eprintln" || n == "print" || n == "eprint";
}
false
}
impl<'ast> Visit<'ast> for HiddenDepFinder {
fn visit_stmt(&mut self, stmt: &'ast syn::Stmt) {
if let syn::Stmt::Macro(stmt_macro) = stmt {
if is_print_macro(&stmt_macro.mac.path) {
self.add_dep(&path_label(&stmt_macro.mac.path));
return;
}
}
syn::visit::visit_stmt(self, stmt);
}
fn visit_expr(&mut self, expr: &'ast syn::Expr) {
if self.handle_expr(expr) {
syn::visit::visit_expr(self, expr);
}
}
fn visit_expr_unsafe(&mut self, _expr: &'ast syn::ExprUnsafe) {
self.add_dep("unsafe { ... }");
}
}
impl HiddenDepFinder {
fn handle_expr(&mut self, expr: &syn::Expr) -> bool {
match expr {
syn::Expr::Call(expr_call) => {
if let syn::Expr::Path(expr_path) = &*expr_call.func {
self.check_path(&expr_path.path);
}
true
}
syn::Expr::MethodCall(expr_method) => self.handle_method_call_expr(expr_method),
syn::Expr::Macro(expr_macro) => self.handle_macro_expr(expr_macro),
_ => true,
}
}
fn handle_method_call_expr(&mut self, expr_method: &syn::ExprMethodCall) -> bool {
if Self::is_bare_self(&expr_method.receiver) {
return false;
}
if let Some(name) = Self::concrete_field_name(&expr_method.receiver) {
if self.concrete_fields.contains(&name) {
let label = format!("self.{name}.{}", expr_method.method);
self.add_dep(&label);
}
}
true
}
fn handle_macro_expr(&mut self, expr_macro: &syn::ExprMacro) -> bool {
if is_print_macro(&expr_macro.mac.path) {
self.add_dep(&path_label(&expr_macro.mac.path));
return false;
}
true
}
fn is_bare_self(receiver: &syn::Expr) -> bool {
matches!(
receiver,
syn::Expr::Path(expr_path)
if expr_path.path.segments.len() == 1 && expr_path.path.segments[0].ident == "self"
)
}
fn concrete_field_name(receiver: &syn::Expr) -> Option<String> {
let syn::Expr::Field(expr_field) = receiver else {
return None;
};
let syn::Expr::Path(expr_path) = &*expr_field.base else {
return None;
};
if expr_path.path.segments.len() != 1 || expr_path.path.segments[0].ident != "self" {
return None;
}
let syn::Member::Named(ident) = &expr_field.member else {
return None;
};
Some(ident.to_string())
}
}