Skip to content
Merged
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

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.
- **`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__/`, `__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

### Added
Expand Down
115 changes: 79 additions & 36 deletions ai-slop-detector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
};

// Syntax-only (non-type-checked) detection: matches by identifier text, not
Expand Down Expand Up @@ -224,24 +228,66 @@ 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;
}

if (ts.isAsExpression(node.expression) && node.expression.type.kind === ts.SyntaxKind.UnknownKeyword) {
const code = sourceText.slice(node.getStart(sourceFile), node.end);
findings.push({
type: 'unsafe_double_type_assertion',
file: fileName,
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: 'chained_as',
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
// 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 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',
file: fileName,
line: asLine,
column: asColumn,
code,
message: "Found unsafe 'as any' type assertion. Use proper type guards or validation.",
severity: 'high',
assertionForm: 'as_unknown_as',
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',
Expand All @@ -254,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',
Expand Down Expand Up @@ -451,14 +502,16 @@ 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: /\s+as\s+any\b/g,
pattern: /(?!)/,
message: "Found unsafe 'as any' type assertion. Use proper type guards or validation.",
severity: 'high',
description: 'Detects unsafe as any assertions',
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'
description: 'Detects unsafe as any assertions (AST-based)',
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,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
{
id: 'index_signature_any',
Expand Down Expand Up @@ -503,21 +556,21 @@ 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.",
pattern: /(?!)/,
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',
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<T> (AST-based)'
Expand Down Expand Up @@ -978,7 +1031,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') {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The regex path now skips unsafe_type_assertion for every target extension, but the AST pass below runs only for .ts and .tsx. As a result, .js and .jsx files no longer produce this finding even though they remain supported scan targets and previously went through the regex detector. Either run the AST detection for those extensions with the appropriate script kind or retain equivalent detection for them. [api mismatch]

Severity Level: Critical 🚨
- ❌ JavaScript assertions are no longer reported.
- ❌ JSX assertion coverage is also lost.
- ⚠️ Supported target extensions behave inconsistently.
Steps of Reproduction ✅
1. Run the detector against a project containing `.js` or `.jsx` files;
`AISlopDetector.targetExtensions` explicitly includes these extensions at
`ai-slop-detector.ts:296`.

2. A target file containing `value as any` reaches the generic pattern loop in
`analyzeFile()` at `ai-slop-detector.ts:982-1000`.

3. The new condition at `ai-slop-detector.ts:998` skips every pattern whose ID is
`unsafe_type_assertion`, so the regex path cannot report the assertion.

4. The AST pass is guarded by `filePath.endsWith('.ts') || filePath.endsWith('.tsx')` at
`ai-slop-detector.ts:1173-1174`, so it never calls `findUnsafeAssertions()` for `.js` or
`.jsx` files. The result is no `unsafe_type_assertion` finding for those supported target
extensions.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** ai-slop-detector.ts
**Line:** 998:998
**Comment:**
	*Api Mismatch: The regex path now skips `unsafe_type_assertion` for every target extension, but the AST pass below runs only for `.ts` and `.tsx`. As a result, `.js` and `.jsx` files no longer produce this finding even though they remain supported scan targets and previously went through the regex detector. Either run the AST detection for those extensions with the appropriate script kind or retain equivalent detection for them.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

continue;
}

Expand Down Expand Up @@ -1120,15 +1174,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;
}
}



// In quiet mode, skip test and mock files for all patterns except production console logs
if (quiet && pattern.id !== 'production_console_log') {
Expand Down Expand Up @@ -1822,8 +1867,6 @@ class AISlopDetector {
}




/**
* Calculate comprehensive KarpeSlop score based on the three axes
*/
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Loading