use std::collections::HashSet;
use std::fs;
use std::path::Path;
#[cfg(test)]
fn is_allowed_by_inline(
allows: &std::collections::HashMap<(String, u32), HashSet<String>>,
file_path: &str,
line: u32,
cause_id: &str,
) -> bool {
let key = (file_path.to_string(), line);
if let Some(causes) = allows.get(&key) {
if causes.contains("*") || causes.contains(cause_id) {
return true;
}
}
if line > 1 {
let prev_key = (file_path.to_string(), line - 1);
if let Some(causes) = allows.get(&prev_key) {
if causes.contains("*") || causes.contains(cause_id) {
return true;
}
}
}
false
}
#[cfg(test)]
fn parse_file_allows(content: &str) -> std::collections::HashMap<u32, HashSet<String>> {
let mut allows: std::collections::HashMap<u32, HashSet<String>> =
std::collections::HashMap::new();
for (idx, line) in content.lines().enumerate() {
let line_num = (idx + 1) as u32;
if let Some(causes) = parse_line_allows(line) {
allows.insert(line_num, causes);
}
}
allows
}
pub fn check_inline_allow(
file_path: &str,
line: u32,
cause_id: &str,
workspace_root: Option<&Path>,
) -> bool {
if line == 0 {
return false;
}
let content = read_source_file_with_root(file_path, workspace_root);
let content = match content {
Some(c) => c,
None => return false,
};
let lines: Vec<&str> = content.lines().collect();
for check_line in [line.saturating_sub(1), line] {
if let Some(line_content) = lines.get((check_line as usize).saturating_sub(1)) {
if let Some(causes) = parse_line_allows(line_content) {
if causes.contains("*") || causes.contains(cause_id) {
return true;
}
for c in &causes {
if let Some(canonical) = crate::panic_cause::PanicCause::normalise_id(c) {
if canonical == cause_id {
return true;
}
if canonical == "overflow"
&& matches!(
cause_id,
"overflow" | "div_overflow" | "rem_overflow" | "shift_overflow"
)
{
return true;
}
}
}
}
}
}
false
}
fn read_source_file_with_root(file_path: &str, workspace_root: Option<&Path>) -> Option<String> {
let path = Path::new(file_path);
if path.exists() {
return fs::read_to_string(path).ok();
}
if let Some(root) = workspace_root {
let candidate = root.join(file_path);
if candidate.exists() {
return fs::read_to_string(&candidate).ok();
}
}
if let Ok(cwd) = std::env::current_dir() {
let mut dir = cwd.as_path();
loop {
let candidate = dir.join(file_path);
if candidate.exists() {
return fs::read_to_string(&candidate).ok();
}
let cargo_toml = dir.join("Cargo.toml");
if cargo_toml.exists() {
if let Ok(content) = fs::read_to_string(&cargo_toml) {
if content.contains("[workspace]") {
let candidate = dir.join(file_path);
if candidate.exists() {
return fs::read_to_string(&candidate).ok();
}
break; }
}
}
match dir.parent() {
Some(parent) => dir = parent,
None => break,
}
}
}
None
}
fn parse_line_allows(line: &str) -> Option<HashSet<String>> {
let prefix_start = line
.find("// jonesy:allow(")
.or_else(|| line.find("// jonesy: allow("))?;
let paren_start = line[prefix_start..].find('(')? + prefix_start + 1;
let rest = &line[paren_start..];
let end = rest.find(')')?;
let causes_str = &rest[..end];
let causes: HashSet<String> = causes_str
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
if causes.is_empty() {
None
} else {
Some(causes)
}
}
pub fn find_unused_inline_allows(
root: &Path,
reported_locations: &HashSet<(String, u32)>,
) -> Vec<(String, u32, String)> {
let mut unused = Vec::new();
fn visit_rs_files(
dir: &Path,
unused: &mut Vec<(String, u32, String)>,
reported: &HashSet<(String, u32)>,
) {
let Ok(entries) = fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if path.file_name().is_some_and(|n| n != "target") {
visit_rs_files(&path, unused, reported);
}
} else if path.extension().is_some_and(|e| e == "rs") {
if let Ok(content) = fs::read_to_string(&path) {
let file_str = path.to_string_lossy().to_string();
for (idx, line) in content.lines().enumerate() {
if let Some(causes) = parse_line_allows(line) {
let comment_line = (idx + 1) as u32;
let causes_str = causes.iter().cloned().collect::<Vec<_>>().join(", ");
let is_used = reported.iter().any(|(f, l)| {
(f.ends_with(&file_str) || file_str.ends_with(f.as_str()))
&& (*l == comment_line || *l == comment_line + 1)
});
if !is_used {
let rel_path = path
.strip_prefix(std::env::current_dir().unwrap_or_default())
.unwrap_or(&path)
.to_string_lossy()
.to_string();
unused.push((rel_path, comment_line, causes_str));
}
}
}
}
}
}
}
visit_rs_files(root, &mut unused, reported_locations);
unused.sort_by(|a, b| (&a.0, a.1).cmp(&(&b.0, b.1)));
unused
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_single_cause() {
let content = r#"
fn foo() {
let x = None.unwrap(); // jonesy:allow(unwrap)
}
"#;
let allows = parse_file_allows(content);
assert!(allows.get(&3).unwrap().contains("unwrap"));
}
#[test]
fn test_parse_multiple_causes() {
let content = r#"
fn foo() {
something(); // jonesy:allow(unwrap, expect, panic)
}
"#;
let allows = parse_file_allows(content);
let causes = allows.get(&3).unwrap();
assert!(causes.contains("unwrap"));
assert!(causes.contains("expect"));
assert!(causes.contains("panic"));
}
#[test]
fn test_parse_wildcard() {
let content = r#"
fn foo() {
dangerous(); // jonesy:allow(*)
}
"#;
let allows = parse_file_allows(content);
assert!(allows.get(&3).unwrap().contains("*"));
}
#[test]
fn test_parse_line_allows() {
let line = " let x = foo(); // jonesy:allow(unwrap, bounds)";
let causes = parse_line_allows(line).unwrap();
assert!(causes.contains("unwrap"));
assert!(causes.contains("bounds"));
}
#[test]
fn test_parse_line_allows_space_after_colon() {
let line = " let x = foo(); // jonesy: allow(overflow)";
let causes = parse_line_allows(line).unwrap();
assert!(causes.contains("overflow"));
}
#[test]
fn test_parse_line_allows_no_match() {
let line = " let x = foo(); // regular comment";
assert!(parse_line_allows(line).is_none());
}
#[test]
fn test_parse_line_allows_empty() {
let line = " let x = foo(); // jonesy:allow()";
assert!(parse_line_allows(line).is_none());
}
#[test]
fn test_parse_file_allows_no_comments() {
let content = "fn foo() {\n bar();\n}\n";
let allows = parse_file_allows(content);
assert!(allows.is_empty());
}
#[test]
fn test_parse_file_allows_multiple_lines() {
let content = r#"
fn foo() {
bar(); // jonesy:allow(panic)
baz();
qux(); // jonesy:allow(unwrap)
}
"#;
let allows = parse_file_allows(content);
assert_eq!(allows.len(), 2);
assert!(allows.get(&3).unwrap().contains("panic"));
assert!(allows.get(&5).unwrap().contains("unwrap"));
}
#[test]
fn test_is_allowed_by_inline_exact_match() {
let mut allows = std::collections::HashMap::new();
let mut causes = HashSet::new();
causes.insert("unwrap".to_string());
allows.insert(("test.rs".to_string(), 10), causes);
assert!(is_allowed_by_inline(&allows, "test.rs", 10, "unwrap"));
assert!(!is_allowed_by_inline(&allows, "test.rs", 10, "panic"));
}
#[test]
fn test_is_allowed_by_inline_wildcard() {
let mut allows = std::collections::HashMap::new();
let mut causes = HashSet::new();
causes.insert("*".to_string());
allows.insert(("test.rs".to_string(), 10), causes);
assert!(is_allowed_by_inline(&allows, "test.rs", 10, "unwrap"));
assert!(is_allowed_by_inline(&allows, "test.rs", 10, "panic"));
assert!(is_allowed_by_inline(&allows, "test.rs", 10, "anything"));
}
#[test]
fn test_is_allowed_by_inline_previous_line() {
let mut allows = std::collections::HashMap::new();
let mut causes = HashSet::new();
causes.insert("unwrap".to_string());
allows.insert(("test.rs".to_string(), 9), causes);
assert!(is_allowed_by_inline(&allows, "test.rs", 10, "unwrap"));
assert!(!is_allowed_by_inline(&allows, "test.rs", 11, "unwrap"));
}
#[test]
fn test_is_allowed_by_inline_no_match() {
let allows = std::collections::HashMap::new();
assert!(!is_allowed_by_inline(&allows, "test.rs", 10, "unwrap"));
}
#[test]
fn test_is_allowed_by_inline_line_one() {
let mut allows = std::collections::HashMap::new();
let mut causes = HashSet::new();
causes.insert("unwrap".to_string());
allows.insert(("test.rs".to_string(), 1), causes);
assert!(is_allowed_by_inline(&allows, "test.rs", 1, "unwrap"));
}
#[test]
fn test_check_inline_allow_with_file() {
use std::io::Write;
let temp_dir = tempfile::TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.rs");
let mut file = std::fs::File::create(&file_path).unwrap();
writeln!(file, "fn foo() {{").unwrap();
writeln!(file, " bar(); // jonesy:allow(unwrap)").unwrap();
writeln!(file, "}}").unwrap();
assert!(check_inline_allow(
file_path.to_str().unwrap(),
2,
"unwrap",
None
));
assert!(check_inline_allow(
file_path.to_str().unwrap(),
3,
"unwrap",
None
));
assert!(!check_inline_allow(
file_path.to_str().unwrap(),
2,
"panic",
None
));
}
#[test]
fn test_check_inline_allow_absolute_path() {
use std::io::Write;
let temp_dir = tempfile::TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.rs");
let mut file = std::fs::File::create(&file_path).unwrap();
writeln!(file, "x(); // jonesy:allow(*)").unwrap();
drop(file);
let result = check_inline_allow(file_path.to_str().unwrap(), 1, "anything", None);
assert!(result);
}
#[test]
fn test_check_inline_allow_nonexistent_file() {
assert!(!check_inline_allow(
"/nonexistent/file.rs",
1,
"unwrap",
None
));
}
#[test]
fn test_check_inline_allow_wildcard() {
use std::io::Write;
let temp_dir = tempfile::TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.rs");
let mut file = std::fs::File::create(&file_path).unwrap();
writeln!(file, "dangerous(); // jonesy:allow(*)").unwrap();
assert!(check_inline_allow(
file_path.to_str().unwrap(),
1,
"anything",
None
));
assert!(check_inline_allow(
file_path.to_str().unwrap(),
1,
"unwrap",
None
));
}
#[test]
fn test_is_allowed_by_inline_wrong_cause() {
let mut allows = std::collections::HashMap::new();
let mut causes = HashSet::new();
causes.insert("unwrap".to_string());
allows.insert(("test.rs".to_string(), 10), causes);
assert!(!is_allowed_by_inline(&allows, "test.rs", 10, "panic"));
}
#[test]
fn test_is_allowed_by_inline_prev_line_wildcard() {
let mut allows = std::collections::HashMap::new();
let mut causes = HashSet::new();
causes.insert("*".to_string());
allows.insert(("test.rs".to_string(), 9), causes);
assert!(is_allowed_by_inline(&allows, "test.rs", 10, "anything"));
}
#[test]
fn test_find_unused_inline_allows() {
let dir = tempfile::tempdir().unwrap();
let src_dir = dir.path().join("src");
std::fs::create_dir_all(&src_dir).unwrap();
std::fs::write(
src_dir.join("main.rs"),
"fn main() {\n // jonesy:allow(unwrap)\n foo();\n // jonesy:allow(bounds)\n bar();\n}\n",
).unwrap();
let mut reported = HashSet::new();
reported.insert((src_dir.join("main.rs").to_string_lossy().to_string(), 3));
let unused = find_unused_inline_allows(dir.path(), &reported);
assert_eq!(unused.len(), 1, "One unused allow (bounds on line 4)");
assert_eq!(unused[0].1, 4);
assert!(unused[0].2.contains("bounds"));
}
#[test]
fn test_find_unused_all_used() {
let dir = tempfile::tempdir().unwrap();
let src_dir = dir.path().join("src");
std::fs::create_dir_all(&src_dir).unwrap();
std::fs::write(src_dir.join("lib.rs"), "// jonesy:allow(unwrap)\nfoo();\n").unwrap();
let mut reported = HashSet::new();
reported.insert((src_dir.join("lib.rs").to_string_lossy().to_string(), 2));
let unused = find_unused_inline_allows(dir.path(), &reported);
assert!(unused.is_empty(), "All allows used");
}
}