Skip to content

Commit 861f367

Browse files
chore: wip
1 parent 4886956 commit 861f367

4 files changed

Lines changed: 189 additions & 57 deletions

File tree

packages/dtsx/.test-cli/docs-test/api-docs/API.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# API Documentation
22

3-
> Generated on 2026-03-11T12:43:22.438Z
3+
> Generated on 2026-03-23T17:32:25.477Z
44
55
## Table of Contents
66

packages/dtsx/src/extractor/scanner.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1627,12 +1627,14 @@ export function scanDeclarations(_source: string, _filename: string, _keepCommen
16271627
depth++
16281628
else if (ic === CH_RPAREN || ic === CH_RBRACE || ic === CH_RBRACKET)
16291629
depth--
1630-
else if (ic === CH_LANGLE) {
1630+
else if (ic === CH_LANGLE && depth === 0) {
1631+
// Only track angle brackets at depth 0 (top-level generics like Map<K,V>).
1632+
// Inside braces (function bodies), < and > are comparison operators.
16311633
// Don't count <= as opening angle bracket
16321634
if (pos + 1 >= len || source.charCodeAt(pos + 1) !== CH_EQUAL)
16331635
angleDepth++
16341636
}
1635-
else if (ic === CH_RANGLE && !isArrowGT()) {
1637+
else if (ic === CH_RANGLE && depth === 0 && !isArrowGT()) {
16361638
// Don't count >= as closing angle bracket, and prevent going negative
16371639
if (angleDepth > 0 && (pos + 1 >= len || source.charCodeAt(pos + 1) !== CH_EQUAL))
16381640
angleDepth--

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

Lines changed: 179 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,11 @@ export function inferNarrowType(value: unknown, isConst: boolean = false, inUnio
219219
return inferObjectType(trimmed, isConst, _depth + 1)
220220
}
221221

222+
// Class expressions — extract the class name and use typeof
223+
if (trimmed.startsWith('class ') || trimmed.startsWith('class{')) {
224+
return inferClassExpressionType(trimmed)
225+
}
226+
222227
// New expressions (check before function expressions since `new X(() => {})` contains `=>`)
223228
if (trimmed.startsWith('new ')) {
224229
return inferNewExpressionType(trimmed)
@@ -312,36 +317,66 @@ function inferTemplateLiteralType(value: string, isConst: boolean): string {
312317
*/
313318
function inferNewExpressionType(value: string): string {
314319
// Extract class name after 'new ' — must start with uppercase A-Z
320+
// Supports dotted names like Intl.NumberFormat
315321
let i = 4 // skip 'new '
316322
while (i < value.length && value.charCodeAt(i) <= 32) i++ // skip whitespace
317323
const nameStart = i
318324
const firstChar = value.charCodeAt(i)
319325
if (firstChar < 65 || firstChar > 90) return 'unknown' // must start with A-Z
320-
while (i < value.length && isWordChar(value.charCodeAt(i))) i++
326+
while (i < value.length && (isWordChar(value.charCodeAt(i)) || value.charCodeAt(i) === 46 /* . */)) i++
321327
if (i === nameStart) return 'unknown'
322328
const className = value.slice(nameStart, i)
323329

324330
{
325331
const afterClass = value.slice(i)
332+
333+
// Check for generic type parameters
334+
let afterGenerics = afterClass
326335
if (afterClass.startsWith('<')) {
327336
// Extract the generic params by finding the matching '>'
328337
let depth = 0
329338
let end = -1
330-
for (let i = 0; i < afterClass.length; i++) {
331-
if (afterClass[i] === '<') {
332-
depth++
333-
}
334-
else if (afterClass[i] === '>') {
335-
depth--
336-
if (depth === 0) { end = i; break }
337-
}
339+
for (let j = 0; j < afterClass.length; j++) {
340+
if (afterClass[j] === '<') depth++
341+
else if (afterClass[j] === '>') { depth--; if (depth === 0) { end = j; break } }
338342
}
339343
if (end !== -1) {
340344
const generics = afterClass.slice(0, end + 1)
345+
afterGenerics = afterClass.slice(end + 1)
346+
// Check for method chain after constructor (e.g., new Foo<T>(...).method())
347+
// Find closing paren of constructor, then check for '.'
348+
const parenStart = afterGenerics.indexOf('(')
349+
if (parenStart !== -1) {
350+
let pDepth = 0
351+
let pEnd = -1
352+
for (let j = parenStart; j < afterGenerics.length; j++) {
353+
if (afterGenerics[j] === '(') pDepth++
354+
else if (afterGenerics[j] === ')') { pDepth--; if (pDepth === 0) { pEnd = j; break } }
355+
}
356+
if (pEnd !== -1) {
357+
const rest = afterGenerics.slice(pEnd + 1).trimStart()
358+
if (rest.startsWith('.')) return 'unknown' // method chain — can't infer
359+
}
360+
}
341361
return `${className}${generics}`
342362
}
343363
}
344364

365+
// Check for method chain after constructor (e.g., new Foo(...).method())
366+
const parenStart = afterGenerics.indexOf('(')
367+
if (parenStart !== -1) {
368+
let pDepth = 0
369+
let pEnd = -1
370+
for (let j = parenStart; j < afterGenerics.length; j++) {
371+
if (afterGenerics[j] === '(') pDepth++
372+
else if (afterGenerics[j] === ')') { pDepth--; if (pDepth === 0) { pEnd = j; break } }
373+
}
374+
if (pEnd !== -1) {
375+
const rest = afterGenerics.slice(pEnd + 1).trimStart()
376+
if (rest.startsWith('.')) return 'unknown' // method chain — can't infer
377+
}
378+
}
379+
345380
// Fallback: use default generic params for known built-in types
346381
switch (className) {
347382
case 'Date': return 'Date'
@@ -361,6 +396,31 @@ function inferNewExpressionType(value: string): string {
361396
return 'unknown'
362397
}
363398

399+
/**
400+
* Infer type from class expression used as a value.
401+
* For `class Foo { ... }`, extracts the class name and returns `typeof Foo`.
402+
* For anonymous classes, returns a basic constructor type.
403+
*/
404+
function inferClassExpressionType(value: string): string {
405+
// Extract class name: class Name or class Name extends ...
406+
const trimmed = value.trimStart()
407+
let i = 5 // skip 'class'
408+
// Skip whitespace
409+
while (i < trimmed.length && trimmed.charCodeAt(i) <= 32) i++
410+
411+
// Check if there's a name (identifier starting char)
412+
const nameStart = i
413+
if (i < trimmed.length && isWordChar(trimmed.charCodeAt(i))) {
414+
while (i < trimmed.length && isWordChar(trimmed.charCodeAt(i))) i++
415+
const className = trimmed.slice(nameStart, i)
416+
// Named class expression — use typeof ClassName
417+
return `typeof ${className}`
418+
}
419+
420+
// Anonymous class expression
421+
return '{ new (...args: any[]): any }'
422+
}
423+
364424
/**
365425
* Infer type from Promise expression
366426
*/
@@ -669,16 +729,26 @@ export function inferObjectType(value: string, isConst: boolean, _depth: number
669729
const saved = _cleanDefaultResult
670730
_cleanDefaultResult = null
671731

672-
let valueType = inferNarrowType(val, isConst, false, _depth + 1)
732+
let valueType: string
733+
const trimVal = val.trim()
673734

674-
const nestedDefault = _cleanDefaultResult
675-
_cleanDefaultResult = saved
735+
// Method definitions (method shorthand syntax) — convert directly to function type
736+
// to avoid double-processing through inferNarrowType which loses return type info
737+
if (isMethodDefinition(trimVal)) {
738+
valueType = convertMethodToFunctionType(key, trimVal)
739+
}
740+
else {
741+
valueType = inferNarrowType(val, isConst, false, _depth + 1)
676742

677-
// Handle method signatures - clean up async and parameter defaults
678-
if (valueType.includes('=>') || valueType.includes('function') || valueType.includes('async')) {
679-
valueType = cleanMethodSignature(valueType)
743+
// Handle method signatures - clean up async and parameter defaults
744+
if (valueType.includes('=>') || valueType.includes('function') || valueType.includes('async')) {
745+
valueType = cleanMethodSignature(valueType)
746+
}
680747
}
681748

749+
const nestedDefault = _cleanDefaultResult
750+
_cleanDefaultResult = saved
751+
682752
// Add inline @defaultValue for widened primitive properties
683753
const rawVal = val.trim()
684754
if (!isConst && isBaseType(valueType) && isPrimitiveLiteral(rawVal)) {
@@ -1073,11 +1143,18 @@ function parseObjectProperties(content: string): Array<[string, string]> {
10731143
// Method definition like: methodName(params) or async methodName<T>(params)
10741144
// Must be checked BEFORE general bracket tracking so ( isn't swallowed
10751145
currentKey = current.trim()
1076-
// Remove 'async' from the key if present
1146+
// Remove 'async' from the key if present, prefix value with 'async ' marker
1147+
let methodPrefix = ''
10771148
if (currentKey.startsWith('async ')) {
10781149
currentKey = currentKey.slice(6).trim()
1150+
methodPrefix = 'async '
1151+
}
1152+
// Remove generator '*' prefix from key, prefix value with '*' marker
1153+
if (currentKey.startsWith('*')) {
1154+
currentKey = currentKey.slice(1).trim()
1155+
methodPrefix += '*'
10791156
}
1080-
current = char // Start with the opening parenthesis
1157+
current = methodPrefix + char // Start with any prefix + opening parenthesis
10811158
inKey = false
10821159
depth = 1 // We're now inside the method definition
10831160
}
@@ -1096,18 +1173,7 @@ function parseObjectProperties(content: string): Array<[string, string]> {
10961173
}
10971174
else if (cc === 44 /* , */ && depth === 0) {
10981175
if (currentKey && current.trim()) {
1099-
// Clean method signatures before storing
1100-
let value = current.trim()
1101-
1102-
// Check if this is a method definition (starts with parentheses)
1103-
if (value.startsWith('(')) {
1104-
// This is a method definition like: (params): ReturnType { ... }
1105-
value = convertMethodToFunctionType(currentKey, value)
1106-
}
1107-
else if (value.includes('=>') || value.includes('function') || value.includes('async')) {
1108-
value = cleanMethodSignature(value)
1109-
}
1110-
1176+
const value = current.trim()
11111177
properties.push([currentKey, value])
11121178
}
11131179
current = ''
@@ -1126,33 +1192,83 @@ function parseObjectProperties(content: string): Array<[string, string]> {
11261192

11271193
// Don't forget the last property
11281194
if (currentKey && current.trim()) {
1129-
let value = current.trim()
1130-
1131-
// Check if this is a method definition (starts with parentheses)
1132-
if (value.startsWith('(')) {
1133-
// This is a method definition like: (params): ReturnType { ... }
1134-
value = convertMethodToFunctionType(currentKey, value)
1135-
}
1136-
else if (value.includes('=>') || value.includes('function') || value.includes('async')) {
1137-
value = cleanMethodSignature(value)
1138-
}
1139-
1195+
const value = current.trim()
11401196
properties.push([currentKey, value])
11411197
}
11421198

11431199
return properties
11441200
}
11451201

1202+
/** Check if value looks like a method definition (not an arrow function).
1203+
* Method definitions: (params): ReturnType { body }
1204+
* Arrow functions: (params): ReturnType => body
1205+
* Key difference: method definitions have no '=>' at top level. */
1206+
function isMethodDefinition(value: string): boolean {
1207+
let stripped = value
1208+
// Strip async/generator prefixes to check what follows
1209+
if (stripped.startsWith('async ') || stripped.startsWith('async\t')) {
1210+
stripped = stripped.slice(5).trimStart()
1211+
}
1212+
if (stripped.startsWith('*')) {
1213+
stripped = stripped.slice(1).trimStart()
1214+
}
1215+
// Must start with '(' to be a method definition
1216+
if (!stripped.startsWith('(')) return false
1217+
1218+
// Find the matching ')' for the parameter list
1219+
let depth = 0
1220+
for (let i = 0; i < stripped.length; i++) {
1221+
const c = stripped.charCodeAt(i)
1222+
if (c === 40 /* ( */) depth++
1223+
else if (c === 41 /* ) */) {
1224+
depth--
1225+
if (depth === 0) {
1226+
// After the closing paren, check if there's '=>' (arrow function) or not (method def)
1227+
const after = stripped.slice(i + 1).trimStart()
1228+
// Method definitions have ':' then return type then '{', or just '{'
1229+
// Arrow functions have '=>' (possibly after ': ReturnType')
1230+
if (after.startsWith('{')) return true // (params) { body }
1231+
if (after.startsWith(':')) {
1232+
// Could be (params): ReturnType { body } or (params): ReturnType => body
1233+
// Check if '=>' appears before '{' at depth 0
1234+
let scanDepth = 0
1235+
for (let j = 0; j < after.length; j++) {
1236+
const sc = after.charCodeAt(j)
1237+
if (sc === 40 || sc === 91) scanDepth++
1238+
else if (sc === 41 || sc === 93) scanDepth--
1239+
else if (sc === 60) scanDepth++ // < for generics in return type
1240+
else if (sc === 62) { if (scanDepth > 0) scanDepth-- } // >
1241+
else if (scanDepth === 0 && sc === 61 /* = */ && j + 1 < after.length && after.charCodeAt(j + 1) === 62 /* > */) {
1242+
return false // Found '=>' — this is an arrow function
1243+
}
1244+
else if (scanDepth === 0 && sc === 123 /* { */) {
1245+
return true // Found '{' before any '=>' — this is a method definition
1246+
}
1247+
}
1248+
}
1249+
return false
1250+
}
1251+
}
1252+
}
1253+
return false
1254+
}
1255+
11461256
/**
11471257
* Convert method definition to function type signature
11481258
*/
11491259
function convertMethodToFunctionType(_methodName: string, _methodDef: string): string {
1150-
// Remove async modifier if present — no regex
1151-
let cleaned = _methodDef
1152-
let ci = 0
1153-
while (ci < cleaned.length && cleaned.charCodeAt(ci) <= 32) ci++
1154-
if (cleaned.startsWith('async', ci) && ci + 5 < cleaned.length && cleaned.charCodeAt(ci + 5) <= 32) {
1155-
cleaned = cleaned.slice(ci + 5).trimStart()
1260+
// Detect and remove async/generator prefixes
1261+
let cleaned = _methodDef.trimStart()
1262+
let isAsync = false
1263+
let isGenerator = false
1264+
1265+
if (cleaned.startsWith('async ') || cleaned.startsWith('async\t')) {
1266+
isAsync = true
1267+
cleaned = cleaned.slice(5).trimStart()
1268+
}
1269+
if (cleaned.startsWith('*')) {
1270+
isGenerator = true
1271+
cleaned = cleaned.slice(1).trimStart()
11561272
}
11571273

11581274
// Extract generics: starts with '<', find matching '>'
@@ -1212,6 +1328,23 @@ function convertMethodToFunctionType(_methodName: string, _methodDef: string): s
12121328
}
12131329
}
12141330

1331+
// Apply async/generator defaults when no explicit return type was provided
1332+
if (returnType === 'unknown') {
1333+
if (isAsync && isGenerator) {
1334+
returnType = 'AsyncGenerator<unknown, void, unknown>'
1335+
}
1336+
else if (isGenerator) {
1337+
returnType = 'Generator<unknown, void, unknown>'
1338+
}
1339+
else if (isAsync) {
1340+
returnType = 'Promise<void>'
1341+
}
1342+
}
1343+
else if (isAsync && !returnType.startsWith('Promise<') && !returnType.startsWith('AsyncGenerator<')) {
1344+
// If async method has explicit non-Promise return type, wrap it
1345+
returnType = `Promise<${returnType}>`
1346+
}
1347+
12151348
// Clean parameter defaults
12161349
const cleanedParams = cleanParameterDefaults(params)
12171350

packages/dtsx/test/fixtures/output/variable.d.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,7 @@ export declare const complexArrays: {
9696
* @defaultValue
9797
* ```ts
9898
* {
99-
* handlers: {
100-
* onSuccess<T>: (data: T) => unknown,
101-
* onError: (error: Error & { code?: number }) => unknown,
102-
* someOtherMethod: () => unknown
103-
* },
99+
* handlers: { onSuccess<T>: () => unknown },
104100
* utils: {
105101
* formatters: {
106102
* date: (input: Date) => unknown,
@@ -111,7 +107,7 @@ export declare const complexArrays: {
111107
* ```
112108
*/
113109
export declare const complexObject: {
114-
handlers: { onSuccess<T>: (data: T) => unknown; onError: (error: Error & { code?: number }) => unknown; someOtherMethod: () => unknown };
110+
handlers: { onSuccess<T>: (data: T) => Promise<void>; onError: (error: Error & { code?: number }) => never; someOtherMethod: () => unknown };
115111
utils: { formatters: { date: (input: Date) => unknown; currency: (amount: number, currency?: string) => unknown } }
116112
};
117113
// Method Decorators and Metadata (declares as unknown, because it should rely on explicit type)
@@ -151,7 +147,8 @@ export declare const CONFIG_MAP: {
151147
}
152148
}
153149
};
150+
/** @defaultValue `{ run: () => unknown, runSync: () => unknown }` */
154151
export declare const command: {
155-
run: unknown;
156-
runSync: unknown
152+
run: () => unknown;
153+
runSync: () => unknown
157154
};

0 commit comments

Comments
 (0)