decy_verify/
lib.rs

1//! Safety property verification for transpiled Rust code.
2//!
3//! Verifies memory safety, type safety, and other Rust safety guarantees.
4//!
5//! # Unsafe Code Auditing
6//!
7//! This module provides comprehensive auditing of unsafe blocks in generated Rust code:
8//! - Detection and counting of all unsafe blocks
9//! - Confidence scoring for elimination potential
10//! - Suggestions for safer alternatives
11//! - Unsafe density metrics (<5 per 1000 LOC target)
12//!
13//! # Example
14//!
15//! ```no_run
16//! use decy_verify::{UnsafeAuditor, audit_rust_code};
17//!
18//! let rust_code = r#"
19//!     fn example() {
20//!         unsafe {
21//!             let ptr = std::ptr::null_mut();
22//!         }
23//!     }
24//! "#;
25//!
26//! let report = audit_rust_code(rust_code).expect("Failed to audit");
27//! println!("Unsafe blocks found: {}", report.unsafe_blocks.len());
28//! println!("Unsafe density: {:.2}%", report.unsafe_density_percent);
29//! ```
30
31#![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/// Represents a single unsafe block found in Rust code
39#[derive(Debug, Clone, PartialEq)]
40pub struct UnsafeBlock {
41    /// Line number where the unsafe block starts
42    pub line: usize,
43    /// Confidence score (0-100) that this block could be eliminated
44    pub confidence: u8,
45    /// Pattern detected (e.g., "raw_pointer_deref", "transmute", etc.)
46    pub pattern: UnsafePattern,
47    /// Suggestion for safer alternative
48    pub suggestion: String,
49}
50
51/// Categories of unsafe patterns
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub enum UnsafePattern {
54    /// Raw pointer dereference (*ptr)
55    RawPointerDeref,
56    /// Type transmutation
57    Transmute,
58    /// Inline assembly
59    Assembly,
60    /// FFI call
61    FfiCall,
62    /// Union field access
63    UnionAccess,
64    /// Mutable static access
65    MutableStatic,
66    /// Other unsafe operation
67    Other,
68}
69
70/// Report summarizing unsafe code in a Rust file
71#[derive(Debug, Clone)]
72pub struct UnsafeAuditReport {
73    /// Total lines of code
74    pub total_lines: usize,
75    /// Lines inside unsafe blocks
76    pub unsafe_lines: usize,
77    /// Unsafe density as percentage
78    pub unsafe_density_percent: f64,
79    /// List of all unsafe blocks found
80    pub unsafe_blocks: Vec<UnsafeBlock>,
81    /// Average confidence score across all blocks
82    pub average_confidence: f64,
83}
84
85impl UnsafeAuditReport {
86    /// Create a new audit report
87    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    /// Check if unsafe density meets the <5% target
114    pub fn meets_density_target(&self) -> bool {
115        self.unsafe_density_percent < 5.0
116    }
117
118    /// Get blocks with high confidence for elimination (≥70)
119    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
127/// Main auditor for analyzing unsafe code
128pub struct UnsafeAuditor {
129    unsafe_blocks: Vec<UnsafeBlock>,
130    total_lines: usize,
131    unsafe_lines: usize,
132    source_code: String,
133}
134
135impl UnsafeAuditor {
136    /// Create a new auditor
137    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    /// Analyze Rust source code and generate an audit report
147    pub fn audit(&mut self, rust_code: &str) -> Result<UnsafeAuditReport> {
148        // Store source code for line counting
149        self.source_code = rust_code.to_string();
150
151        // Count total lines
152        self.total_lines = rust_code.lines().count();
153
154        // Parse the Rust code
155        let syntax_tree = syn::parse_file(rust_code).context("Failed to parse Rust code")?;
156
157        // Visit the AST to find unsafe blocks
158        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    /// Detect the pattern type and assign confidence score
168    fn analyze_unsafe_block(&self, unsafe_block: &ExprUnsafe) -> (UnsafePattern, u8, String) {
169        // Convert block to string for pattern matching
170        let block_str = quote::quote!(#unsafe_block).to_string();
171
172        // Detect patterns and assign confidence scores
173        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    /// Count lines in an unsafe block
216    fn count_block_lines(&self, block: &Block) -> usize {
217        // Rough approximation: count statements and add braces
218        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    /// Visit expressions to find unsafe blocks
230    fn visit_expr(&mut self, expr: &'ast Expr) {
231        if let Expr::Unsafe(unsafe_expr) = expr {
232            // Found an unsafe block!
233            let (pattern, confidence, suggestion) = self.analyze_unsafe_block(unsafe_expr);
234
235            // Count lines in this unsafe block
236            let block_lines = self.count_block_lines(&unsafe_expr.block);
237            self.unsafe_lines += block_lines;
238
239            // Get line number (approximation using span start)
240            let line = 0; // syn doesn't provide easy line number access without proc_macro2 spans
241
242            self.unsafe_blocks.push(UnsafeBlock {
243                line,
244                confidence,
245                pattern,
246                suggestion,
247            });
248        }
249
250        // Continue visiting nested expressions
251        syn::visit::visit_expr(self, expr);
252    }
253
254    /// Visit items to find unsafe functions
255    fn visit_item_fn(&mut self, func: &'ast ItemFn) {
256        // Check if function is marked unsafe
257        if func.sig.unsafety.is_some() {
258            // Unsafe function - count the entire body as unsafe
259            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        // Continue visiting the function body
271        syn::visit::visit_item_fn(self, func);
272    }
273}
274
275/// Convenience function to audit Rust code
276///
277/// # Example
278///
279/// ```
280/// use decy_verify::audit_rust_code;
281///
282/// let code = "fn safe_function() { let x = 42; }";
283/// let report = audit_rust_code(code).expect("Audit failed");
284/// assert_eq!(report.unsafe_blocks.len(), 0);
285/// ```
286pub 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    // RED PHASE: These tests will FAIL
296    // Testing unsafe block detection
297
298    #[test]
299    fn test_no_unsafe_blocks() {
300        // RED: This should pass (no unsafe blocks)
301        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        // RED: This will FAIL - we don't detect unsafe blocks yet
317        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        // RED: This will FAIL
338        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        // RED: This will FAIL
363        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        // Total lines: 9, unsafe lines: 3 (lines 5-7)
376        // Density should be around 33%
377        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        // RED: This will FAIL
384        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        // Should detect nested blocks (implementation choice: count as 2 or 1)
397        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        // RED: This will FAIL
406        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        // RED: This will FAIL - confidence scoring not implemented
435        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        // RED: This will FAIL - pattern detection not implemented
455        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        // RED: This will FAIL - suggestions not implemented
475        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        // RED: This will FAIL
494        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        // Assuming we'll score some blocks as high confidence
503        // This tests the filtering logic
504        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        // RED: This will FAIL
511        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        // This should pass (edge case)
525        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        // Should return error, not panic
534        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        // RED: This will FAIL - unsafe fn detection
542        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        // Should detect unsafe function (entire function body is unsafe context)
550        assert!(!report.unsafe_blocks.is_empty() || report.unsafe_lines > 0);
551    }
552}