use crate::config;
use crate::core::FunctionMetrics;
use crate::priority::FunctionVisibility;
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::sync::OnceLock;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ExternalApiConfig {
#[serde(default = "default_true")]
pub detect_external_api: bool,
#[serde(default)]
pub api_functions: Vec<String>,
#[serde(default)]
pub api_files: Vec<String>,
}
fn default_true() -> bool {
true
}
static CONFIG: OnceLock<ExternalApiConfig> = OnceLock::new();
fn get_config() -> &'static ExternalApiConfig {
CONFIG.get_or_init(|| {
config::get_config()
.external_api
.clone()
.unwrap_or_default()
})
}
fn is_explicitly_marked_api(func: &FunctionMetrics, config: &ExternalApiConfig) -> bool {
let func_matches = config.api_functions.iter().any(|api_func| {
api_func == &func.name || api_func.ends_with(&format!("::{}", func.name))
});
if func_matches {
return true;
}
let file_path_str = func.file.to_string_lossy();
config.api_files.iter().any(|api_file| {
if api_file.contains('*') {
let pattern = api_file.replace("**", ".*").replace('*', "[^/]*");
match regex::Regex::new(&pattern) {
Ok(re) => re.is_match(&file_path_str),
Err(e) => {
log::warn!(
"Invalid regex pattern for API file glob '{}': {}",
api_file,
e
);
false
}
}
} else {
file_path_str.ends_with(api_file) || &file_path_str == api_file
}
})
}
pub fn is_likely_external_api(
func: &FunctionMetrics,
visibility: &FunctionVisibility,
) -> (bool, Vec<String>) {
let config = get_config();
is_likely_external_api_with_config(func, visibility, config)
}
#[doc(hidden)]
pub fn is_likely_external_api_with_config(
func: &FunctionMetrics,
visibility: &FunctionVisibility,
config: &ExternalApiConfig,
) -> (bool, Vec<String>) {
let mut indicators = Vec::new();
let mut confidence_score = 0;
if !matches!(visibility, FunctionVisibility::Public) {
return (false, vec![]);
}
if is_explicitly_marked_api(func, config) {
return (
true,
vec!["Explicitly marked as external API in .debtmap.toml".to_string()],
);
}
if !config.detect_external_api {
return (
false,
vec!["Automatic external API detection disabled in .debtmap.toml".to_string()],
);
}
let (boundary_score, boundary_indicator) = classify_module_boundary(&func.file);
confidence_score += boundary_score;
if let Some(indicator) = boundary_indicator {
indicators.push(indicator);
}
if is_in_public_module(&func.file) {
confidence_score += 2;
indicators.push("In public module hierarchy".to_string());
}
let (pattern_score, pattern_indicators) = classify_api_patterns(&func.name);
confidence_score += pattern_score;
indicators.extend(pattern_indicators);
let (depth_score, depth_indicator) = classify_path_depth(&func.file);
confidence_score += depth_score;
if let Some(indicator) = depth_indicator {
indicators.push(indicator);
}
let is_likely_api = confidence_score >= 4;
(is_likely_api, indicators)
}
fn classify_module_boundary(path: &Path) -> (i32, Option<String>) {
if let Some(file_name) = path.file_name() {
let name = file_name.to_string_lossy();
match name.as_ref() {
"lib.rs" => (3, Some("Defined in lib.rs (library root)".to_string())),
"mod.rs" => (2, Some("Defined in mod.rs (module root)".to_string())),
_ => (0, None),
}
} else {
(0, None)
}
}
fn classify_api_patterns(name: &str) -> (i32, Vec<String>) {
let mut score = 0;
let mut indicators = Vec::new();
if has_api_pattern_in_name(name) {
score += 2;
indicators.push("API pattern".to_string());
}
if is_builder_or_factory(name) {
score += 2;
indicators.push("Builder".to_string());
}
if is_trait_method_pattern(name) {
score += 2;
indicators.push("Common trait method pattern".to_string());
}
if is_constructor_pattern(name) {
score += 3;
indicators.push("Constructor".to_string());
}
if has_public_api_prefix(name) {
score += 2;
indicators.push("Public API prefix".to_string());
}
(score, indicators)
}
fn is_trait_method_pattern(name: &str) -> bool {
name.starts_with("from_") || name == "new" || name == "default"
}
fn classify_path_depth(path: &Path) -> (i32, Option<String>) {
let path_depth = path.components().count();
match path_depth {
0..=3 => (1, Some("Shallow module path (likely public)".to_string())),
4..=5 => (0, None),
_ => (-1, Some("Deep module path (likely internal)".to_string())),
}
}
fn is_in_public_module(path: &Path) -> bool {
let path_str = path.to_string_lossy();
!path_str.contains("/internal/")
&& !path_str.contains("/private/")
&& !path_str.contains("/impl/")
&& !path_str.contains("/detail/")
&& !path_str.contains("/tests/")
&& !path_str.contains("/benches/")
&& !path_str.contains("/examples/")
}
fn has_api_pattern_in_name(name: &str) -> bool {
name.starts_with("get_")
|| name.starts_with("set_")
|| name.starts_with("with_")
|| name.starts_with("try_")
|| name.starts_with("is_")
|| name.starts_with("has_")
|| name.starts_with("create_")
|| name.starts_with("parse_")
|| name.starts_with("to_")
|| name.starts_with("into_")
|| name.starts_with("as_")
}
fn is_builder_or_factory(name: &str) -> bool {
name == "build"
|| name == "builder"
|| name.ends_with("_builder")
|| name.starts_with("create_")
|| name.starts_with("make_")
|| name.ends_with("_factory")
}
fn is_constructor_pattern(name: &str) -> bool {
name == "new"
|| name == "default"
|| name.starts_with("new_")
|| name.starts_with("from_")
|| name == "init"
|| name.starts_with("init_")
}
fn has_public_api_prefix(name: &str) -> bool {
name.starts_with("public_") || name.starts_with("api_") || name.starts_with("export_")
}
fn classify_complexity_hints(func: &FunctionMetrics) -> Option<String> {
match (func.cyclomatic, func.cognitive) {
(c, g) if c <= 3 && g <= 5 => Some("Low complexity - low impact removal".to_string()),
(c, g) if c > 10 || g > 15 => {
Some("High complexity - removing may eliminate significant unused code".to_string())
}
_ => None,
}
}
fn detect_test_helper_hints(func_name: &str) -> Option<String> {
let is_test_helper = (func_name.contains("test") && func_name.contains("helper"))
|| func_name.contains("mock")
|| func_name.contains("fixture")
|| func_name.contains("helper")
|| func_name.contains("util");
if is_test_helper {
Some("Potential test helper - consider moving to test module".to_string())
} else {
None
}
}
pub fn generate_enhanced_dead_code_hints(
func: &FunctionMetrics,
visibility: &FunctionVisibility,
) -> Vec<String> {
let config = get_config();
generate_enhanced_dead_code_hints_with_config(func, visibility, config)
}
#[doc(hidden)]
pub fn generate_enhanced_dead_code_hints_with_config(
func: &FunctionMetrics,
visibility: &FunctionVisibility,
config: &ExternalApiConfig,
) -> Vec<String> {
let mut hints = Vec::new();
let (is_likely_api, api_indicators) =
is_likely_external_api_with_config(func, visibility, config);
if is_likely_api {
hints.push("Likely external API - verify before removing".to_string());
hints.extend(api_indicators);
} else if matches!(visibility, FunctionVisibility::Public) {
hints.push("Public but no external API indicators found".to_string());
}
if let Some(hint) = classify_complexity_hints(func) {
hints.push(hint);
}
if let Some(hint) = detect_test_helper_hints(&func.name) {
hints.push(hint);
}
hints
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn create_test_function(name: &str, path: &str) -> FunctionMetrics {
FunctionMetrics {
name: name.to_string(),
file: PathBuf::from(path),
line: 1,
cyclomatic: 5,
cognitive: 8,
nesting: 2,
length: 20,
is_test: false,
visibility: Some("pub".to_string()),
is_trait_method: false,
in_test_module: false,
entropy_score: None,
is_pure: None,
purity_confidence: None,
purity_reason: None,
call_dependencies: None,
detected_patterns: None,
upstream_callers: None,
downstream_callees: None,
mapping_pattern_result: None,
adjusted_complexity: None,
composition_metrics: None,
language_specific: None,
purity_level: None,
error_swallowing_count: None,
error_swallowing_patterns: None,
entropy_analysis: None,
}
}
fn test_config() -> ExternalApiConfig {
ExternalApiConfig {
detect_external_api: true,
api_functions: vec![],
api_files: vec![],
}
}
#[test]
fn test_lib_rs_detection() {
let func = create_test_function("process_data", "src/lib.rs");
let visibility = FunctionVisibility::Public;
let config = test_config();
let (is_api, indicators) = is_likely_external_api_with_config(&func, &visibility, &config);
assert!(is_api);
assert!(indicators.iter().any(|i| i.contains("lib.rs")));
}
#[test]
fn test_constructor_pattern() {
let func = create_test_function("new", "src/data/processor.rs");
let visibility = FunctionVisibility::Public;
let config = test_config();
let (is_api, indicators) = is_likely_external_api_with_config(&func, &visibility, &config);
assert!(is_api);
assert!(indicators.iter().any(|i| i.contains("Constructor")));
}
#[test]
fn test_deep_internal_module() {
let func = create_test_function("helper", "src/internal/impl/detail/utils/helpers.rs");
let visibility = FunctionVisibility::Public;
let config = test_config();
let (is_api, _indicators) = is_likely_external_api_with_config(&func, &visibility, &config);
assert!(!is_api);
}
#[test]
fn test_api_pattern_names() {
let func = create_test_function("get_configuration", "src/config.rs");
let visibility = FunctionVisibility::Public;
let config = test_config();
let (_is_api, indicators) = is_likely_external_api_with_config(&func, &visibility, &config);
assert!(indicators.iter().any(|i| i.contains("API pattern")));
}
#[test]
fn test_private_function() {
let func = create_test_function("internal_helper", "src/utils.rs");
let visibility = FunctionVisibility::Private;
let config = test_config();
let (is_api, indicators) = is_likely_external_api_with_config(&func, &visibility, &config);
assert!(!is_api);
assert!(indicators.is_empty());
}
#[test]
fn test_explicit_api_marking() {
let config = ExternalApiConfig {
detect_external_api: false,
api_functions: vec!["parse".to_string(), "lib::connect".to_string()],
api_files: vec!["src/api.rs".to_string(), "src/public/*.rs".to_string()],
};
let func1 = create_test_function("parse", "src/internal/parser.rs");
assert!(is_explicitly_marked_api(&func1, &config));
let func2 = create_test_function("connect", "src/lib.rs");
assert!(is_explicitly_marked_api(&func2, &config));
let func3 = create_test_function("anything", "src/api.rs");
assert!(is_explicitly_marked_api(&func3, &config));
let func4 = create_test_function("something", "src/public/client.rs");
assert!(is_explicitly_marked_api(&func4, &config));
let func5 = create_test_function("helper", "src/internal/utils.rs");
assert!(!is_explicitly_marked_api(&func5, &config));
}
#[test]
fn test_classify_complexity_hints_low() {
let mut func = create_test_function("simple_func", "src/lib.rs");
func.cyclomatic = 2;
func.cognitive = 3;
let hint = classify_complexity_hints(&func);
assert_eq!(
hint,
Some("Low complexity - low impact removal".to_string())
);
}
#[test]
fn test_classify_complexity_hints_high_cyclomatic() {
let mut func = create_test_function("complex_func", "src/lib.rs");
func.cyclomatic = 15;
func.cognitive = 10;
let hint = classify_complexity_hints(&func);
assert_eq!(
hint,
Some("High complexity - removing may eliminate significant unused code".to_string())
);
}
#[test]
fn test_classify_complexity_hints_high_cognitive() {
let mut func = create_test_function("cognitive_func", "src/lib.rs");
func.cyclomatic = 8;
func.cognitive = 20;
let hint = classify_complexity_hints(&func);
assert_eq!(
hint,
Some("High complexity - removing may eliminate significant unused code".to_string())
);
}
#[test]
fn test_classify_complexity_hints_medium() {
let mut func = create_test_function("medium_func", "src/lib.rs");
func.cyclomatic = 6;
func.cognitive = 10;
let hint = classify_complexity_hints(&func);
assert_eq!(hint, None);
}
#[test]
fn test_detect_test_helper_hints_test_helper() {
let hint = detect_test_helper_hints("test_helper_function");
assert_eq!(
hint,
Some("Potential test helper - consider moving to test module".to_string())
);
}
#[test]
fn test_detect_test_helper_hints_mock() {
let hint = detect_test_helper_hints("create_mock_data");
assert_eq!(
hint,
Some("Potential test helper - consider moving to test module".to_string())
);
}
#[test]
fn test_detect_test_helper_hints_fixture() {
let hint = detect_test_helper_hints("setup_fixture");
assert_eq!(
hint,
Some("Potential test helper - consider moving to test module".to_string())
);
}
#[test]
fn test_detect_test_helper_hints_general_helper() {
let hint = detect_test_helper_hints("helper_function");
assert_eq!(
hint,
Some("Potential test helper - consider moving to test module".to_string())
);
}
#[test]
fn test_detect_test_helper_hints_util() {
let hint = detect_test_helper_hints("util_process");
assert_eq!(
hint,
Some("Potential test helper - consider moving to test module".to_string())
);
}
#[test]
fn test_detect_test_helper_hints_regular_function() {
let hint = detect_test_helper_hints("process_data");
assert_eq!(hint, None);
}
#[test]
fn test_detect_test_helper_hints_test_without_helper() {
let hint = detect_test_helper_hints("test_something");
assert_eq!(hint, None);
}
}