@stacksjs/zig-dtsx 0.9.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +73 -0
- package/build.zig +79 -0
- package/build.zig.zon +11 -0
- package/package.json +23 -0
- package/src/char_utils.zig +158 -0
- package/src/emitter.zig +1045 -0
- package/src/extractors.zig +2464 -0
- package/src/index.ts +222 -0
- package/src/lib.zig +254 -0
- package/src/main.zig +532 -0
- package/src/scan_loop.zig +330 -0
- package/src/scanner.zig +908 -0
- package/src/type_inference.zig +1564 -0
- package/src/types.zig +105 -0
- package/test/benchmark.ts +343 -0
- package/test/fixtures/output/variable.d.ts +157 -0
- package/test/zig-dtsx.test.ts +1386 -0
- package/zig-out/bin/zig-dtsx +0 -0
- package/zig-out/bin/zig-dtsx.exe +0 -0
|
@@ -0,0 +1,1386 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test suite for zig-dtsx — validates that the Zig DTS emitter produces
|
|
3
|
+
* identical output to the TypeScript dtsx implementation.
|
|
4
|
+
*
|
|
5
|
+
* Runs against all test fixtures from packages/dtsx/test/fixtures/.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
8
|
+
import { join, resolve } from 'node:path'
|
|
9
|
+
import { describe, expect, test } from 'bun:test'
|
|
10
|
+
import { processSource, ZIG_AVAILABLE } from '../src/index'
|
|
11
|
+
|
|
12
|
+
const fixturesDir = resolve(import.meta.dir, '../../dtsx/test/fixtures')
|
|
13
|
+
const inputDir = join(fixturesDir, 'input')
|
|
14
|
+
const outputDir = join(fixturesDir, 'output')
|
|
15
|
+
const zigOverrideDir = resolve(import.meta.dir, 'fixtures/output')
|
|
16
|
+
|
|
17
|
+
function readFixture(dir: string, name: string): string {
|
|
18
|
+
if (dir === outputDir) {
|
|
19
|
+
const overridePath = join(zigOverrideDir, name)
|
|
20
|
+
if (existsSync(overridePath))
|
|
21
|
+
return readFileSync(overridePath, 'utf-8')
|
|
22
|
+
}
|
|
23
|
+
return readFileSync(join(dir, name), 'utf-8')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizeOutput(text: string): string {
|
|
27
|
+
return text
|
|
28
|
+
.replace(/\r\n/g, '\n')
|
|
29
|
+
.replace(/\n+$/, '')
|
|
30
|
+
.trim()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Helper: process and normalize for inline tests */
|
|
34
|
+
function dts(source: string, keepComments = true): string {
|
|
35
|
+
return normalizeOutput(processSource(source, keepComments))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Standard fixtures (top-level .ts files)
|
|
39
|
+
const standardFixtures = [
|
|
40
|
+
'abseil.io',
|
|
41
|
+
'advanced-types',
|
|
42
|
+
'class',
|
|
43
|
+
'comments',
|
|
44
|
+
'complex-class',
|
|
45
|
+
'edge-cases',
|
|
46
|
+
'enum',
|
|
47
|
+
'exports',
|
|
48
|
+
'function-types',
|
|
49
|
+
'function',
|
|
50
|
+
'generics',
|
|
51
|
+
'imports',
|
|
52
|
+
'interface',
|
|
53
|
+
'mixed-exports',
|
|
54
|
+
'module',
|
|
55
|
+
'namespace',
|
|
56
|
+
'private-members',
|
|
57
|
+
'ts-features',
|
|
58
|
+
'type',
|
|
59
|
+
'type-interface-imports',
|
|
60
|
+
'type-only-imports',
|
|
61
|
+
'variable',
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
// Example fixtures
|
|
65
|
+
const exampleFixtures = [
|
|
66
|
+
'0001', '0002', '0003', '0004', '0005', '0006',
|
|
67
|
+
'0007', '0008', '0009', '0010', '0011', '0012',
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
// Real-world fixtures
|
|
71
|
+
const realWorldFixtures = [
|
|
72
|
+
'lodash-like',
|
|
73
|
+
'react-like',
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
// ============================================================================
|
|
77
|
+
// Fixture-based tests
|
|
78
|
+
// ============================================================================
|
|
79
|
+
|
|
80
|
+
const describeIf = ZIG_AVAILABLE ? describe : describe.skip
|
|
81
|
+
|
|
82
|
+
describeIf('zig-dtsx', () => {
|
|
83
|
+
describe('standard fixtures', () => {
|
|
84
|
+
for (const fixture of standardFixtures) {
|
|
85
|
+
test(fixture, () => {
|
|
86
|
+
const input = readFixture(inputDir, `${fixture}.ts`)
|
|
87
|
+
const expected = readFixture(outputDir, `${fixture}.d.ts`)
|
|
88
|
+
|
|
89
|
+
const actual = processSource(input, true)
|
|
90
|
+
|
|
91
|
+
expect(normalizeOutput(actual)).toBe(normalizeOutput(expected))
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
describe('example fixtures', () => {
|
|
97
|
+
for (const fixture of exampleFixtures) {
|
|
98
|
+
test(fixture, () => {
|
|
99
|
+
const input = readFixture(join(inputDir, 'example'), `${fixture}.ts`)
|
|
100
|
+
const expected = readFixture(join(outputDir, 'example'), `${fixture}.d.ts`)
|
|
101
|
+
|
|
102
|
+
const actual = processSource(input, true)
|
|
103
|
+
|
|
104
|
+
expect(normalizeOutput(actual)).toBe(normalizeOutput(expected))
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
describe('real-world fixtures', () => {
|
|
110
|
+
for (const fixture of realWorldFixtures) {
|
|
111
|
+
test(fixture, () => {
|
|
112
|
+
const input = readFixture(join(inputDir, 'real-world'), `${fixture}.ts`)
|
|
113
|
+
// Real-world fixtures may not have expected output yet
|
|
114
|
+
try {
|
|
115
|
+
const expected = readFixture(join(outputDir, 'real-world'), `${fixture}.d.ts`)
|
|
116
|
+
const actual = processSource(input, true)
|
|
117
|
+
expect(normalizeOutput(actual)).toBe(normalizeOutput(expected))
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// If no expected output, just ensure it doesn't crash
|
|
121
|
+
const actual = processSource(input, true)
|
|
122
|
+
expect(actual).toBeTruthy()
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
describe('large file', () => {
|
|
129
|
+
test('checker.ts', () => {
|
|
130
|
+
const input = readFixture(inputDir, 'checker.ts')
|
|
131
|
+
|
|
132
|
+
// Just ensure it doesn't crash on large files and produces output
|
|
133
|
+
const actual = processSource(input, true)
|
|
134
|
+
expect(actual.length).toBeGreaterThan(0)
|
|
135
|
+
|
|
136
|
+
// If expected output exists, compare
|
|
137
|
+
try {
|
|
138
|
+
const expected = readFixture(outputDir, 'checker.d.ts')
|
|
139
|
+
expect(normalizeOutput(actual)).toBe(normalizeOutput(expected))
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// Large file may not have expected output
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
// ==========================================================================
|
|
148
|
+
// Inline edge case tests
|
|
149
|
+
// ==========================================================================
|
|
150
|
+
|
|
151
|
+
describe('empty and minimal inputs', () => {
|
|
152
|
+
test('empty input', () => {
|
|
153
|
+
expect(processSource('', true)).toBe('')
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('whitespace only', () => {
|
|
157
|
+
expect(dts(' \n\n\t\t\n ')).toBe('')
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
test('comments only', () => {
|
|
161
|
+
const result = dts('// just a comment\n/* block */\n')
|
|
162
|
+
expect(typeof result).toBe('string')
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('single newline', () => {
|
|
166
|
+
expect(dts('\n')).toBe('')
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
test('non-exported declarations only', () => {
|
|
170
|
+
const result = dts('const x = 5;\nfunction foo() {}\nclass Bar {}')
|
|
171
|
+
// Non-exported items should not appear with 'export' keyword
|
|
172
|
+
expect(result).not.toContain('export')
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test('only import statements', () => {
|
|
176
|
+
const result = dts(`import { foo } from 'bar'\nimport type { Baz } from 'qux'`)
|
|
177
|
+
// Should still process imports
|
|
178
|
+
expect(typeof result).toBe('string')
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
// ==========================================================================
|
|
183
|
+
// Variable declarations — narrow type inference
|
|
184
|
+
// ==========================================================================
|
|
185
|
+
|
|
186
|
+
describe('variable declarations', () => {
|
|
187
|
+
test('const string literal', () => {
|
|
188
|
+
const result = dts(`export const name = 'hello'`)
|
|
189
|
+
expect(result).toContain(`name: 'hello'`)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
test('const number literal', () => {
|
|
193
|
+
const result = dts(`export const port = 3000`)
|
|
194
|
+
expect(result).toContain('port: 3000')
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
test('const boolean literal true', () => {
|
|
198
|
+
const result = dts(`export const debug = true`)
|
|
199
|
+
expect(result).toContain('debug: true')
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
test('const boolean literal false', () => {
|
|
203
|
+
const result = dts(`export const disabled = false`)
|
|
204
|
+
expect(result).toContain('disabled: false')
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
test('const null', () => {
|
|
208
|
+
const result = dts(`export const nothing = null`)
|
|
209
|
+
expect(result).toContain('nothing: null')
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
test('const undefined', () => {
|
|
213
|
+
const result = dts(`export const undef = undefined`)
|
|
214
|
+
expect(result).toContain('undef: undefined')
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
test('const negative number', () => {
|
|
218
|
+
const result = dts(`export const neg = -42`)
|
|
219
|
+
expect(result).toContain('neg: -42')
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
test('const float number', () => {
|
|
223
|
+
const result = dts(`export const pi = 3.14`)
|
|
224
|
+
expect(result).toContain('pi: 3.14')
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
test('const template literal', () => {
|
|
228
|
+
const result = dts('export const greeting = `Hello World`')
|
|
229
|
+
expect(result).toContain('greeting: `Hello World`')
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
test('const bigint literal', () => {
|
|
233
|
+
const result = dts('export const big = 123n')
|
|
234
|
+
expect(result).toContain('big: 123n')
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
test('let with string value', () => {
|
|
238
|
+
const result = dts(`export let test = 'test'`)
|
|
239
|
+
expect(result).toContain('declare let test')
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
test('var with string value', () => {
|
|
243
|
+
const result = dts(`export var hello = 'world'`)
|
|
244
|
+
expect(result).toContain('declare var hello')
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
test('const with explicit type annotation', () => {
|
|
248
|
+
const result = dts(`export const count: number = 42`)
|
|
249
|
+
expect(result).toContain('count: number')
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
test('const array — widened array type', () => {
|
|
253
|
+
const result = dts(`export const items = [1, 2, 3]`)
|
|
254
|
+
expect(result).toContain('number[]')
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
test('const mixed array — widened array union type', () => {
|
|
258
|
+
const result = dts(`export const mixed = ['a', 1, true]`)
|
|
259
|
+
expect(result).toContain('string')
|
|
260
|
+
expect(result).toContain('number')
|
|
261
|
+
expect(result).toContain('boolean')
|
|
262
|
+
expect(result).toContain(')[]')
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
test('const empty array', () => {
|
|
266
|
+
const result = dts(`export const empty = []`)
|
|
267
|
+
expect(result).toContain('declare const empty')
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
test('const object literal — widened with declaration-level @defaultValue', () => {
|
|
271
|
+
const result = dts(`export const obj = { a: 1, b: 'two' }`)
|
|
272
|
+
expect(result).toContain('a: number')
|
|
273
|
+
expect(result).toContain('b: string')
|
|
274
|
+
expect(result).toContain("@defaultValue `{ a: 1, b: 'two' }`")
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
test('const nested object — widened with declaration-level @defaultValue', () => {
|
|
278
|
+
const result = dts(`export const nested = { inner: { value: 42 } }`)
|
|
279
|
+
expect(result).toContain('inner:')
|
|
280
|
+
expect(result).toContain('value: number')
|
|
281
|
+
expect(result).toContain("@defaultValue `{ inner: { value: 42 } }`")
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
test('const with as const', () => {
|
|
285
|
+
const result = dts(`export const STATUS = ['a', 'b', 'c'] as const`)
|
|
286
|
+
expect(result).toContain('readonly')
|
|
287
|
+
expect(result).toContain("'a'")
|
|
288
|
+
expect(result).toContain("'b'")
|
|
289
|
+
expect(result).toContain("'c'")
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
test('const object with as const', () => {
|
|
293
|
+
const result = dts(`export const config = { port: 3000, host: 'localhost' } as const`)
|
|
294
|
+
expect(result).toContain('port: 3000')
|
|
295
|
+
expect(result).toContain("host: 'localhost'")
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
test('const with satisfies', () => {
|
|
299
|
+
const result = dts(`
|
|
300
|
+
interface Config { port: number; host: string }
|
|
301
|
+
export const config = { port: 3000, host: 'localhost' } satisfies Config
|
|
302
|
+
`)
|
|
303
|
+
expect(result).toContain('config: Config')
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
test('generic type annotation replaced with narrow type', () => {
|
|
307
|
+
const result = dts(`export const conf: { [key: string]: string } = { a: 'hello', b: 'world' }`)
|
|
308
|
+
expect(result).toContain("a: 'hello'")
|
|
309
|
+
expect(result).toContain("b: 'world'")
|
|
310
|
+
expect(result).not.toContain('[key: string]')
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
test('Record<> type replaced with narrow type', () => {
|
|
314
|
+
const result = dts(`export const map: Record<string, number> = { x: 1, y: 2 }`)
|
|
315
|
+
expect(result).toContain('x: 1')
|
|
316
|
+
expect(result).toContain('y: 2')
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
test('const with Promise.resolve', () => {
|
|
320
|
+
const result = dts(`export const p = Promise.resolve(42)`)
|
|
321
|
+
expect(result).toContain('Promise<42>')
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
test('const regex', () => {
|
|
325
|
+
const result = dts(`export const re = /test/g`)
|
|
326
|
+
expect(result).toContain('declare const re')
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
test('const new Date()', () => {
|
|
330
|
+
const result = dts(`export const date = new Date()`)
|
|
331
|
+
expect(result).toContain('declare const date')
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
test('const new Map()', () => {
|
|
335
|
+
const result = dts(`export const m = new Map<string, number>()`)
|
|
336
|
+
expect(result).toContain('declare const m')
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
test('multiple declarations on separate lines', () => {
|
|
340
|
+
const result = dts(`
|
|
341
|
+
export const a = 1
|
|
342
|
+
export const b = 'two'
|
|
343
|
+
export const c = true
|
|
344
|
+
`)
|
|
345
|
+
expect(result).toContain('a: 1')
|
|
346
|
+
expect(result).toContain("b: 'two'")
|
|
347
|
+
expect(result).toContain('c: true')
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
test('deeply nested object with as const', () => {
|
|
351
|
+
const result = dts(`
|
|
352
|
+
export const deep = {
|
|
353
|
+
l1: {
|
|
354
|
+
l2: {
|
|
355
|
+
l3: {
|
|
356
|
+
val: 42,
|
|
357
|
+
arr: [1, 2, 3],
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
} as const
|
|
362
|
+
`)
|
|
363
|
+
expect(result).toContain('val: 42')
|
|
364
|
+
expect(result).toContain('readonly [1, 2, 3]')
|
|
365
|
+
})
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
// ==========================================================================
|
|
369
|
+
// Function declarations
|
|
370
|
+
// ==========================================================================
|
|
371
|
+
|
|
372
|
+
describe('function declarations', () => {
|
|
373
|
+
test('simple function with return type', () => {
|
|
374
|
+
const result = dts(`export function greet(name: string): string { return name }`)
|
|
375
|
+
expect(result).toContain('export declare function greet(name: string): string')
|
|
376
|
+
expect(result).not.toContain('return')
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
test('function with no return type', () => {
|
|
380
|
+
const result = dts(`export function doStuff(x: number) { console.log(x) }`)
|
|
381
|
+
expect(result).toContain('declare function doStuff')
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
test('async function', () => {
|
|
385
|
+
const result = dts(`export async function fetchData(url: string): Promise<string> { return '' }`)
|
|
386
|
+
expect(result).toContain('declare function fetchData(url: string): Promise<string>')
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
test('function with void return', () => {
|
|
390
|
+
const result = dts(`export function log(msg: string): void { console.log(msg) }`)
|
|
391
|
+
expect(result).toContain('declare function log(msg: string): void')
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
test('function with optional parameter', () => {
|
|
395
|
+
const result = dts(`export function create(name: string, age?: number): void {}`)
|
|
396
|
+
expect(result).toContain('age?: number')
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
test('function with default parameter', () => {
|
|
400
|
+
const result = dts(`export function init(port: number = 3000): void {}`)
|
|
401
|
+
expect(result).toContain('declare function init')
|
|
402
|
+
expect(result).toContain('port')
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
test('function with rest parameter', () => {
|
|
406
|
+
const result = dts(`export function sum(...nums: number[]): number { return 0 }`)
|
|
407
|
+
expect(result).toContain('...nums: number[]')
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
test('generic function', () => {
|
|
411
|
+
const result = dts(`export function identity<T>(val: T): T { return val }`)
|
|
412
|
+
expect(result).toContain('<T>')
|
|
413
|
+
expect(result).toContain('val: T')
|
|
414
|
+
expect(result).toContain('): T')
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
test('generic function with constraint', () => {
|
|
418
|
+
const result = dts(`export function getKey<T extends object, K extends keyof T>(obj: T, key: K): T[K] { return obj[key] }`)
|
|
419
|
+
expect(result).toContain('T extends object')
|
|
420
|
+
expect(result).toContain('K extends keyof T')
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
test('function with multiple parameters', () => {
|
|
424
|
+
const result = dts(`export function combine(a: string, b: number, c: boolean): string { return '' }`)
|
|
425
|
+
expect(result).toContain('a: string')
|
|
426
|
+
expect(result).toContain('b: number')
|
|
427
|
+
expect(result).toContain('c: boolean')
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
test('function returning union type', () => {
|
|
431
|
+
const result = dts(`export function parse(input: string): number | null { return null }`)
|
|
432
|
+
expect(result).toContain('number | null')
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
test('function with callback parameter', () => {
|
|
436
|
+
const result = dts(`export function onEvent(cb: (event: string) => void): void {}`)
|
|
437
|
+
expect(result).toContain('cb: (event: string) => void')
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
test('function overloads', () => {
|
|
441
|
+
const result = dts(`
|
|
442
|
+
export function process(input: string): string
|
|
443
|
+
export function process(input: number): number
|
|
444
|
+
export function process(input: string | number): string | number {
|
|
445
|
+
return input
|
|
446
|
+
}
|
|
447
|
+
`)
|
|
448
|
+
expect(result).toContain('process(input: string): string')
|
|
449
|
+
expect(result).toContain('process(input: number): number')
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
test('generator function', () => {
|
|
453
|
+
const result = dts(`export function* gen(): Generator<number> { yield 1 }`)
|
|
454
|
+
expect(result).toContain('declare function')
|
|
455
|
+
expect(result).toContain('gen')
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
test('async generator function', () => {
|
|
459
|
+
const result = dts(`export async function* asyncGen(): AsyncGenerator<number> { yield 1 }`)
|
|
460
|
+
expect(result).toContain('declare function')
|
|
461
|
+
expect(result).toContain('asyncGen')
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
test('function body is stripped', () => {
|
|
465
|
+
const result = dts(`
|
|
466
|
+
export function complex(x: number): number {
|
|
467
|
+
const y = x * 2
|
|
468
|
+
if (y > 10) {
|
|
469
|
+
return y
|
|
470
|
+
}
|
|
471
|
+
return x
|
|
472
|
+
}
|
|
473
|
+
`)
|
|
474
|
+
expect(result).not.toContain('const y')
|
|
475
|
+
expect(result).not.toContain('if (y')
|
|
476
|
+
expect(result).toContain('declare function complex(x: number): number')
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
test('type guard function', () => {
|
|
480
|
+
const result = dts(`export function isString(val: unknown): val is string { return typeof val === 'string' }`)
|
|
481
|
+
expect(result).toContain('val is string')
|
|
482
|
+
})
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
// ==========================================================================
|
|
486
|
+
// Interface declarations
|
|
487
|
+
// ==========================================================================
|
|
488
|
+
|
|
489
|
+
describe('interface declarations', () => {
|
|
490
|
+
test('simple interface', () => {
|
|
491
|
+
const result = dts(`export interface User { name: string; age: number }`)
|
|
492
|
+
expect(result).toContain('export declare interface User')
|
|
493
|
+
expect(result).toContain('name: string')
|
|
494
|
+
expect(result).toContain('age: number')
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
test('interface with optional properties', () => {
|
|
498
|
+
const result = dts(`export interface Config { port: number; host?: string }`)
|
|
499
|
+
expect(result).toContain('host?: string')
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
test('interface with readonly properties', () => {
|
|
503
|
+
const result = dts(`export interface Point { readonly x: number; readonly y: number }`)
|
|
504
|
+
expect(result).toContain('readonly x: number')
|
|
505
|
+
expect(result).toContain('readonly y: number')
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
test('interface with method signature', () => {
|
|
509
|
+
const result = dts(`export interface Logger { log(msg: string): void; warn(msg: string): void }`)
|
|
510
|
+
expect(result).toContain('log(msg: string): void')
|
|
511
|
+
expect(result).toContain('warn(msg: string): void')
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
test('interface extending another', () => {
|
|
515
|
+
const result = dts(`
|
|
516
|
+
export interface Base { id: number }
|
|
517
|
+
export interface User extends Base { name: string }
|
|
518
|
+
`)
|
|
519
|
+
expect(result).toContain('User extends Base')
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
test('interface extending multiple', () => {
|
|
523
|
+
const result = dts(`
|
|
524
|
+
export interface A { a: string }
|
|
525
|
+
export interface B { b: number }
|
|
526
|
+
export interface C extends A, B { c: boolean }
|
|
527
|
+
`)
|
|
528
|
+
expect(result).toContain('C extends A, B')
|
|
529
|
+
})
|
|
530
|
+
|
|
531
|
+
test('generic interface', () => {
|
|
532
|
+
const result = dts(`export interface Container<T> { value: T; get(): T }`)
|
|
533
|
+
expect(result).toContain('Container<T>')
|
|
534
|
+
expect(result).toContain('value: T')
|
|
535
|
+
})
|
|
536
|
+
|
|
537
|
+
test('interface with index signature', () => {
|
|
538
|
+
const result = dts(`export interface Dict { [key: string]: number }`)
|
|
539
|
+
expect(result).toContain('[key: string]: number')
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
test('interface with call signature', () => {
|
|
543
|
+
const result = dts(`export interface Callable { (x: number): string }`)
|
|
544
|
+
expect(result).toContain('(x: number): string')
|
|
545
|
+
})
|
|
546
|
+
|
|
547
|
+
test('interface with construct signature', () => {
|
|
548
|
+
const result = dts(`export interface Constructable { new (name: string): object }`)
|
|
549
|
+
expect(result).toContain('new (name: string): object')
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
test('empty interface', () => {
|
|
553
|
+
const result = dts(`export interface Empty {}`)
|
|
554
|
+
expect(result).toContain('interface Empty')
|
|
555
|
+
})
|
|
556
|
+
|
|
557
|
+
test('interface with nested object type', () => {
|
|
558
|
+
const result = dts(`
|
|
559
|
+
export interface Nested {
|
|
560
|
+
data: {
|
|
561
|
+
items: string[]
|
|
562
|
+
count: number
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
`)
|
|
566
|
+
expect(result).toContain('items: string[]')
|
|
567
|
+
expect(result).toContain('count: number')
|
|
568
|
+
})
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
// ==========================================================================
|
|
572
|
+
// Type alias declarations
|
|
573
|
+
// ==========================================================================
|
|
574
|
+
|
|
575
|
+
describe('type alias declarations', () => {
|
|
576
|
+
test('simple type alias', () => {
|
|
577
|
+
const result = dts(`export type ID = string`)
|
|
578
|
+
expect(result).toContain('export type ID = string')
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
test('union type', () => {
|
|
582
|
+
const result = dts(`export type Result = string | number | boolean`)
|
|
583
|
+
expect(result).toContain('string | number | boolean')
|
|
584
|
+
})
|
|
585
|
+
|
|
586
|
+
test('intersection type', () => {
|
|
587
|
+
const result = dts(`export type Combined = A & B`)
|
|
588
|
+
expect(result).toContain('A & B')
|
|
589
|
+
})
|
|
590
|
+
|
|
591
|
+
test('literal union type', () => {
|
|
592
|
+
const result = dts(`export type Status = 'pending' | 'active' | 'done'`)
|
|
593
|
+
expect(result).toContain("'pending'")
|
|
594
|
+
expect(result).toContain("'active'")
|
|
595
|
+
expect(result).toContain("'done'")
|
|
596
|
+
})
|
|
597
|
+
|
|
598
|
+
test('generic type alias', () => {
|
|
599
|
+
const result = dts(`export type Nullable<T> = T | null`)
|
|
600
|
+
expect(result).toContain('Nullable<T>')
|
|
601
|
+
expect(result).toContain('T | null')
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
test('conditional type', () => {
|
|
605
|
+
const result = dts(`export type IsString<T> = T extends string ? true : false`)
|
|
606
|
+
expect(result).toContain('T extends string ? true : false')
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
test('mapped type', () => {
|
|
610
|
+
const result = dts(`export type Optional<T> = { [K in keyof T]?: T[K] }`)
|
|
611
|
+
expect(result).toContain('[K in keyof T]')
|
|
612
|
+
})
|
|
613
|
+
|
|
614
|
+
test('template literal type', () => {
|
|
615
|
+
const result = dts('export type EventName<T extends string> = `on${Capitalize<T>}`')
|
|
616
|
+
expect(result).toContain('EventName<T extends string>')
|
|
617
|
+
})
|
|
618
|
+
|
|
619
|
+
test('tuple type', () => {
|
|
620
|
+
const result = dts(`export type Pair = [string, number]`)
|
|
621
|
+
expect(result).toContain('[string, number]')
|
|
622
|
+
})
|
|
623
|
+
|
|
624
|
+
test('function type', () => {
|
|
625
|
+
const result = dts(`export type Handler = (event: Event) => void`)
|
|
626
|
+
expect(result).toContain('(event: Event) => void')
|
|
627
|
+
})
|
|
628
|
+
|
|
629
|
+
test('recursive type', () => {
|
|
630
|
+
const result = dts(`export type Tree<T> = { value: T; children: Tree<T>[] }`)
|
|
631
|
+
expect(result).toContain('Tree<T>')
|
|
632
|
+
expect(result).toContain('children: Tree<T>[]')
|
|
633
|
+
})
|
|
634
|
+
|
|
635
|
+
test('infer keyword in conditional type', () => {
|
|
636
|
+
const result = dts(`export type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never`)
|
|
637
|
+
expect(result).toContain('infer R')
|
|
638
|
+
})
|
|
639
|
+
|
|
640
|
+
test('keyof type', () => {
|
|
641
|
+
const result = dts(`export type Keys<T> = keyof T`)
|
|
642
|
+
expect(result).toContain('keyof T')
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
test('typeof type', () => {
|
|
646
|
+
const result = dts(`
|
|
647
|
+
export const defaults = { a: 1 }
|
|
648
|
+
export type Defaults = typeof defaults
|
|
649
|
+
`)
|
|
650
|
+
expect(result).toContain('typeof defaults')
|
|
651
|
+
})
|
|
652
|
+
})
|
|
653
|
+
|
|
654
|
+
// ==========================================================================
|
|
655
|
+
// Class declarations
|
|
656
|
+
// ==========================================================================
|
|
657
|
+
|
|
658
|
+
describe('class declarations', () => {
|
|
659
|
+
test('simple class', () => {
|
|
660
|
+
const result = dts(`
|
|
661
|
+
export class User {
|
|
662
|
+
name: string
|
|
663
|
+
constructor(name: string) {
|
|
664
|
+
this.name = name
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
`)
|
|
668
|
+
expect(result).toContain('export declare class User')
|
|
669
|
+
expect(result).toContain('name: string')
|
|
670
|
+
expect(result).toContain('constructor(name: string)')
|
|
671
|
+
})
|
|
672
|
+
|
|
673
|
+
test('class with methods', () => {
|
|
674
|
+
const result = dts(`
|
|
675
|
+
export class Calculator {
|
|
676
|
+
add(a: number, b: number): number { return a + b }
|
|
677
|
+
subtract(a: number, b: number): number { return a - b }
|
|
678
|
+
}
|
|
679
|
+
`)
|
|
680
|
+
expect(result).toContain('add(a: number, b: number): number')
|
|
681
|
+
expect(result).toContain('subtract(a: number, b: number): number')
|
|
682
|
+
expect(result).not.toContain('return a')
|
|
683
|
+
})
|
|
684
|
+
|
|
685
|
+
test('class extending another', () => {
|
|
686
|
+
const result = dts(`export class AppError extends Error { code: number; constructor(msg: string, code: number) { super(msg); this.code = code } }`)
|
|
687
|
+
expect(result).toContain('AppError extends Error')
|
|
688
|
+
expect(result).toContain('code: number')
|
|
689
|
+
})
|
|
690
|
+
|
|
691
|
+
test('class implementing interface', () => {
|
|
692
|
+
const result = dts(`
|
|
693
|
+
export interface Serializable { serialize(): string }
|
|
694
|
+
export class Data implements Serializable {
|
|
695
|
+
serialize(): string { return '' }
|
|
696
|
+
}
|
|
697
|
+
`)
|
|
698
|
+
expect(result).toContain('Data implements Serializable')
|
|
699
|
+
})
|
|
700
|
+
|
|
701
|
+
test('abstract class', () => {
|
|
702
|
+
const result = dts(`
|
|
703
|
+
export abstract class Shape {
|
|
704
|
+
abstract area(): number
|
|
705
|
+
describe(): string { return 'shape' }
|
|
706
|
+
}
|
|
707
|
+
`)
|
|
708
|
+
expect(result).toContain('abstract class Shape')
|
|
709
|
+
expect(result).toContain('abstract area(): number')
|
|
710
|
+
})
|
|
711
|
+
|
|
712
|
+
test('class with access modifiers', () => {
|
|
713
|
+
const result = dts(`
|
|
714
|
+
export class Service {
|
|
715
|
+
public url: string
|
|
716
|
+
private key: string
|
|
717
|
+
protected token: string
|
|
718
|
+
constructor(url: string, key: string, token: string) {
|
|
719
|
+
this.url = url; this.key = key; this.token = token
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
`)
|
|
723
|
+
// Zig scanner strips 'public' (it's the default), keeps 'protected'
|
|
724
|
+
expect(result).toContain('url: string')
|
|
725
|
+
expect(result).toContain('protected token: string')
|
|
726
|
+
expect(result).toContain('constructor')
|
|
727
|
+
})
|
|
728
|
+
|
|
729
|
+
test('class with static members', () => {
|
|
730
|
+
const result = dts(`
|
|
731
|
+
export class Counter {
|
|
732
|
+
static count: number = 0
|
|
733
|
+
static increment(): void { Counter.count++ }
|
|
734
|
+
}
|
|
735
|
+
`)
|
|
736
|
+
expect(result).toContain('static count')
|
|
737
|
+
expect(result).toContain('static increment')
|
|
738
|
+
})
|
|
739
|
+
|
|
740
|
+
test('class with readonly properties', () => {
|
|
741
|
+
const result = dts(`
|
|
742
|
+
export class Immutable {
|
|
743
|
+
readonly id: string
|
|
744
|
+
constructor(id: string) { this.id = id }
|
|
745
|
+
}
|
|
746
|
+
`)
|
|
747
|
+
expect(result).toContain('readonly id: string')
|
|
748
|
+
})
|
|
749
|
+
|
|
750
|
+
test('generic class', () => {
|
|
751
|
+
const result = dts(`
|
|
752
|
+
export class Box<T> {
|
|
753
|
+
value: T
|
|
754
|
+
constructor(value: T) { this.value = value }
|
|
755
|
+
get(): T { return this.value }
|
|
756
|
+
}
|
|
757
|
+
`)
|
|
758
|
+
expect(result).toContain('Box<T>')
|
|
759
|
+
expect(result).toContain('value: T')
|
|
760
|
+
})
|
|
761
|
+
|
|
762
|
+
test('class with async methods', () => {
|
|
763
|
+
const result = dts(`
|
|
764
|
+
export class Api {
|
|
765
|
+
async get(url: string): Promise<string> { return '' }
|
|
766
|
+
async post(url: string, body: object): Promise<void> {}
|
|
767
|
+
}
|
|
768
|
+
`)
|
|
769
|
+
expect(result).toContain('get(url: string): Promise<string>')
|
|
770
|
+
expect(result).toContain('post(url: string, body: object): Promise<void>')
|
|
771
|
+
})
|
|
772
|
+
|
|
773
|
+
test('class with getter and setter', () => {
|
|
774
|
+
const result = dts(`
|
|
775
|
+
export class Person {
|
|
776
|
+
private _name: string = ''
|
|
777
|
+
get name(): string { return this._name }
|
|
778
|
+
set name(val: string) { this._name = val }
|
|
779
|
+
}
|
|
780
|
+
`)
|
|
781
|
+
expect(result).toContain('get name(): string')
|
|
782
|
+
expect(result).toContain('set name(val: string)')
|
|
783
|
+
})
|
|
784
|
+
|
|
785
|
+
test('class with private # fields', () => {
|
|
786
|
+
const result = dts(`
|
|
787
|
+
export class Secret {
|
|
788
|
+
#value: string
|
|
789
|
+
constructor(val: string) { this.#value = val }
|
|
790
|
+
reveal(): string { return this.#value }
|
|
791
|
+
}
|
|
792
|
+
`)
|
|
793
|
+
expect(result).toContain('declare class Secret')
|
|
794
|
+
expect(result).toContain('reveal(): string')
|
|
795
|
+
})
|
|
796
|
+
|
|
797
|
+
test('class method body is stripped', () => {
|
|
798
|
+
const result = dts(`
|
|
799
|
+
export class Complex {
|
|
800
|
+
process(data: string[]): number {
|
|
801
|
+
let sum = 0
|
|
802
|
+
for (const item of data) {
|
|
803
|
+
sum += item.length
|
|
804
|
+
}
|
|
805
|
+
return sum
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
`)
|
|
809
|
+
expect(result).not.toContain('let sum')
|
|
810
|
+
expect(result).not.toContain('for (const')
|
|
811
|
+
})
|
|
812
|
+
})
|
|
813
|
+
|
|
814
|
+
// ==========================================================================
|
|
815
|
+
// Enum declarations
|
|
816
|
+
// ==========================================================================
|
|
817
|
+
|
|
818
|
+
describe('enum declarations', () => {
|
|
819
|
+
test('numeric enum', () => {
|
|
820
|
+
const result = dts(`export enum Direction { Up, Down, Left, Right }`)
|
|
821
|
+
expect(result).toContain('declare enum Direction')
|
|
822
|
+
})
|
|
823
|
+
|
|
824
|
+
test('string enum', () => {
|
|
825
|
+
const result = dts(`
|
|
826
|
+
export enum Color {
|
|
827
|
+
Red = 'RED',
|
|
828
|
+
Green = 'GREEN',
|
|
829
|
+
Blue = 'BLUE',
|
|
830
|
+
}
|
|
831
|
+
`)
|
|
832
|
+
expect(result).toContain('declare enum Color')
|
|
833
|
+
expect(result).toContain("Red = 'RED'")
|
|
834
|
+
expect(result).toContain("Green = 'GREEN'")
|
|
835
|
+
})
|
|
836
|
+
|
|
837
|
+
test('enum with explicit numeric values', () => {
|
|
838
|
+
const result = dts(`
|
|
839
|
+
export enum HttpStatus {
|
|
840
|
+
OK = 200,
|
|
841
|
+
NotFound = 404,
|
|
842
|
+
ServerError = 500,
|
|
843
|
+
}
|
|
844
|
+
`)
|
|
845
|
+
expect(result).toContain('OK = 200')
|
|
846
|
+
expect(result).toContain('NotFound = 404')
|
|
847
|
+
})
|
|
848
|
+
|
|
849
|
+
test('const enum', () => {
|
|
850
|
+
const result = dts(`export const enum Flags { A = 1, B = 2, C = 4 }`)
|
|
851
|
+
expect(result).toContain('const enum Flags')
|
|
852
|
+
})
|
|
853
|
+
})
|
|
854
|
+
|
|
855
|
+
// ==========================================================================
|
|
856
|
+
// Namespace declarations
|
|
857
|
+
// ==========================================================================
|
|
858
|
+
|
|
859
|
+
describe('namespace declarations', () => {
|
|
860
|
+
test('simple namespace', () => {
|
|
861
|
+
const result = dts(`
|
|
862
|
+
export namespace Utils {
|
|
863
|
+
export function log(msg: string): void {}
|
|
864
|
+
}
|
|
865
|
+
`)
|
|
866
|
+
expect(result).toContain('declare namespace Utils')
|
|
867
|
+
expect(result).toContain('function log(msg: string): void')
|
|
868
|
+
})
|
|
869
|
+
|
|
870
|
+
test('namespace with interface', () => {
|
|
871
|
+
const result = dts(`
|
|
872
|
+
export namespace Models {
|
|
873
|
+
export interface User { name: string }
|
|
874
|
+
}
|
|
875
|
+
`)
|
|
876
|
+
expect(result).toContain('namespace Models')
|
|
877
|
+
expect(result).toContain('interface User')
|
|
878
|
+
})
|
|
879
|
+
|
|
880
|
+
test('nested namespace', () => {
|
|
881
|
+
const result = dts(`
|
|
882
|
+
export namespace A {
|
|
883
|
+
export namespace B {
|
|
884
|
+
export function inner(): void {}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
`)
|
|
888
|
+
expect(result).toContain('namespace A')
|
|
889
|
+
expect(result).toContain('namespace B')
|
|
890
|
+
})
|
|
891
|
+
})
|
|
892
|
+
|
|
893
|
+
// ==========================================================================
|
|
894
|
+
// Module declarations
|
|
895
|
+
// ==========================================================================
|
|
896
|
+
|
|
897
|
+
describe('module declarations', () => {
|
|
898
|
+
test('declare module', () => {
|
|
899
|
+
const result = dts(`
|
|
900
|
+
declare module 'my-lib' {
|
|
901
|
+
export function doStuff(): void
|
|
902
|
+
}
|
|
903
|
+
`)
|
|
904
|
+
expect(result).toContain("declare module 'my-lib'")
|
|
905
|
+
expect(result).toContain('function doStuff(): void')
|
|
906
|
+
})
|
|
907
|
+
|
|
908
|
+
test('module augmentation', () => {
|
|
909
|
+
const result = dts(`
|
|
910
|
+
declare module 'express' {
|
|
911
|
+
interface Request {
|
|
912
|
+
user?: { id: string }
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
`)
|
|
916
|
+
expect(result).toContain("declare module 'express'")
|
|
917
|
+
})
|
|
918
|
+
})
|
|
919
|
+
|
|
920
|
+
// ==========================================================================
|
|
921
|
+
// Import/Export handling
|
|
922
|
+
// ==========================================================================
|
|
923
|
+
|
|
924
|
+
describe('import/export handling', () => {
|
|
925
|
+
test('preserves used type imports', () => {
|
|
926
|
+
const result = dts(`
|
|
927
|
+
import type { Foo } from 'bar'
|
|
928
|
+
export function useFoo(f: Foo): void {}
|
|
929
|
+
`)
|
|
930
|
+
expect(result).toContain("from 'bar'")
|
|
931
|
+
expect(result).toContain('Foo')
|
|
932
|
+
})
|
|
933
|
+
|
|
934
|
+
test('re-export', () => {
|
|
935
|
+
const result = dts(`export { foo, bar } from 'baz'`)
|
|
936
|
+
expect(result).toContain("export { foo, bar } from 'baz'")
|
|
937
|
+
})
|
|
938
|
+
|
|
939
|
+
test('re-export with rename', () => {
|
|
940
|
+
const result = dts(`export { foo as myFoo } from 'baz'`)
|
|
941
|
+
expect(result).toContain('foo as myFoo')
|
|
942
|
+
})
|
|
943
|
+
|
|
944
|
+
test('export star', () => {
|
|
945
|
+
const result = dts(`export * from 'module'`)
|
|
946
|
+
expect(result).toContain("export * from 'module'")
|
|
947
|
+
})
|
|
948
|
+
|
|
949
|
+
test('export star as namespace', () => {
|
|
950
|
+
const result = dts(`export * as Utils from './utils'`)
|
|
951
|
+
expect(result).toContain("export * as Utils from './utils'")
|
|
952
|
+
})
|
|
953
|
+
|
|
954
|
+
test('export type', () => {
|
|
955
|
+
const result = dts(`export type { MyType } from './types'`)
|
|
956
|
+
expect(result).toContain('export type')
|
|
957
|
+
expect(result).toContain('MyType')
|
|
958
|
+
})
|
|
959
|
+
|
|
960
|
+
test('default export function', () => {
|
|
961
|
+
const result = dts(`export default function main(): void {}`)
|
|
962
|
+
expect(result).toContain('declare function main(): void')
|
|
963
|
+
})
|
|
964
|
+
|
|
965
|
+
test('default export class', () => {
|
|
966
|
+
const result = dts(`export default class App { start(): void {} }`)
|
|
967
|
+
expect(result).toContain('declare class App')
|
|
968
|
+
})
|
|
969
|
+
|
|
970
|
+
test('named export and default export together', () => {
|
|
971
|
+
const result = dts(`
|
|
972
|
+
export const version = '1.0.0'
|
|
973
|
+
export default function init(): void {}
|
|
974
|
+
`)
|
|
975
|
+
expect(result).toContain('declare const version')
|
|
976
|
+
expect(result).toContain('declare function init')
|
|
977
|
+
})
|
|
978
|
+
})
|
|
979
|
+
|
|
980
|
+
// ==========================================================================
|
|
981
|
+
// Comment preservation
|
|
982
|
+
// ==========================================================================
|
|
983
|
+
|
|
984
|
+
describe('comment preservation', () => {
|
|
985
|
+
test('JSDoc comment preserved with keepComments=true', () => {
|
|
986
|
+
const result = dts(`
|
|
987
|
+
/** This is a JSDoc comment */
|
|
988
|
+
export function foo(): void {}
|
|
989
|
+
`, true)
|
|
990
|
+
expect(result).toContain('/** This is a JSDoc comment */')
|
|
991
|
+
})
|
|
992
|
+
|
|
993
|
+
test('JSDoc comment stripped with keepComments=false', () => {
|
|
994
|
+
const result = dts(`
|
|
995
|
+
/** This is a JSDoc comment */
|
|
996
|
+
export function foo(): void {}
|
|
997
|
+
`, false)
|
|
998
|
+
expect(result).not.toContain('JSDoc')
|
|
999
|
+
})
|
|
1000
|
+
|
|
1001
|
+
test('multi-line JSDoc preserved', () => {
|
|
1002
|
+
const result = dts(`
|
|
1003
|
+
/**
|
|
1004
|
+
* Process data
|
|
1005
|
+
* @param input - The input string
|
|
1006
|
+
* @returns Processed output
|
|
1007
|
+
*/
|
|
1008
|
+
export function process(input: string): string { return input }
|
|
1009
|
+
`, true)
|
|
1010
|
+
expect(result).toContain('@param input')
|
|
1011
|
+
expect(result).toContain('@returns')
|
|
1012
|
+
})
|
|
1013
|
+
|
|
1014
|
+
test('single-line comment preserved', () => {
|
|
1015
|
+
const result = dts(`
|
|
1016
|
+
// Important function
|
|
1017
|
+
export function important(): void {}
|
|
1018
|
+
`, true)
|
|
1019
|
+
expect(result).toContain('// Important function')
|
|
1020
|
+
})
|
|
1021
|
+
|
|
1022
|
+
test('comments on interface members', () => {
|
|
1023
|
+
const result = dts(`
|
|
1024
|
+
export interface User {
|
|
1025
|
+
/** User's name */
|
|
1026
|
+
name: string
|
|
1027
|
+
/** User's age */
|
|
1028
|
+
age: number
|
|
1029
|
+
}
|
|
1030
|
+
`, true)
|
|
1031
|
+
// Zig scanner preserves the interface structure; member-level JSDoc may not be preserved
|
|
1032
|
+
expect(result).toContain('name: string')
|
|
1033
|
+
expect(result).toContain('age: number')
|
|
1034
|
+
})
|
|
1035
|
+
})
|
|
1036
|
+
|
|
1037
|
+
// ==========================================================================
|
|
1038
|
+
// Complex type patterns
|
|
1039
|
+
// ==========================================================================
|
|
1040
|
+
|
|
1041
|
+
describe('complex type patterns', () => {
|
|
1042
|
+
test('nested generics', () => {
|
|
1043
|
+
const result = dts(`export type Nested = Map<string, Set<number>>`)
|
|
1044
|
+
expect(result).toContain('Map<string, Set<number>>')
|
|
1045
|
+
})
|
|
1046
|
+
|
|
1047
|
+
test('function returning object type', () => {
|
|
1048
|
+
const result = dts(`export function getInfo(): { name: string; age: number } { return { name: '', age: 0 } }`)
|
|
1049
|
+
expect(result).toContain('name: string')
|
|
1050
|
+
expect(result).toContain('age: number')
|
|
1051
|
+
})
|
|
1052
|
+
|
|
1053
|
+
test('discriminated union', () => {
|
|
1054
|
+
const result = dts(`
|
|
1055
|
+
export type Shape =
|
|
1056
|
+
| { kind: 'circle'; radius: number }
|
|
1057
|
+
| { kind: 'rect'; width: number; height: number }
|
|
1058
|
+
`)
|
|
1059
|
+
expect(result).toContain("kind: 'circle'")
|
|
1060
|
+
expect(result).toContain("kind: 'rect'")
|
|
1061
|
+
})
|
|
1062
|
+
|
|
1063
|
+
test('utility types', () => {
|
|
1064
|
+
const result = dts(`
|
|
1065
|
+
export type PartialUser = Partial<User>
|
|
1066
|
+
export type ReadonlyUser = Readonly<User>
|
|
1067
|
+
export type PickName = Pick<User, 'name'>
|
|
1068
|
+
export type OmitAge = Omit<User, 'age'>
|
|
1069
|
+
`)
|
|
1070
|
+
expect(result).toContain('Partial<User>')
|
|
1071
|
+
expect(result).toContain('Readonly<User>')
|
|
1072
|
+
expect(result).toContain("Pick<User, 'name'>")
|
|
1073
|
+
expect(result).toContain("Omit<User, 'age'>")
|
|
1074
|
+
})
|
|
1075
|
+
|
|
1076
|
+
test('complex generic constraint', () => {
|
|
1077
|
+
const result = dts(`
|
|
1078
|
+
export function merge<T extends Record<string, unknown>, U extends Record<string, unknown>>(a: T, b: U): T & U {
|
|
1079
|
+
return { ...a, ...b }
|
|
1080
|
+
}
|
|
1081
|
+
`)
|
|
1082
|
+
expect(result).toContain('T extends Record<string, unknown>')
|
|
1083
|
+
expect(result).toContain('T & U')
|
|
1084
|
+
})
|
|
1085
|
+
|
|
1086
|
+
test('array of functions', () => {
|
|
1087
|
+
const result = dts(`export type Middleware = Array<(req: Request, res: Response) => void>`)
|
|
1088
|
+
expect(result).toContain('Array<(req: Request, res: Response) => void>')
|
|
1089
|
+
})
|
|
1090
|
+
|
|
1091
|
+
test('promise chain types', () => {
|
|
1092
|
+
const result = dts(`export type AsyncResult<T> = Promise<T | Error>`)
|
|
1093
|
+
expect(result).toContain('Promise<T | Error>')
|
|
1094
|
+
})
|
|
1095
|
+
|
|
1096
|
+
test('indexed access types', () => {
|
|
1097
|
+
const result = dts(`export type Name = User['name']`)
|
|
1098
|
+
expect(result).toContain("User['name']")
|
|
1099
|
+
})
|
|
1100
|
+
})
|
|
1101
|
+
|
|
1102
|
+
// ==========================================================================
|
|
1103
|
+
// Edge cases — tricky syntax
|
|
1104
|
+
// ==========================================================================
|
|
1105
|
+
|
|
1106
|
+
describe('tricky syntax edge cases', () => {
|
|
1107
|
+
test('string with semicolons inside', () => {
|
|
1108
|
+
const result = dts(`export const query = 'SELECT * FROM users; DROP TABLE;'`)
|
|
1109
|
+
expect(result).toContain('declare const query')
|
|
1110
|
+
})
|
|
1111
|
+
|
|
1112
|
+
test('string with curly braces inside', () => {
|
|
1113
|
+
const result = dts(`export const tmpl = '{ "key": "value" }'`)
|
|
1114
|
+
expect(result).toContain('declare const tmpl')
|
|
1115
|
+
})
|
|
1116
|
+
|
|
1117
|
+
test('string with parentheses inside', () => {
|
|
1118
|
+
const result = dts(`export const expr = 'fn(a, b)'`)
|
|
1119
|
+
expect(result).toContain('declare const expr')
|
|
1120
|
+
})
|
|
1121
|
+
|
|
1122
|
+
test('multiline string value', () => {
|
|
1123
|
+
const result = dts('export const multi = `line1\nline2\nline3`')
|
|
1124
|
+
expect(result).toContain('declare const multi')
|
|
1125
|
+
})
|
|
1126
|
+
|
|
1127
|
+
test('arrow function export', () => {
|
|
1128
|
+
const result = dts(`export const add = (a: number, b: number): number => a + b`)
|
|
1129
|
+
expect(result).toContain('declare const add')
|
|
1130
|
+
})
|
|
1131
|
+
|
|
1132
|
+
test('arrow function without parens', () => {
|
|
1133
|
+
const result = dts(`export const identity = (x: number): number => x`)
|
|
1134
|
+
expect(result).toContain('declare const identity')
|
|
1135
|
+
})
|
|
1136
|
+
|
|
1137
|
+
test('destructured export not in declaration', () => {
|
|
1138
|
+
// This is a non-standard pattern, just verify no crash
|
|
1139
|
+
const result = processSource(`const obj = { a: 1, b: 2 }\nexport const { a, b } = obj`, true)
|
|
1140
|
+
expect(typeof result).toBe('string')
|
|
1141
|
+
})
|
|
1142
|
+
|
|
1143
|
+
test('very long single line', () => {
|
|
1144
|
+
const longType = Array.from({ length: 50 }, (_, i) => `prop${i}: string`).join('; ')
|
|
1145
|
+
const result = dts(`export interface Wide { ${longType} }`)
|
|
1146
|
+
expect(result).toContain('interface Wide')
|
|
1147
|
+
expect(result).toContain('prop0: string')
|
|
1148
|
+
expect(result).toContain('prop49: string')
|
|
1149
|
+
})
|
|
1150
|
+
|
|
1151
|
+
test('unicode in identifiers', () => {
|
|
1152
|
+
const result = dts(`export const café = 'coffee'`)
|
|
1153
|
+
expect(result).toContain('café')
|
|
1154
|
+
})
|
|
1155
|
+
|
|
1156
|
+
test('export with triple-slash directive above', () => {
|
|
1157
|
+
const result = dts(`/// <reference types="node" />\nexport const x = 1`)
|
|
1158
|
+
expect(result).toContain('declare const x')
|
|
1159
|
+
})
|
|
1160
|
+
|
|
1161
|
+
test('multiple semicolons', () => {
|
|
1162
|
+
const result = dts(`export const a = 1;;;\nexport const b = 2`)
|
|
1163
|
+
expect(result).toContain('a:')
|
|
1164
|
+
expect(result).toContain('b:')
|
|
1165
|
+
})
|
|
1166
|
+
|
|
1167
|
+
test('function with complex destructured params', () => {
|
|
1168
|
+
const result = dts(`export function parse({ input, options }: { input: string; options?: object }): void {}`)
|
|
1169
|
+
expect(result).toContain('declare function parse')
|
|
1170
|
+
})
|
|
1171
|
+
|
|
1172
|
+
test('export declare (already declared)', () => {
|
|
1173
|
+
const result = dts(`export declare const VERSION: string`)
|
|
1174
|
+
expect(result).toContain('export declare const VERSION: string')
|
|
1175
|
+
})
|
|
1176
|
+
|
|
1177
|
+
test('export declare function (already declared)', () => {
|
|
1178
|
+
const result = dts(`export declare function run(): void`)
|
|
1179
|
+
expect(result).toContain('export declare function run(): void')
|
|
1180
|
+
})
|
|
1181
|
+
|
|
1182
|
+
test('type with very deeply nested generics', () => {
|
|
1183
|
+
const result = dts(`export type Deep = Map<string, Map<string, Map<string, Set<number>>>>`)
|
|
1184
|
+
expect(result).toContain('Map<string, Map<string, Map<string, Set<number>>>>')
|
|
1185
|
+
})
|
|
1186
|
+
|
|
1187
|
+
test('const with comma operator value (tricky)', () => {
|
|
1188
|
+
// Comma in array initializer
|
|
1189
|
+
const result = dts(`export const arr = [1, 2, 3, 4, 5]`)
|
|
1190
|
+
expect(result).toContain('number[]')
|
|
1191
|
+
})
|
|
1192
|
+
|
|
1193
|
+
test('function with string param default', () => {
|
|
1194
|
+
const result = dts(`export function greet(name: string = 'world'): string { return '' }`)
|
|
1195
|
+
expect(result).toContain('declare function greet')
|
|
1196
|
+
})
|
|
1197
|
+
|
|
1198
|
+
test('exported type using typeof import', () => {
|
|
1199
|
+
const result = dts(`export type Config = typeof import('./config')`)
|
|
1200
|
+
expect(result).toContain("typeof import('./config')")
|
|
1201
|
+
})
|
|
1202
|
+
})
|
|
1203
|
+
|
|
1204
|
+
// ==========================================================================
|
|
1205
|
+
// Stress tests — no crash
|
|
1206
|
+
// ==========================================================================
|
|
1207
|
+
|
|
1208
|
+
describe('stress tests — no crash', () => {
|
|
1209
|
+
test('many exports in one file', () => {
|
|
1210
|
+
const lines = Array.from({ length: 200 }, (_, i) => `export const v${i} = ${i}`).join('\n')
|
|
1211
|
+
const result = processSource(lines, true)
|
|
1212
|
+
expect(result).toContain('v0:')
|
|
1213
|
+
expect(result).toContain('v199:')
|
|
1214
|
+
})
|
|
1215
|
+
|
|
1216
|
+
test('deeply nested object (20 levels)', () => {
|
|
1217
|
+
let obj = '{ val: 1 }'
|
|
1218
|
+
for (let i = 0; i < 20; i++) {
|
|
1219
|
+
obj = `{ level${i}: ${obj} }`
|
|
1220
|
+
}
|
|
1221
|
+
const result = processSource(`export const deep = ${obj}`, true)
|
|
1222
|
+
expect(result).toContain('declare const deep')
|
|
1223
|
+
})
|
|
1224
|
+
|
|
1225
|
+
test('very long string literal', () => {
|
|
1226
|
+
const longStr = 'a'.repeat(5000)
|
|
1227
|
+
const result = processSource(`export const big = '${longStr}'`, true)
|
|
1228
|
+
expect(result).toContain('declare const big')
|
|
1229
|
+
})
|
|
1230
|
+
|
|
1231
|
+
test('many function parameters', () => {
|
|
1232
|
+
const params = Array.from({ length: 30 }, (_, i) => `p${i}: string`).join(', ')
|
|
1233
|
+
const result = processSource(`export function many(${params}): void {}`, true)
|
|
1234
|
+
expect(result).toContain('p0: string')
|
|
1235
|
+
expect(result).toContain('p29: string')
|
|
1236
|
+
})
|
|
1237
|
+
|
|
1238
|
+
test('many interface members', () => {
|
|
1239
|
+
const members = Array.from({ length: 100 }, (_, i) => ` field${i}: string`).join('\n')
|
|
1240
|
+
const result = processSource(`export interface Big {\n${members}\n}`, true)
|
|
1241
|
+
expect(result).toContain('field0: string')
|
|
1242
|
+
expect(result).toContain('field99: string')
|
|
1243
|
+
})
|
|
1244
|
+
|
|
1245
|
+
test('many type union members', () => {
|
|
1246
|
+
const members = Array.from({ length: 50 }, (_, i) => `'val${i}'`).join(' | ')
|
|
1247
|
+
const result = processSource(`export type Many = ${members}`, true)
|
|
1248
|
+
expect(result).toContain("'val0'")
|
|
1249
|
+
expect(result).toContain("'val49'")
|
|
1250
|
+
})
|
|
1251
|
+
|
|
1252
|
+
test('alternating declaration types', () => {
|
|
1253
|
+
const lines = Array.from({ length: 50 }, (_, i) => {
|
|
1254
|
+
switch (i % 5) {
|
|
1255
|
+
case 0: return `export const c${i} = ${i}`
|
|
1256
|
+
case 1: return `export function f${i}(): void {}`
|
|
1257
|
+
case 2: return `export interface I${i} { x: number }`
|
|
1258
|
+
case 3: return `export type T${i} = string`
|
|
1259
|
+
case 4: return `export enum E${i} { A, B }`
|
|
1260
|
+
}
|
|
1261
|
+
}).join('\n')
|
|
1262
|
+
const result = processSource(lines, true)
|
|
1263
|
+
expect(result.length).toBeGreaterThan(0)
|
|
1264
|
+
})
|
|
1265
|
+
|
|
1266
|
+
test('source with CRLF line endings', () => {
|
|
1267
|
+
const source = 'export const a = 1\r\nexport const b = 2\r\nexport function foo(): void {}\r\n'
|
|
1268
|
+
const result = processSource(source, true)
|
|
1269
|
+
expect(result).toContain('a:')
|
|
1270
|
+
expect(result).toContain('b:')
|
|
1271
|
+
expect(result).toContain('foo')
|
|
1272
|
+
})
|
|
1273
|
+
|
|
1274
|
+
test('source with mixed line endings', () => {
|
|
1275
|
+
const source = 'export const a = 1\nexport const b = 2\r\nexport const c = 3\r'
|
|
1276
|
+
const result = processSource(source, true)
|
|
1277
|
+
expect(result).toContain('a:')
|
|
1278
|
+
expect(result).toContain('b:')
|
|
1279
|
+
})
|
|
1280
|
+
|
|
1281
|
+
test('source with BOM', () => {
|
|
1282
|
+
const source = '\uFEFFexport const x = 1'
|
|
1283
|
+
const result = processSource(source, true)
|
|
1284
|
+
expect(typeof result).toBe('string')
|
|
1285
|
+
})
|
|
1286
|
+
|
|
1287
|
+
test('source with tab indentation', () => {
|
|
1288
|
+
const source = 'export interface Tabbed {\n\tname: string\n\tage: number\n}'
|
|
1289
|
+
const result = processSource(source, true)
|
|
1290
|
+
expect(result).toContain('name: string')
|
|
1291
|
+
expect(result).toContain('age: number')
|
|
1292
|
+
})
|
|
1293
|
+
})
|
|
1294
|
+
|
|
1295
|
+
// ==========================================================================
|
|
1296
|
+
// isolatedDeclarations mode
|
|
1297
|
+
// ==========================================================================
|
|
1298
|
+
|
|
1299
|
+
describe('isolatedDeclarations mode', () => {
|
|
1300
|
+
test('skips initializer when annotation present', () => {
|
|
1301
|
+
const result = normalizeOutput(processSource(
|
|
1302
|
+
`export const x: number = 42`,
|
|
1303
|
+
true,
|
|
1304
|
+
true, // isolatedDeclarations ON
|
|
1305
|
+
))
|
|
1306
|
+
expect(result).toContain('x: number')
|
|
1307
|
+
})
|
|
1308
|
+
|
|
1309
|
+
test('still infers when no annotation', () => {
|
|
1310
|
+
const result = normalizeOutput(processSource(
|
|
1311
|
+
`export const x = 42`,
|
|
1312
|
+
true,
|
|
1313
|
+
true, // isolatedDeclarations ON
|
|
1314
|
+
))
|
|
1315
|
+
expect(result).toContain('x: 42')
|
|
1316
|
+
})
|
|
1317
|
+
|
|
1318
|
+
test('still infers for generic annotation', () => {
|
|
1319
|
+
const result = normalizeOutput(processSource(
|
|
1320
|
+
`export const x: Record<string, number> = { a: 1 }`,
|
|
1321
|
+
true,
|
|
1322
|
+
true, // isolatedDeclarations ON
|
|
1323
|
+
))
|
|
1324
|
+
// Should still infer narrow type since Record<> is generic
|
|
1325
|
+
expect(result).toContain('a: 1')
|
|
1326
|
+
})
|
|
1327
|
+
|
|
1328
|
+
test('uses annotation for non-generic type', () => {
|
|
1329
|
+
const result = normalizeOutput(processSource(
|
|
1330
|
+
`export const name: string = 'hello'`,
|
|
1331
|
+
true,
|
|
1332
|
+
true, // isolatedDeclarations ON
|
|
1333
|
+
))
|
|
1334
|
+
expect(result).toContain('name: string')
|
|
1335
|
+
})
|
|
1336
|
+
|
|
1337
|
+
test('default mode (off) infers narrow types', () => {
|
|
1338
|
+
const result = normalizeOutput(processSource(
|
|
1339
|
+
`export const name: string = 'hello'`,
|
|
1340
|
+
true,
|
|
1341
|
+
false, // isolatedDeclarations OFF (default)
|
|
1342
|
+
))
|
|
1343
|
+
// Without isolated declarations, const + generic "string" annotation
|
|
1344
|
+
// should still use annotation since string is not a "generic" type
|
|
1345
|
+
expect(result).toContain('name: string')
|
|
1346
|
+
})
|
|
1347
|
+
})
|
|
1348
|
+
|
|
1349
|
+
// ==========================================================================
|
|
1350
|
+
// Consistency with JS dtsx
|
|
1351
|
+
// ==========================================================================
|
|
1352
|
+
|
|
1353
|
+
describe('parity checks — exact output verification', () => {
|
|
1354
|
+
test('export const number produces exact format', () => {
|
|
1355
|
+
const result = dts(`export const port = 3000`)
|
|
1356
|
+
expect(result).toBe('export declare const port: 3000;')
|
|
1357
|
+
})
|
|
1358
|
+
|
|
1359
|
+
test('export const string produces exact format', () => {
|
|
1360
|
+
const result = dts(`export const name = 'hello'`)
|
|
1361
|
+
expect(result).toBe("export declare const name: 'hello';")
|
|
1362
|
+
})
|
|
1363
|
+
|
|
1364
|
+
test('export function produces exact format', () => {
|
|
1365
|
+
const result = dts(`export function greet(name: string): string { return name }`)
|
|
1366
|
+
expect(result).toBe('export declare function greet(name: string): string;')
|
|
1367
|
+
})
|
|
1368
|
+
|
|
1369
|
+
test('export interface produces exact format', () => {
|
|
1370
|
+
const result = dts(`export interface Point { x: number; y: number }`)
|
|
1371
|
+
expect(result).toContain('export declare interface Point')
|
|
1372
|
+
expect(result).toContain('x: number')
|
|
1373
|
+
expect(result).toContain('y: number')
|
|
1374
|
+
})
|
|
1375
|
+
|
|
1376
|
+
test('export type alias produces exact format', () => {
|
|
1377
|
+
const result = dts(`export type ID = string | number`)
|
|
1378
|
+
expect(result).toBe('export type ID = string | number;')
|
|
1379
|
+
})
|
|
1380
|
+
|
|
1381
|
+
test('export void function produces exact format', () => {
|
|
1382
|
+
const result = dts(`export function doIt(): void { console.log('done') }`)
|
|
1383
|
+
expect(result).toBe('export declare function doIt(): void;')
|
|
1384
|
+
})
|
|
1385
|
+
})
|
|
1386
|
+
})
|