1#![warn(missing_docs)]
32#![warn(clippy::all)]
33#![deny(unsafe_code)]
34
35pub mod lock_verify;
36
37use anyhow::{Context, Result};
38use syn::{visit::Visit, Block, Expr, ExprUnsafe, ItemFn};
39
40#[derive(Debug, Clone, PartialEq)]
42pub struct UnsafeBlock {
43 pub line: usize,
45 pub confidence: u8,
47 pub pattern: UnsafePattern,
49 pub suggestion: String,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
55pub enum UnsafePattern {
56 RawPointerDeref,
58 Transmute,
60 Assembly,
62 FfiCall,
64 UnionAccess,
66 MutableStatic,
68 Other,
70}
71
72#[derive(Debug, Clone)]
74pub struct UnsafeAuditReport {
75 pub total_lines: usize,
77 pub unsafe_lines: usize,
79 pub unsafe_density_percent: f64,
81 pub unsafe_blocks: Vec<UnsafeBlock>,
83 pub average_confidence: f64,
85}
86
87impl UnsafeAuditReport {
88 pub fn new(total_lines: usize, unsafe_lines: usize, unsafe_blocks: Vec<UnsafeBlock>) -> Self {
90 let unsafe_density_percent = if total_lines > 0 {
91 (unsafe_lines as f64 / total_lines as f64) * 100.0
92 } else {
93 0.0
94 };
95
96 let average_confidence = if !unsafe_blocks.is_empty() {
97 unsafe_blocks
98 .iter()
99 .map(|b| b.confidence as f64)
100 .sum::<f64>()
101 / unsafe_blocks.len() as f64
102 } else {
103 0.0
104 };
105
106 Self {
107 total_lines,
108 unsafe_lines,
109 unsafe_density_percent,
110 unsafe_blocks,
111 average_confidence,
112 }
113 }
114
115 pub fn meets_density_target(&self) -> bool {
117 self.unsafe_density_percent < 5.0
118 }
119
120 pub fn high_confidence_blocks(&self) -> Vec<&UnsafeBlock> {
122 self.unsafe_blocks
123 .iter()
124 .filter(|b| b.confidence >= 70)
125 .collect()
126 }
127}
128
129pub struct UnsafeAuditor {
131 unsafe_blocks: Vec<UnsafeBlock>,
132 total_lines: usize,
133 unsafe_lines: usize,
134 source_code: String,
135}
136
137impl UnsafeAuditor {
138 pub fn new() -> Self {
140 Self {
141 unsafe_blocks: Vec::new(),
142 total_lines: 0,
143 unsafe_lines: 0,
144 source_code: String::new(),
145 }
146 }
147
148 pub fn audit(&mut self, rust_code: &str) -> Result<UnsafeAuditReport> {
150 self.source_code = rust_code.to_string();
152
153 self.total_lines = rust_code.lines().count();
155
156 let syntax_tree = syn::parse_file(rust_code).context("Failed to parse Rust code")?;
158
159 self.visit_file(&syntax_tree);
161
162 Ok(UnsafeAuditReport::new(
163 self.total_lines,
164 self.unsafe_lines,
165 self.unsafe_blocks.clone(),
166 ))
167 }
168
169 fn analyze_unsafe_block(&self, unsafe_block: &ExprUnsafe) -> (UnsafePattern, u8, String) {
171 let block_str = quote::quote!(#unsafe_block).to_string();
173
174 let (pattern, confidence, suggestion) = if block_str.contains("std :: ptr ::")
176 || block_str.contains("* ptr")
177 || block_str.contains("null_mut")
178 || block_str.contains("null()")
179 {
180 (
181 UnsafePattern::RawPointerDeref,
182 85,
183 "Consider using Box<T>, &T, or &mut T with proper lifetimes".to_string(),
184 )
185 } else if block_str.contains("transmute") {
186 (
187 UnsafePattern::Transmute,
188 40,
189 "Consider safe alternatives like From/Into traits or checked conversions"
190 .to_string(),
191 )
192 } else if block_str.contains("asm!") || block_str.contains("global_asm!") {
193 (
194 UnsafePattern::Assembly,
195 15,
196 "No safe alternative - inline assembly required for platform-specific operations"
197 .to_string(),
198 )
199 } else if block_str.contains("extern") {
200 (
201 UnsafePattern::FfiCall,
202 30,
203 "Consider creating a safe wrapper around FFI calls".to_string(),
204 )
205 } else {
206 (
207 UnsafePattern::Other,
208 50,
209 "Review if this unsafe block can be eliminated or replaced with safe alternatives"
210 .to_string(),
211 )
212 };
213
214 (pattern, confidence, suggestion)
215 }
216
217 fn count_block_lines(&self, block: &Block) -> usize {
219 block.stmts.len() + 2
221 }
222}
223
224impl Default for UnsafeAuditor {
225 fn default() -> Self {
226 Self::new()
227 }
228}
229
230impl<'ast> Visit<'ast> for UnsafeAuditor {
231 fn visit_expr(&mut self, expr: &'ast Expr) {
233 if let Expr::Unsafe(unsafe_expr) = expr {
234 let (pattern, confidence, suggestion) = self.analyze_unsafe_block(unsafe_expr);
236
237 let block_lines = self.count_block_lines(&unsafe_expr.block);
239 self.unsafe_lines += block_lines;
240
241 let line = 0; self.unsafe_blocks.push(UnsafeBlock {
245 line,
246 confidence,
247 pattern,
248 suggestion,
249 });
250 }
251
252 syn::visit::visit_expr(self, expr);
254 }
255
256 fn visit_item_fn(&mut self, func: &'ast ItemFn) {
258 if func.sig.unsafety.is_some() {
260 let body_lines = self.count_block_lines(&func.block);
262 self.unsafe_lines += body_lines;
263
264 self.unsafe_blocks.push(UnsafeBlock {
265 line: 0,
266 confidence: 60,
267 pattern: UnsafePattern::Other,
268 suggestion: "Unsafe function - review if entire function needs to be unsafe or just specific blocks".to_string(),
269 });
270 }
271
272 syn::visit::visit_item_fn(self, func);
274 }
275}
276
277pub fn audit_rust_code(rust_code: &str) -> Result<UnsafeAuditReport> {
289 let mut auditor = UnsafeAuditor::new();
290 auditor.audit(rust_code)
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 #[test]
301 fn test_no_unsafe_blocks() {
302 let code = r#"
304 fn safe_function() {
305 let x = 42;
306 println!("{}", x);
307 }
308 "#;
309
310 let report = audit_rust_code(code).expect("Audit failed");
311 assert_eq!(report.unsafe_blocks.len(), 0);
312 assert_eq!(report.unsafe_lines, 0);
313 assert!(report.meets_density_target());
314 }
315
316 #[test]
317 fn test_single_unsafe_block() {
318 let code = r#"
320 fn with_unsafe() {
321 unsafe {
322 let ptr = std::ptr::null_mut::<i32>();
323 *ptr = 42;
324 }
325 }
326 "#;
327
328 let report = audit_rust_code(code).expect("Audit failed");
329 assert_eq!(
330 report.unsafe_blocks.len(),
331 1,
332 "Should detect one unsafe block"
333 );
334 assert!(report.unsafe_lines > 0, "Should count unsafe lines");
335 }
336
337 #[test]
338 fn test_multiple_unsafe_blocks() {
339 let code = r#"
341 fn multiple_unsafe() {
342 unsafe {
343 let ptr1 = std::ptr::null_mut::<i32>();
344 }
345
346 let safe_code = 42;
347
348 unsafe {
349 let ptr2 = std::ptr::null_mut::<f64>();
350 }
351 }
352 "#;
353
354 let report = audit_rust_code(code).expect("Audit failed");
355 assert_eq!(
356 report.unsafe_blocks.len(),
357 2,
358 "Should detect two unsafe blocks"
359 );
360 }
361
362 #[test]
363 fn test_unsafe_density_calculation() {
364 let code = r#"
366fn example() {
367 let x = 1;
368 let y = 2;
369 unsafe {
370 let ptr = std::ptr::null_mut::<i32>();
371 }
372 let z = 3;
373}
374"#;
375 let report = audit_rust_code(code).expect("Audit failed");
376
377 assert!(report.unsafe_density_percent > 20.0);
380 assert!(report.unsafe_density_percent < 50.0);
381 }
382
383 #[test]
384 fn test_nested_unsafe_blocks() {
385 let code = r#"
387 fn nested() {
388 unsafe {
389 let ptr = std::ptr::null_mut::<i32>();
390 unsafe {
391 *ptr = 42;
392 }
393 }
394 }
395 "#;
396
397 let report = audit_rust_code(code).expect("Audit failed");
398 assert!(
400 !report.unsafe_blocks.is_empty(),
401 "Should detect unsafe blocks"
402 );
403 }
404
405 #[test]
406 fn test_unsafe_in_different_items() {
407 let code = r#"
409 fn func1() {
410 unsafe { let x = 1; }
411 }
412
413 fn func2() {
414 unsafe { let y = 2; }
415 }
416
417 impl MyStruct {
418 fn method(&self) {
419 unsafe { let z = 3; }
420 }
421 }
422
423 struct MyStruct;
424 "#;
425
426 let report = audit_rust_code(code).expect("Audit failed");
427 assert_eq!(
428 report.unsafe_blocks.len(),
429 3,
430 "Should detect unsafe in all items"
431 );
432 }
433
434 #[test]
435 fn test_confidence_scoring() {
436 let code = r#"
438 fn with_pointer() {
439 unsafe {
440 let ptr = std::ptr::null_mut::<i32>();
441 *ptr = 42;
442 }
443 }
444 "#;
445
446 let report = audit_rust_code(code).expect("Audit failed");
447 assert_eq!(report.unsafe_blocks.len(), 1);
448
449 let block = &report.unsafe_blocks[0];
450 assert!(block.confidence > 0, "Should have non-zero confidence");
451 assert!(block.confidence <= 100, "Confidence should be 0-100");
452 }
453
454 #[test]
455 fn test_pattern_detection_raw_pointer() {
456 let code = r#"
458 fn deref_pointer() {
459 unsafe {
460 let ptr = std::ptr::null_mut::<i32>();
461 *ptr = 42;
462 }
463 }
464 "#;
465
466 let report = audit_rust_code(code).expect("Audit failed");
467 assert_eq!(report.unsafe_blocks.len(), 1);
468 assert_eq!(
469 report.unsafe_blocks[0].pattern,
470 UnsafePattern::RawPointerDeref
471 );
472 }
473
474 #[test]
475 fn test_suggestion_generation() {
476 let code = r#"
478 fn with_unsafe() {
479 unsafe {
480 let ptr = std::ptr::null_mut::<i32>();
481 }
482 }
483 "#;
484
485 let report = audit_rust_code(code).expect("Audit failed");
486 assert_eq!(report.unsafe_blocks.len(), 1);
487 assert!(
488 !report.unsafe_blocks[0].suggestion.is_empty(),
489 "Should provide a suggestion"
490 );
491 }
492
493 #[test]
494 fn test_high_confidence_blocks() {
495 let code = r#"
497 fn example() {
498 unsafe { let x = 1; }
499 unsafe { let y = 2; }
500 }
501 "#;
502
503 let report = audit_rust_code(code).expect("Audit failed");
504 let high_conf = report.high_confidence_blocks();
507 assert!(high_conf.len() <= report.unsafe_blocks.len());
508 }
509
510 #[test]
511 fn test_average_confidence() {
512 let code = r#"
514 fn example() {
515 unsafe { let x = 1; }
516 }
517 "#;
518
519 let report = audit_rust_code(code).expect("Audit failed");
520 assert!(report.average_confidence >= 0.0);
521 assert!(report.average_confidence <= 100.0);
522 }
523
524 #[test]
525 fn test_empty_code() {
526 let code = "";
528 let report = audit_rust_code(code).expect("Audit failed");
529 assert_eq!(report.unsafe_blocks.len(), 0);
530 assert_eq!(report.total_lines, 0);
531 }
532
533 #[test]
534 fn test_invalid_rust_code() {
535 let code = "fn incomplete(";
537 let result = audit_rust_code(code);
538 assert!(result.is_err(), "Should return error for invalid code");
539 }
540
541 #[test]
542 fn test_unsafe_fn() {
543 let code = r#"
545 unsafe fn dangerous_function() {
546 let x = 42;
547 }
548 "#;
549
550 let report = audit_rust_code(code).expect("Audit failed");
551 assert!(!report.unsafe_blocks.is_empty() || report.unsafe_lines > 0);
553 }
554}