@nudojs/cli 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/{chunk-3HCPXPEW.js → chunk-YDM4HHQ4.js} +1783 -1421
  2. package/dist/evaluator-api.d.ts +12 -1
  3. package/dist/evaluator-api.js +52 -2
  4. package/dist/index.d.ts +1 -2
  5. package/dist/index.js +366 -3572
  6. package/package.json +23 -11
  7. package/CHANGELOG.md +0 -41
  8. package/examples/real-world.js +0 -66
  9. package/examples/validate-real-world.ts +0 -160
  10. package/src/__tests__/builtin-advanced.test.ts +0 -292
  11. package/src/__tests__/builtin-api.test.ts +0 -308
  12. package/src/__tests__/builtin-functions.test.ts +0 -313
  13. package/src/__tests__/builtin-map.test.ts +0 -129
  14. package/src/__tests__/builtin-promise.test.ts +0 -150
  15. package/src/__tests__/builtin-regexp.test.ts +0 -133
  16. package/src/__tests__/builtin-set.test.ts +0 -163
  17. package/src/__tests__/builtin-url.test.ts +0 -256
  18. package/src/__tests__/builtin-web.test.ts +0 -340
  19. package/src/__tests__/combination-scenarios.test.ts +0 -384
  20. package/src/__tests__/edge-cases.test.ts +0 -749
  21. package/src/__tests__/evaluator-loops.test.ts +0 -283
  22. package/src/__tests__/evaluator-nullish-coalescing.test.ts +0 -86
  23. package/src/__tests__/evaluator-optional-chaining.test.ts +0 -37
  24. package/src/__tests__/evaluator-phase2.test.ts +0 -564
  25. package/src/__tests__/evaluator-phase3.test.ts +0 -864
  26. package/src/__tests__/evaluator-refined.test.ts +0 -121
  27. package/src/__tests__/evaluator-string-methods.test.ts +0 -151
  28. package/src/__tests__/evaluator.test.ts +0 -943
  29. package/src/__tests__/mock-arrow-function.test.ts +0 -169
  30. package/src/__tests__/mock-comparison.test.ts +0 -221
  31. package/src/__tests__/mock-comprehensive.test.ts +0 -250
  32. package/src/__tests__/mock-final-integration.test.ts +0 -263
  33. package/src/__tests__/mock-helpers.test.ts +0 -174
  34. package/src/__tests__/mock-realworld.test.ts +0 -191
  35. package/src/__tests__/mock-rest-debug.test.ts +0 -110
  36. package/src/__tests__/mock-rest-edge.test.ts +0 -176
  37. package/src/__tests__/mock-sinon-runtime.test.ts +0 -168
  38. package/src/__tests__/narrowing-discriminated.test.ts +0 -35
  39. package/src/__tests__/narrowing-in-operator.test.ts +0 -56
  40. package/src/__tests__/narrowing-switch.test.ts +0 -56
  41. package/src/__tests__/narrowing-truthiness.test.ts +0 -52
  42. package/src/__tests__/narrowing.test.ts +0 -82
  43. package/src/__tests__/nudo-mock-syntax.test.ts +0 -245
  44. package/src/__tests__/realworld-patterns.test.ts +0 -430
  45. package/src/__tests__/syntax-sugar.test.ts +0 -345
  46. package/src/builtins/builtin-intl.ts +0 -17
  47. package/src/builtins/builtin-map.ts +0 -174
  48. package/src/builtins/builtin-promise.ts +0 -116
  49. package/src/builtins/builtin-reflect.ts +0 -30
  50. package/src/builtins/builtin-regexp.ts +0 -34
  51. package/src/builtins/builtin-set.ts +0 -131
  52. package/src/builtins/builtin-symbol.ts +0 -16
  53. package/src/builtins/builtin-url.ts +0 -58
  54. package/src/builtins/builtin-weak.ts +0 -22
  55. package/src/builtins/builtin-web.ts +0 -111
  56. package/src/builtins/index.ts +0 -61
  57. package/src/config.ts +0 -32
  58. package/src/env-loader.ts +0 -174
  59. package/src/evaluator-api.ts +0 -36
  60. package/src/evaluator.ts +0 -5623
  61. package/src/index.ts +0 -931
  62. package/src/narrowing.ts +0 -537
  63. package/src/resolve-npm.ts +0 -63
  64. package/tsconfig.json +0 -9
