Skip to content

Commit 3dcbfef

Browse files
fix(type-inference): preserve async method shorthand and generic-typed return values
Two related parser issues in object literal property parsing: - A JSDoc comment immediately preceding `async method() {}` left the comment text in the key buffer, so the `async` modifier was rendered as part of the property type, producing invalid `.d.ts`. - The comma inside a generic return type (e.g. `Record<string, unknown>`) was treated as a property separator because depth tracking did not account for being inside a method body's signature. Track method-shorthand state explicitly: skip leading comments before identifier detection, peel off `async`/`*` prefixes onto the value, and suppress comma-splitting until the method body closes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 254ec32 commit 3dcbfef

1 file changed

Lines changed: 63 additions & 10 deletions

File tree

packages/dtsx/src/processor/type-inference.ts

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1130,6 +1130,29 @@ function cleanSingleParam(param: string): string {
11301130
return param
11311131
}
11321132

1133+
/**
1134+
* Find the index after any leading block comments and whitespace in a key.
1135+
* Used to locate the actual identifier so modifiers like `async` can be
1136+
* detected even when JSDoc precedes the method name.
1137+
*/
1138+
function findIdentifierStart(key: string): number {
1139+
let i = 0
1140+
const n = key.length
1141+
while (i < n) {
1142+
// Skip whitespace
1143+
while (i < n && (key.charCodeAt(i) === 32 || key.charCodeAt(i) === 9 || key.charCodeAt(i) === 10 || key.charCodeAt(i) === 13)) i++
1144+
// Skip /* ... */ block comments (handles nested */ since dtsx-stripped)
1145+
if (i + 1 < n && key.charCodeAt(i) === 47 && key.charCodeAt(i + 1) === 42) {
1146+
i += 2
1147+
while (i + 1 < n && !(key.charCodeAt(i) === 42 && key.charCodeAt(i + 1) === 47)) i++
1148+
i += 2 // skip the closing '*/'
1149+
continue
1150+
}
1151+
break
1152+
}
1153+
return i
1154+
}
1155+
11331156
/**
11341157
* Parse object properties
11351158
*/
@@ -1143,6 +1166,13 @@ function parseObjectProperties(content: string): Array<[string, string]> {
11431166
let inKey = true
11441167
let inComment = false
11451168
let commentDepth = 0
1169+
// True while collecting the value of a method shorthand (between the
1170+
// method's '(' and the body's closing '}').
1171+
let inMethodShorthand = false
1172+
// True between the closing ')' of a method shorthand's params and its body '{'.
1173+
// While true, commas at depth 0 belong to the return-type annotation
1174+
// (e.g. `Promise<Record<string, X>>`) and must not split properties.
1175+
let methodAwaitingBody = false
11461176

11471177
for (let i = 0; i < content.length; i++) {
11481178
const cc = content.charCodeAt(i)
@@ -1196,38 +1226,61 @@ function parseObjectProperties(content: string): Array<[string, string]> {
11961226
}
11971227
else if (!inString && !inComment) {
11981228
if (cc === 40 /* ( */ && depth === 0 && inKey) {
1199-
// Method definition like: methodName(params) or async methodName<T>(params)
1200-
// Must be checked BEFORE general bracket tracking so ( isn't swallowed
1229+
// Method definition like: methodName(params) or async methodName<T>(params).
1230+
// Must be checked BEFORE general bracket tracking so ( isn't swallowed.
12011231
currentKey = current.trim()
1202-
// Remove 'async' from the key if present, prefix value with 'async ' marker
1232+
// The key may carry leading JSDoc/block comments. Split off the
1233+
// comment block so `async`/`*` modifiers right before the
1234+
// identifier can still be detected and stripped.
1235+
const idStart = findIdentifierStart(currentKey)
1236+
const commentLead = idStart > 0 ? currentKey.slice(0, idStart) : ''
1237+
let identifier = currentKey.slice(idStart)
12031238
let methodPrefix = ''
1204-
if (currentKey.startsWith('async ')) {
1205-
currentKey = currentKey.slice(6).trim()
1239+
if (identifier.startsWith('async ') || identifier.startsWith('async\t') || identifier.startsWith('async\n')) {
1240+
identifier = identifier.slice(6).trimStart()
12061241
methodPrefix = 'async '
12071242
}
1208-
// Remove generator '*' prefix from key, prefix value with '*' marker
1209-
if (currentKey.startsWith('*')) {
1210-
currentKey = currentKey.slice(1).trim()
1243+
if (identifier.startsWith('*')) {
1244+
identifier = identifier.slice(1).trimStart()
12111245
methodPrefix += '*'
12121246
}
1247+
currentKey = commentLead + identifier
12131248
current = methodPrefix + char // Start with any prefix + opening parenthesis
12141249
inKey = false
1215-
depth = 1 // We're now inside the method definition
1250+
inMethodShorthand = true
1251+
methodAwaitingBody = false
1252+
depth = 1 // We're now inside the method definition's params
12161253
}
12171254
else if (cc === 123 /* { */ || cc === 91 /* [ */ || cc === 40 /* ( */) {
12181255
depth++
12191256
current += char
1257+
// Body '{' of a method shorthand — type annotation phase ends here.
1258+
if (methodAwaitingBody && cc === 123 /* { */) {
1259+
methodAwaitingBody = false
1260+
}
12201261
}
12211262
else if (cc === 125 /* } */ || cc === 93 /* ] */ || cc === 41 /* ) */) {
12221263
depth--
12231264
current += char
1265+
if (inMethodShorthand) {
1266+
// Closing ')' of params at depth 0 → enter return-type annotation
1267+
// phase (commas in `Record<K, V>` etc must not split properties).
1268+
if (cc === 41 /* ) */ && depth === 0 && !methodAwaitingBody) {
1269+
methodAwaitingBody = true
1270+
}
1271+
// Closing '}' at depth 0 ends the method shorthand value entirely.
1272+
else if (cc === 125 /* } */ && depth === 0) {
1273+
inMethodShorthand = false
1274+
methodAwaitingBody = false
1275+
}
1276+
}
12241277
}
12251278
else if (cc === 58 /* : */ && depth === 0 && inKey) {
12261279
currentKey = current.trim()
12271280
current = ''
12281281
inKey = false
12291282
}
1230-
else if (cc === 44 /* , */ && depth === 0) {
1283+
else if (cc === 44 /* , */ && depth === 0 && !methodAwaitingBody) {
12311284
if (currentKey && current.trim()) {
12321285
const value = current.trim()
12331286
properties.push([currentKey, value])

0 commit comments

Comments
 (0)