@@ -253,7 +253,7 @@ impl Analyzer {
253253 Ok ( b) => b,
254254 Err ( e) => {
255255 if self . verbose {
256- eprintln ! ( "Skipping unreadable file: {} ({})" , file. display( ) , e) ;
256+ log :: debug !( "Skipping unreadable file: {} ({})" , file. display( ) , e) ;
257257 }
258258 continue ;
259259 }
@@ -267,7 +267,7 @@ impl Analyzer {
267267 let ( cow, _, had_errors) = encoding_rs:: WINDOWS_1252 . decode ( & raw_bytes) ;
268268 if had_errors {
269269 if self . verbose {
270- eprintln ! (
270+ log :: debug !(
271271 "Skipping non-text file: {} (neither UTF-8 nor Latin-1)" ,
272272 file. display( )
273273 ) ;
@@ -3232,3 +3232,339 @@ impl Analyzer {
32323232 }
32333233 }
32343234}
3235+
3236+ #[ cfg( test) ]
3237+ mod tests {
3238+ use super :: * ;
3239+ use std:: fs;
3240+ use tempfile:: TempDir ;
3241+
3242+ // ---------------------------------------------------------------
3243+ // 1. Language::detect for different file extensions
3244+ // ---------------------------------------------------------------
3245+
3246+ #[ test]
3247+ fn language_detect_common_extensions ( ) {
3248+ assert_eq ! ( Language :: detect( "main.rs" ) , Language :: Rust ) ;
3249+ assert_eq ! ( Language :: detect( "lib.c" ) , Language :: C ) ;
3250+ assert_eq ! ( Language :: detect( "app.cpp" ) , Language :: Cpp ) ;
3251+ assert_eq ! ( Language :: detect( "server.go" ) , Language :: Go ) ;
3252+ assert_eq ! ( Language :: detect( "script.py" ) , Language :: Python ) ;
3253+ assert_eq ! ( Language :: detect( "index.js" ) , Language :: JavaScript ) ;
3254+ assert_eq ! ( Language :: detect( "app.rb" ) , Language :: Ruby ) ;
3255+ assert_eq ! ( Language :: detect( "mix.ex" ) , Language :: Elixir ) ;
3256+ assert_eq ! ( Language :: detect( "gen_server.erl" ) , Language :: Erlang ) ;
3257+ assert_eq ! ( Language :: detect( "router.gleam" ) , Language :: Gleam ) ;
3258+ assert_eq ! ( Language :: detect( "README.md" ) , Language :: Unknown ) ;
3259+ assert_eq ! ( Language :: detect( "Makefile" ) , Language :: Unknown ) ;
3260+ }
3261+
3262+ #[ test]
3263+ fn language_detect_typescript_maps_to_javascript ( ) {
3264+ assert_eq ! ( Language :: detect( "component.ts" ) , Language :: JavaScript ) ;
3265+ assert_eq ! ( Language :: detect( "component.tsx" ) , Language :: JavaScript ) ;
3266+ assert_eq ! ( Language :: detect( "page.jsx" ) , Language :: JavaScript ) ;
3267+ }
3268+
3269+ // ---------------------------------------------------------------
3270+ // 2. Analyzer::new() with a valid temp directory
3271+ // ---------------------------------------------------------------
3272+
3273+ #[ test]
3274+ fn analyzer_new_valid_directory ( ) {
3275+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
3276+ // Create a Rust file so language detection succeeds
3277+ fs:: write ( tmp. path ( ) . join ( "main.rs" ) , "fn main() {}" ) . unwrap ( ) ;
3278+ let analyzer = Analyzer :: new ( tmp. path ( ) ) ;
3279+ assert ! ( analyzer. is_ok( ) , "Analyzer::new should succeed on a valid directory with source files" ) ;
3280+ }
3281+
3282+ // ---------------------------------------------------------------
3283+ // 3. Analyzer::new() with a non-existent path
3284+ // ---------------------------------------------------------------
3285+
3286+ #[ test]
3287+ fn analyzer_new_nonexistent_path ( ) {
3288+ let result = Analyzer :: new ( Path :: new ( "/tmp/this_path_definitely_does_not_exist_29387" ) ) ;
3289+ assert ! ( result. is_err( ) , "Analyzer::new should error on non-existent path" ) ;
3290+ let err_msg = result. err ( ) . expect ( "expected error" ) . to_string ( ) ;
3291+ assert ! (
3292+ err_msg. contains( "does not exist" ) ,
3293+ "Error message should mention 'does not exist', got: {err_msg}"
3294+ ) ;
3295+ }
3296+
3297+ // ---------------------------------------------------------------
3298+ // 4. analyze() on a Rust file containing `unsafe {}` — UnsafeCode
3299+ // ---------------------------------------------------------------
3300+
3301+ #[ test]
3302+ fn analyze_rust_detects_unsafe_code ( ) {
3303+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
3304+ let rust_file = tmp. path ( ) . join ( "danger.rs" ) ;
3305+ fs:: write (
3306+ & rust_file,
3307+ r#"
3308+ fn safe_wrapper() {
3309+ unsafe {
3310+ let ptr = std::ptr::null::<u8>();
3311+ *ptr;
3312+ }
3313+ }
3314+ "# ,
3315+ )
3316+ . unwrap ( ) ;
3317+
3318+ let analyzer = Analyzer :: new ( & rust_file) . unwrap ( ) ;
3319+ let report = analyzer. analyze ( ) . unwrap ( ) ;
3320+
3321+ let unsafe_points: Vec < _ > = report
3322+ . weak_points
3323+ . iter ( )
3324+ . filter ( |wp| wp. category == WeakPointCategory :: UnsafeCode )
3325+ . collect ( ) ;
3326+
3327+ assert ! (
3328+ !unsafe_points. is_empty( ) ,
3329+ "Should detect UnsafeCode weak point for file containing `unsafe {{}}`"
3330+ ) ;
3331+ }
3332+
3333+ // ---------------------------------------------------------------
3334+ // 5. analyze() on a Rust file containing `.unwrap()` — PanicPath
3335+ // ---------------------------------------------------------------
3336+
3337+ #[ test]
3338+ fn analyze_rust_detects_panic_path_from_unwrap ( ) {
3339+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
3340+ let rust_file = tmp. path ( ) . join ( "unwrappy.rs" ) ;
3341+ // The analyzer triggers PanicPath when unwrap_calls > 5,
3342+ // so we need at least 6 unwrap calls.
3343+ fs:: write (
3344+ & rust_file,
3345+ r#"
3346+ fn lots_of_unwraps() {
3347+ let a = Some(1).unwrap();
3348+ let b = Some(2).unwrap();
3349+ let c = Some(3).unwrap();
3350+ let d = Some(4).unwrap();
3351+ let e = Some(5).unwrap();
3352+ let f = Some(6).unwrap();
3353+ let g = Some(7).unwrap();
3354+ }
3355+ "# ,
3356+ )
3357+ . unwrap ( ) ;
3358+
3359+ let analyzer = Analyzer :: new ( & rust_file) . unwrap ( ) ;
3360+ let report = analyzer. analyze ( ) . unwrap ( ) ;
3361+
3362+ let panic_points: Vec < _ > = report
3363+ . weak_points
3364+ . iter ( )
3365+ . filter ( |wp| wp. category == WeakPointCategory :: PanicPath )
3366+ . collect ( ) ;
3367+
3368+ assert ! (
3369+ !panic_points. is_empty( ) ,
3370+ "Should detect PanicPath weak point when >5 unwrap() calls are present"
3371+ ) ;
3372+ }
3373+
3374+ // ---------------------------------------------------------------
3375+ // 6. analyze() on a Python file with `eval(` — DynamicCodeExecution
3376+ // ---------------------------------------------------------------
3377+
3378+ #[ test]
3379+ fn analyze_python_detects_eval ( ) {
3380+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
3381+ let py_file = tmp. path ( ) . join ( "danger.py" ) ;
3382+ fs:: write (
3383+ & py_file,
3384+ r#"
3385+ user_input = input("Enter expression: ")
3386+ result = eval(user_input)
3387+ print(result)
3388+ "# ,
3389+ )
3390+ . unwrap ( ) ;
3391+
3392+ let analyzer = Analyzer :: new ( & py_file) . unwrap ( ) ;
3393+ let report = analyzer. analyze ( ) . unwrap ( ) ;
3394+
3395+ let dyn_exec_points: Vec < _ > = report
3396+ . weak_points
3397+ . iter ( )
3398+ . filter ( |wp| wp. category == WeakPointCategory :: DynamicCodeExecution )
3399+ . collect ( ) ;
3400+
3401+ assert ! (
3402+ !dyn_exec_points. is_empty( ) ,
3403+ "Should detect DynamicCodeExecution for Python eval() usage"
3404+ ) ;
3405+ }
3406+
3407+ // ---------------------------------------------------------------
3408+ // 7. analyze() on a Go file with `exec.Command(` — CommandInjection
3409+ // ---------------------------------------------------------------
3410+
3411+ #[ test]
3412+ fn analyze_go_detects_exec_command ( ) {
3413+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
3414+ let go_file = tmp. path ( ) . join ( "runner.go" ) ;
3415+ fs:: write (
3416+ & go_file,
3417+ r#"
3418+ package main
3419+
3420+ import (
3421+ "os/exec"
3422+ "fmt"
3423+ )
3424+
3425+ func main() {
3426+ cmd := exec.Command("ls", "-la")
3427+ out, _ := cmd.Output()
3428+ fmt.Println(string(out))
3429+ }
3430+ "# ,
3431+ )
3432+ . unwrap ( ) ;
3433+
3434+ let analyzer = Analyzer :: new ( & go_file) . unwrap ( ) ;
3435+ let report = analyzer. analyze ( ) . unwrap ( ) ;
3436+
3437+ let cmd_injection_points: Vec < _ > = report
3438+ . weak_points
3439+ . iter ( )
3440+ . filter ( |wp| wp. category == WeakPointCategory :: CommandInjection )
3441+ . collect ( ) ;
3442+
3443+ assert ! (
3444+ !cmd_injection_points. is_empty( ) ,
3445+ "Should detect CommandInjection for Go exec.Command usage"
3446+ ) ;
3447+ }
3448+
3449+ // ---------------------------------------------------------------
3450+ // 8. analyze() on an empty directory — empty results
3451+ // ---------------------------------------------------------------
3452+
3453+ #[ test]
3454+ fn analyze_empty_directory_produces_empty_results ( ) {
3455+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
3456+ // Create a single file so language detection doesn't fail,
3457+ // but put nothing dangerous in it.
3458+ fs:: write ( tmp. path ( ) . join ( "empty.rs" ) , "" ) . unwrap ( ) ;
3459+
3460+ let analyzer = Analyzer :: new ( tmp. path ( ) ) . unwrap ( ) ;
3461+ let report = analyzer. analyze ( ) . unwrap ( ) ;
3462+
3463+ assert ! (
3464+ report. weak_points. is_empty( ) ,
3465+ "Empty source files should produce no weak points, got: {:?}" ,
3466+ report. weak_points,
3467+ ) ;
3468+ assert_eq ! ( report. statistics. total_lines, 0 ) ;
3469+ }
3470+
3471+ // ---------------------------------------------------------------
3472+ // 9. analyze() should skip files in excluded directories
3473+ // (walk_directory skips node_modules, target, .git, etc.)
3474+ // ---------------------------------------------------------------
3475+
3476+ #[ test]
3477+ fn analyze_skips_excluded_directories ( ) {
3478+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
3479+
3480+ // Create a benign top-level file so language detection succeeds
3481+ fs:: write ( tmp. path ( ) . join ( "lib.rs" ) , "fn safe() {}" ) . unwrap ( ) ;
3482+
3483+ // Create a node_modules directory with a dangerous file inside.
3484+ // walk_directory should skip node_modules entirely.
3485+ let excluded_dir = tmp. path ( ) . join ( "node_modules" ) ;
3486+ fs:: create_dir_all ( & excluded_dir) . unwrap ( ) ;
3487+ fs:: write (
3488+ excluded_dir. join ( "bad.rs" ) ,
3489+ "fn bad() { unsafe { std::ptr::null::<u8>().read(); } }\n " ,
3490+ )
3491+ . unwrap ( ) ;
3492+
3493+ let analyzer = Analyzer :: new ( tmp. path ( ) ) . unwrap ( ) ;
3494+ let report = analyzer. analyze ( ) . unwrap ( ) ;
3495+
3496+ // The dangerous file in node_modules should NOT produce weak points
3497+ let unsafe_in_excluded: Vec < _ > = report
3498+ . weak_points
3499+ . iter ( )
3500+ . filter ( |wp| {
3501+ wp. category == WeakPointCategory :: UnsafeCode
3502+ && wp
3503+ . location
3504+ . as_deref ( )
3505+ . map_or ( false , |loc| loc. contains ( "node_modules" ) )
3506+ } )
3507+ . collect ( ) ;
3508+
3509+ assert ! (
3510+ unsafe_in_excluded. is_empty( ) ,
3511+ "Files inside node_modules/ should be skipped during analysis"
3512+ ) ;
3513+ }
3514+
3515+ // ---------------------------------------------------------------
3516+ // 10. analyze() on a single file produces correct language field
3517+ // ---------------------------------------------------------------
3518+
3519+ #[ test]
3520+ fn analyze_single_file_reports_correct_language ( ) {
3521+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
3522+ let go_file = tmp. path ( ) . join ( "main.go" ) ;
3523+ fs:: write ( & go_file, "package main\n func main() {}\n " ) . unwrap ( ) ;
3524+
3525+ let analyzer = Analyzer :: new ( & go_file) . unwrap ( ) ;
3526+ let report = analyzer. analyze ( ) . unwrap ( ) ;
3527+
3528+ assert_eq ! (
3529+ report. language,
3530+ Language :: Go ,
3531+ "Report should identify Go as the language for a .go file"
3532+ ) ;
3533+ }
3534+
3535+ // ---------------------------------------------------------------
3536+ // 11. Rust file with few unwraps should NOT trigger PanicPath
3537+ // ---------------------------------------------------------------
3538+
3539+ #[ test]
3540+ fn analyze_rust_few_unwraps_no_panic_path ( ) {
3541+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
3542+ let rust_file = tmp. path ( ) . join ( "safe.rs" ) ;
3543+ // Only 3 unwrap calls — threshold is >5
3544+ fs:: write (
3545+ & rust_file,
3546+ r#"
3547+ fn few_unwraps() {
3548+ let a = Some(1).unwrap();
3549+ let b = Some(2).unwrap();
3550+ let c = Some(3).unwrap();
3551+ }
3552+ "# ,
3553+ )
3554+ . unwrap ( ) ;
3555+
3556+ let analyzer = Analyzer :: new ( & rust_file) . unwrap ( ) ;
3557+ let report = analyzer. analyze ( ) . unwrap ( ) ;
3558+
3559+ let panic_points: Vec < _ > = report
3560+ . weak_points
3561+ . iter ( )
3562+ . filter ( |wp| wp. category == WeakPointCategory :: PanicPath )
3563+ . collect ( ) ;
3564+
3565+ assert ! (
3566+ panic_points. is_empty( ) ,
3567+ "Should NOT trigger PanicPath when unwrap count is <= 5"
3568+ ) ;
3569+ }
3570+ }
0 commit comments