use cargo_metadata::Package;
use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::Path;
use super::multi_target_metadata::MultiTargetMetadata;
fn scan_source_for_cfg_features(crate_dir: &Path) -> HashSet<String> {
let mut features = HashSet::new();
for dir_name in ["src", "tests", "benches", "examples"] {
let dir = crate_dir.join(dir_name);
if !dir.exists() {
continue;
}
if let Ok(entries) = glob::glob(&format!("{}/**/*.rs", dir.display())) {
for entry in entries.flatten() {
if let Ok(content) = fs::read_to_string(&entry) {
extract_cfg_features(&content, &mut features);
}
}
}
}
let build_script = crate_dir.join("build.rs");
if build_script.exists()
&& let Ok(content) = fs::read_to_string(build_script)
{
extract_cfg_features(&content, &mut features);
}
features
}
fn extract_cfg_features(content: &str, features: &mut HashSet<String>) {
let mut remaining = content;
while let Some(idx) = remaining.find("feature") {
remaining = &remaining[idx + 7..];
let trimmed = remaining.trim_start();
if !trimmed.starts_with('=') {
continue;
}
let after_eq = trimmed[1..].trim_start();
if !after_eq.starts_with('"') {
continue;
}
let after_quote = &after_eq[1..];
if let Some(end_quote) = after_quote.find('"') {
let feature_name = &after_quote[..end_quote];
if !feature_name.is_empty() && is_valid_feature_name(feature_name) {
features.insert(feature_name.to_string());
}
}
}
}
fn is_valid_feature_name(s: &str) -> bool {
!s.is_empty() && s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
}
#[derive(Debug, Clone)]
pub struct FeatureScanResult {
pub crate_name: String,
pub declared_features: HashSet<String>,
pub enabled_features: HashSet<String>,
pub dead_features: HashSet<String>,
pub optional_features: HashSet<String>,
}
pub struct FeatureScanner;
impl FeatureScanner {
pub fn analyze_crate(
pkg: &Package,
metadata: &MultiTargetMetadata,
referenced_features: &HashMap<String, HashSet<String>>,
) -> FeatureScanResult {
let crate_name = pkg.name.to_string();
let declared_features: HashSet<String> = pkg.features.keys().cloned().collect();
let enabled_across_targets = metadata.all_features(&crate_name);
let enabled_features: HashSet<String> = enabled_across_targets.values().flatten().cloned().collect();
let externally_referenced = referenced_features.get(&crate_name).cloned().unwrap_or_default();
let internally_referenced = Self::build_internally_referenced_features(pkg);
let crate_dir = pkg.manifest_path.parent().map(Path::new).unwrap_or(Path::new("."));
let source_referenced = scan_source_for_cfg_features(crate_dir);
let unused_features: HashSet<String> = declared_features
.difference(&enabled_features)
.filter(|f| *f != "default")
.filter(|f| !externally_referenced.contains(*f))
.filter(|f| !internally_referenced.contains(*f))
.filter(|f| !source_referenced.contains(*f))
.cloned()
.collect();
let mut dead_features = HashSet::new();
let mut optional_features = HashSet::new();
for feature in &unused_features {
if let Some(enables) = pkg.features.get(feature) {
if enables.is_empty() {
dead_features.insert(feature.clone());
} else {
optional_features.insert(feature.clone());
}
}
}
FeatureScanResult {
crate_name,
declared_features,
enabled_features,
dead_features,
optional_features,
}
}
pub fn analyze_workspace(metadata: &MultiTargetMetadata) -> Vec<FeatureScanResult> {
let referenced_features = Self::build_referenced_features_map(metadata);
let workspace_pkg_count = metadata.workspace_packages().len();
let mut results = Vec::with_capacity(workspace_pkg_count);
for pkg in metadata.workspace_packages() {
if pkg.features.is_empty() {
continue;
}
let result = Self::analyze_crate(pkg, metadata, &referenced_features);
if !result.dead_features.is_empty() || !result.optional_features.is_empty() {
results.push(result);
}
}
results
}
fn build_referenced_features_map(metadata: &MultiTargetMetadata) -> HashMap<String, HashSet<String>> {
let mut map: HashMap<String, HashSet<String>> = HashMap::new();
for pkg in metadata.workspace_packages() {
for feature_deps in pkg.features.values() {
for dep_str in feature_deps {
if let Some((dep_name, feature_name)) = Self::parse_feature_reference(dep_str) {
map.entry(dep_name).or_default().insert(feature_name);
}
}
}
}
map
}
fn build_internally_referenced_features(pkg: &Package) -> HashSet<String> {
let mut referenced = HashSet::new();
for feature_deps in pkg.features.values() {
for dep_str in feature_deps {
if dep_str.contains('/') || dep_str.starts_with("dep:") {
continue;
}
referenced.insert(dep_str.clone());
}
}
referenced
}
fn parse_feature_reference(s: &str) -> Option<(String, String)> {
if let Some(idx) = s.find('/') {
let dep_part = &s[..idx];
let feature = &s[idx + 1..];
let dep_name = dep_part.trim_end_matches('?');
if !feature.is_empty() {
return Some((dep_name.to_string(), feature.to_string()));
}
}
None
}
pub fn count_dead_features(results: &[FeatureScanResult]) -> usize {
results.iter().map(|r| r.dead_features.len()).sum()
}
pub fn count_optional_features(results: &[FeatureScanResult]) -> usize {
results.iter().map(|r| r.optional_features.len()).sum()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_feature_scan_result_dead_features() {
let mut declared = HashSet::new();
declared.insert("foo".to_string());
declared.insert("bar".to_string());
declared.insert("default".to_string());
let mut enabled = HashSet::new();
enabled.insert("foo".to_string());
let dead: HashSet<String> = declared
.difference(&enabled)
.filter(|f| *f != "default")
.cloned()
.collect();
assert!(dead.contains("bar"));
assert!(!dead.contains("default"));
assert!(!dead.contains("foo"));
assert_eq!(dead.len(), 1);
}
#[test]
fn test_parse_feature_reference() {
assert_eq!(
FeatureScanner::parse_feature_reference("tikv_alloc/mimalloc"),
Some(("tikv_alloc".to_string(), "mimalloc".to_string()))
);
assert_eq!(
FeatureScanner::parse_feature_reference("serde?/derive"),
Some(("serde".to_string(), "derive".to_string()))
);
assert_eq!(FeatureScanner::parse_feature_reference("std"), None);
assert_eq!(FeatureScanner::parse_feature_reference("dep:serde"), None);
assert_eq!(FeatureScanner::parse_feature_reference("dep/"), None);
}
#[test]
fn test_internally_referenced_filter() {
let feature_deps = vec![
vec!["api-key".to_string(), "server".to_string()],
vec!["build".to_string()],
vec!["dep/feature".to_string()],
vec!["dep:serde".to_string()],
];
let mut referenced = HashSet::new();
for deps in &feature_deps {
for dep_str in deps {
if dep_str.contains('/') || dep_str.starts_with("dep:") {
continue;
}
referenced.insert(dep_str.clone());
}
}
assert!(
referenced.contains("api-key"),
"api-key should be internally referenced"
);
assert!(referenced.contains("server"), "server should be internally referenced");
assert!(referenced.contains("build"), "build should be internally referenced");
assert!(!referenced.contains("dep/feature"), "dep/feature should not be in set");
assert!(!referenced.contains("dep:serde"), "dep:serde should not be in set");
}
#[test]
fn test_extract_cfg_features_basic_patterns() {
let mut features = HashSet::new();
extract_cfg_features(r#"#[cfg(feature = "foo")]"#, &mut features);
assert!(features.contains("foo"), "should detect cfg(feature = \"foo\")");
features.clear();
extract_cfg_features(r#"#[cfg(feature="bar")]"#, &mut features);
assert!(features.contains("bar"), "should detect feature=\"bar\"");
features.clear();
extract_cfg_features(r#"#[cfg(feature = "baz")]"#, &mut features);
assert!(features.contains("baz"), "should detect with extra spaces");
features.clear();
extract_cfg_features(r#"if cfg!(feature = "qux") {"#, &mut features);
assert!(features.contains("qux"), "should detect cfg! macro");
}
#[test]
fn test_extract_cfg_features_compound_patterns() {
let mut features = HashSet::new();
extract_cfg_features(r#"#[cfg(not(feature = "disabled"))]"#, &mut features);
assert!(features.contains("disabled"), "should detect not(feature)");
features.clear();
extract_cfg_features(r#"#[cfg(all(unix, feature = "unix-ext"))]"#, &mut features);
assert!(features.contains("unix-ext"), "should detect in all()");
features.clear();
extract_cfg_features(r#"#[cfg(any(feature = "a", feature = "b"))]"#, &mut features);
assert!(features.contains("a"), "should detect first in any()");
assert!(features.contains("b"), "should detect second in any()");
features.clear();
extract_cfg_features(r#"#[cfg_attr(feature = "serde", derive(Serialize))]"#, &mut features);
assert!(features.contains("serde"), "should detect in cfg_attr");
}
#[test]
fn test_extract_cfg_features_real_world() {
let mut features = HashSet::new();
let helix_code = r#"
#[cfg(feature = "api-key")]
match headers.get("x-api-key") {
Some(key) => validate_key(key),
None => return Err(AuthError),
}
#[cfg(not(feature = "api-key"))]
Ok(())
"#;
extract_cfg_features(helix_code, &mut features);
assert!(
features.contains("api-key"),
"should detect api-key from helix-db pattern"
);
features.clear();
let polars_code = r#"
#[cfg(feature = "gather")]
pub fn gather(&self, indices: &IdxCa) -> PolarsResult<Self> {
// ...
}
"#;
extract_cfg_features(polars_code, &mut features);
assert!(features.contains("gather"), "should detect gather from polars pattern");
features.clear();
let ruff_code = r#"
#[cfg(feature = "singlethreaded")]
fn run_single() { }
#[cfg(not(feature = "singlethreaded"))]
fn run_parallel() { }
"#;
extract_cfg_features(ruff_code, &mut features);
assert!(
features.contains("singlethreaded"),
"should detect singlethreaded from ruff pattern"
);
}
#[test]
fn test_extract_cfg_features_edge_cases() {
let mut features = HashSet::new();
extract_cfg_features(r#"#[cfg(feature = "my-feature")]"#, &mut features);
assert!(features.contains("my-feature"), "should handle hyphens");
features.clear();
extract_cfg_features(r#"#[cfg(feature = "my_feature")]"#, &mut features);
assert!(features.contains("my_feature"), "should handle underscores");
features.clear();
extract_cfg_features(r#"let feature = "not-a-cfg";"#, &mut features);
features.clear();
extract_cfg_features(r#"println!("feature = \"quoted\"");"#, &mut features);
features.clear();
extract_cfg_features(r#"#[cfg(feature = "")]"#, &mut features);
assert!(features.is_empty(), "should not match empty feature name");
}
#[test]
fn test_is_valid_feature_name() {
assert!(is_valid_feature_name("foo"));
assert!(is_valid_feature_name("foo-bar"));
assert!(is_valid_feature_name("foo_bar"));
assert!(is_valid_feature_name("foo123"));
assert!(is_valid_feature_name("FOO"));
assert!(!is_valid_feature_name(""));
assert!(!is_valid_feature_name("foo bar"));
assert!(!is_valid_feature_name("foo/bar"));
assert!(!is_valid_feature_name("foo:bar"));
}
#[test]
fn test_scan_source_for_cfg_features_includes_tests_dir() {
let temp = tempfile::TempDir::new().expect("tempdir");
let crate_dir = temp.path();
fs::create_dir_all(crate_dir.join("src")).expect("mkdir src");
fs::create_dir_all(crate_dir.join("tests")).expect("mkdir tests");
fs::write(crate_dir.join("src/lib.rs"), "pub fn ping() {}").expect("write lib.rs");
fs::write(
crate_dir.join("tests/integration.rs"),
r#"#[cfg(feature = "test-ollama")] fn run() {}"#,
)
.expect("write integration test");
let features = scan_source_for_cfg_features(crate_dir);
assert!(
features.contains("test-ollama"),
"should detect feature cfg used from tests/"
);
}
}