use std::collections::HashSet;
use super::conventions::AuditFinding;
use super::findings::{Finding, Severity};
use super::fingerprint::FileFingerprint;
pub(crate) fn analyze_dead_code(fingerprints: &[&FileFingerprint]) -> Vec<Finding> {
let mut findings = Vec::new();
let mut all_calls: HashSet<String> = HashSet::new();
let mut all_imports: HashSet<String> = HashSet::new();
for fp in fingerprints {
for call in &fp.internal_calls {
all_calls.insert(call.clone());
}
for import in &fp.imports {
all_imports.insert(import.clone());
}
}
for fp in fingerprints {
for unused in &fp.unused_parameters {
findings.push(Finding {
convention: "dead_code".to_string(),
severity: Severity::Warning,
file: fp.relative_path.clone(),
description: format!(
"Unused parameter '{}' in function '{}'",
unused.param, unused.function
),
suggestion:
"Remove the parameter or prefix with underscore to indicate intentional disuse"
.to_string(),
kind: AuditFinding::UnusedParameter,
});
}
for marker in &fp.dead_code_markers {
findings.push(Finding {
convention: "dead_code".to_string(),
severity: Severity::Info,
file: fp.relative_path.clone(),
description: format!(
"Dead code marker on '{}' (line {}, type: {})",
marker.item, marker.line, marker.marker_type
),
suggestion:
"Remove the dead code instead of suppressing the warning, or document why it must stay"
.to_string(),
kind: AuditFinding::DeadCodeMarker,
});
}
for export in &fp.public_api {
let referenced_elsewhere = fingerprints.iter().any(|other| {
if other.relative_path == fp.relative_path {
return false;
}
if other.internal_calls.contains(export) {
return true;
}
let type_name = fp.type_name.as_deref().unwrap_or("");
other.imports.iter().any(|imp| {
imp.contains(export.as_str())
|| (!type_name.is_empty() && imp.contains(type_name))
})
});
if !referenced_elsewhere {
if is_framework_entry_point(export, fp) {
continue;
}
findings.push(Finding {
convention: "dead_code".to_string(),
severity: Severity::Info,
file: fp.relative_path.clone(),
description: format!(
"Public function '{}' is not referenced by any other file",
export
),
suggestion:
"Consider making it private/pub(crate), removing it, or verifying it's used externally"
.to_string(),
kind: AuditFinding::UnreferencedExport,
});
}
}
let private_methods: Vec<&String> = fp
.methods
.iter()
.filter(|m| {
fp.visibility
.get(*m)
.map(|v| v == "private")
.unwrap_or(false)
})
.collect();
for method in private_methods {
if !fp.internal_calls.contains(method) {
findings.push(Finding {
convention: "dead_code".to_string(),
severity: Severity::Warning,
file: fp.relative_path.clone(),
description: format!(
"Private function '{}' is never called within this file",
method
),
suggestion: "Remove the dead function or make it public if used externally"
.to_string(),
kind: AuditFinding::OrphanedInternal,
});
}
}
}
findings.sort_by(|a, b| a.file.cmp(&b.file).then(a.description.cmp(&b.description)));
findings
}
fn is_framework_entry_point(name: &str, fp: &FileFingerprint) -> bool {
let universal_entry_points = [
"main", "new", "default", "from", "try_from", "into", "drop", "clone", "fmt", "display",
"eq", "hash",
];
if universal_entry_points.contains(&name) {
return true;
}
if matches!(fp.language, super::conventions::Language::Rust) {
let rust_trait_methods = [
"serialize",
"deserialize",
"from_str",
"as_ref",
"deref",
"index",
"add",
"sub",
"mul",
"div",
"neg",
"not",
"build",
"run",
"execute",
"augment_args",
"augment_subcommands",
"from_arg_matches",
"update_from_arg_matches",
"command",
"value_variants",
];
if rust_trait_methods.contains(&name) {
return true;
}
}
if matches!(fp.language, super::conventions::Language::Php) {
let php_entry_points = [
"__construct",
"__destruct",
"__get",
"__set",
"__call",
"__callStatic",
"__toString",
"__invoke",
"__clone",
"__sleep",
"__wakeup",
"register",
"init",
"activate",
"deactivate",
"boot",
"setup",
"render",
"handle",
"process",
];
if php_entry_points.contains(&name) {
return true;
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use crate::code_audit::conventions::Language;
use crate::extension::{DeadCodeMarker, UnusedParam};
use std::collections::HashMap;
fn make_fingerprint(
path: &str,
methods: Vec<&str>,
public_api: Vec<&str>,
internal_calls: Vec<&str>,
visibility: Vec<(&str, &str)>,
) -> FileFingerprint {
let vis_map: HashMap<String, String> = visibility
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
FileFingerprint {
relative_path: path.to_string(),
language: Language::Rust,
methods: methods.into_iter().map(String::from).collect(),
visibility: vis_map,
internal_calls: internal_calls.into_iter().map(String::from).collect(),
public_api: public_api.into_iter().map(String::from).collect(),
..Default::default()
}
}
#[test]
fn unused_parameter_produces_warning() {
let mut fp = make_fingerprint("src/foo.rs", vec!["process"], vec![], vec![], vec![]);
fp.unused_parameters.push(UnusedParam {
function: "process".to_string(),
param: "ctx".to_string(),
});
let findings = analyze_dead_code(&[&fp]);
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].kind, AuditFinding::UnusedParameter);
assert!(findings[0].description.contains("ctx"));
assert!(findings[0].description.contains("process"));
}
#[test]
fn dead_code_marker_produces_info() {
let mut fp = make_fingerprint("src/foo.rs", vec!["old_func"], vec![], vec![], vec![]);
fp.dead_code_markers.push(DeadCodeMarker {
item: "old_func".to_string(),
line: 42,
marker_type: "allow_dead_code".to_string(),
});
let findings = analyze_dead_code(&[&fp]);
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].kind, AuditFinding::DeadCodeMarker);
assert_eq!(findings[0].severity, Severity::Info);
}
#[test]
fn unreferenced_export_detected() {
let fp1 = make_fingerprint(
"src/foo.rs",
vec!["compute"],
vec!["compute"],
vec![],
vec![],
);
let fp2 = make_fingerprint(
"src/bar.rs",
vec!["transform"],
vec!["transform"],
vec![],
vec![],
);
let findings = analyze_dead_code(&[&fp1, &fp2]);
let unreferenced: Vec<&Finding> = findings
.iter()
.filter(|f| f.kind == AuditFinding::UnreferencedExport)
.collect();
assert_eq!(unreferenced.len(), 2); }
#[test]
fn referenced_export_not_flagged() {
let fp1 = make_fingerprint(
"src/foo.rs",
vec!["compute"],
vec!["compute"],
vec![],
vec![],
);
let fp2 = make_fingerprint(
"src/bar.rs",
vec!["transform"],
vec!["transform"],
vec!["compute"], vec![],
);
let findings = analyze_dead_code(&[&fp1, &fp2]);
let unreferenced: Vec<&Finding> = findings
.iter()
.filter(|f| f.kind == AuditFinding::UnreferencedExport)
.collect();
assert_eq!(unreferenced.len(), 1);
assert!(unreferenced[0].description.contains("transform"));
}
#[test]
fn orphaned_private_function_detected() {
let fp = make_fingerprint(
"src/foo.rs",
vec!["public_fn", "dead_helper"],
vec!["public_fn"],
vec!["public_fn"], vec![("dead_helper", "private")],
);
let findings = analyze_dead_code(&[&fp]);
let orphaned: Vec<&Finding> = findings
.iter()
.filter(|f| f.kind == AuditFinding::OrphanedInternal)
.collect();
assert_eq!(orphaned.len(), 1);
assert!(orphaned[0].description.contains("dead_helper"));
}
#[test]
fn called_private_function_not_flagged() {
let fp = make_fingerprint(
"src/foo.rs",
vec!["public_fn", "helper"],
vec!["public_fn"],
vec!["helper"], vec![("helper", "private")],
);
let findings = analyze_dead_code(&[&fp]);
let orphaned: Vec<&Finding> = findings
.iter()
.filter(|f| f.kind == AuditFinding::OrphanedInternal)
.collect();
assert!(orphaned.is_empty());
}
#[test]
fn framework_entry_points_not_flagged() {
let fp = make_fingerprint(
"src/foo.rs",
vec!["main", "new", "default"],
vec!["main", "new", "default"],
vec![],
vec![],
);
let findings = analyze_dead_code(&[&fp]);
let unreferenced: Vec<&Finding> = findings
.iter()
.filter(|f| f.kind == AuditFinding::UnreferencedExport)
.collect();
assert!(
unreferenced.is_empty(),
"Framework entry points should not be flagged"
);
}
}