@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
package/src/emitter.zig
ADDED
|
@@ -0,0 +1,1045 @@
|
|
|
1
|
+
/// Emitter module — converts declarations to final .d.ts output.
|
|
2
|
+
/// Port of processor/index.ts.
|
|
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 Declaration = types.Declaration;
|
|
8
|
+
const DeclarationKind = types.DeclarationKind;
|
|
9
|
+
|
|
10
|
+
/// Check if a character is an identifier character (for word boundary checks)
|
|
11
|
+
inline fn isIdentChar(c: u8) bool {
|
|
12
|
+
return (c >= 'A' and c <= 'Z') or (c >= 'a' and c <= 'z') or (c >= '0' and c <= '9') or c == '_' or c == '$' or c > 127;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/// Check if `name` appears as a whole word in `text` (fast indexOf + boundary check)
|
|
16
|
+
pub fn isWordInText(name: []const u8, text: []const u8) bool {
|
|
17
|
+
if (name.len == 0) return false;
|
|
18
|
+
var search_from: usize = 0;
|
|
19
|
+
while (search_from < text.len) {
|
|
20
|
+
const idx = ch.indexOf(text, name, search_from) orelse return false;
|
|
21
|
+
const before: u8 = if (idx > 0) text[idx - 1] else ' ';
|
|
22
|
+
const after: u8 = if (idx + name.len < text.len) text[idx + name.len] else ' ';
|
|
23
|
+
if (!isIdentChar(before) and !isIdentChar(after)) return true;
|
|
24
|
+
search_from = idx + 1;
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// Extract all identifier words from text into an existing HashMap (single pass, O(n))
|
|
30
|
+
fn extractWords(words: *std.StringHashMap(void), text: []const u8) void {
|
|
31
|
+
var i: usize = 0;
|
|
32
|
+
while (i < text.len) {
|
|
33
|
+
const c = text[i];
|
|
34
|
+
if ((c >= 'A' and c <= 'Z') or (c >= 'a' and c <= 'z') or c == '_' or c == '$' or c > 127) {
|
|
35
|
+
const start = i;
|
|
36
|
+
i += 1;
|
|
37
|
+
while (i < text.len and isIdentChar(text[i])) i += 1;
|
|
38
|
+
words.put(text[start..i], {}) catch {};
|
|
39
|
+
} else {
|
|
40
|
+
i += 1;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Extract triple-slash directives from source start
|
|
46
|
+
fn extractTripleSlashDirectives(alloc: std.mem.Allocator, source: []const u8) ![][]const u8 {
|
|
47
|
+
var directives = std.array_list.Managed([]const u8).init(alloc);
|
|
48
|
+
var line_start: usize = 0;
|
|
49
|
+
|
|
50
|
+
var i: usize = 0;
|
|
51
|
+
while (i <= source.len) : (i += 1) {
|
|
52
|
+
if (i == source.len or source[i] == '\n') {
|
|
53
|
+
// Extract and trim line
|
|
54
|
+
var start = line_start;
|
|
55
|
+
var end = i;
|
|
56
|
+
while (start < end and (source[start] == ' ' or source[start] == '\t' or source[start] == '\r')) start += 1;
|
|
57
|
+
while (end > start and (source[end - 1] == ' ' or source[end - 1] == '\t' or source[end - 1] == '\r')) end -= 1;
|
|
58
|
+
const trimmed = source[start..end];
|
|
59
|
+
line_start = i + 1;
|
|
60
|
+
|
|
61
|
+
if (trimmed.len >= 3 and trimmed[0] == '/' and trimmed[1] == '/' and trimmed[2] == '/') {
|
|
62
|
+
// Check for /// <reference .../>
|
|
63
|
+
if (ch.contains(trimmed, "<reference ") or ch.contains(trimmed, "<amd-module ") or ch.contains(trimmed, "<amd-dependency ")) {
|
|
64
|
+
try directives.append(trimmed);
|
|
65
|
+
}
|
|
66
|
+
} else if (trimmed.len == 0 or (trimmed.len >= 2 and trimmed[0] == '/' and trimmed[1] == '/')) {
|
|
67
|
+
continue;
|
|
68
|
+
} else {
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return directives.toOwnedSlice();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// Format leading comments — direct alloc, output ≤ total comment length + newlines
|
|
78
|
+
fn formatComments(alloc: std.mem.Allocator, comments: ?[]const []const u8, keep_comments: bool) ![]const u8 {
|
|
79
|
+
if (!keep_comments) return "";
|
|
80
|
+
const cmts = comments orelse return "";
|
|
81
|
+
if (cmts.len == 0) return "";
|
|
82
|
+
|
|
83
|
+
// Fast path: single comment (very common)
|
|
84
|
+
if (cmts.len == 1) {
|
|
85
|
+
const t = ch.sliceTrimmed(cmts[0], 0, cmts[0].len);
|
|
86
|
+
const buf = try alloc.alloc(u8, t.len + 1);
|
|
87
|
+
@memcpy(buf[0..t.len], t);
|
|
88
|
+
buf[t.len] = '\n';
|
|
89
|
+
return buf;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Pre-compute exact size
|
|
93
|
+
var total_len: usize = cmts.len; // newlines between + trailing
|
|
94
|
+
for (cmts) |c| {
|
|
95
|
+
const t = ch.sliceTrimmed(c, 0, c.len);
|
|
96
|
+
total_len += t.len;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const buf = try alloc.alloc(u8, total_len);
|
|
100
|
+
var pos: usize = 0;
|
|
101
|
+
for (cmts, 0..) |c, idx| {
|
|
102
|
+
if (idx > 0) {
|
|
103
|
+
buf[pos] = '\n';
|
|
104
|
+
pos += 1;
|
|
105
|
+
}
|
|
106
|
+
const t = ch.sliceTrimmed(c, 0, c.len);
|
|
107
|
+
@memcpy(buf[pos..][0..t.len], t);
|
|
108
|
+
pos += t.len;
|
|
109
|
+
}
|
|
110
|
+
buf[pos] = '\n';
|
|
111
|
+
pos += 1;
|
|
112
|
+
return buf[0..pos];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const ParsedImportResult = struct {
|
|
116
|
+
default_name: ?[]const u8,
|
|
117
|
+
named_items: []const []const u8,
|
|
118
|
+
source: []const u8,
|
|
119
|
+
is_type_only: bool,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/// Get parsed import components from a declaration's cached parsed_import.
|
|
123
|
+
fn getParsedImport(decl: Declaration) ?ParsedImportResult {
|
|
124
|
+
const pi = decl.parsed_import orelse return null;
|
|
125
|
+
return .{
|
|
126
|
+
.default_name = pi.default_name,
|
|
127
|
+
.named_items = pi.named_items,
|
|
128
|
+
.source = pi.source,
|
|
129
|
+
.is_type_only = pi.is_type_only,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/// Extract all imported item names from an import declaration (for filtering).
|
|
134
|
+
/// Uses cached parsed_import when available to avoid re-parsing.
|
|
135
|
+
fn extractAllImportedItems(decl: Declaration) []const []const u8 {
|
|
136
|
+
if (decl.parsed_import) |pi| {
|
|
137
|
+
return pi.resolved_items;
|
|
138
|
+
}
|
|
139
|
+
return &.{};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/// Process a variable declaration for DTS output
|
|
143
|
+
fn processVariableDeclaration(alloc: std.mem.Allocator, decl: Declaration, keep_comments: bool) ![]const u8 {
|
|
144
|
+
const comments = try formatComments(alloc, decl.leading_comments, keep_comments);
|
|
145
|
+
|
|
146
|
+
// Fast path: if we have type annotation and no value needing special inference
|
|
147
|
+
if (decl.type_annotation.len > 0 and decl.value.len == 0) {
|
|
148
|
+
if (comments.len == 0) return decl.text;
|
|
149
|
+
const buf = try alloc.alloc(u8, comments.len + decl.text.len);
|
|
150
|
+
@memcpy(buf[0..comments.len], comments);
|
|
151
|
+
@memcpy(buf[comments.len..][0..decl.text.len], decl.text);
|
|
152
|
+
return buf;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Fast path: type annotation set + value exists but doesn't need special handling.
|
|
156
|
+
// The scanner already built the correct DTS text using the type annotation,
|
|
157
|
+
// so we can return it directly without type inference or value processing.
|
|
158
|
+
if (decl.type_annotation.len > 0 and decl.value.len > 0) {
|
|
159
|
+
const trimmed_val = std.mem.trim(u8, decl.value, " \t\n\r");
|
|
160
|
+
if (!ch.endsWith(trimmed_val, "as const") and
|
|
161
|
+
!ch.contains(decl.value, " satisfies "))
|
|
162
|
+
{
|
|
163
|
+
const kind: []const u8 = if (decl.modifiers) |mods| (if (mods.len > 0) mods[0] else "const") else "const";
|
|
164
|
+
if (!std.mem.eql(u8, kind, "const") or !type_inf.isGenericType(decl.type_annotation)) {
|
|
165
|
+
if (comments.len == 0) return decl.text;
|
|
166
|
+
const buf = try alloc.alloc(u8, comments.len + decl.text.len);
|
|
167
|
+
@memcpy(buf[0..comments.len], comments);
|
|
168
|
+
@memcpy(buf[comments.len..][0..decl.text.len], decl.text);
|
|
169
|
+
return buf;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Variable kind from modifiers
|
|
175
|
+
const kind: []const u8 = if (decl.modifiers) |mods| (if (mods.len > 0) mods[0] else "const") else "const";
|
|
176
|
+
|
|
177
|
+
// Determine type annotation
|
|
178
|
+
var type_annotation: []const u8 = decl.type_annotation;
|
|
179
|
+
|
|
180
|
+
if (decl.value.len > 0 and ch.contains(decl.value, " satisfies ")) {
|
|
181
|
+
if (type_inf.extractSatisfiesType(decl.value)) |sat_type| {
|
|
182
|
+
type_annotation = sat_type;
|
|
183
|
+
}
|
|
184
|
+
} else if (decl.value.len > 0 and ch.endsWith(std.mem.trim(u8, decl.value, " \t\n\r"), "as const")) {
|
|
185
|
+
type_annotation = try type_inf.inferNarrowType(alloc, decl.value, true, false, 0);
|
|
186
|
+
} else if (type_annotation.len == 0 and decl.value.len > 0 and std.mem.eql(u8, kind, "const")) {
|
|
187
|
+
const trimmed_val = std.mem.trim(u8, decl.value, " \t\n\r");
|
|
188
|
+
const is_container = trimmed_val.len > 0 and (trimmed_val[0] == '{' or trimmed_val[0] == '[');
|
|
189
|
+
if (is_container) type_inf.enableCleanDefaultCollection();
|
|
190
|
+
type_annotation = try type_inf.inferNarrowType(alloc, decl.value, !is_container, false, 0);
|
|
191
|
+
} else if (type_annotation.len > 0 and decl.value.len > 0 and std.mem.eql(u8, kind, "const") and type_inf.isGenericType(type_annotation)) {
|
|
192
|
+
const inferred = try type_inf.inferNarrowType(alloc, decl.value, true, false, 0);
|
|
193
|
+
if (!std.mem.eql(u8, inferred, "unknown")) {
|
|
194
|
+
type_annotation = inferred;
|
|
195
|
+
}
|
|
196
|
+
} else if (type_annotation.len == 0 and decl.value.len > 0) {
|
|
197
|
+
type_annotation = try type_inf.inferNarrowType(alloc, decl.value, std.mem.eql(u8, kind, "const"), false, 0);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (type_annotation.len == 0) type_annotation = "unknown";
|
|
201
|
+
|
|
202
|
+
// Add @defaultValue JSDoc for widened declarations
|
|
203
|
+
// Skip when value uses 'as const' — types are already narrow/self-documenting
|
|
204
|
+
const val_trimmed_for_check = std.mem.trim(u8, decl.value, " \t\n\r");
|
|
205
|
+
const has_as_const = ch.endsWith(val_trimmed_for_check, "as const");
|
|
206
|
+
if (decl.value.len > 0 and decl.type_annotation.len == 0 and !has_as_const) {
|
|
207
|
+
var default_val: ?[]const u8 = null;
|
|
208
|
+
var is_container = false;
|
|
209
|
+
const trimmed_val = std.mem.trim(u8, decl.value, " \t\n\r");
|
|
210
|
+
|
|
211
|
+
if (!std.mem.eql(u8, kind, "const")) {
|
|
212
|
+
const is_widened = std.mem.eql(u8, type_annotation, "string") or
|
|
213
|
+
std.mem.eql(u8, type_annotation, "number") or
|
|
214
|
+
std.mem.eql(u8, type_annotation, "boolean");
|
|
215
|
+
if (is_widened and trimmed_val.len > 0) {
|
|
216
|
+
default_val = trimmed_val;
|
|
217
|
+
}
|
|
218
|
+
} else if (trimmed_val.len > 0 and (trimmed_val[0] == '{' or trimmed_val[0] == '[')) {
|
|
219
|
+
default_val = type_inf.consumeCleanDefault();
|
|
220
|
+
is_container = true;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Skip generated @defaultValue if user already has one
|
|
224
|
+
if (std.mem.indexOf(u8, comments, "@defaultValue") != null) {
|
|
225
|
+
default_val = null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (default_val) |dv| {
|
|
229
|
+
// Build the @defaultValue tag content using direct alloc
|
|
230
|
+
const tag_max = 24 + dv.len * 4 + 16; // generous upper bound for multi-line
|
|
231
|
+
const tag_mem = try alloc.alloc(u8, tag_max);
|
|
232
|
+
var tp: usize = 0;
|
|
233
|
+
|
|
234
|
+
if (std.mem.indexOf(u8, dv, "\n")) |_| {
|
|
235
|
+
const hdr = "@defaultValue\n * ```ts\n";
|
|
236
|
+
@memcpy(tag_mem[tp..][0..hdr.len], hdr); tp += hdr.len;
|
|
237
|
+
var line_iter = std.mem.splitScalar(u8, dv, '\n');
|
|
238
|
+
while (line_iter.next()) |line| {
|
|
239
|
+
@memcpy(tag_mem[tp..][0..3], " * "); tp += 3;
|
|
240
|
+
@memcpy(tag_mem[tp..][0..line.len], line); tp += line.len;
|
|
241
|
+
tag_mem[tp] = '\n'; tp += 1;
|
|
242
|
+
}
|
|
243
|
+
@memcpy(tag_mem[tp..][0..6], " * ```"); tp += 6;
|
|
244
|
+
} else if (is_container) {
|
|
245
|
+
@memcpy(tag_mem[tp..][0..15], "@defaultValue `"); tp += 15;
|
|
246
|
+
@memcpy(tag_mem[tp..][0..dv.len], dv); tp += dv.len;
|
|
247
|
+
tag_mem[tp] = '`'; tp += 1;
|
|
248
|
+
} else {
|
|
249
|
+
@memcpy(tag_mem[tp..][0..14], "@defaultValue "); tp += 14;
|
|
250
|
+
@memcpy(tag_mem[tp..][0..dv.len], dv); tp += dv.len;
|
|
251
|
+
}
|
|
252
|
+
const default_tag = tag_mem[0..tp];
|
|
253
|
+
|
|
254
|
+
// Build rebuilt output using direct alloc
|
|
255
|
+
const export_prefix: []const u8 = if (decl.is_exported) "export " else "";
|
|
256
|
+
const decl_suffix_len = export_prefix.len + 8 + kind.len + 1 + decl.name.len + 2 + type_annotation.len + 1; // "declare " + kind + ' ' + name + ": " + type + ';'
|
|
257
|
+
const rebuilt_max = comments.len + default_tag.len + decl_suffix_len + 64;
|
|
258
|
+
const rbuf = try alloc.alloc(u8, rebuilt_max);
|
|
259
|
+
var rp: usize = 0;
|
|
260
|
+
|
|
261
|
+
// Try to merge into existing JSDoc comment block
|
|
262
|
+
const closing_idx = std.mem.lastIndexOf(u8, comments, "*/");
|
|
263
|
+
if (closing_idx != null and comments.len > 0) {
|
|
264
|
+
const before_raw = comments[0..closing_idx.?];
|
|
265
|
+
var end = before_raw.len;
|
|
266
|
+
while (end > 0 and (before_raw[end - 1] == ' ' or before_raw[end - 1] == '\t' or before_raw[end - 1] == '\n' or before_raw[end - 1] == '\r')) : (end -= 1) {}
|
|
267
|
+
const before = before_raw[0..end];
|
|
268
|
+
if (before.len > 4 and ch.startsWith(before, "/** ") and std.mem.indexOf(u8, before, "\n") == null) {
|
|
269
|
+
@memcpy(rbuf[rp..][0..7], "/**\n * "); rp += 7;
|
|
270
|
+
@memcpy(rbuf[rp..][0..before.len - 4], before[4..]); rp += before.len - 4;
|
|
271
|
+
} else {
|
|
272
|
+
@memcpy(rbuf[rp..][0..before.len], before); rp += before.len;
|
|
273
|
+
}
|
|
274
|
+
@memcpy(rbuf[rp..][0..4], "\n * "); rp += 4;
|
|
275
|
+
@memcpy(rbuf[rp..][0..default_tag.len], default_tag); rp += default_tag.len;
|
|
276
|
+
@memcpy(rbuf[rp..][0..5], "\n */\n"); rp += 5;
|
|
277
|
+
} else if (comments.len > 0) {
|
|
278
|
+
const trimmed_cmt = std.mem.trim(u8, comments, " \t\n\r");
|
|
279
|
+
const cmt_text = if (ch.startsWith(trimmed_cmt, "// "))
|
|
280
|
+
trimmed_cmt[3..]
|
|
281
|
+
else if (ch.startsWith(trimmed_cmt, "//"))
|
|
282
|
+
trimmed_cmt[2..]
|
|
283
|
+
else
|
|
284
|
+
trimmed_cmt;
|
|
285
|
+
@memcpy(rbuf[rp..][0..7], "/**\n * "); rp += 7;
|
|
286
|
+
@memcpy(rbuf[rp..][0..cmt_text.len], cmt_text); rp += cmt_text.len;
|
|
287
|
+
@memcpy(rbuf[rp..][0..4], "\n * "); rp += 4;
|
|
288
|
+
@memcpy(rbuf[rp..][0..default_tag.len], default_tag); rp += default_tag.len;
|
|
289
|
+
@memcpy(rbuf[rp..][0..5], "\n */\n"); rp += 5;
|
|
290
|
+
} else {
|
|
291
|
+
if (std.mem.indexOf(u8, default_tag, "\n") != null) {
|
|
292
|
+
@memcpy(rbuf[rp..][0..7], "/**\n * "); rp += 7;
|
|
293
|
+
@memcpy(rbuf[rp..][0..default_tag.len], default_tag); rp += default_tag.len;
|
|
294
|
+
@memcpy(rbuf[rp..][0..5], "\n */\n"); rp += 5;
|
|
295
|
+
} else {
|
|
296
|
+
@memcpy(rbuf[rp..][0..4], "/** "); rp += 4;
|
|
297
|
+
@memcpy(rbuf[rp..][0..default_tag.len], default_tag); rp += default_tag.len;
|
|
298
|
+
@memcpy(rbuf[rp..][0..4], " */\n"); rp += 4;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
@memcpy(rbuf[rp..][0..export_prefix.len], export_prefix); rp += export_prefix.len;
|
|
303
|
+
@memcpy(rbuf[rp..][0..8], "declare "); rp += 8;
|
|
304
|
+
@memcpy(rbuf[rp..][0..kind.len], kind); rp += kind.len;
|
|
305
|
+
rbuf[rp] = ' '; rp += 1;
|
|
306
|
+
@memcpy(rbuf[rp..][0..decl.name.len], decl.name); rp += decl.name.len;
|
|
307
|
+
@memcpy(rbuf[rp..][0..2], ": "); rp += 2;
|
|
308
|
+
@memcpy(rbuf[rp..][0..type_annotation.len], type_annotation); rp += type_annotation.len;
|
|
309
|
+
rbuf[rp] = ';'; rp += 1;
|
|
310
|
+
return rbuf[0..rp];
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Build final result with direct alloc
|
|
315
|
+
const export_prefix: []const u8 = if (decl.is_exported) "export " else "";
|
|
316
|
+
const total = comments.len + export_prefix.len + 8 + kind.len + 1 + decl.name.len + 2 + type_annotation.len + 1;
|
|
317
|
+
const buf = try alloc.alloc(u8, total);
|
|
318
|
+
var pos: usize = 0;
|
|
319
|
+
|
|
320
|
+
if (comments.len > 0) { @memcpy(buf[pos..][0..comments.len], comments); pos += comments.len; }
|
|
321
|
+
@memcpy(buf[pos..][0..export_prefix.len], export_prefix); pos += export_prefix.len;
|
|
322
|
+
@memcpy(buf[pos..][0..8], "declare "); pos += 8;
|
|
323
|
+
@memcpy(buf[pos..][0..kind.len], kind); pos += kind.len;
|
|
324
|
+
buf[pos] = ' '; pos += 1;
|
|
325
|
+
@memcpy(buf[pos..][0..decl.name.len], decl.name); pos += decl.name.len;
|
|
326
|
+
@memcpy(buf[pos..][0..2], ": "); pos += 2;
|
|
327
|
+
@memcpy(buf[pos..][0..type_annotation.len], type_annotation); pos += type_annotation.len;
|
|
328
|
+
buf[pos] = ';'; pos += 1;
|
|
329
|
+
|
|
330
|
+
return buf[0..pos];
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/// Process an interface declaration for DTS output
|
|
334
|
+
fn processInterfaceDeclaration(alloc: std.mem.Allocator, decl: Declaration, keep_comments: bool) ![]const u8 {
|
|
335
|
+
const comments = try formatComments(alloc, decl.leading_comments, keep_comments);
|
|
336
|
+
|
|
337
|
+
// If the text already starts with proper keywords, use it
|
|
338
|
+
if (ch.startsWith(decl.text, "export declare interface") or ch.startsWith(decl.text, "declare interface")) {
|
|
339
|
+
if (comments.len == 0) return decl.text;
|
|
340
|
+
const buf = try alloc.alloc(u8, comments.len + decl.text.len);
|
|
341
|
+
@memcpy(buf[0..comments.len], comments);
|
|
342
|
+
@memcpy(buf[comments.len..][0..decl.text.len], decl.text);
|
|
343
|
+
return buf;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Direct alloc: compute max size
|
|
347
|
+
const export_prefix: []const u8 = if (decl.is_exported) "export " else "";
|
|
348
|
+
const extends_kw: []const u8 = if (decl.extends_clause.len > 0) " extends " else "";
|
|
349
|
+
const body_start = ch.indexOfChar(decl.text, '{', 0);
|
|
350
|
+
const body = if (body_start) |bi| decl.text[bi..] else "{}";
|
|
351
|
+
const max_len = comments.len + export_prefix.len + "declare interface ".len +
|
|
352
|
+
decl.name.len + decl.generics.len + extends_kw.len + decl.extends_clause.len + 1 + body.len;
|
|
353
|
+
|
|
354
|
+
const buf = try alloc.alloc(u8, max_len);
|
|
355
|
+
var pos: usize = 0;
|
|
356
|
+
@memcpy(buf[pos..][0..comments.len], comments);
|
|
357
|
+
pos += comments.len;
|
|
358
|
+
@memcpy(buf[pos..][0..export_prefix.len], export_prefix);
|
|
359
|
+
pos += export_prefix.len;
|
|
360
|
+
const di = "declare interface ";
|
|
361
|
+
@memcpy(buf[pos..][0..di.len], di);
|
|
362
|
+
pos += di.len;
|
|
363
|
+
@memcpy(buf[pos..][0..decl.name.len], decl.name);
|
|
364
|
+
pos += decl.name.len;
|
|
365
|
+
if (decl.generics.len > 0) {
|
|
366
|
+
@memcpy(buf[pos..][0..decl.generics.len], decl.generics);
|
|
367
|
+
pos += decl.generics.len;
|
|
368
|
+
}
|
|
369
|
+
if (decl.extends_clause.len > 0) {
|
|
370
|
+
@memcpy(buf[pos..][0..extends_kw.len], extends_kw);
|
|
371
|
+
pos += extends_kw.len;
|
|
372
|
+
@memcpy(buf[pos..][0..decl.extends_clause.len], decl.extends_clause);
|
|
373
|
+
pos += decl.extends_clause.len;
|
|
374
|
+
}
|
|
375
|
+
buf[pos] = ' ';
|
|
376
|
+
pos += 1;
|
|
377
|
+
@memcpy(buf[pos..][0..body.len], body);
|
|
378
|
+
pos += body.len;
|
|
379
|
+
|
|
380
|
+
return buf[0..pos];
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/// Process a type alias declaration for DTS output
|
|
384
|
+
fn processTypeDeclaration(alloc: std.mem.Allocator, decl: Declaration, keep_comments: bool) ![]const u8 {
|
|
385
|
+
const comments = try formatComments(alloc, decl.leading_comments, keep_comments);
|
|
386
|
+
|
|
387
|
+
const export_prefix: []const u8 = if (decl.is_exported) "export " else "";
|
|
388
|
+
const declare_prefix: []const u8 = if (!decl.is_exported and !ch.contains(decl.text, " from ")) "declare " else "";
|
|
389
|
+
|
|
390
|
+
// Extract type definition from original text
|
|
391
|
+
var type_def: []const u8 = undefined;
|
|
392
|
+
var fallback = false;
|
|
393
|
+
if (ch.indexOf(decl.text, "type ", 0)) |type_idx| {
|
|
394
|
+
var td = decl.text[type_idx..];
|
|
395
|
+
var end = td.len;
|
|
396
|
+
while (end > 0 and (td[end - 1] == ';' or td[end - 1] == ' ' or td[end - 1] == '\n' or td[end - 1] == '\r')) end -= 1;
|
|
397
|
+
type_def = td[0..end];
|
|
398
|
+
} else {
|
|
399
|
+
fallback = true;
|
|
400
|
+
type_def = ""; // unused, assembled below
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Calculate total length
|
|
404
|
+
const needs_semi = if (!fallback)
|
|
405
|
+
(type_def.len > 0 and type_def[type_def.len - 1] != ';' and type_def[type_def.len - 1] != '}')
|
|
406
|
+
else
|
|
407
|
+
true; // fallback " = any" always needs semi
|
|
408
|
+
|
|
409
|
+
const body_len = if (!fallback) type_def.len else 5 + decl.name.len + decl.generics.len + 6; // "type " + name + generics + " = any"
|
|
410
|
+
const total = comments.len + export_prefix.len + declare_prefix.len + body_len + @as(usize, if (needs_semi) 1 else 0);
|
|
411
|
+
|
|
412
|
+
const buf = try alloc.alloc(u8, total);
|
|
413
|
+
var pos: usize = 0;
|
|
414
|
+
|
|
415
|
+
if (comments.len > 0) { @memcpy(buf[pos..][0..comments.len], comments); pos += comments.len; }
|
|
416
|
+
if (export_prefix.len > 0) { @memcpy(buf[pos..][0..export_prefix.len], export_prefix); pos += export_prefix.len; }
|
|
417
|
+
if (declare_prefix.len > 0) { @memcpy(buf[pos..][0..declare_prefix.len], declare_prefix); pos += declare_prefix.len; }
|
|
418
|
+
|
|
419
|
+
if (!fallback) {
|
|
420
|
+
@memcpy(buf[pos..][0..type_def.len], type_def);
|
|
421
|
+
pos += type_def.len;
|
|
422
|
+
} else {
|
|
423
|
+
@memcpy(buf[pos..][0..5], "type ");
|
|
424
|
+
pos += 5;
|
|
425
|
+
@memcpy(buf[pos..][0..decl.name.len], decl.name);
|
|
426
|
+
pos += decl.name.len;
|
|
427
|
+
if (decl.generics.len > 0) { @memcpy(buf[pos..][0..decl.generics.len], decl.generics); pos += decl.generics.len; }
|
|
428
|
+
@memcpy(buf[pos..][0..6], " = any");
|
|
429
|
+
pos += 6;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (needs_semi) { buf[pos] = ';'; pos += 1; }
|
|
433
|
+
|
|
434
|
+
return buf[0..pos];
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/// Process an enum declaration for DTS output
|
|
438
|
+
fn processEnumDeclaration(alloc: std.mem.Allocator, decl: Declaration, keep_comments: bool) ![]const u8 {
|
|
439
|
+
const comments = try formatComments(alloc, decl.leading_comments, keep_comments);
|
|
440
|
+
|
|
441
|
+
const export_prefix: []const u8 = if (decl.is_exported) "export " else "";
|
|
442
|
+
var is_const = false;
|
|
443
|
+
if (decl.modifiers) |mods| {
|
|
444
|
+
for (mods) |m| {
|
|
445
|
+
if (std.mem.eql(u8, m, "const")) {
|
|
446
|
+
is_const = true;
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
const const_kw: []const u8 = if (is_const) "const " else "";
|
|
452
|
+
const body_start = ch.indexOfChar(decl.text, '{', 0);
|
|
453
|
+
const body = if (body_start) |bi| decl.text[bi..] else "{}";
|
|
454
|
+
|
|
455
|
+
const buf = try alloc.alloc(u8, comments.len + export_prefix.len + "declare ".len +
|
|
456
|
+
const_kw.len + "enum ".len + decl.name.len + 1 + body.len);
|
|
457
|
+
var pos: usize = 0;
|
|
458
|
+
@memcpy(buf[pos..][0..comments.len], comments);
|
|
459
|
+
pos += comments.len;
|
|
460
|
+
@memcpy(buf[pos..][0..export_prefix.len], export_prefix);
|
|
461
|
+
pos += export_prefix.len;
|
|
462
|
+
const dec = "declare ";
|
|
463
|
+
@memcpy(buf[pos..][0..dec.len], dec);
|
|
464
|
+
pos += dec.len;
|
|
465
|
+
@memcpy(buf[pos..][0..const_kw.len], const_kw);
|
|
466
|
+
pos += const_kw.len;
|
|
467
|
+
const en = "enum ";
|
|
468
|
+
@memcpy(buf[pos..][0..en.len], en);
|
|
469
|
+
pos += en.len;
|
|
470
|
+
@memcpy(buf[pos..][0..decl.name.len], decl.name);
|
|
471
|
+
pos += decl.name.len;
|
|
472
|
+
buf[pos] = ' ';
|
|
473
|
+
pos += 1;
|
|
474
|
+
@memcpy(buf[pos..][0..body.len], body);
|
|
475
|
+
pos += body.len;
|
|
476
|
+
|
|
477
|
+
return buf[0..pos];
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/// Process a module/namespace declaration for DTS output
|
|
481
|
+
fn processModuleDeclaration(alloc: std.mem.Allocator, decl: Declaration, keep_comments: bool) ![]const u8 {
|
|
482
|
+
const comments = try formatComments(alloc, decl.leading_comments, keep_comments);
|
|
483
|
+
|
|
484
|
+
// Global augmentation
|
|
485
|
+
if (ch.startsWith(decl.text, "declare global")) {
|
|
486
|
+
if (comments.len == 0) return decl.text;
|
|
487
|
+
var result = std.array_list.Managed(u8).init(alloc);
|
|
488
|
+
try result.ensureTotalCapacity(comments.len + decl.text.len);
|
|
489
|
+
try result.appendSlice(comments);
|
|
490
|
+
try result.appendSlice(decl.text);
|
|
491
|
+
return result.toOwnedSlice();
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Ambient module (quoted name)
|
|
495
|
+
const is_ambient = decl.source_module.len > 0 or
|
|
496
|
+
(decl.name.len > 0 and (decl.name[0] == '"' or decl.name[0] == '\'' or decl.name[0] == '`'));
|
|
497
|
+
|
|
498
|
+
if (is_ambient) {
|
|
499
|
+
var result = std.array_list.Managed(u8).init(alloc);
|
|
500
|
+
try result.ensureTotalCapacity(comments.len + decl.text.len + 32);
|
|
501
|
+
try result.appendSlice(comments);
|
|
502
|
+
try result.appendSlice("declare module ");
|
|
503
|
+
try result.appendSlice(decl.name);
|
|
504
|
+
|
|
505
|
+
if (ch.indexOfChar(decl.text, '{', 0)) |brace_idx| {
|
|
506
|
+
try result.append(' ');
|
|
507
|
+
try result.appendSlice(decl.text[brace_idx..]);
|
|
508
|
+
} else {
|
|
509
|
+
try result.appendSlice(" {}");
|
|
510
|
+
}
|
|
511
|
+
return result.toOwnedSlice();
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Regular namespace
|
|
515
|
+
var result = std.array_list.Managed(u8).init(alloc);
|
|
516
|
+
try result.ensureTotalCapacity(comments.len + decl.text.len + 32);
|
|
517
|
+
try result.appendSlice(comments);
|
|
518
|
+
|
|
519
|
+
if (decl.is_exported) try result.appendSlice("export ");
|
|
520
|
+
|
|
521
|
+
// Check if declare is already in modifiers
|
|
522
|
+
var has_declare = false;
|
|
523
|
+
if (decl.modifiers) |mods| {
|
|
524
|
+
for (mods) |m| {
|
|
525
|
+
if (std.mem.eql(u8, m, "declare")) {
|
|
526
|
+
has_declare = true;
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
if (!has_declare) try result.appendSlice("declare ");
|
|
532
|
+
|
|
533
|
+
try result.appendSlice("namespace ");
|
|
534
|
+
try result.appendSlice(decl.name);
|
|
535
|
+
|
|
536
|
+
if (ch.indexOfChar(decl.text, '{', 0)) |brace_idx| {
|
|
537
|
+
try result.append(' ');
|
|
538
|
+
try result.appendSlice(decl.text[brace_idx..]);
|
|
539
|
+
} else {
|
|
540
|
+
try result.appendSlice(" {}");
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
return result.toOwnedSlice();
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/// Main entry point: process declarations array into final .d.ts output.
|
|
547
|
+
/// `result_alloc` is used for the final output buffer (may differ from `alloc`
|
|
548
|
+
/// so the result can survive arena reset in FFI mode).
|
|
549
|
+
pub fn processDeclarations(
|
|
550
|
+
alloc: std.mem.Allocator,
|
|
551
|
+
result_alloc: std.mem.Allocator,
|
|
552
|
+
declarations: []const Declaration,
|
|
553
|
+
source_code: []const u8,
|
|
554
|
+
keep_comments: bool,
|
|
555
|
+
import_order: []const []const u8,
|
|
556
|
+
) ![]const u8 {
|
|
557
|
+
var result = std.array_list.Managed(u8).init(result_alloc);
|
|
558
|
+
try result.ensureTotalCapacity(source_code.len);
|
|
559
|
+
|
|
560
|
+
// Extract triple-slash directives
|
|
561
|
+
// Fast check: skip whitespace
|
|
562
|
+
var si: usize = 0;
|
|
563
|
+
while (si < source_code.len and ch.isWhitespace(source_code[si])) si += 1;
|
|
564
|
+
if (si + 2 < source_code.len and source_code[si] == '/' and source_code[si + 1] == '/' and source_code[si + 2] == '/') {
|
|
565
|
+
const directives = try extractTripleSlashDirectives(alloc, source_code);
|
|
566
|
+
for (directives) |d| {
|
|
567
|
+
if (result.items.len > 0) try result.append('\n');
|
|
568
|
+
try result.appendSlice(d);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Group declarations by type (pre-size to avoid incremental reallocation)
|
|
573
|
+
const group_cap: usize = @max(declarations.len / 4, 4);
|
|
574
|
+
var imports = std.array_list.Managed(Declaration).init(alloc);
|
|
575
|
+
try imports.ensureTotalCapacity(group_cap);
|
|
576
|
+
var functions = std.array_list.Managed(Declaration).init(alloc);
|
|
577
|
+
try functions.ensureTotalCapacity(group_cap);
|
|
578
|
+
var variables = std.array_list.Managed(Declaration).init(alloc);
|
|
579
|
+
try variables.ensureTotalCapacity(group_cap);
|
|
580
|
+
var interfaces = std.array_list.Managed(Declaration).init(alloc);
|
|
581
|
+
try interfaces.ensureTotalCapacity(group_cap);
|
|
582
|
+
var type_decls = std.array_list.Managed(Declaration).init(alloc);
|
|
583
|
+
try type_decls.ensureTotalCapacity(group_cap);
|
|
584
|
+
var classes = std.array_list.Managed(Declaration).init(alloc);
|
|
585
|
+
try classes.ensureTotalCapacity(group_cap);
|
|
586
|
+
var enums = std.array_list.Managed(Declaration).init(alloc);
|
|
587
|
+
try enums.ensureTotalCapacity(group_cap);
|
|
588
|
+
var modules = std.array_list.Managed(Declaration).init(alloc);
|
|
589
|
+
try modules.ensureTotalCapacity(group_cap);
|
|
590
|
+
var exports = std.array_list.Managed(Declaration).init(alloc);
|
|
591
|
+
try exports.ensureTotalCapacity(group_cap);
|
|
592
|
+
|
|
593
|
+
for (declarations) |d| {
|
|
594
|
+
switch (d.kind) {
|
|
595
|
+
.import_decl => try imports.append(d),
|
|
596
|
+
.function_decl => try functions.append(d),
|
|
597
|
+
.variable_decl => try variables.append(d),
|
|
598
|
+
.interface_decl => try interfaces.append(d),
|
|
599
|
+
.type_decl => try type_decls.append(d),
|
|
600
|
+
.class_decl => try classes.append(d),
|
|
601
|
+
.enum_decl => try enums.append(d),
|
|
602
|
+
.module_decl, .namespace_decl => try modules.append(d),
|
|
603
|
+
.export_decl => try exports.append(d),
|
|
604
|
+
.unknown_decl => {},
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Parse exports to track exported items
|
|
609
|
+
var exported_items = std.StringHashMap(void).init(alloc);
|
|
610
|
+
try exported_items.ensureTotalCapacity(@intCast(@max(exports.items.len * 4, 8)));
|
|
611
|
+
var type_export_stmts = std.array_list.Managed([]const u8).init(alloc);
|
|
612
|
+
try type_export_stmts.ensureTotalCapacity(@max(exports.items.len / 2, 2));
|
|
613
|
+
var value_export_stmts = std.array_list.Managed([]const u8).init(alloc);
|
|
614
|
+
try value_export_stmts.ensureTotalCapacity(@max(exports.items.len / 2, 2));
|
|
615
|
+
var default_exports = std.array_list.Managed([]const u8).init(alloc);
|
|
616
|
+
var seen_exports = std.StringHashMap(void).init(alloc);
|
|
617
|
+
try seen_exports.ensureTotalCapacity(@intCast(@max(exports.items.len, 4)));
|
|
618
|
+
|
|
619
|
+
// Reusable buffer for building export statements (avoids per-iteration alloc)
|
|
620
|
+
var stmt_buf = std.array_list.Managed(u8).init(alloc);
|
|
621
|
+
try stmt_buf.ensureTotalCapacity(256);
|
|
622
|
+
|
|
623
|
+
for (exports.items) |decl| {
|
|
624
|
+
const comments = try formatComments(alloc, decl.leading_comments, keep_comments);
|
|
625
|
+
|
|
626
|
+
if (ch.startsWith(decl.text, "export default")) {
|
|
627
|
+
stmt_buf.clearRetainingCapacity();
|
|
628
|
+
try stmt_buf.appendSlice(comments);
|
|
629
|
+
try stmt_buf.appendSlice(decl.text);
|
|
630
|
+
if (!ch.endsWith(decl.text, ";")) try stmt_buf.append(';');
|
|
631
|
+
try default_exports.append(try stmt_buf.toOwnedSlice());
|
|
632
|
+
} else {
|
|
633
|
+
var export_text = ch.sliceTrimmed(decl.text, 0, decl.text.len);
|
|
634
|
+
// Ensure semicolon
|
|
635
|
+
if (!ch.endsWith(export_text, ";")) {
|
|
636
|
+
stmt_buf.clearRetainingCapacity();
|
|
637
|
+
try stmt_buf.appendSlice(export_text);
|
|
638
|
+
try stmt_buf.append(';');
|
|
639
|
+
export_text = try stmt_buf.toOwnedSlice();
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Extract exported items for tracking
|
|
643
|
+
// Look for export { items } or export type { items }
|
|
644
|
+
if (ch.indexOfChar(export_text, '{', 0)) |brace_s| {
|
|
645
|
+
if (ch.indexOfChar(export_text, '}', brace_s)) |brace_e| {
|
|
646
|
+
const items_str = export_text[brace_s + 1 .. brace_e];
|
|
647
|
+
var iter = std.mem.splitSequence(u8, items_str, ",");
|
|
648
|
+
while (iter.next()) |item| {
|
|
649
|
+
const trimmed_item = ch.sliceTrimmed(item, 0, item.len);
|
|
650
|
+
if (trimmed_item.len > 0) {
|
|
651
|
+
try exported_items.put(trimmed_item, {});
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
stmt_buf.clearRetainingCapacity();
|
|
658
|
+
try stmt_buf.appendSlice(comments);
|
|
659
|
+
try stmt_buf.appendSlice(export_text);
|
|
660
|
+
const full_text = try stmt_buf.toOwnedSlice();
|
|
661
|
+
|
|
662
|
+
const gop = try seen_exports.getOrPut(full_text);
|
|
663
|
+
if (!gop.found_existing) {
|
|
664
|
+
if (ch.contains(full_text, "export type")) {
|
|
665
|
+
try type_export_stmts.append(full_text);
|
|
666
|
+
} else {
|
|
667
|
+
try value_export_stmts.append(full_text);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Short-circuit: skip combined_words building and import map when there are no imports.
|
|
674
|
+
// For import-free code (e.g. synthetic benchmarks), this avoids O(n) word
|
|
675
|
+
// extraction across all declarations — a significant saving on large inputs.
|
|
676
|
+
var interface_references = std.StringHashMap(void).init(alloc);
|
|
677
|
+
var processed_imports = std.array_list.Managed([]const u8).init(alloc);
|
|
678
|
+
|
|
679
|
+
if (imports.items.len > 0) {
|
|
680
|
+
// Build import-item-to-declaration map (only when imports exist)
|
|
681
|
+
const ImportDeclMap = std.StringHashMap(Declaration);
|
|
682
|
+
var all_imported_items_map = ImportDeclMap.init(alloc);
|
|
683
|
+
try all_imported_items_map.ensureTotalCapacity(@intCast(@max(imports.items.len * 4, 8)));
|
|
684
|
+
for (imports.items) |imp| {
|
|
685
|
+
const items = extractAllImportedItems(imp);
|
|
686
|
+
for (items) |item| {
|
|
687
|
+
try all_imported_items_map.put(item, imp);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
// Build combined word set for interface reference detection AND import filtering.
|
|
691
|
+
// Single pass over all declarations instead of 7 separate loops per group.
|
|
692
|
+
var combined_words = std.StringHashMap(void).init(alloc);
|
|
693
|
+
try combined_words.ensureTotalCapacity(@intCast(@max(declarations.len * 4, 128)));
|
|
694
|
+
for (declarations) |d| {
|
|
695
|
+
switch (d.kind) {
|
|
696
|
+
.function_decl => {
|
|
697
|
+
if (d.is_exported) extractWords(&combined_words, d.text);
|
|
698
|
+
},
|
|
699
|
+
.variable_decl => {
|
|
700
|
+
if (d.is_exported) {
|
|
701
|
+
extractWords(&combined_words, d.text);
|
|
702
|
+
if (d.type_annotation.len > 0)
|
|
703
|
+
extractWords(&combined_words, d.type_annotation);
|
|
704
|
+
}
|
|
705
|
+
},
|
|
706
|
+
.type_decl, .class_decl, .enum_decl, .module_decl, .namespace_decl, .export_decl => {
|
|
707
|
+
extractWords(&combined_words, d.text);
|
|
708
|
+
},
|
|
709
|
+
.interface_decl, .import_decl, .unknown_decl => {},
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Interface reference detection
|
|
714
|
+
try interface_references.ensureTotalCapacity(@intCast(@max(interfaces.items.len, 4)));
|
|
715
|
+
if (interfaces.items.len > 0 and combined_words.count() > 0) {
|
|
716
|
+
for (interfaces.items) |iface| {
|
|
717
|
+
if (combined_words.contains(iface.name)) {
|
|
718
|
+
try interface_references.put(iface.name, {});
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Add interface text to combined_words (after ref detection, before import filtering)
|
|
724
|
+
for (interfaces.items) |iface| {
|
|
725
|
+
if (iface.is_exported or interface_references.contains(iface.name)) {
|
|
726
|
+
extractWords(&combined_words, iface.text);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
var used_import_items = std.StringHashMap(void).init(alloc);
|
|
730
|
+
try used_import_items.ensureTotalCapacity(@intCast(@max(imports.items.len * 4, 8)));
|
|
731
|
+
|
|
732
|
+
if (combined_words.count() > 0) {
|
|
733
|
+
var map_iter = all_imported_items_map.keyIterator();
|
|
734
|
+
while (map_iter.next()) |key_ptr| {
|
|
735
|
+
if (combined_words.contains(key_ptr.*)) {
|
|
736
|
+
try used_import_items.put(key_ptr.*, {});
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Check re-exports
|
|
742
|
+
var exp_iter = exported_items.keyIterator();
|
|
743
|
+
while (exp_iter.next()) |key_ptr| {
|
|
744
|
+
if (all_imported_items_map.contains(key_ptr.*)) {
|
|
745
|
+
try used_import_items.put(key_ptr.*, {});
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Filter and rebuild imports
|
|
750
|
+
try processed_imports.ensureTotalCapacity(imports.items.len);
|
|
751
|
+
// Reusable buffer for building import statements
|
|
752
|
+
var import_buf = std.array_list.Managed(u8).init(alloc);
|
|
753
|
+
try import_buf.ensureTotalCapacity(256);
|
|
754
|
+
var used_named = std.array_list.Managed([]const u8).init(alloc);
|
|
755
|
+
try used_named.ensureTotalCapacity(16);
|
|
756
|
+
|
|
757
|
+
for (imports.items) |imp| {
|
|
758
|
+
// Preserve side-effect imports
|
|
759
|
+
if (imp.is_side_effect) {
|
|
760
|
+
const trimmed_imp = ch.sliceTrimmed(imp.text, 0, imp.text.len);
|
|
761
|
+
import_buf.clearRetainingCapacity();
|
|
762
|
+
try import_buf.appendSlice(trimmed_imp);
|
|
763
|
+
if (!ch.endsWith(trimmed_imp, ";")) try import_buf.append(';');
|
|
764
|
+
try processed_imports.append(try import_buf.toOwnedSlice());
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const parsed = getParsedImport(imp) orelse continue;
|
|
769
|
+
|
|
770
|
+
const used_default = if (parsed.default_name) |dn| used_import_items.contains(dn) else false;
|
|
771
|
+
used_named.clearRetainingCapacity();
|
|
772
|
+
|
|
773
|
+
for (parsed.named_items) |item| {
|
|
774
|
+
var clean_item = item;
|
|
775
|
+
if (ch.startsWith(clean_item, "type ")) {
|
|
776
|
+
clean_item = ch.sliceTrimmed(clean_item, 5, clean_item.len);
|
|
777
|
+
}
|
|
778
|
+
if (ch.indexOf(clean_item, " as ", 0)) |as_idx| {
|
|
779
|
+
clean_item = ch.sliceTrimmed(clean_item, as_idx + 4, clean_item.len);
|
|
780
|
+
}
|
|
781
|
+
if (used_import_items.contains(clean_item)) {
|
|
782
|
+
try used_named.append(item);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
if (used_default or used_named.items.len > 0) {
|
|
787
|
+
import_buf.clearRetainingCapacity();
|
|
788
|
+
if (parsed.is_type_only) {
|
|
789
|
+
try import_buf.appendSlice("import type ");
|
|
790
|
+
} else {
|
|
791
|
+
try import_buf.appendSlice("import ");
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
if (used_default) {
|
|
795
|
+
if (parsed.default_name) |dn| try import_buf.appendSlice(dn);
|
|
796
|
+
if (used_named.items.len > 0) {
|
|
797
|
+
try import_buf.appendSlice(", { ");
|
|
798
|
+
for (used_named.items, 0..) |ni, idx| {
|
|
799
|
+
if (idx > 0) try import_buf.appendSlice(", ");
|
|
800
|
+
try import_buf.appendSlice(ni);
|
|
801
|
+
}
|
|
802
|
+
try import_buf.appendSlice(" }");
|
|
803
|
+
}
|
|
804
|
+
} else if (used_named.items.len > 0) {
|
|
805
|
+
try import_buf.appendSlice("{ ");
|
|
806
|
+
for (used_named.items, 0..) |ni, idx| {
|
|
807
|
+
if (idx > 0) try import_buf.appendSlice(", ");
|
|
808
|
+
try import_buf.appendSlice(ni);
|
|
809
|
+
}
|
|
810
|
+
try import_buf.appendSlice(" }");
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
try import_buf.appendSlice(" from '");
|
|
814
|
+
try import_buf.appendSlice(parsed.source);
|
|
815
|
+
try import_buf.appendSlice("';");
|
|
816
|
+
|
|
817
|
+
try processed_imports.append(try import_buf.toOwnedSlice());
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Sort imports by priority then locale-aware alphabetical
|
|
822
|
+
if (processed_imports.items.len > 1) {
|
|
823
|
+
const SortCtx = struct {
|
|
824
|
+
import_order_items: []const []const u8,
|
|
825
|
+
default_priority: usize,
|
|
826
|
+
|
|
827
|
+
pub fn priority(self: @This(), imp: []const u8) usize {
|
|
828
|
+
for (self.import_order_items, 0..) |p, idx| {
|
|
829
|
+
var found = false;
|
|
830
|
+
if (ch.indexOf(imp, "from '", 0)) |fi| {
|
|
831
|
+
if (ch.indexOf(imp, p, fi + 6) != null) found = true;
|
|
832
|
+
}
|
|
833
|
+
if (!found) {
|
|
834
|
+
if (ch.indexOf(imp, "from \"", 0)) |fi| {
|
|
835
|
+
if (ch.indexOf(imp, p, fi + 6) != null) found = true;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
if (found) return idx;
|
|
839
|
+
}
|
|
840
|
+
return self.default_priority;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/// Locale-aware char sort key: symbols < digits < letters
|
|
844
|
+
/// Matches JavaScript's localeCompare behavior
|
|
845
|
+
fn charSortKey(c_val: u8) u32 {
|
|
846
|
+
if (c_val >= 'a' and c_val <= 'z') return @as(u32, c_val - 'a') * 4 + 1000;
|
|
847
|
+
if (c_val >= 'A' and c_val <= 'Z') return @as(u32, c_val - 'A') * 4 + 1001;
|
|
848
|
+
if (c_val >= '0' and c_val <= '9') return @as(u32, c_val - '0') + 500;
|
|
849
|
+
return @as(u32, c_val);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
pub fn localeCompare(_: @This(), a: []const u8, b: []const u8) bool {
|
|
853
|
+
const min_len = @min(a.len, b.len);
|
|
854
|
+
for (0..min_len) |i| {
|
|
855
|
+
const ak = charSortKey(a[i]);
|
|
856
|
+
const bk = charSortKey(b[i]);
|
|
857
|
+
if (ak != bk) return ak < bk;
|
|
858
|
+
}
|
|
859
|
+
return a.len < b.len;
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
|
|
863
|
+
const ctx = SortCtx{
|
|
864
|
+
.import_order_items = import_order,
|
|
865
|
+
.default_priority = import_order.len,
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
std.mem.sort([]const u8, processed_imports.items, ctx, struct {
|
|
869
|
+
fn lessThan(c_ctx: SortCtx, a: []const u8, b: []const u8) bool {
|
|
870
|
+
const ap = c_ctx.priority(a);
|
|
871
|
+
const bp = c_ctx.priority(b);
|
|
872
|
+
if (ap != bp) return ap < bp;
|
|
873
|
+
return c_ctx.localeCompare(a, b);
|
|
874
|
+
}
|
|
875
|
+
}.lessThan);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Emit imports
|
|
880
|
+
for (processed_imports.items) |imp| {
|
|
881
|
+
if (result.items.len > 0) try result.append('\n');
|
|
882
|
+
try result.appendSlice(imp);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// Emit type exports
|
|
886
|
+
for (type_export_stmts.items) |stmt| {
|
|
887
|
+
if (result.items.len > 0) try result.append('\n');
|
|
888
|
+
try result.appendSlice(stmt);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// Emit declaration groups: functions, variables, interfaces, types, classes, enums, modules
|
|
892
|
+
const decl_groups = [_]struct { items: []const Declaration, kind_tag: u8 }{
|
|
893
|
+
.{ .items = functions.items, .kind_tag = 'f' },
|
|
894
|
+
.{ .items = variables.items, .kind_tag = 'v' },
|
|
895
|
+
.{ .items = interfaces.items, .kind_tag = 'i' },
|
|
896
|
+
.{ .items = type_decls.items, .kind_tag = 't' },
|
|
897
|
+
.{ .items = classes.items, .kind_tag = 'c' },
|
|
898
|
+
.{ .items = enums.items, .kind_tag = 'e' },
|
|
899
|
+
.{ .items = modules.items, .kind_tag = 'm' },
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
for (decl_groups) |group| {
|
|
903
|
+
for (group.items) |decl| {
|
|
904
|
+
switch (decl.kind) {
|
|
905
|
+
.function_decl, .class_decl => {
|
|
906
|
+
// Direct emit: write comments + text straight to result buffer
|
|
907
|
+
// avoiding intermediate allocation + double copy
|
|
908
|
+
if (decl.text.len == 0) continue;
|
|
909
|
+
if (result.items.len > 0) try result.append('\n');
|
|
910
|
+
if (keep_comments) {
|
|
911
|
+
if (decl.leading_comments) |cmts| {
|
|
912
|
+
if (cmts.len > 0) {
|
|
913
|
+
const comments = try formatComments(alloc, cmts, true);
|
|
914
|
+
if (comments.len > 0) try result.appendSlice(comments);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
try result.appendSlice(decl.text);
|
|
919
|
+
},
|
|
920
|
+
.variable_decl => {
|
|
921
|
+
const processed = try processVariableDeclaration(alloc, decl, keep_comments);
|
|
922
|
+
if (processed.len > 0) {
|
|
923
|
+
if (result.items.len > 0) try result.append('\n');
|
|
924
|
+
try result.appendSlice(processed);
|
|
925
|
+
}
|
|
926
|
+
},
|
|
927
|
+
.interface_decl => {
|
|
928
|
+
// Fast path: text already has correct keywords → direct emit
|
|
929
|
+
if (decl.text.len > 0 and (ch.startsWith(decl.text, "export declare interface") or
|
|
930
|
+
ch.startsWith(decl.text, "declare interface")))
|
|
931
|
+
{
|
|
932
|
+
if (result.items.len > 0) try result.append('\n');
|
|
933
|
+
if (keep_comments) {
|
|
934
|
+
if (decl.leading_comments) |cmts| {
|
|
935
|
+
if (cmts.len > 0) {
|
|
936
|
+
const comments = try formatComments(alloc, cmts, true);
|
|
937
|
+
if (comments.len > 0) try result.appendSlice(comments);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
try result.appendSlice(decl.text);
|
|
942
|
+
} else {
|
|
943
|
+
const processed = try processInterfaceDeclaration(alloc, decl, keep_comments);
|
|
944
|
+
if (processed.len > 0) {
|
|
945
|
+
if (result.items.len > 0) try result.append('\n');
|
|
946
|
+
try result.appendSlice(processed);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
},
|
|
950
|
+
.type_decl => {
|
|
951
|
+
const processed = try processTypeDeclaration(alloc, decl, keep_comments);
|
|
952
|
+
if (processed.len > 0) {
|
|
953
|
+
if (result.items.len > 0) try result.append('\n');
|
|
954
|
+
try result.appendSlice(processed);
|
|
955
|
+
}
|
|
956
|
+
},
|
|
957
|
+
.enum_decl => {
|
|
958
|
+
// Fast path: text already has correct keywords → direct emit
|
|
959
|
+
if (decl.text.len > 0 and ch.startsWith(decl.text, "declare ")) {
|
|
960
|
+
if (result.items.len > 0) try result.append('\n');
|
|
961
|
+
if (keep_comments) {
|
|
962
|
+
if (decl.leading_comments) |cmts| {
|
|
963
|
+
if (cmts.len > 0) {
|
|
964
|
+
const comments = try formatComments(alloc, cmts, true);
|
|
965
|
+
if (comments.len > 0) try result.appendSlice(comments);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
if (decl.is_exported) try result.appendSlice("export ");
|
|
970
|
+
try result.appendSlice(decl.text);
|
|
971
|
+
} else {
|
|
972
|
+
const processed = try processEnumDeclaration(alloc, decl, keep_comments);
|
|
973
|
+
if (processed.len > 0) {
|
|
974
|
+
if (result.items.len > 0) try result.append('\n');
|
|
975
|
+
try result.appendSlice(processed);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
},
|
|
979
|
+
.module_decl, .namespace_decl => {
|
|
980
|
+
// Fast path: text already has correct keywords → direct emit
|
|
981
|
+
if (decl.text.len > 0 and (ch.startsWith(decl.text, "export declare namespace") or
|
|
982
|
+
ch.startsWith(decl.text, "declare namespace") or
|
|
983
|
+
ch.startsWith(decl.text, "declare module")))
|
|
984
|
+
{
|
|
985
|
+
if (result.items.len > 0) try result.append('\n');
|
|
986
|
+
if (keep_comments) {
|
|
987
|
+
if (decl.leading_comments) |cmts| {
|
|
988
|
+
if (cmts.len > 0) {
|
|
989
|
+
const comments = try formatComments(alloc, cmts, true);
|
|
990
|
+
if (comments.len > 0) try result.appendSlice(comments);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
try result.appendSlice(decl.text);
|
|
995
|
+
} else {
|
|
996
|
+
const processed = try processModuleDeclaration(alloc, decl, keep_comments);
|
|
997
|
+
if (processed.len > 0) {
|
|
998
|
+
if (result.items.len > 0) try result.append('\n');
|
|
999
|
+
try result.appendSlice(processed);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
},
|
|
1003
|
+
else => {},
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// Emit value exports
|
|
1009
|
+
for (value_export_stmts.items) |stmt| {
|
|
1010
|
+
if (result.items.len > 0) try result.append('\n');
|
|
1011
|
+
try result.appendSlice(stmt);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// Emit default exports last
|
|
1015
|
+
for (default_exports.items) |stmt| {
|
|
1016
|
+
if (result.items.len > 0) try result.append('\n');
|
|
1017
|
+
try result.appendSlice(stmt);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// Append null terminator so FFI callers get a C string without extra copy.
|
|
1021
|
+
// Returned slice length does NOT include the null byte.
|
|
1022
|
+
const content_len = result.items.len;
|
|
1023
|
+
try result.append(0);
|
|
1024
|
+
const owned = try result.toOwnedSlice();
|
|
1025
|
+
return owned[0..content_len];
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// --- Tests ---
|
|
1029
|
+
test "isWordInText" {
|
|
1030
|
+
try std.testing.expect(isWordInText("Foo", "const x: Foo = 1"));
|
|
1031
|
+
try std.testing.expect(!isWordInText("Foo", "const x: FooBar = 1"));
|
|
1032
|
+
try std.testing.expect(isWordInText("Bar", "type X = Bar | Baz"));
|
|
1033
|
+
try std.testing.expect(!isWordInText("ar", "type X = Bar | Baz"));
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
test "extractTripleSlashDirectives" {
|
|
1037
|
+
const alloc = std.testing.allocator;
|
|
1038
|
+
{
|
|
1039
|
+
const source = "/// <reference types=\"node\" />\nimport { foo } from 'bar';";
|
|
1040
|
+
const directives = try extractTripleSlashDirectives(alloc, source);
|
|
1041
|
+
defer alloc.free(directives);
|
|
1042
|
+
try std.testing.expectEqual(@as(usize, 1), directives.len);
|
|
1043
|
+
try std.testing.expectEqualStrings("/// <reference types=\"node\" />", directives[0]);
|
|
1044
|
+
}
|
|
1045
|
+
}
|