Skip to content

Commit 923f207

Browse files
committed
fix(core): stop infinite parent-walk bug in hasCatchChain crashing extraction
1 parent a9e70b3 commit 923f207

3 files changed

Lines changed: 26 additions & 9 deletions

File tree

packages/core/src/parse/api-extractor.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,22 @@ describe('extractApiCalls', () => {
144144
const call = single("fetch('/u')")
145145
expect(call.hasErrorHandler).toBe(false)
146146
})
147+
148+
it('does not throw for await fetch inside async function (regression)', () => {
149+
const source = `
150+
export async function loadItems() {
151+
const res = await fetch('/api/items', { headers: { Accept: 'application/json' } });
152+
const body = await res.json().catch(() => ({}));
153+
return body;
154+
}
155+
`
156+
const calls = extract(source)
157+
expect(calls.length).toBeGreaterThanOrEqual(1)
158+
const fetchCall = calls.find((c) => c.caller === 'fetch')
159+
expect(fetchCall).toBeDefined()
160+
expect(fetchCall!.url).toBe('/api/items')
161+
expect(fetchCall!.hasErrorHandler).toBe(false)
162+
})
147163
})
148164

149165
describe('known false positives', () => {

packages/core/src/parse/api-extractor.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -497,12 +497,12 @@ function isInsideTryCatch(node: ts.Node): boolean {
497497
}
498498

499499
function hasCatchChain(node: ts.CallExpression): boolean {
500-
// Walk up property access chains: fetch(...).then(...).catch(...)
501-
for (let current: ts.Node = node; ; current = current.parent) {
502-
const parent = current.parent
503-
504-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- SourceFile has no parent
505-
if (!parent) return false
500+
// Walk up from node; check whether any ancestor is a .catch / .then(_, reject) chain.
501+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- AST root may lack a parent
502+
for (let parent = node.parent; parent; parent = parent.parent) {
503+
if (ts.isSourceFile(parent)) {
504+
return false
505+
}
506506

507507
if (ts.isPropertyAccessExpression(parent) && parent.name.text === 'catch') {
508508
return true
@@ -522,9 +522,9 @@ function hasCatchChain(node: ts.CallExpression): boolean {
522522
if (ts.isExpressionStatement(parent) || ts.isVariableDeclaration(parent)) {
523523
return false
524524
}
525-
526-
current = parent
527525
}
526+
527+
return false
528528
}
529529

530530
function getLocation(node: ts.Node, sourceFile: ts.SourceFile): SourceLocation {

packages/core/src/runner/scanner.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,10 @@ export async function scan(config: ScanConfig): Promise<ScanResult> {
147147
allApiCalls.push(...calls)
148148
} catch (cause) {
149149
filesErrored++
150+
const detail = cause instanceof Error ? cause.message : String(cause)
150151
diagnostics.push({
151152
kind: 'parse-error',
152-
message: `Failed to parse ${file.relativePath}`,
153+
message: `Failed to parse ${file.relativePath}: ${detail}`,
153154
file: file.absolutePath,
154155
cause: cause instanceof Error ? cause : new Error(String(cause)),
155156
})

0 commit comments

Comments
 (0)