-
Notifications
You must be signed in to change notification settings - Fork 2
feat: migrate unsafe_type_assertion from regex to AST detection #24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f0c4ce2
434c6d5
eef4363
86746fc
7c9ab7c
91c6b76
3a9b942
dbd609b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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', | ||
|
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', | ||
|
|
@@ -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', | ||
|
|
@@ -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, | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| }, | ||
| { | ||
| id: 'index_signature_any', | ||
|
|
@@ -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)' | ||
|
|
@@ -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') { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion: The regex path now skips 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.(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; | ||
| } | ||
|
|
||
|
|
@@ -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') { | ||
|
|
@@ -1822,8 +1867,6 @@ class AISlopDetector { | |
| } | ||
|
|
||
|
|
||
|
|
||
|
|
||
| /** | ||
| * Calculate comprehensive KarpeSlop score based on the three axes | ||
| */ | ||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.