Skip to content

Commit 95841fe

Browse files
Merge pull request #9 from CodeDeficient/fix/false-positive-issues-2-6-7-8
fix: resolve all false positives (issues #2, #6, #7, #8)
2 parents 3eb07de + fd9cf4d commit 95841fe

File tree

5 files changed

+715
-146
lines changed

5 files changed

+715
-146
lines changed

ai-slop-detector.ts

Lines changed: 127 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
* Follows industry best practices based on typescript-eslint patterns.
1010
*/
1111

12-
import fs, { realpathSync } from 'fs';
12+
import fs from 'fs';
1313
import path from 'path';
1414
import { glob } from 'glob';
1515
import { 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*(obviously|clearly|simply|just|easy|trivial|basically|literally|of course|naturally|certainly|surely)\b/gi,
143+
pattern: /\/\/.*\b(obviously|clearly|simply|just|easy|trivial|basically|literally|of course|naturally|certainly|surely)\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}px|#\w{6}|rgba?\([^)]+\)|hsl\(\d+)/g,
172+
pattern: /(\d{3,4}px|#[0-9a-fA-F]{3,8}\b|rgba?\([^)]+\)|hsl\(\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: /(fetch|axios|http)\s*\(/g,
273+
pattern: /\b(fetch|axios|http)\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: /(TODO|FIXME|HACK|XXX|BUG)\b/g,
292+
pattern: /\b(TODO|FIXME|HACK|XXX|BUG)\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(/^as\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 (/^if\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 (/^if\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

Comments
 (0)