@nudojs/core 1.3.0 → 1.4.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/dist/{check-report-BWmfy810.d.ts → check-report-Ci1dDysO.d.ts} +52 -27
- package/dist/{chunk-5YNR4Q7E.js → chunk-7QUVTEYZ.js} +817 -169
- package/dist/{chunk-CKH6GQAI.js → chunk-LFCF6BLE.js} +441 -189
- package/dist/exec.d.ts +8 -16
- package/dist/exec.js +9 -1
- package/dist/index.d.ts +119 -109
- package/dist/index.js +98 -63
- package/dist/internal.d.ts +4 -4
- package/dist/internal.js +39 -27
- package/dist/{run-Dk08Q5zl.d.ts → run-WJfj477N.d.ts} +186 -3
- package/package.json +1 -1
|
@@ -45,7 +45,614 @@ import {
|
|
|
45
45
|
withEvalBudgetOverride,
|
|
46
46
|
withExecPhi,
|
|
47
47
|
withFnBudgetOverride
|
|
48
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-LFCF6BLE.js";
|
|
49
|
+
|
|
50
|
+
// src/algebra/directive-scan.ts
|
|
51
|
+
function cleanDirectiveLine(raw, kind) {
|
|
52
|
+
let s = raw;
|
|
53
|
+
if (kind === "block") {
|
|
54
|
+
s = s.replace(/^\s*\*\s?/, "");
|
|
55
|
+
} else {
|
|
56
|
+
s = s.replace(/^\//, "");
|
|
57
|
+
}
|
|
58
|
+
return s.trim();
|
|
59
|
+
}
|
|
60
|
+
function commentTextToLines(text, kind) {
|
|
61
|
+
return text.split("\n").map((line) => cleanDirectiveLine(line, kind));
|
|
62
|
+
}
|
|
63
|
+
function commentKind(c) {
|
|
64
|
+
return c.type === "CommentLine" ? "line" : "block";
|
|
65
|
+
}
|
|
66
|
+
function buildScope(name, node, wrapper, comments, wrapperComments) {
|
|
67
|
+
const seen = /* @__PURE__ */ new Set();
|
|
68
|
+
const texts = [];
|
|
69
|
+
const startLines = [];
|
|
70
|
+
const lines = [];
|
|
71
|
+
for (const c of [...wrapperComments ?? [], ...comments ?? []]) {
|
|
72
|
+
if (seen.has(c)) continue;
|
|
73
|
+
seen.add(c);
|
|
74
|
+
texts.push(c.value);
|
|
75
|
+
startLines.push(c.loc?.start?.line ?? 0);
|
|
76
|
+
for (const line of commentTextToLines(c.value, commentKind(c))) lines.push(line);
|
|
77
|
+
}
|
|
78
|
+
const loc = node.loc;
|
|
79
|
+
return {
|
|
80
|
+
name,
|
|
81
|
+
node,
|
|
82
|
+
commentTexts: texts,
|
|
83
|
+
commentLines: lines,
|
|
84
|
+
commentStartLines: startLines,
|
|
85
|
+
startLine: loc?.start?.line ?? 0
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function isFnExpr(n) {
|
|
89
|
+
return !!n && (n.type === "ArrowFunctionExpression" || n.type === "FunctionExpression" || n.type === "FunctionDeclaration");
|
|
90
|
+
}
|
|
91
|
+
function keyNameOf(key) {
|
|
92
|
+
if (!key) return void 0;
|
|
93
|
+
const k = key;
|
|
94
|
+
if (k.type === "Identifier") return k.name;
|
|
95
|
+
if (k.type === "StringLiteral" || k.type === "NumericLiteral") return String(k.value);
|
|
96
|
+
return void 0;
|
|
97
|
+
}
|
|
98
|
+
function listFnDirectiveScopes(file) {
|
|
99
|
+
const out = [];
|
|
100
|
+
const pushNamedFn = (name, fnNode, commentNode, wrapper) => {
|
|
101
|
+
out.push(
|
|
102
|
+
buildScope(
|
|
103
|
+
name,
|
|
104
|
+
fnNode,
|
|
105
|
+
wrapper,
|
|
106
|
+
commentNode?.leadingComments,
|
|
107
|
+
wrapper?.leadingComments
|
|
108
|
+
)
|
|
109
|
+
);
|
|
110
|
+
};
|
|
111
|
+
const visit2 = (node, ctx) => {
|
|
112
|
+
if (!node || typeof node !== "object") return;
|
|
113
|
+
const n = node;
|
|
114
|
+
switch (n.type) {
|
|
115
|
+
case "File":
|
|
116
|
+
visit2(n.program, ctx);
|
|
117
|
+
return;
|
|
118
|
+
case "Program": {
|
|
119
|
+
for (const s of n.body ?? []) visit2(s, ctx);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
case "ExportNamedDeclaration":
|
|
123
|
+
case "ExportDefaultDeclaration": {
|
|
124
|
+
const decl = n.declaration;
|
|
125
|
+
if (!decl) return;
|
|
126
|
+
const wrapCtx = { ...ctx, wrapperNode: node };
|
|
127
|
+
visitWithWrapper(decl, node, wrapCtx);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
case "FunctionDeclaration": {
|
|
131
|
+
const name = n.id?.name ?? ctx.fallbackName ?? "<anonymous>";
|
|
132
|
+
pushNamedFn(name, node, node, ctx.wrapperNode);
|
|
133
|
+
visit2(n.body, { owner: name });
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
case "FunctionExpression": {
|
|
137
|
+
const name = n.id?.name ?? ctx.fallbackName ?? "<anonymous>";
|
|
138
|
+
pushNamedFn(name, node, node, ctx.wrapperNode);
|
|
139
|
+
visit2(n.body, { owner: name });
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
case "ArrowFunctionExpression": {
|
|
143
|
+
const name = ctx.fallbackName ?? "<anonymous>";
|
|
144
|
+
pushNamedFn(name, node, ctx.commentHost ?? node, ctx.wrapperNode);
|
|
145
|
+
visit2(n.body, { owner: name });
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
case "VariableDeclaration": {
|
|
149
|
+
for (const d of n.declarations ?? []) {
|
|
150
|
+
const decl = d;
|
|
151
|
+
const idName = decl.id?.type === "Identifier" ? decl.id.name : void 0;
|
|
152
|
+
if (isFnExpr(decl.init)) {
|
|
153
|
+
const fn2 = decl.init;
|
|
154
|
+
const scope = buildScope(
|
|
155
|
+
idName ?? "<anonymous>",
|
|
156
|
+
fn2,
|
|
157
|
+
ctx.wrapperNode,
|
|
158
|
+
node.leadingComments,
|
|
159
|
+
ctx.wrapperNode?.leadingComments
|
|
160
|
+
);
|
|
161
|
+
out.push(scope);
|
|
162
|
+
visit2(fn2, { owner: idName });
|
|
163
|
+
} else {
|
|
164
|
+
visit2(decl.init, { owner: idName, commentHost: node, wrapperNode: ctx.wrapperNode });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
case "ClassDeclaration":
|
|
170
|
+
case "ClassExpression": {
|
|
171
|
+
const cname = n.id?.name ?? ctx.fallbackName ?? "<anonymous>";
|
|
172
|
+
if (n.type === "ClassDeclaration") {
|
|
173
|
+
}
|
|
174
|
+
const body = n.body?.body ?? [];
|
|
175
|
+
for (const m of body) visit2(m, { owner: cname });
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
case "ClassMethod":
|
|
179
|
+
case "ClassPrivateMethod":
|
|
180
|
+
case "ObjectMethod": {
|
|
181
|
+
const key = keyNameOf(n.key);
|
|
182
|
+
const owner = ctx.owner;
|
|
183
|
+
const name = key ? owner ? `${owner}.${key}` : key : "<anonymous>";
|
|
184
|
+
pushNamedFn(name, node, node, void 0);
|
|
185
|
+
visit2(n.body, { owner: name });
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
case "ObjectProperty":
|
|
189
|
+
case "ClassProperty":
|
|
190
|
+
case "ClassPrivateProperty": {
|
|
191
|
+
const key = keyNameOf(n.key);
|
|
192
|
+
const val = n.value ?? n.init;
|
|
193
|
+
if (isFnExpr(val)) {
|
|
194
|
+
const owner = ctx.owner;
|
|
195
|
+
const name = key ? owner ? `${owner}.${key}` : key : "<anonymous>";
|
|
196
|
+
out.push(
|
|
197
|
+
buildScope(
|
|
198
|
+
name,
|
|
199
|
+
val,
|
|
200
|
+
void 0,
|
|
201
|
+
val.leadingComments ?? node.leadingComments,
|
|
202
|
+
void 0
|
|
203
|
+
)
|
|
204
|
+
);
|
|
205
|
+
visit2(val, { owner: name });
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
visit2(val, ctx);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
case "ReturnStatement":
|
|
212
|
+
case "ThrowStatement":
|
|
213
|
+
case "AwaitExpression":
|
|
214
|
+
case "SpreadElement":
|
|
215
|
+
case "RestElement":
|
|
216
|
+
visit2(n.argument, ctx);
|
|
217
|
+
return;
|
|
218
|
+
case "UnaryExpression":
|
|
219
|
+
case "UpdateExpression":
|
|
220
|
+
visit2(n.argument, ctx);
|
|
221
|
+
return;
|
|
222
|
+
case "BinaryExpression":
|
|
223
|
+
case "LogicalExpression":
|
|
224
|
+
case "AssignmentExpression":
|
|
225
|
+
visit2(n.left, ctx);
|
|
226
|
+
visit2(n.right, ctx);
|
|
227
|
+
return;
|
|
228
|
+
case "MemberExpression":
|
|
229
|
+
case "OptionalMemberExpression":
|
|
230
|
+
visit2(n.object, ctx);
|
|
231
|
+
visit2(n.property, ctx);
|
|
232
|
+
return;
|
|
233
|
+
case "CallExpression":
|
|
234
|
+
case "OptionalCallExpression":
|
|
235
|
+
case "NewExpression": {
|
|
236
|
+
visit2(n.callee, ctx);
|
|
237
|
+
for (const a of n.arguments ?? []) visit2(a, ctx);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
case "ConditionalExpression":
|
|
241
|
+
visit2(n.test, ctx);
|
|
242
|
+
visit2(n.consequent, ctx);
|
|
243
|
+
visit2(n.alternate, ctx);
|
|
244
|
+
return;
|
|
245
|
+
case "ArrayExpression": {
|
|
246
|
+
for (const e of n.elements ?? []) visit2(e, ctx);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
case "ObjectExpression": {
|
|
250
|
+
for (const p of n.properties ?? []) visit2(p, ctx);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
case "SequenceExpression": {
|
|
254
|
+
for (const e of n.expressions ?? []) visit2(e, ctx);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
case "ParenthesizedExpression":
|
|
258
|
+
visit2(n.expression, ctx);
|
|
259
|
+
return;
|
|
260
|
+
case "ExpressionStatement":
|
|
261
|
+
visit2(n.expression, ctx);
|
|
262
|
+
return;
|
|
263
|
+
case "BlockStatement": {
|
|
264
|
+
for (const s of n.body ?? []) visit2(s, ctx);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
case "IfStatement":
|
|
268
|
+
visit2(n.test, ctx);
|
|
269
|
+
visit2(n.consequent, ctx);
|
|
270
|
+
visit2(n.alternate, ctx);
|
|
271
|
+
return;
|
|
272
|
+
case "ForStatement":
|
|
273
|
+
visit2(n.init, ctx);
|
|
274
|
+
visit2(n.test, ctx);
|
|
275
|
+
visit2(n.update, ctx);
|
|
276
|
+
visit2(n.body, ctx);
|
|
277
|
+
return;
|
|
278
|
+
case "ForInStatement":
|
|
279
|
+
case "ForOfStatement":
|
|
280
|
+
visit2(n.left, ctx);
|
|
281
|
+
visit2(n.right, ctx);
|
|
282
|
+
visit2(n.body, ctx);
|
|
283
|
+
return;
|
|
284
|
+
case "WhileStatement":
|
|
285
|
+
case "DoWhileStatement":
|
|
286
|
+
visit2(n.test, ctx);
|
|
287
|
+
visit2(n.body, ctx);
|
|
288
|
+
return;
|
|
289
|
+
case "TryStatement":
|
|
290
|
+
visit2(n.block, ctx);
|
|
291
|
+
visit2(n.handler, ctx);
|
|
292
|
+
visit2(n.finalizer, ctx);
|
|
293
|
+
return;
|
|
294
|
+
case "CatchClause":
|
|
295
|
+
visit2(n.body, ctx);
|
|
296
|
+
return;
|
|
297
|
+
case "SwitchStatement":
|
|
298
|
+
visit2(n.discriminant, ctx);
|
|
299
|
+
for (const c of n.cases ?? []) visit2(c, ctx);
|
|
300
|
+
return;
|
|
301
|
+
case "SwitchCase":
|
|
302
|
+
visit2(n.test, ctx);
|
|
303
|
+
for (const s of n.consequent ?? []) visit2(s, ctx);
|
|
304
|
+
return;
|
|
305
|
+
case "LabeledStatement":
|
|
306
|
+
visit2(n.body, ctx);
|
|
307
|
+
return;
|
|
308
|
+
default: {
|
|
309
|
+
for (const key of [
|
|
310
|
+
"body",
|
|
311
|
+
"expression",
|
|
312
|
+
"init",
|
|
313
|
+
"value",
|
|
314
|
+
"argument",
|
|
315
|
+
"declaration",
|
|
316
|
+
"left",
|
|
317
|
+
"right",
|
|
318
|
+
"test",
|
|
319
|
+
"consequent",
|
|
320
|
+
"alternate",
|
|
321
|
+
"block",
|
|
322
|
+
"handler",
|
|
323
|
+
"finalizer",
|
|
324
|
+
"callee",
|
|
325
|
+
"object",
|
|
326
|
+
"property",
|
|
327
|
+
"argument"
|
|
328
|
+
]) {
|
|
329
|
+
const v2 = n[key];
|
|
330
|
+
if (Array.isArray(v2)) v2.forEach((x) => visit2(x, ctx));
|
|
331
|
+
else if (v2 && typeof v2 === "object" && "type" in v2) visit2(v2, ctx);
|
|
332
|
+
}
|
|
333
|
+
for (const key of ["declarations", "properties", "elements", "arguments", "cases", "params"]) {
|
|
334
|
+
const v2 = n[key];
|
|
335
|
+
if (Array.isArray(v2)) v2.forEach((x) => visit2(x, ctx));
|
|
336
|
+
}
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
const visitWithWrapper = (decl, wrapper, ctx) => {
|
|
342
|
+
const d = decl;
|
|
343
|
+
if (d.type === "FunctionDeclaration" || d.type === "FunctionExpression") {
|
|
344
|
+
const name = d.id?.name ?? "default";
|
|
345
|
+
pushNamedFn(name, decl, decl, wrapper);
|
|
346
|
+
visit2(d.body, { owner: name });
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (d.type === "ClassDeclaration" || d.type === "ClassExpression") {
|
|
350
|
+
const cname = d.id?.name ?? "<anonymous>";
|
|
351
|
+
const body = d.body?.body ?? [];
|
|
352
|
+
for (const m of body) visit2(m, { owner: cname });
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (d.type === "VariableDeclaration") {
|
|
356
|
+
for (const raw of d.declarations ?? []) {
|
|
357
|
+
const vd = raw;
|
|
358
|
+
const idName = vd.id?.type === "Identifier" ? vd.id.name : void 0;
|
|
359
|
+
if (isFnExpr(vd.init)) {
|
|
360
|
+
const fn2 = vd.init;
|
|
361
|
+
out.push(
|
|
362
|
+
buildScope(
|
|
363
|
+
idName ?? "default",
|
|
364
|
+
fn2,
|
|
365
|
+
wrapper,
|
|
366
|
+
d.leadingComments,
|
|
367
|
+
wrapper.leadingComments
|
|
368
|
+
)
|
|
369
|
+
);
|
|
370
|
+
visit2(fn2, { owner: idName });
|
|
371
|
+
} else {
|
|
372
|
+
visit2(vd.init, {
|
|
373
|
+
owner: idName,
|
|
374
|
+
commentHost: decl,
|
|
375
|
+
wrapperNode: wrapper
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (d.type === "ArrowFunctionExpression" || d.type === "FunctionExpression") {
|
|
382
|
+
const name = "default";
|
|
383
|
+
out.push(
|
|
384
|
+
buildScope(
|
|
385
|
+
name,
|
|
386
|
+
decl,
|
|
387
|
+
wrapper,
|
|
388
|
+
d.leadingComments,
|
|
389
|
+
wrapper.leadingComments
|
|
390
|
+
)
|
|
391
|
+
);
|
|
392
|
+
visit2(d.body, { owner: name });
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
visit2(decl, { ...ctx, wrapperNode: wrapper });
|
|
396
|
+
};
|
|
397
|
+
for (const stmt of file.program.body) {
|
|
398
|
+
visit2(stmt, {});
|
|
399
|
+
}
|
|
400
|
+
return out;
|
|
401
|
+
}
|
|
402
|
+
function findFnDirectiveScope(file, fnName) {
|
|
403
|
+
return listFnDirectiveScopes(file).find((s) => s.name === fnName);
|
|
404
|
+
}
|
|
405
|
+
function fnDirectiveCommentLines(source, fnName) {
|
|
406
|
+
let file;
|
|
407
|
+
try {
|
|
408
|
+
file = parseSource(source);
|
|
409
|
+
} catch {
|
|
410
|
+
return [];
|
|
411
|
+
}
|
|
412
|
+
return findFnDirectiveScope(file, fnName)?.commentLines ?? [];
|
|
413
|
+
}
|
|
414
|
+
var isPathLikeEnv = (s) => /^[^\s]+$/.test(s) && (s.startsWith("./") || s.startsWith("../") || s.startsWith("/") || /\.(ts|js|mjs|cjs|tsx|jsx)$/.test(s));
|
|
415
|
+
function parseEnvPayload(payload) {
|
|
416
|
+
return payload.split(",").map((e) => e.trim().replace(/^['"]|['"]$/g, "")).filter((e) => e && (/^\w+$/.test(e) || isPathLikeEnv(e)));
|
|
417
|
+
}
|
|
418
|
+
function parseMockModulePayload(payload) {
|
|
419
|
+
const partial2 = payload.match(/^\s*"([^"]+)"\s*\{([^}]+)\}\s*from\s*"([^"]+)"\s*$/);
|
|
420
|
+
if (partial2) {
|
|
421
|
+
const names = partial2[2].split(",").map((n) => n.trim()).filter(Boolean);
|
|
422
|
+
return { source: partial2[1], names, fromPath: partial2[3] };
|
|
423
|
+
}
|
|
424
|
+
const full = payload.match(/^\s*"([^"]+)"\s+from\s*"([^"]+)"\s*$/);
|
|
425
|
+
if (full) {
|
|
426
|
+
return { source: full[1], fromPath: full[2] };
|
|
427
|
+
}
|
|
428
|
+
return void 0;
|
|
429
|
+
}
|
|
430
|
+
function parseNudoImportPayload(payload) {
|
|
431
|
+
const s = payload.trim();
|
|
432
|
+
if (s === "") return void 0;
|
|
433
|
+
const named = s.match(/^\{([^}]+)\}\s*from\s*["']([^"']+)["']$/);
|
|
434
|
+
if (named) {
|
|
435
|
+
const names = named[1].split("\n").map((line) => line.replace(/^\s*\/\/\/?\s?/, "").trim()).join("\n").split(",").map((x) => x.trim()).filter(Boolean).map((x) => x.split(/\s+as\s+/).pop().trim());
|
|
436
|
+
return { names, spec: named[2] };
|
|
437
|
+
}
|
|
438
|
+
const ns = s.match(/^\*\s+as\s+(\w+)\s+from\s*["']([^"']+)["']$/);
|
|
439
|
+
if (ns) {
|
|
440
|
+
return { names: [`*${ns[1]}`], spec: ns[2] };
|
|
441
|
+
}
|
|
442
|
+
if (/^[\w$]+\s+from\b/.test(s) || /^[\w$]+\s*,/.test(s)) return "malformed-default";
|
|
443
|
+
return "malformed";
|
|
444
|
+
}
|
|
445
|
+
var LINE_COMMENT_PREFIX = "(?:^|[^/:])\\/\\/\\/?";
|
|
446
|
+
function fileDirectiveVisibleText(source) {
|
|
447
|
+
return stripStringsKeepComments(source).replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
448
|
+
}
|
|
449
|
+
function extractFileEnvNames(source) {
|
|
450
|
+
const text = fileDirectiveVisibleText(source);
|
|
451
|
+
const re = new RegExp(`${LINE_COMMENT_PREFIX}\\s*@nudo:env\\s+([^\\n*]+)`, "g");
|
|
452
|
+
const out = [];
|
|
453
|
+
let m;
|
|
454
|
+
while (m = re.exec(text)) {
|
|
455
|
+
out.push(...parseEnvPayload(m[1]));
|
|
456
|
+
}
|
|
457
|
+
return out;
|
|
458
|
+
}
|
|
459
|
+
function extractMockModuleRecords(source) {
|
|
460
|
+
const text = fileDirectiveVisibleText(source);
|
|
461
|
+
const re = new RegExp(
|
|
462
|
+
`${LINE_COMMENT_PREFIX}\\s*@nudo:mock-module\\s+([^\\n]+)`,
|
|
463
|
+
"g"
|
|
464
|
+
);
|
|
465
|
+
const out = [];
|
|
466
|
+
let m;
|
|
467
|
+
while (m = re.exec(text)) {
|
|
468
|
+
const rec = parseMockModulePayload(m[1]);
|
|
469
|
+
if (rec) out.push(rec);
|
|
470
|
+
}
|
|
471
|
+
return out;
|
|
472
|
+
}
|
|
473
|
+
function directiveCommentLinesRaw(source) {
|
|
474
|
+
const src = stripStringsKeepComments(source);
|
|
475
|
+
const out = [];
|
|
476
|
+
const lineRe = /(?:^|[^/:])\/\/\/?([^\n]*)/g;
|
|
477
|
+
let m;
|
|
478
|
+
while (m = lineRe.exec(src)) {
|
|
479
|
+
out.push(cleanDirectiveLine(m[1], "line"));
|
|
480
|
+
}
|
|
481
|
+
const blockRe = /\/\*([\s\S]*?)\*\//g;
|
|
482
|
+
while (m = blockRe.exec(src)) {
|
|
483
|
+
out.push(...commentTextToLines(m[1], "block"));
|
|
484
|
+
}
|
|
485
|
+
return out;
|
|
486
|
+
}
|
|
487
|
+
function nudoImportPayloads(source) {
|
|
488
|
+
const lines = directiveCommentLinesRaw(source);
|
|
489
|
+
const out = [];
|
|
490
|
+
for (let i = 0; i < lines.length; i++) {
|
|
491
|
+
const line = lines[i];
|
|
492
|
+
if (!/^@nudo:import\s/.test(line)) continue;
|
|
493
|
+
let payload = line.replace(/^@nudo:import\s+/, "");
|
|
494
|
+
if (payload.startsWith("{")) {
|
|
495
|
+
let depth = 0;
|
|
496
|
+
for (const ch of payload) {
|
|
497
|
+
if (ch === "{") depth++;
|
|
498
|
+
else if (ch === "}") depth--;
|
|
499
|
+
}
|
|
500
|
+
while (depth > 0 && i + 1 < lines.length) {
|
|
501
|
+
i += 1;
|
|
502
|
+
const cont = lines[i];
|
|
503
|
+
if (/^@nudo:/.test(cont)) {
|
|
504
|
+
i -= 1;
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
payload += `
|
|
508
|
+
${cont}`;
|
|
509
|
+
for (const ch of cont) {
|
|
510
|
+
if (ch === "{") depth++;
|
|
511
|
+
else if (ch === "}") depth--;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
out.push(payload);
|
|
516
|
+
}
|
|
517
|
+
return out;
|
|
518
|
+
}
|
|
519
|
+
function extractNudoImportRecords(source) {
|
|
520
|
+
const out = [];
|
|
521
|
+
for (const payload of nudoImportPayloads(source)) {
|
|
522
|
+
const rec = parseNudoImportPayload(payload);
|
|
523
|
+
if (rec && rec !== "malformed" && rec !== "malformed-default") out.push(rec);
|
|
524
|
+
}
|
|
525
|
+
return out;
|
|
526
|
+
}
|
|
527
|
+
function scanMalformedNudoImports(source) {
|
|
528
|
+
const out = [];
|
|
529
|
+
for (const payload of nudoImportPayloads(source)) {
|
|
530
|
+
const rec = parseNudoImportPayload(payload);
|
|
531
|
+
if (rec === "malformed-default" || rec === "malformed") out.push(rec);
|
|
532
|
+
}
|
|
533
|
+
return out;
|
|
534
|
+
}
|
|
535
|
+
function extractBalancedParens(text, startIdx) {
|
|
536
|
+
if (text[startIdx] !== "(") return null;
|
|
537
|
+
let depth = 0;
|
|
538
|
+
let inString = null;
|
|
539
|
+
for (let i = startIdx; i < text.length; i++) {
|
|
540
|
+
const ch = text[i];
|
|
541
|
+
if (inString) {
|
|
542
|
+
if (ch === inString) inString = null;
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
if (ch === '"' || ch === "'") {
|
|
546
|
+
inString = ch;
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
if (ch === "(") depth++;
|
|
550
|
+
if (ch === ")") depth--;
|
|
551
|
+
if (depth === 0) return text.slice(startIdx + 1, i);
|
|
552
|
+
}
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
var CASE_NAME_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*|\/\/\/?\s*)?@nudo:case\s+"([^"]+)"\s*\(/g;
|
|
556
|
+
function extractCaseExpectedExpr(text, afterParen) {
|
|
557
|
+
const lines = text.slice(afterParen).split("\n");
|
|
558
|
+
for (let i = 0; i < lines.length; i++) {
|
|
559
|
+
let line = lines[i];
|
|
560
|
+
if (i > 0) line = line.replace(/^\s*\*\s?/, "").replace(/^\s*\/\/\/?\s?/, "");
|
|
561
|
+
if (/^\s*@nudo:/.test(line)) return void 0;
|
|
562
|
+
const arrow = line.match(/^\s*=>\s*(\S.*)$/);
|
|
563
|
+
if (arrow) return arrow[1].trim();
|
|
564
|
+
if (line.trim() !== "") return void 0;
|
|
565
|
+
}
|
|
566
|
+
return void 0;
|
|
567
|
+
}
|
|
568
|
+
function scanCaseTags(text) {
|
|
569
|
+
const caseArgSpans = [];
|
|
570
|
+
const out = [];
|
|
571
|
+
CASE_NAME_REGEX.lastIndex = 0;
|
|
572
|
+
let m;
|
|
573
|
+
while ((m = CASE_NAME_REGEX.exec(text)) !== null) {
|
|
574
|
+
const parenStart = m.index + m[0].length - 1;
|
|
575
|
+
const argsStr = extractBalancedParens(text, parenStart);
|
|
576
|
+
if (argsStr === null) continue;
|
|
577
|
+
const afterParen = parenStart + argsStr.length + 2;
|
|
578
|
+
if (caseArgSpans.some(([s, e]) => m.index >= s && m.index < e)) continue;
|
|
579
|
+
caseArgSpans.push([m.index, afterParen]);
|
|
580
|
+
const tagOffset = m.index + m[0].indexOf("@nudo:case");
|
|
581
|
+
const expectedText = extractCaseExpectedExpr(text, afterParen);
|
|
582
|
+
out.push({ name: m[1], argsText: argsStr, expectedText, tagOffset });
|
|
583
|
+
}
|
|
584
|
+
return out;
|
|
585
|
+
}
|
|
586
|
+
function scanCaseArgSpans(text) {
|
|
587
|
+
const spans = [];
|
|
588
|
+
CASE_NAME_REGEX.lastIndex = 0;
|
|
589
|
+
let m;
|
|
590
|
+
while ((m = CASE_NAME_REGEX.exec(text)) !== null) {
|
|
591
|
+
const parenStart = m.index + m[0].length - 1;
|
|
592
|
+
const argsStr = extractBalancedParens(text, parenStart);
|
|
593
|
+
if (argsStr === null) continue;
|
|
594
|
+
const afterParen = parenStart + argsStr.length + 2;
|
|
595
|
+
if (spans.some(([s, e]) => m.index >= s && m.index < e)) continue;
|
|
596
|
+
spans.push([m.index, afterParen]);
|
|
597
|
+
}
|
|
598
|
+
return spans;
|
|
599
|
+
}
|
|
600
|
+
function scanContractSegments(lines) {
|
|
601
|
+
const reqs = [];
|
|
602
|
+
for (const line of lines) {
|
|
603
|
+
const m = line.match(/^@nudo:contract\s+(.+)$/);
|
|
604
|
+
if (m) reqs.push(m[1].trim().replace(/\*\/$/, "").trim());
|
|
605
|
+
}
|
|
606
|
+
return reqs;
|
|
607
|
+
}
|
|
608
|
+
function scanThrowsDecl(lines) {
|
|
609
|
+
const kinds = /* @__PURE__ */ new Set();
|
|
610
|
+
let any2 = false;
|
|
611
|
+
for (const line of lines) {
|
|
612
|
+
const th = line.match(/^@nudo:throws\s+(.+)$/i);
|
|
613
|
+
if (th) {
|
|
614
|
+
const spec = th[1].trim();
|
|
615
|
+
if (spec === "*") any2 = true;
|
|
616
|
+
else {
|
|
617
|
+
for (const k of spec.split(/[,\s|]+/).map((s) => s.trim()).filter(Boolean)) {
|
|
618
|
+
if (k === "*") any2 = true;
|
|
619
|
+
else kinds.add(k);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
const cs = line.match(/!!\s*throws(?:\s+([A-Za-z*][\w*|,\s]*))?/i);
|
|
624
|
+
if (cs) {
|
|
625
|
+
const spec = (cs[1] ?? "*").trim();
|
|
626
|
+
if (spec === "" || spec === "*") any2 = true;
|
|
627
|
+
else {
|
|
628
|
+
for (const k of spec.split(/[,\s|]+/).map((s) => s.trim()).filter(Boolean)) {
|
|
629
|
+
if (k === "*") any2 = true;
|
|
630
|
+
else kinds.add(k);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
if (any2) return "*";
|
|
636
|
+
if (kinds.size > 0) return [...kinds].join(",");
|
|
637
|
+
return void 0;
|
|
638
|
+
}
|
|
639
|
+
function scanBudgetDecl(lines) {
|
|
640
|
+
const out = {};
|
|
641
|
+
for (const line of lines) {
|
|
642
|
+
const bd = line.match(/^@nudo:budget\s+(.+)$/i);
|
|
643
|
+
if (!bd) continue;
|
|
644
|
+
for (const part of bd[1].split(/[,\s|]+/).map((s) => s.trim()).filter(Boolean)) {
|
|
645
|
+
const kv = part.match(/^(forks|calls|depth)\s*=\s*(\d+)$/i);
|
|
646
|
+
if (kv) {
|
|
647
|
+
const num = Number(kv[2]);
|
|
648
|
+
if (Number.isFinite(num) && num >= 1) {
|
|
649
|
+
out[kv[1].toLowerCase()] = Math.floor(num);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
655
|
+
}
|
|
49
656
|
|
|
50
657
|
// src/algebra/hash-source.ts
|
|
51
658
|
var lastHashSource;
|
|
@@ -73,6 +680,34 @@ function resetHashSourceCache() {
|
|
|
73
680
|
function normPath(p) {
|
|
74
681
|
return p.replace(/\\/g, "/");
|
|
75
682
|
}
|
|
683
|
+
function stablePathKey(p) {
|
|
684
|
+
let fp = p;
|
|
685
|
+
if (fp.startsWith("file://")) {
|
|
686
|
+
fp = fp.slice("file://".length);
|
|
687
|
+
try {
|
|
688
|
+
fp = decodeURIComponent(fp);
|
|
689
|
+
} catch {
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
fp = normPath(fp);
|
|
693
|
+
const drive = /^\/([A-Za-z]:)(.*)$/.exec(fp);
|
|
694
|
+
if (drive) fp = `${drive[1]}${drive[2]}`;
|
|
695
|
+
if (/^[A-Za-z]:\//.test(fp)) return fp[0].toLowerCase() + fp.slice(1);
|
|
696
|
+
return fp;
|
|
697
|
+
}
|
|
698
|
+
function stablePathKeyGraph(m) {
|
|
699
|
+
const out = /* @__PURE__ */ new Map();
|
|
700
|
+
for (const [k, vs] of m) {
|
|
701
|
+
const key = stablePathKey(k);
|
|
702
|
+
let set = out.get(key);
|
|
703
|
+
if (!set) {
|
|
704
|
+
set = /* @__PURE__ */ new Set();
|
|
705
|
+
out.set(key, set);
|
|
706
|
+
}
|
|
707
|
+
for (const v2 of vs) set.add(stablePathKey(v2));
|
|
708
|
+
}
|
|
709
|
+
return out;
|
|
710
|
+
}
|
|
76
711
|
function resolveDepPath(fromFile, spec) {
|
|
77
712
|
const s = normPath(spec);
|
|
78
713
|
if (!s.startsWith(".")) return s;
|
|
@@ -341,6 +976,28 @@ function omit(c, keys) {
|
|
|
341
976
|
}
|
|
342
977
|
return makeBuilder(c.prim, c.preds, { fields });
|
|
343
978
|
}
|
|
979
|
+
var CONSTRAINT_BUILDERS = {
|
|
980
|
+
number,
|
|
981
|
+
string,
|
|
982
|
+
boolean,
|
|
983
|
+
any,
|
|
984
|
+
array,
|
|
985
|
+
shape,
|
|
986
|
+
lit: litC,
|
|
987
|
+
union,
|
|
988
|
+
nullable,
|
|
989
|
+
fn,
|
|
990
|
+
and: andC,
|
|
991
|
+
partial,
|
|
992
|
+
pick,
|
|
993
|
+
omit
|
|
994
|
+
};
|
|
995
|
+
var CONSTRAINT_BUILDER_NAMES = Object.keys(
|
|
996
|
+
CONSTRAINT_BUILDERS
|
|
997
|
+
);
|
|
998
|
+
var CONSTRAINT_EXPR_RE = new RegExp(
|
|
999
|
+
`^(${CONSTRAINT_BUILDER_NAMES.join("|")})\\s*\\(`
|
|
1000
|
+
);
|
|
344
1001
|
function substTerm(t, paramName) {
|
|
345
1002
|
if (t.op === "var" && t.id === SELF) return v(paramName);
|
|
346
1003
|
if (t.op === "app" && t.fn === "get" && t.args.length === 2) {
|
|
@@ -593,15 +1250,21 @@ function throwConstraintToKinds(c) {
|
|
|
593
1250
|
// src/algebra/refine.ts
|
|
594
1251
|
function extractNudoImports(source) {
|
|
595
1252
|
const out = [];
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
while (m = named.exec(source)) {
|
|
599
|
-
const names = m[1].split(",").map((s) => s.trim()).filter(Boolean).map((s) => s.split(/\s+as\s+/).pop().trim());
|
|
600
|
-
out.push({ names, spec: m[2] });
|
|
1253
|
+
for (const rec of extractNudoImportRecords(source)) {
|
|
1254
|
+
out.push({ names: rec.names, spec: rec.spec });
|
|
601
1255
|
}
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
1256
|
+
for (const bad of scanMalformedNudoImports(source)) {
|
|
1257
|
+
if (bad === "malformed-default") {
|
|
1258
|
+
collectDiag({
|
|
1259
|
+
code: "nudo:contract-syntax",
|
|
1260
|
+
message: `Default @nudo:import is not supported (use named { a, b } or namespace * as ns)`
|
|
1261
|
+
});
|
|
1262
|
+
} else {
|
|
1263
|
+
collectDiag({
|
|
1264
|
+
code: "nudo:contract-syntax",
|
|
1265
|
+
message: `Unrecognized @nudo:import form (expected { names } from "path" or * as ns from "path")`
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
605
1268
|
}
|
|
606
1269
|
return out;
|
|
607
1270
|
}
|
|
@@ -643,21 +1306,7 @@ var NudoSidecarError = class extends Error {
|
|
|
643
1306
|
this.code = code;
|
|
644
1307
|
}
|
|
645
1308
|
};
|
|
646
|
-
var sidecarInjects =
|
|
647
|
-
number,
|
|
648
|
-
string,
|
|
649
|
-
boolean,
|
|
650
|
-
shape,
|
|
651
|
-
array,
|
|
652
|
-
lit: litC,
|
|
653
|
-
union,
|
|
654
|
-
fn,
|
|
655
|
-
and: andC,
|
|
656
|
-
partial,
|
|
657
|
-
pick,
|
|
658
|
-
omit,
|
|
659
|
-
nullable
|
|
660
|
-
};
|
|
1309
|
+
var sidecarInjects = CONSTRAINT_BUILDERS;
|
|
661
1310
|
function isSidecarSpec(spec) {
|
|
662
1311
|
return spec.endsWith(".nudo.js") || spec.endsWith(".nudo.ts");
|
|
663
1312
|
}
|
|
@@ -1024,7 +1673,12 @@ function sidecarDepClosure(src, fromFile, loadModule, out) {
|
|
|
1024
1673
|
if (!isSidecarSpec(spec)) continue;
|
|
1025
1674
|
const canonical = resolveDepPath(fromFile, spec);
|
|
1026
1675
|
if (out.has(canonical)) continue;
|
|
1027
|
-
|
|
1676
|
+
let depSrc;
|
|
1677
|
+
try {
|
|
1678
|
+
depSrc = loadModule(spec, fromFile);
|
|
1679
|
+
} catch {
|
|
1680
|
+
depSrc = void 0;
|
|
1681
|
+
}
|
|
1028
1682
|
out.set(canonical, depSrc);
|
|
1029
1683
|
if (depSrc === void 0) continue;
|
|
1030
1684
|
const r = sidecarDepClosure(depSrc, canonical, loadModule, out);
|
|
@@ -1094,7 +1748,17 @@ function collectConstraints(source, opts) {
|
|
|
1094
1748
|
});
|
|
1095
1749
|
continue;
|
|
1096
1750
|
}
|
|
1097
|
-
|
|
1751
|
+
let src;
|
|
1752
|
+
try {
|
|
1753
|
+
src = opts.loadModule(imp.spec, opts.fromFile);
|
|
1754
|
+
} catch (e) {
|
|
1755
|
+
collectDiag({
|
|
1756
|
+
code: "nudo:interface-load",
|
|
1757
|
+
message: `sidecar '${imp.spec}' failed to load from '${opts.fromFile}': ${e instanceof Error ? e.message : String(e)}`,
|
|
1758
|
+
file: opts.fromFile
|
|
1759
|
+
});
|
|
1760
|
+
continue;
|
|
1761
|
+
}
|
|
1098
1762
|
if (src === void 0) {
|
|
1099
1763
|
collectDiag({
|
|
1100
1764
|
code: "nudo:interface-load",
|
|
@@ -1150,26 +1814,8 @@ function collectConstraints(source, opts) {
|
|
|
1150
1814
|
return map;
|
|
1151
1815
|
}
|
|
1152
1816
|
function extractRefineLines(source, fnName) {
|
|
1153
|
-
const
|
|
1154
|
-
|
|
1155
|
-
`(?:export\\s+(?:default\\s+)?)?(?:async\\s+)?(?:function\\s+${escaped}|const\\s+${escaped}\\s*=)`
|
|
1156
|
-
);
|
|
1157
|
-
const m = source.match(fnRe);
|
|
1158
|
-
if (!m || m.index === void 0) return [];
|
|
1159
|
-
const before = source.slice(0, m.index);
|
|
1160
|
-
const lines = before.split("\n");
|
|
1161
|
-
const reqs = [];
|
|
1162
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1163
|
-
const line = lines[i].trim();
|
|
1164
|
-
if (line === "" || line === "*/") continue;
|
|
1165
|
-
if (line.startsWith("*") || line.startsWith("/*") || line.startsWith("//")) {
|
|
1166
|
-
const rm = line.match(/@nudo:contract\s+(.+)$/);
|
|
1167
|
-
if (rm) reqs.unshift(rm[1].trim().replace(/\*\/$/, "").trim());
|
|
1168
|
-
continue;
|
|
1169
|
-
}
|
|
1170
|
-
break;
|
|
1171
|
-
}
|
|
1172
|
-
return reqs;
|
|
1817
|
+
const lines = fnDirectiveCommentLines(source, fnName);
|
|
1818
|
+
return scanContractSegments(lines);
|
|
1173
1819
|
}
|
|
1174
1820
|
function extractRefinesFromSource(source, fnName, opts = {}) {
|
|
1175
1821
|
if (!stripStringsKeepComments(source).includes("@nudo:contract")) return [];
|
|
@@ -1179,7 +1825,14 @@ function extractRefinesFromSource(source, fnName, opts = {}) {
|
|
|
1179
1825
|
const parts = line.split(/&&|,/).map((s) => s.trim()).filter(Boolean);
|
|
1180
1826
|
for (const part of parts) {
|
|
1181
1827
|
const m = part.match(/^(\w+)\s+([\w.]+)$/);
|
|
1182
|
-
if (!m)
|
|
1828
|
+
if (!m) {
|
|
1829
|
+
if (/^return\b/.test(part)) continue;
|
|
1830
|
+
collectDiag({
|
|
1831
|
+
code: "nudo:contract-syntax",
|
|
1832
|
+
message: `Malformed @nudo:contract segment '${part}' (expected <param> <constraintName> or return <constraintName>; e.g. @nudo:contract x positive)`
|
|
1833
|
+
});
|
|
1834
|
+
continue;
|
|
1835
|
+
}
|
|
1183
1836
|
const [, param, cName] = m;
|
|
1184
1837
|
if (param === "return") continue;
|
|
1185
1838
|
const c = constraints.get(cName);
|
|
@@ -1209,7 +1862,15 @@ function extractRefineReturnFromSource(source, fnName, opts = {}) {
|
|
|
1209
1862
|
const parts = line.split(/&&|,/).map((s) => s.trim()).filter(Boolean);
|
|
1210
1863
|
for (const part of parts) {
|
|
1211
1864
|
const m = part.match(/^return\s+([\w.]+)$/);
|
|
1212
|
-
if (!m)
|
|
1865
|
+
if (!m) {
|
|
1866
|
+
if (/^return\b/.test(part) && !/^return\s+[\w.]+$/.test(part)) {
|
|
1867
|
+
collectDiag({
|
|
1868
|
+
code: "nudo:contract-syntax",
|
|
1869
|
+
message: `Malformed @nudo:contract return segment '${part}' (expected return <constraintName>)`
|
|
1870
|
+
});
|
|
1871
|
+
}
|
|
1872
|
+
continue;
|
|
1873
|
+
}
|
|
1213
1874
|
const cName = m[1];
|
|
1214
1875
|
const c = constraints.get(cName);
|
|
1215
1876
|
if (!c) continue;
|
|
@@ -1223,98 +1884,17 @@ function extractDeclaredThrows(source, fnName) {
|
|
|
1223
1884
|
if (!directiveSrc.includes("@nudo:throws") && !directiveSrc.includes("!! throws")) {
|
|
1224
1885
|
return void 0;
|
|
1225
1886
|
}
|
|
1226
|
-
const
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
);
|
|
1232
|
-
const m = source.match(fnRe);
|
|
1233
|
-
if (!m || m.index === void 0) return void 0;
|
|
1234
|
-
const before = source.slice(0, m.index);
|
|
1235
|
-
const lines = before.split("\n");
|
|
1236
|
-
let seenComment = false;
|
|
1237
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1238
|
-
const line = lines[i].trim();
|
|
1239
|
-
if (line === "") {
|
|
1240
|
-
if (seenComment) break;
|
|
1241
|
-
continue;
|
|
1242
|
-
}
|
|
1243
|
-
if (line === "*/") continue;
|
|
1244
|
-
if (line.startsWith("*") || line.startsWith("/*") || line.startsWith("//")) {
|
|
1245
|
-
seenComment = true;
|
|
1246
|
-
const body = line.replace(/^[*/\s]+/, "").replace(/\*\/$/, "").trim();
|
|
1247
|
-
const th = body.match(/@nudo:throws\s+(.+)$/i);
|
|
1248
|
-
if (th) {
|
|
1249
|
-
const spec = th[1].trim();
|
|
1250
|
-
if (spec === "*") any2 = true;
|
|
1251
|
-
else {
|
|
1252
|
-
for (const k of spec.split(/[,\s|]+/).map((s) => s.trim()).filter(Boolean)) {
|
|
1253
|
-
if (k === "*") any2 = true;
|
|
1254
|
-
else kinds.add(k);
|
|
1255
|
-
}
|
|
1256
|
-
}
|
|
1257
|
-
}
|
|
1258
|
-
const cs = body.match(/!!\s*throws(?:\s+([A-Za-z*][\w*|,\s]*))?/i);
|
|
1259
|
-
if (cs) {
|
|
1260
|
-
const spec = (cs[1] ?? "*").trim();
|
|
1261
|
-
if (spec === "" || spec === "*") any2 = true;
|
|
1262
|
-
else {
|
|
1263
|
-
for (const k of spec.split(/[,\s|]+/).map((s) => s.trim()).filter(Boolean)) {
|
|
1264
|
-
if (k === "*") any2 = true;
|
|
1265
|
-
else kinds.add(k);
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
continue;
|
|
1270
|
-
}
|
|
1271
|
-
break;
|
|
1272
|
-
}
|
|
1273
|
-
if (any2) return "*";
|
|
1274
|
-
if (kinds.size > 0) return [...kinds];
|
|
1275
|
-
return void 0;
|
|
1887
|
+
const lines = fnDirectiveCommentLines(source, fnName);
|
|
1888
|
+
const spec = scanThrowsDecl(lines);
|
|
1889
|
+
if (spec === void 0) return void 0;
|
|
1890
|
+
if (spec === "*") return "*";
|
|
1891
|
+
return spec.split(",").filter(Boolean);
|
|
1276
1892
|
}
|
|
1277
1893
|
function extractFnBudget(source, fnName) {
|
|
1278
1894
|
const directiveSrc = stripStringsKeepComments(source);
|
|
1279
1895
|
if (!directiveSrc.includes("@nudo:budget")) return void 0;
|
|
1280
|
-
const
|
|
1281
|
-
|
|
1282
|
-
`(?:export\\s+(?:default\\s+)?)?(?:async\\s+)?(?:function\\s+${escaped}|const\\s+${escaped}\\s*=)`
|
|
1283
|
-
);
|
|
1284
|
-
const m = source.match(fnRe);
|
|
1285
|
-
if (!m || m.index === void 0) return void 0;
|
|
1286
|
-
const before = source.slice(0, m.index);
|
|
1287
|
-
const lines = before.split("\n");
|
|
1288
|
-
let seenComment = false;
|
|
1289
|
-
const out = {};
|
|
1290
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1291
|
-
const line = lines[i].trim();
|
|
1292
|
-
if (line === "") {
|
|
1293
|
-
if (seenComment) break;
|
|
1294
|
-
continue;
|
|
1295
|
-
}
|
|
1296
|
-
if (line === "*/") continue;
|
|
1297
|
-
if (line.startsWith("*") || line.startsWith("/*") || line.startsWith("//")) {
|
|
1298
|
-
seenComment = true;
|
|
1299
|
-
const body = line.replace(/^[*/\s]+/, "").replace(/\*\/$/, "").trim();
|
|
1300
|
-
const bd = body.match(/@nudo:budget\s+(.+)$/i);
|
|
1301
|
-
if (bd) {
|
|
1302
|
-
for (const part of bd[1].split(/[,\s|]+/).map((s) => s.trim()).filter(Boolean)) {
|
|
1303
|
-
const kv = part.match(/^(forks|calls|depth)\s*=\s*(\d+)$/i);
|
|
1304
|
-
if (kv) {
|
|
1305
|
-
const n = Number(kv[2]);
|
|
1306
|
-
if (Number.isFinite(n) && n >= 1) {
|
|
1307
|
-
const key = kv[1].toLowerCase();
|
|
1308
|
-
out[key] = Math.floor(n);
|
|
1309
|
-
}
|
|
1310
|
-
}
|
|
1311
|
-
}
|
|
1312
|
-
}
|
|
1313
|
-
continue;
|
|
1314
|
-
}
|
|
1315
|
-
break;
|
|
1316
|
-
}
|
|
1317
|
-
return Object.keys(out).length > 0 ? out : void 0;
|
|
1896
|
+
const lines = fnDirectiveCommentLines(source, fnName);
|
|
1897
|
+
return scanBudgetDecl(lines);
|
|
1318
1898
|
}
|
|
1319
1899
|
|
|
1320
1900
|
// src/algebra/interface.ts
|
|
@@ -1631,7 +2211,20 @@ function loadSidecarBinding(source, fnName, opts) {
|
|
|
1631
2211
|
if (!sidecarAutoBindAllowed(sidecarPath, autoBind, projectDir)) return { ok: false };
|
|
1632
2212
|
if (!localNamedExports(source).has(fnName)) return { ok: false };
|
|
1633
2213
|
const spec = `./${sidecarPath.slice(sidecarPath.lastIndexOf("/") + 1)}`;
|
|
1634
|
-
|
|
2214
|
+
let sidecarSrc;
|
|
2215
|
+
try {
|
|
2216
|
+
sidecarSrc = loadModule(spec, fromFile);
|
|
2217
|
+
} catch (e) {
|
|
2218
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
2219
|
+
if (noteSidecarLoadFailure(sidecarPath, reason)) {
|
|
2220
|
+
collectDiag2({
|
|
2221
|
+
code: "nudo:interface-load",
|
|
2222
|
+
message: `sidecar '${spec}' failed to load: ${reason}`,
|
|
2223
|
+
file: sidecarPath
|
|
2224
|
+
});
|
|
2225
|
+
}
|
|
2226
|
+
return { ok: false };
|
|
2227
|
+
}
|
|
1635
2228
|
if (sidecarSrc === void 0) return { ok: false };
|
|
1636
2229
|
if (sidecarSrc === source) return { ok: false };
|
|
1637
2230
|
const scNames = sidecarExportNames(sidecarSrc);
|
|
@@ -1916,7 +2509,12 @@ function sidecarClosureFingerprint(fromFile, opts) {
|
|
|
1916
2509
|
const sidecarPath = sidecarPathOf(fromFile);
|
|
1917
2510
|
if (!sidecarAutoBindAllowed(sidecarPath, autoBind, projectDir)) return void 0;
|
|
1918
2511
|
const spec = `./${sidecarPath.slice(sidecarPath.lastIndexOf("/") + 1)}`;
|
|
1919
|
-
|
|
2512
|
+
let sidecarSrc;
|
|
2513
|
+
try {
|
|
2514
|
+
sidecarSrc = loadModule(spec, fromFile);
|
|
2515
|
+
} catch {
|
|
2516
|
+
return void 0;
|
|
2517
|
+
}
|
|
1920
2518
|
if (sidecarSrc === void 0) return void 0;
|
|
1921
2519
|
const parts = [];
|
|
1922
2520
|
const seen = /* @__PURE__ */ new Set([sidecarPath]);
|
|
@@ -1930,7 +2528,12 @@ function sidecarClosureFingerprint(fromFile, opts) {
|
|
|
1930
2528
|
}
|
|
1931
2529
|
const { spec: s, from } = queue.shift();
|
|
1932
2530
|
const depPath = resolveDepPath(from, s);
|
|
1933
|
-
|
|
2531
|
+
let depSrc;
|
|
2532
|
+
try {
|
|
2533
|
+
depSrc = loadModule(s, from);
|
|
2534
|
+
} catch {
|
|
2535
|
+
depSrc = void 0;
|
|
2536
|
+
}
|
|
1934
2537
|
parts.push(`${depPath}=${depSrc === void 0 ? "miss" : hashSource(depSrc)}`);
|
|
1935
2538
|
if (depSrc !== void 0) enqueueSidecarDeps(depSrc, depPath, seen, queue);
|
|
1936
2539
|
}
|
|
@@ -2229,17 +2832,12 @@ function extractAllLoadSpecs(source) {
|
|
|
2229
2832
|
const specs = /* @__PURE__ */ new Set();
|
|
2230
2833
|
for (const imp of extractNudoImports(source)) specs.add(imp.spec);
|
|
2231
2834
|
const isPathLikeSpec = (spec) => spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/") || /\.(ts|js|mjs|cjs|tsx|jsx)$/.test(spec);
|
|
2232
|
-
const
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
if (spec && isPathLikeSpec(spec)) specs.add(spec);
|
|
2238
|
-
}
|
|
2835
|
+
for (const name of extractFileEnvNames(source)) {
|
|
2836
|
+
if (isPathLikeSpec(name)) specs.add(name);
|
|
2837
|
+
}
|
|
2838
|
+
for (const rec of extractMockModuleRecords(source)) {
|
|
2839
|
+
if (rec.fromPath) specs.add(rec.fromPath);
|
|
2239
2840
|
}
|
|
2240
|
-
const mockFromRe = /@nudo:mock-module\s+"[^"]+"\s+(?:\{[^}]*\}\s+)?from\s+["']([^"']+)["']/g;
|
|
2241
|
-
let mockM;
|
|
2242
|
-
while (mockM = mockFromRe.exec(source)) specs.add(mockM[1]);
|
|
2243
2841
|
const patterns = [
|
|
2244
2842
|
/\bfrom\s*['"]([^'"]+)['"]/g,
|
|
2245
2843
|
/\brequire\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
@@ -2256,6 +2854,13 @@ function sidecarSpecsOf(source) {
|
|
|
2256
2854
|
(spec) => (spec.startsWith(".") || spec.startsWith("/")) && (spec.endsWith(".nudo.js") || spec.endsWith(".nudo.ts"))
|
|
2257
2855
|
);
|
|
2258
2856
|
}
|
|
2857
|
+
function loadOrMiss(loadModule, spec, from) {
|
|
2858
|
+
try {
|
|
2859
|
+
return loadModule(spec, from);
|
|
2860
|
+
} catch {
|
|
2861
|
+
return void 0;
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2259
2864
|
function loadModuleDepsFingerprint(source, loadModule, fromFile) {
|
|
2260
2865
|
const parts = [];
|
|
2261
2866
|
const paths = [];
|
|
@@ -2274,7 +2879,7 @@ function loadModuleDepsFingerprint(source, loadModule, fromFile) {
|
|
|
2274
2879
|
const sidecarPath = sidecarPathOf(entryFile);
|
|
2275
2880
|
if (seen.has(sidecarPath)) return;
|
|
2276
2881
|
const sidecarSpec = `./${sidecarPath.slice(sidecarPath.lastIndexOf("/") + 1)}`;
|
|
2277
|
-
const sidecarSrc = loadModule
|
|
2882
|
+
const sidecarSrc = loadOrMiss(loadModule, sidecarSpec, entryFile);
|
|
2278
2883
|
if (sidecarSrc === void 0) return;
|
|
2279
2884
|
if (n >= MAX_LOAD_DEP_NODES) {
|
|
2280
2885
|
truncated = true;
|
|
@@ -2298,7 +2903,7 @@ function loadModuleDepsFingerprint(source, loadModule, fromFile) {
|
|
|
2298
2903
|
if (seen.has(path)) continue;
|
|
2299
2904
|
seen.add(path);
|
|
2300
2905
|
n++;
|
|
2301
|
-
const src = loadModule
|
|
2906
|
+
const src = loadOrMiss(loadModule, cur.spec, cur.from);
|
|
2302
2907
|
parts.push(`sidecar:${path}=${src === void 0 ? "miss" : hashSource(src)}`);
|
|
2303
2908
|
paths.push(path);
|
|
2304
2909
|
contents.push({ path, content: src ?? null });
|
|
@@ -2319,7 +2924,7 @@ function loadModuleDepsFingerprint(source, loadModule, fromFile) {
|
|
|
2319
2924
|
if (seen.has(path)) continue;
|
|
2320
2925
|
seen.add(path);
|
|
2321
2926
|
n++;
|
|
2322
|
-
const src = loadModule
|
|
2927
|
+
const src = loadOrMiss(loadModule, cur.spec, cur.from);
|
|
2323
2928
|
parts.push(`${path}=${src === void 0 ? "miss" : hashSource(src)}`);
|
|
2324
2929
|
paths.push(path);
|
|
2325
2930
|
contents.push({ path, content: src ?? null });
|
|
@@ -2929,7 +3534,7 @@ function unindexMemoKey(key) {
|
|
|
2929
3534
|
function evictGeneralizeMemoForPaths(paths) {
|
|
2930
3535
|
let n = 0;
|
|
2931
3536
|
for (const raw of paths) {
|
|
2932
|
-
const p =
|
|
3537
|
+
const p = stablePathKey(raw);
|
|
2933
3538
|
const keys = memoDepIndex.get(p);
|
|
2934
3539
|
if (!keys) continue;
|
|
2935
3540
|
for (const key of [...keys]) {
|
|
@@ -2971,7 +3576,7 @@ function generalizeMemoKey(fnName, source, opts) {
|
|
|
2971
3576
|
const budget = opts.budget ?? defaultLeakBudget;
|
|
2972
3577
|
const deps = opts.depsFp ?? (r ? refineDepsFingerprint(source, r) : { fp: "-", paths: [], contents: [], truncated: false });
|
|
2973
3578
|
const sc = opts.sidecarFp ?? (r?.loadModule && r.fromFile ? sidecarClosureFingerprint(r.fromFile, r) : void 0);
|
|
2974
|
-
const scPath = sc !== void 0 && !sc.startsWith("trunc:") && r?.fromFile ?
|
|
3579
|
+
const scPath = sc !== void 0 && !sc.startsWith("trunc:") && r?.fromFile ? sidecarPathOf(r.fromFile) : void 0;
|
|
2975
3580
|
const srcPart = generalizeSourceKeyPart(source, fnName, opts.file);
|
|
2976
3581
|
const key = [
|
|
2977
3582
|
srcPart,
|
|
@@ -3007,8 +3612,9 @@ function generalizeMemoSet(key, value, depPaths) {
|
|
|
3007
3612
|
}
|
|
3008
3613
|
generalizeMemo.set(key, value);
|
|
3009
3614
|
if (depPaths.length > 0) {
|
|
3010
|
-
|
|
3011
|
-
|
|
3615
|
+
const normDeps = [...new Set(depPaths.map(stablePathKey))];
|
|
3616
|
+
memoKeyDeps.set(key, normDeps);
|
|
3617
|
+
for (const p of normDeps) {
|
|
3012
3618
|
let set = memoDepIndex.get(p);
|
|
3013
3619
|
if (!set) {
|
|
3014
3620
|
set = /* @__PURE__ */ new Set();
|
|
@@ -3172,6 +3778,23 @@ function extractFn(source, fnName, fileAst) {
|
|
|
3172
3778
|
formalsByName.set(exp, formalsByName.get(localName) ?? []);
|
|
3173
3779
|
}
|
|
3174
3780
|
}
|
|
3781
|
+
for (const scope of listFnDirectiveScopes(file)) {
|
|
3782
|
+
if (env.fns.has(scope.name)) continue;
|
|
3783
|
+
if (scope.name === "<anonymous>") continue;
|
|
3784
|
+
const n = scope.node;
|
|
3785
|
+
const isFnLike = n.type === "FunctionDeclaration" || n.type === "FunctionExpression" || n.type === "ArrowFunctionExpression" || n.type === "ClassMethod" || n.type === "ClassPrivateMethod" || n.type === "ObjectMethod";
|
|
3786
|
+
if (!isFnLike || !n.body) continue;
|
|
3787
|
+
if ((n.type === "ClassMethod" || n.type === "ClassPrivateMethod" || n.type === "ObjectMethod") && n.kind && n.kind !== "method") {
|
|
3788
|
+
continue;
|
|
3789
|
+
}
|
|
3790
|
+
const formals = formalParamsFromNodes(n.params ?? []);
|
|
3791
|
+
env.fns.set(scope.name, {
|
|
3792
|
+
params: formalParamDisplayNames(formals),
|
|
3793
|
+
body: n.body,
|
|
3794
|
+
async: n.async === true
|
|
3795
|
+
});
|
|
3796
|
+
formalsByName.set(scope.name, formals);
|
|
3797
|
+
}
|
|
3175
3798
|
const fn2 = env.fns.get(fnName);
|
|
3176
3799
|
if (!fn2) return void 0;
|
|
3177
3800
|
return { params: fn2.params, body: fn2.body, env, formals: formalsByName.get(fnName) ?? [] };
|
|
@@ -3553,6 +4176,8 @@ function primMatches(prim, lv) {
|
|
|
3553
4176
|
return typeof lv === "boolean";
|
|
3554
4177
|
case "object":
|
|
3555
4178
|
return lv === null;
|
|
4179
|
+
case "undefined":
|
|
4180
|
+
return lv === void 0;
|
|
3556
4181
|
default:
|
|
3557
4182
|
return false;
|
|
3558
4183
|
}
|
|
@@ -3560,8 +4185,9 @@ function primMatches(prim, lv) {
|
|
|
3560
4185
|
function predHolds(lv, p) {
|
|
3561
4186
|
switch (p.op) {
|
|
3562
4187
|
case "eq": {
|
|
3563
|
-
const
|
|
3564
|
-
|
|
4188
|
+
const litT = isSelfVar(p.a) && p.b.op === "lit" ? p.b : isSelfVar(p.b) && p.a.op === "lit" ? p.a : void 0;
|
|
4189
|
+
if (!litT) return false;
|
|
4190
|
+
return Object.is(lv, litT.value);
|
|
3565
4191
|
}
|
|
3566
4192
|
case "gt":
|
|
3567
4193
|
case "ge":
|
|
@@ -3579,11 +4205,6 @@ function predHolds(lv, p) {
|
|
|
3579
4205
|
return false;
|
|
3580
4206
|
}
|
|
3581
4207
|
}
|
|
3582
|
-
function selfEqLiteral(p) {
|
|
3583
|
-
if (isSelfVar(p.a) && p.b.op === "lit") return p.b.value;
|
|
3584
|
-
if (isSelfVar(p.b) && p.a.op === "lit") return p.a.value;
|
|
3585
|
-
return void 0;
|
|
3586
|
-
}
|
|
3587
4208
|
function cmpHolds(x, op, n) {
|
|
3588
4209
|
switch (op) {
|
|
3589
4210
|
case "gt":
|
|
@@ -3872,7 +4493,8 @@ function extractLiteralEvidence(rec, idx) {
|
|
|
3872
4493
|
const absArg = rec.argAbs?.[idx];
|
|
3873
4494
|
if (!absArg) return void 0;
|
|
3874
4495
|
if (absArg.conf !== "exact" && absArg.conf !== "path") return void 0;
|
|
3875
|
-
const
|
|
4496
|
+
const lvR = litValue(absArg);
|
|
4497
|
+
const lv = lvR.ok ? lvR.value : void 0;
|
|
3876
4498
|
if (typeof lv === "number" || typeof lv === "string" || typeof lv === "boolean") {
|
|
3877
4499
|
return lv;
|
|
3878
4500
|
}
|
|
@@ -4390,7 +5012,8 @@ function scanLiteralCalls(source, knownFns, phi, opts) {
|
|
|
4390
5012
|
}
|
|
4391
5013
|
arg = projected;
|
|
4392
5014
|
}
|
|
4393
|
-
const
|
|
5015
|
+
const lvR = litValue(arg);
|
|
5016
|
+
const lv = lvR.ok ? lvR.value : void 0;
|
|
4394
5017
|
const isStr = arg.shape.k === "prim" && arg.shape.type === "string";
|
|
4395
5018
|
const strLen = typeof lv === "string" ? lv.length : void 0;
|
|
4396
5019
|
const argIsUndef = isExactUndef(arg);
|
|
@@ -4599,7 +5222,8 @@ function scanLiteralCalls(source, knownFns, phi, opts) {
|
|
|
4599
5222
|
}
|
|
4600
5223
|
}
|
|
4601
5224
|
if (isIntFlag(constraint)) {
|
|
4602
|
-
const
|
|
5225
|
+
const ivR = litValue(fieldAbs);
|
|
5226
|
+
const iv = ivR.ok ? ivR.value : void 0;
|
|
4603
5227
|
if (typeof iv === "number" && !Number.isInteger(iv)) {
|
|
4604
5228
|
out.push({
|
|
4605
5229
|
severity: "error",
|
|
@@ -4641,7 +5265,8 @@ function scanLiteralCalls(source, knownFns, phi, opts) {
|
|
|
4641
5265
|
});
|
|
4642
5266
|
}
|
|
4643
5267
|
}
|
|
4644
|
-
const
|
|
5268
|
+
const lvR = litValue(fieldAbs);
|
|
5269
|
+
const lv = lvR.ok ? lvR.value : void 0;
|
|
4645
5270
|
const sv = typeof lv === "string" ? lv.length : void 0;
|
|
4646
5271
|
const isStr = fieldAbs.shape.k === "prim" && fieldAbs.shape.type === "string";
|
|
4647
5272
|
for (const p of constraint.preds) {
|
|
@@ -4694,7 +5319,9 @@ function scanLiteralCalls(source, knownFns, phi, opts) {
|
|
|
4694
5319
|
}
|
|
4695
5320
|
}
|
|
4696
5321
|
}
|
|
4697
|
-
|
|
5322
|
+
const fieldLit = fieldAbs.term?.op === "lit" ? fieldAbs.term.value : void 0;
|
|
5323
|
+
const fieldIsLit = fieldAbs.term?.op === "lit";
|
|
5324
|
+
if (fieldIsLit && (typeof fieldLit === "number" || typeof fieldLit === "string" || typeof fieldLit === "boolean" || fieldLit === null || fieldLit === void 0) && ((constraint.members?.length ?? 0) > 0 || constraint.preds.some((p) => p.op === "eq")) && !literalMeetsConstraint(fieldLit, constraint)) {
|
|
4698
5325
|
out.push({
|
|
4699
5326
|
severity: "error",
|
|
4700
5327
|
code: "nudo:constraint-violated",
|
|
@@ -4778,7 +5405,8 @@ function scanLiteralCalls(source, knownFns, phi, opts) {
|
|
|
4778
5405
|
}
|
|
4779
5406
|
}
|
|
4780
5407
|
if (isIntFlag(c)) {
|
|
4781
|
-
const
|
|
5408
|
+
const ivR = litValue(arg);
|
|
5409
|
+
const iv = ivR.ok ? ivR.value : void 0;
|
|
4782
5410
|
if (typeof iv === "number" && !Number.isInteger(iv)) {
|
|
4783
5411
|
out.push({
|
|
4784
5412
|
severity: "error",
|
|
@@ -5092,9 +5720,26 @@ function scanLiteralCalls(source, knownFns, phi, opts) {
|
|
|
5092
5720
|
}
|
|
5093
5721
|
|
|
5094
5722
|
export {
|
|
5723
|
+
listFnDirectiveScopes,
|
|
5724
|
+
findFnDirectiveScope,
|
|
5725
|
+
fnDirectiveCommentLines,
|
|
5726
|
+
parseEnvPayload,
|
|
5727
|
+
parseMockModulePayload,
|
|
5728
|
+
parseNudoImportPayload,
|
|
5729
|
+
extractFileEnvNames,
|
|
5730
|
+
extractMockModuleRecords,
|
|
5731
|
+
extractNudoImportRecords,
|
|
5732
|
+
extractBalancedParens,
|
|
5733
|
+
scanCaseTags,
|
|
5734
|
+
scanCaseArgSpans,
|
|
5735
|
+
scanContractSegments,
|
|
5736
|
+
scanThrowsDecl,
|
|
5737
|
+
scanBudgetDecl,
|
|
5095
5738
|
hashSource,
|
|
5096
5739
|
resetHashSourceCache,
|
|
5097
5740
|
normPath,
|
|
5741
|
+
stablePathKey,
|
|
5742
|
+
stablePathKeyGraph,
|
|
5098
5743
|
resolveDepPath,
|
|
5099
5744
|
isNodeModulesPath,
|
|
5100
5745
|
sidecarPathOf,
|
|
@@ -5118,6 +5763,9 @@ export {
|
|
|
5118
5763
|
partial,
|
|
5119
5764
|
pick,
|
|
5120
5765
|
omit,
|
|
5766
|
+
CONSTRAINT_BUILDERS,
|
|
5767
|
+
CONSTRAINT_BUILDER_NAMES,
|
|
5768
|
+
CONSTRAINT_EXPR_RE,
|
|
5121
5769
|
instantiateConstraint,
|
|
5122
5770
|
instantiateOnTerm,
|
|
5123
5771
|
constraintToEntryAbs,
|