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)
}
}
#[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"));
}
}