use std::path::PathBuf;
use ra_ap_ide::FilePosition;
use crate::deep::{CargoFeatures, DeepContext, DeepError};
use crate::finding::{EvidenceClass, Finding, Location, OneBasedLine, Origin, Severity};
use crate::functions::walk_functions;
use crate::ingest::Workspace;
use crate::reachability::{self, ReachabilityError};
use crate::rules::dead_code::{DeadCodeError, reachability_error, walk_type_items};
pub const FEATURE_GATED_DEAD_CODE_RULE: &str = "feature-gated-dead-code";
pub const FEATURE_GATED_DEAD_CODE_RULE_REVISION: u32 = 1;
const FEATURE_GATED_DEAD_CODE_REASON: &str = "not reachable from any recognized entry point \
under any of the configured [feature_matrix] combinations, in the examined reachability view";
#[derive(Debug, Default)]
pub struct FeatureMatrixReport {
pub findings: Vec<Finding>,
pub errors: Vec<DeadCodeError>,
pub checked: usize,
}
struct Candidate {
file_path: PathBuf,
qualified_name: String,
offset: u32,
line: usize,
}
fn collect_candidates(workspace: &Workspace) -> (Vec<Candidate>, Vec<DeadCodeError>) {
let mut candidates = Vec::new();
let mut errors = Vec::new();
crate::rules::dead_code::for_each_parsed_file(workspace, &mut errors, |file, ast| {
walk_functions(ast, |site| {
candidates.push(Candidate {
file_path: file.path.clone(),
qualified_name: site.qualified_name,
offset: site.ident_span.byte_range().start as u32,
line: site.ident_span.start().line,
});
});
walk_type_items(ast, |site| {
candidates.push(Candidate {
file_path: file.path.clone(),
qualified_name: site.qualified_name,
offset: site.ident_span.byte_range().start as u32,
line: site.ident_span.start().line,
});
});
});
(candidates, errors)
}
fn finding_for(candidate: &Candidate, combinations: &[Vec<String>]) -> Finding {
let evidence = serde_json::json!({
"tier": "deep",
"file": candidate.file_path,
"item_path": candidate.qualified_name,
"combinations_checked": combinations,
"line": candidate.line,
"reason": FEATURE_GATED_DEAD_CODE_REASON,
});
Finding::new(
format!(
"{FEATURE_GATED_DEAD_CODE_RULE}:{}:{}",
candidate.file_path.display(),
candidate.qualified_name
),
FEATURE_GATED_DEAD_CODE_RULE,
Severity::Warn,
Location {
file: candidate.file_path.clone(),
line: OneBasedLine::new(candidate.line).expect("source line numbers are 1-based"),
item_path: candidate.qualified_name.clone(),
},
EvidenceClass::Heuristic,
Origin::Code,
Some(evidence),
)
}
pub fn analyze_workspace(
workspace: &Workspace,
combinations: &[Vec<String>],
include_tests: bool,
) -> Result<FeatureMatrixReport, DeadCodeError> {
let mut report = FeatureMatrixReport::default();
if combinations.is_empty() {
return Ok(report);
}
let (candidates, collect_errors) = collect_candidates(workspace);
report.errors.extend(collect_errors);
let mut reachable_anywhere = vec![false; candidates.len()];
for combination in combinations {
let features = CargoFeatures::Selected {
features: combination.clone(),
no_default_features: true,
};
let ctx = match DeepContext::load_with_features(&workspace.root, features) {
Ok(ctx) => ctx,
Err(err) => {
report.errors.push(DeadCodeError::Deep(err));
continue;
}
};
let analysis = ctx.analysis();
let entries = match reachability::entry_point_positions(workspace, &ctx, include_tests) {
Ok(entries) => entries,
Err(err) => {
report.errors.push(reachability_error(err));
continue;
}
};
let entry_keys = reachability::entry_keys_from(&entries);
for (index, candidate) in candidates.iter().enumerate() {
report.checked += 1;
let file_id = match ctx.file_id(&candidate.file_path) {
Ok(Some(file_id)) => file_id,
Ok(None) => continue,
Err(err) => {
report.errors.push(DeadCodeError::Deep(err));
continue;
}
};
let position = FilePosition {
file_id,
offset: candidate.offset.into(),
};
match reachability::is_reachable_from_entry(
&analysis,
&entry_keys,
position,
include_tests,
) {
Ok(true) => reachable_anywhere[index] = true,
Ok(false) => {}
Err(ReachabilityError::Deep(DeepError::UnresolvedSymbol(_))) => {}
Err(err) => report.errors.push(reachability_error(err)),
}
}
}
for (index, candidate) in candidates.iter().enumerate() {
if !reachable_anywhere[index] {
report.findings.push(finding_for(candidate, combinations));
}
}
Ok(report)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_util::TempDir;
fn load_single_crate_workspace(dir: &TempDir, lib_source: &str) -> Workspace {
std::fs::write(
dir.join("Cargo.toml"),
r#"
[package]
name = "feature-matrix-fixture"
version = "0.1.0"
edition = "2021"
[features]
default = []
x = []
"#,
)
.unwrap();
std::fs::create_dir_all(dir.join("src")).unwrap();
std::fs::write(dir.join("src/lib.rs"), lib_source).unwrap();
crate::ingest::load(Some(&dir.join("Cargo.toml"))).unwrap()
}
#[test]
fn empty_combinations_skips_analysis_entirely() {
let dir = TempDir::new("feature-matrix-no-config");
let workspace = load_single_crate_workspace(
&dir,
r#"#[cfg(feature = "x")]
pub fn only_under_x() -> i32 {
1
}
"#,
);
let report = analyze_workspace(&workspace, &[], true).unwrap();
assert!(report.findings.is_empty());
assert!(report.errors.is_empty());
assert_eq!(report.checked, 0);
}
#[test]
fn item_gated_behind_an_unconfigured_feature_fires() {
let dir = TempDir::new("feature-matrix-unconfigured-feature-fires");
let workspace = load_single_crate_workspace(
&dir,
r#"#[cfg(feature = "x")]
pub fn only_under_x() -> i32 {
1
}
"#,
);
let combinations = vec![vec![], vec!["default".to_string()]];
let report = analyze_workspace(&workspace, &combinations, true).unwrap();
assert_eq!(
report
.findings
.iter()
.filter(|f| f.rule == FEATURE_GATED_DEAD_CODE_RULE
&& f.location.item_path == "only_under_x")
.count(),
1,
"expected exactly one feature-gated-dead-code finding for only_under_x: {:?}",
report.findings
);
}
#[test]
fn item_reachable_under_a_configured_combination_does_not_fire() {
let dir = TempDir::new("feature-matrix-configured-and-reachable");
std::fs::write(
dir.join("Cargo.toml"),
r#"
[package]
name = "feature-matrix-fixture"
version = "0.1.0"
edition = "2021"
[features]
default = []
x = []
[[bin]]
name = "tool"
path = "src/bin/tool.rs"
"#,
)
.unwrap();
std::fs::create_dir_all(dir.join("src/bin")).unwrap();
std::fs::write(
dir.join("src/lib.rs"),
r#"#[cfg(feature = "x")]
pub fn only_under_x() -> i32 {
1
}
"#,
)
.unwrap();
std::fs::write(
dir.join("src/bin/tool.rs"),
r#"#[cfg(feature = "x")]
fn main() {
feature_matrix_fixture::only_under_x();
}
#[cfg(not(feature = "x"))]
fn main() {}
"#,
)
.unwrap();
let workspace = crate::ingest::load(Some(&dir.join("Cargo.toml"))).unwrap();
let combinations = vec![vec!["x".to_string()]];
let report = analyze_workspace(&workspace, &combinations, true).unwrap();
assert!(
report
.findings
.iter()
.all(|f| f.location.item_path != "only_under_x"),
"only_under_x is reachable under the configured `x` combination and must not fire: \
{:?}",
report.findings
);
}
#[test]
fn ungated_reachable_item_does_not_fire() {
let dir = TempDir::new("feature-matrix-ungated-reachable");
std::fs::write(
dir.join("Cargo.toml"),
r#"
[package]
name = "feature-matrix-fixture"
version = "0.1.0"
edition = "2021"
[features]
default = []
x = []
[[bin]]
name = "tool"
path = "src/bin/tool.rs"
"#,
)
.unwrap();
std::fs::create_dir_all(dir.join("src/bin")).unwrap();
std::fs::write(
dir.join("src/lib.rs"),
"pub fn always_here() -> i32 {\n 1\n}\n",
)
.unwrap();
std::fs::write(
dir.join("src/bin/tool.rs"),
"fn main() {\n feature_matrix_fixture::always_here();\n}\n",
)
.unwrap();
let workspace = crate::ingest::load(Some(&dir.join("Cargo.toml"))).unwrap();
let combinations = vec![vec![], vec!["x".to_string()]];
let report = analyze_workspace(&workspace, &combinations, true).unwrap();
assert!(
report
.findings
.iter()
.all(|f| f.location.item_path != "always_here"),
"always_here is reachable under every combination and must not fire: {:?}",
report.findings
);
}
}