@@ -1,864 +0,0 @@
1
- import { describe, it, expect, beforeEach } from "vitest";
2
- import {
3
- T,
4
- typeValueEquals,
5
- typeValueToString,
6
- createEnvironment,
7
- isSubtypeOf,
8
- } from "@nudojs/core";
9
- import type { TypeValue } from "@nudojs/core";
10
- import { parse } from "@nudojs/parser";
11
- import { evaluate, evaluateFunction, evaluateFunctionFull, evaluateProgram, resetMemo, setModuleResolver, setCurrentFileDir, setUnknownCollector, setMockModules, resetMockModules, type UnknownRecord } from "../evaluator.ts";
12
-
13
- function evalCode(code: string): TypeValue {
14
- const ast = parse(code);
15
- const env = createEnvironment();
16
- return evaluateProgram(ast, env);
17
- }
18
-
19
- function evalFn(code: string, args: TypeValue[]): TypeValue {
20
- const ast = parse(code);
21
- const env = createEnvironment();
22
- evaluateProgram(ast, env);
23
- const fns = ast.type === "File" ? ast.program.body : [];
24
- const fnNode = fns.find(
25
- (n: any) => n.type === "FunctionDeclaration",
26
- );
27
- if (!fnNode) throw new Error("No function found");
28
- return evaluateFunction(fnNode, args, env);
29
- }
30
-
31
- function evalFnFull(code: string, args: TypeValue[]): { value: TypeValue; throws: TypeValue } {
32
- const ast = parse(code);
33
- const env = createEnvironment();
34
- evaluateProgram(ast, env);
35
- const fns = ast.type === "File" ? ast.program.body : [];
36
- const fnNode = fns.find(
37
- (n: any) => n.type === "FunctionDeclaration",
38
- );
39
- if (!fnNode) throw new Error("No function found");
40
- return evaluateFunctionFull(fnNode, args, env);
41
- }
42
-
43
- describe("ThrowStatement", () => {
44
- it("throw produces ThrowSignal captured by try-catch", () => {
45
- const result = evalFn(
46
- `function test(x) {
47
- try {
48
- throw new Error("oops");
49
- } catch (e) {
50
- return e;
51
- }
52
- }`,
53
- [T.number],
54
- );
55
- expect(result.kind).toBe("instance");
56
- if (result.kind === "instance") {
57
- expect(result.className).toBe("Error");
58
- }
59
- });
60
-
61
- it("uncaught throw in function returns never", () => {
62
- const { value, throws } = evalFnFull(
63
- `function test() {
64
- throw new Error("fail");
65
- }`,
66
- [],
67
- );
68
- expect(value.kind).toBe("never");
69
- expect(throws.kind).toBe("instance");
70
- if (throws.kind === "instance") {
71
- expect(throws.className).toBe("Error");
72
- }
73
- });
74
-
75
- it("try-catch digests throws", () => {
76
- const { value, throws } = evalFnFull(
77
- `function test() {
78
- try {
79
- throw new TypeError("bad type");
80
- } catch (e) {
81
- return "recovered";
82
- }
83
- }`,
84
- [],
85
- );
86
- expect(typeValueEquals(value, T.literal("recovered"))).toBe(true);
87
- expect(throws.kind).toBe("never");
88
- });
89
-
90
- it("finally always executes", () => {
91
- const result = evalFn(
92
- `function test() {
93
- let x = 0;
94
- try {
95
- x = 1;
96
- return x;
97
- } finally {
98
- x = 2;
99
- }
100
- }`,
101
- [],
102
- );
103
- expect(typeValueEquals(result, T.literal(1))).toBe(true);
104
- });
105
-
106
- it("catch receives the thrown type", () => {
107
- const result = evalFn(
108
- `function test() {
109
- try {
110
- throw new TypeError("bad");
111
- } catch (e) {
112
- return e.message;
113
- }
114
- }`,
115
- [],
116
- );
117
- expect(typeValueEquals(result, T.literal("bad"))).toBe(true);
118
- });
119
- });
120
-
121
- describe("NewExpression", () => {
122
- it("creates Error instance", () => {
123
- const result = evalCode(`new Error("something went wrong")`);
124
- expect(result.kind).toBe("instance");
125
- if (result.kind === "instance") {
126
- expect(result.className).toBe("Error");
127
- expect(typeValueEquals(result.properties.message, T.literal("something went wrong"))).toBe(true);
128
- }
129
- });
130
-
131
- it("creates TypeError instance", () => {
132
- const result = evalCode(`new TypeError("bad type")`);
133
- expect(result.kind).toBe("instance");
134
- if (result.kind === "instance") {
135
- expect(result.className).toBe("TypeError");
136
- }
137
- });
138
-
139
- it("creates instance from user-defined class", () => {
140
- const result = evalCode(`
141
- class Point {
142
- constructor(x, y) {
143
- this.x = x;
144
- this.y = y;
145
- }
146
- }
147
- new Point(1, 2)
148
- `);
149
- expect(result.kind).toBe("instance");
150
- if (result.kind === "instance") {
151
- expect(result.className).toBe("Point");
152
- expect(typeValueEquals(result.properties.x, T.literal(1))).toBe(true);
153
- expect(typeValueEquals(result.properties.y, T.literal(2))).toBe(true);
154
- }
155
- });
156
- });
157
-
158
- describe("ClassDeclaration", () => {
159
- it("binds class to environment", () => {
160
- const ast = parse(`
161
- class Foo {
162
- constructor(val) {
163
- this.val = val;
164
- }
165
- getVal() {
166
- return this.val;
167
- }
168
- }
169
- const f = new Foo(42);
170
- `);
171
- const env = createEnvironment();
172
- evaluateProgram(ast, env);
173
- const f = env.lookup("f");
174
- expect(f.kind).toBe("instance");
175
- if (f.kind === "instance") {
176
- expect(f.className).toBe("Foo");
177
- expect(typeValueEquals(f.properties.val, T.literal(42))).toBe(true);
178
- expect(f.properties.getVal?.kind).toBe("function");
179
- }
180
- });
181
- });
182
-
183
- describe("instanceof", () => {
184
- it("checks instanceof for known instance", () => {
185
- const result = evalCode(`
186
- const e = new Error("test");
187
- e instanceof Error
188
- `);
189
- expect(typeValueEquals(result, T.literal(true))).toBe(true);
190
- });
191
-
192
- it("instanceof narrowing in if", () => {
193
- const result = evalFn(
194
- `function test(x) {
195
- if (x instanceof Error) {
196
- return x.message;
197
- }
198
- return "not error";
199
- }`,
200
- [T.instanceOf("Error", { message: T.literal("oops") })],
201
- );
202
- expect(typeValueEquals(result, T.literal("oops"))).toBe(true);
203
- });
204
-
205
- it("instanceof returns boolean for unknown types", () => {
206
- const result = evalCode(`
207
- const x = 42;
208
- x instanceof Error
209
- `);
210
- // primitives can never be instanceof anything — decided statically
211
- expect(typeValueEquals(result, T.literal(false))).toBe(true);
212
- // unknown receivers keep the conservative boolean fallback
213
- const fn = evaluateFunction(
214
- parse("function f(x) { return x instanceof Error; }").program.body[0],
215
- [T.unknown],
216
- createEnvironment(),
217
- );
218
- expect(fn).toBe(T.boolean);
219
- });
220
- });
221
-
222
- describe("async/await", () => {
223
- it("async function wraps return in Promise", () => {
224
- const result = evalFn(
225
- `async function fetchData() {
226
- return 42;
227
- }`,
228
- [],
229
- );
230
- expect(result.kind).toBe("promise");
231
- if (result.kind === "promise") {
232
- expect(typeValueEquals(result.value, T.literal(42))).toBe(true);
233
- }
234
- });
235
-
236
- it("await unwraps Promise", () => {
237
- const result = evalFn(
238
- `async function test() {
239
- const p = async function inner() { return 10; };
240
- const val = await p();
241
- return val;
242
- }`,
243
- [],
244
- );
245
- expect(result.kind).toBe("promise");
246
- if (result.kind === "promise") {
247
- expect(typeValueEquals(result.value, T.literal(10))).toBe(true);
248
- }
249
- });
250
-
251
- it("await on non-promise returns value as-is", () => {
252
- const result = evalFn(
253
- `async function test() {
254
- const val = await 42;
255
- return val;
256
- }`,
257
- [],
258
- );
259
- expect(result.kind).toBe("promise");
260
- if (result.kind === "promise") {
261
- expect(typeValueEquals(result.value, T.literal(42))).toBe(true);
262
- }
263
- });
264
- });
265
-
266
- describe("recursion with memoization", () => {
267
- beforeEach(() => {
268
- resetMemo();
269
- });
270
-
271
- it("handles recursive function with memoization", () => {
272
- const ast = parse(`
273
- function factorial(n) {
274
- if (n === 0) return 1;
275
- if (n === 1) return 1;
276
- return n * factorial(n - 1);
277
- }
278
- `);
279
- const env = createEnvironment();
280
- evaluateProgram(ast, env);
281
- const fn = env.lookup("factorial");
282
- expect(fn.kind).toBe("function");
283
- if (fn.kind === "function") {
284
- (fn as any)._memoize = "factorial";
285
- const result = evaluate(
286
- parse(`factorial(5)`).program.body[0],
287
- env,
288
- );
289
- expect(result.kind).toBe("literal");
290
- }
291
- });
292
-
293
- it("recursive call returns unknown for in-progress memo", () => {
294
- const ast = parse(`
295
- function infinite(x) {
296
- return infinite(x);
297
- }
298
- const result = infinite(1);
299
- `);
300
- const env = createEnvironment();
301
- const fn_ast = ast.program.body[0];
302
- evaluate(fn_ast, env);
303
- const fn = env.lookup("infinite");
304
- if (fn.kind === "function") {
305
- (fn as any)._memoize = "infinite";
306
- }
307
- const callAst = ast.program.body[1];
308
- evaluate(callAst, env);
309
- const result = env.lookup("result");
310
- expect(result.kind).toBe("unknown");
311
- });
312
-
313
- it("budget-truncated recursion falls back to the function's observed results instead of unknown", () => {
314
- // eq over chains deeper than MAX_CALL_DEPTH truncates mid-recursion.
315
- // The first truncation has no observations and stays unknown; precise
316
- // calls then accumulate true/false; a later truncation (fresh argument
317
- // shapes, so no memo hit) degrades to the observed union.
318
- const nest = (n: number, leaf: string): string => (n === 0 ? leaf : `{ x: ${nest(n - 1, leaf)} }`);
319
- const ast = parse(`
320
- function eq(a, b) {
321
- if (a === b) return true;
322
- if (typeof a !== "object") return false;
323
- return eq(a.x, b.x);
324
- }
325
- const r1 = eq(${nest(70, "1")}, ${nest(70, "2")});
326
- const t = eq(1, 1);
327
- const f = eq(1, 2);
328
- const r2 = eq(${nest(80, "3")}, ${nest(80, "4")});
329
- `);
330
- const env = createEnvironment();
331
- evaluateProgram(ast, env);
332
- expect(env.lookup("r1").kind).toBe("unknown");
333
- expect(typeValueToString(env.lookup("t"))).toBe("true");
334
- expect(typeValueToString(env.lookup("f"))).toBe("false");
335
- const r2 = env.lookup("r2");
336
- expect(r2.kind).toBe("union");
337
- if (r2.kind === "union") {
338
- expect(r2.members).toHaveLength(2);
339
- expect(typeValueToString(r2)).toBe("true | false");
340
- }
341
- });
342
-
343
- it("budget-truncated recursion without observed results stays unknown", () => {
344
- const nest = (n: number, leaf: string): string => (n === 0 ? leaf : `{ x: ${nest(n - 1, leaf)} }`);
345
- const ast = parse(`
346
- function eq(a, b) {
347
- if (a === b) return true;
348
- if (typeof a !== "object") return false;
349
- return eq(a.x, b.x);
350
- }
351
- const r = eq(${nest(70, "5")}, ${nest(70, "6")});
352
- `);
353
- const env = createEnvironment();
354
- evaluateProgram(ast, env);
355
- expect(env.lookup("r").kind).toBe("unknown");
356
- });
357
- });
358
-
359
- describe("modules import/export", () => {
360
- beforeEach(() => {
361
- resetMemo();
362
- });
363
-
364
- it("handles export named declaration", () => {
365
- const ast = parse(`
366
- export const x = 42;
367
- export function add(a, b) { return a + b; }
368
- `);
369
- const env = createEnvironment();
370
- evaluateProgram(ast, env);
371
- expect(typeValueEquals(env.lookup("x"), T.literal(42))).toBe(true);
372
- expect(typeValueEquals(env.lookup("__export_x"), T.literal(42))).toBe(true);
373
- expect(env.lookup("add").kind).toBe("function");
374
- expect(env.lookup("__export_add").kind).toBe("function");
375
- });
376
-
377
- it("handles export default declaration", () => {
378
- const ast = parse(`
379
- export default function main() { return "hello"; }
380
- `);
381
- const env = createEnvironment();
382
- evaluateProgram(ast, env);
383
- expect(env.lookup("__export_default").kind).toBe("function");
384
- });
385
-
386
- it("handles import with module resolver", () => {
387
- const moduleSource = `
388
- export const PI = 3.14;
389
- export function double(x) { return x * 2; }
390
- `;
391
- const moduleAst = parse(moduleSource);
392
-
393
- setModuleResolver((source) => {
394
- if (source === "./math") {
395
- return { ast: moduleAst, filePath: "/fake/math.js" };
396
- }
397
- return null;
398
- });
399
- setCurrentFileDir("/fake");
400
-
401
- const ast = parse(`
402
- import { PI, double } from "./math";
403
- const result = double(PI);
404
- `);
405
- const env = createEnvironment();
406
- evaluateProgram(ast, env);
407
-
408
- expect(typeValueEquals(env.lookup("PI"), T.literal(3.14))).toBe(true);
409
- const result = env.lookup("result");
410
- expect(typeValueEquals(result, T.literal(6.28))).toBe(true);
411
-
412
- setModuleResolver(null);
413
- });
414
-
415
- it("handles default import", () => {
416
- const moduleSource = `
417
- export default function greet() { return "hi"; }
418
- `;
419
- const moduleAst = parse(moduleSource);
420
-
421
- setModuleResolver((source) => {
422
- if (source === "./greet") {
423
- return { ast: moduleAst, filePath: "/fake/greet.js" };
424
- }
425
- return null;
426
- });
427
- setCurrentFileDir("/fake");
428
-
429
- const ast = parse(`
430
- import greet from "./greet";
431
- `);
432
- const env = createEnvironment();
433
- evaluateProgram(ast, env);
434
-
435
- expect(env.lookup("greet").kind).toBe("function");
436
-
437
- setModuleResolver(null);
438
- });
439
-
440
- it("handles namespace import", () => {
441
- const moduleSource = `
442
- export const a = 1;
443
- export const b = 2;
444
- `;
445
- const moduleAst = parse(moduleSource);
446
-
447
- setModuleResolver((source) => {
448
- if (source === "./vals") {
449
- return { ast: moduleAst, filePath: "/fake/vals.js" };
450
- }
451
- return null;
452
- });
453
- setCurrentFileDir("/fake");
454
-
455
- const ast = parse(`
456
- import * as vals from "./vals";
457
- `);
458
- const env = createEnvironment();
459
- evaluateProgram(ast, env);
460
-
461
- const vals = env.lookup("vals");
462
- expect(vals.kind).toBe("object");
463
- if (vals.kind === "object") {
464
- expect(typeValueEquals(vals.properties.a, T.literal(1))).toBe(true);
465
- expect(typeValueEquals(vals.properties.b, T.literal(2))).toBe(true);
466
- }
467
-
468
- setModuleResolver(null);
469
- });
470
-
471
- it("caches modules - same file evaluated once", () => {
472
- setModuleResolver((source) => {
473
- if (source === "./cached") {
474
- return { ast: parse(`export const x = 1;`), filePath: "/fake/cached.js" };
475
- }
476
- return null;
477
- });
478
- setCurrentFileDir("/fake");
479
-
480
- const ast = parse(`
481
- import { x } from "./cached";
482
- import { x as x2 } from "./cached";
483
- `);
484
- const env = createEnvironment();
485
- evaluateProgram(ast, env);
486
-
487
- expect(typeValueEquals(env.lookup("x"), T.literal(1))).toBe(true);
488
- expect(typeValueEquals(env.lookup("x2"), T.literal(1))).toBe(true);
489
-
490
- setModuleResolver(null);
491
- });
492
-
493
- it("resolves require() of a CJS module exporting an object", () => {
494
- setModuleResolver((source) => {
495
- if (source === "./b") {
496
- return {
497
- ast: parse(`function triple(x) { return x * 3; }\nmodule.exports = { triple };\n`),
498
- filePath: "/fake/b.js",
499
- };
500
- }
501
- return null;
502
- });
503
- setCurrentFileDir("/fake");
504
-
505
- const ast = parse(`
506
- const b = require("./b");
507
- const r = b.triple(4);
508
- `);
509
- const env = createEnvironment();
510
- evaluateProgram(ast, env);
511
-
512
- const b = env.lookup("b");
513
- expect(b.kind).toBe("object");
514
- if (b.kind === "object") {
515
- expect(b.properties.triple.kind).toBe("function");
516
- }
517
- expect(typeValueEquals(env.lookup("r"), T.literal(12))).toBe(true);
518
-
519
- setModuleResolver(null);
520
- });
521
-
522
- it("require() picks up a chained module.exports = internals.x = function assignment", () => {
523
- setModuleResolver((source) => {
524
- if (source === "./clone") {
525
- return {
526
- ast: parse(`
527
- const internals = {};
528
- module.exports = internals.clone = function (obj, options = {}) {
529
- if (options.shallow) {
530
- return obj;
531
- }
532
- return obj;
533
- };
534
- `),
535
- filePath: "/fake/clone.js",
536
- };
537
- }
538
- return null;
539
- });
540
- setCurrentFileDir("/fake");
541
-
542
- const ast = parse(`
543
- const Clone = require("./clone");
544
- const r = Clone(5);
545
- `);
546
- const env = createEnvironment();
547
- evaluateProgram(ast, env);
548
-
549
- expect(env.lookup("Clone").kind).toBe("function");
550
- expect(typeValueEquals(env.lookup("r"), T.literal(5))).toBe(true);
551
-
552
- setModuleResolver(null);
553
- });
554
-
555
- it("supports exports.name assignments", () => {
556
- setModuleResolver((source) => {
557
- if (source === "./b") {
558
- return {
559
- ast: parse(`exports.triple = function (x) { return x * 3; };\n`),
560
- filePath: "/fake/b.js",
561
- };
562
- }
563
- return null;
564
- });
565
- setCurrentFileDir("/fake");
566
-
567
- const ast = parse(`
568
- const b = require("./b");
569
- const r = b.triple(2);
570
- `);
571
- const env = createEnvironment();
572
- evaluateProgram(ast, env);
573
-
574
- expect(typeValueEquals(env.lookup("r"), T.literal(6))).toBe(true);
575
-
576
- setModuleResolver(null);
577
- });
578
-
579
- it("require() of an ES module returns its exports as properties", () => {
580
- setModuleResolver((source) => {
581
- if (source === "./esm") {
582
- return {
583
- ast: parse(`export const n = 1;\nexport function f(x) { return x + 1; }\n`),
584
- filePath: "/fake/esm.js",
585
- };
586
- }
587
- return null;
588
- });
589
- setCurrentFileDir("/fake");
590
-
591
- const ast = parse(`
592
- const b = require("./esm");
593
- const r = b.f(b.n);
594
- `);
595
- const env = createEnvironment();
596
- evaluateProgram(ast, env);
597
-
598
- expect(typeValueEquals(env.lookup("r"), T.literal(2))).toBe(true);
599
-
600
- setModuleResolver(null);
601
- });
602
-
603
- it("require() with a bare specifier returns unknown and records a warning", () => {
604
- setCurrentFileDir("/fake");
605
-
606
- const records: UnknownRecord[] = [];
607
- setUnknownCollector((r) => records.push(r));
608
- const env = createEnvironment();
609
- try {
610
- const ast = parse(`const u = require("@hapi/hoek");\n`);
611
- evaluateProgram(ast, env);
612
- } finally {
613
- setUnknownCollector(null);
614
- setCurrentFileDir("");
615
- }
616
-
617
- expect(env.lookup("u").kind).toBe("unknown");
618
- expect(records.some((r) => r.kind === "global" && r.name === `require('@hapi/hoek')`)).toBe(true);
619
- });
620
-
621
- // --- 模块加载边界守卫(环 / 深度 / 缺失),来源:evaluator.ts loadModuleEnv 加固 ---
622
-
623
- it("reports a circular import chain a -> b -> a and keeps partial-binding semantics", () => {
624
- const aAst = parse(`import { bVal } from "./b.js";\nexport const aVal = 1;\nexport const mixed = bVal;\n`);
625
- const bAst = parse(`import { aVal } from "./a.js";\nexport const bVal = aVal;\n`);
626
- const files: Record<string, { ast: ReturnType<typeof parse>; filePath: string }> = {
627
- "./a.js": { ast: aAst, filePath: "/fake/a.js" },
628
- "./b.js": { ast: bAst, filePath: "/fake/b.js" },
629
- };
630
- setModuleResolver((source) => files[source] ?? null);
631
- setCurrentFileDir("/fake");
632
-
633
- const records: UnknownRecord[] = [];
634
- setUnknownCollector((r) => records.push(r));
635
- const env = createEnvironment();
636
- evaluateProgram(parse(`import { aVal, mixed } from "./a.js";\n`), env);
637
-
638
- const cycle = records.find((r) => r.kind === "global" && r.name.startsWith("module-cycle:"));
639
- expect(cycle).toBeDefined();
640
- expect(cycle?.name).toBe("module-cycle:a.js");
641
- expect(cycle?.reason).toBe("Circular module load: /fake/a.js -> /fake/b.js -> /fake/a.js (bindings inside the cycle resolve to their partially evaluated types)");
642
- // 环内 aVal 在 b 求值时尚未执行 → unknown;环后补齐的导出照常可得
643
- expect(typeValueEquals(env.lookup("aVal"), T.literal(1))).toBe(true);
644
- expect(env.lookup("mixed").kind).toBe("unknown");
645
-
646
- setUnknownCollector(null);
647
- setModuleResolver(null);
648
- setCurrentFileDir("");
649
- });
650
-
651
- it("reports a mock-module cycle when the mock file re-imports the mocked module", () => {
652
- const mockAst = parse(`import { helper } from "calc";\nexport const value = helper;\nexport const own = 2;\n`);
653
- setMockModules(new Map([["calc", { fromPath: "./mockCalc.js" }]]));
654
- setModuleResolver((source) => {
655
- if (source === "./mockCalc.js") return { ast: mockAst, filePath: "/fake/mockCalc.js" };
656
- return null;
657
- });
658
- setCurrentFileDir("/fake");
659
-
660
- const records: UnknownRecord[] = [];
661
- setUnknownCollector((r) => records.push(r));
662
- const env = createEnvironment();
663
- evaluateProgram(parse(`import { value, own } from "calc";\nconst sum = own + 40;\n`), env);
664
-
665
- const cycle = records.find((r) => r.kind === "global" && r.name.startsWith("module-cycle:"));
666
- expect(cycle).toBeDefined();
667
- expect(cycle?.reason).toBe("Circular module load: /fake/mockCalc.js -> /fake/mockCalc.js (bindings inside the cycle resolve to their partially evaluated types)");
668
- // 环截断后 mock 文件其余导出照常求值,分析不中断
669
- expect(env.lookup("value").kind).toBe("unknown");
670
- expect(typeValueEquals(env.lookup("sum"), T.literal(42))).toBe(true);
671
-
672
- setUnknownCollector(null);
673
- setModuleResolver(null);
674
- setCurrentFileDir("");
675
- resetMockModules();
676
- });
677
-
678
- it("truncates module chains deeper than the depth limit and reports the chain", () => {
679
- // f1 -> f2 -> ... -> f20:第 17 层(f17)触发深度上限(MAX_MODULE_LOAD_DEPTH = 16)
680
- const files: Record<string, { ast: ReturnType<typeof parse>; filePath: string }> = {};
681
- for (let i = 1; i <= 20; i++) {
682
- const body = i < 20
683
- ? `import { value as inner } from "./f${i + 1}.js";\nexport const value = inner;\n`
684
- : `export const value = 20;\n`;
685
- files[`./f${i}.js`] = { ast: parse(body), filePath: `/fake/f${i}.js` };
686
- }
687
- setModuleResolver((source) => files[source] ?? null);
688
- setCurrentFileDir("/fake");
689
-
690
- const records: UnknownRecord[] = [];
691
- setUnknownCollector((r) => records.push(r));
692
- const env = createEnvironment();
693
- evaluateProgram(parse(`import { value } from "./f1.js";\n`), env);
694
-
695
- const depth = records.find((r) => r.kind === "global" && r.name.startsWith("module-depth:"));
696
- expect(depth).toBeDefined();
697
- expect(depth?.name).toBe("module-depth:f17.js");
698
- expect(depth?.reason).toContain("depth 17 > 16 max");
699
- const expectedChain = Array.from({ length: 17 }, (_, k) => `/fake/f${k + 1}.js`).join(" -> ");
700
- expect(depth?.reason).toContain(expectedChain);
701
- // 截断为 unknown,而非栈溢出崩溃
702
- expect(env.lookup("value").kind).toBe("unknown");
703
-
704
- setUnknownCollector(null);
705
- setModuleResolver(null);
706
- setCurrentFileDir("");
707
- });
708
-
709
- it("reports a missing @nudo:mock-module target with its full path and falls back to the original", () => {
710
- setMockModules(new Map([["b", { fromPath: "./nope.js" }]]));
711
- setModuleResolver((source) => {
712
- if (source === "b") return { ast: parse(`export const x = 41;\n`), filePath: "/fake/b.js" };
713
- return null; // "./nope.js" 不存在
714
- });
715
- setCurrentFileDir("/fake");
716
-
717
- const records: UnknownRecord[] = [];
718
- setUnknownCollector((r) => records.push(r));
719
- const env = createEnvironment();
720
- evaluateProgram(parse(`import { x } from "b";\nconst y = x + 1;\n`), env);
721
-
722
- const missing = records.find((r) => r.kind === "global" && r.name.startsWith("module-missing:"));
723
- expect(missing).toBeDefined();
724
- expect(missing?.name).toBe("module-missing:nope.js");
725
- expect(missing?.reason).toContain('@nudo:mock-module target for "b" not found');
726
- expect(missing?.reason).toContain("/fake/nope.js");
727
- // 回落到原模块,mock 缺失不中断分析
728
- expect(typeValueEquals(env.lookup("y"), T.literal(42))).toBe(true);
729
-
730
- setUnknownCollector(null);
731
- setModuleResolver(null);
732
- setCurrentFileDir("");
733
- resetMockModules();
734
- });
735
-
736
- it("reports an unresolvable relative import with the attempted full path", () => {
737
- setModuleResolver(() => null);
738
- setCurrentFileDir("/fake");
739
-
740
- const records: UnknownRecord[] = [];
741
- setUnknownCollector((r) => records.push(r));
742
- const env = createEnvironment();
743
- evaluateProgram(parse(`import { x } from "./gone.js";\nconst y = 1;\n`), env);
744
-
745
- const missing = records.find((r) => r.kind === "global" && r.name.startsWith("module-missing:"));
746
- expect(missing).toBeDefined();
747
- expect(missing?.reason).toContain("Cannot resolve import './gone.js'");
748
- expect(missing?.reason).toContain("/fake/gone.js");
749
- expect(typeValueEquals(env.lookup("y"), T.literal(1))).toBe(true);
750
-
751
- setUnknownCollector(null);
752
- setModuleResolver(null);
753
- setCurrentFileDir("");
754
- });
755
-
756
- it("loads nested mock-module dependencies without triggering guard diagnostics", () => {
757
- const helperAst = parse(`export const h = 2;\n`);
758
- const mockAst = parse(`import { h } from "./helper.js";\nexport const answer = h * 21;\n`);
759
- const files: Record<string, { ast: ReturnType<typeof parse>; filePath: string }> = {
760
- "./helper.js": { ast: helperAst, filePath: "/fake/helper.js" },
761
- "./mockCalc.js": { ast: mockAst, filePath: "/fake/mockCalc.js" },
762
- };
763
- setMockModules(new Map([["calc", { fromPath: "./mockCalc.js" }]]));
764
- setModuleResolver((source) => files[source] ?? null);
765
- setCurrentFileDir("/fake");
766
-
767
- const records: UnknownRecord[] = [];
768
- setUnknownCollector((r) => records.push(r));
769
- const env = createEnvironment();
770
- evaluateProgram(parse(`import { answer } from "calc";\nconst life = answer;\n`), env);
771
-
772
- // 正常嵌套 mock:不产生任何守卫诊断
773
- expect(records.filter((r) => r.name.startsWith("module-"))).toHaveLength(0);
774
- expect(typeValueEquals(env.lookup("life"), T.literal(42))).toBe(true);
775
-
776
- setUnknownCollector(null);
777
- setModuleResolver(null);
778
- setCurrentFileDir("");
779
- resetMockModules();
780
- });
781
-
782
- it("pre-binds __dirname and __filename as strings", () => {
783
- const ast = parse(`const d = __dirname;\nconst f = __filename;\n`);
784
- const env = createEnvironment();
785
- evaluateProgram(ast, env);
786
-
787
- expect(typeValueEquals(env.lookup("d"), T.string)).toBe(true);
788
- expect(typeValueEquals(env.lookup("f"), T.string)).toBe(true);
789
- });
790
- });
791
-
792
- describe("SwitchStatement", () => {
793
- it("evaluates matching case", () => {
794
- const result = evalFn(
795
- `function test(x) {
796
- switch (x) {
797
- case 1: return "one";
798
- case 2: return "two";
799
- default: return "other";
800
- }
801
- }`,
802
- [T.literal(1)],
803
- );
804
- expect(typeValueEquals(result, T.literal("one"))).toBe(true);
805
- });
806
-
807
- it("evaluates default case", () => {
808
- const result = evalFn(
809
- `function test(x) {
810
- switch (x) {
811
- case 1: return "one";
812
- default: return "other";
813
- }
814
- }`,
815
- [T.literal(99)],
816
- );
817
- expect(typeValueEquals(result, T.literal("other"))).toBe(true);
818
- });
819
-
820
- it("returns union for abstract discriminant", () => {
821
- const result = evalFn(
822
- `function test(x) {
823
- switch (x) {
824
- case 1: return "one";
825
- case 2: return "two";
826
- default: return "other";
827
- }
828
- }`,
829
- [T.number],
830
- );
831
- expect(result.kind).toBe("union");
832
- });
833
- });
834
-
835
- describe("PromiseType in evaluator", () => {
836
- it("typeof promise is object", () => {
837
- const result = evalCode(`typeof (async function() { return 1; })()`);
838
- expect(typeValueEquals(result, T.literal("object"))).toBe(true);
839
- });
840
- });
841
-
842
- describe("instance property access", () => {
843
- it("accesses instance properties", () => {
844
- const result = evalCode(`
845
- const e = new Error("test");
846
- e.message
847
- `);
848
- expect(typeValueEquals(result, T.literal("test"))).toBe(true);
849
- });
850
- });
851
-
852
- describe("export specifiers", () => {
853
- it("handles export { x, y }", () => {
854
- const ast = parse(`
855
- const x = 1;
856
- const y = 2;
857
- export { x, y };
858
- `);
859
- const env = createEnvironment();
860
- evaluateProgram(ast, env);
861
- expect(typeValueEquals(env.lookup("__export_x"), T.literal(1))).toBe(true);
862
- expect(typeValueEquals(env.lookup("__export_y"), T.literal(2))).toBe(true);
863
- });
864
- });