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
35pub mod lock_verify;
36
37use anyhow::{Context, Result};
38use syn::{visit::Visit, Block, Expr, ExprUnsafe, ItemFn};
39
40/// Represents a single unsafe block found in Rust code
41#[derive(Debug, Clone, PartialEq)]
42pub struct UnsafeBlock {
43    /// Line number where the unsafe block starts
44    pub line: usize,
45    /// Confidence score (0-100) that this block could be eliminated
46    pub confidence: u8,
47    /// Pattern detected (e.g., "raw_pointer_deref", "transmute", etc.)
48    pub pattern: UnsafePattern,
49    /// Suggestion for safer alternative
50    pub suggestion: String,
51}
52
53/// Categories of unsafe patterns
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub enum UnsafePattern {
56    /// Raw pointer dereference (*ptr)
57    RawPointerDeref,
58    /// Type transmutation
59    Transmute,
60    /// Inline assembly
61    Assembly,
62    /// FFI call
63    FfiCall,
64    /// Union field access
65    UnionAccess,
66    /// Mutable static access
67    MutableStatic,
68    /// Other unsafe operation
69    Other,
70}
71
72/// Report summarizing unsafe code in a Rust file
73#[derive(Debug, Clone)]
74pub struct UnsafeAuditReport {
75    /// Total lines of code
76    pub total_lines: usize,
77    /// Lines inside unsafe blocks
78    pub unsafe_lines: usize,
79    /// Unsafe density as percentage
80    pub unsafe_density_percent: f64,
81    /// List of all unsafe blocks found
82    pub unsafe_blocks: Vec<UnsafeBlock>,
83    /// Average confidence score across all blocks
84    pub average_confidence: f64,
85}
86
87impl UnsafeAuditReport {
88    /// Create a new audit report
89    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    /// Check if unsafe density meets the <5% target
116    pub fn meets_density_target(&self) -> bool {
117        self.unsafe_density_percent < 5.0
118    }
119
120    /// Get blocks with high confidence for elimination (≥70)
121    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
129/// Main auditor for analyzing unsafe code
130pub struct UnsafeAuditor {
131    unsafe_blocks: Vec<UnsafeBlock>,
132    total_lines: usize,
133    unsafe_lines: usize,
134    source_code: String,
135}
136
137impl UnsafeAuditor {
138    /// Create a new auditor
139    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    /// Analyze Rust source code and generate an audit report
149    pub fn audit(&mut self, rust_code: &str) -> Result<UnsafeAuditReport> {
150        // Store source code for line counting
151        self.source_code = rust_code.to_string();
152
153        // Count total lines
154        self.total_lines = rust_code.lines().count();
155
156        // Parse the Rust code
157        let syntax_tree = syn::parse_file(rust_code).context("Failed to parse Rust code")?;
158
159        // Visit the AST to find unsafe blocks
160        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    /// Detect the pattern type and assign confidence score
170    fn analyze_unsafe_block(&self, unsafe_block: &ExprUnsafe) -> (UnsafePattern, u8, String) {
171        // Convert block to string for pattern matching
172        let block_str = quote::quote!(#unsafe_block).to_string();
173
174        // Detect patterns and assign confidence scores
175        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    /// Count lines in an unsafe block
218    fn count_block_lines(&self, block: &Block) -> usize {
219        // Rough approximation: count statements and add braces
220        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    /// Visit expressions to find unsafe blocks
232    fn visit_expr(&mut self, expr: &'ast Expr) {
233        if let Expr::Unsafe(unsafe_expr) = expr {
234            // Found an unsafe block!
235            let (pattern, confidence, suggestion) = self.analyze_unsafe_block(unsafe_expr);
236
237            // Count lines in this unsafe block
238            let block_lines = self.count_block_lines(&unsafe_expr.block);
239            self.unsafe_lines += block_lines;
240
241            // Get line number (approximation using span start)
242            let line = 0; // syn doesn't provide easy line number access without proc_macro2 spans
243
244            self.unsafe_blocks.push(UnsafeBlock {
245                line,
246                confidence,
247                pattern,
248                suggestion,
249            });
250        }
251
252        // Continue visiting nested expressions
253        syn::visit::visit_expr(self, expr);
254    }
255
256    /// Visit items to find unsafe functions
257    fn visit_item_fn(&mut self, func: &'ast ItemFn) {
258        // Check if function is marked unsafe
259        if func.sig.unsafety.is_some() {
260            // Unsafe function - count the entire body as unsafe
261            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        // Continue visiting the function body
273        syn::visit::visit_item_fn(self, func);
274    }
275}
276
277/// Convenience function to audit Rust code
278///
279/// # Example
280///
281/// ```
282/// use decy_verify::audit_rust_code;
283///
284/// let code = "fn safe_function() { let x = 42; }";
285/// let report = audit_rust_code(code).expect("Audit failed");
286/// assert_eq!(report.unsafe_blocks.len(), 0);
287/// ```
288pub 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    // RED PHASE: These tests will FAIL
298    // Testing unsafe block detection
299
300    #[test]
301    fn test_no_unsafe_blocks() {
302        // RED: This should pass (no unsafe blocks)
303        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        // RED: This will FAIL - we don't detect unsafe blocks yet
319        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        // RED: This will FAIL
340        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        // RED: This will FAIL
365        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        // Total lines: 9, unsafe lines: 3 (lines 5-7)
378        // Density should be around 33%
379        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        // RED: This will FAIL
386        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        // Should detect nested blocks (implementation choice: count as 2 or 1)
399        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        // RED: This will FAIL
408        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        // RED: This will FAIL - confidence scoring not implemented
437        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        // RED: This will FAIL - pattern detection not implemented
457        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        // RED: This will FAIL - suggestions not implemented
477        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        // RED: This will FAIL
496        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        // Assuming we'll score some blocks as high confidence
505        // This tests the filtering logic
506        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        // RED: This will FAIL
513        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        // This should pass (edge case)
527        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        // Should return error, not panic
536        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        // RED: This will FAIL - unsafe fn detection
544        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        // Should detect unsafe function (entire function body is unsafe context)
552        assert!(!report.unsafe_blocks.is_empty() || report.unsafe_lines > 0);
553    }
554}