@nudojs/service 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +517 -4
- package/dist/index.js +3043 -213
- package/package.json +16 -5
- package/CHANGELOG.md +0 -38
- package/src/__tests__/analyzer.test.ts +0 -812
- package/src/__tests__/as-replace.test.ts +0 -165
- package/src/__tests__/case-emitter.test.ts +0 -636
- package/src/__tests__/diagnostics.test.ts +0 -90
- package/src/__tests__/dts-generator.test.ts +0 -382
- package/src/__tests__/fn-sig-impl.test.ts +0 -247
- package/src/__tests__/guard-generator.test.ts +0 -29
- package/src/__tests__/integration.test.ts +0 -332
- package/src/__tests__/schema-generator.test.ts +0 -38
- package/src/analyzer.ts +0 -1841
- package/src/case-emitter.ts +0 -475
- package/src/dts-generator.ts +0 -291
- package/src/guard-generator.ts +0 -50
- package/src/index.ts +0 -47
- package/src/schema-generator.ts +0 -43
- package/tsconfig.json +0 -13
package/dist/index.js
CHANGED
|
@@ -1,19 +1,37 @@
|
|
|
1
1
|
// src/analyzer.ts
|
|
2
|
-
import { readFileSync, existsSync, statSync, realpathSync } from "fs";
|
|
3
|
-
import { resolve, dirname } from "path";
|
|
2
|
+
import { readFileSync as readFileSync4, existsSync as existsSync3, statSync as statSync3, realpathSync } from "fs";
|
|
3
|
+
import { resolve as resolve4, dirname as dirname5 } from "path";
|
|
4
4
|
import traverse from "@babel/traverse";
|
|
5
5
|
import {
|
|
6
|
-
T,
|
|
7
|
-
typeValueToString,
|
|
6
|
+
T as T2,
|
|
7
|
+
typeValueToString as typeValueToString3,
|
|
8
8
|
simplifyUnion,
|
|
9
9
|
widenLiteral,
|
|
10
10
|
collapseLiteralUnion,
|
|
11
|
-
createEnvironment,
|
|
11
|
+
createEnvironment as createEnvironment2,
|
|
12
12
|
isSubtypeOf,
|
|
13
|
-
mockHelperToTypeValue
|
|
13
|
+
mockHelperToTypeValue,
|
|
14
|
+
getFnSig as getFnSig3,
|
|
15
|
+
generalizeFromAst as generalizeFromAst2,
|
|
16
|
+
termToString,
|
|
17
|
+
predToString,
|
|
18
|
+
analyzeFn,
|
|
19
|
+
evalProgramAbs as evalProgramAbs2,
|
|
20
|
+
setAbsCallCollector,
|
|
21
|
+
setAbsNodeCollector,
|
|
22
|
+
absFunction as absFunction5,
|
|
23
|
+
collectAbsNodeTypes,
|
|
24
|
+
findAbsAtPosition,
|
|
25
|
+
typeValueToAbs as typeValueToAbs5,
|
|
26
|
+
absToTypeValue as absToTypeValue2,
|
|
27
|
+
formatAbs as formatAbs3,
|
|
28
|
+
formatAbsMultiline,
|
|
29
|
+
stableAnalyzeKeySource as stableAnalyzeKeySource2,
|
|
30
|
+
fnFingerprints
|
|
14
31
|
} from "@nudojs/core";
|
|
15
|
-
import { parse, extractDirectives, extractFileDirectives, parseTypeValueExpr } from "@nudojs/parser";
|
|
32
|
+
import { parse as parse5, extractDirectives, extractFileDirectives, parseTypeValueExpr as parseTypeValueExpr2 } from "@nudojs/parser";
|
|
16
33
|
import {
|
|
34
|
+
evaluate,
|
|
17
35
|
evaluateFunction,
|
|
18
36
|
evaluateFunctionFull,
|
|
19
37
|
memberMayExistOn,
|
|
@@ -36,32 +54,1545 @@ import {
|
|
|
36
54
|
setMockModules,
|
|
37
55
|
resetMockModules,
|
|
38
56
|
setCurrentSource,
|
|
39
|
-
loadEnvs,
|
|
57
|
+
loadEnvs as loadEnvs2,
|
|
40
58
|
preloadPathEnvs,
|
|
41
59
|
findProjectConfig,
|
|
42
|
-
resolveNpmNudo
|
|
60
|
+
resolveNpmNudo,
|
|
61
|
+
BUILTIN_PROTOTYPE_METHOD_APPROXIMATIONS
|
|
43
62
|
} from "@nudojs/cli/evaluator";
|
|
63
|
+
|
|
64
|
+
// src/mock-abs.ts
|
|
65
|
+
import { parseTypeValueExpr } from "@nudojs/parser";
|
|
66
|
+
import {
|
|
67
|
+
absFunction,
|
|
68
|
+
emptyEnv,
|
|
69
|
+
typeValueToAbs,
|
|
70
|
+
abs as makeAbs,
|
|
71
|
+
confJoin,
|
|
72
|
+
unknown as absUnknown,
|
|
73
|
+
litValue,
|
|
74
|
+
formatAbs,
|
|
75
|
+
getFnImpl,
|
|
76
|
+
typeValueToString
|
|
77
|
+
} from "@nudojs/core";
|
|
78
|
+
function markMockConf(a) {
|
|
79
|
+
a.conf = confJoin(a.conf, "mock");
|
|
80
|
+
return a;
|
|
81
|
+
}
|
|
82
|
+
function astContentKey(n, depth = 0) {
|
|
83
|
+
if (n == null || depth > 24) return "";
|
|
84
|
+
if (typeof n !== "object") return String(n);
|
|
85
|
+
if (Array.isArray(n)) return n.map((x) => astContentKey(x, depth + 1)).join(",");
|
|
86
|
+
const obj = n;
|
|
87
|
+
const type = typeof obj.type === "string" ? obj.type : "";
|
|
88
|
+
const parts = [type];
|
|
89
|
+
for (const key of ["name", "value", "raw", "operator", "computed"]) {
|
|
90
|
+
const v = obj[key];
|
|
91
|
+
if (v !== void 0 && (typeof v !== "object" || v === null)) parts.push(`${key}=${String(v)}`);
|
|
92
|
+
}
|
|
93
|
+
for (const key of Object.keys(obj)) {
|
|
94
|
+
if (key === "loc" || key === "start" || key === "end" || key === "leadingComments" || key === "trailingComments" || key === "innerComments" || key === "type" || key === "name" || key === "value" || key === "raw" || key === "operator" || key === "computed") {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const v = obj[key];
|
|
98
|
+
if (v && typeof v === "object") parts.push(`${key}:{${astContentKey(v, depth + 1)}}`);
|
|
99
|
+
}
|
|
100
|
+
return parts.join("|");
|
|
101
|
+
}
|
|
102
|
+
function stampFingerprint(a, fp) {
|
|
103
|
+
const impl = getFnImpl(a);
|
|
104
|
+
if (impl) impl.fingerprint = fp;
|
|
105
|
+
return a;
|
|
106
|
+
}
|
|
107
|
+
function constantMockFn(result) {
|
|
108
|
+
const marked = markMockConf(result);
|
|
109
|
+
const mockEnv = emptyEnv();
|
|
110
|
+
const retName = "__nudo_mock_ret";
|
|
111
|
+
mockEnv.vars.set(retName, marked);
|
|
112
|
+
const body = {
|
|
113
|
+
type: "BlockStatement",
|
|
114
|
+
body: [
|
|
115
|
+
{
|
|
116
|
+
type: "ReturnStatement",
|
|
117
|
+
argument: { type: "Identifier", name: retName }
|
|
118
|
+
}
|
|
119
|
+
]
|
|
120
|
+
};
|
|
121
|
+
let retKey;
|
|
122
|
+
try {
|
|
123
|
+
retKey = formatAbs(marked);
|
|
124
|
+
} catch {
|
|
125
|
+
retKey = "?";
|
|
126
|
+
}
|
|
127
|
+
return stampFingerprint(
|
|
128
|
+
markMockConf(absFunction(["...args"], { body, env: mockEnv })),
|
|
129
|
+
`ret=${retKey}`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
function absArgMatches(declared, actual) {
|
|
133
|
+
if (!actual) return false;
|
|
134
|
+
const declaredAbs = typeValueToAbs(declared);
|
|
135
|
+
const av = litValue(actual);
|
|
136
|
+
const dv = litValue(declaredAbs);
|
|
137
|
+
if (dv !== void 0) {
|
|
138
|
+
return av !== void 0 && Object.is(av, dv);
|
|
139
|
+
}
|
|
140
|
+
if (declared.kind === "primitive") {
|
|
141
|
+
if (actual.shape.k === "prim") return actual.shape.type === declared.type;
|
|
142
|
+
if (av !== void 0) {
|
|
143
|
+
const t = typeof av;
|
|
144
|
+
return declared.type === "number" && t === "number" || declared.type === "string" && t === "string" || declared.type === "boolean" && t === "boolean" || declared.type === "bigint" && t === "bigint";
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
function dispatchMockFn(defaultReturn, cases) {
|
|
150
|
+
if (!cases?.length) return constantMockFn(defaultReturn);
|
|
151
|
+
const caseAbs = cases.map((c) => ({
|
|
152
|
+
declared: c.args,
|
|
153
|
+
result: markMockConf(typeValueToAbs(c.returnValue))
|
|
154
|
+
}));
|
|
155
|
+
const dummyBody = {
|
|
156
|
+
type: "BlockStatement",
|
|
157
|
+
body: [{ type: "ReturnStatement", argument: null }]
|
|
158
|
+
};
|
|
159
|
+
const markedDefault = markMockConf(defaultReturn);
|
|
160
|
+
let defaultKey;
|
|
161
|
+
try {
|
|
162
|
+
defaultKey = formatAbs(markedDefault);
|
|
163
|
+
} catch {
|
|
164
|
+
defaultKey = "?";
|
|
165
|
+
}
|
|
166
|
+
const caseKey = cases.map(
|
|
167
|
+
(c) => `${c.args.map((a) => typeValueToString(a)).join(",")}->${typeValueToString(c.returnValue)}`
|
|
168
|
+
).join("|");
|
|
169
|
+
return stampFingerprint(
|
|
170
|
+
markMockConf(
|
|
171
|
+
absFunction(["...args"], {
|
|
172
|
+
body: dummyBody,
|
|
173
|
+
env: emptyEnv(),
|
|
174
|
+
apply: (args) => {
|
|
175
|
+
for (const c of caseAbs) {
|
|
176
|
+
if (c.declared.every((d, i) => absArgMatches(d, args[i]))) {
|
|
177
|
+
return c.result;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return markedDefault;
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
),
|
|
184
|
+
`dispatch=default=${defaultKey};cases=${caseKey}`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
function absFromMockHelper(h) {
|
|
188
|
+
if (h.callsFakeImpl && h.callsFakeImpl.kind === "function") {
|
|
189
|
+
const fn = h.callsFakeImpl;
|
|
190
|
+
const body = fn.body;
|
|
191
|
+
return stampFingerprint(
|
|
192
|
+
absFunction(fn.params, { body, async: false }),
|
|
193
|
+
`fake=${fn.params.join(",")}:${astContentKey(body)}`
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
let defaultReturn;
|
|
197
|
+
if (h.resolvedValue) {
|
|
198
|
+
defaultReturn = makeAbs(
|
|
199
|
+
{ k: "eff", eff: "promise", inner: typeValueToAbs(h.resolvedValue) },
|
|
200
|
+
void 0,
|
|
201
|
+
void 0,
|
|
202
|
+
"path"
|
|
203
|
+
);
|
|
204
|
+
} else if (h.rejectedValue) {
|
|
205
|
+
defaultReturn = makeAbs({ k: "never" }, void 0, void 0, "exact");
|
|
206
|
+
} else if (h.returnValue) {
|
|
207
|
+
defaultReturn = typeValueToAbs(h.returnValue);
|
|
208
|
+
} else if (h.onFirstCallValue) {
|
|
209
|
+
defaultReturn = typeValueToAbs(h.onFirstCallValue);
|
|
210
|
+
} else {
|
|
211
|
+
defaultReturn = absUnknown;
|
|
212
|
+
}
|
|
213
|
+
return dispatchMockFn(defaultReturn, h.withArgsCases);
|
|
214
|
+
}
|
|
215
|
+
function absFromSinon(sinonExpr) {
|
|
216
|
+
if (sinonExpr.resolvedValue) {
|
|
217
|
+
return constantMockFn(
|
|
218
|
+
makeAbs(
|
|
219
|
+
{ k: "eff", eff: "promise", inner: typeValueToAbs(sinonExpr.resolvedValue) },
|
|
220
|
+
void 0,
|
|
221
|
+
void 0,
|
|
222
|
+
"path"
|
|
223
|
+
)
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
if (sinonExpr.rejectedValue) {
|
|
227
|
+
return constantMockFn(makeAbs({ k: "never" }, void 0, void 0, "exact"));
|
|
228
|
+
}
|
|
229
|
+
if (sinonExpr.returnValue) {
|
|
230
|
+
return constantMockFn(typeValueToAbs(sinonExpr.returnValue));
|
|
231
|
+
}
|
|
232
|
+
return constantMockFn(absUnknown);
|
|
233
|
+
}
|
|
234
|
+
function mockSeedsToAbsMocks(seeds) {
|
|
235
|
+
const out = { ...seeds.seedVars };
|
|
236
|
+
for (const [name, fn] of Object.entries(seeds.seedFns)) {
|
|
237
|
+
out[name] = absFunction(fn.params, { body: fn.body, async: fn.async ?? false });
|
|
238
|
+
}
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
241
|
+
function mockDirectivesToAbsSeeds(functions) {
|
|
242
|
+
const seedVars = {};
|
|
243
|
+
const seedFns = {};
|
|
244
|
+
for (const fn of functions) {
|
|
245
|
+
for (const d of fn.directives) {
|
|
246
|
+
if (d.kind !== "mock") continue;
|
|
247
|
+
if (d.arrowFn) {
|
|
248
|
+
const body = d.arrowFn.body;
|
|
249
|
+
seedFns[d.name] = {
|
|
250
|
+
params: d.arrowFn.params,
|
|
251
|
+
body,
|
|
252
|
+
async: false,
|
|
253
|
+
fingerprint: `fake=${d.arrowFn.params.join(",")}:${astContentKey(body)}`
|
|
254
|
+
};
|
|
255
|
+
} else if (d.nudoMock) {
|
|
256
|
+
seedVars[d.name] = markMockConf(absFromMockHelper(d.nudoMock));
|
|
257
|
+
} else if (d.sinonExpr) {
|
|
258
|
+
seedVars[d.name] = markMockConf(absFromSinon(d.sinonExpr));
|
|
259
|
+
} else if (d.expression) {
|
|
260
|
+
try {
|
|
261
|
+
seedVars[d.name] = typeValueToAbs(parseTypeValueExpr(d.expression));
|
|
262
|
+
} catch {
|
|
263
|
+
seedVars[d.name] = absUnknown;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return { seedVars, seedFns };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// src/harvest-auto.ts
|
|
272
|
+
import { parse as parse2 } from "@nudojs/parser";
|
|
273
|
+
|
|
274
|
+
// src/static-imports.ts
|
|
275
|
+
import { parse } from "@nudojs/parser";
|
|
276
|
+
import { readFileSync, existsSync } from "fs";
|
|
277
|
+
import { resolve, dirname, join } from "path";
|
|
278
|
+
import { generalizeFromAst } from "@nudojs/core";
|
|
279
|
+
function resolveRelative(fromFile, spec) {
|
|
280
|
+
if (!spec.startsWith(".") && !spec.startsWith("/")) return void 0;
|
|
281
|
+
const base = resolve(dirname(fromFile), spec);
|
|
282
|
+
for (const cand of [
|
|
283
|
+
base,
|
|
284
|
+
base + ".js",
|
|
285
|
+
base + ".cjs",
|
|
286
|
+
base + ".mjs",
|
|
287
|
+
base + ".ts",
|
|
288
|
+
base + ".mts",
|
|
289
|
+
base + ".cts",
|
|
290
|
+
join(base, "index.js"),
|
|
291
|
+
join(base, "index.cjs"),
|
|
292
|
+
join(base, "index.mjs"),
|
|
293
|
+
join(base, "index.ts")
|
|
294
|
+
]) {
|
|
295
|
+
if (existsSync(cand)) return cand;
|
|
296
|
+
}
|
|
297
|
+
return void 0;
|
|
298
|
+
}
|
|
299
|
+
function collectDependencySpecs(ast) {
|
|
300
|
+
const specs = [];
|
|
301
|
+
for (const stmt of ast.program.body) {
|
|
302
|
+
if (stmt.type === "ImportDeclaration") {
|
|
303
|
+
specs.push(stmt.source.value);
|
|
304
|
+
}
|
|
305
|
+
if (stmt.type === "ExportNamedDeclaration" && stmt.source) {
|
|
306
|
+
specs.push(stmt.source.value);
|
|
307
|
+
}
|
|
308
|
+
if (stmt.type === "ExportAllDeclaration" && stmt.source) {
|
|
309
|
+
specs.push(stmt.source.value);
|
|
310
|
+
}
|
|
311
|
+
collectRequires(stmt, specs);
|
|
312
|
+
}
|
|
313
|
+
return specs;
|
|
314
|
+
}
|
|
315
|
+
function collectRequires(node, out) {
|
|
316
|
+
const visit = (n) => {
|
|
317
|
+
if (!n || typeof n !== "object") return;
|
|
318
|
+
const obj = n;
|
|
319
|
+
if (obj.type === "CallExpression" && obj.callee?.type === "Identifier" && obj.callee.name === "require" && obj.arguments?.[0]?.type === "StringLiteral") {
|
|
320
|
+
const spec = obj.arguments[0].value;
|
|
321
|
+
if (typeof spec === "string") out.push(spec);
|
|
322
|
+
}
|
|
323
|
+
for (const key of Object.keys(obj)) {
|
|
324
|
+
if (key === "loc" || key === "start" || key === "end") continue;
|
|
325
|
+
const v = obj[key];
|
|
326
|
+
if (Array.isArray(v)) v.forEach(visit);
|
|
327
|
+
else if (v && typeof v === "object") visit(v);
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
visit(node);
|
|
331
|
+
}
|
|
332
|
+
function analyzeExportsFromSource(filePath, source) {
|
|
333
|
+
const file = parse(source);
|
|
334
|
+
const named = /* @__PURE__ */ new Map();
|
|
335
|
+
let defaultExport;
|
|
336
|
+
const poly = /* @__PURE__ */ new Map();
|
|
337
|
+
for (const stmt of file.program.body) {
|
|
338
|
+
let decl = stmt;
|
|
339
|
+
if (stmt.type === "ExportNamedDeclaration" && stmt.declaration) {
|
|
340
|
+
decl = stmt.declaration;
|
|
341
|
+
}
|
|
342
|
+
if (decl.type === "FunctionDeclaration" && decl.id) {
|
|
343
|
+
named.set(decl.id.name, decl.id.name);
|
|
344
|
+
}
|
|
345
|
+
if (decl.type === "VariableDeclaration") {
|
|
346
|
+
for (const d of decl.declarations) {
|
|
347
|
+
if (d.id.type === "Identifier") named.set(d.id.name, d.id.name);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (decl.type === "ClassDeclaration" && decl.id) {
|
|
351
|
+
named.set(decl.id.name, decl.id.name);
|
|
352
|
+
}
|
|
353
|
+
if (stmt.type === "ExportDefaultDeclaration") {
|
|
354
|
+
const d = stmt.declaration;
|
|
355
|
+
if (d.type === "FunctionDeclaration" && d.id) defaultExport = d.id.name;
|
|
356
|
+
if (d.type === "Identifier") defaultExport = d.name;
|
|
357
|
+
}
|
|
358
|
+
collectCjsExports(stmt, named, (n) => {
|
|
359
|
+
defaultExport = n;
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
for (const name of named.values()) {
|
|
363
|
+
const g = generalizeFromAst(name, source);
|
|
364
|
+
if (g) poly.set(name, g);
|
|
365
|
+
}
|
|
366
|
+
if (defaultExport) {
|
|
367
|
+
const g = generalizeFromAst(defaultExport, source);
|
|
368
|
+
if (g) poly.set("default", g);
|
|
369
|
+
}
|
|
370
|
+
return { path: filePath, named, defaultExport, source, poly };
|
|
371
|
+
}
|
|
372
|
+
function collectCjsExports(stmt, named, setDefault) {
|
|
373
|
+
const visit = (n) => {
|
|
374
|
+
if (!n || typeof n !== "object") return;
|
|
375
|
+
const obj = n;
|
|
376
|
+
if (obj.type !== "AssignmentExpression") {
|
|
377
|
+
for (const key of Object.keys(obj)) {
|
|
378
|
+
if (key === "loc" || key === "start" || key === "end") continue;
|
|
379
|
+
const v = obj[key];
|
|
380
|
+
if (Array.isArray(v)) v.forEach(visit);
|
|
381
|
+
else if (v && typeof v === "object") visit(v);
|
|
382
|
+
}
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const left = obj.left;
|
|
386
|
+
const right = obj.right;
|
|
387
|
+
if (left?.type === "MemberExpression" && left.object?.type === "Identifier" && left.object.name === "exports" && left.property?.type === "Identifier" && left.property.name) {
|
|
388
|
+
named.set(left.property.name, left.property.name);
|
|
389
|
+
}
|
|
390
|
+
if (left?.type === "MemberExpression" && left.object?.type === "Identifier" && left.object.name === "module" && left.property?.type === "Identifier" && left.property.name === "exports") {
|
|
391
|
+
if (right?.type === "Identifier" && typeof right.name === "string") {
|
|
392
|
+
named.set(right.name, right.name);
|
|
393
|
+
setDefault(right.name);
|
|
394
|
+
}
|
|
395
|
+
if (right?.type === "ObjectExpression") {
|
|
396
|
+
const props = right.properties;
|
|
397
|
+
for (const p of props) {
|
|
398
|
+
if (p.type !== "ObjectProperty") continue;
|
|
399
|
+
const key = p.key;
|
|
400
|
+
const val = p.value;
|
|
401
|
+
const exportName = key?.type === "Identifier" ? key.name : key?.type === "StringLiteral" ? key.value : void 0;
|
|
402
|
+
if (exportName) named.set(exportName, exportName);
|
|
403
|
+
if (val?.type === "Identifier" && val.name && exportName) {
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
visit(stmt);
|
|
410
|
+
}
|
|
411
|
+
function collectStaticImports(entryFile, maxDepth = 8) {
|
|
412
|
+
const graph = /* @__PURE__ */ new Map();
|
|
413
|
+
const queue = [
|
|
414
|
+
{ file: resolve(entryFile), depth: 0 }
|
|
415
|
+
];
|
|
416
|
+
const seen = /* @__PURE__ */ new Set();
|
|
417
|
+
while (queue.length > 0) {
|
|
418
|
+
const { file, depth } = queue.shift();
|
|
419
|
+
if (seen.has(file) || depth > maxDepth) continue;
|
|
420
|
+
seen.add(file);
|
|
421
|
+
if (!existsSync(file)) continue;
|
|
422
|
+
const source = readFileSync(file, "utf8");
|
|
423
|
+
const mod = analyzeExportsFromSource(file, source);
|
|
424
|
+
graph.set(file, mod);
|
|
425
|
+
const ast = parse(source);
|
|
426
|
+
for (const spec of collectDependencySpecs(ast)) {
|
|
427
|
+
const resolved = resolveRelative(file, spec);
|
|
428
|
+
if (resolved) queue.push({ file: resolved, depth: depth + 1 });
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return graph;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// src/harvest-package.ts
|
|
435
|
+
import { existsSync as existsSync2, readdirSync, statSync, readFileSync as readFileSync2 } from "fs";
|
|
436
|
+
import { join as join2, resolve as resolve2, dirname as dirname2, basename } from "path";
|
|
437
|
+
import { harvestDts } from "@nudojs/harvester";
|
|
438
|
+
import { typeValueToString as typeValueToString2 } from "@nudojs/core";
|
|
439
|
+
function resolvePackageRoot(pkg, fromDir = process.cwd()) {
|
|
440
|
+
let dir = resolve2(fromDir);
|
|
441
|
+
const bare = pkg.replace(/^@/, "").replace(/\//g, "__");
|
|
442
|
+
for (let i = 0; i < 8; i++) {
|
|
443
|
+
const typesPath = join2(dir, "node_modules", "@types", bare);
|
|
444
|
+
if (existsSync2(typesPath)) return typesPath;
|
|
445
|
+
const pkgPath = join2(dir, "node_modules", pkg);
|
|
446
|
+
if (existsSync2(pkgPath)) return pkgPath;
|
|
447
|
+
const parent = resolve2(dir, "..");
|
|
448
|
+
if (parent === dir) break;
|
|
449
|
+
dir = parent;
|
|
450
|
+
}
|
|
451
|
+
return void 0;
|
|
452
|
+
}
|
|
453
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "test", "tests", "docs", "doc", "examples", "__tests__"]);
|
|
454
|
+
var MAX_DTS_BYTES = 15e5;
|
|
455
|
+
function dtsPriority(p, root) {
|
|
456
|
+
const base = basename(p);
|
|
457
|
+
const rel = p.slice(root.length);
|
|
458
|
+
if (base === "index.d.ts") return 0;
|
|
459
|
+
if (base === "index.d.mts") return 1;
|
|
460
|
+
if (rel.includes("/types/") && base === "index.d.ts") return 2;
|
|
461
|
+
if (base.endsWith(".d.ts") && !base.includes(".")) return 3;
|
|
462
|
+
return 10;
|
|
463
|
+
}
|
|
464
|
+
function collectDtsFiles(root, maxFiles = 8) {
|
|
465
|
+
const out = [];
|
|
466
|
+
const walk = (dir) => {
|
|
467
|
+
if (out.length >= maxFiles * 3) return;
|
|
468
|
+
let entries;
|
|
469
|
+
try {
|
|
470
|
+
entries = readdirSync(dir);
|
|
471
|
+
} catch {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
for (const name of entries) {
|
|
475
|
+
if (out.length >= maxFiles * 3) return;
|
|
476
|
+
if (SKIP_DIRS.has(name)) continue;
|
|
477
|
+
const p = join2(dir, name);
|
|
478
|
+
let st;
|
|
479
|
+
try {
|
|
480
|
+
st = statSync(p);
|
|
481
|
+
} catch {
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
if (st.isDirectory()) walk(p);
|
|
485
|
+
else if (name.endsWith(".d.ts") && !name.endsWith(".d.ts.map")) {
|
|
486
|
+
if (st.size > MAX_DTS_BYTES) continue;
|
|
487
|
+
out.push(p);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
walk(root);
|
|
492
|
+
out.sort((a, b) => dtsPriority(a, root) - dtsPriority(b, root) || a.length - b.length);
|
|
493
|
+
return out.slice(0, maxFiles);
|
|
494
|
+
}
|
|
495
|
+
function entryDtsFromPackageJson(root) {
|
|
496
|
+
const pj = join2(root, "package.json");
|
|
497
|
+
if (!existsSync2(pj)) return void 0;
|
|
498
|
+
try {
|
|
499
|
+
const raw = JSON.parse(readFileSync2(pj, "utf8"));
|
|
500
|
+
const entry = raw.types ?? raw.typings;
|
|
501
|
+
if (typeof entry === "string" && entry.endsWith(".d.ts")) {
|
|
502
|
+
const p = resolve2(root, entry);
|
|
503
|
+
if (existsSync2(p)) {
|
|
504
|
+
const st = statSync(p);
|
|
505
|
+
if (st.size <= MAX_DTS_BYTES) return p;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
} catch {
|
|
509
|
+
}
|
|
510
|
+
return void 0;
|
|
511
|
+
}
|
|
512
|
+
function collectDtsFromEntry(entry, maxFiles = 200) {
|
|
513
|
+
const files = [];
|
|
514
|
+
const seen = /* @__PURE__ */ new Set();
|
|
515
|
+
const queue = [entry];
|
|
516
|
+
const REFERENCE_PATH_REGEX = /<reference\s+path=["']([^"']+)["']\s*\/>/g;
|
|
517
|
+
const RELATIVE_FROM_REGEX = /\bfrom\s+["'](\.[^"']+)["']/g;
|
|
518
|
+
while (queue.length > 0 && files.length < maxFiles) {
|
|
519
|
+
const current = queue.shift();
|
|
520
|
+
if (seen.has(current) || !existsSync2(current)) continue;
|
|
521
|
+
seen.add(current);
|
|
522
|
+
if (!current.endsWith(".d.ts")) continue;
|
|
523
|
+
files.push(current);
|
|
524
|
+
let text;
|
|
525
|
+
try {
|
|
526
|
+
text = readFileSync2(current, "utf-8");
|
|
527
|
+
} catch {
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
const dir = dirname2(current);
|
|
531
|
+
for (const match of text.matchAll(REFERENCE_PATH_REGEX)) {
|
|
532
|
+
queue.push(resolve2(dir, match[1]));
|
|
533
|
+
}
|
|
534
|
+
for (const match of text.matchAll(RELATIVE_FROM_REGEX)) {
|
|
535
|
+
const base = resolve2(dir, match[1]);
|
|
536
|
+
for (const candidate of [`${base}.d.ts`, join2(base, "index.d.ts")]) {
|
|
537
|
+
if (existsSync2(candidate)) {
|
|
538
|
+
queue.push(candidate);
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return files;
|
|
545
|
+
}
|
|
546
|
+
function harvestPackage(pkg, fromDir, maxFiles = 8) {
|
|
547
|
+
const root = resolvePackageRoot(pkg, fromDir);
|
|
548
|
+
if (!root) return { error: `package not found: ${pkg}` };
|
|
549
|
+
const entry = entryDtsFromPackageJson(root) ?? join2(root, "index.d.ts");
|
|
550
|
+
let dtsFiles = existsSync2(entry) ? collectDtsFromEntry(entry, maxFiles) : [];
|
|
551
|
+
if (dtsFiles.length === 0) {
|
|
552
|
+
const collected = collectDtsFiles(root, maxFiles);
|
|
553
|
+
dtsFiles = entry && existsSync2(entry) && !collected.includes(entry) ? [entry, ...collected.filter((f) => f !== entry)].slice(0, maxFiles) : collected;
|
|
554
|
+
}
|
|
555
|
+
if (dtsFiles.length === 0) {
|
|
556
|
+
return { error: `no .d.ts under ${root}` };
|
|
557
|
+
}
|
|
558
|
+
const env = harvestDts(dtsFiles);
|
|
559
|
+
return { pkg, root, dtsFiles, env };
|
|
560
|
+
}
|
|
561
|
+
function formatHarvestSummary(h) {
|
|
562
|
+
const lines = [];
|
|
563
|
+
lines.push(`${h.pkg}: ${h.dtsFiles.length} d.ts, ${h.env.stats.symbols} symbols`);
|
|
564
|
+
const mods = Object.keys(h.env.modules);
|
|
565
|
+
for (const m of mods.slice(0, 5)) {
|
|
566
|
+
const exports = h.env.modules[m];
|
|
567
|
+
const names = Object.keys(exports).slice(0, 12);
|
|
568
|
+
lines.push(` module "${m}": ${names.join(", ")}${Object.keys(exports).length > 12 ? "\u2026" : ""}`);
|
|
569
|
+
}
|
|
570
|
+
if (mods.length > 5) lines.push(` \u2026 ${mods.length - 5} more modules`);
|
|
571
|
+
return lines.join("\n");
|
|
572
|
+
}
|
|
573
|
+
function lookupHarvested(h, moduleName, exportName) {
|
|
574
|
+
const mod = h.env.modules[moduleName];
|
|
575
|
+
const tv = mod?.[exportName] ?? h.env.globals[exportName];
|
|
576
|
+
if (!tv) return void 0;
|
|
577
|
+
return typeValueToString2(tv);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// src/harvest-auto.ts
|
|
581
|
+
function barePackageName(spec) {
|
|
582
|
+
if (!spec) return void 0;
|
|
583
|
+
if (spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("node:")) return void 0;
|
|
584
|
+
const parts = spec.split("/");
|
|
585
|
+
if (spec.startsWith("@")) {
|
|
586
|
+
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : void 0;
|
|
587
|
+
}
|
|
588
|
+
return parts[0];
|
|
589
|
+
}
|
|
590
|
+
function collectBarePackages(source) {
|
|
591
|
+
try {
|
|
592
|
+
const ast = parse2(source);
|
|
593
|
+
const names = /* @__PURE__ */ new Set();
|
|
594
|
+
for (const spec of collectDependencySpecs(ast)) {
|
|
595
|
+
const pkg = barePackageName(spec);
|
|
596
|
+
if (pkg) names.add(pkg);
|
|
597
|
+
}
|
|
598
|
+
return [...names];
|
|
599
|
+
} catch {
|
|
600
|
+
return [];
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
var harvestCache = /* @__PURE__ */ new Map();
|
|
604
|
+
function harvestPackageCached(pkg, fromDir) {
|
|
605
|
+
const key = `${fromDir}::${pkg}`;
|
|
606
|
+
if (harvestCache.has(key)) return harvestCache.get(key);
|
|
607
|
+
let result = null;
|
|
608
|
+
try {
|
|
609
|
+
const h = harvestPackage(pkg, fromDir);
|
|
610
|
+
if (!("error" in h)) result = h;
|
|
611
|
+
} catch {
|
|
612
|
+
result = null;
|
|
613
|
+
}
|
|
614
|
+
harvestCache.set(key, result);
|
|
615
|
+
return result;
|
|
616
|
+
}
|
|
617
|
+
function clearHarvestCache() {
|
|
618
|
+
harvestCache.clear();
|
|
619
|
+
}
|
|
620
|
+
function autoHarvestModules(source, fromDir) {
|
|
621
|
+
const packages = collectBarePackages(source);
|
|
622
|
+
if (packages.length === 0) return {};
|
|
623
|
+
const modules = {};
|
|
624
|
+
for (const pkg of packages) {
|
|
625
|
+
const h = harvestPackageCached(pkg, fromDir);
|
|
626
|
+
if (!h) continue;
|
|
627
|
+
for (const [mod, exports] of Object.entries(h.env.modules)) {
|
|
628
|
+
modules[mod] = exports;
|
|
629
|
+
if (!modules[pkg]) modules[pkg] = exports;
|
|
630
|
+
const base = mod.replace(/^@types\//, "").replace(/\\/g, "/");
|
|
631
|
+
if (base && !modules[base]) modules[base] = exports;
|
|
632
|
+
}
|
|
633
|
+
for (const [k, v] of Object.entries(h.env.globals)) {
|
|
634
|
+
if (!modules[pkg]) modules[pkg] = {};
|
|
635
|
+
if (modules[pkg][k] === void 0) modules[pkg][k] = v;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return modules;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// src/abs-modules-graph.ts
|
|
642
|
+
import { readFileSync as readFileSync3, statSync as statSync2 } from "fs";
|
|
643
|
+
import { dirname as dirname4, resolve as resolve3 } from "path";
|
|
644
|
+
import { parse as parse3 } from "@nudojs/parser";
|
|
645
|
+
import {
|
|
646
|
+
evalProgramAbs,
|
|
647
|
+
collectAbsExports,
|
|
648
|
+
absFunction as absFunction3
|
|
649
|
+
} from "@nudojs/core";
|
|
650
|
+
|
|
651
|
+
// src/harvest-to-abs.ts
|
|
652
|
+
import {
|
|
653
|
+
T,
|
|
654
|
+
typeValueToAbs as typeValueToAbs2,
|
|
655
|
+
confJoin as confJoin2,
|
|
656
|
+
getFnSig,
|
|
657
|
+
absFunction as absFunction2
|
|
658
|
+
} from "@nudojs/core";
|
|
659
|
+
import { dirname as dirname3 } from "path";
|
|
660
|
+
function markMockConf2(a) {
|
|
661
|
+
a.conf = confJoin2(a.conf, "mock");
|
|
662
|
+
return a;
|
|
663
|
+
}
|
|
664
|
+
function harvestedValueToAbs(tv) {
|
|
665
|
+
if (!tv) return markMockConf2(typeValueToAbs2(T.unknown));
|
|
666
|
+
const sig = getFnSig(tv);
|
|
667
|
+
if (sig) {
|
|
668
|
+
const ret = markMockConf2(typeValueToAbs2(sig.returnType));
|
|
669
|
+
const params = tv.kind === "function" && tv.params?.length ? tv.params : sig.paramTypes.map((_, i) => `_arg${i}`);
|
|
670
|
+
const dummyBody = {
|
|
671
|
+
type: "BlockStatement",
|
|
672
|
+
body: [],
|
|
673
|
+
directives: []
|
|
674
|
+
};
|
|
675
|
+
return markMockConf2(
|
|
676
|
+
absFunction2(params, {
|
|
677
|
+
body: dummyBody,
|
|
678
|
+
apply: () => ret
|
|
679
|
+
})
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
return markMockConf2(typeValueToAbs2(tv));
|
|
683
|
+
}
|
|
684
|
+
function harvestToAbsModules(pkg, fromDir) {
|
|
685
|
+
const h = harvestPackageCached(pkg, fromDir);
|
|
686
|
+
if (!h) return {};
|
|
687
|
+
return packageHarvestToAbsModules(pkg, h);
|
|
688
|
+
}
|
|
689
|
+
function packageHarvestToAbsModules(pkg, h) {
|
|
690
|
+
const out = {};
|
|
691
|
+
const convertRecord = (rec) => {
|
|
692
|
+
const named = {};
|
|
693
|
+
for (const [k, v] of Object.entries(rec)) {
|
|
694
|
+
named[k] = harvestedValueToAbs(v);
|
|
695
|
+
}
|
|
696
|
+
return named;
|
|
697
|
+
};
|
|
698
|
+
const register = (key, exports) => {
|
|
699
|
+
if (!out[key]) out[key] = exports;
|
|
700
|
+
};
|
|
701
|
+
for (const [mod, rec] of Object.entries(h.env.modules)) {
|
|
702
|
+
const named = convertRecord(rec);
|
|
703
|
+
const exports = { named };
|
|
704
|
+
const def = rec["default"];
|
|
705
|
+
if (def) exports.default = harvestedValueToAbs(def);
|
|
706
|
+
register(mod, exports);
|
|
707
|
+
register(pkg, exports);
|
|
708
|
+
const stripped = mod.replace(/^@types\//, "").replace(/\\/g, "/");
|
|
709
|
+
if (stripped) register(stripped, exports);
|
|
710
|
+
const base = stripped.split("/").filter(Boolean).pop();
|
|
711
|
+
if (base && base !== pkg) register(base, exports);
|
|
712
|
+
}
|
|
713
|
+
const globalNamed = convertRecord(h.env.globals);
|
|
714
|
+
if (Object.keys(globalNamed).length > 0) {
|
|
715
|
+
const existing = out[pkg];
|
|
716
|
+
if (existing) {
|
|
717
|
+
out[pkg] = { named: { ...globalNamed, ...existing.named }, default: existing.default };
|
|
718
|
+
} else {
|
|
719
|
+
out[pkg] = { named: globalNamed };
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
return out;
|
|
723
|
+
}
|
|
724
|
+
function bareSpecToAbsModules(spec, fromFile) {
|
|
725
|
+
if (!spec || spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("node:")) {
|
|
726
|
+
return void 0;
|
|
727
|
+
}
|
|
728
|
+
const parts = spec.split("/");
|
|
729
|
+
const pkg = spec.startsWith("@") ? parts.length >= 2 ? `${parts[0]}/${parts[1]}` : void 0 : parts[0];
|
|
730
|
+
if (!pkg) return void 0;
|
|
731
|
+
const table = harvestToAbsModules(pkg, dirname3(fromFile));
|
|
732
|
+
return table[spec] ?? table[pkg];
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// src/abs-modules-graph.ts
|
|
736
|
+
function defaultAbsLoadModule(spec, fromFile) {
|
|
737
|
+
if (!spec.startsWith(".") && !spec.startsWith("/")) return void 0;
|
|
738
|
+
try {
|
|
739
|
+
const p = resolve3(dirname4(resolve3(fromFile)), spec);
|
|
740
|
+
for (const cand of [p, `${p}.js`, `${p}.mjs`, `${p}.ts`, resolve3(p, "index.js")]) {
|
|
741
|
+
try {
|
|
742
|
+
return readFileSync3(cand, "utf-8");
|
|
743
|
+
} catch {
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
return void 0;
|
|
747
|
+
} catch {
|
|
748
|
+
return void 0;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
function relCandidates(spec, fromFile) {
|
|
752
|
+
const p = resolve3(dirname4(resolve3(fromFile)), spec);
|
|
753
|
+
return [p, `${p}.js`, `${p}.mjs`, `${p}.ts`, resolve3(p, "index.js")];
|
|
754
|
+
}
|
|
755
|
+
function resolveRel(spec, fromFile) {
|
|
756
|
+
if (!spec.startsWith(".") && !spec.startsWith("/")) return null;
|
|
757
|
+
for (const cand of relCandidates(spec, fromFile)) {
|
|
758
|
+
try {
|
|
759
|
+
readFileSync3(cand, "utf-8");
|
|
760
|
+
return cand;
|
|
761
|
+
} catch {
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
766
|
+
function importSpecs(source) {
|
|
767
|
+
try {
|
|
768
|
+
const file = parse3(source);
|
|
769
|
+
const out = [];
|
|
770
|
+
for (const stmt of file.program.body) {
|
|
771
|
+
if (stmt.type === "ImportDeclaration" || stmt.type === "ExportNamedDeclaration" || stmt.type === "ExportAllDeclaration") {
|
|
772
|
+
const src = stmt.source;
|
|
773
|
+
if (src && typeof src.value === "string") {
|
|
774
|
+
out.push(src.value);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
collectRequireSpecs(stmt, out);
|
|
778
|
+
}
|
|
779
|
+
return out;
|
|
780
|
+
} catch {
|
|
781
|
+
return [];
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
function collectRequireSpecs(node, out) {
|
|
785
|
+
const visit = (n) => {
|
|
786
|
+
if (!n || typeof n !== "object") return;
|
|
787
|
+
const o = n;
|
|
788
|
+
if (o.type === "CallExpression" && o.callee?.type === "Identifier" && o.callee.name === "require" && o.arguments?.[0]?.type === "StringLiteral") {
|
|
789
|
+
const spec = o.arguments[0].value;
|
|
790
|
+
if (typeof spec === "string") out.push(spec);
|
|
791
|
+
}
|
|
792
|
+
for (const key of Object.keys(o)) {
|
|
793
|
+
if (key === "loc" || key === "start" || key === "end") continue;
|
|
794
|
+
const v = o[key];
|
|
795
|
+
if (Array.isArray(v)) v.forEach(visit);
|
|
796
|
+
else if (v && typeof v === "object") visit(v);
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
visit(node);
|
|
800
|
+
}
|
|
801
|
+
function buildModulesForFile(source, fromFile, load, evalDep, depth, maxDepth, onMissing) {
|
|
802
|
+
const modules = {};
|
|
803
|
+
for (const spec of importSpecs(source)) {
|
|
804
|
+
if (spec.startsWith(".") || spec.startsWith("/")) {
|
|
805
|
+
const childPath = resolveRel(spec, fromFile);
|
|
806
|
+
if (!childPath) {
|
|
807
|
+
onMissing(spec, fromFile, relCandidates(spec, fromFile));
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
modules[spec] = evalDep(childPath, spec, fromFile, depth + 1);
|
|
811
|
+
} else if (!spec.startsWith("node:")) {
|
|
812
|
+
const bare = bareSpecToAbsModules(spec, fromFile);
|
|
813
|
+
if (bare) modules[spec] = bare;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
void maxDepth;
|
|
817
|
+
return modules;
|
|
818
|
+
}
|
|
819
|
+
var absModuleCache = /* @__PURE__ */ new Map();
|
|
820
|
+
function clearAbsModuleCache() {
|
|
821
|
+
absModuleCache.clear();
|
|
822
|
+
}
|
|
823
|
+
function evictAbsModuleCacheFiles(paths) {
|
|
824
|
+
for (const p of paths) absModuleCache.delete(p);
|
|
825
|
+
}
|
|
826
|
+
function moduleLabel(p) {
|
|
827
|
+
const parts = p.split(/[/\\]/);
|
|
828
|
+
return parts[parts.length - 1] || p;
|
|
829
|
+
}
|
|
830
|
+
function evalAbsModuleGraph(entrySource, entryFile, opts = {}) {
|
|
831
|
+
const load = opts.loadModule ?? defaultAbsLoadModule;
|
|
832
|
+
const maxDepth = opts.maxDepth ?? 16;
|
|
833
|
+
const cache = /* @__PURE__ */ new Map();
|
|
834
|
+
const loading = [];
|
|
835
|
+
const issues = [];
|
|
836
|
+
const seenIssue = /* @__PURE__ */ new Set();
|
|
837
|
+
const pushIssue = (kind, label, reason) => {
|
|
838
|
+
const key = `${kind}:${label}`;
|
|
839
|
+
if (seenIssue.has(key)) return;
|
|
840
|
+
seenIssue.add(key);
|
|
841
|
+
issues.push({ kind, label, reason });
|
|
842
|
+
};
|
|
843
|
+
function evalDep(absPath, spec, fromFile, depth) {
|
|
844
|
+
const cycleIndex = loading.indexOf(absPath);
|
|
845
|
+
if (cycleIndex !== -1) {
|
|
846
|
+
const chain = [...loading.slice(cycleIndex), absPath];
|
|
847
|
+
pushIssue(
|
|
848
|
+
"cycle",
|
|
849
|
+
moduleLabel(absPath),
|
|
850
|
+
`Circular module load: ${chain.join(" -> ")} (bindings inside the cycle resolve to their partially evaluated types)`
|
|
851
|
+
);
|
|
852
|
+
return cache.get(absPath) ?? { named: {} };
|
|
853
|
+
}
|
|
854
|
+
const shared = absModuleCache.get(absPath);
|
|
855
|
+
if (shared) {
|
|
856
|
+
try {
|
|
857
|
+
const st = statSync2(absPath);
|
|
858
|
+
if (st.mtimeMs === shared.mtimeMs && st.size === shared.size) {
|
|
859
|
+
for (const iss of shared.issues) pushIssue(iss.kind, iss.label, iss.reason);
|
|
860
|
+
cache.set(absPath, shared.exports);
|
|
861
|
+
return shared.exports;
|
|
862
|
+
}
|
|
863
|
+
} catch {
|
|
864
|
+
}
|
|
865
|
+
absModuleCache.delete(absPath);
|
|
866
|
+
}
|
|
867
|
+
if (cache.has(absPath)) return cache.get(absPath);
|
|
868
|
+
if (depth > maxDepth) {
|
|
869
|
+
const chain = [...loading, absPath];
|
|
870
|
+
pushIssue(
|
|
871
|
+
"depth",
|
|
872
|
+
moduleLabel(absPath),
|
|
873
|
+
`Module load chain too deep (depth ${chain.length} > ${maxDepth} max): ${chain.join(" -> ")} (loading truncated, deeper modules typed as unknown)`
|
|
874
|
+
);
|
|
875
|
+
const empty = { named: {} };
|
|
876
|
+
cache.set(absPath, empty);
|
|
877
|
+
return empty;
|
|
878
|
+
}
|
|
879
|
+
cache.set(absPath, { named: {} });
|
|
880
|
+
loading.push(absPath);
|
|
881
|
+
const source = load(spec, fromFile);
|
|
882
|
+
if (source === void 0) {
|
|
883
|
+
pushIssue(
|
|
884
|
+
"missing",
|
|
885
|
+
spec,
|
|
886
|
+
`Module file not found for '${spec}' (from ${moduleLabel(fromFile)}); tried: ${absPath}`
|
|
887
|
+
);
|
|
888
|
+
const empty = { named: {} };
|
|
889
|
+
cache.set(absPath, empty);
|
|
890
|
+
loading.pop();
|
|
891
|
+
return empty;
|
|
892
|
+
}
|
|
893
|
+
const issueStart = issues.length;
|
|
894
|
+
const modules2 = buildModulesForFile(
|
|
895
|
+
source,
|
|
896
|
+
absPath,
|
|
897
|
+
load,
|
|
898
|
+
evalDep,
|
|
899
|
+
depth,
|
|
900
|
+
maxDepth,
|
|
901
|
+
(spec2, fromFile2, tried) => {
|
|
902
|
+
pushIssue(
|
|
903
|
+
"missing",
|
|
904
|
+
spec2,
|
|
905
|
+
`Module file not found for '${spec2}' (from ${moduleLabel(fromFile2)}); tried: ${tried.join(", ")}`
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
);
|
|
909
|
+
let exports;
|
|
910
|
+
try {
|
|
911
|
+
const file = parse3(source);
|
|
912
|
+
const { env } = evalProgramAbs(source, { file, modules: modules2 });
|
|
913
|
+
exports = collectAbsExports(file, env, modules2);
|
|
914
|
+
} catch {
|
|
915
|
+
exports = { named: {} };
|
|
916
|
+
} finally {
|
|
917
|
+
loading.pop();
|
|
918
|
+
}
|
|
919
|
+
cache.set(absPath, exports);
|
|
920
|
+
let fingerprint;
|
|
921
|
+
try {
|
|
922
|
+
const st = statSync2(absPath);
|
|
923
|
+
fingerprint = { mtimeMs: st.mtimeMs, size: st.size };
|
|
924
|
+
} catch {
|
|
925
|
+
}
|
|
926
|
+
if (fingerprint) {
|
|
927
|
+
absModuleCache.set(absPath, {
|
|
928
|
+
...fingerprint,
|
|
929
|
+
exports,
|
|
930
|
+
issues: issues.slice(issueStart)
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
return exports;
|
|
934
|
+
}
|
|
935
|
+
const modules = buildModulesForFile(
|
|
936
|
+
entrySource,
|
|
937
|
+
entryFile,
|
|
938
|
+
load,
|
|
939
|
+
evalDep,
|
|
940
|
+
0,
|
|
941
|
+
maxDepth,
|
|
942
|
+
(spec, fromFile, tried) => {
|
|
943
|
+
pushIssue(
|
|
944
|
+
"missing",
|
|
945
|
+
spec,
|
|
946
|
+
`Module file not found for '${spec}' (from ${moduleLabel(fromFile)}); tried: ${tried.join(", ")}`
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
);
|
|
950
|
+
return { modules, byPath: cache, issues };
|
|
951
|
+
}
|
|
952
|
+
function evalProgramAbsWithModules(source, entryFile, opts = {}) {
|
|
953
|
+
const { modules } = evalAbsModuleGraph(source, entryFile, opts);
|
|
954
|
+
return evalProgramAbs(source, {
|
|
955
|
+
file: opts.file,
|
|
956
|
+
modules,
|
|
957
|
+
seedVars: opts.seedVars,
|
|
958
|
+
seedFns: opts.seedFns
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
function collectAbsBindingsFromGraph(source, filePath, opts = {}) {
|
|
962
|
+
const out = /* @__PURE__ */ new Map();
|
|
963
|
+
try {
|
|
964
|
+
const { env } = evalProgramAbsWithModules(source, filePath, opts);
|
|
965
|
+
for (const [k, v] of env.vars) {
|
|
966
|
+
out.set(k, v);
|
|
967
|
+
}
|
|
968
|
+
for (const [name, impl] of env.fns) {
|
|
969
|
+
if (!out.has(name)) {
|
|
970
|
+
out.set(
|
|
971
|
+
name,
|
|
972
|
+
absFunction3(impl.params, { body: impl.body, async: impl.async, env })
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
} catch {
|
|
977
|
+
}
|
|
978
|
+
return out;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// src/bpath-run.ts
|
|
982
|
+
import {
|
|
983
|
+
runTranspiled,
|
|
984
|
+
callTranspiledExport,
|
|
985
|
+
callTranspiledExportFull,
|
|
986
|
+
setBCallCollector,
|
|
987
|
+
setMemberDiagCollector,
|
|
988
|
+
setAbsTruncationCollector,
|
|
989
|
+
typeValueToAbs as typeValueToAbs4,
|
|
990
|
+
createEnvironment,
|
|
991
|
+
stableAnalyzeKeySource,
|
|
992
|
+
formatAbs as formatAbs2,
|
|
993
|
+
hashSource,
|
|
994
|
+
getFnImpl as getFnImpl2
|
|
995
|
+
} from "@nudojs/core";
|
|
996
|
+
import { parse as parse4, extractInlineDirectives } from "@nudojs/parser";
|
|
997
|
+
import { loadEnvs } from "@nudojs/cli/evaluator";
|
|
998
|
+
|
|
999
|
+
// src/env-to-abs.ts
|
|
1000
|
+
import {
|
|
1001
|
+
typeValueToAbs as typeValueToAbs3,
|
|
1002
|
+
absToTypeValue,
|
|
1003
|
+
getFnSig as getFnSig2,
|
|
1004
|
+
absFunction as absFunction4,
|
|
1005
|
+
relationFn,
|
|
1006
|
+
objOf
|
|
1007
|
+
} from "@nudojs/core";
|
|
1008
|
+
function envValueToAbs(tv) {
|
|
1009
|
+
if (!tv) return typeValueToAbs3({ kind: "unknown" });
|
|
1010
|
+
if (tv.kind === "function") {
|
|
1011
|
+
const sig = getFnSig2(tv);
|
|
1012
|
+
if (sig) {
|
|
1013
|
+
const paramTypes = sig.paramTypes.map(typeValueToAbs3);
|
|
1014
|
+
const returnType = typeValueToAbs3(sig.returnType);
|
|
1015
|
+
if (sig.impl) {
|
|
1016
|
+
const params = tv.params?.length ? tv.params : sig.paramTypes.map((_, i) => `_arg${i}`);
|
|
1017
|
+
const dummyBody = {
|
|
1018
|
+
type: "BlockStatement",
|
|
1019
|
+
body: [],
|
|
1020
|
+
directives: []
|
|
1021
|
+
};
|
|
1022
|
+
const impl = sig.impl;
|
|
1023
|
+
return absFunction4(params, {
|
|
1024
|
+
body: dummyBody,
|
|
1025
|
+
apply: (args) => {
|
|
1026
|
+
const tvArgs = args.map((a) => absToTypeValue(a));
|
|
1027
|
+
try {
|
|
1028
|
+
const r = impl(tvArgs);
|
|
1029
|
+
if (!r) return returnType;
|
|
1030
|
+
return envValueToAbs(r);
|
|
1031
|
+
} catch {
|
|
1032
|
+
return returnType;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
return relationFn(paramTypes, returnType, {
|
|
1038
|
+
params: tv.params?.length ? tv.params : void 0
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
if (tv.kind === "object") {
|
|
1043
|
+
const slots = {};
|
|
1044
|
+
for (const [k, v] of Object.entries(tv.properties)) {
|
|
1045
|
+
slots[k] = { value: envValueToAbs(v) };
|
|
1046
|
+
}
|
|
1047
|
+
return objOf(slots);
|
|
1048
|
+
}
|
|
1049
|
+
return typeValueToAbs3(tv);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// src/analysis-file-cache.ts
|
|
1053
|
+
var analysisByFile = /* @__PURE__ */ new Map();
|
|
1054
|
+
var MAX_ANALYSIS_FILE_CACHE = 64;
|
|
1055
|
+
function clearAnalysisFileCache() {
|
|
1056
|
+
analysisByFile.clear();
|
|
1057
|
+
}
|
|
1058
|
+
function getAnalysisFileCacheSize() {
|
|
1059
|
+
return analysisByFile.size;
|
|
1060
|
+
}
|
|
1061
|
+
function analysisCacheGet(filePath, source, auxKey) {
|
|
1062
|
+
const e = analysisByFile.get(filePath);
|
|
1063
|
+
if (!e) return void 0;
|
|
1064
|
+
if (e.source !== source || e.auxKey !== auxKey) return void 0;
|
|
1065
|
+
analysisByFile.delete(filePath);
|
|
1066
|
+
analysisByFile.set(filePath, e);
|
|
1067
|
+
return e.value;
|
|
1068
|
+
}
|
|
1069
|
+
function analysisCacheSet(filePath, source, auxKey, value) {
|
|
1070
|
+
if (analysisByFile.size >= MAX_ANALYSIS_FILE_CACHE && !analysisByFile.has(filePath)) {
|
|
1071
|
+
const oldest = analysisByFile.keys().next().value;
|
|
1072
|
+
if (oldest !== void 0) analysisByFile.delete(oldest);
|
|
1073
|
+
}
|
|
1074
|
+
analysisByFile.set(filePath, { source, auxKey, value });
|
|
1075
|
+
}
|
|
1076
|
+
function evictAnalysisFileCacheForFiles(files) {
|
|
1077
|
+
let n = 0;
|
|
1078
|
+
for (const f of files) {
|
|
1079
|
+
if (analysisByFile.delete(f)) n++;
|
|
1080
|
+
}
|
|
1081
|
+
return n;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// src/fn-analysis-cache.ts
|
|
1085
|
+
var fnAnalysisCache = /* @__PURE__ */ new Map();
|
|
1086
|
+
var MAX_FN_ANALYSIS_CACHE = 1024;
|
|
1087
|
+
function clearFnAnalysisCache() {
|
|
1088
|
+
fnAnalysisCache.clear();
|
|
1089
|
+
}
|
|
1090
|
+
function fnAnalysisCacheGet(key) {
|
|
1091
|
+
const hit = fnAnalysisCache.get(key);
|
|
1092
|
+
if (hit !== void 0) {
|
|
1093
|
+
fnAnalysisCache.delete(key);
|
|
1094
|
+
fnAnalysisCache.set(key, hit);
|
|
1095
|
+
}
|
|
1096
|
+
return hit;
|
|
1097
|
+
}
|
|
1098
|
+
function fnAnalysisCacheSet(key, value) {
|
|
1099
|
+
if (fnAnalysisCache.size >= MAX_FN_ANALYSIS_CACHE && !fnAnalysisCache.has(key)) {
|
|
1100
|
+
const oldest = fnAnalysisCache.keys().next().value;
|
|
1101
|
+
if (oldest !== void 0) fnAnalysisCache.delete(oldest);
|
|
1102
|
+
}
|
|
1103
|
+
fnAnalysisCache.set(key, value);
|
|
1104
|
+
}
|
|
1105
|
+
function evictFnAnalysisCacheForFiles(files) {
|
|
1106
|
+
if (files.length === 0 || fnAnalysisCache.size === 0) return 0;
|
|
1107
|
+
const prefixes = files.map((f) => `${f}\0`);
|
|
1108
|
+
let n = 0;
|
|
1109
|
+
for (const key of [...fnAnalysisCache.keys()]) {
|
|
1110
|
+
if (prefixes.some((p) => key.startsWith(p))) {
|
|
1111
|
+
fnAnalysisCache.delete(key);
|
|
1112
|
+
n++;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
return n;
|
|
1116
|
+
}
|
|
1117
|
+
function caseDirectiveKey(directives, typeValueToString5) {
|
|
1118
|
+
return directives.map((d) => {
|
|
1119
|
+
if (d.kind === "case") {
|
|
1120
|
+
const args = (d.args ?? []).map((a) => typeValueToString5(a)).join(",");
|
|
1121
|
+
return `c:${d.name ?? ""}:${args}`;
|
|
1122
|
+
}
|
|
1123
|
+
return d.kind;
|
|
1124
|
+
}).join("|");
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// src/bpath-run.ts
|
|
1128
|
+
function absSeedPart(a) {
|
|
1129
|
+
const impl = getFnImpl2(a);
|
|
1130
|
+
if (impl?.fingerprint) return impl.fingerprint;
|
|
1131
|
+
if (a.shape.k === "fn") {
|
|
1132
|
+
const ret = impl?.env?.vars?.get("__nudo_mock_ret");
|
|
1133
|
+
if (ret) {
|
|
1134
|
+
try {
|
|
1135
|
+
return `ret=${formatAbs2(ret)}`;
|
|
1136
|
+
} catch {
|
|
1137
|
+
return "ret=?";
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
if (impl?.apply) return "apply";
|
|
1141
|
+
}
|
|
1142
|
+
try {
|
|
1143
|
+
return formatAbs2(a);
|
|
1144
|
+
} catch {
|
|
1145
|
+
return "?";
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
function seedFnPart(name, fn) {
|
|
1149
|
+
if (fn.fingerprint) return `${name}:${fn.fingerprint}`;
|
|
1150
|
+
const body = fn.body;
|
|
1151
|
+
const bodyKey = body && typeof body.start === "number" && typeof body.end === "number" ? `${body.start}:${body.end}:${body.type ?? ""}` : String(body?.type ?? "?");
|
|
1152
|
+
return `${name}(${fn.params.join(",")})@${bodyKey}`;
|
|
1153
|
+
}
|
|
1154
|
+
function mockSeedFingerprint(mocks, seedFns) {
|
|
1155
|
+
const parts = [];
|
|
1156
|
+
if (mocks) {
|
|
1157
|
+
const names = Object.keys(mocks).sort();
|
|
1158
|
+
for (const n of names) parts.push(`${n}=${absSeedPart(mocks[n])}`);
|
|
1159
|
+
}
|
|
1160
|
+
if (seedFns) {
|
|
1161
|
+
const names = Object.keys(seedFns).sort();
|
|
1162
|
+
for (const n of names) parts.push(`fn:${seedFnPart(n, seedFns[n])}`);
|
|
1163
|
+
}
|
|
1164
|
+
if (parts.length === 0) return "-";
|
|
1165
|
+
return hashSource(parts.join(";"));
|
|
1166
|
+
}
|
|
1167
|
+
function collectEnvGlobals(envNames) {
|
|
1168
|
+
if (envNames.length === 0) return {};
|
|
1169
|
+
const env = createEnvironment();
|
|
1170
|
+
try {
|
|
1171
|
+
loadEnvs(envNames, env);
|
|
1172
|
+
} catch {
|
|
1173
|
+
return {};
|
|
1174
|
+
}
|
|
1175
|
+
const out = {};
|
|
1176
|
+
for (const [k, v] of Object.entries(env.getOwnBindings())) {
|
|
1177
|
+
out[k] = envValueToAbs(v);
|
|
1178
|
+
}
|
|
1179
|
+
return out;
|
|
1180
|
+
}
|
|
1181
|
+
function collectEnvModules(envNames) {
|
|
1182
|
+
if (envNames.length === 0) return {};
|
|
1183
|
+
const env = createEnvironment();
|
|
1184
|
+
let mods = {};
|
|
1185
|
+
try {
|
|
1186
|
+
mods = loadEnvs(envNames, env).modules;
|
|
1187
|
+
} catch {
|
|
1188
|
+
return {};
|
|
1189
|
+
}
|
|
1190
|
+
const out = {};
|
|
1191
|
+
for (const [spec, exports] of Object.entries(mods)) {
|
|
1192
|
+
const named = {};
|
|
1193
|
+
for (const [k, v] of Object.entries(exports)) {
|
|
1194
|
+
named[k] = envValueToAbs(v);
|
|
1195
|
+
}
|
|
1196
|
+
out[spec] = { named };
|
|
1197
|
+
}
|
|
1198
|
+
return out;
|
|
1199
|
+
}
|
|
1200
|
+
function collectBPathReplacements(source) {
|
|
1201
|
+
const targets = [];
|
|
1202
|
+
const values = {};
|
|
1203
|
+
const asTargets = [];
|
|
1204
|
+
const asValues = {};
|
|
1205
|
+
let i = 0;
|
|
1206
|
+
try {
|
|
1207
|
+
const file = parse4(source);
|
|
1208
|
+
const visitStmts = (stmts) => {
|
|
1209
|
+
for (const stmt of stmts) {
|
|
1210
|
+
if (!stmt || typeof stmt !== "object") continue;
|
|
1211
|
+
const loc = stmt.loc;
|
|
1212
|
+
const dirs = extractInlineDirectives(stmt);
|
|
1213
|
+
for (const d of dirs) {
|
|
1214
|
+
if (d.kind === "replace") {
|
|
1215
|
+
const varName = `__rep${i++}`;
|
|
1216
|
+
targets.push({
|
|
1217
|
+
target: d.targetSource,
|
|
1218
|
+
varName,
|
|
1219
|
+
stmtStart: loc?.start.line,
|
|
1220
|
+
stmtEnd: loc?.end.line
|
|
1221
|
+
});
|
|
1222
|
+
values[varName] = typeValueToAbs4(d.typeExpr);
|
|
1223
|
+
} else if (d.kind === "as" && loc) {
|
|
1224
|
+
const varName = `__as${i++}`;
|
|
1225
|
+
asTargets.push({
|
|
1226
|
+
varName,
|
|
1227
|
+
stmtStart: loc.start.line,
|
|
1228
|
+
stmtEnd: loc.end.line
|
|
1229
|
+
});
|
|
1230
|
+
asValues[varName] = typeValueToAbs4(d.typeExpr);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
const s = stmt;
|
|
1234
|
+
if (s.type === "BlockStatement" && Array.isArray(s.body)) visitStmts(s.body);
|
|
1235
|
+
if (s.type === "FunctionDeclaration" || s.type === "FunctionExpression") {
|
|
1236
|
+
if (s.body) visitStmts([s.body]);
|
|
1237
|
+
}
|
|
1238
|
+
if (s.type === "ExportNamedDeclaration" && s.declaration) {
|
|
1239
|
+
visitStmts([s.declaration]);
|
|
1240
|
+
}
|
|
1241
|
+
if (s.type === "IfStatement") {
|
|
1242
|
+
if (s.consequent) visitStmts([s.consequent]);
|
|
1243
|
+
if (s.alternate) visitStmts([s.alternate]);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
};
|
|
1247
|
+
visitStmts(file.program.body);
|
|
1248
|
+
} catch {
|
|
1249
|
+
}
|
|
1250
|
+
return { targets, values, asTargets, asValues };
|
|
1251
|
+
}
|
|
1252
|
+
function isBPathCapable(source, envNames = []) {
|
|
1253
|
+
void envNames;
|
|
1254
|
+
if (/(^|[^.\w$])this\s*\./.test(source) && !/\bclass\s+/.test(source)) return false;
|
|
1255
|
+
return true;
|
|
1256
|
+
}
|
|
1257
|
+
var bRunByFile = /* @__PURE__ */ new Map();
|
|
1258
|
+
var MAX_B_RUN_CACHE = 32;
|
|
1259
|
+
function clearBPathCache() {
|
|
1260
|
+
bRunByFile.clear();
|
|
1261
|
+
clearAnalysisFileCache();
|
|
1262
|
+
clearFnAnalysisCache();
|
|
1263
|
+
}
|
|
1264
|
+
function evictBPathCacheForFiles(files) {
|
|
1265
|
+
let n = 0;
|
|
1266
|
+
for (const f of files) {
|
|
1267
|
+
if (bRunByFile.delete(f)) n++;
|
|
1268
|
+
}
|
|
1269
|
+
return n;
|
|
1270
|
+
}
|
|
1271
|
+
function bPathCacheSet(filePath, stableSource, mode, envKey, mockKey, value) {
|
|
1272
|
+
if (bRunByFile.size >= MAX_B_RUN_CACHE && !bRunByFile.has(filePath)) {
|
|
1273
|
+
const oldest = bRunByFile.keys().next().value;
|
|
1274
|
+
if (oldest !== void 0) bRunByFile.delete(oldest);
|
|
1275
|
+
}
|
|
1276
|
+
bRunByFile.set(filePath, { stableSource, mode, envKey, mockKey, value });
|
|
1277
|
+
}
|
|
1278
|
+
function tryRunBPath(source, filePath, opts = {}) {
|
|
1279
|
+
if (!isBPathCapable(source, opts.envNames ?? [])) return void 0;
|
|
1280
|
+
const mode = opts.mode ?? "analyze";
|
|
1281
|
+
const envKey = (opts.envNames ?? []).join(",");
|
|
1282
|
+
const mockKey = mockSeedFingerprint(opts.mocks);
|
|
1283
|
+
const stable = stableAnalyzeKeySource(source);
|
|
1284
|
+
const cached = bRunByFile.get(filePath);
|
|
1285
|
+
if (cached && cached.stableSource === stable && cached.mode === mode && cached.envKey === envKey && cached.mockKey === mockKey) {
|
|
1286
|
+
bRunByFile.delete(filePath);
|
|
1287
|
+
bRunByFile.set(filePath, cached);
|
|
1288
|
+
return cached.value ?? void 0;
|
|
1289
|
+
}
|
|
1290
|
+
let out = null;
|
|
1291
|
+
try {
|
|
1292
|
+
const memberDiags = [];
|
|
1293
|
+
const truncated = /* @__PURE__ */ new Set();
|
|
1294
|
+
const topCalls = [];
|
|
1295
|
+
setMemberDiagCollector((d) => memberDiags.push(d));
|
|
1296
|
+
setAbsTruncationCollector((label) => truncated.add(label));
|
|
1297
|
+
setBCallCollector((r) => topCalls.push(r));
|
|
1298
|
+
try {
|
|
1299
|
+
const { modules: graphMods, issues } = evalAbsModuleGraph(source, filePath);
|
|
1300
|
+
const envMods = collectEnvModules(opts.envNames ?? []);
|
|
1301
|
+
const modules = { ...envMods, ...graphMods };
|
|
1302
|
+
const { targets, values, asTargets, asValues } = collectBPathReplacements(source);
|
|
1303
|
+
const envGlobals = {
|
|
1304
|
+
...collectEnvGlobals(opts.envNames ?? []),
|
|
1305
|
+
...opts.mocks ?? {}
|
|
1306
|
+
};
|
|
1307
|
+
const exports = runTranspiled(source, {
|
|
1308
|
+
modules,
|
|
1309
|
+
maxLoopIters: opts.maxLoopIters,
|
|
1310
|
+
mode: opts.mode ?? "analyze",
|
|
1311
|
+
replacementTargets: targets.length ? targets : void 0,
|
|
1312
|
+
replacements: targets.length ? values : void 0,
|
|
1313
|
+
asOverrideTargets: asTargets.length ? asTargets : void 0,
|
|
1314
|
+
asOverrides: asTargets.length ? asValues : void 0,
|
|
1315
|
+
envGlobals: Object.keys(envGlobals).length ? envGlobals : void 0
|
|
1316
|
+
});
|
|
1317
|
+
out = {
|
|
1318
|
+
exports,
|
|
1319
|
+
modules,
|
|
1320
|
+
memberDiags: memberDiags.length ? memberDiags : void 0,
|
|
1321
|
+
moduleIssues: issues.length ? issues : void 0,
|
|
1322
|
+
truncatedFns: truncated.size ? [...truncated] : void 0,
|
|
1323
|
+
calls: topCalls.length ? topCalls : void 0
|
|
1324
|
+
};
|
|
1325
|
+
} finally {
|
|
1326
|
+
setMemberDiagCollector(null);
|
|
1327
|
+
setAbsTruncationCollector(null);
|
|
1328
|
+
setBCallCollector(null);
|
|
1329
|
+
}
|
|
1330
|
+
} catch {
|
|
1331
|
+
out = null;
|
|
1332
|
+
}
|
|
1333
|
+
bPathCacheSet(filePath, stable, mode, envKey, mockKey, out);
|
|
1334
|
+
return out ?? void 0;
|
|
1335
|
+
}
|
|
1336
|
+
function tryBPathCallFull(source, filePath, fnName, args, opts = {}) {
|
|
1337
|
+
const run = tryRunBPath(source, filePath, { envNames: opts.envNames, mocks: opts.mocks });
|
|
1338
|
+
if (!run) return void 0;
|
|
1339
|
+
if (!(fnName in run.exports)) return void 0;
|
|
1340
|
+
const collected = [];
|
|
1341
|
+
const memberDiags = [];
|
|
1342
|
+
if (opts.collectCalls) {
|
|
1343
|
+
setBCallCollector((r) => collected.push(r));
|
|
1344
|
+
}
|
|
1345
|
+
if (opts.collectMemberDiags ?? true) {
|
|
1346
|
+
setMemberDiagCollector((d) => memberDiags.push(d));
|
|
1347
|
+
}
|
|
1348
|
+
try {
|
|
1349
|
+
const full = callTranspiledExportFull(run.exports, fnName, args);
|
|
1350
|
+
const all = [...run.memberDiags ?? [], ...memberDiags];
|
|
1351
|
+
return {
|
|
1352
|
+
...full,
|
|
1353
|
+
calls: opts.collectCalls ? collected : void 0,
|
|
1354
|
+
memberDiags: all.length ? all : void 0,
|
|
1355
|
+
moduleIssues: run.moduleIssues,
|
|
1356
|
+
truncatedFns: run.truncatedFns
|
|
1357
|
+
};
|
|
1358
|
+
} finally {
|
|
1359
|
+
if (opts.collectCalls) setBCallCollector(null);
|
|
1360
|
+
setMemberDiagCollector(null);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
function tryBPathCall(source, filePath, fnName, args, opts = {}) {
|
|
1364
|
+
const full = tryBPathCallFull(source, filePath, fnName, args, opts);
|
|
1365
|
+
if (!full) return void 0;
|
|
1366
|
+
const r = full.result;
|
|
1367
|
+
if (!r) return void 0;
|
|
1368
|
+
if (r.shape.k === "never" && full.throws.shape.k !== "never") {
|
|
1369
|
+
return void 0;
|
|
1370
|
+
}
|
|
1371
|
+
if (r.shape.k === "unknown" && !r.term) return void 0;
|
|
1372
|
+
return r;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// src/bpath-diagnostics.ts
|
|
1376
|
+
import { parseSource } from "@nudojs/core";
|
|
1377
|
+
function locOf(node) {
|
|
1378
|
+
if (!node.loc) return null;
|
|
1379
|
+
return {
|
|
1380
|
+
start: { line: node.loc.start.line, column: node.loc.start.column },
|
|
1381
|
+
end: { line: node.loc.end.line, column: node.loc.end.column }
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
function isExit(stmt) {
|
|
1385
|
+
return stmt.type === "ReturnStatement" || stmt.type === "ThrowStatement";
|
|
1386
|
+
}
|
|
1387
|
+
function walkUnreachable(node, out) {
|
|
1388
|
+
const n = node;
|
|
1389
|
+
const checkBlock = (block) => {
|
|
1390
|
+
if (!block) return;
|
|
1391
|
+
const body = block.body ?? (Array.isArray(block) ? block : null);
|
|
1392
|
+
if (!Array.isArray(body)) return;
|
|
1393
|
+
for (let i = 0; i < body.length; i++) {
|
|
1394
|
+
walkUnreachable(body[i], out);
|
|
1395
|
+
if (isExit(body[i])) {
|
|
1396
|
+
for (let j = i + 1; j < body.length; j++) {
|
|
1397
|
+
const r = locOf(body[j]);
|
|
1398
|
+
if (r) out.push({ range: r });
|
|
1399
|
+
}
|
|
1400
|
+
break;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
};
|
|
1404
|
+
switch (n.type) {
|
|
1405
|
+
case "Program":
|
|
1406
|
+
case "BlockStatement":
|
|
1407
|
+
checkBlock(n.body);
|
|
1408
|
+
break;
|
|
1409
|
+
case "FunctionDeclaration":
|
|
1410
|
+
case "FunctionExpression":
|
|
1411
|
+
case "ArrowFunctionExpression":
|
|
1412
|
+
if (n.body) walkUnreachable(n.body, out);
|
|
1413
|
+
break;
|
|
1414
|
+
case "IfStatement":
|
|
1415
|
+
if (n.consequent) walkUnreachable(n.consequent, out);
|
|
1416
|
+
if (n.alternate) walkUnreachable(n.alternate, out);
|
|
1417
|
+
break;
|
|
1418
|
+
case "ForStatement":
|
|
1419
|
+
case "ForInStatement":
|
|
1420
|
+
case "ForOfStatement":
|
|
1421
|
+
case "WhileStatement":
|
|
1422
|
+
case "DoWhileStatement":
|
|
1423
|
+
if (n.body) walkUnreachable(n.body, out);
|
|
1424
|
+
break;
|
|
1425
|
+
case "TryStatement":
|
|
1426
|
+
if (n.block) walkUnreachable(n.block, out);
|
|
1427
|
+
if (n.handler?.body) walkUnreachable(n.handler.body, out);
|
|
1428
|
+
if (n.finalizer) walkUnreachable(n.finalizer, out);
|
|
1429
|
+
break;
|
|
1430
|
+
case "SwitchStatement":
|
|
1431
|
+
for (const c of n.cases ?? []) {
|
|
1432
|
+
checkBlock(c.consequent);
|
|
1433
|
+
}
|
|
1434
|
+
break;
|
|
1435
|
+
case "ExportNamedDeclaration":
|
|
1436
|
+
case "ExportDefaultDeclaration": {
|
|
1437
|
+
const decl = n.declaration;
|
|
1438
|
+
if (decl) walkUnreachable(decl, out);
|
|
1439
|
+
break;
|
|
1440
|
+
}
|
|
1441
|
+
default:
|
|
1442
|
+
break;
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
function collectDeclared(file) {
|
|
1446
|
+
const names = /* @__PURE__ */ new Set();
|
|
1447
|
+
const addId = (id) => {
|
|
1448
|
+
if (!id) return;
|
|
1449
|
+
if (id.type === "Identifier") names.add(id.name);
|
|
1450
|
+
if (id.type === "ObjectPattern") {
|
|
1451
|
+
for (const p of id.properties) {
|
|
1452
|
+
addId(p.value);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
if (id.type === "ArrayPattern") {
|
|
1456
|
+
for (const el of id.elements) addId(el);
|
|
1457
|
+
}
|
|
1458
|
+
};
|
|
1459
|
+
const visit = (node) => {
|
|
1460
|
+
if (!node || typeof node !== "object") return;
|
|
1461
|
+
const o = node;
|
|
1462
|
+
switch (o.type) {
|
|
1463
|
+
case "FunctionDeclaration":
|
|
1464
|
+
case "ClassDeclaration":
|
|
1465
|
+
addId(o.id);
|
|
1466
|
+
break;
|
|
1467
|
+
case "VariableDeclarator":
|
|
1468
|
+
addId(o.id);
|
|
1469
|
+
break;
|
|
1470
|
+
case "ImportSpecifier":
|
|
1471
|
+
case "ImportDefaultSpecifier":
|
|
1472
|
+
case "ImportNamespaceSpecifier":
|
|
1473
|
+
addId(o.local);
|
|
1474
|
+
break;
|
|
1475
|
+
default:
|
|
1476
|
+
break;
|
|
1477
|
+
}
|
|
1478
|
+
if (o.params && Array.isArray(o.params)) {
|
|
1479
|
+
for (const p of o.params) addId(p);
|
|
1480
|
+
}
|
|
1481
|
+
for (const key of Object.keys(o)) {
|
|
1482
|
+
if (key === "loc" || key === "start" || key === "end") continue;
|
|
1483
|
+
const v = o[key];
|
|
1484
|
+
if (Array.isArray(v)) v.forEach(visit);
|
|
1485
|
+
else if (v && typeof v === "object") visit(v);
|
|
1486
|
+
}
|
|
1487
|
+
};
|
|
1488
|
+
visit(file);
|
|
1489
|
+
return names;
|
|
1490
|
+
}
|
|
1491
|
+
var KNOWN_GLOBALS = /* @__PURE__ */ new Set([
|
|
1492
|
+
"Object",
|
|
1493
|
+
"Array",
|
|
1494
|
+
"String",
|
|
1495
|
+
"Number",
|
|
1496
|
+
"Boolean",
|
|
1497
|
+
"Symbol",
|
|
1498
|
+
"Math",
|
|
1499
|
+
"JSON",
|
|
1500
|
+
"Promise",
|
|
1501
|
+
"Error",
|
|
1502
|
+
"TypeError",
|
|
1503
|
+
"RangeError",
|
|
1504
|
+
"SyntaxError",
|
|
1505
|
+
"RegExp",
|
|
1506
|
+
"Date",
|
|
1507
|
+
"Map",
|
|
1508
|
+
"Set",
|
|
1509
|
+
"WeakMap",
|
|
1510
|
+
"WeakSet",
|
|
1511
|
+
"console",
|
|
1512
|
+
"undefined",
|
|
1513
|
+
"NaN",
|
|
1514
|
+
"Infinity",
|
|
1515
|
+
"globalThis",
|
|
1516
|
+
"parseInt",
|
|
1517
|
+
"parseFloat",
|
|
1518
|
+
"isNaN",
|
|
1519
|
+
"isFinite"
|
|
1520
|
+
]);
|
|
1521
|
+
function isFreeUnknown(name, declared, seen, known) {
|
|
1522
|
+
return !declared.has(name) && !known.has(name) && !name.startsWith("$") && !seen.has(name);
|
|
1523
|
+
}
|
|
1524
|
+
function walkBuiltinUnknown(node, declared, out, seen, known, parentKey) {
|
|
1525
|
+
const n = node;
|
|
1526
|
+
const flag = (idNode, name) => {
|
|
1527
|
+
if (!isFreeUnknown(name, declared, seen, known)) return;
|
|
1528
|
+
const range = locOf(idNode) ?? locOf(node);
|
|
1529
|
+
if (range) {
|
|
1530
|
+
out.push({ name, range });
|
|
1531
|
+
seen.add(name);
|
|
1532
|
+
}
|
|
1533
|
+
};
|
|
1534
|
+
if (n.type === "CallExpression" && n.callee?.type === "Identifier") {
|
|
1535
|
+
flag(n.callee, n.callee.name);
|
|
1536
|
+
} else if (n.type === "Identifier" && typeof n.name === "string" && parentKey !== "key" && parentKey !== "property" && parentKey !== "local" && parentKey !== "id") {
|
|
1537
|
+
flag(node, n.name);
|
|
1538
|
+
}
|
|
1539
|
+
for (const key of Object.keys(n)) {
|
|
1540
|
+
if (key === "loc" || key === "start" || key === "end") continue;
|
|
1541
|
+
const v = n[key];
|
|
1542
|
+
if (Array.isArray(v)) {
|
|
1543
|
+
v.forEach((x) => {
|
|
1544
|
+
if (x && typeof x === "object" && "type" in x) {
|
|
1545
|
+
walkBuiltinUnknown(x, declared, out, seen, known, key);
|
|
1546
|
+
}
|
|
1547
|
+
});
|
|
1548
|
+
} else if (v && typeof v === "object" && "type" in v) {
|
|
1549
|
+
walkBuiltinUnknown(v, declared, out, seen, known, key);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
function collectBPathDiagnostics(source, extraKnown) {
|
|
1554
|
+
let file;
|
|
1555
|
+
try {
|
|
1556
|
+
file = parseSource(source);
|
|
1557
|
+
} catch {
|
|
1558
|
+
return { unreachable: [], builtinUnknown: [] };
|
|
1559
|
+
}
|
|
1560
|
+
const unreachable = [];
|
|
1561
|
+
const root = file.program ?? file;
|
|
1562
|
+
walkUnreachable(root, unreachable);
|
|
1563
|
+
const declared = collectDeclared(file);
|
|
1564
|
+
const known = new Set(KNOWN_GLOBALS);
|
|
1565
|
+
if (extraKnown) {
|
|
1566
|
+
for (const n of extraKnown) known.add(n);
|
|
1567
|
+
}
|
|
1568
|
+
const builtinUnknown = [];
|
|
1569
|
+
walkBuiltinUnknown(file, declared, builtinUnknown, /* @__PURE__ */ new Set(), known);
|
|
1570
|
+
return { unreachable, builtinUnknown };
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
// src/analyzer.ts
|
|
1574
|
+
import { setAbsTruncationCollector as setAbsTruncationCollector2 } from "@nudojs/core";
|
|
44
1575
|
function resolveModule(source, fromDir) {
|
|
45
1576
|
const extensions = [".js", ".ts", ".mjs"];
|
|
46
1577
|
const nudoPath = resolveNpmNudo(source, fromDir);
|
|
47
1578
|
if (nudoPath) {
|
|
48
|
-
const src =
|
|
49
|
-
return { ast:
|
|
1579
|
+
const src = readFileSync4(nudoPath, "utf-8");
|
|
1580
|
+
return { ast: parse5(src), filePath: nudoPath };
|
|
50
1581
|
}
|
|
51
|
-
const basePath =
|
|
1582
|
+
const basePath = resolve4(fromDir, source);
|
|
52
1583
|
for (const ext of ["", ...extensions]) {
|
|
53
1584
|
const candidate = basePath + ext;
|
|
54
|
-
if (!
|
|
55
|
-
if (
|
|
1585
|
+
if (!existsSync3(candidate)) continue;
|
|
1586
|
+
if (statSync3(candidate).isDirectory()) {
|
|
56
1587
|
let entry = null;
|
|
57
|
-
const pkgPath =
|
|
58
|
-
if (
|
|
1588
|
+
const pkgPath = resolve4(candidate, "package.json");
|
|
1589
|
+
if (existsSync3(pkgPath)) {
|
|
59
1590
|
try {
|
|
60
|
-
const main = JSON.parse(
|
|
1591
|
+
const main = JSON.parse(readFileSync4(pkgPath, "utf-8")).main;
|
|
61
1592
|
if (typeof main === "string") {
|
|
62
1593
|
for (const e of ["", ...extensions]) {
|
|
63
|
-
const p =
|
|
64
|
-
if (
|
|
1594
|
+
const p = resolve4(candidate, main + e);
|
|
1595
|
+
if (existsSync3(p) && statSync3(p).isFile()) {
|
|
65
1596
|
entry = p;
|
|
66
1597
|
break;
|
|
67
1598
|
}
|
|
@@ -72,34 +1603,34 @@ function resolveModule(source, fromDir) {
|
|
|
72
1603
|
}
|
|
73
1604
|
if (!entry) {
|
|
74
1605
|
for (const e of ["", ...extensions]) {
|
|
75
|
-
const p =
|
|
76
|
-
if (
|
|
1606
|
+
const p = resolve4(candidate, "index" + e);
|
|
1607
|
+
if (existsSync3(p) && statSync3(p).isFile()) {
|
|
77
1608
|
entry = p;
|
|
78
1609
|
break;
|
|
79
1610
|
}
|
|
80
1611
|
}
|
|
81
1612
|
}
|
|
82
1613
|
if (!entry) return null;
|
|
83
|
-
const src2 =
|
|
84
|
-
return { ast:
|
|
1614
|
+
const src2 = readFileSync4(entry, "utf-8");
|
|
1615
|
+
return { ast: parse5(src2), filePath: entry };
|
|
85
1616
|
}
|
|
86
1617
|
if (candidate.endsWith(".json")) {
|
|
87
1618
|
try {
|
|
88
|
-
return { ast:
|
|
1619
|
+
return { ast: parse5("module.exports = undefined;"), filePath: candidate, json: JSON.parse(readFileSync4(candidate, "utf-8")) };
|
|
89
1620
|
} catch {
|
|
90
1621
|
return null;
|
|
91
1622
|
}
|
|
92
1623
|
}
|
|
93
|
-
const src =
|
|
94
|
-
return { ast:
|
|
1624
|
+
const src = readFileSync4(candidate, "utf-8");
|
|
1625
|
+
return { ast: parse5(src), filePath: candidate };
|
|
95
1626
|
}
|
|
96
1627
|
return null;
|
|
97
1628
|
}
|
|
98
1629
|
function resolveImportPath(specifier, fromDir) {
|
|
99
|
-
const basePath =
|
|
1630
|
+
const basePath = resolve4(fromDir, specifier);
|
|
100
1631
|
for (const ext of ["", ".js", ".ts", ".mjs"]) {
|
|
101
1632
|
const candidate = basePath + ext;
|
|
102
|
-
if (
|
|
1633
|
+
if (existsSync3(candidate)) return candidate;
|
|
103
1634
|
}
|
|
104
1635
|
return null;
|
|
105
1636
|
}
|
|
@@ -119,7 +1650,7 @@ function buildModuleGraph(files, cache) {
|
|
|
119
1650
|
if (cache) {
|
|
120
1651
|
let stat = null;
|
|
121
1652
|
try {
|
|
122
|
-
stat =
|
|
1653
|
+
stat = statSync3(file);
|
|
123
1654
|
} catch {
|
|
124
1655
|
}
|
|
125
1656
|
const cached = stat ? cache.get(file) : void 0;
|
|
@@ -140,7 +1671,7 @@ function extractImportEdges(file) {
|
|
|
140
1671
|
const edges = [];
|
|
141
1672
|
let ast;
|
|
142
1673
|
try {
|
|
143
|
-
ast =
|
|
1674
|
+
ast = parse5(readFileSync4(file, "utf-8"));
|
|
144
1675
|
} catch {
|
|
145
1676
|
return edges;
|
|
146
1677
|
}
|
|
@@ -148,7 +1679,7 @@ function extractImportEdges(file) {
|
|
|
148
1679
|
if (stmt.type !== "ImportDeclaration") continue;
|
|
149
1680
|
const specifier = stmt.source.value;
|
|
150
1681
|
if (!specifier.startsWith(".") && !specifier.startsWith("/")) continue;
|
|
151
|
-
const resolved = resolveImportPath(specifier,
|
|
1682
|
+
const resolved = resolveImportPath(specifier, dirname5(file));
|
|
152
1683
|
if (resolved) edges.push(resolved);
|
|
153
1684
|
}
|
|
154
1685
|
return edges;
|
|
@@ -201,7 +1732,7 @@ function applyMocks(directives, env, filePath, diagnostics) {
|
|
|
201
1732
|
for (const d of directives) {
|
|
202
1733
|
if (d.kind !== "mock") continue;
|
|
203
1734
|
if (d.arrowFn) {
|
|
204
|
-
const fnType =
|
|
1735
|
+
const fnType = T2.fn(d.arrowFn.params, d.arrowFn.body, env);
|
|
205
1736
|
fnType._paramPatterns = d.arrowFn.paramPatterns;
|
|
206
1737
|
env.bind(d.name, fnType);
|
|
207
1738
|
} else if (d.nudoMock) {
|
|
@@ -224,12 +1755,12 @@ function applyMocks(directives, env, filePath, diagnostics) {
|
|
|
224
1755
|
]
|
|
225
1756
|
});
|
|
226
1757
|
}
|
|
227
|
-
env.bind(d.name,
|
|
1758
|
+
env.bind(d.name, parseTypeValueExpr2(d.expression));
|
|
228
1759
|
} else if (d.fromPath) {
|
|
229
|
-
const mockPath =
|
|
230
|
-
const mockSource =
|
|
231
|
-
const mockAst =
|
|
232
|
-
const mockEnv =
|
|
1760
|
+
const mockPath = resolve4(dirname5(filePath), d.fromPath);
|
|
1761
|
+
const mockSource = readFileSync4(mockPath, "utf-8");
|
|
1762
|
+
const mockAst = parse5(mockSource);
|
|
1763
|
+
const mockEnv = createEnvironment2();
|
|
233
1764
|
evaluateProgram(mockAst, mockEnv);
|
|
234
1765
|
const mockVal = mockEnv.lookup(d.name);
|
|
235
1766
|
env.bind(d.name, mockVal);
|
|
@@ -239,19 +1770,19 @@ function applyMocks(directives, env, filePath, diagnostics) {
|
|
|
239
1770
|
function createSinonTypeValue(sinonExpr, env) {
|
|
240
1771
|
if (sinonExpr.type === "stub" || sinonExpr.type === "spy") {
|
|
241
1772
|
const body = { type: "BlockStatement", body: [] };
|
|
242
|
-
const fn =
|
|
1773
|
+
const fn = T2.fn(["...args"], body, env);
|
|
243
1774
|
if (sinonExpr.returnValue) {
|
|
244
1775
|
fn._directReturn = sinonExpr.returnValue;
|
|
245
1776
|
} else if (sinonExpr.resolvedValue) {
|
|
246
|
-
fn._directReturn =
|
|
1777
|
+
fn._directReturn = T2.promise(sinonExpr.resolvedValue);
|
|
247
1778
|
} else if (sinonExpr.rejectedValue) {
|
|
248
|
-
fn._directReturn =
|
|
1779
|
+
fn._directReturn = T2.never;
|
|
249
1780
|
} else {
|
|
250
|
-
fn._directReturn =
|
|
1781
|
+
fn._directReturn = T2.unknown;
|
|
251
1782
|
}
|
|
252
1783
|
return fn;
|
|
253
1784
|
}
|
|
254
|
-
return
|
|
1785
|
+
return T2.unknown;
|
|
255
1786
|
}
|
|
256
1787
|
function locFromNode(node) {
|
|
257
1788
|
return {
|
|
@@ -516,7 +2047,7 @@ function synthesizeExternalFunctions(records, currentFile) {
|
|
|
516
2047
|
(_, i) => (
|
|
517
2048
|
// 缺参按真实 JS 语义 widen 成 undefined 而非 unknown——可选参守卫
|
|
518
2049
|
// (target || [] 等)对 unknown 全塌,对 undefined 正常走默认分支
|
|
519
|
-
widenType(simplifyUnion(remaining.map((rec) => rec.argTypes[i] ??
|
|
2050
|
+
widenType(simplifyUnion(remaining.map((rec) => rec.argTypes[i] ?? T2.undefined)))
|
|
520
2051
|
)
|
|
521
2052
|
),
|
|
522
2053
|
result: collapseLiteralUnion(simplifyUnion(remaining.map((r) => r.resultType)), COLLAPSE_LITERAL_THRESHOLD),
|
|
@@ -548,7 +2079,7 @@ function receiverTypeToDisplay(tv) {
|
|
|
548
2079
|
case "union":
|
|
549
2080
|
return tv.members.map(receiverTypeToDisplay).join(" | ");
|
|
550
2081
|
default:
|
|
551
|
-
return
|
|
2082
|
+
return typeValueToString3(tv);
|
|
552
2083
|
}
|
|
553
2084
|
}
|
|
554
2085
|
function receiverIsConcrete(tv) {
|
|
@@ -645,34 +2176,34 @@ function unknownRecordsToDiagnostics(records) {
|
|
|
645
2176
|
return out;
|
|
646
2177
|
}
|
|
647
2178
|
function collectEnvNames(filePath, source, includeProject) {
|
|
648
|
-
const ast =
|
|
2179
|
+
const ast = parse5(source);
|
|
649
2180
|
const fileDirectives = extractFileDirectives(ast);
|
|
650
2181
|
const fileEnvNames = fileDirectives.filter((d) => d.kind === "env").flatMap((d) => d.envs);
|
|
651
2182
|
if (!includeProject) return fileEnvNames;
|
|
652
|
-
const projectConfig = findProjectConfig(
|
|
2183
|
+
const projectConfig = findProjectConfig(dirname5(filePath));
|
|
653
2184
|
const projectEnvNames = projectConfig?.config.env ?? [];
|
|
654
2185
|
return [.../* @__PURE__ */ new Set([...projectEnvNames, ...fileEnvNames])];
|
|
655
2186
|
}
|
|
656
2187
|
async function analyzeFileAsync(filePath, source, activeCases, externalCallRecords) {
|
|
657
2188
|
const envNames = collectEnvNames(filePath, source, true);
|
|
658
2189
|
if (envNames.length > 0) {
|
|
659
|
-
await preloadPathEnvs(envNames,
|
|
2190
|
+
await preloadPathEnvs(envNames, dirname5(filePath));
|
|
660
2191
|
}
|
|
661
2192
|
return analyzeFile(filePath, source, activeCases, externalCallRecords);
|
|
662
2193
|
}
|
|
663
2194
|
function collectCallRecords(filePath, source) {
|
|
664
2195
|
const records = [];
|
|
665
|
-
const ast =
|
|
2196
|
+
const ast = parse5(source, { errorRecovery: true });
|
|
666
2197
|
resetMemo();
|
|
667
2198
|
resetUnreachableRanges();
|
|
668
2199
|
resetEnvModules();
|
|
669
2200
|
resetMockModules();
|
|
670
2201
|
setModuleResolver(resolveModule);
|
|
671
|
-
setCurrentFileDir(
|
|
2202
|
+
setCurrentFileDir(dirname5(filePath));
|
|
672
2203
|
setCurrentSource(source);
|
|
673
2204
|
setCallCollector((record) => records.push(record));
|
|
674
2205
|
try {
|
|
675
|
-
const env =
|
|
2206
|
+
const env = createEnvironment2();
|
|
676
2207
|
evaluateProgram(ast, env);
|
|
677
2208
|
const runCallbacks = (statements) => {
|
|
678
2209
|
for (const stmt of statements) {
|
|
@@ -688,7 +2219,7 @@ function collectCallRecords(filePath, source) {
|
|
|
688
2219
|
if (!cb) continue;
|
|
689
2220
|
try {
|
|
690
2221
|
const params = cb.params ?? [];
|
|
691
|
-
evaluateFunction(cb, params.map(() =>
|
|
2222
|
+
evaluateFunction(cb, params.map(() => T2.unknown), env);
|
|
692
2223
|
} catch {
|
|
693
2224
|
}
|
|
694
2225
|
if (name === "describe" && cb.body.type === "BlockStatement") {
|
|
@@ -706,11 +2237,88 @@ function collectCallRecords(filePath, source) {
|
|
|
706
2237
|
return records;
|
|
707
2238
|
}
|
|
708
2239
|
var TEST_CALLBACK_NAMES = /* @__PURE__ */ new Set(["it", "test", "describe"]);
|
|
2240
|
+
var externalRecordIds = /* @__PURE__ */ new WeakMap();
|
|
2241
|
+
var nextExternalRecordId = 1;
|
|
2242
|
+
function analysisFileCacheKey(filePath, source, activeCases, externalCallRecords) {
|
|
2243
|
+
let cases = "-";
|
|
2244
|
+
if (activeCases && activeCases.size > 0) {
|
|
2245
|
+
cases = [...activeCases.entries()].sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0).map(([k, v]) => `${k}=${v}`).join(",");
|
|
2246
|
+
}
|
|
2247
|
+
let ext = "-";
|
|
2248
|
+
if (externalCallRecords && externalCallRecords.length > 0) {
|
|
2249
|
+
let id = externalRecordIds.get(externalCallRecords);
|
|
2250
|
+
if (id === void 0) {
|
|
2251
|
+
id = nextExternalRecordId++;
|
|
2252
|
+
externalRecordIds.set(externalCallRecords, id);
|
|
2253
|
+
}
|
|
2254
|
+
ext = `n${externalCallRecords.length}#id${id}`;
|
|
2255
|
+
}
|
|
2256
|
+
return {
|
|
2257
|
+
filePath,
|
|
2258
|
+
// 尾部无 @nudo 注释/空行不进键:comment-only 编辑命中 AnalysisResult
|
|
2259
|
+
source: stableAnalyzeKeySource2(source),
|
|
2260
|
+
auxKey: `${cases}\0${ext}`
|
|
2261
|
+
};
|
|
2262
|
+
}
|
|
2263
|
+
function cloneAnalysisResult(r) {
|
|
2264
|
+
return {
|
|
2265
|
+
functions: r.functions.map(cloneFunctionAnalysis),
|
|
2266
|
+
diagnostics: r.diagnostics.map((d) => ({ ...d })),
|
|
2267
|
+
bindings: new Map(r.bindings),
|
|
2268
|
+
// Node 键与 AST LRU 共享身份;Map 浅拷贝即可
|
|
2269
|
+
nodeTypeMap: new Map(r.nodeTypeMap),
|
|
2270
|
+
caseHints: r.caseHints.map((h) => ({ ...h })),
|
|
2271
|
+
...r.externalFunctions ? { externalFunctions: r.externalFunctions.map(cloneFunctionAnalysis) } : {}
|
|
2272
|
+
};
|
|
2273
|
+
}
|
|
2274
|
+
function cloneFunctionAnalysis(a) {
|
|
2275
|
+
return {
|
|
2276
|
+
...a,
|
|
2277
|
+
paramNames: [...a.paramNames],
|
|
2278
|
+
cases: a.cases.map((c) => ({
|
|
2279
|
+
...c,
|
|
2280
|
+
args: [...c.args],
|
|
2281
|
+
...c.intension ? { intension: { ...c.intension } } : {}
|
|
2282
|
+
})),
|
|
2283
|
+
loc: { start: { ...a.loc.start }, end: { ...a.loc.end } }
|
|
2284
|
+
};
|
|
2285
|
+
}
|
|
2286
|
+
function shiftSourceLoc(loc, lineDelta) {
|
|
2287
|
+
return {
|
|
2288
|
+
start: { line: loc.start.line + lineDelta, column: loc.start.column },
|
|
2289
|
+
end: { line: loc.end.line + lineDelta, column: loc.end.column }
|
|
2290
|
+
};
|
|
2291
|
+
}
|
|
2292
|
+
function shiftDiagnosticLines(d, lineDelta) {
|
|
2293
|
+
if (lineDelta === 0) return { ...d };
|
|
2294
|
+
const out = { ...d, range: shiftSourceLoc(d.range, lineDelta) };
|
|
2295
|
+
if (d.origin) {
|
|
2296
|
+
out.origin = { line: d.origin.line + lineDelta, column: d.origin.column };
|
|
2297
|
+
}
|
|
2298
|
+
return out;
|
|
2299
|
+
}
|
|
2300
|
+
function shiftCallRecordLines(r, lineDelta) {
|
|
2301
|
+
if (lineDelta === 0 || !r.callLoc) return { ...r };
|
|
2302
|
+
return {
|
|
2303
|
+
...r,
|
|
2304
|
+
callLoc: { line: r.callLoc.line + lineDelta, column: r.callLoc.column }
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
709
2307
|
function analyzeFile(filePath, source, activeCases, externalCallRecords) {
|
|
2308
|
+
const k = analysisFileCacheKey(filePath, source, activeCases, externalCallRecords);
|
|
2309
|
+
const hit = analysisCacheGet(k.filePath, k.source, k.auxKey);
|
|
2310
|
+
if (hit !== void 0) {
|
|
2311
|
+
return cloneAnalysisResult(hit);
|
|
2312
|
+
}
|
|
2313
|
+
const result = analyzeFileUncached(filePath, source, activeCases, externalCallRecords);
|
|
2314
|
+
analysisCacheSet(k.filePath, k.source, k.auxKey, result);
|
|
2315
|
+
return cloneAnalysisResult(result);
|
|
2316
|
+
}
|
|
2317
|
+
function analyzeFileUncached(filePath, source, activeCases, externalCallRecords) {
|
|
710
2318
|
for (const rec of externalCallRecords ?? []) {
|
|
711
2319
|
for (const a of rec.argTypes) setUsageSiteTag(a);
|
|
712
2320
|
}
|
|
713
|
-
const ast =
|
|
2321
|
+
const ast = parse5(source);
|
|
714
2322
|
const functions = extractDirectives(ast);
|
|
715
2323
|
const diagnostics = [];
|
|
716
2324
|
const bindings = /* @__PURE__ */ new Map();
|
|
@@ -722,14 +2330,15 @@ function analyzeFile(filePath, source, activeCases, externalCallRecords) {
|
|
|
722
2330
|
resetEnvModules();
|
|
723
2331
|
resetMockModules();
|
|
724
2332
|
setModuleResolver(resolveModule);
|
|
725
|
-
setCurrentFileDir(
|
|
2333
|
+
setCurrentFileDir(dirname5(filePath));
|
|
726
2334
|
setCurrentSource(source);
|
|
727
2335
|
const fileDirectives = extractFileDirectives(ast);
|
|
728
2336
|
const fileEnvNames = fileDirectives.filter((d) => d.kind === "env").flatMap((d) => d.envs);
|
|
729
|
-
const projectConfig = findProjectConfig(
|
|
2337
|
+
const projectConfig = findProjectConfig(dirname5(filePath));
|
|
730
2338
|
const projectEnvNames = projectConfig?.config.env ?? [];
|
|
731
2339
|
const envNames = [.../* @__PURE__ */ new Set([...projectEnvNames, ...fileEnvNames])];
|
|
732
2340
|
setUnknownBuiltinHandler((name, loc) => {
|
|
2341
|
+
if (isBPathCapable(source, envNames)) return;
|
|
733
2342
|
diagnostics.push({
|
|
734
2343
|
range: loc ? { start: loc.start, end: loc.end } : { start: { line: 0, column: 0 }, end: { line: 0, column: 0 } },
|
|
735
2344
|
severity: "warning",
|
|
@@ -737,19 +2346,21 @@ function analyzeFile(filePath, source, activeCases, externalCallRecords) {
|
|
|
737
2346
|
code: "nudo:builtin-unknown",
|
|
738
2347
|
suggestions: [
|
|
739
2348
|
`Use @nudo:mock to define the type: @nudo:mock ${name} = stub().returns(...)`,
|
|
740
|
-
`Or use @nudo:
|
|
2349
|
+
`Or use @nudo:refine return <constraint> to declare the return contract`
|
|
741
2350
|
]
|
|
742
2351
|
});
|
|
743
2352
|
});
|
|
744
|
-
const globalEnv =
|
|
745
|
-
|
|
746
|
-
const loaded =
|
|
747
|
-
|
|
2353
|
+
const globalEnv = createEnvironment2();
|
|
2354
|
+
{
|
|
2355
|
+
const loaded = envNames.length > 0 ? loadEnvs2(envNames, globalEnv) : { modules: {} };
|
|
2356
|
+
const auto = autoHarvestModules(source, dirname5(filePath));
|
|
2357
|
+
const modules = { ...loaded.modules, ...auto };
|
|
2358
|
+
if (Object.keys(modules).length > 0) setEnvModules(modules);
|
|
748
2359
|
}
|
|
749
2360
|
const mocks = /* @__PURE__ */ new Map();
|
|
750
2361
|
if (projectConfig?.config.mocks) {
|
|
751
2362
|
for (const [source2, mockPath] of Object.entries(projectConfig.config.mocks)) {
|
|
752
|
-
mocks.set(source2, { fromPath:
|
|
2363
|
+
mocks.set(source2, { fromPath: resolve4(projectConfig.projectDir, mockPath) });
|
|
753
2364
|
}
|
|
754
2365
|
}
|
|
755
2366
|
const mockModuleDirectives = fileDirectives.filter((d) => d.kind === "mock-module");
|
|
@@ -764,27 +2375,225 @@ function analyzeFile(filePath, source, activeCases, externalCallRecords) {
|
|
|
764
2375
|
const unknownRecords = [];
|
|
765
2376
|
setUnknownCollector((r) => unknownRecords.push(r));
|
|
766
2377
|
setProvenanceTracking(true);
|
|
2378
|
+
const bMemberDiagNames = /* @__PURE__ */ new Set();
|
|
2379
|
+
const bMemberDiagSeen = /* @__PURE__ */ new Set();
|
|
2380
|
+
const pushBMemberDiag = (d, fallbackLine) => {
|
|
2381
|
+
bMemberDiagNames.add(d.name);
|
|
2382
|
+
const key = `${d.kind}:${d.name}:${d.receiver}:${d.line ?? fallbackLine}:${d.column ?? 0}`;
|
|
2383
|
+
if (bMemberDiagSeen.has(key)) return;
|
|
2384
|
+
bMemberDiagSeen.add(key);
|
|
2385
|
+
if (d.receiver === "unknown") {
|
|
2386
|
+
diagnostics.push({
|
|
2387
|
+
range: {
|
|
2388
|
+
start: { line: d.line ?? fallbackLine, column: d.column ?? 0 },
|
|
2389
|
+
end: { line: d.line ?? fallbackLine, column: (d.column ?? 0) + d.name.length }
|
|
2390
|
+
},
|
|
2391
|
+
severity: "warning",
|
|
2392
|
+
message: `Cannot resolve '${d.name}' on unknown value`,
|
|
2393
|
+
code: "nudo:unknown-recv",
|
|
2394
|
+
...d.origin ? { origin: d.origin } : {}
|
|
2395
|
+
});
|
|
2396
|
+
return;
|
|
2397
|
+
}
|
|
2398
|
+
diagnostics.push({
|
|
2399
|
+
range: {
|
|
2400
|
+
start: { line: d.line ?? fallbackLine, column: d.column ?? 0 },
|
|
2401
|
+
end: { line: d.line ?? fallbackLine, column: (d.column ?? 0) + d.name.length }
|
|
2402
|
+
},
|
|
2403
|
+
severity: d.receiver === "number" || d.receiver === "boolean" || d.receiver === "bigint" || d.receiver === "symbol" ? "error" : "warning",
|
|
2404
|
+
message: d.kind === "method" ? `Method '${d.name}' does not exist on type '${d.receiver}'` : `Property '${d.name}' does not exist on type '${d.receiver}'`,
|
|
2405
|
+
code: "nudo:no-method",
|
|
2406
|
+
...d.origin ? { origin: d.origin } : {}
|
|
2407
|
+
});
|
|
2408
|
+
};
|
|
767
2409
|
for (const fn of functions) {
|
|
768
2410
|
applyMocks(fn.directives, globalEnv, filePath, diagnostics);
|
|
769
2411
|
}
|
|
770
|
-
|
|
771
|
-
const
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
2412
|
+
const selfContained = isSelfContainedSource(source, envNames);
|
|
2413
|
+
const canAbsModules = absModulesOk(source, envNames);
|
|
2414
|
+
const bCapable = isBPathCapable(source, envNames);
|
|
2415
|
+
const bModuleIssueKinds = /* @__PURE__ */ new Set();
|
|
2416
|
+
const bTruncatedFns = /* @__PURE__ */ new Set();
|
|
2417
|
+
const bBuiltinUnknownNames = /* @__PURE__ */ new Set();
|
|
2418
|
+
let bHostedEval = false;
|
|
2419
|
+
const pushBModuleIssues = (issues) => {
|
|
2420
|
+
if (!issues) return;
|
|
2421
|
+
for (const iss of issues) {
|
|
2422
|
+
bModuleIssueKinds.add(iss.kind);
|
|
2423
|
+
const code = iss.kind === "cycle" ? "nudo:module-cycle" : iss.kind === "depth" ? "nudo:module-depth" : "nudo:module-missing";
|
|
2424
|
+
diagnostics.push({
|
|
2425
|
+
range: { start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
2426
|
+
severity: iss.kind === "missing" ? "error" : "warning",
|
|
2427
|
+
message: iss.reason,
|
|
2428
|
+
code
|
|
2429
|
+
});
|
|
2430
|
+
}
|
|
2431
|
+
};
|
|
2432
|
+
const seeds = mockDirectivesToAbsSeeds(functions);
|
|
2433
|
+
let absCallRecords = [];
|
|
2434
|
+
let bTopCallRecords = [];
|
|
2435
|
+
let absGraphModules;
|
|
2436
|
+
let absBindsShared;
|
|
2437
|
+
let absNodesShared;
|
|
2438
|
+
if (bCapable && filePath) {
|
|
2439
|
+
try {
|
|
2440
|
+
const g = evalAbsModuleGraph(source, filePath, {
|
|
2441
|
+
seedVars: seeds.seedVars,
|
|
2442
|
+
seedFns: seeds.seedFns
|
|
2443
|
+
});
|
|
2444
|
+
absGraphModules = { ...collectEnvModules(envNames), ...g.modules };
|
|
2445
|
+
pushBModuleIssues(g.issues);
|
|
2446
|
+
} catch {
|
|
2447
|
+
}
|
|
2448
|
+
const bRun = tryRunBPath(source, filePath, {
|
|
2449
|
+
envNames,
|
|
2450
|
+
mocks: mockSeedsToAbsMocks(seeds)
|
|
780
2451
|
});
|
|
2452
|
+
if (bRun) {
|
|
2453
|
+
bHostedEval = true;
|
|
2454
|
+
if (bRun.memberDiags?.length) {
|
|
2455
|
+
for (const d of bRun.memberDiags) {
|
|
2456
|
+
pushBMemberDiag(d, 1);
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
if (bRun.truncatedFns) {
|
|
2460
|
+
for (const fn of bRun.truncatedFns) bTruncatedFns.add(fn);
|
|
2461
|
+
}
|
|
2462
|
+
if (bRun.calls?.length) {
|
|
2463
|
+
const impMap = buildAbsImportLocalMap(source, filePath);
|
|
2464
|
+
for (const c of bRun.calls) {
|
|
2465
|
+
const safeAbs = (a) => {
|
|
2466
|
+
if (!a || typeof a !== "object" || !("shape" in a) || !a.shape) {
|
|
2467
|
+
return T2.unknown;
|
|
2468
|
+
}
|
|
2469
|
+
try {
|
|
2470
|
+
return absToTypeValue2(a);
|
|
2471
|
+
} catch {
|
|
2472
|
+
return T2.unknown;
|
|
2473
|
+
}
|
|
2474
|
+
};
|
|
2475
|
+
const rec = {
|
|
2476
|
+
fnName: c.fnName,
|
|
2477
|
+
argTypes: c.args.map(safeAbs),
|
|
2478
|
+
resultType: c.threw ? T2.never : safeAbs(c.result),
|
|
2479
|
+
throws: c.threw ? safeAbs(c.result) : T2.never,
|
|
2480
|
+
callLoc: c.callLoc
|
|
2481
|
+
};
|
|
2482
|
+
const imp = impMap.get(c.fnName);
|
|
2483
|
+
if (imp) {
|
|
2484
|
+
rec.targetModule = imp.modulePath;
|
|
2485
|
+
rec.targetExport = imp.exportName;
|
|
2486
|
+
}
|
|
2487
|
+
bTopCallRecords.push(rec);
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
if (selfContained || canAbsModules) {
|
|
2493
|
+
setAbsTruncationCollector2((label) => bTruncatedFns.add(label));
|
|
2494
|
+
try {
|
|
2495
|
+
absCallRecords = collectAbsCallRecords(source, seeds, filePath, absGraphModules, envNames);
|
|
2496
|
+
} finally {
|
|
2497
|
+
setAbsTruncationCollector2(null);
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
if (bHostedEval) {
|
|
2501
|
+
try {
|
|
2502
|
+
const collected = collectAbsBindsAndNodes(source, seeds, absGraphModules);
|
|
2503
|
+
absBindsShared = collected.binds;
|
|
2504
|
+
absNodesShared = collected.nodes;
|
|
2505
|
+
for (const [name, absVal] of absBindsShared) {
|
|
2506
|
+
if (absVal?.shape?.k === "fn") continue;
|
|
2507
|
+
if (!globalEnv.has(name)) {
|
|
2508
|
+
globalEnv.bind(name, absToTypeValue2(absVal));
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
for (const [node, absVal] of absNodesShared) {
|
|
2512
|
+
nodeTypeMap.set(node, absToTypeValue2(absVal));
|
|
2513
|
+
}
|
|
2514
|
+
} catch {
|
|
2515
|
+
}
|
|
2516
|
+
} else {
|
|
2517
|
+
evaluateProgram(ast, globalEnv);
|
|
2518
|
+
}
|
|
2519
|
+
if ((selfContained || canAbsModules) && absCallRecords.length > 0) {
|
|
2520
|
+
callRecords.length = 0;
|
|
2521
|
+
callRecords.push(...absCallRecords);
|
|
2522
|
+
} else if (bTopCallRecords.length > 0) {
|
|
2523
|
+
callRecords.length = 0;
|
|
2524
|
+
callRecords.push(...bTopCallRecords);
|
|
2525
|
+
}
|
|
2526
|
+
const unreachableRanges = bCapable ? [] : getUnreachableRanges();
|
|
2527
|
+
if (bCapable) {
|
|
2528
|
+
const bKnownGlobals = /* @__PURE__ */ new Set([
|
|
2529
|
+
...Object.keys(collectEnvGlobals(envNames)),
|
|
2530
|
+
...Object.keys(mockSeedsToAbsMocks(seeds))
|
|
2531
|
+
]);
|
|
2532
|
+
const bDiag = collectBPathDiagnostics(source, bKnownGlobals);
|
|
2533
|
+
for (const ur of bDiag.unreachable) {
|
|
2534
|
+
diagnostics.push({
|
|
2535
|
+
range: ur.range,
|
|
2536
|
+
severity: "info",
|
|
2537
|
+
message: "Code after return/throw statement is unreachable",
|
|
2538
|
+
tags: ["unnecessary"],
|
|
2539
|
+
code: "nudo-unreachable",
|
|
2540
|
+
suggestions: ["Remove the unreachable code after the return/throw statement"]
|
|
2541
|
+
});
|
|
2542
|
+
}
|
|
2543
|
+
for (const b of bDiag.builtinUnknown) {
|
|
2544
|
+
bBuiltinUnknownNames.add(b.name);
|
|
2545
|
+
diagnostics.push({
|
|
2546
|
+
range: b.range,
|
|
2547
|
+
severity: "warning",
|
|
2548
|
+
message: `Built-in API "${b.name}" is not covered by Nudo's type inference`,
|
|
2549
|
+
code: "nudo:builtin-unknown",
|
|
2550
|
+
suggestions: [
|
|
2551
|
+
`Use @nudo:mock to define the type: @nudo:mock ${b.name} = stub().returns(...)`,
|
|
2552
|
+
`Or use @nudo:refine return <constraint> to declare the return contract`
|
|
2553
|
+
]
|
|
2554
|
+
});
|
|
2555
|
+
}
|
|
2556
|
+
} else {
|
|
2557
|
+
for (const ur of unreachableRanges) {
|
|
2558
|
+
diagnostics.push({
|
|
2559
|
+
range: ur,
|
|
2560
|
+
severity: "info",
|
|
2561
|
+
message: "Code after return/throw statement is unreachable",
|
|
2562
|
+
tags: ["unnecessary"],
|
|
2563
|
+
code: "nudo-unreachable",
|
|
2564
|
+
suggestions: ["Remove the unreachable code after the return/throw statement"]
|
|
2565
|
+
});
|
|
2566
|
+
}
|
|
781
2567
|
}
|
|
782
2568
|
collectBindings(ast, globalEnv, bindings);
|
|
2569
|
+
if (isBPathCapable(source, envNames)) {
|
|
2570
|
+
try {
|
|
2571
|
+
const absBinds = absBindsShared ?? collectAbsBindingsFromGraph(source, filePath, {
|
|
2572
|
+
seedVars: seeds.seedVars,
|
|
2573
|
+
seedFns: seeds.seedFns
|
|
2574
|
+
});
|
|
2575
|
+
for (const [name, absVal] of absBinds) {
|
|
2576
|
+
const prev = bindings.get(name);
|
|
2577
|
+
bindings.set(name, {
|
|
2578
|
+
type: absToTypeValue2(absVal),
|
|
2579
|
+
loc: prev?.loc
|
|
2580
|
+
});
|
|
2581
|
+
}
|
|
2582
|
+
} catch {
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
783
2585
|
const synthCandidates = [];
|
|
2586
|
+
let fnFpMap;
|
|
2587
|
+
try {
|
|
2588
|
+
fnFpMap = fnFingerprints(source, ast);
|
|
2589
|
+
} catch {
|
|
2590
|
+
fnFpMap = void 0;
|
|
2591
|
+
}
|
|
2592
|
+
const envKeyFn = envNames.join(",");
|
|
2593
|
+
const mockKeyFn = mockSeedFingerprint(seeds.seedVars, seeds.seedFns);
|
|
784
2594
|
for (const fn of functions) {
|
|
785
2595
|
const isPure = fn.directives.some((d) => d.kind === "pure");
|
|
786
2596
|
const skipDirective = fn.directives.find((d) => d.kind === "skip");
|
|
787
|
-
const returnsDirective = fn.directives.find((d) => d.kind === "returns");
|
|
788
2597
|
const fnLoc = locFromNode(fn.node);
|
|
789
2598
|
const paramNames = extractParamNames(fn.node);
|
|
790
2599
|
const analysis = { name: fn.name, loc: fnLoc, paramNames, cases: [] };
|
|
@@ -796,6 +2605,51 @@ function analyzeFile(filePath, source, activeCases, externalCallRecords) {
|
|
|
796
2605
|
functionResults.push(analysis);
|
|
797
2606
|
continue;
|
|
798
2607
|
}
|
|
2608
|
+
const caseDirectives = fn.directives.filter((d) => d.kind === "case");
|
|
2609
|
+
const activeCaseIdx = activeCases?.get(fn.name) ?? 0;
|
|
2610
|
+
const fp = fnFpMap?.get(fn.name);
|
|
2611
|
+
const fnCacheKey = fp && caseDirectives.length > 0 ? [
|
|
2612
|
+
filePath,
|
|
2613
|
+
fp.own,
|
|
2614
|
+
fp.deps,
|
|
2615
|
+
String(activeCaseIdx),
|
|
2616
|
+
caseDirectiveKey(caseDirectives, typeValueToString3),
|
|
2617
|
+
envKeyFn,
|
|
2618
|
+
mockKeyFn
|
|
2619
|
+
].join("\0") : void 0;
|
|
2620
|
+
const dLen0 = diagnostics.length;
|
|
2621
|
+
const hLen0 = caseHints.length;
|
|
2622
|
+
const cLen0 = callRecords.length;
|
|
2623
|
+
if (fnCacheKey) {
|
|
2624
|
+
const hitFn = fnAnalysisCacheGet(fnCacheKey);
|
|
2625
|
+
if (hitFn) {
|
|
2626
|
+
const cached = cloneFunctionAnalysis(hitFn.analysis);
|
|
2627
|
+
const lineDelta = fnLoc.start.line - cached.loc.start.line;
|
|
2628
|
+
cached.loc = fnLoc;
|
|
2629
|
+
if (lineDelta !== 0) {
|
|
2630
|
+
for (const c of cached.cases) {
|
|
2631
|
+
if (c.throwLoc) c.throwLoc = shiftSourceLoc(c.throwLoc, lineDelta);
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
if (lineDelta === 0) {
|
|
2635
|
+
diagnostics.push(...hitFn.diagnostics);
|
|
2636
|
+
caseHints.push(...hitFn.caseHints);
|
|
2637
|
+
callRecords.push(...hitFn.callRecords);
|
|
2638
|
+
} else {
|
|
2639
|
+
for (const d of hitFn.diagnostics) {
|
|
2640
|
+
diagnostics.push(shiftDiagnosticLines(d, lineDelta));
|
|
2641
|
+
}
|
|
2642
|
+
for (const h of hitFn.caseHints) {
|
|
2643
|
+
caseHints.push({ ...h, line: h.line + lineDelta });
|
|
2644
|
+
}
|
|
2645
|
+
for (const r of hitFn.callRecords) {
|
|
2646
|
+
callRecords.push(shiftCallRecordLines(r, lineDelta));
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
functionResults.push(cached);
|
|
2650
|
+
continue;
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
799
2653
|
if (isPure) {
|
|
800
2654
|
const fnVal = globalEnv.has(fn.name) ? globalEnv.lookup(fn.name) : null;
|
|
801
2655
|
if (fnVal && fnVal.kind === "function") {
|
|
@@ -808,37 +2662,114 @@ function analyzeFile(filePath, source, activeCases, externalCallRecords) {
|
|
|
808
2662
|
} else {
|
|
809
2663
|
setSampleCount(3);
|
|
810
2664
|
}
|
|
811
|
-
const caseDirectives = fn.directives.filter((d) => d.kind === "case");
|
|
812
|
-
const activeCaseIdx = activeCases?.get(fn.name) ?? 0;
|
|
813
2665
|
if (caseDirectives.length === 0) {
|
|
814
2666
|
synthCandidates.push({ name: fn.name, node: fn.node, analysis });
|
|
815
2667
|
}
|
|
816
2668
|
for (let ci = 0; ci < caseDirectives.length; ci++) {
|
|
817
2669
|
const directive = caseDirectives[ci];
|
|
818
2670
|
resetUnreachableRanges();
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
2671
|
+
let caseValue;
|
|
2672
|
+
let caseAbs;
|
|
2673
|
+
let caseThrows;
|
|
2674
|
+
let fullResult = null;
|
|
2675
|
+
let caseUnreachable = [];
|
|
2676
|
+
const bCapable2 = isBPathCapable(source, envNames);
|
|
2677
|
+
const bPrimary = bCapable2;
|
|
2678
|
+
if (bCapable2 && filePath) {
|
|
2679
|
+
const bFull = tryBPathCallFull(
|
|
2680
|
+
source,
|
|
2681
|
+
filePath,
|
|
2682
|
+
fn.name,
|
|
2683
|
+
directive.args.map((a) => typeValueToAbs5(a)),
|
|
2684
|
+
{ collectCalls: true, envNames, mocks: mockSeedsToAbsMocks(seeds) }
|
|
2685
|
+
);
|
|
2686
|
+
const res = bFull?.result;
|
|
2687
|
+
const weakUnknown = !!res && res.shape.k === "unknown" && (!res.term || res.term.op === "lit" && res.term.value === void 0);
|
|
2688
|
+
const bOk = !!res && (bHostedEval || !weakUnknown && res.conf !== "opaque");
|
|
2689
|
+
if (bOk && bFull && bPrimary) {
|
|
2690
|
+
caseAbs = bFull.result;
|
|
2691
|
+
caseValue = absToTypeValue2(bFull.result);
|
|
2692
|
+
caseThrows = bFull.throws.shape.k !== "never" ? absToTypeValue2(bFull.throws) : T2.never;
|
|
2693
|
+
fullResult = { value: caseValue, throws: caseThrows };
|
|
2694
|
+
for (const d of bFull.memberDiags ?? []) {
|
|
2695
|
+
pushBMemberDiag(d, fnLoc.start.line);
|
|
2696
|
+
}
|
|
2697
|
+
if (bFull.calls?.length) {
|
|
2698
|
+
const impMap = buildAbsImportLocalMap(source, filePath);
|
|
2699
|
+
const safeAbs = (a) => {
|
|
2700
|
+
if (!a || typeof a !== "object" || !("shape" in a) || !a.shape) {
|
|
2701
|
+
return T2.unknown;
|
|
2702
|
+
}
|
|
2703
|
+
try {
|
|
2704
|
+
return absToTypeValue2(a);
|
|
2705
|
+
} catch {
|
|
2706
|
+
return T2.unknown;
|
|
2707
|
+
}
|
|
2708
|
+
};
|
|
2709
|
+
for (const c of bFull.calls) {
|
|
2710
|
+
const rec = {
|
|
2711
|
+
fnName: c.fnName,
|
|
2712
|
+
argTypes: c.args.map(safeAbs),
|
|
2713
|
+
resultType: c.threw ? T2.never : safeAbs(c.result),
|
|
2714
|
+
throws: c.threw ? safeAbs(c.result) : T2.never,
|
|
2715
|
+
callLoc: c.callLoc
|
|
2716
|
+
};
|
|
2717
|
+
const imp = impMap.get(c.fnName);
|
|
2718
|
+
if (imp) {
|
|
2719
|
+
rec.targetModule = imp.modulePath;
|
|
2720
|
+
rec.targetExport = imp.exportName;
|
|
2721
|
+
}
|
|
2722
|
+
callRecords.push(rec);
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
if (!fullResult) {
|
|
2728
|
+
if (bHostedEval) {
|
|
2729
|
+
fullResult = { value: T2.unknown, throws: T2.never };
|
|
2730
|
+
caseValue = T2.unknown;
|
|
2731
|
+
} else {
|
|
2732
|
+
fullResult = evaluateFunctionFull(fn.node, directive.args, globalEnv);
|
|
2733
|
+
caseUnreachable = [...getUnreachableRanges()];
|
|
2734
|
+
caseValue = fullResult.value;
|
|
2735
|
+
if ((selfContained || canAbsModules) && fullResult.value.kind !== "never") {
|
|
2736
|
+
caseAbs = tryEvalAbsRaw(source, fn.name, directive.args, filePath, mockSeedsToAbsMocks(seeds));
|
|
2737
|
+
if (caseAbs) {
|
|
2738
|
+
const projected = absToTypeValue2(caseAbs);
|
|
2739
|
+
if (absIsBetter(projected, fullResult.value)) caseValue = projected;
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
const tv = caseValue;
|
|
2745
|
+
const throwsTv = caseThrows ?? fullResult.throws;
|
|
2746
|
+
const caseEntry = {
|
|
822
2747
|
name: directive.name,
|
|
823
2748
|
args: directive.args,
|
|
824
|
-
result:
|
|
825
|
-
throws:
|
|
826
|
-
throwLoc: fullResult.throwLoc
|
|
827
|
-
|
|
2749
|
+
result: tv,
|
|
2750
|
+
throws: throwsTv,
|
|
2751
|
+
throwLoc: fullResult.throwLoc,
|
|
2752
|
+
expected: directive.expected,
|
|
2753
|
+
source: "directive"
|
|
2754
|
+
};
|
|
2755
|
+
tryAttachIntension(caseEntry, source, fn.name);
|
|
2756
|
+
if (caseAbs) attachAbsToIntension(caseEntry, caseAbs, fn.name);
|
|
2757
|
+
analysis.cases.push(caseEntry);
|
|
828
2758
|
if (directive.commentLine) {
|
|
829
|
-
const hasThrow =
|
|
830
|
-
const resultStr =
|
|
831
|
-
const throwStr = hasThrow ? `throws ${
|
|
2759
|
+
const hasThrow = throwsTv.kind !== "never";
|
|
2760
|
+
const resultStr = tv.kind !== "never" ? typeValueToString3(tv) : "";
|
|
2761
|
+
const throwStr = hasThrow ? `throws ${typeValueToString3(throwsTv)}` : "";
|
|
832
2762
|
const label = [resultStr, throwStr].filter(Boolean).join(" ");
|
|
833
2763
|
const hintLabel = `=> ${label}`;
|
|
834
2764
|
let ok = true;
|
|
835
2765
|
if (directive.expected) {
|
|
836
|
-
ok = isSubtypeOf(
|
|
2766
|
+
ok = isSubtypeOf(tv, directive.expected);
|
|
837
2767
|
if (!ok) {
|
|
838
2768
|
diagnostics.push({
|
|
839
2769
|
range: { start: { line: directive.commentLine, column: 0 }, end: { line: directive.commentLine, column: 999 } },
|
|
840
2770
|
severity: "error",
|
|
841
|
-
message: `Case "${directive.name}": expected ${
|
|
2771
|
+
message: `Case "${directive.name}": expected ${typeValueToString3(directive.expected)}, got ${typeValueToString3(tv)}. The inferred return type does not match the expected type declared in the @nudo:case directive`,
|
|
2772
|
+
code: "nudo:case-expected"
|
|
842
2773
|
});
|
|
843
2774
|
}
|
|
844
2775
|
}
|
|
@@ -846,24 +2777,26 @@ function analyzeFile(filePath, source, activeCases, externalCallRecords) {
|
|
|
846
2777
|
}
|
|
847
2778
|
const isActive = ci === Math.min(activeCaseIdx, caseDirectives.length - 1);
|
|
848
2779
|
if (isActive) {
|
|
849
|
-
if (
|
|
850
|
-
const throwRange = fullResult
|
|
2780
|
+
if (throwsTv.kind !== "never") {
|
|
2781
|
+
const throwRange = fullResult?.throwLoc ?? fnLoc;
|
|
851
2782
|
diagnostics.push({
|
|
852
2783
|
range: throwRange,
|
|
853
2784
|
severity: "warning",
|
|
854
|
-
message: `Function "${fn.name}" case "${directive.name}" may throw: ${
|
|
2785
|
+
message: `Function "${fn.name}" case "${directive.name}" may throw: ${typeValueToString3(throwsTv)}. Consider adding a try-catch block or using @nudo:refine return <constraint>`,
|
|
855
2786
|
code: "nudo-may-throw"
|
|
856
2787
|
});
|
|
857
2788
|
}
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
2789
|
+
if (!isBPathCapable(source, envNames)) {
|
|
2790
|
+
for (const ur of caseUnreachable) {
|
|
2791
|
+
diagnostics.push({
|
|
2792
|
+
range: ur,
|
|
2793
|
+
severity: "info",
|
|
2794
|
+
message: "Code after return/throw statement is unreachable",
|
|
2795
|
+
tags: ["unnecessary"],
|
|
2796
|
+
code: "nudo-unreachable",
|
|
2797
|
+
suggestions: ["Remove the unreachable code after the return/throw statement"]
|
|
2798
|
+
});
|
|
2799
|
+
}
|
|
867
2800
|
}
|
|
868
2801
|
}
|
|
869
2802
|
}
|
|
@@ -872,26 +2805,13 @@ function analyzeFile(filePath, source, activeCases, externalCallRecords) {
|
|
|
872
2805
|
} else if (analysis.cases.length === 1) {
|
|
873
2806
|
analysis.combined = analysis.cases[0].result;
|
|
874
2807
|
}
|
|
875
|
-
if (
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
analysis.assertionErrors.push(msg);
|
|
883
|
-
diagnostics.push({
|
|
884
|
-
range: fnLoc,
|
|
885
|
-
severity: "error",
|
|
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
|
-
]
|
|
892
|
-
});
|
|
893
|
-
}
|
|
894
|
-
}
|
|
2808
|
+
if (fnCacheKey) {
|
|
2809
|
+
fnAnalysisCacheSet(fnCacheKey, {
|
|
2810
|
+
analysis: cloneFunctionAnalysis(analysis),
|
|
2811
|
+
diagnostics: diagnostics.slice(dLen0),
|
|
2812
|
+
caseHints: caseHints.slice(hLen0),
|
|
2813
|
+
callRecords: callRecords.slice(cLen0)
|
|
2814
|
+
});
|
|
895
2815
|
}
|
|
896
2816
|
functionResults.push(analysis);
|
|
897
2817
|
}
|
|
@@ -918,7 +2838,7 @@ function analyzeFile(filePath, source, activeCases, externalCallRecords) {
|
|
|
918
2838
|
}
|
|
919
2839
|
return n;
|
|
920
2840
|
};
|
|
921
|
-
const currentModulePath = normalizeModulePath(
|
|
2841
|
+
const currentModulePath = normalizeModulePath(resolve4(filePath));
|
|
922
2842
|
const singleExportFn = findSingleModuleExportsFunction(ast);
|
|
923
2843
|
for (const candidate of synthCandidates) {
|
|
924
2844
|
const singleExportHit = singleExportFn !== null && candidate.node === singleExportFn;
|
|
@@ -948,13 +2868,25 @@ function analyzeFile(filePath, source, activeCases, externalCallRecords) {
|
|
|
948
2868
|
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
2869
|
const precise = ordered.slice(0, MAX_PRECISE_CALLSITE_CASES);
|
|
950
2870
|
for (const rec of precise) {
|
|
951
|
-
|
|
2871
|
+
let absRaw;
|
|
2872
|
+
let absResult;
|
|
2873
|
+
if (rec.resultType.kind !== "never" && rec.argTypes.length > 0) {
|
|
2874
|
+
absRaw = tryEvalAbsRaw(source, candidate.name, rec.argTypes, filePath, mockSeedsToAbsMocks(seeds));
|
|
2875
|
+
if (absRaw) {
|
|
2876
|
+
const projected = absToTypeValue2(absRaw);
|
|
2877
|
+
if (absIsBetter(projected, rec.resultType)) absResult = projected;
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
const caseResult2 = {
|
|
952
2881
|
name: `call@L${rec.callLoc?.line ?? candidate.analysis.loc.start.line}`,
|
|
953
2882
|
args: rec.argTypes,
|
|
954
|
-
result: rec.resultType,
|
|
2883
|
+
result: absResult ?? rec.resultType,
|
|
955
2884
|
throws: rec.throws,
|
|
956
2885
|
source: "callsite"
|
|
957
|
-
}
|
|
2886
|
+
};
|
|
2887
|
+
tryAttachIntension(caseResult2, source, candidate.name);
|
|
2888
|
+
if (absRaw) attachAbsToIntension(caseResult2, absRaw, candidate.name);
|
|
2889
|
+
candidate.analysis.cases.push(caseResult2);
|
|
958
2890
|
}
|
|
959
2891
|
const remaining = ordered.slice(MAX_PRECISE_CALLSITE_CASES).filter((rec) => !rec.argTypes.some((a) => a.kind === "unknown"));
|
|
960
2892
|
if (remaining.length > 0) {
|
|
@@ -965,19 +2897,50 @@ function analyzeFile(filePath, source, activeCases, externalCallRecords) {
|
|
|
965
2897
|
(_, i) => (
|
|
966
2898
|
// 缺参按真实 JS 语义 widen 成 undefined 而非 unknown——可选参守卫
|
|
967
2899
|
// (target || [] 等)对 unknown 全塌,对 undefined 正常走默认分支
|
|
968
|
-
widenType(simplifyUnion(remaining.map((rec) => rec.argTypes[i] ??
|
|
2900
|
+
widenType(simplifyUnion(remaining.map((rec) => rec.argTypes[i] ?? T2.undefined)))
|
|
969
2901
|
)
|
|
970
2902
|
);
|
|
971
|
-
const
|
|
972
|
-
|
|
2903
|
+
const absSymRaw = tryEvalAbs(source, candidate.name, widenedArgs, filePath, mockSeedsToAbsMocks(seeds));
|
|
2904
|
+
const absSym = absSymRaw && absIsBetter(
|
|
2905
|
+
absSymRaw,
|
|
2906
|
+
/* 无先验:仅 unknown 时 */
|
|
2907
|
+
{ kind: "unknown" }
|
|
2908
|
+
) ? absSymRaw : void 0;
|
|
2909
|
+
let symValue;
|
|
2910
|
+
let symThrows = T2.never;
|
|
2911
|
+
let symLoc;
|
|
2912
|
+
let symAbs;
|
|
2913
|
+
if (isBPathCapable(source, envNames) && filePath) {
|
|
2914
|
+
const bSym = tryBPathCall(
|
|
2915
|
+
source,
|
|
2916
|
+
filePath,
|
|
2917
|
+
candidate.name,
|
|
2918
|
+
widenedArgs.map((a) => typeValueToAbs5(a)),
|
|
2919
|
+
{ envNames, mocks: mockSeedsToAbsMocks(seeds) }
|
|
2920
|
+
);
|
|
2921
|
+
if (bSym && (bHostedEval || !(bSym.shape.k === "unknown" && !bSym.term))) {
|
|
2922
|
+
symAbs = bSym;
|
|
2923
|
+
symValue = absToTypeValue2(bSym);
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
if (!symAbs && !bHostedEval) {
|
|
2927
|
+
const full = evaluateFunctionFull(fnNode2, widenedArgs, globalEnv);
|
|
2928
|
+
symValue = full.value.kind === "unknown" && absSym ? absSym : full.value;
|
|
2929
|
+
symThrows = full.throws;
|
|
2930
|
+
symLoc = full.throwLoc;
|
|
2931
|
+
}
|
|
2932
|
+
const symCase = {
|
|
973
2933
|
name: "call@symbolic",
|
|
974
2934
|
args: widenedArgs,
|
|
975
|
-
result:
|
|
976
|
-
throws:
|
|
977
|
-
throwLoc:
|
|
2935
|
+
result: symValue ?? T2.unknown,
|
|
2936
|
+
throws: symThrows,
|
|
2937
|
+
throwLoc: symLoc,
|
|
978
2938
|
source: "callsite",
|
|
979
2939
|
aggregatedFrom: remaining.length
|
|
980
|
-
}
|
|
2940
|
+
};
|
|
2941
|
+
tryAttachIntension(symCase, source, candidate.name);
|
|
2942
|
+
if (symAbs) attachAbsToIntension(symCase, symAbs, candidate.name);
|
|
2943
|
+
candidate.analysis.cases.push(symCase);
|
|
981
2944
|
}
|
|
982
2945
|
candidate.analysis.combined = collapseLiteralUnion(
|
|
983
2946
|
simplifyUnion(records.map((r) => r.resultType)),
|
|
@@ -985,20 +2948,59 @@ function analyzeFile(filePath, source, activeCases, externalCallRecords) {
|
|
|
985
2948
|
);
|
|
986
2949
|
continue;
|
|
987
2950
|
}
|
|
988
|
-
const fnNode = resolveFunctionNode(candidate.node);
|
|
989
|
-
const args = extractParamNames(fnNode).map(() =>
|
|
990
|
-
|
|
991
|
-
|
|
2951
|
+
const fnNode = resolveFunctionNode(candidate.node);
|
|
2952
|
+
const args = extractParamNames(fnNode).map(() => T2.unknown);
|
|
2953
|
+
let entryValue;
|
|
2954
|
+
let entryThrows = T2.never;
|
|
2955
|
+
let entryLoc;
|
|
2956
|
+
let entryAbs;
|
|
2957
|
+
if (isBPathCapable(source, envNames) && filePath) {
|
|
2958
|
+
const bEntryFull = tryBPathCallFull(
|
|
2959
|
+
source,
|
|
2960
|
+
filePath,
|
|
2961
|
+
candidate.analysis.name,
|
|
2962
|
+
args.map((a) => typeValueToAbs5(a)),
|
|
2963
|
+
{ envNames, mocks: mockSeedsToAbsMocks(seeds), collectMemberDiags: true }
|
|
2964
|
+
);
|
|
2965
|
+
if (bEntryFull?.memberDiags?.length) {
|
|
2966
|
+
for (const d of bEntryFull.memberDiags) {
|
|
2967
|
+
pushBMemberDiag(d, candidate.analysis.loc.start.line);
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
const bEntry = bEntryFull?.result;
|
|
2971
|
+
if (bEntry && (bHostedEval || !(bEntry.shape.k === "unknown" && !bEntry.term))) {
|
|
2972
|
+
entryAbs = bEntry;
|
|
2973
|
+
entryValue = absToTypeValue2(bEntry);
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
if (!entryValue) {
|
|
2977
|
+
if (bHostedEval) {
|
|
2978
|
+
entryValue = T2.unknown;
|
|
2979
|
+
entryThrows = T2.never;
|
|
2980
|
+
} else {
|
|
2981
|
+
const absEntry = tryEvalEntryAbs(source, candidate.analysis.name, args, filePath, seeds.seedVars);
|
|
2982
|
+
const full = absEntry ? { value: absEntry, throws: T2.never, throwLoc: void 0 } : evaluateFunctionFull(fnNode, args, globalEnv);
|
|
2983
|
+
entryValue = full.value;
|
|
2984
|
+
entryThrows = full.throws;
|
|
2985
|
+
entryLoc = full.throwLoc;
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
const caseResult = {
|
|
992
2989
|
name: `entry@L${candidate.analysis.loc.start.line}`,
|
|
993
2990
|
args,
|
|
994
|
-
result:
|
|
995
|
-
throws:
|
|
996
|
-
throwLoc:
|
|
997
|
-
}
|
|
2991
|
+
result: entryValue,
|
|
2992
|
+
throws: entryThrows,
|
|
2993
|
+
throwLoc: entryLoc
|
|
2994
|
+
};
|
|
2995
|
+
tryAttachIntension(caseResult, source, candidate.analysis.name);
|
|
2996
|
+
if (entryAbs) attachAbsToIntension(caseResult, entryAbs, candidate.analysis.name);
|
|
2997
|
+
candidate.analysis.cases.push(caseResult);
|
|
998
2998
|
candidate.analysis.entryOnly = true;
|
|
999
|
-
candidate.analysis.combined = collapseLiteralUnion(
|
|
2999
|
+
candidate.analysis.combined = collapseLiteralUnion(entryValue, COLLAPSE_LITERAL_THRESHOLD);
|
|
3000
|
+
}
|
|
3001
|
+
if (!bHostedEval) {
|
|
3002
|
+
buildNodeTypeMap(ast, globalEnv, nodeTypeMap);
|
|
1000
3003
|
}
|
|
1001
|
-
buildNodeTypeMap(ast, globalEnv, nodeTypeMap);
|
|
1002
3004
|
setUnknownBuiltinHandler(null);
|
|
1003
3005
|
setCallCollector(null);
|
|
1004
3006
|
setUnknownCollector(null);
|
|
@@ -1009,9 +3011,29 @@ function analyzeFile(filePath, source, activeCases, externalCallRecords) {
|
|
|
1009
3011
|
const maxLine = source.split("\n").length;
|
|
1010
3012
|
diagnostics.push(
|
|
1011
3013
|
...unknownRecordsToDiagnostics(
|
|
1012
|
-
unknownRecords.filter(
|
|
1013
|
-
|
|
1014
|
-
|
|
3014
|
+
unknownRecords.filter((r) => {
|
|
3015
|
+
if ((r.loc?.line ?? 0) > maxLine) return false;
|
|
3016
|
+
if (r.originModule === USAGE_SITE_MODULE) return false;
|
|
3017
|
+
if (bHostedEval && (r.kind === "method" || r.kind === "property")) {
|
|
3018
|
+
return false;
|
|
3019
|
+
}
|
|
3020
|
+
if ((r.kind === "method" || r.kind === "property") && bMemberDiagNames.has(r.name)) {
|
|
3021
|
+
return false;
|
|
3022
|
+
}
|
|
3023
|
+
if (r.kind === "global") {
|
|
3024
|
+
if (r.name.startsWith("module-cycle:") && bModuleIssueKinds.has("cycle")) return false;
|
|
3025
|
+
if (r.name.startsWith("module-depth:") && bModuleIssueKinds.has("depth")) return false;
|
|
3026
|
+
if (r.name.startsWith("module-missing:") && bModuleIssueKinds.has("missing")) return false;
|
|
3027
|
+
if (r.name.startsWith("recursion:")) {
|
|
3028
|
+
const fn = r.name.slice("recursion:".length);
|
|
3029
|
+
if (bTruncatedFns.has(fn) || bTruncatedFns.has(fn.split(".").pop() ?? fn)) {
|
|
3030
|
+
return false;
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
if (bBuiltinUnknownNames.has(r.name)) return false;
|
|
3034
|
+
}
|
|
3035
|
+
return true;
|
|
3036
|
+
})
|
|
1015
3037
|
)
|
|
1016
3038
|
);
|
|
1017
3039
|
const externalFunctions = synthesizeExternalFunctions(callRecords, filePath);
|
|
@@ -1028,19 +3050,19 @@ function collectBindings(ast, env, bindings) {
|
|
|
1028
3050
|
if (ast.type !== "File") return;
|
|
1029
3051
|
for (const stmt of ast.program.body) {
|
|
1030
3052
|
if (stmt.type === "FunctionDeclaration" && stmt.id) {
|
|
1031
|
-
const val = env.has(stmt.id.name) ? env.lookup(stmt.id.name) :
|
|
3053
|
+
const val = env.has(stmt.id.name) ? env.lookup(stmt.id.name) : T2.unknown;
|
|
1032
3054
|
bindings.set(stmt.id.name, { type: val, loc: locFromNode(stmt) });
|
|
1033
3055
|
}
|
|
1034
3056
|
if (stmt.type === "VariableDeclaration") {
|
|
1035
3057
|
for (const decl of stmt.declarations) {
|
|
1036
3058
|
if (decl.id.type === "Identifier") {
|
|
1037
|
-
const val = env.has(decl.id.name) ? env.lookup(decl.id.name) :
|
|
3059
|
+
const val = env.has(decl.id.name) ? env.lookup(decl.id.name) : T2.unknown;
|
|
1038
3060
|
bindings.set(decl.id.name, { type: val, loc: locFromNode(decl) });
|
|
1039
3061
|
}
|
|
1040
3062
|
}
|
|
1041
3063
|
}
|
|
1042
3064
|
if (stmt.type === "ClassDeclaration" && stmt.id) {
|
|
1043
|
-
const val = env.has(stmt.id.name) ? env.lookup(stmt.id.name) :
|
|
3065
|
+
const val = env.has(stmt.id.name) ? env.lookup(stmt.id.name) : T2.unknown;
|
|
1044
3066
|
bindings.set(stmt.id.name, { type: val, loc: locFromNode(stmt) });
|
|
1045
3067
|
}
|
|
1046
3068
|
if (stmt.type === "ExportNamedDeclaration" && stmt.declaration) {
|
|
@@ -1061,16 +3083,16 @@ function buildNodeTypeMap(ast, env, nodeTypeMap) {
|
|
|
1061
3083
|
}
|
|
1062
3084
|
}
|
|
1063
3085
|
if (node.type === "NumericLiteral") {
|
|
1064
|
-
nodeTypeMap.set(node,
|
|
3086
|
+
nodeTypeMap.set(node, T2.literal(node.value));
|
|
1065
3087
|
}
|
|
1066
3088
|
if (node.type === "StringLiteral") {
|
|
1067
|
-
nodeTypeMap.set(node,
|
|
3089
|
+
nodeTypeMap.set(node, T2.literal(node.value));
|
|
1068
3090
|
}
|
|
1069
3091
|
if (node.type === "BooleanLiteral") {
|
|
1070
|
-
nodeTypeMap.set(node,
|
|
3092
|
+
nodeTypeMap.set(node, T2.literal(node.value));
|
|
1071
3093
|
}
|
|
1072
3094
|
if (node.type === "NullLiteral") {
|
|
1073
|
-
nodeTypeMap.set(node,
|
|
3095
|
+
nodeTypeMap.set(node, T2.null);
|
|
1074
3096
|
}
|
|
1075
3097
|
} catch {
|
|
1076
3098
|
}
|
|
@@ -1080,7 +3102,7 @@ function buildNodeTypeMap(ast, env, nodeTypeMap) {
|
|
|
1080
3102
|
}
|
|
1081
3103
|
}
|
|
1082
3104
|
function getCasesForFile(filePath, source) {
|
|
1083
|
-
const ast =
|
|
3105
|
+
const ast = parse5(source);
|
|
1084
3106
|
const functions = extractDirectives(ast);
|
|
1085
3107
|
return functions.map((fn) => {
|
|
1086
3108
|
const cases = fn.directives.filter((d) => d.kind === "case").map((d, i) => ({ name: d.name, index: i }));
|
|
@@ -1090,23 +3112,48 @@ function getCasesForFile(filePath, source) {
|
|
|
1090
3112
|
async function getTypeAtPositionAsync(filePath, source, line, column, activeCases) {
|
|
1091
3113
|
const envNames = collectEnvNames(filePath, source, false);
|
|
1092
3114
|
if (envNames.length > 0) {
|
|
1093
|
-
await preloadPathEnvs(envNames,
|
|
3115
|
+
await preloadPathEnvs(envNames, dirname5(filePath));
|
|
1094
3116
|
}
|
|
1095
3117
|
return getTypeAtPosition(filePath, source, line, column, activeCases);
|
|
1096
3118
|
}
|
|
1097
3119
|
function getTypeAtPosition(filePath, source, line, column, activeCases) {
|
|
1098
|
-
const ast =
|
|
3120
|
+
const ast = parse5(source);
|
|
3121
|
+
const fileDirectives = extractFileDirectives(ast);
|
|
3122
|
+
const envNames = fileDirectives.filter((d) => d.kind === "env").flatMap((d) => d.envs);
|
|
3123
|
+
if (isBPathCapable(source, envNames)) {
|
|
3124
|
+
try {
|
|
3125
|
+
const seeds = mockDirectivesToAbsSeeds(extractDirectives(ast));
|
|
3126
|
+
const { modules } = evalAbsModuleGraph(source, filePath);
|
|
3127
|
+
const absNodes = collectAbsNodeTypes(source, {
|
|
3128
|
+
...seeds,
|
|
3129
|
+
modules,
|
|
3130
|
+
file: ast
|
|
3131
|
+
});
|
|
3132
|
+
const absAt = findAbsAtPosition(absNodes, line, column);
|
|
3133
|
+
if (absAt) return absToTypeValue2(absAt);
|
|
3134
|
+
const ident = findIdentNameAtPosition(source, line, column, ast);
|
|
3135
|
+
if (ident) {
|
|
3136
|
+
const binds = collectAbsBindingsFromGraph(source, filePath, {
|
|
3137
|
+
seedVars: seeds.seedVars,
|
|
3138
|
+
seedFns: seeds.seedFns
|
|
3139
|
+
});
|
|
3140
|
+
const bound = binds.get(ident);
|
|
3141
|
+
if (bound) return absToTypeValue2(bound);
|
|
3142
|
+
}
|
|
3143
|
+
} catch {
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
1099
3146
|
resetMemo();
|
|
1100
3147
|
resetEnvModules();
|
|
1101
3148
|
setModuleResolver(resolveModule);
|
|
1102
|
-
setCurrentFileDir(
|
|
3149
|
+
setCurrentFileDir(dirname5(filePath));
|
|
1103
3150
|
setCurrentSource(source);
|
|
1104
|
-
const
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
const
|
|
1109
|
-
|
|
3151
|
+
const globalEnv = createEnvironment2();
|
|
3152
|
+
{
|
|
3153
|
+
const loaded = envNames.length > 0 ? loadEnvs2(envNames, globalEnv) : { modules: {} };
|
|
3154
|
+
const auto = autoHarvestModules(source, dirname5(filePath));
|
|
3155
|
+
const modules = { ...loaded.modules, ...auto };
|
|
3156
|
+
if (Object.keys(modules).length > 0) setEnvModules(modules);
|
|
1110
3157
|
}
|
|
1111
3158
|
evaluateProgram(ast, globalEnv);
|
|
1112
3159
|
const nodeTypeMap = /* @__PURE__ */ new Map();
|
|
@@ -1132,6 +3179,167 @@ function getTypeAtPosition(filePath, source, line, column, activeCases) {
|
|
|
1132
3179
|
resetMockModules();
|
|
1133
3180
|
return findBestTypeAtPosition(nodeTypeMap, globalEnv, ast, line, column);
|
|
1134
3181
|
}
|
|
3182
|
+
function getHoverAtPosition(filePath, source, line, column, activeCases) {
|
|
3183
|
+
let file;
|
|
3184
|
+
try {
|
|
3185
|
+
file = parse5(source);
|
|
3186
|
+
} catch {
|
|
3187
|
+
file = void 0;
|
|
3188
|
+
}
|
|
3189
|
+
const envNames = collectEnvNames(filePath, source, false);
|
|
3190
|
+
const fnName = findFunctionNameAtPosition(source, line, column, file);
|
|
3191
|
+
let gDisplay;
|
|
3192
|
+
let gAbs;
|
|
3193
|
+
let gMulti;
|
|
3194
|
+
if (fnName) {
|
|
3195
|
+
try {
|
|
3196
|
+
const g = generalizeFromAst2(fnName, source, file ? { file } : {});
|
|
3197
|
+
if (g) {
|
|
3198
|
+
gDisplay = g.display;
|
|
3199
|
+
gAbs = formatAbs3(g.symbolic);
|
|
3200
|
+
gMulti = formatAbsMultiline(g.symbolic, fnName);
|
|
3201
|
+
}
|
|
3202
|
+
} catch {
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
const attachIntension = (info2) => {
|
|
3206
|
+
if (!gDisplay) return info2;
|
|
3207
|
+
if (!info2) {
|
|
3208
|
+
return { typeText: gDisplay, intension: gDisplay, abs: gAbs, absMultiline: gMulti };
|
|
3209
|
+
}
|
|
3210
|
+
return {
|
|
3211
|
+
...info2,
|
|
3212
|
+
intension: gDisplay,
|
|
3213
|
+
// 外延侧已有更准 Abs 时保留;否则用 symbolic 兜底
|
|
3214
|
+
abs: info2.abs ?? gAbs,
|
|
3215
|
+
absMultiline: info2.absMultiline ?? gMulti
|
|
3216
|
+
};
|
|
3217
|
+
};
|
|
3218
|
+
if (isBPathCapable(source, envNames)) {
|
|
3219
|
+
try {
|
|
3220
|
+
const seeds = mockDirectivesToAbsSeeds(extractDirectives(file ?? parse5(source)));
|
|
3221
|
+
const { modules } = evalAbsModuleGraph(source, filePath);
|
|
3222
|
+
const absNodes = collectAbsNodeTypes(source, {
|
|
3223
|
+
...seeds,
|
|
3224
|
+
modules,
|
|
3225
|
+
...file ? { file } : {}
|
|
3226
|
+
});
|
|
3227
|
+
const absAt = findAbsAtPosition(absNodes, line, column);
|
|
3228
|
+
const ident2 = findIdentNameAtPosition(source, line, column, file);
|
|
3229
|
+
if (ident2 && !fnName) {
|
|
3230
|
+
const binds = collectAbsBindingsFromGraph(source, filePath, {
|
|
3231
|
+
seedVars: seeds.seedVars,
|
|
3232
|
+
seedFns: seeds.seedFns
|
|
3233
|
+
});
|
|
3234
|
+
const bound = binds.get(ident2);
|
|
3235
|
+
if (bound) {
|
|
3236
|
+
const absLine = formatAbs3(bound);
|
|
3237
|
+
const absMulti = formatAbsMultiline(bound, ident2);
|
|
3238
|
+
return attachIntension({ typeText: absLine, abs: absLine, absMultiline: absMulti });
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3241
|
+
if (absAt) {
|
|
3242
|
+
const absLine = formatAbs3(absAt);
|
|
3243
|
+
const absMulti = formatAbsMultiline(absAt, void 0);
|
|
3244
|
+
return attachIntension({ typeText: absLine, abs: absLine, absMultiline: absMulti });
|
|
3245
|
+
}
|
|
3246
|
+
} catch {
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
const tv = getTypeAtPosition(filePath, source, line, column, activeCases);
|
|
3250
|
+
const info = tv ? { typeText: typeValueToString3(tv) } : null;
|
|
3251
|
+
const ident = findIdentNameAtPosition(source, line, column, file);
|
|
3252
|
+
if (ident && !fnName) {
|
|
3253
|
+
try {
|
|
3254
|
+
if (isBPathCapable(source, []) || !/\brequire\s*\(/.test(source)) {
|
|
3255
|
+
const seeds = mockDirectivesToAbsSeeds(extractDirectives(file ?? parse5(source)));
|
|
3256
|
+
const absBinds = collectAbsBindingsFromGraph(source, filePath, {
|
|
3257
|
+
seedVars: seeds.seedVars,
|
|
3258
|
+
seedFns: seeds.seedFns
|
|
3259
|
+
});
|
|
3260
|
+
const absBound = absBinds.get(ident);
|
|
3261
|
+
if (absBound) {
|
|
3262
|
+
const absLine = formatAbs3(absBound);
|
|
3263
|
+
const absMulti = formatAbsMultiline(absBound, ident);
|
|
3264
|
+
if (info) {
|
|
3265
|
+
info.abs = absLine;
|
|
3266
|
+
info.absMultiline = absMulti;
|
|
3267
|
+
return attachIntension(info);
|
|
3268
|
+
}
|
|
3269
|
+
return attachIntension({ typeText: absLine, abs: absLine, absMultiline: absMulti });
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
} catch {
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
try {
|
|
3276
|
+
if (isBPathCapable(source, []) || !/\brequire\s*\(|\bimport\s*[{'"*]/.test(source)) {
|
|
3277
|
+
const seeds = mockDirectivesToAbsSeeds(
|
|
3278
|
+
extractDirectives(file ?? parse5(source))
|
|
3279
|
+
);
|
|
3280
|
+
const { modules } = evalAbsModuleGraph(source, filePath);
|
|
3281
|
+
const nodeTypes = collectAbsNodeTypes(source, {
|
|
3282
|
+
...seeds,
|
|
3283
|
+
modules,
|
|
3284
|
+
...file ? { file } : {}
|
|
3285
|
+
});
|
|
3286
|
+
const absAt = findAbsAtPosition(nodeTypes, line, column);
|
|
3287
|
+
if (absAt) {
|
|
3288
|
+
const absLine = formatAbs3(absAt);
|
|
3289
|
+
const absMulti = formatAbsMultiline(absAt, void 0);
|
|
3290
|
+
if (info) {
|
|
3291
|
+
info.abs = absLine;
|
|
3292
|
+
info.absMultiline = absMulti;
|
|
3293
|
+
} else {
|
|
3294
|
+
return attachIntension({ typeText: absLine, abs: absLine, absMultiline: absMulti });
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
} catch {
|
|
3299
|
+
}
|
|
3300
|
+
return attachIntension(info);
|
|
3301
|
+
}
|
|
3302
|
+
function findIdentNameAtPosition(source, line, column, fileAst) {
|
|
3303
|
+
try {
|
|
3304
|
+
const ast = fileAst ?? parse5(source);
|
|
3305
|
+
let found;
|
|
3306
|
+
traverse(ast, {
|
|
3307
|
+
Identifier(path) {
|
|
3308
|
+
const loc = path.node.loc;
|
|
3309
|
+
if (!loc) return;
|
|
3310
|
+
if (loc.start.line !== line) return;
|
|
3311
|
+
if (column < loc.start.column || column > loc.end.column) return;
|
|
3312
|
+
found = path.node.name;
|
|
3313
|
+
}
|
|
3314
|
+
});
|
|
3315
|
+
return found;
|
|
3316
|
+
} catch {
|
|
3317
|
+
return void 0;
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
function findFunctionNameAtPosition(source, line, column, fileAst) {
|
|
3321
|
+
try {
|
|
3322
|
+
const ast = fileAst ?? parse5(source);
|
|
3323
|
+
let found;
|
|
3324
|
+
traverse(ast, {
|
|
3325
|
+
Identifier(path) {
|
|
3326
|
+
const loc = path.node.loc;
|
|
3327
|
+
if (!loc) return;
|
|
3328
|
+
if (loc.start.line !== line) return;
|
|
3329
|
+
if (column < loc.start.column || column > loc.end.column) return;
|
|
3330
|
+
const parent = path.parent;
|
|
3331
|
+
if (parent.type === "FunctionDeclaration" && parent.id === path.node) {
|
|
3332
|
+
found = path.node.name;
|
|
3333
|
+
} else if (parent.type === "CallExpression" && parent.callee === path.node) {
|
|
3334
|
+
found = path.node.name;
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
});
|
|
3338
|
+
return found;
|
|
3339
|
+
} catch {
|
|
3340
|
+
return void 0;
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
1135
3343
|
function findEnclosingFunction(functions, line) {
|
|
1136
3344
|
for (const fn of functions) {
|
|
1137
3345
|
const loc = fn.node.loc;
|
|
@@ -1190,18 +3398,18 @@ function getCompletionsAtPosition(filePath, source, line, column) {
|
|
|
1190
3398
|
const safeSource = sanitizeSourceForParsing(source);
|
|
1191
3399
|
let ast;
|
|
1192
3400
|
try {
|
|
1193
|
-
ast =
|
|
3401
|
+
ast = parse5(safeSource);
|
|
1194
3402
|
} catch {
|
|
1195
3403
|
try {
|
|
1196
|
-
ast =
|
|
3404
|
+
ast = parse5(source);
|
|
1197
3405
|
} catch {
|
|
1198
3406
|
return [];
|
|
1199
3407
|
}
|
|
1200
3408
|
}
|
|
1201
3409
|
resetMemo();
|
|
1202
3410
|
setModuleResolver(resolveModule);
|
|
1203
|
-
setCurrentFileDir(
|
|
1204
|
-
const globalEnv =
|
|
3411
|
+
setCurrentFileDir(dirname5(filePath));
|
|
3412
|
+
const globalEnv = createEnvironment2();
|
|
1205
3413
|
evaluateProgram(ast, globalEnv);
|
|
1206
3414
|
if (!globalEnv.has(objName)) {
|
|
1207
3415
|
setModuleResolver(null);
|
|
@@ -1221,11 +3429,11 @@ function getTextBeforePosition(source, line, column) {
|
|
|
1221
3429
|
return lines[line - 1].slice(0, column);
|
|
1222
3430
|
}
|
|
1223
3431
|
function getVariableCompletions(filePath, source) {
|
|
1224
|
-
const ast =
|
|
3432
|
+
const ast = parse5(source);
|
|
1225
3433
|
resetMemo();
|
|
1226
3434
|
setModuleResolver(resolveModule);
|
|
1227
|
-
setCurrentFileDir(
|
|
1228
|
-
const globalEnv =
|
|
3435
|
+
setCurrentFileDir(dirname5(filePath));
|
|
3436
|
+
const globalEnv = createEnvironment2();
|
|
1229
3437
|
evaluateProgram(ast, globalEnv);
|
|
1230
3438
|
const ownBindings = globalEnv.getOwnBindings();
|
|
1231
3439
|
const completions = [];
|
|
@@ -1234,12 +3442,97 @@ function getVariableCompletions(filePath, source) {
|
|
|
1234
3442
|
completions.push({
|
|
1235
3443
|
label: name,
|
|
1236
3444
|
kind: tv.kind === "function" ? "method" : "variable",
|
|
1237
|
-
detail:
|
|
3445
|
+
detail: typeValueToString3(tv)
|
|
1238
3446
|
});
|
|
1239
3447
|
}
|
|
1240
3448
|
setModuleResolver(null);
|
|
1241
3449
|
return completions;
|
|
1242
3450
|
}
|
|
3451
|
+
function builtinMemberType(memberExpr) {
|
|
3452
|
+
try {
|
|
3453
|
+
const result = evaluate(parse5(`${memberExpr};`), createEnvironment2());
|
|
3454
|
+
if (!result || typeof result !== "object" || !("kind" in result)) return null;
|
|
3455
|
+
return result;
|
|
3456
|
+
} catch {
|
|
3457
|
+
return null;
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
function builtinProtoMembers(className) {
|
|
3461
|
+
const table = BUILTIN_PROTOTYPE_METHOD_APPROXIMATIONS[className];
|
|
3462
|
+
return table ? Object.keys(table) : [];
|
|
3463
|
+
}
|
|
3464
|
+
function describeMember(label, tv, fallbackClass) {
|
|
3465
|
+
if (tv) {
|
|
3466
|
+
const sig = getFnSig3(tv);
|
|
3467
|
+
if (sig) {
|
|
3468
|
+
const paramNames = tv.kind === "function" ? tv.params : [];
|
|
3469
|
+
const params = sig.paramTypes.map((p, i) => `${paramNames[i] ?? `arg${i}`}: ${typeValueToString3(p)}`).join(", ");
|
|
3470
|
+
return `(${params}) => ${typeValueToString3(sig.returnType)}`;
|
|
3471
|
+
}
|
|
3472
|
+
if (tv.kind !== "function") return typeValueToString3(tv);
|
|
3473
|
+
}
|
|
3474
|
+
return `${label}(\u2026)@${fallbackClass}`;
|
|
3475
|
+
}
|
|
3476
|
+
function getArrayCompletions(tv) {
|
|
3477
|
+
const completions = [];
|
|
3478
|
+
for (const m of builtinProtoMembers("Array")) {
|
|
3479
|
+
const detail = describeMember(m, builtinMemberType(`Array.prototype.${m}`), "Array");
|
|
3480
|
+
completions.push({ label: m, kind: "method", detail });
|
|
3481
|
+
}
|
|
3482
|
+
completions.push({
|
|
3483
|
+
label: "length",
|
|
3484
|
+
kind: "property",
|
|
3485
|
+
// tuple 长度是精确字面量;array 是 number(保持原展示语义)
|
|
3486
|
+
detail: tv.kind === "tuple" ? `${tv.elements.length}` : "number"
|
|
3487
|
+
});
|
|
3488
|
+
return completions;
|
|
3489
|
+
}
|
|
3490
|
+
function getPromiseCompletions() {
|
|
3491
|
+
return builtinProtoMembers("Promise").map((m) => ({
|
|
3492
|
+
label: m,
|
|
3493
|
+
kind: "method",
|
|
3494
|
+
detail: describeMember(m, builtinMemberType(`Promise.prototype.${m}`), "Promise")
|
|
3495
|
+
}));
|
|
3496
|
+
}
|
|
3497
|
+
function getStringCompletions() {
|
|
3498
|
+
const completions = [];
|
|
3499
|
+
for (const m of builtinProtoMembers("String")) {
|
|
3500
|
+
completions.push({
|
|
3501
|
+
label: m,
|
|
3502
|
+
kind: "method",
|
|
3503
|
+
detail: describeMember(m, builtinMemberType(`"s".${m}`), "String")
|
|
3504
|
+
});
|
|
3505
|
+
}
|
|
3506
|
+
completions.push({ label: "length", kind: "property", detail: "number" });
|
|
3507
|
+
return completions;
|
|
3508
|
+
}
|
|
3509
|
+
function getUnionCompletions(tv) {
|
|
3510
|
+
const members = tv.members;
|
|
3511
|
+
if (members.length === 0) return [];
|
|
3512
|
+
const labelsByMember = members.map((m) => getCompletionsForType(m));
|
|
3513
|
+
const baseIdx = labelsByMember.findIndex((labels) => labels.length > 0);
|
|
3514
|
+
if (baseIdx === -1) return [];
|
|
3515
|
+
const common = [];
|
|
3516
|
+
for (const base of labelsByMember[baseIdx]) {
|
|
3517
|
+
let allPresent = true;
|
|
3518
|
+
const memberTypes = [base.detail ?? base.label];
|
|
3519
|
+
for (let i = 0; i < members.length; i++) {
|
|
3520
|
+
if (i === baseIdx) continue;
|
|
3521
|
+
const labels = labelsByMember[i];
|
|
3522
|
+
if (labels.length === 0) continue;
|
|
3523
|
+
const hit = labels.find((l) => l.label === base.label);
|
|
3524
|
+
if (!hit) {
|
|
3525
|
+
allPresent = false;
|
|
3526
|
+
break;
|
|
3527
|
+
}
|
|
3528
|
+
memberTypes.push(hit.detail ?? hit.label);
|
|
3529
|
+
}
|
|
3530
|
+
if (allPresent) {
|
|
3531
|
+
common.push({ ...base, detail: memberTypes.join(" | ") });
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
return common;
|
|
3535
|
+
}
|
|
1243
3536
|
function getCompletionsForType(tv) {
|
|
1244
3537
|
const completions = [];
|
|
1245
3538
|
if (tv.kind === "object") {
|
|
@@ -1247,7 +3540,7 @@ function getCompletionsForType(tv) {
|
|
|
1247
3540
|
completions.push({
|
|
1248
3541
|
label: key,
|
|
1249
3542
|
kind: val.kind === "function" ? "method" : "property",
|
|
1250
|
-
detail:
|
|
3543
|
+
detail: typeValueToString3(val)
|
|
1251
3544
|
});
|
|
1252
3545
|
}
|
|
1253
3546
|
return completions;
|
|
@@ -1257,52 +3550,526 @@ function getCompletionsForType(tv) {
|
|
|
1257
3550
|
completions.push({
|
|
1258
3551
|
label: key,
|
|
1259
3552
|
kind: val.kind === "function" ? "method" : "property",
|
|
1260
|
-
detail:
|
|
3553
|
+
detail: typeValueToString3(val)
|
|
1261
3554
|
});
|
|
1262
3555
|
}
|
|
1263
3556
|
return completions;
|
|
1264
3557
|
}
|
|
3558
|
+
if (tv.kind === "union") {
|
|
3559
|
+
return getUnionCompletions(tv);
|
|
3560
|
+
}
|
|
1265
3561
|
if (tv.kind === "array" || tv.kind === "tuple") {
|
|
1266
|
-
|
|
1267
|
-
{ label: "map", detail: "map(callback)" },
|
|
1268
|
-
{ label: "filter", detail: "filter(callback)" },
|
|
1269
|
-
{ label: "reduce", detail: "reduce(callback, init)" },
|
|
1270
|
-
{ label: "find", detail: "find(callback)" },
|
|
1271
|
-
{ label: "some", detail: "some(callback)" },
|
|
1272
|
-
{ label: "every", detail: "every(callback)" },
|
|
1273
|
-
{ label: "forEach", detail: "forEach(callback)" },
|
|
1274
|
-
{ label: "flatMap", detail: "flatMap(callback)" },
|
|
1275
|
-
{ label: "includes", detail: "includes(value)" },
|
|
1276
|
-
{ label: "indexOf", detail: "indexOf(value)" },
|
|
1277
|
-
{ label: "join", detail: "join(separator)" },
|
|
1278
|
-
{ label: "slice", detail: "slice(start, end)" },
|
|
1279
|
-
{ label: "concat", detail: "concat(other)" },
|
|
1280
|
-
{ label: "push", detail: "push(value)" },
|
|
1281
|
-
{ label: "length", detail: tv.kind === "tuple" ? `${tv.elements.length}` : "number" }
|
|
1282
|
-
];
|
|
1283
|
-
for (const m of arrayMethods) {
|
|
1284
|
-
completions.push({ label: m.label, kind: "method", detail: m.detail });
|
|
1285
|
-
}
|
|
1286
|
-
return completions;
|
|
3562
|
+
return getArrayCompletions(tv);
|
|
1287
3563
|
}
|
|
1288
3564
|
if (tv.kind === "promise") {
|
|
1289
|
-
|
|
1290
|
-
completions.push({ label: "catch", kind: "method", detail: "catch(callback)" });
|
|
1291
|
-
completions.push({ label: "finally", kind: "method", detail: "finally(callback)" });
|
|
1292
|
-
return completions;
|
|
3565
|
+
return getPromiseCompletions();
|
|
1293
3566
|
}
|
|
1294
3567
|
if (tv.kind === "primitive" && tv.type === "string") {
|
|
1295
|
-
|
|
1296
|
-
for (const m of stringMethods) {
|
|
1297
|
-
completions.push({ label: m, kind: "method", detail: `string.${m}` });
|
|
1298
|
-
}
|
|
1299
|
-
return completions;
|
|
3568
|
+
return getStringCompletions();
|
|
1300
3569
|
}
|
|
1301
3570
|
return completions;
|
|
1302
3571
|
}
|
|
3572
|
+
function absIsBetter(absTv, prev) {
|
|
3573
|
+
if (prev.kind === "unknown") return absTv.kind !== "unknown";
|
|
3574
|
+
if (absTv.kind === "unknown" || absTv.kind === "never") return false;
|
|
3575
|
+
if (prev.kind === "literal" || prev.kind === "object" || prev.kind === "array" || prev.kind === "tuple" || prev.kind === "union" || prev.kind === "function" || prev.kind === "instance" || prev.kind === "promise") {
|
|
3576
|
+
return false;
|
|
3577
|
+
}
|
|
3578
|
+
if (absTv.kind === "refined" && prev.kind === "primitive") return true;
|
|
3579
|
+
return false;
|
|
3580
|
+
}
|
|
3581
|
+
function isSelfContainedSource(source, envNames) {
|
|
3582
|
+
if (envNames.length > 0) return false;
|
|
3583
|
+
return !/\brequire\s*\(|\bimport\s*[{'"*]/.test(source);
|
|
3584
|
+
}
|
|
3585
|
+
function absModulesOk(source, envNames) {
|
|
3586
|
+
void source;
|
|
3587
|
+
void envNames;
|
|
3588
|
+
return true;
|
|
3589
|
+
}
|
|
3590
|
+
function collectAbsCallRecords(source, seeds, filePath, precomputedModules, envNames = []) {
|
|
3591
|
+
const absCalls = [];
|
|
3592
|
+
let modules = precomputedModules;
|
|
3593
|
+
let importLocals = /* @__PURE__ */ new Map();
|
|
3594
|
+
if (filePath && absModulesOk(source, envNames)) {
|
|
3595
|
+
if (!modules) {
|
|
3596
|
+
try {
|
|
3597
|
+
const graph = evalAbsModuleGraph(source, filePath, {
|
|
3598
|
+
seedVars: seeds?.seedVars,
|
|
3599
|
+
seedFns: seeds?.seedFns
|
|
3600
|
+
});
|
|
3601
|
+
modules = graph.modules;
|
|
3602
|
+
} catch {
|
|
3603
|
+
modules = void 0;
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
if (envNames.length > 0) {
|
|
3607
|
+
modules = { ...collectEnvModules(envNames), ...modules ?? {} };
|
|
3608
|
+
}
|
|
3609
|
+
try {
|
|
3610
|
+
importLocals = buildAbsImportLocalMap(source, filePath);
|
|
3611
|
+
} catch {
|
|
3612
|
+
importLocals = /* @__PURE__ */ new Map();
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3615
|
+
setAbsCallCollector((r) => absCalls.push(r));
|
|
3616
|
+
try {
|
|
3617
|
+
evalProgramAbs2(source, { ...seeds, modules });
|
|
3618
|
+
} catch {
|
|
3619
|
+
} finally {
|
|
3620
|
+
setAbsCallCollector(null);
|
|
3621
|
+
}
|
|
3622
|
+
return absCalls.map((r) => {
|
|
3623
|
+
const rec = {
|
|
3624
|
+
fnName: r.fnName,
|
|
3625
|
+
argTypes: r.args.map((a) => absToTypeValue2(a)),
|
|
3626
|
+
resultType: r.threw ? T2.never : absToTypeValue2(r.result),
|
|
3627
|
+
throws: r.threw ? absToTypeValue2(r.result) : T2.never,
|
|
3628
|
+
callLoc: r.callLoc
|
|
3629
|
+
};
|
|
3630
|
+
const imp = importLocals.get(r.fnName);
|
|
3631
|
+
if (imp) {
|
|
3632
|
+
rec.targetModule = imp.modulePath;
|
|
3633
|
+
rec.targetExport = imp.exportName;
|
|
3634
|
+
}
|
|
3635
|
+
return rec;
|
|
3636
|
+
});
|
|
3637
|
+
}
|
|
3638
|
+
function collectAbsBindsAndNodes(source, seeds, modules) {
|
|
3639
|
+
const binds = /* @__PURE__ */ new Map();
|
|
3640
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
3641
|
+
setAbsNodeCollector((node, value) => {
|
|
3642
|
+
nodes.set(node, value);
|
|
3643
|
+
});
|
|
3644
|
+
try {
|
|
3645
|
+
const { env } = evalProgramAbs2(source, {
|
|
3646
|
+
...seeds,
|
|
3647
|
+
modules
|
|
3648
|
+
});
|
|
3649
|
+
for (const [k, v] of env.vars) binds.set(k, v);
|
|
3650
|
+
for (const [name, impl] of env.fns) {
|
|
3651
|
+
if (!binds.has(name)) {
|
|
3652
|
+
binds.set(
|
|
3653
|
+
name,
|
|
3654
|
+
absFunction5(impl.params, { body: impl.body, async: impl.async, env })
|
|
3655
|
+
);
|
|
3656
|
+
}
|
|
3657
|
+
}
|
|
3658
|
+
} catch {
|
|
3659
|
+
} finally {
|
|
3660
|
+
setAbsNodeCollector(null);
|
|
3661
|
+
}
|
|
3662
|
+
return { binds, nodes };
|
|
3663
|
+
}
|
|
3664
|
+
function buildAbsImportLocalMap(source, fromFile) {
|
|
3665
|
+
const out = /* @__PURE__ */ new Map();
|
|
3666
|
+
try {
|
|
3667
|
+
const file = parse5(source);
|
|
3668
|
+
for (const stmt of file.program.body) {
|
|
3669
|
+
if (stmt.type !== "ImportDeclaration") continue;
|
|
3670
|
+
const spec = stmt.source.value;
|
|
3671
|
+
let modulePath = null;
|
|
3672
|
+
if (spec.startsWith(".") || spec.startsWith("/")) {
|
|
3673
|
+
modulePath = resolveImportAbs(spec, fromFile);
|
|
3674
|
+
} else if (!spec.startsWith("node:")) {
|
|
3675
|
+
modulePath = spec;
|
|
3676
|
+
}
|
|
3677
|
+
if (!modulePath) continue;
|
|
3678
|
+
for (const s of stmt.specifiers) {
|
|
3679
|
+
if (s.type === "ImportSpecifier") {
|
|
3680
|
+
const imported = s.imported.type === "Identifier" ? s.imported.name : String(s.imported);
|
|
3681
|
+
out.set(s.local.name, { modulePath, exportName: imported });
|
|
3682
|
+
} else if (s.type === "ImportDefaultSpecifier") {
|
|
3683
|
+
out.set(s.local.name, { modulePath, exportName: "default" });
|
|
3684
|
+
}
|
|
3685
|
+
}
|
|
3686
|
+
}
|
|
3687
|
+
} catch {
|
|
3688
|
+
}
|
|
3689
|
+
return out;
|
|
3690
|
+
}
|
|
3691
|
+
function resolveImportAbs(spec, fromFile) {
|
|
3692
|
+
const base = dirname5(resolve4(fromFile));
|
|
3693
|
+
const p = resolve4(base, spec);
|
|
3694
|
+
for (const cand of [p, `${p}.js`, `${p}.mjs`, `${p}.ts`, resolve4(p, "index.js")]) {
|
|
3695
|
+
if (existsSync3(cand) && !statSync3(cand).isDirectory()) return cand;
|
|
3696
|
+
}
|
|
3697
|
+
return null;
|
|
3698
|
+
}
|
|
3699
|
+
function tryEvalAbs(source, fnName, args, filePath, mocks) {
|
|
3700
|
+
const raw = tryEvalAbsRaw(source, fnName, args, filePath, mocks);
|
|
3701
|
+
return raw ? absToTypeValue2(raw) : void 0;
|
|
3702
|
+
}
|
|
3703
|
+
function tryEvalAbsRaw(source, fnName, args, filePath, mocks) {
|
|
3704
|
+
if (/\brequire\s*\(/.test(source)) return void 0;
|
|
3705
|
+
try {
|
|
3706
|
+
const absArgs = args.map((a) => typeValueToAbs5(a));
|
|
3707
|
+
if (filePath) {
|
|
3708
|
+
const viaB = tryBPathCall(source, filePath, fnName, absArgs, { mocks });
|
|
3709
|
+
if (viaB) return viaB;
|
|
3710
|
+
}
|
|
3711
|
+
let modules;
|
|
3712
|
+
if (filePath && /\bimport\s*[{'"*]/.test(source)) {
|
|
3713
|
+
modules = evalAbsModuleGraph(source, filePath).modules;
|
|
3714
|
+
}
|
|
3715
|
+
const result = analyzeFn(source, fnName, absArgs, void 0, void 0, void 0, modules);
|
|
3716
|
+
if (result.shape.k === "unknown" && !result.term) {
|
|
3717
|
+
return void 0;
|
|
3718
|
+
}
|
|
3719
|
+
return result;
|
|
3720
|
+
} catch {
|
|
3721
|
+
return void 0;
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
function tryEvalEntryAbs(source, fnName, args, filePath, mocks) {
|
|
3725
|
+
return tryEvalAbs(source, fnName, args, filePath, mocks);
|
|
3726
|
+
}
|
|
3727
|
+
function tryAttachIntension(caseResult, source, fnName) {
|
|
3728
|
+
try {
|
|
3729
|
+
const g = generalizeFromAst2(fnName, source);
|
|
3730
|
+
if (!g) return;
|
|
3731
|
+
const sym = g.symbolic;
|
|
3732
|
+
const intension = {
|
|
3733
|
+
display: g.display,
|
|
3734
|
+
abs: formatAbs3(sym),
|
|
3735
|
+
absMultiline: formatAbsMultiline(sym, fnName)
|
|
3736
|
+
};
|
|
3737
|
+
if (sym.term && sym.term.op !== "lit") {
|
|
3738
|
+
intension.term = termToString(sym.term);
|
|
3739
|
+
}
|
|
3740
|
+
if (sym.pred && sym.pred.op !== "true") {
|
|
3741
|
+
intension.pred = predToString(sym.pred);
|
|
3742
|
+
}
|
|
3743
|
+
intension.conf = sym.conf;
|
|
3744
|
+
caseResult.intension = intension;
|
|
3745
|
+
} catch {
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3748
|
+
function attachAbsToIntension(caseResult, absVal, label) {
|
|
3749
|
+
const prev = caseResult.intension ?? {};
|
|
3750
|
+
caseResult.abs = absVal;
|
|
3751
|
+
caseResult.intension = {
|
|
3752
|
+
...prev,
|
|
3753
|
+
abs: formatAbs3(absVal),
|
|
3754
|
+
absMultiline: formatAbsMultiline(absVal, label ?? caseResult.name),
|
|
3755
|
+
conf: absVal.conf
|
|
3756
|
+
};
|
|
3757
|
+
}
|
|
3758
|
+
|
|
3759
|
+
// src/index.ts
|
|
3760
|
+
import { collectAbsInlays } from "@nudojs/core";
|
|
3761
|
+
|
|
3762
|
+
// src/infer-json.ts
|
|
3763
|
+
import { typeValueToString as typeValueToString4 } from "@nudojs/core";
|
|
3764
|
+
function mapCase(c) {
|
|
3765
|
+
const out = {
|
|
3766
|
+
name: c.name,
|
|
3767
|
+
args: c.args.map((a) => typeValueToString4(a)),
|
|
3768
|
+
result: typeValueToString4(c.result),
|
|
3769
|
+
throws: c.throws.kind !== "never" ? typeValueToString4(c.throws) : null,
|
|
3770
|
+
source: c.source ?? null
|
|
3771
|
+
};
|
|
3772
|
+
if (c.aggregatedFrom !== void 0) out.aggregatedFrom = c.aggregatedFrom;
|
|
3773
|
+
if (c.intension) {
|
|
3774
|
+
const i = c.intension;
|
|
3775
|
+
out.intension = {
|
|
3776
|
+
...i.display !== void 0 ? { display: i.display } : {},
|
|
3777
|
+
...i.abs !== void 0 ? { abs: i.abs } : {},
|
|
3778
|
+
...i.absMultiline !== void 0 ? { absMultiline: i.absMultiline } : {},
|
|
3779
|
+
...i.term !== void 0 ? { term: i.term } : {},
|
|
3780
|
+
...i.pred !== void 0 ? { pred: i.pred } : {},
|
|
3781
|
+
...i.conf !== void 0 ? { conf: i.conf } : {}
|
|
3782
|
+
};
|
|
3783
|
+
}
|
|
3784
|
+
return out;
|
|
3785
|
+
}
|
|
3786
|
+
function mapFunction(f) {
|
|
3787
|
+
const out = {
|
|
3788
|
+
name: f.name,
|
|
3789
|
+
loc: f.loc,
|
|
3790
|
+
entryOnly: f.entryOnly ?? false,
|
|
3791
|
+
cases: f.cases.map(mapCase)
|
|
3792
|
+
};
|
|
3793
|
+
if (f.noDeclaration) out.noDeclaration = true;
|
|
3794
|
+
if (f.combined) out.combined = typeValueToString4(f.combined);
|
|
3795
|
+
return out;
|
|
3796
|
+
}
|
|
3797
|
+
function serializeInferJson(result, file) {
|
|
3798
|
+
const functions = result.functions.map(mapFunction);
|
|
3799
|
+
const external = result.externalFunctions?.map((f) => ({
|
|
3800
|
+
name: f.name,
|
|
3801
|
+
fromModule: f.fromModule,
|
|
3802
|
+
cases: f.cases.map(mapCase)
|
|
3803
|
+
}));
|
|
3804
|
+
const caseCount = functions.reduce((n, f) => n + f.cases.length, 0) + (external ?? []).reduce((n, f) => n + f.cases.length, 0);
|
|
3805
|
+
const json = {
|
|
3806
|
+
version: 1,
|
|
3807
|
+
file,
|
|
3808
|
+
summary: {
|
|
3809
|
+
functions: functions.length,
|
|
3810
|
+
externalFunctions: external?.length ?? 0,
|
|
3811
|
+
cases: caseCount,
|
|
3812
|
+
diagnostics: result.diagnostics.length
|
|
3813
|
+
},
|
|
3814
|
+
functions,
|
|
3815
|
+
diagnostics: result.diagnostics.map((d) => ({
|
|
3816
|
+
range: d.range,
|
|
3817
|
+
severity: d.severity,
|
|
3818
|
+
message: d.message,
|
|
3819
|
+
...d.code !== void 0 ? { code: d.code } : {},
|
|
3820
|
+
...d.suggestions ? { suggestions: d.suggestions } : {},
|
|
3821
|
+
...d.tags ? { tags: d.tags } : {},
|
|
3822
|
+
...d.origin ? { origin: d.origin } : {}
|
|
3823
|
+
}))
|
|
3824
|
+
};
|
|
3825
|
+
if (external) json.externalFunctions = external;
|
|
3826
|
+
return json;
|
|
3827
|
+
}
|
|
3828
|
+
|
|
3829
|
+
// src/target-path.ts
|
|
3830
|
+
function isNudoTargetPath(path) {
|
|
3831
|
+
const lower = path.toLowerCase();
|
|
3832
|
+
if (lower.endsWith(".d.ts")) return false;
|
|
3833
|
+
return lower.endsWith(".js") || lower.endsWith(".mjs") || lower.endsWith(".ts");
|
|
3834
|
+
}
|
|
3835
|
+
|
|
3836
|
+
// src/load-module.ts
|
|
3837
|
+
import { existsSync as existsSync4, statSync as statSync4, readFileSync as readFileSync5 } from "fs";
|
|
3838
|
+
import { dirname as dirname6, resolve as resolve5, join as join3 } from "path";
|
|
3839
|
+
function defaultLoadModule(spec, fromFile) {
|
|
3840
|
+
if (!spec.startsWith(".") && !spec.startsWith("/")) return void 0;
|
|
3841
|
+
try {
|
|
3842
|
+
const base = dirname6(resolve5(fromFile));
|
|
3843
|
+
const p = resolve5(base, spec);
|
|
3844
|
+
for (const cand of [
|
|
3845
|
+
p,
|
|
3846
|
+
`${p}.js`,
|
|
3847
|
+
`${p}.mjs`,
|
|
3848
|
+
`${p}.ts`,
|
|
3849
|
+
join3(p, "index.js"),
|
|
3850
|
+
join3(p, "index.mjs"),
|
|
3851
|
+
join3(p, "index.ts")
|
|
3852
|
+
]) {
|
|
3853
|
+
if (existsSync4(cand) && !statSync4(cand).isDirectory()) {
|
|
3854
|
+
return readFileSync5(cand, "utf-8");
|
|
3855
|
+
}
|
|
3856
|
+
}
|
|
3857
|
+
return void 0;
|
|
3858
|
+
} catch {
|
|
3859
|
+
return void 0;
|
|
3860
|
+
}
|
|
3861
|
+
}
|
|
3862
|
+
|
|
3863
|
+
// src/session-cache.ts
|
|
3864
|
+
import {
|
|
3865
|
+
resetGeneralizeMemo,
|
|
3866
|
+
resetCheckSourceMemo,
|
|
3867
|
+
resetNudoModuleExecCache,
|
|
3868
|
+
resetParseSourceCache
|
|
3869
|
+
} from "@nudojs/core";
|
|
3870
|
+
function evictAnalysisCachesForFiles(files) {
|
|
3871
|
+
if (files.length === 0) return;
|
|
3872
|
+
evictBPathCacheForFiles(files);
|
|
3873
|
+
evictAnalysisFileCacheForFiles(files);
|
|
3874
|
+
evictFnAnalysisCacheForFiles(files);
|
|
3875
|
+
evictAbsModuleCacheFiles(files);
|
|
3876
|
+
}
|
|
3877
|
+
function clearAnalysisSessionCaches() {
|
|
3878
|
+
clearBPathCache();
|
|
3879
|
+
clearAbsModuleCache();
|
|
3880
|
+
resetGeneralizeMemo();
|
|
3881
|
+
resetCheckSourceMemo();
|
|
3882
|
+
resetNudoModuleExecCache();
|
|
3883
|
+
}
|
|
3884
|
+
function resetAllAnalysisCaches() {
|
|
3885
|
+
clearAnalysisSessionCaches();
|
|
3886
|
+
resetParseSourceCache();
|
|
3887
|
+
}
|
|
3888
|
+
|
|
3889
|
+
// src/harvest-node.ts
|
|
3890
|
+
import { existsSync as existsSync5 } from "fs";
|
|
3891
|
+
import { harvestDts as harvestDts2 } from "@nudojs/harvester";
|
|
3892
|
+
function harvestNodeTypes(fromDir, maxFiles = 12, maxMs = 2500) {
|
|
3893
|
+
const root = resolvePackageRoot("@types/node", fromDir) ?? resolvePackageRoot("node", fromDir);
|
|
3894
|
+
if (!root || !existsSync5(root)) {
|
|
3895
|
+
return { ok: false, error: "@types/node not found" };
|
|
3896
|
+
}
|
|
3897
|
+
const dts = collectDtsFiles(root, maxFiles);
|
|
3898
|
+
if (dts.length === 0) {
|
|
3899
|
+
return { ok: false, error: `no .d.ts under ${root}` };
|
|
3900
|
+
}
|
|
3901
|
+
const env = harvestDts2(dts, { maxMs });
|
|
3902
|
+
return { ok: true, env, root, files: env.stats.files };
|
|
3903
|
+
}
|
|
3904
|
+
function summarizeNodeEnv(env) {
|
|
3905
|
+
return {
|
|
3906
|
+
modules: Object.keys(env.modules).sort(),
|
|
3907
|
+
globals: Object.keys(env.globals).sort().slice(0, 50),
|
|
3908
|
+
symbolCount: env.stats.symbols
|
|
3909
|
+
};
|
|
3910
|
+
}
|
|
3911
|
+
|
|
3912
|
+
// src/semantic-tokens.ts
|
|
3913
|
+
import { dirname as dirname7 } from "path";
|
|
3914
|
+
import { createEnvironment as createEnvironment3 } from "@nudojs/core";
|
|
3915
|
+
import { parse as parse6 } from "@nudojs/parser";
|
|
3916
|
+
import {
|
|
3917
|
+
evaluateProgram as evaluateProgram2,
|
|
3918
|
+
setModuleResolver as setModuleResolver2,
|
|
3919
|
+
setCurrentFileDir as setCurrentFileDir2,
|
|
3920
|
+
resetMemo as resetMemo2
|
|
3921
|
+
} from "@nudojs/cli/evaluator";
|
|
3922
|
+
var SEMANTIC_TOKEN_TYPES = [
|
|
3923
|
+
"function",
|
|
3924
|
+
"variable",
|
|
3925
|
+
"parameter",
|
|
3926
|
+
"property",
|
|
3927
|
+
"type",
|
|
3928
|
+
"keyword",
|
|
3929
|
+
"string",
|
|
3930
|
+
"number",
|
|
3931
|
+
"comment",
|
|
3932
|
+
"decorator",
|
|
3933
|
+
"method"
|
|
3934
|
+
];
|
|
3935
|
+
var SEMANTIC_TOKEN_MODIFIERS = [
|
|
3936
|
+
"declaration",
|
|
3937
|
+
"readonly",
|
|
3938
|
+
"deprecated",
|
|
3939
|
+
"unreachable"
|
|
3940
|
+
];
|
|
3941
|
+
function encodeSemanticTokens(tokens) {
|
|
3942
|
+
const result = [];
|
|
3943
|
+
let prevLine = 0;
|
|
3944
|
+
let prevChar = 0;
|
|
3945
|
+
for (const token of tokens) {
|
|
3946
|
+
const deltaLine = token.line - prevLine;
|
|
3947
|
+
const deltaChar = deltaLine === 0 ? token.char - prevChar : token.char;
|
|
3948
|
+
result.push(deltaLine, deltaChar, token.length, token.typeIndex, token.modifierBitmask);
|
|
3949
|
+
prevLine = token.line;
|
|
3950
|
+
prevChar = token.char;
|
|
3951
|
+
}
|
|
3952
|
+
return result;
|
|
3953
|
+
}
|
|
3954
|
+
var TYPE_FUNCTION = SEMANTIC_TOKEN_TYPES.indexOf("function");
|
|
3955
|
+
var TYPE_VARIABLE = SEMANTIC_TOKEN_TYPES.indexOf("variable");
|
|
3956
|
+
var TYPE_PARAMETER = SEMANTIC_TOKEN_TYPES.indexOf("parameter");
|
|
3957
|
+
var TYPE_PROPERTY = SEMANTIC_TOKEN_TYPES.indexOf("property");
|
|
3958
|
+
var TYPE_METHOD = SEMANTIC_TOKEN_TYPES.indexOf("method");
|
|
3959
|
+
var MOD_DECLARATION = 1 << SEMANTIC_TOKEN_MODIFIERS.indexOf("declaration");
|
|
3960
|
+
function buildSemanticTokens(filePath, source) {
|
|
3961
|
+
let ast;
|
|
3962
|
+
try {
|
|
3963
|
+
ast = parse6(source);
|
|
3964
|
+
} catch {
|
|
3965
|
+
return [];
|
|
3966
|
+
}
|
|
3967
|
+
const env = createEnvironment3();
|
|
3968
|
+
resetMemo2();
|
|
3969
|
+
setModuleResolver2(resolveModule);
|
|
3970
|
+
setCurrentFileDir2(dirname7(filePath));
|
|
3971
|
+
try {
|
|
3972
|
+
evaluateProgram2(ast, env);
|
|
3973
|
+
} catch {
|
|
3974
|
+
} finally {
|
|
3975
|
+
setModuleResolver2(null);
|
|
3976
|
+
}
|
|
3977
|
+
const program = ast.program ?? ast;
|
|
3978
|
+
const ownBindings = env.getOwnBindings();
|
|
3979
|
+
const topLevelDeclarators = /* @__PURE__ */ new Set();
|
|
3980
|
+
for (const stmt of program.body ?? []) {
|
|
3981
|
+
const decl = stmt.type === "ExportNamedDeclaration" || stmt.type === "ExportDefaultDeclaration" ? stmt.declaration ?? null : stmt;
|
|
3982
|
+
if (decl && decl.type === "VariableDeclaration") {
|
|
3983
|
+
for (const d of decl.declarations) {
|
|
3984
|
+
topLevelDeclarators.add(d);
|
|
3985
|
+
}
|
|
3986
|
+
}
|
|
3987
|
+
}
|
|
3988
|
+
const tokens = [];
|
|
3989
|
+
const pushIdentifier = (id, typeIndex) => {
|
|
3990
|
+
const loc = id.loc;
|
|
3991
|
+
const name = id.name;
|
|
3992
|
+
if (!loc || typeof name !== "string") return;
|
|
3993
|
+
tokens.push({
|
|
3994
|
+
line: loc.start.line - 1,
|
|
3995
|
+
char: loc.start.column,
|
|
3996
|
+
length: name.length,
|
|
3997
|
+
typeIndex,
|
|
3998
|
+
modifierBitmask: MOD_DECLARATION
|
|
3999
|
+
});
|
|
4000
|
+
};
|
|
4001
|
+
const collectParams = (params) => {
|
|
4002
|
+
for (const p of params) {
|
|
4003
|
+
if (!p) continue;
|
|
4004
|
+
if (p.type === "Identifier") pushIdentifier(p, TYPE_PARAMETER);
|
|
4005
|
+
else if (p.type === "AssignmentPattern" && p.left?.type === "Identifier") {
|
|
4006
|
+
pushIdentifier(p.left, TYPE_PARAMETER);
|
|
4007
|
+
} else if (p.type === "RestElement" && p.argument?.type === "Identifier") {
|
|
4008
|
+
pushIdentifier(p.argument, TYPE_PARAMETER);
|
|
4009
|
+
}
|
|
4010
|
+
}
|
|
4011
|
+
};
|
|
4012
|
+
const isFunctionValue = (name) => ownBindings[name]?.kind === "function";
|
|
4013
|
+
const visit = (node) => {
|
|
4014
|
+
if (!node || typeof node !== "object") return;
|
|
4015
|
+
const n = node;
|
|
4016
|
+
switch (n.type) {
|
|
4017
|
+
case "VariableDeclarator": {
|
|
4018
|
+
const id = n.id;
|
|
4019
|
+
if (id?.type === "Identifier") {
|
|
4020
|
+
const isTopLevel = topLevelDeclarators.has(node);
|
|
4021
|
+
const typeIndex = isTopLevel && isFunctionValue(id.name) ? TYPE_FUNCTION : TYPE_VARIABLE;
|
|
4022
|
+
pushIdentifier(id, typeIndex);
|
|
4023
|
+
}
|
|
4024
|
+
break;
|
|
4025
|
+
}
|
|
4026
|
+
case "FunctionDeclaration":
|
|
4027
|
+
case "FunctionExpression": {
|
|
4028
|
+
const id = n.id;
|
|
4029
|
+
if (id?.type === "Identifier") pushIdentifier(id, TYPE_FUNCTION);
|
|
4030
|
+
collectParams(n.params ?? []);
|
|
4031
|
+
break;
|
|
4032
|
+
}
|
|
4033
|
+
case "ArrowFunctionExpression":
|
|
4034
|
+
collectParams(n.params ?? []);
|
|
4035
|
+
break;
|
|
4036
|
+
case "ObjectMethod": {
|
|
4037
|
+
const key = n.key;
|
|
4038
|
+
if (key?.type === "Identifier" && !n.computed) pushIdentifier(key, TYPE_METHOD);
|
|
4039
|
+
collectParams(n.params ?? []);
|
|
4040
|
+
break;
|
|
4041
|
+
}
|
|
4042
|
+
case "ObjectProperty": {
|
|
4043
|
+
const key = n.key;
|
|
4044
|
+
const value = n.value;
|
|
4045
|
+
if (key?.type === "Identifier" && !n.computed) {
|
|
4046
|
+
const isFn = value?.type === "FunctionExpression" || value?.type === "ArrowFunctionExpression";
|
|
4047
|
+
pushIdentifier(key, isFn ? TYPE_METHOD : TYPE_PROPERTY);
|
|
4048
|
+
}
|
|
4049
|
+
break;
|
|
4050
|
+
}
|
|
4051
|
+
}
|
|
4052
|
+
for (const key of Object.keys(n)) {
|
|
4053
|
+
if (key === "loc" || key === "start" || key === "end" || key === "leadingComments" || key === "trailingComments" || key === "innerComments") continue;
|
|
4054
|
+
const child = n[key];
|
|
4055
|
+
if (Array.isArray(child)) {
|
|
4056
|
+
for (const item of child) {
|
|
4057
|
+
if (item && typeof item === "object" && typeof item.type === "string") {
|
|
4058
|
+
visit(item);
|
|
4059
|
+
}
|
|
4060
|
+
}
|
|
4061
|
+
} else if (child && typeof child === "object" && typeof child.type === "string") {
|
|
4062
|
+
visit(child);
|
|
4063
|
+
}
|
|
4064
|
+
}
|
|
4065
|
+
};
|
|
4066
|
+
visit(program);
|
|
4067
|
+
tokens.sort((a, b) => a.line - b.line || a.char - b.char);
|
|
4068
|
+
return encodeSemanticTokens(tokens);
|
|
4069
|
+
}
|
|
1303
4070
|
|
|
1304
4071
|
// src/dts-generator.ts
|
|
1305
|
-
import { T as
|
|
4072
|
+
import { T as T3, isTemplate, getTemplateParts, simplifyUnion as simplifyUnion2, typeValueEquals, getFnSig as getFnSig4 } from "@nudojs/core";
|
|
1306
4073
|
function typeValueToTSType(tv) {
|
|
1307
4074
|
switch (tv.kind) {
|
|
1308
4075
|
case "literal": {
|
|
@@ -1337,8 +4104,8 @@ function typeValueToTSType(tv) {
|
|
|
1337
4104
|
}
|
|
1338
4105
|
case "function": {
|
|
1339
4106
|
const params = tv.params.map((p) => `${p}: unknown`).join(", ");
|
|
1340
|
-
const
|
|
1341
|
-
const retStr = returnType ? typeValueToTSType(returnType) : "unknown";
|
|
4107
|
+
const sig = getFnSig4(tv);
|
|
4108
|
+
const retStr = sig?.returnType ? typeValueToTSType(sig.returnType) : "unknown";
|
|
1342
4109
|
return `(${params}) => ${retStr}`;
|
|
1343
4110
|
}
|
|
1344
4111
|
case "promise":
|
|
@@ -1367,10 +4134,10 @@ function getParamName(fn, index) {
|
|
|
1367
4134
|
function widenLiteralToBase(tv) {
|
|
1368
4135
|
if (tv.kind !== "literal") return tv;
|
|
1369
4136
|
const v = tv.value;
|
|
1370
|
-
if (typeof v === "number") return
|
|
1371
|
-
if (typeof v === "string") return
|
|
1372
|
-
if (typeof v === "boolean") return
|
|
1373
|
-
if (typeof v === "bigint") return
|
|
4137
|
+
if (typeof v === "number") return T3.number;
|
|
4138
|
+
if (typeof v === "string") return T3.string;
|
|
4139
|
+
if (typeof v === "boolean") return T3.boolean;
|
|
4140
|
+
if (typeof v === "bigint") return T3.bigint;
|
|
1374
4141
|
return tv;
|
|
1375
4142
|
}
|
|
1376
4143
|
function widenTopLevel(tv) {
|
|
@@ -1392,18 +4159,18 @@ function widenParamType(tv) {
|
|
|
1392
4159
|
const widened = tv.elements.map(widenParamType);
|
|
1393
4160
|
const first = widened[0];
|
|
1394
4161
|
if (first && widened.every((el) => typeValueEquals(el, first))) {
|
|
1395
|
-
return
|
|
4162
|
+
return T3.array(first);
|
|
1396
4163
|
}
|
|
1397
|
-
return
|
|
4164
|
+
return T3.tuple(widened);
|
|
1398
4165
|
}
|
|
1399
4166
|
case "array":
|
|
1400
|
-
return
|
|
4167
|
+
return T3.array(widenParamType(tv.element));
|
|
1401
4168
|
case "object":
|
|
1402
|
-
return
|
|
4169
|
+
return T3.object(widenProperties(tv.properties));
|
|
1403
4170
|
case "promise":
|
|
1404
|
-
return
|
|
4171
|
+
return T3.promise(widenParamType(tv.value));
|
|
1405
4172
|
case "instance":
|
|
1406
|
-
return
|
|
4173
|
+
return T3.instanceOf(tv.className, widenProperties(tv.properties));
|
|
1407
4174
|
case "refined":
|
|
1408
4175
|
return widenParamType(tv.base);
|
|
1409
4176
|
default:
|
|
@@ -1539,12 +4306,19 @@ function typeValueToZodSchema(tv) {
|
|
|
1539
4306
|
}
|
|
1540
4307
|
|
|
1541
4308
|
// src/guard-generator.ts
|
|
4309
|
+
import { denoteGuard } from "@nudojs/core";
|
|
1542
4310
|
function generateGuardFunction(name, tv) {
|
|
1543
4311
|
const body = generateGuardBody(tv, "data");
|
|
1544
4312
|
return `export function ${name}(data) {
|
|
1545
4313
|
return ${body};
|
|
1546
4314
|
}`;
|
|
1547
4315
|
}
|
|
4316
|
+
function generateGuardFunctionFromAbs(name, abs) {
|
|
4317
|
+
const body = denoteGuard(abs, "data");
|
|
4318
|
+
return `export function ${name}(data) {
|
|
4319
|
+
return ${body};
|
|
4320
|
+
}`;
|
|
4321
|
+
}
|
|
1548
4322
|
function generateGuardBody(tv, varName) {
|
|
1549
4323
|
switch (tv.kind) {
|
|
1550
4324
|
case "literal": {
|
|
@@ -1590,7 +4364,7 @@ function generateGuardBody(tv, varName) {
|
|
|
1590
4364
|
}
|
|
1591
4365
|
|
|
1592
4366
|
// src/case-emitter.ts
|
|
1593
|
-
import { parse as
|
|
4367
|
+
import { parse as parse7, extractDirectives as extractDirectives2 } from "@nudojs/parser";
|
|
1594
4368
|
var GENERATED_PREFIX = "call@";
|
|
1595
4369
|
var GENERATED_CASE_LINE_REGEX = /^\s*\*?\s*@nudo:case\s+"(call@[^"]*)"/;
|
|
1596
4370
|
var IDENT_KEY_REGEX = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
@@ -1636,6 +4410,7 @@ function serializeCaseArg(tv) {
|
|
|
1636
4410
|
if (typeof v === "boolean") return v ? "true" : "false";
|
|
1637
4411
|
if (v === null) return "null";
|
|
1638
4412
|
if (v === void 0) return "undefined";
|
|
4413
|
+
if (typeof v === "bigint") return null;
|
|
1639
4414
|
return serializeStringLiteral(v);
|
|
1640
4415
|
}
|
|
1641
4416
|
case "union": {
|
|
@@ -1732,7 +4507,7 @@ function collectExistingCases(source) {
|
|
|
1732
4507
|
const byName = /* @__PURE__ */ new Map();
|
|
1733
4508
|
let ast;
|
|
1734
4509
|
try {
|
|
1735
|
-
ast =
|
|
4510
|
+
ast = parse7(source, { errorRecovery: true });
|
|
1736
4511
|
} catch {
|
|
1737
4512
|
return byName;
|
|
1738
4513
|
}
|
|
@@ -1942,23 +4717,78 @@ function unifiedDiff(a, b, path) {
|
|
|
1942
4717
|
return out.join("\n") + "\n";
|
|
1943
4718
|
}
|
|
1944
4719
|
export {
|
|
4720
|
+
SEMANTIC_TOKEN_MODIFIERS,
|
|
4721
|
+
SEMANTIC_TOKEN_TYPES,
|
|
4722
|
+
analyzeExportsFromSource,
|
|
1945
4723
|
analyzeFile,
|
|
1946
4724
|
analyzeFileAsync,
|
|
4725
|
+
autoHarvestModules,
|
|
4726
|
+
barePackageName,
|
|
4727
|
+
bareSpecToAbsModules,
|
|
1947
4728
|
buildCaseDirective,
|
|
1948
4729
|
buildModuleGraph,
|
|
4730
|
+
buildSemanticTokens,
|
|
4731
|
+
clearAbsModuleCache,
|
|
4732
|
+
clearAnalysisFileCache,
|
|
4733
|
+
clearAnalysisSessionCaches,
|
|
4734
|
+
clearBPathCache,
|
|
4735
|
+
clearFnAnalysisCache,
|
|
4736
|
+
clearHarvestCache,
|
|
4737
|
+
collectAbsBindingsFromGraph,
|
|
4738
|
+
collectAbsInlays,
|
|
4739
|
+
collectBPathDiagnostics,
|
|
4740
|
+
collectBPathReplacements,
|
|
4741
|
+
collectBarePackages,
|
|
1949
4742
|
collectCallRecords,
|
|
4743
|
+
collectDependencySpecs,
|
|
4744
|
+
collectDtsFromEntry,
|
|
4745
|
+
collectEnvGlobals,
|
|
4746
|
+
collectEnvModules,
|
|
4747
|
+
collectStaticImports,
|
|
1950
4748
|
computeDirtySet,
|
|
4749
|
+
defaultAbsLoadModule,
|
|
4750
|
+
defaultLoadModule,
|
|
4751
|
+
encodeSemanticTokens,
|
|
4752
|
+
envValueToAbs,
|
|
4753
|
+
evalAbsModuleGraph,
|
|
4754
|
+
evalProgramAbsWithModules,
|
|
4755
|
+
evictAbsModuleCacheFiles,
|
|
4756
|
+
evictAnalysisCachesForFiles,
|
|
4757
|
+
evictAnalysisFileCacheForFiles,
|
|
4758
|
+
evictBPathCacheForFiles,
|
|
4759
|
+
evictFnAnalysisCacheForFiles,
|
|
4760
|
+
formatHarvestSummary,
|
|
1951
4761
|
generateDts,
|
|
1952
4762
|
generateFunctionDtsLines,
|
|
1953
4763
|
generateGuardFunction,
|
|
4764
|
+
generateGuardFunctionFromAbs,
|
|
4765
|
+
getAnalysisFileCacheSize,
|
|
1954
4766
|
getCasesForFile,
|
|
1955
4767
|
getCompletionsAtPosition,
|
|
4768
|
+
getHoverAtPosition,
|
|
1956
4769
|
getTypeAtPosition,
|
|
1957
4770
|
getTypeAtPositionAsync,
|
|
4771
|
+
harvestNodeTypes,
|
|
4772
|
+
harvestPackage,
|
|
4773
|
+
harvestPackageCached,
|
|
4774
|
+
harvestToAbsModules,
|
|
4775
|
+
harvestedValueToAbs,
|
|
1958
4776
|
insertGeneratedCaseDirectives,
|
|
4777
|
+
isBPathCapable,
|
|
4778
|
+
isNudoTargetPath,
|
|
4779
|
+
lookupHarvested,
|
|
4780
|
+
mockDirectivesToAbsSeeds,
|
|
4781
|
+
packageHarvestToAbsModules,
|
|
4782
|
+
resetAllAnalysisCaches,
|
|
4783
|
+
resolvePackageRoot,
|
|
1959
4784
|
serializeCaseArg,
|
|
4785
|
+
serializeInferJson,
|
|
1960
4786
|
stripGeneratedCaseDirectives,
|
|
4787
|
+
summarizeNodeEnv,
|
|
1961
4788
|
topoSortDirty,
|
|
4789
|
+
tryBPathCall,
|
|
4790
|
+
tryBPathCallFull,
|
|
4791
|
+
tryRunBPath,
|
|
1962
4792
|
typeValueToTSType,
|
|
1963
4793
|
typeValueToZodSchema,
|
|
1964
4794
|
unifiedDiff
|