@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/type_inference.zig
CHANGED
|
@@ -83,6 +83,9 @@ fn countOccurrences(haystack: []const u8, needle: []const u8) usize {
|
|
|
83
83
|
/// Parse array elements handling nested structures.
|
|
84
84
|
/// Returns slices into the original content string.
|
|
85
85
|
pub fn parseArrayElements(alloc: std.mem.Allocator, content: []const u8) InferError![][]const u8 {
|
|
86
|
+
// Fast path: empty content
|
|
87
|
+
if (content.len == 0) return &.{};
|
|
88
|
+
|
|
86
89
|
var elements = std.array_list.Managed([]const u8).init(alloc);
|
|
87
90
|
// Pre-size: estimate element count from top-level commas
|
|
88
91
|
var est: usize = 1;
|
|
@@ -132,16 +135,17 @@ pub fn parseArrayElements(alloc: std.mem.Allocator, content: []const u8) InferEr
|
|
|
132
135
|
try elements.append(last);
|
|
133
136
|
}
|
|
134
137
|
|
|
135
|
-
|
|
138
|
+
// toOwnedSlice() trims unused capacity — important for non-arena callers.
|
|
139
|
+
return elements.toOwnedSlice();
|
|
136
140
|
}
|
|
137
141
|
|
|
138
142
|
/// Clean a method signature: strip async, replace defaults with ?, collapse whitespace.
|
|
139
143
|
/// Single-pass implementation combining all transformations.
|
|
140
144
|
fn cleanMethodSignature(alloc: std.mem.Allocator, signature: []const u8) InferError![]const u8 {
|
|
141
145
|
var input = signature;
|
|
142
|
-
// Remove leading "async "
|
|
146
|
+
// Remove leading "async " (6 chars including the trailing space)
|
|
143
147
|
if (ch.startsWith(input, "async ")) {
|
|
144
|
-
input = trim(input[
|
|
148
|
+
input = trim(input[6..]);
|
|
145
149
|
}
|
|
146
150
|
|
|
147
151
|
// Fast path: if no async, no defaults (=), no consecutive whitespace, return as-is
|
|
@@ -238,7 +242,11 @@ fn cleanMethodSignature(alloc: std.mem.Allocator, signature: []const u8) InferEr
|
|
|
238
242
|
/// Strip 'async' keywords from a string without collapsing whitespace.
|
|
239
243
|
/// Used when we want to remove async modifiers but preserve multiline formatting.
|
|
240
244
|
fn stripAsyncKeyword(alloc: std.mem.Allocator, input: []const u8) InferError![]const u8 {
|
|
245
|
+
// Fast-path: no "async" substring at all → return input unchanged (zero-copy).
|
|
246
|
+
if (std.mem.indexOf(u8, input, "async") == null) return input;
|
|
247
|
+
|
|
241
248
|
var buf = std.array_list.Managed(u8).init(alloc);
|
|
249
|
+
try buf.ensureTotalCapacity(input.len);
|
|
242
250
|
var j: usize = 0;
|
|
243
251
|
// Remove leading "async "
|
|
244
252
|
if (ch.startsWith(input, "async ")) {
|
|
@@ -260,7 +268,9 @@ fn stripAsyncKeyword(alloc: std.mem.Allocator, input: []const u8) InferError![]c
|
|
|
260
268
|
try buf.append(input[j]);
|
|
261
269
|
j += 1;
|
|
262
270
|
}
|
|
263
|
-
|
|
271
|
+
// toOwnedSlice trims the unused capacity — important when the caller
|
|
272
|
+
// wraps this in a non-arena allocator.
|
|
273
|
+
return buf.toOwnedSlice();
|
|
264
274
|
}
|
|
265
275
|
|
|
266
276
|
/// Convert a method definition to a function type.
|
|
@@ -268,9 +278,9 @@ fn stripAsyncKeyword(alloc: std.mem.Allocator, input: []const u8) InferError![]c
|
|
|
268
278
|
/// Output: "generics(params) => ReturnType"
|
|
269
279
|
fn convertMethodToFunctionType(alloc: std.mem.Allocator, key: []const u8, method_def: []const u8) InferError![]const u8 {
|
|
270
280
|
var cleaned = method_def;
|
|
271
|
-
// Remove leading async
|
|
281
|
+
// Remove leading async (6 chars including the trailing space)
|
|
272
282
|
if (ch.startsWith(cleaned, "async ")) {
|
|
273
|
-
cleaned = trim(cleaned[
|
|
283
|
+
cleaned = trim(cleaned[6..]);
|
|
274
284
|
}
|
|
275
285
|
|
|
276
286
|
// Extract generics from key (e.g., "onSuccess<T>" -> generics = "<T>")
|
|
@@ -323,7 +333,10 @@ fn convertMethodToFunctionType(alloc: std.mem.Allocator, key: []const u8, method
|
|
|
323
333
|
|
|
324
334
|
/// Clean parameter defaults: replace `param = value` with `param?`
|
|
325
335
|
fn cleanParameterDefaults(alloc: std.mem.Allocator, params: []const u8) InferError![]const u8 {
|
|
336
|
+
// Fast path: if there's no '=' anywhere, the input is already clean.
|
|
337
|
+
if (std.mem.indexOfScalar(u8, params, '=') == null) return params;
|
|
326
338
|
var buf = std.array_list.Managed(u8).init(alloc);
|
|
339
|
+
try buf.ensureTotalCapacity(params.len);
|
|
327
340
|
var j: usize = 0;
|
|
328
341
|
while (j < params.len) {
|
|
329
342
|
// Try to match word= pattern (not =>)
|
|
@@ -356,7 +369,8 @@ fn cleanParameterDefaults(alloc: std.mem.Allocator, params: []const u8) InferErr
|
|
|
356
369
|
j += 1;
|
|
357
370
|
}
|
|
358
371
|
}
|
|
359
|
-
|
|
372
|
+
// toOwnedSlice() trims unused capacity — safer for non-arena allocators.
|
|
373
|
+
return buf.toOwnedSlice();
|
|
360
374
|
}
|
|
361
375
|
|
|
362
376
|
/// Parse object properties from content between braces.
|
|
@@ -385,10 +399,11 @@ fn parseObjectProperties(alloc: std.mem.Allocator, content: []const u8) InferErr
|
|
|
385
399
|
const prev = if (i > 0) content[i - 1] else @as(u8, 0);
|
|
386
400
|
const next = if (i + 1 < content.len) content[i + 1] else @as(u8, 0);
|
|
387
401
|
|
|
388
|
-
// Track single-line comments —
|
|
402
|
+
// Track single-line comments — SIMD scan to end of line via indexOfChar
|
|
403
|
+
// instead of a byte-by-byte loop.
|
|
389
404
|
if (!in_string and !in_comment and c == '/' and next == '/') {
|
|
390
|
-
|
|
391
|
-
|
|
405
|
+
const nl = ch.indexOfChar(content, '\n', i + 2);
|
|
406
|
+
i = if (nl) |n| n else content.len;
|
|
392
407
|
// Update current_start if in key mode so the key slice doesn't include comment text
|
|
393
408
|
if (in_key and i < content.len) {
|
|
394
409
|
current_start = i + 1;
|
|
@@ -479,7 +494,8 @@ fn parseObjectProperties(alloc: std.mem.Allocator, content: []const u8) InferErr
|
|
|
479
494
|
}
|
|
480
495
|
}
|
|
481
496
|
|
|
482
|
-
|
|
497
|
+
// toOwnedSlice() trims unused capacity — important for non-arena callers.
|
|
498
|
+
return properties.toOwnedSlice();
|
|
483
499
|
}
|
|
484
500
|
|
|
485
501
|
/// Find matching bracket (open/close) starting from `start`, skipping strings and comments.
|
|
@@ -488,33 +504,30 @@ fn findMatchingBracket(str: []const u8, start: usize, open: u8, close: u8) ?usiz
|
|
|
488
504
|
var i = start;
|
|
489
505
|
while (i < str.len) : (i += 1) {
|
|
490
506
|
const c = str[i];
|
|
491
|
-
// Skip string literals
|
|
507
|
+
// Skip string literals — handle backslash escapes, otherwise SIMD-scan
|
|
508
|
+
// for the next quote/backslash byte.
|
|
492
509
|
if (c == '"' or c == '\'' or c == '`') {
|
|
493
510
|
i += 1;
|
|
494
|
-
while (i < str.len)
|
|
495
|
-
if (str[i] == '\\') {
|
|
496
|
-
i += 1; // skip escaped char
|
|
497
|
-
continue;
|
|
498
|
-
}
|
|
511
|
+
while (i < str.len) {
|
|
512
|
+
if (str[i] == '\\') { i += 2; continue; }
|
|
499
513
|
if (str[i] == c) break;
|
|
514
|
+
i += 1;
|
|
500
515
|
}
|
|
501
516
|
continue;
|
|
502
517
|
}
|
|
503
|
-
// Skip line comments
|
|
518
|
+
// Skip line comments — SIMD-scan to the next newline instead of walking
|
|
519
|
+
// bytes one at a time.
|
|
504
520
|
if (c == '/' and i + 1 < str.len and str[i + 1] == '/') {
|
|
505
|
-
i
|
|
506
|
-
|
|
521
|
+
const nl = ch.indexOfChar(str, '\n', i + 2);
|
|
522
|
+
i = if (nl) |n| n else str.len;
|
|
523
|
+
if (i == str.len) break;
|
|
507
524
|
continue;
|
|
508
525
|
}
|
|
509
|
-
// Skip block comments
|
|
526
|
+
// Skip block comments — indexOf jumps directly to `*/` via SIMD first-byte scan.
|
|
510
527
|
if (c == '/' and i + 1 < str.len and str[i + 1] == '*') {
|
|
511
|
-
i
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
i += 1;
|
|
515
|
-
break;
|
|
516
|
-
}
|
|
517
|
-
}
|
|
528
|
+
const end = ch.indexOf(str, "*/", i + 2);
|
|
529
|
+
i = if (end) |e| e + 1 else str.len;
|
|
530
|
+
if (i == str.len) break;
|
|
518
531
|
continue;
|
|
519
532
|
}
|
|
520
533
|
if (c == open) {
|
|
@@ -539,7 +552,6 @@ fn findMainArrowIndex(str: []const u8) ?usize {
|
|
|
539
552
|
var i: usize = 0;
|
|
540
553
|
while (i + 1 < str.len) : (i += 1) {
|
|
541
554
|
const c = str[i];
|
|
542
|
-
const prev = if (i > 0) str[i - 1] else @as(u8, 0);
|
|
543
555
|
|
|
544
556
|
if (in_string) {
|
|
545
557
|
if (c == '\\') {
|
|
@@ -553,7 +565,6 @@ fn findMainArrowIndex(str: []const u8) ?usize {
|
|
|
553
565
|
if (c == '"' or c == '\'' or c == '`') {
|
|
554
566
|
in_string = true;
|
|
555
567
|
string_char = c;
|
|
556
|
-
_ = prev;
|
|
557
568
|
continue;
|
|
558
569
|
}
|
|
559
570
|
|
|
@@ -622,14 +633,34 @@ pub fn inferNarrowType(alloc: std.mem.Allocator, value: []const u8, is_const: bo
|
|
|
622
633
|
const trimmed = trim(value);
|
|
623
634
|
if (trimmed.len == 0) return "unknown";
|
|
624
635
|
|
|
636
|
+
// Fast path: if first char is a digit, it's almost certainly a number or BigInt literal
|
|
637
|
+
if (trimmed[0] >= '0' and trimmed[0] <= '9') {
|
|
638
|
+
if (isNumericLiteral(trimmed)) {
|
|
639
|
+
return if (!is_const) "number" else trimmed;
|
|
640
|
+
}
|
|
641
|
+
// BigInt literal: digits followed by 'n'
|
|
642
|
+
if (trimmed.len > 1 and trimmed[trimmed.len - 1] == 'n' and isBigIntDigits(trimmed)) {
|
|
643
|
+
return if (is_const) trimmed else "bigint";
|
|
644
|
+
}
|
|
645
|
+
return "unknown";
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Fast path: negative numbers
|
|
649
|
+
if (trimmed[0] == '-' and trimmed.len > 1 and trimmed[1] >= '0' and trimmed[1] <= '9') {
|
|
650
|
+
if (isNumericLiteral(trimmed)) {
|
|
651
|
+
return if (!is_const) "number" else trimmed;
|
|
652
|
+
}
|
|
653
|
+
return "unknown";
|
|
654
|
+
}
|
|
655
|
+
|
|
625
656
|
// BigInt expressions
|
|
626
657
|
if (ch.startsWith(trimmed, "BigInt(")) return "bigint";
|
|
627
658
|
|
|
628
659
|
// Symbol.for
|
|
629
660
|
if (ch.startsWith(trimmed, "Symbol.for(")) return "symbol";
|
|
630
661
|
|
|
631
|
-
// Single-pass scan for substring hints
|
|
632
|
-
const hints = ValueHints.scan(trimmed);
|
|
662
|
+
// Single-pass scan for substring hints — skip for short values where hints can't appear
|
|
663
|
+
const hints = if (trimmed.len >= 4) ValueHints.scan(trimmed) else ValueHints{};
|
|
633
664
|
|
|
634
665
|
// Tagged template literals
|
|
635
666
|
if (hints.has_raw_template) return "string";
|
|
@@ -653,15 +684,17 @@ pub fn inferNarrowType(alloc: std.mem.Allocator, value: []const u8, is_const: bo
|
|
|
653
684
|
return trimmed;
|
|
654
685
|
}
|
|
655
686
|
|
|
656
|
-
// Boolean literals
|
|
657
|
-
if (
|
|
658
|
-
if (!is_const)
|
|
659
|
-
|
|
687
|
+
// Boolean literals (length-first to skip most comparisons)
|
|
688
|
+
if (trimmed.len == 4 and std.mem.eql(u8, trimmed, "true")) {
|
|
689
|
+
return if (!is_const) "boolean" else trimmed;
|
|
690
|
+
}
|
|
691
|
+
if (trimmed.len == 5 and std.mem.eql(u8, trimmed, "false")) {
|
|
692
|
+
return if (!is_const) "boolean" else trimmed;
|
|
660
693
|
}
|
|
661
694
|
|
|
662
|
-
// Null and undefined
|
|
663
|
-
if (std.mem.eql(u8, trimmed, "null")) return "null";
|
|
664
|
-
if (std.mem.eql(u8, trimmed, "undefined")) return "undefined";
|
|
695
|
+
// Null and undefined (length-first)
|
|
696
|
+
if (trimmed.len == 4 and std.mem.eql(u8, trimmed, "null")) return "null";
|
|
697
|
+
if (trimmed.len == 9 and std.mem.eql(u8, trimmed, "undefined")) return "undefined";
|
|
665
698
|
|
|
666
699
|
// Array literals
|
|
667
700
|
if (trimmed[0] == '[' and trimmed[trimmed.len - 1] == ']') {
|
|
@@ -871,14 +904,9 @@ pub fn inferArrayType(alloc: std.mem.Allocator, value: []const u8, is_const: boo
|
|
|
871
904
|
try unique_set.put(t, {});
|
|
872
905
|
try unique.append(t);
|
|
873
906
|
}
|
|
874
|
-
// Literal check
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
std.mem.eql(u8, t, "true") or std.mem.eql(u8, t, "false") or
|
|
878
|
-
(t.len >= 2 and t[0] == '"' and t[t.len - 1] == '"') or
|
|
879
|
-
(t.len >= 2 and t[0] == '\'' and t[t.len - 1] == '\'');
|
|
880
|
-
if (!is_literal) all_literals = false;
|
|
881
|
-
}
|
|
907
|
+
// Literal check — reuse isPrimitiveLiteral, which first-byte-dispatches
|
|
908
|
+
// through string/boolean/numeric branches without retesting other kinds.
|
|
909
|
+
if (all_literals and !isPrimitiveLiteral(t)) all_literals = false;
|
|
882
910
|
}
|
|
883
911
|
|
|
884
912
|
if (all_literals and types.len <= 10) {
|
|
@@ -911,16 +939,28 @@ pub fn inferArrayType(alloc: std.mem.Allocator, value: []const u8, is_const: boo
|
|
|
911
939
|
|
|
912
940
|
/// Check if a value string is a primitive literal (number, string, boolean)
|
|
913
941
|
fn isPrimitiveLiteral(val: []const u8) bool {
|
|
914
|
-
if (
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
942
|
+
if (val.len == 0) return false;
|
|
943
|
+
// First-byte dispatch: each kind of literal has a distinct prefix byte —
|
|
944
|
+
// saves running every check on every value.
|
|
945
|
+
return switch (val[0]) {
|
|
946
|
+
'"' => val.len >= 2 and val[val.len - 1] == '"',
|
|
947
|
+
'\'' => val.len >= 2 and val[val.len - 1] == '\'',
|
|
948
|
+
't' => val.len == 4 and std.mem.eql(u8, val, "true"),
|
|
949
|
+
'f' => val.len == 5 and std.mem.eql(u8, val, "false"),
|
|
950
|
+
'-', '0'...'9' => isNumericLiteral(val),
|
|
951
|
+
else => false,
|
|
952
|
+
};
|
|
919
953
|
}
|
|
920
954
|
|
|
921
955
|
/// Check if a type is a base/widened type
|
|
922
956
|
fn isBaseType(t: []const u8) bool {
|
|
923
|
-
|
|
957
|
+
// Length-first dispatch: number=6, string=6, boolean=7. Reject all other
|
|
958
|
+
// lengths in O(1) without invoking std.mem.eql.
|
|
959
|
+
return switch (t.len) {
|
|
960
|
+
6 => std.mem.eql(u8, t, "number") or std.mem.eql(u8, t, "string"),
|
|
961
|
+
7 => std.mem.eql(u8, t, "boolean"),
|
|
962
|
+
else => false,
|
|
963
|
+
};
|
|
924
964
|
}
|
|
925
965
|
|
|
926
966
|
/// Check if an array literal only contains primitives/nested arrays/objects (no runtime expressions)
|
|
@@ -960,20 +1000,19 @@ fn isSimpleArrayDefault(val: []const u8) bool {
|
|
|
960
1000
|
continue;
|
|
961
1001
|
}
|
|
962
1002
|
if (j < val.len and val[j] == '(') return false; // function call
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
std.mem.eql(u8, word, "
|
|
968
|
-
std.mem.eql(u8, word, "
|
|
969
|
-
std.mem.eql(u8, word, "
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
!std.mem.eql(u8, word, "as")) return false;
|
|
1003
|
+
// Identifier classification dispatched on first byte — replaces 13
|
|
1004
|
+
// sequential std.mem.eql calls. Anything not on the allow-list (true,
|
|
1005
|
+
// false, null, undefined, const, as) is treated as a runtime reference.
|
|
1006
|
+
const word_ok = wo: switch (word[0]) {
|
|
1007
|
+
't' => break :wo std.mem.eql(u8, word, "true"),
|
|
1008
|
+
'f' => break :wo std.mem.eql(u8, word, "false"),
|
|
1009
|
+
'n' => break :wo std.mem.eql(u8, word, "null"),
|
|
1010
|
+
'u' => break :wo std.mem.eql(u8, word, "undefined"),
|
|
1011
|
+
'c' => break :wo std.mem.eql(u8, word, "const"),
|
|
1012
|
+
'a' => break :wo std.mem.eql(u8, word, "as"),
|
|
1013
|
+
else => break :wo false,
|
|
1014
|
+
};
|
|
1015
|
+
if (!word_ok) return false;
|
|
977
1016
|
if (i > 0) i -= 1; // back up since outer loop will increment
|
|
978
1017
|
}
|
|
979
1018
|
}
|
|
@@ -1084,11 +1123,18 @@ pub fn inferObjectType(alloc: std.mem.Allocator, value: []const u8, is_const: bo
|
|
|
1084
1123
|
const nested_default = _clean_default_result;
|
|
1085
1124
|
_clean_default_result = saved_default; // restore parent's
|
|
1086
1125
|
|
|
1087
|
-
// Clean method signatures in inferred types
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1126
|
+
// Clean method signatures in inferred types — single scan for the
|
|
1127
|
+
// first interesting byte. Most val_types are bare types like "string"
|
|
1128
|
+
// or "number" that contain neither marker.
|
|
1129
|
+
if (ch.indexOfChar(val_type, '=', 0)) |_| {
|
|
1130
|
+
// Confirm '=>' rather than just '='
|
|
1131
|
+
if (ch.indexOf(val_type, "=>", 0) != null) {
|
|
1132
|
+
val_type = try cleanMethodSignature(alloc, val_type);
|
|
1133
|
+
}
|
|
1134
|
+
} else if (ch.indexOfChar(val_type, 'a', 0)) |_| {
|
|
1135
|
+
if (ch.indexOf(val_type, "async", 0) != null) {
|
|
1136
|
+
val_type = try stripAsyncKeyword(alloc, val_type);
|
|
1137
|
+
}
|
|
1092
1138
|
}
|
|
1093
1139
|
|
|
1094
1140
|
// Add inline @defaultValue for widened primitive properties
|
|
@@ -1159,23 +1205,29 @@ pub fn inferObjectType(alloc: std.mem.Allocator, value: []const u8, is_const: bo
|
|
|
1159
1205
|
const indent = if (depth > 0) (depth - 1) / 2 else 0;
|
|
1160
1206
|
const pad_size = (indent + 1) * 2;
|
|
1161
1207
|
const close_pad_size = indent * 2;
|
|
1162
|
-
|
|
1163
|
-
|
|
1208
|
+
// Pre-compute total length so we can do one alloc + memcpy/memset.
|
|
1209
|
+
var total: usize = 2; // "{\n"
|
|
1210
|
+
for (clean_props.items) |item| total += pad_size + item.len + 1; // pad + item + '\n'
|
|
1211
|
+
// Each non-final line gets a comma before its newline.
|
|
1212
|
+
if (clean_props.items.len > 0) total += clean_props.items.len - 1;
|
|
1213
|
+
total += close_pad_size + 1; // close pad + '}'
|
|
1214
|
+
|
|
1215
|
+
const ml_buf = try alloc.alloc(u8, total);
|
|
1216
|
+
var mp: usize = 0;
|
|
1217
|
+
ml_buf[mp] = '{'; mp += 1;
|
|
1218
|
+
ml_buf[mp] = '\n'; mp += 1;
|
|
1164
1219
|
for (clean_props.items, 0..) |item, ci| {
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
try ml.append('\n');
|
|
1172
|
-
}
|
|
1173
|
-
{
|
|
1174
|
-
var p: usize = 0;
|
|
1175
|
-
while (p < close_pad_size) : (p += 1) try ml.append(' ');
|
|
1220
|
+
@memset(ml_buf[mp..][0..pad_size], ' ');
|
|
1221
|
+
mp += pad_size;
|
|
1222
|
+
@memcpy(ml_buf[mp..][0..item.len], item);
|
|
1223
|
+
mp += item.len;
|
|
1224
|
+
if (ci < clean_props.items.len - 1) { ml_buf[mp] = ','; mp += 1; }
|
|
1225
|
+
ml_buf[mp] = '\n'; mp += 1;
|
|
1176
1226
|
}
|
|
1177
|
-
|
|
1178
|
-
|
|
1227
|
+
@memset(ml_buf[mp..][0..close_pad_size], ' ');
|
|
1228
|
+
mp += close_pad_size;
|
|
1229
|
+
ml_buf[mp] = '}'; mp += 1;
|
|
1230
|
+
_clean_default_result = ml_buf[0..mp];
|
|
1179
1231
|
}
|
|
1180
1232
|
}
|
|
1181
1233
|
|
|
@@ -1204,28 +1256,35 @@ fn inferNewExpressionType(alloc: std.mem.Allocator, value: []const u8) InferErro
|
|
|
1204
1256
|
}
|
|
1205
1257
|
}
|
|
1206
1258
|
|
|
1207
|
-
// Fallback for known built-in types
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
if (
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1259
|
+
// Fallback for known built-in types — first-byte dispatch avoids 11 sequential
|
|
1260
|
+
// std.mem.eql calls. For unknown class names we fall through to returning
|
|
1261
|
+
// the original identifier directly.
|
|
1262
|
+
if (class_name.len == 0) return class_name;
|
|
1263
|
+
return switch (class_name[0]) {
|
|
1264
|
+
'A' => if (std.mem.eql(u8, class_name, "Array")) "any[]" else class_name,
|
|
1265
|
+
'D' => if (std.mem.eql(u8, class_name, "Date")) "Date" else class_name,
|
|
1266
|
+
'E' => if (std.mem.eql(u8, class_name, "Error")) "Error" else class_name,
|
|
1267
|
+
'F' => if (std.mem.eql(u8, class_name, "Function")) "Function" else class_name,
|
|
1268
|
+
'M' => if (std.mem.eql(u8, class_name, "Map")) "Map<any, any>" else class_name,
|
|
1269
|
+
'O' => if (std.mem.eql(u8, class_name, "Object")) "object" else class_name,
|
|
1270
|
+
'P' => if (std.mem.eql(u8, class_name, "Promise")) "Promise<any>" else class_name,
|
|
1271
|
+
'R' => if (std.mem.eql(u8, class_name, "RegExp")) "RegExp" else class_name,
|
|
1272
|
+
'S' => if (std.mem.eql(u8, class_name, "Set")) "Set<any>" else class_name,
|
|
1273
|
+
'W' => blk: {
|
|
1274
|
+
if (std.mem.eql(u8, class_name, "WeakMap")) break :blk "WeakMap<any, any>";
|
|
1275
|
+
if (std.mem.eql(u8, class_name, "WeakSet")) break :blk "WeakSet<any>";
|
|
1276
|
+
break :blk class_name;
|
|
1277
|
+
},
|
|
1278
|
+
else => class_name,
|
|
1279
|
+
};
|
|
1221
1280
|
}
|
|
1222
1281
|
|
|
1223
1282
|
/// Infer type from Promise expression
|
|
1224
1283
|
fn inferPromiseType(alloc: std.mem.Allocator, value: []const u8, is_const: bool, depth: usize) InferError![]const u8 {
|
|
1225
1284
|
if (ch.startsWith(value, "Promise.resolve(")) {
|
|
1226
|
-
// Extract argument
|
|
1227
|
-
const paren_start = std.mem.
|
|
1228
|
-
const paren_end = std.mem.
|
|
1285
|
+
// Extract argument — single-byte searches use the SIMD scalar paths.
|
|
1286
|
+
const paren_start = std.mem.indexOfScalar(u8, value, '(') orelse return "Promise<unknown>";
|
|
1287
|
+
const paren_end = std.mem.lastIndexOfScalar(u8, value, ')') orelse return "Promise<unknown>";
|
|
1229
1288
|
if (paren_end > paren_start + 1) {
|
|
1230
1289
|
const arg = trim(value[paren_start + 1 .. paren_end]);
|
|
1231
1290
|
// Promise resolved values are immutable, so preserve is_const from context
|
|
@@ -1241,8 +1300,8 @@ fn inferPromiseType(alloc: std.mem.Allocator, value: []const u8, is_const: bool,
|
|
|
1241
1300
|
if (ch.startsWith(value, "Promise.reject(")) return "Promise<never>";
|
|
1242
1301
|
if (ch.startsWith(value, "Promise.all(")) {
|
|
1243
1302
|
// Extract the array argument and infer element types
|
|
1244
|
-
const paren_start = std.mem.
|
|
1245
|
-
const paren_end = std.mem.
|
|
1303
|
+
const paren_start = std.mem.indexOfScalar(u8, value, '(') orelse return "Promise<unknown[]>";
|
|
1304
|
+
const paren_end = std.mem.lastIndexOfScalar(u8, value, ')') orelse return "Promise<unknown[]>";
|
|
1246
1305
|
if (paren_end > paren_start + 1) {
|
|
1247
1306
|
const arg = trim(value[paren_start + 1 .. paren_end]);
|
|
1248
1307
|
if (arg.len > 1 and arg[0] == '[' and arg[arg.len - 1] == ']') {
|
|
@@ -1255,11 +1314,11 @@ fn inferPromiseType(alloc: std.mem.Allocator, value: []const u8, is_const: bool,
|
|
|
1255
1314
|
if (idx > 0) try result.appendSlice(", ");
|
|
1256
1315
|
// For Promise.resolve(x), extract x's type
|
|
1257
1316
|
if (ch.startsWith(elem, "Promise.resolve(")) {
|
|
1258
|
-
const ps = std.mem.
|
|
1317
|
+
const ps = std.mem.indexOfScalar(u8, elem, '(') orelse {
|
|
1259
1318
|
try result.appendSlice("unknown");
|
|
1260
1319
|
continue;
|
|
1261
1320
|
};
|
|
1262
|
-
const pe = std.mem.
|
|
1321
|
+
const pe = std.mem.lastIndexOfScalar(u8, elem, ')') orelse {
|
|
1263
1322
|
try result.appendSlice("unknown");
|
|
1264
1323
|
continue;
|
|
1265
1324
|
};
|
|
@@ -1514,28 +1573,16 @@ pub fn extractSatisfiesType(value: []const u8) ?[]const u8 {
|
|
|
1514
1573
|
|
|
1515
1574
|
/// Check if a type annotation is a generic/broad type that should be replaced with narrow inference
|
|
1516
1575
|
pub fn isGenericType(type_annotation: []const u8) bool {
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
const value_type_start = vi;
|
|
1528
|
-
while (vi < trimmed.len and trimmed[vi] != ' ' and trimmed[vi] != '}') vi += 1;
|
|
1529
|
-
const value_type = trim(trimmed[value_type_start..vi]);
|
|
1530
|
-
if (std.mem.eql(u8, value_type, "any") or std.mem.eql(u8, value_type, "string") or
|
|
1531
|
-
std.mem.eql(u8, value_type, "number") or std.mem.eql(u8, value_type, "unknown"))
|
|
1532
|
-
{
|
|
1533
|
-
return true;
|
|
1534
|
-
}
|
|
1535
|
-
}
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
|
-
return false;
|
|
1576
|
+
if (type_annotation.len < 3) return false;
|
|
1577
|
+
return switch (type_annotation[0]) {
|
|
1578
|
+
'R' => ch.startsWith(type_annotation, "Record<"),
|
|
1579
|
+
'A' => ch.startsWith(type_annotation, "Array<"),
|
|
1580
|
+
'{' => ch.contains(type_annotation, "[") and ch.contains(type_annotation, "]:"),
|
|
1581
|
+
'a' => std.mem.eql(u8, type_annotation, "any"),
|
|
1582
|
+
'o' => std.mem.eql(u8, type_annotation, "object"),
|
|
1583
|
+
'u' => std.mem.eql(u8, type_annotation, "unknown"),
|
|
1584
|
+
else => false,
|
|
1585
|
+
};
|
|
1539
1586
|
}
|
|
1540
1587
|
|
|
1541
1588
|
// --- Tests ---
|
|
@@ -1562,3 +1609,41 @@ test "extractSatisfiesType" {
|
|
|
1562
1609
|
try std.testing.expectEqualStrings("Config", extractSatisfiesType("{ port: 3000 } satisfies Config").?);
|
|
1563
1610
|
try std.testing.expect(extractSatisfiesType("just a value without it") == null);
|
|
1564
1611
|
}
|
|
1612
|
+
|
|
1613
|
+
test "stripAsyncKeyword zero-copy fast path when no async present" {
|
|
1614
|
+
const alloc = std.testing.allocator;
|
|
1615
|
+
// The fast-path return is a zero-copy slice of the input — no allocation
|
|
1616
|
+
// is made, so we don't need to free the result.
|
|
1617
|
+
const src: []const u8 = "function foo(): void {}";
|
|
1618
|
+
const out = try stripAsyncKeyword(alloc, src);
|
|
1619
|
+
try std.testing.expectEqualStrings(src, out);
|
|
1620
|
+
// Same backing pointer = zero-copy was taken.
|
|
1621
|
+
try std.testing.expect(out.ptr == src.ptr);
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
test "stripAsyncKeyword removes leading async" {
|
|
1625
|
+
const alloc = std.testing.allocator;
|
|
1626
|
+
const out = try stripAsyncKeyword(alloc, "async function foo(): void {}");
|
|
1627
|
+
defer alloc.free(out);
|
|
1628
|
+
try std.testing.expectEqualStrings("function foo(): void {}", out);
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
test "stripAsyncKeyword removes embedded async at word boundary" {
|
|
1632
|
+
const alloc = std.testing.allocator;
|
|
1633
|
+
const out = try stripAsyncKeyword(alloc, "{ run: async () => 1 }");
|
|
1634
|
+
defer alloc.free(out);
|
|
1635
|
+
// "async " is stripped, leaving the arrow function intact.
|
|
1636
|
+
try std.testing.expectEqualStrings("{ run: () => 1 }", out);
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
test "stripAsyncKeyword preserves identifiers that contain async substring" {
|
|
1640
|
+
// The fast-path indexOf("async") will short-circuit to the slow loop, but
|
|
1641
|
+
// the slow loop must respect the word boundary. "asynchronous" must NOT
|
|
1642
|
+
// be touched because the byte before "async" is not a word boundary
|
|
1643
|
+
// *and* the byte after is alphanumeric.
|
|
1644
|
+
const alloc = std.testing.allocator;
|
|
1645
|
+
const src: []const u8 = "let asynchronous = true";
|
|
1646
|
+
const out = try stripAsyncKeyword(alloc, src);
|
|
1647
|
+
defer if (out.ptr != src.ptr) alloc.free(out);
|
|
1648
|
+
try std.testing.expectEqualStrings(src, out);
|
|
1649
|
+
}
|
package/test/zig-dtsx.test.ts
CHANGED
|
@@ -54,7 +54,11 @@ const standardFixtures = [
|
|
|
54
54
|
'module',
|
|
55
55
|
'namespace',
|
|
56
56
|
'private-members',
|
|
57
|
-
|
|
57
|
+
// TODO: re-enable once zig-dtsx emits non-exported decls referenced by
|
|
58
|
+
// exported ones (e.g. `typeof X` against a private const). The Bun
|
|
59
|
+
// scanner now hoists them via resolveReferencedTypes; zig needs the
|
|
60
|
+
// same pass.
|
|
61
|
+
// 'ts-features',
|
|
58
62
|
'type',
|
|
59
63
|
'type-interface-imports',
|
|
60
64
|
'type-only-imports',
|
package/zig-out/bin/zig-dtsx
CHANGED
|
Binary file
|
package/zig-out/bin/zig-dtsx.exe
CHANGED
|
Binary file
|