99 * Follows industry best practices based on typescript-eslint patterns.
1010 */
1111
12- import fs , { realpathSync } from 'fs' ;
12+ import fs from 'fs' ;
1313import path from 'path' ;
1414import { glob } from 'glob' ;
1515import { fileURLToPath } from 'url' ;
@@ -140,7 +140,7 @@ class AISlopDetector {
140140 // ==================== AXIS 3: STYLE / TASTE (The Vibe Check) ====================
141141 {
142142 id : 'overconfident_comment' ,
143- pattern : / \/ \/ \s * ( o b v i o u s l y | c l e a r l y | s i m p l y | j u s t | e a s y | t r i v i a l | b a s i c a l l y | l i t e r a l l y | o f c o u r s e | n a t u r a l l y | c e r t a i n l y | s u r e l y ) \b / gi,
143+ pattern : / \/ \/ . * \b ( o b v i o u s l y | c l e a r l y | s i m p l y | j u s t | e a s y | t r i v i a l | b a s i c a l l y | l i t e r a l l y | o f c o u r s e | n a t u r a l l y | c e r t a i n l y | s u r e l y ) \b / gi,
144144 message : "Overconfident comment — AI pretending it understands when it doesn't" ,
145145 severity : 'high' ,
146146 description : 'Overconfident language indicating false certainty'
@@ -169,7 +169,7 @@ class AISlopDetector {
169169 } ,
170170 {
171171 id : 'magic_css_value' ,
172- pattern : / \b ( \d { 3 , 4 } p x | # \w { 6 } | r g b a ? \( [ ^ ) ] + \) | h s l \( \d + ) / g,
172+ pattern : / ( \d { 3 , 4 } p x | # [ 0 - 9 a - f A - F ] { 3 , 8 } \b | r g b a ? \( [ ^ ) ] + \) | h s l \( \d + ) / g,
173173 message : "Magic CSS value — extract to design token or const" ,
174174 severity : 'low' ,
175175 description : 'Hardcoded CSS values that should be constants' ,
@@ -270,7 +270,7 @@ class AISlopDetector {
270270 } ,
271271 {
272272 id : 'missing_error_handling' ,
273- pattern : / ( f e t c h | a x i o s | h t t p ) \s * \( / g,
273+ pattern : / \b ( f e t c h | a x i o s | h t t p ) \s * \( / g,
274274 message : "Potential missing error handling for promise. Consider adding try/catch or .catch()." ,
275275 severity : 'medium' ,
276276 description : 'Detects calls that might need error handling' ,
@@ -289,7 +289,7 @@ class AISlopDetector {
289289 } ,
290290 {
291291 id : 'todo_comment' ,
292- pattern : / ( T O D O | F I X M E | H A C K | X X X | B U G ) \b / g,
292+ pattern : / \b ( T O D O | F I X M E | H A C K | X X X | B U G ) \b / g,
293293 message : "Found TODO/FIXME/HACK comment indicating incomplete implementation." ,
294294 severity : 'medium' ,
295295 description : 'Detects incomplete implementation markers'
@@ -492,10 +492,10 @@ class AISlopDetector {
492492 '**/coverage/**' , // Coverage reports
493493 '**/out/**' , // Next.js output directory
494494 '**/temp/**' , // Temporary files
495- '**/lib/**' , // Generated library files
496495 'scripts/ai-slop-detector.ts' , // Exclude the detector script itself to avoid false positives
497496 'ai-slop-detector.ts' , // Also exclude when in root directory
498- 'improved-ai-slop-detector.ts' // Exclude the improved detector script to avoid false positives
497+ 'improved-ai-slop-detector.ts' , // Exclude the improved detector script to avoid false positives
498+ ...this . customIgnorePaths
499499 ]
500500 } ) ;
501501
@@ -525,7 +525,7 @@ class AISlopDetector {
525525 /**
526526 * Check if a fetch call is properly handled with try/catch or .catch()
527527 */
528- private isFetchCallProperlyHandled ( lines : string [ ] , fetchLineIndex : number , fetchCallIndex : number ) : boolean {
528+ private isFetchCallProperlyHandled ( lines : string [ ] , fetchLineIndex : number ) : boolean {
529529 // Look in a reasonable range around the fetch call to see if it's in a try/catch block
530530 // or has a .catch() or similar error handling
531531
@@ -536,10 +536,11 @@ class AISlopDetector {
536536 // Look backwards to find the start of the function
537537 for ( let i = fetchLineIndex ; i >= Math . max ( 0 , fetchLineIndex - 20 ) ; i -- ) {
538538 const line = lines [ i ] ;
539+ const isReactHook = line . includes ( 'const' ) && ( line . includes ( 'useState' ) || line . includes ( 'useEffect' ) || line . includes ( 'useCallback' ) || line . includes ( 'useMemo' ) ) ;
539540 if ( line . includes ( 'async function' ) ||
540541 line . includes ( 'function' ) ||
541542 line . includes ( '=>' ) ||
542- ( line . includes ( 'const' ) && ( line . includes ( 'useState' ) || line . includes ( 'useEffect' ) || line . includes ( 'useCallback' ) || line . includes ( 'useMemo' ) ) ) ||
543+ isReactHook ||
543544 line . includes ( 'export default function' ) ) {
544545 // Check if this looks like the start of our function
545546 if ( line . includes ( '{' ) || line . includes ( '=>' ) ) {
@@ -729,21 +730,36 @@ class AISlopDetector {
729730
730731 // Special handling for missing error handling - look for properly handled fetch calls
731732 if ( pattern . id === 'missing_error_handling' ) {
733+ const fullLine = line . trim ( ) ;
734+ // Skip matches inside comment lines (single-line, JSDoc, block)
735+ if ( fullLine . startsWith ( '//' ) || fullLine . startsWith ( '*' ) || fullLine . startsWith ( '/*' ) ) {
736+ continue ;
737+ }
732738 // Check if this fetch call is part of a properly handled async function
733- const isProperlyHandled = this . isFetchCallProperlyHandled ( lines , i , match . index ) ;
739+ const isProperlyHandled = this . isFetchCallProperlyHandled ( lines , i ) ;
734740 if ( isProperlyHandled ) {
735- continue ; // Skip this fetch call as it's properly handled
741+ continue ;
736742 }
737743 }
738744
739745 // Special handling for unsafe_double_type_assertion - skip legitimate UI library patterns
740746 if ( pattern . id === 'unsafe_double_type_assertion' ) {
741- // Check the full line context to identify potentially legitimate patterns
742747 const fullLine = line . trim ( ) ;
743- // Skip patterns that are actually safe (as unknown as Type) since we changed the regex
744- // but double-check to be extra sure
748+ // Skip patterns that are actually safe (as unknown as Type)
745749 if ( fullLine . includes ( 'as unknown as' ) ) {
746- continue ; // This is actually safe - skip it
750+ continue ;
751+ }
752+ // Skip matches inside comment lines (e.g., "as soon as React")
753+ if ( fullLine . startsWith ( '//' ) || fullLine . startsWith ( '*' ) || fullLine . startsWith ( '/*' ) ) {
754+ continue ;
755+ }
756+ // Skip matches where the first word after "as" is a common English word
757+ // indicating natural language rather than a type assertion
758+ // e.g., "as soon as React hydrates" — "soon" is English, not a type
759+ const firstWord = match [ 0 ] . match ( / ^ a s \s + ( \w + ) / i) ?. [ 1 ] ?. toLowerCase ( ) ;
760+ const englishWords = [ 'soon' , 'quick' , 'quickly' , 'fast' , 'smooth' , 'long' , 'much' , 'little' , 'well' , 'good' , 'bad' , 'easy' , 'hard' , 'simple' , 'clear' , 'many' , 'few' , 'close' , 'far' , 'near' ] ;
761+ if ( firstWord && englishWords . includes ( firstWord ) ) {
762+ continue ;
747763 }
748764 }
749765
@@ -756,6 +772,20 @@ class AISlopDetector {
756772 continue ;
757773 }
758774
775+ // Skip console calls guarded by a conditional on the same line
776+ // e.g., if (isDev) console.log('debug');
777+ if ( / ^ i f \s * \( / . test ( fullLine ) ) {
778+ continue ;
779+ }
780+
781+ // Skip console calls inside a conditional block opened on a prior line
782+ if ( i > 0 ) {
783+ const prevLine = lines [ i - 1 ] . trim ( ) ;
784+ if ( / ^ i f \s * \( / . test ( prevLine ) && ! prevLine . includes ( 'function' ) && ( prevLine . includes ( '{' ) || fullLine . startsWith ( '{' ) === false ) ) {
785+ continue ;
786+ }
787+ }
788+
759789 // Skip general debugging logs that might be intentional in development
760790 if ( fullLine . includes ( 'console.log(' ) &&
761791 ( fullLine . includes ( 'Debug' ) || fullLine . includes ( 'debug' ) || fullLine . includes ( 'debug:' ) ) ) {
@@ -792,6 +822,8 @@ class AISlopDetector {
792822 }
793823 }
794824
825+
826+
795827 // In quiet mode, skip test and mock files for all patterns except production console logs
796828 if ( quiet && pattern . id !== 'production_console_log' ) {
797829 const isTestFile = filePath . includes ( '__tests__' ) ||
@@ -877,57 +909,91 @@ class AISlopDetector {
877909 * Used to determine if console.error is legitimate error handling
878910 */
879911 private isInTryCatchBlock ( lines : string [ ] , lineIndex : number ) : boolean {
880- // Look backwards from the given line to find try/catch blocks
881- let tryBlockDepth = 0 ;
882- let catchBlockStartLine = - 1 ;
912+ let braceDepth = 0 ;
913+ let inCatchBlock = false ;
914+ let catchBlockDepth = - 1 ;
915+ let nestedDepth = 0 ;
916+ let pendingExit = false ;
883917
884- // Track opening and closing braces to understand block scope
885- for ( let i = lineIndex ; i >= 0 ; i -- ) {
918+ for ( let i = 0 ; i <= lineIndex ; i ++ ) {
886919 const line = lines [ i ] ;
920+ const hasCatch = line . includes ( 'catch (' ) || line . includes ( 'catch(' ) ;
921+ const catchOnSameLineAsCloseBrace = hasCatch && line . trim ( ) . startsWith ( '}' ) ;
887922
888- // Check for catch blocks (which are often where error logging happens)
889- if ( line . includes ( 'catch (' ) ) {
890- catchBlockStartLine = i ;
891- // Find the opening brace of the catch block
892- if ( line . includes ( '{' ) ) {
893- return true ;
894- } else {
895- // If the catch is on its own line, the next line with { is the start
896- for ( let j = i + 1 ; j < Math . min ( i + 5 , lines . length ) ; j ++ ) {
897- if ( lines [ j ] . includes ( '{' ) ) {
898- return true ;
923+ for ( let j = 0 ; j < line . length ; j ++ ) {
924+ if ( line [ j ] === '{' ) {
925+ if ( inCatchBlock && ! catchOnSameLineAsCloseBrace ) {
926+ if ( braceDepth >= catchBlockDepth ) {
927+ nestedDepth ++ ;
928+ }
929+ }
930+ braceDepth ++ ;
931+ } else if ( line [ j ] === '}' ) {
932+ braceDepth -- ;
933+ if ( inCatchBlock ) {
934+ if ( nestedDepth > 0 ) {
935+ nestedDepth -- ;
936+ if ( nestedDepth === 0 ) {
937+ pendingExit = true ;
938+ }
939+ } else if ( braceDepth <= catchBlockDepth ) {
940+ inCatchBlock = false ;
941+ nestedDepth = 0 ;
942+ pendingExit = false ;
899943 }
900944 }
901945 }
902946 }
903947
904- // Check for try blocks
905- if ( line . includes ( 'try {' ) || ( line . includes ( 'try' ) && line . includes ( '{' ) ) ) {
906- if ( catchBlockStartLine > i ) {
907- return true ; // We found a try block that encompasses the current line
908- }
909- }
910-
911- // More sophisticated brace tracking to identify block depth
912- const openBraces = ( line . match ( / { / g) || [ ] ) . length ;
913- const closeBraces = ( line . match ( / } / g) || [ ] ) . length ;
914-
915- if ( openBraces > closeBraces ) {
916- tryBlockDepth ++ ;
917- } else if ( closeBraces > openBraces ) {
918- tryBlockDepth = Math . max ( 0 , tryBlockDepth - closeBraces + openBraces ) ;
948+ if ( pendingExit ) {
949+ pendingExit = false ;
919950 }
920951
921- // If we're at top level (depth 0) and haven't found a try/catch, we're outside
922- if ( tryBlockDepth === 0 ) {
923- // Check if there was a catch block before we exited
924- if ( catchBlockStartLine > i ) {
925- return true ;
952+ if ( hasCatch ) {
953+ if ( line . includes ( '{' ) ) {
954+ if ( catchOnSameLineAsCloseBrace ) {
955+ const closeBraceIdx = line . indexOf ( '}' ) ;
956+ const catchIdx = line . indexOf ( 'catch' ) ;
957+ const openBraceIdx = line . indexOf ( '{' , catchIdx ) ;
958+ if ( closeBraceIdx !== - 1 && closeBraceIdx < catchIdx && openBraceIdx > catchIdx ) {
959+ braceDepth -- ;
960+ }
961+ for ( let j = 0 ; j < openBraceIdx ; j ++ ) {
962+ if ( line [ j ] === '{' ) braceDepth ++ ;
963+ }
964+ for ( let j = openBraceIdx + 1 ; j < line . length ; j ++ ) {
965+ if ( line [ j ] === '{' ) nestedDepth ++ ;
966+ else if ( line [ j ] === '}' ) {
967+ nestedDepth -- ;
968+ if ( nestedDepth === 0 ) {
969+ pendingExit = true ;
970+ }
971+ }
972+ }
973+ inCatchBlock = true ;
974+ catchBlockDepth = braceDepth ;
975+ nestedDepth = 0 ;
976+ } else {
977+ inCatchBlock = true ;
978+ catchBlockDepth = braceDepth - 1 ;
979+ nestedDepth = 0 ;
980+ pendingExit = false ;
981+ }
982+ } else {
983+ for ( let j = i + 1 ; j < Math . min ( i + 5 , lines . length ) ; j ++ ) {
984+ if ( lines [ j ] . includes ( '{' ) ) {
985+ inCatchBlock = true ;
986+ catchBlockDepth = braceDepth ;
987+ nestedDepth = 0 ;
988+ pendingExit = false ;
989+ break ;
990+ }
991+ }
926992 }
927993 }
928994 }
929995
930- return false ;
996+ return inCatchBlock ;
931997 }
932998
933999 /**
@@ -1102,6 +1168,13 @@ class AISlopDetector {
11021168 console . log ( '5. Remove development artifacts like TODO comments and console logs' ) ;
11031169 }
11041170
1171+ /**
1172+ * Get the current configuration
1173+ */
1174+ getConfig ( ) : KarpeSlopConfig {
1175+ return { ...this . config } ;
1176+ }
1177+
11051178 /**
11061179 * Get the number of issues found
11071180 */
@@ -1212,20 +1285,6 @@ class AISlopDetector {
12121285 console . log ( `\n📈 Results exported to: ${ outputPath } ` ) ;
12131286 }
12141287
1215- /**
1216- * Get issues grouped by type
1217- */
1218- private getIssuesByType ( ) : Record < string , AISlopIssue [ ] > {
1219- const byType : Record < string , AISlopIssue [ ] > = { } ;
1220- this . issues . forEach ( issue => {
1221- if ( ! byType [ issue . type ] ) {
1222- byType [ issue . type ] = [ ] ;
1223- }
1224- byType [ issue . type ] . push ( issue ) ;
1225- } ) ;
1226- return byType ;
1227- }
1228-
12291288
12301289
12311290
@@ -1325,8 +1384,8 @@ The tool detects the three axes of AI slop:
13251384 process . exit ( 0 ) ;
13261385 }
13271386
1328- const quiet = args . includes ( '--quiet' ) || args . includes ( '-q' ) ;
1329- const strict = args . includes ( '--strict' ) || args . includes ( '-s' ) ;
1387+ const quiet = args . includes ( '--quiet' ) || args . includes ( '-q' ) ;
1388+ const strict = args . includes ( '--strict' ) || args . includes ( '-s' ) || ! ! detector . getConfig ( ) . blockOnCritical ;
13301389
13311390 try {
13321391 const issues = await detector . detect ( quiet ) ;
0 commit comments