#![warn(missing_docs)]
#![warn(clippy::all)]
#![deny(unsafe_code)]
use anyhow::{Context, Result};
use syn::{visit::Visit, Block, Expr, ExprUnsafe, ItemFn};
#[derive(Debug, Clone, PartialEq)]
pub struct UnsafeBlock {
pub line: usize,
pub confidence: u8,
pub pattern: UnsafePattern,
pub suggestion: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UnsafePattern {
RawPointerDeref,
Transmute,
Assembly,
FfiCall,
UnionAccess,
MutableStatic,
Other,
}
#[derive(Debug, Clone)]
pub struct UnsafeAuditReport {
pub total_lines: usize,
pub unsafe_lines: usize,
pub unsafe_density_percent: f64,
pub unsafe_blocks: Vec<UnsafeBlock>,
pub average_confidence: f64,
}
impl UnsafeAuditReport {
pub fn new(total_lines: usize, unsafe_lines: usize, unsafe_blocks: Vec<UnsafeBlock>) -> Self {
let unsafe_density_percent = if total_lines > 0 {
(unsafe_lines as f64 / total_lines as f64) * 100.0
} else {
0.0
};
let average_confidence = if !unsafe_blocks.is_empty() {
unsafe_blocks
.iter()
.map(|b| b.confidence as f64)
.sum::<f64>()
/ unsafe_blocks.len() as f64
} else {
0.0
};
Self {
total_lines,
unsafe_lines,
unsafe_density_percent,
unsafe_blocks,
average_confidence,
}
}
pub fn meets_density_target(&self) -> bool {
self.unsafe_density_percent < 5.0
}
pub fn high_confidence_blocks(&self) -> Vec<&UnsafeBlock> {
self.unsafe_blocks
.iter()
.filter(|b| b.confidence >= 70)
.collect()
}
}
pub struct UnsafeAuditor {
unsafe_blocks: Vec<UnsafeBlock>,
total_lines: usize,
unsafe_lines: usize,
source_code: String,
}
impl UnsafeAuditor {
pub fn new() -> Self {
Self {
unsafe_blocks: Vec::new(),
total_lines: 0,
unsafe_lines: 0,
source_code: String::new(),
}
}
pub fn audit(&mut self, rust_code: &str) -> Result<UnsafeAuditReport> {
self.source_code = rust_code.to_string();
self.total_lines = rust_code.lines().count();
let syntax_tree = syn::parse_file(rust_code).context("Failed to parse Rust code")?;
self.visit_file(&syntax_tree);
Ok(UnsafeAuditReport::new(
self.total_lines,
self.unsafe_lines,
self.unsafe_blocks.clone(),
))
}
fn analyze_unsafe_block(&self, unsafe_block: &ExprUnsafe) -> (UnsafePattern, u8, String) {
let block_str = quote::quote!(#unsafe_block).to_string();
let (pattern, confidence, suggestion) = if block_str.contains("std :: ptr ::")
|| block_str.contains("* ptr")
|| block_str.contains("null_mut")
|| block_str.contains("null()")
{
(
UnsafePattern::RawPointerDeref,
85,
"Consider using Box<T>, &T, or &mut T with proper lifetimes".to_string(),
)
} else if block_str.contains("transmute") {
(
UnsafePattern::Transmute,
40,
"Consider safe alternatives like From/Into traits or checked conversions"
.to_string(),
)
} else if block_str.contains("asm!") || block_str.contains("global_asm!") {
(
UnsafePattern::Assembly,
15,
"No safe alternative - inline assembly required for platform-specific operations"
.to_string(),
)
} else if block_str.contains("extern") {
(
UnsafePattern::FfiCall,
30,
"Consider creating a safe wrapper around FFI calls".to_string(),
)
} else {
(
UnsafePattern::Other,
50,
"Review if this unsafe block can be eliminated or replaced with safe alternatives"
.to_string(),
)
};
(pattern, confidence, suggestion)
}
fn count_block_lines(&self, block: &Block) -> usize {
block.stmts.len() + 2
}
}
impl Default for UnsafeAuditor {
fn default() -> Self {
Self::new()
}
}
impl<'ast> Visit<'ast> for UnsafeAuditor {
fn visit_expr(&mut self, expr: &'ast Expr) {
if let Expr::Unsafe(unsafe_expr) = expr {
let (pattern, confidence, suggestion) = self.analyze_unsafe_block(unsafe_expr);
let block_lines = self.count_block_lines(&unsafe_expr.block);
self.unsafe_lines += block_lines;
let line = 0;
self.unsafe_blocks.push(UnsafeBlock {
line,
confidence,
pattern,
suggestion,
});
}
syn::visit::visit_expr(self, expr);
}
fn visit_item_fn(&mut self, func: &'ast ItemFn) {
if func.sig.unsafety.is_some() {
let body_lines = self.count_block_lines(&func.block);
self.unsafe_lines += body_lines;
self.unsafe_blocks.push(UnsafeBlock {
line: 0,
confidence: 60,
pattern: UnsafePattern::Other,
suggestion: "Unsafe function - review if entire function needs to be unsafe or just specific blocks".to_string(),
});
}
syn::visit::visit_item_fn(self, func);
}
}
pub fn audit_rust_code(rust_code: &str) -> Result<UnsafeAuditReport> {
let mut auditor = UnsafeAuditor::new();
auditor.audit(rust_code)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_no_unsafe_blocks() {
let code = r#"
fn safe_function() {
let x = 42;
println!("{}", x);
}
"#;
let report = audit_rust_code(code).expect("Audit failed");
assert_eq!(report.unsafe_blocks.len(), 0);
assert_eq!(report.unsafe_lines, 0);
assert!(report.meets_density_target());
}
#[test]
fn test_single_unsafe_block() {
let code = r#"
fn with_unsafe() {
unsafe {
let ptr = std::ptr::null_mut::<i32>();
*ptr = 42;
}
}
"#;
let report = audit_rust_code(code).expect("Audit failed");
assert_eq!(
report.unsafe_blocks.len(),
1,
"Should detect one unsafe block"
);
assert!(report.unsafe_lines > 0, "Should count unsafe lines");
}
#[test]
fn test_multiple_unsafe_blocks() {
let code = r#"
fn multiple_unsafe() {
unsafe {
let ptr1 = std::ptr::null_mut::<i32>();
}
let safe_code = 42;
unsafe {
let ptr2 = std::ptr::null_mut::<f64>();
}
}
"#;
let report = audit_rust_code(code).expect("Audit failed");
assert_eq!(
report.unsafe_blocks.len(),
2,
"Should detect two unsafe blocks"
);
}
#[test]
fn test_unsafe_density_calculation() {
let code = r#"
fn example() {
let x = 1;
let y = 2;
unsafe {
let ptr = std::ptr::null_mut::<i32>();
}
let z = 3;
}
"#;
let report = audit_rust_code(code).expect("Audit failed");
assert!(report.unsafe_density_percent > 20.0);
assert!(report.unsafe_density_percent < 50.0);
}
#[test]
fn test_nested_unsafe_blocks() {
let code = r#"
fn nested() {
unsafe {
let ptr = std::ptr::null_mut::<i32>();
unsafe {
*ptr = 42;
}
}
}
"#;
let report = audit_rust_code(code).expect("Audit failed");
assert!(
!report.unsafe_blocks.is_empty(),
"Should detect unsafe blocks"
);
}
#[test]
fn test_unsafe_in_different_items() {
let code = r#"
fn func1() {
unsafe { let x = 1; }
}
fn func2() {
unsafe { let y = 2; }
}
impl MyStruct {
fn method(&self) {
unsafe { let z = 3; }
}
}
struct MyStruct;
"#;
let report = audit_rust_code(code).expect("Audit failed");
assert_eq!(
report.unsafe_blocks.len(),
3,
"Should detect unsafe in all items"
);
}
#[test]
fn test_confidence_scoring() {
let code = r#"
fn with_pointer() {
unsafe {
let ptr = std::ptr::null_mut::<i32>();
*ptr = 42;
}
}
"#;
let report = audit_rust_code(code).expect("Audit failed");
assert_eq!(report.unsafe_blocks.len(), 1);
let block = &report.unsafe_blocks[0];
assert!(block.confidence > 0, "Should have non-zero confidence");
assert!(block.confidence <= 100, "Confidence should be 0-100");
}
#[test]
fn test_pattern_detection_raw_pointer() {
let code = r#"
fn deref_pointer() {
unsafe {
let ptr = std::ptr::null_mut::<i32>();
*ptr = 42;
}
}
"#;
let report = audit_rust_code(code).expect("Audit failed");
assert_eq!(report.unsafe_blocks.len(), 1);
assert_eq!(
report.unsafe_blocks[0].pattern,
UnsafePattern::RawPointerDeref
);
}
#[test]
fn test_suggestion_generation() {
let code = r#"
fn with_unsafe() {
unsafe {
let ptr = std::ptr::null_mut::<i32>();
}
}
"#;
let report = audit_rust_code(code).expect("Audit failed");
assert_eq!(report.unsafe_blocks.len(), 1);
assert!(
!report.unsafe_blocks[0].suggestion.is_empty(),
"Should provide a suggestion"
);
}
#[test]
fn test_high_confidence_blocks() {
let code = r#"
fn example() {
unsafe { let x = 1; }
unsafe { let y = 2; }
}
"#;
let report = audit_rust_code(code).expect("Audit failed");
let high_conf = report.high_confidence_blocks();
assert!(high_conf.len() <= report.unsafe_blocks.len());
}
#[test]
fn test_average_confidence() {
let code = r#"
fn example() {
unsafe { let x = 1; }
}
"#;
let report = audit_rust_code(code).expect("Audit failed");
assert!(report.average_confidence >= 0.0);
assert!(report.average_confidence <= 100.0);
}
#[test]
fn test_empty_code() {
let code = "";
let report = audit_rust_code(code).expect("Audit failed");
assert_eq!(report.unsafe_blocks.len(), 0);
assert_eq!(report.total_lines, 0);
}
#[test]
fn test_invalid_rust_code() {
let code = "fn incomplete(";
let result = audit_rust_code(code);
assert!(result.is_err(), "Should return error for invalid code");
}
#[test]
fn test_unsafe_fn() {
let code = r#"
unsafe fn dangerous_function() {
let x = 42;
}
"#;
let report = audit_rust_code(code).expect("Audit failed");
assert!(!report.unsafe_blocks.is_empty() || report.unsafe_lines > 0);
}
}