use std::collections::HashSet;
use syn::visit::Visit;
use super::{
has_allow_dead_code, has_cfg_test, has_test_attr, qualify_name, DeclaredFunction, FileVisitor,
};
pub(crate) struct DeclaredFnCollector {
pub(crate) file: String,
pub(crate) functions: Vec<DeclaredFunction>,
in_test: bool,
parent_type: Option<String>,
is_trait_impl: bool,
}
impl DeclaredFnCollector {
pub(crate) fn new() -> Self {
Self {
file: String::new(),
functions: Vec::new(),
in_test: false,
parent_type: None,
is_trait_impl: false,
}
}
}
impl FileVisitor for DeclaredFnCollector {
fn reset_for_file(&mut self, file_path: &str) {
self.file = file_path.to_string();
self.in_test = false;
self.parent_type = None;
self.is_trait_impl = false;
}
}
impl<'ast> Visit<'ast> for DeclaredFnCollector {
fn visit_item_fn(&mut self, node: &'ast syn::ItemFn) {
let name = node.sig.ident.to_string();
let line = node.sig.ident.span().start().line;
self.functions.push(DeclaredFunction {
qualified_name: qualify_name(&self.parent_type, &name),
is_main: name == "main",
is_test: self.in_test || has_test_attr(&node.attrs) || has_cfg_test(&node.attrs),
is_trait_impl: false,
has_allow_dead_code: has_allow_dead_code(&node.attrs),
is_api: false,
is_test_helper: false,
name,
file: self.file.clone(),
line,
});
syn::visit::visit_item_fn(self, node);
}
fn visit_item_impl(&mut self, node: &'ast syn::ItemImpl) {
let prev_parent = self.parent_type.take();
let prev_is_trait = self.is_trait_impl;
let prev_in_test = self.in_test;
if has_cfg_test(&node.attrs) {
self.in_test = true;
}
self.is_trait_impl = node.trait_.is_some();
if let syn::Type::Path(tp) = &*node.self_ty {
if let Some(seg) = tp.path.segments.last() {
self.parent_type = Some(seg.ident.to_string());
}
}
syn::visit::visit_item_impl(self, node);
self.parent_type = prev_parent;
self.is_trait_impl = prev_is_trait;
self.in_test = prev_in_test;
}
fn visit_impl_item_fn(&mut self, node: &'ast syn::ImplItemFn) {
let name = node.sig.ident.to_string();
let line = node.sig.ident.span().start().line;
self.functions.push(DeclaredFunction {
qualified_name: qualify_name(&self.parent_type, &name),
is_main: false,
is_test: self.in_test || has_test_attr(&node.attrs) || has_cfg_test(&node.attrs),
is_trait_impl: self.is_trait_impl,
has_allow_dead_code: has_allow_dead_code(&node.attrs),
is_api: false,
is_test_helper: false,
name,
file: self.file.clone(),
line,
});
}
fn visit_trait_item_fn(&mut self, node: &'ast syn::TraitItemFn) {
if node.default.is_some() {
let name = node.sig.ident.to_string();
let line = node.sig.ident.span().start().line;
self.functions.push(DeclaredFunction {
qualified_name: qualify_name(&self.parent_type, &name),
is_main: false,
is_test: self.in_test,
is_trait_impl: true,
has_allow_dead_code: false,
is_api: false,
is_test_helper: false,
name,
file: self.file.clone(),
line,
});
}
}
fn visit_item_mod(&mut self, node: &'ast syn::ItemMod) {
let prev_in_test = self.in_test;
if has_cfg_test(&node.attrs) {
self.in_test = true;
}
syn::visit::visit_item_mod(self, node);
self.in_test = prev_in_test;
}
}
#[derive(Debug, Clone)]
pub struct DeadCodeWarning {
pub function_name: String,
pub qualified_name: String,
pub file: String,
pub line: usize,
pub kind: DeadCodeKind,
pub suggestion: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DeadCodeKind {
Uncalled,
TestOnly,
}
pub fn detect_dead_code(
parsed: &[(String, String, syn::File)],
config: &crate::config::Config,
api_lines: &std::collections::HashMap<String, std::collections::HashSet<usize>>,
test_helper_lines: &std::collections::HashMap<String, std::collections::HashSet<usize>>,
cfg_test_files: &std::collections::HashSet<String>,
) -> Vec<DeadCodeWarning> {
let declared = super::collect_declared_functions(parsed);
let mut declared = mark_cfg_test_declarations(declared, cfg_test_files);
mark_api_declarations(&mut declared, api_lines);
mark_test_helper_declarations(&mut declared, test_helper_lines);
let (prod_calls, test_calls) = collect_all_calls(parsed, cfg_test_files);
let uncalled = find_uncalled(&declared, &prod_calls, &test_calls, config);
let test_only = find_test_only(&declared, &prod_calls, &test_calls, config);
merge_warnings(uncalled, test_only)
}
pub(crate) fn mark_api_declarations(
declared: &mut [super::DeclaredFunction],
api_lines: &std::collections::HashMap<String, std::collections::HashSet<usize>>,
) {
declared.iter_mut().for_each(|d| {
if let Some(lines) = api_lines.get(&d.file) {
if crate::findings::has_annotation_in_window(lines, d.line) {
d.is_api = true;
}
}
});
}
pub(crate) fn mark_test_helper_declarations(
declared: &mut [super::DeclaredFunction],
test_helper_lines: &std::collections::HashMap<String, std::collections::HashSet<usize>>,
) {
declared.iter_mut().for_each(|d| {
if let Some(lines) = test_helper_lines.get(&d.file) {
if crate::findings::has_annotation_in_window(lines, d.line) {
d.is_test_helper = true;
}
}
});
}
fn merge_warnings(
mut uncalled: Vec<DeadCodeWarning>,
test_only: Vec<DeadCodeWarning>,
) -> Vec<DeadCodeWarning> {
uncalled.extend(test_only);
uncalled
}
pub(crate) use crate::adapters::shared::cfg_test_files::collect_cfg_test_file_paths;
fn mark_cfg_test_declarations(
mut declared: Vec<super::DeclaredFunction>,
cfg_test_files: &HashSet<String>,
) -> Vec<super::DeclaredFunction> {
declared.iter_mut().for_each(|d| {
if cfg_test_files.contains(&d.file) {
d.is_test = true;
}
});
declared
}
pub(crate) use super::call_targets::collect_all_calls;
fn find_uncalled(
declared: &[DeclaredFunction],
prod_calls: &HashSet<String>,
test_calls: &HashSet<String>,
config: &crate::config::Config,
) -> Vec<DeadCodeWarning> {
declared
.iter()
.filter(|d| !should_exclude_uncalled(d, config))
.filter(|d| !prod_calls.contains(&d.name) && !test_calls.contains(&d.name))
.filter(|d| {
!prod_calls.contains(&d.qualified_name) && !test_calls.contains(&d.qualified_name)
})
.map(|d| DeadCodeWarning {
function_name: d.name.clone(),
qualified_name: d.qualified_name.clone(),
file: d.file.clone(),
line: d.line,
kind: DeadCodeKind::Uncalled,
suggestion: "never called; consider removing".to_string(),
})
.collect()
}
fn find_test_only(
declared: &[DeclaredFunction],
prod_calls: &HashSet<String>,
test_calls: &HashSet<String>,
config: &crate::config::Config,
) -> Vec<DeadCodeWarning> {
declared
.iter()
.filter(|d| !should_exclude_test_only(d, config))
.filter(|d| {
let called_from_tests =
test_calls.contains(&d.name) || test_calls.contains(&d.qualified_name);
let called_from_prod =
prod_calls.contains(&d.name) || prod_calls.contains(&d.qualified_name);
called_from_tests && !called_from_prod
})
.map(|d| DeadCodeWarning {
function_name: d.name.clone(),
qualified_name: d.qualified_name.clone(),
file: d.file.clone(),
line: d.line,
kind: DeadCodeKind::TestOnly,
suggestion: concat!(
"only called from test code; move to tests/ or annotate with ",
"// qual:api (public API) or // qual:test_helper (test-only helper)"
)
.to_string(),
})
.collect()
}
fn should_exclude_uncalled(d: &DeclaredFunction, config: &crate::config::Config) -> bool {
let is_ignored = |name: &str| config.is_ignored_function(name);
d.is_main
|| d.is_test
|| d.is_trait_impl
|| d.has_allow_dead_code
|| d.is_api
|| is_ignored(&d.name)
}
fn should_exclude_test_only(d: &DeclaredFunction, config: &crate::config::Config) -> bool {
d.is_test_helper || should_exclude_uncalled(d, config)
}