@nudojs/service 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/README.md +27 -0
- package/dist/index.d.ts +638 -5
- package/dist/index.js +4439 -208
- package/package.json +17 -4
- package/src/__tests__/abs-bindings.test.ts +53 -0
- package/src/__tests__/abs-call-records.test.ts +33 -0
- package/src/__tests__/abs-calls.test.ts +59 -0
- package/src/__tests__/abs-module-cache.test.ts +121 -0
- package/src/__tests__/abs-modules-graph.test.ts +100 -0
- package/src/__tests__/abs-typevalue-diff.test.ts +111 -0
- package/src/__tests__/analyzer.test.ts +745 -2
- package/src/__tests__/as-replace.test.ts +165 -0
- package/src/__tests__/bpath-arg-provenance.test.ts +104 -0
- package/src/__tests__/bpath-as.test.ts +54 -0
- package/src/__tests__/bpath-cache-key.test.ts +33 -0
- package/src/__tests__/bpath-calls.test.ts +55 -0
- package/src/__tests__/bpath-diagnostics.test.ts +78 -0
- package/src/__tests__/bpath-env-modules.test.ts +30 -0
- package/src/__tests__/bpath-env.test.ts +80 -0
- package/src/__tests__/bpath-hosted-skip.test.ts +71 -0
- package/src/__tests__/bpath-import-default-ns.test.ts +106 -0
- package/src/__tests__/bpath-method-dedupe.test.ts +65 -0
- package/src/__tests__/bpath-method-diag.test.ts +45 -0
- package/src/__tests__/bpath-module-diags.test.ts +101 -0
- package/src/__tests__/bpath-path-env.test.ts +46 -0
- package/src/__tests__/bpath-reexport.test.ts +131 -0
- package/src/__tests__/bpath-replace.test.ts +52 -0
- package/src/__tests__/bpath-require.test.ts +66 -0
- package/src/__tests__/bpath-run.test.ts +109 -0
- package/src/__tests__/case-emitter.test.ts +636 -0
- package/src/__tests__/cjs-require.test.ts +54 -0
- package/src/__tests__/diagnostics.test.ts +60 -0
- package/src/__tests__/dts-generator.test.ts +290 -6
- package/src/__tests__/fn-cache-evict.test.ts +56 -0
- package/src/__tests__/fn-sig-impl.test.ts +247 -0
- package/src/__tests__/guard-generator.test.ts +29 -0
- package/src/__tests__/harvest-auto.test.ts +43 -0
- package/src/__tests__/harvest-node.test.ts +24 -0
- package/src/__tests__/harvest-package.test.ts +38 -0
- package/src/__tests__/harvest-to-abs.test.ts +46 -0
- package/src/__tests__/hover-intension.test.ts +39 -0
- package/src/__tests__/infer-json.test.ts +35 -0
- package/src/__tests__/integration.test.ts +421 -0
- package/src/__tests__/m3-intension.test.ts +28 -0
- package/src/__tests__/mock-seed-fingerprint.test.ts +64 -0
- package/src/__tests__/schema-generator.test.ts +38 -0
- package/src/__tests__/session-cache-evict.test.ts +90 -0
- package/src/__tests__/static-imports.test.ts +44 -0
- package/src/abs-modules-graph.ts +387 -0
- package/src/analysis-file-cache.ts +51 -0
- package/src/analyzer.ts +2569 -94
- package/src/bpath-diagnostics.ts +283 -0
- package/src/bpath-run.ts +409 -0
- package/src/case-emitter.ts +477 -0
- package/src/dts-generator.ts +220 -24
- package/src/env-to-abs.ts +57 -0
- package/src/fn-analysis-cache.ts +119 -0
- package/src/guard-generator.ts +57 -0
- package/src/harvest-auto.ts +86 -0
- package/src/harvest-node.ts +48 -0
- package/src/harvest-package.ts +204 -0
- package/src/harvest-to-abs.ts +129 -0
- package/src/index.ts +128 -0
- package/src/infer-json.ts +137 -0
- package/src/load-module.ts +34 -0
- package/src/mock-abs.ts +275 -0
- package/src/schema-generator.ts +43 -0
- package/src/semantic-tokens.ts +216 -0
- package/src/session-cache.ts +50 -0
- package/src/static-imports.ts +241 -0
- package/src/target-path.ts +17 -0
package/dist/index.js
CHANGED
|
@@ -1,18 +1,42 @@
|
|
|
1
1
|
// src/analyzer.ts
|
|
2
|
-
import { readFileSync, existsSync } 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
|
-
|
|
10
|
-
|
|
9
|
+
widenLiteral,
|
|
10
|
+
collapseLiteralUnion,
|
|
11
|
+
createEnvironment as createEnvironment2,
|
|
12
|
+
isSubtypeOf,
|
|
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
|
|
11
31
|
} from "@nudojs/core";
|
|
12
|
-
import { parse, extractDirectives, parseTypeValueExpr } from "@nudojs/parser";
|
|
32
|
+
import { parse as parse5, extractDirectives, extractFileDirectives, parseTypeValueExpr } from "@nudojs/parser";
|
|
13
33
|
import {
|
|
34
|
+
evaluate,
|
|
14
35
|
evaluateFunction,
|
|
15
36
|
evaluateFunctionFull,
|
|
37
|
+
memberMayExistOn,
|
|
38
|
+
setUsageSiteTag,
|
|
39
|
+
USAGE_SITE_MODULE,
|
|
16
40
|
evaluateProgram,
|
|
17
41
|
setModuleResolver,
|
|
18
42
|
setCurrentFileDir,
|
|
@@ -20,44 +44,2266 @@ import {
|
|
|
20
44
|
getUnreachableRanges,
|
|
21
45
|
resetUnreachableRanges,
|
|
22
46
|
setNodeTypeCollector,
|
|
23
|
-
|
|
47
|
+
setCallCollector,
|
|
48
|
+
setUnknownCollector,
|
|
49
|
+
setProvenanceTracking,
|
|
50
|
+
setSampleCount,
|
|
51
|
+
setUnknownBuiltinHandler,
|
|
52
|
+
setEnvModules,
|
|
53
|
+
resetEnvModules,
|
|
54
|
+
setMockModules,
|
|
55
|
+
resetMockModules,
|
|
56
|
+
setCurrentSource,
|
|
57
|
+
loadEnvs as loadEnvs2,
|
|
58
|
+
preloadPathEnvs,
|
|
59
|
+
findProjectConfig,
|
|
60
|
+
resolveNpmNudo,
|
|
61
|
+
BUILTIN_PROTOTYPE_METHOD_APPROXIMATIONS
|
|
24
62
|
} from "@nudojs/cli/evaluator";
|
|
63
|
+
|
|
64
|
+
// src/mock-abs.ts
|
|
65
|
+
import {
|
|
66
|
+
absFunction,
|
|
67
|
+
emptyEnv,
|
|
68
|
+
typeValueToAbs,
|
|
69
|
+
abs as makeAbs,
|
|
70
|
+
confJoin,
|
|
71
|
+
unknown as absUnknown,
|
|
72
|
+
litValue,
|
|
73
|
+
formatAbs,
|
|
74
|
+
getFnImpl,
|
|
75
|
+
typeValueToString
|
|
76
|
+
} from "@nudojs/core";
|
|
77
|
+
function markMockConf(a) {
|
|
78
|
+
a.conf = confJoin(a.conf, "mock");
|
|
79
|
+
return a;
|
|
80
|
+
}
|
|
81
|
+
function astContentKey(n, depth = 0) {
|
|
82
|
+
if (n == null || depth > 24) return "";
|
|
83
|
+
if (typeof n !== "object") return String(n);
|
|
84
|
+
if (Array.isArray(n)) return n.map((x) => astContentKey(x, depth + 1)).join(",");
|
|
85
|
+
const obj = n;
|
|
86
|
+
const type = typeof obj.type === "string" ? obj.type : "";
|
|
87
|
+
const parts = [type];
|
|
88
|
+
for (const key of ["name", "value", "raw", "operator", "computed"]) {
|
|
89
|
+
const v = obj[key];
|
|
90
|
+
if (v !== void 0 && (typeof v !== "object" || v === null)) parts.push(`${key}=${String(v)}`);
|
|
91
|
+
}
|
|
92
|
+
for (const key of Object.keys(obj)) {
|
|
93
|
+
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") {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const v = obj[key];
|
|
97
|
+
if (v && typeof v === "object") parts.push(`${key}:{${astContentKey(v, depth + 1)}}`);
|
|
98
|
+
}
|
|
99
|
+
return parts.join("|");
|
|
100
|
+
}
|
|
101
|
+
function stampFingerprint(a, fp) {
|
|
102
|
+
const impl = getFnImpl(a);
|
|
103
|
+
if (impl) impl.fingerprint = fp;
|
|
104
|
+
return a;
|
|
105
|
+
}
|
|
106
|
+
function constantMockFn(result) {
|
|
107
|
+
const marked = markMockConf(result);
|
|
108
|
+
const mockEnv = emptyEnv();
|
|
109
|
+
const retName = "__nudo_mock_ret";
|
|
110
|
+
mockEnv.vars.set(retName, marked);
|
|
111
|
+
const body = {
|
|
112
|
+
type: "BlockStatement",
|
|
113
|
+
body: [
|
|
114
|
+
{
|
|
115
|
+
type: "ReturnStatement",
|
|
116
|
+
argument: { type: "Identifier", name: retName }
|
|
117
|
+
}
|
|
118
|
+
]
|
|
119
|
+
};
|
|
120
|
+
let retKey;
|
|
121
|
+
try {
|
|
122
|
+
retKey = formatAbs(marked);
|
|
123
|
+
} catch {
|
|
124
|
+
retKey = "?";
|
|
125
|
+
}
|
|
126
|
+
return stampFingerprint(
|
|
127
|
+
markMockConf(absFunction(["...args"], { body, env: mockEnv })),
|
|
128
|
+
`ret=${retKey}`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
function absArgMatches(declared, actual) {
|
|
132
|
+
if (!actual) return false;
|
|
133
|
+
const declaredAbs = typeValueToAbs(declared);
|
|
134
|
+
const av = litValue(actual);
|
|
135
|
+
const dv = litValue(declaredAbs);
|
|
136
|
+
if (dv !== void 0) {
|
|
137
|
+
return av !== void 0 && Object.is(av, dv);
|
|
138
|
+
}
|
|
139
|
+
if (declared.kind === "primitive") {
|
|
140
|
+
if (actual.shape.k === "prim") return actual.shape.type === declared.type;
|
|
141
|
+
if (av !== void 0) {
|
|
142
|
+
const t = typeof av;
|
|
143
|
+
return declared.type === "number" && t === "number" || declared.type === "string" && t === "string" || declared.type === "boolean" && t === "boolean" || declared.type === "bigint" && t === "bigint";
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
function dispatchMockFn(defaultReturn, cases) {
|
|
149
|
+
if (!cases?.length) return constantMockFn(defaultReturn);
|
|
150
|
+
const caseAbs = cases.map((c) => ({
|
|
151
|
+
declared: c.args,
|
|
152
|
+
result: markMockConf(typeValueToAbs(c.returnValue))
|
|
153
|
+
}));
|
|
154
|
+
const dummyBody = {
|
|
155
|
+
type: "BlockStatement",
|
|
156
|
+
body: [{ type: "ReturnStatement", argument: null }]
|
|
157
|
+
};
|
|
158
|
+
const markedDefault = markMockConf(defaultReturn);
|
|
159
|
+
let defaultKey;
|
|
160
|
+
try {
|
|
161
|
+
defaultKey = formatAbs(markedDefault);
|
|
162
|
+
} catch {
|
|
163
|
+
defaultKey = "?";
|
|
164
|
+
}
|
|
165
|
+
const caseKey = cases.map(
|
|
166
|
+
(c) => `${c.args.map((a) => typeValueToString(a)).join(",")}->${typeValueToString(c.returnValue)}`
|
|
167
|
+
).join("|");
|
|
168
|
+
return stampFingerprint(
|
|
169
|
+
markMockConf(
|
|
170
|
+
absFunction(["...args"], {
|
|
171
|
+
body: dummyBody,
|
|
172
|
+
env: emptyEnv(),
|
|
173
|
+
apply: (args) => {
|
|
174
|
+
for (const c of caseAbs) {
|
|
175
|
+
if (c.declared.every((d, i) => absArgMatches(d, args[i]))) {
|
|
176
|
+
return c.result;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return markedDefault;
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
),
|
|
183
|
+
`dispatch=default=${defaultKey};cases=${caseKey}`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
function absFromMockHelper(h) {
|
|
187
|
+
if (h.callsFakeImpl && h.callsFakeImpl.kind === "function") {
|
|
188
|
+
const fn = h.callsFakeImpl;
|
|
189
|
+
const body = fn.body;
|
|
190
|
+
return stampFingerprint(
|
|
191
|
+
absFunction(fn.params, { body, async: false }),
|
|
192
|
+
`fake=${fn.params.join(",")}:${astContentKey(body)}`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
let defaultReturn;
|
|
196
|
+
if (h.resolvedValue) {
|
|
197
|
+
defaultReturn = makeAbs(
|
|
198
|
+
{ k: "eff", eff: "promise", inner: typeValueToAbs(h.resolvedValue) },
|
|
199
|
+
void 0,
|
|
200
|
+
void 0,
|
|
201
|
+
"path"
|
|
202
|
+
);
|
|
203
|
+
} else if (h.rejectedValue) {
|
|
204
|
+
defaultReturn = makeAbs({ k: "never" }, void 0, void 0, "exact");
|
|
205
|
+
} else if (h.returnValue) {
|
|
206
|
+
defaultReturn = typeValueToAbs(h.returnValue);
|
|
207
|
+
} else if (h.onFirstCallValue) {
|
|
208
|
+
defaultReturn = typeValueToAbs(h.onFirstCallValue);
|
|
209
|
+
} else {
|
|
210
|
+
defaultReturn = absUnknown;
|
|
211
|
+
}
|
|
212
|
+
return dispatchMockFn(defaultReturn, h.withArgsCases);
|
|
213
|
+
}
|
|
214
|
+
function absFromSinon(sinonExpr) {
|
|
215
|
+
if (sinonExpr.resolvedValue) {
|
|
216
|
+
return constantMockFn(
|
|
217
|
+
makeAbs(
|
|
218
|
+
{ k: "eff", eff: "promise", inner: typeValueToAbs(sinonExpr.resolvedValue) },
|
|
219
|
+
void 0,
|
|
220
|
+
void 0,
|
|
221
|
+
"path"
|
|
222
|
+
)
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
if (sinonExpr.rejectedValue) {
|
|
226
|
+
return constantMockFn(makeAbs({ k: "never" }, void 0, void 0, "exact"));
|
|
227
|
+
}
|
|
228
|
+
if (sinonExpr.returnValue) {
|
|
229
|
+
return constantMockFn(typeValueToAbs(sinonExpr.returnValue));
|
|
230
|
+
}
|
|
231
|
+
return constantMockFn(absUnknown);
|
|
232
|
+
}
|
|
233
|
+
function mockSeedsToAbsMocks(seeds) {
|
|
234
|
+
const out = { ...seeds.seedVars };
|
|
235
|
+
for (const [name, fn] of Object.entries(seeds.seedFns)) {
|
|
236
|
+
out[name] = absFunction(fn.params, { body: fn.body, async: fn.async ?? false });
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
function mockDirectivesToAbsSeeds(functions) {
|
|
241
|
+
const seedVars = {};
|
|
242
|
+
const seedFns = {};
|
|
243
|
+
for (const fn of functions) {
|
|
244
|
+
for (const d of fn.directives) {
|
|
245
|
+
if (d.kind !== "mock") continue;
|
|
246
|
+
if (d.arrowFn) {
|
|
247
|
+
const body = d.arrowFn.body;
|
|
248
|
+
seedFns[d.name] = {
|
|
249
|
+
params: d.arrowFn.params,
|
|
250
|
+
body,
|
|
251
|
+
async: false,
|
|
252
|
+
fingerprint: `fake=${d.arrowFn.params.join(",")}:${astContentKey(body)}`
|
|
253
|
+
};
|
|
254
|
+
} else if (d.nudoMock) {
|
|
255
|
+
seedVars[d.name] = markMockConf(absFromMockHelper(d.nudoMock));
|
|
256
|
+
} else if (d.sinonExpr) {
|
|
257
|
+
seedVars[d.name] = markMockConf(absFromSinon(d.sinonExpr));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return { seedVars, seedFns };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/harvest-auto.ts
|
|
265
|
+
import { parse as parse2 } from "@nudojs/parser";
|
|
266
|
+
|
|
267
|
+
// src/static-imports.ts
|
|
268
|
+
import { parse } from "@nudojs/parser";
|
|
269
|
+
import { readFileSync, existsSync } from "fs";
|
|
270
|
+
import { resolve, dirname, join } from "path";
|
|
271
|
+
import { generalizeFromAst } from "@nudojs/core";
|
|
272
|
+
function resolveRelative(fromFile, spec) {
|
|
273
|
+
if (!spec.startsWith(".") && !spec.startsWith("/")) return void 0;
|
|
274
|
+
const base = resolve(dirname(fromFile), spec);
|
|
275
|
+
for (const cand of [
|
|
276
|
+
base,
|
|
277
|
+
base + ".js",
|
|
278
|
+
base + ".cjs",
|
|
279
|
+
base + ".mjs",
|
|
280
|
+
base + ".ts",
|
|
281
|
+
base + ".mts",
|
|
282
|
+
base + ".cts",
|
|
283
|
+
join(base, "index.js"),
|
|
284
|
+
join(base, "index.cjs"),
|
|
285
|
+
join(base, "index.mjs"),
|
|
286
|
+
join(base, "index.ts")
|
|
287
|
+
]) {
|
|
288
|
+
if (existsSync(cand)) return cand;
|
|
289
|
+
}
|
|
290
|
+
return void 0;
|
|
291
|
+
}
|
|
292
|
+
function collectDependencySpecs(ast) {
|
|
293
|
+
const specs = [];
|
|
294
|
+
for (const stmt of ast.program.body) {
|
|
295
|
+
if (stmt.type === "ImportDeclaration") {
|
|
296
|
+
specs.push(stmt.source.value);
|
|
297
|
+
}
|
|
298
|
+
if (stmt.type === "ExportNamedDeclaration" && stmt.source) {
|
|
299
|
+
specs.push(stmt.source.value);
|
|
300
|
+
}
|
|
301
|
+
if (stmt.type === "ExportAllDeclaration" && stmt.source) {
|
|
302
|
+
specs.push(stmt.source.value);
|
|
303
|
+
}
|
|
304
|
+
collectRequires(stmt, specs);
|
|
305
|
+
}
|
|
306
|
+
return specs;
|
|
307
|
+
}
|
|
308
|
+
function collectRequires(node, out) {
|
|
309
|
+
const visit = (n) => {
|
|
310
|
+
if (!n || typeof n !== "object") return;
|
|
311
|
+
const obj = n;
|
|
312
|
+
if (obj.type === "CallExpression" && obj.callee?.type === "Identifier" && obj.callee.name === "require" && obj.arguments?.[0]?.type === "StringLiteral") {
|
|
313
|
+
const spec = obj.arguments[0].value;
|
|
314
|
+
if (typeof spec === "string") out.push(spec);
|
|
315
|
+
}
|
|
316
|
+
for (const key of Object.keys(obj)) {
|
|
317
|
+
if (key === "loc" || key === "start" || key === "end") continue;
|
|
318
|
+
const v = obj[key];
|
|
319
|
+
if (Array.isArray(v)) v.forEach(visit);
|
|
320
|
+
else if (v && typeof v === "object") visit(v);
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
visit(node);
|
|
324
|
+
}
|
|
325
|
+
function analyzeExportsFromSource(filePath, source) {
|
|
326
|
+
const file = parse(source);
|
|
327
|
+
const named = /* @__PURE__ */ new Map();
|
|
328
|
+
let defaultExport;
|
|
329
|
+
const poly = /* @__PURE__ */ new Map();
|
|
330
|
+
for (const stmt of file.program.body) {
|
|
331
|
+
let decl = stmt;
|
|
332
|
+
if (stmt.type === "ExportNamedDeclaration" && stmt.declaration) {
|
|
333
|
+
decl = stmt.declaration;
|
|
334
|
+
}
|
|
335
|
+
if (decl.type === "FunctionDeclaration" && decl.id) {
|
|
336
|
+
named.set(decl.id.name, decl.id.name);
|
|
337
|
+
}
|
|
338
|
+
if (decl.type === "VariableDeclaration") {
|
|
339
|
+
for (const d of decl.declarations) {
|
|
340
|
+
if (d.id.type === "Identifier") named.set(d.id.name, d.id.name);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (decl.type === "ClassDeclaration" && decl.id) {
|
|
344
|
+
named.set(decl.id.name, decl.id.name);
|
|
345
|
+
}
|
|
346
|
+
if (stmt.type === "ExportDefaultDeclaration") {
|
|
347
|
+
const d = stmt.declaration;
|
|
348
|
+
if (d.type === "FunctionDeclaration" && d.id) defaultExport = d.id.name;
|
|
349
|
+
if (d.type === "Identifier") defaultExport = d.name;
|
|
350
|
+
}
|
|
351
|
+
collectCjsExports(stmt, named, (n) => {
|
|
352
|
+
defaultExport = n;
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
for (const name of named.values()) {
|
|
356
|
+
const g = generalizeFromAst(name, source);
|
|
357
|
+
if (g) poly.set(name, g);
|
|
358
|
+
}
|
|
359
|
+
if (defaultExport) {
|
|
360
|
+
const g = generalizeFromAst(defaultExport, source);
|
|
361
|
+
if (g) poly.set("default", g);
|
|
362
|
+
}
|
|
363
|
+
return { path: filePath, named, defaultExport, source, poly };
|
|
364
|
+
}
|
|
365
|
+
function collectCjsExports(stmt, named, setDefault) {
|
|
366
|
+
const visit = (n) => {
|
|
367
|
+
if (!n || typeof n !== "object") return;
|
|
368
|
+
const obj = n;
|
|
369
|
+
if (obj.type !== "AssignmentExpression") {
|
|
370
|
+
for (const key of Object.keys(obj)) {
|
|
371
|
+
if (key === "loc" || key === "start" || key === "end") continue;
|
|
372
|
+
const v = obj[key];
|
|
373
|
+
if (Array.isArray(v)) v.forEach(visit);
|
|
374
|
+
else if (v && typeof v === "object") visit(v);
|
|
375
|
+
}
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const left = obj.left;
|
|
379
|
+
const right = obj.right;
|
|
380
|
+
if (left?.type === "MemberExpression" && left.object?.type === "Identifier" && left.object.name === "exports" && left.property?.type === "Identifier" && left.property.name) {
|
|
381
|
+
named.set(left.property.name, left.property.name);
|
|
382
|
+
}
|
|
383
|
+
if (left?.type === "MemberExpression" && left.object?.type === "Identifier" && left.object.name === "module" && left.property?.type === "Identifier" && left.property.name === "exports") {
|
|
384
|
+
if (right?.type === "Identifier" && typeof right.name === "string") {
|
|
385
|
+
named.set(right.name, right.name);
|
|
386
|
+
setDefault(right.name);
|
|
387
|
+
}
|
|
388
|
+
if (right?.type === "ObjectExpression") {
|
|
389
|
+
const props = right.properties;
|
|
390
|
+
for (const p of props) {
|
|
391
|
+
if (p.type !== "ObjectProperty") continue;
|
|
392
|
+
const key = p.key;
|
|
393
|
+
const val = p.value;
|
|
394
|
+
const exportName = key?.type === "Identifier" ? key.name : key?.type === "StringLiteral" ? key.value : void 0;
|
|
395
|
+
if (exportName) named.set(exportName, exportName);
|
|
396
|
+
if (val?.type === "Identifier" && val.name && exportName) {
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
visit(stmt);
|
|
403
|
+
}
|
|
404
|
+
function collectStaticImports(entryFile, maxDepth = 8) {
|
|
405
|
+
const graph = /* @__PURE__ */ new Map();
|
|
406
|
+
const queue = [
|
|
407
|
+
{ file: resolve(entryFile), depth: 0 }
|
|
408
|
+
];
|
|
409
|
+
const seen = /* @__PURE__ */ new Set();
|
|
410
|
+
while (queue.length > 0) {
|
|
411
|
+
const { file, depth } = queue.shift();
|
|
412
|
+
if (seen.has(file) || depth > maxDepth) continue;
|
|
413
|
+
seen.add(file);
|
|
414
|
+
if (!existsSync(file)) continue;
|
|
415
|
+
const source = readFileSync(file, "utf8");
|
|
416
|
+
const mod = analyzeExportsFromSource(file, source);
|
|
417
|
+
graph.set(file, mod);
|
|
418
|
+
const ast = parse(source);
|
|
419
|
+
for (const spec of collectDependencySpecs(ast)) {
|
|
420
|
+
const resolved = resolveRelative(file, spec);
|
|
421
|
+
if (resolved) queue.push({ file: resolved, depth: depth + 1 });
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return graph;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// src/harvest-package.ts
|
|
428
|
+
import { existsSync as existsSync2, readdirSync, statSync, readFileSync as readFileSync2 } from "fs";
|
|
429
|
+
import { join as join2, resolve as resolve2, dirname as dirname2, basename } from "path";
|
|
430
|
+
import { harvestDts } from "@nudojs/harvester";
|
|
431
|
+
import { typeValueToString as typeValueToString2 } from "@nudojs/core";
|
|
432
|
+
function resolvePackageRoot(pkg, fromDir = process.cwd()) {
|
|
433
|
+
let dir = resolve2(fromDir);
|
|
434
|
+
const bare = pkg.replace(/^@/, "").replace(/\//g, "__");
|
|
435
|
+
for (let i = 0; i < 8; i++) {
|
|
436
|
+
const typesPath = join2(dir, "node_modules", "@types", bare);
|
|
437
|
+
if (existsSync2(typesPath)) return typesPath;
|
|
438
|
+
const pkgPath = join2(dir, "node_modules", pkg);
|
|
439
|
+
if (existsSync2(pkgPath)) return pkgPath;
|
|
440
|
+
const parent = resolve2(dir, "..");
|
|
441
|
+
if (parent === dir) break;
|
|
442
|
+
dir = parent;
|
|
443
|
+
}
|
|
444
|
+
return void 0;
|
|
445
|
+
}
|
|
446
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "test", "tests", "docs", "doc", "examples", "__tests__"]);
|
|
447
|
+
var MAX_DTS_BYTES = 15e5;
|
|
448
|
+
function dtsPriority(p, root) {
|
|
449
|
+
const base = basename(p);
|
|
450
|
+
const rel = p.slice(root.length);
|
|
451
|
+
if (base === "index.d.ts") return 0;
|
|
452
|
+
if (base === "index.d.mts") return 1;
|
|
453
|
+
if (rel.includes("/types/") && base === "index.d.ts") return 2;
|
|
454
|
+
if (base.endsWith(".d.ts") && !base.includes(".")) return 3;
|
|
455
|
+
return 10;
|
|
456
|
+
}
|
|
457
|
+
function collectDtsFiles(root, maxFiles = 8) {
|
|
458
|
+
const out = [];
|
|
459
|
+
const walk = (dir) => {
|
|
460
|
+
if (out.length >= maxFiles * 3) return;
|
|
461
|
+
let entries;
|
|
462
|
+
try {
|
|
463
|
+
entries = readdirSync(dir);
|
|
464
|
+
} catch {
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
for (const name of entries) {
|
|
468
|
+
if (out.length >= maxFiles * 3) return;
|
|
469
|
+
if (SKIP_DIRS.has(name)) continue;
|
|
470
|
+
const p = join2(dir, name);
|
|
471
|
+
let st;
|
|
472
|
+
try {
|
|
473
|
+
st = statSync(p);
|
|
474
|
+
} catch {
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
if (st.isDirectory()) walk(p);
|
|
478
|
+
else if (name.endsWith(".d.ts") && !name.endsWith(".d.ts.map")) {
|
|
479
|
+
if (st.size > MAX_DTS_BYTES) continue;
|
|
480
|
+
out.push(p);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
walk(root);
|
|
485
|
+
out.sort((a, b) => dtsPriority(a, root) - dtsPriority(b, root) || a.length - b.length);
|
|
486
|
+
return out.slice(0, maxFiles);
|
|
487
|
+
}
|
|
488
|
+
function entryDtsFromPackageJson(root) {
|
|
489
|
+
const pj = join2(root, "package.json");
|
|
490
|
+
if (!existsSync2(pj)) return void 0;
|
|
491
|
+
try {
|
|
492
|
+
const raw = JSON.parse(readFileSync2(pj, "utf8"));
|
|
493
|
+
const entry = raw.types ?? raw.typings;
|
|
494
|
+
if (typeof entry === "string" && entry.endsWith(".d.ts")) {
|
|
495
|
+
const p = resolve2(root, entry);
|
|
496
|
+
if (existsSync2(p)) {
|
|
497
|
+
const st = statSync(p);
|
|
498
|
+
if (st.size <= MAX_DTS_BYTES) return p;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
} catch {
|
|
502
|
+
}
|
|
503
|
+
return void 0;
|
|
504
|
+
}
|
|
505
|
+
function collectDtsFromEntry(entry, maxFiles = 200) {
|
|
506
|
+
const files = [];
|
|
507
|
+
const seen = /* @__PURE__ */ new Set();
|
|
508
|
+
const queue = [entry];
|
|
509
|
+
const REFERENCE_PATH_REGEX = /<reference\s+path=["']([^"']+)["']\s*\/>/g;
|
|
510
|
+
const RELATIVE_FROM_REGEX = /\bfrom\s+["'](\.[^"']+)["']/g;
|
|
511
|
+
while (queue.length > 0 && files.length < maxFiles) {
|
|
512
|
+
const current = queue.shift();
|
|
513
|
+
if (seen.has(current) || !existsSync2(current)) continue;
|
|
514
|
+
seen.add(current);
|
|
515
|
+
if (!current.endsWith(".d.ts")) continue;
|
|
516
|
+
files.push(current);
|
|
517
|
+
let text;
|
|
518
|
+
try {
|
|
519
|
+
text = readFileSync2(current, "utf-8");
|
|
520
|
+
} catch {
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
const dir = dirname2(current);
|
|
524
|
+
for (const match of text.matchAll(REFERENCE_PATH_REGEX)) {
|
|
525
|
+
queue.push(resolve2(dir, match[1]));
|
|
526
|
+
}
|
|
527
|
+
for (const match of text.matchAll(RELATIVE_FROM_REGEX)) {
|
|
528
|
+
const base = resolve2(dir, match[1]);
|
|
529
|
+
for (const candidate of [`${base}.d.ts`, join2(base, "index.d.ts")]) {
|
|
530
|
+
if (existsSync2(candidate)) {
|
|
531
|
+
queue.push(candidate);
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return files;
|
|
538
|
+
}
|
|
539
|
+
function harvestPackage(pkg, fromDir, maxFiles = 8) {
|
|
540
|
+
const root = resolvePackageRoot(pkg, fromDir);
|
|
541
|
+
if (!root) return { error: `package not found: ${pkg}` };
|
|
542
|
+
const entry = entryDtsFromPackageJson(root) ?? join2(root, "index.d.ts");
|
|
543
|
+
let dtsFiles = existsSync2(entry) ? collectDtsFromEntry(entry, maxFiles) : [];
|
|
544
|
+
if (dtsFiles.length === 0) {
|
|
545
|
+
const collected = collectDtsFiles(root, maxFiles);
|
|
546
|
+
dtsFiles = entry && existsSync2(entry) && !collected.includes(entry) ? [entry, ...collected.filter((f) => f !== entry)].slice(0, maxFiles) : collected;
|
|
547
|
+
}
|
|
548
|
+
if (dtsFiles.length === 0) {
|
|
549
|
+
return { error: `no .d.ts under ${root}` };
|
|
550
|
+
}
|
|
551
|
+
const env = harvestDts(dtsFiles);
|
|
552
|
+
return { pkg, root, dtsFiles, env };
|
|
553
|
+
}
|
|
554
|
+
function formatHarvestSummary(h) {
|
|
555
|
+
const lines = [];
|
|
556
|
+
lines.push(`${h.pkg}: ${h.dtsFiles.length} d.ts, ${h.env.stats.symbols} symbols`);
|
|
557
|
+
const mods = Object.keys(h.env.modules);
|
|
558
|
+
for (const m of mods.slice(0, 5)) {
|
|
559
|
+
const exports = h.env.modules[m];
|
|
560
|
+
const names = Object.keys(exports).slice(0, 12);
|
|
561
|
+
lines.push(` module "${m}": ${names.join(", ")}${Object.keys(exports).length > 12 ? "\u2026" : ""}`);
|
|
562
|
+
}
|
|
563
|
+
if (mods.length > 5) lines.push(` \u2026 ${mods.length - 5} more modules`);
|
|
564
|
+
return lines.join("\n");
|
|
565
|
+
}
|
|
566
|
+
function lookupHarvested(h, moduleName, exportName) {
|
|
567
|
+
const mod = h.env.modules[moduleName];
|
|
568
|
+
const tv = mod?.[exportName] ?? h.env.globals[exportName];
|
|
569
|
+
if (!tv) return void 0;
|
|
570
|
+
return typeValueToString2(tv);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// src/harvest-auto.ts
|
|
574
|
+
function barePackageName(spec) {
|
|
575
|
+
if (!spec) return void 0;
|
|
576
|
+
if (spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("node:")) return void 0;
|
|
577
|
+
const parts = spec.split("/");
|
|
578
|
+
if (spec.startsWith("@")) {
|
|
579
|
+
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : void 0;
|
|
580
|
+
}
|
|
581
|
+
return parts[0];
|
|
582
|
+
}
|
|
583
|
+
function collectBarePackages(source) {
|
|
584
|
+
try {
|
|
585
|
+
const ast = parse2(source);
|
|
586
|
+
const names = /* @__PURE__ */ new Set();
|
|
587
|
+
for (const spec of collectDependencySpecs(ast)) {
|
|
588
|
+
const pkg = barePackageName(spec);
|
|
589
|
+
if (pkg) names.add(pkg);
|
|
590
|
+
}
|
|
591
|
+
return [...names];
|
|
592
|
+
} catch {
|
|
593
|
+
return [];
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
var harvestCache = /* @__PURE__ */ new Map();
|
|
597
|
+
function harvestPackageCached(pkg, fromDir) {
|
|
598
|
+
const key = `${fromDir}::${pkg}`;
|
|
599
|
+
if (harvestCache.has(key)) return harvestCache.get(key);
|
|
600
|
+
let result = null;
|
|
601
|
+
try {
|
|
602
|
+
const h = harvestPackage(pkg, fromDir);
|
|
603
|
+
if (!("error" in h)) result = h;
|
|
604
|
+
} catch {
|
|
605
|
+
result = null;
|
|
606
|
+
}
|
|
607
|
+
harvestCache.set(key, result);
|
|
608
|
+
return result;
|
|
609
|
+
}
|
|
610
|
+
function clearHarvestCache() {
|
|
611
|
+
harvestCache.clear();
|
|
612
|
+
}
|
|
613
|
+
function autoHarvestModules(source, fromDir) {
|
|
614
|
+
const packages = collectBarePackages(source);
|
|
615
|
+
if (packages.length === 0) return {};
|
|
616
|
+
const modules = {};
|
|
617
|
+
for (const pkg of packages) {
|
|
618
|
+
const h = harvestPackageCached(pkg, fromDir);
|
|
619
|
+
if (!h) continue;
|
|
620
|
+
for (const [mod, exports] of Object.entries(h.env.modules)) {
|
|
621
|
+
modules[mod] = exports;
|
|
622
|
+
if (!modules[pkg]) modules[pkg] = exports;
|
|
623
|
+
const base = mod.replace(/^@types\//, "").replace(/\\/g, "/");
|
|
624
|
+
if (base && !modules[base]) modules[base] = exports;
|
|
625
|
+
}
|
|
626
|
+
for (const [k, v] of Object.entries(h.env.globals)) {
|
|
627
|
+
if (!modules[pkg]) modules[pkg] = {};
|
|
628
|
+
if (modules[pkg][k] === void 0) modules[pkg][k] = v;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return modules;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// src/abs-modules-graph.ts
|
|
635
|
+
import { readFileSync as readFileSync3, statSync as statSync2 } from "fs";
|
|
636
|
+
import { dirname as dirname4, resolve as resolve3 } from "path";
|
|
637
|
+
import { parse as parse3 } from "@nudojs/parser";
|
|
638
|
+
import {
|
|
639
|
+
evalProgramAbs,
|
|
640
|
+
collectAbsExports,
|
|
641
|
+
absFunction as absFunction3
|
|
642
|
+
} from "@nudojs/core";
|
|
643
|
+
|
|
644
|
+
// src/harvest-to-abs.ts
|
|
645
|
+
import {
|
|
646
|
+
T,
|
|
647
|
+
typeValueToAbs as typeValueToAbs2,
|
|
648
|
+
confJoin as confJoin2,
|
|
649
|
+
getFnSig,
|
|
650
|
+
absFunction as absFunction2
|
|
651
|
+
} from "@nudojs/core";
|
|
652
|
+
import { dirname as dirname3 } from "path";
|
|
653
|
+
function markMockConf2(a) {
|
|
654
|
+
a.conf = confJoin2(a.conf, "mock");
|
|
655
|
+
return a;
|
|
656
|
+
}
|
|
657
|
+
function harvestedValueToAbs(tv) {
|
|
658
|
+
if (!tv) return markMockConf2(typeValueToAbs2(T.unknown));
|
|
659
|
+
const sig = getFnSig(tv);
|
|
660
|
+
if (sig) {
|
|
661
|
+
const ret = markMockConf2(typeValueToAbs2(sig.returnType));
|
|
662
|
+
const params = tv.kind === "function" && tv.params?.length ? tv.params : sig.paramTypes.map((_, i) => `_arg${i}`);
|
|
663
|
+
const dummyBody = {
|
|
664
|
+
type: "BlockStatement",
|
|
665
|
+
body: [],
|
|
666
|
+
directives: []
|
|
667
|
+
};
|
|
668
|
+
return markMockConf2(
|
|
669
|
+
absFunction2(params, {
|
|
670
|
+
body: dummyBody,
|
|
671
|
+
apply: () => ret
|
|
672
|
+
})
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
return markMockConf2(typeValueToAbs2(tv));
|
|
676
|
+
}
|
|
677
|
+
function harvestToAbsModules(pkg, fromDir) {
|
|
678
|
+
const h = harvestPackageCached(pkg, fromDir);
|
|
679
|
+
if (!h) return {};
|
|
680
|
+
return packageHarvestToAbsModules(pkg, h);
|
|
681
|
+
}
|
|
682
|
+
function packageHarvestToAbsModules(pkg, h) {
|
|
683
|
+
const out = {};
|
|
684
|
+
const convertRecord = (rec) => {
|
|
685
|
+
const named = {};
|
|
686
|
+
for (const [k, v] of Object.entries(rec)) {
|
|
687
|
+
named[k] = harvestedValueToAbs(v);
|
|
688
|
+
}
|
|
689
|
+
return named;
|
|
690
|
+
};
|
|
691
|
+
const register = (key, exports) => {
|
|
692
|
+
if (!out[key]) out[key] = exports;
|
|
693
|
+
};
|
|
694
|
+
for (const [mod, rec] of Object.entries(h.env.modules)) {
|
|
695
|
+
const named = convertRecord(rec);
|
|
696
|
+
const exports = { named };
|
|
697
|
+
const def = rec["default"];
|
|
698
|
+
if (def) exports.default = harvestedValueToAbs(def);
|
|
699
|
+
register(mod, exports);
|
|
700
|
+
register(pkg, exports);
|
|
701
|
+
const stripped = mod.replace(/^@types\//, "").replace(/\\/g, "/");
|
|
702
|
+
if (stripped) register(stripped, exports);
|
|
703
|
+
const base = stripped.split("/").filter(Boolean).pop();
|
|
704
|
+
if (base && base !== pkg) register(base, exports);
|
|
705
|
+
}
|
|
706
|
+
const globalNamed = convertRecord(h.env.globals);
|
|
707
|
+
if (Object.keys(globalNamed).length > 0) {
|
|
708
|
+
const existing = out[pkg];
|
|
709
|
+
if (existing) {
|
|
710
|
+
out[pkg] = { named: { ...globalNamed, ...existing.named }, default: existing.default };
|
|
711
|
+
} else {
|
|
712
|
+
out[pkg] = { named: globalNamed };
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
return out;
|
|
716
|
+
}
|
|
717
|
+
function bareSpecToAbsModules(spec, fromFile) {
|
|
718
|
+
if (!spec || spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("node:")) {
|
|
719
|
+
return void 0;
|
|
720
|
+
}
|
|
721
|
+
const parts = spec.split("/");
|
|
722
|
+
const pkg = spec.startsWith("@") ? parts.length >= 2 ? `${parts[0]}/${parts[1]}` : void 0 : parts[0];
|
|
723
|
+
if (!pkg) return void 0;
|
|
724
|
+
const table = harvestToAbsModules(pkg, dirname3(fromFile));
|
|
725
|
+
return table[spec] ?? table[pkg];
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// src/abs-modules-graph.ts
|
|
729
|
+
function defaultAbsLoadModule(spec, fromFile) {
|
|
730
|
+
if (!spec.startsWith(".") && !spec.startsWith("/")) return void 0;
|
|
731
|
+
try {
|
|
732
|
+
const p = resolve3(dirname4(resolve3(fromFile)), spec);
|
|
733
|
+
for (const cand of [p, `${p}.js`, `${p}.mjs`, `${p}.ts`, resolve3(p, "index.js")]) {
|
|
734
|
+
try {
|
|
735
|
+
return readFileSync3(cand, "utf-8");
|
|
736
|
+
} catch {
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return void 0;
|
|
740
|
+
} catch {
|
|
741
|
+
return void 0;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
function relCandidates(spec, fromFile) {
|
|
745
|
+
const p = resolve3(dirname4(resolve3(fromFile)), spec);
|
|
746
|
+
return [p, `${p}.js`, `${p}.mjs`, `${p}.ts`, resolve3(p, "index.js")];
|
|
747
|
+
}
|
|
748
|
+
function resolveRel(spec, fromFile) {
|
|
749
|
+
if (!spec.startsWith(".") && !spec.startsWith("/")) return null;
|
|
750
|
+
for (const cand of relCandidates(spec, fromFile)) {
|
|
751
|
+
try {
|
|
752
|
+
readFileSync3(cand, "utf-8");
|
|
753
|
+
return cand;
|
|
754
|
+
} catch {
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
759
|
+
function importSpecs(source) {
|
|
760
|
+
try {
|
|
761
|
+
const file = parse3(source);
|
|
762
|
+
const out = [];
|
|
763
|
+
for (const stmt of file.program.body) {
|
|
764
|
+
if (stmt.type === "ImportDeclaration" || stmt.type === "ExportNamedDeclaration" || stmt.type === "ExportAllDeclaration") {
|
|
765
|
+
const src = stmt.source;
|
|
766
|
+
if (src && typeof src.value === "string") {
|
|
767
|
+
out.push(src.value);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
collectRequireSpecs(stmt, out);
|
|
771
|
+
}
|
|
772
|
+
return out;
|
|
773
|
+
} catch {
|
|
774
|
+
return [];
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
function collectRequireSpecs(node, out) {
|
|
778
|
+
const visit = (n) => {
|
|
779
|
+
if (!n || typeof n !== "object") return;
|
|
780
|
+
const o = n;
|
|
781
|
+
if (o.type === "CallExpression" && o.callee?.type === "Identifier" && o.callee.name === "require" && o.arguments?.[0]?.type === "StringLiteral") {
|
|
782
|
+
const spec = o.arguments[0].value;
|
|
783
|
+
if (typeof spec === "string") out.push(spec);
|
|
784
|
+
}
|
|
785
|
+
for (const key of Object.keys(o)) {
|
|
786
|
+
if (key === "loc" || key === "start" || key === "end") continue;
|
|
787
|
+
const v = o[key];
|
|
788
|
+
if (Array.isArray(v)) v.forEach(visit);
|
|
789
|
+
else if (v && typeof v === "object") visit(v);
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
visit(node);
|
|
793
|
+
}
|
|
794
|
+
function buildModulesForFile(source, fromFile, load, evalDep, depth, maxDepth, onMissing) {
|
|
795
|
+
const modules = {};
|
|
796
|
+
for (const spec of importSpecs(source)) {
|
|
797
|
+
if (spec.startsWith(".") || spec.startsWith("/")) {
|
|
798
|
+
const childPath = resolveRel(spec, fromFile);
|
|
799
|
+
if (!childPath) {
|
|
800
|
+
onMissing(spec, fromFile, relCandidates(spec, fromFile));
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
modules[spec] = evalDep(childPath, spec, fromFile, depth + 1);
|
|
804
|
+
} else if (!spec.startsWith("node:")) {
|
|
805
|
+
const bare = bareSpecToAbsModules(spec, fromFile);
|
|
806
|
+
if (bare) modules[spec] = bare;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
void maxDepth;
|
|
810
|
+
return modules;
|
|
811
|
+
}
|
|
812
|
+
var absModuleCache = /* @__PURE__ */ new Map();
|
|
813
|
+
function clearAbsModuleCache() {
|
|
814
|
+
absModuleCache.clear();
|
|
815
|
+
}
|
|
816
|
+
function evictAbsModuleCacheFiles(paths) {
|
|
817
|
+
for (const p of paths) absModuleCache.delete(p);
|
|
818
|
+
}
|
|
819
|
+
function moduleLabel(p) {
|
|
820
|
+
const parts = p.split(/[/\\]/);
|
|
821
|
+
return parts[parts.length - 1] || p;
|
|
822
|
+
}
|
|
823
|
+
function evalAbsModuleGraph(entrySource, entryFile, opts = {}) {
|
|
824
|
+
const load = opts.loadModule ?? defaultAbsLoadModule;
|
|
825
|
+
const maxDepth = opts.maxDepth ?? 16;
|
|
826
|
+
const cache = /* @__PURE__ */ new Map();
|
|
827
|
+
const loading = [];
|
|
828
|
+
const issues = [];
|
|
829
|
+
const seenIssue = /* @__PURE__ */ new Set();
|
|
830
|
+
const pushIssue = (kind, label, reason) => {
|
|
831
|
+
const key = `${kind}:${label}`;
|
|
832
|
+
if (seenIssue.has(key)) return;
|
|
833
|
+
seenIssue.add(key);
|
|
834
|
+
issues.push({ kind, label, reason });
|
|
835
|
+
};
|
|
836
|
+
function evalDep(absPath, spec, fromFile, depth) {
|
|
837
|
+
const cycleIndex = loading.indexOf(absPath);
|
|
838
|
+
if (cycleIndex !== -1) {
|
|
839
|
+
const chain = [...loading.slice(cycleIndex), absPath];
|
|
840
|
+
pushIssue(
|
|
841
|
+
"cycle",
|
|
842
|
+
moduleLabel(absPath),
|
|
843
|
+
`Circular module load: ${chain.join(" -> ")} (bindings inside the cycle resolve to their partially evaluated types)`
|
|
844
|
+
);
|
|
845
|
+
return cache.get(absPath) ?? { named: {} };
|
|
846
|
+
}
|
|
847
|
+
const shared = absModuleCache.get(absPath);
|
|
848
|
+
if (shared) {
|
|
849
|
+
try {
|
|
850
|
+
const st = statSync2(absPath);
|
|
851
|
+
if (st.mtimeMs === shared.mtimeMs && st.size === shared.size) {
|
|
852
|
+
for (const iss of shared.issues) pushIssue(iss.kind, iss.label, iss.reason);
|
|
853
|
+
cache.set(absPath, shared.exports);
|
|
854
|
+
return shared.exports;
|
|
855
|
+
}
|
|
856
|
+
} catch {
|
|
857
|
+
}
|
|
858
|
+
absModuleCache.delete(absPath);
|
|
859
|
+
}
|
|
860
|
+
if (cache.has(absPath)) return cache.get(absPath);
|
|
861
|
+
if (depth > maxDepth) {
|
|
862
|
+
const chain = [...loading, absPath];
|
|
863
|
+
pushIssue(
|
|
864
|
+
"depth",
|
|
865
|
+
moduleLabel(absPath),
|
|
866
|
+
`Module load chain too deep (depth ${chain.length} > ${maxDepth} max): ${chain.join(" -> ")} (loading truncated, deeper modules typed as unknown)`
|
|
867
|
+
);
|
|
868
|
+
const empty = { named: {} };
|
|
869
|
+
cache.set(absPath, empty);
|
|
870
|
+
return empty;
|
|
871
|
+
}
|
|
872
|
+
cache.set(absPath, { named: {} });
|
|
873
|
+
loading.push(absPath);
|
|
874
|
+
const source = load(spec, fromFile);
|
|
875
|
+
if (source === void 0) {
|
|
876
|
+
pushIssue(
|
|
877
|
+
"missing",
|
|
878
|
+
spec,
|
|
879
|
+
`Module file not found for '${spec}' (from ${moduleLabel(fromFile)}); tried: ${absPath}`
|
|
880
|
+
);
|
|
881
|
+
const empty = { named: {} };
|
|
882
|
+
cache.set(absPath, empty);
|
|
883
|
+
loading.pop();
|
|
884
|
+
return empty;
|
|
885
|
+
}
|
|
886
|
+
const issueStart = issues.length;
|
|
887
|
+
const modules2 = buildModulesForFile(
|
|
888
|
+
source,
|
|
889
|
+
absPath,
|
|
890
|
+
load,
|
|
891
|
+
evalDep,
|
|
892
|
+
depth,
|
|
893
|
+
maxDepth,
|
|
894
|
+
(spec2, fromFile2, tried) => {
|
|
895
|
+
pushIssue(
|
|
896
|
+
"missing",
|
|
897
|
+
spec2,
|
|
898
|
+
`Module file not found for '${spec2}' (from ${moduleLabel(fromFile2)}); tried: ${tried.join(", ")}`
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
);
|
|
902
|
+
let exports;
|
|
903
|
+
try {
|
|
904
|
+
const file = parse3(source);
|
|
905
|
+
const { env } = evalProgramAbs(source, { file, modules: modules2 });
|
|
906
|
+
exports = collectAbsExports(file, env, modules2);
|
|
907
|
+
} catch {
|
|
908
|
+
exports = { named: {} };
|
|
909
|
+
} finally {
|
|
910
|
+
loading.pop();
|
|
911
|
+
}
|
|
912
|
+
cache.set(absPath, exports);
|
|
913
|
+
let fingerprint;
|
|
914
|
+
try {
|
|
915
|
+
const st = statSync2(absPath);
|
|
916
|
+
fingerprint = { mtimeMs: st.mtimeMs, size: st.size };
|
|
917
|
+
} catch {
|
|
918
|
+
}
|
|
919
|
+
if (fingerprint) {
|
|
920
|
+
absModuleCache.set(absPath, {
|
|
921
|
+
...fingerprint,
|
|
922
|
+
exports,
|
|
923
|
+
issues: issues.slice(issueStart)
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
return exports;
|
|
927
|
+
}
|
|
928
|
+
const modules = buildModulesForFile(
|
|
929
|
+
entrySource,
|
|
930
|
+
entryFile,
|
|
931
|
+
load,
|
|
932
|
+
evalDep,
|
|
933
|
+
0,
|
|
934
|
+
maxDepth,
|
|
935
|
+
(spec, fromFile, tried) => {
|
|
936
|
+
pushIssue(
|
|
937
|
+
"missing",
|
|
938
|
+
spec,
|
|
939
|
+
`Module file not found for '${spec}' (from ${moduleLabel(fromFile)}); tried: ${tried.join(", ")}`
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
);
|
|
943
|
+
return { modules, byPath: cache, issues };
|
|
944
|
+
}
|
|
945
|
+
function evalProgramAbsWithModules(source, entryFile, opts = {}) {
|
|
946
|
+
const { modules } = evalAbsModuleGraph(source, entryFile, opts);
|
|
947
|
+
return evalProgramAbs(source, {
|
|
948
|
+
file: opts.file,
|
|
949
|
+
modules,
|
|
950
|
+
seedVars: opts.seedVars,
|
|
951
|
+
seedFns: opts.seedFns
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
function collectAbsBindingsFromGraph(source, filePath, opts = {}) {
|
|
955
|
+
const out = /* @__PURE__ */ new Map();
|
|
956
|
+
try {
|
|
957
|
+
const { env } = evalProgramAbsWithModules(source, filePath, opts);
|
|
958
|
+
for (const [k, v] of env.vars) {
|
|
959
|
+
out.set(k, v);
|
|
960
|
+
}
|
|
961
|
+
for (const [name, impl] of env.fns) {
|
|
962
|
+
if (!out.has(name)) {
|
|
963
|
+
out.set(
|
|
964
|
+
name,
|
|
965
|
+
absFunction3(impl.params, { body: impl.body, async: impl.async, env })
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
} catch {
|
|
970
|
+
}
|
|
971
|
+
return out;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// src/bpath-run.ts
|
|
975
|
+
import {
|
|
976
|
+
runTranspiled,
|
|
977
|
+
callTranspiledExport,
|
|
978
|
+
callTranspiledExportFull,
|
|
979
|
+
setBCallCollector,
|
|
980
|
+
setMemberDiagCollector,
|
|
981
|
+
setAbsTruncationCollector,
|
|
982
|
+
typeValueToAbs as typeValueToAbs4,
|
|
983
|
+
createEnvironment,
|
|
984
|
+
stableAnalyzeKeySource,
|
|
985
|
+
formatAbs as formatAbs2,
|
|
986
|
+
hashSource,
|
|
987
|
+
getFnImpl as getFnImpl2
|
|
988
|
+
} from "@nudojs/core";
|
|
989
|
+
import { parse as parse4, extractInlineDirectives } from "@nudojs/parser";
|
|
990
|
+
import { loadEnvs } from "@nudojs/cli/evaluator";
|
|
991
|
+
|
|
992
|
+
// src/env-to-abs.ts
|
|
993
|
+
import {
|
|
994
|
+
typeValueToAbs as typeValueToAbs3,
|
|
995
|
+
absToTypeValue,
|
|
996
|
+
getFnSig as getFnSig2,
|
|
997
|
+
absFunction as absFunction4,
|
|
998
|
+
objOf
|
|
999
|
+
} from "@nudojs/core";
|
|
1000
|
+
function envValueToAbs(tv) {
|
|
1001
|
+
if (!tv) return typeValueToAbs3({ kind: "unknown" });
|
|
1002
|
+
if (tv.kind === "function") {
|
|
1003
|
+
const sig = getFnSig2(tv);
|
|
1004
|
+
if (sig?.impl) {
|
|
1005
|
+
const params = tv.kind === "function" && tv.params?.length ? tv.params : sig.paramTypes.map((_, i) => `_arg${i}`);
|
|
1006
|
+
const dummyBody = {
|
|
1007
|
+
type: "BlockStatement",
|
|
1008
|
+
body: [],
|
|
1009
|
+
directives: []
|
|
1010
|
+
};
|
|
1011
|
+
const impl = sig.impl;
|
|
1012
|
+
return absFunction4(params, {
|
|
1013
|
+
body: dummyBody,
|
|
1014
|
+
apply: (args) => {
|
|
1015
|
+
const tvArgs = args.map((a) => absToTypeValue(a));
|
|
1016
|
+
try {
|
|
1017
|
+
const r = impl(tvArgs);
|
|
1018
|
+
if (!r) return typeValueToAbs3(sig.returnType);
|
|
1019
|
+
return envValueToAbs(r);
|
|
1020
|
+
} catch {
|
|
1021
|
+
return typeValueToAbs3(sig.returnType);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
if (tv.kind === "object") {
|
|
1028
|
+
const slots = {};
|
|
1029
|
+
for (const [k, v] of Object.entries(tv.properties)) {
|
|
1030
|
+
slots[k] = { value: envValueToAbs(v) };
|
|
1031
|
+
}
|
|
1032
|
+
return objOf(slots);
|
|
1033
|
+
}
|
|
1034
|
+
return typeValueToAbs3(tv);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// src/analysis-file-cache.ts
|
|
1038
|
+
var analysisByFile = /* @__PURE__ */ new Map();
|
|
1039
|
+
var MAX_ANALYSIS_FILE_CACHE = 64;
|
|
1040
|
+
function clearAnalysisFileCache() {
|
|
1041
|
+
analysisByFile.clear();
|
|
1042
|
+
}
|
|
1043
|
+
function getAnalysisFileCacheSize() {
|
|
1044
|
+
return analysisByFile.size;
|
|
1045
|
+
}
|
|
1046
|
+
function analysisCacheGet(filePath, source, auxKey) {
|
|
1047
|
+
const e = analysisByFile.get(filePath);
|
|
1048
|
+
if (!e) return void 0;
|
|
1049
|
+
if (e.source !== source || e.auxKey !== auxKey) return void 0;
|
|
1050
|
+
analysisByFile.delete(filePath);
|
|
1051
|
+
analysisByFile.set(filePath, e);
|
|
1052
|
+
return e.value;
|
|
1053
|
+
}
|
|
1054
|
+
function analysisCacheSet(filePath, source, auxKey, value) {
|
|
1055
|
+
if (analysisByFile.size >= MAX_ANALYSIS_FILE_CACHE && !analysisByFile.has(filePath)) {
|
|
1056
|
+
const oldest = analysisByFile.keys().next().value;
|
|
1057
|
+
if (oldest !== void 0) analysisByFile.delete(oldest);
|
|
1058
|
+
}
|
|
1059
|
+
analysisByFile.set(filePath, { source, auxKey, value });
|
|
1060
|
+
}
|
|
1061
|
+
function evictAnalysisFileCacheForFiles(files) {
|
|
1062
|
+
let n = 0;
|
|
1063
|
+
for (const f of files) {
|
|
1064
|
+
if (analysisByFile.delete(f)) n++;
|
|
1065
|
+
}
|
|
1066
|
+
return n;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// src/fn-analysis-cache.ts
|
|
1070
|
+
var fnAnalysisCache = /* @__PURE__ */ new Map();
|
|
1071
|
+
var MAX_FN_ANALYSIS_CACHE = 1024;
|
|
1072
|
+
function clearFnAnalysisCache() {
|
|
1073
|
+
fnAnalysisCache.clear();
|
|
1074
|
+
}
|
|
1075
|
+
function fnAnalysisCacheGet(key) {
|
|
1076
|
+
const hit = fnAnalysisCache.get(key);
|
|
1077
|
+
if (hit !== void 0) {
|
|
1078
|
+
fnAnalysisCache.delete(key);
|
|
1079
|
+
fnAnalysisCache.set(key, hit);
|
|
1080
|
+
}
|
|
1081
|
+
return hit;
|
|
1082
|
+
}
|
|
1083
|
+
function fnAnalysisCacheSet(key, value) {
|
|
1084
|
+
if (fnAnalysisCache.size >= MAX_FN_ANALYSIS_CACHE && !fnAnalysisCache.has(key)) {
|
|
1085
|
+
const oldest = fnAnalysisCache.keys().next().value;
|
|
1086
|
+
if (oldest !== void 0) fnAnalysisCache.delete(oldest);
|
|
1087
|
+
}
|
|
1088
|
+
fnAnalysisCache.set(key, value);
|
|
1089
|
+
}
|
|
1090
|
+
function evictFnAnalysisCacheForFiles(files) {
|
|
1091
|
+
if (files.length === 0 || fnAnalysisCache.size === 0) return 0;
|
|
1092
|
+
const prefixes = files.map((f) => `${f}\0`);
|
|
1093
|
+
let n = 0;
|
|
1094
|
+
for (const key of [...fnAnalysisCache.keys()]) {
|
|
1095
|
+
if (prefixes.some((p) => key.startsWith(p))) {
|
|
1096
|
+
fnAnalysisCache.delete(key);
|
|
1097
|
+
n++;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
return n;
|
|
1101
|
+
}
|
|
1102
|
+
function caseDirectiveKey(directives, typeValueToString5) {
|
|
1103
|
+
return directives.map((d) => {
|
|
1104
|
+
if (d.kind === "case") {
|
|
1105
|
+
const args = (d.args ?? []).map((a) => typeValueToString5(a)).join(",");
|
|
1106
|
+
return `c:${d.name ?? ""}:${args}`;
|
|
1107
|
+
}
|
|
1108
|
+
return d.kind;
|
|
1109
|
+
}).join("|");
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// src/bpath-run.ts
|
|
1113
|
+
function absSeedPart(a) {
|
|
1114
|
+
const impl = getFnImpl2(a);
|
|
1115
|
+
if (impl?.fingerprint) return impl.fingerprint;
|
|
1116
|
+
if (a.shape.k === "fn") {
|
|
1117
|
+
const ret = impl?.env?.vars?.get("__nudo_mock_ret");
|
|
1118
|
+
if (ret) {
|
|
1119
|
+
try {
|
|
1120
|
+
return `ret=${formatAbs2(ret)}`;
|
|
1121
|
+
} catch {
|
|
1122
|
+
return "ret=?";
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
if (impl?.apply) return "apply";
|
|
1126
|
+
}
|
|
1127
|
+
try {
|
|
1128
|
+
return formatAbs2(a);
|
|
1129
|
+
} catch {
|
|
1130
|
+
return "?";
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
function seedFnPart(name, fn) {
|
|
1134
|
+
if (fn.fingerprint) return `${name}:${fn.fingerprint}`;
|
|
1135
|
+
const body = fn.body;
|
|
1136
|
+
const bodyKey = body && typeof body.start === "number" && typeof body.end === "number" ? `${body.start}:${body.end}:${body.type ?? ""}` : String(body?.type ?? "?");
|
|
1137
|
+
return `${name}(${fn.params.join(",")})@${bodyKey}`;
|
|
1138
|
+
}
|
|
1139
|
+
function mockSeedFingerprint(mocks, seedFns) {
|
|
1140
|
+
const parts = [];
|
|
1141
|
+
if (mocks) {
|
|
1142
|
+
const names = Object.keys(mocks).sort();
|
|
1143
|
+
for (const n of names) parts.push(`${n}=${absSeedPart(mocks[n])}`);
|
|
1144
|
+
}
|
|
1145
|
+
if (seedFns) {
|
|
1146
|
+
const names = Object.keys(seedFns).sort();
|
|
1147
|
+
for (const n of names) parts.push(`fn:${seedFnPart(n, seedFns[n])}`);
|
|
1148
|
+
}
|
|
1149
|
+
if (parts.length === 0) return "-";
|
|
1150
|
+
return hashSource(parts.join(";"));
|
|
1151
|
+
}
|
|
1152
|
+
function collectEnvGlobals(envNames) {
|
|
1153
|
+
if (envNames.length === 0) return {};
|
|
1154
|
+
const env = createEnvironment();
|
|
1155
|
+
try {
|
|
1156
|
+
loadEnvs(envNames, env);
|
|
1157
|
+
} catch {
|
|
1158
|
+
return {};
|
|
1159
|
+
}
|
|
1160
|
+
const out = {};
|
|
1161
|
+
for (const [k, v] of Object.entries(env.getOwnBindings())) {
|
|
1162
|
+
out[k] = envValueToAbs(v);
|
|
1163
|
+
}
|
|
1164
|
+
return out;
|
|
1165
|
+
}
|
|
1166
|
+
function collectEnvModules(envNames) {
|
|
1167
|
+
if (envNames.length === 0) return {};
|
|
1168
|
+
const env = createEnvironment();
|
|
1169
|
+
let mods = {};
|
|
1170
|
+
try {
|
|
1171
|
+
mods = loadEnvs(envNames, env).modules;
|
|
1172
|
+
} catch {
|
|
1173
|
+
return {};
|
|
1174
|
+
}
|
|
1175
|
+
const out = {};
|
|
1176
|
+
for (const [spec, exports] of Object.entries(mods)) {
|
|
1177
|
+
const named = {};
|
|
1178
|
+
for (const [k, v] of Object.entries(exports)) {
|
|
1179
|
+
named[k] = envValueToAbs(v);
|
|
1180
|
+
}
|
|
1181
|
+
out[spec] = { named };
|
|
1182
|
+
}
|
|
1183
|
+
return out;
|
|
1184
|
+
}
|
|
1185
|
+
function collectBPathReplacements(source) {
|
|
1186
|
+
const targets = [];
|
|
1187
|
+
const values = {};
|
|
1188
|
+
const asTargets = [];
|
|
1189
|
+
const asValues = {};
|
|
1190
|
+
let i = 0;
|
|
1191
|
+
try {
|
|
1192
|
+
const file = parse4(source);
|
|
1193
|
+
const visitStmts = (stmts) => {
|
|
1194
|
+
for (const stmt of stmts) {
|
|
1195
|
+
if (!stmt || typeof stmt !== "object") continue;
|
|
1196
|
+
const loc = stmt.loc;
|
|
1197
|
+
const dirs = extractInlineDirectives(stmt);
|
|
1198
|
+
for (const d of dirs) {
|
|
1199
|
+
if (d.kind === "replace") {
|
|
1200
|
+
const varName = `__rep${i++}`;
|
|
1201
|
+
targets.push({
|
|
1202
|
+
target: d.targetSource,
|
|
1203
|
+
varName,
|
|
1204
|
+
stmtStart: loc?.start.line,
|
|
1205
|
+
stmtEnd: loc?.end.line
|
|
1206
|
+
});
|
|
1207
|
+
values[varName] = typeValueToAbs4(d.typeExpr);
|
|
1208
|
+
} else if (d.kind === "as" && loc) {
|
|
1209
|
+
const varName = `__as${i++}`;
|
|
1210
|
+
asTargets.push({
|
|
1211
|
+
varName,
|
|
1212
|
+
stmtStart: loc.start.line,
|
|
1213
|
+
stmtEnd: loc.end.line
|
|
1214
|
+
});
|
|
1215
|
+
asValues[varName] = typeValueToAbs4(d.typeExpr);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
const s = stmt;
|
|
1219
|
+
if (s.type === "BlockStatement" && Array.isArray(s.body)) visitStmts(s.body);
|
|
1220
|
+
if (s.type === "FunctionDeclaration" || s.type === "FunctionExpression") {
|
|
1221
|
+
if (s.body) visitStmts([s.body]);
|
|
1222
|
+
}
|
|
1223
|
+
if (s.type === "ExportNamedDeclaration" && s.declaration) {
|
|
1224
|
+
visitStmts([s.declaration]);
|
|
1225
|
+
}
|
|
1226
|
+
if (s.type === "IfStatement") {
|
|
1227
|
+
if (s.consequent) visitStmts([s.consequent]);
|
|
1228
|
+
if (s.alternate) visitStmts([s.alternate]);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
};
|
|
1232
|
+
visitStmts(file.program.body);
|
|
1233
|
+
} catch {
|
|
1234
|
+
}
|
|
1235
|
+
return { targets, values, asTargets, asValues };
|
|
1236
|
+
}
|
|
1237
|
+
function isBPathCapable(source, envNames = []) {
|
|
1238
|
+
void envNames;
|
|
1239
|
+
if (/(^|[^.\w$])this\s*\./.test(source) && !/\bclass\s+/.test(source)) return false;
|
|
1240
|
+
return true;
|
|
1241
|
+
}
|
|
1242
|
+
var bRunByFile = /* @__PURE__ */ new Map();
|
|
1243
|
+
var MAX_B_RUN_CACHE = 32;
|
|
1244
|
+
function clearBPathCache() {
|
|
1245
|
+
bRunByFile.clear();
|
|
1246
|
+
clearAnalysisFileCache();
|
|
1247
|
+
clearFnAnalysisCache();
|
|
1248
|
+
}
|
|
1249
|
+
function evictBPathCacheForFiles(files) {
|
|
1250
|
+
let n = 0;
|
|
1251
|
+
for (const f of files) {
|
|
1252
|
+
if (bRunByFile.delete(f)) n++;
|
|
1253
|
+
}
|
|
1254
|
+
return n;
|
|
1255
|
+
}
|
|
1256
|
+
function bPathCacheSet(filePath, stableSource, mode, envKey, mockKey, value) {
|
|
1257
|
+
if (bRunByFile.size >= MAX_B_RUN_CACHE && !bRunByFile.has(filePath)) {
|
|
1258
|
+
const oldest = bRunByFile.keys().next().value;
|
|
1259
|
+
if (oldest !== void 0) bRunByFile.delete(oldest);
|
|
1260
|
+
}
|
|
1261
|
+
bRunByFile.set(filePath, { stableSource, mode, envKey, mockKey, value });
|
|
1262
|
+
}
|
|
1263
|
+
function tryRunBPath(source, filePath, opts = {}) {
|
|
1264
|
+
if (!isBPathCapable(source, opts.envNames ?? [])) return void 0;
|
|
1265
|
+
const mode = opts.mode ?? "analyze";
|
|
1266
|
+
const envKey = (opts.envNames ?? []).join(",");
|
|
1267
|
+
const mockKey = mockSeedFingerprint(opts.mocks);
|
|
1268
|
+
const stable = stableAnalyzeKeySource(source);
|
|
1269
|
+
const cached = bRunByFile.get(filePath);
|
|
1270
|
+
if (cached && cached.stableSource === stable && cached.mode === mode && cached.envKey === envKey && cached.mockKey === mockKey) {
|
|
1271
|
+
bRunByFile.delete(filePath);
|
|
1272
|
+
bRunByFile.set(filePath, cached);
|
|
1273
|
+
return cached.value ?? void 0;
|
|
1274
|
+
}
|
|
1275
|
+
let out = null;
|
|
1276
|
+
try {
|
|
1277
|
+
const memberDiags = [];
|
|
1278
|
+
const truncated = /* @__PURE__ */ new Set();
|
|
1279
|
+
const topCalls = [];
|
|
1280
|
+
setMemberDiagCollector((d) => memberDiags.push(d));
|
|
1281
|
+
setAbsTruncationCollector((label) => truncated.add(label));
|
|
1282
|
+
setBCallCollector((r) => topCalls.push(r));
|
|
1283
|
+
try {
|
|
1284
|
+
const { modules: graphMods, issues } = evalAbsModuleGraph(source, filePath);
|
|
1285
|
+
const envMods = collectEnvModules(opts.envNames ?? []);
|
|
1286
|
+
const modules = { ...envMods, ...graphMods };
|
|
1287
|
+
const { targets, values, asTargets, asValues } = collectBPathReplacements(source);
|
|
1288
|
+
const envGlobals = {
|
|
1289
|
+
...collectEnvGlobals(opts.envNames ?? []),
|
|
1290
|
+
...opts.mocks ?? {}
|
|
1291
|
+
};
|
|
1292
|
+
const exports = runTranspiled(source, {
|
|
1293
|
+
modules,
|
|
1294
|
+
maxLoopIters: opts.maxLoopIters,
|
|
1295
|
+
mode: opts.mode ?? "analyze",
|
|
1296
|
+
replacementTargets: targets.length ? targets : void 0,
|
|
1297
|
+
replacements: targets.length ? values : void 0,
|
|
1298
|
+
asOverrideTargets: asTargets.length ? asTargets : void 0,
|
|
1299
|
+
asOverrides: asTargets.length ? asValues : void 0,
|
|
1300
|
+
envGlobals: Object.keys(envGlobals).length ? envGlobals : void 0
|
|
1301
|
+
});
|
|
1302
|
+
out = {
|
|
1303
|
+
exports,
|
|
1304
|
+
modules,
|
|
1305
|
+
memberDiags: memberDiags.length ? memberDiags : void 0,
|
|
1306
|
+
moduleIssues: issues.length ? issues : void 0,
|
|
1307
|
+
truncatedFns: truncated.size ? [...truncated] : void 0,
|
|
1308
|
+
calls: topCalls.length ? topCalls : void 0
|
|
1309
|
+
};
|
|
1310
|
+
} finally {
|
|
1311
|
+
setMemberDiagCollector(null);
|
|
1312
|
+
setAbsTruncationCollector(null);
|
|
1313
|
+
setBCallCollector(null);
|
|
1314
|
+
}
|
|
1315
|
+
} catch {
|
|
1316
|
+
out = null;
|
|
1317
|
+
}
|
|
1318
|
+
bPathCacheSet(filePath, stable, mode, envKey, mockKey, out);
|
|
1319
|
+
return out ?? void 0;
|
|
1320
|
+
}
|
|
1321
|
+
function tryBPathCallFull(source, filePath, fnName, args, opts = {}) {
|
|
1322
|
+
const run = tryRunBPath(source, filePath, { envNames: opts.envNames, mocks: opts.mocks });
|
|
1323
|
+
if (!run) return void 0;
|
|
1324
|
+
if (!(fnName in run.exports)) return void 0;
|
|
1325
|
+
const collected = [...run.calls ?? []];
|
|
1326
|
+
const memberDiags = [];
|
|
1327
|
+
if (opts.collectCalls) {
|
|
1328
|
+
setBCallCollector((r) => collected.push(r));
|
|
1329
|
+
}
|
|
1330
|
+
if (opts.collectMemberDiags ?? true) {
|
|
1331
|
+
setMemberDiagCollector((d) => memberDiags.push(d));
|
|
1332
|
+
}
|
|
1333
|
+
try {
|
|
1334
|
+
const full = callTranspiledExportFull(run.exports, fnName, args);
|
|
1335
|
+
const all = [...run.memberDiags ?? [], ...memberDiags];
|
|
1336
|
+
return {
|
|
1337
|
+
...full,
|
|
1338
|
+
calls: opts.collectCalls || run.calls?.length ? collected : void 0,
|
|
1339
|
+
memberDiags: all.length ? all : void 0,
|
|
1340
|
+
moduleIssues: run.moduleIssues,
|
|
1341
|
+
truncatedFns: run.truncatedFns
|
|
1342
|
+
};
|
|
1343
|
+
} finally {
|
|
1344
|
+
if (opts.collectCalls) setBCallCollector(null);
|
|
1345
|
+
setMemberDiagCollector(null);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
function tryBPathCall(source, filePath, fnName, args, opts = {}) {
|
|
1349
|
+
const full = tryBPathCallFull(source, filePath, fnName, args, opts);
|
|
1350
|
+
if (!full) return void 0;
|
|
1351
|
+
const r = full.result;
|
|
1352
|
+
if (!r) return void 0;
|
|
1353
|
+
if (r.shape.k === "never" && full.throws.shape.k !== "never") {
|
|
1354
|
+
return void 0;
|
|
1355
|
+
}
|
|
1356
|
+
if (r.shape.k === "unknown" && !r.term) return void 0;
|
|
1357
|
+
return r;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// src/bpath-diagnostics.ts
|
|
1361
|
+
import { parseSource } from "@nudojs/core";
|
|
1362
|
+
function locOf(node) {
|
|
1363
|
+
if (!node.loc) return null;
|
|
1364
|
+
return {
|
|
1365
|
+
start: { line: node.loc.start.line, column: node.loc.start.column },
|
|
1366
|
+
end: { line: node.loc.end.line, column: node.loc.end.column }
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
function isExit(stmt) {
|
|
1370
|
+
return stmt.type === "ReturnStatement" || stmt.type === "ThrowStatement";
|
|
1371
|
+
}
|
|
1372
|
+
function walkUnreachable(node, out) {
|
|
1373
|
+
const n = node;
|
|
1374
|
+
const checkBlock = (block) => {
|
|
1375
|
+
if (!block) return;
|
|
1376
|
+
const body = block.body ?? (Array.isArray(block) ? block : null);
|
|
1377
|
+
if (!Array.isArray(body)) return;
|
|
1378
|
+
for (let i = 0; i < body.length; i++) {
|
|
1379
|
+
walkUnreachable(body[i], out);
|
|
1380
|
+
if (isExit(body[i])) {
|
|
1381
|
+
for (let j = i + 1; j < body.length; j++) {
|
|
1382
|
+
const r = locOf(body[j]);
|
|
1383
|
+
if (r) out.push({ range: r });
|
|
1384
|
+
}
|
|
1385
|
+
break;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
};
|
|
1389
|
+
switch (n.type) {
|
|
1390
|
+
case "Program":
|
|
1391
|
+
case "BlockStatement":
|
|
1392
|
+
checkBlock(n.body);
|
|
1393
|
+
break;
|
|
1394
|
+
case "FunctionDeclaration":
|
|
1395
|
+
case "FunctionExpression":
|
|
1396
|
+
case "ArrowFunctionExpression":
|
|
1397
|
+
if (n.body) walkUnreachable(n.body, out);
|
|
1398
|
+
break;
|
|
1399
|
+
case "IfStatement":
|
|
1400
|
+
if (n.consequent) walkUnreachable(n.consequent, out);
|
|
1401
|
+
if (n.alternate) walkUnreachable(n.alternate, out);
|
|
1402
|
+
break;
|
|
1403
|
+
case "ForStatement":
|
|
1404
|
+
case "ForInStatement":
|
|
1405
|
+
case "ForOfStatement":
|
|
1406
|
+
case "WhileStatement":
|
|
1407
|
+
case "DoWhileStatement":
|
|
1408
|
+
if (n.body) walkUnreachable(n.body, out);
|
|
1409
|
+
break;
|
|
1410
|
+
case "TryStatement":
|
|
1411
|
+
if (n.block) walkUnreachable(n.block, out);
|
|
1412
|
+
if (n.handler?.body) walkUnreachable(n.handler.body, out);
|
|
1413
|
+
if (n.finalizer) walkUnreachable(n.finalizer, out);
|
|
1414
|
+
break;
|
|
1415
|
+
case "SwitchStatement":
|
|
1416
|
+
for (const c of n.cases ?? []) {
|
|
1417
|
+
checkBlock(c.consequent);
|
|
1418
|
+
}
|
|
1419
|
+
break;
|
|
1420
|
+
case "ExportNamedDeclaration":
|
|
1421
|
+
case "ExportDefaultDeclaration": {
|
|
1422
|
+
const decl = n.declaration;
|
|
1423
|
+
if (decl) walkUnreachable(decl, out);
|
|
1424
|
+
break;
|
|
1425
|
+
}
|
|
1426
|
+
default:
|
|
1427
|
+
break;
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
function collectDeclared(file) {
|
|
1431
|
+
const names = /* @__PURE__ */ new Set();
|
|
1432
|
+
const addId = (id) => {
|
|
1433
|
+
if (!id) return;
|
|
1434
|
+
if (id.type === "Identifier") names.add(id.name);
|
|
1435
|
+
if (id.type === "ObjectPattern") {
|
|
1436
|
+
for (const p of id.properties) {
|
|
1437
|
+
addId(p.value);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
if (id.type === "ArrayPattern") {
|
|
1441
|
+
for (const el of id.elements) addId(el);
|
|
1442
|
+
}
|
|
1443
|
+
};
|
|
1444
|
+
const visit = (node) => {
|
|
1445
|
+
if (!node || typeof node !== "object") return;
|
|
1446
|
+
const o = node;
|
|
1447
|
+
switch (o.type) {
|
|
1448
|
+
case "FunctionDeclaration":
|
|
1449
|
+
case "ClassDeclaration":
|
|
1450
|
+
addId(o.id);
|
|
1451
|
+
break;
|
|
1452
|
+
case "VariableDeclarator":
|
|
1453
|
+
addId(o.id);
|
|
1454
|
+
break;
|
|
1455
|
+
case "ImportSpecifier":
|
|
1456
|
+
case "ImportDefaultSpecifier":
|
|
1457
|
+
case "ImportNamespaceSpecifier":
|
|
1458
|
+
addId(o.local);
|
|
1459
|
+
break;
|
|
1460
|
+
default:
|
|
1461
|
+
break;
|
|
1462
|
+
}
|
|
1463
|
+
if (o.params && Array.isArray(o.params)) {
|
|
1464
|
+
for (const p of o.params) addId(p);
|
|
1465
|
+
}
|
|
1466
|
+
for (const key of Object.keys(o)) {
|
|
1467
|
+
if (key === "loc" || key === "start" || key === "end") continue;
|
|
1468
|
+
const v = o[key];
|
|
1469
|
+
if (Array.isArray(v)) v.forEach(visit);
|
|
1470
|
+
else if (v && typeof v === "object") visit(v);
|
|
1471
|
+
}
|
|
1472
|
+
};
|
|
1473
|
+
visit(file);
|
|
1474
|
+
return names;
|
|
1475
|
+
}
|
|
1476
|
+
var KNOWN_GLOBALS = /* @__PURE__ */ new Set([
|
|
1477
|
+
"Object",
|
|
1478
|
+
"Array",
|
|
1479
|
+
"String",
|
|
1480
|
+
"Number",
|
|
1481
|
+
"Boolean",
|
|
1482
|
+
"Symbol",
|
|
1483
|
+
"Math",
|
|
1484
|
+
"JSON",
|
|
1485
|
+
"Promise",
|
|
1486
|
+
"Error",
|
|
1487
|
+
"TypeError",
|
|
1488
|
+
"RangeError",
|
|
1489
|
+
"SyntaxError",
|
|
1490
|
+
"RegExp",
|
|
1491
|
+
"Date",
|
|
1492
|
+
"Map",
|
|
1493
|
+
"Set",
|
|
1494
|
+
"WeakMap",
|
|
1495
|
+
"WeakSet",
|
|
1496
|
+
"console",
|
|
1497
|
+
"undefined",
|
|
1498
|
+
"NaN",
|
|
1499
|
+
"Infinity",
|
|
1500
|
+
"globalThis",
|
|
1501
|
+
"parseInt",
|
|
1502
|
+
"parseFloat",
|
|
1503
|
+
"isNaN",
|
|
1504
|
+
"isFinite"
|
|
1505
|
+
]);
|
|
1506
|
+
function isFreeUnknown(name, declared, seen, known) {
|
|
1507
|
+
return !declared.has(name) && !known.has(name) && !name.startsWith("$") && !seen.has(name);
|
|
1508
|
+
}
|
|
1509
|
+
function walkBuiltinUnknown(node, declared, out, seen, known, parentKey) {
|
|
1510
|
+
const n = node;
|
|
1511
|
+
const flag = (idNode, name) => {
|
|
1512
|
+
if (!isFreeUnknown(name, declared, seen, known)) return;
|
|
1513
|
+
const range = locOf(idNode) ?? locOf(node);
|
|
1514
|
+
if (range) {
|
|
1515
|
+
out.push({ name, range });
|
|
1516
|
+
seen.add(name);
|
|
1517
|
+
}
|
|
1518
|
+
};
|
|
1519
|
+
if (n.type === "CallExpression" && n.callee?.type === "Identifier") {
|
|
1520
|
+
flag(n.callee, n.callee.name);
|
|
1521
|
+
} else if (n.type === "Identifier" && typeof n.name === "string" && parentKey !== "key" && parentKey !== "property" && parentKey !== "local" && parentKey !== "id") {
|
|
1522
|
+
flag(node, n.name);
|
|
1523
|
+
}
|
|
1524
|
+
for (const key of Object.keys(n)) {
|
|
1525
|
+
if (key === "loc" || key === "start" || key === "end") continue;
|
|
1526
|
+
const v = n[key];
|
|
1527
|
+
if (Array.isArray(v)) {
|
|
1528
|
+
v.forEach((x) => {
|
|
1529
|
+
if (x && typeof x === "object" && "type" in x) {
|
|
1530
|
+
walkBuiltinUnknown(x, declared, out, seen, known, key);
|
|
1531
|
+
}
|
|
1532
|
+
});
|
|
1533
|
+
} else if (v && typeof v === "object" && "type" in v) {
|
|
1534
|
+
walkBuiltinUnknown(v, declared, out, seen, known, key);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
function collectBPathDiagnostics(source, extraKnown) {
|
|
1539
|
+
let file;
|
|
1540
|
+
try {
|
|
1541
|
+
file = parseSource(source);
|
|
1542
|
+
} catch {
|
|
1543
|
+
return { unreachable: [], builtinUnknown: [] };
|
|
1544
|
+
}
|
|
1545
|
+
const unreachable = [];
|
|
1546
|
+
const root = file.program ?? file;
|
|
1547
|
+
walkUnreachable(root, unreachable);
|
|
1548
|
+
const declared = collectDeclared(file);
|
|
1549
|
+
const known = new Set(KNOWN_GLOBALS);
|
|
1550
|
+
if (extraKnown) {
|
|
1551
|
+
for (const n of extraKnown) known.add(n);
|
|
1552
|
+
}
|
|
1553
|
+
const builtinUnknown = [];
|
|
1554
|
+
walkBuiltinUnknown(file, declared, builtinUnknown, /* @__PURE__ */ new Set(), known);
|
|
1555
|
+
return { unreachable, builtinUnknown };
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
// src/analyzer.ts
|
|
1559
|
+
import { setAbsTruncationCollector as setAbsTruncationCollector2 } from "@nudojs/core";
|
|
25
1560
|
function resolveModule(source, fromDir) {
|
|
26
1561
|
const extensions = [".js", ".ts", ".mjs"];
|
|
27
|
-
const
|
|
1562
|
+
const nudoPath = resolveNpmNudo(source, fromDir);
|
|
1563
|
+
if (nudoPath) {
|
|
1564
|
+
const src = readFileSync4(nudoPath, "utf-8");
|
|
1565
|
+
return { ast: parse5(src), filePath: nudoPath };
|
|
1566
|
+
}
|
|
1567
|
+
const basePath = resolve4(fromDir, source);
|
|
28
1568
|
for (const ext of ["", ...extensions]) {
|
|
29
1569
|
const candidate = basePath + ext;
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
1570
|
+
if (!existsSync3(candidate)) continue;
|
|
1571
|
+
if (statSync3(candidate).isDirectory()) {
|
|
1572
|
+
let entry = null;
|
|
1573
|
+
const pkgPath = resolve4(candidate, "package.json");
|
|
1574
|
+
if (existsSync3(pkgPath)) {
|
|
1575
|
+
try {
|
|
1576
|
+
const main = JSON.parse(readFileSync4(pkgPath, "utf-8")).main;
|
|
1577
|
+
if (typeof main === "string") {
|
|
1578
|
+
for (const e of ["", ...extensions]) {
|
|
1579
|
+
const p = resolve4(candidate, main + e);
|
|
1580
|
+
if (existsSync3(p) && statSync3(p).isFile()) {
|
|
1581
|
+
entry = p;
|
|
1582
|
+
break;
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
} catch {
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
if (!entry) {
|
|
1590
|
+
for (const e of ["", ...extensions]) {
|
|
1591
|
+
const p = resolve4(candidate, "index" + e);
|
|
1592
|
+
if (existsSync3(p) && statSync3(p).isFile()) {
|
|
1593
|
+
entry = p;
|
|
1594
|
+
break;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
if (!entry) return null;
|
|
1599
|
+
const src2 = readFileSync4(entry, "utf-8");
|
|
1600
|
+
return { ast: parse5(src2), filePath: entry };
|
|
1601
|
+
}
|
|
1602
|
+
if (candidate.endsWith(".json")) {
|
|
1603
|
+
try {
|
|
1604
|
+
return { ast: parse5("module.exports = undefined;"), filePath: candidate, json: JSON.parse(readFileSync4(candidate, "utf-8")) };
|
|
1605
|
+
} catch {
|
|
1606
|
+
return null;
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
const src = readFileSync4(candidate, "utf-8");
|
|
1610
|
+
return { ast: parse5(src), filePath: candidate };
|
|
1611
|
+
}
|
|
1612
|
+
return null;
|
|
1613
|
+
}
|
|
1614
|
+
function resolveImportPath(specifier, fromDir) {
|
|
1615
|
+
const basePath = resolve4(fromDir, specifier);
|
|
1616
|
+
for (const ext of ["", ".js", ".ts", ".mjs"]) {
|
|
1617
|
+
const candidate = basePath + ext;
|
|
1618
|
+
if (existsSync3(candidate)) return candidate;
|
|
1619
|
+
}
|
|
1620
|
+
return null;
|
|
1621
|
+
}
|
|
1622
|
+
function buildModuleGraph(files, cache) {
|
|
1623
|
+
const imports = /* @__PURE__ */ new Map();
|
|
1624
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
1625
|
+
const edge = (from, to) => {
|
|
1626
|
+
if (!imports.has(from)) imports.set(from, /* @__PURE__ */ new Set());
|
|
1627
|
+
if (!dependents.has(to)) dependents.set(to, /* @__PURE__ */ new Set());
|
|
1628
|
+
imports.get(from).add(to);
|
|
1629
|
+
dependents.get(to).add(from);
|
|
1630
|
+
};
|
|
1631
|
+
for (const file of files) {
|
|
1632
|
+
if (!imports.has(file)) imports.set(file, /* @__PURE__ */ new Set());
|
|
1633
|
+
if (!dependents.has(file)) dependents.set(file, /* @__PURE__ */ new Set());
|
|
1634
|
+
let edges;
|
|
1635
|
+
if (cache) {
|
|
1636
|
+
let stat = null;
|
|
1637
|
+
try {
|
|
1638
|
+
stat = statSync3(file);
|
|
1639
|
+
} catch {
|
|
1640
|
+
}
|
|
1641
|
+
const cached = stat ? cache.get(file) : void 0;
|
|
1642
|
+
if (stat && cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
|
|
1643
|
+
edges = cached.edges;
|
|
1644
|
+
} else {
|
|
1645
|
+
edges = extractImportEdges(file);
|
|
1646
|
+
if (stat) cache.set(file, { mtimeMs: stat.mtimeMs, size: stat.size, edges });
|
|
1647
|
+
}
|
|
1648
|
+
} else {
|
|
1649
|
+
edges = extractImportEdges(file);
|
|
1650
|
+
}
|
|
1651
|
+
for (const to of edges) edge(file, to);
|
|
1652
|
+
}
|
|
1653
|
+
return { imports, dependents };
|
|
1654
|
+
}
|
|
1655
|
+
function extractImportEdges(file) {
|
|
1656
|
+
const edges = [];
|
|
1657
|
+
let ast;
|
|
1658
|
+
try {
|
|
1659
|
+
ast = parse5(readFileSync4(file, "utf-8"));
|
|
1660
|
+
} catch {
|
|
1661
|
+
return edges;
|
|
1662
|
+
}
|
|
1663
|
+
for (const stmt of ast.program.body) {
|
|
1664
|
+
if (stmt.type !== "ImportDeclaration") continue;
|
|
1665
|
+
const specifier = stmt.source.value;
|
|
1666
|
+
if (!specifier.startsWith(".") && !specifier.startsWith("/")) continue;
|
|
1667
|
+
const resolved = resolveImportPath(specifier, dirname5(file));
|
|
1668
|
+
if (resolved) edges.push(resolved);
|
|
1669
|
+
}
|
|
1670
|
+
return edges;
|
|
1671
|
+
}
|
|
1672
|
+
function computeDirtySet(dependents, changedFile) {
|
|
1673
|
+
const dirty = [];
|
|
1674
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1675
|
+
const queue = [changedFile];
|
|
1676
|
+
while (queue.length > 0) {
|
|
1677
|
+
const file = queue.shift();
|
|
1678
|
+
if (visited.has(file)) continue;
|
|
1679
|
+
visited.add(file);
|
|
1680
|
+
dirty.push(file);
|
|
1681
|
+
for (const dep of dependents.get(file) ?? []) {
|
|
1682
|
+
if (!visited.has(dep)) queue.push(dep);
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
return dirty;
|
|
1686
|
+
}
|
|
1687
|
+
function topoSortDirty(imports, dirty) {
|
|
1688
|
+
const inSet = new Set(dirty);
|
|
1689
|
+
const pending = /* @__PURE__ */ new Map();
|
|
1690
|
+
for (const file of dirty) {
|
|
1691
|
+
let count = 0;
|
|
1692
|
+
for (const dep of imports.get(file) ?? []) {
|
|
1693
|
+
if (inSet.has(dep)) count++;
|
|
33
1694
|
}
|
|
1695
|
+
pending.set(file, count);
|
|
34
1696
|
}
|
|
35
|
-
|
|
1697
|
+
const ordered = [];
|
|
1698
|
+
const ready = dirty.filter((f) => pending.get(f) === 0);
|
|
1699
|
+
while (ready.length > 0) {
|
|
1700
|
+
const file = ready.shift();
|
|
1701
|
+
ordered.push(file);
|
|
1702
|
+
for (const other of dirty) {
|
|
1703
|
+
if (pending.get(other) === void 0) continue;
|
|
1704
|
+
if ((imports.get(other) ?? /* @__PURE__ */ new Set()).has(file)) {
|
|
1705
|
+
const next = pending.get(other) - 1;
|
|
1706
|
+
pending.set(other, next);
|
|
1707
|
+
if (next === 0) ready.push(other);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
for (const file of dirty) {
|
|
1712
|
+
if (pending.has(file) && pending.get(file) > 0) ordered.push(file);
|
|
1713
|
+
}
|
|
1714
|
+
return ordered;
|
|
36
1715
|
}
|
|
37
|
-
function applyMocks(directives, env, filePath) {
|
|
1716
|
+
function applyMocks(directives, env, filePath, diagnostics) {
|
|
38
1717
|
for (const d of directives) {
|
|
39
1718
|
if (d.kind !== "mock") continue;
|
|
40
|
-
if (d.
|
|
1719
|
+
if (d.arrowFn) {
|
|
1720
|
+
const fnType = T2.fn(d.arrowFn.params, d.arrowFn.body, env);
|
|
1721
|
+
fnType._paramPatterns = d.arrowFn.paramPatterns;
|
|
1722
|
+
env.bind(d.name, fnType);
|
|
1723
|
+
} else if (d.nudoMock) {
|
|
1724
|
+
const typeVal = mockHelperToTypeValue(d.nudoMock, env);
|
|
1725
|
+
env.bind(d.name, typeVal);
|
|
1726
|
+
} else if (d.sinonExpr) {
|
|
1727
|
+
const sinonType = createSinonTypeValue(d.sinonExpr, env);
|
|
1728
|
+
env.bind(d.name, sinonType);
|
|
1729
|
+
} else if (d.expression) {
|
|
1730
|
+
const expr = d.expression.trim();
|
|
1731
|
+
if (expr.includes("(") && expr.includes(")") && !expr.startsWith("T.")) {
|
|
1732
|
+
diagnostics.push({
|
|
1733
|
+
range: { start: { line: 0, column: 0 }, end: { line: 0, column: 0 } },
|
|
1734
|
+
severity: "warning",
|
|
1735
|
+
message: `Mock expression "${expr}" could not be parsed as a known pattern`,
|
|
1736
|
+
code: "nudo:mock-invalid",
|
|
1737
|
+
suggestions: [
|
|
1738
|
+
"Supported formats: stub(), stub().returns(value), spy(), mock()",
|
|
1739
|
+
"Arrow functions: (args) => expression or (args) => { statements; return value; }"
|
|
1740
|
+
]
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
41
1743
|
env.bind(d.name, parseTypeValueExpr(d.expression));
|
|
42
1744
|
} else if (d.fromPath) {
|
|
43
|
-
const mockPath =
|
|
44
|
-
const mockSource =
|
|
45
|
-
const mockAst =
|
|
46
|
-
const mockEnv =
|
|
1745
|
+
const mockPath = resolve4(dirname5(filePath), d.fromPath);
|
|
1746
|
+
const mockSource = readFileSync4(mockPath, "utf-8");
|
|
1747
|
+
const mockAst = parse5(mockSource);
|
|
1748
|
+
const mockEnv = createEnvironment2();
|
|
47
1749
|
evaluateProgram(mockAst, mockEnv);
|
|
48
1750
|
const mockVal = mockEnv.lookup(d.name);
|
|
49
1751
|
env.bind(d.name, mockVal);
|
|
50
1752
|
}
|
|
51
1753
|
}
|
|
52
1754
|
}
|
|
1755
|
+
function createSinonTypeValue(sinonExpr, env) {
|
|
1756
|
+
if (sinonExpr.type === "stub" || sinonExpr.type === "spy") {
|
|
1757
|
+
const body = { type: "BlockStatement", body: [] };
|
|
1758
|
+
const fn = T2.fn(["...args"], body, env);
|
|
1759
|
+
if (sinonExpr.returnValue) {
|
|
1760
|
+
fn._directReturn = sinonExpr.returnValue;
|
|
1761
|
+
} else if (sinonExpr.resolvedValue) {
|
|
1762
|
+
fn._directReturn = T2.promise(sinonExpr.resolvedValue);
|
|
1763
|
+
} else if (sinonExpr.rejectedValue) {
|
|
1764
|
+
fn._directReturn = T2.never;
|
|
1765
|
+
} else {
|
|
1766
|
+
fn._directReturn = T2.unknown;
|
|
1767
|
+
}
|
|
1768
|
+
return fn;
|
|
1769
|
+
}
|
|
1770
|
+
return T2.unknown;
|
|
1771
|
+
}
|
|
53
1772
|
function locFromNode(node) {
|
|
54
1773
|
return {
|
|
55
1774
|
start: { line: node.loc?.start.line ?? 1, column: node.loc?.start.column ?? 0 },
|
|
56
1775
|
end: { line: node.loc?.end.line ?? 1, column: node.loc?.end.column ?? 0 }
|
|
57
1776
|
};
|
|
58
1777
|
}
|
|
59
|
-
function
|
|
60
|
-
const
|
|
1778
|
+
function extractParamNames(node) {
|
|
1779
|
+
const fn = node.type === "ExportDefaultDeclaration" ? node.declaration : node;
|
|
1780
|
+
if (fn.type === "FunctionDeclaration" || fn.type === "FunctionExpression" || fn.type === "ArrowFunctionExpression") {
|
|
1781
|
+
return fn.params.map((p) => {
|
|
1782
|
+
if (p.type === "Identifier") return p.name;
|
|
1783
|
+
if (p.type === "AssignmentPattern" && p.left.type === "Identifier") return p.left.name;
|
|
1784
|
+
if (p.type === "RestElement" && p.argument.type === "Identifier") return `...${p.argument.name}`;
|
|
1785
|
+
return "_";
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
if (fn.type === "VariableDeclaration") {
|
|
1789
|
+
const decl = fn.declarations[0];
|
|
1790
|
+
if (decl.init?.type === "FunctionExpression" || decl.init?.type === "ArrowFunctionExpression") {
|
|
1791
|
+
return decl.init.params.map((p) => {
|
|
1792
|
+
if (p.type === "Identifier") return p.name;
|
|
1793
|
+
if (p.type === "AssignmentPattern" && p.left.type === "Identifier") return p.left.name;
|
|
1794
|
+
if (p.type === "RestElement" && p.argument.type === "Identifier") return `...${p.argument.name}`;
|
|
1795
|
+
return "_";
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
return [];
|
|
1800
|
+
}
|
|
1801
|
+
function resolveFunctionNode(node) {
|
|
1802
|
+
if (node.type === "ExportNamedDeclaration" && node.declaration) return resolveFunctionNode(node.declaration);
|
|
1803
|
+
if (node.type === "ExportDefaultDeclaration") return resolveFunctionNode(node.declaration);
|
|
1804
|
+
if (node.type === "VariableDeclaration") {
|
|
1805
|
+
const init = node.declarations[0]?.init;
|
|
1806
|
+
if (init && (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression")) return init;
|
|
1807
|
+
}
|
|
1808
|
+
return node;
|
|
1809
|
+
}
|
|
1810
|
+
function isFnExprValue(node) {
|
|
1811
|
+
return !!node && (node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression");
|
|
1812
|
+
}
|
|
1813
|
+
function namedFnExprId(node) {
|
|
1814
|
+
return node.type === "FunctionExpression" && node.id ? node.id.name : null;
|
|
1815
|
+
}
|
|
1816
|
+
function isModuleExportsTarget(node) {
|
|
1817
|
+
return node.type === "MemberExpression" && !node.computed && node.object?.type === "Identifier" && node.object.name === "module" && node.property?.type === "Identifier" && node.property.name === "exports";
|
|
1818
|
+
}
|
|
1819
|
+
function memberPropertyKey(member) {
|
|
1820
|
+
if (member.type !== "MemberExpression" || member.computed) return null;
|
|
1821
|
+
const prop = member.property;
|
|
1822
|
+
if (prop?.type === "Identifier") return prop.name;
|
|
1823
|
+
if (prop?.type === "StringLiteral") return String(prop.value);
|
|
1824
|
+
return null;
|
|
1825
|
+
}
|
|
1826
|
+
function deepestAssignValue(expr) {
|
|
1827
|
+
let cur = expr;
|
|
1828
|
+
while (cur?.type === "AssignmentExpression") cur = cur.right;
|
|
1829
|
+
return cur ?? null;
|
|
1830
|
+
}
|
|
1831
|
+
function assignmentChainName(expr) {
|
|
1832
|
+
let cur = expr;
|
|
1833
|
+
while (cur?.type === "AssignmentExpression") {
|
|
1834
|
+
const target = cur.left;
|
|
1835
|
+
if (target.type === "Identifier") return target.name;
|
|
1836
|
+
if (target.type === "MemberExpression") {
|
|
1837
|
+
if (isModuleExportsTarget(target)) {
|
|
1838
|
+
cur = cur.right;
|
|
1839
|
+
continue;
|
|
1840
|
+
}
|
|
1841
|
+
return memberPropertyKey(target);
|
|
1842
|
+
}
|
|
1843
|
+
return null;
|
|
1844
|
+
}
|
|
1845
|
+
return null;
|
|
1846
|
+
}
|
|
1847
|
+
function collectTopLevelFunctions(ast) {
|
|
1848
|
+
const results = [];
|
|
1849
|
+
if (ast.type !== "File") return results;
|
|
1850
|
+
for (const stmt of ast.program.body) {
|
|
1851
|
+
const decl = resolveFunctionNode(stmt);
|
|
1852
|
+
if (decl.type === "FunctionDeclaration" && decl.id) {
|
|
1853
|
+
results.push({ name: decl.id.name, node: decl, stmt, noDeclaration: false });
|
|
1854
|
+
continue;
|
|
1855
|
+
}
|
|
1856
|
+
if (stmt.type === "VariableDeclaration") {
|
|
1857
|
+
for (const declarator of stmt.declarations) {
|
|
1858
|
+
if (declarator.id?.type === "Identifier" && isFnExprValue(declarator.init)) {
|
|
1859
|
+
results.push({ name: declarator.id.name, node: declarator.init, stmt, noDeclaration: true });
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
continue;
|
|
1863
|
+
}
|
|
1864
|
+
if (stmt.type === "ExpressionStatement" && stmt.expression?.type === "AssignmentExpression") {
|
|
1865
|
+
const expr = stmt.expression;
|
|
1866
|
+
const fn = deepestAssignValue(expr);
|
|
1867
|
+
if (isFnExprValue(fn)) {
|
|
1868
|
+
const name = namedFnExprId(fn) ?? assignmentChainName(expr) ?? "default";
|
|
1869
|
+
results.push({ name, node: fn, stmt, noDeclaration: true });
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
return results;
|
|
1874
|
+
}
|
|
1875
|
+
function findSingleModuleExportsFunction(ast) {
|
|
1876
|
+
if (ast.type !== "File") return null;
|
|
1877
|
+
let found = null;
|
|
1878
|
+
for (const stmt of ast.program.body) {
|
|
1879
|
+
if (stmt.type !== "ExpressionStatement") continue;
|
|
1880
|
+
const expr = stmt.expression;
|
|
1881
|
+
if (expr?.type !== "AssignmentExpression") continue;
|
|
1882
|
+
const fn = deepestAssignValue(expr);
|
|
1883
|
+
if (!isFnExprValue(fn)) continue;
|
|
1884
|
+
let targetsModuleExports = false;
|
|
1885
|
+
let cur = expr;
|
|
1886
|
+
while (cur?.type === "AssignmentExpression") {
|
|
1887
|
+
if (isModuleExportsTarget(cur.left)) {
|
|
1888
|
+
targetsModuleExports = true;
|
|
1889
|
+
break;
|
|
1890
|
+
}
|
|
1891
|
+
cur = cur.right;
|
|
1892
|
+
}
|
|
1893
|
+
if (!targetsModuleExports) continue;
|
|
1894
|
+
if (found) return null;
|
|
1895
|
+
found = fn;
|
|
1896
|
+
}
|
|
1897
|
+
return found;
|
|
1898
|
+
}
|
|
1899
|
+
function typeStructureKey(tv) {
|
|
1900
|
+
return typeStructureKeyUncached(tv, /* @__PURE__ */ new Set());
|
|
1901
|
+
}
|
|
1902
|
+
function typeStructureKeyUncached(tv, seen) {
|
|
1903
|
+
const enter = (inner) => {
|
|
1904
|
+
if (inner && typeof inner === "object") {
|
|
1905
|
+
if (seen.has(inner)) return "\xABcycle\xBB";
|
|
1906
|
+
seen.add(inner);
|
|
1907
|
+
const out = typeStructureKeyUncached(inner, seen);
|
|
1908
|
+
seen.delete(inner);
|
|
1909
|
+
return out;
|
|
1910
|
+
}
|
|
1911
|
+
return typeStructureKeyUncached(inner, seen);
|
|
1912
|
+
};
|
|
1913
|
+
switch (tv.kind) {
|
|
1914
|
+
case "literal":
|
|
1915
|
+
return `lit(${typeof tv.value}:${String(tv.value)})`;
|
|
1916
|
+
case "primitive":
|
|
1917
|
+
return `prim(${tv.type})`;
|
|
1918
|
+
case "array":
|
|
1919
|
+
return `arr(${enter(tv.element)})`;
|
|
1920
|
+
case "tuple":
|
|
1921
|
+
return `tup(${tv.elements.map(enter).join(",")})`;
|
|
1922
|
+
case "object":
|
|
1923
|
+
return `obj(${Object.keys(tv.properties).sort().map((k) => `${k}:${enter(tv.properties[k])}`).join(",")})`;
|
|
1924
|
+
case "function":
|
|
1925
|
+
return `fn(${tv.params.join(",")})`;
|
|
1926
|
+
case "promise":
|
|
1927
|
+
return `prom(${enter(tv.value)})`;
|
|
1928
|
+
case "instance":
|
|
1929
|
+
return `inst(${tv.className})`;
|
|
1930
|
+
case "refined":
|
|
1931
|
+
return `ref(${enter(tv.base)})`;
|
|
1932
|
+
case "union":
|
|
1933
|
+
return `uni(${tv.members.map(enter).sort().join("|")})`;
|
|
1934
|
+
default:
|
|
1935
|
+
return tv.kind;
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
var MAX_PRECISE_CALLSITE_CASES = 3;
|
|
1939
|
+
var COLLAPSE_LITERAL_THRESHOLD = 4;
|
|
1940
|
+
function widenType(tv) {
|
|
1941
|
+
if (tv.kind === "union") return simplifyUnion(tv.members.map(widenLiteral));
|
|
1942
|
+
return widenLiteral(tv);
|
|
1943
|
+
}
|
|
1944
|
+
function dedupeCallRecords(records) {
|
|
1945
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1946
|
+
const out = [];
|
|
1947
|
+
for (const rec of records) {
|
|
1948
|
+
if (isOversizedRecord(rec)) continue;
|
|
1949
|
+
const key = rec.argTypes.map(typeStructureKey).join(",") + "=>" + typeStructureKey(rec.resultType) + "!" + typeStructureKey(rec.throws);
|
|
1950
|
+
if (seen.has(key)) continue;
|
|
1951
|
+
seen.add(key);
|
|
1952
|
+
out.push(rec);
|
|
1953
|
+
}
|
|
1954
|
+
return out;
|
|
1955
|
+
}
|
|
1956
|
+
var MAX_RECORD_TYPE_NODES = 2e3;
|
|
1957
|
+
function typeNodeCount(tv, seen) {
|
|
1958
|
+
if (tv === null || typeof tv !== "object") return 1;
|
|
1959
|
+
if (seen.has(tv)) return 0;
|
|
1960
|
+
seen.add(tv);
|
|
1961
|
+
switch (tv.kind) {
|
|
1962
|
+
case "array":
|
|
1963
|
+
return 1 + typeNodeCount(tv.element, seen);
|
|
1964
|
+
case "tuple":
|
|
1965
|
+
return 1 + tv.elements.reduce((acc, e) => acc + typeNodeCount(e, seen), 0);
|
|
1966
|
+
case "object": {
|
|
1967
|
+
let n = 1;
|
|
1968
|
+
for (const v of Object.values(tv.properties)) n += typeNodeCount(v, seen);
|
|
1969
|
+
return n;
|
|
1970
|
+
}
|
|
1971
|
+
case "promise":
|
|
1972
|
+
return 1 + typeNodeCount(tv.value, seen);
|
|
1973
|
+
case "union":
|
|
1974
|
+
return 1 + tv.members.reduce((acc, e) => acc + typeNodeCount(e, seen), 0);
|
|
1975
|
+
case "refined":
|
|
1976
|
+
return 1 + typeNodeCount(tv.base, seen);
|
|
1977
|
+
default:
|
|
1978
|
+
return 1;
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
function isOversizedRecord(rec) {
|
|
1982
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1983
|
+
for (const a of rec.argTypes) {
|
|
1984
|
+
if (typeNodeCount(a, seen) > MAX_RECORD_TYPE_NODES) return true;
|
|
1985
|
+
}
|
|
1986
|
+
return typeNodeCount(rec.resultType, seen) > MAX_RECORD_TYPE_NODES;
|
|
1987
|
+
}
|
|
1988
|
+
function locFromCallLoc(loc) {
|
|
1989
|
+
const p = loc ?? { line: 0, column: 0 };
|
|
1990
|
+
return { start: { line: p.line, column: p.column }, end: { line: p.line, column: p.column } };
|
|
1991
|
+
}
|
|
1992
|
+
function synthesizeExternalFunctions(records, currentFile) {
|
|
1993
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1994
|
+
for (const rec of records) {
|
|
1995
|
+
if (!rec.targetModule || !rec.targetExport) continue;
|
|
1996
|
+
if (rec.targetModule === currentFile) continue;
|
|
1997
|
+
const key = `${rec.targetModule}\0${rec.targetExport}`;
|
|
1998
|
+
let group = groups.get(key);
|
|
1999
|
+
if (!group) {
|
|
2000
|
+
group = { module: rec.targetModule, exportName: rec.targetExport, records: [] };
|
|
2001
|
+
groups.set(key, group);
|
|
2002
|
+
}
|
|
2003
|
+
group.records.push(rec);
|
|
2004
|
+
}
|
|
2005
|
+
const out = [];
|
|
2006
|
+
for (const { module, exportName, records: records2 } of groups.values()) {
|
|
2007
|
+
const deduped = dedupeCallRecords(records2);
|
|
2008
|
+
const arity = Math.max(...deduped.map((r) => r.argTypes.length));
|
|
2009
|
+
const analysis = {
|
|
2010
|
+
name: exportName,
|
|
2011
|
+
loc: locFromCallLoc(deduped[0].callLoc),
|
|
2012
|
+
paramNames: Array.from({ length: arity }, (_, i) => `arg${i}`),
|
|
2013
|
+
cases: [],
|
|
2014
|
+
fromModule: module
|
|
2015
|
+
};
|
|
2016
|
+
const precise = deduped.slice(0, MAX_PRECISE_CALLSITE_CASES);
|
|
2017
|
+
for (const rec of precise) {
|
|
2018
|
+
analysis.cases.push({
|
|
2019
|
+
name: `call@L${rec.callLoc?.line ?? 0}`,
|
|
2020
|
+
args: rec.argTypes,
|
|
2021
|
+
result: rec.resultType,
|
|
2022
|
+
throws: rec.throws,
|
|
2023
|
+
source: "callsite"
|
|
2024
|
+
});
|
|
2025
|
+
}
|
|
2026
|
+
const remaining = deduped.slice(MAX_PRECISE_CALLSITE_CASES);
|
|
2027
|
+
if (remaining.length > 0) {
|
|
2028
|
+
analysis.cases.push({
|
|
2029
|
+
name: "call@symbolic",
|
|
2030
|
+
args: Array.from(
|
|
2031
|
+
{ length: arity },
|
|
2032
|
+
(_, i) => (
|
|
2033
|
+
// 缺参按真实 JS 语义 widen 成 undefined 而非 unknown——可选参守卫
|
|
2034
|
+
// (target || [] 等)对 unknown 全塌,对 undefined 正常走默认分支
|
|
2035
|
+
widenType(simplifyUnion(remaining.map((rec) => rec.argTypes[i] ?? T2.undefined)))
|
|
2036
|
+
)
|
|
2037
|
+
),
|
|
2038
|
+
result: collapseLiteralUnion(simplifyUnion(remaining.map((r) => r.resultType)), COLLAPSE_LITERAL_THRESHOLD),
|
|
2039
|
+
throws: simplifyUnion(remaining.map((r) => r.throws)),
|
|
2040
|
+
source: "callsite",
|
|
2041
|
+
aggregatedFrom: remaining.length
|
|
2042
|
+
});
|
|
2043
|
+
}
|
|
2044
|
+
if (deduped.length > 1) {
|
|
2045
|
+
analysis.combined = collapseLiteralUnion(
|
|
2046
|
+
simplifyUnion(deduped.map((r) => r.resultType)),
|
|
2047
|
+
COLLAPSE_LITERAL_THRESHOLD
|
|
2048
|
+
);
|
|
2049
|
+
} else {
|
|
2050
|
+
analysis.combined = deduped[0].resultType;
|
|
2051
|
+
}
|
|
2052
|
+
out.push(analysis);
|
|
2053
|
+
}
|
|
2054
|
+
return out;
|
|
2055
|
+
}
|
|
2056
|
+
function receiverTypeToDisplay(tv) {
|
|
2057
|
+
switch (tv.kind) {
|
|
2058
|
+
case "literal": {
|
|
2059
|
+
const v = tv.value;
|
|
2060
|
+
if (v === null) return "null";
|
|
2061
|
+
if (v === void 0) return "undefined";
|
|
2062
|
+
return typeof v;
|
|
2063
|
+
}
|
|
2064
|
+
case "union":
|
|
2065
|
+
return tv.members.map(receiverTypeToDisplay).join(" | ");
|
|
2066
|
+
default:
|
|
2067
|
+
return typeValueToString3(tv);
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
function receiverIsConcrete(tv) {
|
|
2071
|
+
if (tv.kind === "unknown") return false;
|
|
2072
|
+
if (tv.kind === "union") return tv.members.every((m) => m.kind !== "unknown");
|
|
2073
|
+
return true;
|
|
2074
|
+
}
|
|
2075
|
+
function unknownRecordsToDiagnostics(records) {
|
|
2076
|
+
const out = [];
|
|
2077
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2078
|
+
for (const r of records) {
|
|
2079
|
+
const line = r.loc?.line ?? 0;
|
|
2080
|
+
const column = r.loc?.column ?? 0;
|
|
2081
|
+
const key = `${line}:${column}:${r.name}`;
|
|
2082
|
+
if (seen.has(key)) continue;
|
|
2083
|
+
seen.add(key);
|
|
2084
|
+
const range = {
|
|
2085
|
+
start: { line, column },
|
|
2086
|
+
end: { line, column: column + r.name.length }
|
|
2087
|
+
};
|
|
2088
|
+
if (r.kind === "global") {
|
|
2089
|
+
if (r.name.startsWith("recursion:")) {
|
|
2090
|
+
out.push({
|
|
2091
|
+
range,
|
|
2092
|
+
severity: "warning",
|
|
2093
|
+
message: `Recursive evaluation of '${r.name.slice("recursion:".length)}' was truncated (depth/size budget); result widened to unknown`,
|
|
2094
|
+
code: "nudo:recursion-truncated"
|
|
2095
|
+
});
|
|
2096
|
+
continue;
|
|
2097
|
+
}
|
|
2098
|
+
if (r.name.startsWith("module-cycle:")) {
|
|
2099
|
+
out.push({
|
|
2100
|
+
range,
|
|
2101
|
+
severity: "warning",
|
|
2102
|
+
message: r.reason ?? "Circular module load detected",
|
|
2103
|
+
code: "nudo:module-cycle"
|
|
2104
|
+
});
|
|
2105
|
+
continue;
|
|
2106
|
+
}
|
|
2107
|
+
if (r.name.startsWith("module-depth:")) {
|
|
2108
|
+
out.push({
|
|
2109
|
+
range,
|
|
2110
|
+
severity: "warning",
|
|
2111
|
+
message: r.reason ?? "Module load chain too deep",
|
|
2112
|
+
code: "nudo:module-depth"
|
|
2113
|
+
});
|
|
2114
|
+
continue;
|
|
2115
|
+
}
|
|
2116
|
+
if (r.name.startsWith("module-missing:")) {
|
|
2117
|
+
out.push({
|
|
2118
|
+
range,
|
|
2119
|
+
severity: "error",
|
|
2120
|
+
message: r.reason ?? "Module file not found",
|
|
2121
|
+
code: "nudo:module-missing"
|
|
2122
|
+
});
|
|
2123
|
+
continue;
|
|
2124
|
+
}
|
|
2125
|
+
out.push({
|
|
2126
|
+
range,
|
|
2127
|
+
severity: "warning",
|
|
2128
|
+
message: `Unknown global identifier '${r.name}'`,
|
|
2129
|
+
code: "nudo:unknown-global"
|
|
2130
|
+
});
|
|
2131
|
+
continue;
|
|
2132
|
+
}
|
|
2133
|
+
const receiver = r.receiverType;
|
|
2134
|
+
if (receiver && receiverIsConcrete(receiver)) {
|
|
2135
|
+
const isApprox = (() => {
|
|
2136
|
+
const members = receiver.kind === "union" ? receiver.members : [receiver];
|
|
2137
|
+
return members.some(
|
|
2138
|
+
(m) => m.kind === "instance" || m.kind === "function" || m.kind === "tuple" || m.kind === "array" || m.kind === "object" || m.kind === "refined" || // wrapper/instance 近似表上可能持有该成员('x'.charCodeAt 于
|
|
2139
|
+
// number|string:string 侧存在,number 侧缺失 → 不确定 → warning)
|
|
2140
|
+
memberMayExistOn(m, r.name)
|
|
2141
|
+
);
|
|
2142
|
+
})();
|
|
2143
|
+
const kindLabel = r.kind === "method" ? "Method" : "Property";
|
|
2144
|
+
out.push({
|
|
2145
|
+
range,
|
|
2146
|
+
severity: isApprox ? "warning" : "error",
|
|
2147
|
+
message: `${kindLabel} '${r.name}' does not exist on type '${receiverTypeToDisplay(receiver)}'`,
|
|
2148
|
+
code: "nudo:no-method",
|
|
2149
|
+
...r.origin ? { origin: r.origin } : {}
|
|
2150
|
+
});
|
|
2151
|
+
} else {
|
|
2152
|
+
out.push({
|
|
2153
|
+
range,
|
|
2154
|
+
severity: "warning",
|
|
2155
|
+
message: `Cannot resolve '${r.name}' on unknown value`,
|
|
2156
|
+
code: "nudo:unknown-recv",
|
|
2157
|
+
...r.origin ? { origin: r.origin } : {}
|
|
2158
|
+
});
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
return out;
|
|
2162
|
+
}
|
|
2163
|
+
function collectEnvNames(filePath, source, includeProject) {
|
|
2164
|
+
const ast = parse5(source);
|
|
2165
|
+
const fileDirectives = extractFileDirectives(ast);
|
|
2166
|
+
const fileEnvNames = fileDirectives.filter((d) => d.kind === "env").flatMap((d) => d.envs);
|
|
2167
|
+
if (!includeProject) return fileEnvNames;
|
|
2168
|
+
const projectConfig = findProjectConfig(dirname5(filePath));
|
|
2169
|
+
const projectEnvNames = projectConfig?.config.env ?? [];
|
|
2170
|
+
return [.../* @__PURE__ */ new Set([...projectEnvNames, ...fileEnvNames])];
|
|
2171
|
+
}
|
|
2172
|
+
async function analyzeFileAsync(filePath, source, activeCases, externalCallRecords) {
|
|
2173
|
+
const envNames = collectEnvNames(filePath, source, true);
|
|
2174
|
+
if (envNames.length > 0) {
|
|
2175
|
+
await preloadPathEnvs(envNames, dirname5(filePath));
|
|
2176
|
+
}
|
|
2177
|
+
return analyzeFile(filePath, source, activeCases, externalCallRecords);
|
|
2178
|
+
}
|
|
2179
|
+
function collectCallRecords(filePath, source) {
|
|
2180
|
+
const records = [];
|
|
2181
|
+
const ast = parse5(source, { errorRecovery: true });
|
|
2182
|
+
resetMemo();
|
|
2183
|
+
resetUnreachableRanges();
|
|
2184
|
+
resetEnvModules();
|
|
2185
|
+
resetMockModules();
|
|
2186
|
+
setModuleResolver(resolveModule);
|
|
2187
|
+
setCurrentFileDir(dirname5(filePath));
|
|
2188
|
+
setCurrentSource(source);
|
|
2189
|
+
setCallCollector((record) => records.push(record));
|
|
2190
|
+
try {
|
|
2191
|
+
const env = createEnvironment2();
|
|
2192
|
+
evaluateProgram(ast, env);
|
|
2193
|
+
const runCallbacks = (statements) => {
|
|
2194
|
+
for (const stmt of statements) {
|
|
2195
|
+
if (stmt.type !== "ExpressionStatement" || !("expression" in stmt)) continue;
|
|
2196
|
+
const expr = stmt.expression;
|
|
2197
|
+
if (expr.type !== "CallExpression") continue;
|
|
2198
|
+
const callee = expr.callee;
|
|
2199
|
+
if (callee.type !== "MemberExpression" && callee.type !== "Identifier") continue;
|
|
2200
|
+
const name = callee.type === "Identifier" ? callee.name : callee.property.type === "Identifier" ? callee.property.name : null;
|
|
2201
|
+
if (!name || !TEST_CALLBACK_NAMES.has(name)) continue;
|
|
2202
|
+
const args = expr.arguments;
|
|
2203
|
+
const cb = args.find((a) => a.type === "ArrowFunctionExpression" || a.type === "FunctionExpression");
|
|
2204
|
+
if (!cb) continue;
|
|
2205
|
+
try {
|
|
2206
|
+
const params = cb.params ?? [];
|
|
2207
|
+
evaluateFunction(cb, params.map(() => T2.unknown), env);
|
|
2208
|
+
} catch {
|
|
2209
|
+
}
|
|
2210
|
+
if (name === "describe" && cb.body.type === "BlockStatement") {
|
|
2211
|
+
runCallbacks(cb.body.body);
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
};
|
|
2215
|
+
runCallbacks(ast.program.body);
|
|
2216
|
+
} catch {
|
|
2217
|
+
} finally {
|
|
2218
|
+
setCallCollector(null);
|
|
2219
|
+
setModuleResolver(null);
|
|
2220
|
+
setUnknownBuiltinHandler(null);
|
|
2221
|
+
}
|
|
2222
|
+
return records;
|
|
2223
|
+
}
|
|
2224
|
+
var TEST_CALLBACK_NAMES = /* @__PURE__ */ new Set(["it", "test", "describe"]);
|
|
2225
|
+
var externalRecordIds = /* @__PURE__ */ new WeakMap();
|
|
2226
|
+
var nextExternalRecordId = 1;
|
|
2227
|
+
function analysisFileCacheKey(filePath, source, activeCases, externalCallRecords) {
|
|
2228
|
+
let cases = "-";
|
|
2229
|
+
if (activeCases && activeCases.size > 0) {
|
|
2230
|
+
cases = [...activeCases.entries()].sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0).map(([k, v]) => `${k}=${v}`).join(",");
|
|
2231
|
+
}
|
|
2232
|
+
let ext = "-";
|
|
2233
|
+
if (externalCallRecords && externalCallRecords.length > 0) {
|
|
2234
|
+
let id = externalRecordIds.get(externalCallRecords);
|
|
2235
|
+
if (id === void 0) {
|
|
2236
|
+
id = nextExternalRecordId++;
|
|
2237
|
+
externalRecordIds.set(externalCallRecords, id);
|
|
2238
|
+
}
|
|
2239
|
+
ext = `n${externalCallRecords.length}#id${id}`;
|
|
2240
|
+
}
|
|
2241
|
+
return {
|
|
2242
|
+
filePath,
|
|
2243
|
+
// 尾部无 @nudo 注释/空行不进键:comment-only 编辑命中 AnalysisResult
|
|
2244
|
+
source: stableAnalyzeKeySource2(source),
|
|
2245
|
+
auxKey: `${cases}\0${ext}`
|
|
2246
|
+
};
|
|
2247
|
+
}
|
|
2248
|
+
function cloneAnalysisResult(r) {
|
|
2249
|
+
return {
|
|
2250
|
+
functions: r.functions.map(cloneFunctionAnalysis),
|
|
2251
|
+
diagnostics: r.diagnostics.map((d) => ({ ...d })),
|
|
2252
|
+
bindings: new Map(r.bindings),
|
|
2253
|
+
// Node 键与 AST LRU 共享身份;Map 浅拷贝即可
|
|
2254
|
+
nodeTypeMap: new Map(r.nodeTypeMap),
|
|
2255
|
+
caseHints: r.caseHints.map((h) => ({ ...h })),
|
|
2256
|
+
...r.externalFunctions ? { externalFunctions: r.externalFunctions.map(cloneFunctionAnalysis) } : {}
|
|
2257
|
+
};
|
|
2258
|
+
}
|
|
2259
|
+
function cloneFunctionAnalysis(a) {
|
|
2260
|
+
return {
|
|
2261
|
+
...a,
|
|
2262
|
+
paramNames: [...a.paramNames],
|
|
2263
|
+
cases: a.cases.map((c) => ({
|
|
2264
|
+
...c,
|
|
2265
|
+
args: [...c.args],
|
|
2266
|
+
...c.intension ? { intension: { ...c.intension } } : {}
|
|
2267
|
+
})),
|
|
2268
|
+
loc: { start: { ...a.loc.start }, end: { ...a.loc.end } }
|
|
2269
|
+
};
|
|
2270
|
+
}
|
|
2271
|
+
function shiftSourceLoc(loc, lineDelta) {
|
|
2272
|
+
return {
|
|
2273
|
+
start: { line: loc.start.line + lineDelta, column: loc.start.column },
|
|
2274
|
+
end: { line: loc.end.line + lineDelta, column: loc.end.column }
|
|
2275
|
+
};
|
|
2276
|
+
}
|
|
2277
|
+
function shiftDiagnosticLines(d, lineDelta) {
|
|
2278
|
+
if (lineDelta === 0) return { ...d };
|
|
2279
|
+
const out = { ...d, range: shiftSourceLoc(d.range, lineDelta) };
|
|
2280
|
+
if (d.origin) {
|
|
2281
|
+
out.origin = { line: d.origin.line + lineDelta, column: d.origin.column };
|
|
2282
|
+
}
|
|
2283
|
+
return out;
|
|
2284
|
+
}
|
|
2285
|
+
function shiftCallRecordLines(r, lineDelta) {
|
|
2286
|
+
if (lineDelta === 0 || !r.callLoc) return { ...r };
|
|
2287
|
+
return {
|
|
2288
|
+
...r,
|
|
2289
|
+
callLoc: { line: r.callLoc.line + lineDelta, column: r.callLoc.column }
|
|
2290
|
+
};
|
|
2291
|
+
}
|
|
2292
|
+
function analyzeFile(filePath, source, activeCases, externalCallRecords) {
|
|
2293
|
+
const k = analysisFileCacheKey(filePath, source, activeCases, externalCallRecords);
|
|
2294
|
+
const hit = analysisCacheGet(k.filePath, k.source, k.auxKey);
|
|
2295
|
+
if (hit !== void 0) {
|
|
2296
|
+
return cloneAnalysisResult(hit);
|
|
2297
|
+
}
|
|
2298
|
+
const result = analyzeFileUncached(filePath, source, activeCases, externalCallRecords);
|
|
2299
|
+
analysisCacheSet(k.filePath, k.source, k.auxKey, result);
|
|
2300
|
+
return cloneAnalysisResult(result);
|
|
2301
|
+
}
|
|
2302
|
+
function analyzeFileUncached(filePath, source, activeCases, externalCallRecords) {
|
|
2303
|
+
for (const rec of externalCallRecords ?? []) {
|
|
2304
|
+
for (const a of rec.argTypes) setUsageSiteTag(a);
|
|
2305
|
+
}
|
|
2306
|
+
const ast = parse5(source);
|
|
61
2307
|
const functions = extractDirectives(ast);
|
|
62
2308
|
const diagnostics = [];
|
|
63
2309
|
const bindings = /* @__PURE__ */ new Map();
|
|
@@ -66,34 +2312,329 @@ function analyzeFile(filePath, source, activeCases) {
|
|
|
66
2312
|
const caseHints = [];
|
|
67
2313
|
resetMemo();
|
|
68
2314
|
resetUnreachableRanges();
|
|
2315
|
+
resetEnvModules();
|
|
2316
|
+
resetMockModules();
|
|
69
2317
|
setModuleResolver(resolveModule);
|
|
70
|
-
setCurrentFileDir(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
2318
|
+
setCurrentFileDir(dirname5(filePath));
|
|
2319
|
+
setCurrentSource(source);
|
|
2320
|
+
const fileDirectives = extractFileDirectives(ast);
|
|
2321
|
+
const fileEnvNames = fileDirectives.filter((d) => d.kind === "env").flatMap((d) => d.envs);
|
|
2322
|
+
const projectConfig = findProjectConfig(dirname5(filePath));
|
|
2323
|
+
const projectEnvNames = projectConfig?.config.env ?? [];
|
|
2324
|
+
const envNames = [.../* @__PURE__ */ new Set([...projectEnvNames, ...fileEnvNames])];
|
|
2325
|
+
setUnknownBuiltinHandler((name, loc) => {
|
|
2326
|
+
if (isBPathCapable(source, envNames)) return;
|
|
2327
|
+
diagnostics.push({
|
|
2328
|
+
range: loc ? { start: loc.start, end: loc.end } : { start: { line: 0, column: 0 }, end: { line: 0, column: 0 } },
|
|
2329
|
+
severity: "warning",
|
|
2330
|
+
message: `Built-in API "${name}" is not covered by Nudo's type inference`,
|
|
2331
|
+
code: "nudo:builtin-unknown",
|
|
2332
|
+
suggestions: [
|
|
2333
|
+
`Use @nudo:mock to define the type: @nudo:mock ${name} = stub().returns(...)`,
|
|
2334
|
+
`Or use @nudo:refine return <constraint> to declare the return contract`
|
|
2335
|
+
]
|
|
2336
|
+
});
|
|
2337
|
+
});
|
|
2338
|
+
const globalEnv = createEnvironment2();
|
|
2339
|
+
{
|
|
2340
|
+
const loaded = envNames.length > 0 ? loadEnvs2(envNames, globalEnv) : { modules: {} };
|
|
2341
|
+
const auto = autoHarvestModules(source, dirname5(filePath));
|
|
2342
|
+
const modules = { ...loaded.modules, ...auto };
|
|
2343
|
+
if (Object.keys(modules).length > 0) setEnvModules(modules);
|
|
2344
|
+
}
|
|
2345
|
+
const mocks = /* @__PURE__ */ new Map();
|
|
2346
|
+
if (projectConfig?.config.mocks) {
|
|
2347
|
+
for (const [source2, mockPath] of Object.entries(projectConfig.config.mocks)) {
|
|
2348
|
+
mocks.set(source2, { fromPath: resolve4(projectConfig.projectDir, mockPath) });
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
const mockModuleDirectives = fileDirectives.filter((d) => d.kind === "mock-module");
|
|
2352
|
+
for (const d of mockModuleDirectives) {
|
|
2353
|
+
mocks.set(d.source, { fromPath: d.fromPath, names: d.names });
|
|
2354
|
+
}
|
|
2355
|
+
if (mocks.size > 0) {
|
|
2356
|
+
setMockModules(mocks);
|
|
2357
|
+
}
|
|
2358
|
+
const callRecords = [];
|
|
2359
|
+
setCallCollector((record) => callRecords.push(record));
|
|
2360
|
+
const unknownRecords = [];
|
|
2361
|
+
setUnknownCollector((r) => unknownRecords.push(r));
|
|
2362
|
+
setProvenanceTracking(true);
|
|
2363
|
+
const bMemberDiagNames = /* @__PURE__ */ new Set();
|
|
2364
|
+
const bMemberDiagSeen = /* @__PURE__ */ new Set();
|
|
2365
|
+
const pushBMemberDiag = (d, fallbackLine) => {
|
|
2366
|
+
bMemberDiagNames.add(d.name);
|
|
2367
|
+
const key = `${d.kind}:${d.name}:${d.receiver}:${d.line ?? fallbackLine}:${d.column ?? 0}`;
|
|
2368
|
+
if (bMemberDiagSeen.has(key)) return;
|
|
2369
|
+
bMemberDiagSeen.add(key);
|
|
2370
|
+
if (d.receiver === "unknown") {
|
|
2371
|
+
diagnostics.push({
|
|
2372
|
+
range: {
|
|
2373
|
+
start: { line: d.line ?? fallbackLine, column: d.column ?? 0 },
|
|
2374
|
+
end: { line: d.line ?? fallbackLine, column: (d.column ?? 0) + d.name.length }
|
|
2375
|
+
},
|
|
2376
|
+
severity: "warning",
|
|
2377
|
+
message: `Cannot resolve '${d.name}' on unknown value`,
|
|
2378
|
+
code: "nudo:unknown-recv",
|
|
2379
|
+
...d.origin ? { origin: d.origin } : {}
|
|
2380
|
+
});
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
75
2383
|
diagnostics.push({
|
|
76
|
-
range:
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
2384
|
+
range: {
|
|
2385
|
+
start: { line: d.line ?? fallbackLine, column: d.column ?? 0 },
|
|
2386
|
+
end: { line: d.line ?? fallbackLine, column: (d.column ?? 0) + d.name.length }
|
|
2387
|
+
},
|
|
2388
|
+
severity: d.receiver === "number" || d.receiver === "boolean" || d.receiver === "bigint" || d.receiver === "symbol" ? "error" : "warning",
|
|
2389
|
+
message: d.kind === "method" ? `Method '${d.name}' does not exist on type '${d.receiver}'` : `Property '${d.name}' does not exist on type '${d.receiver}'`,
|
|
2390
|
+
code: "nudo:no-method",
|
|
2391
|
+
...d.origin ? { origin: d.origin } : {}
|
|
2392
|
+
});
|
|
2393
|
+
};
|
|
2394
|
+
for (const fn of functions) {
|
|
2395
|
+
applyMocks(fn.directives, globalEnv, filePath, diagnostics);
|
|
2396
|
+
}
|
|
2397
|
+
const selfContained = isSelfContainedSource(source, envNames);
|
|
2398
|
+
const canAbsModules = absModulesOk(source, envNames);
|
|
2399
|
+
const bCapable = isBPathCapable(source, envNames);
|
|
2400
|
+
const bModuleIssueKinds = /* @__PURE__ */ new Set();
|
|
2401
|
+
const bTruncatedFns = /* @__PURE__ */ new Set();
|
|
2402
|
+
const bBuiltinUnknownNames = /* @__PURE__ */ new Set();
|
|
2403
|
+
let bHostedEval = false;
|
|
2404
|
+
const pushBModuleIssues = (issues) => {
|
|
2405
|
+
if (!issues) return;
|
|
2406
|
+
for (const iss of issues) {
|
|
2407
|
+
bModuleIssueKinds.add(iss.kind);
|
|
2408
|
+
const code = iss.kind === "cycle" ? "nudo:module-cycle" : iss.kind === "depth" ? "nudo:module-depth" : "nudo:module-missing";
|
|
2409
|
+
diagnostics.push({
|
|
2410
|
+
range: { start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
2411
|
+
severity: iss.kind === "missing" ? "error" : "warning",
|
|
2412
|
+
message: iss.reason,
|
|
2413
|
+
code
|
|
2414
|
+
});
|
|
2415
|
+
}
|
|
2416
|
+
};
|
|
2417
|
+
const seeds = mockDirectivesToAbsSeeds(functions);
|
|
2418
|
+
let absCallRecords = [];
|
|
2419
|
+
let bTopCallRecords = [];
|
|
2420
|
+
let absGraphModules;
|
|
2421
|
+
let absBindsShared;
|
|
2422
|
+
let absNodesShared;
|
|
2423
|
+
if (bCapable && filePath) {
|
|
2424
|
+
try {
|
|
2425
|
+
const g = evalAbsModuleGraph(source, filePath, {
|
|
2426
|
+
seedVars: seeds.seedVars,
|
|
2427
|
+
seedFns: seeds.seedFns
|
|
2428
|
+
});
|
|
2429
|
+
absGraphModules = g.modules;
|
|
2430
|
+
pushBModuleIssues(g.issues);
|
|
2431
|
+
} catch {
|
|
2432
|
+
}
|
|
2433
|
+
const bRun = tryRunBPath(source, filePath, {
|
|
2434
|
+
envNames,
|
|
2435
|
+
mocks: mockSeedsToAbsMocks(seeds)
|
|
80
2436
|
});
|
|
2437
|
+
if (bRun) {
|
|
2438
|
+
bHostedEval = true;
|
|
2439
|
+
if (bRun.memberDiags?.length) {
|
|
2440
|
+
for (const d of bRun.memberDiags) {
|
|
2441
|
+
pushBMemberDiag(d, 1);
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
if (bRun.truncatedFns) {
|
|
2445
|
+
for (const fn of bRun.truncatedFns) bTruncatedFns.add(fn);
|
|
2446
|
+
}
|
|
2447
|
+
if (bRun.calls?.length) {
|
|
2448
|
+
const impMap = buildAbsImportLocalMap(source, filePath);
|
|
2449
|
+
for (const c of bRun.calls) {
|
|
2450
|
+
const safeAbs = (a) => {
|
|
2451
|
+
if (!a || typeof a !== "object" || !("shape" in a) || !a.shape) {
|
|
2452
|
+
return T2.unknown;
|
|
2453
|
+
}
|
|
2454
|
+
try {
|
|
2455
|
+
return absToTypeValue2(a);
|
|
2456
|
+
} catch {
|
|
2457
|
+
return T2.unknown;
|
|
2458
|
+
}
|
|
2459
|
+
};
|
|
2460
|
+
const rec = {
|
|
2461
|
+
fnName: c.fnName,
|
|
2462
|
+
argTypes: c.args.map(safeAbs),
|
|
2463
|
+
resultType: c.threw ? T2.never : safeAbs(c.result),
|
|
2464
|
+
throws: c.threw ? safeAbs(c.result) : T2.never,
|
|
2465
|
+
callLoc: c.callLoc
|
|
2466
|
+
};
|
|
2467
|
+
const imp = impMap.get(c.fnName);
|
|
2468
|
+
if (imp) {
|
|
2469
|
+
rec.targetModule = imp.modulePath;
|
|
2470
|
+
rec.targetExport = imp.exportName;
|
|
2471
|
+
}
|
|
2472
|
+
bTopCallRecords.push(rec);
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
if (selfContained || canAbsModules) {
|
|
2478
|
+
setAbsTruncationCollector2((label) => bTruncatedFns.add(label));
|
|
2479
|
+
try {
|
|
2480
|
+
absCallRecords = collectAbsCallRecords(source, seeds, filePath, absGraphModules);
|
|
2481
|
+
} finally {
|
|
2482
|
+
setAbsTruncationCollector2(null);
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
if (bHostedEval) {
|
|
2486
|
+
try {
|
|
2487
|
+
const collected = collectAbsBindsAndNodes(source, seeds, absGraphModules);
|
|
2488
|
+
absBindsShared = collected.binds;
|
|
2489
|
+
absNodesShared = collected.nodes;
|
|
2490
|
+
for (const [name, absVal] of absBindsShared) {
|
|
2491
|
+
if (absVal?.shape?.k === "fn") continue;
|
|
2492
|
+
if (!globalEnv.has(name)) {
|
|
2493
|
+
globalEnv.bind(name, absToTypeValue2(absVal));
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
for (const [node, absVal] of absNodesShared) {
|
|
2497
|
+
nodeTypeMap.set(node, absToTypeValue2(absVal));
|
|
2498
|
+
}
|
|
2499
|
+
} catch {
|
|
2500
|
+
}
|
|
2501
|
+
} else {
|
|
2502
|
+
evaluateProgram(ast, globalEnv);
|
|
2503
|
+
}
|
|
2504
|
+
if ((selfContained || canAbsModules) && absCallRecords.length > 0) {
|
|
2505
|
+
callRecords.length = 0;
|
|
2506
|
+
callRecords.push(...absCallRecords);
|
|
2507
|
+
} else if (bTopCallRecords.length > 0) {
|
|
2508
|
+
callRecords.length = 0;
|
|
2509
|
+
callRecords.push(...bTopCallRecords);
|
|
2510
|
+
}
|
|
2511
|
+
const unreachableRanges = bCapable ? [] : getUnreachableRanges();
|
|
2512
|
+
if (bCapable) {
|
|
2513
|
+
const bKnownGlobals = /* @__PURE__ */ new Set([
|
|
2514
|
+
...Object.keys(collectEnvGlobals(envNames)),
|
|
2515
|
+
...Object.keys(mockSeedsToAbsMocks(seeds))
|
|
2516
|
+
]);
|
|
2517
|
+
const bDiag = collectBPathDiagnostics(source, bKnownGlobals);
|
|
2518
|
+
for (const ur of bDiag.unreachable) {
|
|
2519
|
+
diagnostics.push({
|
|
2520
|
+
range: ur.range,
|
|
2521
|
+
severity: "info",
|
|
2522
|
+
message: "Code after return/throw statement is unreachable",
|
|
2523
|
+
tags: ["unnecessary"],
|
|
2524
|
+
code: "nudo-unreachable",
|
|
2525
|
+
suggestions: ["Remove the unreachable code after the return/throw statement"]
|
|
2526
|
+
});
|
|
2527
|
+
}
|
|
2528
|
+
for (const b of bDiag.builtinUnknown) {
|
|
2529
|
+
bBuiltinUnknownNames.add(b.name);
|
|
2530
|
+
diagnostics.push({
|
|
2531
|
+
range: b.range,
|
|
2532
|
+
severity: "warning",
|
|
2533
|
+
message: `Built-in API "${b.name}" is not covered by Nudo's type inference`,
|
|
2534
|
+
code: "nudo:builtin-unknown",
|
|
2535
|
+
suggestions: [
|
|
2536
|
+
`Use @nudo:mock to define the type: @nudo:mock ${b.name} = stub().returns(...)`,
|
|
2537
|
+
`Or use @nudo:refine return <constraint> to declare the return contract`
|
|
2538
|
+
]
|
|
2539
|
+
});
|
|
2540
|
+
}
|
|
2541
|
+
} else {
|
|
2542
|
+
for (const ur of unreachableRanges) {
|
|
2543
|
+
diagnostics.push({
|
|
2544
|
+
range: ur,
|
|
2545
|
+
severity: "info",
|
|
2546
|
+
message: "Code after return/throw statement is unreachable",
|
|
2547
|
+
tags: ["unnecessary"],
|
|
2548
|
+
code: "nudo-unreachable",
|
|
2549
|
+
suggestions: ["Remove the unreachable code after the return/throw statement"]
|
|
2550
|
+
});
|
|
2551
|
+
}
|
|
81
2552
|
}
|
|
82
2553
|
collectBindings(ast, globalEnv, bindings);
|
|
2554
|
+
if (isBPathCapable(source, envNames)) {
|
|
2555
|
+
try {
|
|
2556
|
+
const absBinds = absBindsShared ?? collectAbsBindingsFromGraph(source, filePath, {
|
|
2557
|
+
seedVars: seeds.seedVars,
|
|
2558
|
+
seedFns: seeds.seedFns
|
|
2559
|
+
});
|
|
2560
|
+
for (const [name, absVal] of absBinds) {
|
|
2561
|
+
const prev = bindings.get(name);
|
|
2562
|
+
bindings.set(name, {
|
|
2563
|
+
type: absToTypeValue2(absVal),
|
|
2564
|
+
loc: prev?.loc
|
|
2565
|
+
});
|
|
2566
|
+
}
|
|
2567
|
+
} catch {
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
const synthCandidates = [];
|
|
2571
|
+
let fnFpMap;
|
|
2572
|
+
try {
|
|
2573
|
+
fnFpMap = fnFingerprints(source, ast);
|
|
2574
|
+
} catch {
|
|
2575
|
+
fnFpMap = void 0;
|
|
2576
|
+
}
|
|
2577
|
+
const envKeyFn = envNames.join(",");
|
|
2578
|
+
const mockKeyFn = mockSeedFingerprint(seeds.seedVars, seeds.seedFns);
|
|
83
2579
|
for (const fn of functions) {
|
|
84
|
-
applyMocks(fn.directives, globalEnv, filePath);
|
|
85
2580
|
const isPure = fn.directives.some((d) => d.kind === "pure");
|
|
86
2581
|
const skipDirective = fn.directives.find((d) => d.kind === "skip");
|
|
87
|
-
const returnsDirective = fn.directives.find((d) => d.kind === "returns");
|
|
88
2582
|
const fnLoc = locFromNode(fn.node);
|
|
89
|
-
const
|
|
2583
|
+
const paramNames = extractParamNames(fn.node);
|
|
2584
|
+
const analysis = { name: fn.name, loc: fnLoc, paramNames, cases: [] };
|
|
90
2585
|
if (skipDirective && skipDirective.kind === "skip") {
|
|
2586
|
+
analysis.skipped = true;
|
|
91
2587
|
if (skipDirective.returns) {
|
|
92
2588
|
analysis.combined = skipDirective.returns;
|
|
93
2589
|
}
|
|
94
2590
|
functionResults.push(analysis);
|
|
95
2591
|
continue;
|
|
96
2592
|
}
|
|
2593
|
+
const caseDirectives = fn.directives.filter((d) => d.kind === "case");
|
|
2594
|
+
const activeCaseIdx = activeCases?.get(fn.name) ?? 0;
|
|
2595
|
+
const fp = fnFpMap?.get(fn.name);
|
|
2596
|
+
const fnCacheKey = fp && caseDirectives.length > 0 ? [
|
|
2597
|
+
filePath,
|
|
2598
|
+
fp.own,
|
|
2599
|
+
fp.deps,
|
|
2600
|
+
String(activeCaseIdx),
|
|
2601
|
+
caseDirectiveKey(caseDirectives, typeValueToString3),
|
|
2602
|
+
envKeyFn,
|
|
2603
|
+
mockKeyFn
|
|
2604
|
+
].join("\0") : void 0;
|
|
2605
|
+
const dLen0 = diagnostics.length;
|
|
2606
|
+
const hLen0 = caseHints.length;
|
|
2607
|
+
const cLen0 = callRecords.length;
|
|
2608
|
+
if (fnCacheKey) {
|
|
2609
|
+
const hitFn = fnAnalysisCacheGet(fnCacheKey);
|
|
2610
|
+
if (hitFn) {
|
|
2611
|
+
const cached = cloneFunctionAnalysis(hitFn.analysis);
|
|
2612
|
+
const lineDelta = fnLoc.start.line - cached.loc.start.line;
|
|
2613
|
+
cached.loc = fnLoc;
|
|
2614
|
+
if (lineDelta !== 0) {
|
|
2615
|
+
for (const c of cached.cases) {
|
|
2616
|
+
if (c.throwLoc) c.throwLoc = shiftSourceLoc(c.throwLoc, lineDelta);
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
if (lineDelta === 0) {
|
|
2620
|
+
diagnostics.push(...hitFn.diagnostics);
|
|
2621
|
+
caseHints.push(...hitFn.caseHints);
|
|
2622
|
+
callRecords.push(...hitFn.callRecords);
|
|
2623
|
+
} else {
|
|
2624
|
+
for (const d of hitFn.diagnostics) {
|
|
2625
|
+
diagnostics.push(shiftDiagnosticLines(d, lineDelta));
|
|
2626
|
+
}
|
|
2627
|
+
for (const h of hitFn.caseHints) {
|
|
2628
|
+
caseHints.push({ ...h, line: h.line + lineDelta });
|
|
2629
|
+
}
|
|
2630
|
+
for (const r of hitFn.callRecords) {
|
|
2631
|
+
callRecords.push(shiftCallRecordLines(r, lineDelta));
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
functionResults.push(cached);
|
|
2635
|
+
continue;
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
97
2638
|
if (isPure) {
|
|
98
2639
|
const fnVal = globalEnv.has(fn.name) ? globalEnv.lookup(fn.name) : null;
|
|
99
2640
|
if (fnVal && fnVal.kind === "function") {
|
|
@@ -106,34 +2647,114 @@ function analyzeFile(filePath, source, activeCases) {
|
|
|
106
2647
|
} else {
|
|
107
2648
|
setSampleCount(3);
|
|
108
2649
|
}
|
|
109
|
-
|
|
110
|
-
|
|
2650
|
+
if (caseDirectives.length === 0) {
|
|
2651
|
+
synthCandidates.push({ name: fn.name, node: fn.node, analysis });
|
|
2652
|
+
}
|
|
111
2653
|
for (let ci = 0; ci < caseDirectives.length; ci++) {
|
|
112
2654
|
const directive = caseDirectives[ci];
|
|
113
2655
|
resetUnreachableRanges();
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
2656
|
+
let caseValue;
|
|
2657
|
+
let caseAbs;
|
|
2658
|
+
let caseThrows;
|
|
2659
|
+
let fullResult = null;
|
|
2660
|
+
let caseUnreachable = [];
|
|
2661
|
+
const bCapable2 = isBPathCapable(source, envNames);
|
|
2662
|
+
const bPrimary = bCapable2;
|
|
2663
|
+
if (bCapable2 && filePath) {
|
|
2664
|
+
const bFull = tryBPathCallFull(
|
|
2665
|
+
source,
|
|
2666
|
+
filePath,
|
|
2667
|
+
fn.name,
|
|
2668
|
+
directive.args.map((a) => typeValueToAbs5(a)),
|
|
2669
|
+
{ collectCalls: true, envNames, mocks: mockSeedsToAbsMocks(seeds) }
|
|
2670
|
+
);
|
|
2671
|
+
const res = bFull?.result;
|
|
2672
|
+
const weakUnknown = !!res && res.shape.k === "unknown" && (!res.term || res.term.op === "lit" && res.term.value === void 0);
|
|
2673
|
+
const bOk = !!res && (bHostedEval || !weakUnknown && res.conf !== "opaque");
|
|
2674
|
+
if (bOk && bFull && bPrimary) {
|
|
2675
|
+
caseAbs = bFull.result;
|
|
2676
|
+
caseValue = absToTypeValue2(bFull.result);
|
|
2677
|
+
caseThrows = bFull.throws.shape.k !== "never" ? absToTypeValue2(bFull.throws) : T2.never;
|
|
2678
|
+
fullResult = { value: caseValue, throws: caseThrows };
|
|
2679
|
+
for (const d of bFull.memberDiags ?? []) {
|
|
2680
|
+
pushBMemberDiag(d, fnLoc.start.line);
|
|
2681
|
+
}
|
|
2682
|
+
if (bFull.calls?.length) {
|
|
2683
|
+
const impMap = buildAbsImportLocalMap(source, filePath);
|
|
2684
|
+
const safeAbs = (a) => {
|
|
2685
|
+
if (!a || typeof a !== "object" || !("shape" in a) || !a.shape) {
|
|
2686
|
+
return T2.unknown;
|
|
2687
|
+
}
|
|
2688
|
+
try {
|
|
2689
|
+
return absToTypeValue2(a);
|
|
2690
|
+
} catch {
|
|
2691
|
+
return T2.unknown;
|
|
2692
|
+
}
|
|
2693
|
+
};
|
|
2694
|
+
for (const c of bFull.calls) {
|
|
2695
|
+
const rec = {
|
|
2696
|
+
fnName: c.fnName,
|
|
2697
|
+
argTypes: c.args.map(safeAbs),
|
|
2698
|
+
resultType: c.threw ? T2.never : safeAbs(c.result),
|
|
2699
|
+
throws: c.threw ? safeAbs(c.result) : T2.never,
|
|
2700
|
+
callLoc: c.callLoc
|
|
2701
|
+
};
|
|
2702
|
+
const imp = impMap.get(c.fnName);
|
|
2703
|
+
if (imp) {
|
|
2704
|
+
rec.targetModule = imp.modulePath;
|
|
2705
|
+
rec.targetExport = imp.exportName;
|
|
2706
|
+
}
|
|
2707
|
+
callRecords.push(rec);
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
if (!fullResult) {
|
|
2713
|
+
if (bHostedEval) {
|
|
2714
|
+
fullResult = { value: T2.unknown, throws: T2.never };
|
|
2715
|
+
caseValue = T2.unknown;
|
|
2716
|
+
} else {
|
|
2717
|
+
fullResult = evaluateFunctionFull(fn.node, directive.args, globalEnv);
|
|
2718
|
+
caseUnreachable = [...getUnreachableRanges()];
|
|
2719
|
+
caseValue = fullResult.value;
|
|
2720
|
+
if ((selfContained || canAbsModules) && fullResult.value.kind !== "never") {
|
|
2721
|
+
caseAbs = tryEvalAbsRaw(source, fn.name, directive.args, filePath, mockSeedsToAbsMocks(seeds));
|
|
2722
|
+
if (caseAbs) {
|
|
2723
|
+
const projected = absToTypeValue2(caseAbs);
|
|
2724
|
+
if (absIsBetter(projected, fullResult.value)) caseValue = projected;
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
const tv = caseValue;
|
|
2730
|
+
const throwsTv = caseThrows ?? fullResult.throws;
|
|
2731
|
+
const caseEntry = {
|
|
117
2732
|
name: directive.name,
|
|
118
2733
|
args: directive.args,
|
|
119
|
-
result:
|
|
120
|
-
throws:
|
|
121
|
-
throwLoc: fullResult.throwLoc
|
|
122
|
-
|
|
2734
|
+
result: tv,
|
|
2735
|
+
throws: throwsTv,
|
|
2736
|
+
throwLoc: fullResult.throwLoc,
|
|
2737
|
+
expected: directive.expected,
|
|
2738
|
+
source: "directive"
|
|
2739
|
+
};
|
|
2740
|
+
tryAttachIntension(caseEntry, source, fn.name);
|
|
2741
|
+
if (caseAbs) attachAbsToIntension(caseEntry, caseAbs, fn.name);
|
|
2742
|
+
analysis.cases.push(caseEntry);
|
|
123
2743
|
if (directive.commentLine) {
|
|
124
|
-
const hasThrow =
|
|
125
|
-
const resultStr =
|
|
126
|
-
const throwStr = hasThrow ? `throws ${
|
|
2744
|
+
const hasThrow = throwsTv.kind !== "never";
|
|
2745
|
+
const resultStr = tv.kind !== "never" ? typeValueToString3(tv) : "";
|
|
2746
|
+
const throwStr = hasThrow ? `throws ${typeValueToString3(throwsTv)}` : "";
|
|
127
2747
|
const label = [resultStr, throwStr].filter(Boolean).join(" ");
|
|
128
2748
|
const hintLabel = `=> ${label}`;
|
|
129
2749
|
let ok = true;
|
|
130
2750
|
if (directive.expected) {
|
|
131
|
-
ok = isSubtypeOf(
|
|
2751
|
+
ok = isSubtypeOf(tv, directive.expected);
|
|
132
2752
|
if (!ok) {
|
|
133
2753
|
diagnostics.push({
|
|
134
2754
|
range: { start: { line: directive.commentLine, column: 0 }, end: { line: directive.commentLine, column: 999 } },
|
|
135
2755
|
severity: "error",
|
|
136
|
-
message: `Case "${directive.name}": expected ${
|
|
2756
|
+
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`,
|
|
2757
|
+
code: "nudo:case-expected"
|
|
137
2758
|
});
|
|
138
2759
|
}
|
|
139
2760
|
}
|
|
@@ -141,74 +2762,292 @@ function analyzeFile(filePath, source, activeCases) {
|
|
|
141
2762
|
}
|
|
142
2763
|
const isActive = ci === Math.min(activeCaseIdx, caseDirectives.length - 1);
|
|
143
2764
|
if (isActive) {
|
|
144
|
-
if (
|
|
145
|
-
const throwRange = fullResult
|
|
2765
|
+
if (throwsTv.kind !== "never") {
|
|
2766
|
+
const throwRange = fullResult?.throwLoc ?? fnLoc;
|
|
146
2767
|
diagnostics.push({
|
|
147
2768
|
range: throwRange,
|
|
148
2769
|
severity: "warning",
|
|
149
|
-
message: `Function "${fn.name}" case "${directive.name}" may throw: ${
|
|
2770
|
+
message: `Function "${fn.name}" case "${directive.name}" may throw: ${typeValueToString3(throwsTv)}. Consider adding a try-catch block or using @nudo:refine return <constraint>`,
|
|
2771
|
+
code: "nudo-may-throw"
|
|
150
2772
|
});
|
|
151
2773
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
2774
|
+
if (!isBPathCapable(source, envNames)) {
|
|
2775
|
+
for (const ur of caseUnreachable) {
|
|
2776
|
+
diagnostics.push({
|
|
2777
|
+
range: ur,
|
|
2778
|
+
severity: "info",
|
|
2779
|
+
message: "Code after return/throw statement is unreachable",
|
|
2780
|
+
tags: ["unnecessary"],
|
|
2781
|
+
code: "nudo-unreachable",
|
|
2782
|
+
suggestions: ["Remove the unreachable code after the return/throw statement"]
|
|
2783
|
+
});
|
|
2784
|
+
}
|
|
159
2785
|
}
|
|
160
2786
|
}
|
|
161
2787
|
}
|
|
162
2788
|
if (analysis.cases.length > 1) {
|
|
163
|
-
analysis.combined = simplifyUnion(analysis.cases.map((c) => c.result));
|
|
2789
|
+
analysis.combined = collapseLiteralUnion(simplifyUnion(analysis.cases.map((c) => c.result)), COLLAPSE_LITERAL_THRESHOLD);
|
|
164
2790
|
} else if (analysis.cases.length === 1) {
|
|
165
2791
|
analysis.combined = analysis.cases[0].result;
|
|
166
2792
|
}
|
|
167
|
-
if (
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
2793
|
+
if (fnCacheKey) {
|
|
2794
|
+
fnAnalysisCacheSet(fnCacheKey, {
|
|
2795
|
+
analysis: cloneFunctionAnalysis(analysis),
|
|
2796
|
+
diagnostics: diagnostics.slice(dLen0),
|
|
2797
|
+
caseHints: caseHints.slice(hLen0),
|
|
2798
|
+
callRecords: callRecords.slice(cLen0)
|
|
2799
|
+
});
|
|
2800
|
+
}
|
|
2801
|
+
functionResults.push(analysis);
|
|
2802
|
+
}
|
|
2803
|
+
const directiveFnNames = new Set(functions.map((f) => f.name));
|
|
2804
|
+
const directiveFnStmts = new Set(functions.map((f) => f.node));
|
|
2805
|
+
for (const { name, node, stmt, noDeclaration } of collectTopLevelFunctions(ast)) {
|
|
2806
|
+
if (directiveFnNames.has(name)) continue;
|
|
2807
|
+
if (directiveFnStmts.has(stmt)) continue;
|
|
2808
|
+
const analysis = { name, loc: locFromNode(node), paramNames: extractParamNames(node), cases: [] };
|
|
2809
|
+
if (noDeclaration) analysis.noDeclaration = true;
|
|
2810
|
+
functionResults.push(analysis);
|
|
2811
|
+
synthCandidates.push({ name, node, analysis });
|
|
2812
|
+
}
|
|
2813
|
+
const modulePathCache = /* @__PURE__ */ new Map();
|
|
2814
|
+
const normalizeModulePath = (p) => {
|
|
2815
|
+
let n = modulePathCache.get(p);
|
|
2816
|
+
if (n === void 0) {
|
|
2817
|
+
try {
|
|
2818
|
+
n = realpathSync(p);
|
|
2819
|
+
} catch {
|
|
2820
|
+
n = p;
|
|
2821
|
+
}
|
|
2822
|
+
modulePathCache.set(p, n);
|
|
2823
|
+
}
|
|
2824
|
+
return n;
|
|
2825
|
+
};
|
|
2826
|
+
const currentModulePath = normalizeModulePath(resolve4(filePath));
|
|
2827
|
+
const singleExportFn = findSingleModuleExportsFunction(ast);
|
|
2828
|
+
for (const candidate of synthCandidates) {
|
|
2829
|
+
const singleExportHit = singleExportFn !== null && candidate.node === singleExportFn;
|
|
2830
|
+
const nameRoutes = (r) => r.fnName === candidate.name || r.targetExport === candidate.name || (r.targetAliases?.includes(candidate.name) ?? false) || singleExportHit && (r.fnModule !== void 0 || r.targetModule !== void 0);
|
|
2831
|
+
const matchingLocal = (r) => {
|
|
2832
|
+
const targetsThisFile = r.targetModule === void 0 || normalizeModulePath(r.targetModule) === currentModulePath;
|
|
2833
|
+
if (!targetsThisFile) return false;
|
|
2834
|
+
return nameRoutes(r);
|
|
2835
|
+
};
|
|
2836
|
+
const matchingExternal = (r) => {
|
|
2837
|
+
const attributed = r.fnModule !== void 0 && normalizeModulePath(r.fnModule) === currentModulePath || r.targetModule !== void 0 && normalizeModulePath(r.targetModule) === currentModulePath;
|
|
2838
|
+
if (!attributed) return false;
|
|
2839
|
+
return nameRoutes(r);
|
|
2840
|
+
};
|
|
2841
|
+
const records = dedupeCallRecords(
|
|
2842
|
+
[
|
|
2843
|
+
...callRecords.filter(matchingLocal),
|
|
2844
|
+
...(externalCallRecords ?? []).filter(matchingExternal)
|
|
2845
|
+
].filter((r) => !(r.resultType.kind === "never" && r.throws.kind === "never"))
|
|
2846
|
+
);
|
|
2847
|
+
if (records.length > 0) {
|
|
2848
|
+
const informativeness = (r) => {
|
|
2849
|
+
if (r.resultType.kind === "unknown") return 2;
|
|
2850
|
+
if (r.resultType.kind === "never") return 1;
|
|
2851
|
+
return 0;
|
|
2852
|
+
};
|
|
2853
|
+
const ordered = records.map((r, i) => ({ r, i })).sort((a, b) => informativeness(a.r) - informativeness(b.r) || a.i - b.i).map(({ r }) => r);
|
|
2854
|
+
const precise = ordered.slice(0, MAX_PRECISE_CALLSITE_CASES);
|
|
2855
|
+
for (const rec of precise) {
|
|
2856
|
+
let absRaw;
|
|
2857
|
+
let absResult;
|
|
2858
|
+
if (rec.resultType.kind !== "never" && rec.argTypes.length > 0) {
|
|
2859
|
+
absRaw = tryEvalAbsRaw(source, candidate.name, rec.argTypes, filePath, mockSeedsToAbsMocks(seeds));
|
|
2860
|
+
if (absRaw) {
|
|
2861
|
+
const projected = absToTypeValue2(absRaw);
|
|
2862
|
+
if (absIsBetter(projected, rec.resultType)) absResult = projected;
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
const caseResult2 = {
|
|
2866
|
+
name: `call@L${rec.callLoc?.line ?? candidate.analysis.loc.start.line}`,
|
|
2867
|
+
args: rec.argTypes,
|
|
2868
|
+
result: absResult ?? rec.resultType,
|
|
2869
|
+
throws: rec.throws,
|
|
2870
|
+
source: "callsite"
|
|
2871
|
+
};
|
|
2872
|
+
tryAttachIntension(caseResult2, source, candidate.name);
|
|
2873
|
+
if (absRaw) attachAbsToIntension(caseResult2, absRaw, candidate.name);
|
|
2874
|
+
candidate.analysis.cases.push(caseResult2);
|
|
2875
|
+
}
|
|
2876
|
+
const remaining = ordered.slice(MAX_PRECISE_CALLSITE_CASES).filter((rec) => !rec.argTypes.some((a) => a.kind === "unknown"));
|
|
2877
|
+
if (remaining.length > 0) {
|
|
2878
|
+
const fnNode2 = resolveFunctionNode(candidate.node);
|
|
2879
|
+
const paramCount = extractParamNames(fnNode2).length;
|
|
2880
|
+
const widenedArgs = Array.from(
|
|
2881
|
+
{ length: paramCount },
|
|
2882
|
+
(_, i) => (
|
|
2883
|
+
// 缺参按真实 JS 语义 widen 成 undefined 而非 unknown——可选参守卫
|
|
2884
|
+
// (target || [] 等)对 unknown 全塌,对 undefined 正常走默认分支
|
|
2885
|
+
widenType(simplifyUnion(remaining.map((rec) => rec.argTypes[i] ?? T2.undefined)))
|
|
2886
|
+
)
|
|
2887
|
+
);
|
|
2888
|
+
const absSymRaw = tryEvalAbs(source, candidate.name, widenedArgs, filePath, mockSeedsToAbsMocks(seeds));
|
|
2889
|
+
const absSym = absSymRaw && absIsBetter(
|
|
2890
|
+
absSymRaw,
|
|
2891
|
+
/* 无先验:仅 unknown 时 */
|
|
2892
|
+
{ kind: "unknown" }
|
|
2893
|
+
) ? absSymRaw : void 0;
|
|
2894
|
+
let symValue;
|
|
2895
|
+
let symThrows = T2.never;
|
|
2896
|
+
let symLoc;
|
|
2897
|
+
let symAbs;
|
|
2898
|
+
if (isBPathCapable(source, envNames) && filePath) {
|
|
2899
|
+
const bSym = tryBPathCall(
|
|
2900
|
+
source,
|
|
2901
|
+
filePath,
|
|
2902
|
+
candidate.name,
|
|
2903
|
+
widenedArgs.map((a) => typeValueToAbs5(a)),
|
|
2904
|
+
{ envNames, mocks: mockSeedsToAbsMocks(seeds) }
|
|
2905
|
+
);
|
|
2906
|
+
if (bSym && (bHostedEval || !(bSym.shape.k === "unknown" && !bSym.term))) {
|
|
2907
|
+
symAbs = bSym;
|
|
2908
|
+
symValue = absToTypeValue2(bSym);
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
if (!symAbs && !bHostedEval) {
|
|
2912
|
+
const full = evaluateFunctionFull(fnNode2, widenedArgs, globalEnv);
|
|
2913
|
+
symValue = full.value.kind === "unknown" && absSym ? absSym : full.value;
|
|
2914
|
+
symThrows = full.throws;
|
|
2915
|
+
symLoc = full.throwLoc;
|
|
2916
|
+
}
|
|
2917
|
+
const symCase = {
|
|
2918
|
+
name: "call@symbolic",
|
|
2919
|
+
args: widenedArgs,
|
|
2920
|
+
result: symValue ?? T2.unknown,
|
|
2921
|
+
throws: symThrows,
|
|
2922
|
+
throwLoc: symLoc,
|
|
2923
|
+
source: "callsite",
|
|
2924
|
+
aggregatedFrom: remaining.length
|
|
2925
|
+
};
|
|
2926
|
+
tryAttachIntension(symCase, source, candidate.name);
|
|
2927
|
+
if (symAbs) attachAbsToIntension(symCase, symAbs, candidate.name);
|
|
2928
|
+
candidate.analysis.cases.push(symCase);
|
|
2929
|
+
}
|
|
2930
|
+
candidate.analysis.combined = collapseLiteralUnion(
|
|
2931
|
+
simplifyUnion(records.map((r) => r.resultType)),
|
|
2932
|
+
COLLAPSE_LITERAL_THRESHOLD
|
|
2933
|
+
);
|
|
2934
|
+
continue;
|
|
2935
|
+
}
|
|
2936
|
+
const fnNode = resolveFunctionNode(candidate.node);
|
|
2937
|
+
const args = extractParamNames(fnNode).map(() => T2.unknown);
|
|
2938
|
+
let entryValue;
|
|
2939
|
+
let entryThrows = T2.never;
|
|
2940
|
+
let entryLoc;
|
|
2941
|
+
let entryAbs;
|
|
2942
|
+
if (isBPathCapable(source, envNames) && filePath) {
|
|
2943
|
+
const bEntryFull = tryBPathCallFull(
|
|
2944
|
+
source,
|
|
2945
|
+
filePath,
|
|
2946
|
+
candidate.analysis.name,
|
|
2947
|
+
args.map((a) => typeValueToAbs5(a)),
|
|
2948
|
+
{ envNames, mocks: mockSeedsToAbsMocks(seeds), collectMemberDiags: true }
|
|
2949
|
+
);
|
|
2950
|
+
if (bEntryFull?.memberDiags?.length) {
|
|
2951
|
+
for (const d of bEntryFull.memberDiags) {
|
|
2952
|
+
pushBMemberDiag(d, candidate.analysis.loc.start.line);
|
|
180
2953
|
}
|
|
181
2954
|
}
|
|
2955
|
+
const bEntry = bEntryFull?.result;
|
|
2956
|
+
if (bEntry && (bHostedEval || !(bEntry.shape.k === "unknown" && !bEntry.term))) {
|
|
2957
|
+
entryAbs = bEntry;
|
|
2958
|
+
entryValue = absToTypeValue2(bEntry);
|
|
2959
|
+
}
|
|
182
2960
|
}
|
|
183
|
-
|
|
2961
|
+
if (!entryValue) {
|
|
2962
|
+
if (bHostedEval) {
|
|
2963
|
+
entryValue = T2.unknown;
|
|
2964
|
+
entryThrows = T2.never;
|
|
2965
|
+
} else {
|
|
2966
|
+
const absEntry = tryEvalEntryAbs(source, candidate.analysis.name, args, filePath, seeds.seedVars);
|
|
2967
|
+
const full = absEntry ? { value: absEntry, throws: T2.never, throwLoc: void 0 } : evaluateFunctionFull(fnNode, args, globalEnv);
|
|
2968
|
+
entryValue = full.value;
|
|
2969
|
+
entryThrows = full.throws;
|
|
2970
|
+
entryLoc = full.throwLoc;
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
const caseResult = {
|
|
2974
|
+
name: `entry@L${candidate.analysis.loc.start.line}`,
|
|
2975
|
+
args,
|
|
2976
|
+
result: entryValue,
|
|
2977
|
+
throws: entryThrows,
|
|
2978
|
+
throwLoc: entryLoc
|
|
2979
|
+
};
|
|
2980
|
+
tryAttachIntension(caseResult, source, candidate.analysis.name);
|
|
2981
|
+
if (entryAbs) attachAbsToIntension(caseResult, entryAbs, candidate.analysis.name);
|
|
2982
|
+
candidate.analysis.cases.push(caseResult);
|
|
2983
|
+
candidate.analysis.entryOnly = true;
|
|
2984
|
+
candidate.analysis.combined = collapseLiteralUnion(entryValue, COLLAPSE_LITERAL_THRESHOLD);
|
|
184
2985
|
}
|
|
185
|
-
|
|
2986
|
+
if (!bHostedEval) {
|
|
2987
|
+
buildNodeTypeMap(ast, globalEnv, nodeTypeMap);
|
|
2988
|
+
}
|
|
2989
|
+
setUnknownBuiltinHandler(null);
|
|
2990
|
+
setCallCollector(null);
|
|
2991
|
+
setUnknownCollector(null);
|
|
2992
|
+
setProvenanceTracking(false);
|
|
186
2993
|
setModuleResolver(null);
|
|
2994
|
+
resetEnvModules();
|
|
2995
|
+
resetMockModules();
|
|
2996
|
+
const maxLine = source.split("\n").length;
|
|
2997
|
+
diagnostics.push(
|
|
2998
|
+
...unknownRecordsToDiagnostics(
|
|
2999
|
+
unknownRecords.filter((r) => {
|
|
3000
|
+
if ((r.loc?.line ?? 0) > maxLine) return false;
|
|
3001
|
+
if (r.originModule === USAGE_SITE_MODULE) return false;
|
|
3002
|
+
if (bHostedEval && (r.kind === "method" || r.kind === "property")) {
|
|
3003
|
+
return false;
|
|
3004
|
+
}
|
|
3005
|
+
if ((r.kind === "method" || r.kind === "property") && bMemberDiagNames.has(r.name)) {
|
|
3006
|
+
return false;
|
|
3007
|
+
}
|
|
3008
|
+
if (r.kind === "global") {
|
|
3009
|
+
if (r.name.startsWith("module-cycle:") && bModuleIssueKinds.has("cycle")) return false;
|
|
3010
|
+
if (r.name.startsWith("module-depth:") && bModuleIssueKinds.has("depth")) return false;
|
|
3011
|
+
if (r.name.startsWith("module-missing:") && bModuleIssueKinds.has("missing")) return false;
|
|
3012
|
+
if (r.name.startsWith("recursion:")) {
|
|
3013
|
+
const fn = r.name.slice("recursion:".length);
|
|
3014
|
+
if (bTruncatedFns.has(fn) || bTruncatedFns.has(fn.split(".").pop() ?? fn)) {
|
|
3015
|
+
return false;
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
if (bBuiltinUnknownNames.has(r.name)) return false;
|
|
3019
|
+
}
|
|
3020
|
+
return true;
|
|
3021
|
+
})
|
|
3022
|
+
)
|
|
3023
|
+
);
|
|
3024
|
+
const externalFunctions = synthesizeExternalFunctions(callRecords, filePath);
|
|
187
3025
|
return {
|
|
188
3026
|
functions: functionResults,
|
|
189
3027
|
diagnostics,
|
|
190
3028
|
bindings,
|
|
191
3029
|
nodeTypeMap,
|
|
192
|
-
caseHints
|
|
3030
|
+
caseHints,
|
|
3031
|
+
...externalFunctions.length > 0 ? { externalFunctions } : {}
|
|
193
3032
|
};
|
|
194
3033
|
}
|
|
195
3034
|
function collectBindings(ast, env, bindings) {
|
|
196
3035
|
if (ast.type !== "File") return;
|
|
197
3036
|
for (const stmt of ast.program.body) {
|
|
198
3037
|
if (stmt.type === "FunctionDeclaration" && stmt.id) {
|
|
199
|
-
const val = env.has(stmt.id.name) ? env.lookup(stmt.id.name) :
|
|
3038
|
+
const val = env.has(stmt.id.name) ? env.lookup(stmt.id.name) : T2.unknown;
|
|
200
3039
|
bindings.set(stmt.id.name, { type: val, loc: locFromNode(stmt) });
|
|
201
3040
|
}
|
|
202
3041
|
if (stmt.type === "VariableDeclaration") {
|
|
203
3042
|
for (const decl of stmt.declarations) {
|
|
204
3043
|
if (decl.id.type === "Identifier") {
|
|
205
|
-
const val = env.has(decl.id.name) ? env.lookup(decl.id.name) :
|
|
3044
|
+
const val = env.has(decl.id.name) ? env.lookup(decl.id.name) : T2.unknown;
|
|
206
3045
|
bindings.set(decl.id.name, { type: val, loc: locFromNode(decl) });
|
|
207
3046
|
}
|
|
208
3047
|
}
|
|
209
3048
|
}
|
|
210
3049
|
if (stmt.type === "ClassDeclaration" && stmt.id) {
|
|
211
|
-
const val = env.has(stmt.id.name) ? env.lookup(stmt.id.name) :
|
|
3050
|
+
const val = env.has(stmt.id.name) ? env.lookup(stmt.id.name) : T2.unknown;
|
|
212
3051
|
bindings.set(stmt.id.name, { type: val, loc: locFromNode(stmt) });
|
|
213
3052
|
}
|
|
214
3053
|
if (stmt.type === "ExportNamedDeclaration" && stmt.declaration) {
|
|
@@ -229,16 +3068,16 @@ function buildNodeTypeMap(ast, env, nodeTypeMap) {
|
|
|
229
3068
|
}
|
|
230
3069
|
}
|
|
231
3070
|
if (node.type === "NumericLiteral") {
|
|
232
|
-
nodeTypeMap.set(node,
|
|
3071
|
+
nodeTypeMap.set(node, T2.literal(node.value));
|
|
233
3072
|
}
|
|
234
3073
|
if (node.type === "StringLiteral") {
|
|
235
|
-
nodeTypeMap.set(node,
|
|
3074
|
+
nodeTypeMap.set(node, T2.literal(node.value));
|
|
236
3075
|
}
|
|
237
3076
|
if (node.type === "BooleanLiteral") {
|
|
238
|
-
nodeTypeMap.set(node,
|
|
3077
|
+
nodeTypeMap.set(node, T2.literal(node.value));
|
|
239
3078
|
}
|
|
240
3079
|
if (node.type === "NullLiteral") {
|
|
241
|
-
nodeTypeMap.set(node,
|
|
3080
|
+
nodeTypeMap.set(node, T2.null);
|
|
242
3081
|
}
|
|
243
3082
|
} catch {
|
|
244
3083
|
}
|
|
@@ -248,19 +3087,59 @@ function buildNodeTypeMap(ast, env, nodeTypeMap) {
|
|
|
248
3087
|
}
|
|
249
3088
|
}
|
|
250
3089
|
function getCasesForFile(filePath, source) {
|
|
251
|
-
const ast =
|
|
3090
|
+
const ast = parse5(source);
|
|
252
3091
|
const functions = extractDirectives(ast);
|
|
253
3092
|
return functions.map((fn) => {
|
|
254
3093
|
const cases = fn.directives.filter((d) => d.kind === "case").map((d, i) => ({ name: d.name, index: i }));
|
|
255
3094
|
return { functionName: fn.name, cases, loc: locFromNode(fn.node) };
|
|
256
3095
|
});
|
|
257
3096
|
}
|
|
3097
|
+
async function getTypeAtPositionAsync(filePath, source, line, column, activeCases) {
|
|
3098
|
+
const envNames = collectEnvNames(filePath, source, false);
|
|
3099
|
+
if (envNames.length > 0) {
|
|
3100
|
+
await preloadPathEnvs(envNames, dirname5(filePath));
|
|
3101
|
+
}
|
|
3102
|
+
return getTypeAtPosition(filePath, source, line, column, activeCases);
|
|
3103
|
+
}
|
|
258
3104
|
function getTypeAtPosition(filePath, source, line, column, activeCases) {
|
|
259
|
-
const ast =
|
|
3105
|
+
const ast = parse5(source);
|
|
3106
|
+
const fileDirectives = extractFileDirectives(ast);
|
|
3107
|
+
const envNames = fileDirectives.filter((d) => d.kind === "env").flatMap((d) => d.envs);
|
|
3108
|
+
if (isBPathCapable(source, envNames)) {
|
|
3109
|
+
try {
|
|
3110
|
+
const seeds = mockDirectivesToAbsSeeds(extractDirectives(ast));
|
|
3111
|
+
const { modules } = evalAbsModuleGraph(source, filePath);
|
|
3112
|
+
const absNodes = collectAbsNodeTypes(source, {
|
|
3113
|
+
...seeds,
|
|
3114
|
+
modules,
|
|
3115
|
+
file: ast
|
|
3116
|
+
});
|
|
3117
|
+
const absAt = findAbsAtPosition(absNodes, line, column);
|
|
3118
|
+
if (absAt) return absToTypeValue2(absAt);
|
|
3119
|
+
const ident = findIdentNameAtPosition(source, line, column, ast);
|
|
3120
|
+
if (ident) {
|
|
3121
|
+
const binds = collectAbsBindingsFromGraph(source, filePath, {
|
|
3122
|
+
seedVars: seeds.seedVars,
|
|
3123
|
+
seedFns: seeds.seedFns
|
|
3124
|
+
});
|
|
3125
|
+
const bound = binds.get(ident);
|
|
3126
|
+
if (bound) return absToTypeValue2(bound);
|
|
3127
|
+
}
|
|
3128
|
+
} catch {
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
260
3131
|
resetMemo();
|
|
3132
|
+
resetEnvModules();
|
|
261
3133
|
setModuleResolver(resolveModule);
|
|
262
|
-
setCurrentFileDir(
|
|
263
|
-
|
|
3134
|
+
setCurrentFileDir(dirname5(filePath));
|
|
3135
|
+
setCurrentSource(source);
|
|
3136
|
+
const globalEnv = createEnvironment2();
|
|
3137
|
+
{
|
|
3138
|
+
const loaded = envNames.length > 0 ? loadEnvs2(envNames, globalEnv) : { modules: {} };
|
|
3139
|
+
const auto = autoHarvestModules(source, dirname5(filePath));
|
|
3140
|
+
const modules = { ...loaded.modules, ...auto };
|
|
3141
|
+
if (Object.keys(modules).length > 0) setEnvModules(modules);
|
|
3142
|
+
}
|
|
264
3143
|
evaluateProgram(ast, globalEnv);
|
|
265
3144
|
const nodeTypeMap = /* @__PURE__ */ new Map();
|
|
266
3145
|
buildNodeTypeMap(ast, globalEnv, nodeTypeMap);
|
|
@@ -281,8 +3160,164 @@ function getTypeAtPosition(filePath, source, line, column, activeCases) {
|
|
|
281
3160
|
}
|
|
282
3161
|
}
|
|
283
3162
|
setModuleResolver(null);
|
|
3163
|
+
resetEnvModules();
|
|
3164
|
+
resetMockModules();
|
|
284
3165
|
return findBestTypeAtPosition(nodeTypeMap, globalEnv, ast, line, column);
|
|
285
3166
|
}
|
|
3167
|
+
function getHoverAtPosition(filePath, source, line, column, activeCases) {
|
|
3168
|
+
let file;
|
|
3169
|
+
try {
|
|
3170
|
+
file = parse5(source);
|
|
3171
|
+
} catch {
|
|
3172
|
+
file = void 0;
|
|
3173
|
+
}
|
|
3174
|
+
const envNames = collectEnvNames(filePath, source, false);
|
|
3175
|
+
if (isBPathCapable(source, envNames)) {
|
|
3176
|
+
try {
|
|
3177
|
+
const seeds = mockDirectivesToAbsSeeds(extractDirectives(file ?? parse5(source)));
|
|
3178
|
+
const { modules } = evalAbsModuleGraph(source, filePath);
|
|
3179
|
+
const absNodes = collectAbsNodeTypes(source, {
|
|
3180
|
+
...seeds,
|
|
3181
|
+
modules,
|
|
3182
|
+
...file ? { file } : {}
|
|
3183
|
+
});
|
|
3184
|
+
const absAt = findAbsAtPosition(absNodes, line, column);
|
|
3185
|
+
const ident2 = findIdentNameAtPosition(source, line, column, file);
|
|
3186
|
+
if (ident2 && !findFunctionNameAtPosition(source, line, column, file)) {
|
|
3187
|
+
const binds = collectAbsBindingsFromGraph(source, filePath, {
|
|
3188
|
+
seedVars: seeds.seedVars,
|
|
3189
|
+
seedFns: seeds.seedFns
|
|
3190
|
+
});
|
|
3191
|
+
const bound = binds.get(ident2);
|
|
3192
|
+
if (bound) {
|
|
3193
|
+
const absLine = formatAbs3(bound);
|
|
3194
|
+
const absMulti = formatAbsMultiline(bound, ident2);
|
|
3195
|
+
return { typeText: absLine, abs: absLine, absMultiline: absMulti };
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
if (absAt) {
|
|
3199
|
+
const absLine = formatAbs3(absAt);
|
|
3200
|
+
const absMulti = formatAbsMultiline(absAt, void 0);
|
|
3201
|
+
return { typeText: absLine, abs: absLine, absMultiline: absMulti };
|
|
3202
|
+
}
|
|
3203
|
+
} catch {
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
const tv = getTypeAtPosition(filePath, source, line, column, activeCases);
|
|
3207
|
+
const info = tv ? { typeText: typeValueToString3(tv) } : null;
|
|
3208
|
+
const fnName = findFunctionNameAtPosition(source, line, column, file);
|
|
3209
|
+
if (fnName) {
|
|
3210
|
+
try {
|
|
3211
|
+
const g = generalizeFromAst2(fnName, source, file ? { file } : {});
|
|
3212
|
+
if (g) {
|
|
3213
|
+
if (info) {
|
|
3214
|
+
info.intension = g.display;
|
|
3215
|
+
info.abs = formatAbs3(g.symbolic);
|
|
3216
|
+
info.absMultiline = formatAbsMultiline(g.symbolic, fnName);
|
|
3217
|
+
} else {
|
|
3218
|
+
return {
|
|
3219
|
+
typeText: g.display,
|
|
3220
|
+
intension: g.display,
|
|
3221
|
+
abs: formatAbs3(g.symbolic),
|
|
3222
|
+
absMultiline: formatAbsMultiline(g.symbolic, fnName)
|
|
3223
|
+
};
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
} catch {
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
const ident = findIdentNameAtPosition(source, line, column, file);
|
|
3230
|
+
if (ident && !fnName) {
|
|
3231
|
+
try {
|
|
3232
|
+
if (isBPathCapable(source, []) || !/\brequire\s*\(/.test(source)) {
|
|
3233
|
+
const seeds = mockDirectivesToAbsSeeds(extractDirectives(file ?? parse5(source)));
|
|
3234
|
+
const absBinds = collectAbsBindingsFromGraph(source, filePath, {
|
|
3235
|
+
seedVars: seeds.seedVars,
|
|
3236
|
+
seedFns: seeds.seedFns
|
|
3237
|
+
});
|
|
3238
|
+
const absBound = absBinds.get(ident);
|
|
3239
|
+
if (absBound) {
|
|
3240
|
+
const absLine = formatAbs3(absBound);
|
|
3241
|
+
const absMulti = formatAbsMultiline(absBound, ident);
|
|
3242
|
+
if (info) {
|
|
3243
|
+
info.abs = absLine;
|
|
3244
|
+
info.absMultiline = absMulti;
|
|
3245
|
+
return info;
|
|
3246
|
+
}
|
|
3247
|
+
return { typeText: absLine, abs: absLine, absMultiline: absMulti };
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
} catch {
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
try {
|
|
3254
|
+
if (isBPathCapable(source, []) || !/\brequire\s*\(|\bimport\s*[{'"*]/.test(source)) {
|
|
3255
|
+
const seeds = mockDirectivesToAbsSeeds(
|
|
3256
|
+
extractDirectives(file ?? parse5(source))
|
|
3257
|
+
);
|
|
3258
|
+
const { modules } = evalAbsModuleGraph(source, filePath);
|
|
3259
|
+
const nodeTypes = collectAbsNodeTypes(source, {
|
|
3260
|
+
...seeds,
|
|
3261
|
+
modules,
|
|
3262
|
+
...file ? { file } : {}
|
|
3263
|
+
});
|
|
3264
|
+
const absAt = findAbsAtPosition(nodeTypes, line, column);
|
|
3265
|
+
if (absAt) {
|
|
3266
|
+
const absLine = formatAbs3(absAt);
|
|
3267
|
+
const absMulti = formatAbsMultiline(absAt, void 0);
|
|
3268
|
+
if (info) {
|
|
3269
|
+
info.abs = absLine;
|
|
3270
|
+
info.absMultiline = absMulti;
|
|
3271
|
+
} else {
|
|
3272
|
+
return { typeText: absLine, abs: absLine, absMultiline: absMulti };
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
} catch {
|
|
3277
|
+
}
|
|
3278
|
+
return info;
|
|
3279
|
+
}
|
|
3280
|
+
function findIdentNameAtPosition(source, line, column, fileAst) {
|
|
3281
|
+
try {
|
|
3282
|
+
const ast = fileAst ?? parse5(source);
|
|
3283
|
+
let found;
|
|
3284
|
+
traverse(ast, {
|
|
3285
|
+
Identifier(path) {
|
|
3286
|
+
const loc = path.node.loc;
|
|
3287
|
+
if (!loc) return;
|
|
3288
|
+
if (loc.start.line !== line) return;
|
|
3289
|
+
if (column < loc.start.column || column > loc.end.column) return;
|
|
3290
|
+
found = path.node.name;
|
|
3291
|
+
}
|
|
3292
|
+
});
|
|
3293
|
+
return found;
|
|
3294
|
+
} catch {
|
|
3295
|
+
return void 0;
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
function findFunctionNameAtPosition(source, line, column, fileAst) {
|
|
3299
|
+
try {
|
|
3300
|
+
const ast = fileAst ?? parse5(source);
|
|
3301
|
+
let found;
|
|
3302
|
+
traverse(ast, {
|
|
3303
|
+
Identifier(path) {
|
|
3304
|
+
const loc = path.node.loc;
|
|
3305
|
+
if (!loc) return;
|
|
3306
|
+
if (loc.start.line !== line) return;
|
|
3307
|
+
if (column < loc.start.column || column > loc.end.column) return;
|
|
3308
|
+
const parent = path.parent;
|
|
3309
|
+
if (parent.type === "FunctionDeclaration" && parent.id === path.node) {
|
|
3310
|
+
found = path.node.name;
|
|
3311
|
+
} else if (parent.type === "CallExpression" && parent.callee === path.node) {
|
|
3312
|
+
found = path.node.name;
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
});
|
|
3316
|
+
return found;
|
|
3317
|
+
} catch {
|
|
3318
|
+
return void 0;
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
286
3321
|
function findEnclosingFunction(functions, line) {
|
|
287
3322
|
for (const fn of functions) {
|
|
288
3323
|
const loc = fn.node.loc;
|
|
@@ -331,129 +3366,685 @@ function findIdentifierAtPosition(ast, line, column) {
|
|
|
331
3366
|
});
|
|
332
3367
|
} catch {
|
|
333
3368
|
}
|
|
334
|
-
return found;
|
|
3369
|
+
return found;
|
|
3370
|
+
}
|
|
3371
|
+
function getCompletionsAtPosition(filePath, source, line, column) {
|
|
3372
|
+
const textBefore = getTextBeforePosition(source, line, column);
|
|
3373
|
+
const dotMatch = textBefore.match(/(\w+)\.\s*\w*$/);
|
|
3374
|
+
if (!dotMatch) return getVariableCompletions(filePath, source);
|
|
3375
|
+
const objName = dotMatch[1];
|
|
3376
|
+
const safeSource = sanitizeSourceForParsing(source);
|
|
3377
|
+
let ast;
|
|
3378
|
+
try {
|
|
3379
|
+
ast = parse5(safeSource);
|
|
3380
|
+
} catch {
|
|
3381
|
+
try {
|
|
3382
|
+
ast = parse5(source);
|
|
3383
|
+
} catch {
|
|
3384
|
+
return [];
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3387
|
+
resetMemo();
|
|
3388
|
+
setModuleResolver(resolveModule);
|
|
3389
|
+
setCurrentFileDir(dirname5(filePath));
|
|
3390
|
+
const globalEnv = createEnvironment2();
|
|
3391
|
+
evaluateProgram(ast, globalEnv);
|
|
3392
|
+
if (!globalEnv.has(objName)) {
|
|
3393
|
+
setModuleResolver(null);
|
|
3394
|
+
return [];
|
|
3395
|
+
}
|
|
3396
|
+
const objType = globalEnv.lookup(objName);
|
|
3397
|
+
const completions = getCompletionsForType(objType);
|
|
3398
|
+
setModuleResolver(null);
|
|
3399
|
+
return completions;
|
|
3400
|
+
}
|
|
3401
|
+
function sanitizeSourceForParsing(source) {
|
|
3402
|
+
return source.replace(/(\w+)\.\s*$/gm, "$1._ ");
|
|
3403
|
+
}
|
|
3404
|
+
function getTextBeforePosition(source, line, column) {
|
|
3405
|
+
const lines = source.split("\n");
|
|
3406
|
+
if (line < 1 || line > lines.length) return "";
|
|
3407
|
+
return lines[line - 1].slice(0, column);
|
|
3408
|
+
}
|
|
3409
|
+
function getVariableCompletions(filePath, source) {
|
|
3410
|
+
const ast = parse5(source);
|
|
3411
|
+
resetMemo();
|
|
3412
|
+
setModuleResolver(resolveModule);
|
|
3413
|
+
setCurrentFileDir(dirname5(filePath));
|
|
3414
|
+
const globalEnv = createEnvironment2();
|
|
3415
|
+
evaluateProgram(ast, globalEnv);
|
|
3416
|
+
const ownBindings = globalEnv.getOwnBindings();
|
|
3417
|
+
const completions = [];
|
|
3418
|
+
for (const [name, tv] of Object.entries(ownBindings)) {
|
|
3419
|
+
if (name.startsWith("__export_")) continue;
|
|
3420
|
+
completions.push({
|
|
3421
|
+
label: name,
|
|
3422
|
+
kind: tv.kind === "function" ? "method" : "variable",
|
|
3423
|
+
detail: typeValueToString3(tv)
|
|
3424
|
+
});
|
|
3425
|
+
}
|
|
3426
|
+
setModuleResolver(null);
|
|
3427
|
+
return completions;
|
|
3428
|
+
}
|
|
3429
|
+
function builtinMemberType(memberExpr) {
|
|
3430
|
+
try {
|
|
3431
|
+
const result = evaluate(parse5(`${memberExpr};`), createEnvironment2());
|
|
3432
|
+
if (!result || typeof result !== "object" || !("kind" in result)) return null;
|
|
3433
|
+
return result;
|
|
3434
|
+
} catch {
|
|
3435
|
+
return null;
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
function builtinProtoMembers(className) {
|
|
3439
|
+
const table = BUILTIN_PROTOTYPE_METHOD_APPROXIMATIONS[className];
|
|
3440
|
+
return table ? Object.keys(table) : [];
|
|
3441
|
+
}
|
|
3442
|
+
function describeMember(label, tv, fallbackClass) {
|
|
3443
|
+
if (tv) {
|
|
3444
|
+
const sig = getFnSig3(tv);
|
|
3445
|
+
if (sig) {
|
|
3446
|
+
const paramNames = tv.kind === "function" ? tv.params : [];
|
|
3447
|
+
const params = sig.paramTypes.map((p, i) => `${paramNames[i] ?? `arg${i}`}: ${typeValueToString3(p)}`).join(", ");
|
|
3448
|
+
return `(${params}) => ${typeValueToString3(sig.returnType)}`;
|
|
3449
|
+
}
|
|
3450
|
+
if (tv.kind !== "function") return typeValueToString3(tv);
|
|
3451
|
+
}
|
|
3452
|
+
return `${label}(\u2026)@${fallbackClass}`;
|
|
3453
|
+
}
|
|
3454
|
+
function getArrayCompletions(tv) {
|
|
3455
|
+
const completions = [];
|
|
3456
|
+
for (const m of builtinProtoMembers("Array")) {
|
|
3457
|
+
const detail = describeMember(m, builtinMemberType(`Array.prototype.${m}`), "Array");
|
|
3458
|
+
completions.push({ label: m, kind: "method", detail });
|
|
3459
|
+
}
|
|
3460
|
+
completions.push({
|
|
3461
|
+
label: "length",
|
|
3462
|
+
kind: "property",
|
|
3463
|
+
// tuple 长度是精确字面量;array 是 number(保持原展示语义)
|
|
3464
|
+
detail: tv.kind === "tuple" ? `${tv.elements.length}` : "number"
|
|
3465
|
+
});
|
|
3466
|
+
return completions;
|
|
3467
|
+
}
|
|
3468
|
+
function getPromiseCompletions() {
|
|
3469
|
+
return builtinProtoMembers("Promise").map((m) => ({
|
|
3470
|
+
label: m,
|
|
3471
|
+
kind: "method",
|
|
3472
|
+
detail: describeMember(m, builtinMemberType(`Promise.prototype.${m}`), "Promise")
|
|
3473
|
+
}));
|
|
3474
|
+
}
|
|
3475
|
+
function getStringCompletions() {
|
|
3476
|
+
const completions = [];
|
|
3477
|
+
for (const m of builtinProtoMembers("String")) {
|
|
3478
|
+
completions.push({
|
|
3479
|
+
label: m,
|
|
3480
|
+
kind: "method",
|
|
3481
|
+
detail: describeMember(m, builtinMemberType(`"s".${m}`), "String")
|
|
3482
|
+
});
|
|
3483
|
+
}
|
|
3484
|
+
completions.push({ label: "length", kind: "property", detail: "number" });
|
|
3485
|
+
return completions;
|
|
3486
|
+
}
|
|
3487
|
+
function getUnionCompletions(tv) {
|
|
3488
|
+
const members = tv.members;
|
|
3489
|
+
if (members.length === 0) return [];
|
|
3490
|
+
const labelsByMember = members.map((m) => getCompletionsForType(m));
|
|
3491
|
+
const baseIdx = labelsByMember.findIndex((labels) => labels.length > 0);
|
|
3492
|
+
if (baseIdx === -1) return [];
|
|
3493
|
+
const common = [];
|
|
3494
|
+
for (const base of labelsByMember[baseIdx]) {
|
|
3495
|
+
let allPresent = true;
|
|
3496
|
+
const memberTypes = [base.detail ?? base.label];
|
|
3497
|
+
for (let i = 0; i < members.length; i++) {
|
|
3498
|
+
if (i === baseIdx) continue;
|
|
3499
|
+
const labels = labelsByMember[i];
|
|
3500
|
+
if (labels.length === 0) continue;
|
|
3501
|
+
const hit = labels.find((l) => l.label === base.label);
|
|
3502
|
+
if (!hit) {
|
|
3503
|
+
allPresent = false;
|
|
3504
|
+
break;
|
|
3505
|
+
}
|
|
3506
|
+
memberTypes.push(hit.detail ?? hit.label);
|
|
3507
|
+
}
|
|
3508
|
+
if (allPresent) {
|
|
3509
|
+
common.push({ ...base, detail: memberTypes.join(" | ") });
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
3512
|
+
return common;
|
|
3513
|
+
}
|
|
3514
|
+
function getCompletionsForType(tv) {
|
|
3515
|
+
const completions = [];
|
|
3516
|
+
if (tv.kind === "object") {
|
|
3517
|
+
for (const [key, val] of Object.entries(tv.properties)) {
|
|
3518
|
+
completions.push({
|
|
3519
|
+
label: key,
|
|
3520
|
+
kind: val.kind === "function" ? "method" : "property",
|
|
3521
|
+
detail: typeValueToString3(val)
|
|
3522
|
+
});
|
|
3523
|
+
}
|
|
3524
|
+
return completions;
|
|
3525
|
+
}
|
|
3526
|
+
if (tv.kind === "instance") {
|
|
3527
|
+
for (const [key, val] of Object.entries(tv.properties)) {
|
|
3528
|
+
completions.push({
|
|
3529
|
+
label: key,
|
|
3530
|
+
kind: val.kind === "function" ? "method" : "property",
|
|
3531
|
+
detail: typeValueToString3(val)
|
|
3532
|
+
});
|
|
3533
|
+
}
|
|
3534
|
+
return completions;
|
|
3535
|
+
}
|
|
3536
|
+
if (tv.kind === "union") {
|
|
3537
|
+
return getUnionCompletions(tv);
|
|
3538
|
+
}
|
|
3539
|
+
if (tv.kind === "array" || tv.kind === "tuple") {
|
|
3540
|
+
return getArrayCompletions(tv);
|
|
3541
|
+
}
|
|
3542
|
+
if (tv.kind === "promise") {
|
|
3543
|
+
return getPromiseCompletions();
|
|
3544
|
+
}
|
|
3545
|
+
if (tv.kind === "primitive" && tv.type === "string") {
|
|
3546
|
+
return getStringCompletions();
|
|
3547
|
+
}
|
|
3548
|
+
return completions;
|
|
3549
|
+
}
|
|
3550
|
+
function absIsBetter(absTv, prev) {
|
|
3551
|
+
if (prev.kind === "unknown") return absTv.kind !== "unknown";
|
|
3552
|
+
if (absTv.kind === "unknown" || absTv.kind === "never") return false;
|
|
3553
|
+
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") {
|
|
3554
|
+
return false;
|
|
3555
|
+
}
|
|
3556
|
+
if (absTv.kind === "refined" && prev.kind === "primitive") return true;
|
|
3557
|
+
return false;
|
|
3558
|
+
}
|
|
3559
|
+
function isSelfContainedSource(source, envNames) {
|
|
3560
|
+
if (envNames.length > 0) return false;
|
|
3561
|
+
return !/\brequire\s*\(|\bimport\s*[{'"*]/.test(source);
|
|
3562
|
+
}
|
|
3563
|
+
function absModulesOk(source, envNames) {
|
|
3564
|
+
void source;
|
|
3565
|
+
void envNames;
|
|
3566
|
+
return true;
|
|
3567
|
+
}
|
|
3568
|
+
function collectAbsCallRecords(source, seeds, filePath, precomputedModules) {
|
|
3569
|
+
const absCalls = [];
|
|
3570
|
+
let modules = precomputedModules;
|
|
3571
|
+
let importLocals = /* @__PURE__ */ new Map();
|
|
3572
|
+
if (filePath && absModulesOk(source, [])) {
|
|
3573
|
+
if (!modules) {
|
|
3574
|
+
try {
|
|
3575
|
+
const graph = evalAbsModuleGraph(source, filePath, {
|
|
3576
|
+
seedVars: seeds?.seedVars,
|
|
3577
|
+
seedFns: seeds?.seedFns
|
|
3578
|
+
});
|
|
3579
|
+
modules = graph.modules;
|
|
3580
|
+
} catch {
|
|
3581
|
+
modules = void 0;
|
|
3582
|
+
}
|
|
3583
|
+
}
|
|
3584
|
+
try {
|
|
3585
|
+
importLocals = buildAbsImportLocalMap(source, filePath);
|
|
3586
|
+
} catch {
|
|
3587
|
+
importLocals = /* @__PURE__ */ new Map();
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3590
|
+
setAbsCallCollector((r) => absCalls.push(r));
|
|
3591
|
+
try {
|
|
3592
|
+
evalProgramAbs2(source, { ...seeds, modules });
|
|
3593
|
+
} catch {
|
|
3594
|
+
} finally {
|
|
3595
|
+
setAbsCallCollector(null);
|
|
3596
|
+
}
|
|
3597
|
+
return absCalls.map((r) => {
|
|
3598
|
+
const rec = {
|
|
3599
|
+
fnName: r.fnName,
|
|
3600
|
+
argTypes: r.args.map((a) => absToTypeValue2(a)),
|
|
3601
|
+
resultType: r.threw ? T2.never : absToTypeValue2(r.result),
|
|
3602
|
+
throws: r.threw ? absToTypeValue2(r.result) : T2.never,
|
|
3603
|
+
callLoc: r.callLoc
|
|
3604
|
+
};
|
|
3605
|
+
const imp = importLocals.get(r.fnName);
|
|
3606
|
+
if (imp) {
|
|
3607
|
+
rec.targetModule = imp.modulePath;
|
|
3608
|
+
rec.targetExport = imp.exportName;
|
|
3609
|
+
}
|
|
3610
|
+
return rec;
|
|
3611
|
+
});
|
|
3612
|
+
}
|
|
3613
|
+
function collectAbsBindsAndNodes(source, seeds, modules) {
|
|
3614
|
+
const binds = /* @__PURE__ */ new Map();
|
|
3615
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
3616
|
+
setAbsNodeCollector((node, value) => {
|
|
3617
|
+
nodes.set(node, value);
|
|
3618
|
+
});
|
|
3619
|
+
try {
|
|
3620
|
+
const { env } = evalProgramAbs2(source, {
|
|
3621
|
+
...seeds,
|
|
3622
|
+
modules
|
|
3623
|
+
});
|
|
3624
|
+
for (const [k, v] of env.vars) binds.set(k, v);
|
|
3625
|
+
for (const [name, impl] of env.fns) {
|
|
3626
|
+
if (!binds.has(name)) {
|
|
3627
|
+
binds.set(
|
|
3628
|
+
name,
|
|
3629
|
+
absFunction5(impl.params, { body: impl.body, async: impl.async, env })
|
|
3630
|
+
);
|
|
3631
|
+
}
|
|
3632
|
+
}
|
|
3633
|
+
} catch {
|
|
3634
|
+
} finally {
|
|
3635
|
+
setAbsNodeCollector(null);
|
|
3636
|
+
}
|
|
3637
|
+
return { binds, nodes };
|
|
3638
|
+
}
|
|
3639
|
+
function buildAbsImportLocalMap(source, fromFile) {
|
|
3640
|
+
const out = /* @__PURE__ */ new Map();
|
|
3641
|
+
try {
|
|
3642
|
+
const file = parse5(source);
|
|
3643
|
+
for (const stmt of file.program.body) {
|
|
3644
|
+
if (stmt.type !== "ImportDeclaration") continue;
|
|
3645
|
+
const spec = stmt.source.value;
|
|
3646
|
+
let modulePath = null;
|
|
3647
|
+
if (spec.startsWith(".") || spec.startsWith("/")) {
|
|
3648
|
+
modulePath = resolveImportAbs(spec, fromFile);
|
|
3649
|
+
} else if (!spec.startsWith("node:")) {
|
|
3650
|
+
modulePath = spec;
|
|
3651
|
+
}
|
|
3652
|
+
if (!modulePath) continue;
|
|
3653
|
+
for (const s of stmt.specifiers) {
|
|
3654
|
+
if (s.type === "ImportSpecifier") {
|
|
3655
|
+
const imported = s.imported.type === "Identifier" ? s.imported.name : String(s.imported);
|
|
3656
|
+
out.set(s.local.name, { modulePath, exportName: imported });
|
|
3657
|
+
} else if (s.type === "ImportDefaultSpecifier") {
|
|
3658
|
+
out.set(s.local.name, { modulePath, exportName: "default" });
|
|
3659
|
+
}
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
3662
|
+
} catch {
|
|
3663
|
+
}
|
|
3664
|
+
return out;
|
|
3665
|
+
}
|
|
3666
|
+
function resolveImportAbs(spec, fromFile) {
|
|
3667
|
+
const base = dirname5(resolve4(fromFile));
|
|
3668
|
+
const p = resolve4(base, spec);
|
|
3669
|
+
for (const cand of [p, `${p}.js`, `${p}.mjs`, `${p}.ts`, resolve4(p, "index.js")]) {
|
|
3670
|
+
if (existsSync3(cand) && !statSync3(cand).isDirectory()) return cand;
|
|
3671
|
+
}
|
|
3672
|
+
return null;
|
|
3673
|
+
}
|
|
3674
|
+
function tryEvalAbs(source, fnName, args, filePath, mocks) {
|
|
3675
|
+
const raw = tryEvalAbsRaw(source, fnName, args, filePath, mocks);
|
|
3676
|
+
return raw ? absToTypeValue2(raw) : void 0;
|
|
3677
|
+
}
|
|
3678
|
+
function tryEvalAbsRaw(source, fnName, args, filePath, mocks) {
|
|
3679
|
+
if (/\brequire\s*\(/.test(source)) return void 0;
|
|
3680
|
+
try {
|
|
3681
|
+
const absArgs = args.map((a) => typeValueToAbs5(a));
|
|
3682
|
+
if (filePath) {
|
|
3683
|
+
const viaB = tryBPathCall(source, filePath, fnName, absArgs, { mocks });
|
|
3684
|
+
if (viaB) return viaB;
|
|
3685
|
+
}
|
|
3686
|
+
let modules;
|
|
3687
|
+
if (filePath && /\bimport\s*[{'"*]/.test(source)) {
|
|
3688
|
+
modules = evalAbsModuleGraph(source, filePath).modules;
|
|
3689
|
+
}
|
|
3690
|
+
const result = analyzeFn(source, fnName, absArgs, void 0, void 0, void 0, modules);
|
|
3691
|
+
if (result.shape.k === "unknown" && !result.term) {
|
|
3692
|
+
return void 0;
|
|
3693
|
+
}
|
|
3694
|
+
return result;
|
|
3695
|
+
} catch {
|
|
3696
|
+
return void 0;
|
|
3697
|
+
}
|
|
3698
|
+
}
|
|
3699
|
+
function tryEvalEntryAbs(source, fnName, args, filePath, mocks) {
|
|
3700
|
+
return tryEvalAbs(source, fnName, args, filePath, mocks);
|
|
3701
|
+
}
|
|
3702
|
+
function tryAttachIntension(caseResult, source, fnName) {
|
|
3703
|
+
try {
|
|
3704
|
+
const g = generalizeFromAst2(fnName, source);
|
|
3705
|
+
if (!g) return;
|
|
3706
|
+
const sym = g.symbolic;
|
|
3707
|
+
const intension = {
|
|
3708
|
+
display: g.display,
|
|
3709
|
+
abs: formatAbs3(sym),
|
|
3710
|
+
absMultiline: formatAbsMultiline(sym, fnName)
|
|
3711
|
+
};
|
|
3712
|
+
if (sym.term && sym.term.op !== "lit") {
|
|
3713
|
+
intension.term = termToString(sym.term);
|
|
3714
|
+
}
|
|
3715
|
+
if (sym.pred && sym.pred.op !== "true") {
|
|
3716
|
+
intension.pred = predToString(sym.pred);
|
|
3717
|
+
}
|
|
3718
|
+
intension.conf = sym.conf;
|
|
3719
|
+
caseResult.intension = intension;
|
|
3720
|
+
} catch {
|
|
3721
|
+
}
|
|
3722
|
+
}
|
|
3723
|
+
function attachAbsToIntension(caseResult, absVal, label) {
|
|
3724
|
+
const prev = caseResult.intension ?? {};
|
|
3725
|
+
caseResult.abs = absVal;
|
|
3726
|
+
caseResult.intension = {
|
|
3727
|
+
...prev,
|
|
3728
|
+
abs: formatAbs3(absVal),
|
|
3729
|
+
absMultiline: formatAbsMultiline(absVal, label ?? caseResult.name),
|
|
3730
|
+
conf: absVal.conf
|
|
3731
|
+
};
|
|
3732
|
+
}
|
|
3733
|
+
|
|
3734
|
+
// src/index.ts
|
|
3735
|
+
import { collectAbsInlays } from "@nudojs/core";
|
|
3736
|
+
|
|
3737
|
+
// src/infer-json.ts
|
|
3738
|
+
import { typeValueToString as typeValueToString4 } from "@nudojs/core";
|
|
3739
|
+
function mapCase(c) {
|
|
3740
|
+
const out = {
|
|
3741
|
+
name: c.name,
|
|
3742
|
+
args: c.args.map((a) => typeValueToString4(a)),
|
|
3743
|
+
result: typeValueToString4(c.result),
|
|
3744
|
+
throws: c.throws.kind !== "never" ? typeValueToString4(c.throws) : null,
|
|
3745
|
+
source: c.source ?? null
|
|
3746
|
+
};
|
|
3747
|
+
if (c.aggregatedFrom !== void 0) out.aggregatedFrom = c.aggregatedFrom;
|
|
3748
|
+
if (c.intension) {
|
|
3749
|
+
const i = c.intension;
|
|
3750
|
+
out.intension = {
|
|
3751
|
+
...i.display !== void 0 ? { display: i.display } : {},
|
|
3752
|
+
...i.abs !== void 0 ? { abs: i.abs } : {},
|
|
3753
|
+
...i.absMultiline !== void 0 ? { absMultiline: i.absMultiline } : {},
|
|
3754
|
+
...i.term !== void 0 ? { term: i.term } : {},
|
|
3755
|
+
...i.pred !== void 0 ? { pred: i.pred } : {},
|
|
3756
|
+
...i.conf !== void 0 ? { conf: i.conf } : {}
|
|
3757
|
+
};
|
|
3758
|
+
}
|
|
3759
|
+
return out;
|
|
3760
|
+
}
|
|
3761
|
+
function mapFunction(f) {
|
|
3762
|
+
const out = {
|
|
3763
|
+
name: f.name,
|
|
3764
|
+
loc: f.loc,
|
|
3765
|
+
entryOnly: f.entryOnly ?? false,
|
|
3766
|
+
cases: f.cases.map(mapCase)
|
|
3767
|
+
};
|
|
3768
|
+
if (f.noDeclaration) out.noDeclaration = true;
|
|
3769
|
+
if (f.combined) out.combined = typeValueToString4(f.combined);
|
|
3770
|
+
return out;
|
|
3771
|
+
}
|
|
3772
|
+
function serializeInferJson(result, file) {
|
|
3773
|
+
const functions = result.functions.map(mapFunction);
|
|
3774
|
+
const external = result.externalFunctions?.map((f) => ({
|
|
3775
|
+
name: f.name,
|
|
3776
|
+
fromModule: f.fromModule,
|
|
3777
|
+
cases: f.cases.map(mapCase)
|
|
3778
|
+
}));
|
|
3779
|
+
const caseCount = functions.reduce((n, f) => n + f.cases.length, 0) + (external ?? []).reduce((n, f) => n + f.cases.length, 0);
|
|
3780
|
+
const json = {
|
|
3781
|
+
version: 1,
|
|
3782
|
+
file,
|
|
3783
|
+
summary: {
|
|
3784
|
+
functions: functions.length,
|
|
3785
|
+
externalFunctions: external?.length ?? 0,
|
|
3786
|
+
cases: caseCount,
|
|
3787
|
+
diagnostics: result.diagnostics.length
|
|
3788
|
+
},
|
|
3789
|
+
functions,
|
|
3790
|
+
diagnostics: result.diagnostics.map((d) => ({
|
|
3791
|
+
range: d.range,
|
|
3792
|
+
severity: d.severity,
|
|
3793
|
+
message: d.message,
|
|
3794
|
+
...d.code !== void 0 ? { code: d.code } : {},
|
|
3795
|
+
...d.suggestions ? { suggestions: d.suggestions } : {},
|
|
3796
|
+
...d.tags ? { tags: d.tags } : {},
|
|
3797
|
+
...d.origin ? { origin: d.origin } : {}
|
|
3798
|
+
}))
|
|
3799
|
+
};
|
|
3800
|
+
if (external) json.externalFunctions = external;
|
|
3801
|
+
return json;
|
|
3802
|
+
}
|
|
3803
|
+
|
|
3804
|
+
// src/target-path.ts
|
|
3805
|
+
function isNudoTargetPath(path) {
|
|
3806
|
+
const lower = path.toLowerCase();
|
|
3807
|
+
if (lower.endsWith(".d.ts")) return false;
|
|
3808
|
+
return lower.endsWith(".js") || lower.endsWith(".mjs") || lower.endsWith(".ts");
|
|
3809
|
+
}
|
|
3810
|
+
|
|
3811
|
+
// src/load-module.ts
|
|
3812
|
+
import { existsSync as existsSync4, statSync as statSync4, readFileSync as readFileSync5 } from "fs";
|
|
3813
|
+
import { dirname as dirname6, resolve as resolve5, join as join3 } from "path";
|
|
3814
|
+
function defaultLoadModule(spec, fromFile) {
|
|
3815
|
+
if (!spec.startsWith(".") && !spec.startsWith("/")) return void 0;
|
|
3816
|
+
try {
|
|
3817
|
+
const base = dirname6(resolve5(fromFile));
|
|
3818
|
+
const p = resolve5(base, spec);
|
|
3819
|
+
for (const cand of [
|
|
3820
|
+
p,
|
|
3821
|
+
`${p}.js`,
|
|
3822
|
+
`${p}.mjs`,
|
|
3823
|
+
`${p}.ts`,
|
|
3824
|
+
join3(p, "index.js"),
|
|
3825
|
+
join3(p, "index.mjs"),
|
|
3826
|
+
join3(p, "index.ts")
|
|
3827
|
+
]) {
|
|
3828
|
+
if (existsSync4(cand) && !statSync4(cand).isDirectory()) {
|
|
3829
|
+
return readFileSync5(cand, "utf-8");
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
return void 0;
|
|
3833
|
+
} catch {
|
|
3834
|
+
return void 0;
|
|
3835
|
+
}
|
|
3836
|
+
}
|
|
3837
|
+
|
|
3838
|
+
// src/session-cache.ts
|
|
3839
|
+
import {
|
|
3840
|
+
resetGeneralizeMemo,
|
|
3841
|
+
resetCheckSourceMemo,
|
|
3842
|
+
resetNudoModuleExecCache,
|
|
3843
|
+
resetParseSourceCache
|
|
3844
|
+
} from "@nudojs/core";
|
|
3845
|
+
function evictAnalysisCachesForFiles(files) {
|
|
3846
|
+
if (files.length === 0) return;
|
|
3847
|
+
evictBPathCacheForFiles(files);
|
|
3848
|
+
evictAnalysisFileCacheForFiles(files);
|
|
3849
|
+
evictFnAnalysisCacheForFiles(files);
|
|
3850
|
+
evictAbsModuleCacheFiles(files);
|
|
3851
|
+
}
|
|
3852
|
+
function clearAnalysisSessionCaches() {
|
|
3853
|
+
clearBPathCache();
|
|
3854
|
+
clearAbsModuleCache();
|
|
3855
|
+
resetGeneralizeMemo();
|
|
3856
|
+
resetCheckSourceMemo();
|
|
3857
|
+
resetNudoModuleExecCache();
|
|
3858
|
+
}
|
|
3859
|
+
function resetAllAnalysisCaches() {
|
|
3860
|
+
clearAnalysisSessionCaches();
|
|
3861
|
+
resetParseSourceCache();
|
|
3862
|
+
}
|
|
3863
|
+
|
|
3864
|
+
// src/harvest-node.ts
|
|
3865
|
+
import { existsSync as existsSync5 } from "fs";
|
|
3866
|
+
import { harvestDts as harvestDts2 } from "@nudojs/harvester";
|
|
3867
|
+
function harvestNodeTypes(fromDir, maxFiles = 12, maxMs = 2500) {
|
|
3868
|
+
const root = resolvePackageRoot("@types/node", fromDir) ?? resolvePackageRoot("node", fromDir);
|
|
3869
|
+
if (!root || !existsSync5(root)) {
|
|
3870
|
+
return { ok: false, error: "@types/node not found" };
|
|
3871
|
+
}
|
|
3872
|
+
const dts = collectDtsFiles(root, maxFiles);
|
|
3873
|
+
if (dts.length === 0) {
|
|
3874
|
+
return { ok: false, error: `no .d.ts under ${root}` };
|
|
3875
|
+
}
|
|
3876
|
+
const env = harvestDts2(dts, { maxMs });
|
|
3877
|
+
return { ok: true, env, root, files: env.stats.files };
|
|
335
3878
|
}
|
|
336
|
-
function
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
3879
|
+
function summarizeNodeEnv(env) {
|
|
3880
|
+
return {
|
|
3881
|
+
modules: Object.keys(env.modules).sort(),
|
|
3882
|
+
globals: Object.keys(env.globals).sort().slice(0, 50),
|
|
3883
|
+
symbolCount: env.stats.symbols
|
|
3884
|
+
};
|
|
3885
|
+
}
|
|
3886
|
+
|
|
3887
|
+
// src/semantic-tokens.ts
|
|
3888
|
+
import { dirname as dirname7 } from "path";
|
|
3889
|
+
import { createEnvironment as createEnvironment3 } from "@nudojs/core";
|
|
3890
|
+
import { parse as parse6 } from "@nudojs/parser";
|
|
3891
|
+
import {
|
|
3892
|
+
evaluateProgram as evaluateProgram2,
|
|
3893
|
+
setModuleResolver as setModuleResolver2,
|
|
3894
|
+
setCurrentFileDir as setCurrentFileDir2,
|
|
3895
|
+
resetMemo as resetMemo2
|
|
3896
|
+
} from "@nudojs/cli/evaluator";
|
|
3897
|
+
var SEMANTIC_TOKEN_TYPES = [
|
|
3898
|
+
"function",
|
|
3899
|
+
"variable",
|
|
3900
|
+
"parameter",
|
|
3901
|
+
"property",
|
|
3902
|
+
"type",
|
|
3903
|
+
"keyword",
|
|
3904
|
+
"string",
|
|
3905
|
+
"number",
|
|
3906
|
+
"comment",
|
|
3907
|
+
"decorator",
|
|
3908
|
+
"method"
|
|
3909
|
+
];
|
|
3910
|
+
var SEMANTIC_TOKEN_MODIFIERS = [
|
|
3911
|
+
"declaration",
|
|
3912
|
+
"readonly",
|
|
3913
|
+
"deprecated",
|
|
3914
|
+
"unreachable"
|
|
3915
|
+
];
|
|
3916
|
+
function encodeSemanticTokens(tokens) {
|
|
3917
|
+
const result = [];
|
|
3918
|
+
let prevLine = 0;
|
|
3919
|
+
let prevChar = 0;
|
|
3920
|
+
for (const token of tokens) {
|
|
3921
|
+
const deltaLine = token.line - prevLine;
|
|
3922
|
+
const deltaChar = deltaLine === 0 ? token.char - prevChar : token.char;
|
|
3923
|
+
result.push(deltaLine, deltaChar, token.length, token.typeIndex, token.modifierBitmask);
|
|
3924
|
+
prevLine = token.line;
|
|
3925
|
+
prevChar = token.char;
|
|
3926
|
+
}
|
|
3927
|
+
return result;
|
|
3928
|
+
}
|
|
3929
|
+
var TYPE_FUNCTION = SEMANTIC_TOKEN_TYPES.indexOf("function");
|
|
3930
|
+
var TYPE_VARIABLE = SEMANTIC_TOKEN_TYPES.indexOf("variable");
|
|
3931
|
+
var TYPE_PARAMETER = SEMANTIC_TOKEN_TYPES.indexOf("parameter");
|
|
3932
|
+
var TYPE_PROPERTY = SEMANTIC_TOKEN_TYPES.indexOf("property");
|
|
3933
|
+
var TYPE_METHOD = SEMANTIC_TOKEN_TYPES.indexOf("method");
|
|
3934
|
+
var MOD_DECLARATION = 1 << SEMANTIC_TOKEN_MODIFIERS.indexOf("declaration");
|
|
3935
|
+
function buildSemanticTokens(filePath, source) {
|
|
342
3936
|
let ast;
|
|
343
3937
|
try {
|
|
344
|
-
ast =
|
|
3938
|
+
ast = parse6(source);
|
|
345
3939
|
} catch {
|
|
346
|
-
try {
|
|
347
|
-
ast = parse(source);
|
|
348
|
-
} catch {
|
|
349
|
-
return [];
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
resetMemo();
|
|
353
|
-
setModuleResolver(resolveModule);
|
|
354
|
-
setCurrentFileDir(dirname(filePath));
|
|
355
|
-
const globalEnv = createEnvironment();
|
|
356
|
-
evaluateProgram(ast, globalEnv);
|
|
357
|
-
if (!globalEnv.has(objName)) {
|
|
358
|
-
setModuleResolver(null);
|
|
359
3940
|
return [];
|
|
360
3941
|
}
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const lines = source.split("\n");
|
|
371
|
-
if (line < 1 || line > lines.length) return "";
|
|
372
|
-
return lines[line - 1].slice(0, column);
|
|
373
|
-
}
|
|
374
|
-
function getVariableCompletions(filePath, source) {
|
|
375
|
-
const ast = parse(source);
|
|
376
|
-
resetMemo();
|
|
377
|
-
setModuleResolver(resolveModule);
|
|
378
|
-
setCurrentFileDir(dirname(filePath));
|
|
379
|
-
const globalEnv = createEnvironment();
|
|
380
|
-
evaluateProgram(ast, globalEnv);
|
|
381
|
-
const ownBindings = globalEnv.getOwnBindings();
|
|
382
|
-
const completions = [];
|
|
383
|
-
for (const [name, tv] of Object.entries(ownBindings)) {
|
|
384
|
-
if (name.startsWith("__export_")) continue;
|
|
385
|
-
completions.push({
|
|
386
|
-
label: name,
|
|
387
|
-
kind: tv.kind === "function" ? "method" : "variable",
|
|
388
|
-
detail: typeValueToString(tv)
|
|
389
|
-
});
|
|
3942
|
+
const env = createEnvironment3();
|
|
3943
|
+
resetMemo2();
|
|
3944
|
+
setModuleResolver2(resolveModule);
|
|
3945
|
+
setCurrentFileDir2(dirname7(filePath));
|
|
3946
|
+
try {
|
|
3947
|
+
evaluateProgram2(ast, env);
|
|
3948
|
+
} catch {
|
|
3949
|
+
} finally {
|
|
3950
|
+
setModuleResolver2(null);
|
|
390
3951
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
kind: val.kind === "function" ? "method" : "property",
|
|
401
|
-
detail: typeValueToString(val)
|
|
402
|
-
});
|
|
3952
|
+
const program = ast.program ?? ast;
|
|
3953
|
+
const ownBindings = env.getOwnBindings();
|
|
3954
|
+
const topLevelDeclarators = /* @__PURE__ */ new Set();
|
|
3955
|
+
for (const stmt of program.body ?? []) {
|
|
3956
|
+
const decl = stmt.type === "ExportNamedDeclaration" || stmt.type === "ExportDefaultDeclaration" ? stmt.declaration ?? null : stmt;
|
|
3957
|
+
if (decl && decl.type === "VariableDeclaration") {
|
|
3958
|
+
for (const d of decl.declarations) {
|
|
3959
|
+
topLevelDeclarators.add(d);
|
|
3960
|
+
}
|
|
403
3961
|
}
|
|
404
|
-
return completions;
|
|
405
3962
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
3963
|
+
const tokens = [];
|
|
3964
|
+
const pushIdentifier = (id, typeIndex) => {
|
|
3965
|
+
const loc = id.loc;
|
|
3966
|
+
const name = id.name;
|
|
3967
|
+
if (!loc || typeof name !== "string") return;
|
|
3968
|
+
tokens.push({
|
|
3969
|
+
line: loc.start.line - 1,
|
|
3970
|
+
char: loc.start.column,
|
|
3971
|
+
length: name.length,
|
|
3972
|
+
typeIndex,
|
|
3973
|
+
modifierBitmask: MOD_DECLARATION
|
|
3974
|
+
});
|
|
3975
|
+
};
|
|
3976
|
+
const collectParams = (params) => {
|
|
3977
|
+
for (const p of params) {
|
|
3978
|
+
if (!p) continue;
|
|
3979
|
+
if (p.type === "Identifier") pushIdentifier(p, TYPE_PARAMETER);
|
|
3980
|
+
else if (p.type === "AssignmentPattern" && p.left?.type === "Identifier") {
|
|
3981
|
+
pushIdentifier(p.left, TYPE_PARAMETER);
|
|
3982
|
+
} else if (p.type === "RestElement" && p.argument?.type === "Identifier") {
|
|
3983
|
+
pushIdentifier(p.argument, TYPE_PARAMETER);
|
|
3984
|
+
}
|
|
413
3985
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
3986
|
+
};
|
|
3987
|
+
const isFunctionValue = (name) => ownBindings[name]?.kind === "function";
|
|
3988
|
+
const visit = (node) => {
|
|
3989
|
+
if (!node || typeof node !== "object") return;
|
|
3990
|
+
const n = node;
|
|
3991
|
+
switch (n.type) {
|
|
3992
|
+
case "VariableDeclarator": {
|
|
3993
|
+
const id = n.id;
|
|
3994
|
+
if (id?.type === "Identifier") {
|
|
3995
|
+
const isTopLevel = topLevelDeclarators.has(node);
|
|
3996
|
+
const typeIndex = isTopLevel && isFunctionValue(id.name) ? TYPE_FUNCTION : TYPE_VARIABLE;
|
|
3997
|
+
pushIdentifier(id, typeIndex);
|
|
3998
|
+
}
|
|
3999
|
+
break;
|
|
4000
|
+
}
|
|
4001
|
+
case "FunctionDeclaration":
|
|
4002
|
+
case "FunctionExpression": {
|
|
4003
|
+
const id = n.id;
|
|
4004
|
+
if (id?.type === "Identifier") pushIdentifier(id, TYPE_FUNCTION);
|
|
4005
|
+
collectParams(n.params ?? []);
|
|
4006
|
+
break;
|
|
4007
|
+
}
|
|
4008
|
+
case "ArrowFunctionExpression":
|
|
4009
|
+
collectParams(n.params ?? []);
|
|
4010
|
+
break;
|
|
4011
|
+
case "ObjectMethod": {
|
|
4012
|
+
const key = n.key;
|
|
4013
|
+
if (key?.type === "Identifier" && !n.computed) pushIdentifier(key, TYPE_METHOD);
|
|
4014
|
+
collectParams(n.params ?? []);
|
|
4015
|
+
break;
|
|
4016
|
+
}
|
|
4017
|
+
case "ObjectProperty": {
|
|
4018
|
+
const key = n.key;
|
|
4019
|
+
const value = n.value;
|
|
4020
|
+
if (key?.type === "Identifier" && !n.computed) {
|
|
4021
|
+
const isFn = value?.type === "FunctionExpression" || value?.type === "ArrowFunctionExpression";
|
|
4022
|
+
pushIdentifier(key, isFn ? TYPE_METHOD : TYPE_PROPERTY);
|
|
4023
|
+
}
|
|
4024
|
+
break;
|
|
4025
|
+
}
|
|
436
4026
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
4027
|
+
for (const key of Object.keys(n)) {
|
|
4028
|
+
if (key === "loc" || key === "start" || key === "end" || key === "leadingComments" || key === "trailingComments" || key === "innerComments") continue;
|
|
4029
|
+
const child = n[key];
|
|
4030
|
+
if (Array.isArray(child)) {
|
|
4031
|
+
for (const item of child) {
|
|
4032
|
+
if (item && typeof item === "object" && typeof item.type === "string") {
|
|
4033
|
+
visit(item);
|
|
4034
|
+
}
|
|
4035
|
+
}
|
|
4036
|
+
} else if (child && typeof child === "object" && typeof child.type === "string") {
|
|
4037
|
+
visit(child);
|
|
4038
|
+
}
|
|
449
4039
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
4040
|
+
};
|
|
4041
|
+
visit(program);
|
|
4042
|
+
tokens.sort((a, b) => a.line - b.line || a.char - b.char);
|
|
4043
|
+
return encodeSemanticTokens(tokens);
|
|
453
4044
|
}
|
|
454
4045
|
|
|
455
4046
|
// src/dts-generator.ts
|
|
456
|
-
import { isTemplate, getTemplateParts } from "@nudojs/core";
|
|
4047
|
+
import { T as T3, isTemplate, getTemplateParts, simplifyUnion as simplifyUnion2, typeValueEquals, getFnSig as getFnSig4 } from "@nudojs/core";
|
|
457
4048
|
function typeValueToTSType(tv) {
|
|
458
4049
|
switch (tv.kind) {
|
|
459
4050
|
case "literal": {
|
|
@@ -487,8 +4078,10 @@ function typeValueToTSType(tv) {
|
|
|
487
4078
|
return `[${inner}]`;
|
|
488
4079
|
}
|
|
489
4080
|
case "function": {
|
|
490
|
-
const params = tv.params.map((p
|
|
491
|
-
|
|
4081
|
+
const params = tv.params.map((p) => `${p}: unknown`).join(", ");
|
|
4082
|
+
const sig = getFnSig4(tv);
|
|
4083
|
+
const retStr = sig?.returnType ? typeValueToTSType(sig.returnType) : "unknown";
|
|
4084
|
+
return `(${params}) => ${retStr}`;
|
|
492
4085
|
}
|
|
493
4086
|
case "promise":
|
|
494
4087
|
return `Promise<${typeValueToTSType(tv.value)}>`;
|
|
@@ -507,33 +4100,671 @@ function wrapComplexType(tv) {
|
|
|
507
4100
|
if (tv.kind === "union") return `(${ts})`;
|
|
508
4101
|
return ts;
|
|
509
4102
|
}
|
|
4103
|
+
function getParamName(fn, index) {
|
|
4104
|
+
if (fn.paramNames && fn.paramNames[index]) {
|
|
4105
|
+
return fn.paramNames[index];
|
|
4106
|
+
}
|
|
4107
|
+
return `arg${index}`;
|
|
4108
|
+
}
|
|
4109
|
+
function widenLiteralToBase(tv) {
|
|
4110
|
+
if (tv.kind !== "literal") return tv;
|
|
4111
|
+
const v = tv.value;
|
|
4112
|
+
if (typeof v === "number") return T3.number;
|
|
4113
|
+
if (typeof v === "string") return T3.string;
|
|
4114
|
+
if (typeof v === "boolean") return T3.boolean;
|
|
4115
|
+
if (typeof v === "bigint") return T3.bigint;
|
|
4116
|
+
return tv;
|
|
4117
|
+
}
|
|
4118
|
+
function widenTopLevel(tv) {
|
|
4119
|
+
if (tv.kind !== "union") return widenLiteralToBase(tv);
|
|
4120
|
+
return simplifyUnion2(tv.members.map(widenLiteralToBase));
|
|
4121
|
+
}
|
|
4122
|
+
function widenProperties(properties) {
|
|
4123
|
+
const out = {};
|
|
4124
|
+
for (const [k, v] of Object.entries(properties)) out[k] = widenParamType(v);
|
|
4125
|
+
return out;
|
|
4126
|
+
}
|
|
4127
|
+
function widenParamType(tv) {
|
|
4128
|
+
switch (tv.kind) {
|
|
4129
|
+
case "literal":
|
|
4130
|
+
return widenLiteralToBase(tv);
|
|
4131
|
+
case "union":
|
|
4132
|
+
return simplifyUnion2(tv.members.map(widenParamType));
|
|
4133
|
+
case "tuple": {
|
|
4134
|
+
const widened = tv.elements.map(widenParamType);
|
|
4135
|
+
const first = widened[0];
|
|
4136
|
+
if (first && widened.every((el) => typeValueEquals(el, first))) {
|
|
4137
|
+
return T3.array(first);
|
|
4138
|
+
}
|
|
4139
|
+
return T3.tuple(widened);
|
|
4140
|
+
}
|
|
4141
|
+
case "array":
|
|
4142
|
+
return T3.array(widenParamType(tv.element));
|
|
4143
|
+
case "object":
|
|
4144
|
+
return T3.object(widenProperties(tv.properties));
|
|
4145
|
+
case "promise":
|
|
4146
|
+
return T3.promise(widenParamType(tv.value));
|
|
4147
|
+
case "instance":
|
|
4148
|
+
return T3.instanceOf(tv.className, widenProperties(tv.properties));
|
|
4149
|
+
case "refined":
|
|
4150
|
+
return widenParamType(tv.base);
|
|
4151
|
+
default:
|
|
4152
|
+
return tv;
|
|
4153
|
+
}
|
|
4154
|
+
}
|
|
4155
|
+
function widenedUnion(members) {
|
|
4156
|
+
const flat = [];
|
|
4157
|
+
const collect = (tv) => {
|
|
4158
|
+
if (tv.kind === "union") {
|
|
4159
|
+
tv.members.forEach(collect);
|
|
4160
|
+
return;
|
|
4161
|
+
}
|
|
4162
|
+
flat.push(tv);
|
|
4163
|
+
};
|
|
4164
|
+
members.forEach(collect);
|
|
4165
|
+
const widened = flat.map(widenParamType);
|
|
4166
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4167
|
+
const deduped = widened.filter((tv) => {
|
|
4168
|
+
const key = typeValueToTSType(tv);
|
|
4169
|
+
if (seen.has(key)) return false;
|
|
4170
|
+
seen.add(key);
|
|
4171
|
+
return true;
|
|
4172
|
+
});
|
|
4173
|
+
return simplifyUnion2(deduped);
|
|
4174
|
+
}
|
|
4175
|
+
function computeMainSignature(fn) {
|
|
4176
|
+
const arity = Math.max(...fn.cases.map((c) => c.args.length));
|
|
4177
|
+
const minArity = Math.min(...fn.cases.map((c) => c.args.length));
|
|
4178
|
+
const params = [];
|
|
4179
|
+
const paramNames = [];
|
|
4180
|
+
const paramTypes = [];
|
|
4181
|
+
const usedNames = /* @__PURE__ */ new Set();
|
|
4182
|
+
for (let i = 0; i < arity; i++) {
|
|
4183
|
+
const members = fn.cases.filter((c) => i < c.args.length).map((c) => c.args[i]);
|
|
4184
|
+
const typeStr = typeValueToTSType(widenedUnion(members));
|
|
4185
|
+
let name = getParamName(fn, i);
|
|
4186
|
+
if (usedNames.has(name)) {
|
|
4187
|
+
let n = 2;
|
|
4188
|
+
while (usedNames.has(`${name}${n}`)) n++;
|
|
4189
|
+
name = `${name}${n}`;
|
|
4190
|
+
}
|
|
4191
|
+
usedNames.add(name);
|
|
4192
|
+
const optional = i >= minArity && !name.startsWith("...");
|
|
4193
|
+
params.push(optional ? `${name}?: ${typeStr}` : `${name}: ${typeStr}`);
|
|
4194
|
+
paramNames.push(name);
|
|
4195
|
+
paramTypes.push(typeStr);
|
|
4196
|
+
}
|
|
4197
|
+
const combined = fn.combined ?? simplifyUnion2(fn.cases.map((c) => c.result));
|
|
4198
|
+
const returnType = typeValueToTSType(widenTopLevel(combined));
|
|
4199
|
+
return { params, paramNames, paramTypes, returnType };
|
|
4200
|
+
}
|
|
4201
|
+
function generateJSDoc(fn, sig) {
|
|
4202
|
+
if (fn.cases.length === 0) return "";
|
|
4203
|
+
const lines = ["/**"];
|
|
4204
|
+
for (const c of fn.cases) {
|
|
4205
|
+
const preciseDiffers = c.args.length !== sig.paramTypes.length || c.args.some((a, i) => typeValueToTSType(a) !== sig.paramTypes[i]) || typeValueToTSType(c.result) !== sig.returnType;
|
|
4206
|
+
if (!preciseDiffers) continue;
|
|
4207
|
+
const argsStr = c.args.map(typeValueToTSType).join(", ");
|
|
4208
|
+
lines.push(` * Case: ${c.name} (${argsStr}) => ${typeValueToTSType(c.result)}`);
|
|
4209
|
+
}
|
|
4210
|
+
for (let i = 0; i < sig.paramTypes.length; i++) {
|
|
4211
|
+
lines.push(` * @param ${sig.paramNames[i]} - ${sig.paramTypes[i]}`);
|
|
4212
|
+
}
|
|
4213
|
+
lines.push(` * @returns ${sig.returnType}`);
|
|
4214
|
+
lines.push(" */");
|
|
4215
|
+
return lines.join("\n");
|
|
4216
|
+
}
|
|
4217
|
+
function generateFunctionDtsLines(fn) {
|
|
4218
|
+
if (fn.noDeclaration) return [];
|
|
4219
|
+
if (fn.cases.length === 0) {
|
|
4220
|
+
if (fn.combined) {
|
|
4221
|
+
return [
|
|
4222
|
+
`export declare function ${fn.name}(...args: unknown[]): ${typeValueToTSType(fn.combined)};`
|
|
4223
|
+
];
|
|
4224
|
+
}
|
|
4225
|
+
return [];
|
|
4226
|
+
}
|
|
4227
|
+
const sig = computeMainSignature(fn);
|
|
4228
|
+
const jsdoc = generateJSDoc(fn, sig);
|
|
4229
|
+
const lines = [];
|
|
4230
|
+
if (jsdoc) lines.push(jsdoc);
|
|
4231
|
+
lines.push(`export declare function ${fn.name}(${sig.params.join(", ")}): ${sig.returnType};`);
|
|
4232
|
+
return lines;
|
|
4233
|
+
}
|
|
510
4234
|
function generateDts(result) {
|
|
511
4235
|
const lines = [];
|
|
512
4236
|
for (const fn of result.functions) {
|
|
513
|
-
|
|
514
|
-
|
|
4237
|
+
lines.push(...generateFunctionDtsLines(fn));
|
|
4238
|
+
}
|
|
4239
|
+
return lines.join("\n") + "\n";
|
|
4240
|
+
}
|
|
4241
|
+
|
|
4242
|
+
// src/schema-generator.ts
|
|
4243
|
+
function typeValueToZodSchema(tv) {
|
|
4244
|
+
switch (tv.kind) {
|
|
4245
|
+
case "literal": {
|
|
4246
|
+
if (tv.value === null) return "z.null()";
|
|
4247
|
+
if (tv.value === void 0) return "z.undefined()";
|
|
4248
|
+
if (typeof tv.value === "string") return `z.literal(${JSON.stringify(tv.value)})`;
|
|
4249
|
+
if (typeof tv.value === "boolean") return `z.literal(${tv.value})`;
|
|
4250
|
+
return `z.literal(${tv.value})`;
|
|
4251
|
+
}
|
|
4252
|
+
case "primitive":
|
|
4253
|
+
return `z.${tv.type}()`;
|
|
4254
|
+
case "refined":
|
|
4255
|
+
return typeValueToZodSchema(tv.base);
|
|
4256
|
+
case "object": {
|
|
4257
|
+
const entries = Object.entries(tv.properties).map(([k, v]) => `${k}: ${typeValueToZodSchema(v)}`).join(", ");
|
|
4258
|
+
return `z.object({ ${entries} })`;
|
|
4259
|
+
}
|
|
4260
|
+
case "array":
|
|
4261
|
+
return `z.array(${typeValueToZodSchema(tv.element)})`;
|
|
4262
|
+
case "tuple": {
|
|
4263
|
+
const inner = tv.elements.map(typeValueToZodSchema).join(", ");
|
|
4264
|
+
return `z.tuple([${inner}])`;
|
|
4265
|
+
}
|
|
4266
|
+
case "function":
|
|
4267
|
+
return "z.function()";
|
|
4268
|
+
case "promise":
|
|
4269
|
+
return `z.promise(${typeValueToZodSchema(tv.value)})`;
|
|
4270
|
+
case "instance":
|
|
4271
|
+
return `z.instanceof(${tv.className})`;
|
|
4272
|
+
case "union": {
|
|
4273
|
+
const members = tv.members.map(typeValueToZodSchema).join(", ");
|
|
4274
|
+
return `z.union([${members}])`;
|
|
4275
|
+
}
|
|
4276
|
+
case "never":
|
|
4277
|
+
return "z.never()";
|
|
4278
|
+
case "unknown":
|
|
4279
|
+
return "z.unknown()";
|
|
4280
|
+
}
|
|
4281
|
+
}
|
|
4282
|
+
|
|
4283
|
+
// src/guard-generator.ts
|
|
4284
|
+
import { denoteGuard } from "@nudojs/core";
|
|
4285
|
+
function generateGuardFunction(name, tv) {
|
|
4286
|
+
const body = generateGuardBody(tv, "data");
|
|
4287
|
+
return `export function ${name}(data) {
|
|
4288
|
+
return ${body};
|
|
4289
|
+
}`;
|
|
4290
|
+
}
|
|
4291
|
+
function generateGuardFunctionFromAbs(name, abs) {
|
|
4292
|
+
const body = denoteGuard(abs, "data");
|
|
4293
|
+
return `export function ${name}(data) {
|
|
4294
|
+
return ${body};
|
|
4295
|
+
}`;
|
|
4296
|
+
}
|
|
4297
|
+
function generateGuardBody(tv, varName) {
|
|
4298
|
+
switch (tv.kind) {
|
|
4299
|
+
case "literal": {
|
|
4300
|
+
if (tv.value === null) return `${varName} === null`;
|
|
4301
|
+
if (tv.value === void 0) return `${varName} === undefined`;
|
|
4302
|
+
return `${varName} === ${JSON.stringify(tv.value)}`;
|
|
4303
|
+
}
|
|
4304
|
+
case "primitive":
|
|
4305
|
+
return `typeof ${varName} === '${tv.type}'`;
|
|
4306
|
+
case "refined":
|
|
4307
|
+
return generateGuardBody(tv.base, varName);
|
|
4308
|
+
case "object": {
|
|
4309
|
+
const checks = [`typeof ${varName} === 'object'`, `${varName} !== null`];
|
|
4310
|
+
for (const [key, val] of Object.entries(tv.properties)) {
|
|
4311
|
+
checks.push(generateGuardBody(val, `${varName}.${key}`));
|
|
4312
|
+
}
|
|
4313
|
+
return checks.join(" && ");
|
|
4314
|
+
}
|
|
4315
|
+
case "array":
|
|
4316
|
+
return `Array.isArray(${varName}) && ${varName}.every(item => ${generateGuardBody(tv.element, "item")})`;
|
|
4317
|
+
case "tuple": {
|
|
4318
|
+
const checks = [`Array.isArray(${varName})`, `${varName}.length === ${tv.elements.length}`];
|
|
4319
|
+
tv.elements.forEach((el, i) => {
|
|
4320
|
+
checks.push(generateGuardBody(el, `${varName}[${i}]`));
|
|
4321
|
+
});
|
|
4322
|
+
return checks.join(" && ");
|
|
4323
|
+
}
|
|
4324
|
+
case "union": {
|
|
4325
|
+
const members = tv.members.map((m) => generateGuardBody(m, varName));
|
|
4326
|
+
return `(${members.join(" || ")})`;
|
|
4327
|
+
}
|
|
4328
|
+
case "never":
|
|
4329
|
+
return "false";
|
|
4330
|
+
case "unknown":
|
|
4331
|
+
return "true";
|
|
4332
|
+
case "function":
|
|
4333
|
+
return `typeof ${varName} === 'function'`;
|
|
4334
|
+
case "promise":
|
|
4335
|
+
return `${varName} instanceof Promise`;
|
|
4336
|
+
case "instance":
|
|
4337
|
+
return `${varName} instanceof ${tv.className}`;
|
|
4338
|
+
}
|
|
4339
|
+
}
|
|
4340
|
+
|
|
4341
|
+
// src/case-emitter.ts
|
|
4342
|
+
import { parse as parse7, extractDirectives as extractDirectives2 } from "@nudojs/parser";
|
|
4343
|
+
var GENERATED_PREFIX = "call@";
|
|
4344
|
+
var GENERATED_CASE_LINE_REGEX = /^\s*\*?\s*@nudo:case\s+"(call@[^"]*)"/;
|
|
4345
|
+
var IDENT_KEY_REGEX = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
4346
|
+
var NUMBER_LITERAL_REGEX = /^-?\d+(\.\d+)?$/;
|
|
4347
|
+
var UNSAFE_STRING_CHARS = /[,()[\]{}]/;
|
|
4348
|
+
var UNSAFE_KEY_CHARS = /[,()[\]{}:]/;
|
|
4349
|
+
var CONTROL_CHARS = /[\x00-\x1f\x7f]/;
|
|
4350
|
+
function serializeStringLiteral(value) {
|
|
4351
|
+
if (CONTROL_CHARS.test(value)) return null;
|
|
4352
|
+
if (value.includes("*/")) return null;
|
|
4353
|
+
if (UNSAFE_STRING_CHARS.test(value)) return null;
|
|
4354
|
+
const hasDbl = value.includes('"');
|
|
4355
|
+
const hasSgl = value.includes("'");
|
|
4356
|
+
if (hasDbl && hasSgl) return null;
|
|
4357
|
+
return hasDbl ? `'${value}'` : `"${value}"`;
|
|
4358
|
+
}
|
|
4359
|
+
function serializeObjectKey(key) {
|
|
4360
|
+
if (IDENT_KEY_REGEX.test(key)) return key;
|
|
4361
|
+
if (CONTROL_CHARS.test(key)) return null;
|
|
4362
|
+
if (key.includes('"')) return null;
|
|
4363
|
+
if (UNSAFE_KEY_CHARS.test(key) || key.includes("*/")) return null;
|
|
4364
|
+
if (/^['"]|['"]$/.test(key)) return null;
|
|
4365
|
+
return `"${key}"`;
|
|
4366
|
+
}
|
|
4367
|
+
function serializeCaseArg(tv) {
|
|
4368
|
+
switch (tv.kind) {
|
|
4369
|
+
case "primitive":
|
|
4370
|
+
if (tv.type === "number" || tv.type === "string" || tv.type === "boolean") {
|
|
4371
|
+
return `T.${tv.type}`;
|
|
4372
|
+
}
|
|
4373
|
+
return null;
|
|
4374
|
+
case "unknown":
|
|
4375
|
+
return "T.unknown";
|
|
4376
|
+
case "never":
|
|
4377
|
+
return "T.never";
|
|
4378
|
+
case "literal": {
|
|
4379
|
+
const v = tv.value;
|
|
4380
|
+
if (typeof v === "number") {
|
|
4381
|
+
if (!Number.isFinite(v)) return null;
|
|
4382
|
+
const s = String(v);
|
|
4383
|
+
return NUMBER_LITERAL_REGEX.test(s) ? s : null;
|
|
4384
|
+
}
|
|
4385
|
+
if (typeof v === "boolean") return v ? "true" : "false";
|
|
4386
|
+
if (v === null) return "null";
|
|
4387
|
+
if (v === void 0) return "undefined";
|
|
4388
|
+
if (typeof v === "bigint") return null;
|
|
4389
|
+
return serializeStringLiteral(v);
|
|
4390
|
+
}
|
|
4391
|
+
case "union": {
|
|
4392
|
+
const parts = [];
|
|
4393
|
+
for (const member of tv.members) {
|
|
4394
|
+
const s = serializeCaseArg(member);
|
|
4395
|
+
if (s === null) return null;
|
|
4396
|
+
parts.push(s);
|
|
4397
|
+
}
|
|
4398
|
+
return `T.union(${parts.join(", ")})`;
|
|
4399
|
+
}
|
|
4400
|
+
case "array": {
|
|
4401
|
+
const el = serializeCaseArg(tv.element);
|
|
4402
|
+
return el === null ? null : `T.array(${el})`;
|
|
4403
|
+
}
|
|
4404
|
+
case "tuple": {
|
|
4405
|
+
const parts = [];
|
|
4406
|
+
for (const el of tv.elements) {
|
|
4407
|
+
const s = serializeCaseArg(el);
|
|
4408
|
+
if (s === null) return null;
|
|
4409
|
+
parts.push(s);
|
|
4410
|
+
}
|
|
4411
|
+
return `[${parts.join(", ")}]`;
|
|
4412
|
+
}
|
|
4413
|
+
case "object": {
|
|
4414
|
+
const parts = [];
|
|
4415
|
+
for (const [key, value] of Object.entries(tv.properties)) {
|
|
4416
|
+
const vs = serializeCaseArg(value);
|
|
4417
|
+
if (vs === null) return null;
|
|
4418
|
+
const ks = serializeObjectKey(key);
|
|
4419
|
+
if (ks === null) return null;
|
|
4420
|
+
parts.push(`${ks}: ${vs}`);
|
|
4421
|
+
}
|
|
4422
|
+
return parts.length === 0 ? "{}" : `{ ${parts.join(", ")} }`;
|
|
4423
|
+
}
|
|
4424
|
+
default:
|
|
4425
|
+
return null;
|
|
4426
|
+
}
|
|
4427
|
+
}
|
|
4428
|
+
function buildCaseDirective(name, args) {
|
|
4429
|
+
if (name.includes('"') || /[\r\n]/.test(name)) return null;
|
|
4430
|
+
const parts = [];
|
|
4431
|
+
for (const arg of args) {
|
|
4432
|
+
const s = serializeCaseArg(arg);
|
|
4433
|
+
if (s === null) return null;
|
|
4434
|
+
parts.push(s);
|
|
4435
|
+
}
|
|
4436
|
+
return ` * @nudo:case "${name}" (${parts.join(", ")})`;
|
|
4437
|
+
}
|
|
4438
|
+
function stripGeneratedCaseDirectives(source) {
|
|
4439
|
+
const lines = source.split("\n");
|
|
4440
|
+
const removed = [];
|
|
4441
|
+
const drop = new Array(lines.length).fill(false);
|
|
4442
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4443
|
+
const m = lines[i].match(GENERATED_CASE_LINE_REGEX);
|
|
4444
|
+
if (m) {
|
|
4445
|
+
drop[i] = true;
|
|
4446
|
+
removed.push(m[1]);
|
|
4447
|
+
}
|
|
4448
|
+
}
|
|
4449
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4450
|
+
if (!/^\s*\/\*\*\s*$/.test(lines[i])) continue;
|
|
4451
|
+
let j = i + 1;
|
|
4452
|
+
let clean = true;
|
|
4453
|
+
while (j < lines.length) {
|
|
4454
|
+
if (/\*\//.test(lines[j])) break;
|
|
4455
|
+
if (/\/\*/.test(lines[j])) {
|
|
4456
|
+
clean = false;
|
|
4457
|
+
break;
|
|
4458
|
+
}
|
|
4459
|
+
j++;
|
|
4460
|
+
}
|
|
4461
|
+
if (!clean || j >= lines.length) continue;
|
|
4462
|
+
if (!/^\s*\*\/\s*$/.test(lines[j])) continue;
|
|
4463
|
+
let onlyFiller = true;
|
|
4464
|
+
for (let k = i + 1; k < j; k++) {
|
|
4465
|
+
if (drop[k]) continue;
|
|
4466
|
+
const t = lines[k].trim();
|
|
4467
|
+
if (t !== "" && !/^\*{1,2}$/.test(t)) {
|
|
4468
|
+
onlyFiller = false;
|
|
4469
|
+
break;
|
|
4470
|
+
}
|
|
4471
|
+
}
|
|
4472
|
+
if (onlyFiller) {
|
|
4473
|
+
for (let k = i; k <= j; k++) drop[k] = true;
|
|
4474
|
+
}
|
|
4475
|
+
}
|
|
4476
|
+
return {
|
|
4477
|
+
source: lines.filter((_, i) => !drop[i]).join("\n"),
|
|
4478
|
+
removed
|
|
4479
|
+
};
|
|
4480
|
+
}
|
|
4481
|
+
function collectExistingCases(source) {
|
|
4482
|
+
const byName = /* @__PURE__ */ new Map();
|
|
4483
|
+
let ast;
|
|
4484
|
+
try {
|
|
4485
|
+
ast = parse7(source, { errorRecovery: true });
|
|
4486
|
+
} catch {
|
|
4487
|
+
return byName;
|
|
4488
|
+
}
|
|
4489
|
+
for (const { name, directives } of extractDirectives2(ast)) {
|
|
4490
|
+
for (const d of directives) {
|
|
4491
|
+
if (d.kind !== "case") continue;
|
|
4492
|
+
const list = byName.get(name);
|
|
4493
|
+
if (list) list.push(d);
|
|
4494
|
+
else byName.set(name, [d]);
|
|
4495
|
+
}
|
|
4496
|
+
}
|
|
4497
|
+
return byName;
|
|
4498
|
+
}
|
|
4499
|
+
function insertGeneratedCaseDirectives(source, analysis) {
|
|
4500
|
+
const written = [];
|
|
4501
|
+
const skipped = [];
|
|
4502
|
+
const existing = collectExistingCases(source);
|
|
4503
|
+
const edits = [];
|
|
4504
|
+
for (const fn of analysis.functions) {
|
|
4505
|
+
const existingCases = existing.get(fn.name) ?? [];
|
|
4506
|
+
if (existingCases.some((c) => !c.name.startsWith(GENERATED_PREFIX))) {
|
|
4507
|
+
skipped.push({ fn: fn.name, reason: "hand-written" });
|
|
4508
|
+
continue;
|
|
4509
|
+
}
|
|
4510
|
+
if (existingCases.length > 0) {
|
|
4511
|
+
skipped.push({ fn: fn.name, reason: "already-generated" });
|
|
4512
|
+
continue;
|
|
4513
|
+
}
|
|
4514
|
+
if (fn.skipped) {
|
|
4515
|
+
skipped.push({ fn: fn.name, reason: "skipped" });
|
|
4516
|
+
continue;
|
|
4517
|
+
}
|
|
4518
|
+
if (fn.noDeclaration) {
|
|
4519
|
+
skipped.push({ fn: fn.name, reason: "no-declaration" });
|
|
4520
|
+
continue;
|
|
4521
|
+
}
|
|
4522
|
+
if (fn.entryOnly) {
|
|
4523
|
+
skipped.push({ fn: fn.name, reason: "entry-only" });
|
|
4524
|
+
continue;
|
|
4525
|
+
}
|
|
4526
|
+
const callsiteCases = fn.cases.filter((c) => c.source === "callsite");
|
|
4527
|
+
if (callsiteCases.length === 0) {
|
|
4528
|
+
skipped.push({ fn: fn.name, reason: "no-serializable-cases", detail: "no callsite cases" });
|
|
515
4529
|
continue;
|
|
516
4530
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
const
|
|
521
|
-
|
|
4531
|
+
const built = [];
|
|
4532
|
+
const names = [];
|
|
4533
|
+
for (const c of callsiteCases) {
|
|
4534
|
+
const directive = buildCaseDirective(c.name, c.args);
|
|
4535
|
+
if (directive === null) {
|
|
4536
|
+
skipped.push({ fn: fn.name, reason: "no-serializable-cases", detail: `case ${c.name} not serializable` });
|
|
4537
|
+
} else {
|
|
4538
|
+
built.push(directive);
|
|
4539
|
+
names.push(c.name);
|
|
4540
|
+
}
|
|
4541
|
+
}
|
|
4542
|
+
if (built.length === 0) continue;
|
|
4543
|
+
edits.push({ line: fn.loc.start.line, column: fn.loc.start.column, directives: built, fn: fn.name });
|
|
4544
|
+
written.push({ fn: fn.name, cases: names });
|
|
4545
|
+
}
|
|
4546
|
+
edits.sort((a, b) => b.line - a.line);
|
|
4547
|
+
const lines = source.split("\n");
|
|
4548
|
+
for (const edit of edits) {
|
|
4549
|
+
const declIdx = edit.line - 1;
|
|
4550
|
+
const indent = " ".repeat(edit.column);
|
|
4551
|
+
let insertIdx = -1;
|
|
4552
|
+
let blockIndent = "";
|
|
4553
|
+
const aboveIdx = declIdx - 1;
|
|
4554
|
+
if (aboveIdx >= 0 && /^\s*\*\/\s*$/.test(lines[aboveIdx])) {
|
|
4555
|
+
let s = aboveIdx - 1;
|
|
4556
|
+
while (s >= 0) {
|
|
4557
|
+
const t = lines[s];
|
|
4558
|
+
if (/^\s*\/\*\*/.test(t)) {
|
|
4559
|
+
insertIdx = s + 1;
|
|
4560
|
+
blockIndent = t.match(/^\s*/)?.[0] ?? "";
|
|
4561
|
+
break;
|
|
4562
|
+
}
|
|
4563
|
+
if (/^\s*\*/.test(t) || t.trim() === "") {
|
|
4564
|
+
s--;
|
|
4565
|
+
continue;
|
|
4566
|
+
}
|
|
4567
|
+
break;
|
|
4568
|
+
}
|
|
4569
|
+
}
|
|
4570
|
+
if (insertIdx >= 0) {
|
|
4571
|
+
lines.splice(insertIdx, 0, ...edit.directives.map((d) => blockIndent + d));
|
|
4572
|
+
} else {
|
|
4573
|
+
lines.splice(declIdx, 0, `${indent}/**`, ...edit.directives.map((d) => indent + d), `${indent} */`);
|
|
4574
|
+
}
|
|
4575
|
+
}
|
|
4576
|
+
return {
|
|
4577
|
+
source: lines.join("\n"),
|
|
4578
|
+
changed: written.length > 0,
|
|
4579
|
+
written,
|
|
4580
|
+
skipped
|
|
4581
|
+
};
|
|
4582
|
+
}
|
|
4583
|
+
function diffOps(a, b) {
|
|
4584
|
+
let start = 0;
|
|
4585
|
+
while (start < a.length && start < b.length && a[start] === b[start]) start++;
|
|
4586
|
+
let endA = a.length;
|
|
4587
|
+
let endB = b.length;
|
|
4588
|
+
while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) {
|
|
4589
|
+
endA--;
|
|
4590
|
+
endB--;
|
|
4591
|
+
}
|
|
4592
|
+
const midA = a.slice(start, endA);
|
|
4593
|
+
const midB = b.slice(start, endB);
|
|
4594
|
+
const n = midA.length;
|
|
4595
|
+
const m = midB.length;
|
|
4596
|
+
const dp = new Int32Array((n + 1) * (m + 1));
|
|
4597
|
+
for (let i2 = n - 1; i2 >= 0; i2--) {
|
|
4598
|
+
for (let j2 = m - 1; j2 >= 0; j2--) {
|
|
4599
|
+
dp[i2 * (m + 1) + j2] = midA[i2] === midB[j2] ? dp[(i2 + 1) * (m + 1) + (j2 + 1)] + 1 : Math.max(dp[(i2 + 1) * (m + 1) + j2], dp[i2 * (m + 1) + (j2 + 1)]);
|
|
4600
|
+
}
|
|
4601
|
+
}
|
|
4602
|
+
const ops = [];
|
|
4603
|
+
for (let k = 0; k < start; k++) ops.push({ type: "equal", text: a[k] });
|
|
4604
|
+
let i = 0;
|
|
4605
|
+
let j = 0;
|
|
4606
|
+
while (i < n && j < m) {
|
|
4607
|
+
if (midA[i] === midB[j]) {
|
|
4608
|
+
ops.push({ type: "equal", text: midA[i] });
|
|
4609
|
+
i++;
|
|
4610
|
+
j++;
|
|
4611
|
+
} else if (dp[(i + 1) * (m + 1) + j] >= dp[i * (m + 1) + (j + 1)]) {
|
|
4612
|
+
ops.push({ type: "delete", text: midA[i] });
|
|
4613
|
+
i++;
|
|
4614
|
+
} else {
|
|
4615
|
+
ops.push({ type: "insert", text: midB[j] });
|
|
4616
|
+
j++;
|
|
4617
|
+
}
|
|
4618
|
+
}
|
|
4619
|
+
while (i < n) ops.push({ type: "delete", text: midA[i++] });
|
|
4620
|
+
while (j < m) ops.push({ type: "insert", text: midB[j++] });
|
|
4621
|
+
for (let k = endA; k < a.length; k++) ops.push({ type: "equal", text: a[k] });
|
|
4622
|
+
return ops;
|
|
4623
|
+
}
|
|
4624
|
+
var DIFF_CONTEXT = 3;
|
|
4625
|
+
function formatRange(start, count) {
|
|
4626
|
+
const s = count === 0 ? start - 1 : start;
|
|
4627
|
+
return count === 1 ? `${s}` : `${s},${count}`;
|
|
4628
|
+
}
|
|
4629
|
+
function unifiedDiff(a, b, path) {
|
|
4630
|
+
if (a === b) return "";
|
|
4631
|
+
const ops = diffOps(a.split("\n"), b.split("\n"));
|
|
4632
|
+
const hunks = [];
|
|
4633
|
+
let idx = 0;
|
|
4634
|
+
while (idx < ops.length) {
|
|
4635
|
+
if (ops[idx].type === "equal") {
|
|
4636
|
+
idx++;
|
|
522
4637
|
continue;
|
|
523
4638
|
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
4639
|
+
let start = idx;
|
|
4640
|
+
let ctx = 0;
|
|
4641
|
+
while (start > 0 && ops[start - 1].type === "equal" && ctx < DIFF_CONTEXT) {
|
|
4642
|
+
start--;
|
|
4643
|
+
ctx++;
|
|
4644
|
+
}
|
|
4645
|
+
let j = idx;
|
|
4646
|
+
let equalRun = 0;
|
|
4647
|
+
let lastChange = idx;
|
|
4648
|
+
while (j < ops.length) {
|
|
4649
|
+
if (ops[j].type === "equal") {
|
|
4650
|
+
equalRun++;
|
|
4651
|
+
if (equalRun > DIFF_CONTEXT * 2) break;
|
|
4652
|
+
} else {
|
|
4653
|
+
equalRun = 0;
|
|
4654
|
+
lastChange = j;
|
|
4655
|
+
}
|
|
4656
|
+
j++;
|
|
4657
|
+
}
|
|
4658
|
+
let end = lastChange + 1;
|
|
4659
|
+
ctx = 0;
|
|
4660
|
+
while (end < ops.length && ops[end].type === "equal" && ctx < DIFF_CONTEXT) {
|
|
4661
|
+
end++;
|
|
4662
|
+
ctx++;
|
|
528
4663
|
}
|
|
4664
|
+
hunks.push({ start, ops: ops.slice(start, end) });
|
|
4665
|
+
idx = end;
|
|
529
4666
|
}
|
|
530
|
-
|
|
4667
|
+
const out = [`--- a/${path}`, `+++ b/${path}`];
|
|
4668
|
+
for (let h = 0; h < hunks.length; h++) {
|
|
4669
|
+
const { start, ops: hunkOps } = hunks[h];
|
|
4670
|
+
let oldCount = 0;
|
|
4671
|
+
let newCount = 0;
|
|
4672
|
+
for (let k = 0; k < start; k++) {
|
|
4673
|
+
if (ops[k].type !== "insert") oldCount++;
|
|
4674
|
+
if (ops[k].type !== "delete") newCount++;
|
|
4675
|
+
}
|
|
4676
|
+
let dels = 0;
|
|
4677
|
+
let ins = 0;
|
|
4678
|
+
for (const op of hunkOps) {
|
|
4679
|
+
if (op.type === "delete") dels++;
|
|
4680
|
+
else if (op.type === "insert") ins++;
|
|
4681
|
+
else {
|
|
4682
|
+
dels++;
|
|
4683
|
+
ins++;
|
|
4684
|
+
}
|
|
4685
|
+
}
|
|
4686
|
+
out.push(`@@ -${formatRange(oldCount + 1, dels)} +${formatRange(newCount + 1, ins)} @@`);
|
|
4687
|
+
for (const op of hunkOps) {
|
|
4688
|
+
const prefix = op.type === "equal" ? " " : op.type === "delete" ? "-" : "+";
|
|
4689
|
+
out.push(prefix + op.text);
|
|
4690
|
+
}
|
|
4691
|
+
}
|
|
4692
|
+
return out.join("\n") + "\n";
|
|
531
4693
|
}
|
|
532
4694
|
export {
|
|
4695
|
+
SEMANTIC_TOKEN_MODIFIERS,
|
|
4696
|
+
SEMANTIC_TOKEN_TYPES,
|
|
4697
|
+
analyzeExportsFromSource,
|
|
533
4698
|
analyzeFile,
|
|
4699
|
+
analyzeFileAsync,
|
|
4700
|
+
autoHarvestModules,
|
|
4701
|
+
barePackageName,
|
|
4702
|
+
bareSpecToAbsModules,
|
|
4703
|
+
buildCaseDirective,
|
|
4704
|
+
buildModuleGraph,
|
|
4705
|
+
buildSemanticTokens,
|
|
4706
|
+
clearAbsModuleCache,
|
|
4707
|
+
clearAnalysisFileCache,
|
|
4708
|
+
clearAnalysisSessionCaches,
|
|
4709
|
+
clearBPathCache,
|
|
4710
|
+
clearFnAnalysisCache,
|
|
4711
|
+
clearHarvestCache,
|
|
4712
|
+
collectAbsBindingsFromGraph,
|
|
4713
|
+
collectAbsInlays,
|
|
4714
|
+
collectBPathDiagnostics,
|
|
4715
|
+
collectBPathReplacements,
|
|
4716
|
+
collectBarePackages,
|
|
4717
|
+
collectCallRecords,
|
|
4718
|
+
collectDependencySpecs,
|
|
4719
|
+
collectDtsFromEntry,
|
|
4720
|
+
collectEnvGlobals,
|
|
4721
|
+
collectEnvModules,
|
|
4722
|
+
collectStaticImports,
|
|
4723
|
+
computeDirtySet,
|
|
4724
|
+
defaultAbsLoadModule,
|
|
4725
|
+
defaultLoadModule,
|
|
4726
|
+
encodeSemanticTokens,
|
|
4727
|
+
envValueToAbs,
|
|
4728
|
+
evalAbsModuleGraph,
|
|
4729
|
+
evalProgramAbsWithModules,
|
|
4730
|
+
evictAbsModuleCacheFiles,
|
|
4731
|
+
evictAnalysisCachesForFiles,
|
|
4732
|
+
evictAnalysisFileCacheForFiles,
|
|
4733
|
+
evictBPathCacheForFiles,
|
|
4734
|
+
evictFnAnalysisCacheForFiles,
|
|
4735
|
+
formatHarvestSummary,
|
|
534
4736
|
generateDts,
|
|
4737
|
+
generateFunctionDtsLines,
|
|
4738
|
+
generateGuardFunction,
|
|
4739
|
+
generateGuardFunctionFromAbs,
|
|
4740
|
+
getAnalysisFileCacheSize,
|
|
535
4741
|
getCasesForFile,
|
|
536
4742
|
getCompletionsAtPosition,
|
|
4743
|
+
getHoverAtPosition,
|
|
537
4744
|
getTypeAtPosition,
|
|
538
|
-
|
|
4745
|
+
getTypeAtPositionAsync,
|
|
4746
|
+
harvestNodeTypes,
|
|
4747
|
+
harvestPackage,
|
|
4748
|
+
harvestPackageCached,
|
|
4749
|
+
harvestToAbsModules,
|
|
4750
|
+
harvestedValueToAbs,
|
|
4751
|
+
insertGeneratedCaseDirectives,
|
|
4752
|
+
isBPathCapable,
|
|
4753
|
+
isNudoTargetPath,
|
|
4754
|
+
lookupHarvested,
|
|
4755
|
+
mockDirectivesToAbsSeeds,
|
|
4756
|
+
packageHarvestToAbsModules,
|
|
4757
|
+
resetAllAnalysisCaches,
|
|
4758
|
+
resolvePackageRoot,
|
|
4759
|
+
serializeCaseArg,
|
|
4760
|
+
serializeInferJson,
|
|
4761
|
+
stripGeneratedCaseDirectives,
|
|
4762
|
+
summarizeNodeEnv,
|
|
4763
|
+
topoSortDirty,
|
|
4764
|
+
tryBPathCall,
|
|
4765
|
+
tryBPathCallFull,
|
|
4766
|
+
tryRunBPath,
|
|
4767
|
+
typeValueToTSType,
|
|
4768
|
+
typeValueToZodSchema,
|
|
4769
|
+
unifiedDiff
|
|
539
4770
|
};
|