@nudojs/service 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/index.d.ts +502 -4
  3. package/dist/index.js +3017 -212
  4. package/package.json +5 -4
  5. package/src/__tests__/abs-bindings.test.ts +53 -0
  6. package/src/__tests__/abs-call-records.test.ts +33 -0
  7. package/src/__tests__/abs-calls.test.ts +59 -0
  8. package/src/__tests__/abs-module-cache.test.ts +121 -0
  9. package/src/__tests__/abs-modules-graph.test.ts +100 -0
  10. package/src/__tests__/abs-typevalue-diff.test.ts +111 -0
  11. package/src/__tests__/analyzer.test.ts +71 -3
  12. package/src/__tests__/bpath-arg-provenance.test.ts +104 -0
  13. package/src/__tests__/bpath-as.test.ts +54 -0
  14. package/src/__tests__/bpath-cache-key.test.ts +33 -0
  15. package/src/__tests__/bpath-calls.test.ts +55 -0
  16. package/src/__tests__/bpath-diagnostics.test.ts +78 -0
  17. package/src/__tests__/bpath-env-modules.test.ts +30 -0
  18. package/src/__tests__/bpath-env.test.ts +80 -0
  19. package/src/__tests__/bpath-hosted-skip.test.ts +71 -0
  20. package/src/__tests__/bpath-import-default-ns.test.ts +106 -0
  21. package/src/__tests__/bpath-method-dedupe.test.ts +65 -0
  22. package/src/__tests__/bpath-method-diag.test.ts +45 -0
  23. package/src/__tests__/bpath-module-diags.test.ts +101 -0
  24. package/src/__tests__/bpath-path-env.test.ts +46 -0
  25. package/src/__tests__/bpath-reexport.test.ts +131 -0
  26. package/src/__tests__/bpath-replace.test.ts +52 -0
  27. package/src/__tests__/bpath-require.test.ts +66 -0
  28. package/src/__tests__/bpath-run.test.ts +109 -0
  29. package/src/__tests__/cjs-require.test.ts +54 -0
  30. package/src/__tests__/diagnostics.test.ts +0 -30
  31. package/src/__tests__/fn-cache-evict.test.ts +56 -0
  32. package/src/__tests__/harvest-auto.test.ts +43 -0
  33. package/src/__tests__/harvest-node.test.ts +24 -0
  34. package/src/__tests__/harvest-package.test.ts +38 -0
  35. package/src/__tests__/harvest-to-abs.test.ts +46 -0
  36. package/src/__tests__/hover-intension.test.ts +39 -0
  37. package/src/__tests__/infer-json.test.ts +35 -0
  38. package/src/__tests__/integration.test.ts +89 -0
  39. package/src/__tests__/m3-intension.test.ts +28 -0
  40. package/src/__tests__/mock-seed-fingerprint.test.ts +64 -0
  41. package/src/__tests__/session-cache-evict.test.ts +90 -0
  42. package/src/__tests__/static-imports.test.ts +44 -0
  43. package/src/abs-modules-graph.ts +387 -0
  44. package/src/analysis-file-cache.ts +51 -0
  45. package/src/analyzer.ts +1416 -124
  46. package/src/bpath-diagnostics.ts +283 -0
  47. package/src/bpath-run.ts +409 -0
  48. package/src/case-emitter.ts +2 -0
  49. package/src/dts-generator.ts +3 -3
  50. package/src/env-to-abs.ts +57 -0
  51. package/src/fn-analysis-cache.ts +119 -0
  52. package/src/guard-generator.ts +8 -1
  53. package/src/harvest-auto.ts +86 -0
  54. package/src/harvest-node.ts +48 -0
  55. package/src/harvest-package.ts +204 -0
  56. package/src/harvest-to-abs.ts +129 -0
  57. package/src/index.ts +104 -1
  58. package/src/infer-json.ts +137 -0
  59. package/src/load-module.ts +34 -0
  60. package/src/mock-abs.ts +275 -0
  61. package/src/semantic-tokens.ts +216 -0
  62. package/src/session-cache.ts +50 -0
  63. package/src/static-imports.ts +241 -0
  64. package/src/target-path.ts +17 -0
