@nudojs/service 0.3.0 → 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.
- package/dist/index.d.ts +18 -3
- package/dist/index.js +80 -55
- package/package.json +16 -6
- package/CHANGELOG.md +0 -68
- package/src/__tests__/abs-bindings.test.ts +0 -53
- package/src/__tests__/abs-call-records.test.ts +0 -33
- package/src/__tests__/abs-calls.test.ts +0 -59
- package/src/__tests__/abs-module-cache.test.ts +0 -121
- package/src/__tests__/abs-modules-graph.test.ts +0 -100
- package/src/__tests__/abs-typevalue-diff.test.ts +0 -111
- package/src/__tests__/analyzer.test.ts +0 -880
- package/src/__tests__/as-replace.test.ts +0 -165
- package/src/__tests__/bpath-arg-provenance.test.ts +0 -104
- package/src/__tests__/bpath-as.test.ts +0 -54
- package/src/__tests__/bpath-cache-key.test.ts +0 -33
- package/src/__tests__/bpath-calls.test.ts +0 -55
- package/src/__tests__/bpath-diagnostics.test.ts +0 -78
- package/src/__tests__/bpath-env-modules.test.ts +0 -30
- package/src/__tests__/bpath-env.test.ts +0 -80
- package/src/__tests__/bpath-hosted-skip.test.ts +0 -71
- package/src/__tests__/bpath-import-default-ns.test.ts +0 -106
- package/src/__tests__/bpath-method-dedupe.test.ts +0 -65
- package/src/__tests__/bpath-method-diag.test.ts +0 -45
- package/src/__tests__/bpath-module-diags.test.ts +0 -101
- package/src/__tests__/bpath-path-env.test.ts +0 -46
- package/src/__tests__/bpath-reexport.test.ts +0 -131
- package/src/__tests__/bpath-replace.test.ts +0 -52
- package/src/__tests__/bpath-require.test.ts +0 -66
- package/src/__tests__/bpath-run.test.ts +0 -109
- package/src/__tests__/case-emitter.test.ts +0 -636
- package/src/__tests__/cjs-require.test.ts +0 -54
- package/src/__tests__/diagnostics.test.ts +0 -60
- package/src/__tests__/dts-generator.test.ts +0 -382
- package/src/__tests__/fn-cache-evict.test.ts +0 -56
- package/src/__tests__/fn-sig-impl.test.ts +0 -247
- package/src/__tests__/guard-generator.test.ts +0 -29
- package/src/__tests__/harvest-auto.test.ts +0 -43
- package/src/__tests__/harvest-node.test.ts +0 -24
- package/src/__tests__/harvest-package.test.ts +0 -38
- package/src/__tests__/harvest-to-abs.test.ts +0 -46
- package/src/__tests__/hover-intension.test.ts +0 -39
- package/src/__tests__/infer-json.test.ts +0 -35
- package/src/__tests__/integration.test.ts +0 -421
- package/src/__tests__/m3-intension.test.ts +0 -28
- package/src/__tests__/mock-seed-fingerprint.test.ts +0 -64
- package/src/__tests__/schema-generator.test.ts +0 -38
- package/src/__tests__/session-cache-evict.test.ts +0 -90
- package/src/__tests__/static-imports.test.ts +0 -44
- package/src/abs-modules-graph.ts +0 -387
- package/src/analysis-file-cache.ts +0 -51
- package/src/analyzer.ts +0 -3133
- package/src/bpath-diagnostics.ts +0 -283
- package/src/bpath-run.ts +0 -409
- package/src/case-emitter.ts +0 -477
- package/src/dts-generator.ts +0 -291
- package/src/env-to-abs.ts +0 -57
- package/src/fn-analysis-cache.ts +0 -119
- package/src/guard-generator.ts +0 -57
- package/src/harvest-auto.ts +0 -86
- package/src/harvest-node.ts +0 -48
- package/src/harvest-package.ts +0 -204
- package/src/harvest-to-abs.ts +0 -129
- package/src/index.ts +0 -150
- package/src/infer-json.ts +0 -137
- package/src/load-module.ts +0 -34
- package/src/mock-abs.ts +0 -275
- package/src/schema-generator.ts +0 -43
- package/src/semantic-tokens.ts +0 -216
- package/src/session-cache.ts +0 -50
- package/src/static-imports.ts +0 -241
- package/src/target-path.ts +0 -17
- package/tsconfig.json +0 -13
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { typeValueToString } from "@nudojs/core";
|
|
3
|
-
import { analyzeFile } from "../analyzer.ts";
|
|
4
|
-
|
|
5
|
-
describe("@nudo:as directive", () => {
|
|
6
|
-
it("overrides VariableDeclaration init value", () => {
|
|
7
|
-
const source = `
|
|
8
|
-
/// @nudo:env es
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* @nudo:case "test" (T.string)
|
|
12
|
-
*/
|
|
13
|
-
function loadConfig(path) {
|
|
14
|
-
// @nudo:as T.object({ port: T.number, host: T.string })
|
|
15
|
-
const config = JSON.parse(path);
|
|
16
|
-
return config;
|
|
17
|
-
}
|
|
18
|
-
`;
|
|
19
|
-
const result = analyzeFile("/test/as.js", source);
|
|
20
|
-
const res = typeValueToString(result.functions[0].cases[0].result);
|
|
21
|
-
expect(res).toContain("port: number");
|
|
22
|
-
expect(res).toContain("host: string");
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("overrides ReturnStatement value", () => {
|
|
26
|
-
const source = `
|
|
27
|
-
/// @nudo:env es
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* @nudo:case "test" (T.string)
|
|
31
|
-
*/
|
|
32
|
-
function loadConfig(path) {
|
|
33
|
-
// @nudo:as T.object({ name: T.string, age: T.number })
|
|
34
|
-
return JSON.parse(path);
|
|
35
|
-
}
|
|
36
|
-
`;
|
|
37
|
-
const result = analyzeFile("/test/as.js", source);
|
|
38
|
-
const res = typeValueToString(result.functions[0].cases[0].result);
|
|
39
|
-
expect(res).toContain("name: string");
|
|
40
|
-
expect(res).toContain("age: number");
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("does not affect statements without @nudo:as", () => {
|
|
44
|
-
const source = `
|
|
45
|
-
/// @nudo:env es
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* @nudo:case "test" (5)
|
|
49
|
-
*/
|
|
50
|
-
function add(x) {
|
|
51
|
-
// @nudo:as T.string
|
|
52
|
-
const a = x + 1;
|
|
53
|
-
const b = x + 2;
|
|
54
|
-
return b;
|
|
55
|
-
}
|
|
56
|
-
`;
|
|
57
|
-
const result = analyzeFile("/test/as.js", source);
|
|
58
|
-
expect(typeValueToString(result.functions[0].cases[0].result)).toBe("7");
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
describe("@nudo:replace directive", () => {
|
|
63
|
-
it("replaces a sub-expression by source text match", () => {
|
|
64
|
-
const source = `
|
|
65
|
-
/// @nudo:env es
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* @nudo:case "test" (T.string)
|
|
69
|
-
*/
|
|
70
|
-
function process(input) {
|
|
71
|
-
// @nudo:replace JSON.parse(input) T.object({ id: T.number })
|
|
72
|
-
const data = JSON.parse(input);
|
|
73
|
-
return data.id;
|
|
74
|
-
}
|
|
75
|
-
`;
|
|
76
|
-
const result = analyzeFile("/test/replace.js", source);
|
|
77
|
-
expect(typeValueToString(result.functions[0].cases[0].result)).toBe("number");
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("replaces only the matching sub-expression, not the whole line", () => {
|
|
81
|
-
const source = `
|
|
82
|
-
/**
|
|
83
|
-
* @nudo:case "test" (5, 10)
|
|
84
|
-
*/
|
|
85
|
-
function compute(a, b) {
|
|
86
|
-
// @nudo:replace a T.literal(100)
|
|
87
|
-
const result = a + b;
|
|
88
|
-
return result;
|
|
89
|
-
}
|
|
90
|
-
`;
|
|
91
|
-
const result = analyzeFile("/test/replace.js", source);
|
|
92
|
-
expect(typeValueToString(result.functions[0].cases[0].result)).toBe("110");
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("does not match partial identifiers", () => {
|
|
96
|
-
const source = `
|
|
97
|
-
/**
|
|
98
|
-
* @nudo:case "test" ()
|
|
99
|
-
*/
|
|
100
|
-
function test() {
|
|
101
|
-
const aa = 1;
|
|
102
|
-
const a = 2;
|
|
103
|
-
// @nudo:replace a T.literal(99)
|
|
104
|
-
const result = aa + a;
|
|
105
|
-
return result;
|
|
106
|
-
}
|
|
107
|
-
`;
|
|
108
|
-
const result = analyzeFile("/test/replace.js", source);
|
|
109
|
-
expect(typeValueToString(result.functions[0].cases[0].result)).toBe("100");
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it("supports member expression replacement", () => {
|
|
113
|
-
const source = `
|
|
114
|
-
/**
|
|
115
|
-
* @nudo:case "test" (T.object({ data: T.unknown }))
|
|
116
|
-
*/
|
|
117
|
-
function process(res) {
|
|
118
|
-
// @nudo:replace res.data T.array(T.object({ id: T.number, name: T.string }))
|
|
119
|
-
const items = res.data;
|
|
120
|
-
return items;
|
|
121
|
-
}
|
|
122
|
-
`;
|
|
123
|
-
const result = analyzeFile("/test/replace.js", source);
|
|
124
|
-
const res = typeValueToString(result.functions[0].cases[0].result);
|
|
125
|
-
expect(res).toContain("id: number");
|
|
126
|
-
expect(res).toContain("name: string");
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it("replacement only affects the next statement", () => {
|
|
130
|
-
const source = `
|
|
131
|
-
/**
|
|
132
|
-
* @nudo:case "test" (5, 10)
|
|
133
|
-
*/
|
|
134
|
-
function compute(a, b) {
|
|
135
|
-
// @nudo:replace a T.literal(100)
|
|
136
|
-
const x = a + b;
|
|
137
|
-
const y = a + b;
|
|
138
|
-
return y;
|
|
139
|
-
}
|
|
140
|
-
`;
|
|
141
|
-
const result = analyzeFile("/test/replace.js", source);
|
|
142
|
-
expect(typeValueToString(result.functions[0].cases[0].result)).toBe("15");
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
describe("@nudo:as and @nudo:replace coexistence", () => {
|
|
147
|
-
it("both can be used in the same function", () => {
|
|
148
|
-
const source = `
|
|
149
|
-
/// @nudo:env es
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* @nudo:case "test" (T.string)
|
|
153
|
-
*/
|
|
154
|
-
function process(input) {
|
|
155
|
-
// @nudo:as T.object({ name: T.string, score: T.number })
|
|
156
|
-
const data = JSON.parse(input);
|
|
157
|
-
// @nudo:replace data.score T.literal(100)
|
|
158
|
-
const result = data.score;
|
|
159
|
-
return result;
|
|
160
|
-
}
|
|
161
|
-
`;
|
|
162
|
-
const result = analyzeFile("/test/both.js", source);
|
|
163
|
-
expect(typeValueToString(result.functions[0].cases[0].result)).toBe("100");
|
|
164
|
-
});
|
|
165
|
-
});
|
|
@@ -1,104 +0,0 @@
|
|
|
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 analyze(src: string) {
|
|
13
|
-
const dir = mkdtempSync(join(tmpdir(), "nudo-prov-"));
|
|
14
|
-
dirs.push(dir);
|
|
15
|
-
writeFileSync(join(dir, "package.json"), JSON.stringify({ name: "prov", version: "1.0.0" }));
|
|
16
|
-
writeFileSync(join(dir, "index.js"), src);
|
|
17
|
-
clearBPathCache();
|
|
18
|
-
return analyzeFile(join(dir, "index.js"), src);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
describe("B argument provenance on method-missing", () => {
|
|
22
|
-
it("origin points at the argument literal, not just the callee", () => {
|
|
23
|
-
const src = `function badNum(n) {
|
|
24
|
-
return n.toUpperCase();
|
|
25
|
-
}
|
|
26
|
-
const boom = badNum(42);
|
|
27
|
-
`;
|
|
28
|
-
const r = analyze(src);
|
|
29
|
-
const diags = r.diagnostics.filter(
|
|
30
|
-
(d) => d.code === "nudo:no-method" && d.message.includes("toUpperCase"),
|
|
31
|
-
);
|
|
32
|
-
expect(diags.length).toBe(1);
|
|
33
|
-
const origin = diags[0]!.origin;
|
|
34
|
-
expect(origin).toBeDefined();
|
|
35
|
-
// `const boom = badNum(42);` — 42 starts at column 20
|
|
36
|
-
expect(origin!.line).toBe(4);
|
|
37
|
-
expect(origin!.column).toBeGreaterThanOrEqual(18);
|
|
38
|
-
expect(origin!.column).toBeLessThanOrEqual(22);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("mocked top-level call does not hit real fetch", () => {
|
|
42
|
-
const src = `// @nudo:mock fetch = sinon.stub().onFirstCall().returns({ data: "first" })
|
|
43
|
-
function run() { return fetch(); }
|
|
44
|
-
const r = run();
|
|
45
|
-
function badNum(n) {
|
|
46
|
-
return n.toUpperCase();
|
|
47
|
-
}
|
|
48
|
-
const boom = badNum(7);
|
|
49
|
-
`;
|
|
50
|
-
const r = analyze(src);
|
|
51
|
-
const fetchErrs = r.diagnostics.filter(
|
|
52
|
-
(d) => d.message.includes("fetch") && d.severity === "error",
|
|
53
|
-
);
|
|
54
|
-
expect(fetchErrs).toHaveLength(0);
|
|
55
|
-
const diags = r.diagnostics.filter(
|
|
56
|
-
(d) => d.code === "nudo:no-method" && d.message.includes("toUpperCase"),
|
|
57
|
-
);
|
|
58
|
-
expect(diags.length).toBe(1);
|
|
59
|
-
expect(diags[0]!.origin?.column).toBeGreaterThanOrEqual(16);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("unknown-global is not double-reported with builtin-unknown", () => {
|
|
63
|
-
const src = `function f() {
|
|
64
|
-
return WeakRef;
|
|
65
|
-
}
|
|
66
|
-
const boom = f();
|
|
67
|
-
`;
|
|
68
|
-
const r = analyze(src);
|
|
69
|
-
const globals = r.diagnostics.filter((d) => d.code === "nudo:unknown-global");
|
|
70
|
-
const builtins = r.diagnostics.filter((d) => d.code === "nudo:builtin-unknown");
|
|
71
|
-
expect(builtins.some((d) => d.message.includes("WeakRef"))).toBe(true);
|
|
72
|
-
expect(globals.filter((d) => d.message.includes("WeakRef"))).toHaveLength(0);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("B reports unknown-recv for method on unknown param (no TypeValue double)", () => {
|
|
76
|
-
const src = `function lonely(u) {
|
|
77
|
-
return u.toUpperCase();
|
|
78
|
-
}
|
|
79
|
-
`;
|
|
80
|
-
const r = analyze(src);
|
|
81
|
-
const unknownRecv = r.diagnostics.filter(
|
|
82
|
-
(d) => d.code === "nudo:unknown-recv" && d.message.includes("toUpperCase"),
|
|
83
|
-
);
|
|
84
|
-
expect(unknownRecv.length).toBe(1);
|
|
85
|
-
expect(unknownRecv[0]!.severity).toBe("warning");
|
|
86
|
-
// B 已报则不应再有 no-method 同名
|
|
87
|
-
expect(
|
|
88
|
-
r.diagnostics.filter((d) => d.code === "nudo:no-method" && d.message.includes("toUpperCase")),
|
|
89
|
-
).toHaveLength(0);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("B reports unknown-recv for property on unknown", () => {
|
|
93
|
-
const src = `function lonely(u) {
|
|
94
|
-
return u.foo;
|
|
95
|
-
}
|
|
96
|
-
`;
|
|
97
|
-
const r = analyze(src);
|
|
98
|
-
const diags = r.diagnostics.filter(
|
|
99
|
-
(d) => (d.code === "nudo:unknown-recv" || d.code === "nudo:no-method") && d.message.includes("foo"),
|
|
100
|
-
);
|
|
101
|
-
expect(diags.length).toBeGreaterThanOrEqual(1);
|
|
102
|
-
expect(diags.some((d) => d.code === "nudo:unknown-recv")).toBe(true);
|
|
103
|
-
});
|
|
104
|
-
});
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { analyzeFile } from "@nudojs/service";
|
|
3
|
-
import { typeValueToString } from "@nudojs/core";
|
|
4
|
-
|
|
5
|
-
describe("@nudo:as on B path", () => {
|
|
6
|
-
it("overrides VariableDeclaration init value", () => {
|
|
7
|
-
const source = `
|
|
8
|
-
/**
|
|
9
|
-
* @nudo:case "test" ("{}")
|
|
10
|
-
*/
|
|
11
|
-
function loadConfig(path) {
|
|
12
|
-
// @nudo:as T.object({ port: T.number, host: T.string })
|
|
13
|
-
const config = JSON.parse(path);
|
|
14
|
-
return config;
|
|
15
|
-
}
|
|
16
|
-
`;
|
|
17
|
-
const result = analyzeFile("/test/as.js", source);
|
|
18
|
-
const res = typeValueToString(result.functions[0].cases[0].result);
|
|
19
|
-
expect(res).toContain("port: number");
|
|
20
|
-
expect(res).toContain("host: string");
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("overrides ReturnStatement value", () => {
|
|
24
|
-
const source = `
|
|
25
|
-
/**
|
|
26
|
-
* @nudo:case "test" ("{}")
|
|
27
|
-
*/
|
|
28
|
-
function loadConfig(path) {
|
|
29
|
-
// @nudo:as T.object({ name: T.string, age: T.number })
|
|
30
|
-
return JSON.parse(path);
|
|
31
|
-
}
|
|
32
|
-
`;
|
|
33
|
-
const result = analyzeFile("/test/as2.js", source);
|
|
34
|
-
const res = typeValueToString(result.functions[0].cases[0].result);
|
|
35
|
-
expect(res).toContain("name: string");
|
|
36
|
-
expect(res).toContain("age: number");
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("does not affect statements without @nudo:as", () => {
|
|
40
|
-
const source = `
|
|
41
|
-
/**
|
|
42
|
-
* @nudo:case "test" (5)
|
|
43
|
-
*/
|
|
44
|
-
function add(x) {
|
|
45
|
-
// @nudo:as T.literal(99)
|
|
46
|
-
const y = x + 1;
|
|
47
|
-
const z = x + 2;
|
|
48
|
-
return z;
|
|
49
|
-
}
|
|
50
|
-
`;
|
|
51
|
-
const result = analyzeFile("/test/as3.js", source);
|
|
52
|
-
expect(typeValueToString(result.functions[0].cases[0].result)).toBe("7");
|
|
53
|
-
});
|
|
54
|
-
});
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { it, expect, describe, afterEach } from "vitest";
|
|
2
|
-
import { tryRunBPath, tryBPathCall, clearBPathCache } from "@nudojs/service";
|
|
3
|
-
import { litValue } from "@nudojs/core";
|
|
4
|
-
|
|
5
|
-
afterEach(() => clearBPathCache());
|
|
6
|
-
|
|
7
|
-
describe("bRunCache key", () => {
|
|
8
|
-
// 同长度、前 200+ 字符完全相同、尾部不同的两份源码:
|
|
9
|
-
// 旧前缀截断键会命中同一条缓存,静默返回陈旧结果。
|
|
10
|
-
const head = `// ${"x".repeat(210)}\nexport function f() { return `;
|
|
11
|
-
const srcA = `${head}1; }`;
|
|
12
|
-
const srcB = `${head}2; }`;
|
|
13
|
-
|
|
14
|
-
it("same source twice returns the identical cached run", () => {
|
|
15
|
-
const a = tryRunBPath(srcA, "/test/cache-key.js");
|
|
16
|
-
const b = tryRunBPath(srcA, "/test/cache-key.js");
|
|
17
|
-
expect(a).toBeDefined();
|
|
18
|
-
expect(b).toBe(a);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it("same length + same prefix but different tail must not collide", () => {
|
|
22
|
-
const r1 = tryRunBPath(srcA, "/test/cache-key.js");
|
|
23
|
-
const r2 = tryRunBPath(srcB, "/test/cache-key.js");
|
|
24
|
-
expect(r1).toBeDefined();
|
|
25
|
-
expect(r2).toBeDefined();
|
|
26
|
-
expect(r2).not.toBe(r1);
|
|
27
|
-
|
|
28
|
-
// 语义正确性:srcB 的求值必须看到 f() = 2,而非 srcA 的陈旧 1
|
|
29
|
-
const out = tryBPathCall(srcB, "/test/cache-key.js", "f", []);
|
|
30
|
-
if (!out) throw new Error("expected f to resolve via B path");
|
|
31
|
-
expect(litValue(out)).toBe(2);
|
|
32
|
-
});
|
|
33
|
-
});
|
|
@@ -1,55 +0,0 @@
|
|
|
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 { tryBPathCallFull, analyzeFile } from "@nudojs/service";
|
|
6
|
-
import { $lit, litValue, 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 call site collection", () => {
|
|
14
|
-
it("records calls during B evaluation", () => {
|
|
15
|
-
const dir = mkdtempSync(join(tmpdir(), "nudo-bcall-"));
|
|
16
|
-
dirs.push(dir);
|
|
17
|
-
const src = `
|
|
18
|
-
function helper(x) { return x + 1; }
|
|
19
|
-
export function caller(n) { return helper(n); }
|
|
20
|
-
`;
|
|
21
|
-
const p = join(dir, "main.js");
|
|
22
|
-
writeFileSync(p, src, "utf-8");
|
|
23
|
-
const full = tryBPathCallFull(src, p, "caller", [$lit(4)], { collectCalls: true });
|
|
24
|
-
expect(full).toBeDefined();
|
|
25
|
-
expect(litValue(full!.result)).toBe(5);
|
|
26
|
-
expect(full!.calls?.length).toBeGreaterThanOrEqual(1);
|
|
27
|
-
expect(full!.calls!.map((c) => c.fnName)).toContain("helper");
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("analyzeFile still synthesizes call@ for uncalled helper", () => {
|
|
31
|
-
const source = `
|
|
32
|
-
function uncalled(x) {
|
|
33
|
-
return x * 2;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* @nudo:case "t" (5)
|
|
38
|
-
*/
|
|
39
|
-
function caller(y) {
|
|
40
|
-
return uncalled(y);
|
|
41
|
-
}
|
|
42
|
-
`;
|
|
43
|
-
const result = analyzeFile("/test/bcall.js", source);
|
|
44
|
-
const uncalled = result.functions.find((f) => f.name === "uncalled");
|
|
45
|
-
expect(uncalled).toBeDefined();
|
|
46
|
-
expect(uncalled!.cases.length).toBeGreaterThanOrEqual(1);
|
|
47
|
-
const names = uncalled!.cases.map((c) => c.name);
|
|
48
|
-
expect(names.some((n) => n.startsWith("call@") || n.startsWith("entry@"))).toBe(true);
|
|
49
|
-
// 结果应为 10
|
|
50
|
-
const hit = uncalled!.cases.find((c) => c.name.startsWith("call@"));
|
|
51
|
-
if (hit) {
|
|
52
|
-
expect(typeValueToString(hit.result)).toBe("10");
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
});
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { collectBPathDiagnostics } from "@nudojs/service";
|
|
3
|
-
|
|
4
|
-
describe("B-path static diagnostics", () => {
|
|
5
|
-
it("marks statements after return as unreachable", () => {
|
|
6
|
-
const src = `
|
|
7
|
-
function f() {
|
|
8
|
-
return 1;
|
|
9
|
-
const dead = 2;
|
|
10
|
-
console.log("no");
|
|
11
|
-
}
|
|
12
|
-
`;
|
|
13
|
-
const d = collectBPathDiagnostics(src);
|
|
14
|
-
expect(d.unreachable.length).toBeGreaterThanOrEqual(1);
|
|
15
|
-
// dead 声明与后续 console
|
|
16
|
-
const lines = d.unreachable.map((u) => u.range.start.line);
|
|
17
|
-
expect(lines.some((l) => l >= 4)).toBe(true);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it("marks statements after throw as unreachable", () => {
|
|
21
|
-
const src = `
|
|
22
|
-
function f(n) {
|
|
23
|
-
if (n) {
|
|
24
|
-
throw new Error("x");
|
|
25
|
-
return 1;
|
|
26
|
-
}
|
|
27
|
-
return 0;
|
|
28
|
-
}
|
|
29
|
-
`;
|
|
30
|
-
const d = collectBPathDiagnostics(src);
|
|
31
|
-
expect(d.unreachable.length).toBeGreaterThanOrEqual(1);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("does not flag code after if/else both return", () => {
|
|
35
|
-
const src = `
|
|
36
|
-
function f(n) {
|
|
37
|
-
if (n) { return 1; } else { return 2; }
|
|
38
|
-
}
|
|
39
|
-
`;
|
|
40
|
-
const d = collectBPathDiagnostics(src);
|
|
41
|
-
// if 内无后续语句
|
|
42
|
-
expect(d.unreachable).toHaveLength(0);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("flags unknown global calls as builtin-unknown", () => {
|
|
46
|
-
const src = `
|
|
47
|
-
function f() {
|
|
48
|
-
return someNativeApi(1);
|
|
49
|
-
}
|
|
50
|
-
`;
|
|
51
|
-
const d = collectBPathDiagnostics(src);
|
|
52
|
-
expect(d.builtinUnknown.map((b) => b.name)).toContain("someNativeApi");
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("extraKnown (@nudo:mock / @nudo:env covered) globals are not flagged", () => {
|
|
56
|
-
const src = `
|
|
57
|
-
function f() {
|
|
58
|
-
return someNativeApi(1);
|
|
59
|
-
}
|
|
60
|
-
`;
|
|
61
|
-
expect(collectBPathDiagnostics(src, ["someNativeApi"]).builtinUnknown).toHaveLength(0);
|
|
62
|
-
// 无覆盖时仍报
|
|
63
|
-
expect(collectBPathDiagnostics(src).builtinUnknown.map((b) => b.name)).toContain("someNativeApi");
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("does not flag locals, imports, or known globals", () => {
|
|
67
|
-
const src = `
|
|
68
|
-
import { helper } from "./h.js";
|
|
69
|
-
function local(x) { return x; }
|
|
70
|
-
function f() {
|
|
71
|
-
const y = local(1);
|
|
72
|
-
return helper(y) + Math.max(1, 2) + JSON.stringify(y);
|
|
73
|
-
}
|
|
74
|
-
`;
|
|
75
|
-
const d = collectBPathDiagnostics(src);
|
|
76
|
-
expect(d.builtinUnknown).toHaveLength(0);
|
|
77
|
-
});
|
|
78
|
-
});
|
|
@@ -1,30 +0,0 @@
|
|
|
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 } 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 env modules (node:path)", () => {
|
|
14
|
-
it("path.join via import from node:path", () => {
|
|
15
|
-
clearBPathCache();
|
|
16
|
-
const source = `/// @nudo:env node
|
|
17
|
-
|
|
18
|
-
import { join } from "node:path";
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* @nudo:case "test" ("src", "utils", "index.js")
|
|
22
|
-
*/
|
|
23
|
-
function buildPath(a, b, c) {
|
|
24
|
-
return join(a, b, c);
|
|
25
|
-
}
|
|
26
|
-
`;
|
|
27
|
-
const result = analyzeFile("/test/pathmod.js", source);
|
|
28
|
-
expect(typeValueToString(result.functions[0].cases[0].result)).toBe('"src/utils/index.js"');
|
|
29
|
-
});
|
|
30
|
-
});
|
|
@@ -1,80 +0,0 @@
|
|
|
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, tryBPathCall, isBPathCapable, clearBPathCache } from "@nudojs/service";
|
|
6
|
-
import { typeValueToString, $lit, absToString } 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 @nudo:env", () => {
|
|
14
|
-
it("isBPathCapable allows builtin and path env", () => {
|
|
15
|
-
expect(isBPathCapable("function f() { return 1; }", ["es"])).toBe(true);
|
|
16
|
-
expect(isBPathCapable("function f() { return 1; }", ["./custom.ts"])).toBe(true);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("JSON.parse via @nudo:env es", () => {
|
|
20
|
-
clearBPathCache();
|
|
21
|
-
const dir = mkdtempSync(join(tmpdir(), "nudo-env-"));
|
|
22
|
-
dirs.push(dir);
|
|
23
|
-
const main = `/// @nudo:env es
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* @nudo:case "t" ("{}")
|
|
27
|
-
*/
|
|
28
|
-
export function parseId(s) {
|
|
29
|
-
return typeof JSON.parse(s);
|
|
30
|
-
}
|
|
31
|
-
`;
|
|
32
|
-
const p = join(dir, "main.js");
|
|
33
|
-
writeFileSync(p, main, "utf-8");
|
|
34
|
-
const r = tryBPathCall(main, p, "parseId", [$lit("{}")], { envNames: ["es"] });
|
|
35
|
-
expect(r).toBeDefined();
|
|
36
|
-
expect(absToString(r!)).toContain("string");
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("analyzeFile works with @nudo:env es", () => {
|
|
40
|
-
clearBPathCache();
|
|
41
|
-
const source = `/// @nudo:env es
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* @nudo:case "t" (5)
|
|
45
|
-
*/
|
|
46
|
-
function addOne(n) {
|
|
47
|
-
return Math.floor(n) + 1;
|
|
48
|
-
}
|
|
49
|
-
`;
|
|
50
|
-
const result = analyzeFile("/test/env.js", source);
|
|
51
|
-
const fn = result.functions.find((f) => f.name === "addOne");
|
|
52
|
-
expect(fn).toBeDefined();
|
|
53
|
-
expect(typeValueToString(fn!.cases[0].result)).toBe("6");
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("arrowFn @nudo:mock injects into B-path (no real fetch call)", () => {
|
|
57
|
-
clearBPathCache();
|
|
58
|
-
const dir = mkdtempSync(join(tmpdir(), "nudo-mock-b-"));
|
|
59
|
-
dirs.push(dir);
|
|
60
|
-
// 回归:arrowFn mock 落在 seedFns,此前 B 路径注入只吃 seedVars——
|
|
61
|
-
// 函数体内的 fetch 调用落到真实原生 fetch,拿 Abs 当 URL 直接崩
|
|
62
|
-
const source = `// @nudo:mock fetch = (url) => ({ ok: true, json: () => ({ id: 1, name: "ada" }) })
|
|
63
|
-
async function loadUser(id) {
|
|
64
|
-
const res = await fetch("/users/" + id);
|
|
65
|
-
return res.json();
|
|
66
|
-
}
|
|
67
|
-
loadUser(42);
|
|
68
|
-
`;
|
|
69
|
-
const p = join(dir, "main.js");
|
|
70
|
-
writeFileSync(p, source, "utf-8");
|
|
71
|
-
const result = analyzeFile(p, source);
|
|
72
|
-
const fn = result.functions.find((f) => f.name === "loadUser");
|
|
73
|
-
expect(fn).toBeDefined();
|
|
74
|
-
const call = fn!.cases.find((c) => c.name.startsWith("call@"));
|
|
75
|
-
expect(call).toBeDefined();
|
|
76
|
-
expect(call!.intension?.abs).toContain("ada");
|
|
77
|
-
// mock 覆盖后 fetch 不再是 builtin-unknown
|
|
78
|
-
expect(result.diagnostics.some((d) => d.code === "nudo:builtin-unknown")).toBe(false);
|
|
79
|
-
});
|
|
80
|
-
});
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { it, expect, describe, afterAll } from "vitest";
|
|
2
|
-
import { analyzeFile, clearBPathCache, isBPathCapable } 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 analyze(src: string, file = "index.js") {
|
|
13
|
-
const dir = mkdtempSync(join(tmpdir(), "nudo-hosted-"));
|
|
14
|
-
dirs.push(dir);
|
|
15
|
-
writeFileSync(join(dir, "package.json"), JSON.stringify({ name: "hosted", version: "1.0.0" }));
|
|
16
|
-
const p = join(dir, file);
|
|
17
|
-
writeFileSync(p, src);
|
|
18
|
-
clearBPathCache();
|
|
19
|
-
return { result: analyzeFile(p, src), path: p, src };
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
describe("B-hosted skips TypeValue evaluateProgram", () => {
|
|
23
|
-
it("capable file is B-hosted and still infers cases", () => {
|
|
24
|
-
const src = `/**
|
|
25
|
-
* @nudo:case "n" (3)
|
|
26
|
-
*/
|
|
27
|
-
export function scale(x) {
|
|
28
|
-
return x * 2 + 1;
|
|
29
|
-
}
|
|
30
|
-
`;
|
|
31
|
-
const { result, path } = analyze(src);
|
|
32
|
-
expect(isBPathCapable(src, [])).toBe(true);
|
|
33
|
-
const fn = result.functions.find((f) => f.name === "scale");
|
|
34
|
-
expect(fn?.cases).toHaveLength(1);
|
|
35
|
-
expect(fn?.cases[0]?.result.kind).toBe("literal");
|
|
36
|
-
// 无 TypeValue 专有叠报
|
|
37
|
-
expect(result.diagnostics.filter((d) => d.code === "nudo:unknown-global")).toHaveLength(0);
|
|
38
|
-
expect(result.bindings.has("scale")).toBe(true);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("import + top-level call still yields call@ cases", () => {
|
|
42
|
-
const dir = mkdtempSync(join(tmpdir(), "nudo-hosted2-"));
|
|
43
|
-
dirs.push(dir);
|
|
44
|
-
writeFileSync(join(dir, "package.json"), JSON.stringify({ name: "h2", version: "1.0.0" }));
|
|
45
|
-
writeFileSync(
|
|
46
|
-
join(dir, "util.js"),
|
|
47
|
-
`export function twice(n) { return n * 2; }
|
|
48
|
-
`,
|
|
49
|
-
);
|
|
50
|
-
const src = `import { twice } from "./util.js";
|
|
51
|
-
const boom = twice(21);
|
|
52
|
-
export function go(n) { return twice(n); }
|
|
53
|
-
`;
|
|
54
|
-
writeFileSync(join(dir, "index.js"), src);
|
|
55
|
-
clearBPathCache();
|
|
56
|
-
const r = analyzeFile(join(dir, "index.js"), src);
|
|
57
|
-
const go = r.functions.find((f) => f.name === "go");
|
|
58
|
-
const twice = r.functions.find((f) => f.name === "twice");
|
|
59
|
-
expect(go).toBeDefined();
|
|
60
|
-
// 跨文件导出可能合成 externalFunctions 或本文件 twice
|
|
61
|
-
expect(go!.cases.length + (twice?.cases.length ?? 0)).toBeGreaterThan(0);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("nodeTypeMap is populated without TypeValue env walk", () => {
|
|
65
|
-
const src = `const n = 1;
|
|
66
|
-
export function id(x) { return x; }
|
|
67
|
-
`;
|
|
68
|
-
const { result } = analyze(src);
|
|
69
|
-
expect(result.nodeTypeMap.size).toBeGreaterThan(0);
|
|
70
|
-
});
|
|
71
|
-
});
|