@@ -594,7 +594,8 @@ impl Analyzer {
594594
595595 if path. is_dir ( ) {
596596 let name = path. file_name ( ) . and_then ( |n| n. to_str ( ) ) . unwrap_or ( "" ) ;
597- // Skip build artifacts, hidden dirs, and dependency dirs
597+ // Skip build artifacts, hidden dirs, dependency dirs, and
598+ // generated runtime artifacts (e.g. amuck mutation runs).
598599 if ![
599600 "target" ,
600601 "build" ,
@@ -608,6 +609,7 @@ impl Analyzer {
608609 "deps" ,
609610 "_deps" ,
610611 "zig-cache" ,
612+ ".zig-cache" ,
611613 "zig-out" ,
612614 ".elixir_ls" ,
613615 ".lexical" ,
@@ -619,6 +621,7 @@ impl Analyzer {
619621 ".nimble" ,
620622 ".dub" ,
621623 "obj" ,
624+ "runtime" ,
622625 ]
623626 . contains ( & name)
624627 {
@@ -677,6 +680,7 @@ impl Analyzer {
677680 "dist-newstyle" ,
678681 "deps" ,
679682 "zig-cache" ,
683+ ".zig-cache" ,
680684 "zig-out" ,
681685 "ebin" ,
682686 "external_corpora" ,
@@ -686,6 +690,7 @@ impl Analyzer {
686690 "fixtures" ,
687691 "corpus" ,
688692 "corpora" ,
693+ "runtime" ,
689694 ]
690695 . contains ( & name_str)
691696 {
@@ -748,8 +753,14 @@ impl Analyzer {
748753 } ) ;
749754 }
750755
756+ // For dangerous pattern checks (transmute, mem::forget, raw pointer casts),
757+ // strip string literal contents first. Detection tools like panic-attack
758+ // itself embed these patterns as string literals for matching — scanning the
759+ // literals would produce false positives.
760+ let code_only = Self :: strip_string_literals_rs ( content) ;
761+
751762 // mem::transmute — type-punning bypasses Rust's type system entirely
752- if content . contains ( "transmute(" ) || content . contains ( "transmute::<" ) {
763+ if code_only . contains ( "transmute(" ) || code_only . contains ( "transmute::<" ) {
753764 weak_points. push ( WeakPoint {
754765 category : WeakPointCategory :: UnsafeCode ,
755766 location : Some ( file_path. to_string ( ) ) ,
@@ -760,7 +771,7 @@ impl Analyzer {
760771 }
761772
762773 // mem::forget — deliberately leaks resources without running destructors
763- if content . contains ( "mem::forget(" ) || content . contains ( "forget(" ) && content . contains ( "use std::mem" ) {
774+ if code_only . contains ( "mem::forget(" ) || ( code_only . contains ( "forget(" ) && code_only . contains ( "use std::mem" ) ) {
764775 weak_points. push ( WeakPoint {
765776 category : WeakPointCategory :: ResourceLeak ,
766777 location : Some ( file_path. to_string ( ) ) ,
@@ -771,7 +782,7 @@ impl Analyzer {
771782 }
772783
773784 // Raw pointer casts — escape safe Rust's borrow checker guarantees
774- if content . contains ( "as *const " ) || content . contains ( "as *mut " ) {
785+ if code_only . contains ( "as *const " ) || code_only . contains ( "as *mut " ) {
775786 weak_points. push ( WeakPoint {
776787 category : WeakPointCategory :: UnsafeCode ,
777788 location : Some ( file_path. to_string ( ) ) ,
@@ -784,6 +795,95 @@ impl Analyzer {
784795 Ok ( ( ) )
785796 }
786797
798+ /// Strip the contents (but not the delimiters) of Rust string literals from
799+ /// `content`, replacing each string body with a single space.
800+ ///
801+ /// This prevents dangerous-pattern checks from triggering on detection
802+ /// strings embedded as literals (e.g. `content.contains("transmute(")`).
803+ /// Both regular strings (`"…"`) and raw strings (`r"…"`, `r#"…"#`, …) are
804+ /// handled. The function is intentionally conservative — it is not a full
805+ /// Rust parser and will not strip every edge case (e.g. string interpolation
806+ /// via macros), but it eliminates the common false-positive sources.
807+ fn strip_string_literals_rs ( content : & str ) -> String {
808+ let mut out = String :: with_capacity ( content. len ( ) ) ;
809+ let chars: Vec < char > = content. chars ( ) . collect ( ) ;
810+ let n = chars. len ( ) ;
811+ let mut i = 0 ;
812+
813+ while i < n {
814+ // Raw string: optional 'b' prefix then r#*"…"#*
815+ let is_raw = chars[ i] == 'r'
816+ || ( chars[ i] == 'b' && i + 1 < n && chars[ i + 1 ] == 'r' ) ;
817+
818+ if is_raw {
819+ let prefix_end = if chars[ i] == 'b' { i + 2 } else { i + 1 } ;
820+ let mut j = prefix_end;
821+ while j < n && chars[ j] == '#' {
822+ j += 1 ;
823+ }
824+ let hash_count = j - prefix_end;
825+ if j < n && chars[ j] == '"' {
826+ // Raw string opener confirmed — emit prefix + opening delimiter.
827+ for k in i..=j {
828+ out. push ( chars[ k] ) ;
829+ }
830+ i = j + 1 ;
831+ // Build the closing sequence: " followed by hash_count '#'.
832+ let closing: Vec < char > = std:: iter:: once ( '"' )
833+ . chain ( std:: iter:: repeat ( '#' ) . take ( hash_count) )
834+ . collect ( ) ;
835+ // Search for the closing sequence.
836+ let mut found = false ;
837+ let remaining = n. saturating_sub ( i) ;
838+ for start in 0 ..=remaining. saturating_sub ( closing. len ( ) ) {
839+ if i + start + closing. len ( ) <= n
840+ && chars[ i + start..i + start + closing. len ( ) ] == closing[ ..]
841+ {
842+ out. push ( ' ' ) ;
843+ out. extend ( closing. iter ( ) ) ;
844+ i += start + closing. len ( ) ;
845+ found = true ;
846+ break ;
847+ }
848+ }
849+ if !found {
850+ // Unterminated raw string — copy remainder verbatim.
851+ out. extend ( chars[ i..] . iter ( ) ) ;
852+ break ;
853+ }
854+ continue ;
855+ }
856+ // Not a raw string opener — fall through to normal char handling.
857+ }
858+
859+ match chars[ i] {
860+ '"' => {
861+ // Regular string literal.
862+ out. push ( '"' ) ;
863+ i += 1 ;
864+ while i < n {
865+ if chars[ i] == '\\' {
866+ i += 2 ; // skip escape sequence (one extra char)
867+ } else if chars[ i] == '"' {
868+ break ;
869+ } else {
870+ i += 1 ;
871+ }
872+ }
873+ out. push ( '"' ) ;
874+ if i < n {
875+ i += 1 ;
876+ }
877+ }
878+ c => {
879+ out. push ( c) ;
880+ i += 1 ;
881+ }
882+ }
883+ }
884+ out
885+ }
886+
787887 fn analyze_c_cpp (
788888 & self ,
789889 content : & str ,
0 commit comments