@@ -0,0 +1,45 @@
1
+ import { describe, it, expect, afterAll } from "vitest";
2
+ import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { analyzeFile, clearBPathCache, tryBPathCallFull } from "@nudojs/service";
6
+ import { $lit } from "@nudojs/core";
7
+
8
+ const dirs: string[] = [];
9
+ afterAll(() => {
10
+ for (const d of dirs) rmSync(d, { recursive: true, force: true });
11
+ });
12
+
13
+ describe("B-path method-missing diagnostics", () => {
14
+ it("records no-method on number receiver via B execution", () => {
15
+ clearBPathCache();
16
+ const dir = mkdtempSync(join(tmpdir(), "nudo-md-"));
17
+ dirs.push(dir);
18
+ const src = `
19
+ export function badNum(n) {
20
+ return n.toUpperCase();
21
+ }
22
+ `;
23
+ const p = join(dir, "m.js");
24
+ writeFileSync(p, src, "utf-8");
25
+ const full = tryBPathCallFull(src, p, "badNum", [$lit(42)]);
26
+ expect(full).toBeDefined();
27
+ expect(full!.memberDiags?.length).toBeGreaterThanOrEqual(1);
28
+ expect(full!.memberDiags![0]!.name).toBe("toUpperCase");
29
+ expect(full!.memberDiags![0]!.receiver).toBe("number");
30
+ });
31
+
32
+ it("analyzeFile still reports no-method for top-level call", () => {
33
+ clearBPathCache();
34
+ const source = `
35
+ function badNum(n) {
36
+ return n.toUpperCase();
37
+ }
38
+ const boom = badNum(42);
39
+ `;
40
+ const result = analyzeFile("/test/badnum2.js", source);
41
+ const diags = result.diagnostics.filter((d) => d.code === "nudo:no-method");
42
+ expect(diags.length).toBeGreaterThanOrEqual(1);
43
+ expect(diags[0]!.message).toContain("toUpperCase");
44
+ });
45
+ });
@@ -0,0 +1,101 @@
1
+ import { it, expect, describe, afterAll } from "vitest";
2
+ import { analyzeFile, clearBPathCache, evalAbsModuleGraph } from "@nudojs/service";
3
+ import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+
7
+ const dirs: string[] = [];
8
+ afterAll(() => {
9
+ for (const d of dirs) rmSync(d, { recursive: true, force: true });
10
+ });
11
+
12
+ function setup(files: Record<string, string>) {
13
+ const dir = mkdtempSync(join(tmpdir(), "nudo-bdiag-"));
14
+ dirs.push(dir);
15
+ writeFileSync(join(dir, "package.json"), JSON.stringify({ name: "bdiag", version: "1.0.0" }));
16
+ for (const [name, src] of Object.entries(files)) {
17
+ writeFileSync(join(dir, name), src);
18
+ }
19
+ return dir;
20
+ }
21
+
22
+ function analyze(dir: string, src: string, file = "index.js") {
23
+ writeFileSync(join(dir, file), src);
24
+ clearBPathCache();
25
+ return analyzeFile(join(dir, file), src);
26
+ }
27
+
28
+ describe("B module-load diagnostics (host, no TypeValue double)", () => {
29
+ it("reports circular import once via B module graph", () => {
30
+ const dir = setup({
31
+ "a.js": `import { b } from "./b.js";
32
+ export function a(n) { return n <= 0 ? 0 : b(n - 1); }
33
+ `,
34
+ "b.js": `import { a } from "./a.js";
35
+ export function b(n) { return n <= 0 ? 1 : a(n - 1); }
36
+ `,
37
+ });
38
+ const src = `import { a } from "./a.js";
39
+ /**
40
+ * @nudo:case "n" (2)
41
+ */
42
+ export function go(n) { return a(n); }
43
+ `;
44
+ const r = analyze(dir, src);
45
+ const cycles = r.diagnostics.filter((d) => d.code === "nudo:module-cycle");
46
+ expect(cycles.length).toBe(1);
47
+ expect(cycles[0]!.message).toContain("Circular module load");
48
+ expect(cycles[0]!.message).toContain("->");
49
+ });
50
+
51
+ it("reports missing relative import once via B module graph", () => {
52
+ const dir = setup({});
53
+ const src = `import { x } from "./nope.js";
54
+ /**
55
+ * @nudo:case
56
+ */
57
+ export function go() { return x; }
58
+ `;
59
+ const r = analyze(dir, src);
60
+ const missing = r.diagnostics.filter((d) => d.code === "nudo:module-missing");
61
+ expect(missing.length).toBe(1);
62
+ expect(missing[0]!.message).toContain("nope.js");
63
+ });
64
+
65
+ it("B module graph issues come from evalAbsModuleGraph", () => {
66
+ const dir = setup({
67
+ "a.js": `import { b } from "./b.js";
68
+ export function a() { return b(); }
69
+ `,
70
+ "b.js": `import { a } from "./a.js";
71
+ export function b() { return a(); }
72
+ `,
73
+ });
74
+ const src = `import { a } from "./a.js";
75
+ export function go() { return a(); }
76
+ `;
77
+ writeFileSync(join(dir, "index.js"), src);
78
+ clearBPathCache();
79
+ const g = evalAbsModuleGraph(src, join(dir, "index.js"));
80
+ expect(g.issues.some((i) => i.kind === "cycle")).toBe(true);
81
+ });
82
+
83
+ it("recursion-truncated is not double-reported when Abs budget fires", () => {
84
+ const dir = setup({});
85
+ // 抽象参数触发调用预算截断;B Abs 路径与 TypeValue 都可能记 recursion:*
86
+ const src = `
87
+ /**
88
+ * @nudo:case "n" (T.number)
89
+ */
90
+ export function deep(n) {
91
+ if (n <= 0) return 0;
92
+ return 1 + deep(n - 1);
93
+ }
94
+ const boom = deep(3);
95
+ `;
96
+ const r = analyze(dir, src);
97
+ const rec = r.diagnostics.filter((d) => d.code === "nudo:recursion-truncated");
98
+ // 至多一条(B 截断则压 TypeValue;否则 TypeValue 自报)
99
+ expect(rec.length).toBeLessThanOrEqual(1);
100
+ });
101
+ });
@@ -0,0 +1,46 @@
1
+ import { describe, it, expect, afterAll } from "vitest";
2
+ import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { analyzeFileAsync, clearBPathCache } from "@nudojs/service";
6
+ import { typeValueToString } from "@nudojs/core";
7
+
8
+ const dirs: string[] = [];
9
+ afterAll(() => {
10
+ for (const d of dirs) rmSync(d, { recursive: true, force: true });
11
+ });
12
+
13
+ describe("B-path path-based @nudo:env", () => {
14
+ it("loads ./custom.env.ts globals", async () => {
15
+ clearBPathCache();
16
+ const dir = mkdtempSync(join(tmpdir(), "nudo-path-env-"));
17
+ dirs.push(dir);
18
+ writeFileSync(
19
+ join(dir, "custom.env.ts"),
20
+ `import { T } from "@nudojs/core";
21
+ export function defineEnv() {
22
+ return {
23
+ globals: {
24
+ MAGIC: T.literal(99),
25
+ },
26
+ };
27
+ }
28
+ `,
29
+ );
30
+ const main = `/// @nudo:env ./custom.env.ts
31
+
32
+ /**
33
+ * @nudo:case "t" ()
34
+ */
35
+ function getMagic() {
36
+ return MAGIC;
37
+ }
38
+ `;
39
+ const p = join(dir, "main.js");
40
+ writeFileSync(p, main, "utf-8");
41
+ const result = await analyzeFileAsync(p, main);
42
+ const fn = result.functions.find((f) => f.name === "getMagic");
43
+ expect(fn).toBeDefined();
44
+ expect(typeValueToString(fn!.cases[0].result)).toBe("99");
45
+ });
46
+ });
@@ -0,0 +1,131 @@
1
+ import { it, expect, describe, afterAll } from "vitest";
2
+ import { analyzeFile, clearBPathCache } from "@nudojs/service";
3
+ import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+
7
+ const dirs: string[] = [];
8
+ afterAll(() => {
9
+ for (const d of dirs) rmSync(d, { recursive: true, force: true });
10
+ });
11
+
12
+ function setup(files: Record<string, string>) {
13
+ const dir = mkdtempSync(join(tmpdir(), "nudo-mod-"));
14
+ dirs.push(dir);
15
+ writeFileSync(join(dir, "package.json"), JSON.stringify({ name: "mod", version: "1.0.0" }));
16
+ for (const [name, src] of Object.entries(files)) {
17
+ writeFileSync(join(dir, name), src);
18
+ }
19
+ return dir;
20
+ }
21
+
22
+ function goResult(dir: string, src: string) {
23
+ writeFileSync(join(dir, "index.js"), src);
24
+ clearBPathCache();
25
+ const r = analyzeFile(join(dir, "index.js"), src);
26
+ const f = r.functions.find((x) => x.name === "go");
27
+ return { result: f?.cases[0]?.result, diags: r.diagnostics };
28
+ }
29
+
30
+ describe("B-path re-export / export *", () => {
31
+ it("export { add } from './core.js'", () => {
32
+ const dir = setup({
33
+ "core.js": `export function add(a, b) { return a + b; }
34
+ `,
35
+ "barrel.js": `export { add } from "./core.js";
36
+ `,
37
+ });
38
+ const { result } = goResult(
39
+ dir,
40
+ `import { add } from "./barrel.js";
41
+ /**
42
+ * @nudo:case "n" (2, 3)
43
+ */
44
+ export function go(a, b) { return add(a, b); }
45
+ `,
46
+ );
47
+ expect(result).toMatchObject({ kind: "literal", value: 5 });
48
+ });
49
+
50
+ it("export { add as default } from './core.js'", () => {
51
+ const dir = setup({
52
+ "core.js": `export function add(a, b) { return a + b; }
53
+ `,
54
+ "barrel.js": `export { add as default } from "./core.js";
55
+ `,
56
+ });
57
+ const { result } = goResult(
58
+ dir,
59
+ `import add from "./barrel.js";
60
+ /**
61
+ * @nudo:case "n" (4, 6)
62
+ */
63
+ export function go(a, b) { return add(a, b); }
64
+ `,
65
+ );
66
+ expect(result).toMatchObject({ kind: "literal", value: 10 });
67
+ });
68
+
69
+ it("export * from './core.js'", () => {
70
+ const dir = setup({
71
+ "core.js": `export function add(a, b) { return a + b; }
72
+ export function mul(a, b) { return a * b; }
73
+ `,
74
+ "barrel.js": `export * from "./core.js";
75
+ `,
76
+ });
77
+ const { result } = goResult(
78
+ dir,
79
+ `import { mul } from "./barrel.js";
80
+ /**
81
+ * @nudo:case "n" (3, 5)
82
+ */
83
+ export function go(a, b) { return mul(a, b); }
84
+ `,
85
+ );
86
+ expect(result).toMatchObject({ kind: "literal", value: 15 });
87
+ });
88
+
89
+ it("class default export", () => {
90
+ const dir = setup({
91
+ "box.js": `export default class Box {
92
+ constructor(v) { this.v = v; }
93
+ get() { return this.v; }
94
+ }
95
+ `,
96
+ });
97
+ const { result } = goResult(
98
+ dir,
99
+ `import Box from "./box.js";
100
+ /**
101
+ * @nudo:case "v" (7)
102
+ */
103
+ export function go(v) {
104
+ const b = new Box(v);
105
+ return b.get();
106
+ }
107
+ `,
108
+ );
109
+ expect(result).toMatchObject({ kind: "literal", value: 7 });
110
+ });
111
+
112
+ it("async default export", () => {
113
+ const dir = setup({
114
+ "afn.js": `export default async function load(x) { return x + 1; }
115
+ `,
116
+ });
117
+ const { result } = goResult(
118
+ dir,
119
+ `import load from "./afn.js";
120
+ /**
121
+ * @nudo:case "x" (9)
122
+ */
123
+ export async function go(x) { return await load(x); }
124
+ `,
125
+ );
126
+ expect(result).toMatchObject({
127
+ kind: "promise",
128
+ value: { kind: "literal", value: 10 },
129
+ });
130
+ });
131
+ });
@@ -0,0 +1,52 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { analyzeFile } from "@nudojs/service";
3
+ import { typeValueToString } from "@nudojs/core";
4
+
5
+ describe("@nudo:replace on B path", () => {
6
+ it("replaces only matching sub-expression", () => {
7
+ const source = `
8
+ /**
9
+ * @nudo:case "test" (5, 10)
10
+ */
11
+ function compute(a, b) {
12
+ // @nudo:replace a T.literal(100)
13
+ const result = a + b;
14
+ return result;
15
+ }
16
+ `;
17
+ const result = analyzeFile("/test/replace.js", source);
18
+ expect(typeValueToString(result.functions[0].cases[0].result)).toBe("110");
19
+ });
20
+
21
+ it("does not match partial identifiers", () => {
22
+ const source = `
23
+ /**
24
+ * @nudo:case "test" ()
25
+ */
26
+ function test() {
27
+ const aa = 1;
28
+ const a = 2;
29
+ // @nudo:replace a T.literal(99)
30
+ const result = aa + a;
31
+ return result;
32
+ }
33
+ `;
34
+ const result = analyzeFile("/test/replace2.js", source);
35
+ expect(typeValueToString(result.functions[0].cases[0].result)).toBe("100");
36
+ });
37
+
38
+ it("replaces call expression target", () => {
39
+ const source = `
40
+ /**
41
+ * @nudo:case "test" ("{\"id\":1}")
42
+ */
43
+ function process(input) {
44
+ // @nudo:replace JSON.parse(input) T.object({ id: T.number })
45
+ const data = JSON.parse(input);
46
+ return data.id;
47
+ }
48
+ `;
49
+ const result = analyzeFile("/test/replace3.js", source);
50
+ expect(typeValueToString(result.functions[0].cases[0].result)).toBe("number");
51
+ });
52
+ });
@@ -0,0 +1,66 @@
1
+ import { describe, it, expect, afterAll } from "vitest";
2
+ import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import {
6
+ analyzeFile,
7
+ tryBPathCall,
8
+ isBPathCapable,
9
+ clearBPathCache,
10
+ } from "@nudojs/service";
11
+ import { typeValueToString, $lit, litValue } from "@nudojs/core";
12
+
13
+ const dirs: string[] = [];
14
+ afterAll(() => {
15
+ for (const d of dirs) rmSync(d, { recursive: true, force: true });
16
+ });
17
+
18
+ describe("B-path require", () => {
19
+ it("relative require injects Abs exports", () => {
20
+ clearBPathCache();
21
+ const dir = mkdtempSync(join(tmpdir(), "nudo-req-"));
22
+ dirs.push(dir);
23
+ writeFileSync(
24
+ join(dir, "math.js"),
25
+ `function double(x) { return x * 2; }\nexport { double };\n`,
26
+ );
27
+ const main = `
28
+ const math = require("./math.js");
29
+ /**
30
+ * @nudo:case "t" (21)
31
+ */
32
+ export function go(n) {
33
+ return math.double(n);
34
+ }
35
+ `;
36
+ const p = join(dir, "main.js");
37
+ writeFileSync(p, main, "utf-8");
38
+ expect(isBPathCapable(main)).toBe(true);
39
+ const r = tryBPathCall(main, p, "go", [$lit(21)]);
40
+ expect(r).toBeDefined();
41
+ expect(litValue(r!)).toBe(42);
42
+ });
43
+
44
+ it("analyzeFile case works with require", () => {
45
+ clearBPathCache();
46
+ const dir = mkdtempSync(join(tmpdir(), "nudo-req2-"));
47
+ dirs.push(dir);
48
+ writeFileSync(join(dir, "util.js"), `export function inc(x) { return x + 1; }\n`);
49
+ const main = `
50
+ const util = require("./util.js");
51
+ /**
52
+ * @nudo:case "t" (1)
53
+ */
54
+ function go(n) {
55
+ return util.inc(n);
56
+ }
57
+ `;
58
+ const p = join(dir, "main.js");
59
+ writeFileSync(p, main, "utf-8");
60
+ const result = analyzeFile(p, main);
61
+ const go = result.functions.find((f) => f.name === "go");
62
+ expect(go).toBeDefined();
63
+ const c = go!.cases.find((x) => x.name === "t");
64
+ expect(typeValueToString(c!.result)).toBe("2");
65
+ });
66
+ });
@@ -0,0 +1,109 @@
1
+ import { describe, it, expect, afterAll } from "vitest";
2
+ import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { runTranspiled, callTranspiledExport, $lit, litValue } from "@nudojs/core";
6
+ import { tryBPathCall, isBPathCapable, analyzeFile } from "@nudojs/service";
7
+ import { typeValueToString } from "@nudojs/core";
8
+
9
+ const dirs: string[] = [];
10
+ afterAll(() => {
11
+ for (const d of dirs) rmSync(d, { recursive: true, force: true });
12
+ });
13
+
14
+ function tmpPair(math: string, main: string) {
15
+ const dir = mkdtempSync(join(tmpdir(), "nudo-bpath-imp-"));
16
+ dirs.push(dir);
17
+ writeFileSync(join(dir, "math.js"), math, "utf-8");
18
+ writeFileSync(join(dir, "main.js"), main, "utf-8");
19
+ return { dir, mainPath: join(dir, "main.js"), main };
20
+ }
21
+
22
+ describe("B-path imported functions", () => {
23
+ it("runTranspiled + injected JS exports from dep", () => {
24
+ const dep = runTranspiled(`export function double(x) { return x * 2; }`);
25
+ const main = runTranspiled(
26
+ `import { double } from "./math.js";
27
+ export function run(n) { return double(n); }`,
28
+ { modules: { "./math.js": dep as never } },
29
+ );
30
+ const r = callTranspiledExport(main, "run", [$lit(21)]);
31
+ expect(litValue(r)).toBe(42);
32
+ });
33
+
34
+ it("tryBPathCall via module graph (Abs absFunction dep)", () => {
35
+ const { main, mainPath } = tmpPair(
36
+ `export function double(x) { return x * 2; }`,
37
+ `import { double } from "./math.js";
38
+ export function run(n) { return double(n); }
39
+ `,
40
+ );
41
+ const r = tryBPathCall(main, mainPath, "run", [$lit(21)]);
42
+ expect(r).toBeDefined();
43
+ expect(litValue(r!)).toBe(42);
44
+ });
45
+
46
+ it("analyzeFile case polish uses B path for relative import", () => {
47
+ const { dir, main, mainPath } = tmpPair(
48
+ `export function triple(x) { return x * 3; }`,
49
+ `import { triple } from "./b.js";
50
+ /**
51
+ * @nudo:case "four" (4)
52
+ */
53
+ export function run(n) { return triple(n); }
54
+ `.replace("./b.js", "./math.js"),
55
+ );
56
+ void dir;
57
+ const result = analyzeFile(mainPath, main);
58
+ const run = result.functions.find((f) => f.name === "run");
59
+ expect(run).toBeDefined();
60
+ const c = run!.cases.find((x) => x.name === "four");
61
+ expect(c).toBeDefined();
62
+ expect(typeValueToString(c!.result)).toBe("12");
63
+ // B 路径应挂上 intension
64
+ expect(c!.intension?.abs ?? c!.intension?.display).toBeTruthy();
65
+ });
66
+
67
+ it("isBPathCapable allows class, async, and require; rejects env", () => {
68
+ expect(isBPathCapable("function f() { return 1; }")).toBe(true);
69
+ expect(isBPathCapable("class A { constructor() { this.x = 1; } }")).toBe(true);
70
+ expect(isBPathCapable("async function f() { return 1; }")).toBe(true);
71
+ expect(isBPathCapable("const x = require('lodash');")).toBe(true);
72
+ });
73
+
74
+ it("object slots holding functions become first-class Abs fns (no raw JS leak)", () => {
75
+ // 回归:transpile 把函数表达式编译成真实 JS 函数;流进对象槽后
76
+ // 下游 absToTypeValue 读 .shape 曾裸崩(Cannot read properties of undefined (reading 'k'))
77
+ const dir = mkdtempSync(join(tmpdir(), "nudo-bpath-fnslot-"));
78
+ dirs.push(dir);
79
+ const p = join(dir, "main.js");
80
+ const src = `
81
+ function helper(a, b) { return a; }
82
+ export function make() {
83
+ const f = (x) => x + 1;
84
+ return { load: f, arrow: (y) => y, ref: helper };
85
+ }
86
+ `;
87
+ writeFileSync(p, src, "utf-8");
88
+ const r = tryBPathCall(src, p, "make", []);
89
+ expect(r).toBeDefined();
90
+ expect(r!.shape.k).toBe("obj");
91
+ const slots = (r!.shape as { slots: Record<string, { value: { shape: { k: string; params?: string[] } } }> }).slots;
92
+ expect(slots.load.value.shape.k).toBe("fn");
93
+ expect(slots.arrow.value.shape.params).toEqual(["y"]);
94
+ // 声明函数引用无参数名可恢复 → argN 回退(与 analyzer extractParamNames 口径一致)
95
+ expect(slots.ref.value.shape.params).toEqual(["arg0", "arg1"]);
96
+ });
97
+ });
98
+
99
+ describe("transpile import emission", () => {
100
+ it("preserves relative import", async () => {
101
+ const { transpile } = await import("@nudojs/core");
102
+ const out = transpile(
103
+ `import { double } from "./math.js";
104
+ export function run(n) { return double(n); }`,
105
+ );
106
+ expect(out).toContain('from "./math.js"');
107
+ expect(out).toContain("double");
108
+ });
109
+ });
@@ -0,0 +1,54 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { writeFileSync, mkdtempSync, rmSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { collectStaticImports, collectDependencySpecs } from "../static-imports.ts";
6
+ import { parse } from "@nudojs/parser";
7
+
8
+ describe("CJS require static resolution", () => {
9
+ it("follows require('./x')", () => {
10
+ const dir = mkdtempSync(join(tmpdir(), "nudo-cjs-"));
11
+ try {
12
+ writeFileSync(
13
+ join(dir, "math.js"),
14
+ `function add(a, b) { return a + b; }\nexports.add = add;\nexports.scale = function (x) { return add(x, 1); };\n`,
15
+ );
16
+ writeFileSync(
17
+ join(dir, "index.js"),
18
+ `const { add } = require("./math.js");\nexports.double = function (x) { return add(x, x); };\n`,
19
+ );
20
+ const graph = collectStaticImports(join(dir, "index.js"));
21
+ expect(graph.size).toBe(2);
22
+ const index = graph.get(join(dir, "index.js"))!;
23
+ expect(index.named.has("double")).toBe(true);
24
+ const math = graph.get(join(dir, "math.js"))!;
25
+ expect(math.named.has("add")).toBe(true);
26
+ expect(math.poly.get("add")?.display).toContain("A1");
27
+ } finally {
28
+ rmSync(dir, { recursive: true, force: true });
29
+ }
30
+ });
31
+
32
+ it("collectDependencySpecs finds require strings", () => {
33
+ const ast = parse(`const a = require("./a.js");\nconst b = require("./b.js");\n`);
34
+ const specs = collectDependencySpecs(ast);
35
+ expect(specs).toContain("./a.js");
36
+ expect(specs).toContain("./b.js");
37
+ });
38
+
39
+ it("module.exports = fn registers export", () => {
40
+ const dir = mkdtempSync(join(tmpdir(), "nudo-cjs2-"));
41
+ try {
42
+ writeFileSync(
43
+ join(dir, "lib.js"),
44
+ `function hello() { return "hi"; }\nmodule.exports = hello;\n`,
45
+ );
46
+ writeFileSync(join(dir, "app.js"), `const hello = require("./lib.js");\nexports.f = hello;\n`);
47
+ const graph = collectStaticImports(join(dir, "app.js"));
48
+ const lib = graph.get(join(dir, "lib.js"))!;
49
+ expect(lib.named.has("hello")).toBe(true);
50
+ } finally {
51
+ rmSync(dir, { recursive: true, force: true });
52
+ }
53
+ });
54
+ });
@@ -30,36 +30,6 @@ function fn(x) {
30
30
  expect(unreachable!.suggestions!.length).toBeGreaterThan(0);
31
31
  });
32
32
 
33
- it("should include code for assertion-failed diagnostics", () => {
34
- const source = `
35
- /** @nudo:case "test" (1)
36
- * @nudo:returns (T.string)
37
- */
38
- function fn(x) {
39
- return x;
40
- }
41
- `;
42
- const result = analyzeFile("test.js", source);
43
- const assertion = result.diagnostics.find(d => d.message.includes("assertion"));
44
- expect(assertion).toBeDefined();
45
- expect(assertion!.code).toBe("nudo-assertion-failed");
46
- });
47
-
48
- it("should include suggestions for assertion-failed diagnostics", () => {
49
- const source = `
50
- /** @nudo:case "test" (1)
51
- * @nudo:returns (T.string)
52
- */
53
- function fn(x) {
54
- return x;
55
- }
56
- `;
57
- const result = analyzeFile("test.js", source);
58
- const assertion = result.diagnostics.find(d => d.message.includes("assertion"));
59
- expect(assertion!.suggestions).toBeDefined();
60
- expect(assertion!.suggestions!.length).toBeGreaterThan(0);
61
- });
62
-
63
33
  it("should generate nudo:builtin-unknown for uncovered APIs", () => {
64
34
  const source = `
65
35
  // @nudo:case "test" ()