@stacksjs/zig-dtsx 0.9.13 → 0.9.14
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/README.md +28 -0
- package/build.zig +1 -1
- package/package.json +2 -2
- package/src/char_utils.zig +78 -12
- package/src/emitter.zig +324 -179
- package/src/extractors.zig +724 -404
- package/src/lib.zig +35 -8
- package/src/main.zig +108 -77
- package/src/scan_loop.zig +101 -65
- package/src/scanner.zig +293 -106
- package/src/type_inference.zig +215 -130
- package/test/zig-dtsx.test.ts +5 -1
- package/zig-out/bin/zig-dtsx +0 -0
- package/zig-out/bin/zig-dtsx.exe +0 -0
package/src/emitter.zig
CHANGED
|
@@ -12,29 +12,40 @@ inline fn isIdentChar(c: u8) bool {
|
|
|
12
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
13
|
}
|
|
14
14
|
|
|
15
|
-
/// Check if `name` appears as a whole word in `text` (
|
|
16
|
-
pub
|
|
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
|
-
}
|
|
15
|
+
/// Check if `name` appears as a whole word in `text` — delegated to scanner.zig (single source of truth)
|
|
16
|
+
pub const isWordInText = @import("scanner.zig").isWordInText;
|
|
28
17
|
|
|
29
|
-
/// Extract all identifier words from text into an existing HashMap (single pass, O(n))
|
|
18
|
+
/// Extract all identifier words from text into an existing HashMap (single pass, O(n)).
|
|
19
|
+
/// Uses SIMD to skip 16 non-identifier bytes at a time.
|
|
30
20
|
fn extractWords(words: *std.StringHashMap(void), text: []const u8) void {
|
|
31
21
|
var i: usize = 0;
|
|
32
22
|
while (i < text.len) {
|
|
23
|
+
// SIMD fast-skip: tighten the "interesting byte" predicate to actual
|
|
24
|
+
// identifier-start chars. Previously the `>= 'A'` check accepted brace
|
|
25
|
+
// and bracket characters too (all in 91..96, 123..126), causing
|
|
26
|
+
// unnecessary scalar fall-throughs for type-heavy declarations.
|
|
27
|
+
while (i + 16 <= text.len) {
|
|
28
|
+
const chunk: @Vector(16, u8) = text[i..][0..16].*;
|
|
29
|
+
const upper = (chunk >= @as(@Vector(16, u8), @splat('A'))) & (chunk <= @as(@Vector(16, u8), @splat('Z')));
|
|
30
|
+
const lower = (chunk >= @as(@Vector(16, u8), @splat('a'))) & (chunk <= @as(@Vector(16, u8), @splat('z')));
|
|
31
|
+
const is_dollar = chunk == @as(@Vector(16, u8), @splat('$'));
|
|
32
|
+
const is_under = chunk == @as(@Vector(16, u8), @splat('_'));
|
|
33
|
+
const is_high = chunk > @as(@Vector(16, u8), @splat(127));
|
|
34
|
+
const interesting = upper | lower | is_dollar | is_under | is_high;
|
|
35
|
+
if (@reduce(.Or, interesting)) {
|
|
36
|
+
const bits: u16 = @bitCast(interesting);
|
|
37
|
+
i += @ctz(bits);
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
i += 16;
|
|
41
|
+
}
|
|
42
|
+
if (i >= text.len) break;
|
|
43
|
+
|
|
33
44
|
const c = text[i];
|
|
34
|
-
if ((c
|
|
45
|
+
if (ch.isIdentStart(c)) {
|
|
35
46
|
const start = i;
|
|
36
47
|
i += 1;
|
|
37
|
-
while (i < text.len and isIdentChar(text[i])) i += 1;
|
|
48
|
+
while (i < text.len and ch.isIdentChar(text[i])) i += 1;
|
|
38
49
|
words.put(text[start..i], {}) catch {};
|
|
39
50
|
} else {
|
|
40
51
|
i += 1;
|
|
@@ -45,6 +56,8 @@ fn extractWords(words: *std.StringHashMap(void), text: []const u8) void {
|
|
|
45
56
|
/// Extract triple-slash directives from source start
|
|
46
57
|
fn extractTripleSlashDirectives(alloc: std.mem.Allocator, source: []const u8) ![][]const u8 {
|
|
47
58
|
var directives = std.array_list.Managed([]const u8).init(alloc);
|
|
59
|
+
// Pre-allocate small capacity — typical files have 0-3 directives.
|
|
60
|
+
try directives.ensureTotalCapacity(2);
|
|
48
61
|
var line_start: usize = 0;
|
|
49
62
|
|
|
50
63
|
var i: usize = 0;
|
|
@@ -59,8 +72,14 @@ fn extractTripleSlashDirectives(alloc: std.mem.Allocator, source: []const u8) ![
|
|
|
59
72
|
line_start = i + 1;
|
|
60
73
|
|
|
61
74
|
if (trimmed.len >= 3 and trimmed[0] == '/' and trimmed[1] == '/' and trimmed[2] == '/') {
|
|
62
|
-
//
|
|
63
|
-
|
|
75
|
+
// Inline char-by-char check: avoids three full ch.contains scans.
|
|
76
|
+
// Look for "<reference ", "<amd-module ", "<amd-dependency ".
|
|
77
|
+
const lt = ch.indexOfChar(trimmed, '<', 3) orelse continue;
|
|
78
|
+
const tail = trimmed[lt..];
|
|
79
|
+
if ((tail.len > 11 and std.mem.eql(u8, tail[0..11], "<reference ")) or
|
|
80
|
+
(tail.len > 12 and std.mem.eql(u8, tail[0..12], "<amd-module ")) or
|
|
81
|
+
(tail.len > 16 and std.mem.eql(u8, tail[0..16], "<amd-dependency ")))
|
|
82
|
+
{
|
|
64
83
|
try directives.append(trimmed);
|
|
65
84
|
}
|
|
66
85
|
} else if (trimmed.len == 0 or (trimmed.len >= 2 and trimmed[0] == '/' and trimmed[1] == '/')) {
|
|
@@ -74,7 +93,7 @@ fn extractTripleSlashDirectives(alloc: std.mem.Allocator, source: []const u8) ![
|
|
|
74
93
|
return directives.toOwnedSlice();
|
|
75
94
|
}
|
|
76
95
|
|
|
77
|
-
/// Format leading comments —
|
|
96
|
+
/// Format leading comments — avoids allocation for the most common case (single comment).
|
|
78
97
|
fn formatComments(alloc: std.mem.Allocator, comments: ?[]const []const u8, keep_comments: bool) ![]const u8 {
|
|
79
98
|
if (!keep_comments) return "";
|
|
80
99
|
const cmts = comments orelse return "";
|
|
@@ -82,28 +101,49 @@ fn formatComments(alloc: std.mem.Allocator, comments: ?[]const []const u8, keep_
|
|
|
82
101
|
|
|
83
102
|
// Fast path: single comment (very common)
|
|
84
103
|
if (cmts.len == 1) {
|
|
85
|
-
const
|
|
104
|
+
const raw = cmts[0];
|
|
105
|
+
const t = ch.sliceTrimmed(raw, 0, raw.len);
|
|
106
|
+
// Zero-alloc fast path: if the trimmed comment is the full slice AND
|
|
107
|
+
// there's a '\n' right after it in memory, return a slice that includes
|
|
108
|
+
// the newline. This works because comments point into the source buffer.
|
|
109
|
+
if (t.len == raw.len and t.len > 0) {
|
|
110
|
+
// Check if the byte after the comment in the source buffer is '\n'
|
|
111
|
+
const end_ptr = t.ptr + t.len;
|
|
112
|
+
if (@intFromPtr(end_ptr) > 0 and end_ptr[0] == '\n') {
|
|
113
|
+
return t.ptr[0 .. t.len + 1];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
86
116
|
const buf = try alloc.alloc(u8, t.len + 1);
|
|
87
117
|
@memcpy(buf[0..t.len], t);
|
|
88
118
|
buf[t.len] = '\n';
|
|
89
119
|
return buf;
|
|
90
120
|
}
|
|
91
121
|
|
|
92
|
-
//
|
|
122
|
+
// Two-pass approach: cache trimmed slices in a small stack-allocated array
|
|
123
|
+
// when the count is reasonable, or a heap-allocated array otherwise. This
|
|
124
|
+
// avoids running sliceTrimmed twice for each comment.
|
|
125
|
+
var inline_buf: [8][]const u8 = undefined;
|
|
126
|
+
const trimmed_slices = if (cmts.len <= inline_buf.len)
|
|
127
|
+
inline_buf[0..cmts.len]
|
|
128
|
+
else
|
|
129
|
+
try alloc.alloc([]const u8, cmts.len);
|
|
130
|
+
defer if (cmts.len > inline_buf.len) alloc.free(trimmed_slices);
|
|
131
|
+
|
|
93
132
|
var total_len: usize = cmts.len; // newlines between + trailing
|
|
94
|
-
for (cmts) |c| {
|
|
133
|
+
for (cmts, 0..) |c, i| {
|
|
95
134
|
const t = ch.sliceTrimmed(c, 0, c.len);
|
|
135
|
+
trimmed_slices[i] = t;
|
|
96
136
|
total_len += t.len;
|
|
97
137
|
}
|
|
98
138
|
|
|
99
139
|
const buf = try alloc.alloc(u8, total_len);
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
140
|
+
// First slice — write without leading newline.
|
|
141
|
+
@memcpy(buf[0..trimmed_slices[0].len], trimmed_slices[0]);
|
|
142
|
+
var pos: usize = trimmed_slices[0].len;
|
|
143
|
+
// Subsequent slices — newline-separated.
|
|
144
|
+
for (trimmed_slices[1..]) |t| {
|
|
145
|
+
buf[pos] = '\n';
|
|
146
|
+
pos += 1;
|
|
107
147
|
@memcpy(buf[pos..][0..t.len], t);
|
|
108
148
|
pos += t.len;
|
|
109
149
|
}
|
|
@@ -156,10 +196,13 @@ fn processVariableDeclaration(alloc: std.mem.Allocator, decl: Declaration, keep_
|
|
|
156
196
|
// The scanner already built the correct DTS text using the type annotation,
|
|
157
197
|
// so we can return it directly without type inference or value processing.
|
|
158
198
|
if (decl.type_annotation.len > 0 and decl.value.len > 0) {
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
199
|
+
// Cheaper "ends with as const" check that skips the trim allocation.
|
|
200
|
+
// We just need to ignore trailing whitespace.
|
|
201
|
+
var ve: usize = decl.value.len;
|
|
202
|
+
while (ve > 0 and (decl.value[ve - 1] == ' ' or decl.value[ve - 1] == '\t' or
|
|
203
|
+
decl.value[ve - 1] == '\n' or decl.value[ve - 1] == '\r')) : (ve -= 1) {}
|
|
204
|
+
const ends_as_const = ve >= 8 and std.mem.eql(u8, decl.value[ve - 8 .. ve], "as const");
|
|
205
|
+
if (!ends_as_const and !ch.contains(decl.value, " satisfies ")) {
|
|
163
206
|
const kind: []const u8 = if (decl.modifiers) |mods| (if (mods.len > 0) mods[0] else "const") else "const";
|
|
164
207
|
if (!std.mem.eql(u8, kind, "const") or !type_inf.isGenericType(decl.type_annotation)) {
|
|
165
208
|
if (comments.len == 0) return decl.text;
|
|
@@ -200,18 +243,24 @@ fn processVariableDeclaration(alloc: std.mem.Allocator, decl: Declaration, keep_
|
|
|
200
243
|
if (type_annotation.len == 0) type_annotation = "unknown";
|
|
201
244
|
|
|
202
245
|
// Add @defaultValue JSDoc for widened declarations
|
|
203
|
-
// Skip when value uses 'as const' — types are already narrow/self-documenting
|
|
246
|
+
// Skip when value uses 'as const' — types are already narrow/self-documenting.
|
|
247
|
+
// Cache the trimmed value so we don't trim a second time below.
|
|
204
248
|
const val_trimmed_for_check = std.mem.trim(u8, decl.value, " \t\n\r");
|
|
205
249
|
const has_as_const = ch.endsWith(val_trimmed_for_check, "as const");
|
|
206
250
|
if (decl.value.len > 0 and decl.type_annotation.len == 0 and !has_as_const) {
|
|
207
251
|
var default_val: ?[]const u8 = null;
|
|
208
252
|
var is_container = false;
|
|
209
|
-
const trimmed_val =
|
|
253
|
+
const trimmed_val = val_trimmed_for_check;
|
|
210
254
|
|
|
211
255
|
if (!std.mem.eql(u8, kind, "const")) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
256
|
+
// Length-first dispatch — three primitive types have distinct lengths
|
|
257
|
+
// (string=6, number=6, boolean=7) so we can short-circuit before
|
|
258
|
+
// running std.mem.eql on a non-matching annotation.
|
|
259
|
+
const is_widened = switch (type_annotation.len) {
|
|
260
|
+
6 => std.mem.eql(u8, type_annotation, "string") or std.mem.eql(u8, type_annotation, "number"),
|
|
261
|
+
7 => std.mem.eql(u8, type_annotation, "boolean"),
|
|
262
|
+
else => false,
|
|
263
|
+
};
|
|
215
264
|
if (is_widened and trimmed_val.len > 0) {
|
|
216
265
|
default_val = trimmed_val;
|
|
217
266
|
}
|
|
@@ -231,7 +280,7 @@ fn processVariableDeclaration(alloc: std.mem.Allocator, decl: Declaration, keep_
|
|
|
231
280
|
const tag_mem = try alloc.alloc(u8, tag_max);
|
|
232
281
|
var tp: usize = 0;
|
|
233
282
|
|
|
234
|
-
if (std.mem.
|
|
283
|
+
if (std.mem.indexOfScalar(u8, dv, '\n')) |_| {
|
|
235
284
|
const hdr = "@defaultValue\n * ```ts\n";
|
|
236
285
|
@memcpy(tag_mem[tp..][0..hdr.len], hdr); tp += hdr.len;
|
|
237
286
|
var line_iter = std.mem.splitScalar(u8, dv, '\n');
|
|
@@ -265,7 +314,7 @@ fn processVariableDeclaration(alloc: std.mem.Allocator, decl: Declaration, keep_
|
|
|
265
314
|
var end = before_raw.len;
|
|
266
315
|
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
316
|
const before = before_raw[0..end];
|
|
268
|
-
if (before.len > 4 and ch.startsWith(before, "/** ") and std.mem.
|
|
317
|
+
if (before.len > 4 and ch.startsWith(before, "/** ") and std.mem.indexOfScalar(u8, before, '\n') == null) {
|
|
269
318
|
@memcpy(rbuf[rp..][0..7], "/**\n * "); rp += 7;
|
|
270
319
|
@memcpy(rbuf[rp..][0..before.len - 4], before[4..]); rp += before.len - 4;
|
|
271
320
|
} else {
|
|
@@ -288,7 +337,7 @@ fn processVariableDeclaration(alloc: std.mem.Allocator, decl: Declaration, keep_
|
|
|
288
337
|
@memcpy(rbuf[rp..][0..default_tag.len], default_tag); rp += default_tag.len;
|
|
289
338
|
@memcpy(rbuf[rp..][0..5], "\n */\n"); rp += 5;
|
|
290
339
|
} else {
|
|
291
|
-
if (std.mem.
|
|
340
|
+
if (std.mem.indexOfScalar(u8, default_tag, '\n') != null) {
|
|
292
341
|
@memcpy(rbuf[rp..][0..7], "/**\n * "); rp += 7;
|
|
293
342
|
@memcpy(rbuf[rp..][0..default_tag.len], default_tag); rp += default_tag.len;
|
|
294
343
|
@memcpy(rbuf[rp..][0..5], "\n */\n"); rp += 5;
|
|
@@ -496,29 +545,22 @@ fn processModuleDeclaration(alloc: std.mem.Allocator, decl: Declaration, keep_co
|
|
|
496
545
|
(decl.name.len > 0 and (decl.name[0] == '"' or decl.name[0] == '\'' or decl.name[0] == '`'));
|
|
497
546
|
|
|
498
547
|
if (is_ambient) {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
try
|
|
504
|
-
|
|
505
|
-
if (
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
return result.toOwnedSlice();
|
|
548
|
+
// Direct alloc (no ArrayList overhead)
|
|
549
|
+
const dm = "declare module ";
|
|
550
|
+
const body_ambient = if (ch.indexOfChar(decl.text, '{', 0)) |brace_idx| decl.text[brace_idx..] else "{}";
|
|
551
|
+
const total_ambient = comments.len + dm.len + decl.name.len + 1 + body_ambient.len;
|
|
552
|
+
const buf_ambient = try alloc.alloc(u8, total_ambient);
|
|
553
|
+
var ap: usize = 0;
|
|
554
|
+
if (comments.len > 0) { @memcpy(buf_ambient[ap..][0..comments.len], comments); ap += comments.len; }
|
|
555
|
+
@memcpy(buf_ambient[ap..][0..dm.len], dm); ap += dm.len;
|
|
556
|
+
@memcpy(buf_ambient[ap..][0..decl.name.len], decl.name); ap += decl.name.len;
|
|
557
|
+
buf_ambient[ap] = ' '; ap += 1;
|
|
558
|
+
@memcpy(buf_ambient[ap..][0..body_ambient.len], body_ambient); ap += body_ambient.len;
|
|
559
|
+
return buf_ambient[0..ap];
|
|
512
560
|
}
|
|
513
561
|
|
|
514
|
-
// Regular namespace
|
|
515
|
-
|
|
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
|
|
562
|
+
// Regular namespace — direct alloc (no ArrayList overhead)
|
|
563
|
+
const export_prefix: []const u8 = if (decl.is_exported) "export " else "";
|
|
522
564
|
var has_declare = false;
|
|
523
565
|
if (decl.modifiers) |mods| {
|
|
524
566
|
for (mods) |m| {
|
|
@@ -528,19 +570,20 @@ fn processModuleDeclaration(alloc: std.mem.Allocator, decl: Declaration, keep_co
|
|
|
528
570
|
}
|
|
529
571
|
}
|
|
530
572
|
}
|
|
531
|
-
if (!has_declare)
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
573
|
+
const declare_kw: []const u8 = if (!has_declare) "declare " else "";
|
|
574
|
+
const body = if (ch.indexOfChar(decl.text, '{', 0)) |brace_idx| decl.text[brace_idx..] else "{}";
|
|
575
|
+
const ns = "namespace ";
|
|
576
|
+
const total = comments.len + export_prefix.len + declare_kw.len + ns.len + decl.name.len + 1 + body.len;
|
|
577
|
+
const buf = try alloc.alloc(u8, total);
|
|
578
|
+
var pos: usize = 0;
|
|
579
|
+
if (comments.len > 0) { @memcpy(buf[pos..][0..comments.len], comments); pos += comments.len; }
|
|
580
|
+
@memcpy(buf[pos..][0..export_prefix.len], export_prefix); pos += export_prefix.len;
|
|
581
|
+
@memcpy(buf[pos..][0..declare_kw.len], declare_kw); pos += declare_kw.len;
|
|
582
|
+
@memcpy(buf[pos..][0..ns.len], ns); pos += ns.len;
|
|
583
|
+
@memcpy(buf[pos..][0..decl.name.len], decl.name); pos += decl.name.len;
|
|
584
|
+
buf[pos] = ' '; pos += 1;
|
|
585
|
+
@memcpy(buf[pos..][0..body.len], body); pos += body.len;
|
|
586
|
+
return buf[0..pos];
|
|
544
587
|
}
|
|
545
588
|
|
|
546
589
|
/// Main entry point: process declarations array into final .d.ts output.
|
|
@@ -555,7 +598,9 @@ pub fn processDeclarations(
|
|
|
555
598
|
import_order: []const []const u8,
|
|
556
599
|
) ![]const u8 {
|
|
557
600
|
var result = std.array_list.Managed(u8).init(result_alloc);
|
|
558
|
-
|
|
601
|
+
// DTS output is typically 30-60% of source size (bodies stripped, types added).
|
|
602
|
+
// Bias toward 60% + 256 byte slack to avoid the final 1-2 reallocs on average files.
|
|
603
|
+
try result.ensureTotalCapacity((source_code.len * 6) / 10 + 256);
|
|
559
604
|
|
|
560
605
|
// Extract triple-slash directives
|
|
561
606
|
// Fast check: skip whitespace
|
|
@@ -569,58 +614,94 @@ pub fn processDeclarations(
|
|
|
569
614
|
}
|
|
570
615
|
}
|
|
571
616
|
|
|
572
|
-
//
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
var
|
|
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);
|
|
617
|
+
// Index-based grouping: store indices into the declarations array instead of
|
|
618
|
+
// copying Declaration structs. A single allocation for the index array replaces
|
|
619
|
+
// 9 separate ArrayList allocations, dramatically reducing overhead for small files.
|
|
620
|
+
const GROUP_COUNT = 9;
|
|
621
|
+
// Group order: imports(0), functions(1), variables(2), interfaces(3), types(4),
|
|
622
|
+
// classes(5), enums(6), modules(7), exports(8)
|
|
623
|
+
var group_counts = [_]u32{0} ** GROUP_COUNT;
|
|
592
624
|
|
|
625
|
+
// First pass: count declarations per group
|
|
593
626
|
for (declarations) |d| {
|
|
594
|
-
switch (d.kind) {
|
|
595
|
-
.import_decl =>
|
|
596
|
-
.function_decl =>
|
|
597
|
-
.variable_decl =>
|
|
598
|
-
.interface_decl =>
|
|
599
|
-
.type_decl =>
|
|
600
|
-
.class_decl =>
|
|
601
|
-
.enum_decl =>
|
|
602
|
-
.module_decl, .namespace_decl =>
|
|
603
|
-
.export_decl =>
|
|
604
|
-
.unknown_decl =>
|
|
605
|
-
}
|
|
627
|
+
const g: usize = switch (d.kind) {
|
|
628
|
+
.import_decl => 0,
|
|
629
|
+
.function_decl => 1,
|
|
630
|
+
.variable_decl => 2,
|
|
631
|
+
.interface_decl => 3,
|
|
632
|
+
.type_decl => 4,
|
|
633
|
+
.class_decl => 5,
|
|
634
|
+
.enum_decl => 6,
|
|
635
|
+
.module_decl, .namespace_decl => 7,
|
|
636
|
+
.export_decl => 8,
|
|
637
|
+
.unknown_decl => continue,
|
|
638
|
+
};
|
|
639
|
+
group_counts[g] += 1;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Compute offsets for each group in the flat index array. Reuse the
|
|
643
|
+
// cumulative running total as the final size — saves one O(GROUP_COUNT)
|
|
644
|
+
// pass over the counts.
|
|
645
|
+
var group_offsets = [_]u32{0} ** GROUP_COUNT;
|
|
646
|
+
var total_indexed: u32 = 0;
|
|
647
|
+
for (0..GROUP_COUNT) |g| {
|
|
648
|
+
group_offsets[g] = total_indexed;
|
|
649
|
+
total_indexed += group_counts[g];
|
|
606
650
|
}
|
|
607
651
|
|
|
652
|
+
// Single allocation for all group indices
|
|
653
|
+
const group_indices = try alloc.alloc(u32, total_indexed);
|
|
654
|
+
var write_pos = [_]u32{0} ** GROUP_COUNT;
|
|
655
|
+
@memcpy(&write_pos, &group_offsets);
|
|
656
|
+
|
|
657
|
+
// Second pass: fill indices
|
|
658
|
+
for (declarations, 0..) |d, di| {
|
|
659
|
+
const g: usize = switch (d.kind) {
|
|
660
|
+
.import_decl => 0,
|
|
661
|
+
.function_decl => 1,
|
|
662
|
+
.variable_decl => 2,
|
|
663
|
+
.interface_decl => 3,
|
|
664
|
+
.type_decl => 4,
|
|
665
|
+
.class_decl => 5,
|
|
666
|
+
.enum_decl => 6,
|
|
667
|
+
.module_decl, .namespace_decl => 7,
|
|
668
|
+
.export_decl => 8,
|
|
669
|
+
.unknown_decl => continue,
|
|
670
|
+
};
|
|
671
|
+
group_indices[write_pos[g]] = @intCast(di);
|
|
672
|
+
write_pos[g] += 1;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Helper to get a group's slice of declaration indices
|
|
676
|
+
const imports_idx = group_indices[group_offsets[0]..write_pos[0]];
|
|
677
|
+
const functions_idx = group_indices[group_offsets[1]..write_pos[1]];
|
|
678
|
+
const variables_idx = group_indices[group_offsets[2]..write_pos[2]];
|
|
679
|
+
const interfaces_idx = group_indices[group_offsets[3]..write_pos[3]];
|
|
680
|
+
const type_decls_idx = group_indices[group_offsets[4]..write_pos[4]];
|
|
681
|
+
const classes_idx = group_indices[group_offsets[5]..write_pos[5]];
|
|
682
|
+
const enums_idx = group_indices[group_offsets[6]..write_pos[6]];
|
|
683
|
+
const modules_idx = group_indices[group_offsets[7]..write_pos[7]];
|
|
684
|
+
const exports_idx = group_indices[group_offsets[8]..write_pos[8]];
|
|
685
|
+
|
|
608
686
|
// Parse exports to track exported items
|
|
687
|
+
const exports_len = exports_idx.len;
|
|
609
688
|
var exported_items = std.StringHashMap(void).init(alloc);
|
|
610
|
-
try exported_items.ensureTotalCapacity(@intCast(@max(
|
|
689
|
+
try exported_items.ensureTotalCapacity(@intCast(@max(exports_len * 4, 8)));
|
|
611
690
|
var type_export_stmts = std.array_list.Managed([]const u8).init(alloc);
|
|
612
|
-
try type_export_stmts.ensureTotalCapacity(@max(
|
|
691
|
+
try type_export_stmts.ensureTotalCapacity(@max(exports_len / 2, 2));
|
|
613
692
|
var value_export_stmts = std.array_list.Managed([]const u8).init(alloc);
|
|
614
|
-
try value_export_stmts.ensureTotalCapacity(@max(
|
|
693
|
+
try value_export_stmts.ensureTotalCapacity(@max(exports_len / 2, 2));
|
|
615
694
|
var default_exports = std.array_list.Managed([]const u8).init(alloc);
|
|
616
|
-
|
|
617
|
-
|
|
695
|
+
// Use linear array for small export counts (faster than HashMap for <32 entries)
|
|
696
|
+
var seen_exports_arr = std.array_list.Managed([]const u8).init(alloc);
|
|
697
|
+
try seen_exports_arr.ensureTotalCapacity(@max(exports_len, 4));
|
|
618
698
|
|
|
619
699
|
// Reusable buffer for building export statements (avoids per-iteration alloc)
|
|
620
700
|
var stmt_buf = std.array_list.Managed(u8).init(alloc);
|
|
621
701
|
try stmt_buf.ensureTotalCapacity(256);
|
|
622
702
|
|
|
623
|
-
for (
|
|
703
|
+
for (exports_idx) |eidx| {
|
|
704
|
+
const decl = declarations[eidx];
|
|
624
705
|
const comments = try formatComments(alloc, decl.leading_comments, keep_comments);
|
|
625
706
|
|
|
626
707
|
if (ch.startsWith(decl.text, "export default")) {
|
|
@@ -644,7 +725,8 @@ pub fn processDeclarations(
|
|
|
644
725
|
if (ch.indexOfChar(export_text, '{', 0)) |brace_s| {
|
|
645
726
|
if (ch.indexOfChar(export_text, '}', brace_s)) |brace_e| {
|
|
646
727
|
const items_str = export_text[brace_s + 1 .. brace_e];
|
|
647
|
-
|
|
728
|
+
// splitScalar is faster than splitSequence for a 1-byte separator.
|
|
729
|
+
var iter = std.mem.splitScalar(u8, items_str, ',');
|
|
648
730
|
while (iter.next()) |item| {
|
|
649
731
|
const trimmed_item = ch.sliceTrimmed(item, 0, item.len);
|
|
650
732
|
if (trimmed_item.len > 0) {
|
|
@@ -654,14 +736,30 @@ pub fn processDeclarations(
|
|
|
654
736
|
}
|
|
655
737
|
}
|
|
656
738
|
|
|
739
|
+
// Determine type-only-ness BEFORE concatenation. Checking
|
|
740
|
+
// ch.contains on the full (comments + export_text) text scanned
|
|
741
|
+
// arbitrarily long comment text on every export.
|
|
742
|
+
const is_type_export = ch.startsWith(export_text, "export type");
|
|
743
|
+
|
|
657
744
|
stmt_buf.clearRetainingCapacity();
|
|
658
745
|
try stmt_buf.appendSlice(comments);
|
|
659
746
|
try stmt_buf.appendSlice(export_text);
|
|
660
747
|
const full_text = try stmt_buf.toOwnedSlice();
|
|
661
748
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
749
|
+
// Linear dedup: faster than HashMap for typical export counts (<32).
|
|
750
|
+
// Length pre-check rejects most non-matches without invoking
|
|
751
|
+
// std.mem.eql, which compares byte-by-byte even on length mismatch.
|
|
752
|
+
var is_dup = false;
|
|
753
|
+
const ft_len = full_text.len;
|
|
754
|
+
for (seen_exports_arr.items) |seen| {
|
|
755
|
+
if (seen.len == ft_len and std.mem.eql(u8, seen, full_text)) {
|
|
756
|
+
is_dup = true;
|
|
757
|
+
break;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
if (!is_dup) {
|
|
761
|
+
try seen_exports_arr.append(full_text);
|
|
762
|
+
if (is_type_export) {
|
|
665
763
|
try type_export_stmts.append(full_text);
|
|
666
764
|
} else {
|
|
667
765
|
try value_export_stmts.append(full_text);
|
|
@@ -673,15 +771,18 @@ pub fn processDeclarations(
|
|
|
673
771
|
// Short-circuit: skip combined_words building and import map when there are no imports.
|
|
674
772
|
// For import-free code (e.g. synthetic benchmarks), this avoids O(n) word
|
|
675
773
|
// extraction across all declarations — a significant saving on large inputs.
|
|
774
|
+
// Lazy-init: only allocate when there are interfaces to check
|
|
676
775
|
var interface_references = std.StringHashMap(void).init(alloc);
|
|
776
|
+
_ = &interface_references; // prevent unused warning in no-imports path
|
|
677
777
|
var processed_imports = std.array_list.Managed([]const u8).init(alloc);
|
|
678
778
|
|
|
679
|
-
if (
|
|
779
|
+
if (imports_idx.len > 0) {
|
|
680
780
|
// Build import-item-to-declaration map (only when imports exist)
|
|
681
781
|
const ImportDeclMap = std.StringHashMap(Declaration);
|
|
682
782
|
var all_imported_items_map = ImportDeclMap.init(alloc);
|
|
683
|
-
try all_imported_items_map.ensureTotalCapacity(@intCast(@max(
|
|
684
|
-
for (
|
|
783
|
+
try all_imported_items_map.ensureTotalCapacity(@intCast(@max(imports_idx.len * 4, 8)));
|
|
784
|
+
for (imports_idx) |iidx| {
|
|
785
|
+
const imp = declarations[iidx];
|
|
685
786
|
const items = extractAllImportedItems(imp);
|
|
686
787
|
for (items) |item| {
|
|
687
788
|
try all_imported_items_map.put(item, imp);
|
|
@@ -711,9 +812,10 @@ pub fn processDeclarations(
|
|
|
711
812
|
}
|
|
712
813
|
|
|
713
814
|
// Interface reference detection
|
|
714
|
-
try interface_references.ensureTotalCapacity(@intCast(@max(
|
|
715
|
-
if (
|
|
716
|
-
for (
|
|
815
|
+
try interface_references.ensureTotalCapacity(@intCast(@max(interfaces_idx.len, 4)));
|
|
816
|
+
if (interfaces_idx.len > 0 and combined_words.count() > 0) {
|
|
817
|
+
for (interfaces_idx) |iidx| {
|
|
818
|
+
const iface = declarations[iidx];
|
|
717
819
|
if (combined_words.contains(iface.name)) {
|
|
718
820
|
try interface_references.put(iface.name, {});
|
|
719
821
|
}
|
|
@@ -721,13 +823,14 @@ pub fn processDeclarations(
|
|
|
721
823
|
}
|
|
722
824
|
|
|
723
825
|
// Add interface text to combined_words (after ref detection, before import filtering)
|
|
724
|
-
for (
|
|
826
|
+
for (interfaces_idx) |iidx| {
|
|
827
|
+
const iface = declarations[iidx];
|
|
725
828
|
if (iface.is_exported or interface_references.contains(iface.name)) {
|
|
726
829
|
extractWords(&combined_words, iface.text);
|
|
727
830
|
}
|
|
728
831
|
}
|
|
729
832
|
var used_import_items = std.StringHashMap(void).init(alloc);
|
|
730
|
-
try used_import_items.ensureTotalCapacity(@intCast(@max(
|
|
833
|
+
try used_import_items.ensureTotalCapacity(@intCast(@max(imports_idx.len * 4, 8)));
|
|
731
834
|
|
|
732
835
|
if (combined_words.count() > 0) {
|
|
733
836
|
var map_iter = all_imported_items_map.keyIterator();
|
|
@@ -747,14 +850,15 @@ pub fn processDeclarations(
|
|
|
747
850
|
}
|
|
748
851
|
|
|
749
852
|
// Filter and rebuild imports
|
|
750
|
-
try processed_imports.ensureTotalCapacity(
|
|
853
|
+
try processed_imports.ensureTotalCapacity(imports_idx.len);
|
|
751
854
|
// Reusable buffer for building import statements
|
|
752
855
|
var import_buf = std.array_list.Managed(u8).init(alloc);
|
|
753
856
|
try import_buf.ensureTotalCapacity(256);
|
|
754
857
|
var used_named = std.array_list.Managed([]const u8).init(alloc);
|
|
755
858
|
try used_named.ensureTotalCapacity(16);
|
|
756
859
|
|
|
757
|
-
for (
|
|
860
|
+
for (imports_idx) |iidx| {
|
|
861
|
+
const imp = declarations[iidx];
|
|
758
862
|
// Preserve side-effect imports
|
|
759
863
|
if (imp.is_side_effect) {
|
|
760
864
|
const trimmed_imp = ch.sliceTrimmed(imp.text, 0, imp.text.len);
|
|
@@ -795,16 +899,20 @@ pub fn processDeclarations(
|
|
|
795
899
|
if (parsed.default_name) |dn| try import_buf.appendSlice(dn);
|
|
796
900
|
if (used_named.items.len > 0) {
|
|
797
901
|
try import_buf.appendSlice(", { ");
|
|
798
|
-
|
|
799
|
-
|
|
902
|
+
// Emit first item, then comma-prefixed subsequent items —
|
|
903
|
+
// skips the per-iteration `if (idx > 0)` branch.
|
|
904
|
+
try import_buf.appendSlice(used_named.items[0]);
|
|
905
|
+
for (used_named.items[1..]) |ni| {
|
|
906
|
+
try import_buf.appendSlice(", ");
|
|
800
907
|
try import_buf.appendSlice(ni);
|
|
801
908
|
}
|
|
802
909
|
try import_buf.appendSlice(" }");
|
|
803
910
|
}
|
|
804
911
|
} else if (used_named.items.len > 0) {
|
|
805
912
|
try import_buf.appendSlice("{ ");
|
|
806
|
-
|
|
807
|
-
|
|
913
|
+
try import_buf.appendSlice(used_named.items[0]);
|
|
914
|
+
for (used_named.items[1..]) |ni| {
|
|
915
|
+
try import_buf.appendSlice(", ");
|
|
808
916
|
try import_buf.appendSlice(ni);
|
|
809
917
|
}
|
|
810
918
|
try import_buf.appendSlice(" }");
|
|
@@ -818,61 +926,70 @@ pub fn processDeclarations(
|
|
|
818
926
|
}
|
|
819
927
|
}
|
|
820
928
|
|
|
821
|
-
// Sort imports by priority then locale-aware alphabetical
|
|
929
|
+
// Sort imports by priority then locale-aware alphabetical.
|
|
930
|
+
// Pre-compute priorities once (avoids re-scanning during O(n log n) sort).
|
|
822
931
|
if (processed_imports.items.len > 1) {
|
|
823
|
-
const
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
if (found) return idx;
|
|
932
|
+
const default_priority = import_order.len;
|
|
933
|
+
const priorities = try alloc.alloc(usize, processed_imports.items.len);
|
|
934
|
+
for (processed_imports.items, 0..) |imp, pi| {
|
|
935
|
+
// Locate the source-string slice. Single scan via indexOf for
|
|
936
|
+
// the " from " marker, then dispatch on the quote byte we find
|
|
937
|
+
// there — avoids two separate searches for the 7-byte needle.
|
|
938
|
+
var src_start: usize = 0;
|
|
939
|
+
var src_end: usize = imp.len;
|
|
940
|
+
if (ch.indexOf(imp, " from ", 0)) |fi| {
|
|
941
|
+
var qi = fi + 6;
|
|
942
|
+
while (qi < imp.len and (imp[qi] == ' ' or imp[qi] == '\t')) qi += 1;
|
|
943
|
+
if (qi < imp.len and (imp[qi] == '\'' or imp[qi] == '"')) {
|
|
944
|
+
const quote = imp[qi];
|
|
945
|
+
src_start = qi + 1;
|
|
946
|
+
if (ch.indexOfChar(imp, quote, src_start)) |qe| src_end = qe;
|
|
839
947
|
}
|
|
840
|
-
return self.default_priority;
|
|
841
948
|
}
|
|
949
|
+
const src = if (src_start < src_end) imp[src_start..src_end] else imp;
|
|
950
|
+
var prio = default_priority;
|
|
951
|
+
for (import_order, 0..) |p, idx| {
|
|
952
|
+
if (ch.indexOf(src, p, 0) != null) { prio = idx; break; }
|
|
953
|
+
}
|
|
954
|
+
priorities[pi] = prio;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Sort import indices by (priority, locale-order)
|
|
958
|
+
const indices = try alloc.alloc(usize, processed_imports.items.len);
|
|
959
|
+
for (0..indices.len) |ii| indices[ii] = ii;
|
|
960
|
+
|
|
961
|
+
const SortCtx2 = struct {
|
|
962
|
+
prios: []const usize,
|
|
963
|
+
items: []const []const u8,
|
|
842
964
|
|
|
843
|
-
/// Locale-aware char sort key: symbols < digits < letters
|
|
844
|
-
/// Matches JavaScript's localeCompare behavior
|
|
845
965
|
fn charSortKey(c_val: u8) u32 {
|
|
846
966
|
if (c_val >= 'a' and c_val <= 'z') return @as(u32, c_val - 'a') * 4 + 1000;
|
|
847
967
|
if (c_val >= 'A' and c_val <= 'Z') return @as(u32, c_val - 'A') * 4 + 1001;
|
|
848
968
|
if (c_val >= '0' and c_val <= '9') return @as(u32, c_val - '0') + 500;
|
|
849
969
|
return @as(u32, c_val);
|
|
850
970
|
}
|
|
971
|
+
};
|
|
851
972
|
|
|
852
|
-
|
|
973
|
+
const sort_ctx = SortCtx2{ .prios = priorities, .items = processed_imports.items };
|
|
974
|
+
std.mem.sort(usize, indices, sort_ctx, struct {
|
|
975
|
+
fn lessThan(ctx: SortCtx2, ai: usize, bi: usize) bool {
|
|
976
|
+
if (ctx.prios[ai] != ctx.prios[bi]) return ctx.prios[ai] < ctx.prios[bi];
|
|
977
|
+
const a = ctx.items[ai];
|
|
978
|
+
const b = ctx.items[bi];
|
|
853
979
|
const min_len = @min(a.len, b.len);
|
|
854
980
|
for (0..min_len) |i| {
|
|
855
|
-
const ak = charSortKey(a[i]);
|
|
856
|
-
const bk = charSortKey(b[i]);
|
|
981
|
+
const ak = SortCtx2.charSortKey(a[i]);
|
|
982
|
+
const bk = SortCtx2.charSortKey(b[i]);
|
|
857
983
|
if (ak != bk) return ak < bk;
|
|
858
984
|
}
|
|
859
985
|
return a.len < b.len;
|
|
860
986
|
}
|
|
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
987
|
}.lessThan);
|
|
988
|
+
|
|
989
|
+
// Reorder in-place using sorted indices
|
|
990
|
+
const sorted = try alloc.alloc([]const u8, processed_imports.items.len);
|
|
991
|
+
for (indices, 0..) |idx_val, di| sorted[di] = processed_imports.items[idx_val];
|
|
992
|
+
@memcpy(processed_imports.items, sorted);
|
|
876
993
|
}
|
|
877
994
|
}
|
|
878
995
|
|
|
@@ -889,18 +1006,19 @@ pub fn processDeclarations(
|
|
|
889
1006
|
}
|
|
890
1007
|
|
|
891
1008
|
// Emit declaration groups: functions, variables, interfaces, types, classes, enums, modules
|
|
892
|
-
const
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
1009
|
+
const decl_group_indices = [_][]const u32{
|
|
1010
|
+
functions_idx,
|
|
1011
|
+
variables_idx,
|
|
1012
|
+
interfaces_idx,
|
|
1013
|
+
type_decls_idx,
|
|
1014
|
+
classes_idx,
|
|
1015
|
+
enums_idx,
|
|
1016
|
+
modules_idx,
|
|
900
1017
|
};
|
|
901
1018
|
|
|
902
|
-
for (
|
|
903
|
-
for (
|
|
1019
|
+
for (decl_group_indices) |group_idx| {
|
|
1020
|
+
for (group_idx) |didx| {
|
|
1021
|
+
const decl = declarations[didx];
|
|
904
1022
|
switch (decl.kind) {
|
|
905
1023
|
.function_decl, .class_decl => {
|
|
906
1024
|
// Direct emit: write comments + text straight to result buffer
|
|
@@ -1043,3 +1161,30 @@ test "extractTripleSlashDirectives" {
|
|
|
1043
1161
|
try std.testing.expectEqualStrings("/// <reference types=\"node\" />", directives[0]);
|
|
1044
1162
|
}
|
|
1045
1163
|
}
|
|
1164
|
+
|
|
1165
|
+
test "extractTripleSlashDirectives recognizes amd-module and amd-dependency" {
|
|
1166
|
+
const alloc = std.testing.allocator;
|
|
1167
|
+
const src = "/// <amd-module name=\"m\" />\n/// <amd-dependency path=\"./d\" />\nexport {};";
|
|
1168
|
+
const directives = try extractTripleSlashDirectives(alloc, src);
|
|
1169
|
+
defer alloc.free(directives);
|
|
1170
|
+
try std.testing.expectEqual(@as(usize, 2), directives.len);
|
|
1171
|
+
try std.testing.expect(std.mem.indexOf(u8, directives[0], "amd-module") != null);
|
|
1172
|
+
try std.testing.expect(std.mem.indexOf(u8, directives[1], "amd-dependency") != null);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
test "extractTripleSlashDirectives skips triple-slash comments without `<` (fast pre-filter)" {
|
|
1176
|
+
const alloc = std.testing.allocator;
|
|
1177
|
+
const src = "/// just a comment, no angle bracket\nexport {};";
|
|
1178
|
+
const directives = try extractTripleSlashDirectives(alloc, src);
|
|
1179
|
+
defer alloc.free(directives);
|
|
1180
|
+
try std.testing.expectEqual(@as(usize, 0), directives.len);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
test "extractTripleSlashDirectives stops at first non-comment statement" {
|
|
1184
|
+
const alloc = std.testing.allocator;
|
|
1185
|
+
const src = "/// <reference types=\"node\" />\nexport const x = 1;\n/// <reference types=\"bun\" />";
|
|
1186
|
+
const directives = try extractTripleSlashDirectives(alloc, src);
|
|
1187
|
+
defer alloc.free(directives);
|
|
1188
|
+
try std.testing.expectEqual(@as(usize, 1), directives.len);
|
|
1189
|
+
try std.testing.expect(std.mem.indexOf(u8, directives[0], "node") != null);
|
|
1190
|
+
}
|