@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.
@@ -0,0 +1,2464 @@
1
+ /// Declaration extractors - extract specific declaration types from the scanner.
2
+ /// Port of scanner.ts high-level extraction functions (lines 590-2598).
3
+ const std = @import("std");
4
+ const ch = @import("char_utils.zig");
5
+ const types = @import("types.zig");
6
+ const type_inf = @import("type_inference.zig");
7
+ const Scanner = @import("scanner.zig").Scanner;
8
+ const Declaration = types.Declaration;
9
+ const DeclarationKind = types.DeclarationKind;
10
+ const Allocator = std.mem.Allocator;
11
+
12
+ // ========================================================================
13
+ // Comment extraction
14
+ // ========================================================================
15
+
16
+ /// Extract leading JSDoc/block/single-line comments before position
17
+ pub fn extractLeadingComments(s: *Scanner, decl_start: usize) ?[]const []const u8 {
18
+ if (!s.keep_comments) return null;
19
+
20
+ var p: isize = @as(isize, @intCast(decl_start)) - 1;
21
+ while (p >= 0 and ch.isWhitespace(s.source[@intCast(p)])) p -= 1;
22
+ if (p < 0) return null;
23
+
24
+ var comments = std.array_list.Managed([]const u8).init(s.allocator);
25
+ comments.ensureTotalCapacity(4) catch {};
26
+ var has_block_comment = false;
27
+
28
+ while (p >= 0) {
29
+ const pu: usize = @intCast(p);
30
+ // Check for block comment ending with */
31
+ if (p >= 1 and s.source[pu] == ch.CH_SLASH and s.source[pu - 1] == ch.CH_STAR) {
32
+ // Find matching /* or /**
33
+ var start: isize = p - 2;
34
+ while (start >= 1) {
35
+ const su: usize = @intCast(start);
36
+ if (s.source[su] == ch.CH_SLASH and s.source[su + 1] == ch.CH_STAR) break;
37
+ start -= 1;
38
+ }
39
+ if (start >= 0) {
40
+ const su: usize = @intCast(start);
41
+ if (s.source[su] == ch.CH_SLASH and s.source[su + 1] == ch.CH_STAR) {
42
+ comments.append(s.source[su .. pu + 1]) catch {};
43
+ has_block_comment = true;
44
+ p = start - 1;
45
+ while (p >= 0 and ch.isWhitespace(s.source[@intCast(p)])) p -= 1;
46
+ continue;
47
+ }
48
+ }
49
+ break;
50
+ }
51
+
52
+ // Check for single-line comments
53
+ var line_start: usize = pu;
54
+ while (line_start > 0 and s.source[line_start - 1] != ch.CH_LF) line_start -= 1;
55
+ const line_text = ch.sliceTrimmed(s.source, line_start, pu + 1);
56
+
57
+ if (line_text.len >= 2 and line_text[0] == '/' and line_text[1] == '/') {
58
+ if (has_block_comment) break;
59
+
60
+ var single_lines = std.array_list.Managed([]const u8).init(s.allocator);
61
+ single_lines.append(line_text) catch {};
62
+ p = @as(isize, @intCast(line_start)) - 1;
63
+ while (p >= 0 and (s.source[@intCast(p)] == ch.CH_LF or s.source[@intCast(p)] == ch.CH_CR)) p -= 1;
64
+
65
+ while (p >= 0) {
66
+ var ls: usize = @intCast(p);
67
+ while (ls > 0 and s.source[ls - 1] != ch.CH_LF) ls -= 1;
68
+ const lt = ch.sliceTrimmed(s.source, ls, @as(usize, @intCast(p)) + 1);
69
+ if (lt.len >= 2 and lt[0] == '/' and lt[1] == '/') {
70
+ single_lines.append(lt) catch {};
71
+ p = @as(isize, @intCast(ls)) - 1;
72
+ while (p >= 0 and (s.source[@intCast(p)] == ch.CH_LF or s.source[@intCast(p)] == ch.CH_CR)) p -= 1;
73
+ } else if (lt.len == 0) {
74
+ p = @as(isize, @intCast(ls)) - 1;
75
+ while (p >= 0 and (s.source[@intCast(p)] == ch.CH_LF or s.source[@intCast(p)] == ch.CH_CR)) p -= 1;
76
+ } else {
77
+ break;
78
+ }
79
+ }
80
+
81
+ // Reverse and join with newlines
82
+ std.mem.reverse([]const u8, single_lines.items);
83
+ var total_len: usize = 0;
84
+ for (single_lines.items, 0..) |line, i| {
85
+ total_len += line.len;
86
+ if (i < single_lines.items.len - 1) total_len += 1;
87
+ }
88
+ const joined = s.allocator.alloc(u8, total_len) catch break;
89
+ var offset: usize = 0;
90
+ for (single_lines.items, 0..) |line, i| {
91
+ @memcpy(joined[offset .. offset + line.len], line);
92
+ offset += line.len;
93
+ if (i < single_lines.items.len - 1) {
94
+ joined[offset] = '\n';
95
+ offset += 1;
96
+ }
97
+ }
98
+ comments.append(joined) catch {};
99
+ continue;
100
+ }
101
+ break;
102
+ }
103
+
104
+ if (comments.items.len == 0) return null;
105
+ std.mem.reverse([]const u8, comments.items);
106
+ return comments.toOwnedSlice() catch null;
107
+ }
108
+
109
+ // ========================================================================
110
+ // Import extraction
111
+ // ========================================================================
112
+
113
+ /// Extract import statement text from current position
114
+ pub fn extractImport(s: *Scanner, start: usize) Declaration {
115
+ const stmt_start = start;
116
+ var found_quote = false;
117
+ while (s.pos < s.len) {
118
+ const c = s.source[s.pos];
119
+ if (c == ch.CH_SEMI) {
120
+ s.pos += 1;
121
+ break;
122
+ }
123
+ if (c == ch.CH_SQUOTE or c == ch.CH_DQUOTE) {
124
+ s.skipString(c);
125
+ found_quote = true;
126
+ while (s.pos < s.len and (s.source[s.pos] == ch.CH_SPACE or s.source[s.pos] == ch.CH_TAB)) s.pos += 1;
127
+ if (s.pos < s.len and s.source[s.pos] == ch.CH_SEMI) s.pos += 1;
128
+ break;
129
+ }
130
+ if (c == ch.CH_LF and found_quote) break;
131
+ s.pos += 1;
132
+ }
133
+
134
+ const text = s.sliceTrimmed(stmt_start, s.pos);
135
+ // Check for 'import type '
136
+ const is_type_only = text.len > 11 and text[7] == 't' and
137
+ (ch.startsWith(text, "import type ") or ch.startsWith(text, "import type{"));
138
+
139
+ // Detect side-effect imports
140
+ var is_side_effect = false;
141
+ {
142
+ var si: usize = 6; // skip 'import'
143
+ while (si < text.len and (text[si] == ch.CH_SPACE or text[si] == ch.CH_TAB)) si += 1;
144
+ if (si < text.len and text[si] == 't' and si + 4 <= text.len and std.mem.eql(u8, text[si .. si + 4], "type")) {
145
+ si += 4;
146
+ while (si < text.len and (text[si] == ch.CH_SPACE or text[si] == ch.CH_TAB)) si += 1;
147
+ }
148
+ if (si < text.len) {
149
+ const qc = text[si];
150
+ is_side_effect = qc == ch.CH_SQUOTE or qc == ch.CH_DQUOTE;
151
+ }
152
+ }
153
+
154
+ // Extract source module
155
+ var module_src: []const u8 = "";
156
+ {
157
+ const from_idx = ch.indexOf(text, "from ", 0);
158
+ if (from_idx) |fi| {
159
+ var mi = fi + 5;
160
+ while (mi < text.len and (text[mi] == ch.CH_SPACE or text[mi] == ch.CH_TAB)) mi += 1;
161
+ if (mi < text.len) {
162
+ const q = text[mi];
163
+ if (q == ch.CH_SQUOTE or q == ch.CH_DQUOTE) {
164
+ const q_str: []const u8 = if (q == ch.CH_SQUOTE) "'" else "\"";
165
+ const end_idx = ch.indexOf(text, q_str, mi + 1);
166
+ if (end_idx) |ei| {
167
+ module_src = text[mi + 1 .. ei];
168
+ }
169
+ }
170
+ }
171
+ } else if (is_side_effect) {
172
+ var mi: usize = 6;
173
+ while (mi < text.len and text[mi] != ch.CH_SQUOTE and text[mi] != ch.CH_DQUOTE) mi += 1;
174
+ if (mi < text.len) {
175
+ const q = text[mi];
176
+ const q_str: []const u8 = if (q == ch.CH_SQUOTE) "'" else "\"";
177
+ const end_idx = ch.indexOf(text, q_str, mi + 1);
178
+ if (end_idx) |ei| {
179
+ module_src = text[mi + 1 .. ei];
180
+ }
181
+ }
182
+ }
183
+ }
184
+
185
+ const comments = extractLeadingComments(s, stmt_start);
186
+
187
+ // Parse import clause once (cached in Declaration to avoid re-parsing in emitter)
188
+ var parsed_import: ?types.ParsedImport = null;
189
+ if (!is_side_effect) pi: {
190
+ const from_idx = ch.indexOf(text, " from ", 0) orelse break :pi;
191
+ var import_part = ch.sliceTrimmed(text, 0, from_idx);
192
+
193
+ // Strip 'import' and optional 'type'
194
+ if (ch.startsWith(import_part, "import type ")) {
195
+ import_part = ch.sliceTrimmed(import_part, 12, import_part.len);
196
+ } else if (ch.startsWith(import_part, "import ")) {
197
+ import_part = ch.sliceTrimmed(import_part, 7, import_part.len);
198
+ }
199
+ if (ch.startsWith(import_part, "type ")) {
200
+ import_part = ch.sliceTrimmed(import_part, 5, import_part.len);
201
+ }
202
+
203
+ var default_name: ?[]const u8 = null;
204
+ var named_items = std.array_list.Managed([]const u8).init(s.allocator);
205
+ named_items.ensureTotalCapacity(8) catch break :pi;
206
+
207
+ // Also collect "resolved" items (with type/as stripped) for filtering
208
+ var resolved_items = std.array_list.Managed([]const u8).init(s.allocator);
209
+ resolved_items.ensureTotalCapacity(8) catch break :pi;
210
+
211
+ var is_namespace = false;
212
+ var namespace_name: ?[]const u8 = null;
213
+
214
+ const brace_start = ch.indexOfChar(import_part, '{', 0);
215
+ const brace_end = std.mem.lastIndexOf(u8, import_part, "}");
216
+
217
+ // Check for namespace import: * as Name
218
+ if (ch.indexOf(import_part, "* as ", 0)) |star_idx| {
219
+ is_namespace = true;
220
+ const ns_name = ch.sliceTrimmed(import_part, star_idx + 5, if (brace_start) |bs| bs else import_part.len);
221
+ if (ns_name.len > 0) {
222
+ namespace_name = ns_name;
223
+ resolved_items.append(ns_name) catch {};
224
+ }
225
+ }
226
+
227
+ if (brace_start != null and brace_end != null) {
228
+ const bs = brace_start.?;
229
+ const be = brace_end.?;
230
+
231
+ // Default import before braces
232
+ if (bs > 0) {
233
+ var before = ch.sliceTrimmed(import_part, 0, bs);
234
+ if (before.len > 0 and before[before.len - 1] == ',') {
235
+ before = ch.sliceTrimmed(before, 0, before.len - 1);
236
+ }
237
+ if (before.len > 0 and !ch.contains(before, "*")) {
238
+ default_name = before;
239
+ resolved_items.append(before) catch {};
240
+ }
241
+ }
242
+
243
+ // Named imports
244
+ const named_part = import_part[bs + 1 .. be];
245
+ var iter = std.mem.splitSequence(u8, named_part, ",");
246
+ while (iter.next()) |raw_item| {
247
+ const trimmed = ch.sliceTrimmed(raw_item, 0, raw_item.len);
248
+ if (trimmed.len == 0) continue;
249
+ named_items.append(trimmed) catch {};
250
+
251
+ // Resolve: strip 'type ' prefix, use alias after ' as '
252
+ var resolved = trimmed;
253
+ if (ch.startsWith(resolved, "type ")) {
254
+ resolved = ch.sliceTrimmed(resolved, 5, resolved.len);
255
+ }
256
+ if (ch.indexOf(resolved, " as ", 0)) |as_idx| {
257
+ resolved = ch.sliceTrimmed(resolved, as_idx + 4, resolved.len);
258
+ }
259
+ if (resolved.len > 0) resolved_items.append(resolved) catch {};
260
+ }
261
+ } else if (!is_namespace) {
262
+ // Default import only
263
+ if (import_part.len > 0 and !ch.contains(import_part, "*")) {
264
+ default_name = import_part;
265
+ resolved_items.append(import_part) catch {};
266
+ }
267
+ }
268
+
269
+ parsed_import = .{
270
+ .default_name = default_name,
271
+ .named_items = named_items.items,
272
+ .source = module_src,
273
+ .is_type_only = is_type_only,
274
+ .is_namespace = is_namespace,
275
+ .namespace_name = namespace_name,
276
+ .resolved_items = resolved_items.items,
277
+ };
278
+ }
279
+
280
+ return .{
281
+ .kind = .import_decl,
282
+ .name = "",
283
+ .text = text,
284
+ .is_exported = false,
285
+ .is_type_only = is_type_only,
286
+ .is_side_effect = is_side_effect,
287
+ .source_module = module_src,
288
+ .leading_comments = comments,
289
+ .start = stmt_start,
290
+ .end = s.pos,
291
+ .parsed_import = parsed_import,
292
+ };
293
+ }
294
+
295
+ // ========================================================================
296
+ // Generics, params, return type extraction
297
+ // ========================================================================
298
+
299
+ /// Extract type parameters <...> (normalized to single line)
300
+ pub fn extractGenerics(s: *Scanner) []const u8 {
301
+ if (s.pos >= s.len or s.source[s.pos] != ch.CH_LANGLE) return "";
302
+ const start = s.pos;
303
+ _ = s.findMatchingClose(ch.CH_LANGLE, ch.CH_RANGLE);
304
+ const raw = s.source[start..s.pos];
305
+ // Normalize multi-line generics to single line
306
+ if (ch.indexOf(raw, "\n", 0) != null) {
307
+ // Direct alloc: output ≤ input length (whitespace collapsed)
308
+ const buf = s.allocator.alloc(u8, raw.len) catch return raw;
309
+ var pos: usize = 0;
310
+ var prev_space = false;
311
+ for (raw) |c| {
312
+ if (c == ' ' or c == '\t' or c == '\n' or c == '\r') {
313
+ if (!prev_space and pos > 0 and buf[pos - 1] != '<') {
314
+ buf[pos] = ' ';
315
+ pos += 1;
316
+ prev_space = true;
317
+ }
318
+ } else {
319
+ if (c == '>' and prev_space and pos > 0 and buf[pos - 1] == ' ') {
320
+ pos -= 1;
321
+ }
322
+ buf[pos] = c;
323
+ pos += 1;
324
+ prev_space = false;
325
+ }
326
+ }
327
+ return buf[0..pos];
328
+ }
329
+ return raw;
330
+ }
331
+
332
+ /// Extract parameter list (...) as raw text
333
+ pub fn extractParamList(s: *Scanner) []const u8 {
334
+ if (s.pos >= s.len or s.source[s.pos] != ch.CH_LPAREN) return "()";
335
+ const start = s.pos;
336
+ _ = s.findMatchingClose(ch.CH_LPAREN, ch.CH_RPAREN);
337
+ return s.source[start..s.pos];
338
+ }
339
+
340
+ /// Extract return type annotation `: ReturnType` after params
341
+ pub fn extractReturnType(s: *Scanner) []const u8 {
342
+ s.skipWhitespaceAndComments();
343
+ if (s.pos < s.len and s.source[s.pos] == ch.CH_COLON) {
344
+ s.pos += 1; // skip :
345
+ s.skipWhitespaceAndComments();
346
+ const start = s.pos;
347
+ var depth: isize = 0;
348
+ while (s.pos < s.len) {
349
+ if (s.skipNonCode()) continue;
350
+ const c = s.source[s.pos];
351
+ if (c == ch.CH_LPAREN or c == ch.CH_LBRACKET or c == ch.CH_LANGLE) {
352
+ depth += 1;
353
+ } else if (c == ch.CH_RPAREN or c == ch.CH_RBRACKET or (c == ch.CH_RANGLE and !s.isArrowGT())) {
354
+ depth -= 1;
355
+ } else if (c == ch.CH_LBRACE) {
356
+ if (depth > 0) {
357
+ depth += 1;
358
+ } else {
359
+ const text_so_far = ch.sliceTrimmed(s.source, start, s.pos);
360
+ const is_type_ctx = text_so_far.len == 0 or
361
+ ch.endsWith(text_so_far, "|") or
362
+ ch.endsWith(text_so_far, "&") or
363
+ endsWithWord(text_so_far, "is") or
364
+ endsWithWord(text_so_far, "extends");
365
+ if (is_type_ctx) {
366
+ depth += 1;
367
+ } else {
368
+ break; // function body
369
+ }
370
+ }
371
+ } else if (c == ch.CH_RBRACE) {
372
+ if (depth == 0) break;
373
+ depth -= 1;
374
+ } else if (depth == 0 and c == ch.CH_SEMI) {
375
+ break;
376
+ }
377
+ if (depth == 0 and s.checkASIMember()) break;
378
+ s.pos += 1;
379
+ }
380
+ return ch.sliceTrimmed(s.source, start, s.pos);
381
+ }
382
+ return "";
383
+ }
384
+
385
+ fn endsWithWord(text: []const u8, word: []const u8) bool {
386
+ if (text.len < word.len) return false;
387
+ const idx = text.len - word.len;
388
+ if (!std.mem.eql(u8, text[idx..], word)) return false;
389
+ return idx == 0 or !ch.isIdentChar(text[idx - 1]);
390
+ }
391
+
392
+ // ========================================================================
393
+ // Parameter processing
394
+ // ========================================================================
395
+
396
+ /// Check if string is a numeric literal
397
+ pub fn isNumericLiteral(v: []const u8) bool {
398
+ var i: usize = 0;
399
+ if (i < v.len and v[i] == '-') i += 1;
400
+ if (i >= v.len) return false;
401
+ if (v[i] < '0' or v[i] > '9') return false;
402
+ while (i < v.len and v[i] >= '0' and v[i] <= '9') i += 1;
403
+ if (i < v.len and v[i] == '.') {
404
+ i += 1;
405
+ if (i >= v.len or v[i] < '0' or v[i] > '9') return false;
406
+ while (i < v.len and v[i] >= '0' and v[i] <= '9') i += 1;
407
+ }
408
+ return i == v.len;
409
+ }
410
+
411
+ /// Infer type from a default value expression (simple cases)
412
+ pub fn inferTypeFromDefault(value: []const u8) []const u8 {
413
+ const v = std.mem.trim(u8, value, " \t\r\n");
414
+ if (std.mem.eql(u8, v, "true") or std.mem.eql(u8, v, "false")) return "boolean";
415
+ if (isNumericLiteral(v)) return "number";
416
+ if (v.len >= 2 and ((v[0] == '\'' and v[v.len - 1] == '\'') or (v[0] == '"' and v[v.len - 1] == '"'))) return "string";
417
+ if (v.len > 0 and v[0] == '[') return "unknown[]";
418
+ if (v.len > 0 and v[0] == '{') return "Record<string, unknown>";
419
+ return "unknown";
420
+ }
421
+
422
+ /// Infer literal type from initializer value (for const-like / static readonly)
423
+ pub fn inferLiteralType(value: []const u8) []const u8 {
424
+ const v = std.mem.trim(u8, value, " \t\r\n");
425
+ if (std.mem.eql(u8, v, "true") or std.mem.eql(u8, v, "false")) return v;
426
+ if (isNumericLiteral(v)) return v;
427
+ if (v.len >= 2 and ((v[0] == '\'' and v[v.len - 1] == '\'') or (v[0] == '"' and v[v.len - 1] == '"'))) return v;
428
+ return "unknown";
429
+ }
430
+
431
+ /// Extract type from `as Type` assertion in initializer
432
+ pub fn extractAssertion(init_text: []const u8) ?[]const u8 {
433
+ if (ch.endsWith(init_text, "as const")) return null;
434
+ // Only find " as " at depth 0 (not inside nested brackets/braces/parens)
435
+ var last_as: ?usize = null;
436
+ var depth: isize = 0;
437
+ var in_str: u8 = 0;
438
+ var i: usize = 0;
439
+ while (i < init_text.len) {
440
+ const c = init_text[i];
441
+ if (in_str != 0) {
442
+ if (c == '\\') {
443
+ i += 2;
444
+ continue;
445
+ }
446
+ if (c == in_str) in_str = 0;
447
+ i += 1;
448
+ continue;
449
+ }
450
+ if (c == '\'' or c == '"' or c == '`') {
451
+ in_str = c;
452
+ } else if (c == '(' or c == '[' or c == '{' or c == '<') {
453
+ depth += 1;
454
+ } else if (c == ')' or c == ']' or c == '}' or c == '>') {
455
+ depth -= 1;
456
+ } else if (depth == 0 and i + 4 <= init_text.len and std.mem.eql(u8, init_text[i .. i + 4], " as ")) {
457
+ last_as = i;
458
+ }
459
+ i += 1;
460
+ }
461
+ if (last_as) |idx| {
462
+ const after = std.mem.trim(u8, init_text[idx + 4 ..], " \t\r\n");
463
+ if (after.len > 0 and !std.mem.eql(u8, after, "const")) return after;
464
+ }
465
+ return null;
466
+ }
467
+
468
+ /// Build DTS-safe parameter text from raw parameter text
469
+ pub fn buildDtsParams(s: *Scanner, raw_params: []const u8) []const u8 {
470
+ if (raw_params.len < 2) return "()";
471
+ const inner = std.mem.trim(u8, raw_params[1 .. raw_params.len - 1], " \t\r\n");
472
+ if (inner.len == 0) return "()";
473
+
474
+ // Fast path: single-pass analysis — allow { and [ in type positions (after colon).
475
+ // Only reject destructuring ({ or [ before colon), defaults (=), and decorators (@).
476
+ if (ch.indexOfChar(raw_params, '\n', 0) == null and inner.len > 0) {
477
+ var depth: isize = 0;
478
+ var seen_colon = false;
479
+ var colons: usize = 0;
480
+ var commas: usize = 0;
481
+ var can_passthrough = true;
482
+ var fp_i: usize = 0;
483
+ while (fp_i < inner.len) : (fp_i += 1) {
484
+ const c = inner[fp_i];
485
+ if (c == ch.CH_LPAREN or c == ch.CH_LANGLE) {
486
+ depth += 1;
487
+ } else if (c == ch.CH_RPAREN or c == ch.CH_RANGLE) {
488
+ depth -= 1;
489
+ } else if (c == ch.CH_LBRACE or c == ch.CH_LBRACKET) {
490
+ if (depth == 0 and !seen_colon) { can_passthrough = false; break; }
491
+ depth += 1;
492
+ } else if (c == ch.CH_RBRACE or c == ch.CH_RBRACKET) {
493
+ depth -= 1;
494
+ } else if (depth == 0) {
495
+ if (c == ch.CH_COLON) { colons += 1; seen_colon = true; } else if (c == ch.CH_COMMA) { commas += 1; seen_colon = false; } else if (c == ch.CH_EQUAL and (fp_i + 1 >= inner.len or (inner[fp_i + 1] != ch.CH_RANGLE and inner[fp_i + 1] != ch.CH_EQUAL))) {
496
+ can_passthrough = false;
497
+ break;
498
+ } else if (c == ch.CH_AT) {
499
+ can_passthrough = false;
500
+ break;
501
+ }
502
+ }
503
+ }
504
+ if (can_passthrough and colons >= commas + 1) {
505
+ // Check for parameter modifiers
506
+ var has_modifier = false;
507
+ for (types.PARAM_MODIFIERS) |mod| {
508
+ if (ch.indexOf(inner, mod, 0)) |mod_idx| {
509
+ const after_idx = mod_idx + mod.len;
510
+ const before_ok = mod_idx == 0 or !ch.isIdentChar(inner[mod_idx - 1]);
511
+ const after_ok = after_idx >= inner.len or !ch.isIdentChar(inner[after_idx]);
512
+ if (before_ok and after_ok) {
513
+ has_modifier = true;
514
+ break;
515
+ }
516
+ }
517
+ }
518
+ if (!has_modifier) return raw_params;
519
+ }
520
+ }
521
+
522
+ // Split parameters by comma at depth 0
523
+ var params = std.array_list.Managed([]const u8).init(s.allocator);
524
+ params.ensureTotalCapacity(8) catch {};
525
+ var param_start: usize = 0;
526
+ var depth: isize = 0;
527
+ var in_str = false;
528
+ var str_ch: u8 = 0;
529
+ var skip_next = false;
530
+
531
+ for (inner, 0..) |c, i| {
532
+ if (skip_next) {
533
+ skip_next = false;
534
+ continue;
535
+ }
536
+ if (in_str) {
537
+ if (c == ch.CH_BACKSLASH) {
538
+ skip_next = true; // skip the escaped character
539
+ continue;
540
+ }
541
+ if (c == str_ch) in_str = false;
542
+ continue;
543
+ }
544
+ if (c == ch.CH_SQUOTE or c == ch.CH_DQUOTE or c == ch.CH_BACKTICK) {
545
+ in_str = true;
546
+ str_ch = c;
547
+ continue;
548
+ }
549
+ if (c == ch.CH_LPAREN or c == ch.CH_LBRACE or c == ch.CH_LBRACKET or c == ch.CH_LANGLE) {
550
+ depth += 1;
551
+ } else if (c == ch.CH_RPAREN or c == ch.CH_RBRACE or c == ch.CH_RBRACKET or c == ch.CH_RANGLE) {
552
+ depth -= 1;
553
+ } else if (c == ch.CH_COMMA and depth == 0) {
554
+ params.append(std.mem.trim(u8, inner[param_start..i], " \t\r\n")) catch {};
555
+ param_start = i + 1;
556
+ }
557
+ }
558
+ params.append(std.mem.trim(u8, inner[param_start..], " \t\r\n")) catch {};
559
+
560
+ // Build DTS params — direct alloc, output ≤ raw_params.len + extra for type annotations
561
+ const buf = s.allocator.alloc(u8, raw_params.len * 2 + 16) catch return "()";
562
+ var pos: usize = 0;
563
+ buf[pos] = '(';
564
+ pos += 1;
565
+ var first = true;
566
+ for (params.items) |param| {
567
+ if (param.len == 0) continue;
568
+ if (!first) {
569
+ @memcpy(buf[pos..][0..2], ", ");
570
+ pos += 2;
571
+ }
572
+ first = false;
573
+ const dts_param = buildSingleDtsParam(s, param);
574
+ @memcpy(buf[pos..][0..dts_param.len], dts_param);
575
+ pos += dts_param.len;
576
+ }
577
+ buf[pos] = ')';
578
+ pos += 1;
579
+ return buf[0..pos];
580
+ }
581
+
582
+ /// Build a single DTS parameter from raw source text
583
+ pub fn buildSingleDtsParam(s: *Scanner, raw: []const u8) []const u8 {
584
+ var p = std.mem.trim(u8, raw, " \t\r\n");
585
+
586
+ // Handle rest parameter
587
+ const is_rest = ch.startsWith(p, "...");
588
+ if (is_rest) p = std.mem.trim(u8, p[3..], " \t\r\n");
589
+
590
+ // Handle decorators (skip @... before param)
591
+ while (p.len > 0 and p[0] == '@') {
592
+ var di: usize = 1;
593
+ while (di < p.len and ch.isIdentChar(p[di])) di += 1;
594
+ if (di < p.len and p[di] == ch.CH_LPAREN) {
595
+ var dd: isize = 1;
596
+ di += 1;
597
+ while (di < p.len and dd > 0) {
598
+ if (p[di] == ch.CH_LPAREN) dd += 1 else if (p[di] == ch.CH_RPAREN) dd -= 1;
599
+ di += 1;
600
+ }
601
+ }
602
+ p = std.mem.trim(u8, p[di..], " \t\r\n");
603
+ }
604
+
605
+ // Strip parameter modifiers
606
+ var stripped = true;
607
+ while (stripped) {
608
+ stripped = false;
609
+ for (types.PARAM_MODIFIERS) |mod| {
610
+ if (p.len > mod.len and ch.startsWith(p, mod) and !ch.isIdentChar(p[mod.len])) {
611
+ p = std.mem.trim(u8, p[mod.len..], " \t\r\n");
612
+ stripped = true;
613
+ break;
614
+ }
615
+ }
616
+ }
617
+
618
+ // Find : and = at depth 0
619
+ var colon_idx: ?usize = null;
620
+ var equal_idx: ?usize = null;
621
+ var depth: isize = 0;
622
+ var in_str2 = false;
623
+ var str_ch2: u8 = 0;
624
+ var skip_next2 = false;
625
+
626
+ for (p, 0..) |c, i| {
627
+ if (skip_next2) {
628
+ skip_next2 = false;
629
+ continue;
630
+ }
631
+ if (in_str2) {
632
+ if (c == ch.CH_BACKSLASH) {
633
+ skip_next2 = true;
634
+ continue;
635
+ }
636
+ if (c == str_ch2) in_str2 = false;
637
+ continue;
638
+ }
639
+ if (c == ch.CH_SQUOTE or c == ch.CH_DQUOTE or c == ch.CH_BACKTICK) {
640
+ in_str2 = true;
641
+ str_ch2 = c;
642
+ continue;
643
+ }
644
+ if (c == ch.CH_LPAREN or c == ch.CH_LBRACE or c == ch.CH_LBRACKET or c == ch.CH_LANGLE) {
645
+ depth += 1;
646
+ } else if (c == ch.CH_RPAREN or c == ch.CH_RBRACE or c == ch.CH_RBRACKET or c == ch.CH_RANGLE) {
647
+ depth -= 1;
648
+ } else if (depth == 0) {
649
+ if (c == ch.CH_COLON and colon_idx == null) {
650
+ colon_idx = i;
651
+ } else if (c == ch.CH_EQUAL and equal_idx == null) {
652
+ // Check it's not == or =>
653
+ const not_double = i == 0 or p[i - 1] != ch.CH_EQUAL;
654
+ const not_arrow = i + 1 >= p.len or (p[i + 1] != ch.CH_EQUAL and p[i + 1] != ch.CH_RANGLE);
655
+ if (not_double and not_arrow) {
656
+ equal_idx = i;
657
+ }
658
+ }
659
+ }
660
+ }
661
+
662
+ var name: []const u8 = undefined;
663
+ var param_type: []const u8 = undefined;
664
+ const has_default = equal_idx != null;
665
+
666
+ if (colon_idx) |ci| {
667
+ if (equal_idx == null or ci < equal_idx.?) {
668
+ name = std.mem.trim(u8, p[0..ci], " \t\r\n");
669
+ if (equal_idx) |ei| {
670
+ param_type = std.mem.trim(u8, p[ci + 1 .. ei], " \t\r\n");
671
+ } else {
672
+ param_type = std.mem.trim(u8, p[ci + 1 ..], " \t\r\n");
673
+ }
674
+ } else {
675
+ name = std.mem.trim(u8, p[0..equal_idx.?], " \t\r\n");
676
+ param_type = inferTypeFromDefault(std.mem.trim(u8, p[equal_idx.? + 1 ..], " \t\r\n"));
677
+ }
678
+ } else if (equal_idx) |ei| {
679
+ name = std.mem.trim(u8, p[0..ei], " \t\r\n");
680
+ param_type = inferTypeFromDefault(std.mem.trim(u8, p[ei + 1 ..], " \t\r\n"));
681
+ } else {
682
+ name = p;
683
+ param_type = "unknown";
684
+ }
685
+
686
+ // Clean destructured patterns: strip defaults and rest operators
687
+ if (name.len > 0 and (name[0] == '{' or name[0] == '[')) {
688
+ name = cleanDestructuredPattern(s.allocator, name);
689
+ }
690
+
691
+ // Handle optional marker
692
+ const is_optional = (name.len > 0 and name[name.len - 1] == '?') or has_default;
693
+ if (name.len > 0 and name[name.len - 1] == '?') {
694
+ name = std.mem.trim(u8, name[0 .. name.len - 1], " \t\r\n");
695
+ }
696
+ const opt_marker: []const u8 = if (is_optional and !is_rest) "?" else "";
697
+
698
+ // Build result
699
+ var result = std.array_list.Managed(u8).init(s.allocator);
700
+ if (is_rest) result.appendSlice("...") catch {};
701
+ result.appendSlice(name) catch {};
702
+ result.appendSlice(opt_marker) catch {};
703
+ result.appendSlice(": ") catch {};
704
+ result.appendSlice(param_type) catch {};
705
+ return result.toOwnedSlice() catch "unknown: unknown";
706
+ }
707
+
708
+ /// Clean a destructured pattern by stripping default values and rest operators.
709
+ /// E.g., "{ name, age = 0, ...props }" → "{ name, age, props }"
710
+ /// Also handles multiline patterns like:
711
+ /// "{\n name,\n headers = { ... },\n}" → "{\n name,\n headers,\n}"
712
+ fn cleanDestructuredPattern(alloc: std.mem.Allocator, pattern: []const u8) []const u8 {
713
+ var result = std.array_list.Managed(u8).init(alloc);
714
+ result.ensureTotalCapacity(pattern.len) catch {};
715
+ var i: usize = 0;
716
+ var depth: isize = 0;
717
+ var in_str = false;
718
+ var str_c: u8 = 0;
719
+
720
+ while (i < pattern.len) : (i += 1) {
721
+ const c = pattern[i];
722
+
723
+ // String tracking
724
+ if (!in_str and (c == '\'' or c == '"' or c == '`')) {
725
+ in_str = true;
726
+ str_c = c;
727
+ result.append(c) catch {};
728
+ continue;
729
+ }
730
+ if (in_str) {
731
+ result.append(c) catch {};
732
+ if (c == str_c and (i == 0 or pattern[i - 1] != '\\')) {
733
+ in_str = false;
734
+ }
735
+ continue;
736
+ }
737
+
738
+ // Track depth
739
+ if (c == '{' or c == '[' or c == '(') depth += 1;
740
+ if (c == '}' or c == ']' or c == ')') depth -= 1;
741
+
742
+ // At depth 1 (inside the outermost braces), handle defaults and rest
743
+ if (depth == 1) {
744
+ // Skip "..." rest operator before identifiers
745
+ if (c == '.' and i + 2 < pattern.len and pattern[i + 1] == '.' and pattern[i + 2] == '.') {
746
+ i += 2; // skip 2 more dots (loop will advance 1 more)
747
+ continue;
748
+ }
749
+
750
+ // Skip "= value" default values
751
+ if (c == '=' and i + 1 < pattern.len and pattern[i + 1] != '>' and (i == 0 or pattern[i - 1] != '!')) {
752
+ // Skip whitespace before '='
753
+ while (result.items.len > 0 and (result.items[result.items.len - 1] == ' ' or result.items[result.items.len - 1] == '\t')) {
754
+ _ = result.pop();
755
+ }
756
+ // Skip the default value: everything up to ',' or '}'/']' at this depth
757
+ i += 1; // skip '='
758
+ var inner_depth: isize = 0;
759
+ while (i < pattern.len) {
760
+ const dc = pattern[i];
761
+ if (!in_str and (dc == '\'' or dc == '"' or dc == '`')) {
762
+ in_str = true;
763
+ str_c = dc;
764
+ i += 1;
765
+ continue;
766
+ }
767
+ if (in_str) {
768
+ if (dc == str_c and (i == 0 or pattern[i - 1] != '\\')) {
769
+ in_str = false;
770
+ }
771
+ i += 1;
772
+ continue;
773
+ }
774
+ if (dc == '{' or dc == '[' or dc == '(') inner_depth += 1;
775
+ if (dc == '}' or dc == ']' or dc == ')') {
776
+ if (inner_depth == 0) break; // Hit the closing brace
777
+ inner_depth -= 1;
778
+ }
779
+ if (dc == ',' and inner_depth == 0) break;
780
+ i += 1;
781
+ }
782
+ // Don't advance i further — the loop's :i+=1 will handle it,
783
+ // but we need to emit the comma or closing brace
784
+ i -= 1; // compensate for the loop's += 1
785
+ continue;
786
+ }
787
+ }
788
+
789
+ result.append(c) catch {};
790
+ }
791
+
792
+ const cleaned = result.toOwnedSlice() catch return pattern;
793
+
794
+ // If the pattern contains newlines, try to collapse to single line if short enough
795
+ if (ch.indexOf(cleaned, "\n", 0) != null) {
796
+ // Collapse newlines and extra whitespace to single spaces
797
+ var collapsed = std.array_list.Managed(u8).init(alloc);
798
+ collapsed.ensureTotalCapacity(cleaned.len) catch {};
799
+ var in_ws = false;
800
+ for (cleaned) |c2| {
801
+ if (c2 == '\n' or c2 == '\r' or c2 == ' ' or c2 == '\t') {
802
+ if (!in_ws) {
803
+ collapsed.append(' ') catch {};
804
+ in_ws = true;
805
+ }
806
+ } else {
807
+ collapsed.append(c2) catch {};
808
+ in_ws = false;
809
+ }
810
+ }
811
+ const collapsed_str = std.mem.trim(u8, collapsed.items, " \t\r\n");
812
+ if (collapsed_str.len <= 40) {
813
+ return collapsed_str;
814
+ }
815
+ // Keep multiline but normalize indent
816
+ var normalized = std.array_list.Managed(u8).init(alloc);
817
+ normalized.ensureTotalCapacity(cleaned.len) catch {};
818
+ var line_start: usize = 0;
819
+ var ci: usize = 0;
820
+ while (ci <= cleaned.len) : (ci += 1) {
821
+ if (ci == cleaned.len or cleaned[ci] == '\n') {
822
+ const line = std.mem.trim(u8, cleaned[line_start..ci], " \t\r\n");
823
+ if (line.len > 0) {
824
+ if (normalized.items.len > 0) normalized.append('\n') catch {};
825
+ if (line[0] == '{' or line[0] == '}' or line[0] == '[' or line[0] == ']') {
826
+ normalized.appendSlice(line) catch {};
827
+ } else {
828
+ normalized.appendSlice(" ") catch {};
829
+ normalized.appendSlice(line) catch {};
830
+ }
831
+ }
832
+ line_start = ci + 1;
833
+ }
834
+ }
835
+ return normalized.toOwnedSlice() catch cleaned;
836
+ }
837
+
838
+ return cleaned;
839
+ }
840
+
841
+ // ========================================================================
842
+ // Function extraction
843
+ // ========================================================================
844
+
845
+ /// Extract a function declaration and build DTS text
846
+ pub fn extractFunction(s: *Scanner, decl_start: usize, is_exported: bool, is_async: bool, is_default: bool) ?Declaration {
847
+ s.pos += 8; // skip 'function'
848
+ s.skipWhitespaceAndComments();
849
+
850
+ const is_generator = s.pos < s.len and s.source[s.pos] == ch.CH_STAR;
851
+ if (is_generator) {
852
+ s.pos += 1;
853
+ s.skipWhitespaceAndComments();
854
+ }
855
+
856
+ const name = s.readIdent();
857
+ if (name.len == 0 and !is_default) return null;
858
+ s.skipWhitespaceAndComments();
859
+
860
+ const generics = extractGenerics(s);
861
+ s.skipWhitespaceAndComments();
862
+
863
+ const raw_params = extractParamList(s);
864
+ s.skipWhitespaceAndComments();
865
+
866
+ var return_type = extractReturnType(s);
867
+ if (return_type.len == 0) {
868
+ if (is_async and is_generator) {
869
+ return_type = "AsyncGenerator<unknown, void, unknown>";
870
+ } else if (is_generator) {
871
+ return_type = "Generator<unknown, void, unknown>";
872
+ } else if (is_async) {
873
+ return_type = "Promise<void>";
874
+ } else {
875
+ return_type = "void";
876
+ }
877
+ }
878
+
879
+ s.skipWhitespaceAndComments();
880
+ var has_body = false;
881
+ if (s.pos < s.len and s.source[s.pos] == ch.CH_LBRACE) {
882
+ has_body = true;
883
+ _ = s.findMatchingClose(ch.CH_LBRACE, ch.CH_RBRACE);
884
+ } else if (s.pos < s.len and s.source[s.pos] == ch.CH_SEMI) {
885
+ s.pos += 1;
886
+ }
887
+
888
+ const dts_params = buildDtsParams(s, raw_params);
889
+ const func_name = if (name.len > 0) name else "default";
890
+
891
+ // Build DTS text
892
+ var text = std.array_list.Managed(u8).init(s.allocator);
893
+ text.ensureTotalCapacity(128) catch {};
894
+ if (is_exported) text.appendSlice("export ") catch {};
895
+ text.appendSlice("declare function ") catch {};
896
+ text.appendSlice(func_name) catch {};
897
+ text.appendSlice(generics) catch {};
898
+ text.appendSlice(dts_params) catch {};
899
+ text.appendSlice(": ") catch {};
900
+ text.appendSlice(return_type) catch {};
901
+ text.append(';') catch {};
902
+
903
+ const dts_text = text.toOwnedSlice() catch "";
904
+ const comments = extractLeadingComments(s, decl_start);
905
+
906
+ if (has_body) {
907
+ s.func_body_indices.put(s.declarations.items.len, {}) catch {};
908
+ }
909
+
910
+ return .{
911
+ .kind = .function_decl,
912
+ .name = func_name,
913
+ .text = dts_text,
914
+ .is_exported = is_exported,
915
+ .is_default = is_default,
916
+ .is_async = is_async,
917
+ .is_generator = is_generator,
918
+ .generics = generics,
919
+ .leading_comments = comments,
920
+ .start = decl_start,
921
+ .end = s.pos,
922
+ .has_body = has_body,
923
+ };
924
+ }
925
+
926
+ // ========================================================================
927
+ // Variable extraction
928
+ // ========================================================================
929
+
930
+ /// Extract variable declaration(s)
931
+ pub fn extractVariable(s: *Scanner, decl_start: usize, kind: []const u8, is_exported: bool) []const Declaration {
932
+ s.pos += kind.len; // skip const/let/var
933
+ s.skipWhitespaceAndComments();
934
+
935
+ var results = std.array_list.Managed(Declaration).init(s.allocator);
936
+ results.ensureTotalCapacity(2) catch {};
937
+
938
+ if (s.pos >= s.len) return results.toOwnedSlice() catch &.{};
939
+
940
+ const c = s.source[s.pos];
941
+ // Skip destructuring patterns
942
+ if (c == ch.CH_LBRACE or c == ch.CH_LBRACKET) {
943
+ s.skipToStatementEnd();
944
+ return results.toOwnedSlice() catch &.{};
945
+ }
946
+
947
+ const name = s.readIdent();
948
+ if (name.len == 0) {
949
+ s.skipToStatementEnd();
950
+ return results.toOwnedSlice() catch &.{};
951
+ }
952
+ s.skipWhitespaceAndComments();
953
+
954
+ var type_annotation: []const u8 = "";
955
+ var initializer_text: []const u8 = "";
956
+ var is_as_const = false;
957
+
958
+ // Type annotation
959
+ if (s.pos < s.len and s.source[s.pos] == ch.CH_COLON) {
960
+ s.pos += 1;
961
+ s.skipWhitespaceAndComments();
962
+ const type_start = s.pos;
963
+ var depth: isize = 0;
964
+ while (s.pos < s.len) {
965
+ if (s.skipNonCode()) continue;
966
+ const tc = s.source[s.pos];
967
+ if (tc == ch.CH_LPAREN or tc == ch.CH_LBRACE or tc == ch.CH_LBRACKET or tc == ch.CH_LANGLE) {
968
+ depth += 1;
969
+ } else if (tc == ch.CH_RPAREN or tc == ch.CH_RBRACE or tc == ch.CH_RBRACKET or (tc == ch.CH_RANGLE and !s.isArrowGT())) {
970
+ depth -= 1;
971
+ } else if (depth == 0 and (tc == ch.CH_EQUAL or tc == ch.CH_SEMI or tc == ch.CH_COMMA)) {
972
+ break;
973
+ }
974
+ if (depth == 0 and s.checkASITopLevel()) break;
975
+ s.pos += 1;
976
+ }
977
+ type_annotation = s.sliceTrimmed(type_start, s.pos);
978
+ }
979
+
980
+ // Initializer
981
+ if (s.pos < s.len and s.source[s.pos] == ch.CH_EQUAL) {
982
+ if (s.isolated_declarations and type_annotation.len > 0 and !type_inf.isGenericType(type_annotation)) {
983
+ s.skipToStatementEnd();
984
+ } else {
985
+ s.pos += 1;
986
+ s.skipWhitespaceAndComments();
987
+ const init_start = s.pos;
988
+ var depth: isize = 0;
989
+ while (s.pos < s.len) {
990
+ if (s.skipNonCode()) continue;
991
+ const ic = s.source[s.pos];
992
+ if (ic == ch.CH_LPAREN or ic == ch.CH_LBRACE or ic == ch.CH_LBRACKET or ic == ch.CH_LANGLE) {
993
+ depth += 1;
994
+ } else if (ic == ch.CH_RPAREN or ic == ch.CH_RBRACE or ic == ch.CH_RBRACKET or (ic == ch.CH_RANGLE and !s.isArrowGT())) {
995
+ depth -= 1;
996
+ } else if (depth == 0 and (ic == ch.CH_SEMI or ic == ch.CH_COMMA)) {
997
+ break;
998
+ }
999
+ if (depth == 0 and s.checkASITopLevel()) break;
1000
+ s.pos += 1;
1001
+ }
1002
+ initializer_text = s.sliceTrimmed(init_start, s.pos);
1003
+ if (ch.endsWith(initializer_text, " as const") or std.mem.eql(u8, initializer_text, "const")) {
1004
+ is_as_const = true;
1005
+ if (type_annotation.len == 0) {
1006
+ const val = if (ch.endsWith(initializer_text, " as const"))
1007
+ std.mem.trim(u8, initializer_text[0 .. initializer_text.len - 9], " \t\r\n")
1008
+ else
1009
+ initializer_text;
1010
+ const lit = inferLiteralType(val);
1011
+ if (!std.mem.eql(u8, lit, "unknown")) {
1012
+ type_annotation = lit;
1013
+ }
1014
+ }
1015
+ } else if (type_annotation.len == 0) {
1016
+ const as_type = extractAssertion(initializer_text);
1017
+ if (as_type) |t| type_annotation = t;
1018
+ }
1019
+ }
1020
+ }
1021
+
1022
+ // Skip comma or semicolon
1023
+ if (s.pos < s.len) {
1024
+ const sc = s.source[s.pos];
1025
+ if (sc == ch.CH_SEMI) s.pos += 1;
1026
+ }
1027
+
1028
+ const comments = extractLeadingComments(s, decl_start);
1029
+ const final_type = if (type_annotation.len > 0) type_annotation else "unknown";
1030
+
1031
+ // Build DTS text
1032
+ var text = std.array_list.Managed(u8).init(s.allocator);
1033
+ text.ensureTotalCapacity(128) catch {};
1034
+ if (is_exported) text.appendSlice("export ") catch {};
1035
+ text.appendSlice("declare ") catch {};
1036
+ text.appendSlice(kind) catch {};
1037
+ text.append(' ') catch {};
1038
+ text.appendSlice(name) catch {};
1039
+ text.appendSlice(": ") catch {};
1040
+ text.appendSlice(final_type) catch {};
1041
+ text.append(';') catch {};
1042
+
1043
+ // Store the variable kind in modifiers
1044
+ const mods = s.allocator.alloc([]const u8, 1) catch null;
1045
+ if (mods) |m| {
1046
+ m[0] = kind;
1047
+ }
1048
+
1049
+ results.append(.{
1050
+ .kind = .variable_decl,
1051
+ .name = name,
1052
+ .text = text.toOwnedSlice() catch "",
1053
+ .is_exported = is_exported,
1054
+ .modifiers = mods,
1055
+ .type_annotation = type_annotation,
1056
+ .value = initializer_text,
1057
+ .leading_comments = comments,
1058
+ .start = decl_start,
1059
+ .end = s.pos,
1060
+ }) catch {};
1061
+
1062
+ return results.toOwnedSlice() catch &.{};
1063
+ }
1064
+
1065
+ // ========================================================================
1066
+ // Interface extraction
1067
+ // ========================================================================
1068
+
1069
+ /// Extract interface declaration
1070
+ pub fn extractInterface(s: *Scanner, decl_start: usize, is_exported: bool) Declaration {
1071
+ s.pos += 9; // skip 'interface'
1072
+ s.skipWhitespaceAndComments();
1073
+
1074
+ const name = s.readIdent();
1075
+ s.skipWhitespaceAndComments();
1076
+
1077
+ const generics = extractGenerics(s);
1078
+ s.skipWhitespaceAndComments();
1079
+
1080
+ var extends_clause: []const u8 = "";
1081
+ if (s.matchWord("extends")) {
1082
+ s.pos += 7;
1083
+ s.skipWhitespaceAndComments();
1084
+ const ext_start = s.pos;
1085
+ var depth: isize = 0;
1086
+ while (s.pos < s.len) {
1087
+ if (s.skipNonCode()) continue;
1088
+ const c = s.source[s.pos];
1089
+ if (c == ch.CH_LANGLE) {
1090
+ depth += 1;
1091
+ } else if (c == ch.CH_RANGLE and !s.isArrowGT()) {
1092
+ depth -= 1;
1093
+ } else if (c == ch.CH_LBRACE and depth == 0) {
1094
+ break;
1095
+ }
1096
+ s.pos += 1;
1097
+ }
1098
+ extends_clause = s.sliceTrimmed(ext_start, s.pos);
1099
+ }
1100
+
1101
+ s.skipWhitespaceAndComments();
1102
+ const raw_body = if (s.pos < s.len and s.source[s.pos] == ch.CH_LBRACE) s.extractBraceBlock() else "{}";
1103
+ const body = cleanBraceBlock(s, raw_body);
1104
+
1105
+ // Build DTS text
1106
+ var text = std.array_list.Managed(u8).init(s.allocator);
1107
+ text.ensureTotalCapacity(128) catch {};
1108
+ if (is_exported) text.appendSlice("export ") catch {};
1109
+ text.appendSlice("declare interface ") catch {};
1110
+ text.appendSlice(name) catch {};
1111
+ text.appendSlice(generics) catch {};
1112
+ if (extends_clause.len > 0) {
1113
+ text.appendSlice(" extends ") catch {};
1114
+ text.appendSlice(extends_clause) catch {};
1115
+ }
1116
+ text.append(' ') catch {};
1117
+ text.appendSlice(body) catch {};
1118
+
1119
+ const comments = extractLeadingComments(s, decl_start);
1120
+
1121
+ return .{
1122
+ .kind = .interface_decl,
1123
+ .name = name,
1124
+ .text = text.toOwnedSlice() catch "",
1125
+ .is_exported = is_exported,
1126
+ .extends_clause = extends_clause,
1127
+ .generics = generics,
1128
+ .leading_comments = comments,
1129
+ .start = decl_start,
1130
+ .end = s.pos,
1131
+ };
1132
+ }
1133
+
1134
+ // ========================================================================
1135
+ // Type alias extraction
1136
+ // ========================================================================
1137
+
1138
+ /// Extract type alias declaration
1139
+ pub fn extractTypeAlias(s: *Scanner, decl_start: usize, is_exported: bool) Declaration {
1140
+ s.pos += 4; // skip 'type'
1141
+ s.skipWhitespaceAndComments();
1142
+
1143
+ const name = s.readIdent();
1144
+ s.skipWhitespaceAndComments();
1145
+
1146
+ const generics = extractGenerics(s);
1147
+ s.skipWhitespaceAndComments();
1148
+
1149
+ if (s.pos < s.len and s.source[s.pos] == ch.CH_EQUAL) s.pos += 1;
1150
+ s.skipWhitespaceAndComments();
1151
+
1152
+ const type_start = s.pos;
1153
+ var depth: isize = 0;
1154
+ while (s.pos < s.len) {
1155
+ if (s.skipNonCode()) continue;
1156
+ const c = s.source[s.pos];
1157
+ if (c == ch.CH_LPAREN or c == ch.CH_LBRACE or c == ch.CH_LBRACKET or c == ch.CH_LANGLE) {
1158
+ depth += 1;
1159
+ } else if (c == ch.CH_RPAREN or c == ch.CH_RBRACE or c == ch.CH_RBRACKET or (c == ch.CH_RANGLE and !s.isArrowGT())) {
1160
+ depth -= 1;
1161
+ } else if (depth == 0 and c == ch.CH_SEMI) {
1162
+ break;
1163
+ }
1164
+ if (depth == 0 and s.checkASITopLevel()) break;
1165
+ s.pos += 1;
1166
+ }
1167
+ const type_body = s.sliceTrimmed(type_start, s.pos);
1168
+ if (s.pos < s.len and s.source[s.pos] == ch.CH_SEMI) s.pos += 1;
1169
+
1170
+ var text = std.array_list.Managed(u8).init(s.allocator);
1171
+ text.ensureTotalCapacity(128) catch {};
1172
+ if (is_exported) text.appendSlice("export ") catch {};
1173
+ text.appendSlice("type ") catch {};
1174
+ text.appendSlice(name) catch {};
1175
+ text.appendSlice(generics) catch {};
1176
+ text.appendSlice(" = ") catch {};
1177
+ text.appendSlice(type_body) catch {};
1178
+
1179
+ const comments = extractLeadingComments(s, decl_start);
1180
+
1181
+ return .{
1182
+ .kind = .type_decl,
1183
+ .name = name,
1184
+ .text = text.toOwnedSlice() catch "",
1185
+ .is_exported = is_exported,
1186
+ .generics = generics,
1187
+ .leading_comments = comments,
1188
+ .start = decl_start,
1189
+ .end = s.pos,
1190
+ };
1191
+ }
1192
+
1193
+ // ========================================================================
1194
+ // Enum extraction
1195
+ // ========================================================================
1196
+
1197
+ /// Extract enum declaration
1198
+ pub fn extractEnum(s: *Scanner, decl_start: usize, is_exported: bool, is_const: bool) Declaration {
1199
+ s.pos += 4; // skip 'enum'
1200
+ s.skipWhitespaceAndComments();
1201
+
1202
+ const name = s.readIdent();
1203
+ s.skipWhitespaceAndComments();
1204
+
1205
+ if (s.pos < s.len and s.source[s.pos] == ch.CH_LBRACE) {
1206
+ _ = s.findMatchingClose(ch.CH_LBRACE, ch.CH_RBRACE);
1207
+ }
1208
+
1209
+ const raw_text = s.sliceTrimmed(decl_start, s.pos);
1210
+ const comments = extractLeadingComments(s, decl_start);
1211
+
1212
+ // Store const modifier
1213
+ const mods: ?[]const []const u8 = if (is_const) blk: {
1214
+ const mod_list = s.allocator.alloc([]const u8, 1) catch break :blk null;
1215
+ mod_list[0] = "const";
1216
+ break :blk mod_list;
1217
+ } else null;
1218
+
1219
+ return .{
1220
+ .kind = .enum_decl,
1221
+ .name = name,
1222
+ .text = raw_text,
1223
+ .is_exported = is_exported,
1224
+ .modifiers = mods,
1225
+ .leading_comments = comments,
1226
+ .start = decl_start,
1227
+ .end = s.pos,
1228
+ };
1229
+ }
1230
+
1231
+ // ========================================================================
1232
+ // Class extraction
1233
+ // ========================================================================
1234
+
1235
+ /// Extract class declaration and build DTS
1236
+ pub fn extractClass(s: *Scanner, decl_start: usize, is_exported: bool, is_abstract: bool) Declaration {
1237
+ s.pos += 5; // skip 'class'
1238
+ s.skipWhitespaceAndComments();
1239
+
1240
+ const name_raw = s.readIdent();
1241
+ const name = if (name_raw.len > 0) name_raw else "AnonymousClass";
1242
+ s.skipWhitespaceAndComments();
1243
+
1244
+ const generics = extractGenerics(s);
1245
+ s.skipWhitespaceAndComments();
1246
+
1247
+ var extends_clause: []const u8 = "";
1248
+ if (s.matchWord("extends")) {
1249
+ s.pos += 7;
1250
+ s.skipWhitespaceAndComments();
1251
+ const ext_start = s.pos;
1252
+ var depth: isize = 0;
1253
+ while (s.pos < s.len) {
1254
+ if (s.skipNonCode()) continue;
1255
+ const c = s.source[s.pos];
1256
+ if (c == ch.CH_LANGLE) {
1257
+ depth += 1;
1258
+ } else if (c == ch.CH_RANGLE and !s.isArrowGT()) {
1259
+ depth -= 1;
1260
+ } else if (depth == 0 and (c == ch.CH_LBRACE or s.matchWord("implements"))) {
1261
+ break;
1262
+ }
1263
+ s.pos += 1;
1264
+ }
1265
+ extends_clause = s.sliceTrimmed(ext_start, s.pos);
1266
+ }
1267
+
1268
+ var implements_text: []const u8 = "";
1269
+ if (s.matchWord("implements")) {
1270
+ s.pos += 10;
1271
+ s.skipWhitespaceAndComments();
1272
+ const impl_start = s.pos;
1273
+ var depth: isize = 0;
1274
+ while (s.pos < s.len) {
1275
+ if (s.skipNonCode()) continue;
1276
+ const c = s.source[s.pos];
1277
+ if (c == ch.CH_LANGLE) {
1278
+ depth += 1;
1279
+ } else if (c == ch.CH_RANGLE and !s.isArrowGT()) {
1280
+ depth -= 1;
1281
+ } else if (depth == 0 and c == ch.CH_LBRACE) {
1282
+ break;
1283
+ }
1284
+ s.pos += 1;
1285
+ }
1286
+ implements_text = s.sliceTrimmed(impl_start, s.pos);
1287
+ }
1288
+
1289
+ s.skipWhitespaceAndComments();
1290
+ const class_body = buildClassBodyDts(s);
1291
+
1292
+ // Build DTS text
1293
+ var text = std.array_list.Managed(u8).init(s.allocator);
1294
+ text.ensureTotalCapacity(128) catch {};
1295
+ if (is_exported) text.appendSlice("export ") catch {};
1296
+ text.appendSlice("declare ") catch {};
1297
+ if (is_abstract) text.appendSlice("abstract ") catch {};
1298
+ text.appendSlice("class ") catch {};
1299
+ text.appendSlice(name) catch {};
1300
+ text.appendSlice(generics) catch {};
1301
+ if (extends_clause.len > 0) {
1302
+ text.appendSlice(" extends ") catch {};
1303
+ text.appendSlice(extends_clause) catch {};
1304
+ }
1305
+ if (implements_text.len > 0) {
1306
+ text.appendSlice(" implements ") catch {};
1307
+ text.appendSlice(implements_text) catch {};
1308
+ }
1309
+ text.append(' ') catch {};
1310
+ text.appendSlice(class_body) catch {};
1311
+
1312
+ const comments = extractLeadingComments(s, decl_start);
1313
+
1314
+ return .{
1315
+ .kind = .class_decl,
1316
+ .name = name,
1317
+ .text = text.toOwnedSlice() catch "",
1318
+ .is_exported = is_exported,
1319
+ .extends_clause = extends_clause,
1320
+ .generics = generics,
1321
+ .leading_comments = comments,
1322
+ .start = decl_start,
1323
+ .end = s.pos,
1324
+ };
1325
+ }
1326
+
1327
+ /// Build class body DTS (members only, no implementations)
1328
+ fn buildClassBodyDts(s: *Scanner) []const u8 {
1329
+ if (s.pos >= s.len or s.source[s.pos] != ch.CH_LBRACE) return "{}";
1330
+ s.pos += 1; // skip {
1331
+
1332
+ var members = std.array_list.Managed([]const u8).init(s.allocator);
1333
+ members.ensureTotalCapacity(16) catch {};
1334
+
1335
+ while (s.pos < s.len) {
1336
+ s.skipWhitespaceAndComments();
1337
+ if (s.pos >= s.len) break;
1338
+ if (s.source[s.pos] == ch.CH_RBRACE) {
1339
+ s.pos += 1;
1340
+ break;
1341
+ }
1342
+ if (s.source[s.pos] == ch.CH_SEMI) {
1343
+ s.pos += 1;
1344
+ continue;
1345
+ }
1346
+
1347
+ // Skip static blocks
1348
+ if (s.matchWord("static") and s.peekAfterWord("static") == ch.CH_LBRACE) {
1349
+ s.pos += 6;
1350
+ s.skipWhitespaceAndComments();
1351
+ _ = s.findMatchingClose(ch.CH_LBRACE, ch.CH_RBRACE);
1352
+ continue;
1353
+ }
1354
+
1355
+ // Collect modifiers
1356
+ var is_private = false;
1357
+ var is_protected = false;
1358
+ var is_static = false;
1359
+ var is_abstract = false;
1360
+ var is_readonly = false;
1361
+ var is_async = false;
1362
+
1363
+ while (true) {
1364
+ s.skipWhitespaceAndComments();
1365
+ if (s.matchWord("private")) {
1366
+ is_private = true;
1367
+ s.pos += 7;
1368
+ } else if (s.matchWord("protected")) {
1369
+ is_protected = true;
1370
+ s.pos += 9;
1371
+ } else if (s.matchWord("public")) {
1372
+ s.pos += 6;
1373
+ } else if (s.matchWord("static")) {
1374
+ is_static = true;
1375
+ s.pos += 6;
1376
+ } else if (s.matchWord("abstract")) {
1377
+ is_abstract = true;
1378
+ s.pos += 8;
1379
+ } else if (s.matchWord("readonly")) {
1380
+ is_readonly = true;
1381
+ s.pos += 8;
1382
+ } else if (s.matchWord("override")) {
1383
+ s.pos += 8;
1384
+ } else if (s.matchWord("accessor")) {
1385
+ s.pos += 8;
1386
+ } else if (s.matchWord("async")) {
1387
+ is_async = true;
1388
+ s.pos += 5;
1389
+ } else if (s.matchWord("declare")) {
1390
+ s.pos += 7;
1391
+ } else break;
1392
+ }
1393
+
1394
+ s.skipWhitespaceAndComments();
1395
+ if (s.pos >= s.len or s.source[s.pos] == ch.CH_RBRACE) break;
1396
+
1397
+ // Skip private # members
1398
+ if (s.source[s.pos] == ch.CH_HASH) is_private = true;
1399
+
1400
+ if (is_private) {
1401
+ s.skipClassMember();
1402
+ continue;
1403
+ }
1404
+
1405
+ // Build modifier prefix
1406
+ var mod_prefix = std.array_list.Managed(u8).init(s.allocator);
1407
+ mod_prefix.appendSlice(" ") catch {};
1408
+ if (is_protected) mod_prefix.appendSlice("protected ") catch {};
1409
+ if (is_static) mod_prefix.appendSlice("static ") catch {};
1410
+ if (is_abstract) mod_prefix.appendSlice("abstract ") catch {};
1411
+ if (is_readonly) mod_prefix.appendSlice("readonly ") catch {};
1412
+ const prefix = mod_prefix.toOwnedSlice() catch " ";
1413
+
1414
+ // Detect member type
1415
+ if (s.matchWord("constructor")) {
1416
+ s.pos += 11;
1417
+ s.skipWhitespaceAndComments();
1418
+ const raw_params = extractParamList(s);
1419
+ s.skipWhitespaceAndComments();
1420
+
1421
+ // Extract parameter properties
1422
+ extractParamProperties(s, raw_params, &members);
1423
+
1424
+ // Skip constructor body
1425
+ if (s.pos < s.len and s.source[s.pos] == ch.CH_LBRACE) {
1426
+ _ = s.findMatchingClose(ch.CH_LBRACE, ch.CH_RBRACE);
1427
+ } else if (s.pos < s.len and s.source[s.pos] == ch.CH_SEMI) {
1428
+ s.pos += 1;
1429
+ }
1430
+
1431
+ const dts_params = buildDtsParams(s, raw_params);
1432
+ var member = std.array_list.Managed(u8).init(s.allocator);
1433
+ member.appendSlice(" constructor") catch {};
1434
+ member.appendSlice(dts_params) catch {};
1435
+ member.append(';') catch {};
1436
+ members.append(member.toOwnedSlice() catch "") catch {};
1437
+ } else if (s.matchWord("get") and isAccessorFollowed(s)) {
1438
+ s.pos += 3;
1439
+ s.skipWhitespaceAndComments();
1440
+ const member_name = s.readMemberName();
1441
+ if (ch.startsWith(member_name, "#")) {
1442
+ skipAccessorBody(s);
1443
+ continue;
1444
+ }
1445
+ s.skipWhitespaceAndComments();
1446
+ _ = extractParamList(s);
1447
+ s.skipWhitespaceAndComments();
1448
+ const ret_type_raw = extractReturnType(s);
1449
+ const ret_type = if (ret_type_raw.len > 0) ret_type_raw else "unknown";
1450
+ s.skipWhitespaceAndComments();
1451
+ if (s.pos < s.len and s.source[s.pos] == ch.CH_LBRACE) {
1452
+ _ = s.findMatchingClose(ch.CH_LBRACE, ch.CH_RBRACE);
1453
+ } else if (s.pos < s.len and s.source[s.pos] == ch.CH_SEMI) {
1454
+ s.pos += 1;
1455
+ }
1456
+ var member = std.array_list.Managed(u8).init(s.allocator);
1457
+ member.appendSlice(prefix) catch {};
1458
+ member.appendSlice("get ") catch {};
1459
+ member.appendSlice(member_name) catch {};
1460
+ member.appendSlice("(): ") catch {};
1461
+ member.appendSlice(ret_type) catch {};
1462
+ member.append(';') catch {};
1463
+ members.append(member.toOwnedSlice() catch "") catch {};
1464
+ } else if (s.matchWord("set") and isAccessorFollowed(s)) {
1465
+ s.pos += 3;
1466
+ s.skipWhitespaceAndComments();
1467
+ const member_name = s.readMemberName();
1468
+ if (ch.startsWith(member_name, "#")) {
1469
+ skipAccessorBody(s);
1470
+ continue;
1471
+ }
1472
+ s.skipWhitespaceAndComments();
1473
+ const raw_params = extractParamList(s);
1474
+ s.skipWhitespaceAndComments();
1475
+ if (s.pos < s.len and s.source[s.pos] == ch.CH_LBRACE) {
1476
+ _ = s.findMatchingClose(ch.CH_LBRACE, ch.CH_RBRACE);
1477
+ } else if (s.pos < s.len and s.source[s.pos] == ch.CH_SEMI) {
1478
+ s.pos += 1;
1479
+ }
1480
+ const dts_params = buildDtsParams(s, raw_params);
1481
+ var member = std.array_list.Managed(u8).init(s.allocator);
1482
+ member.appendSlice(prefix) catch {};
1483
+ member.appendSlice("set ") catch {};
1484
+ member.appendSlice(member_name) catch {};
1485
+ member.appendSlice(dts_params) catch {};
1486
+ member.append(';') catch {};
1487
+ members.append(member.toOwnedSlice() catch "") catch {};
1488
+ } else {
1489
+ // Regular method or property
1490
+ const is_generator = s.source[s.pos] == ch.CH_STAR;
1491
+ if (is_generator) {
1492
+ s.pos += 1;
1493
+ s.skipWhitespaceAndComments();
1494
+ }
1495
+ const member_name = s.readMemberName();
1496
+ if (member_name.len == 0) {
1497
+ s.skipClassMember();
1498
+ continue;
1499
+ }
1500
+ handleMethodOrPropertyAfterName(s, member_name, prefix, is_static, is_readonly, is_generator, is_abstract, is_async, &members);
1501
+ }
1502
+ }
1503
+
1504
+ if (members.items.len == 0) return "{}";
1505
+
1506
+ // Join members — pre-calculate total length
1507
+ var total_len: usize = 4; // "{\n" + "\n}"
1508
+ for (members.items) |m| total_len += m.len + 1;
1509
+ var result = std.array_list.Managed(u8).init(s.allocator);
1510
+ result.ensureTotalCapacity(total_len) catch {};
1511
+ result.appendSlice("{\n") catch {};
1512
+ for (members.items, 0..) |m, i| {
1513
+ result.appendSlice(m) catch {};
1514
+ if (i < members.items.len - 1) result.append('\n') catch {};
1515
+ }
1516
+ result.appendSlice("\n}") catch {};
1517
+ return result.toOwnedSlice() catch "{}";
1518
+ }
1519
+
1520
+ fn isAccessorFollowed(s: *const Scanner) bool {
1521
+ const next_after = s.peekAfterWord(if (s.matchWord("get")) "get" else "set");
1522
+ return next_after != 0 and (ch.isIdentStart(next_after) or next_after == ch.CH_LBRACKET or next_after == ch.CH_HASH);
1523
+ }
1524
+
1525
+ fn skipAccessorBody(s: *Scanner) void {
1526
+ s.skipWhitespaceAndComments();
1527
+ if (s.pos < s.len and s.source[s.pos] == ch.CH_LPAREN) _ = extractParamList(s);
1528
+ s.skipWhitespaceAndComments();
1529
+ _ = extractReturnType(s);
1530
+ s.skipWhitespaceAndComments();
1531
+ if (s.pos < s.len and s.source[s.pos] == ch.CH_LBRACE) {
1532
+ _ = s.findMatchingClose(ch.CH_LBRACE, ch.CH_RBRACE);
1533
+ } else if (s.pos < s.len and s.source[s.pos] == ch.CH_SEMI) {
1534
+ s.pos += 1;
1535
+ }
1536
+ }
1537
+
1538
+ /// Handle method or property after reading member name
1539
+ fn handleMethodOrPropertyAfterName(s: *Scanner, member_name: []const u8, mod_prefix: []const u8, is_static: bool, is_readonly: bool, is_generator: bool, is_abstract: bool, is_async: bool, members: *std.array_list.Managed([]const u8)) void {
1540
+ _ = is_abstract;
1541
+ s.skipWhitespaceAndComments();
1542
+ if (s.pos >= s.len) return;
1543
+
1544
+ var c = s.source[s.pos];
1545
+ var is_optional = false;
1546
+ if (c == ch.CH_QUESTION) {
1547
+ is_optional = true;
1548
+ s.pos += 1;
1549
+ s.skipWhitespaceAndComments();
1550
+ }
1551
+ // Definite assignment !
1552
+ if (s.pos < s.len and s.source[s.pos] == ch.CH_EXCL) {
1553
+ s.pos += 1;
1554
+ s.skipWhitespaceAndComments();
1555
+ }
1556
+
1557
+ const next_ch: u8 = if (s.pos < s.len) s.source[s.pos] else 0;
1558
+
1559
+ if (next_ch == ch.CH_LPAREN or next_ch == ch.CH_LANGLE) {
1560
+ // Method
1561
+ const generics = if (next_ch == ch.CH_LANGLE) extractGenerics(s) else "";
1562
+ s.skipWhitespaceAndComments();
1563
+ const raw_params = extractParamList(s);
1564
+ s.skipWhitespaceAndComments();
1565
+ var ret_type = extractReturnType(s);
1566
+ if (ret_type.len == 0) {
1567
+ if (is_async and is_generator) {
1568
+ ret_type = "AsyncGenerator<unknown, void, unknown>";
1569
+ } else if (is_generator) {
1570
+ ret_type = "Generator<unknown, void, unknown>";
1571
+ } else if (is_async) {
1572
+ ret_type = "Promise<void>";
1573
+ } else {
1574
+ ret_type = "void";
1575
+ }
1576
+ }
1577
+ s.skipWhitespaceAndComments();
1578
+ if (s.pos < s.len and s.source[s.pos] == ch.CH_LBRACE) {
1579
+ _ = s.findMatchingClose(ch.CH_LBRACE, ch.CH_RBRACE);
1580
+ } else if (s.pos < s.len and s.source[s.pos] == ch.CH_SEMI) {
1581
+ s.pos += 1;
1582
+ }
1583
+ const dts_params = buildDtsParams(s, raw_params);
1584
+ const opt_mark: []const u8 = if (is_optional) "?" else "";
1585
+ const gen_text: []const u8 = if (is_generator) "*" else "";
1586
+
1587
+ var member = std.array_list.Managed(u8).init(s.allocator);
1588
+ member.appendSlice(mod_prefix) catch {};
1589
+ member.appendSlice(gen_text) catch {};
1590
+ member.appendSlice(member_name) catch {};
1591
+ member.appendSlice(opt_mark) catch {};
1592
+ member.appendSlice(generics) catch {};
1593
+ member.appendSlice(dts_params) catch {};
1594
+ member.appendSlice(": ") catch {};
1595
+ member.appendSlice(ret_type) catch {};
1596
+ member.append(';') catch {};
1597
+ members.append(member.toOwnedSlice() catch "") catch {};
1598
+ } else if (next_ch == ch.CH_COLON or next_ch == ch.CH_EQUAL or next_ch == ch.CH_SEMI or next_ch == ch.CH_RBRACE or next_ch == ch.CH_LF or next_ch == ch.CH_CR) {
1599
+ // Property
1600
+ var prop_type: []const u8 = "";
1601
+ if (next_ch == ch.CH_COLON) {
1602
+ s.pos += 1;
1603
+ s.skipWhitespaceAndComments();
1604
+ const type_start = s.pos;
1605
+ var depth: isize = 0;
1606
+ while (s.pos < s.len) {
1607
+ if (s.skipNonCode()) continue;
1608
+ c = s.source[s.pos];
1609
+ if (c == ch.CH_LPAREN or c == ch.CH_LBRACE or c == ch.CH_LBRACKET or c == ch.CH_LANGLE) {
1610
+ depth += 1;
1611
+ } else if (c == ch.CH_RPAREN or c == ch.CH_RBRACE or c == ch.CH_RBRACKET or (c == ch.CH_RANGLE and !s.isArrowGT())) {
1612
+ if (depth == 0) break;
1613
+ depth -= 1;
1614
+ } else if (depth == 0 and (c == ch.CH_SEMI or c == ch.CH_EQUAL or c == ch.CH_COMMA)) {
1615
+ break;
1616
+ }
1617
+ if (depth == 0 and s.checkASIMember()) break;
1618
+ s.pos += 1;
1619
+ }
1620
+ prop_type = s.sliceTrimmed(type_start, s.pos);
1621
+ }
1622
+
1623
+ // Capture initializer
1624
+ var init_text: []const u8 = "";
1625
+ if (s.pos < s.len and s.source[s.pos] == ch.CH_EQUAL) {
1626
+ s.pos += 1;
1627
+ s.skipWhitespaceAndComments();
1628
+ const init_start = s.pos;
1629
+ var depth: isize = 0;
1630
+ while (s.pos < s.len) {
1631
+ if (s.skipNonCode()) continue;
1632
+ const ic = s.source[s.pos];
1633
+ if (ic == ch.CH_LPAREN or ic == ch.CH_LBRACE or ic == ch.CH_LBRACKET) {
1634
+ depth += 1;
1635
+ } else if (ic == ch.CH_RPAREN or ic == ch.CH_RBRACE or ic == ch.CH_RBRACKET) {
1636
+ if (depth == 0 and ic == ch.CH_RBRACE) break;
1637
+ depth -= 1;
1638
+ } else if (depth == 0 and ic == ch.CH_SEMI) {
1639
+ break;
1640
+ }
1641
+ if (depth == 0 and s.checkASIMember()) break;
1642
+ s.pos += 1;
1643
+ }
1644
+ init_text = s.sliceTrimmed(init_start, s.pos);
1645
+ }
1646
+
1647
+ if (s.pos < s.len and s.source[s.pos] == ch.CH_SEMI) s.pos += 1;
1648
+
1649
+ if (prop_type.len == 0) {
1650
+ if (init_text.len > 0) {
1651
+ const as_type = extractAssertion(init_text);
1652
+ if (as_type) |t| {
1653
+ prop_type = t;
1654
+ } else {
1655
+ const is_const_like = is_static and is_readonly;
1656
+ prop_type = if (is_const_like) inferLiteralType(init_text) else inferTypeFromDefault(init_text);
1657
+ }
1658
+ } else {
1659
+ prop_type = "unknown";
1660
+ }
1661
+ }
1662
+
1663
+ const opt_mark: []const u8 = if (is_optional) "?" else "";
1664
+ var member = std.array_list.Managed(u8).init(s.allocator);
1665
+ member.appendSlice(mod_prefix) catch {};
1666
+ member.appendSlice(member_name) catch {};
1667
+ member.appendSlice(opt_mark) catch {};
1668
+ member.appendSlice(": ") catch {};
1669
+ member.appendSlice(prop_type) catch {};
1670
+ member.append(';') catch {};
1671
+ members.append(member.toOwnedSlice() catch "") catch {};
1672
+ } else {
1673
+ s.skipClassMember();
1674
+ }
1675
+ }
1676
+
1677
+ /// Extract parameter properties from constructor params
1678
+ fn extractParamProperties(s: *Scanner, raw_params: []const u8, members: *std.array_list.Managed([]const u8)) void {
1679
+ if (raw_params.len < 2) return;
1680
+ const inner = std.mem.trim(u8, raw_params[1 .. raw_params.len - 1], " \t\r\n");
1681
+ if (inner.len == 0) return;
1682
+
1683
+ // Split by comma at depth 0
1684
+ var params = std.array_list.Managed([]const u8).init(s.allocator);
1685
+ var start: usize = 0;
1686
+ var depth: isize = 0;
1687
+ var in_str = false;
1688
+ var str_ch_val: u8 = 0;
1689
+ var skip_next3 = false;
1690
+ for (inner, 0..) |c, i| {
1691
+ if (skip_next3) {
1692
+ skip_next3 = false;
1693
+ continue;
1694
+ }
1695
+ if (in_str) {
1696
+ if (c == ch.CH_BACKSLASH) {
1697
+ skip_next3 = true;
1698
+ continue;
1699
+ }
1700
+ if (c == str_ch_val) in_str = false;
1701
+ continue;
1702
+ }
1703
+ if (c == ch.CH_SQUOTE or c == ch.CH_DQUOTE or c == ch.CH_BACKTICK) {
1704
+ in_str = true;
1705
+ str_ch_val = c;
1706
+ continue;
1707
+ }
1708
+ if (c == ch.CH_LPAREN or c == ch.CH_LBRACE or c == ch.CH_LBRACKET or c == ch.CH_LANGLE) {
1709
+ depth += 1;
1710
+ } else if (c == ch.CH_RPAREN or c == ch.CH_RBRACE or c == ch.CH_RBRACKET or c == ch.CH_RANGLE) {
1711
+ depth -= 1;
1712
+ } else if (c == ch.CH_COMMA and depth == 0) {
1713
+ params.append(std.mem.trim(u8, inner[start..i], " \t\r\n")) catch {};
1714
+ start = i + 1;
1715
+ }
1716
+ }
1717
+ params.append(std.mem.trim(u8, inner[start..], " \t\r\n")) catch {};
1718
+
1719
+ for (params.items) |param| {
1720
+ const has_public = ch.startsWith(param, "public ") or ch.startsWith(param, "public\t");
1721
+ const has_protected = ch.startsWith(param, "protected ") or ch.startsWith(param, "protected\t");
1722
+ const has_private = ch.startsWith(param, "private ") or ch.startsWith(param, "private\t");
1723
+ const has_readonly = ch.contains(param, "readonly ");
1724
+
1725
+ if (!has_public and !has_protected and !has_private and !has_readonly) continue;
1726
+ if (has_private) continue;
1727
+
1728
+ var p = param;
1729
+ var mods = std.array_list.Managed([]const u8).init(s.allocator);
1730
+ if (has_public) {
1731
+ var si: usize = 6;
1732
+ while (si < p.len and ch.isWhitespace(p[si])) si += 1;
1733
+ p = p[si..];
1734
+ mods.append("public") catch {};
1735
+ }
1736
+ if (has_protected) {
1737
+ var si: usize = 9;
1738
+ while (si < p.len and ch.isWhitespace(p[si])) si += 1;
1739
+ p = p[si..];
1740
+ mods.append("protected") catch {};
1741
+ }
1742
+ if (has_readonly) {
1743
+ if (ch.indexOf(p, "readonly ", 0)) |ri| {
1744
+ var si = ri + 8;
1745
+ while (si < p.len and ch.isWhitespace(p[si])) si += 1;
1746
+ // Reconstruct without 'readonly '
1747
+ const before = p[0..ri];
1748
+ const after = p[si..];
1749
+ const new_p = s.allocator.alloc(u8, before.len + after.len) catch continue;
1750
+ @memcpy(new_p[0..before.len], before);
1751
+ @memcpy(new_p[before.len..], after);
1752
+ p = new_p;
1753
+ }
1754
+ mods.append("readonly") catch {};
1755
+ }
1756
+
1757
+ // Build mod text
1758
+ var mod_text = std.array_list.Managed(u8).init(s.allocator);
1759
+ for (mods.items, 0..) |m, i| {
1760
+ mod_text.appendSlice(m) catch {};
1761
+ if (i < mods.items.len - 1) mod_text.append(' ') catch {};
1762
+ }
1763
+ if (mods.items.len > 0) mod_text.append(' ') catch {};
1764
+
1765
+ const dts_param = buildSingleDtsParam(s, p);
1766
+ var member = std.array_list.Managed(u8).init(s.allocator);
1767
+ member.appendSlice(" ") catch {};
1768
+ member.appendSlice(mod_text.toOwnedSlice() catch "") catch {};
1769
+ member.appendSlice(dts_param) catch {};
1770
+ member.append(';') catch {};
1771
+ members.append(member.toOwnedSlice() catch "") catch {};
1772
+ }
1773
+ }
1774
+
1775
+ // ========================================================================
1776
+ // Module/Namespace extraction
1777
+ // ========================================================================
1778
+
1779
+ /// Extract module/namespace declaration
1780
+ pub fn extractModule(s: *Scanner, decl_start: usize, is_exported: bool, keyword: []const u8) Declaration {
1781
+ s.pos += keyword.len;
1782
+ s.skipWhitespaceAndComments();
1783
+
1784
+ var name: []const u8 = "";
1785
+ if (s.pos >= s.len) return .{
1786
+ .kind = .module_decl,
1787
+ .name = name,
1788
+ .text = "",
1789
+ .is_exported = is_exported,
1790
+ .start = decl_start,
1791
+ .end = s.pos,
1792
+ };
1793
+ const c = s.source[s.pos];
1794
+ if (c == ch.CH_SQUOTE or c == ch.CH_DQUOTE) {
1795
+ const quote_start = s.pos;
1796
+ s.skipString(c);
1797
+ name = s.source[quote_start..s.pos];
1798
+ } else {
1799
+ name = s.readIdent();
1800
+ while (s.pos < s.len and s.source[s.pos] == ch.CH_DOT) {
1801
+ s.pos += 1;
1802
+ const next_ident = s.readIdent();
1803
+ const new_name = s.allocator.alloc(u8, name.len + 1 + next_ident.len) catch break;
1804
+ @memcpy(new_name[0..name.len], name);
1805
+ new_name[name.len] = '.';
1806
+ @memcpy(new_name[name.len + 1 ..], next_ident);
1807
+ name = new_name;
1808
+ }
1809
+ }
1810
+
1811
+ s.skipWhitespaceAndComments();
1812
+ const body = if (s.pos < s.len and s.source[s.pos] == ch.CH_LBRACE)
1813
+ buildNamespaceBodyDts(s, " ")
1814
+ else
1815
+ "{}";
1816
+
1817
+ var text = std.array_list.Managed(u8).init(s.allocator);
1818
+ text.ensureTotalCapacity(128) catch {};
1819
+ if (is_exported) text.appendSlice("export ") catch {};
1820
+ text.appendSlice("declare ") catch {};
1821
+ text.appendSlice(keyword) catch {};
1822
+ text.append(' ') catch {};
1823
+ text.appendSlice(name) catch {};
1824
+ text.append(' ') catch {};
1825
+ text.appendSlice(body) catch {};
1826
+
1827
+ const comments = extractLeadingComments(s, decl_start);
1828
+ const is_ambient = name.len > 0 and (name[0] == '\'' or name[0] == '"');
1829
+
1830
+ return .{
1831
+ .kind = .module_decl,
1832
+ .name = name,
1833
+ .text = text.toOwnedSlice() catch "",
1834
+ .is_exported = is_exported,
1835
+ .source_module = if (is_ambient and name.len > 2) name[1 .. name.len - 1] else "",
1836
+ .leading_comments = comments,
1837
+ .start = decl_start,
1838
+ .end = s.pos,
1839
+ };
1840
+ }
1841
+
1842
+ /// Build DTS text for namespace/module body by processing inner declarations
1843
+ pub fn buildNamespaceBodyDts(s: *Scanner, indent: []const u8) []const u8 {
1844
+ if (s.pos >= s.len or s.source[s.pos] != ch.CH_LBRACE) return "{}";
1845
+ s.pos += 1; // skip {
1846
+
1847
+ var lines = std.array_list.Managed([]const u8).init(s.allocator);
1848
+ lines.ensureTotalCapacity(16) catch {};
1849
+
1850
+ while (s.pos < s.len) {
1851
+ s.skipWhitespaceAndComments();
1852
+ if (s.pos >= s.len) break;
1853
+ if (s.source[s.pos] == ch.CH_RBRACE) {
1854
+ s.pos += 1;
1855
+ break;
1856
+ }
1857
+ if (s.source[s.pos] == ch.CH_SEMI) {
1858
+ s.pos += 1;
1859
+ continue;
1860
+ }
1861
+
1862
+ var has_export = false;
1863
+ if (s.matchWord("export")) {
1864
+ has_export = true;
1865
+ s.pos += 6;
1866
+ s.skipWhitespaceAndComments();
1867
+ }
1868
+ if (s.matchWord("declare")) {
1869
+ s.pos += 7;
1870
+ s.skipWhitespaceAndComments();
1871
+ }
1872
+
1873
+ const prefix: []const u8 = if (has_export) "export " else "";
1874
+
1875
+ if (s.matchWord("function") or (s.matchWord("async") and s.peekAfterKeyword("async", "function"))) {
1876
+ var is_async = false;
1877
+ if (s.matchWord("async")) {
1878
+ is_async = true;
1879
+ s.pos += 5;
1880
+ s.skipWhitespaceAndComments();
1881
+ }
1882
+ s.pos += 8; // function
1883
+ s.skipWhitespaceAndComments();
1884
+ const is_gen = s.pos < s.len and s.source[s.pos] == ch.CH_STAR;
1885
+ if (is_gen) {
1886
+ s.pos += 1;
1887
+ s.skipWhitespaceAndComments();
1888
+ }
1889
+ const fname = s.readIdent();
1890
+ s.skipWhitespaceAndComments();
1891
+ const generics = extractGenerics(s);
1892
+ s.skipWhitespaceAndComments();
1893
+ const raw_params = extractParamList(s);
1894
+ s.skipWhitespaceAndComments();
1895
+ var ret_type = extractReturnType(s);
1896
+ if (ret_type.len == 0) ret_type = if (is_async) "Promise<void>" else "void";
1897
+ s.skipWhitespaceAndComments();
1898
+ if (s.pos < s.len and s.source[s.pos] == ch.CH_LBRACE) {
1899
+ _ = s.findMatchingClose(ch.CH_LBRACE, ch.CH_RBRACE);
1900
+ } else if (s.pos < s.len and s.source[s.pos] == ch.CH_SEMI) {
1901
+ s.pos += 1;
1902
+ }
1903
+ const dts_params = buildDtsParams(s, raw_params);
1904
+ var line = std.array_list.Managed(u8).init(s.allocator);
1905
+ line.appendSlice(indent) catch {};
1906
+ line.appendSlice(prefix) catch {};
1907
+ line.appendSlice("function ") catch {};
1908
+ line.appendSlice(fname) catch {};
1909
+ line.appendSlice(generics) catch {};
1910
+ line.appendSlice(dts_params) catch {};
1911
+ line.appendSlice(": ") catch {};
1912
+ line.appendSlice(ret_type) catch {};
1913
+ line.append(';') catch {};
1914
+ lines.append(line.toOwnedSlice() catch "") catch {};
1915
+ } else if (s.matchWord("const") or s.matchWord("let") or s.matchWord("var")) {
1916
+ const kw: []const u8 = if (s.matchWord("const")) "const" else if (s.matchWord("let")) "let" else "var";
1917
+ s.pos += kw.len;
1918
+ s.skipWhitespaceAndComments();
1919
+
1920
+ // Check for const enum
1921
+ if (std.mem.eql(u8, kw, "const") and s.matchWord("enum")) {
1922
+ s.pos += 4;
1923
+ s.skipWhitespaceAndComments();
1924
+ const ce_name = s.readIdent();
1925
+ s.skipWhitespaceAndComments();
1926
+ const ce_body = if (s.pos < s.len and s.source[s.pos] == ch.CH_LBRACE) s.extractBraceBlock() else "{}";
1927
+ var line = std.array_list.Managed(u8).init(s.allocator);
1928
+ line.appendSlice(indent) catch {};
1929
+ line.appendSlice(prefix) catch {};
1930
+ line.appendSlice("const enum ") catch {};
1931
+ line.appendSlice(ce_name) catch {};
1932
+ line.append(' ') catch {};
1933
+ line.appendSlice(ce_body) catch {};
1934
+ lines.append(line.toOwnedSlice() catch "") catch {};
1935
+ continue;
1936
+ }
1937
+
1938
+ const vname = s.readIdent();
1939
+ if (vname.len == 0) {
1940
+ s.skipToStatementEnd();
1941
+ continue;
1942
+ }
1943
+ s.skipWhitespaceAndComments();
1944
+ var vtype: []const u8 = "";
1945
+ if (s.pos < s.len and s.source[s.pos] == ch.CH_COLON) {
1946
+ s.pos += 1;
1947
+ s.skipWhitespaceAndComments();
1948
+ const ts = s.pos;
1949
+ var depth: isize = 0;
1950
+ while (s.pos < s.len) {
1951
+ if (s.skipNonCode()) continue;
1952
+ const tc = s.source[s.pos];
1953
+ if (tc == ch.CH_LPAREN or tc == ch.CH_LBRACE or tc == ch.CH_LBRACKET or tc == ch.CH_LANGLE) {
1954
+ depth += 1;
1955
+ } else if (tc == ch.CH_RPAREN or tc == ch.CH_RBRACE or tc == ch.CH_RBRACKET or (tc == ch.CH_RANGLE and !s.isArrowGT())) {
1956
+ depth -= 1;
1957
+ } else if (depth == 0 and (tc == ch.CH_EQUAL or tc == ch.CH_SEMI or tc == ch.CH_COMMA)) {
1958
+ break;
1959
+ }
1960
+ if (depth == 0 and s.checkASITopLevel()) break;
1961
+ s.pos += 1;
1962
+ }
1963
+ vtype = s.sliceTrimmed(ts, s.pos);
1964
+ }
1965
+ var init_text: []const u8 = "";
1966
+ if (s.pos < s.len and s.source[s.pos] == ch.CH_EQUAL) {
1967
+ s.pos += 1;
1968
+ s.skipWhitespaceAndComments();
1969
+ const is2 = s.pos;
1970
+ var depth: isize = 0;
1971
+ while (s.pos < s.len) {
1972
+ if (s.skipNonCode()) continue;
1973
+ const ic = s.source[s.pos];
1974
+ if (ic == ch.CH_LPAREN or ic == ch.CH_LBRACE or ic == ch.CH_LBRACKET or ic == ch.CH_LANGLE) {
1975
+ depth += 1;
1976
+ } else if (ic == ch.CH_RPAREN or ic == ch.CH_RBRACE or ic == ch.CH_RBRACKET or (ic == ch.CH_RANGLE and !s.isArrowGT())) {
1977
+ depth -= 1;
1978
+ } else if (depth == 0 and (ic == ch.CH_SEMI or ic == ch.CH_COMMA)) {
1979
+ break;
1980
+ }
1981
+ if (depth == 0 and s.checkASITopLevel()) break;
1982
+ s.pos += 1;
1983
+ }
1984
+ init_text = s.sliceTrimmed(is2, s.pos);
1985
+ }
1986
+ if (s.pos < s.len and s.source[s.pos] == ch.CH_SEMI) s.pos += 1;
1987
+
1988
+ if (vtype.len == 0 and init_text.len > 0) {
1989
+ const as_type = extractAssertion(init_text);
1990
+ if (as_type) |t| {
1991
+ vtype = t;
1992
+ } else if (std.mem.eql(u8, kw, "const")) {
1993
+ vtype = inferLiteralType(init_text);
1994
+ } else {
1995
+ vtype = inferTypeFromDefault(init_text);
1996
+ }
1997
+ }
1998
+ if (vtype.len == 0) vtype = "unknown";
1999
+
2000
+ var line = std.array_list.Managed(u8).init(s.allocator);
2001
+ line.appendSlice(indent) catch {};
2002
+ line.appendSlice(prefix) catch {};
2003
+ line.appendSlice(kw) catch {};
2004
+ line.append(' ') catch {};
2005
+ line.appendSlice(vname) catch {};
2006
+ line.appendSlice(": ") catch {};
2007
+ line.appendSlice(vtype) catch {};
2008
+ line.append(';') catch {};
2009
+ lines.append(line.toOwnedSlice() catch "") catch {};
2010
+ } else if (s.matchWord("interface")) {
2011
+ s.pos += 9;
2012
+ s.skipWhitespaceAndComments();
2013
+ const iname = s.readIdent();
2014
+ s.skipWhitespaceAndComments();
2015
+ const generics = extractGenerics(s);
2016
+ s.skipWhitespaceAndComments();
2017
+ var ext: []const u8 = "";
2018
+ if (s.matchWord("extends")) {
2019
+ const ext_start = s.pos;
2020
+ while (s.pos < s.len and s.source[s.pos] != ch.CH_LBRACE) {
2021
+ if (s.skipNonCode()) continue;
2022
+ s.pos += 1;
2023
+ }
2024
+ ext = s.source[ext_start..s.pos];
2025
+ }
2026
+ const body = cleanBraceBlock(s, s.extractBraceBlock());
2027
+ var line = std.array_list.Managed(u8).init(s.allocator);
2028
+ line.appendSlice(indent) catch {};
2029
+ line.appendSlice(prefix) catch {};
2030
+ line.appendSlice("interface ") catch {};
2031
+ line.appendSlice(iname) catch {};
2032
+ line.appendSlice(generics) catch {};
2033
+ line.appendSlice(ext) catch {};
2034
+ line.append(' ') catch {};
2035
+ line.appendSlice(body) catch {};
2036
+ lines.append(line.toOwnedSlice() catch "") catch {};
2037
+ } else if (s.matchWord("type")) {
2038
+ s.pos += 4;
2039
+ s.skipWhitespaceAndComments();
2040
+ const tname = s.readIdent();
2041
+ s.skipWhitespaceAndComments();
2042
+ const generics = extractGenerics(s);
2043
+ s.skipWhitespaceAndComments();
2044
+ if (s.pos < s.len and s.source[s.pos] == ch.CH_EQUAL) {
2045
+ s.pos += 1;
2046
+ s.skipWhitespaceAndComments();
2047
+ const ts = s.pos;
2048
+ var depth: isize = 0;
2049
+ while (s.pos < s.len) {
2050
+ if (s.skipNonCode()) continue;
2051
+ const tc = s.source[s.pos];
2052
+ if (tc == ch.CH_LPAREN or tc == ch.CH_LBRACE or tc == ch.CH_LBRACKET or tc == ch.CH_LANGLE) {
2053
+ depth += 1;
2054
+ } else if (tc == ch.CH_RPAREN or tc == ch.CH_RBRACE or tc == ch.CH_RBRACKET or (tc == ch.CH_RANGLE and !s.isArrowGT())) {
2055
+ depth -= 1;
2056
+ } else if (depth == 0 and tc == ch.CH_SEMI) {
2057
+ break;
2058
+ }
2059
+ if (depth == 0 and s.checkASITopLevel()) break;
2060
+ s.pos += 1;
2061
+ }
2062
+ const type_body = s.sliceTrimmed(ts, s.pos);
2063
+ if (s.pos < s.len and s.source[s.pos] == ch.CH_SEMI) s.pos += 1;
2064
+ var line = std.array_list.Managed(u8).init(s.allocator);
2065
+ line.appendSlice(indent) catch {};
2066
+ line.appendSlice(prefix) catch {};
2067
+ line.appendSlice("type ") catch {};
2068
+ line.appendSlice(tname) catch {};
2069
+ line.appendSlice(generics) catch {};
2070
+ line.appendSlice(" = ") catch {};
2071
+ line.appendSlice(type_body) catch {};
2072
+ lines.append(line.toOwnedSlice() catch "") catch {};
2073
+ }
2074
+ } else if (s.matchWord("enum")) {
2075
+ // Handle enum inside namespace
2076
+ s.pos += 4; // enum
2077
+ s.skipWhitespaceAndComments();
2078
+ const ename = s.readIdent();
2079
+ s.skipWhitespaceAndComments();
2080
+ const ebody = if (s.pos < s.len and s.source[s.pos] == ch.CH_LBRACE) s.extractBraceBlock() else "{}";
2081
+ var line = std.array_list.Managed(u8).init(s.allocator);
2082
+ line.appendSlice(indent) catch {};
2083
+ line.appendSlice(prefix) catch {};
2084
+ line.appendSlice("enum ") catch {};
2085
+ line.appendSlice(ename) catch {};
2086
+ line.append(' ') catch {};
2087
+ line.appendSlice(ebody) catch {};
2088
+ lines.append(line.toOwnedSlice() catch "") catch {};
2089
+ } else if (s.matchWord("namespace") or s.matchWord("module")) {
2090
+ // Handle nested namespace/module
2091
+ const ns_kw: []const u8 = if (s.matchWord("namespace")) "namespace" else "module";
2092
+ s.pos += ns_kw.len;
2093
+ s.skipWhitespaceAndComments();
2094
+ const ns_name = s.readIdent();
2095
+ s.skipWhitespaceAndComments();
2096
+ // Use same indent level for nested namespace body (matches TS behavior)
2097
+ const ns_body = if (s.pos < s.len and s.source[s.pos] == ch.CH_LBRACE) buildNamespaceBodyDts(s, indent) else "{}";
2098
+ var line = std.array_list.Managed(u8).init(s.allocator);
2099
+ line.appendSlice(indent) catch {};
2100
+ line.appendSlice(prefix) catch {};
2101
+ line.appendSlice(ns_kw) catch {};
2102
+ line.append(' ') catch {};
2103
+ line.appendSlice(ns_name) catch {};
2104
+ line.append(' ') catch {};
2105
+ line.appendSlice(ns_body) catch {};
2106
+ lines.append(line.toOwnedSlice() catch "") catch {};
2107
+ } else if (s.matchWord("class") or s.matchWord("abstract")) {
2108
+ // Handle class inside namespace
2109
+ var is_abs = false;
2110
+ if (s.matchWord("abstract")) {
2111
+ is_abs = true;
2112
+ s.pos += 8;
2113
+ s.skipWhitespaceAndComments();
2114
+ }
2115
+ if (s.matchWord("class")) {
2116
+ s.pos += 5;
2117
+ s.skipWhitespaceAndComments();
2118
+ const cname = s.readIdent();
2119
+ s.skipWhitespaceAndComments();
2120
+ const cgen = extractGenerics(s);
2121
+ s.skipWhitespaceAndComments();
2122
+ // Skip extends/implements
2123
+ while (s.matchWord("extends") or s.matchWord("implements")) {
2124
+ const kw_len: usize = if (s.matchWord("extends")) 7 else 10;
2125
+ s.pos += kw_len;
2126
+ s.skipWhitespaceAndComments();
2127
+ var depth: isize = 0;
2128
+ while (s.pos < s.len) {
2129
+ if (s.skipNonCode()) continue;
2130
+ const tc = s.source[s.pos];
2131
+ if (tc == ch.CH_LANGLE) depth += 1 else if (tc == ch.CH_RANGLE and !s.isArrowGT()) depth -= 1 else if (depth == 0 and (tc == ch.CH_LBRACE or s.matchWord("implements"))) break;
2132
+ s.pos += 1;
2133
+ }
2134
+ }
2135
+ s.skipWhitespaceAndComments();
2136
+ const cbody = if (s.pos < s.len and s.source[s.pos] == ch.CH_LBRACE) buildClassBodyDts(s) else "{}";
2137
+ var line = std.array_list.Managed(u8).init(s.allocator);
2138
+ line.appendSlice(indent) catch {};
2139
+ line.appendSlice(prefix) catch {};
2140
+ if (is_abs) line.appendSlice("abstract ") catch {};
2141
+ line.appendSlice("class ") catch {};
2142
+ line.appendSlice(cname) catch {};
2143
+ line.appendSlice(cgen) catch {};
2144
+ line.append(' ') catch {};
2145
+ line.appendSlice(cbody) catch {};
2146
+ lines.append(line.toOwnedSlice() catch "") catch {};
2147
+ } else {
2148
+ s.skipToStatementEnd();
2149
+ }
2150
+ } else if (has_export and s.matchWord("default")) {
2151
+ s.pos += 7;
2152
+ s.skipWhitespaceAndComments();
2153
+ const def_start = s.pos;
2154
+ s.skipToStatementEnd();
2155
+ var def_text = s.sliceTrimmed(def_start, s.pos);
2156
+ if (def_text.len > 0 and def_text[def_text.len - 1] == ';') def_text = def_text[0 .. def_text.len - 1];
2157
+ if (def_text.len > 0) {
2158
+ var line = std.array_list.Managed(u8).init(s.allocator);
2159
+ line.appendSlice(indent) catch {};
2160
+ line.appendSlice("export default ") catch {};
2161
+ line.appendSlice(def_text) catch {};
2162
+ line.append(';') catch {};
2163
+ lines.append(line.toOwnedSlice() catch "") catch {};
2164
+ }
2165
+ } else {
2166
+ s.skipToStatementEnd();
2167
+ }
2168
+ }
2169
+
2170
+ if (lines.items.len == 0) return "{}";
2171
+ var result = std.array_list.Managed(u8).init(s.allocator);
2172
+ result.appendSlice("{\n") catch {};
2173
+ for (lines.items, 0..) |line, i| {
2174
+ result.appendSlice(line) catch {};
2175
+ if (i < lines.items.len - 1) result.append('\n') catch {};
2176
+ }
2177
+ result.appendSlice("\n}") catch {};
2178
+ return result.toOwnedSlice() catch "{}";
2179
+ }
2180
+
2181
+ // ========================================================================
2182
+ // Clean brace block helper
2183
+ // ========================================================================
2184
+
2185
+ /// Strip trailing inline comments from a line (respecting strings)
2186
+ fn stripTrailingInlineComment(line: []const u8) []const u8 {
2187
+ var in_string: u8 = 0;
2188
+ var i: usize = 0;
2189
+ while (i < line.len) {
2190
+ const c = line[i];
2191
+ if (in_string != 0) {
2192
+ if (c == '\\') {
2193
+ i += 2;
2194
+ continue;
2195
+ }
2196
+ if (c == in_string) in_string = 0;
2197
+ i += 1;
2198
+ continue;
2199
+ }
2200
+ if (c == '\'' or c == '"' or c == '`') {
2201
+ in_string = c;
2202
+ i += 1;
2203
+ continue;
2204
+ }
2205
+ if (c == '/' and i + 1 < line.len and line[i + 1] == '/') {
2206
+ // Trim trailing whitespace before //
2207
+ var end = i;
2208
+ while (end > 0 and (line[end - 1] == ' ' or line[end - 1] == '\t')) end -= 1;
2209
+ return line[0..end];
2210
+ }
2211
+ i += 1;
2212
+ }
2213
+ // Trim trailing whitespace
2214
+ var end = line.len;
2215
+ while (end > 0 and (line[end - 1] == ' ' or line[end - 1] == '\t' or line[end - 1] == '\r')) end -= 1;
2216
+ return line[0..end];
2217
+ }
2218
+
2219
+ /// Strip inline comments from a brace block and normalize indentation
2220
+ pub fn cleanBraceBlock(s: *Scanner, raw: []const u8) []const u8 {
2221
+ if (raw.len < 2) return raw;
2222
+
2223
+ // Fast path: no comment markers, no semicolons, and ≤2-space indent → body is already clean.
2224
+ // This avoids line-by-line processing for simple interface/namespace bodies.
2225
+ if (ch.indexOfChar(raw, '/', 0) == null and ch.indexOfChar(raw, ';', 0) == null) {
2226
+ // Verify first member line has ≤ 2-space indentation (else re-indent is needed)
2227
+ var needs_reindent = false;
2228
+ if (ch.indexOfChar(raw, '\n', 0)) |nl| {
2229
+ var after = nl + 1;
2230
+ var indent: usize = 0;
2231
+ while (after < raw.len and (raw[after] == ' ' or raw[after] == '\t')) {
2232
+ indent += 1;
2233
+ after += 1;
2234
+ }
2235
+ if (after < raw.len and raw[after] != '\n' and raw[after] != '\r' and
2236
+ raw[after] != '}' and raw[after] != ']' and indent > 2)
2237
+ {
2238
+ needs_reindent = true;
2239
+ }
2240
+ }
2241
+ if (!needs_reindent) return raw;
2242
+ }
2243
+
2244
+ // Check if there are any comment markers
2245
+ const has_comments = ch.contains(raw, "//") or ch.contains(raw, "/*");
2246
+
2247
+ // Split raw text by newlines and process line by line
2248
+ const est_lines = @max(raw.len / 30, 4);
2249
+ var filtered = std.array_list.Managed([]const u8).init(s.allocator);
2250
+ filtered.ensureTotalCapacity(est_lines) catch {};
2251
+ var indent_cache = std.array_list.Managed(usize).init(s.allocator);
2252
+ indent_cache.ensureTotalCapacity(est_lines) catch {};
2253
+ var in_block_comment = false;
2254
+ var min_indent: usize = std.math.maxInt(usize);
2255
+
2256
+ var line_start: usize = 0;
2257
+ var li: usize = 0;
2258
+ while (li <= raw.len) : (li += 1) {
2259
+ if (li == raw.len or raw[li] == '\n') {
2260
+ const line = raw[line_start..li];
2261
+ line_start = li + 1;
2262
+
2263
+ if (has_comments) {
2264
+ if (in_block_comment) {
2265
+ if (ch.contains(line, "*/"))
2266
+ in_block_comment = false;
2267
+ continue;
2268
+ }
2269
+
2270
+ const trimmed = ch.sliceTrimmed(line, 0, line.len);
2271
+ if (trimmed.len == 0) continue;
2272
+
2273
+ // Skip standalone comment lines
2274
+ if (trimmed[0] == '/') {
2275
+ if (trimmed.len > 1 and trimmed[1] == '/') continue; // //
2276
+ if (trimmed.len > 1 and trimmed[1] == '*') { // /* or /**
2277
+ if (!ch.contains(trimmed, "*/"))
2278
+ in_block_comment = true;
2279
+ continue;
2280
+ }
2281
+ }
2282
+ if (trimmed[0] == '*') continue; // continuation of block comment
2283
+
2284
+ // Strip trailing inline comments and whitespace
2285
+ var cleaned_line = stripTrailingInlineComment(line);
2286
+ // Strip trailing semicolons (DTS convention for interfaces)
2287
+ if (cleaned_line.len > 0 and cleaned_line[cleaned_line.len - 1] == ';')
2288
+ cleaned_line = cleaned_line[0 .. cleaned_line.len - 1];
2289
+ const ct = ch.sliceTrimmed(cleaned_line, 0, cleaned_line.len);
2290
+ if (ct.len == 0) continue;
2291
+
2292
+ filtered.append(cleaned_line) catch {};
2293
+
2294
+ // Compute indent
2295
+ var iw: usize = 0;
2296
+ while (iw < cleaned_line.len and (cleaned_line[iw] == ' ' or cleaned_line[iw] == '\t')) iw += 1;
2297
+ if (!std.mem.eql(u8, ct, "{") and !std.mem.eql(u8, ct, "}")) {
2298
+ if (iw < min_indent) min_indent = iw;
2299
+ }
2300
+ indent_cache.append(iw) catch {};
2301
+ } else {
2302
+ // No comments — just trim trailing whitespace
2303
+ var end = line.len;
2304
+ while (end > 0 and (line[end - 1] == ' ' or line[end - 1] == '\t' or line[end - 1] == '\r')) end -= 1;
2305
+ if (end == 0) continue;
2306
+ var cleaned_line = line[0..end];
2307
+ // Strip trailing semicolons
2308
+ if (cleaned_line.len > 0 and cleaned_line[cleaned_line.len - 1] == ';')
2309
+ cleaned_line = cleaned_line[0 .. cleaned_line.len - 1];
2310
+ const ct = ch.sliceTrimmed(cleaned_line, 0, cleaned_line.len);
2311
+ if (ct.len == 0) continue;
2312
+
2313
+ filtered.append(cleaned_line) catch {};
2314
+
2315
+ var iw: usize = 0;
2316
+ while (iw < cleaned_line.len and (cleaned_line[iw] == ' ' or cleaned_line[iw] == '\t')) iw += 1;
2317
+ if (!std.mem.eql(u8, ct, "{") and !std.mem.eql(u8, ct, "}")) {
2318
+ if (iw < min_indent) min_indent = iw;
2319
+ }
2320
+ indent_cache.append(iw) catch {};
2321
+ }
2322
+ }
2323
+ }
2324
+
2325
+ if (filtered.items.len == 0) return "{}";
2326
+
2327
+ // Direct alloc: output ≤ raw.len (we're only removing/shifting content)
2328
+ const buf = s.allocator.alloc(u8, raw.len) catch return raw;
2329
+ var pos: usize = 0;
2330
+
2331
+ // If minIndent <= 2, return filtered lines as-is
2332
+ if (min_indent == std.math.maxInt(usize) or min_indent <= 2) {
2333
+ for (filtered.items, 0..) |line, i| {
2334
+ if (i > 0) {
2335
+ buf[pos] = '\n';
2336
+ pos += 1;
2337
+ }
2338
+ @memcpy(buf[pos..][0..line.len], line);
2339
+ pos += line.len;
2340
+ }
2341
+ return buf[0..pos];
2342
+ }
2343
+
2344
+ // Re-indent: offset = minIndent - 2
2345
+ const offs = min_indent - 2;
2346
+ for (filtered.items, 0..) |line, i| {
2347
+ if (i > 0) {
2348
+ buf[pos] = '\n';
2349
+ pos += 1;
2350
+ }
2351
+
2352
+ const ct = ch.sliceTrimmed(line, 0, line.len);
2353
+ if (std.mem.eql(u8, ct, "{")) {
2354
+ @memcpy(buf[pos..][0..ct.len], ct);
2355
+ pos += ct.len;
2356
+ continue;
2357
+ }
2358
+
2359
+ const current_indent = indent_cache.items[i];
2360
+ if (current_indent > min_indent) {
2361
+ @memcpy(buf[pos..][0..line.len], line);
2362
+ pos += line.len;
2363
+ } else if (current_indent == min_indent and ct.len > 0 and (ct[0] == '}' or ct[0] == ']' or ct[0] == ')')) {
2364
+ @memcpy(buf[pos..][0..line.len], line);
2365
+ pos += line.len;
2366
+ } else {
2367
+ const new_indent = if (current_indent >= offs) current_indent - offs else 0;
2368
+ @memset(buf[pos..][0..new_indent], ' ');
2369
+ pos += new_indent;
2370
+ @memcpy(buf[pos..][0..ct.len], ct);
2371
+ pos += ct.len;
2372
+ }
2373
+ }
2374
+
2375
+ return buf[0..pos];
2376
+ }
2377
+
2378
+ // ========================================================================
2379
+ // Declare handler
2380
+ // ========================================================================
2381
+
2382
+ /// Handle `declare ...` after `declare` keyword
2383
+ pub fn handleDeclare(s: *Scanner, stmt_start: usize, is_exported: bool) void {
2384
+ if (s.matchWord("function")) {
2385
+ const decl = extractFunction(s, stmt_start, is_exported, false, false);
2386
+ if (decl) |d| s.declarations.append(d) catch {};
2387
+ } else if (s.matchWord("async")) {
2388
+ s.pos += 5;
2389
+ s.skipWhitespaceAndComments();
2390
+ if (s.matchWord("function")) {
2391
+ const decl = extractFunction(s, stmt_start, is_exported, true, false);
2392
+ if (decl) |d| s.declarations.append(d) catch {};
2393
+ }
2394
+ } else if (s.matchWord("class")) {
2395
+ const decl = extractClass(s, stmt_start, is_exported, false);
2396
+ s.declarations.append(decl) catch {};
2397
+ } else if (s.matchWord("abstract")) {
2398
+ s.pos += 8;
2399
+ s.skipWhitespaceAndComments();
2400
+ if (s.matchWord("class")) {
2401
+ const decl = extractClass(s, stmt_start, is_exported, true);
2402
+ s.declarations.append(decl) catch {};
2403
+ }
2404
+ } else if (s.matchWord("interface")) {
2405
+ const decl = extractInterface(s, stmt_start, is_exported);
2406
+ s.declarations.append(decl) catch {};
2407
+ } else if (s.matchWord("type")) {
2408
+ const decl = extractTypeAlias(s, stmt_start, is_exported);
2409
+ s.declarations.append(decl) catch {};
2410
+ } else if (s.matchWord("enum")) {
2411
+ const decl = extractEnum(s, stmt_start, is_exported, false);
2412
+ s.declarations.append(decl) catch {};
2413
+ } else if (s.matchWord("const")) {
2414
+ const saved_pos = s.pos;
2415
+ s.pos += 5;
2416
+ s.skipWhitespaceAndComments();
2417
+ if (s.matchWord("enum")) {
2418
+ s.pos = saved_pos + 5;
2419
+ s.skipWhitespaceAndComments();
2420
+ const decl = extractEnum(s, stmt_start, is_exported, true);
2421
+ s.declarations.append(decl) catch {};
2422
+ } else if (is_exported) {
2423
+ s.pos = saved_pos;
2424
+ const decls = extractVariable(s, stmt_start, "const", true);
2425
+ for (decls) |d| s.declarations.append(d) catch {};
2426
+ } else {
2427
+ s.skipToStatementEnd();
2428
+ }
2429
+ } else if (s.matchWord("let") or s.matchWord("var")) {
2430
+ if (is_exported) {
2431
+ const kind: []const u8 = if (s.matchWord("let")) "let" else "var";
2432
+ const decls = extractVariable(s, stmt_start, kind, true);
2433
+ for (decls) |d| s.declarations.append(d) catch {};
2434
+ } else {
2435
+ s.skipToStatementEnd();
2436
+ }
2437
+ } else if (s.matchWord("module")) {
2438
+ const decl = extractModule(s, stmt_start, is_exported, "module");
2439
+ s.declarations.append(decl) catch {};
2440
+ } else if (s.matchWord("namespace")) {
2441
+ const decl = extractModule(s, stmt_start, is_exported, "namespace");
2442
+ s.declarations.append(decl) catch {};
2443
+ } else if (s.matchWord("global")) {
2444
+ s.pos += 6;
2445
+ s.skipWhitespaceAndComments();
2446
+ const body = if (s.pos < s.len and s.source[s.pos] == ch.CH_LBRACE) buildNamespaceBodyDts(s, " ") else "{}";
2447
+ var text = std.array_list.Managed(u8).init(s.allocator);
2448
+ text.ensureTotalCapacity(128) catch {};
2449
+ text.appendSlice("declare global ") catch {};
2450
+ text.appendSlice(body) catch {};
2451
+ const comments = extractLeadingComments(s, stmt_start);
2452
+ s.declarations.append(.{
2453
+ .kind = .module_decl,
2454
+ .name = "global",
2455
+ .text = text.toOwnedSlice() catch "",
2456
+ .is_exported = false,
2457
+ .leading_comments = comments,
2458
+ .start = stmt_start,
2459
+ .end = s.pos,
2460
+ }) catch {};
2461
+ } else {
2462
+ s.skipToStatementEnd();
2463
+ }
2464
+ }