impl CargoDeadCodeAnalyzer {
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub async fn analyze(&self) -> Result<AccurateDeadCodeReport> {
if let Some(cached) = self.try_load_cache() {
return Ok(cached);
}
let deadline = std::time::Instant::now() + self.timeout;
let mut all_dead_items = self.scan_for_suppression_attributes()?;
self.check_deadline(deadline)?;
let cargo_output = self.run_cargo_check(deadline).await?;
let compiler_dead_items = self.parse_cargo_warnings(&cargo_output)?;
all_dead_items.extend(compiler_dead_items);
let files_with_dead_code = self.group_by_file(all_dead_items);
let report = self.calculate_metrics(files_with_dead_code).await?;
self.check_deadline(deadline)?;
self.save_cache(&report);
Ok(report)
}
fn check_deadline(&self, deadline: std::time::Instant) -> Result<()> {
if std::time::Instant::now() >= deadline {
return Err(self.timeout_error());
}
Ok(())
}
fn timeout_error(&self) -> anyhow::Error {
anyhow::anyhow!(
"Dead code analysis timed out after {} seconds",
self.timeout.as_secs()
)
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
fn scan_for_suppression_attributes(&self) -> Result<Vec<(PathBuf, DeadItem)>> {
use regex::Regex;
use std::fs;
let mut suppressed_items = Vec::new();
let suppression_re =
Regex::new(r#"#!?\[allow\((dead_code|unused)\)\]"#).expect("Invalid regex");
let item_re = Regex::new(
r#"^\s*(?:pub\s+)?(?:async\s+)?(?:const\s+)?(?:static\s+)?(?:unsafe\s+)?(fn|struct|enum|type|trait|mod|const|static)\s+(\w+)"#
).expect("Invalid regex");
for entry in ignore::WalkBuilder::new(&self.project_path)
.max_depth(Some(self.max_depth))
.hidden(true)
.git_ignore(true)
.git_global(true)
.build()
.filter_map(std::result::Result::ok)
{
let path = entry.path();
if path.starts_with(self.project_path.join("target")) {
continue;
}
if path.extension().and_then(|s| s.to_str()) != Some("rs") {
continue;
}
if self.is_excluded_source(path) {
continue;
}
let content = match fs::read_to_string(path) {
Ok(c) => c,
Err(_) => continue,
};
let lines: Vec<&str> = content.lines().collect();
self.scan_file_for_suppressions(
path,
&lines,
&suppression_re,
&item_re,
&mut suppressed_items,
);
}
tracing::debug!(
"Layer 1 (suppression scan): found {} items with ",
suppressed_items.len()
);
Ok(suppressed_items)
}
fn is_excluded_source(&self, path: &Path) -> bool {
let relative = path.strip_prefix(&self.project_path).unwrap_or(path);
let under = |dir: &str| {
relative
.components()
.any(|c| c.as_os_str().to_string_lossy() == dir)
};
if self.exclude_tests && (under("tests") || is_test_file_name(relative)) {
return true;
}
if self.exclude_examples && under("examples") {
return true;
}
if self.exclude_benches && (under("benches") || under("benchmarks")) {
return true;
}
false
}
fn scan_file_for_suppressions(
&self,
path: &Path,
lines: &[&str],
suppression_re: ®ex::Regex,
item_re: ®ex::Regex,
suppressed_items: &mut Vec<(PathBuf, DeadItem)>,
) {
for (i, line) in lines.iter().enumerate() {
if suppression_re.is_match(line) {
let mut item_line = i + 1;
while item_line < lines.len() {
let next_line = lines[item_line];
if next_line.trim().starts_with("#[")
|| next_line.trim().starts_with("#![")
|| next_line.trim().is_empty()
{
item_line += 1;
continue;
}
if let Some(caps) = item_re.captures(next_line) {
let kind_str = caps.get(1).map(|m| m.as_str()).unwrap_or("unknown");
let name = caps.get(2).map(|m| m.as_str()).unwrap_or("unknown");
let relative_path = path
.strip_prefix(&self.project_path)
.unwrap_or(path)
.to_path_buf();
suppressed_items.push((
relative_path,
DeadItem {
kind: suppressed_item_kind(kind_str),
name: name.to_string(),
line: item_line + 1, column: 1,
message: format!(
"{kind_str} `{name}` carries an allow(dead_code) \
suppression (explicit dead code admission)"
),
},
));
}
break;
}
}
}
}
async fn run_cargo_check(&self, deadline: std::time::Instant) -> Result<String> {
let Some(cmd) = self.build_cargo_check_command() else {
return Ok(r#"{"reason":"build-finished","success":true}"#.to_string());
};
self.wait_for_cargo_check(cmd, deadline).await
}
fn build_cargo_check_command(&self) -> Option<Command> {
if std::env::var("PMAT_DEAD_CODE_SKIP").is_ok() {
return None;
}
let mut cmd = Command::new("cargo");
cmd.current_dir(&self.project_path)
.arg("check")
.arg("--message-format=json");
let has_lib = project_has_library(&self.project_path);
if has_lib {
cmd.arg("--lib");
}
cmd.arg("--bins");
if !self.exclude_tests {
cmd.arg("--tests");
}
if !self.exclude_examples {
for name in named_targets(&self.project_path, "example") {
cmd.arg("--example").arg(name);
}
}
if !self.exclude_benches {
for name in named_targets(&self.project_path, "bench") {
cmd.arg("--bench").arg(name);
}
}
Some(cmd)
}
async fn wait_for_cargo_check(
&self,
mut cmd: Command,
deadline: std::time::Instant,
) -> Result<String> {
use std::io::Read;
use std::process::Stdio;
let mut child = cmd
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.context("Failed to run cargo check")?;
let mut stdout_pipe = child.stdout.take().expect("stdout was piped");
let mut stderr_pipe = child.stderr.take().expect("stderr was piped");
let stdout_reader = std::thread::spawn(move || {
let mut buf = Vec::new();
let _ = stdout_pipe.read_to_end(&mut buf);
buf
});
let stderr_reader = std::thread::spawn(move || {
let mut buf = Vec::new();
let _ = stderr_pipe.read_to_end(&mut buf);
buf
});
let status = loop {
match child.try_wait().context("Failed to run cargo check")? {
Some(status) => break status,
None => {
if std::time::Instant::now() >= deadline {
let _ = child.kill();
let _ = child.wait();
let _ = stdout_reader.join();
let _ = stderr_reader.join();
return Err(self.timeout_error());
}
tokio::time::sleep(std::time::Duration::from_millis(25)).await;
}
}
};
let stdout = stdout_reader.join().unwrap_or_default();
let stderr = stderr_reader.join().unwrap_or_default();
if !status.success() {
return Err(anyhow::anyhow!(
"Cargo check failed: {}",
String::from_utf8_lossy(&stderr)
));
}
Ok(String::from_utf8_lossy(&stdout).to_string())
}
}
fn suppressed_item_kind(kind_str: &str) -> DeadCodeKind {
match kind_str {
"fn" => DeadCodeKind::Function,
"struct" => DeadCodeKind::Struct,
"enum" => DeadCodeKind::Enum,
"type" => DeadCodeKind::TypeAlias,
"trait" => DeadCodeKind::Trait,
"mod" => DeadCodeKind::Module,
"const" => DeadCodeKind::Constant,
"static" => DeadCodeKind::Static,
other => DeadCodeKind::Other(other.to_string()),
}
}
fn is_test_file_name(path: &std::path::Path) -> bool {
let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
return false;
};
stem == "tests"
|| stem == "test"
|| stem.ends_with("_test")
|| stem.ends_with("_tests")
|| stem.starts_with("test_")
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
fn project_has_library(project_path: &std::path::Path) -> bool {
if project_path.join("src/lib.rs").exists() {
return true;
}
let cargo_toml = project_path.join("Cargo.toml");
if let Ok(content) = std::fs::read_to_string(&cargo_toml) {
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with("[lib]") || trimmed.starts_with("[lib.") {
return true;
}
}
}
false
}
fn named_targets(project_path: &std::path::Path, kind: &str) -> Vec<String> {
let Ok(output) = Command::new("cargo")
.current_dir(project_path)
.args(["metadata", "--no-deps", "--format-version", "1"])
.output()
else {
return Vec::new();
};
if !output.status.success() {
return Vec::new();
}
let Ok(meta) = serde_json::from_slice::<Value>(&output.stdout) else {
return Vec::new();
};
let manifest = project_path.join("Cargo.toml");
let mut names: Vec<String> = Vec::new();
for package in meta["packages"].as_array().into_iter().flatten() {
if package["manifest_path"].as_str().map(std::path::Path::new) != Some(manifest.as_path()) {
continue;
}
for target in package["targets"].as_array().into_iter().flatten() {
let is_kind = target["kind"]
.as_array()
.into_iter()
.flatten()
.any(|k| k.as_str() == Some(kind));
if !is_kind {
continue;
}
let needs_features = target["required-features"]
.as_array()
.is_some_and(|f| !f.is_empty());
if needs_features {
continue;
}
if let Some(name) = target["name"].as_str() {
names.push(name.to_string());
}
}
}
names.sort();
names
}
#[cfg(test)]
mod project_has_library_tests {
use super::project_has_library;
#[test]
fn test_detects_src_lib_rs() {
let tmp = tempfile::tempdir().unwrap();
std::fs::create_dir(tmp.path().join("src")).unwrap();
std::fs::write(tmp.path().join("src/lib.rs"), "pub fn x() {}").unwrap();
assert!(project_has_library(tmp.path()));
}
#[test]
fn test_detects_explicit_lib_section() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("Cargo.toml"),
"[package]\nname = \"x\"\nversion = \"0.1.0\"\n[lib]\nname = \"x\"\npath = \"src/x.rs\"\n",
)
.unwrap();
assert!(project_has_library(tmp.path()));
}
#[test]
fn test_bin_only_returns_false() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("Cargo.toml"),
"[package]\nname = \"bin-only\"\nversion = \"0.1.0\"\n[[bin]]\nname = \"bin-only\"\npath = \"src/main.rs\"\n",
)
.unwrap();
std::fs::create_dir(tmp.path().join("src")).unwrap();
std::fs::write(tmp.path().join("src/main.rs"), "fn main() {}").unwrap();
assert!(!project_has_library(tmp.path()));
}
#[test]
fn test_no_cargo_toml_returns_false() {
let tmp = tempfile::tempdir().unwrap();
assert!(!project_has_library(tmp.path()));
}
#[test]
fn test_lib_section_with_dotted_target_detected() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("Cargo.toml"),
"[package]\nname = \"x\"\nversion = \"0.1.0\"\n[lib.foo]\n",
)
.unwrap();
assert!(project_has_library(tmp.path()));
}
}
#[cfg(test)]
mod include_tests_flag_tests {
use super::*;
fn fixture() -> tempfile::TempDir {
let tmp = tempfile::Builder::new()
.prefix("dcx")
.tempdir()
.expect("tempdir");
let root = tmp.path();
std::fs::create_dir_all(root.join("src")).expect("mkdir src");
std::fs::create_dir_all(root.join("tests")).expect("mkdir tests");
std::fs::create_dir_all(root.join("examples")).expect("mkdir examples");
std::fs::create_dir_all(root.join("benches")).expect("mkdir benches");
let body = |name: &str| format!("#[allow(dead_code)]\nfn {name}() {{}}\n");
std::fs::write(root.join("src/lib.rs"), body("in_lib")).expect("write lib");
std::fs::write(root.join("src/helpers_tests.rs"), body("in_src_tests")).expect("write");
std::fs::write(root.join("tests/it.rs"), body("in_it")).expect("write it");
std::fs::write(root.join("examples/demo.rs"), body("in_demo")).expect("write demo");
std::fs::write(root.join("benches/bench.rs"), body("in_bench")).expect("write bench");
tmp
}
fn names(items: &[(PathBuf, DeadItem)]) -> Vec<String> {
let mut v: Vec<String> = items.iter().map(|(_, i)| i.name.clone()).collect();
v.sort();
v
}
#[test]
fn test_suppression_scan_excludes_tests_by_default() {
let tmp = fixture();
let analyzer = CargoDeadCodeAnalyzer::new(tmp.path());
let found = names(&analyzer.scan_for_suppression_attributes().expect("scan"));
assert_eq!(
found,
vec![
"in_bench".to_string(),
"in_demo".to_string(),
"in_lib".to_string()
],
"only the test tree is out of scope without --include-tests"
);
}
#[test]
fn test_include_tests_adds_the_test_trees() {
let tmp = fixture();
let analyzer = CargoDeadCodeAnalyzer::new(tmp.path()).include_tests();
let found = names(&analyzer.scan_for_suppression_attributes().expect("scan"));
assert!(found.contains(&"in_it".to_string()), "{found:?}");
assert!(found.contains(&"in_src_tests".to_string()), "{found:?}");
assert!(found.contains(&"in_lib".to_string()), "{found:?}");
assert!(found.contains(&"in_demo".to_string()), "{found:?}");
assert!(found.contains(&"in_bench".to_string()), "{found:?}");
}
#[test]
fn test_examples_and_benches_are_in_scope_without_any_flag() {
let tmp = fixture();
let analyzer = CargoDeadCodeAnalyzer::new(tmp.path());
let found = names(&analyzer.scan_for_suppression_attributes().expect("scan"));
assert!(found.contains(&"in_demo".to_string()), "{found:?}");
assert!(found.contains(&"in_bench".to_string()), "{found:?}");
assert!(!found.contains(&"in_it".to_string()), "{found:?}");
}
#[test]
fn test_is_test_file_name_conventions() {
for name in ["tests.rs", "foo_tests.rs", "foo_test.rs", "test_foo.rs"] {
assert!(is_test_file_name(Path::new(name)), "{name}");
}
for name in ["lib.rs", "attestation.rs", "contest.rs"] {
assert!(!is_test_file_name(Path::new(name)), "{name}");
}
}
}