@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,943 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { mkdtempSync, writeFileSync, readFileSync, rmSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import { T, typeValueEquals, typeValueToString, createEnvironment } from "@nudojs/core";
6
- import { parse, extractDirectives } from "@nudojs/parser";
7
- import { evaluateFunction, evaluateProgram, evaluateFunctionFull, setUnknownCollector, setProvenanceTracking, setCallCollector, setModuleResolver, setCurrentFileDir, type UnknownRecord, type CallRecord } from "../evaluator.ts";
8
-
9
- function inferFromSource(source: string) {
10
- const ast = parse(source);
11
- const functions = extractDirectives(ast);
12
- const env = createEnvironment();
13
- return functions.map((fn) => ({
14
- name: fn.name,
15
- cases: fn.directives.map((d) => ({
16
- caseName: d.name,
17
- result: evaluateFunction(fn.node, d.args, env),
18
- })),
19
- }));
20
- }
21
-
22
- describe("End-to-end: calc example from design doc", () => {
23
- const source = `
24
- /**
25
- * @nudo:case "concrete" (1, 2)
26
- * @nudo:case "symbolic" (T.number, T.number)
27
- */
28
- function calc(a, b) {
29
- if (a > b) return a - b;
30
- return a + b;
31
- }
32
- `;
33
-
34
- it("infers concrete case: calc(1, 2) => 3", () => {
35
- const results = inferFromSource(source);
36
- expect(results).toHaveLength(1);
37
- expect(results[0].name).toBe("calc");
38
- const concreteCase = results[0].cases[0];
39
- expect(concreteCase.caseName).toBe("concrete");
40
- expect(typeValueEquals(concreteCase.result, T.literal(3))).toBe(true);
41
- });
42
-
43
- it("infers symbolic case: calc(number, number) => number", () => {
44
- const results = inferFromSource(source);
45
- const symbolicCase = results[0].cases[1];
46
- expect(symbolicCase.caseName).toBe("symbolic");
47
- expect(typeValueEquals(symbolicCase.result, T.number)).toBe(true);
48
- });
49
- });
50
-
51
- describe("End-to-end: subtract example", () => {
52
- const source = `
53
- /**
54
- * @nudo:case "positive numbers" (5, 3)
55
- * @nudo:case "negative result" (1, 10)
56
- * @nudo:case "symbolic" (T.number, T.number)
57
- */
58
- function subtract(a, b) {
59
- return a - b;
60
- }
61
- `;
62
-
63
- it("infers subtract(5, 3) => 2", () => {
64
- const results = inferFromSource(source);
65
- const c = results[0].cases[0];
66
- expect(typeValueEquals(c.result, T.literal(2))).toBe(true);
67
- });
68
-
69
- it("infers subtract(1, 10) => -9", () => {
70
- const results = inferFromSource(source);
71
- const c = results[0].cases[1];
72
- expect(typeValueEquals(c.result, T.literal(-9))).toBe(true);
73
- });
74
-
75
- it("infers subtract(number, number) => number", () => {
76
- const results = inferFromSource(source);
77
- const c = results[0].cases[2];
78
- expect(typeValueEquals(c.result, T.number)).toBe(true);
79
- });
80
- });
81
-
82
- describe("End-to-end: typeof narrowing", () => {
83
- const source = `
84
- /**
85
- * @nudo:case "with number" (42)
86
- * @nudo:case "with string" ("hello")
87
- * @nudo:case "symbolic" (T.union(T.number, T.string))
88
- */
89
- function describe(x) {
90
- if (typeof x === "number") return x + 1;
91
- return x;
92
- }
93
- `;
94
-
95
- it("infers describe(42) => 43", () => {
96
- const results = inferFromSource(source);
97
- const c = results[0].cases[0];
98
- expect(typeValueEquals(c.result, T.literal(43))).toBe(true);
99
- });
100
-
101
- it("infers describe('hello') => 'hello'", () => {
102
- const results = inferFromSource(source);
103
- const c = results[0].cases[1];
104
- expect(typeValueEquals(c.result, T.literal("hello"))).toBe(true);
105
- });
106
-
107
- it("infers describe(number | string) => number | string", () => {
108
- const results = inferFromSource(source);
109
- const c = results[0].cases[2];
110
- expect(typeValueToString(c.result)).toBe("number | string");
111
- });
112
- });
113
-
114
- describe("End-to-end: strict equality narrowing", () => {
115
- const source = `
116
- /**
117
- * @nudo:case "null case" (null)
118
- * @nudo:case "number case" (5)
119
- * @nudo:case "symbolic" (T.union(T.null, T.number))
120
- */
121
- function safe(x) {
122
- if (x === null) return 0;
123
- return x;
124
- }
125
- `;
126
-
127
- it("infers safe(null) => 0", () => {
128
- const results = inferFromSource(source);
129
- const c = results[0].cases[0];
130
- expect(typeValueEquals(c.result, T.literal(0))).toBe(true);
131
- });
132
-
133
- it("infers safe(5) => 5", () => {
134
- const results = inferFromSource(source);
135
- const c = results[0].cases[1];
136
- expect(typeValueEquals(c.result, T.literal(5))).toBe(true);
137
- });
138
-
139
- it("infers safe(null | number) => 0 | number", () => {
140
- const results = inferFromSource(source);
141
- const c = results[0].cases[2];
142
- const str = typeValueToString(c.result);
143
- // 行为已修复:吸收律生效,字面量 0 被共存的 number 吸收(原期望 "0 | number")
144
- expect(str).toBe("number");
145
- });
146
- });
147
-
148
- describe("Evaluator: basic expressions", () => {
149
- it("evaluates arithmetic", () => {
150
- const ast = parse("1 + 2 * 3");
151
- const env = createEnvironment();
152
- const result = evaluateProgram(ast, env);
153
- expect(typeValueEquals(result, T.literal(7))).toBe(true);
154
- });
155
-
156
- it("evaluates variable declaration and usage", () => {
157
- const ast = parse("const x = 10; x + 5;");
158
- const env = createEnvironment();
159
- const result = evaluateProgram(ast, env);
160
- expect(typeValueEquals(result, T.literal(15))).toBe(true);
161
- });
162
-
163
- it("evaluates ternary expression", () => {
164
- const ast = parse("true ? 1 : 2");
165
- const env = createEnvironment();
166
- const result = evaluateProgram(ast, env);
167
- expect(typeValueEquals(result, T.literal(1))).toBe(true);
168
- });
169
-
170
- it("evaluates object literal", () => {
171
- const ast = parse('const obj = { x: 1, y: "hi" }; obj.x;');
172
- const env = createEnvironment();
173
- const result = evaluateProgram(ast, env);
174
- expect(typeValueEquals(result, T.literal(1))).toBe(true);
175
- });
176
-
177
- it("evaluates array literal", () => {
178
- const ast = parse("const arr = [1, 2, 3]; arr.length;");
179
- const env = createEnvironment();
180
- const result = evaluateProgram(ast, env);
181
- expect(typeValueEquals(result, T.literal(3))).toBe(true);
182
- });
183
-
184
- it("evaluates function call", () => {
185
- const ast = parse(`
186
- function add(a, b) { return a + b; }
187
- add(3, 4);
188
- `);
189
- const env = createEnvironment();
190
- const result = evaluateProgram(ast, env);
191
- expect(typeValueEquals(result, T.literal(7))).toBe(true);
192
- });
193
-
194
- it("evaluates arrow function", () => {
195
- const ast = parse(`
196
- const double = (x) => x + x;
197
- double(5);
198
- `);
199
- const env = createEnvironment();
200
- const result = evaluateProgram(ast, env);
201
- expect(typeValueEquals(result, T.literal(10))).toBe(true);
202
- });
203
-
204
- it("resolves Array.prototype.constructor === Array literally", () => {
205
- const ast = parse(`
206
- const proto = Object.getPrototypeOf([1]);
207
- const ctor = proto.constructor;
208
- ctor === Array;
209
- `);
210
- const env = createEnvironment();
211
- const result = evaluateProgram(ast, env);
212
- expect(typeValueEquals(result, T.literal(true))).toBe(true);
213
- });
214
-
215
- it("constructs through a reflected constructor: new (proto.constructor)(3) is unknown[]", () => {
216
- const ast = parse(`
217
- const proto = Object.getPrototypeOf([1]);
218
- const C = proto.constructor;
219
- const arr = new C(3);
220
- arr;
221
- `);
222
- const env = createEnvironment();
223
- const result = evaluateProgram(ast, env);
224
- expect(typeValueToString(result)).toBe("unknown[]");
225
- });
226
-
227
- it("spreads a Set instance into an array literal element-wise", () => {
228
- const ast = parse(`
229
- const s = new Set([1, 2]);
230
- const spread = [...s];
231
- spread;
232
- `);
233
- const env = createEnvironment();
234
- const result = evaluateProgram(ast, env);
235
- expect(typeValueToString(result)).toBe("[1, 2]");
236
- });
237
-
238
- it("evaluates logical operators", () => {
239
- const ast = parse("true && 42");
240
- const env = createEnvironment();
241
- const result = evaluateProgram(ast, env);
242
- expect(typeValueEquals(result, T.literal(42))).toBe(true);
243
-
244
- const ast2 = parse("false || 99");
245
- const result2 = evaluateProgram(ast2, createEnvironment());
246
- expect(typeValueEquals(result2, T.literal(99))).toBe(true);
247
- });
248
-
249
- it("evaluates nullish coalescing", () => {
250
- const ast = parse("null ?? 42");
251
- const env = createEnvironment();
252
- const result = evaluateProgram(ast, env);
253
- expect(typeValueEquals(result, T.literal(42))).toBe(true);
254
-
255
- const ast2 = parse("1 ?? 42");
256
- const result2 = evaluateProgram(ast2, createEnvironment());
257
- expect(typeValueEquals(result2, T.literal(1))).toBe(true);
258
- });
259
-
260
- it("evaluates unary operators", () => {
261
- const ast = parse("typeof 42");
262
- const env = createEnvironment();
263
- const result = evaluateProgram(ast, env);
264
- expect(typeValueEquals(result, T.literal("number"))).toBe(true);
265
-
266
- const ast2 = parse("!true");
267
- const result2 = evaluateProgram(ast2, createEnvironment());
268
- expect(typeValueEquals(result2, T.literal(false))).toBe(true);
269
-
270
- const ast3 = parse("-5");
271
- const result3 = evaluateProgram(ast3, createEnvironment());
272
- expect(typeValueEquals(result3, T.literal(-5))).toBe(true);
273
- });
274
-
275
- it("evaluates template literal", () => {
276
- const ast = parse("`hello`");
277
- const env = createEnvironment();
278
- const result = evaluateProgram(ast, env);
279
- expect(typeValueEquals(result, T.literal("hello"))).toBe(true);
280
- });
281
-
282
- it("collects method dispatch failure on number receiver via setUnknownCollector", () => {
283
- const ast = parse("function f(n) { return n.toUpperCase(); }");
284
- const fnNode = (ast.program.body as any[]).find((s) => s.type === "FunctionDeclaration");
285
- const records: UnknownRecord[] = [];
286
- setUnknownCollector((r) => records.push(r));
287
- try {
288
- const result = evaluateFunctionFull(fnNode, [T.number], createEnvironment());
289
- // Evaluation behavior unchanged: still degrades to unknown
290
- expect(result.value.kind).toBe("unknown");
291
- const methodRecords = records.filter((r) => r.kind === "method");
292
- expect(methodRecords.length).toBeGreaterThanOrEqual(1);
293
- const rec = methodRecords.find((r) => r.name === "toUpperCase");
294
- expect(rec).toBeDefined();
295
- expect(rec!.receiverType?.kind).toBe("primitive");
296
- expect((rec!.receiverType as any).type).toBe("number");
297
- expect(rec!.reason).toBe("no method 'toUpperCase' on primitive");
298
- expect(rec!.loc).toBeDefined();
299
- expect(typeof rec!.loc!.line).toBe("number");
300
- expect(typeof rec!.loc!.column).toBe("number");
301
- } finally {
302
- setUnknownCollector(null);
303
- }
304
- });
305
-
306
- it("collects method dispatch failure even when receiver is unknown", () => {
307
- const ast = parse("function f(n) { return n.toUpperCase(); }");
308
- const fnNode = (ast.program.body as any[]).find((s) => s.type === "FunctionDeclaration");
309
- const records: UnknownRecord[] = [];
310
- setUnknownCollector((r) => records.push(r));
311
- try {
312
- const result = evaluateFunctionFull(fnNode, [T.unknown], createEnvironment());
313
- expect(result.value.kind).toBe("unknown");
314
- const rec = records.find((r) => r.kind === "method" && r.name === "toUpperCase");
315
- expect(rec).toBeDefined();
316
- expect(rec!.receiverType?.kind).toBe("unknown");
317
- } finally {
318
- setUnknownCollector(null);
319
- }
320
- });
321
-
322
- it("does not record method failures for valid string methods", () => {
323
- const ast = parse('function g(s) { return s.toUpperCase(); }');
324
- const fnNode = (ast.program.body as any[]).find((s) => s.type === "FunctionDeclaration");
325
- const records: UnknownRecord[] = [];
326
- setUnknownCollector((r) => records.push(r));
327
- try {
328
- const result = evaluateFunctionFull(fnNode, [T.string], createEnvironment());
329
- expect(typeValueEquals(result.value, T.string)).toBe(true);
330
- expect(records.filter((r) => r.name === "toUpperCase")).toHaveLength(0);
331
- } finally {
332
- setUnknownCollector(null);
333
- }
334
- });
335
-
336
- it("collects unknown global identifiers as kind 'global'", () => {
337
- const ast = parse("function h() { return Foo(); }");
338
- const fnNode = (ast.program.body as any[]).find((s) => s.type === "FunctionDeclaration");
339
- const records: UnknownRecord[] = [];
340
- setUnknownCollector((r) => records.push(r));
341
- try {
342
- evaluateFunctionFull(fnNode, [], createEnvironment());
343
- const rec = records.find((r) => r.kind === "global" && r.name === "Foo");
344
- expect(rec).toBeDefined();
345
- expect(rec!.reason).toBe("unknown global identifier 'Foo'");
346
- } finally {
347
- setUnknownCollector(null);
348
- }
349
- });
350
-
351
- it("behaves identically without a collector installed (regression)", () => {
352
- const ast = parse("function f(n) { return n.toUpperCase(); }");
353
- const fnNode = (ast.program.body as any[]).find((s) => s.type === "FunctionDeclaration");
354
- setUnknownCollector(null);
355
- const result = evaluateFunctionFull(fnNode, [T.number], createEnvironment());
356
- expect(result.value.kind).toBe("unknown");
357
- });
358
-
359
- it("attaches origin pointing at the call-site argument when provenance tracking is enabled", () => {
360
- // 42 is on line 5, column 20 (0-based) of this source.
361
- const source = `
362
- function badNum(n) {
363
- return n.toUpperCase();
364
- }
365
- const boom = badNum(42);
366
- `;
367
- const ast = parse(source);
368
- const records: UnknownRecord[] = [];
369
- setUnknownCollector((r) => records.push(r));
370
- setProvenanceTracking(true);
371
- try {
372
- evaluateProgram(ast, createEnvironment());
373
- const rec = records.find((r) => r.kind === "method" && r.name === "toUpperCase");
374
- expect(rec).toBeDefined();
375
- expect(rec!.receiverType?.kind).toBe("literal");
376
- expect((rec!.receiverType as any).value).toBe(42);
377
- expect(rec!.origin).toEqual({ line: 5, column: 20 });
378
- } finally {
379
- setProvenanceTracking(false);
380
- setUnknownCollector(null);
381
- }
382
- });
383
-
384
- it("emits no origin when provenance tracking is disabled (default)", () => {
385
- const source = `
386
- function badNum(n) {
387
- return n.toUpperCase();
388
- }
389
- const boom = badNum(42);
390
- `;
391
- const ast = parse(source);
392
- const records: UnknownRecord[] = [];
393
- setUnknownCollector((r) => records.push(r));
394
- setProvenanceTracking(false);
395
- try {
396
- evaluateProgram(ast, createEnvironment());
397
- const rec = records.find((r) => r.kind === "method" && r.name === "toUpperCase");
398
- expect(rec).toBeDefined();
399
- expect(rec!.origin).toBeUndefined();
400
- } finally {
401
- setUnknownCollector(null);
402
- }
403
- });
404
-
405
- it("tags call records of imported functions with targetModule/targetExport", () => {
406
- const dir = mkdtempSync(join(tmpdir(), "nudo-export-tag-"));
407
- try {
408
- const bPath = join(dir, "b.js");
409
- const aPath = join(dir, "a.js");
410
- writeFileSync(bPath, "export function triple(x) { return x * 3; }\n");
411
- writeFileSync(
412
- aPath,
413
- 'import { triple } from "./b.js";\nfunction localQuad(y) { return y * 4; }\nconst r1 = triple(4);\nconst r2 = localQuad(2);\n',
414
- );
415
-
416
- setModuleResolver((source) => {
417
- if (source === "./b.js") {
418
- return { ast: parse(readFileSync(bPath, "utf8")), filePath: bPath };
419
- }
420
- return null;
421
- });
422
- setCurrentFileDir(dir);
423
-
424
- const records: CallRecord[] = [];
425
- setCallCollector((r) => records.push(r));
426
- try {
427
- evaluateProgram(parse(readFileSync(aPath, "utf8")), createEnvironment());
428
- } finally {
429
- setCallCollector(null);
430
- setModuleResolver(null);
431
- setCurrentFileDir("");
432
- }
433
-
434
- const imported = records.find((r) => r.fnName === "triple");
435
- expect(imported).toBeDefined();
436
- expect(imported!.targetModule).toBe(bPath);
437
- expect(imported!.targetModule).toContain("b.js");
438
- expect(imported!.targetExport).toBe("triple");
439
- expect(typeValueEquals(imported!.resultType, T.literal(12))).toBe(true);
440
-
441
- const local = records.find((r) => r.fnName === "localQuad");
442
- expect(local).toBeDefined();
443
- expect(local!.targetModule).toBeUndefined();
444
- expect(local!.targetExport).toBeUndefined();
445
- expect(typeValueEquals(local!.resultType, T.literal(8))).toBe(true);
446
- } finally {
447
- rmSync(dir, { recursive: true, force: true });
448
- }
449
- });
450
-
451
- it("types X.prototype on built-in classes as instances without error-level unknowns", () => {
452
- const records: UnknownRecord[] = [];
453
- setUnknownCollector((r) => records.push(r));
454
- const env = createEnvironment();
455
- try {
456
- const ast = parse(`
457
- const d = Date.prototype;
458
- const a = Array.prototype;
459
- const e = Error.prototype;
460
- const m = Map.prototype;
461
- const o = Object.prototype;
462
- `);
463
- evaluateProgram(ast, env);
464
- } finally {
465
- setUnknownCollector(null);
466
- }
467
-
468
- const cases: Array<[string, string]> = [
469
- ["d", "Date"],
470
- ["a", "Array"],
471
- ["e", "Error"],
472
- ["m", "Map"],
473
- ["o", "Object"],
474
- ];
475
- for (const [binding, className] of cases) {
476
- const tv = env.lookup(binding);
477
- expect(tv.kind).toBe("instance");
478
- if (tv.kind === "instance") {
479
- expect(tv.className).toBe(className);
480
- }
481
- }
482
- // Error-level diagnostics come from property/method unknowns with a
483
- // concrete receiver — there must be none for `.prototype` access.
484
- expect(records.filter((r) => r.kind === "property" || r.kind === "method")).toEqual([]);
485
- });
486
-
487
- it("evaluates unrecognized built-in identifiers to unknown (not undefined)", () => {
488
- const ast = parse(`const u = WeakRef;\n`);
489
- const env = createEnvironment();
490
- evaluateProgram(ast, env);
491
- expect(env.lookup("u").kind).toBe("unknown");
492
- });
493
-
494
- it("types Object.prototype.toString.call(x) as a brand literal and never leaks native JS members", () => {
495
- const ast = parse(`
496
- const brand = Object.prototype.toString.call(5);
497
- const direct = Object.prototype.toString;
498
- const plain = ({}).toString();
499
- const mapStr = new Map().toString;
500
- `);
501
- const env = createEnvironment();
502
- evaluateProgram(ast, env);
503
- expect(typeValueToString(env.lookup("brand"))).toBe('"[object Number]"');
504
- expect(env.lookup("direct").kind).toBe("function");
505
- expect(typeValueToString(env.lookup("plain"))).toBe('"[object Object]"');
506
- expect(env.lookup("mapStr").kind).toBe("function");
507
- // The bug this locks down: property records are plain objects, so bracket
508
- // access used to return the real native JS toString (a non-TypeValue).
509
- for (const name of ["brand", "direct", "plain", "mapStr"]) {
510
- const tv = env.lookup(name) as unknown as { kind?: string };
511
- expect(typeof tv.kind).toBe("string");
512
- }
513
- });
514
-
515
- it("tags call records of require()d CJS functions with targetModule/targetExport", () => {
516
- const dir = mkdtempSync(join(tmpdir(), "nudo-cjs-tag-"));
517
- try {
518
- const bPath = join(dir, "b.js");
519
- const cPath = join(dir, "c.js");
520
- const aPath = join(dir, "a.js");
521
- writeFileSync(bPath, 'function triple(x) { return x * 3; }\nmodule.exports = { triple };\n');
522
- writeFileSync(cPath, 'module.exports = function (x) { return x; };\n');
523
- writeFileSync(
524
- aPath,
525
- 'const b = require("./b");\nconst triple = b.triple;\nconst r1 = triple(4);\nconst id = require("./c");\nconst r2 = id(9);\n',
526
- );
527
-
528
- setModuleResolver((source) => {
529
- if (source === "./b") {
530
- return { ast: parse(readFileSync(bPath, "utf8")), filePath: bPath };
531
- }
532
- if (source === "./c") {
533
- return { ast: parse(readFileSync(cPath, "utf8")), filePath: cPath };
534
- }
535
- return null;
536
- });
537
- setCurrentFileDir(dir);
538
-
539
- const records: CallRecord[] = [];
540
- setCallCollector((r) => records.push(r));
541
- try {
542
- evaluateProgram(parse(readFileSync(aPath, "utf8")), createEnvironment());
543
- } finally {
544
- setCallCollector(null);
545
- setModuleResolver(null);
546
- setCurrentFileDir("");
547
- }
548
-
549
- const named = records.find((r) => r.fnName === "triple");
550
- expect(named).toBeDefined();
551
- expect(named!.targetModule).toBe(bPath);
552
- expect(named!.targetExport).toBe("triple");
553
- expect(typeValueEquals(named!.resultType, T.literal(12))).toBe(true);
554
-
555
- const main = records.find((r) => r.fnName === "id");
556
- expect(main).toBeDefined();
557
- expect(main!.targetModule).toBe(cPath);
558
- expect(main!.targetExport).toBe("default");
559
- expect(typeValueEquals(main!.resultType, T.literal(9))).toBe(true);
560
- } finally {
561
- rmSync(dir, { recursive: true, force: true });
562
- }
563
- });
564
-
565
- it("keeps definition-site attribution through re-export chains and accumulates export aliases", () => {
566
- const dir = mkdtempSync(join(tmpdir(), "nudo-reexport-tag-"));
567
- try {
568
- const defPath = join(dir, "parse-chunked.js");
569
- const indexPath = join(dir, "index.js");
570
- const libPath = join(dir, "lib.js");
571
- const testPath = join(dir, "test.js");
572
- writeFileSync(defPath, "module.exports = function (x) { return x + 1; };\n");
573
- // barrel:属性名转发(jsonext index.js 形态)
574
- writeFileSync(indexPath, "module.exports = { parseChunked: require('./parse-chunked.js') };\n");
575
- // CJS 转发 shim(test/helpers/lib.js 形态)
576
- writeFileSync(libPath, "module.exports = require('./index.js');\n");
577
- writeFileSync(testPath, "const { parseChunked } = require('./lib.js');\nconst r = parseChunked(4);\n");
578
-
579
- const bySpec = new Map<string, string>([
580
- ["./lib.js", libPath],
581
- ["./index.js", indexPath],
582
- ["./parse-chunked.js", defPath],
583
- ]);
584
- setModuleResolver((source) => {
585
- const p = bySpec.get(source);
586
- return p ? { ast: parse(readFileSync(p, "utf8")), filePath: p } : null;
587
- });
588
- setCurrentFileDir(dir);
589
-
590
- const records: CallRecord[] = [];
591
- setCallCollector((r) => records.push(r));
592
- try {
593
- evaluateProgram(parse(readFileSync(testPath, "utf8")), createEnvironment());
594
- } finally {
595
- setCallCollector(null);
596
- setModuleResolver(null);
597
- setCurrentFileDir("");
598
- }
599
-
600
- const rec = records.find((r) => r.fnName === "parseChunked");
601
- expect(rec).toBeDefined();
602
- // 归属仍是定义文件,而非中转的 index.js / lib.js
603
- expect(rec!.targetModule).toBe(defPath);
604
- expect(rec!.targetModule).not.toBe(indexPath);
605
- expect(rec!.targetModule).not.toBe(libPath);
606
- expect(rec!.targetExport).toBe("default");
607
- // 中转出现的导出名进入 aliases(去重后)
608
- expect(rec!.targetAliases).toContain("parseChunked");
609
- expect(typeValueEquals(rec!.resultType, T.literal(5))).toBe(true);
610
- } finally {
611
- rmSync(dir, { recursive: true, force: true });
612
- }
613
- });
614
-
615
- it("evaluates imported calls identically with no call collector installed (regression)", () => {
616
- const dir = mkdtempSync(join(tmpdir(), "nudo-export-tag-"));
617
- try {
618
- const bPath = join(dir, "b.js");
619
- const aPath = join(dir, "a.js");
620
- writeFileSync(bPath, "export function triple(x) { return x * 3; }\n");
621
- writeFileSync(aPath, 'import { triple } from "./b.js";\nconst r1 = triple(4);\n');
622
-
623
- setModuleResolver((source) => {
624
- if (source === "./b.js") {
625
- return { ast: parse(readFileSync(bPath, "utf8")), filePath: bPath };
626
- }
627
- return null;
628
- });
629
- setCurrentFileDir(dir);
630
-
631
- try {
632
- const env = createEnvironment();
633
- evaluateProgram(parse(readFileSync(aPath, "utf8")), env);
634
- expect(typeValueEquals(env.lookup("r1"), T.literal(12))).toBe(true);
635
- } finally {
636
- setModuleResolver(null);
637
- setCurrentFileDir("");
638
- }
639
- } finally {
640
- rmSync(dir, { recursive: true, force: true });
641
- }
642
- });
643
-
644
- it("terminates self-recursive functions via the call-depth budget instead of overflowing the stack", () => {
645
- const records: UnknownRecord[] = [];
646
- setUnknownCollector((r) => records.push(r));
647
- const env = createEnvironment();
648
- try {
649
- // Isomorphic to hoek's clone(): under abstract args `seen.has(obj)`
650
- // cannot concretely short-circuit, so the recursion must be cut off by
651
- // MAX_CALL_DEPTH and soundly degrade to unknown.
652
- evaluateProgram(parse(`
653
- function clone(obj, seen = new Map()) {
654
- if (obj === null) return obj;
655
- if (seen.has(obj)) return obj;
656
- return clone(obj, seen);
657
- }
658
- const c = clone({ a: 1 });
659
- `), env);
660
- } finally {
661
- setUnknownCollector(null);
662
- }
663
-
664
- const c = env.lookup("c");
665
- const members = c.kind === "union" ? c.members : [c];
666
- expect(members.some((m) => m.kind === "unknown")).toBe(true);
667
- expect(records.some((r) => r.kind === "global" && r.name === "recursion:clone")).toBe(true);
668
- });
669
-
670
- it("supports Function.prototype.call/apply/bind on function values", () => {
671
- const records: UnknownRecord[] = [];
672
- setUnknownCollector((r) => records.push(r));
673
- const env = createEnvironment();
674
- try {
675
- evaluateProgram(parse(`
676
- function fn(x) { return x * 2; }
677
- const a = fn.call(null, 5);
678
- const b = fn.apply(null, [6]);
679
- const g = fn.bind(null, 7);
680
- `), env);
681
- } finally {
682
- setUnknownCollector(null);
683
- }
684
-
685
- expect(typeValueEquals(env.lookup("a"), T.literal(10))).toBe(true);
686
- expect(typeValueEquals(env.lookup("b"), T.literal(12))).toBe(true);
687
- // bind approximates to the original function value
688
- expect(env.lookup("g").kind).toBe("function");
689
- expect(records.filter((r) => r.kind === "method" || r.kind === "property")).toEqual([]);
690
- });
691
-
692
- it("types Object.prototype.toString.call(obj) as brand literals without error-level unknowns", () => {
693
- const records: UnknownRecord[] = [];
694
- setUnknownCollector((r) => records.push(r));
695
- const env = createEnvironment();
696
- try {
697
- evaluateProgram(parse(`
698
- const s = Object.prototype.toString.call({ a: 1 });
699
- const u = Object.prototype.toString.call(5);
700
- `), env);
701
- } finally {
702
- setUnknownCollector(null);
703
- }
704
-
705
- expect(typeValueEquals(env.lookup("s"), T.literal("[object Object]"))).toBe(true);
706
- expect(typeValueEquals(env.lookup("u"), T.literal("[object Number]"))).toBe(true);
707
- expect(records.filter((r) => r.kind === "method" || r.kind === "property")).toEqual([]);
708
- });
709
-
710
- it("analyzes deep non-recursive call chains within the depth budget", () => {
711
- // 50 alternating a->b->a calls plus the leaf: depth 51 <= MAX_CALL_DEPTH,
712
- // so nothing is truncated and the concrete result survives exactly.
713
- const pairs = 25;
714
- let source = "function leaf(x) { return x * 2; }\n";
715
- for (let i = 0; i < pairs; i++) {
716
- source += `function a${i}(x) { return b${i}(x); }\n`;
717
- source += `function b${i}(x) { return ${i < pairs - 1 ? `a${i + 1}` : "leaf"}(x); }\n`;
718
- }
719
- source += "const deep = a0(5);\n";
720
-
721
- const env = createEnvironment();
722
- evaluateProgram(parse(source), env);
723
- expect(typeValueEquals(env.lookup("deep"), T.literal(10))).toBe(true);
724
- });
725
-
726
- it("binds the receiver of obj.f() as the callee's this", () => {
727
- const env = createEnvironment();
728
- evaluateProgram(parse(`
729
- const o = { n: 1 };
730
- o.f = function () { return this.n; };
731
- const r1 = o.f();
732
- const o2 = { n: 2, f() { return this.n; } };
733
- const r2 = o2.f();
734
- `), env);
735
- expect(typeValueEquals(env.lookup("r1"), T.literal(1))).toBe(true);
736
- expect(typeValueEquals(env.lookup("r2"), T.literal(2))).toBe(true);
737
- });
738
-
739
- it("passes the thisArg of f.call(obj, ...) into the callee", () => {
740
- const env = createEnvironment();
741
- evaluateProgram(parse(`
742
- function g() { return this.n; }
743
- const r = g.call({ n: 5 });
744
- `), env);
745
- expect(typeValueEquals(env.lookup("r"), T.literal(5))).toBe(true);
746
- });
747
-
748
- it("degrades unbound this to unknown (not undefined) in receiver-less calls", () => {
749
- const records: UnknownRecord[] = [];
750
- setUnknownCollector((r) => records.push(r));
751
- const env = createEnvironment();
752
- try {
753
- // this-style function called without a receiver (as in a
754
- // callsite-synthesized entry case): `this.x` must degrade to
755
- // unknown-receiver records (warnings), never "on type 'undefined'".
756
- evaluateProgram(parse(`
757
- function push() {
758
- this.push(this._stack.value);
759
- return 1;
760
- }
761
- const r = push();
762
- `), env);
763
- } finally {
764
- setUnknownCollector(null);
765
- }
766
- expect(typeValueEquals(env.lookup("r"), T.literal(1))).toBe(true);
767
- const receiverKinds = records
768
- .filter((r) => r.kind === "method" || r.kind === "property")
769
- .map((r) => r.receiverType?.kind);
770
- expect(receiverKinds.length).toBeGreaterThan(0);
771
- expect(receiverKinds.every((k) => k === "unknown")).toBe(true);
772
- });
773
-
774
- it("keeps constructor this bindings intact for new expressions", () => {
775
- const env = createEnvironment();
776
- evaluateProgram(parse(`
777
- class A { constructor() { this.x = 42; } }
778
- const a = new A();
779
- const r = a.x;
780
- `), env);
781
- expect(typeValueEquals(env.lookup("r"), T.literal(42))).toBe(true);
782
- });
783
-
784
- it("caches X.prototype as an identity-stable singleton instance", () => {
785
- const env = createEnvironment();
786
- evaluateProgram(parse(`
787
- const a = Array.prototype;
788
- const b = Array.prototype;
789
- const r1 = a === b;
790
- const r2 = Array.prototype === Array.prototype;
791
- const r3 = Object.prototype === Map.prototype;
792
- `), env);
793
- const proto = env.lookup("a");
794
- expect(proto.kind).toBe("instance");
795
- if (proto.kind === "instance") expect(proto.className).toBe("Array");
796
- expect(typeValueEquals(env.lookup("r1"), T.literal(true))).toBe(true);
797
- expect(typeValueEquals(env.lookup("r2"), T.literal(true))).toBe(true);
798
- expect(typeValueEquals(env.lookup("r3"), T.literal(false))).toBe(true);
799
- });
800
-
801
- it("compares getPrototypeOf results against prototype singletons literally", () => {
802
- const env = createEnvironment();
803
- evaluateProgram(parse(`
804
- const o = { n: 1 };
805
- const same = Object.getPrototypeOf(o) === Object.prototype;
806
- const diff = Object.getPrototypeOf(o) === Date.prototype;
807
- `), env);
808
- expect(typeValueEquals(env.lookup("same"), T.literal(true))).toBe(true);
809
- expect(typeValueEquals(env.lookup("diff"), T.literal(false))).toBe(true);
810
- });
811
-
812
- it("Array.isArray decides literally for structurally-known receivers", () => {
813
- const env = createEnvironment();
814
- evaluateProgram(parse(`
815
- const a = Array.isArray([1, 2]);
816
- const b = Array.isArray({ x: 1 });
817
- const c = Array.isArray(new Map());
818
- `), env);
819
- expect(typeValueEquals(env.lookup("a"), T.literal(true))).toBe(true);
820
- expect(typeValueEquals(env.lookup("b"), T.literal(false))).toBe(true);
821
- expect(typeValueEquals(env.lookup("c"), T.literal(false))).toBe(true);
822
- });
823
-
824
- it("instanceof decides literally for structurally-known receivers", () => {
825
- const env = createEnvironment();
826
- evaluateProgram(parse(`
827
- const a = ({ x: 1 }) instanceof Date;
828
- const b = [1] instanceof Array;
829
- const c = [1] instanceof Object;
830
- const d = ({ x: 1 }) instanceof Object;
831
- `), env);
832
- expect(typeValueEquals(env.lookup("a"), T.literal(false))).toBe(true);
833
- expect(typeValueEquals(env.lookup("b"), T.literal(true))).toBe(true);
834
- expect(typeValueEquals(env.lookup("c"), T.literal(true))).toBe(true);
835
- expect(typeValueEquals(env.lookup("d"), T.literal(true))).toBe(true);
836
- });
837
-
838
- it("&& / || short-circuit on always-truthy object-ish operands", () => {
839
- const env = createEnvironment();
840
- evaluateProgram(parse(`
841
- const bools = [true, false];
842
- const r1 = bools && bools.length;
843
- const r2 = null || 'fallback';
844
- const r3 = bools || [];
845
- `), env);
846
- expect(typeValueEquals(env.lookup("r1"), T.literal(2))).toBe(true);
847
- expect(typeValueEquals(env.lookup("r2"), T.literal("fallback"))).toBe(true);
848
- // bools is a tuple (always truthy): || keeps the left side
849
- expect(env.lookup("r3").kind).toBe("tuple");
850
- });
851
-
852
- it("key-set access: uniform object values collapse to the shared type", () => {
853
- // All property values share one structure — obj[key] answers with the
854
- // common type no matter which string key arrives at runtime.
855
- const results = inferFromSource(`
856
- /** @nudo:case "uniform" ({ a: T.number, b: T.number, c: T.number }, T.string) */
857
- function pick(obj, key) { return obj[key]; }
858
- `);
859
- expect(typeValueEquals(results[0].cases[0].result, T.number)).toBe(true);
860
- });
861
-
862
- it("key-set access: heterogeneous object values union", () => {
863
- const results = inferFromSource(`
864
- /** @nudo:case "mixed" ({ a: 1, b: "x" }, T.string) */
865
- function pick(obj, key) { return obj[key]; }
866
- `);
867
- const str = typeValueToString(results[0].cases[0].result);
868
- expect(str).toContain("1");
869
- expect(str).toContain('"x"');
870
- });
871
-
872
- it("key-set access: dynamic tuple index unions the elements", () => {
873
- const results = inferFromSource(`
874
- /** @nudo:case "index" (["a", "b"], T.number) */
875
- function at(path, i) { return path[i]; }
876
- `);
877
- const str = typeValueToString(results[0].cases[0].result);
878
- expect(str).toContain('"a"');
879
- expect(str).toContain('"b"');
880
- });
881
- });
882
-
883
- describe("Function-valued callback args through builtin array HOFs", () => {
884
- it("union receiver map: each array member flows through the named callback", () => {
885
- const results = inferFromSource(`
886
- // @nudo:case "go" ()
887
- function f() {
888
- const xs = Math.random() > 0.5 ? [1, 2] : [3, 4, 5];
889
- function double(x) { return x * 2; }
890
- return xs.map(double);
891
- }
892
- `);
893
- expect(typeValueToString(results[0].cases[0].result)).toBe("[2, 4] | [6, 8, 10]");
894
- });
895
-
896
- it("union receiver filter/reduce/some/every/forEach with named callbacks", () => {
897
- const results = inferFromSource(`
898
- // @nudo:case "go" ()
899
- function f() {
900
- const xs = Math.random() > 0.5 ? [1, 2] : [3, 4, 5];
901
- function big(x) { return x > 2; }
902
- function add(a, b) { return a + b; }
903
- const kept = xs.filter(big);
904
- const total = xs.reduce(add, 0);
905
- const any = xs.some(big);
906
- const all = xs.every(big);
907
- xs.forEach(big);
908
- return [kept, total, any, all];
909
- }
910
- `);
911
- const str = typeValueToString(results[0].cases[0].result);
912
- // filter 不丢元素类型([] 与 3|4|5 数组)、reduce 按成员累计(3 | 12)、
913
- // some/every 保持布尔语义
914
- expect(str).toContain("3");
915
- expect(str).toContain("12");
916
- expect(str).toContain("false");
917
- expect(str).toContain("true");
918
- });
919
-
920
- it("union-of-functions callback distributes over each signature", () => {
921
- const results = inferFromSource(`
922
- // @nudo:case "go" ()
923
- function f() {
924
- function double(x) { return x * 2; }
925
- function triple(x) { return x * 3; }
926
- const fn = Math.random() > 0.5 ? double : triple;
927
- return [1, 2].map(fn);
928
- }
929
- `);
930
- expect(typeValueToString(results[0].cases[0].result)).toBe("[2, 4] | [3, 6]");
931
- });
932
-
933
- it("unknown callback (no signature info) stays unknown by design", () => {
934
- const results = inferFromSource(`
935
- // @nudo:case "xs" ([1, 2, 3])
936
- function hof(xs, fn) {
937
- return xs.map(fn);
938
- }
939
- `);
940
- // 已知设计限制:回调无任何类型信息时不伪造类型
941
- expect(typeValueToString(results[0].cases[0].result)).toBe("[unknown, unknown, unknown]");
942
- });
943
- });