fn is_comment_line(trimmed: &str) -> bool {
trimmed.starts_with("//") || trimmed.starts_with("/*") || trimmed.starts_with("*")
}
fn scan_single_rs_file(
entry: &Path,
check: &mut impl FnMut(&str, &str, usize, &mut Vec<CbPatternViolation>),
violations: &mut Vec<CbPatternViolation>,
) {
let content = match fs::read_to_string(entry) {
Ok(c) => c,
Err(_) => return,
};
let lines: Vec<&str> = content.lines().collect();
let test_lines = compute_test_code_lines(&lines);
let file_path = entry.display().to_string();
for (line_num, line) in lines.iter().enumerate() {
if test_lines.contains(&line_num) || is_comment_line(line.trim()) {
continue;
}
check(line.trim(), &file_path, line_num, violations);
}
}
pub(super) fn scan_rs_production_lines(
project_path: &Path,
skip_test_files: bool,
mut check: impl FnMut(&str, &str, usize, &mut Vec<CbPatternViolation>),
) -> Vec<CbPatternViolation> {
let mut violations = Vec::new();
let src_dir = project_path.join("src");
if !src_dir.exists() {
return violations;
}
let entries = match walkdir_rs_files(&src_dir) {
Ok(e) => e,
Err(_) => return violations,
};
for entry in entries {
if skip_test_files && is_test_file(&entry) {
continue;
}
scan_single_rs_file(&entry, &mut check, &mut violations);
}
violations
}
pub(super) fn is_in_string_literal(line: &str, pattern: &str) -> bool {
if let Some(idx) = line.find(pattern) {
let quote_count = line
.get(..idx)
.unwrap_or_default()
.chars()
.filter(|&c| c == '"')
.count();
quote_count % 2 == 1
} else {
false
}
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub fn detect_cb120_nan_unsafe_comparison(project_path: &Path) -> Vec<CbPatternViolation> {
scan_rs_production_lines(
project_path,
false,
|trimmed, file_path, line_num, violations| {
if is_in_string_literal(trimmed, "partial_cmp") {
return;
}
if !trimmed.contains("partial_cmp") {
return;
}
let has_unwrap = trimmed.contains(DOT_UNWRAP_STR) && !trimmed.contains(UNWRAP_OR_STR);
let has_expect = trimmed.contains(".expect(");
let suffix = if has_unwrap {
concat!("unwr", "ap()")
} else if has_expect {
"expect()"
} else {
return;
};
violations.push(CbPatternViolation {
pattern_id: "CB-120".to_string(),
file: file_path.to_string(),
line: line_num + 1,
description: format!("NaN-unsafe: .partial_cmp().{suffix} panics on NaN. Use .total_cmp() or .unwrap_or()"),
severity: Severity::Error,
});
},
)
}