@nudojs/service 0.1.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 +24 -0
- package/LICENSE +21 -0
- package/README.md +27 -0
- package/dist/index.d.ts +137 -2
- package/dist/index.js +1479 -37
- package/package.json +26 -4
- 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 +1209 -18
- package/src/case-emitter.ts +475 -0
- package/src/dts-generator.ts +230 -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,42 +1,229 @@
|
|
|
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,
|
|
19
25
|
resetMemo,
|
|
20
26
|
getUnreachableRanges,
|
|
21
27
|
resetUnreachableRanges,
|
|
22
|
-
setNodeTypeCollector
|
|
28
|
+
setNodeTypeCollector,
|
|
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
|
|
23
43
|
} from "@nudojs/cli/evaluator";
|
|
24
44
|
function resolveModule(source, fromDir) {
|
|
25
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
|
+
}
|
|
26
51
|
const basePath = resolve(fromDir, source);
|
|
27
52
|
for (const ext of ["", ...extensions]) {
|
|
28
53
|
const candidate = basePath + ext;
|
|
29
|
-
if (existsSync(candidate))
|
|
30
|
-
|
|
31
|
-
|
|
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
|
+
}
|
|
32
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;
|
|
33
103
|
}
|
|
34
104
|
return null;
|
|
35
105
|
}
|
|
36
|
-
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) {
|
|
37
201
|
for (const d of directives) {
|
|
38
202
|
if (d.kind !== "mock") continue;
|
|
39
|
-
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
|
+
}
|
|
40
227
|
env.bind(d.name, parseTypeValueExpr(d.expression));
|
|
41
228
|
} else if (d.fromPath) {
|
|
42
229
|
const mockPath = resolve(dirname(filePath), d.fromPath);
|
|
@@ -49,13 +236,480 @@ function applyMocks(directives, env, filePath) {
|
|
|
49
236
|
}
|
|
50
237
|
}
|
|
51
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
|
+
}
|
|
52
256
|
function locFromNode(node) {
|
|
53
257
|
return {
|
|
54
258
|
start: { line: node.loc?.start.line ?? 1, column: node.loc?.start.column ?? 0 },
|
|
55
259
|
end: { line: node.loc?.end.line ?? 1, column: node.loc?.end.column ?? 0 }
|
|
56
260
|
};
|
|
57
261
|
}
|
|
58
|
-
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
|
+
}
|
|
59
713
|
const ast = parse(source);
|
|
60
714
|
const functions = extractDirectives(ast);
|
|
61
715
|
const diagnostics = [];
|
|
@@ -65,28 +719,77 @@ function analyzeFile(filePath, source, activeCases) {
|
|
|
65
719
|
const caseHints = [];
|
|
66
720
|
resetMemo();
|
|
67
721
|
resetUnreachableRanges();
|
|
722
|
+
resetEnvModules();
|
|
723
|
+
resetMockModules();
|
|
68
724
|
setModuleResolver(resolveModule);
|
|
69
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
|
+
});
|
|
70
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
|
+
}
|
|
71
770
|
evaluateProgram(ast, globalEnv);
|
|
72
771
|
const unreachableRanges = getUnreachableRanges();
|
|
73
772
|
for (const ur of unreachableRanges) {
|
|
74
773
|
diagnostics.push({
|
|
75
774
|
range: ur,
|
|
76
775
|
severity: "info",
|
|
77
|
-
message: "
|
|
78
|
-
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"]
|
|
79
780
|
});
|
|
80
781
|
}
|
|
81
782
|
collectBindings(ast, globalEnv, bindings);
|
|
783
|
+
const synthCandidates = [];
|
|
82
784
|
for (const fn of functions) {
|
|
83
|
-
applyMocks(fn.directives, globalEnv, filePath);
|
|
84
785
|
const isPure = fn.directives.some((d) => d.kind === "pure");
|
|
85
786
|
const skipDirective = fn.directives.find((d) => d.kind === "skip");
|
|
86
787
|
const returnsDirective = fn.directives.find((d) => d.kind === "returns");
|
|
87
788
|
const fnLoc = locFromNode(fn.node);
|
|
88
|
-
const
|
|
789
|
+
const paramNames = extractParamNames(fn.node);
|
|
790
|
+
const analysis = { name: fn.name, loc: fnLoc, paramNames, cases: [] };
|
|
89
791
|
if (skipDirective && skipDirective.kind === "skip") {
|
|
792
|
+
analysis.skipped = true;
|
|
90
793
|
if (skipDirective.returns) {
|
|
91
794
|
analysis.combined = skipDirective.returns;
|
|
92
795
|
}
|
|
@@ -99,8 +802,17 @@ function analyzeFile(filePath, source, activeCases) {
|
|
|
99
802
|
fnVal._memoize = fn.name;
|
|
100
803
|
}
|
|
101
804
|
}
|
|
805
|
+
const sampleDirective = fn.directives.find((d) => d.kind === "sample");
|
|
806
|
+
if (sampleDirective && sampleDirective.kind === "sample") {
|
|
807
|
+
setSampleCount(sampleDirective.count);
|
|
808
|
+
} else {
|
|
809
|
+
setSampleCount(3);
|
|
810
|
+
}
|
|
102
811
|
const caseDirectives = fn.directives.filter((d) => d.kind === "case");
|
|
103
812
|
const activeCaseIdx = activeCases?.get(fn.name) ?? 0;
|
|
813
|
+
if (caseDirectives.length === 0) {
|
|
814
|
+
synthCandidates.push({ name: fn.name, node: fn.node, analysis });
|
|
815
|
+
}
|
|
104
816
|
for (let ci = 0; ci < caseDirectives.length; ci++) {
|
|
105
817
|
const directive = caseDirectives[ci];
|
|
106
818
|
resetUnreachableRanges();
|
|
@@ -126,7 +838,7 @@ function analyzeFile(filePath, source, activeCases) {
|
|
|
126
838
|
diagnostics.push({
|
|
127
839
|
range: { start: { line: directive.commentLine, column: 0 }, end: { line: directive.commentLine, column: 999 } },
|
|
128
840
|
severity: "error",
|
|
129
|
-
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`
|
|
130
842
|
});
|
|
131
843
|
}
|
|
132
844
|
}
|
|
@@ -139,21 +851,24 @@ function analyzeFile(filePath, source, activeCases) {
|
|
|
139
851
|
diagnostics.push({
|
|
140
852
|
range: throwRange,
|
|
141
853
|
severity: "warning",
|
|
142
|
-
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"
|
|
143
856
|
});
|
|
144
857
|
}
|
|
145
858
|
for (const ur of caseUnreachable) {
|
|
146
859
|
diagnostics.push({
|
|
147
860
|
range: ur,
|
|
148
861
|
severity: "info",
|
|
149
|
-
message: "
|
|
150
|
-
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"]
|
|
151
866
|
});
|
|
152
867
|
}
|
|
153
868
|
}
|
|
154
869
|
}
|
|
155
870
|
if (analysis.cases.length > 1) {
|
|
156
|
-
analysis.combined = simplifyUnion(analysis.cases.map((c) => c.result));
|
|
871
|
+
analysis.combined = collapseLiteralUnion(simplifyUnion(analysis.cases.map((c) => c.result)), COLLAPSE_LITERAL_THRESHOLD);
|
|
157
872
|
} else if (analysis.cases.length === 1) {
|
|
158
873
|
analysis.combined = analysis.cases[0].result;
|
|
159
874
|
}
|
|
@@ -163,26 +878,150 @@ function analyzeFile(filePath, source, activeCases) {
|
|
|
163
878
|
const result = evaluateFunction(fn.node, directive.args, globalEnv);
|
|
164
879
|
const matches = isSubtypeOf(result, returnsDirective.expected);
|
|
165
880
|
if (!matches) {
|
|
166
|
-
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`;
|
|
167
882
|
analysis.assertionErrors.push(msg);
|
|
168
883
|
diagnostics.push({
|
|
169
884
|
range: fnLoc,
|
|
170
885
|
severity: "error",
|
|
171
|
-
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
|
+
]
|
|
172
892
|
});
|
|
173
893
|
}
|
|
174
894
|
}
|
|
175
895
|
}
|
|
176
896
|
functionResults.push(analysis);
|
|
177
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
|
+
}
|
|
178
1001
|
buildNodeTypeMap(ast, globalEnv, nodeTypeMap);
|
|
1002
|
+
setUnknownBuiltinHandler(null);
|
|
1003
|
+
setCallCollector(null);
|
|
1004
|
+
setUnknownCollector(null);
|
|
1005
|
+
setProvenanceTracking(false);
|
|
179
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);
|
|
180
1018
|
return {
|
|
181
1019
|
functions: functionResults,
|
|
182
1020
|
diagnostics,
|
|
183
1021
|
bindings,
|
|
184
1022
|
nodeTypeMap,
|
|
185
|
-
caseHints
|
|
1023
|
+
caseHints,
|
|
1024
|
+
...externalFunctions.length > 0 ? { externalFunctions } : {}
|
|
186
1025
|
};
|
|
187
1026
|
}
|
|
188
1027
|
function collectBindings(ast, env, bindings) {
|
|
@@ -248,12 +1087,27 @@ function getCasesForFile(filePath, source) {
|
|
|
248
1087
|
return { functionName: fn.name, cases, loc: locFromNode(fn.node) };
|
|
249
1088
|
});
|
|
250
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
|
+
}
|
|
251
1097
|
function getTypeAtPosition(filePath, source, line, column, activeCases) {
|
|
252
1098
|
const ast = parse(source);
|
|
253
1099
|
resetMemo();
|
|
1100
|
+
resetEnvModules();
|
|
254
1101
|
setModuleResolver(resolveModule);
|
|
255
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);
|
|
256
1106
|
const globalEnv = createEnvironment();
|
|
1107
|
+
if (envNames.length > 0) {
|
|
1108
|
+
const loaded = loadEnvs(envNames, globalEnv);
|
|
1109
|
+
setEnvModules(loaded.modules);
|
|
1110
|
+
}
|
|
257
1111
|
evaluateProgram(ast, globalEnv);
|
|
258
1112
|
const nodeTypeMap = /* @__PURE__ */ new Map();
|
|
259
1113
|
buildNodeTypeMap(ast, globalEnv, nodeTypeMap);
|
|
@@ -274,6 +1128,8 @@ function getTypeAtPosition(filePath, source, line, column, activeCases) {
|
|
|
274
1128
|
}
|
|
275
1129
|
}
|
|
276
1130
|
setModuleResolver(null);
|
|
1131
|
+
resetEnvModules();
|
|
1132
|
+
resetMockModules();
|
|
277
1133
|
return findBestTypeAtPosition(nodeTypeMap, globalEnv, ast, line, column);
|
|
278
1134
|
}
|
|
279
1135
|
function findEnclosingFunction(functions, line) {
|
|
@@ -446,6 +1302,7 @@ function getCompletionsForType(tv) {
|
|
|
446
1302
|
}
|
|
447
1303
|
|
|
448
1304
|
// src/dts-generator.ts
|
|
1305
|
+
import { T as T2, isTemplate, getTemplateParts, simplifyUnion as simplifyUnion2, typeValueEquals } from "@nudojs/core";
|
|
449
1306
|
function typeValueToTSType(tv) {
|
|
450
1307
|
switch (tv.kind) {
|
|
451
1308
|
case "literal": {
|
|
@@ -458,6 +1315,14 @@ function typeValueToTSType(tv) {
|
|
|
458
1315
|
}
|
|
459
1316
|
case "primitive":
|
|
460
1317
|
return tv.type;
|
|
1318
|
+
case "refined": {
|
|
1319
|
+
if (isTemplate(tv)) {
|
|
1320
|
+
const parts = getTemplateParts(tv);
|
|
1321
|
+
const inner = parts.map((p) => p.kind === "literal" && typeof p.value === "string" ? p.value : `\${${typeValueToTSType(p)}}`).join("");
|
|
1322
|
+
return `\`${inner}\``;
|
|
1323
|
+
}
|
|
1324
|
+
return typeValueToTSType(tv.base);
|
|
1325
|
+
}
|
|
461
1326
|
case "object": {
|
|
462
1327
|
const entries = Object.entries(tv.properties);
|
|
463
1328
|
if (entries.length === 0) return "{}";
|
|
@@ -471,8 +1336,10 @@ function typeValueToTSType(tv) {
|
|
|
471
1336
|
return `[${inner}]`;
|
|
472
1337
|
}
|
|
473
1338
|
case "function": {
|
|
474
|
-
const params = tv.params.map((p
|
|
475
|
-
|
|
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}`;
|
|
476
1343
|
}
|
|
477
1344
|
case "promise":
|
|
478
1345
|
return `Promise<${typeValueToTSType(tv.value)}>`;
|
|
@@ -491,33 +1358,608 @@ function wrapComplexType(tv) {
|
|
|
491
1358
|
if (tv.kind === "union") return `(${ts})`;
|
|
492
1359
|
return ts;
|
|
493
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
|
+
}
|
|
494
1492
|
function generateDts(result) {
|
|
495
1493
|
const lines = [];
|
|
496
1494
|
for (const fn of result.functions) {
|
|
497
|
-
|
|
498
|
-
|
|
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" });
|
|
499
1758
|
continue;
|
|
500
1759
|
}
|
|
501
|
-
if (
|
|
502
|
-
|
|
503
|
-
const params = c.args.map((a, i) => `arg${i}: ${typeValueToTSType(a)}`).join(", ");
|
|
504
|
-
const ret = typeValueToTSType(c.result);
|
|
505
|
-
lines.push(`export declare function ${fn.name}(${params}): ${ret};`);
|
|
1760
|
+
if (existingCases.length > 0) {
|
|
1761
|
+
skipped.push({ fn: fn.name, reason: "already-generated" });
|
|
506
1762
|
continue;
|
|
507
1763
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
1764
|
+
if (fn.skipped) {
|
|
1765
|
+
skipped.push({ fn: fn.name, reason: "skipped" });
|
|
1766
|
+
continue;
|
|
1767
|
+
}
|
|
1768
|
+
if (fn.noDeclaration) {
|
|
1769
|
+
skipped.push({ fn: fn.name, reason: "no-declaration" });
|
|
1770
|
+
continue;
|
|
512
1771
|
}
|
|
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 });
|
|
513
1795
|
}
|
|
514
|
-
|
|
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} */`);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
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";
|
|
515
1943
|
}
|
|
516
1944
|
export {
|
|
517
1945
|
analyzeFile,
|
|
1946
|
+
analyzeFileAsync,
|
|
1947
|
+
buildCaseDirective,
|
|
1948
|
+
buildModuleGraph,
|
|
1949
|
+
collectCallRecords,
|
|
1950
|
+
computeDirtySet,
|
|
518
1951
|
generateDts,
|
|
1952
|
+
generateFunctionDtsLines,
|
|
1953
|
+
generateGuardFunction,
|
|
519
1954
|
getCasesForFile,
|
|
520
1955
|
getCompletionsAtPosition,
|
|
521
1956
|
getTypeAtPosition,
|
|
522
|
-
|
|
1957
|
+
getTypeAtPositionAsync,
|
|
1958
|
+
insertGeneratedCaseDirectives,
|
|
1959
|
+
serializeCaseArg,
|
|
1960
|
+
stripGeneratedCaseDirectives,
|
|
1961
|
+
topoSortDirty,
|
|
1962
|
+
typeValueToTSType,
|
|
1963
|
+
typeValueToZodSchema,
|
|
1964
|
+
unifiedDiff
|
|
523
1965
|
};
|