@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/CHANGELOG.md +7 -0
- package/README.md +27 -0
- package/dist/index.d.ts +137 -2
- package/dist/index.js +1464 -38
- package/package.json +14 -2
- package/src/__tests__/analyzer.test.ts +677 -2
- package/src/__tests__/as-replace.test.ts +165 -0
- package/src/__tests__/case-emitter.test.ts +636 -0
- package/src/__tests__/diagnostics.test.ts +90 -0
- package/src/__tests__/dts-generator.test.ts +290 -6
- package/src/__tests__/fn-sig-impl.test.ts +247 -0
- package/src/__tests__/guard-generator.test.ts +29 -0
- package/src/__tests__/integration.test.ts +332 -0
- package/src/__tests__/schema-generator.test.ts +38 -0
- package/src/analyzer.ts +1201 -18
- package/src/case-emitter.ts +475 -0
- package/src/dts-generator.ts +220 -24
- package/src/guard-generator.ts +50 -0
- package/src/index.ts +25 -0
- package/src/schema-generator.ts +43 -0
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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
|
|
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.
|
|
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
|
|
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: "
|
|
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
|
|
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: "
|
|
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
|
|
491
|
-
|
|
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
|
-
|
|
514
|
-
|
|
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.
|
|
518
|
-
|
|
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
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
|
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
|
-
|
|
1957
|
+
getTypeAtPositionAsync,
|
|
1958
|
+
insertGeneratedCaseDirectives,
|
|
1959
|
+
serializeCaseArg,
|
|
1960
|
+
stripGeneratedCaseDirectives,
|
|
1961
|
+
topoSortDirty,
|
|
1962
|
+
typeValueToTSType,
|
|
1963
|
+
typeValueToZodSchema,
|
|
1964
|
+
unifiedDiff
|
|
539
1965
|
};
|