@nudojs/service 0.2.0 → 0.2.1

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/dist/index.js CHANGED
@@ -1,18 +1,24 @@
1
1
  // src/analyzer.ts
2
- import { readFileSync, existsSync } from "fs";
2
+ import { readFileSync, existsSync, statSync, realpathSync } from "fs";
3
3
  import { resolve, dirname } from "path";
4
4
  import traverse from "@babel/traverse";
5
5
  import {
6
6
  T,
7
7
  typeValueToString,
8
8
  simplifyUnion,
9
+ widenLiteral,
10
+ collapseLiteralUnion,
9
11
  createEnvironment,
10
- isSubtypeOf
12
+ isSubtypeOf,
13
+ mockHelperToTypeValue
11
14
  } from "@nudojs/core";
12
- import { parse, extractDirectives, parseTypeValueExpr } from "@nudojs/parser";
15
+ import { parse, extractDirectives, extractFileDirectives, parseTypeValueExpr } from "@nudojs/parser";
13
16
  import {
14
17
  evaluateFunction,
15
18
  evaluateFunctionFull,
19
+ memberMayExistOn,
20
+ setUsageSiteTag,
21
+ USAGE_SITE_MODULE,
16
22
  evaluateProgram,
17
23
  setModuleResolver,
18
24
  setCurrentFileDir,
@@ -20,24 +26,204 @@ import {
20
26
  getUnreachableRanges,
21
27
  resetUnreachableRanges,
22
28
  setNodeTypeCollector,
23
- setSampleCount
29
+ setCallCollector,
30
+ setUnknownCollector,
31
+ setProvenanceTracking,
32
+ setSampleCount,
33
+ setUnknownBuiltinHandler,
34
+ setEnvModules,
35
+ resetEnvModules,
36
+ setMockModules,
37
+ resetMockModules,
38
+ setCurrentSource,
39
+ loadEnvs,
40
+ preloadPathEnvs,
41
+ findProjectConfig,
42
+ resolveNpmNudo
24
43
  } from "@nudojs/cli/evaluator";
25
44
  function resolveModule(source, fromDir) {
26
45
  const extensions = [".js", ".ts", ".mjs"];
46
+ const nudoPath = resolveNpmNudo(source, fromDir);
47
+ if (nudoPath) {
48
+ const src = readFileSync(nudoPath, "utf-8");
49
+ return { ast: parse(src), filePath: nudoPath };
50
+ }
27
51
  const basePath = resolve(fromDir, source);
28
52
  for (const ext of ["", ...extensions]) {
29
53
  const candidate = basePath + ext;
30
- if (existsSync(candidate)) {
31
- const src = readFileSync(candidate, "utf-8");
32
- return { ast: parse(src), filePath: candidate };
54
+ if (!existsSync(candidate)) continue;
55
+ if (statSync(candidate).isDirectory()) {
56
+ let entry = null;
57
+ const pkgPath = resolve(candidate, "package.json");
58
+ if (existsSync(pkgPath)) {
59
+ try {
60
+ const main = JSON.parse(readFileSync(pkgPath, "utf-8")).main;
61
+ if (typeof main === "string") {
62
+ for (const e of ["", ...extensions]) {
63
+ const p = resolve(candidate, main + e);
64
+ if (existsSync(p) && statSync(p).isFile()) {
65
+ entry = p;
66
+ break;
67
+ }
68
+ }
69
+ }
70
+ } catch {
71
+ }
72
+ }
73
+ if (!entry) {
74
+ for (const e of ["", ...extensions]) {
75
+ const p = resolve(candidate, "index" + e);
76
+ if (existsSync(p) && statSync(p).isFile()) {
77
+ entry = p;
78
+ break;
79
+ }
80
+ }
81
+ }
82
+ if (!entry) return null;
83
+ const src2 = readFileSync(entry, "utf-8");
84
+ return { ast: parse(src2), filePath: entry };
85
+ }
86
+ if (candidate.endsWith(".json")) {
87
+ try {
88
+ return { ast: parse("module.exports = undefined;"), filePath: candidate, json: JSON.parse(readFileSync(candidate, "utf-8")) };
89
+ } catch {
90
+ return null;
91
+ }
33
92
  }
93
+ const src = readFileSync(candidate, "utf-8");
94
+ return { ast: parse(src), filePath: candidate };
95
+ }
96
+ return null;
97
+ }
98
+ function resolveImportPath(specifier, fromDir) {
99
+ const basePath = resolve(fromDir, specifier);
100
+ for (const ext of ["", ".js", ".ts", ".mjs"]) {
101
+ const candidate = basePath + ext;
102
+ if (existsSync(candidate)) return candidate;
34
103
  }
35
104
  return null;
36
105
  }
37
- function applyMocks(directives, env, filePath) {
106
+ function buildModuleGraph(files, cache) {
107
+ const imports = /* @__PURE__ */ new Map();
108
+ const dependents = /* @__PURE__ */ new Map();
109
+ const edge = (from, to) => {
110
+ if (!imports.has(from)) imports.set(from, /* @__PURE__ */ new Set());
111
+ if (!dependents.has(to)) dependents.set(to, /* @__PURE__ */ new Set());
112
+ imports.get(from).add(to);
113
+ dependents.get(to).add(from);
114
+ };
115
+ for (const file of files) {
116
+ if (!imports.has(file)) imports.set(file, /* @__PURE__ */ new Set());
117
+ if (!dependents.has(file)) dependents.set(file, /* @__PURE__ */ new Set());
118
+ let edges;
119
+ if (cache) {
120
+ let stat = null;
121
+ try {
122
+ stat = statSync(file);
123
+ } catch {
124
+ }
125
+ const cached = stat ? cache.get(file) : void 0;
126
+ if (stat && cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
127
+ edges = cached.edges;
128
+ } else {
129
+ edges = extractImportEdges(file);
130
+ if (stat) cache.set(file, { mtimeMs: stat.mtimeMs, size: stat.size, edges });
131
+ }
132
+ } else {
133
+ edges = extractImportEdges(file);
134
+ }
135
+ for (const to of edges) edge(file, to);
136
+ }
137
+ return { imports, dependents };
138
+ }
139
+ function extractImportEdges(file) {
140
+ const edges = [];
141
+ let ast;
142
+ try {
143
+ ast = parse(readFileSync(file, "utf-8"));
144
+ } catch {
145
+ return edges;
146
+ }
147
+ for (const stmt of ast.program.body) {
148
+ if (stmt.type !== "ImportDeclaration") continue;
149
+ const specifier = stmt.source.value;
150
+ if (!specifier.startsWith(".") && !specifier.startsWith("/")) continue;
151
+ const resolved = resolveImportPath(specifier, dirname(file));
152
+ if (resolved) edges.push(resolved);
153
+ }
154
+ return edges;
155
+ }
156
+ function computeDirtySet(dependents, changedFile) {
157
+ const dirty = [];
158
+ const visited = /* @__PURE__ */ new Set();
159
+ const queue = [changedFile];
160
+ while (queue.length > 0) {
161
+ const file = queue.shift();
162
+ if (visited.has(file)) continue;
163
+ visited.add(file);
164
+ dirty.push(file);
165
+ for (const dep of dependents.get(file) ?? []) {
166
+ if (!visited.has(dep)) queue.push(dep);
167
+ }
168
+ }
169
+ return dirty;
170
+ }
171
+ function topoSortDirty(imports, dirty) {
172
+ const inSet = new Set(dirty);
173
+ const pending = /* @__PURE__ */ new Map();
174
+ for (const file of dirty) {
175
+ let count = 0;
176
+ for (const dep of imports.get(file) ?? []) {
177
+ if (inSet.has(dep)) count++;
178
+ }
179
+ pending.set(file, count);
180
+ }
181
+ const ordered = [];
182
+ const ready = dirty.filter((f) => pending.get(f) === 0);
183
+ while (ready.length > 0) {
184
+ const file = ready.shift();
185
+ ordered.push(file);
186
+ for (const other of dirty) {
187
+ if (pending.get(other) === void 0) continue;
188
+ if ((imports.get(other) ?? /* @__PURE__ */ new Set()).has(file)) {
189
+ const next = pending.get(other) - 1;
190
+ pending.set(other, next);
191
+ if (next === 0) ready.push(other);
192
+ }
193
+ }
194
+ }
195
+ for (const file of dirty) {
196
+ if (pending.has(file) && pending.get(file) > 0) ordered.push(file);
197
+ }
198
+ return ordered;
199
+ }
200
+ function applyMocks(directives, env, filePath, diagnostics) {
38
201
  for (const d of directives) {
39
202
  if (d.kind !== "mock") continue;
40
- if (d.expression) {
203
+ if (d.arrowFn) {
204
+ const fnType = T.fn(d.arrowFn.params, d.arrowFn.body, env);
205
+ fnType._paramPatterns = d.arrowFn.paramPatterns;
206
+ env.bind(d.name, fnType);
207
+ } else if (d.nudoMock) {
208
+ const typeVal = mockHelperToTypeValue(d.nudoMock, env);
209
+ env.bind(d.name, typeVal);
210
+ } else if (d.sinonExpr) {
211
+ const sinonType = createSinonTypeValue(d.sinonExpr, env);
212
+ env.bind(d.name, sinonType);
213
+ } else if (d.expression) {
214
+ const expr = d.expression.trim();
215
+ if (expr.includes("(") && expr.includes(")") && !expr.startsWith("T.")) {
216
+ diagnostics.push({
217
+ range: { start: { line: 0, column: 0 }, end: { line: 0, column: 0 } },
218
+ severity: "warning",
219
+ message: `Mock expression "${expr}" could not be parsed as a known pattern`,
220
+ code: "nudo:mock-invalid",
221
+ suggestions: [
222
+ "Supported formats: stub(), stub().returns(value), spy(), mock()",
223
+ "Arrow functions: (args) => expression or (args) => { statements; return value; }"
224
+ ]
225
+ });
226
+ }
41
227
  env.bind(d.name, parseTypeValueExpr(d.expression));
42
228
  } else if (d.fromPath) {
43
229
  const mockPath = resolve(dirname(filePath), d.fromPath);
@@ -50,13 +236,480 @@ function applyMocks(directives, env, filePath) {
50
236
  }
51
237
  }
52
238
  }
239
+ function createSinonTypeValue(sinonExpr, env) {
240
+ if (sinonExpr.type === "stub" || sinonExpr.type === "spy") {
241
+ const body = { type: "BlockStatement", body: [] };
242
+ const fn = T.fn(["...args"], body, env);
243
+ if (sinonExpr.returnValue) {
244
+ fn._directReturn = sinonExpr.returnValue;
245
+ } else if (sinonExpr.resolvedValue) {
246
+ fn._directReturn = T.promise(sinonExpr.resolvedValue);
247
+ } else if (sinonExpr.rejectedValue) {
248
+ fn._directReturn = T.never;
249
+ } else {
250
+ fn._directReturn = T.unknown;
251
+ }
252
+ return fn;
253
+ }
254
+ return T.unknown;
255
+ }
53
256
  function locFromNode(node) {
54
257
  return {
55
258
  start: { line: node.loc?.start.line ?? 1, column: node.loc?.start.column ?? 0 },
56
259
  end: { line: node.loc?.end.line ?? 1, column: node.loc?.end.column ?? 0 }
57
260
  };
58
261
  }
59
- function analyzeFile(filePath, source, activeCases) {
262
+ function extractParamNames(node) {
263
+ const fn = node.type === "ExportDefaultDeclaration" ? node.declaration : node;
264
+ if (fn.type === "FunctionDeclaration" || fn.type === "FunctionExpression" || fn.type === "ArrowFunctionExpression") {
265
+ return fn.params.map((p) => {
266
+ if (p.type === "Identifier") return p.name;
267
+ if (p.type === "AssignmentPattern" && p.left.type === "Identifier") return p.left.name;
268
+ if (p.type === "RestElement" && p.argument.type === "Identifier") return `...${p.argument.name}`;
269
+ return "_";
270
+ });
271
+ }
272
+ if (fn.type === "VariableDeclaration") {
273
+ const decl = fn.declarations[0];
274
+ if (decl.init?.type === "FunctionExpression" || decl.init?.type === "ArrowFunctionExpression") {
275
+ return decl.init.params.map((p) => {
276
+ if (p.type === "Identifier") return p.name;
277
+ if (p.type === "AssignmentPattern" && p.left.type === "Identifier") return p.left.name;
278
+ if (p.type === "RestElement" && p.argument.type === "Identifier") return `...${p.argument.name}`;
279
+ return "_";
280
+ });
281
+ }
282
+ }
283
+ return [];
284
+ }
285
+ function resolveFunctionNode(node) {
286
+ if (node.type === "ExportNamedDeclaration" && node.declaration) return resolveFunctionNode(node.declaration);
287
+ if (node.type === "ExportDefaultDeclaration") return resolveFunctionNode(node.declaration);
288
+ if (node.type === "VariableDeclaration") {
289
+ const init = node.declarations[0]?.init;
290
+ if (init && (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression")) return init;
291
+ }
292
+ return node;
293
+ }
294
+ function isFnExprValue(node) {
295
+ return !!node && (node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression");
296
+ }
297
+ function namedFnExprId(node) {
298
+ return node.type === "FunctionExpression" && node.id ? node.id.name : null;
299
+ }
300
+ function isModuleExportsTarget(node) {
301
+ return node.type === "MemberExpression" && !node.computed && node.object?.type === "Identifier" && node.object.name === "module" && node.property?.type === "Identifier" && node.property.name === "exports";
302
+ }
303
+ function memberPropertyKey(member) {
304
+ if (member.type !== "MemberExpression" || member.computed) return null;
305
+ const prop = member.property;
306
+ if (prop?.type === "Identifier") return prop.name;
307
+ if (prop?.type === "StringLiteral") return String(prop.value);
308
+ return null;
309
+ }
310
+ function deepestAssignValue(expr) {
311
+ let cur = expr;
312
+ while (cur?.type === "AssignmentExpression") cur = cur.right;
313
+ return cur ?? null;
314
+ }
315
+ function assignmentChainName(expr) {
316
+ let cur = expr;
317
+ while (cur?.type === "AssignmentExpression") {
318
+ const target = cur.left;
319
+ if (target.type === "Identifier") return target.name;
320
+ if (target.type === "MemberExpression") {
321
+ if (isModuleExportsTarget(target)) {
322
+ cur = cur.right;
323
+ continue;
324
+ }
325
+ return memberPropertyKey(target);
326
+ }
327
+ return null;
328
+ }
329
+ return null;
330
+ }
331
+ function collectTopLevelFunctions(ast) {
332
+ const results = [];
333
+ if (ast.type !== "File") return results;
334
+ for (const stmt of ast.program.body) {
335
+ const decl = resolveFunctionNode(stmt);
336
+ if (decl.type === "FunctionDeclaration" && decl.id) {
337
+ results.push({ name: decl.id.name, node: decl, stmt, noDeclaration: false });
338
+ continue;
339
+ }
340
+ if (stmt.type === "VariableDeclaration") {
341
+ for (const declarator of stmt.declarations) {
342
+ if (declarator.id?.type === "Identifier" && isFnExprValue(declarator.init)) {
343
+ results.push({ name: declarator.id.name, node: declarator.init, stmt, noDeclaration: true });
344
+ }
345
+ }
346
+ continue;
347
+ }
348
+ if (stmt.type === "ExpressionStatement" && stmt.expression?.type === "AssignmentExpression") {
349
+ const expr = stmt.expression;
350
+ const fn = deepestAssignValue(expr);
351
+ if (isFnExprValue(fn)) {
352
+ const name = namedFnExprId(fn) ?? assignmentChainName(expr) ?? "default";
353
+ results.push({ name, node: fn, stmt, noDeclaration: true });
354
+ }
355
+ }
356
+ }
357
+ return results;
358
+ }
359
+ function findSingleModuleExportsFunction(ast) {
360
+ if (ast.type !== "File") return null;
361
+ let found = null;
362
+ for (const stmt of ast.program.body) {
363
+ if (stmt.type !== "ExpressionStatement") continue;
364
+ const expr = stmt.expression;
365
+ if (expr?.type !== "AssignmentExpression") continue;
366
+ const fn = deepestAssignValue(expr);
367
+ if (!isFnExprValue(fn)) continue;
368
+ let targetsModuleExports = false;
369
+ let cur = expr;
370
+ while (cur?.type === "AssignmentExpression") {
371
+ if (isModuleExportsTarget(cur.left)) {
372
+ targetsModuleExports = true;
373
+ break;
374
+ }
375
+ cur = cur.right;
376
+ }
377
+ if (!targetsModuleExports) continue;
378
+ if (found) return null;
379
+ found = fn;
380
+ }
381
+ return found;
382
+ }
383
+ function typeStructureKey(tv) {
384
+ return typeStructureKeyUncached(tv, /* @__PURE__ */ new Set());
385
+ }
386
+ function typeStructureKeyUncached(tv, seen) {
387
+ const enter = (inner) => {
388
+ if (inner && typeof inner === "object") {
389
+ if (seen.has(inner)) return "\xABcycle\xBB";
390
+ seen.add(inner);
391
+ const out = typeStructureKeyUncached(inner, seen);
392
+ seen.delete(inner);
393
+ return out;
394
+ }
395
+ return typeStructureKeyUncached(inner, seen);
396
+ };
397
+ switch (tv.kind) {
398
+ case "literal":
399
+ return `lit(${typeof tv.value}:${String(tv.value)})`;
400
+ case "primitive":
401
+ return `prim(${tv.type})`;
402
+ case "array":
403
+ return `arr(${enter(tv.element)})`;
404
+ case "tuple":
405
+ return `tup(${tv.elements.map(enter).join(",")})`;
406
+ case "object":
407
+ return `obj(${Object.keys(tv.properties).sort().map((k) => `${k}:${enter(tv.properties[k])}`).join(",")})`;
408
+ case "function":
409
+ return `fn(${tv.params.join(",")})`;
410
+ case "promise":
411
+ return `prom(${enter(tv.value)})`;
412
+ case "instance":
413
+ return `inst(${tv.className})`;
414
+ case "refined":
415
+ return `ref(${enter(tv.base)})`;
416
+ case "union":
417
+ return `uni(${tv.members.map(enter).sort().join("|")})`;
418
+ default:
419
+ return tv.kind;
420
+ }
421
+ }
422
+ var MAX_PRECISE_CALLSITE_CASES = 3;
423
+ var COLLAPSE_LITERAL_THRESHOLD = 4;
424
+ function widenType(tv) {
425
+ if (tv.kind === "union") return simplifyUnion(tv.members.map(widenLiteral));
426
+ return widenLiteral(tv);
427
+ }
428
+ function dedupeCallRecords(records) {
429
+ const seen = /* @__PURE__ */ new Set();
430
+ const out = [];
431
+ for (const rec of records) {
432
+ if (isOversizedRecord(rec)) continue;
433
+ const key = rec.argTypes.map(typeStructureKey).join(",") + "=>" + typeStructureKey(rec.resultType) + "!" + typeStructureKey(rec.throws);
434
+ if (seen.has(key)) continue;
435
+ seen.add(key);
436
+ out.push(rec);
437
+ }
438
+ return out;
439
+ }
440
+ var MAX_RECORD_TYPE_NODES = 2e3;
441
+ function typeNodeCount(tv, seen) {
442
+ if (tv === null || typeof tv !== "object") return 1;
443
+ if (seen.has(tv)) return 0;
444
+ seen.add(tv);
445
+ switch (tv.kind) {
446
+ case "array":
447
+ return 1 + typeNodeCount(tv.element, seen);
448
+ case "tuple":
449
+ return 1 + tv.elements.reduce((acc, e) => acc + typeNodeCount(e, seen), 0);
450
+ case "object": {
451
+ let n = 1;
452
+ for (const v of Object.values(tv.properties)) n += typeNodeCount(v, seen);
453
+ return n;
454
+ }
455
+ case "promise":
456
+ return 1 + typeNodeCount(tv.value, seen);
457
+ case "union":
458
+ return 1 + tv.members.reduce((acc, e) => acc + typeNodeCount(e, seen), 0);
459
+ case "refined":
460
+ return 1 + typeNodeCount(tv.base, seen);
461
+ default:
462
+ return 1;
463
+ }
464
+ }
465
+ function isOversizedRecord(rec) {
466
+ const seen = /* @__PURE__ */ new Set();
467
+ for (const a of rec.argTypes) {
468
+ if (typeNodeCount(a, seen) > MAX_RECORD_TYPE_NODES) return true;
469
+ }
470
+ return typeNodeCount(rec.resultType, seen) > MAX_RECORD_TYPE_NODES;
471
+ }
472
+ function locFromCallLoc(loc) {
473
+ const p = loc ?? { line: 0, column: 0 };
474
+ return { start: { line: p.line, column: p.column }, end: { line: p.line, column: p.column } };
475
+ }
476
+ function synthesizeExternalFunctions(records, currentFile) {
477
+ const groups = /* @__PURE__ */ new Map();
478
+ for (const rec of records) {
479
+ if (!rec.targetModule || !rec.targetExport) continue;
480
+ if (rec.targetModule === currentFile) continue;
481
+ const key = `${rec.targetModule}\0${rec.targetExport}`;
482
+ let group = groups.get(key);
483
+ if (!group) {
484
+ group = { module: rec.targetModule, exportName: rec.targetExport, records: [] };
485
+ groups.set(key, group);
486
+ }
487
+ group.records.push(rec);
488
+ }
489
+ const out = [];
490
+ for (const { module, exportName, records: records2 } of groups.values()) {
491
+ const deduped = dedupeCallRecords(records2);
492
+ const arity = Math.max(...deduped.map((r) => r.argTypes.length));
493
+ const analysis = {
494
+ name: exportName,
495
+ loc: locFromCallLoc(deduped[0].callLoc),
496
+ paramNames: Array.from({ length: arity }, (_, i) => `arg${i}`),
497
+ cases: [],
498
+ fromModule: module
499
+ };
500
+ const precise = deduped.slice(0, MAX_PRECISE_CALLSITE_CASES);
501
+ for (const rec of precise) {
502
+ analysis.cases.push({
503
+ name: `call@L${rec.callLoc?.line ?? 0}`,
504
+ args: rec.argTypes,
505
+ result: rec.resultType,
506
+ throws: rec.throws,
507
+ source: "callsite"
508
+ });
509
+ }
510
+ const remaining = deduped.slice(MAX_PRECISE_CALLSITE_CASES);
511
+ if (remaining.length > 0) {
512
+ analysis.cases.push({
513
+ name: "call@symbolic",
514
+ args: Array.from(
515
+ { length: arity },
516
+ (_, i) => (
517
+ // 缺参按真实 JS 语义 widen 成 undefined 而非 unknown——可选参守卫
518
+ // (target || [] 等)对 unknown 全塌,对 undefined 正常走默认分支
519
+ widenType(simplifyUnion(remaining.map((rec) => rec.argTypes[i] ?? T.undefined)))
520
+ )
521
+ ),
522
+ result: collapseLiteralUnion(simplifyUnion(remaining.map((r) => r.resultType)), COLLAPSE_LITERAL_THRESHOLD),
523
+ throws: simplifyUnion(remaining.map((r) => r.throws)),
524
+ source: "callsite",
525
+ aggregatedFrom: remaining.length
526
+ });
527
+ }
528
+ if (deduped.length > 1) {
529
+ analysis.combined = collapseLiteralUnion(
530
+ simplifyUnion(deduped.map((r) => r.resultType)),
531
+ COLLAPSE_LITERAL_THRESHOLD
532
+ );
533
+ } else {
534
+ analysis.combined = deduped[0].resultType;
535
+ }
536
+ out.push(analysis);
537
+ }
538
+ return out;
539
+ }
540
+ function receiverTypeToDisplay(tv) {
541
+ switch (tv.kind) {
542
+ case "literal": {
543
+ const v = tv.value;
544
+ if (v === null) return "null";
545
+ if (v === void 0) return "undefined";
546
+ return typeof v;
547
+ }
548
+ case "union":
549
+ return tv.members.map(receiverTypeToDisplay).join(" | ");
550
+ default:
551
+ return typeValueToString(tv);
552
+ }
553
+ }
554
+ function receiverIsConcrete(tv) {
555
+ if (tv.kind === "unknown") return false;
556
+ if (tv.kind === "union") return tv.members.every((m) => m.kind !== "unknown");
557
+ return true;
558
+ }
559
+ function unknownRecordsToDiagnostics(records) {
560
+ const out = [];
561
+ const seen = /* @__PURE__ */ new Set();
562
+ for (const r of records) {
563
+ const line = r.loc?.line ?? 0;
564
+ const column = r.loc?.column ?? 0;
565
+ const key = `${line}:${column}:${r.name}`;
566
+ if (seen.has(key)) continue;
567
+ seen.add(key);
568
+ const range = {
569
+ start: { line, column },
570
+ end: { line, column: column + r.name.length }
571
+ };
572
+ if (r.kind === "global") {
573
+ if (r.name.startsWith("recursion:")) {
574
+ out.push({
575
+ range,
576
+ severity: "warning",
577
+ message: `Recursive evaluation of '${r.name.slice("recursion:".length)}' was truncated (depth/size budget); result widened to unknown`,
578
+ code: "nudo:recursion-truncated"
579
+ });
580
+ continue;
581
+ }
582
+ if (r.name.startsWith("module-cycle:")) {
583
+ out.push({
584
+ range,
585
+ severity: "warning",
586
+ message: r.reason ?? "Circular module load detected",
587
+ code: "nudo:module-cycle"
588
+ });
589
+ continue;
590
+ }
591
+ if (r.name.startsWith("module-depth:")) {
592
+ out.push({
593
+ range,
594
+ severity: "warning",
595
+ message: r.reason ?? "Module load chain too deep",
596
+ code: "nudo:module-depth"
597
+ });
598
+ continue;
599
+ }
600
+ if (r.name.startsWith("module-missing:")) {
601
+ out.push({
602
+ range,
603
+ severity: "error",
604
+ message: r.reason ?? "Module file not found",
605
+ code: "nudo:module-missing"
606
+ });
607
+ continue;
608
+ }
609
+ out.push({
610
+ range,
611
+ severity: "warning",
612
+ message: `Unknown global identifier '${r.name}'`,
613
+ code: "nudo:unknown-global"
614
+ });
615
+ continue;
616
+ }
617
+ const receiver = r.receiverType;
618
+ if (receiver && receiverIsConcrete(receiver)) {
619
+ const isApprox = (() => {
620
+ const members = receiver.kind === "union" ? receiver.members : [receiver];
621
+ return members.some(
622
+ (m) => m.kind === "instance" || m.kind === "function" || m.kind === "tuple" || m.kind === "array" || m.kind === "object" || m.kind === "refined" || // wrapper/instance 近似表上可能持有该成员('x'.charCodeAt 于
623
+ // number|string:string 侧存在,number 侧缺失 → 不确定 → warning)
624
+ memberMayExistOn(m, r.name)
625
+ );
626
+ })();
627
+ const kindLabel = r.kind === "method" ? "Method" : "Property";
628
+ out.push({
629
+ range,
630
+ severity: isApprox ? "warning" : "error",
631
+ message: `${kindLabel} '${r.name}' does not exist on type '${receiverTypeToDisplay(receiver)}'`,
632
+ code: "nudo:no-method",
633
+ ...r.origin ? { origin: r.origin } : {}
634
+ });
635
+ } else {
636
+ out.push({
637
+ range,
638
+ severity: "warning",
639
+ message: `Cannot resolve '${r.name}' on unknown value`,
640
+ code: "nudo:unknown-recv",
641
+ ...r.origin ? { origin: r.origin } : {}
642
+ });
643
+ }
644
+ }
645
+ return out;
646
+ }
647
+ function collectEnvNames(filePath, source, includeProject) {
648
+ const ast = parse(source);
649
+ const fileDirectives = extractFileDirectives(ast);
650
+ const fileEnvNames = fileDirectives.filter((d) => d.kind === "env").flatMap((d) => d.envs);
651
+ if (!includeProject) return fileEnvNames;
652
+ const projectConfig = findProjectConfig(dirname(filePath));
653
+ const projectEnvNames = projectConfig?.config.env ?? [];
654
+ return [.../* @__PURE__ */ new Set([...projectEnvNames, ...fileEnvNames])];
655
+ }
656
+ async function analyzeFileAsync(filePath, source, activeCases, externalCallRecords) {
657
+ const envNames = collectEnvNames(filePath, source, true);
658
+ if (envNames.length > 0) {
659
+ await preloadPathEnvs(envNames, dirname(filePath));
660
+ }
661
+ return analyzeFile(filePath, source, activeCases, externalCallRecords);
662
+ }
663
+ function collectCallRecords(filePath, source) {
664
+ const records = [];
665
+ const ast = parse(source, { errorRecovery: true });
666
+ resetMemo();
667
+ resetUnreachableRanges();
668
+ resetEnvModules();
669
+ resetMockModules();
670
+ setModuleResolver(resolveModule);
671
+ setCurrentFileDir(dirname(filePath));
672
+ setCurrentSource(source);
673
+ setCallCollector((record) => records.push(record));
674
+ try {
675
+ const env = createEnvironment();
676
+ evaluateProgram(ast, env);
677
+ const runCallbacks = (statements) => {
678
+ for (const stmt of statements) {
679
+ if (stmt.type !== "ExpressionStatement" || !("expression" in stmt)) continue;
680
+ const expr = stmt.expression;
681
+ if (expr.type !== "CallExpression") continue;
682
+ const callee = expr.callee;
683
+ if (callee.type !== "MemberExpression" && callee.type !== "Identifier") continue;
684
+ const name = callee.type === "Identifier" ? callee.name : callee.property.type === "Identifier" ? callee.property.name : null;
685
+ if (!name || !TEST_CALLBACK_NAMES.has(name)) continue;
686
+ const args = expr.arguments;
687
+ const cb = args.find((a) => a.type === "ArrowFunctionExpression" || a.type === "FunctionExpression");
688
+ if (!cb) continue;
689
+ try {
690
+ const params = cb.params ?? [];
691
+ evaluateFunction(cb, params.map(() => T.unknown), env);
692
+ } catch {
693
+ }
694
+ if (name === "describe" && cb.body.type === "BlockStatement") {
695
+ runCallbacks(cb.body.body);
696
+ }
697
+ }
698
+ };
699
+ runCallbacks(ast.program.body);
700
+ } catch {
701
+ } finally {
702
+ setCallCollector(null);
703
+ setModuleResolver(null);
704
+ setUnknownBuiltinHandler(null);
705
+ }
706
+ return records;
707
+ }
708
+ var TEST_CALLBACK_NAMES = /* @__PURE__ */ new Set(["it", "test", "describe"]);
709
+ function analyzeFile(filePath, source, activeCases, externalCallRecords) {
710
+ for (const rec of externalCallRecords ?? []) {
711
+ for (const a of rec.argTypes) setUsageSiteTag(a);
712
+ }
60
713
  const ast = parse(source);
61
714
  const functions = extractDirectives(ast);
62
715
  const diagnostics = [];
@@ -66,28 +719,77 @@ function analyzeFile(filePath, source, activeCases) {
66
719
  const caseHints = [];
67
720
  resetMemo();
68
721
  resetUnreachableRanges();
722
+ resetEnvModules();
723
+ resetMockModules();
69
724
  setModuleResolver(resolveModule);
70
725
  setCurrentFileDir(dirname(filePath));
726
+ setCurrentSource(source);
727
+ const fileDirectives = extractFileDirectives(ast);
728
+ const fileEnvNames = fileDirectives.filter((d) => d.kind === "env").flatMap((d) => d.envs);
729
+ const projectConfig = findProjectConfig(dirname(filePath));
730
+ const projectEnvNames = projectConfig?.config.env ?? [];
731
+ const envNames = [.../* @__PURE__ */ new Set([...projectEnvNames, ...fileEnvNames])];
732
+ setUnknownBuiltinHandler((name, loc) => {
733
+ diagnostics.push({
734
+ range: loc ? { start: loc.start, end: loc.end } : { start: { line: 0, column: 0 }, end: { line: 0, column: 0 } },
735
+ severity: "warning",
736
+ message: `Built-in API "${name}" is not covered by Nudo's type inference`,
737
+ code: "nudo:builtin-unknown",
738
+ suggestions: [
739
+ `Use @nudo:mock to define the type: @nudo:mock ${name} = stub().returns(...)`,
740
+ `Or use @nudo:returns to declare the expected return type`
741
+ ]
742
+ });
743
+ });
71
744
  const globalEnv = createEnvironment();
745
+ if (envNames.length > 0) {
746
+ const loaded = loadEnvs(envNames, globalEnv);
747
+ setEnvModules(loaded.modules);
748
+ }
749
+ const mocks = /* @__PURE__ */ new Map();
750
+ if (projectConfig?.config.mocks) {
751
+ for (const [source2, mockPath] of Object.entries(projectConfig.config.mocks)) {
752
+ mocks.set(source2, { fromPath: resolve(projectConfig.projectDir, mockPath) });
753
+ }
754
+ }
755
+ const mockModuleDirectives = fileDirectives.filter((d) => d.kind === "mock-module");
756
+ for (const d of mockModuleDirectives) {
757
+ mocks.set(d.source, { fromPath: d.fromPath, names: d.names });
758
+ }
759
+ if (mocks.size > 0) {
760
+ setMockModules(mocks);
761
+ }
762
+ const callRecords = [];
763
+ setCallCollector((record) => callRecords.push(record));
764
+ const unknownRecords = [];
765
+ setUnknownCollector((r) => unknownRecords.push(r));
766
+ setProvenanceTracking(true);
767
+ for (const fn of functions) {
768
+ applyMocks(fn.directives, globalEnv, filePath, diagnostics);
769
+ }
72
770
  evaluateProgram(ast, globalEnv);
73
771
  const unreachableRanges = getUnreachableRanges();
74
772
  for (const ur of unreachableRanges) {
75
773
  diagnostics.push({
76
774
  range: ur,
77
775
  severity: "info",
78
- message: "Unreachable code",
79
- tags: ["unnecessary"]
776
+ message: "Code after return/throw statement is unreachable",
777
+ tags: ["unnecessary"],
778
+ code: "nudo-unreachable",
779
+ suggestions: ["Remove the unreachable code after the return/throw statement"]
80
780
  });
81
781
  }
82
782
  collectBindings(ast, globalEnv, bindings);
783
+ const synthCandidates = [];
83
784
  for (const fn of functions) {
84
- applyMocks(fn.directives, globalEnv, filePath);
85
785
  const isPure = fn.directives.some((d) => d.kind === "pure");
86
786
  const skipDirective = fn.directives.find((d) => d.kind === "skip");
87
787
  const returnsDirective = fn.directives.find((d) => d.kind === "returns");
88
788
  const fnLoc = locFromNode(fn.node);
89
- const analysis = { name: fn.name, loc: fnLoc, cases: [] };
789
+ const paramNames = extractParamNames(fn.node);
790
+ const analysis = { name: fn.name, loc: fnLoc, paramNames, cases: [] };
90
791
  if (skipDirective && skipDirective.kind === "skip") {
792
+ analysis.skipped = true;
91
793
  if (skipDirective.returns) {
92
794
  analysis.combined = skipDirective.returns;
93
795
  }
@@ -108,6 +810,9 @@ function analyzeFile(filePath, source, activeCases) {
108
810
  }
109
811
  const caseDirectives = fn.directives.filter((d) => d.kind === "case");
110
812
  const activeCaseIdx = activeCases?.get(fn.name) ?? 0;
813
+ if (caseDirectives.length === 0) {
814
+ synthCandidates.push({ name: fn.name, node: fn.node, analysis });
815
+ }
111
816
  for (let ci = 0; ci < caseDirectives.length; ci++) {
112
817
  const directive = caseDirectives[ci];
113
818
  resetUnreachableRanges();
@@ -133,7 +838,7 @@ function analyzeFile(filePath, source, activeCases) {
133
838
  diagnostics.push({
134
839
  range: { start: { line: directive.commentLine, column: 0 }, end: { line: directive.commentLine, column: 999 } },
135
840
  severity: "error",
136
- message: `Case "${directive.name}": expected ${typeValueToString(directive.expected)}, got ${typeValueToString(fullResult.value)}`
841
+ message: `Case "${directive.name}": expected ${typeValueToString(directive.expected)}, got ${typeValueToString(fullResult.value)}. The inferred return type does not match the expected type declared in the @nudo:case directive`
137
842
  });
138
843
  }
139
844
  }
@@ -146,21 +851,24 @@ function analyzeFile(filePath, source, activeCases) {
146
851
  diagnostics.push({
147
852
  range: throwRange,
148
853
  severity: "warning",
149
- message: `Function "${fn.name}" case "${directive.name}" may throw: ${typeValueToString(fullResult.throws)}`
854
+ message: `Function "${fn.name}" case "${directive.name}" may throw: ${typeValueToString(fullResult.throws)}. Consider adding a try-catch block or using @nudo:returns to declare expected behavior`,
855
+ code: "nudo-may-throw"
150
856
  });
151
857
  }
152
858
  for (const ur of caseUnreachable) {
153
859
  diagnostics.push({
154
860
  range: ur,
155
861
  severity: "info",
156
- message: "Unreachable code",
157
- tags: ["unnecessary"]
862
+ message: "Code after return/throw statement is unreachable",
863
+ tags: ["unnecessary"],
864
+ code: "nudo-unreachable",
865
+ suggestions: ["Remove the unreachable code after the return/throw statement"]
158
866
  });
159
867
  }
160
868
  }
161
869
  }
162
870
  if (analysis.cases.length > 1) {
163
- analysis.combined = simplifyUnion(analysis.cases.map((c) => c.result));
871
+ analysis.combined = collapseLiteralUnion(simplifyUnion(analysis.cases.map((c) => c.result)), COLLAPSE_LITERAL_THRESHOLD);
164
872
  } else if (analysis.cases.length === 1) {
165
873
  analysis.combined = analysis.cases[0].result;
166
874
  }
@@ -170,26 +878,150 @@ function analyzeFile(filePath, source, activeCases) {
170
878
  const result = evaluateFunction(fn.node, directive.args, globalEnv);
171
879
  const matches = isSubtypeOf(result, returnsDirective.expected);
172
880
  if (!matches) {
173
- const msg = `@nudo:returns assertion failed for case "${directive.name}": expected ${typeValueToString(returnsDirective.expected)}, got ${typeValueToString(result)}`;
881
+ const msg = `@nudo:returns assertion failed for case "${directive.name}": expected ${typeValueToString(returnsDirective.expected)}, got ${typeValueToString(result)}. Update the @nudo:returns directive to match the inferred type, or fix the function implementation`;
174
882
  analysis.assertionErrors.push(msg);
175
883
  diagnostics.push({
176
884
  range: fnLoc,
177
885
  severity: "error",
178
- message: msg
886
+ message: msg,
887
+ code: "nudo-assertion-failed",
888
+ suggestions: [
889
+ "Update @nudo:returns to match the inferred type",
890
+ "Fix the function to return the expected type"
891
+ ]
179
892
  });
180
893
  }
181
894
  }
182
895
  }
183
896
  functionResults.push(analysis);
184
897
  }
898
+ const directiveFnNames = new Set(functions.map((f) => f.name));
899
+ const directiveFnStmts = new Set(functions.map((f) => f.node));
900
+ for (const { name, node, stmt, noDeclaration } of collectTopLevelFunctions(ast)) {
901
+ if (directiveFnNames.has(name)) continue;
902
+ if (directiveFnStmts.has(stmt)) continue;
903
+ const analysis = { name, loc: locFromNode(node), paramNames: extractParamNames(node), cases: [] };
904
+ if (noDeclaration) analysis.noDeclaration = true;
905
+ functionResults.push(analysis);
906
+ synthCandidates.push({ name, node, analysis });
907
+ }
908
+ const modulePathCache = /* @__PURE__ */ new Map();
909
+ const normalizeModulePath = (p) => {
910
+ let n = modulePathCache.get(p);
911
+ if (n === void 0) {
912
+ try {
913
+ n = realpathSync(p);
914
+ } catch {
915
+ n = p;
916
+ }
917
+ modulePathCache.set(p, n);
918
+ }
919
+ return n;
920
+ };
921
+ const currentModulePath = normalizeModulePath(resolve(filePath));
922
+ const singleExportFn = findSingleModuleExportsFunction(ast);
923
+ for (const candidate of synthCandidates) {
924
+ const singleExportHit = singleExportFn !== null && candidate.node === singleExportFn;
925
+ const nameRoutes = (r) => r.fnName === candidate.name || r.targetExport === candidate.name || (r.targetAliases?.includes(candidate.name) ?? false) || singleExportHit && (r.fnModule !== void 0 || r.targetModule !== void 0);
926
+ const matchingLocal = (r) => {
927
+ const targetsThisFile = r.targetModule === void 0 || normalizeModulePath(r.targetModule) === currentModulePath;
928
+ if (!targetsThisFile) return false;
929
+ return nameRoutes(r);
930
+ };
931
+ const matchingExternal = (r) => {
932
+ const attributed = r.fnModule !== void 0 && normalizeModulePath(r.fnModule) === currentModulePath || r.targetModule !== void 0 && normalizeModulePath(r.targetModule) === currentModulePath;
933
+ if (!attributed) return false;
934
+ return nameRoutes(r);
935
+ };
936
+ const records = dedupeCallRecords(
937
+ [
938
+ ...callRecords.filter(matchingLocal),
939
+ ...(externalCallRecords ?? []).filter(matchingExternal)
940
+ ].filter((r) => !(r.resultType.kind === "never" && r.throws.kind === "never"))
941
+ );
942
+ if (records.length > 0) {
943
+ const informativeness = (r) => {
944
+ if (r.resultType.kind === "unknown") return 2;
945
+ if (r.resultType.kind === "never") return 1;
946
+ return 0;
947
+ };
948
+ const ordered = records.map((r, i) => ({ r, i })).sort((a, b) => informativeness(a.r) - informativeness(b.r) || a.i - b.i).map(({ r }) => r);
949
+ const precise = ordered.slice(0, MAX_PRECISE_CALLSITE_CASES);
950
+ for (const rec of precise) {
951
+ candidate.analysis.cases.push({
952
+ name: `call@L${rec.callLoc?.line ?? candidate.analysis.loc.start.line}`,
953
+ args: rec.argTypes,
954
+ result: rec.resultType,
955
+ throws: rec.throws,
956
+ source: "callsite"
957
+ });
958
+ }
959
+ const remaining = ordered.slice(MAX_PRECISE_CALLSITE_CASES).filter((rec) => !rec.argTypes.some((a) => a.kind === "unknown"));
960
+ if (remaining.length > 0) {
961
+ const fnNode2 = resolveFunctionNode(candidate.node);
962
+ const paramCount = extractParamNames(fnNode2).length;
963
+ const widenedArgs = Array.from(
964
+ { length: paramCount },
965
+ (_, i) => (
966
+ // 缺参按真实 JS 语义 widen 成 undefined 而非 unknown——可选参守卫
967
+ // (target || [] 等)对 unknown 全塌,对 undefined 正常走默认分支
968
+ widenType(simplifyUnion(remaining.map((rec) => rec.argTypes[i] ?? T.undefined)))
969
+ )
970
+ );
971
+ const full2 = evaluateFunctionFull(fnNode2, widenedArgs, globalEnv);
972
+ candidate.analysis.cases.push({
973
+ name: "call@symbolic",
974
+ args: widenedArgs,
975
+ result: full2.value,
976
+ throws: full2.throws,
977
+ throwLoc: full2.throwLoc,
978
+ source: "callsite",
979
+ aggregatedFrom: remaining.length
980
+ });
981
+ }
982
+ candidate.analysis.combined = collapseLiteralUnion(
983
+ simplifyUnion(records.map((r) => r.resultType)),
984
+ COLLAPSE_LITERAL_THRESHOLD
985
+ );
986
+ continue;
987
+ }
988
+ const fnNode = resolveFunctionNode(candidate.node);
989
+ const args = extractParamNames(fnNode).map(() => T.unknown);
990
+ const full = evaluateFunctionFull(fnNode, args, globalEnv);
991
+ candidate.analysis.cases.push({
992
+ name: `entry@L${candidate.analysis.loc.start.line}`,
993
+ args,
994
+ result: full.value,
995
+ throws: full.throws,
996
+ throwLoc: full.throwLoc
997
+ });
998
+ candidate.analysis.entryOnly = true;
999
+ candidate.analysis.combined = collapseLiteralUnion(full.value, COLLAPSE_LITERAL_THRESHOLD);
1000
+ }
185
1001
  buildNodeTypeMap(ast, globalEnv, nodeTypeMap);
1002
+ setUnknownBuiltinHandler(null);
1003
+ setCallCollector(null);
1004
+ setUnknownCollector(null);
1005
+ setProvenanceTracking(false);
186
1006
  setModuleResolver(null);
1007
+ resetEnvModules();
1008
+ resetMockModules();
1009
+ const maxLine = source.split("\n").length;
1010
+ diagnostics.push(
1011
+ ...unknownRecordsToDiagnostics(
1012
+ unknownRecords.filter(
1013
+ (r) => (r.loc?.line ?? 0) <= maxLine && r.originModule !== USAGE_SITE_MODULE
1014
+ )
1015
+ )
1016
+ );
1017
+ const externalFunctions = synthesizeExternalFunctions(callRecords, filePath);
187
1018
  return {
188
1019
  functions: functionResults,
189
1020
  diagnostics,
190
1021
  bindings,
191
1022
  nodeTypeMap,
192
- caseHints
1023
+ caseHints,
1024
+ ...externalFunctions.length > 0 ? { externalFunctions } : {}
193
1025
  };
194
1026
  }
195
1027
  function collectBindings(ast, env, bindings) {
@@ -255,12 +1087,27 @@ function getCasesForFile(filePath, source) {
255
1087
  return { functionName: fn.name, cases, loc: locFromNode(fn.node) };
256
1088
  });
257
1089
  }
1090
+ async function getTypeAtPositionAsync(filePath, source, line, column, activeCases) {
1091
+ const envNames = collectEnvNames(filePath, source, false);
1092
+ if (envNames.length > 0) {
1093
+ await preloadPathEnvs(envNames, dirname(filePath));
1094
+ }
1095
+ return getTypeAtPosition(filePath, source, line, column, activeCases);
1096
+ }
258
1097
  function getTypeAtPosition(filePath, source, line, column, activeCases) {
259
1098
  const ast = parse(source);
260
1099
  resetMemo();
1100
+ resetEnvModules();
261
1101
  setModuleResolver(resolveModule);
262
1102
  setCurrentFileDir(dirname(filePath));
1103
+ setCurrentSource(source);
1104
+ const fileDirectives = extractFileDirectives(ast);
1105
+ const envNames = fileDirectives.filter((d) => d.kind === "env").flatMap((d) => d.envs);
263
1106
  const globalEnv = createEnvironment();
1107
+ if (envNames.length > 0) {
1108
+ const loaded = loadEnvs(envNames, globalEnv);
1109
+ setEnvModules(loaded.modules);
1110
+ }
264
1111
  evaluateProgram(ast, globalEnv);
265
1112
  const nodeTypeMap = /* @__PURE__ */ new Map();
266
1113
  buildNodeTypeMap(ast, globalEnv, nodeTypeMap);
@@ -281,6 +1128,8 @@ function getTypeAtPosition(filePath, source, line, column, activeCases) {
281
1128
  }
282
1129
  }
283
1130
  setModuleResolver(null);
1131
+ resetEnvModules();
1132
+ resetMockModules();
284
1133
  return findBestTypeAtPosition(nodeTypeMap, globalEnv, ast, line, column);
285
1134
  }
286
1135
  function findEnclosingFunction(functions, line) {
@@ -453,7 +1302,7 @@ function getCompletionsForType(tv) {
453
1302
  }
454
1303
 
455
1304
  // src/dts-generator.ts
456
- import { isTemplate, getTemplateParts } from "@nudojs/core";
1305
+ import { T as T2, isTemplate, getTemplateParts, simplifyUnion as simplifyUnion2, typeValueEquals } from "@nudojs/core";
457
1306
  function typeValueToTSType(tv) {
458
1307
  switch (tv.kind) {
459
1308
  case "literal": {
@@ -487,8 +1336,10 @@ function typeValueToTSType(tv) {
487
1336
  return `[${inner}]`;
488
1337
  }
489
1338
  case "function": {
490
- const params = tv.params.map((p, i) => `${p}: unknown`).join(", ");
491
- return `(${params}) => unknown`;
1339
+ const params = tv.params.map((p) => `${p}: unknown`).join(", ");
1340
+ const returnType = tv._returnType;
1341
+ const retStr = returnType ? typeValueToTSType(returnType) : "unknown";
1342
+ return `(${params}) => ${retStr}`;
492
1343
  }
493
1344
  case "promise":
494
1345
  return `Promise<${typeValueToTSType(tv.value)}>`;
@@ -507,33 +1358,608 @@ function wrapComplexType(tv) {
507
1358
  if (tv.kind === "union") return `(${ts})`;
508
1359
  return ts;
509
1360
  }
1361
+ function getParamName(fn, index) {
1362
+ if (fn.paramNames && fn.paramNames[index]) {
1363
+ return fn.paramNames[index];
1364
+ }
1365
+ return `arg${index}`;
1366
+ }
1367
+ function widenLiteralToBase(tv) {
1368
+ if (tv.kind !== "literal") return tv;
1369
+ const v = tv.value;
1370
+ if (typeof v === "number") return T2.number;
1371
+ if (typeof v === "string") return T2.string;
1372
+ if (typeof v === "boolean") return T2.boolean;
1373
+ if (typeof v === "bigint") return T2.bigint;
1374
+ return tv;
1375
+ }
1376
+ function widenTopLevel(tv) {
1377
+ if (tv.kind !== "union") return widenLiteralToBase(tv);
1378
+ return simplifyUnion2(tv.members.map(widenLiteralToBase));
1379
+ }
1380
+ function widenProperties(properties) {
1381
+ const out = {};
1382
+ for (const [k, v] of Object.entries(properties)) out[k] = widenParamType(v);
1383
+ return out;
1384
+ }
1385
+ function widenParamType(tv) {
1386
+ switch (tv.kind) {
1387
+ case "literal":
1388
+ return widenLiteralToBase(tv);
1389
+ case "union":
1390
+ return simplifyUnion2(tv.members.map(widenParamType));
1391
+ case "tuple": {
1392
+ const widened = tv.elements.map(widenParamType);
1393
+ const first = widened[0];
1394
+ if (first && widened.every((el) => typeValueEquals(el, first))) {
1395
+ return T2.array(first);
1396
+ }
1397
+ return T2.tuple(widened);
1398
+ }
1399
+ case "array":
1400
+ return T2.array(widenParamType(tv.element));
1401
+ case "object":
1402
+ return T2.object(widenProperties(tv.properties));
1403
+ case "promise":
1404
+ return T2.promise(widenParamType(tv.value));
1405
+ case "instance":
1406
+ return T2.instanceOf(tv.className, widenProperties(tv.properties));
1407
+ case "refined":
1408
+ return widenParamType(tv.base);
1409
+ default:
1410
+ return tv;
1411
+ }
1412
+ }
1413
+ function widenedUnion(members) {
1414
+ const flat = [];
1415
+ const collect = (tv) => {
1416
+ if (tv.kind === "union") {
1417
+ tv.members.forEach(collect);
1418
+ return;
1419
+ }
1420
+ flat.push(tv);
1421
+ };
1422
+ members.forEach(collect);
1423
+ const widened = flat.map(widenParamType);
1424
+ const seen = /* @__PURE__ */ new Set();
1425
+ const deduped = widened.filter((tv) => {
1426
+ const key = typeValueToTSType(tv);
1427
+ if (seen.has(key)) return false;
1428
+ seen.add(key);
1429
+ return true;
1430
+ });
1431
+ return simplifyUnion2(deduped);
1432
+ }
1433
+ function computeMainSignature(fn) {
1434
+ const arity = Math.max(...fn.cases.map((c) => c.args.length));
1435
+ const minArity = Math.min(...fn.cases.map((c) => c.args.length));
1436
+ const params = [];
1437
+ const paramNames = [];
1438
+ const paramTypes = [];
1439
+ const usedNames = /* @__PURE__ */ new Set();
1440
+ for (let i = 0; i < arity; i++) {
1441
+ const members = fn.cases.filter((c) => i < c.args.length).map((c) => c.args[i]);
1442
+ const typeStr = typeValueToTSType(widenedUnion(members));
1443
+ let name = getParamName(fn, i);
1444
+ if (usedNames.has(name)) {
1445
+ let n = 2;
1446
+ while (usedNames.has(`${name}${n}`)) n++;
1447
+ name = `${name}${n}`;
1448
+ }
1449
+ usedNames.add(name);
1450
+ const optional = i >= minArity && !name.startsWith("...");
1451
+ params.push(optional ? `${name}?: ${typeStr}` : `${name}: ${typeStr}`);
1452
+ paramNames.push(name);
1453
+ paramTypes.push(typeStr);
1454
+ }
1455
+ const combined = fn.combined ?? simplifyUnion2(fn.cases.map((c) => c.result));
1456
+ const returnType = typeValueToTSType(widenTopLevel(combined));
1457
+ return { params, paramNames, paramTypes, returnType };
1458
+ }
1459
+ function generateJSDoc(fn, sig) {
1460
+ if (fn.cases.length === 0) return "";
1461
+ const lines = ["/**"];
1462
+ for (const c of fn.cases) {
1463
+ const preciseDiffers = c.args.length !== sig.paramTypes.length || c.args.some((a, i) => typeValueToTSType(a) !== sig.paramTypes[i]) || typeValueToTSType(c.result) !== sig.returnType;
1464
+ if (!preciseDiffers) continue;
1465
+ const argsStr = c.args.map(typeValueToTSType).join(", ");
1466
+ lines.push(` * Case: ${c.name} (${argsStr}) => ${typeValueToTSType(c.result)}`);
1467
+ }
1468
+ for (let i = 0; i < sig.paramTypes.length; i++) {
1469
+ lines.push(` * @param ${sig.paramNames[i]} - ${sig.paramTypes[i]}`);
1470
+ }
1471
+ lines.push(` * @returns ${sig.returnType}`);
1472
+ lines.push(" */");
1473
+ return lines.join("\n");
1474
+ }
1475
+ function generateFunctionDtsLines(fn) {
1476
+ if (fn.noDeclaration) return [];
1477
+ if (fn.cases.length === 0) {
1478
+ if (fn.combined) {
1479
+ return [
1480
+ `export declare function ${fn.name}(...args: unknown[]): ${typeValueToTSType(fn.combined)};`
1481
+ ];
1482
+ }
1483
+ return [];
1484
+ }
1485
+ const sig = computeMainSignature(fn);
1486
+ const jsdoc = generateJSDoc(fn, sig);
1487
+ const lines = [];
1488
+ if (jsdoc) lines.push(jsdoc);
1489
+ lines.push(`export declare function ${fn.name}(${sig.params.join(", ")}): ${sig.returnType};`);
1490
+ return lines;
1491
+ }
510
1492
  function generateDts(result) {
511
1493
  const lines = [];
512
1494
  for (const fn of result.functions) {
513
- if (fn.cases.length === 0 && fn.combined) {
514
- lines.push(`export declare function ${fn.name}(...args: unknown[]): ${typeValueToTSType(fn.combined)};`);
1495
+ lines.push(...generateFunctionDtsLines(fn));
1496
+ }
1497
+ return lines.join("\n") + "\n";
1498
+ }
1499
+
1500
+ // src/schema-generator.ts
1501
+ function typeValueToZodSchema(tv) {
1502
+ switch (tv.kind) {
1503
+ case "literal": {
1504
+ if (tv.value === null) return "z.null()";
1505
+ if (tv.value === void 0) return "z.undefined()";
1506
+ if (typeof tv.value === "string") return `z.literal(${JSON.stringify(tv.value)})`;
1507
+ if (typeof tv.value === "boolean") return `z.literal(${tv.value})`;
1508
+ return `z.literal(${tv.value})`;
1509
+ }
1510
+ case "primitive":
1511
+ return `z.${tv.type}()`;
1512
+ case "refined":
1513
+ return typeValueToZodSchema(tv.base);
1514
+ case "object": {
1515
+ const entries = Object.entries(tv.properties).map(([k, v]) => `${k}: ${typeValueToZodSchema(v)}`).join(", ");
1516
+ return `z.object({ ${entries} })`;
1517
+ }
1518
+ case "array":
1519
+ return `z.array(${typeValueToZodSchema(tv.element)})`;
1520
+ case "tuple": {
1521
+ const inner = tv.elements.map(typeValueToZodSchema).join(", ");
1522
+ return `z.tuple([${inner}])`;
1523
+ }
1524
+ case "function":
1525
+ return "z.function()";
1526
+ case "promise":
1527
+ return `z.promise(${typeValueToZodSchema(tv.value)})`;
1528
+ case "instance":
1529
+ return `z.instanceof(${tv.className})`;
1530
+ case "union": {
1531
+ const members = tv.members.map(typeValueToZodSchema).join(", ");
1532
+ return `z.union([${members}])`;
1533
+ }
1534
+ case "never":
1535
+ return "z.never()";
1536
+ case "unknown":
1537
+ return "z.unknown()";
1538
+ }
1539
+ }
1540
+
1541
+ // src/guard-generator.ts
1542
+ function generateGuardFunction(name, tv) {
1543
+ const body = generateGuardBody(tv, "data");
1544
+ return `export function ${name}(data) {
1545
+ return ${body};
1546
+ }`;
1547
+ }
1548
+ function generateGuardBody(tv, varName) {
1549
+ switch (tv.kind) {
1550
+ case "literal": {
1551
+ if (tv.value === null) return `${varName} === null`;
1552
+ if (tv.value === void 0) return `${varName} === undefined`;
1553
+ return `${varName} === ${JSON.stringify(tv.value)}`;
1554
+ }
1555
+ case "primitive":
1556
+ return `typeof ${varName} === '${tv.type}'`;
1557
+ case "refined":
1558
+ return generateGuardBody(tv.base, varName);
1559
+ case "object": {
1560
+ const checks = [`typeof ${varName} === 'object'`, `${varName} !== null`];
1561
+ for (const [key, val] of Object.entries(tv.properties)) {
1562
+ checks.push(generateGuardBody(val, `${varName}.${key}`));
1563
+ }
1564
+ return checks.join(" && ");
1565
+ }
1566
+ case "array":
1567
+ return `Array.isArray(${varName}) && ${varName}.every(item => ${generateGuardBody(tv.element, "item")})`;
1568
+ case "tuple": {
1569
+ const checks = [`Array.isArray(${varName})`, `${varName}.length === ${tv.elements.length}`];
1570
+ tv.elements.forEach((el, i) => {
1571
+ checks.push(generateGuardBody(el, `${varName}[${i}]`));
1572
+ });
1573
+ return checks.join(" && ");
1574
+ }
1575
+ case "union": {
1576
+ const members = tv.members.map((m) => generateGuardBody(m, varName));
1577
+ return `(${members.join(" || ")})`;
1578
+ }
1579
+ case "never":
1580
+ return "false";
1581
+ case "unknown":
1582
+ return "true";
1583
+ case "function":
1584
+ return `typeof ${varName} === 'function'`;
1585
+ case "promise":
1586
+ return `${varName} instanceof Promise`;
1587
+ case "instance":
1588
+ return `${varName} instanceof ${tv.className}`;
1589
+ }
1590
+ }
1591
+
1592
+ // src/case-emitter.ts
1593
+ import { parse as parse2, extractDirectives as extractDirectives2 } from "@nudojs/parser";
1594
+ var GENERATED_PREFIX = "call@";
1595
+ var GENERATED_CASE_LINE_REGEX = /^\s*\*?\s*@nudo:case\s+"(call@[^"]*)"/;
1596
+ var IDENT_KEY_REGEX = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
1597
+ var NUMBER_LITERAL_REGEX = /^-?\d+(\.\d+)?$/;
1598
+ var UNSAFE_STRING_CHARS = /[,()[\]{}]/;
1599
+ var UNSAFE_KEY_CHARS = /[,()[\]{}:]/;
1600
+ var CONTROL_CHARS = /[\x00-\x1f\x7f]/;
1601
+ function serializeStringLiteral(value) {
1602
+ if (CONTROL_CHARS.test(value)) return null;
1603
+ if (value.includes("*/")) return null;
1604
+ if (UNSAFE_STRING_CHARS.test(value)) return null;
1605
+ const hasDbl = value.includes('"');
1606
+ const hasSgl = value.includes("'");
1607
+ if (hasDbl && hasSgl) return null;
1608
+ return hasDbl ? `'${value}'` : `"${value}"`;
1609
+ }
1610
+ function serializeObjectKey(key) {
1611
+ if (IDENT_KEY_REGEX.test(key)) return key;
1612
+ if (CONTROL_CHARS.test(key)) return null;
1613
+ if (key.includes('"')) return null;
1614
+ if (UNSAFE_KEY_CHARS.test(key) || key.includes("*/")) return null;
1615
+ if (/^['"]|['"]$/.test(key)) return null;
1616
+ return `"${key}"`;
1617
+ }
1618
+ function serializeCaseArg(tv) {
1619
+ switch (tv.kind) {
1620
+ case "primitive":
1621
+ if (tv.type === "number" || tv.type === "string" || tv.type === "boolean") {
1622
+ return `T.${tv.type}`;
1623
+ }
1624
+ return null;
1625
+ case "unknown":
1626
+ return "T.unknown";
1627
+ case "never":
1628
+ return "T.never";
1629
+ case "literal": {
1630
+ const v = tv.value;
1631
+ if (typeof v === "number") {
1632
+ if (!Number.isFinite(v)) return null;
1633
+ const s = String(v);
1634
+ return NUMBER_LITERAL_REGEX.test(s) ? s : null;
1635
+ }
1636
+ if (typeof v === "boolean") return v ? "true" : "false";
1637
+ if (v === null) return "null";
1638
+ if (v === void 0) return "undefined";
1639
+ return serializeStringLiteral(v);
1640
+ }
1641
+ case "union": {
1642
+ const parts = [];
1643
+ for (const member of tv.members) {
1644
+ const s = serializeCaseArg(member);
1645
+ if (s === null) return null;
1646
+ parts.push(s);
1647
+ }
1648
+ return `T.union(${parts.join(", ")})`;
1649
+ }
1650
+ case "array": {
1651
+ const el = serializeCaseArg(tv.element);
1652
+ return el === null ? null : `T.array(${el})`;
1653
+ }
1654
+ case "tuple": {
1655
+ const parts = [];
1656
+ for (const el of tv.elements) {
1657
+ const s = serializeCaseArg(el);
1658
+ if (s === null) return null;
1659
+ parts.push(s);
1660
+ }
1661
+ return `[${parts.join(", ")}]`;
1662
+ }
1663
+ case "object": {
1664
+ const parts = [];
1665
+ for (const [key, value] of Object.entries(tv.properties)) {
1666
+ const vs = serializeCaseArg(value);
1667
+ if (vs === null) return null;
1668
+ const ks = serializeObjectKey(key);
1669
+ if (ks === null) return null;
1670
+ parts.push(`${ks}: ${vs}`);
1671
+ }
1672
+ return parts.length === 0 ? "{}" : `{ ${parts.join(", ")} }`;
1673
+ }
1674
+ default:
1675
+ return null;
1676
+ }
1677
+ }
1678
+ function buildCaseDirective(name, args) {
1679
+ if (name.includes('"') || /[\r\n]/.test(name)) return null;
1680
+ const parts = [];
1681
+ for (const arg of args) {
1682
+ const s = serializeCaseArg(arg);
1683
+ if (s === null) return null;
1684
+ parts.push(s);
1685
+ }
1686
+ return ` * @nudo:case "${name}" (${parts.join(", ")})`;
1687
+ }
1688
+ function stripGeneratedCaseDirectives(source) {
1689
+ const lines = source.split("\n");
1690
+ const removed = [];
1691
+ const drop = new Array(lines.length).fill(false);
1692
+ for (let i = 0; i < lines.length; i++) {
1693
+ const m = lines[i].match(GENERATED_CASE_LINE_REGEX);
1694
+ if (m) {
1695
+ drop[i] = true;
1696
+ removed.push(m[1]);
1697
+ }
1698
+ }
1699
+ for (let i = 0; i < lines.length; i++) {
1700
+ if (!/^\s*\/\*\*\s*$/.test(lines[i])) continue;
1701
+ let j = i + 1;
1702
+ let clean = true;
1703
+ while (j < lines.length) {
1704
+ if (/\*\//.test(lines[j])) break;
1705
+ if (/\/\*/.test(lines[j])) {
1706
+ clean = false;
1707
+ break;
1708
+ }
1709
+ j++;
1710
+ }
1711
+ if (!clean || j >= lines.length) continue;
1712
+ if (!/^\s*\*\/\s*$/.test(lines[j])) continue;
1713
+ let onlyFiller = true;
1714
+ for (let k = i + 1; k < j; k++) {
1715
+ if (drop[k]) continue;
1716
+ const t = lines[k].trim();
1717
+ if (t !== "" && !/^\*{1,2}$/.test(t)) {
1718
+ onlyFiller = false;
1719
+ break;
1720
+ }
1721
+ }
1722
+ if (onlyFiller) {
1723
+ for (let k = i; k <= j; k++) drop[k] = true;
1724
+ }
1725
+ }
1726
+ return {
1727
+ source: lines.filter((_, i) => !drop[i]).join("\n"),
1728
+ removed
1729
+ };
1730
+ }
1731
+ function collectExistingCases(source) {
1732
+ const byName = /* @__PURE__ */ new Map();
1733
+ let ast;
1734
+ try {
1735
+ ast = parse2(source, { errorRecovery: true });
1736
+ } catch {
1737
+ return byName;
1738
+ }
1739
+ for (const { name, directives } of extractDirectives2(ast)) {
1740
+ for (const d of directives) {
1741
+ if (d.kind !== "case") continue;
1742
+ const list = byName.get(name);
1743
+ if (list) list.push(d);
1744
+ else byName.set(name, [d]);
1745
+ }
1746
+ }
1747
+ return byName;
1748
+ }
1749
+ function insertGeneratedCaseDirectives(source, analysis) {
1750
+ const written = [];
1751
+ const skipped = [];
1752
+ const existing = collectExistingCases(source);
1753
+ const edits = [];
1754
+ for (const fn of analysis.functions) {
1755
+ const existingCases = existing.get(fn.name) ?? [];
1756
+ if (existingCases.some((c) => !c.name.startsWith(GENERATED_PREFIX))) {
1757
+ skipped.push({ fn: fn.name, reason: "hand-written" });
1758
+ continue;
1759
+ }
1760
+ if (existingCases.length > 0) {
1761
+ skipped.push({ fn: fn.name, reason: "already-generated" });
1762
+ continue;
1763
+ }
1764
+ if (fn.skipped) {
1765
+ skipped.push({ fn: fn.name, reason: "skipped" });
515
1766
  continue;
516
1767
  }
517
- if (fn.cases.length === 1) {
518
- const c = fn.cases[0];
519
- const params = c.args.map((a, i) => `arg${i}: ${typeValueToTSType(a)}`).join(", ");
520
- const ret = typeValueToTSType(c.result);
521
- lines.push(`export declare function ${fn.name}(${params}): ${ret};`);
1768
+ if (fn.noDeclaration) {
1769
+ skipped.push({ fn: fn.name, reason: "no-declaration" });
522
1770
  continue;
523
1771
  }
524
- for (const c of fn.cases) {
525
- const params = c.args.map((a, i) => `arg${i}: ${typeValueToTSType(a)}`).join(", ");
526
- const ret = typeValueToTSType(c.result);
527
- lines.push(`export declare function ${fn.name}(${params}): ${ret};`);
1772
+ if (fn.entryOnly) {
1773
+ skipped.push({ fn: fn.name, reason: "entry-only" });
1774
+ continue;
1775
+ }
1776
+ const callsiteCases = fn.cases.filter((c) => c.source === "callsite");
1777
+ if (callsiteCases.length === 0) {
1778
+ skipped.push({ fn: fn.name, reason: "no-serializable-cases", detail: "no callsite cases" });
1779
+ continue;
1780
+ }
1781
+ const built = [];
1782
+ const names = [];
1783
+ for (const c of callsiteCases) {
1784
+ const directive = buildCaseDirective(c.name, c.args);
1785
+ if (directive === null) {
1786
+ skipped.push({ fn: fn.name, reason: "no-serializable-cases", detail: `case ${c.name} not serializable` });
1787
+ } else {
1788
+ built.push(directive);
1789
+ names.push(c.name);
1790
+ }
1791
+ }
1792
+ if (built.length === 0) continue;
1793
+ edits.push({ line: fn.loc.start.line, column: fn.loc.start.column, directives: built, fn: fn.name });
1794
+ written.push({ fn: fn.name, cases: names });
1795
+ }
1796
+ edits.sort((a, b) => b.line - a.line);
1797
+ const lines = source.split("\n");
1798
+ for (const edit of edits) {
1799
+ const declIdx = edit.line - 1;
1800
+ const indent = " ".repeat(edit.column);
1801
+ let insertIdx = -1;
1802
+ let blockIndent = "";
1803
+ const aboveIdx = declIdx - 1;
1804
+ if (aboveIdx >= 0 && /^\s*\*\/\s*$/.test(lines[aboveIdx])) {
1805
+ let s = aboveIdx - 1;
1806
+ while (s >= 0) {
1807
+ const t = lines[s];
1808
+ if (/^\s*\/\*\*/.test(t)) {
1809
+ insertIdx = s + 1;
1810
+ blockIndent = t.match(/^\s*/)?.[0] ?? "";
1811
+ break;
1812
+ }
1813
+ if (/^\s*\*/.test(t) || t.trim() === "") {
1814
+ s--;
1815
+ continue;
1816
+ }
1817
+ break;
1818
+ }
1819
+ }
1820
+ if (insertIdx >= 0) {
1821
+ lines.splice(insertIdx, 0, ...edit.directives.map((d) => blockIndent + d));
1822
+ } else {
1823
+ lines.splice(declIdx, 0, `${indent}/**`, ...edit.directives.map((d) => indent + d), `${indent} */`);
528
1824
  }
529
1825
  }
530
- return lines.join("\n") + "\n";
1826
+ return {
1827
+ source: lines.join("\n"),
1828
+ changed: written.length > 0,
1829
+ written,
1830
+ skipped
1831
+ };
1832
+ }
1833
+ function diffOps(a, b) {
1834
+ let start = 0;
1835
+ while (start < a.length && start < b.length && a[start] === b[start]) start++;
1836
+ let endA = a.length;
1837
+ let endB = b.length;
1838
+ while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) {
1839
+ endA--;
1840
+ endB--;
1841
+ }
1842
+ const midA = a.slice(start, endA);
1843
+ const midB = b.slice(start, endB);
1844
+ const n = midA.length;
1845
+ const m = midB.length;
1846
+ const dp = new Int32Array((n + 1) * (m + 1));
1847
+ for (let i2 = n - 1; i2 >= 0; i2--) {
1848
+ for (let j2 = m - 1; j2 >= 0; j2--) {
1849
+ dp[i2 * (m + 1) + j2] = midA[i2] === midB[j2] ? dp[(i2 + 1) * (m + 1) + (j2 + 1)] + 1 : Math.max(dp[(i2 + 1) * (m + 1) + j2], dp[i2 * (m + 1) + (j2 + 1)]);
1850
+ }
1851
+ }
1852
+ const ops = [];
1853
+ for (let k = 0; k < start; k++) ops.push({ type: "equal", text: a[k] });
1854
+ let i = 0;
1855
+ let j = 0;
1856
+ while (i < n && j < m) {
1857
+ if (midA[i] === midB[j]) {
1858
+ ops.push({ type: "equal", text: midA[i] });
1859
+ i++;
1860
+ j++;
1861
+ } else if (dp[(i + 1) * (m + 1) + j] >= dp[i * (m + 1) + (j + 1)]) {
1862
+ ops.push({ type: "delete", text: midA[i] });
1863
+ i++;
1864
+ } else {
1865
+ ops.push({ type: "insert", text: midB[j] });
1866
+ j++;
1867
+ }
1868
+ }
1869
+ while (i < n) ops.push({ type: "delete", text: midA[i++] });
1870
+ while (j < m) ops.push({ type: "insert", text: midB[j++] });
1871
+ for (let k = endA; k < a.length; k++) ops.push({ type: "equal", text: a[k] });
1872
+ return ops;
1873
+ }
1874
+ var DIFF_CONTEXT = 3;
1875
+ function formatRange(start, count) {
1876
+ const s = count === 0 ? start - 1 : start;
1877
+ return count === 1 ? `${s}` : `${s},${count}`;
1878
+ }
1879
+ function unifiedDiff(a, b, path) {
1880
+ if (a === b) return "";
1881
+ const ops = diffOps(a.split("\n"), b.split("\n"));
1882
+ const hunks = [];
1883
+ let idx = 0;
1884
+ while (idx < ops.length) {
1885
+ if (ops[idx].type === "equal") {
1886
+ idx++;
1887
+ continue;
1888
+ }
1889
+ let start = idx;
1890
+ let ctx = 0;
1891
+ while (start > 0 && ops[start - 1].type === "equal" && ctx < DIFF_CONTEXT) {
1892
+ start--;
1893
+ ctx++;
1894
+ }
1895
+ let j = idx;
1896
+ let equalRun = 0;
1897
+ let lastChange = idx;
1898
+ while (j < ops.length) {
1899
+ if (ops[j].type === "equal") {
1900
+ equalRun++;
1901
+ if (equalRun > DIFF_CONTEXT * 2) break;
1902
+ } else {
1903
+ equalRun = 0;
1904
+ lastChange = j;
1905
+ }
1906
+ j++;
1907
+ }
1908
+ let end = lastChange + 1;
1909
+ ctx = 0;
1910
+ while (end < ops.length && ops[end].type === "equal" && ctx < DIFF_CONTEXT) {
1911
+ end++;
1912
+ ctx++;
1913
+ }
1914
+ hunks.push({ start, ops: ops.slice(start, end) });
1915
+ idx = end;
1916
+ }
1917
+ const out = [`--- a/${path}`, `+++ b/${path}`];
1918
+ for (let h = 0; h < hunks.length; h++) {
1919
+ const { start, ops: hunkOps } = hunks[h];
1920
+ let oldCount = 0;
1921
+ let newCount = 0;
1922
+ for (let k = 0; k < start; k++) {
1923
+ if (ops[k].type !== "insert") oldCount++;
1924
+ if (ops[k].type !== "delete") newCount++;
1925
+ }
1926
+ let dels = 0;
1927
+ let ins = 0;
1928
+ for (const op of hunkOps) {
1929
+ if (op.type === "delete") dels++;
1930
+ else if (op.type === "insert") ins++;
1931
+ else {
1932
+ dels++;
1933
+ ins++;
1934
+ }
1935
+ }
1936
+ out.push(`@@ -${formatRange(oldCount + 1, dels)} +${formatRange(newCount + 1, ins)} @@`);
1937
+ for (const op of hunkOps) {
1938
+ const prefix = op.type === "equal" ? " " : op.type === "delete" ? "-" : "+";
1939
+ out.push(prefix + op.text);
1940
+ }
1941
+ }
1942
+ return out.join("\n") + "\n";
531
1943
  }
532
1944
  export {
533
1945
  analyzeFile,
1946
+ analyzeFileAsync,
1947
+ buildCaseDirective,
1948
+ buildModuleGraph,
1949
+ collectCallRecords,
1950
+ computeDirtySet,
534
1951
  generateDts,
1952
+ generateFunctionDtsLines,
1953
+ generateGuardFunction,
535
1954
  getCasesForFile,
536
1955
  getCompletionsAtPosition,
537
1956
  getTypeAtPosition,
538
- typeValueToTSType
1957
+ getTypeAtPositionAsync,
1958
+ insertGeneratedCaseDirectives,
1959
+ serializeCaseArg,
1960
+ stripGeneratedCaseDirectives,
1961
+ topoSortDirty,
1962
+ typeValueToTSType,
1963
+ typeValueToZodSchema,
1964
+ unifiedDiff
539
1965
  };