From f0c4ce2f6bbdcad43bdca5a9ee88ffe3bc1ec897 Mon Sep 17 00:00:00 2001 From: Daniel King <89734689+CodeDeficient@users.noreply.github.com> Date: Mon, 27 Jul 2026 11:42:25 -0400 Subject: [PATCH 1/8] feat: migrate unsafe_type_assertion from regex to AST detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace regex-based detection with TypeScript Compiler API - Comments and strings containing are no longer reported - Nested and multiline assertions handled syntactically - Pattern ID unchanged — existing severityOverrides continue to work - 10 new regression tests covering detection, false positives, suppression --- CHANGELOG.md | 5 ++ ai-slop-detector.ts | 30 +++++++---- package.json | 2 +- tests/ai-slop-detector.behavior.test.ts | 70 +++++++++++++++++++++++++ 4 files changed, 96 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3d9f2d..666eef9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes will be documented in this file. +## [1.0.29] - 2026-07-27 + +### Changed +- **`unsafe_type_assertion` migrated from regex to AST detection**: `as any` assertions are now detected by the TypeScript Compiler API instead of a line-based regex. Comments and strings containing `as any` are no longer reported. Nested and multiline assertions are handled syntactically. The pattern ID and configuration key remain unchanged — existing `severityOverrides` continue to work. + ## [1.0.28] - 2026-07-27 ### Added diff --git a/ai-slop-detector.ts b/ai-slop-detector.ts index e459b9a..ad7138f 100644 --- a/ai-slop-detector.ts +++ b/ai-slop-detector.ts @@ -241,6 +241,22 @@ function visit(node: ts.Node) { severity: 'high', assertionForm: 'as_unknown_as', }); + } else if (node.type.kind === ts.SyntaxKind.AnyKeyword) { + // Use the position of the type node (same line as `as` keyword) for correct line reporting + const { line: asLineIdx, character: asCharIdx } = sourceFile.getLineAndCharacterOfPosition(node.type.getStart(sourceFile)); + const asLine = asLineIdx + 1; + const asColumn = asCharIdx + 1; + const code = sourceText.slice(node.getStart(sourceFile), node.end); + findings.push({ + type: 'unsafe_type_assertion', + file: fileName, + line: asLine, + column: asColumn, + code, + message: "Found unsafe 'as any' type assertion. Use proper type guards or validation.", + severity: 'high', + assertionForm: 'as_any', + }); } else if (isUnsafeObjectType(node.type)) { const code = sourceText.slice(node.getStart(sourceFile), node.end); findings.push({ @@ -453,10 +469,10 @@ class AISlopDetector { }, { id: 'unsafe_type_assertion', - pattern: /\s+as\s+any\b/g, + pattern: /unused_ast_detected/, message: "Found unsafe 'as any' type assertion. Use proper type guards or validation.", severity: 'high', - description: 'Detects unsafe as any assertions', + description: 'Detects unsafe as any assertions (AST-based)', fix: "Use 'as unknown as TargetType' or implement a runtime type guard with validation", learnMore: 'https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates' }, @@ -978,7 +994,8 @@ class AISlopDetector { // Skip AST-based patterns (detected by findUnsafeAssertions, not regex) if (pattern.id === 'unsafe_double_type_assertion' || pattern.id === 'unsafe_object_assertion' || - pattern.id === 'unsafe_array_assertion') { + pattern.id === 'unsafe_array_assertion' || + pattern.id === 'unsafe_type_assertion') { continue; } @@ -1120,13 +1137,6 @@ class AISlopDetector { } } - // Special handling for unsafe_type_assertion - skip legitimate test patterns - if (pattern.id === 'unsafe_type_assertion') { - // Skip these in test files where they might be legitimate for testing - if (filePath.includes('test') || filePath.includes('spec') || filePath.includes('__tests__')) { - continue; - } - } diff --git a/package.json b/package.json index 7a29409..732b927 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "karpeslop", - "version": "1.0.28", + "version": "1.0.29", "description": "The linter Andrej Karpathy wishes existed. Detects the three axes of AI slop with extreme prejudice.", "type": "module", "packageManager": "npm@10.9.7", diff --git a/tests/ai-slop-detector.behavior.test.ts b/tests/ai-slop-detector.behavior.test.ts index 8cb3482..0df5212 100644 --- a/tests/ai-slop-detector.behavior.test.ts +++ b/tests/ai-slop-detector.behavior.test.ts @@ -223,6 +223,76 @@ test('findUnsafeAssertions on the fixtures/unsafe-assertions.ts fixture reports assert.ok(objFindings[0].code.includes('Record')); }); +test('findUnsafeAssertions detects unsafe as any assertion', () => { + const code = 'const x = value as any;'; + + const findings = findUnsafeAssertions(code, 'x.ts'); + + assert.equal(findings.length, 1); + assert.equal(findings[0].type, 'unsafe_type_assertion'); + assert.equal(findings[0].line, 1); + assert.equal(findings[0].severity, 'high'); + assert.ok(findings[0].code.includes('as any')); +}); + +test('findUnsafeAssertions does not report as any inside a line comment', () => { + const code = '// this is as any comment'; + assert.equal(findUnsafeAssertions(code, 'x.ts').length, 0); +}); + +test('findUnsafeAssertions does not report as any inside a string', () => { + const code = 'const msg = "this is as any test";'; + assert.equal(findUnsafeAssertions(code, 'x.ts').length, 0); +}); + +test('findUnsafeAssertions does not report as any inside a template literal', () => { + const code = 'const msg = `this is as any test`;'; + assert.equal(findUnsafeAssertions(code, 'x.ts').length, 0); +}); + +test('findUnsafeAssertions detects unsafe as any assertion in nested expression', () => { + const code = 'const x = foo(value as any);'; + + const findings = findUnsafeAssertions(code, 'x.ts'); + + assert.equal(findings.length, 1); + assert.equal(findings[0].type, 'unsafe_type_assertion'); + assert.equal(findings[0].line, 1); +}); + +test('findUnsafeAssertions detects unsafe as any assertion in multiline expression', () => { + const code = 'const x = value as\n any;'; + + const findings = findUnsafeAssertions(code, 'x.ts'); + + assert.equal(findings.length, 1); + assert.equal(findings[0].type, 'unsafe_type_assertion'); + assert.equal(findings[0].line, 2); +}); + +test('findUnsafeAssertions detects multiple as any assertions on separate lines', () => { + const code = 'const a = x as any;\nconst b = y as any;'; + + const findings = findUnsafeAssertions(code, 'x.ts'); + + assert.equal(findings.length, 2); + assert.ok(findings.every(f => f.type === 'unsafe_type_assertion')); +}); + +test('findUnsafeAssertions does not report as any when preceded by @ts-expect-error', () => { + const code = '// @ts-expect-error\nconst x = value as any;'; + assert.equal(findUnsafeAssertions(code, 'x.ts').length, 0); +}); + +test('findUnsafeAssertions does not report as any when preceded by eslint-disable-next-line', () => { + const code = '// eslint-disable-next-line @typescript-eslint/no-unused-vars\nconst x = value as any;'; + assert.equal(findUnsafeAssertions(code, 'x.ts').length, 0); +}); + +test('findUnsafeAssertions does not report as any in .d.ts files', () => { + assert.equal(findUnsafeAssertions('const x = value as any;', 'types.d.ts').length, 0); +}); + test('findUnsafeAssertions detects chained as unknown as T as unsafe_double_type_assertion', () => { const code = 'const rows = value as unknown as EventRow[];'; From 434c6d5a134a37400545592aa21af0853fcba0b7 Mon Sep 17 00:00:00 2001 From: Daniel King <89734689+CodeDeficient@users.noreply.github.com> Date: Mon, 27 Jul 2026 11:55:48 -0400 Subject: [PATCH 2/8] fix: preserve unsafe_type_assertion for chained as unknown as any When the outer assertion type is also , both unsafe_double_type_assertion and unsafe_type_assertion are now emitted independently. Disabling one rule does not suppress the other. --- ai-slop-detector.ts | 15 ++++++++++ tests/ai-slop-detector.behavior.test.ts | 39 +++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/ai-slop-detector.ts b/ai-slop-detector.ts index ad7138f..780e319 100644 --- a/ai-slop-detector.ts +++ b/ai-slop-detector.ts @@ -241,6 +241,21 @@ function visit(node: ts.Node) { severity: 'high', assertionForm: 'as_unknown_as', }); + // When the target type is also `any`, report it independently so disabling + // unsafe_double_type_assertion doesn't silently hide the unsafe `as any`. + if (node.type.kind === ts.SyntaxKind.AnyKeyword) { + const { line: asLineIdx, character: asCharIdx } = sourceFile.getLineAndCharacterOfPosition(node.type.getStart(sourceFile)); + findings.push({ + type: 'unsafe_type_assertion', + file: fileName, + line: asLineIdx + 1, + column: asCharIdx + 1, + code, + message: "Found unsafe 'as any' type assertion. Use proper type guards or validation.", + severity: 'high', + assertionForm: 'as_any', + }); + } } else if (node.type.kind === ts.SyntaxKind.AnyKeyword) { // Use the position of the type node (same line as `as` keyword) for correct line reporting const { line: asLineIdx, character: asCharIdx } = sourceFile.getLineAndCharacterOfPosition(node.type.getStart(sourceFile)); diff --git a/tests/ai-slop-detector.behavior.test.ts b/tests/ai-slop-detector.behavior.test.ts index 0df5212..e09a187 100644 --- a/tests/ai-slop-detector.behavior.test.ts +++ b/tests/ai-slop-detector.behavior.test.ts @@ -306,6 +306,45 @@ test('findUnsafeAssertions detects chained as unknown as T as unsafe_double_type assert.ok(finding.code.includes('as unknown as')); }); +test('findUnsafeAssertions reports both double and any assertion for chained as unknown as any', () => { + const code = 'const value = input as unknown as any;'; + + const findings = findUnsafeAssertions(code, 'x.ts'); + + assert.equal(findings.length, 2); + const types = findings.map(f => f.type); + assert.ok(types.includes('unsafe_double_type_assertion'), 'should report double assertion'); + assert.ok(types.includes('unsafe_type_assertion'), 'should report any assertion'); + assert.ok(findings.every(f => f.severity === 'high')); +}); + +test('severityOverrides disabling unsafe_double_type_assertion still reports unsafe_type_assertion for chained as unknown as any', () => { + const fixtureFile = path.resolve(process.cwd(), 'tests/fixtures/temp-chained-any-fixture.ts'); + const configFile = path.resolve(process.cwd(), '.karpesloprc.json'); + const savedConfig = fs.readFileSync(configFile, 'utf-8'); + + fs.writeFileSync(fixtureFile, 'const value = input as unknown as any;\n', 'utf-8'); + + try { + const overriddenConfig = JSON.parse(savedConfig); + overriddenConfig.severityOverrides = { "unsafe_double_type_assertion": "off" }; + fs.writeFileSync(configFile, JSON.stringify(overriddenConfig), 'utf-8'); + + const result = spawnSync( + process.execPath, + ['--import', 'tsx', path.resolve(process.cwd(), 'ai-slop-detector.ts'), '--strict', fixtureFile], + { encoding: 'utf-8', cwd: process.cwd() } + ); + + assert.equal(result.status, 1, `Expected exit code 1 (high severity finding) but got ${result.status}. stdout:"${result.stdout}" stderr:"${result.stderr}"`); + assert.ok(!result.stdout.includes('unsafe_double'), 'should not mention the suppressed double assertion'); + assert.ok(result.stdout.includes('unsafe_type_assertion'), 'should still mention the any assertion'); + } finally { + fs.unlinkSync(fixtureFile); + fs.writeFileSync(configFile, savedConfig, 'utf-8'); + } +}); + test('-- separator lets paths starting with - be treated as targets, not flags', () => { const tmpDir = fs.mkdtempSync(path.join(process.platform === 'win32' ? process.env.TEMP! : '/tmp', 'karpeslop-dash-')); const fixtureFile = path.join(tmpDir, '-my-file.ts'); From eef4363864df86b87b0df5d6290bf2b0cab254e6 Mon Sep 17 00:00:00 2001 From: Daniel King <89734689+CodeDeficient@users.noreply.github.com> Date: Mon, 27 Jul 2026 12:09:54 -0400 Subject: [PATCH 3/8] fix: restore test-file exclusion for unsafe_type_assertion in AST detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The AST migration (f0c4ce2) dropped the old regex-path logic that excluded test/spec/__tests__ files from unsafe_type_assertion findings in default (non-quiet) mode. Adding skipTests:true to the pattern definition restores this behavior — the existing decorator guard in the AST findings loop (analyzeFile, line 1202) already applies skipTests to AST findings. Package-lock bumped to match 1.0.29. --- ai-slop-detector.ts | 3 +- package-lock.json | 4 +-- tests/ai-slop-detector.behavior.test.ts | 42 +++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/ai-slop-detector.ts b/ai-slop-detector.ts index 780e319..2d4a473 100644 --- a/ai-slop-detector.ts +++ b/ai-slop-detector.ts @@ -489,7 +489,8 @@ class AISlopDetector { severity: 'high', description: 'Detects unsafe as any assertions (AST-based)', fix: "Use 'as unknown as TargetType' or implement a runtime type guard with validation", - learnMore: 'https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates' + learnMore: 'https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates', + skipTests: true, }, { id: 'index_signature_any', diff --git a/package-lock.json b/package-lock.json index bdecd1e..fc68a5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "karpeslop", - "version": "1.0.28", + "version": "1.0.29", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "karpeslop", - "version": "1.0.28", + "version": "1.0.29", "license": "MIT", "dependencies": { "glob": "13.0.6", diff --git a/tests/ai-slop-detector.behavior.test.ts b/tests/ai-slop-detector.behavior.test.ts index e09a187..921dad6 100644 --- a/tests/ai-slop-detector.behavior.test.ts +++ b/tests/ai-slop-detector.behavior.test.ts @@ -345,6 +345,48 @@ test('severityOverrides disabling unsafe_double_type_assertion still reports uns } }); +test('default mode skips unsafe_type_assertion in test files', () => { + const tmpDir = fs.mkdtempSync(path.join(process.platform === 'win32' ? process.env.TEMP! : '/tmp', 'karpeslop-spec-')); + const fixtureFile = path.join(tmpDir, 'component.test.ts'); + + fs.writeFileSync(fixtureFile, 'const x = value as any;\n', 'utf-8'); + + const result = spawnSync( + process.execPath, + ['--import', 'tsx', path.resolve(process.cwd(), 'ai-slop-detector.ts'), fixtureFile], + { encoding: 'utf-8', cwd: process.cwd() } + ); + + try { + assert.ok(!result.stdout.includes('unsafe_type_assertion'), + `should not report as any in test files. stdout:"${result.stdout}"`); + } finally { + fs.unlinkSync(fixtureFile); + fs.rmdirSync(tmpDir); + } +}); + +test('default mode still reports unsafe_type_assertion in non-test files', () => { + const tmpDir = fs.mkdtempSync(path.join(process.platform === 'win32' ? process.env.TEMP! : '/tmp', 'karpeslop-src-')); + const fixtureFile = path.join(tmpDir, 'component.ts'); + + fs.writeFileSync(fixtureFile, 'const x = value as any;\n', 'utf-8'); + + const result = spawnSync( + process.execPath, + ['--import', 'tsx', path.resolve(process.cwd(), 'ai-slop-detector.ts'), fixtureFile], + { encoding: 'utf-8', cwd: process.cwd() } + ); + + try { + assert.ok(result.stdout.includes('unsafe_type_assertion'), + `should report as any in non-test files. stdout:"${result.stdout}"`); + } finally { + fs.unlinkSync(fixtureFile); + fs.rmdirSync(tmpDir); + } +}); + test('-- separator lets paths starting with - be treated as targets, not flags', () => { const tmpDir = fs.mkdtempSync(path.join(process.platform === 'win32' ? process.env.TEMP! : '/tmp', 'karpeslop-dash-')); const fixtureFile = path.join(tmpDir, '-my-file.ts'); From 86746fc0a9971fc4cba40a5d5669b34a907fd554 Mon Sep 17 00:00:00 2001 From: Daniel King <89734689+CodeDeficient@users.noreply.github.com> Date: Mon, 27 Jul 2026 12:19:03 -0400 Subject: [PATCH 4/8] fix: restore broad chained assertion detection and first-line inline suppression P1: The AST migration (f0c4ce2) only recognized as unknown as T as a double assertion. Broaden condition to any nested AsExpression so that value as Foo as Bar continues to produce unsafe_double_type_assertion. P2: lineIsSuppressed returned false for line <= 1 before checking the current line. Reorder so the current line is always checked for inline suppressions (eslint-disable-line, @ts-ignore, etc.), and the previous line is checked only when it exists. --- ai-slop-detector.ts | 30 ++++++++++++++----------- tests/ai-slop-detector.behavior.test.ts | 17 ++++++++++++++ 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/ai-slop-detector.ts b/ai-slop-detector.ts index 2d4a473..dd4f55f 100644 --- a/ai-slop-detector.ts +++ b/ai-slop-detector.ts @@ -181,19 +181,23 @@ export function findUnsafeAssertions(sourceText: string, fileName: string): Unsa const lines = sourceText.split('\n'); const lineIsSuppressed = (line: number): boolean => { - if (line <= 1) return false; - const prevLine = lines[line - 2]; const currentLine = lines[line - 1]; - return ( - prevLine.includes('@ts-expect-error') || - prevLine.includes('@ts-ignore') || - prevLine.includes('eslint-disable-next-line') || - prevLine.includes('eslint-disable') || + if ( currentLine.includes('@ts-expect-error') || currentLine.includes('@ts-ignore') || currentLine.includes('eslint-disable-line') || currentLine.includes('eslint-disable') - ); + ) return true; + if (line > 1) { + const prevLine = lines[line - 2]; + return ( + prevLine.includes('@ts-expect-error') || + prevLine.includes('@ts-ignore') || + prevLine.includes('eslint-disable-next-line') || + prevLine.includes('eslint-disable') + ); + } + return false; }; // Syntax-only (non-type-checked) detection: matches by identifier text, not @@ -229,7 +233,7 @@ function visit(node: ts.Node) { return; } - if (ts.isAsExpression(node.expression) && node.expression.type.kind === ts.SyntaxKind.UnknownKeyword) { + if (ts.isAsExpression(node.expression)) { const code = sourceText.slice(node.getStart(sourceFile), node.end); findings.push({ type: 'unsafe_double_type_assertion', @@ -237,9 +241,9 @@ function visit(node: ts.Node) { line: sourceLine, column, code, - message: "Found unsafe double type assertion via 'as unknown as'. Use proper type guards or validation instead.", + message: "Found unsafe double type assertion. Use proper type guards or validation instead.", severity: 'high', - assertionForm: 'as_unknown_as', + assertionForm: 'chained_as', }); // When the target type is also `any`, report it independently so disabling // unsafe_double_type_assertion doesn't silently hide the unsafe `as any`. @@ -536,9 +540,9 @@ class AISlopDetector { { id: 'unsafe_double_type_assertion', pattern: /unused_ast_detected/, - message: "Found unsafe double type assertion via 'as unknown as'. Use proper type guards or runtime validation.", + message: "Found unsafe double type assertion. Use proper type guards or runtime validation.", severity: 'high', - description: 'Detects as unknown as T (AST-based)' + description: 'Detects chained type assertions (AST-based)' }, { id: 'unsafe_object_assertion', diff --git a/tests/ai-slop-detector.behavior.test.ts b/tests/ai-slop-detector.behavior.test.ts index 921dad6..5ec5c7f 100644 --- a/tests/ai-slop-detector.behavior.test.ts +++ b/tests/ai-slop-detector.behavior.test.ts @@ -293,6 +293,11 @@ test('findUnsafeAssertions does not report as any in .d.ts files', () => { assert.equal(findUnsafeAssertions('const x = value as any;', 'types.d.ts').length, 0); }); +test('findUnsafeAssertions suppresses as any on line 1 with eslint-disable-line', () => { + const code = 'const x = value as any; // eslint-disable-line'; + assert.equal(findUnsafeAssertions(code, 'x.ts').length, 0); +}); + test('findUnsafeAssertions detects chained as unknown as T as unsafe_double_type_assertion', () => { const code = 'const rows = value as unknown as EventRow[];'; @@ -306,6 +311,18 @@ test('findUnsafeAssertions detects chained as unknown as T as unsafe_double_type assert.ok(finding.code.includes('as unknown as')); }); +test('findUnsafeAssertions detects chained as Foo as Bar as unsafe_double_type_assertion', () => { + const code = 'const value = input as Foo as Bar;'; + + const findings = findUnsafeAssertions(code, 'x.ts'); + + assert.equal(findings.length, 1); + const finding = findings[0]; + assert.equal(finding.type, 'unsafe_double_type_assertion'); + assert.equal(finding.line, 1); + assert.equal(finding.severity, 'high'); +}); + test('findUnsafeAssertions reports both double and any assertion for chained as unknown as any', () => { const code = 'const value = input as unknown as any;'; From 7c9ab7ca1bcdf6c95f15379f68884d2662393349 Mon Sep 17 00:00:00 2001 From: Daniel King <89734689+CodeDeficient@users.noreply.github.com> Date: Mon, 27 Jul 2026 12:47:23 -0400 Subject: [PATCH 5/8] docs: document behavioral changes in 1.0.29 changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 666eef9..a1f76fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ All notable changes will be documented in this file. ### Changed - **`unsafe_type_assertion` migrated from regex to AST detection**: `as any` assertions are now detected by the TypeScript Compiler API instead of a line-based regex. Comments and strings containing `as any` are no longer reported. Nested and multiline assertions are handled syntactically. The pattern ID and configuration key remain unchanged — existing `severityOverrides` continue to work. +- **`unsafe_double_type_assertion` broadened**: Now detects any chained assertion (`as X as Y`), not only `as unknown as Y`. The `assertionForm` metadata field changed from `as_unknown_as` to `chained_as`. The message no longer references `as unknown as`. +- **First-line inline suppressions fixed**: `// eslint-disable-line` and `@ts-ignore` on line 1 now suppress findings correctly. +- **Test-file exclusion restored**: `unsafe_type_assertion` is once again skipped in `isTestFile` paths (`.test.*`, `.spec.*`, `__tests__/`, etc.) during normal scans, matching the pre-migration behavior. ## [1.0.28] - 2026-07-27 From 91c6b76fcc7e7764cd4c392e484824a7eab8a304 Mon Sep 17 00:00:00 2001 From: Daniel King <89734689+CodeDeficient@users.noreply.github.com> Date: Mon, 27 Jul 2026 12:51:24 -0400 Subject: [PATCH 6/8] refactor: replace unused_ast_detected placeholder with never-matching regex Also removes orphaned blank lines left from the test-file exclusion block removal in f0c4ce2. --- ai-slop-detector.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/ai-slop-detector.ts b/ai-slop-detector.ts index dd4f55f..68b044d 100644 --- a/ai-slop-detector.ts +++ b/ai-slop-detector.ts @@ -488,7 +488,7 @@ class AISlopDetector { }, { id: 'unsafe_type_assertion', - pattern: /unused_ast_detected/, + pattern: /(?!)/, message: "Found unsafe 'as any' type assertion. Use proper type guards or validation.", severity: 'high', description: 'Detects unsafe as any assertions (AST-based)', @@ -539,21 +539,21 @@ class AISlopDetector { }, { id: 'unsafe_double_type_assertion', - pattern: /unused_ast_detected/, + pattern: /(?!)/, message: "Found unsafe double type assertion. Use proper type guards or runtime validation.", severity: 'high', description: 'Detects chained type assertions (AST-based)' }, { id: 'unsafe_object_assertion', - pattern: /unused_ast_detected/, + pattern: /(?!)/, message: "Found unsafe object type assertion. Use proper type guards or runtime validation.", severity: 'high', description: 'Detects as Record<...> or as { ... } (AST-based)' }, { id: 'unsafe_array_assertion', - pattern: /unused_ast_detected/, + pattern: /(?!)/, message: "Found unsafe array type assertion. Use proper type guards or runtime validation.", severity: 'high', description: 'Detects as T[] or as Array (AST-based)' @@ -1158,8 +1158,6 @@ class AISlopDetector { } - - // In quiet mode, skip test and mock files for all patterns except production console logs if (quiet && pattern.id !== 'production_console_log') { const isTestFile = filePath.includes('__tests__') || @@ -1852,8 +1850,6 @@ class AISlopDetector { } - - /** * Calculate comprehensive KarpeSlop score based on the three axes */ From 3a9b9427b648a2f560f31c7d87b5d07da8bd6ced Mon Sep 17 00:00:00 2001 From: Daniel King <89734689+CodeDeficient@users.noreply.github.com> Date: Mon, 27 Jul 2026 13:13:37 -0400 Subject: [PATCH 7/8] test: lock precise chain-count behavior and add sentinel regex comment Adds tests verifying exact finding counts for chained assertions at every depth (1-chain->1, 2-chain->2, 3-chain->3, 4-chain->3) per the user's requirement that findings reflect the precise AST count with no deduplication. Also adds a comment on the first /(?!)/ sentinel regex explaining it is AST-driven and the pattern is a never-match placeholder. --- ai-slop-detector.ts | 1 + tests/ai-slop-detector.behavior.test.ts | 32 +++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/ai-slop-detector.ts b/ai-slop-detector.ts index 68b044d..349a826 100644 --- a/ai-slop-detector.ts +++ b/ai-slop-detector.ts @@ -486,6 +486,7 @@ class AISlopDetector { severity: 'high', description: 'Detects function parameters with any type' }, + // AST-driven; regex is a never-match sentinel — detection is in findUnsafeAssertions { id: 'unsafe_type_assertion', pattern: /(?!)/, diff --git a/tests/ai-slop-detector.behavior.test.ts b/tests/ai-slop-detector.behavior.test.ts index 5ec5c7f..7f5e5c2 100644 --- a/tests/ai-slop-detector.behavior.test.ts +++ b/tests/ai-slop-detector.behavior.test.ts @@ -323,6 +323,38 @@ test('findUnsafeAssertions detects chained as Foo as Bar as unsafe_double_type_a assert.equal(finding.severity, 'high'); }); +test('findUnsafeAssertions reports 2 findings for 3-deep chain', () => { + const code = 'const value = input as Bar as Baz as Qux;'; + + const findings = findUnsafeAssertions(code, 'x.ts'); + + assert.equal(findings.length, 2); + assert.ok(findings.every(f => f.type === 'unsafe_double_type_assertion')); +}); + +test('findUnsafeAssertions reports 3 findings for 4-deep chain', () => { + const code = 'const value = input as Bar as Baz as Qux as Zap;'; + + const findings = findUnsafeAssertions(code, 'x.ts'); + + assert.equal(findings.length, 3); + assert.ok(findings.every(f => f.type === 'unsafe_double_type_assertion')); +}); + +test('findUnsafeAssertions reports double and any for chained as Bar as any', () => { + const code = 'const value = input as Bar as any;'; + + const findings = findUnsafeAssertions(code, 'x.ts'); + + assert.equal(findings.length, 2); + const types = findings.map(f => f.type); + assert.ok(types.includes('unsafe_double_type_assertion')); + assert.ok(types.includes('unsafe_type_assertion')); + + const anyFinding = findings.find(f => f.type === 'unsafe_type_assertion')!; + assert.ok(anyFinding.code.includes('as any')); +}); + test('findUnsafeAssertions reports both double and any assertion for chained as unknown as any', () => { const code = 'const value = input as unknown as any;'; From dbd609ba2158bad7093f968996913e6d0e3de39b Mon Sep 17 00:00:00 2001 From: Daniel King <89734689+CodeDeficient@users.noreply.github.com> Date: Mon, 27 Jul 2026 14:30:55 -0400 Subject: [PATCH 8/8] fix: remove self-contradictory guidance and fix multiline suppression anchoring - Fix: safe_type_assertion guidance no longer recommends 'as unknown as TargetType', which is itself flagged by unsafe_double_type_assertion - Fix: Multiline as any suppression now checks the 'any' token line, not the outer 'as' expression line - Updated fixture comment and CHANGELOG with accurate description of test-file exclusion scope --- CHANGELOG.md | 4 +++- ai-slop-detector.ts | 30 +++++++++++++++++++------ tests/ai-slop-detector.behavior.test.ts | 16 +++++++++++++ tests/t4-educational-output.ts | 2 +- 4 files changed, 43 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1f76fe..28a7018 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,9 @@ All notable changes will be documented in this file. - **`unsafe_type_assertion` migrated from regex to AST detection**: `as any` assertions are now detected by the TypeScript Compiler API instead of a line-based regex. Comments and strings containing `as any` are no longer reported. Nested and multiline assertions are handled syntactically. The pattern ID and configuration key remain unchanged — existing `severityOverrides` continue to work. - **`unsafe_double_type_assertion` broadened**: Now detects any chained assertion (`as X as Y`), not only `as unknown as Y`. The `assertionForm` metadata field changed from `as_unknown_as` to `chained_as`. The message no longer references `as unknown as`. - **First-line inline suppressions fixed**: `// eslint-disable-line` and `@ts-ignore` on line 1 now suppress findings correctly. -- **Test-file exclusion restored**: `unsafe_type_assertion` is once again skipped in `isTestFile` paths (`.test.*`, `.spec.*`, `__tests__/`, etc.) during normal scans, matching the pre-migration behavior. +- **Test-file exclusion restored**: `unsafe_type_assertion` is once again skipped in `isTestFile` paths (`.test.*`, `.spec.*`, `__tests__/`, `__mocks__/`, `test-`) during normal scans. The exclusion is narrower than the pre-migration heuristic (`includes('test')`), so paths merely containing `test` or `spec` (e.g. `tests/utils/helper.ts`, `my-test-utils.ts`) may now be analyzed. +- **`unsafe_type_assertion` guidance no longer recommends chained assertions**: The `fix` field previously told users to `Use 'as unknown as TargetType'`, which conflicted with the broadened `unsafe_double_type_assertion` rule. The new guidance advocates a migration-first strategy with runtime validation. +- **Multiline suppression anchored to the `any` token line**: Suppression checks for multiline `as any` assertions now evaluate the line containing the `any` keyword, not the outer `as` expression line. Inline suppressions on the `any` line work correctly. ## [1.0.28] - 2026-07-27 diff --git a/ai-slop-detector.ts b/ai-slop-detector.ts index 349a826..7b73481 100644 --- a/ai-slop-detector.ts +++ b/ai-slop-detector.ts @@ -228,12 +228,12 @@ function visit(node: ts.Node) { const sourceLine = lineIdx + 1; const column = charIdx + 1; - if (lineIsSuppressed(sourceLine)) { - ts.forEachChild(node, visit); - return; - } - if (ts.isAsExpression(node.expression)) { + if (lineIsSuppressed(sourceLine)) { + ts.forEachChild(node, visit); + return; + } + const code = sourceText.slice(node.getStart(sourceFile), node.end); findings.push({ type: 'unsafe_double_type_assertion', @@ -261,10 +261,16 @@ function visit(node: ts.Node) { }); } } else if (node.type.kind === ts.SyntaxKind.AnyKeyword) { - // Use the position of the type node (same line as `as` keyword) for correct line reporting + // Use the position of the type node for correct line reporting and suppression anchoring const { line: asLineIdx, character: asCharIdx } = sourceFile.getLineAndCharacterOfPosition(node.type.getStart(sourceFile)); const asLine = asLineIdx + 1; const asColumn = asCharIdx + 1; + + if (lineIsSuppressed(asLine)) { + ts.forEachChild(node, visit); + return; + } + const code = sourceText.slice(node.getStart(sourceFile), node.end); findings.push({ type: 'unsafe_type_assertion', @@ -277,6 +283,11 @@ function visit(node: ts.Node) { assertionForm: 'as_any', }); } else if (isUnsafeObjectType(node.type)) { + if (lineIsSuppressed(sourceLine)) { + ts.forEachChild(node, visit); + return; + } + const code = sourceText.slice(node.getStart(sourceFile), node.end); findings.push({ type: 'unsafe_object_assertion', @@ -289,6 +300,11 @@ function visit(node: ts.Node) { assertionForm: 'as_object', }); } else if (isArrayType(node.type)) { + if (lineIsSuppressed(sourceLine)) { + ts.forEachChild(node, visit); + return; + } + const code = sourceText.slice(node.getStart(sourceFile), node.end); findings.push({ type: 'unsafe_array_assertion', @@ -493,7 +509,7 @@ class AISlopDetector { message: "Found unsafe 'as any' type assertion. Use proper type guards or validation.", severity: 'high', description: 'Detects unsafe as any assertions (AST-based)', - fix: "Use 'as unknown as TargetType' or implement a runtime type guard with validation", + fix: "Prefer migrating the source/data contract first. Otherwise validate the value at the boundary with a runtime type guard or validation schema, then narrow the resulting unknown value. Do not use chained type assertions.", learnMore: 'https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates', skipTests: true, }, diff --git a/tests/ai-slop-detector.behavior.test.ts b/tests/ai-slop-detector.behavior.test.ts index 7f5e5c2..6c9d569 100644 --- a/tests/ai-slop-detector.behavior.test.ts +++ b/tests/ai-slop-detector.behavior.test.ts @@ -270,6 +270,22 @@ test('findUnsafeAssertions detects unsafe as any assertion in multiline expressi assert.equal(findings[0].line, 2); }); +test('findUnsafeAssertions suppresses multiline as any with eslint-disable-line on the any line', () => { + const code = 'const x = value as\nany; // eslint-disable-line'; + assert.equal(findUnsafeAssertions(code, 'x.ts').length, 0); +}); + +test('unsafe_type_assertion guidance does not recommend chained assertion pattern to users', () => { + const result = spawnSync( + process.execPath, + ['--import', 'tsx', path.resolve(process.cwd(), 'ai-slop-detector.ts'), path.resolve(process.cwd(), 'tests/t4-educational-output.ts')], + { encoding: 'utf-8', cwd: process.cwd() } + ); + + assert.ok(!result.stdout.includes('as unknown as TargetType'), + `guidance should not recommend chained assertions. stdout:"${result.stdout}"`); +}); + test('findUnsafeAssertions detects multiple as any assertions on separate lines', () => { const code = 'const a = x as any;\nconst b = y as any;'; diff --git a/tests/t4-educational-output.ts b/tests/t4-educational-output.ts index 5d23fa4..4a7dd51 100644 --- a/tests/t4-educational-output.ts +++ b/tests/t4-educational-output.ts @@ -6,7 +6,7 @@ // Should show: Fix: Replace with 'unknown' and use type guards const data: any = {}; -// Should show: Fix: Use 'as unknown as TargetType' +// Should show: Fix: Prefer migrating the source/data contract first const value = something as any; // Should show: Fix: Actually implement the logic