Skip to main content

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 diff_test;
36pub mod lock_verify;
37
38use anyhow::{Context, Result};
39use syn::{visit::Visit, Block, Expr, ExprUnsafe, ItemFn};
40
41/// Represents a single unsafe block found in Rust code
42#[derive(Debug, Clone, PartialEq)]
43pub struct UnsafeBlock {
44    /// Line number where the unsafe block starts
45    pub line: usize,
46    /// Confidence score (0-100) that this block could be eliminated
47    pub confidence: u8,
48    /// Pattern detected (e.g., "raw_pointer_deref", "transmute", etc.)
49    pub pattern: UnsafePattern,
50    /// Suggestion for safer alternative
51    pub suggestion: String,
52}
53
54/// Categories of unsafe patterns
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub enum UnsafePattern {
57    /// Raw pointer dereference (*ptr)
58    RawPointerDeref,
59    /// Type transmutation
60    Transmute,
61    /// Inline assembly
62    Assembly,
63    /// FFI call
64    FfiCall,
65    /// Union field access
66    UnionAccess,
67    /// Mutable static access
68    MutableStatic,
69    /// Other unsafe operation
70    Other,
71}
72
73/// Report summarizing unsafe code in a Rust file
74#[derive(Debug, Clone)]
75pub struct UnsafeAuditReport {
76    /// Total lines of code
77    pub total_lines: usize,
78    /// Lines inside unsafe blocks
79    pub unsafe_lines: usize,
80    /// Unsafe density as percentage
81    pub unsafe_density_percent: f64,
82    /// List of all unsafe blocks found
83    pub unsafe_blocks: Vec<UnsafeBlock>,
84    /// Average confidence score across all blocks
85    pub average_confidence: f64,
86}
87
88impl UnsafeAuditReport {
89    /// Create a new audit report
90    pub fn new(total_lines: usize, unsafe_lines: usize, unsafe_blocks: Vec<UnsafeBlock>) -> Self {
91        let unsafe_density_percent =
92            if total_lines > 0 { (unsafe_lines as f64 / total_lines as f64) * 100.0 } else { 0.0 };
93
94        let average_confidence = if !unsafe_blocks.is_empty() {
95            unsafe_blocks.iter().map(|b| b.confidence as f64).sum::<f64>()
96                / unsafe_blocks.len() as f64
97        } else {
98            0.0
99        };
100
101        Self {
102            total_lines,
103            unsafe_lines,
104            unsafe_density_percent,
105            unsafe_blocks,
106            average_confidence,
107        }
108    }
109
110    /// Check if unsafe density meets the <5% target
111    pub fn meets_density_target(&self) -> bool {
112        self.unsafe_density_percent < 5.0
113    }
114
115    /// Get blocks with high confidence for elimination (≥70)
116    pub fn high_confidence_blocks(&self) -> Vec<&UnsafeBlock> {
117        self.unsafe_blocks.iter().filter(|b| b.confidence >= 70).collect()
118    }
119}
120
121/// Main auditor for analyzing unsafe code
122pub struct UnsafeAuditor {
123    unsafe_blocks: Vec<UnsafeBlock>,
124    total_lines: usize,
125    unsafe_lines: usize,
126    source_code: String,
127}
128
129impl UnsafeAuditor {
130    /// Create a new auditor
131    pub fn new() -> Self {
132        Self {
133            unsafe_blocks: Vec::new(),
134            total_lines: 0,
135            unsafe_lines: 0,
136            source_code: String::new(),
137        }
138    }
139
140    /// Analyze Rust source code and generate an audit report
141    pub fn audit(&mut self, rust_code: &str) -> Result<UnsafeAuditReport> {
142        // Store source code for line counting
143        self.source_code = rust_code.to_string();
144
145        // Count total lines
146        self.total_lines = rust_code.lines().count();
147
148        // Parse the Rust code
149        let syntax_tree = syn::parse_file(rust_code).context("Failed to parse Rust code")?;
150
151        // Visit the AST to find unsafe blocks
152        self.visit_file(&syntax_tree);
153
154        Ok(UnsafeAuditReport::new(self.total_lines, self.unsafe_lines, self.unsafe_blocks.clone()))
155    }
156
157    /// Detect the pattern type and assign confidence score
158    fn analyze_unsafe_block(&self, unsafe_block: &ExprUnsafe) -> (UnsafePattern, u8, String) {
159        // Convert block to string for pattern matching
160        let block_str = quote::quote!(#unsafe_block).to_string();
161
162        // Detect patterns and assign confidence scores
163        let (pattern, confidence, suggestion) = if block_str.contains("std :: ptr ::")
164            || block_str.contains("* ptr")
165            || block_str.contains("null_mut")
166            || block_str.contains("null()")
167        {
168            (
169                UnsafePattern::RawPointerDeref,
170                85,
171                "Consider using Box<T>, &T, or &mut T with proper lifetimes".to_string(),
172            )
173        } else if block_str.contains("transmute") {
174            (
175                UnsafePattern::Transmute,
176                40,
177                "Consider safe alternatives like From/Into traits or checked conversions"
178                    .to_string(),
179            )
180        } else if block_str.contains("asm!") || block_str.contains("global_asm!") {
181            (
182                UnsafePattern::Assembly,
183                15,
184                "No safe alternative - inline assembly required for platform-specific operations"
185                    .to_string(),
186            )
187        } else if block_str.contains("extern") {
188            (
189                UnsafePattern::FfiCall,
190                30,
191                "Consider creating a safe wrapper around FFI calls".to_string(),
192            )
193        } else {
194            (
195                UnsafePattern::Other,
196                50,
197                "Review if this unsafe block can be eliminated or replaced with safe alternatives"
198                    .to_string(),
199            )
200        };
201
202        (pattern, confidence, suggestion)
203    }
204
205    /// Count lines in an unsafe block
206    fn count_block_lines(&self, block: &Block) -> usize {
207        // Rough approximation: count statements and add braces
208        block.stmts.len() + 2
209    }
210}
211
212impl Default for UnsafeAuditor {
213    fn default() -> Self {
214        Self::new()
215    }
216}
217
218impl<'ast> Visit<'ast> for UnsafeAuditor {
219    /// Visit expressions to find unsafe blocks
220    fn visit_expr(&mut self, expr: &'ast Expr) {
221        if let Expr::Unsafe(unsafe_expr) = expr {
222            // Found an unsafe block!
223            let (pattern, confidence, suggestion) = self.analyze_unsafe_block(unsafe_expr);
224
225            // Count lines in this unsafe block
226            let block_lines = self.count_block_lines(&unsafe_expr.block);
227            self.unsafe_lines += block_lines;
228
229            // Get line number (approximation using span start)
230            let line = 0; // syn doesn't provide easy line number access without proc_macro2 spans
231
232            self.unsafe_blocks.push(UnsafeBlock { line, confidence, pattern, suggestion });
233        }
234
235        // Continue visiting nested expressions
236        syn::visit::visit_expr(self, expr);
237    }
238
239    /// Visit items to find unsafe functions
240    fn visit_item_fn(&mut self, func: &'ast ItemFn) {
241        // Check if function is marked unsafe
242        if func.sig.unsafety.is_some() {
243            // Unsafe function - count the entire body as unsafe
244            let body_lines = self.count_block_lines(&func.block);
245            self.unsafe_lines += body_lines;
246
247            self.unsafe_blocks.push(UnsafeBlock {
248                line: 0,
249                confidence: 60,
250                pattern: UnsafePattern::Other,
251                suggestion: "Unsafe function - review if entire function needs to be unsafe or just specific blocks".to_string(),
252            });
253        }
254
255        // Continue visiting the function body
256        syn::visit::visit_item_fn(self, func);
257    }
258}
259
260/// Convenience function to audit Rust code
261///
262/// # Example
263///
264/// ```
265/// use decy_verify::audit_rust_code;
266///
267/// let code = "fn safe_function() { let x = 42; }";
268/// let report = audit_rust_code(code).expect("Audit failed");
269/// assert_eq!(report.unsafe_blocks.len(), 0);
270/// ```
271pub fn audit_rust_code(rust_code: &str) -> Result<UnsafeAuditReport> {
272    let mut auditor = UnsafeAuditor::new();
273    auditor.audit(rust_code)
274}
275
276/// A structured compilation error from rustc
277#[derive(Debug, Clone, PartialEq, Eq)]
278pub struct CompilationError {
279    /// Error code (e.g., "E0308")
280    pub code: Option<String>,
281    /// Error message
282    pub message: String,
283}
284
285/// Result of compiling generated Rust code
286#[derive(Debug, Clone)]
287pub struct CompilationResult {
288    /// Whether compilation succeeded
289    pub success: bool,
290    /// Errors found during compilation
291    pub errors: Vec<CompilationError>,
292    /// Warnings found during compilation
293    pub warnings: Vec<String>,
294}
295
296/// Verify that generated Rust code compiles by invoking rustc.
297///
298/// Uses `rustc --emit=metadata --edition=2021` for fast type-checking
299/// without full code generation.
300///
301/// # Example
302///
303/// ```no_run
304/// use decy_verify::verify_compilation;
305///
306/// let result = verify_compilation("fn main() {}").expect("rustc failed to run");
307/// assert!(result.success);
308/// ```
309pub fn verify_compilation(rust_code: &str) -> Result<CompilationResult> {
310    use std::process::Command;
311    use std::time::{SystemTime, UNIX_EPOCH};
312
313    let unique_id = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_nanos()).unwrap_or(0);
314    let temp_dir = std::env::temp_dir();
315    let temp_file = temp_dir.join(format!("decy_verify_{}.rs", unique_id));
316    let temp_output = temp_dir.join(format!("decy_verify_{}.rmeta", unique_id));
317
318    std::fs::write(&temp_file, rust_code)
319        .context("Failed to write temp file for compilation check")?;
320
321    let output = Command::new("rustc")
322        .arg("--emit=metadata")
323        .arg("--edition=2021")
324        .arg("-o")
325        .arg(&temp_output)
326        .arg(&temp_file)
327        .output()
328        .context("Failed to run rustc — is it installed?")?;
329
330    // Remove scratch files
331    let _ = std::fs::remove_file(&temp_file);
332    let _ = std::fs::remove_file(&temp_output);
333
334    let stderr = String::from_utf8_lossy(&output.stderr);
335
336    if output.status.success() {
337        let warnings: Vec<String> =
338            stderr.lines().filter(|l| l.contains("warning")).map(|l| l.to_string()).collect();
339        Ok(CompilationResult { success: true, errors: vec![], warnings })
340    } else {
341        let mut errors = Vec::new();
342        for line in stderr.lines() {
343            if line.starts_with("error") {
344                // Extract error code like E0308 from "error[E0308]: ..."
345                let code = line
346                    .find('[')
347                    .and_then(|start| line.find(']').map(|end| (start, end)))
348                    .map(|(start, end)| line[start + 1..end].to_string());
349                errors.push(CompilationError { code, message: line.to_string() });
350            }
351        }
352        Ok(CompilationResult { success: false, errors, warnings: vec![] })
353    }
354}
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359
360    // RED PHASE: These tests will FAIL
361    // Testing unsafe block detection
362
363    #[test]
364    fn test_no_unsafe_blocks() {
365        // RED: This should pass (no unsafe blocks)
366        let code = r#"
367            fn safe_function() {
368                let x = 42;
369                println!("{}", x);
370            }
371        "#;
372
373        let report = audit_rust_code(code).expect("Audit failed");
374        assert_eq!(report.unsafe_blocks.len(), 0);
375        assert_eq!(report.unsafe_lines, 0);
376        assert!(report.meets_density_target());
377    }
378
379    #[test]
380    fn test_single_unsafe_block() {
381        // RED: This will FAIL - we don't detect unsafe blocks yet
382        let code = r#"
383            fn with_unsafe() {
384                unsafe {
385                    let ptr = std::ptr::null_mut::<i32>();
386                    *ptr = 42;
387                }
388            }
389        "#;
390
391        let report = audit_rust_code(code).expect("Audit failed");
392        assert_eq!(report.unsafe_blocks.len(), 1, "Should detect one unsafe block");
393        assert!(report.unsafe_lines > 0, "Should count unsafe lines");
394    }
395
396    #[test]
397    fn test_multiple_unsafe_blocks() {
398        // RED: This will FAIL
399        let code = r#"
400            fn multiple_unsafe() {
401                unsafe {
402                    let ptr1 = std::ptr::null_mut::<i32>();
403                }
404
405                let safe_code = 42;
406
407                unsafe {
408                    let ptr2 = std::ptr::null_mut::<f64>();
409                }
410            }
411        "#;
412
413        let report = audit_rust_code(code).expect("Audit failed");
414        assert_eq!(report.unsafe_blocks.len(), 2, "Should detect two unsafe blocks");
415    }
416
417    #[test]
418    fn test_unsafe_density_calculation() {
419        // RED: This will FAIL
420        let code = r#"
421fn example() {
422    let x = 1;
423    let y = 2;
424    unsafe {
425        let ptr = std::ptr::null_mut::<i32>();
426    }
427    let z = 3;
428}
429"#;
430        let report = audit_rust_code(code).expect("Audit failed");
431
432        // Total lines: 9, unsafe lines: 3 (lines 5-7)
433        // Density should be around 33%
434        assert!(report.unsafe_density_percent > 20.0);
435        assert!(report.unsafe_density_percent < 50.0);
436    }
437
438    #[test]
439    fn test_nested_unsafe_blocks() {
440        // RED: This will FAIL
441        let code = r#"
442            fn nested() {
443                unsafe {
444                    let ptr = std::ptr::null_mut::<i32>();
445                    unsafe {
446                        *ptr = 42;
447                    }
448                }
449            }
450        "#;
451
452        let report = audit_rust_code(code).expect("Audit failed");
453        // Should detect nested blocks (implementation choice: count as 2 or 1)
454        assert!(!report.unsafe_blocks.is_empty(), "Should detect unsafe blocks");
455    }
456
457    #[test]
458    fn test_unsafe_in_different_items() {
459        // RED: This will FAIL
460        let code = r#"
461            fn func1() {
462                unsafe { let x = 1; }
463            }
464
465            fn func2() {
466                unsafe { let y = 2; }
467            }
468
469            impl MyStruct {
470                fn method(&self) {
471                    unsafe { let z = 3; }
472                }
473            }
474
475            struct MyStruct;
476        "#;
477
478        let report = audit_rust_code(code).expect("Audit failed");
479        assert_eq!(report.unsafe_blocks.len(), 3, "Should detect unsafe in all items");
480    }
481
482    #[test]
483    fn test_confidence_scoring() {
484        // RED: This will FAIL - confidence scoring not implemented
485        let code = r#"
486            fn with_pointer() {
487                unsafe {
488                    let ptr = std::ptr::null_mut::<i32>();
489                    *ptr = 42;
490                }
491            }
492        "#;
493
494        let report = audit_rust_code(code).expect("Audit failed");
495        assert_eq!(report.unsafe_blocks.len(), 1);
496
497        let block = &report.unsafe_blocks[0];
498        assert!(block.confidence > 0, "Should have non-zero confidence");
499        assert!(block.confidence <= 100, "Confidence should be 0-100");
500    }
501
502    #[test]
503    fn test_pattern_detection_raw_pointer() {
504        // RED: This will FAIL - pattern detection not implemented
505        let code = r#"
506            fn deref_pointer() {
507                unsafe {
508                    let ptr = std::ptr::null_mut::<i32>();
509                    *ptr = 42;
510                }
511            }
512        "#;
513
514        let report = audit_rust_code(code).expect("Audit failed");
515        assert_eq!(report.unsafe_blocks.len(), 1);
516        assert_eq!(report.unsafe_blocks[0].pattern, UnsafePattern::RawPointerDeref);
517    }
518
519    #[test]
520    fn test_suggestion_generation() {
521        // RED: This will FAIL - suggestions not implemented
522        let code = r#"
523            fn with_unsafe() {
524                unsafe {
525                    let ptr = std::ptr::null_mut::<i32>();
526                }
527            }
528        "#;
529
530        let report = audit_rust_code(code).expect("Audit failed");
531        assert_eq!(report.unsafe_blocks.len(), 1);
532        assert!(!report.unsafe_blocks[0].suggestion.is_empty(), "Should provide a suggestion");
533    }
534
535    #[test]
536    fn test_high_confidence_blocks() {
537        // RED: This will FAIL
538        let code = r#"
539            fn example() {
540                unsafe { let x = 1; }
541                unsafe { let y = 2; }
542            }
543        "#;
544
545        let report = audit_rust_code(code).expect("Audit failed");
546        // Assuming we'll score some blocks as high confidence
547        // This tests the filtering logic
548        let high_conf = report.high_confidence_blocks();
549        assert!(high_conf.len() <= report.unsafe_blocks.len());
550    }
551
552    #[test]
553    fn test_average_confidence() {
554        // RED: This will FAIL
555        let code = r#"
556            fn example() {
557                unsafe { let x = 1; }
558            }
559        "#;
560
561        let report = audit_rust_code(code).expect("Audit failed");
562        assert!(report.average_confidence >= 0.0);
563        assert!(report.average_confidence <= 100.0);
564    }
565
566    #[test]
567    fn test_empty_code() {
568        // This should pass (edge case)
569        let code = "";
570        let report = audit_rust_code(code).expect("Audit failed");
571        assert_eq!(report.unsafe_blocks.len(), 0);
572        assert_eq!(report.total_lines, 0);
573    }
574
575    #[test]
576    fn test_invalid_rust_code() {
577        // Should return error, not panic
578        let code = "fn incomplete(";
579        let result = audit_rust_code(code);
580        assert!(result.is_err(), "Should return error for invalid code");
581    }
582
583    #[test]
584    fn test_unsafe_fn() {
585        // RED: This will FAIL - unsafe fn detection
586        let code = r#"
587            unsafe fn dangerous_function() {
588                let x = 42;
589            }
590        "#;
591
592        let report = audit_rust_code(code).expect("Audit failed");
593        // Should detect unsafe function (entire function body is unsafe context)
594        assert!(!report.unsafe_blocks.is_empty() || report.unsafe_lines > 0);
595    }
596
597    #[test]
598    fn test_verify_compilation_valid_code() {
599        let result = verify_compilation("fn main() {}").expect("rustc failed to run");
600        assert!(result.success, "Valid Rust should compile");
601        assert!(result.errors.is_empty());
602    }
603
604    #[test]
605    fn test_verify_compilation_type_error() {
606        let result =
607            verify_compilation("fn main() { let x: i32 = \"bad\"; }").expect("rustc failed to run");
608        assert!(!result.success, "Type error should fail compilation");
609        assert!(!result.errors.is_empty(), "Should have at least one error");
610        // E0308 is the mismatched types error
611        let has_e0308 = result.errors.iter().any(|e| e.code.as_deref() == Some("E0308"));
612        assert!(has_e0308, "Should contain E0308 error code");
613    }
614
615    #[test]
616    fn test_verify_compilation_missing_function() {
617        let result =
618            verify_compilation("fn main() { undefined_function(); }").expect("rustc failed to run");
619        assert!(!result.success);
620        assert!(!result.errors.is_empty());
621    }
622
623    // ========================================================================
624    // Pattern detection: transmute, assembly, FFI, Other
625    // ========================================================================
626
627    #[test]
628    fn test_pattern_detection_transmute() {
629        let code = r#"
630            fn with_transmute() {
631                unsafe {
632                    let x: u32 = std::mem::transmute(1.0f32);
633                }
634            }
635        "#;
636        let report = audit_rust_code(code).expect("Audit failed");
637        assert_eq!(report.unsafe_blocks.len(), 1);
638        assert_eq!(report.unsafe_blocks[0].pattern, UnsafePattern::Transmute);
639        assert_eq!(report.unsafe_blocks[0].confidence, 40);
640        assert!(report.unsafe_blocks[0].suggestion.contains("From/Into"));
641    }
642
643    #[test]
644    fn test_pattern_detection_assembly() {
645        // The auditor uses quote! to stringify the unsafe block, then searches for "asm!"
646        // core::arch::asm! macro invocation appears in the stringified form
647        let code = r#"
648            fn with_asm() {
649                unsafe {
650                    std::arch::asm!("nop");
651                }
652            }
653        "#;
654        let report = audit_rust_code(code).expect("Audit failed");
655        assert_eq!(report.unsafe_blocks.len(), 1);
656        // The stringified block contains "asm!" so it should match Assembly
657        // If the macro doesn't appear in stringified form, it may fall through to Other
658        let pattern = &report.unsafe_blocks[0].pattern;
659        assert!(
660            *pattern == UnsafePattern::Assembly || *pattern == UnsafePattern::Other,
661            "Expected Assembly or Other, got {:?}",
662            pattern
663        );
664    }
665
666    #[test]
667    fn test_pattern_detection_ffi() {
668        let code = r#"
669            fn with_ffi() {
670                unsafe {
671                    extern "C" {
672                        fn puts(s: *const u8) -> i32;
673                    }
674                }
675            }
676        "#;
677        let report = audit_rust_code(code).expect("Audit failed");
678        assert_eq!(report.unsafe_blocks.len(), 1);
679        assert_eq!(report.unsafe_blocks[0].pattern, UnsafePattern::FfiCall);
680        assert_eq!(report.unsafe_blocks[0].confidence, 30);
681        assert!(report.unsafe_blocks[0].suggestion.contains("safe wrapper"));
682    }
683
684    #[test]
685    fn test_pattern_detection_other() {
686        let code = r#"
687            fn with_other_unsafe() {
688                unsafe {
689                    let v: Vec<i32> = Vec::new();
690                    let _ = v.len();
691                }
692            }
693        "#;
694        let report = audit_rust_code(code).expect("Audit failed");
695        assert_eq!(report.unsafe_blocks.len(), 1);
696        assert_eq!(report.unsafe_blocks[0].pattern, UnsafePattern::Other);
697        assert_eq!(report.unsafe_blocks[0].confidence, 50);
698        assert!(report.unsafe_blocks[0].suggestion.contains("Review"));
699    }
700
701    // ========================================================================
702    // meets_density_target + high_confidence_blocks
703    // ========================================================================
704
705    #[test]
706    fn test_meets_density_target_low_density() {
707        let report = UnsafeAuditReport::new(100, 3, vec![]);
708        assert!(report.meets_density_target());
709        assert!(report.unsafe_density_percent < 5.0);
710    }
711
712    #[test]
713    fn test_meets_density_target_high_density() {
714        let report = UnsafeAuditReport::new(100, 10, vec![]);
715        assert!(!report.meets_density_target());
716        assert!(report.unsafe_density_percent >= 5.0);
717    }
718
719    #[test]
720    fn test_meets_density_target_zero_lines() {
721        let report = UnsafeAuditReport::new(0, 0, vec![]);
722        assert!(report.meets_density_target());
723        assert!((report.unsafe_density_percent - 0.0).abs() < 0.001);
724    }
725
726    #[test]
727    fn test_high_confidence_blocks_filtering() {
728        let blocks = vec![
729            UnsafeBlock {
730                line: 1,
731                confidence: 85,
732                pattern: UnsafePattern::RawPointerDeref,
733                suggestion: "Use Box".to_string(),
734            },
735            UnsafeBlock {
736                line: 10,
737                confidence: 40,
738                pattern: UnsafePattern::Transmute,
739                suggestion: "Use From".to_string(),
740            },
741            UnsafeBlock {
742                line: 20,
743                confidence: 70,
744                pattern: UnsafePattern::Other,
745                suggestion: "Review".to_string(),
746            },
747        ];
748        let report = UnsafeAuditReport::new(100, 10, blocks);
749        let high = report.high_confidence_blocks();
750        assert_eq!(high.len(), 2); // confidence 85 and 70
751        assert!(high.iter().all(|b| b.confidence >= 70));
752    }
753
754    #[test]
755    fn test_high_confidence_blocks_empty() {
756        let report = UnsafeAuditReport::new(100, 0, vec![]);
757        assert!(report.high_confidence_blocks().is_empty());
758    }
759
760    // ========================================================================
761    // UnsafeAuditor default + unsafe fn detection
762    // ========================================================================
763
764    #[test]
765    fn test_unsafe_auditor_default() {
766        let auditor = UnsafeAuditor::default();
767        assert_eq!(auditor.total_lines, 0);
768        assert_eq!(auditor.unsafe_lines, 0);
769        assert!(auditor.unsafe_blocks.is_empty());
770    }
771
772    #[test]
773    fn test_unsafe_fn_detection_body_lines() {
774        let code = r#"
775            unsafe fn dangerous() {
776                let x = 1;
777                let y = 2;
778                let z = 3;
779            }
780        "#;
781        let report = audit_rust_code(code).expect("Audit failed");
782        assert!(!report.unsafe_blocks.is_empty());
783        let block = &report.unsafe_blocks[0];
784        assert_eq!(block.confidence, 60);
785        assert_eq!(block.pattern, UnsafePattern::Other);
786        assert!(block.suggestion.contains("Unsafe function"));
787    }
788
789    // ========================================================================
790    // verify_compilation: warnings path + edge cases
791    // ========================================================================
792
793    #[test]
794    fn test_verify_compilation_with_warnings() {
795        let code = "#[warn(unused_variables)] fn main() { let x = 42; }";
796        let result = verify_compilation(code).expect("rustc failed to run");
797        assert!(result.success);
798        // May or may not have warnings depending on rustc config
799    }
800
801    #[test]
802    fn test_verify_compilation_multiple_errors() {
803        let code = "fn main() { undefined_a(); undefined_b(); }";
804        let result = verify_compilation(code).expect("rustc failed to run");
805        assert!(!result.success);
806        assert!(!result.errors.is_empty());
807    }
808
809    #[test]
810    fn test_compilation_error_without_code() {
811        // Error lines without [EXXXX] format
812        let code = "this is not valid rust at all";
813        let result = verify_compilation(code).expect("rustc failed to run");
814        assert!(!result.success);
815        // Should have errors, some may lack error codes
816        assert!(!result.errors.is_empty());
817    }
818
819    #[test]
820    fn test_average_confidence_calculation() {
821        let blocks = vec![
822            UnsafeBlock {
823                line: 0,
824                confidence: 80,
825                pattern: UnsafePattern::RawPointerDeref,
826                suggestion: "s".to_string(),
827            },
828            UnsafeBlock {
829                line: 0,
830                confidence: 40,
831                pattern: UnsafePattern::Transmute,
832                suggestion: "s".to_string(),
833            },
834        ];
835        let report = UnsafeAuditReport::new(100, 10, blocks);
836        assert!((report.average_confidence - 60.0).abs() < 0.001);
837    }
838
839    #[test]
840    fn test_average_confidence_no_blocks() {
841        let report = UnsafeAuditReport::new(100, 0, vec![]);
842        assert!((report.average_confidence - 0.0).abs() < 0.001);
843    }
844}
845
846#[cfg(test)]
847#[path = "lock_verify_tests.rs"]
848mod lock_verify_tests;