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