impl CargoDeadCodeAnalyzer {
fn parse_cargo_warnings(&self, output: &str) -> Result<Vec<(PathBuf, DeadItem)>> {
let mut dead_items = Vec::new();
for line in output.lines() {
if line.trim().is_empty() {
continue;
}
let json: Value = match serde_json::from_str(line) {
Ok(v) => v,
Err(_) => continue, };
if json["reason"] != "compiler-message" {
continue;
}
let message = &json["message"];
if let Some(code) = message["code"]["code"].as_str() {
if code == "dead_code" {
if let Some(item) = self.extract_dead_item(message) {
if self.is_excluded_source(&item.0) {
continue;
}
dead_items.push(item);
}
} else if code == "unreachable_code" {
if let Some(item) = self.extract_unreachable_item(message) {
if self.is_excluded_source(&item.0) {
continue;
}
dead_items.push(item);
}
}
}
}
Ok(dead_items)
}
fn extract_unreachable_item(&self, message: &Value) -> Option<(PathBuf, DeadItem)> {
let spans = message["spans"].as_array()?;
let primary_span = spans
.iter()
.find(|s| s["is_primary"].as_bool() == Some(true))?;
let file_path = PathBuf::from(primary_span["file_name"].as_str()?);
let line = primary_span["line_start"].as_u64()? as usize;
let column = primary_span["column_start"].as_u64()? as usize;
let message_text = message["message"].as_str()?;
let name = primary_span["text"]
.as_array()
.and_then(|texts| texts.first())
.and_then(|t| t["text"].as_str())
.map_or_else(|| format!("line {line}"), |t| t.trim().to_string());
Some((
file_path,
DeadItem {
name,
kind: DeadCodeKind::UnreachableCode,
line,
column,
message: message_text.to_string(),
},
))
}
fn extract_dead_item(&self, message: &Value) -> Option<(PathBuf, DeadItem)> {
let spans = message["spans"].as_array()?;
let primary_span = spans
.iter()
.find(|s| s["is_primary"].as_bool() == Some(true))?;
let file_path = PathBuf::from(primary_span["file_name"].as_str()?);
let line = primary_span["line_start"].as_u64()? as usize;
let column = primary_span["column_start"].as_u64()? as usize;
let message_text = message["message"].as_str()?;
let (name, kind) = self.parse_message(message_text)?;
Some((
file_path,
DeadItem {
name,
kind,
line,
column,
message: message_text.to_string(),
},
))
}
fn parse_message(&self, message: &str) -> Option<(String, DeadCodeKind)> {
let patterns = [
("function `", "` is never used", DeadCodeKind::Function),
("method `", "` is never used", DeadCodeKind::Method),
("struct `", "` is never constructed", DeadCodeKind::Struct),
("enum `", "` is never used", DeadCodeKind::Enum),
("variant `", "` is never constructed", DeadCodeKind::Variant),
("field `", "` is never read", DeadCodeKind::Field),
("constant `", "` is never used", DeadCodeKind::Constant),
("static `", "` is never used", DeadCodeKind::Static),
("module `", "` is never used", DeadCodeKind::Module),
("trait `", "` is never used", DeadCodeKind::Trait),
("type alias `", "` is never used", DeadCodeKind::TypeAlias),
];
for (prefix, suffix, kind) in &patterns {
if let Some(start) = message.find(prefix) {
let name_start = start + prefix.len();
if let Some(end) = message[name_start..].find(suffix) {
let name = message[name_start..name_start + end].to_string();
return Some((name, kind.clone()));
}
}
}
if message.contains("is never") || message.contains("never used") {
if let Some(start) = message.find('`') {
if let Some(end) = message[start + 1..].find('`') {
let name = message[start + 1..start + 1 + end].to_string();
return Some((name, DeadCodeKind::Other("unknown".to_string())));
}
}
}
None
}
fn group_by_file(&self, items: Vec<(PathBuf, DeadItem)>) -> Vec<FileDeadCode> {
let mut file_map: HashMap<PathBuf, Vec<DeadItem>> = HashMap::new();
for (path, item) in items {
file_map.entry(path).or_default().push(item);
}
file_map
.into_iter()
.map(|(file_path, items)| {
let (unreachable_items, dead_items): (Vec<DeadItem>, Vec<DeadItem>) = items
.into_iter()
.partition(|item| item.kind == DeadCodeKind::UnreachableCode);
let total_lines = self.count_file_lines(&file_path);
let file_dead_percentage = file_percentage(total_lines, &dead_items);
FileDeadCode {
file_path,
dead_items,
unreachable_items,
file_dead_percentage,
total_lines,
}
})
.collect()
}
fn count_file_lines(&self, file_path: &Path) -> Option<usize> {
let full_path = if file_path.is_absolute() {
file_path.to_path_buf()
} else {
self.project_path.join(file_path)
};
std::fs::read_to_string(&full_path)
.ok()
.map(|content| content.lines().count())
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
async fn calculate_metrics(&self, files: Vec<FileDeadCode>) -> Result<AccurateDeadCodeReport> {
let mut total_lines = 0;
let mut total_files = 0;
let mut project_files = 0;
let mut dead_lines = 0;
let mut dead_by_type = HashMap::new();
let total_dead_items = files.iter().map(|f| f.dead_items.len()).sum();
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") {
project_files += 1;
if self.is_excluded_source(path) {
continue;
}
total_files += 1;
if let Ok(content) = std::fs::read_to_string(path) {
total_lines += content.lines().count();
}
}
}
for file in &files {
for item in &file.dead_items {
let kind_str = dead_code_kind_to_str(&item.kind);
*dead_by_type.entry(kind_str.to_string()).or_insert(0) += 1;
}
dead_lines += estimated_dead_lines_bounded(&file.dead_items, file.total_lines);
}
let dead_code_percentage = if total_lines > 0 {
#[allow(clippy::cast_precision_loss)]
let pct = (dead_lines as f64 / total_lines as f64) * 100.0;
pct.min(100.0)
} else {
0.0
};
Ok(AccurateDeadCodeReport {
files_with_dead_code: files,
total_dead_items,
dead_code_percentage,
total_lines,
total_files,
project_files,
dead_lines,
dead_by_type,
})
}
}
pub(crate) fn estimated_dead_lines(items: &[DeadItem]) -> usize {
items
.iter()
.map(|item| match item.kind {
DeadCodeKind::Function | DeadCodeKind::Method => 5,
DeadCodeKind::Struct | DeadCodeKind::Enum => 3,
_ => 2,
})
.sum()
}
pub(crate) fn estimated_dead_lines_bounded(items: &[DeadItem], total_lines: Option<usize>) -> usize {
let estimate = estimated_dead_lines(items);
total_lines.map_or(estimate, |lines| estimate.min(lines))
}
fn file_percentage(total_lines: Option<usize>, items: &[DeadItem]) -> f64 {
match total_lines {
Some(lines) if lines > 0 => {
#[allow(clippy::cast_precision_loss)]
let pct = (estimated_dead_lines(items) as f64 / lines as f64) * 100.0;
pct.min(100.0)
}
_ => 0.0,
}
}
fn dead_code_kind_to_str(kind: &DeadCodeKind) -> &str {
match kind {
DeadCodeKind::Function => "function",
DeadCodeKind::Method => "method",
DeadCodeKind::Struct => "struct",
DeadCodeKind::Enum => "enum",
DeadCodeKind::Variant => "variant",
DeadCodeKind::Field => "field",
DeadCodeKind::Constant => "constant",
DeadCodeKind::Static => "static",
DeadCodeKind::Module => "module",
DeadCodeKind::Trait => "trait",
DeadCodeKind::TypeAlias => "type_alias",
DeadCodeKind::UnreachableCode => "unreachable",
DeadCodeKind::Other(s) => s,
}
}
#[cfg(test)]
mod unreachable_lint_tests {
use super::*;
fn diagnostic(code: &str, message: &str, file: &str) -> String {
serde_json::json!({
"reason": "compiler-message",
"message": {
"code": { "code": code },
"message": message,
"spans": [{
"is_primary": true,
"file_name": file,
"line_start": 3,
"column_start": 5,
"text": [{ "text": " let y = x * 2;" }],
}],
}
})
.to_string()
}
#[test]
fn unreachable_code_warnings_are_collected_and_tagged() {
let analyzer = CargoDeadCodeAnalyzer::new(std::path::Path::new("."));
let output = format!(
"{}\n{}\n",
diagnostic("unreachable_code", "unreachable statement", "src/lib.rs"),
diagnostic("dead_code", "function `helper` is never used", "src/lib.rs"),
);
let items = analyzer
.parse_cargo_warnings(&output)
.expect("parse cargo output");
let unreachable: Vec<_> = items
.iter()
.filter(|(_, i)| i.kind == DeadCodeKind::UnreachableCode)
.collect();
assert_eq!(
unreachable.len(),
1,
"the unreachable_code warning was dropped: {items:?}"
);
assert_eq!(unreachable[0].1.line, 3);
assert_eq!(
unreachable[0].1.name, "let y = x * 2;",
"the source line is the only name rustc gives this lint"
);
}
#[test]
fn grouping_keeps_unreachable_out_of_dead_items() {
let analyzer = CargoDeadCodeAnalyzer::new(std::path::Path::new("."));
let path = PathBuf::from("src/lib.rs");
let items = vec![
(
path.clone(),
DeadItem {
name: "helper".to_string(),
kind: DeadCodeKind::Function,
line: 10,
column: 1,
message: "`helper` is never used".to_string(),
},
),
(
path.clone(),
DeadItem {
name: "let y = 1;".to_string(),
kind: DeadCodeKind::UnreachableCode,
line: 3,
column: 5,
message: "unreachable statement".to_string(),
},
),
];
let grouped = analyzer.group_by_file(items);
assert_eq!(grouped.len(), 1);
assert_eq!(grouped[0].dead_items.len(), 1);
assert_eq!(grouped[0].unreachable_items.len(), 1);
assert_eq!(
estimated_dead_lines(&grouped[0].dead_items),
5,
"an unreachable statement must not be charged as dead lines"
);
}
}