@stacksjs/zig-dtsx 0.9.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +73 -0
- package/build.zig +79 -0
- package/build.zig.zon +11 -0
- package/package.json +23 -0
- package/src/char_utils.zig +158 -0
- package/src/emitter.zig +1045 -0
- package/src/extractors.zig +2464 -0
- package/src/index.ts +222 -0
- package/src/lib.zig +254 -0
- package/src/main.zig +532 -0
- package/src/scan_loop.zig +330 -0
- package/src/scanner.zig +908 -0
- package/src/type_inference.zig +1564 -0
- package/src/types.zig +105 -0
- package/test/benchmark.ts +343 -0
- package/test/fixtures/output/variable.d.ts +157 -0
- package/test/zig-dtsx.test.ts +1386 -0
- package/zig-out/bin/zig-dtsx +0 -0
- package/zig-out/bin/zig-dtsx.exe +0 -0
|
@@ -0,0 +1,1564 @@
|
|
|
1
|
+
/// Type inference utilities for DTS generation.
|
|
2
|
+
/// Port of processor/type-inference.ts.
|
|
3
|
+
const std = @import("std");
|
|
4
|
+
const ch = @import("char_utils.zig");
|
|
5
|
+
|
|
6
|
+
const MAX_INFERENCE_DEPTH = 20;
|
|
7
|
+
|
|
8
|
+
/// Error type for type inference operations
|
|
9
|
+
pub const InferError = std.mem.Allocator.Error;
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Module-level storage for computing clean default alongside type inference.
|
|
13
|
+
// This avoids double-parsing: inferObjectType/inferArrayType build the
|
|
14
|
+
// @defaultValue content during the same pass that infers types.
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
threadlocal var _collect_clean_default: bool = false;
|
|
17
|
+
threadlocal var _clean_default_result: ?[]const u8 = null;
|
|
18
|
+
|
|
19
|
+
/// Enable clean default collection for the next type inference pass.
|
|
20
|
+
/// Must be called before inferNarrowType when you need a @defaultValue.
|
|
21
|
+
pub fn enableCleanDefaultCollection() void {
|
|
22
|
+
_collect_clean_default = true;
|
|
23
|
+
_clean_default_result = null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// Consume the computed clean default (also disables collection).
|
|
27
|
+
/// Returns null if no clean default was computed.
|
|
28
|
+
pub fn consumeCleanDefault() ?[]const u8 {
|
|
29
|
+
_collect_clean_default = false;
|
|
30
|
+
const val = _clean_default_result;
|
|
31
|
+
_clean_default_result = null;
|
|
32
|
+
return val;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Check if a string is a numeric literal (matches /^-?\d+(\.\d+)?$/)
|
|
36
|
+
pub fn isNumericLiteral(s: []const u8) bool {
|
|
37
|
+
if (s.len == 0) return false;
|
|
38
|
+
var i: usize = 0;
|
|
39
|
+
if (s[i] == '-') i += 1;
|
|
40
|
+
if (i >= s.len) return false;
|
|
41
|
+
const digit_start = i;
|
|
42
|
+
while (i < s.len and s[i] >= '0' and s[i] <= '9') i += 1;
|
|
43
|
+
if (i == digit_start) return false;
|
|
44
|
+
if (i < s.len and s[i] == '.') {
|
|
45
|
+
i += 1;
|
|
46
|
+
const frac_start = i;
|
|
47
|
+
while (i < s.len and s[i] >= '0' and s[i] <= '9') i += 1;
|
|
48
|
+
if (i == frac_start) return false;
|
|
49
|
+
}
|
|
50
|
+
return i == s.len;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// Check if s (excluding last char 'n') is all digits — for BigInt literals
|
|
54
|
+
fn isBigIntDigits(s: []const u8) bool {
|
|
55
|
+
if (s.len < 2) return false;
|
|
56
|
+
for (s[0 .. s.len - 1]) |c| {
|
|
57
|
+
if (c < '0' or c > '9') return false;
|
|
58
|
+
}
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Trim whitespace from both ends
|
|
63
|
+
fn trim(s: []const u8) []const u8 {
|
|
64
|
+
var start: usize = 0;
|
|
65
|
+
var end: usize = s.len;
|
|
66
|
+
while (start < end and ch.isWhitespace(s[start])) start += 1;
|
|
67
|
+
while (end > start and ch.isWhitespace(s[end - 1])) end -= 1;
|
|
68
|
+
return s[start..end];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// Count occurrences of needle in haystack
|
|
72
|
+
fn countOccurrences(haystack: []const u8, needle: []const u8) usize {
|
|
73
|
+
if (needle.len == 0) return 0;
|
|
74
|
+
var count: usize = 0;
|
|
75
|
+
var pos: usize = 0;
|
|
76
|
+
while (ch.indexOf(haystack, needle, pos)) |idx| {
|
|
77
|
+
count += 1;
|
|
78
|
+
pos = idx + needle.len;
|
|
79
|
+
}
|
|
80
|
+
return count;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/// Parse array elements handling nested structures.
|
|
84
|
+
/// Returns slices into the original content string.
|
|
85
|
+
pub fn parseArrayElements(alloc: std.mem.Allocator, content: []const u8) InferError![][]const u8 {
|
|
86
|
+
var elements = std.array_list.Managed([]const u8).init(alloc);
|
|
87
|
+
// Pre-size: estimate element count from top-level commas
|
|
88
|
+
var est: usize = 1;
|
|
89
|
+
for (content) |cc| if (cc == ',') {
|
|
90
|
+
est += 1;
|
|
91
|
+
};
|
|
92
|
+
try elements.ensureTotalCapacity(est);
|
|
93
|
+
var current_start: usize = 0;
|
|
94
|
+
var depth: i32 = 0;
|
|
95
|
+
var in_string = false;
|
|
96
|
+
var string_char: u8 = 0;
|
|
97
|
+
var i: usize = 0;
|
|
98
|
+
|
|
99
|
+
// Skip leading whitespace for current_start
|
|
100
|
+
while (current_start < content.len and ch.isWhitespace(content[current_start])) current_start += 1;
|
|
101
|
+
|
|
102
|
+
while (i < content.len) : (i += 1) {
|
|
103
|
+
const c = content[i];
|
|
104
|
+
const prev = if (i > 0) content[i - 1] else @as(u8, 0);
|
|
105
|
+
|
|
106
|
+
if (!in_string and (c == '"' or c == '\'' or c == '`')) {
|
|
107
|
+
in_string = true;
|
|
108
|
+
string_char = c;
|
|
109
|
+
} else if (in_string and c == string_char and prev != '\\') {
|
|
110
|
+
in_string = false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!in_string) {
|
|
114
|
+
if (c == '[' or c == '{' or c == '(') depth += 1;
|
|
115
|
+
if (c == ']' or c == '}' or c == ')') depth -= 1;
|
|
116
|
+
|
|
117
|
+
if (c == ',' and depth == 0) {
|
|
118
|
+
const elem = trim(content[current_start..i]);
|
|
119
|
+
if (elem.len > 0) {
|
|
120
|
+
try elements.append(elem);
|
|
121
|
+
}
|
|
122
|
+
current_start = i + 1;
|
|
123
|
+
while (current_start < content.len and ch.isWhitespace(content[current_start])) current_start += 1;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Last element
|
|
130
|
+
const last = trim(content[current_start..content.len]);
|
|
131
|
+
if (last.len > 0) {
|
|
132
|
+
try elements.append(last);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return elements.items;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/// Clean a method signature: strip async, replace defaults with ?, collapse whitespace.
|
|
139
|
+
/// Single-pass implementation combining all transformations.
|
|
140
|
+
fn cleanMethodSignature(alloc: std.mem.Allocator, signature: []const u8) InferError![]const u8 {
|
|
141
|
+
var input = signature;
|
|
142
|
+
// Remove leading "async "
|
|
143
|
+
if (ch.startsWith(input, "async ")) {
|
|
144
|
+
input = trim(input[5..]);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Fast path: if no async, no defaults (=), no consecutive whitespace, return as-is
|
|
148
|
+
const needs_clean = blk: {
|
|
149
|
+
var prev_ws = false;
|
|
150
|
+
for (input, 0..) |c, i| {
|
|
151
|
+
if (c == '=' and (i + 1 >= input.len or input[i + 1] != '>')) break :blk true;
|
|
152
|
+
const is_ws = ch.isWhitespace(c);
|
|
153
|
+
if (is_ws and prev_ws) break :blk true;
|
|
154
|
+
prev_ws = is_ws;
|
|
155
|
+
if (c == 'a' and i > 0 and !ch.isIdentChar(input[i - 1]) and i + 5 < input.len and
|
|
156
|
+
input[i + 1] == 's' and input[i + 2] == 'y' and
|
|
157
|
+
input[i + 3] == 'n' and input[i + 4] == 'c' and ch.isWhitespace(input[i + 5]))
|
|
158
|
+
break :blk true;
|
|
159
|
+
}
|
|
160
|
+
break :blk false;
|
|
161
|
+
};
|
|
162
|
+
if (!needs_clean) return input;
|
|
163
|
+
|
|
164
|
+
// Single pass: remove async keywords, replace defaults with ?, collapse whitespace
|
|
165
|
+
var buf = std.array_list.Managed(u8).init(alloc);
|
|
166
|
+
try buf.ensureTotalCapacity(input.len);
|
|
167
|
+
var j: usize = 0;
|
|
168
|
+
var in_ws = false;
|
|
169
|
+
|
|
170
|
+
while (j < input.len) {
|
|
171
|
+
const c = input[j];
|
|
172
|
+
|
|
173
|
+
// Skip "async " at word boundaries
|
|
174
|
+
if (j > 0 and !ch.isIdentChar(input[j - 1]) and j + 5 < input.len and
|
|
175
|
+
input[j] == 'a' and input[j + 1] == 's' and input[j + 2] == 'y' and
|
|
176
|
+
input[j + 3] == 'n' and input[j + 4] == 'c' and ch.isWhitespace(input[j + 5]))
|
|
177
|
+
{
|
|
178
|
+
j += 6;
|
|
179
|
+
while (j < input.len and ch.isWhitespace(input[j])) j += 1;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Handle identifiers - check for default value patterns (word = value)
|
|
184
|
+
if (ch.isIdentChar(c)) {
|
|
185
|
+
const word_start = j;
|
|
186
|
+
while (j < input.len and ch.isIdentChar(input[j])) j += 1;
|
|
187
|
+
const word_end = j;
|
|
188
|
+
|
|
189
|
+
// Peek past whitespace for '='
|
|
190
|
+
var peek = j;
|
|
191
|
+
while (peek < input.len and ch.isWhitespace(input[peek])) peek += 1;
|
|
192
|
+
|
|
193
|
+
if (peek < input.len and input[peek] == '=' and (peek + 1 >= input.len or input[peek + 1] != '>')) {
|
|
194
|
+
// Default value: skip to , or ) and replace with word?
|
|
195
|
+
var skip = peek + 1;
|
|
196
|
+
while (skip < input.len and input[skip] != ',' and input[skip] != ')') skip += 1;
|
|
197
|
+
// Emit word with collapsed whitespace
|
|
198
|
+
for (input[word_start..word_end]) |wc| {
|
|
199
|
+
if (ch.isWhitespace(wc)) {
|
|
200
|
+
if (!in_ws) { try buf.append(' '); in_ws = true; }
|
|
201
|
+
} else {
|
|
202
|
+
try buf.append(wc); in_ws = false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
try buf.append('?');
|
|
206
|
+
in_ws = false;
|
|
207
|
+
j = skip;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Not a default - emit the word + any whitespace we peeked past
|
|
212
|
+
for (input[word_start..j]) |wc| {
|
|
213
|
+
if (ch.isWhitespace(wc)) {
|
|
214
|
+
if (!in_ws) { try buf.append(' '); in_ws = true; }
|
|
215
|
+
} else {
|
|
216
|
+
try buf.append(wc); in_ws = false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Collapse whitespace
|
|
223
|
+
if (ch.isWhitespace(c)) {
|
|
224
|
+
if (!in_ws) {
|
|
225
|
+
try buf.append(' ');
|
|
226
|
+
in_ws = true;
|
|
227
|
+
}
|
|
228
|
+
j += 1;
|
|
229
|
+
} else {
|
|
230
|
+
try buf.append(c);
|
|
231
|
+
in_ws = false;
|
|
232
|
+
j += 1;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return trim(buf.items);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/// Strip 'async' keywords from a string without collapsing whitespace.
|
|
239
|
+
/// Used when we want to remove async modifiers but preserve multiline formatting.
|
|
240
|
+
fn stripAsyncKeyword(alloc: std.mem.Allocator, input: []const u8) InferError![]const u8 {
|
|
241
|
+
var buf = std.array_list.Managed(u8).init(alloc);
|
|
242
|
+
var j: usize = 0;
|
|
243
|
+
// Remove leading "async "
|
|
244
|
+
if (ch.startsWith(input, "async ")) {
|
|
245
|
+
j = 6;
|
|
246
|
+
// Skip extra whitespace after
|
|
247
|
+
while (j < input.len and input[j] == ' ') j += 1;
|
|
248
|
+
}
|
|
249
|
+
while (j < input.len) {
|
|
250
|
+
if (j > 0 and ch.startsWith(input[j..], "async ")) {
|
|
251
|
+
// Check word boundary: char before must not be alphanumeric or _
|
|
252
|
+
const before = input[j - 1];
|
|
253
|
+
if (!ch.isIdentChar(before)) {
|
|
254
|
+
j += 6; // skip "async "
|
|
255
|
+
// Skip extra spaces (but not newlines) after
|
|
256
|
+
while (j < input.len and input[j] == ' ') j += 1;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
try buf.append(input[j]);
|
|
261
|
+
j += 1;
|
|
262
|
+
}
|
|
263
|
+
return buf.items;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/// Convert a method definition to a function type.
|
|
267
|
+
/// Input: key = method name (may include generics), value = "(params): ReturnType { body }"
|
|
268
|
+
/// Output: "generics(params) => ReturnType"
|
|
269
|
+
fn convertMethodToFunctionType(alloc: std.mem.Allocator, key: []const u8, method_def: []const u8) InferError![]const u8 {
|
|
270
|
+
var cleaned = method_def;
|
|
271
|
+
// Remove leading async
|
|
272
|
+
if (ch.startsWith(cleaned, "async ")) {
|
|
273
|
+
cleaned = trim(cleaned[5..]);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Extract generics from key (e.g., "onSuccess<T>" -> generics = "<T>")
|
|
277
|
+
var generics: []const u8 = "";
|
|
278
|
+
_ = key; // key is already clean, generics are at start of value if present
|
|
279
|
+
if (cleaned.len > 0 and cleaned[0] == '<') {
|
|
280
|
+
if (findMatchingBracket(cleaned, 0, '<', '>')) |gen_end| {
|
|
281
|
+
generics = cleaned[0 .. gen_end + 1];
|
|
282
|
+
cleaned = trim(cleaned[gen_end + 1 ..]);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Find parameter list
|
|
287
|
+
const param_start = ch.indexOfChar(cleaned, '(', 0) orelse return "() => unknown";
|
|
288
|
+
const param_end = findMatchingBracket(cleaned, param_start, '(', ')') orelse return "() => unknown";
|
|
289
|
+
const params = cleaned[param_start .. param_end + 1];
|
|
290
|
+
|
|
291
|
+
// Extract return type
|
|
292
|
+
var return_type: []const u8 = "unknown";
|
|
293
|
+
const after_params = trim(cleaned[param_end + 1 ..]);
|
|
294
|
+
if (after_params.len > 0 and after_params[0] == ':') {
|
|
295
|
+
// Find return type - everything up to '{' or end
|
|
296
|
+
const type_start: usize = 1; // skip ':'
|
|
297
|
+
var type_end: usize = after_params.len;
|
|
298
|
+
// Look for opening brace (function body)
|
|
299
|
+
var j: usize = type_start;
|
|
300
|
+
var d: i32 = 0;
|
|
301
|
+
while (j < after_params.len) : (j += 1) {
|
|
302
|
+
if (after_params[j] == '<') d += 1 else if (after_params[j] == '>') d -= 1;
|
|
303
|
+
if (d == 0 and after_params[j] == '{') {
|
|
304
|
+
type_end = j;
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const rt = trim(after_params[type_start..type_end]);
|
|
309
|
+
if (rt.len > 0) return_type = rt;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Clean parameter defaults
|
|
313
|
+
const clean_params = try cleanParameterDefaults(alloc, params);
|
|
314
|
+
|
|
315
|
+
// Build result
|
|
316
|
+
var result = std.array_list.Managed(u8).init(alloc);
|
|
317
|
+
try result.appendSlice(generics);
|
|
318
|
+
try result.appendSlice(clean_params);
|
|
319
|
+
try result.appendSlice(" => ");
|
|
320
|
+
try result.appendSlice(return_type);
|
|
321
|
+
return result.toOwnedSlice();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/// Clean parameter defaults: replace `param = value` with `param?`
|
|
325
|
+
fn cleanParameterDefaults(alloc: std.mem.Allocator, params: []const u8) InferError![]const u8 {
|
|
326
|
+
var buf = std.array_list.Managed(u8).init(alloc);
|
|
327
|
+
var j: usize = 0;
|
|
328
|
+
while (j < params.len) {
|
|
329
|
+
// Try to match word= pattern (not =>)
|
|
330
|
+
const word_start = j;
|
|
331
|
+
while (j < params.len and ch.isIdentChar(params[j])) j += 1;
|
|
332
|
+
const word_end = j;
|
|
333
|
+
if (word_end > word_start) {
|
|
334
|
+
// Skip whitespace
|
|
335
|
+
while (j < params.len and ch.isWhitespace(params[j])) j += 1;
|
|
336
|
+
if (j < params.len and params[j] == '=' and (j + 1 >= params.len or params[j + 1] != '>')) {
|
|
337
|
+
// Default value - skip to , or )
|
|
338
|
+
j += 1;
|
|
339
|
+
var d: i32 = 0;
|
|
340
|
+
while (j < params.len) {
|
|
341
|
+
if (params[j] == '(' or params[j] == '[' or params[j] == '{') d += 1;
|
|
342
|
+
if (params[j] == ')' or params[j] == ']' or params[j] == '}') {
|
|
343
|
+
if (d == 0) break;
|
|
344
|
+
d -= 1;
|
|
345
|
+
}
|
|
346
|
+
if (params[j] == ',' and d == 0) break;
|
|
347
|
+
j += 1;
|
|
348
|
+
}
|
|
349
|
+
try buf.appendSlice(params[word_start..word_end]);
|
|
350
|
+
try buf.append('?');
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
try buf.appendSlice(params[word_start..j]);
|
|
354
|
+
} else {
|
|
355
|
+
try buf.append(params[j]);
|
|
356
|
+
j += 1;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return buf.items;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/// Parse object properties from content between braces.
|
|
363
|
+
/// Returns array of [key, value] pairs as slices into content.
|
|
364
|
+
fn parseObjectProperties(alloc: std.mem.Allocator, content: []const u8) InferError![][2][]const u8 {
|
|
365
|
+
var properties = std.array_list.Managed([2][]const u8).init(alloc);
|
|
366
|
+
// Pre-size: estimate property count from top-level commas
|
|
367
|
+
var est: usize = 1;
|
|
368
|
+
for (content) |cc| if (cc == ',') {
|
|
369
|
+
est += 1;
|
|
370
|
+
};
|
|
371
|
+
try properties.ensureTotalCapacity(est);
|
|
372
|
+
var current_start: usize = 0;
|
|
373
|
+
var key_start: usize = 0;
|
|
374
|
+
var key_end: usize = 0;
|
|
375
|
+
var depth: i32 = 0;
|
|
376
|
+
var in_string = false;
|
|
377
|
+
var string_char: u8 = 0;
|
|
378
|
+
var in_key = true;
|
|
379
|
+
var in_comment = false;
|
|
380
|
+
var is_method = false;
|
|
381
|
+
var i: usize = 0;
|
|
382
|
+
|
|
383
|
+
while (i < content.len) : (i += 1) {
|
|
384
|
+
const c = content[i];
|
|
385
|
+
const prev = if (i > 0) content[i - 1] else @as(u8, 0);
|
|
386
|
+
const next = if (i + 1 < content.len) content[i + 1] else @as(u8, 0);
|
|
387
|
+
|
|
388
|
+
// Track single-line comments — skip to end of line
|
|
389
|
+
if (!in_string and !in_comment and c == '/' and next == '/') {
|
|
390
|
+
i += 2; // Skip '//'
|
|
391
|
+
while (i < content.len and content[i] != '\n') : (i += 1) {}
|
|
392
|
+
// Update current_start if in key mode so the key slice doesn't include comment text
|
|
393
|
+
if (in_key and i < content.len) {
|
|
394
|
+
current_start = i + 1;
|
|
395
|
+
while (current_start < content.len and ch.isWhitespace(content[current_start])) current_start += 1;
|
|
396
|
+
}
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Track block comments
|
|
401
|
+
if (!in_string and !in_comment and c == '/' and next == '*') {
|
|
402
|
+
in_comment = true;
|
|
403
|
+
i += 1;
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
if (in_comment and c == '*' and next == '/') {
|
|
407
|
+
in_comment = false;
|
|
408
|
+
i += 1;
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
if (in_comment) continue;
|
|
412
|
+
|
|
413
|
+
if (!in_string and (c == '"' or c == '\'' or c == '`')) {
|
|
414
|
+
in_string = true;
|
|
415
|
+
string_char = c;
|
|
416
|
+
} else if (in_string and c == string_char and prev != '\\') {
|
|
417
|
+
in_string = false;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (!in_string) {
|
|
421
|
+
if (c == '(' and depth == 0 and in_key) {
|
|
422
|
+
// Method definition — must check BEFORE general bracket tracking
|
|
423
|
+
key_start = current_start;
|
|
424
|
+
key_end = i;
|
|
425
|
+
current_start = i;
|
|
426
|
+
in_key = false;
|
|
427
|
+
is_method = true;
|
|
428
|
+
depth = 1;
|
|
429
|
+
} else if (c == '{' or c == '[' or c == '(') {
|
|
430
|
+
depth += 1;
|
|
431
|
+
} else if (c == '}' or c == ']' or c == ')') {
|
|
432
|
+
depth -= 1;
|
|
433
|
+
} else if (c == ':' and depth == 0 and in_key) {
|
|
434
|
+
key_start = current_start;
|
|
435
|
+
key_end = i;
|
|
436
|
+
current_start = i + 1;
|
|
437
|
+
in_key = false;
|
|
438
|
+
is_method = false;
|
|
439
|
+
} else if (c == ',' and depth == 0) {
|
|
440
|
+
if (!in_key) {
|
|
441
|
+
var key = trim(content[key_start..key_end]);
|
|
442
|
+
var val = trim(content[current_start..i]);
|
|
443
|
+
if (key.len > 0 and val.len > 0) {
|
|
444
|
+
// Strip async from key if method
|
|
445
|
+
if (is_method and ch.startsWith(key, "async ")) {
|
|
446
|
+
key = trim(key[6..]);
|
|
447
|
+
}
|
|
448
|
+
// Process value based on type - match TS behavior:
|
|
449
|
+
// ANY value starting with '(' goes through convertMethodToFunctionType
|
|
450
|
+
if (val.len > 0 and val[0] == '(') {
|
|
451
|
+
val = try convertMethodToFunctionType(alloc, key, val);
|
|
452
|
+
} else if (ch.contains(val, "=>") or ch.startsWith(val, "function") or ch.startsWith(val, "async")) {
|
|
453
|
+
val = try cleanMethodSignature(alloc, val);
|
|
454
|
+
}
|
|
455
|
+
try properties.append(.{ key, val });
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
current_start = i + 1;
|
|
459
|
+
in_key = true;
|
|
460
|
+
is_method = false;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Last property
|
|
466
|
+
if (!in_key) {
|
|
467
|
+
var key = trim(content[key_start..key_end]);
|
|
468
|
+
var val = trim(content[current_start..content.len]);
|
|
469
|
+
if (key.len > 0 and val.len > 0) {
|
|
470
|
+
if (is_method and ch.startsWith(key, "async ")) {
|
|
471
|
+
key = trim(key[6..]);
|
|
472
|
+
}
|
|
473
|
+
if (val.len > 0 and val[0] == '(') {
|
|
474
|
+
val = try convertMethodToFunctionType(alloc, key, val);
|
|
475
|
+
} else if (ch.contains(val, "=>") or ch.startsWith(val, "function") or ch.startsWith(val, "async")) {
|
|
476
|
+
val = try cleanMethodSignature(alloc, val);
|
|
477
|
+
}
|
|
478
|
+
try properties.append(.{ key, val });
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return properties.items;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/// Find matching bracket (open/close) starting from `start`, skipping strings and comments.
|
|
486
|
+
fn findMatchingBracket(str: []const u8, start: usize, open: u8, close: u8) ?usize {
|
|
487
|
+
var depth: i32 = 0;
|
|
488
|
+
var i = start;
|
|
489
|
+
while (i < str.len) : (i += 1) {
|
|
490
|
+
const c = str[i];
|
|
491
|
+
// Skip string literals
|
|
492
|
+
if (c == '"' or c == '\'' or c == '`') {
|
|
493
|
+
i += 1;
|
|
494
|
+
while (i < str.len) : (i += 1) {
|
|
495
|
+
if (str[i] == '\\') {
|
|
496
|
+
i += 1; // skip escaped char
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
if (str[i] == c) break;
|
|
500
|
+
}
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
// Skip line comments
|
|
504
|
+
if (c == '/' and i + 1 < str.len and str[i + 1] == '/') {
|
|
505
|
+
i += 2;
|
|
506
|
+
while (i < str.len and str[i] != '\n') : (i += 1) {}
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
// Skip block comments
|
|
510
|
+
if (c == '/' and i + 1 < str.len and str[i + 1] == '*') {
|
|
511
|
+
i += 2;
|
|
512
|
+
while (i + 1 < str.len) : (i += 1) {
|
|
513
|
+
if (str[i] == '*' and str[i + 1] == '/') {
|
|
514
|
+
i += 1;
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
if (c == open) {
|
|
521
|
+
depth += 1;
|
|
522
|
+
} else if (c == close) {
|
|
523
|
+
depth -= 1;
|
|
524
|
+
if (depth == 0) return i;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/// Find the main arrow (=>) in a function, ignoring nested arrows
|
|
531
|
+
fn findMainArrowIndex(str: []const u8) ?usize {
|
|
532
|
+
var paren_depth: i32 = 0;
|
|
533
|
+
var bracket_depth: i32 = 0;
|
|
534
|
+
var brace_depth: i32 = 0;
|
|
535
|
+
var angle_depth: i32 = 0;
|
|
536
|
+
var in_string = false;
|
|
537
|
+
var string_char: u8 = 0;
|
|
538
|
+
|
|
539
|
+
var i: usize = 0;
|
|
540
|
+
while (i + 1 < str.len) : (i += 1) {
|
|
541
|
+
const c = str[i];
|
|
542
|
+
const prev = if (i > 0) str[i - 1] else @as(u8, 0);
|
|
543
|
+
|
|
544
|
+
if (in_string) {
|
|
545
|
+
if (c == '\\') {
|
|
546
|
+
i += 1; // skip escaped char
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
if (c == string_char) in_string = false;
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (c == '"' or c == '\'' or c == '`') {
|
|
554
|
+
in_string = true;
|
|
555
|
+
string_char = c;
|
|
556
|
+
_ = prev;
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (c == '(') paren_depth += 1 else if (c == ')') paren_depth -= 1 else if (c == '[') bracket_depth += 1 else if (c == ']') bracket_depth -= 1 else if (c == '{') brace_depth += 1 else if (c == '}') brace_depth -= 1 else if (c == '<') angle_depth += 1 else if (c == '>') {
|
|
561
|
+
if (angle_depth > 0) angle_depth -= 1;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (c == '=' and str[i + 1] == '>' and paren_depth == 0 and bracket_depth == 0 and brace_depth == 0 and angle_depth == 0) {
|
|
565
|
+
return i;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/// Extract inner function signature from a higher-order function body.
|
|
572
|
+
/// For bodies like "(value: number) => value * factor", extracts "(value: number) => any".
|
|
573
|
+
/// For generic functions where generics include 'T' and inner params include 'T',
|
|
574
|
+
/// uses 'T' as the return type instead of 'any'.
|
|
575
|
+
fn extractInnerFunctionSignature(alloc: std.mem.Allocator, body: []const u8, generics: []const u8) InferError![]const u8 {
|
|
576
|
+
const trimmed_body = trim(body);
|
|
577
|
+
// Match pattern: \s*(params)\s*=>
|
|
578
|
+
if (trimmed_body.len > 0 and trimmed_body[0] == '(') {
|
|
579
|
+
if (findMatchingBracket(trimmed_body, 0, '(', ')')) |paren_end| {
|
|
580
|
+
const inner_params = trim(trimmed_body[1..paren_end]);
|
|
581
|
+
// Check if this is a generic function where T appears in both generics and inner params
|
|
582
|
+
const has_generic_t = generics.len > 0 and ch.contains(generics, "T");
|
|
583
|
+
const inner_has_t = ch.contains(inner_params, "T");
|
|
584
|
+
const inner_return = if (has_generic_t and inner_has_t) "T" else "any";
|
|
585
|
+
var result = std.array_list.Managed(u8).init(alloc);
|
|
586
|
+
try result.appendSlice("(");
|
|
587
|
+
try result.appendSlice(inner_params);
|
|
588
|
+
try result.appendSlice(") => ");
|
|
589
|
+
try result.appendSlice(inner_return);
|
|
590
|
+
return result.toOwnedSlice();
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return "any";
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/// Single-pass scan hints to avoid multiple ch.contains() calls.
|
|
597
|
+
const ValueHints = struct {
|
|
598
|
+
has_dollar_brace: bool = false, // "${" — template interpolation
|
|
599
|
+
has_arrow: bool = false, // "=>" — arrow function
|
|
600
|
+
has_raw_template: bool = false, // ".raw`" — tagged template literal
|
|
601
|
+
|
|
602
|
+
fn scan(s: []const u8) ValueHints {
|
|
603
|
+
var h = ValueHints{};
|
|
604
|
+
if (s.len < 2) return h;
|
|
605
|
+
var i: usize = 0;
|
|
606
|
+
while (i < s.len - 1) : (i += 1) {
|
|
607
|
+
const c = s[i];
|
|
608
|
+
if (c == '$' and s[i + 1] == '{') { h.has_dollar_brace = true; }
|
|
609
|
+
if (c == '=' and s[i + 1] == '>') { h.has_arrow = true; }
|
|
610
|
+
if (c == '.' and i + 4 < s.len and s[i + 1] == 'r' and s[i + 2] == 'a' and s[i + 3] == 'w' and s[i + 4] == '`') { h.has_raw_template = true; }
|
|
611
|
+
}
|
|
612
|
+
return h;
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
/// Infer narrow type from a value expression.
|
|
617
|
+
/// Returns a type string (allocated from `alloc`).
|
|
618
|
+
pub fn inferNarrowType(alloc: std.mem.Allocator, value: []const u8, is_const: bool, in_union: bool, depth: usize) InferError![]const u8 {
|
|
619
|
+
if (value.len == 0) return "unknown";
|
|
620
|
+
if (depth >= MAX_INFERENCE_DEPTH) return "unknown";
|
|
621
|
+
|
|
622
|
+
const trimmed = trim(value);
|
|
623
|
+
if (trimmed.len == 0) return "unknown";
|
|
624
|
+
|
|
625
|
+
// BigInt expressions
|
|
626
|
+
if (ch.startsWith(trimmed, "BigInt(")) return "bigint";
|
|
627
|
+
|
|
628
|
+
// Symbol.for
|
|
629
|
+
if (ch.startsWith(trimmed, "Symbol.for(")) return "symbol";
|
|
630
|
+
|
|
631
|
+
// Single-pass scan for substring hints (replaces 5 separate ch.contains calls)
|
|
632
|
+
const hints = ValueHints.scan(trimmed);
|
|
633
|
+
|
|
634
|
+
// Tagged template literals
|
|
635
|
+
if (hints.has_raw_template) return "string";
|
|
636
|
+
|
|
637
|
+
// String literals
|
|
638
|
+
if ((trimmed[0] == '"' and trimmed[trimmed.len - 1] == '"') or
|
|
639
|
+
(trimmed[0] == '\'' and trimmed[trimmed.len - 1] == '\'') or
|
|
640
|
+
(trimmed[0] == '`' and trimmed[trimmed.len - 1] == '`'))
|
|
641
|
+
{
|
|
642
|
+
if (!hints.has_dollar_brace) {
|
|
643
|
+
if (!is_const) return "string";
|
|
644
|
+
return trimmed;
|
|
645
|
+
}
|
|
646
|
+
if (is_const) return trimmed;
|
|
647
|
+
return "string";
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Number literals
|
|
651
|
+
if (isNumericLiteral(trimmed)) {
|
|
652
|
+
if (!is_const) return "number";
|
|
653
|
+
return trimmed;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Boolean literals
|
|
657
|
+
if (std.mem.eql(u8, trimmed, "true") or std.mem.eql(u8, trimmed, "false")) {
|
|
658
|
+
if (!is_const) return "boolean";
|
|
659
|
+
return trimmed;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Null and undefined
|
|
663
|
+
if (std.mem.eql(u8, trimmed, "null")) return "null";
|
|
664
|
+
if (std.mem.eql(u8, trimmed, "undefined")) return "undefined";
|
|
665
|
+
|
|
666
|
+
// Array literals
|
|
667
|
+
if (trimmed[0] == '[' and trimmed[trimmed.len - 1] == ']') {
|
|
668
|
+
return inferArrayType(alloc, trimmed, is_const, depth + 1);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Object literals
|
|
672
|
+
if (trimmed[0] == '{' and trimmed[trimmed.len - 1] == '}') {
|
|
673
|
+
return inferObjectType(alloc, trimmed, is_const, depth + 1);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// New expressions
|
|
677
|
+
if (ch.startsWith(trimmed, "new ")) {
|
|
678
|
+
return inferNewExpressionType(alloc, trimmed);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Function expressions
|
|
682
|
+
if (hints.has_arrow or ch.startsWith(trimmed, "function") or ch.startsWith(trimmed, "async")) {
|
|
683
|
+
return inferFunctionType(alloc, trimmed, in_union, depth, is_const);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// As const assertions
|
|
687
|
+
if (ch.endsWith(trimmed, "as const")) {
|
|
688
|
+
const without_as_const = trim(trimmed[0 .. trimmed.len - 8]);
|
|
689
|
+
if (without_as_const.len > 1 and without_as_const[0] == '[' and without_as_const[without_as_const.len - 1] == ']') {
|
|
690
|
+
const content = trim(without_as_const[1 .. without_as_const.len - 1]);
|
|
691
|
+
if (content.len == 0) return "readonly []";
|
|
692
|
+
const elements = try parseArrayElements(alloc, content);
|
|
693
|
+
var parts = std.array_list.Managed(u8).init(alloc);
|
|
694
|
+
try parts.appendSlice("readonly [");
|
|
695
|
+
for (elements, 0..) |el, idx| {
|
|
696
|
+
if (idx > 0) try parts.appendSlice(", ");
|
|
697
|
+
const el_type = try inferNarrowType(alloc, el, true, false, depth + 1);
|
|
698
|
+
try parts.appendSlice(el_type);
|
|
699
|
+
}
|
|
700
|
+
try parts.append(']');
|
|
701
|
+
return parts.toOwnedSlice();
|
|
702
|
+
}
|
|
703
|
+
return inferNarrowType(alloc, without_as_const, true, in_union, depth + 1);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Template literal
|
|
707
|
+
if (trimmed[0] == '`' and trimmed[trimmed.len - 1] == '`') {
|
|
708
|
+
if (!is_const) return "string";
|
|
709
|
+
if (!hints.has_dollar_brace) return trimmed;
|
|
710
|
+
return "string";
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Promise expressions
|
|
714
|
+
if (ch.startsWith(trimmed, "Promise.")) {
|
|
715
|
+
return inferPromiseType(alloc, trimmed, is_const, depth);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Await expressions
|
|
719
|
+
if (ch.startsWith(trimmed, "await ")) return "unknown";
|
|
720
|
+
|
|
721
|
+
// BigInt literals (digits followed by 'n')
|
|
722
|
+
if (trimmed.len > 1 and trimmed[trimmed.len - 1] == 'n' and isBigIntDigits(trimmed)) {
|
|
723
|
+
if (is_const) return trimmed;
|
|
724
|
+
return "bigint";
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Symbol
|
|
728
|
+
if (ch.startsWith(trimmed, "Symbol(") or std.mem.eql(u8, trimmed, "Symbol.for")) return "symbol";
|
|
729
|
+
|
|
730
|
+
return "unknown";
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/// Infer narrow type in union context (widens number/boolean)
|
|
734
|
+
pub fn inferNarrowTypeInUnion(alloc: std.mem.Allocator, value: []const u8, is_const: bool, depth: usize) InferError![]const u8 {
|
|
735
|
+
return inferNarrowType(alloc, value, is_const, true, depth);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/// Infer array type from array literal
|
|
739
|
+
pub fn inferArrayType(alloc: std.mem.Allocator, value: []const u8, is_const: bool, depth: usize) InferError![]const u8 {
|
|
740
|
+
const content = trim(value[1 .. value.len - 1]);
|
|
741
|
+
if (content.len == 0) return "never[]";
|
|
742
|
+
if (depth >= MAX_INFERENCE_DEPTH) return "unknown[]";
|
|
743
|
+
|
|
744
|
+
const elements = try parseArrayElements(alloc, content);
|
|
745
|
+
|
|
746
|
+
// Check for 'as const' in any element
|
|
747
|
+
var has_as_const = false;
|
|
748
|
+
for (elements) |el| {
|
|
749
|
+
if (ch.endsWith(trim(el), "as const")) {
|
|
750
|
+
has_as_const = true;
|
|
751
|
+
break;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
if (has_as_const) {
|
|
756
|
+
var parts = std.array_list.Managed(u8).init(alloc);
|
|
757
|
+
try parts.ensureTotalCapacity(content.len + 32);
|
|
758
|
+
try parts.appendSlice("readonly [\n ");
|
|
759
|
+
for (elements, 0..) |el, idx| {
|
|
760
|
+
if (idx > 0) try parts.appendSlice(" |\n ");
|
|
761
|
+
const trimmed_el = trim(el);
|
|
762
|
+
if (ch.endsWith(trimmed_el, "as const")) {
|
|
763
|
+
const without = trim(trimmed_el[0 .. trimmed_el.len - 8]);
|
|
764
|
+
if (without.len > 1 and without[0] == '[' and without[without.len - 1] == ']') {
|
|
765
|
+
const inner_content = trim(without[1 .. without.len - 1]);
|
|
766
|
+
const inner_elements = try parseArrayElements(alloc, inner_content);
|
|
767
|
+
try parts.appendSlice("readonly [");
|
|
768
|
+
for (inner_elements, 0..) |inner_el, iidx| {
|
|
769
|
+
if (iidx > 0) try parts.appendSlice(", ");
|
|
770
|
+
const t = try inferNarrowType(alloc, inner_el, true, false, depth + 1);
|
|
771
|
+
try parts.appendSlice(t);
|
|
772
|
+
}
|
|
773
|
+
try parts.append(']');
|
|
774
|
+
} else {
|
|
775
|
+
const t = try inferNarrowType(alloc, without, true, false, depth + 1);
|
|
776
|
+
try parts.appendSlice(t);
|
|
777
|
+
}
|
|
778
|
+
} else if (trimmed_el.len > 1 and trimmed_el[0] == '[' and trimmed_el[trimmed_el.len - 1] == ']') {
|
|
779
|
+
const t = try inferArrayType(alloc, trimmed_el, true, depth + 1);
|
|
780
|
+
try parts.appendSlice(t);
|
|
781
|
+
} else {
|
|
782
|
+
const t = try inferNarrowType(alloc, trimmed_el, true, false, depth + 1);
|
|
783
|
+
try parts.appendSlice(t);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
try parts.appendSlice("\n ]");
|
|
787
|
+
return parts.toOwnedSlice();
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Regular array processing — also track nested defaults for clean default building
|
|
791
|
+
const track_defaults = _collect_clean_default and !is_const;
|
|
792
|
+
var element_types = std.array_list.Managed([]const u8).init(alloc);
|
|
793
|
+
try element_types.ensureTotalCapacity(elements.len);
|
|
794
|
+
var nested_defaults = std.array_list.Managed(?[]const u8).init(alloc);
|
|
795
|
+
if (track_defaults) try nested_defaults.ensureTotalCapacity(elements.len);
|
|
796
|
+
for (elements) |el| {
|
|
797
|
+
const trimmed_el = trim(el);
|
|
798
|
+
const saved = _clean_default_result;
|
|
799
|
+
_clean_default_result = null;
|
|
800
|
+
if (trimmed_el.len > 1 and trimmed_el[0] == '[' and trimmed_el[trimmed_el.len - 1] == ']') {
|
|
801
|
+
try element_types.append(try inferArrayType(alloc, trimmed_el, is_const, depth + 1));
|
|
802
|
+
} else {
|
|
803
|
+
try element_types.append(try inferNarrowTypeInUnion(alloc, trimmed_el, is_const, depth + 1));
|
|
804
|
+
}
|
|
805
|
+
if (track_defaults) try nested_defaults.append(_clean_default_result);
|
|
806
|
+
_clean_default_result = saved;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const types = element_types.items;
|
|
810
|
+
|
|
811
|
+
// Build clean default for non-const arrays (same pass, no re-parse)
|
|
812
|
+
if (track_defaults) {
|
|
813
|
+
if (isSimpleArrayDefault(value)) {
|
|
814
|
+
_clean_default_result = try collapseWhitespace(alloc, value);
|
|
815
|
+
} else {
|
|
816
|
+
var clean_elems = std.array_list.Managed([]const u8).init(alloc);
|
|
817
|
+
try clean_elems.ensureTotalCapacity(elements.len);
|
|
818
|
+
for (elements, 0..) |el, ei| {
|
|
819
|
+
const te = trim(el);
|
|
820
|
+
if (ch.endsWith(te, " as const") or ch.endsWith(te, "as const")) continue;
|
|
821
|
+
if (isPrimitiveLiteral(te) or std.mem.eql(u8, te, "null") or std.mem.eql(u8, te, "undefined")) {
|
|
822
|
+
try clean_elems.append(te);
|
|
823
|
+
} else if (te.len > 0 and te[0] == '[' and isSimpleArrayDefault(te)) {
|
|
824
|
+
try clean_elems.append(try collapseWhitespace(alloc, te));
|
|
825
|
+
} else if (te.len > 0 and te[0] == '{') {
|
|
826
|
+
if (nested_defaults.items[ei]) |nd| try clean_elems.append(nd);
|
|
827
|
+
} else {
|
|
828
|
+
// Re-infer without union context for the clean default
|
|
829
|
+
// (types[ei] was inferred via inferNarrowTypeInUnion which
|
|
830
|
+
// wraps function types in parens and widens return types)
|
|
831
|
+
const clean_type = try inferNarrowType(alloc, te, false, false, 0);
|
|
832
|
+
if (!std.mem.eql(u8, clean_type, "unknown")) {
|
|
833
|
+
try clean_elems.append(clean_type);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
if (clean_elems.items.len > 0) {
|
|
838
|
+
var buf = std.array_list.Managed(u8).init(alloc);
|
|
839
|
+
try buf.append('[');
|
|
840
|
+
for (clean_elems.items, 0..) |item, ci| {
|
|
841
|
+
if (ci > 0) try buf.appendSlice(", ");
|
|
842
|
+
try buf.appendSlice(item);
|
|
843
|
+
}
|
|
844
|
+
try buf.append(']');
|
|
845
|
+
_clean_default_result = try buf.toOwnedSlice();
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// For const arrays, always create readonly tuples
|
|
851
|
+
if (is_const) {
|
|
852
|
+
var parts = std.array_list.Managed(u8).init(alloc);
|
|
853
|
+
try parts.ensureTotalCapacity(content.len + 16);
|
|
854
|
+
try parts.appendSlice("readonly [");
|
|
855
|
+
for (types, 0..) |t, idx| {
|
|
856
|
+
if (idx > 0) try parts.appendSlice(", ");
|
|
857
|
+
try parts.appendSlice(t);
|
|
858
|
+
}
|
|
859
|
+
try parts.append(']');
|
|
860
|
+
return parts.toOwnedSlice();
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// Single-pass: deduplicate types (O(1) HashMap lookup) AND check if all are literals
|
|
864
|
+
var unique = std.array_list.Managed([]const u8).init(alloc);
|
|
865
|
+
var unique_set = std.StringHashMap(void).init(alloc);
|
|
866
|
+
try unique_set.ensureTotalCapacity(@intCast(@max(types.len, 4)));
|
|
867
|
+
var all_literals = true;
|
|
868
|
+
for (types) |t| {
|
|
869
|
+
// O(1) dedup check via HashMap
|
|
870
|
+
if (!unique_set.contains(t)) {
|
|
871
|
+
try unique_set.put(t, {});
|
|
872
|
+
try unique.append(t);
|
|
873
|
+
}
|
|
874
|
+
// Literal check
|
|
875
|
+
if (all_literals) {
|
|
876
|
+
const is_literal = isNumericLiteral(t) or
|
|
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
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
if (all_literals and types.len <= 10) {
|
|
885
|
+
var parts = std.array_list.Managed(u8).init(alloc);
|
|
886
|
+
try parts.appendSlice("readonly [");
|
|
887
|
+
for (types, 0..) |t, idx| {
|
|
888
|
+
if (idx > 0) try parts.appendSlice(", ");
|
|
889
|
+
try parts.appendSlice(t);
|
|
890
|
+
}
|
|
891
|
+
try parts.append(']');
|
|
892
|
+
return parts.toOwnedSlice();
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
if (unique.items.len == 1) {
|
|
896
|
+
var parts = std.array_list.Managed(u8).init(alloc);
|
|
897
|
+
try parts.appendSlice(unique.items[0]);
|
|
898
|
+
try parts.appendSlice("[]");
|
|
899
|
+
return parts.toOwnedSlice();
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
var parts = std.array_list.Managed(u8).init(alloc);
|
|
903
|
+
try parts.append('(');
|
|
904
|
+
for (unique.items, 0..) |t, idx| {
|
|
905
|
+
if (idx > 0) try parts.appendSlice(" | ");
|
|
906
|
+
try parts.appendSlice(t);
|
|
907
|
+
}
|
|
908
|
+
try parts.appendSlice(")[]");
|
|
909
|
+
return parts.toOwnedSlice();
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/// Check if a value string is a primitive literal (number, string, boolean)
|
|
913
|
+
fn isPrimitiveLiteral(val: []const u8) bool {
|
|
914
|
+
if (isNumericLiteral(val)) return true;
|
|
915
|
+
if (std.mem.eql(u8, val, "true") or std.mem.eql(u8, val, "false")) return true;
|
|
916
|
+
if (val.len >= 2 and ((val[0] == '"' and val[val.len - 1] == '"') or
|
|
917
|
+
(val[0] == '\'' and val[val.len - 1] == '\''))) return true;
|
|
918
|
+
return false;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/// Check if a type is a base/widened type
|
|
922
|
+
fn isBaseType(t: []const u8) bool {
|
|
923
|
+
return std.mem.eql(u8, t, "number") or std.mem.eql(u8, t, "string") or std.mem.eql(u8, t, "boolean");
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/// Check if an array literal only contains primitives/nested arrays/objects (no runtime expressions)
|
|
927
|
+
fn isSimpleArrayDefault(val: []const u8) bool {
|
|
928
|
+
// Quick scan: reject if it contains runtime keywords or arrow functions
|
|
929
|
+
var i: usize = 0;
|
|
930
|
+
var in_string: bool = false;
|
|
931
|
+
var quote_char: u8 = 0;
|
|
932
|
+
while (i < val.len) : (i += 1) {
|
|
933
|
+
const c = val[i];
|
|
934
|
+
if (in_string) {
|
|
935
|
+
if (c == '\\') {
|
|
936
|
+
i += 1; // skip escaped char
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
if (c == quote_char) in_string = false;
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
if (c == '\'' or c == '"' or c == '`') {
|
|
943
|
+
in_string = true;
|
|
944
|
+
quote_char = c;
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
// Check for arrow =>
|
|
948
|
+
if (c == '=' and i + 1 < val.len and val[i + 1] == '>') return false;
|
|
949
|
+
// Check for keywords: new, console, process, async, await, function, yield
|
|
950
|
+
if (ch.isIdentStart(c)) {
|
|
951
|
+
const start = i;
|
|
952
|
+
while (i < val.len and ch.isIdentChar(val[i])) : (i += 1) {}
|
|
953
|
+
const word = val[start..i];
|
|
954
|
+
// Check what follows the identifier
|
|
955
|
+
var j = i;
|
|
956
|
+
while (j < val.len and ch.isWhitespace(val[j])) : (j += 1) {}
|
|
957
|
+
// If followed by ':', it's an object property key — skip it
|
|
958
|
+
if (j < val.len and val[j] == ':') {
|
|
959
|
+
if (i > 0) i -= 1;
|
|
960
|
+
continue;
|
|
961
|
+
}
|
|
962
|
+
if (j < val.len and val[j] == '(') return false; // function call
|
|
963
|
+
if (std.mem.eql(u8, word, "new") or
|
|
964
|
+
std.mem.eql(u8, word, "console") or
|
|
965
|
+
std.mem.eql(u8, word, "process") or
|
|
966
|
+
std.mem.eql(u8, word, "async") or
|
|
967
|
+
std.mem.eql(u8, word, "await") or
|
|
968
|
+
std.mem.eql(u8, word, "function") or
|
|
969
|
+
std.mem.eql(u8, word, "yield")) return false;
|
|
970
|
+
// Bare identifiers that aren't true/false are also runtime refs
|
|
971
|
+
if (!std.mem.eql(u8, word, "true") and
|
|
972
|
+
!std.mem.eql(u8, word, "false") and
|
|
973
|
+
!std.mem.eql(u8, word, "null") and
|
|
974
|
+
!std.mem.eql(u8, word, "undefined") and
|
|
975
|
+
!std.mem.eql(u8, word, "const") and
|
|
976
|
+
!std.mem.eql(u8, word, "as")) return false;
|
|
977
|
+
if (i > 0) i -= 1; // back up since outer loop will increment
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
return true;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/// Collapse whitespace in a string to single spaces
|
|
984
|
+
pub fn collapseWhitespace(alloc: std.mem.Allocator, val: []const u8) ![]const u8 {
|
|
985
|
+
// Fast path: check if there's actually any consecutive whitespace or non-space ws
|
|
986
|
+
var needs_collapse = false;
|
|
987
|
+
{
|
|
988
|
+
var prev_ws = false;
|
|
989
|
+
var in_str = false;
|
|
990
|
+
var qc: u8 = 0;
|
|
991
|
+
for (val) |c| {
|
|
992
|
+
if (in_str) {
|
|
993
|
+
if (c == '\\') {
|
|
994
|
+
prev_ws = false;
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
if (c == qc) in_str = false;
|
|
998
|
+
prev_ws = false;
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
if (c == '\'' or c == '"' or c == '`') {
|
|
1002
|
+
in_str = true;
|
|
1003
|
+
qc = c;
|
|
1004
|
+
prev_ws = false;
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
if (ch.isWhitespace(c)) {
|
|
1008
|
+
if (c != ' ' or prev_ws) {
|
|
1009
|
+
needs_collapse = true;
|
|
1010
|
+
break;
|
|
1011
|
+
}
|
|
1012
|
+
prev_ws = true;
|
|
1013
|
+
} else {
|
|
1014
|
+
prev_ws = false;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
if (!needs_collapse) return val;
|
|
1019
|
+
|
|
1020
|
+
// Slow path: actually collapse
|
|
1021
|
+
var result = std.array_list.Managed(u8).init(alloc);
|
|
1022
|
+
try result.ensureTotalCapacity(val.len);
|
|
1023
|
+
var in_ws = false;
|
|
1024
|
+
var in_string = false;
|
|
1025
|
+
var quote_char: u8 = 0;
|
|
1026
|
+
for (val) |c| {
|
|
1027
|
+
if (in_string) {
|
|
1028
|
+
try result.append(c);
|
|
1029
|
+
if (c == '\\') {
|
|
1030
|
+
// next char is escaped, handled on next iteration
|
|
1031
|
+
} else if (c == quote_char) {
|
|
1032
|
+
in_string = false;
|
|
1033
|
+
}
|
|
1034
|
+
continue;
|
|
1035
|
+
}
|
|
1036
|
+
if (c == '\'' or c == '"' or c == '`') {
|
|
1037
|
+
in_string = true;
|
|
1038
|
+
quote_char = c;
|
|
1039
|
+
in_ws = false;
|
|
1040
|
+
try result.append(c);
|
|
1041
|
+
continue;
|
|
1042
|
+
}
|
|
1043
|
+
if (ch.isWhitespace(c)) {
|
|
1044
|
+
if (!in_ws) {
|
|
1045
|
+
try result.append(' ');
|
|
1046
|
+
in_ws = true;
|
|
1047
|
+
}
|
|
1048
|
+
} else {
|
|
1049
|
+
in_ws = false;
|
|
1050
|
+
try result.append(c);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
return result.toOwnedSlice();
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
/// Infer object type from object literal.
|
|
1058
|
+
/// When _collect_clean_default is set and !is_const, also builds the @defaultValue
|
|
1059
|
+
/// content during the same pass — avoiding double-parsing of parseObjectProperties.
|
|
1060
|
+
pub fn inferObjectType(alloc: std.mem.Allocator, value: []const u8, is_const: bool, depth: usize) InferError![]const u8 {
|
|
1061
|
+
const content = trim(value[1 .. value.len - 1]);
|
|
1062
|
+
if (content.len == 0) return "{}";
|
|
1063
|
+
if (depth >= MAX_INFERENCE_DEPTH) return "Record<string, unknown>";
|
|
1064
|
+
|
|
1065
|
+
const properties = try parseObjectProperties(alloc, content);
|
|
1066
|
+
|
|
1067
|
+
// Track clean default parts when collecting and this is a non-const container
|
|
1068
|
+
const build_default = _collect_clean_default and !is_const;
|
|
1069
|
+
var clean_props = std.array_list.Managed([]const u8).init(alloc);
|
|
1070
|
+
|
|
1071
|
+
var parts = std.array_list.Managed(u8).init(alloc);
|
|
1072
|
+
try parts.ensureTotalCapacity(content.len + 32);
|
|
1073
|
+
try parts.appendSlice("{\n ");
|
|
1074
|
+
for (properties, 0..) |prop, idx| {
|
|
1075
|
+
if (idx > 0) try parts.appendSlice(";\n ");
|
|
1076
|
+
|
|
1077
|
+
// Save parent's clean default before recursive call (nested objects overwrite it)
|
|
1078
|
+
const saved_default = _clean_default_result;
|
|
1079
|
+
_clean_default_result = null;
|
|
1080
|
+
|
|
1081
|
+
var val_type = try inferNarrowType(alloc, prop[1], is_const, false, depth + 1);
|
|
1082
|
+
|
|
1083
|
+
// Capture nested clean default (set by recursive inferObjectType/inferArrayType)
|
|
1084
|
+
const nested_default = _clean_default_result;
|
|
1085
|
+
_clean_default_result = saved_default; // restore parent's
|
|
1086
|
+
|
|
1087
|
+
// Clean method signatures in inferred types
|
|
1088
|
+
if (ch.contains(val_type, "=>")) {
|
|
1089
|
+
val_type = try cleanMethodSignature(alloc, val_type);
|
|
1090
|
+
} else if (ch.contains(val_type, "async")) {
|
|
1091
|
+
val_type = try stripAsyncKeyword(alloc, val_type);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// Add inline @defaultValue for widened primitive properties
|
|
1095
|
+
const raw_val = trim(prop[1]);
|
|
1096
|
+
if (!is_const and isBaseType(val_type) and isPrimitiveLiteral(raw_val)) {
|
|
1097
|
+
try parts.appendSlice("/** @defaultValue ");
|
|
1098
|
+
try parts.appendSlice(raw_val);
|
|
1099
|
+
try parts.appendSlice(" */\n ");
|
|
1100
|
+
}
|
|
1101
|
+
try parts.appendSlice(prop[0]); // key
|
|
1102
|
+
try parts.appendSlice(": ");
|
|
1103
|
+
try parts.appendSlice(val_type);
|
|
1104
|
+
|
|
1105
|
+
// Build clean default entry for this property (same loop, no re-parse)
|
|
1106
|
+
if (build_default) {
|
|
1107
|
+
if (ch.endsWith(raw_val, " as const") or ch.endsWith(raw_val, "as const")) {
|
|
1108
|
+
// skip — type already narrow
|
|
1109
|
+
} else if (isPrimitiveLiteral(raw_val)) {
|
|
1110
|
+
var ps = std.array_list.Managed(u8).init(alloc);
|
|
1111
|
+
try ps.appendSlice(prop[0]);
|
|
1112
|
+
try ps.appendSlice(": ");
|
|
1113
|
+
try ps.appendSlice(raw_val);
|
|
1114
|
+
try clean_props.append(try ps.toOwnedSlice());
|
|
1115
|
+
} else if (raw_val.len > 0 and raw_val[0] == '[' and isSimpleArrayDefault(raw_val)) {
|
|
1116
|
+
var ps = std.array_list.Managed(u8).init(alloc);
|
|
1117
|
+
try ps.appendSlice(prop[0]);
|
|
1118
|
+
try ps.appendSlice(": ");
|
|
1119
|
+
try ps.appendSlice(try collapseWhitespace(alloc, raw_val));
|
|
1120
|
+
try clean_props.append(try ps.toOwnedSlice());
|
|
1121
|
+
} else if (raw_val.len > 0 and raw_val[0] == '{') {
|
|
1122
|
+
if (nested_default) |nd| {
|
|
1123
|
+
var ps = std.array_list.Managed(u8).init(alloc);
|
|
1124
|
+
try ps.appendSlice(prop[0]);
|
|
1125
|
+
try ps.appendSlice(": ");
|
|
1126
|
+
try ps.appendSlice(nd);
|
|
1127
|
+
try clean_props.append(try ps.toOwnedSlice());
|
|
1128
|
+
}
|
|
1129
|
+
} else if (raw_val.len > 0 and raw_val[0] != '[' and
|
|
1130
|
+
(ch.contains(raw_val, "=>") or ch.startsWith(raw_val, "function") or ch.startsWith(raw_val, "async")))
|
|
1131
|
+
{
|
|
1132
|
+
// Use already-computed val_type instead of re-inferring
|
|
1133
|
+
var ps = std.array_list.Managed(u8).init(alloc);
|
|
1134
|
+
try ps.appendSlice(prop[0]);
|
|
1135
|
+
try ps.appendSlice(": ");
|
|
1136
|
+
try ps.appendSlice(val_type);
|
|
1137
|
+
try clean_props.append(try ps.toOwnedSlice());
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
try parts.appendSlice("\n}");
|
|
1142
|
+
|
|
1143
|
+
// Store computed clean default for parent/emitter to consume
|
|
1144
|
+
if (build_default and clean_props.items.len > 0) {
|
|
1145
|
+
var one_line = std.array_list.Managed(u8).init(alloc);
|
|
1146
|
+
try one_line.appendSlice("{ ");
|
|
1147
|
+
for (clean_props.items, 0..) |item, ci| {
|
|
1148
|
+
if (ci > 0) try one_line.appendSlice(", ");
|
|
1149
|
+
try one_line.appendSlice(item);
|
|
1150
|
+
}
|
|
1151
|
+
try one_line.appendSlice(" }");
|
|
1152
|
+
const one_line_str = try one_line.toOwnedSlice();
|
|
1153
|
+
if (one_line_str.len <= 80) {
|
|
1154
|
+
_clean_default_result = one_line_str;
|
|
1155
|
+
} else {
|
|
1156
|
+
// Multi-line with proper indentation based on nesting depth.
|
|
1157
|
+
// depth increments by 2 per nesting level (once in inferNarrowType, once here),
|
|
1158
|
+
// so indent = (depth - 1) / 2 maps depth to the correct indent level.
|
|
1159
|
+
const indent = if (depth > 0) (depth - 1) / 2 else 0;
|
|
1160
|
+
const pad_size = (indent + 1) * 2;
|
|
1161
|
+
const close_pad_size = indent * 2;
|
|
1162
|
+
var ml = std.array_list.Managed(u8).init(alloc);
|
|
1163
|
+
try ml.appendSlice("{\n");
|
|
1164
|
+
for (clean_props.items, 0..) |item, ci| {
|
|
1165
|
+
{
|
|
1166
|
+
var p: usize = 0;
|
|
1167
|
+
while (p < pad_size) : (p += 1) try ml.append(' ');
|
|
1168
|
+
}
|
|
1169
|
+
try ml.appendSlice(item);
|
|
1170
|
+
if (ci < clean_props.items.len - 1) try ml.append(',');
|
|
1171
|
+
try ml.append('\n');
|
|
1172
|
+
}
|
|
1173
|
+
{
|
|
1174
|
+
var p: usize = 0;
|
|
1175
|
+
while (p < close_pad_size) : (p += 1) try ml.append(' ');
|
|
1176
|
+
}
|
|
1177
|
+
try ml.append('}');
|
|
1178
|
+
_clean_default_result = try ml.toOwnedSlice();
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
return parts.toOwnedSlice();
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
/// Infer type from new expression
|
|
1186
|
+
fn inferNewExpressionType(alloc: std.mem.Allocator, value: []const u8) InferError![]const u8 {
|
|
1187
|
+
// Extract class name after "new "
|
|
1188
|
+
var i: usize = 4; // skip "new "
|
|
1189
|
+
while (i < value.len and ch.isWhitespace(value[i])) i += 1;
|
|
1190
|
+
const name_start = i;
|
|
1191
|
+
|
|
1192
|
+
// Read class name (must start with uppercase)
|
|
1193
|
+
if (i >= value.len or value[i] < 'A' or value[i] > 'Z') return "unknown";
|
|
1194
|
+
while (i < value.len and ch.isIdentChar(value[i])) i += 1;
|
|
1195
|
+
const class_name = value[name_start..i];
|
|
1196
|
+
|
|
1197
|
+
// Check for explicit generic type parameters
|
|
1198
|
+
if (i < value.len and value[i] == '<') {
|
|
1199
|
+
if (findMatchingBracket(value, i, '<', '>')) |end| {
|
|
1200
|
+
var result = std.array_list.Managed(u8).init(alloc);
|
|
1201
|
+
try result.appendSlice(class_name);
|
|
1202
|
+
try result.appendSlice(value[i .. end + 1]);
|
|
1203
|
+
return result.toOwnedSlice();
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// Fallback for known built-in types
|
|
1208
|
+
if (std.mem.eql(u8, class_name, "Date")) return "Date";
|
|
1209
|
+
if (std.mem.eql(u8, class_name, "Map")) return "Map<any, any>";
|
|
1210
|
+
if (std.mem.eql(u8, class_name, "Set")) return "Set<any>";
|
|
1211
|
+
if (std.mem.eql(u8, class_name, "WeakMap")) return "WeakMap<any, any>";
|
|
1212
|
+
if (std.mem.eql(u8, class_name, "WeakSet")) return "WeakSet<any>";
|
|
1213
|
+
if (std.mem.eql(u8, class_name, "RegExp")) return "RegExp";
|
|
1214
|
+
if (std.mem.eql(u8, class_name, "Error")) return "Error";
|
|
1215
|
+
if (std.mem.eql(u8, class_name, "Array")) return "any[]";
|
|
1216
|
+
if (std.mem.eql(u8, class_name, "Object")) return "object";
|
|
1217
|
+
if (std.mem.eql(u8, class_name, "Function")) return "Function";
|
|
1218
|
+
if (std.mem.eql(u8, class_name, "Promise")) return "Promise<any>";
|
|
1219
|
+
|
|
1220
|
+
return class_name;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
/// Infer type from Promise expression
|
|
1224
|
+
fn inferPromiseType(alloc: std.mem.Allocator, value: []const u8, is_const: bool, depth: usize) InferError![]const u8 {
|
|
1225
|
+
if (ch.startsWith(value, "Promise.resolve(")) {
|
|
1226
|
+
// Extract argument
|
|
1227
|
+
const paren_start = std.mem.indexOf(u8, value, "(") orelse return "Promise<unknown>";
|
|
1228
|
+
const paren_end = std.mem.lastIndexOf(u8, value, ")") orelse return "Promise<unknown>";
|
|
1229
|
+
if (paren_end > paren_start + 1) {
|
|
1230
|
+
const arg = trim(value[paren_start + 1 .. paren_end]);
|
|
1231
|
+
// Promise resolved values are immutable, so preserve is_const from context
|
|
1232
|
+
const arg_type = try inferNarrowType(alloc, arg, is_const, false, depth + 1);
|
|
1233
|
+
var result = std.array_list.Managed(u8).init(alloc);
|
|
1234
|
+
try result.appendSlice("Promise<");
|
|
1235
|
+
try result.appendSlice(arg_type);
|
|
1236
|
+
try result.append('>');
|
|
1237
|
+
return result.toOwnedSlice();
|
|
1238
|
+
}
|
|
1239
|
+
return "Promise<unknown>";
|
|
1240
|
+
}
|
|
1241
|
+
if (ch.startsWith(value, "Promise.reject(")) return "Promise<never>";
|
|
1242
|
+
if (ch.startsWith(value, "Promise.all(")) {
|
|
1243
|
+
// Extract the array argument and infer element types
|
|
1244
|
+
const paren_start = std.mem.indexOf(u8, value, "(") orelse return "Promise<unknown[]>";
|
|
1245
|
+
const paren_end = std.mem.lastIndexOf(u8, value, ")") orelse return "Promise<unknown[]>";
|
|
1246
|
+
if (paren_end > paren_start + 1) {
|
|
1247
|
+
const arg = trim(value[paren_start + 1 .. paren_end]);
|
|
1248
|
+
if (arg.len > 1 and arg[0] == '[' and arg[arg.len - 1] == ']') {
|
|
1249
|
+
// It's an array argument — infer as tuple
|
|
1250
|
+
const elements = try parseArrayElements(alloc, arg[1 .. arg.len - 1]);
|
|
1251
|
+
if (elements.len > 0) {
|
|
1252
|
+
var result = std.array_list.Managed(u8).init(alloc);
|
|
1253
|
+
try result.appendSlice("Promise<[");
|
|
1254
|
+
for (elements, 0..) |elem, idx| {
|
|
1255
|
+
if (idx > 0) try result.appendSlice(", ");
|
|
1256
|
+
// For Promise.resolve(x), extract x's type
|
|
1257
|
+
if (ch.startsWith(elem, "Promise.resolve(")) {
|
|
1258
|
+
const ps = std.mem.indexOf(u8, elem, "(") orelse {
|
|
1259
|
+
try result.appendSlice("unknown");
|
|
1260
|
+
continue;
|
|
1261
|
+
};
|
|
1262
|
+
const pe = std.mem.lastIndexOf(u8, elem, ")") orelse {
|
|
1263
|
+
try result.appendSlice("unknown");
|
|
1264
|
+
continue;
|
|
1265
|
+
};
|
|
1266
|
+
if (pe > ps + 1) {
|
|
1267
|
+
const inner_arg = trim(elem[ps + 1 .. pe]);
|
|
1268
|
+
const inner_type = try inferNarrowType(alloc, inner_arg, is_const, false, depth + 1);
|
|
1269
|
+
try result.appendSlice(inner_type);
|
|
1270
|
+
} else {
|
|
1271
|
+
try result.appendSlice("unknown");
|
|
1272
|
+
}
|
|
1273
|
+
} else {
|
|
1274
|
+
const elem_type = try inferNarrowType(alloc, elem, is_const, false, depth + 1);
|
|
1275
|
+
try result.appendSlice(elem_type);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
try result.appendSlice("]>");
|
|
1279
|
+
return result.toOwnedSlice();
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
return "Promise<unknown[]>";
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
return "Promise<unknown>";
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
/// Infer function type from function expression
|
|
1290
|
+
pub fn inferFunctionType(alloc: std.mem.Allocator, value: []const u8, in_union: bool, depth: usize, is_const: bool) InferError![]const u8 {
|
|
1291
|
+
const trimmed = trim(value);
|
|
1292
|
+
|
|
1293
|
+
// Handle very complex function types early
|
|
1294
|
+
if (trimmed.len > 200 and countOccurrences(trimmed, "=>") > 2 and countOccurrences(trimmed, "<") > 5 and !ch.startsWith(trimmed, "function")) {
|
|
1295
|
+
const func_type = "(...args: any[]) => any";
|
|
1296
|
+
if (in_union) {
|
|
1297
|
+
var result = std.array_list.Managed(u8).init(alloc);
|
|
1298
|
+
try result.append('(');
|
|
1299
|
+
try result.appendSlice(func_type);
|
|
1300
|
+
try result.append(')');
|
|
1301
|
+
return result.toOwnedSlice();
|
|
1302
|
+
}
|
|
1303
|
+
return func_type;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
// Handle async arrow functions
|
|
1307
|
+
if (ch.startsWith(trimmed, "async ") and ch.contains(trimmed, "=>")) {
|
|
1308
|
+
const async_removed = trim(trimmed[5..]);
|
|
1309
|
+
if (findMainArrowIndex(async_removed)) |arrow_idx| {
|
|
1310
|
+
var params = trim(async_removed[0..arrow_idx]);
|
|
1311
|
+
const body = trim(async_removed[arrow_idx + 2 ..]);
|
|
1312
|
+
|
|
1313
|
+
// Wrap bare params
|
|
1314
|
+
if (params.len == 0 or std.mem.eql(u8, params, "()")) {
|
|
1315
|
+
params = "()";
|
|
1316
|
+
} else if (params[0] != '(') {
|
|
1317
|
+
var p = std.array_list.Managed(u8).init(alloc);
|
|
1318
|
+
try p.append('(');
|
|
1319
|
+
try p.appendSlice(params);
|
|
1320
|
+
try p.append(')');
|
|
1321
|
+
params = try p.toOwnedSlice();
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
var return_type: []const u8 = "unknown";
|
|
1325
|
+
if (body.len > 0 and body[0] != '{') {
|
|
1326
|
+
return_type = try inferNarrowType(alloc, body, is_const, false, depth + 1);
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
var result = std.array_list.Managed(u8).init(alloc);
|
|
1330
|
+
try result.appendSlice(params);
|
|
1331
|
+
try result.appendSlice(" => Promise<");
|
|
1332
|
+
try result.appendSlice(return_type);
|
|
1333
|
+
try result.append('>');
|
|
1334
|
+
const func_type = try result.toOwnedSlice();
|
|
1335
|
+
|
|
1336
|
+
if (in_union) {
|
|
1337
|
+
var wrapped = std.array_list.Managed(u8).init(alloc);
|
|
1338
|
+
try wrapped.append('(');
|
|
1339
|
+
try wrapped.appendSlice(func_type);
|
|
1340
|
+
try wrapped.append(')');
|
|
1341
|
+
return wrapped.toOwnedSlice();
|
|
1342
|
+
}
|
|
1343
|
+
return func_type;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// Regular arrow functions
|
|
1348
|
+
if (ch.contains(trimmed, "=>")) {
|
|
1349
|
+
var generics: []const u8 = "";
|
|
1350
|
+
var remaining = trimmed;
|
|
1351
|
+
|
|
1352
|
+
if (trimmed[0] == '<') {
|
|
1353
|
+
if (findMatchingBracket(trimmed, 0, '<', '>')) |gen_end| {
|
|
1354
|
+
generics = trimmed[0 .. gen_end + 1];
|
|
1355
|
+
remaining = trim(trimmed[gen_end + 1 ..]);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
if (findMainArrowIndex(remaining)) |arrow_idx| {
|
|
1360
|
+
var params = trim(remaining[0..arrow_idx]);
|
|
1361
|
+
const body = trim(remaining[arrow_idx + 2 ..]);
|
|
1362
|
+
|
|
1363
|
+
// Check for explicit return type annotation
|
|
1364
|
+
var explicit_return_type: []const u8 = "";
|
|
1365
|
+
// Look for ): ReturnType pattern at end of params
|
|
1366
|
+
if (std.mem.lastIndexOf(u8, params, "):")) |ri| {
|
|
1367
|
+
explicit_return_type = trim(params[ri + 2 ..]);
|
|
1368
|
+
params = params[0 .. ri + 1];
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
if (params.len == 0 or std.mem.eql(u8, params, "()")) {
|
|
1372
|
+
params = "()";
|
|
1373
|
+
} else if (params[0] != '(') {
|
|
1374
|
+
var p = std.array_list.Managed(u8).init(alloc);
|
|
1375
|
+
try p.append('(');
|
|
1376
|
+
try p.appendSlice(params);
|
|
1377
|
+
try p.append(')');
|
|
1378
|
+
params = try p.toOwnedSlice();
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
var return_type: []const u8 = "unknown";
|
|
1382
|
+
if (explicit_return_type.len > 0) {
|
|
1383
|
+
return_type = explicit_return_type;
|
|
1384
|
+
} else if (body.len > 0 and body[0] == '{') {
|
|
1385
|
+
return_type = "unknown";
|
|
1386
|
+
} else if (ch.contains(body, "=>")) {
|
|
1387
|
+
// Higher-order function returning another function
|
|
1388
|
+
// Try to extract the outer function signature: (params) =>
|
|
1389
|
+
const inner = try extractInnerFunctionSignature(alloc, body, generics);
|
|
1390
|
+
return_type = inner;
|
|
1391
|
+
} else if (!in_union) {
|
|
1392
|
+
return_type = try inferNarrowType(alloc, body, is_const, false, depth + 1);
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
var result = std.array_list.Managed(u8).init(alloc);
|
|
1396
|
+
try result.appendSlice(generics);
|
|
1397
|
+
try result.appendSlice(params);
|
|
1398
|
+
try result.appendSlice(" => ");
|
|
1399
|
+
try result.appendSlice(return_type);
|
|
1400
|
+
const func_type = try result.toOwnedSlice();
|
|
1401
|
+
|
|
1402
|
+
if (in_union) {
|
|
1403
|
+
var wrapped = std.array_list.Managed(u8).init(alloc);
|
|
1404
|
+
try wrapped.append('(');
|
|
1405
|
+
try wrapped.appendSlice(func_type);
|
|
1406
|
+
try wrapped.append(')');
|
|
1407
|
+
return wrapped.toOwnedSlice();
|
|
1408
|
+
}
|
|
1409
|
+
return func_type;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
const fallback = "() => unknown";
|
|
1413
|
+
if (in_union) {
|
|
1414
|
+
var result = std.array_list.Managed(u8).init(alloc);
|
|
1415
|
+
try result.append('(');
|
|
1416
|
+
try result.appendSlice(fallback);
|
|
1417
|
+
try result.append(')');
|
|
1418
|
+
return result.toOwnedSlice();
|
|
1419
|
+
}
|
|
1420
|
+
return fallback;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// function expressions
|
|
1424
|
+
if (ch.startsWith(trimmed, "function")) {
|
|
1425
|
+
// Try to extract params
|
|
1426
|
+
if (ch.indexOfChar(trimmed, '(', 0)) |paren_start| {
|
|
1427
|
+
if (findMatchingBracket(trimmed, paren_start, '(', ')')) |paren_end| {
|
|
1428
|
+
const params = trim(trimmed[paren_start .. paren_end + 1]);
|
|
1429
|
+
// Check for generator
|
|
1430
|
+
const is_generator = ch.indexOfChar(trimmed[0..paren_start], '*', 0) != null;
|
|
1431
|
+
// Check for generics
|
|
1432
|
+
var generics: []const u8 = "";
|
|
1433
|
+
if (ch.indexOfChar(trimmed, '<', 0)) |angle_start| {
|
|
1434
|
+
if (angle_start < paren_start) {
|
|
1435
|
+
if (findMatchingBracket(trimmed, angle_start, '<', '>')) |angle_end| {
|
|
1436
|
+
generics = trimmed[angle_start .. angle_end + 1];
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
// Check for explicit return type annotation after params
|
|
1442
|
+
var return_type: []const u8 = if (is_generator) "Generator<any, any, any>" else "unknown";
|
|
1443
|
+
const after_params = trim(trimmed[paren_end + 1 ..]);
|
|
1444
|
+
if (after_params.len > 0 and after_params[0] == ':') {
|
|
1445
|
+
// Extract return type up to '{'
|
|
1446
|
+
var rt_end: usize = after_params.len;
|
|
1447
|
+
var rt_depth: i32 = 0;
|
|
1448
|
+
var rt_i: usize = 1;
|
|
1449
|
+
while (rt_i < after_params.len) : (rt_i += 1) {
|
|
1450
|
+
if (after_params[rt_i] == '<') rt_depth += 1 else if (after_params[rt_i] == '>') rt_depth -= 1;
|
|
1451
|
+
if (rt_depth == 0 and after_params[rt_i] == '{') {
|
|
1452
|
+
rt_end = rt_i;
|
|
1453
|
+
break;
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
const rt = trim(after_params[1..rt_end]);
|
|
1457
|
+
if (rt.len > 0) return_type = rt;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
var result = std.array_list.Managed(u8).init(alloc);
|
|
1461
|
+
if (in_union) try result.append('(');
|
|
1462
|
+
try result.appendSlice(generics);
|
|
1463
|
+
try result.appendSlice(params);
|
|
1464
|
+
try result.appendSlice(" => ");
|
|
1465
|
+
try result.appendSlice(return_type);
|
|
1466
|
+
if (in_union) try result.append(')');
|
|
1467
|
+
return result.toOwnedSlice();
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
const fallback = "(...args: any[]) => unknown";
|
|
1472
|
+
if (in_union) {
|
|
1473
|
+
var result = std.array_list.Managed(u8).init(alloc);
|
|
1474
|
+
try result.append('(');
|
|
1475
|
+
try result.appendSlice(fallback);
|
|
1476
|
+
try result.append(')');
|
|
1477
|
+
return result.toOwnedSlice();
|
|
1478
|
+
}
|
|
1479
|
+
return fallback;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
const fallback = "() => unknown";
|
|
1483
|
+
if (in_union) {
|
|
1484
|
+
var result = std.array_list.Managed(u8).init(alloc);
|
|
1485
|
+
try result.append('(');
|
|
1486
|
+
try result.appendSlice(fallback);
|
|
1487
|
+
try result.append(')');
|
|
1488
|
+
return result.toOwnedSlice();
|
|
1489
|
+
}
|
|
1490
|
+
return fallback;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
/// Extract type from 'satisfies' operator
|
|
1494
|
+
pub fn extractSatisfiesType(value: []const u8) ?[]const u8 {
|
|
1495
|
+
const needle = " satisfies ";
|
|
1496
|
+
// Find last occurrence
|
|
1497
|
+
var last_idx: ?usize = null;
|
|
1498
|
+
var search_from: usize = 0;
|
|
1499
|
+
while (ch.indexOf(value, needle, search_from)) |idx| {
|
|
1500
|
+
last_idx = idx;
|
|
1501
|
+
search_from = idx + 1;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
if (last_idx) |si| {
|
|
1505
|
+
var type_str = trim(value[si + needle.len ..]);
|
|
1506
|
+
// Remove trailing semicolon
|
|
1507
|
+
if (type_str.len > 0 and type_str[type_str.len - 1] == ';') {
|
|
1508
|
+
type_str = trim(type_str[0 .. type_str.len - 1]);
|
|
1509
|
+
}
|
|
1510
|
+
if (type_str.len > 0) return type_str;
|
|
1511
|
+
}
|
|
1512
|
+
return null;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
/// Check if a type annotation is a generic/broad type that should be replaced with narrow inference
|
|
1516
|
+
pub fn isGenericType(type_annotation: []const u8) bool {
|
|
1517
|
+
const trimmed = trim(type_annotation);
|
|
1518
|
+
if (std.mem.eql(u8, trimmed, "any") or std.mem.eql(u8, trimmed, "object") or std.mem.eql(u8, trimmed, "unknown")) return true;
|
|
1519
|
+
if (ch.startsWith(trimmed, "Record<") and ch.endsWith(trimmed, ">")) return true;
|
|
1520
|
+
if (ch.startsWith(trimmed, "Array<") and ch.endsWith(trimmed, ">")) return true;
|
|
1521
|
+
// Object types like { [key: string]: any|string|number|unknown }
|
|
1522
|
+
if (trimmed.len > 4 and trimmed[0] == '{' and trimmed[trimmed.len - 1] == '}') {
|
|
1523
|
+
if (ch.indexOfChar(trimmed, '[', 0)) |bracket_start| {
|
|
1524
|
+
if (ch.indexOfChar(trimmed, ']', bracket_start)) |bracket_end| {
|
|
1525
|
+
var vi = bracket_end + 1;
|
|
1526
|
+
while (vi < trimmed.len and (trimmed[vi] == ':' or trimmed[vi] == ' ')) vi += 1;
|
|
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;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
// --- Tests ---
|
|
1542
|
+
test "isNumericLiteral" {
|
|
1543
|
+
try std.testing.expect(isNumericLiteral("42"));
|
|
1544
|
+
try std.testing.expect(isNumericLiteral("-3.14"));
|
|
1545
|
+
try std.testing.expect(isNumericLiteral("0"));
|
|
1546
|
+
try std.testing.expect(!isNumericLiteral(""));
|
|
1547
|
+
try std.testing.expect(!isNumericLiteral("abc"));
|
|
1548
|
+
try std.testing.expect(!isNumericLiteral("-"));
|
|
1549
|
+
try std.testing.expect(!isNumericLiteral("3."));
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
test "inferNarrowType basics" {
|
|
1553
|
+
const alloc = std.testing.allocator;
|
|
1554
|
+
try std.testing.expectEqualStrings("42", try inferNarrowType(alloc, "42", true, false, 0));
|
|
1555
|
+
try std.testing.expectEqualStrings("number", try inferNarrowType(alloc, "42", false, true, 0));
|
|
1556
|
+
try std.testing.expectEqualStrings("true", try inferNarrowType(alloc, "true", true, false, 0));
|
|
1557
|
+
try std.testing.expectEqualStrings("null", try inferNarrowType(alloc, "null", false, false, 0));
|
|
1558
|
+
try std.testing.expectEqualStrings("unknown", try inferNarrowType(alloc, "", false, false, 0));
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
test "extractSatisfiesType" {
|
|
1562
|
+
try std.testing.expectEqualStrings("Config", extractSatisfiesType("{ port: 3000 } satisfies Config").?);
|
|
1563
|
+
try std.testing.expect(extractSatisfiesType("just a value without it") == null);
|
|
1564
|
+
}
|