use crate::adapters::analyzers::dry::dead_code::*;
use crate::adapters::analyzers::dry::{
has_allow_dead_code, has_cfg_test, has_test_attr, qualify_name, DeclaredFunction, FileVisitor,
};
use crate::config::Config;
use std::collections::HashSet;
use syn::visit::Visit;
fn parse(code: &str) -> Vec<(String, String, syn::File)> {
let syntax = syn::parse_file(code).expect("parse failed");
vec![("test.rs".to_string(), code.to_string(), syntax)]
}
#[test]
fn test_detect_dead_code_empty() {
let parsed = parse("");
let config = Config::default();
let warnings = detect_dead_code(
&parsed,
&config,
&std::collections::HashMap::new(),
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
);
assert!(warnings.is_empty());
}
#[test]
fn test_uncalled_function_detected() {
let code = r#"
fn called_fn() { let x = 1; }
fn caller() { called_fn(); }
fn never_called() { let y = 2; }
"#;
let parsed = parse(code);
let config = Config::default();
let warnings = detect_dead_code(
&parsed,
&config,
&std::collections::HashMap::new(),
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
);
let uncalled: Vec<_> = warnings
.iter()
.filter(|w| w.kind == DeadCodeKind::Uncalled)
.collect();
assert!(
uncalled.iter().any(|w| w.function_name == "never_called"),
"never_called should be flagged as uncalled"
);
assert!(
!uncalled.iter().any(|w| w.function_name == "called_fn"),
"called_fn should not be flagged"
);
}
#[test]
fn test_called_function_not_flagged() {
let code = r#"
fn helper() { let x = 1; }
fn main() { helper(); }
"#;
let parsed = parse(code);
let config = Config::default();
let warnings = detect_dead_code(
&parsed,
&config,
&std::collections::HashMap::new(),
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
);
assert!(
!warnings.iter().any(|w| w.function_name == "helper"),
"called function should not be flagged"
);
}
#[test]
fn test_main_excluded_from_dead_code() {
let code = "fn main() {}";
let parsed = parse(code);
let config = Config::default();
let warnings = detect_dead_code(
&parsed,
&config,
&std::collections::HashMap::new(),
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
);
assert!(
!warnings.iter().any(|w| w.function_name == "main"),
"main should never be flagged"
);
}
#[test]
fn test_test_function_excluded() {
let code = r#"
#[test]
fn test_something() { let x = 1; }
"#;
let parsed = parse(code);
let config = Config::default();
let warnings = detect_dead_code(
&parsed,
&config,
&std::collections::HashMap::new(),
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
);
assert!(
!warnings.iter().any(|w| w.function_name == "test_something"),
"test functions should be excluded"
);
}
#[test]
fn test_trait_impl_excluded() {
let code = r#"
trait Foo { fn bar(&self); }
struct S;
impl Foo for S {
fn bar(&self) {}
}
"#;
let parsed = parse(code);
let config = Config::default();
let warnings = detect_dead_code(
&parsed,
&config,
&std::collections::HashMap::new(),
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
);
assert!(
!warnings.iter().any(|w| w.function_name == "bar"),
"trait impl methods should be excluded"
);
}
#[test]
fn test_allow_dead_code_excluded() {
let code = r#"
#[allow(dead_code)]
fn intentionally_unused() { let x = 1; }
"#;
let parsed = parse(code);
let config = Config::default();
let warnings = detect_dead_code(
&parsed,
&config,
&std::collections::HashMap::new(),
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
);
assert!(
!warnings
.iter()
.any(|w| w.function_name == "intentionally_unused"),
"Functions with #[allow(dead_code)] should be excluded"
);
}
#[test]
fn test_ignored_function_excluded() {
let code = r#"
fn visit_expr(&self) { let x = 1; }
"#;
let parsed = parse(code);
let mut config = Config::default();
config.ignore_functions = vec!["visit_*".to_string()];
config.compile();
let warnings = detect_dead_code(
&parsed,
&config,
&std::collections::HashMap::new(),
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
);
assert!(
!warnings.iter().any(|w| w.function_name == "visit_expr"),
"Ignored functions should be excluded"
);
}
#[test]
fn test_test_only_function_detected() {
let code = r#"
fn helper() { let x = 1; }
fn production() { let y = 2; }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_it() { helper(); }
}
"#;
let parsed = parse(code);
let config = Config::default();
let warnings = detect_dead_code(
&parsed,
&config,
&std::collections::HashMap::new(),
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
);
let test_only: Vec<_> = warnings
.iter()
.filter(|w| w.kind == DeadCodeKind::TestOnly)
.collect();
assert!(
test_only.iter().any(|w| w.function_name == "helper"),
"helper called only from tests should be flagged as test-only"
);
}
#[test]
fn testonly_suggestion_mentions_qual_api_and_test_helper() {
let code = r#"
pub fn helper() { let x = 1; }
#[cfg(test)]
mod tests {
use super::helper;
#[test]
fn t() { helper(); }
}
"#;
let parsed = parse(code);
let config = Config::default();
let warnings = detect_dead_code(
&parsed,
&config,
&std::collections::HashMap::new(),
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
);
let testonly = warnings
.iter()
.find(|w| w.kind == DeadCodeKind::TestOnly)
.expect("helper should be flagged test-only");
assert!(
testonly.suggestion.contains("qual:api")
&& testonly.suggestion.contains("qual:test_helper"),
"testonly suggestion should mention both escape hatches, got: {:?}",
testonly.suggestion
);
}
#[test]
fn test_dead_code_always_runs_when_called_directly() {
let code = r#"
fn never_called() { let x = 1; }
"#;
let parsed = parse(code);
let mut config = Config::default();
config.duplicates.detect_dead_code = false;
let warnings = detect_dead_code(
&parsed,
&config,
&std::collections::HashMap::new(),
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
);
assert!(
!warnings.is_empty(),
"detect_dead_code runs regardless — pipeline guards the config flag"
);
}
#[test]
fn test_method_call_detected() {
let code = r#"
struct S;
impl S {
fn helper(&self) { let x = 1; }
fn caller(&self) { self.helper(); }
}
"#;
let parsed = parse(code);
let config = Config::default();
let warnings = detect_dead_code(
&parsed,
&config,
&std::collections::HashMap::new(),
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
);
assert!(
!warnings.iter().any(|w| w.function_name == "helper"),
"Method called via self.helper() should not be flagged"
);
}
#[test]
fn test_function_reference_as_call_argument() {
let code = r#"
fn some_fn(x: i32) -> i32 { x + 1 }
fn caller() {
let items = vec![1, 2, 3];
let _: Vec<_> = items.into_iter().map(some_fn).collect();
}
"#;
let parsed = parse(code);
let config = Config::default();
let warnings = detect_dead_code(
&parsed,
&config,
&std::collections::HashMap::new(),
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
);
assert!(
!warnings.iter().any(|w| w.function_name == "some_fn"),
"Function passed as argument to map() should be detected as called"
);
}
#[test]
fn test_function_reference_as_method_argument() {
let code = r#"
fn process(x: i32) { let _ = x; }
fn caller() {
let items = vec![1, 2, 3];
items.iter().for_each(process);
}
"#;
let parsed = parse(code);
let config = Config::default();
let warnings = detect_dead_code(
&parsed,
&config,
&std::collections::HashMap::new(),
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
);
assert!(
!warnings.iter().any(|w| w.function_name == "process"),
"Function passed as argument to for_each() should be detected as called"
);
}
#[test]
fn test_qualified_function_reference_as_argument() {
let code = r#"
mod report {
pub fn print_item(x: &i32) { let _ = x; }
}
fn caller() {
let items = vec![1, 2, 3];
items.iter().for_each(report::print_item);
}
"#;
let parsed = parse(code);
let config = Config::default();
let warnings = detect_dead_code(
&parsed,
&config,
&std::collections::HashMap::new(),
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
);
assert!(
!warnings.iter().any(|w| w.function_name == "print_item"),
"Qualified function reference (module::fn) should be detected as called"
);
}
#[test]
fn test_qualified_call_detected() {
let code = r#"
struct Config;
impl Config {
fn load() -> Self { Config }
}
fn main() { let c = Config::load(); }
"#;
let parsed = parse(code);
let config = Config::default();
let warnings = detect_dead_code(
&parsed,
&config,
&std::collections::HashMap::new(),
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
);
assert!(
!warnings.iter().any(|w| w.function_name == "load"),
"Config::load() should be detected as called"
);
}
#[test]
fn test_pub_use_reexport_not_dead_code() {
let code = r#"
mod foo { pub fn do_work() { let x = 1; } }
pub use foo::do_work;
fn main() {}
"#;
let parsed = parse(code);
let config = Config::default();
let warnings = detect_dead_code(
&parsed,
&config,
&std::collections::HashMap::new(),
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
);
assert!(
!warnings.iter().any(|w| w.function_name == "do_work"),
"pub use re-exported function should not be flagged as dead code"
);
}
#[test]
fn test_pub_use_rename_not_dead_code() {
let code = r#"
mod foo { pub fn do_work() { let x = 1; } }
pub use foo::do_work as perform_work;
fn main() {}
"#;
let parsed = parse(code);
let config = Config::default();
let warnings = detect_dead_code(
&parsed,
&config,
&std::collections::HashMap::new(),
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
);
assert!(
!warnings.iter().any(|w| w.function_name == "do_work"),
"pub use rename re-export should record original name, not alias"
);
}
#[test]
fn test_pub_use_group_reexport_not_dead_code() {
let code = r#"
mod foo {
pub fn bar() { let x = 1; }
pub fn baz() { let y = 2; }
}
pub use foo::{bar, baz};
fn main() {}
"#;
let parsed = parse(code);
let config = Config::default();
let warnings = detect_dead_code(
&parsed,
&config,
&std::collections::HashMap::new(),
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
);
assert!(
!warnings.iter().any(|w| w.function_name == "bar"),
"grouped pub use re-export: bar should not be flagged"
);
assert!(
!warnings.iter().any(|w| w.function_name == "baz"),
"grouped pub use re-export: baz should not be flagged"
);
}
#[test]
fn test_private_use_does_not_count_as_reexport() {
let code = r#"
mod foo { pub fn helper() { let x = 1; } }
use foo::helper;
fn main() {}
"#;
let parsed = parse(code);
let config = Config::default();
let warnings = detect_dead_code(
&parsed,
&config,
&std::collections::HashMap::new(),
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
);
assert!(
warnings.iter().any(|w| w.function_name == "helper"),
"private use import (no call) should still be flagged as uncalled"
);
}
#[test]
fn test_cfg_test_mod_file_not_flagged() {
let parent_code = r#"
fn production_fn() { let x = 1; }
#[cfg(test)]
mod helpers;
"#;
let child_code = r#"
pub fn test_helper() { let x = 1; }
"#;
let parent_ast = syn::parse_file(parent_code).expect("parse parent");
let child_ast = syn::parse_file(child_code).expect("parse child");
let parsed = vec![
(
"src/mod.rs".to_string(),
parent_code.to_string(),
parent_ast,
),
(
"src/helpers.rs".to_string(),
child_code.to_string(),
child_ast,
),
];
let config = Config::default();
let cfg_test_files =
crate::adapters::shared::cfg_test_files::collect_cfg_test_file_paths(&parsed);
let warnings = detect_dead_code(
&parsed,
&config,
&std::collections::HashMap::new(),
&std::collections::HashMap::new(),
&cfg_test_files,
);
assert!(
!warnings.iter().any(|w| w.function_name == "test_helper"),
"Functions in #[cfg(test)] mod file should not be flagged as dead code"
);
}
#[test]
fn test_cfg_test_mod_calls_classified_as_test() {
let parent_code = r#"
fn used_by_test_helpers() { let x = 1; }
fn used_by_production() { let y = 2; }
fn caller() { used_by_production(); }
#[cfg(test)]
mod helpers;
"#;
let child_code = r#"
pub fn test_helper() { super::used_by_test_helpers(); }
"#;
let parent_ast = syn::parse_file(parent_code).expect("parse parent");
let child_ast = syn::parse_file(child_code).expect("parse child");
let parsed = vec![
(
"src/lib.rs".to_string(),
parent_code.to_string(),
parent_ast,
),
(
"src/helpers.rs".to_string(),
child_code.to_string(),
child_ast,
),
];
let config = Config::default();
let cfg_test_files =
crate::adapters::shared::cfg_test_files::collect_cfg_test_file_paths(&parsed);
let warnings = detect_dead_code(
&parsed,
&config,
&std::collections::HashMap::new(),
&std::collections::HashMap::new(),
&cfg_test_files,
);
let test_only: Vec<_> = warnings
.iter()
.filter(|w| w.kind == DeadCodeKind::TestOnly)
.collect();
assert!(
test_only
.iter()
.any(|w| w.function_name == "used_by_test_helpers"),
"Function called only from cfg(test) file should be flagged as test-only"
);
assert!(
!warnings
.iter()
.any(|w| w.function_name == "used_by_production"),
"Function called from production should not be flagged"
);
}
#[test]
fn test_cfg_test_mod_dir_module() {
let parent_code = r#"
fn prod() { let x = 1; }
#[cfg(test)]
mod helpers;
"#;
let child_code = r#"
pub fn test_util() { let x = 1; }
"#;
let parent_ast = syn::parse_file(parent_code).expect("parse parent");
let child_ast = syn::parse_file(child_code).expect("parse child");
let parsed = vec![
(
"src/foo/mod.rs".to_string(),
parent_code.to_string(),
parent_ast,
),
(
"src/foo/helpers/mod.rs".to_string(),
child_code.to_string(),
child_ast,
),
];
let config = Config::default();
let cfg_test_files =
crate::adapters::shared::cfg_test_files::collect_cfg_test_file_paths(&parsed);
let warnings = detect_dead_code(
&parsed,
&config,
&std::collections::HashMap::new(),
&std::collections::HashMap::new(),
&cfg_test_files,
);
assert!(
!warnings.iter().any(|w| w.function_name == "test_util"),
"Functions in #[cfg(test)] dir module (mod.rs) should not be flagged"
);
}
#[test]
fn test_cfg_test_file_path_from_non_mod_parent() {
let parent_code = r#"
fn prod() { let x = 1; }
#[cfg(test)]
mod test_utils;
"#;
let child_code = r#"
pub fn helper() { let x = 1; }
"#;
let parent_ast = syn::parse_file(parent_code).expect("parse parent");
let child_ast = syn::parse_file(child_code).expect("parse child");
let parsed = vec![
(
"src/foo.rs".to_string(),
parent_code.to_string(),
parent_ast,
),
(
"src/foo/test_utils.rs".to_string(),
child_code.to_string(),
child_ast,
),
];
let config = Config::default();
let cfg_test_files =
crate::adapters::shared::cfg_test_files::collect_cfg_test_file_paths(&parsed);
let warnings = detect_dead_code(
&parsed,
&config,
&std::collections::HashMap::new(),
&std::collections::HashMap::new(),
&cfg_test_files,
);
assert!(
!warnings.iter().any(|w| w.function_name == "helper"),
"Functions in cfg(test) child of foo.rs → foo/test_utils.rs should be excluded"
);
}
#[test]
fn test_serde_deserialize_with_not_dead_code() {
let code = r#"
fn custom_de<'de, D: serde::Deserializer<'de>>(d: D) -> Result<i32, D::Error> {
let v: i32 = serde::Deserialize::deserialize(d)?;
Ok(v)
}
#[derive(serde::Deserialize)]
struct Foo {
#[serde(deserialize_with = "custom_de")]
value: i32,
}
"#;
let parsed = parse(code);
let config = Config::default();
let warnings = detect_dead_code(
&parsed,
&config,
&std::collections::HashMap::new(),
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
);
assert!(
!warnings.iter().any(|w| w.function_name == "custom_de"),
"Function referenced by #[serde(deserialize_with)] should not be flagged"
);
}
#[test]
fn test_serde_serialize_with_not_dead_code() {
let code = r#"
fn custom_ser<S: serde::Serializer>(v: &i32, s: S) -> Result<S::Ok, S::Error> {
s.serialize_i32(*v)
}
#[derive(serde::Serialize)]
struct Foo {
#[serde(serialize_with = "custom_ser")]
value: i32,
}
"#;
let parsed = parse(code);
let config = Config::default();
let warnings = detect_dead_code(
&parsed,
&config,
&std::collections::HashMap::new(),
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
);
assert!(
!warnings.iter().any(|w| w.function_name == "custom_ser"),
"Function referenced by #[serde(serialize_with)] should not be flagged"
);
}
#[test]
fn test_serde_default_fn_not_dead_code() {
let code = r#"
fn default_val() -> i32 { 42 }
#[derive(serde::Deserialize)]
struct Foo {
#[serde(default = "default_val")]
value: i32,
}
"#;
let parsed = parse(code);
let config = Config::default();
let warnings = detect_dead_code(
&parsed,
&config,
&std::collections::HashMap::new(),
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
);
assert!(
!warnings.iter().any(|w| w.function_name == "default_val"),
"Function referenced by #[serde(default = \"fn\")] should not be flagged"
);
}
#[test]
fn test_serde_qualified_path_not_dead_code() {
let code = r#"
mod helpers {
pub fn custom_de<'de, D: serde::Deserializer<'de>>(d: D) -> Result<i32, D::Error> {
let v: i32 = serde::Deserialize::deserialize(d)?;
Ok(v)
}
}
#[derive(serde::Deserialize)]
struct Foo {
#[serde(deserialize_with = "helpers::custom_de")]
value: i32,
}
"#;
let parsed = parse(code);
let config = Config::default();
let warnings = detect_dead_code(
&parsed,
&config,
&std::collections::HashMap::new(),
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
);
assert!(
!warnings.iter().any(|w| w.function_name == "custom_de"),
"Qualified serde fn ref (helpers::custom_de) should not be flagged"
);
}
#[test]
fn test_serde_with_module_not_dead_code() {
let code = r#"
mod my_format {
pub fn serialize<S: serde::Serializer>(_v: &i32, s: S) -> Result<S::Ok, S::Error> {
s.serialize_i32(0)
}
pub fn deserialize<'de, D: serde::Deserializer<'de>>(d: D) -> Result<i32, D::Error> {
let v: i32 = serde::Deserialize::deserialize(d)?;
Ok(v)
}
}
struct Foo {
#[serde(with = "my_format")]
value: i32,
}
"#;
let parsed = parse(code);
let config = Config::default();
let warnings = detect_dead_code(
&parsed,
&config,
&std::collections::HashMap::new(),
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
);
assert!(
!warnings.iter().any(|w| w.function_name == "serialize"),
"Function referenced via #[serde(with)] should not be flagged"
);
}
#[test]
fn test_serde_default_without_value_ignored() {
let code = r#"
fn unused_fn() { let x = 1; }
#[derive(serde::Deserialize)]
struct Foo {
#[serde(default)]
value: i32,
}
"#;
let parsed = parse(code);
let config = Config::default();
let warnings = detect_dead_code(
&parsed,
&config,
&std::collections::HashMap::new(),
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
);
assert!(
warnings.iter().any(|w| w.function_name == "unused_fn"),
"Unrelated unused function should still be flagged"
);
}
#[test]
fn test_serde_default_fn_cross_file_not_dead_code() {
let code_a = r#"
pub fn default_true() -> bool { true }
pub fn default_adx_period() -> u32 { 14 }
"#;
let code_b = r#"
#[derive(serde::Deserialize)]
struct Config {
#[serde(default = "default_true")]
enabled: bool,
#[serde(default = "default_adx_period")]
adx_period: u32,
}
"#;
let ast_a = syn::parse_file(code_a).expect("parse code_a");
let ast_b = syn::parse_file(code_b).expect("parse code_b");
let parsed = vec![
(
"src/config_defaults.rs".to_string(),
code_a.to_string(),
ast_a,
),
("src/config.rs".to_string(), code_b.to_string(), ast_b),
];
let config = Config::default();
let warnings = detect_dead_code(
&parsed,
&config,
&std::collections::HashMap::new(),
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
);
assert!(
!warnings.iter().any(|w| w.function_name == "default_true"),
"default_true referenced via #[serde(default)] in another file should not be flagged"
);
assert!(
!warnings
.iter()
.any(|w| w.function_name == "default_adx_period"),
"default_adx_period referenced via #[serde(default)] in another file should not be flagged"
);
}
#[test]
fn test_serde_default_fn_realistic_pattern() {
let code = r#"
fn default_true() -> bool { true }
fn default_false() -> bool { false }
fn default_period() -> u32 { 14 }
fn default_threshold() -> f64 { 0.5 }
#[derive(serde::Deserialize)]
struct IndicatorConfig {
#[serde(default = "default_true")]
enabled: bool,
#[serde(default = "default_false")]
verbose: bool,
#[serde(default = "default_period")]
period: u32,
#[serde(default = "default_threshold")]
threshold: f64,
}
"#;
let parsed = parse(code);
let config = Config::default();
let warnings = detect_dead_code(
&parsed,
&config,
&std::collections::HashMap::new(),
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
);
let flagged: Vec<&str> = warnings.iter().map(|w| w.function_name.as_str()).collect();
assert!(
!flagged.contains(&"default_true"),
"default_true should not be flagged, got: {flagged:?}"
);
assert!(
!flagged.contains(&"default_false"),
"default_false should not be flagged, got: {flagged:?}"
);
assert!(
!flagged.contains(&"default_period"),
"default_period should not be flagged, got: {flagged:?}"
);
assert!(
!flagged.contains(&"default_threshold"),
"default_threshold should not be flagged, got: {flagged:?}"
);
}
#[test]
fn test_call_inside_assert_detected_as_test_call() {
let code = r#"
fn helper() -> bool { true }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_it() {
assert!(helper());
}
}
"#;
let parsed = parse(code);
let cfg_test_files = collect_cfg_test_file_paths(&parsed);
let (_prod_calls, test_calls) = collect_all_calls(&parsed, &cfg_test_files);
assert!(
test_calls.contains("helper"),
"Call inside assert!() should be in test_calls"
);
}
#[test]
fn test_call_inside_assert_eq_detected() {
let code = r#"
fn compute() -> usize { 42 }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_it() {
assert_eq!(compute(), 42);
}
}
"#;
let parsed = parse(code);
let cfg_test_files = collect_cfg_test_file_paths(&parsed);
let (_prod, test_calls) = collect_all_calls(&parsed, &cfg_test_files);
assert!(
test_calls.contains("compute"),
"Call inside assert_eq!() should be in test_calls"
);
}
#[test]
fn test_collect_cfg_test_file_paths_inline_mod_ignored() {
let code = r#"
#[cfg(test)]
mod tests {
fn helper() {}
}
"#;
let ast = syn::parse_file(code).unwrap();
let parsed = vec![("src/lib.rs".to_string(), code.to_string(), ast)];
let result = collect_cfg_test_file_paths(&parsed);
assert!(
result.is_empty(),
"Inline cfg(test) mod should not produce cfg-test file entries"
);
}
#[test]
fn test_api_function_excluded_from_dead_code() {
let code = r#"
// qual:api
pub fn public_api() { let x = 1; }
// spacer to move internal_unused outside annotation window
// another spacer line
fn internal_unused() { let y = 2; }
"#;
let parsed = parse(code);
let config = Config::default();
let mut api_lines = std::collections::HashMap::new();
api_lines.insert(
"test.rs".to_string(),
[2usize]
.into_iter()
.collect::<std::collections::HashSet<_>>(),
);
let warnings = detect_dead_code(
&parsed,
&config,
&api_lines,
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
);
let names: Vec<&str> = warnings.iter().map(|w| w.function_name.as_str()).collect();
assert!(
!names.contains(&"public_api"),
"API-marked function should be excluded"
);
assert!(
names.contains(&"internal_unused"),
"Non-API function should still be flagged"
);
}
#[test]
fn test_api_does_not_count_as_suppression() {
assert!(crate::findings::parse_suppression(1, "// qual:api").is_none());
}
#[test]
fn test_function_pointer_ref_recognized_as_call() {
let code = r#"
fn format_item(s: &str) -> String { s.to_string() }
fn process(items: &[&str], transform: &dyn Fn(&str) -> String) {
items.iter().for_each(|i| { transform(i); });
}
fn run() {
process(&["a"], &format_item);
}
"#;
let parsed = parse(code);
let cfg_test_files = collect_cfg_test_file_paths(&parsed);
let (prod_calls, _test_calls) = collect_all_calls(&parsed, &cfg_test_files);
assert!(
prod_calls.contains("format_item"),
"&format_item passed as argument should be recognized as a call"
);
}
#[test]
fn test_struct_field_function_pointer_recognized_as_call() {
let code = r#"
struct Config { handler: fn() -> i32 }
fn my_handler() -> i32 { 42 }
fn setup() -> Config {
Config { handler: my_handler }
}
"#;
let parsed = parse(code);
let cfg_test_files = collect_cfg_test_file_paths(&parsed);
let (prod_calls, _test_calls) = collect_all_calls(&parsed, &cfg_test_files);
assert!(
prod_calls.contains("my_handler"),
"Bare function name in struct field should be recognized as a call"
);
}
#[test]
fn test_helper_marker_suppresses_testonly_dead_code() {
let code = r#"
// qual:test_helper
pub fn shared_asserter(x: i32) {
let _ = x + 1;
}
#[cfg(test)]
mod tests {
use super::shared_asserter;
#[test]
fn t1() { shared_asserter(1); }
}
"#;
let parsed = parse(code);
let config = Config::default();
let mut test_helper_lines = std::collections::HashMap::new();
test_helper_lines.insert(
"test.rs".to_string(),
[2usize]
.into_iter()
.collect::<std::collections::HashSet<_>>(),
);
let cfg_test_files = collect_cfg_test_file_paths(&parsed);
let warnings = detect_dead_code(
&parsed,
&config,
&std::collections::HashMap::new(),
&test_helper_lines,
&cfg_test_files,
);
let names: Vec<&str> = warnings.iter().map(|w| w.function_name.as_str()).collect();
assert!(
!names.contains(&"shared_asserter"),
"test-helper-marked function should be excluded from dead code, got {names:?}"
);
}
#[test]
fn test_helper_marker_does_not_suppress_uncalled() {
let code = r#"
// qual:test_helper
pub fn marked_but_not_called() { let _ = 1; }
// spacer to move unmarked out of annotation window
// another spacer line
fn unmarked_and_uncalled() { let _ = 2; }
"#;
let parsed = parse(code);
let config = Config::default();
let mut test_helper_lines = std::collections::HashMap::new();
test_helper_lines.insert(
"test.rs".to_string(),
[2usize]
.into_iter()
.collect::<std::collections::HashSet<_>>(),
);
let warnings = detect_dead_code(
&parsed,
&config,
&std::collections::HashMap::new(),
&test_helper_lines,
&std::collections::HashSet::new(),
);
let uncalled: Vec<&str> = warnings
.iter()
.filter(|w| w.kind == DeadCodeKind::Uncalled)
.map(|w| w.function_name.as_str())
.collect();
assert!(
uncalled.contains(&"marked_but_not_called"),
"test_helper on a function with no callers must still flag as Uncalled, got {uncalled:?}"
);
assert!(
uncalled.contains(&"unmarked_and_uncalled"),
"unmarked uncalled function must still flag, got {uncalled:?}"
);
}
#[test]
fn helper_reached_via_trait_blanket_dispatch_is_not_dead_code() {
let ports_reporter = (
"src/ports/reporter.rs".to_string(),
String::new(),
syn::parse_file(
r#"
pub trait ReporterImpl {
type Output;
fn publish(&self) -> Self::Output;
}
pub trait Reporter {
type Output;
fn render(&self) -> Self::Output;
}
impl<T: ReporterImpl> Reporter for T {
type Output = T::Output;
fn render(&self) -> Self::Output { self.publish() }
}
"#,
)
.unwrap(),
);
let sarif_rules = (
"src/adapters/report/sarif/rules.rs".to_string(),
String::new(),
syn::parse_file(
r#"
pub(super) fn sarif_rules() -> Vec<String> {
vec![String::from("rule")]
}
"#,
)
.unwrap(),
);
let sarif_mod = (
"src/adapters/report/sarif/mod.rs".to_string(),
String::new(),
syn::parse_file(
r#"
use super::rules::sarif_rules;
pub struct SarifReporter;
impl ReporterImpl for SarifReporter {
type Output = String;
fn publish(&self) -> Self::Output {
let _r = sarif_rules();
String::new()
}
}
pub fn build_sarif_string() -> String {
let r = SarifReporter;
r.render()
}
#[cfg(test)]
mod tests {
use super::build_sarif_string;
#[test]
fn it() { let _ = build_sarif_string(); }
}
"#,
)
.unwrap(),
);
let parsed = vec![ports_reporter, sarif_rules, sarif_mod];
let config = Config::default();
let warnings = detect_dead_code(
&parsed,
&config,
&std::collections::HashMap::new(),
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
);
assert!(
!warnings.iter().any(|w| w.function_name == "sarif_rules"),
"sarif_rules is reached via trait-blanket dispatch and must not be flagged dead-code, got: {:?}",
warnings.iter().map(|w| (&w.function_name, &w.kind)).collect::<Vec<_>>()
);
}